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

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package cm
     8  
     9  import (
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  
    14  	"github.com/google/uuid"
    15  	"github.com/piprate/json-gold/ld"
    16  
    17  	"github.com/hyperledger/aries-framework-go/pkg/doc/presexch"
    18  	"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
    19  )
    20  
    21  const (
    22  	// CredentialApplicationAttachmentFormat defines the format type of Credential Application when used as an
    23  	// attachment in the WACI issuance flow.
    24  	// Refer to https://identity.foundation/waci-presentation-exchange/#issuance-2 for more info.
    25  	CredentialApplicationAttachmentFormat = "dif/credential-manifest/application@v1.0"
    26  	// CredentialApplicationPresentationContext defines the context type of Credential Application when used as part of
    27  	// a presentation attachment in the WACI issuance flow.
    28  	// Refer to https://identity.foundation/waci-presentation-exchange/#issuance-2 for more info.
    29  	CredentialApplicationPresentationContext = "https://identity.foundation/credential-manifest/application/v1"
    30  	credentialApplicationPresentationType    = "CredentialApplication"
    31  )
    32  
    33  // CredentialApplication represents a credential_application object as defined in
    34  // https://identity.foundation/credential-manifest/#credential-application.
    35  // Note that the term "Credential Application" is overloaded in the spec - a "Credential Application" may be referring
    36  // to one of two different, but related, concepts. A "Credential Application" can be the object defined below, which is
    37  // intended to be embedded in an envelope like a Verifiable Presentation. Additionally, when that envelope contains
    38  // the object defined below under a field named "credential_application", then that envelope itself can be called
    39  // a "Credential Application". The larger "envelope version" of a Credential Application may also have a sibling
    40  // presentation_submission object within the envelope, as demonstrated by the PresentCredentialApplication method.
    41  // See https://github.com/decentralized-identity/credential-manifest/issues/73 for more information about this name
    42  // overloading.
    43  type CredentialApplication struct {
    44  	ID string `json:"id,omitempty"` // mandatory property
    45  	// The value of this property MUST be the ID of a valid Credential Manifest.
    46  	ManifestID string `json:"manifest_id,omitempty"` // mandatory property
    47  	// Must be a subset of the format property of the CredentialManifest that this CredentialApplication is related to
    48  	Format presexch.Format `json:"format,omitempty"` // mandatory property
    49  }
    50  
    51  // UnmarshalAndValidateAgainstCredentialManifest unmarshals the credentialApplicationBytes into a CredentialApplication
    52  // object (performing verification in the process), and after that verifies that the Credential Application is valid
    53  // against the given Credential Manifest. It's simply a convenience method that allows you to unmarshal and perform
    54  // validation against a Credential Manifest in one call.
    55  func UnmarshalAndValidateAgainstCredentialManifest(credentialApplicationBytes []byte,
    56  	cm *CredentialManifest) (CredentialApplication, error) {
    57  	var credentialApplication CredentialApplication
    58  
    59  	err := json.Unmarshal(credentialApplicationBytes, &credentialApplication)
    60  	if err != nil {
    61  		return CredentialApplication{}, err
    62  	}
    63  
    64  	err = credentialApplication.ValidateAgainstCredentialManifest(cm)
    65  	if err != nil {
    66  		return CredentialApplication{}, err
    67  	}
    68  
    69  	return credentialApplication, nil
    70  }
    71  
    72  // ValidateCredentialApplication validates credential application presentation by validating
    73  // the embedded Credential Application object against the given Credential Manifest.
    74  // There are 3 requirements for the Credential Application to be valid against the Credential Manifest:
    75  //  1. Credential Application's manifest ID must match the Credential Manifest's ID.
    76  //  2. If the Credential Manifest has a format property, the Credential Application must also have a
    77  //     format property which is a subset of the Credential Manifest's.
    78  //  3. If the Credential Manifest contains a presentation_definition property, the Credential Application
    79  //     must have a matching presentation_submission property.
    80  //
    81  // Proof of all individual credentials can also be validated by using options.
    82  // Refer to https://identity.foundation/credential-manifest/#credential-application for more info.
    83  func ValidateCredentialApplication(application *verifiable.Presentation, cm *CredentialManifest,
    84  	contextLoader ld.DocumentLoader, options ...presexch.MatchOption) error {
    85  	// The credential application object is embedded into the application presentation in which
    86  	// this function is validating.
    87  	credentialApplicationMap, ok := lookUpMap(application.CustomFields, "credential_application")
    88  	if !ok {
    89  		return errors.New("invalid credential application, missing 'credential_application'")
    90  	}
    91  
    92  	var ca CredentialApplication
    93  
    94  	caBits, err := json.Marshal(credentialApplicationMap)
    95  	if err != nil {
    96  		return fmt.Errorf("failed to marshal credential application: %w", err)
    97  	}
    98  
    99  	err = json.Unmarshal(caBits, &ca)
   100  	if err != nil {
   101  		return fmt.Errorf("failed to unmarshal credential application: %w", err)
   102  	}
   103  
   104  	err = ca.ValidateAgainstCredentialManifest(cm)
   105  	if err != nil {
   106  		return fmt.Errorf("credential application does not match credential manifest: %w", err)
   107  	}
   108  
   109  	// Credential Application must have a matching presentation submission if the related credential manifest has
   110  	// a presentation definition.
   111  	if cm.PresentationDefinition == nil {
   112  		return nil
   113  	}
   114  
   115  	_, err = cm.PresentationDefinition.Match([]*verifiable.Presentation{application}, contextLoader, options...)
   116  
   117  	return err
   118  }
   119  
   120  // UnmarshalJSON is the custom unmarshal function gets called automatically when the standard json.Unmarshal is called.
   121  // It also ensures that the given data is a valid CredentialApplication object per the specification.
   122  func (ca *CredentialApplication) UnmarshalJSON(data []byte) error {
   123  	err := ca.standardUnmarshal(data)
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	err = ca.validate()
   129  	if err != nil {
   130  		return fmt.Errorf("invalid Credential Application: %w", err)
   131  	}
   132  
   133  	return nil
   134  }
   135  
   136  // ValidateAgainstCredentialManifest verifies that the Credential Application is valid against the given
   137  // Credential Manifest.
   138  func (ca *CredentialApplication) ValidateAgainstCredentialManifest(cm *CredentialManifest) error {
   139  	if ca.ManifestID != cm.ID {
   140  		return fmt.Errorf("the Manifest ID of the Credential Application (%s) does not match the given "+
   141  			"Credential Manifest's ID (%s)", ca.ManifestID, cm.ID)
   142  	}
   143  
   144  	err := ca.validateFormatAgainstCredManifestFormat(cm)
   145  	if err != nil {
   146  		return fmt.Errorf("invalid format for the given Credential Manifest: %w", err)
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  func (ca *CredentialApplication) standardUnmarshal(data []byte) error {
   153  	// The type alias below is used as to allow the standard json.Unmarshal to be called within a custom unmarshal
   154  	// function without causing infinite recursion. See https://stackoverflow.com/a/43178272 for more information.
   155  	type credentialApplicationWithoutMethods *CredentialApplication
   156  
   157  	err := json.Unmarshal(data, credentialApplicationWithoutMethods(ca))
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  func (ca *CredentialApplication) validate() error {
   166  	if ca.ID == "" {
   167  		return errors.New("missing ID")
   168  	}
   169  
   170  	if ca.ManifestID == "" {
   171  		return errors.New("missing manifest ID")
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func (ca *CredentialApplication) validateFormatAgainstCredManifestFormat(cm *CredentialManifest) error {
   178  	// Credential Application's format must be a subset of the related Credential Manifest's format
   179  	if !ca.hasFormat() && !cm.hasFormat() {
   180  		return nil
   181  	}
   182  
   183  	if !cm.hasFormat() {
   184  		return errors.New("the Credential Application specifies a format but the Credential Manifest does not")
   185  	}
   186  
   187  	if !ca.hasFormat() {
   188  		return errors.New("the Credential Manifest specifies a format but the Credential Application does not")
   189  	}
   190  
   191  	err := ca.ensureFormatIsSubsetOfCredManifestFormat(*cm.Format)
   192  	if err != nil {
   193  		return fmt.Errorf("invalid format request: %w", err)
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  func (ca *CredentialApplication) hasFormat() bool {
   200  	return hasAnyAlgorithmsOrProofTypes(ca.Format)
   201  }
   202  
   203  func (ca *CredentialApplication) ensureFormatIsSubsetOfCredManifestFormat(credManiFmt presexch.Format) error {
   204  	err := ensureCredAppJWTAlgsAreSubsetOfCredManiJWTAlgs("JWT", ca.Format.Jwt, credManiFmt.Jwt)
   205  	if err != nil {
   206  		return err
   207  	}
   208  
   209  	err = ensureCredAppJWTAlgsAreSubsetOfCredManiJWTAlgs("JWT VC", ca.Format.JwtVC, credManiFmt.JwtVC)
   210  	if err != nil {
   211  		return err
   212  	}
   213  
   214  	err = ensureCredAppJWTAlgsAreSubsetOfCredManiJWTAlgs("JWT VP", ca.Format.JwtVP, credManiFmt.JwtVP)
   215  	if err != nil {
   216  		return err
   217  	}
   218  
   219  	err = ensureCredAppLDPProofTypesAreSubsetOfCredManiProofTypes("LDP", ca.Format.Ldp, credManiFmt.Ldp)
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	err = ensureCredAppLDPProofTypesAreSubsetOfCredManiProofTypes("LDP VC", ca.Format.LdpVC, credManiFmt.LdpVC)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	err = ensureCredAppLDPProofTypesAreSubsetOfCredManiProofTypes("LDP VP", ca.Format.LdpVP, credManiFmt.LdpVP)
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	return nil
   235  }
   236  
   237  // presentCredentialApplicationOpts holds options for the PresentCredentialApplication method.
   238  type presentCredentialApplicationOpts struct {
   239  	existingPresentation    verifiable.Presentation
   240  	existingPresentationSet bool
   241  }
   242  
   243  // PresentCredentialApplicationOpt is an option for the PresentCredentialApplication method.
   244  type PresentCredentialApplicationOpt func(opts *presentCredentialApplicationOpts)
   245  
   246  // WithExistingPresentationForPresentCredentialApplication is an option for the PresentCredentialApplication method
   247  // that allows Credential Application data to be added to an existing Presentation
   248  // (turning it into a Credential Application in the process). The existing Presentation should not already have
   249  // Credential Application data.
   250  func WithExistingPresentationForPresentCredentialApplication(
   251  	presentation *verifiable.Presentation) PresentCredentialApplicationOpt {
   252  	return func(opts *presentCredentialApplicationOpts) {
   253  		opts.existingPresentation = *presentation
   254  		opts.existingPresentationSet = true
   255  	}
   256  }
   257  
   258  // PresentCredentialApplication creates a minimal Presentation (without proofs) with Credential Application data based
   259  // on credentialManifest. The WithExistingPresentationForPresentCredentialResponse can be used to add the Credential
   260  // Application data to an existing Presentation object instead. If the
   261  // "https://identity.foundation/presentation-exchange/submission/v1" context is found, it will be replaced with
   262  // the "https://identity.foundation/credential-manifest/application/v1" context. Note that any existing proofs are
   263  // not updated. Note also the following assumptions/limitations of this method:
   264  //  1. The format of all claims in the Presentation Submission are assumed to be ldp_vp and will be set as such.
   265  //  2. The format for the Credential Application object will be set to match the format from the Credential Manifest
   266  //     exactly. If a caller wants to use a smaller subset of the Credential Manifest's format, then they will have to
   267  //     set it manually.
   268  //  3. The location of the Verifiable Credentials is assumed to be an array at the root under a field called
   269  //     "verifiableCredential".
   270  //  4. The Verifiable Credentials in the presentation is assumed to be in the same order as the Output Descriptors in
   271  //     the Credential Manifest.
   272  func PresentCredentialApplication(credentialManifest *CredentialManifest,
   273  	opts ...PresentCredentialApplicationOpt) (*verifiable.Presentation, error) {
   274  	if credentialManifest == nil {
   275  		return nil, errors.New("credential manifest argument cannot be nil")
   276  	}
   277  
   278  	appliedOptions := getPresentCredentialApplicationOpts(opts)
   279  
   280  	var presentation verifiable.Presentation
   281  
   282  	if appliedOptions.existingPresentationSet {
   283  		presentation = appliedOptions.existingPresentation
   284  	} else {
   285  		newPresentation, err := verifiable.NewPresentation()
   286  		if err != nil {
   287  			return nil, err
   288  		}
   289  
   290  		presentation = *newPresentation
   291  	}
   292  
   293  	setCredentialApplicationContext(&presentation)
   294  
   295  	presentation.Type = append(presentation.Type, credentialApplicationPresentationType)
   296  
   297  	setCustomFields(&presentation, credentialManifest)
   298  
   299  	return &presentation, nil
   300  }
   301  
   302  func getPresentCredentialApplicationOpts(opts []PresentCredentialApplicationOpt) *presentCredentialApplicationOpts {
   303  	processedOptions := &presentCredentialApplicationOpts{}
   304  
   305  	for _, opt := range opts {
   306  		if opt != nil {
   307  			opt(processedOptions)
   308  		}
   309  	}
   310  
   311  	return processedOptions
   312  }
   313  
   314  func setCredentialApplicationContext(presentation *verifiable.Presentation) {
   315  	var newContextSet bool
   316  
   317  	for i := range presentation.Context {
   318  		if presentation.Context[i] == presexch.PresentationSubmissionJSONLDContextIRI {
   319  			presentation.Context[i] = CredentialApplicationPresentationContext
   320  			newContextSet = true
   321  
   322  			break
   323  		}
   324  	}
   325  
   326  	if !newContextSet {
   327  		presentation.Context = append(presentation.Context, CredentialApplicationPresentationContext)
   328  	}
   329  }
   330  
   331  func setCustomFields(presentation *verifiable.Presentation, credentialManifest *CredentialManifest) {
   332  	format := presexch.Format{}
   333  	if credentialManifest.Format != nil {
   334  		format = *credentialManifest.Format
   335  	}
   336  
   337  	application := CredentialApplication{
   338  		ID:         uuid.New().String(),
   339  		ManifestID: credentialManifest.ID,
   340  		Format:     format,
   341  	}
   342  
   343  	if presentation.CustomFields == nil {
   344  		presentation.CustomFields = make(map[string]interface{})
   345  	}
   346  
   347  	presentation.CustomFields["credential_application"] = application
   348  
   349  	if credentialManifest.PresentationDefinition != nil {
   350  		submission := makePresentationSubmission(credentialManifest.PresentationDefinition)
   351  
   352  		presentation.CustomFields["presentation_submission"] = submission
   353  	}
   354  }
   355  
   356  func makePresentationSubmission(presentationDef *presexch.PresentationDefinition) presexch.PresentationSubmission {
   357  	descriptorMap := make([]*presexch.InputDescriptorMapping,
   358  		len(presentationDef.InputDescriptors))
   359  
   360  	for i := range presentationDef.InputDescriptors {
   361  		descriptorMap[i] = &presexch.InputDescriptorMapping{
   362  			ID:     presentationDef.InputDescriptors[i].ID,
   363  			Format: "ldp_vp",
   364  			Path:   fmt.Sprintf("$.verifiableCredential[%d]", i),
   365  		}
   366  	}
   367  
   368  	submission := presexch.PresentationSubmission{
   369  		ID:            uuid.New().String(),
   370  		DefinitionID:  presentationDef.ID,
   371  		DescriptorMap: descriptorMap,
   372  	}
   373  
   374  	return submission
   375  }
   376  
   377  func ensureCredAppJWTAlgsAreSubsetOfCredManiJWTAlgs(algType string,
   378  	credAppJWTType, credManifestJWTType *presexch.JwtType) error {
   379  	if credAppJWTType != nil { //nolint:nestif // hard to resolve without creating a worse issue
   380  		if credManifestJWTType != nil {
   381  			if !arrayIsSubsetOfAnother(credAppJWTType.Alg, credManifestJWTType.Alg) {
   382  				return makeAlgorithmsSubsetError(algType, credAppJWTType.Alg, credManifestJWTType.Alg)
   383  			}
   384  		} else {
   385  			if len(credAppJWTType.Alg) > 0 {
   386  				return makeAlgorithmsSubsetError(algType, credAppJWTType.Alg, nil)
   387  			}
   388  		}
   389  	}
   390  
   391  	return nil
   392  }
   393  
   394  func ensureCredAppLDPProofTypesAreSubsetOfCredManiProofTypes(ldpType string,
   395  	credAppLDPType, credManifestLDPType *presexch.LdpType) error {
   396  	if credAppLDPType != nil { //nolint:nestif // hard to resolve without creating a worse issue
   397  		if credManifestLDPType != nil {
   398  			if !arrayIsSubsetOfAnother(credAppLDPType.ProofType, credManifestLDPType.ProofType) {
   399  				return makeProofTypesSubsetError(ldpType, credAppLDPType.ProofType, credManifestLDPType.ProofType)
   400  			}
   401  		} else {
   402  			if len(credAppLDPType.ProofType) > 0 {
   403  				return makeProofTypesSubsetError(ldpType, credAppLDPType.ProofType, nil)
   404  			}
   405  		}
   406  	}
   407  
   408  	return nil
   409  }
   410  
   411  func makeAlgorithmsSubsetError(algType string, credAppJWTTypeAlgs, credManifestJWTTypeAlgs []string) error {
   412  	return makeSubsetError(algType, "algorithms", credAppJWTTypeAlgs, credManifestJWTTypeAlgs)
   413  }
   414  
   415  func makeProofTypesSubsetError(ldpType string, credAppLDPTypeProofTypes, credManifestLDPTypeProofTypes []string) error {
   416  	return makeSubsetError(ldpType, "proof types", credAppLDPTypeProofTypes, credManifestLDPTypeProofTypes)
   417  }
   418  
   419  func makeSubsetError(typeInCategory, category string, credAppJWTTypeAlgs, credManifestJWTTypeAlgs []string) error {
   420  	return fmt.Errorf("the Credential Application lists the following %s %s: %v. "+
   421  		"One or more of these are not in the Credential Manifest's supported %s %s: %v",
   422  		typeInCategory, category, credAppJWTTypeAlgs, typeInCategory, category, credManifestJWTTypeAlgs)
   423  }
   424  
   425  func arrayIsSubsetOfAnother(array1, array2 []string) bool {
   426  	for _, element := range array1 {
   427  		if !contains(array2, element) {
   428  			return false
   429  		}
   430  	}
   431  
   432  	return true
   433  }
   434  
   435  func contains(array []string, element string) bool {
   436  	for _, arrayElement := range array {
   437  		if arrayElement == element {
   438  			return true
   439  		}
   440  	}
   441  
   442  	return false
   443  }