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 }