github.com/hyperledger/aries-framework-go@v0.3.2/pkg/doc/verifiable/credential_sdjwt.go (about)

     1  /*
     2  Copyright Avast Software. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package verifiable
     8  
     9  import (
    10  	"crypto"
    11  	"encoding/json"
    12  	"fmt"
    13  
    14  	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
    15  	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common"
    16  	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder"
    17  	"github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer"
    18  	json2 "github.com/hyperledger/aries-framework-go/pkg/doc/util/json"
    19  )
    20  
    21  type marshalDisclosureOpts struct {
    22  	includeAllDisclosures bool
    23  	discloseIfAvailable   []string
    24  	discloseRequired      []string
    25  	holderBinding         *holder.BindingInfo
    26  	signer                jose.Signer
    27  	signingKeyID          string
    28  }
    29  
    30  // MarshalDisclosureOption provides an option for Credential.MarshalWithDisclosure.
    31  type MarshalDisclosureOption func(opts *marshalDisclosureOpts)
    32  
    33  // TODO: should DiscloseGiven(IfAvailable|Required) have path semantics for disclosure?
    34  
    35  // DiscloseGivenIfAvailable sets that the disclosures with the given claim names will be disclosed by
    36  // Credential.MarshalWithDisclosure.
    37  //
    38  // If any name provided does not have a matching disclosure, Credential.MarshalWithDisclosure will skip the name.
    39  //
    40  // Will result in an error if this option is provided alongside DiscloseAll.
    41  func DiscloseGivenIfAvailable(disclosureNames []string) MarshalDisclosureOption {
    42  	return func(opts *marshalDisclosureOpts) {
    43  		opts.discloseIfAvailable = disclosureNames
    44  	}
    45  }
    46  
    47  // DiscloseGivenRequired sets that the disclosures with the given claim names will be disclosed by
    48  // Credential.MarshalWithDisclosure.
    49  //
    50  // If any name provided does not have a matching disclosure, Credential.MarshalWithDisclosure will return an error.
    51  //
    52  // Will result in an error if this option is provided alongside DiscloseAll.
    53  func DiscloseGivenRequired(disclosureNames []string) MarshalDisclosureOption {
    54  	return func(opts *marshalDisclosureOpts) {
    55  		opts.discloseRequired = disclosureNames
    56  	}
    57  }
    58  
    59  // DiscloseAll sets that all disclosures in the given Credential will be disclosed by Credential.MarshalWithDisclosure.
    60  //
    61  // Will result in an error if this option is provided alongside DiscloseGivenIfAvailable or DiscloseGivenRequired.
    62  func DiscloseAll() MarshalDisclosureOption {
    63  	return func(opts *marshalDisclosureOpts) {
    64  		opts.includeAllDisclosures = true
    65  	}
    66  }
    67  
    68  // DisclosureHolderBinding option configures Credential.MarshalWithDisclosure to include a holder binding.
    69  func DisclosureHolderBinding(binding *holder.BindingInfo) MarshalDisclosureOption {
    70  	return func(opts *marshalDisclosureOpts) {
    71  		opts.holderBinding = binding
    72  	}
    73  }
    74  
    75  // DisclosureSigner option provides Credential.MarshalWithDisclosure with a signer that will be used to create an SD-JWT
    76  // if the given Credential wasn't already parsed from SD-JWT.
    77  func DisclosureSigner(signer jose.Signer, signingKeyID string) MarshalDisclosureOption {
    78  	return func(opts *marshalDisclosureOpts) {
    79  		opts.signer = signer
    80  		opts.signingKeyID = signingKeyID
    81  	}
    82  }
    83  
    84  // MarshalWithDisclosure marshals a SD-JWT credential in combined format for presentation, including precisely
    85  // the disclosures indicated by provided options, and optionally a holder binding if given the requisite option.
    86  func (vc *Credential) MarshalWithDisclosure(opts ...MarshalDisclosureOption) (string, error) {
    87  	options := &marshalDisclosureOpts{}
    88  
    89  	for _, opt := range opts {
    90  		opt(options)
    91  	}
    92  
    93  	if options.includeAllDisclosures && (len(options.discloseIfAvailable) > 0 || len(options.discloseRequired) > 0) {
    94  		return "", fmt.Errorf("incompatible options provided")
    95  	}
    96  
    97  	if vc.JWT != "" && vc.SDJWTHashAlg != "" {
    98  		return filterSDJWTVC(vc, options)
    99  	}
   100  
   101  	if options.signer == nil {
   102  		return "", fmt.Errorf("credential needs signer to create SD-JWT")
   103  	}
   104  
   105  	return createSDJWTPresentation(vc, options)
   106  }
   107  
   108  func filterSDJWTVC(vc *Credential, options *marshalDisclosureOpts) (string, error) {
   109  	disclosureCodes, err := filteredDisclosureCodes(vc.SDJWTDisclosures, options)
   110  	if err != nil {
   111  		return "", err
   112  	}
   113  
   114  	cf := common.CombinedFormatForPresentation{
   115  		SDJWT:         vc.JWT,
   116  		Disclosures:   disclosureCodes,
   117  		HolderBinding: vc.SDHolderBinding,
   118  	}
   119  
   120  	if options.holderBinding != nil {
   121  		cf.HolderBinding, err = holder.CreateHolderBinding(options.holderBinding)
   122  		if err != nil {
   123  			return "", fmt.Errorf("failed to create holder binding: %w", err)
   124  		}
   125  	}
   126  
   127  	return cf.Serialize(), nil
   128  }
   129  
   130  func createSDJWTPresentation(vc *Credential, options *marshalDisclosureOpts) (string, error) {
   131  	issued, err := makeSDJWT(vc, options.signer, options.signingKeyID)
   132  	if err != nil {
   133  		return "", fmt.Errorf("creating SD-JWT from Credential: %w", err)
   134  	}
   135  
   136  	disclosureClaims, err := common.GetDisclosureClaims(issued.Disclosures)
   137  	if err != nil {
   138  		return "", fmt.Errorf("parsing disclosure claims from vc sdjwt: %w", err)
   139  	}
   140  
   141  	disclosureCodes, err := filteredDisclosureCodes(disclosureClaims, options)
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  
   146  	var presOpts []holder.Option
   147  
   148  	if options.holderBinding != nil {
   149  		presOpts = append(presOpts, holder.WithHolderBinding(options.holderBinding))
   150  	}
   151  
   152  	issuedSerialized, err := issued.Serialize(false)
   153  	if err != nil {
   154  		return "", fmt.Errorf("serializing SD-JWT for presentation: %w", err)
   155  	}
   156  
   157  	combinedSDJWT, err := holder.CreatePresentation(issuedSerialized, disclosureCodes, presOpts...)
   158  	if err != nil {
   159  		return "", fmt.Errorf("create SD-JWT presentation: %w", err)
   160  	}
   161  
   162  	return combinedSDJWT, nil
   163  }
   164  
   165  func filteredDisclosureCodes(
   166  	availableDisclosures []*common.DisclosureClaim,
   167  	options *marshalDisclosureOpts,
   168  ) ([]string, error) {
   169  	var (
   170  		useDisclosures  []*common.DisclosureClaim
   171  		err             error
   172  		disclosureCodes []string
   173  	)
   174  
   175  	if options.includeAllDisclosures {
   176  		useDisclosures = availableDisclosures
   177  	} else {
   178  		useDisclosures, err = filterDisclosures(availableDisclosures,
   179  			options.discloseIfAvailable, options.discloseRequired)
   180  		if err != nil {
   181  			return nil, err
   182  		}
   183  	}
   184  
   185  	for _, disclosure := range useDisclosures {
   186  		disclosureCodes = append(disclosureCodes, disclosure.Disclosure)
   187  	}
   188  
   189  	return disclosureCodes, nil
   190  }
   191  
   192  func filterDisclosures(
   193  	disclosures []*common.DisclosureClaim,
   194  	ifAvailable, required []string,
   195  ) ([]*common.DisclosureClaim, error) {
   196  	ifAvailMap := map[string]*common.DisclosureClaim{}
   197  	reqMap := map[string]*common.DisclosureClaim{}
   198  
   199  	for _, name := range ifAvailable {
   200  		ifAvailMap[name] = nil
   201  	}
   202  
   203  	for _, name := range required {
   204  		reqMap[name] = nil
   205  
   206  		delete(ifAvailMap, name) // avoid listing a disclosure twice, if it's in both lists
   207  	}
   208  
   209  	for _, disclosure := range disclosures {
   210  		if _, ok := ifAvailMap[disclosure.Name]; ok {
   211  			ifAvailMap[disclosure.Name] = disclosure
   212  		}
   213  
   214  		if _, ok := reqMap[disclosure.Name]; ok {
   215  			reqMap[disclosure.Name] = disclosure
   216  		}
   217  	}
   218  
   219  	var out []*common.DisclosureClaim
   220  
   221  	for _, claim := range ifAvailMap {
   222  		if claim != nil {
   223  			out = append(out, claim)
   224  		}
   225  	}
   226  
   227  	for _, claim := range reqMap {
   228  		if claim == nil {
   229  			return nil, fmt.Errorf("disclosure list missing required claim")
   230  		}
   231  
   232  		out = append(out, claim)
   233  	}
   234  
   235  	return out, nil
   236  }
   237  
   238  type makeSDJWTOpts struct {
   239  	hashAlg crypto.Hash
   240  }
   241  
   242  // MakeSDJWTOption provides an option for creating an SD-JWT from a VC.
   243  type MakeSDJWTOption func(opts *makeSDJWTOpts)
   244  
   245  // MakeSDJWTWithHash sets the hash to use for an SD-JWT VC.
   246  func MakeSDJWTWithHash(hash crypto.Hash) MakeSDJWTOption {
   247  	return func(opts *makeSDJWTOpts) {
   248  		opts.hashAlg = hash
   249  	}
   250  }
   251  
   252  // MakeSDJWT creates an SD-JWT in combined format for issuance, with all fields in credentialSubject converted
   253  // recursively into selectively-disclosable SD-JWT claims.
   254  func (vc *Credential) MakeSDJWT(signer jose.Signer, signingKeyID string, options ...MakeSDJWTOption) (string, error) {
   255  	sdjwt, err := makeSDJWT(vc, signer, signingKeyID, options...)
   256  	if err != nil {
   257  		return "", err
   258  	}
   259  
   260  	sdjwtSerialized, err := sdjwt.Serialize(false)
   261  	if err != nil {
   262  		return "", fmt.Errorf("serializing SD-JWT: %w", err)
   263  	}
   264  
   265  	return sdjwtSerialized, nil
   266  }
   267  
   268  func makeSDJWT(vc *Credential, signer jose.Signer, signingKeyID string, options ...MakeSDJWTOption,
   269  ) (*issuer.SelectiveDisclosureJWT, error) {
   270  	opts := &makeSDJWTOpts{}
   271  
   272  	for _, option := range options {
   273  		option(opts)
   274  	}
   275  
   276  	claims, err := vc.JWTClaims(false)
   277  	if err != nil {
   278  		return nil, fmt.Errorf("constructing VC JWT claims: %w", err)
   279  	}
   280  
   281  	claimBytes, err := json.Marshal(claims)
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  
   286  	claimMap := map[string]interface{}{}
   287  
   288  	err = json.Unmarshal(claimBytes, &claimMap)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	headers := map[string]interface{}{
   294  		jose.HeaderKeyID: signingKeyID,
   295  	}
   296  
   297  	issuerOptions := []issuer.NewOpt{
   298  		issuer.WithStructuredClaims(true),
   299  		issuer.WithNonSelectivelyDisclosableClaims([]string{"id"}),
   300  	}
   301  
   302  	if opts.hashAlg != 0 {
   303  		issuerOptions = append(issuerOptions, issuer.WithHashAlgorithm(opts.hashAlg))
   304  	}
   305  
   306  	sdjwt, err := issuer.NewFromVC(claimMap, headers, signer, issuerOptions...)
   307  	if err != nil {
   308  		return nil, fmt.Errorf("creating SD-JWT from VC: %w", err)
   309  	}
   310  
   311  	return sdjwt, nil
   312  }
   313  
   314  type displayCredOpts struct {
   315  	displayAll   bool
   316  	displayGiven []string
   317  }
   318  
   319  // DisplayCredentialOption provides an option for Credential.CreateDisplayCredential.
   320  type DisplayCredentialOption func(opts *displayCredOpts)
   321  
   322  // DisplayAllDisclosures sets that Credential.CreateDisplayCredential will include all disclosures in the generated
   323  // credential.
   324  func DisplayAllDisclosures() DisplayCredentialOption {
   325  	return func(opts *displayCredOpts) {
   326  		opts.displayAll = true
   327  	}
   328  }
   329  
   330  // DisplayGivenDisclosures sets that Credential.CreateDisplayCredential will include only the given disclosures in the
   331  // generated credential.
   332  func DisplayGivenDisclosures(given []string) DisplayCredentialOption {
   333  	return func(opts *displayCredOpts) {
   334  		opts.displayGiven = append(opts.displayGiven, given...)
   335  	}
   336  }
   337  
   338  // CreateDisplayCredential creates, for SD-JWT credentials, a Credential whose selective-disclosure subject fields
   339  // are replaced with the disclosure data.
   340  //
   341  // Options may be provided to filter the disclosures that will be included in the display credential. If a disclosure is
   342  // not included, the associated claim will not be present in the returned credential.
   343  //
   344  // If the calling Credential is not an SD-JWT credential, this method returns the credential itself.
   345  func (vc *Credential) CreateDisplayCredential( // nolint:funlen,gocyclo
   346  	opts ...DisplayCredentialOption,
   347  ) (*Credential, error) {
   348  	options := &displayCredOpts{}
   349  
   350  	for _, opt := range opts {
   351  		opt(options)
   352  	}
   353  
   354  	if options.displayAll && len(options.displayGiven) > 0 {
   355  		return nil, fmt.Errorf("incompatible options provided")
   356  	}
   357  
   358  	if vc.SDJWTHashAlg == "" || vc.JWT == "" {
   359  		return vc, nil
   360  	}
   361  
   362  	credClaims, err := unmarshalJWSClaims(vc.JWT, false, nil)
   363  	if err != nil {
   364  		return nil, fmt.Errorf("unmarshal VC JWT claims: %w", err)
   365  	}
   366  
   367  	credClaims.refineFromJWTClaims()
   368  
   369  	useDisclosures := filterDisclosureList(vc.SDJWTDisclosures, options)
   370  
   371  	newVCObj, err := common.GetDisclosedClaims(useDisclosures, credClaims.VC)
   372  	if err != nil {
   373  		return nil, fmt.Errorf("assembling disclosed claims into vc: %w", err)
   374  	}
   375  
   376  	if subj, ok := newVCObj["credentialSubject"].(map[string]interface{}); ok {
   377  		clearEmpty(subj)
   378  	}
   379  
   380  	vcBytes, err := json.Marshal(&newVCObj)
   381  	if err != nil {
   382  		return nil, fmt.Errorf("marshalling vc object to JSON: %w", err)
   383  	}
   384  
   385  	newVC, err := populateCredential(vcBytes, nil)
   386  	if err != nil {
   387  		return nil, fmt.Errorf("parsing new VC from JSON: %w", err)
   388  	}
   389  
   390  	return newVC, nil
   391  }
   392  
   393  // CreateDisplayCredentialMap creates, for SD-JWT credentials, a Credential whose selective-disclosure subject fields
   394  // are replaced with the disclosure data.
   395  //
   396  // Options may be provided to filter the disclosures that will be included in the display credential. If a disclosure is
   397  // not included, the associated claim will not be present in the returned credential.
   398  //
   399  // If the calling Credential is not an SD-JWT credential, this method returns the credential itself.
   400  func (vc *Credential) CreateDisplayCredentialMap( // nolint:funlen,gocyclo
   401  	opts ...DisplayCredentialOption,
   402  ) (map[string]interface{}, error) {
   403  	options := &displayCredOpts{}
   404  
   405  	for _, opt := range opts {
   406  		opt(options)
   407  	}
   408  
   409  	if options.displayAll && len(options.displayGiven) > 0 {
   410  		return nil, fmt.Errorf("incompatible options provided")
   411  	}
   412  
   413  	if vc.SDJWTHashAlg == "" || vc.JWT == "" {
   414  		bytes, err := vc.MarshalJSON()
   415  		if err != nil {
   416  			return nil, err
   417  		}
   418  
   419  		return json2.ToMap(bytes)
   420  	}
   421  
   422  	credClaims, err := unmarshalJWSClaims(vc.JWT, false, nil)
   423  	if err != nil {
   424  		return nil, fmt.Errorf("unmarshal VC JWT claims: %w", err)
   425  	}
   426  
   427  	credClaims.refineFromJWTClaims()
   428  
   429  	useDisclosures := filterDisclosureList(vc.SDJWTDisclosures, options)
   430  
   431  	newVCObj, err := common.GetDisclosedClaims(useDisclosures, credClaims.VC)
   432  	if err != nil {
   433  		return nil, fmt.Errorf("assembling disclosed claims into vc: %w", err)
   434  	}
   435  
   436  	if subj, ok := newVCObj["credentialSubject"].(map[string]interface{}); ok {
   437  		clearEmpty(subj)
   438  	}
   439  
   440  	return newVCObj, nil
   441  }
   442  
   443  func filterDisclosureList(disclosures []*common.DisclosureClaim, options *displayCredOpts) []*common.DisclosureClaim {
   444  	if options.displayAll {
   445  		return disclosures
   446  	}
   447  
   448  	displayGivenMap := map[string]struct{}{}
   449  
   450  	for _, given := range options.displayGiven {
   451  		displayGivenMap[given] = struct{}{}
   452  	}
   453  
   454  	var out []*common.DisclosureClaim
   455  
   456  	for _, disclosure := range disclosures {
   457  		if _, ok := displayGivenMap[disclosure.Name]; ok {
   458  			out = append(out, disclosure)
   459  		}
   460  	}
   461  
   462  	return out
   463  }
   464  
   465  func clearEmpty(claims map[string]interface{}) {
   466  	for name, value := range claims {
   467  		if valueObj, ok := value.(map[string]interface{}); ok {
   468  			clearEmpty(valueObj)
   469  
   470  			if len(valueObj) == 0 {
   471  				delete(claims, name)
   472  			}
   473  		}
   474  	}
   475  }