github.com/hyperledger/aries-framework-go@v0.3.2/pkg/doc/cm/credentialmanifest.go (about) 1 /* 2 Copyright SecureKey Technologies Inc. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 // Package cm contains methods that are useful for parsing and validating the objects defined in the Credential Manifest 8 // spec: https://identity.foundation/credential-manifest. 9 package cm 10 11 import ( 12 "context" 13 "encoding/json" 14 "errors" 15 "fmt" 16 17 "github.com/PaesslerAG/gval" 18 "github.com/PaesslerAG/jsonpath" 19 20 "github.com/hyperledger/aries-framework-go/pkg/doc/presexch" 21 ) 22 23 // CredentialManifestAttachmentFormat defines the format type of Credential Manifest when used as an attachment in the 24 // WACI issuance flow. Refer to https://identity.foundation/waci-presentation-exchange/#issuance-2 for more info. 25 const CredentialManifestAttachmentFormat = "dif/credential-manifest/manifest@v1.0" 26 27 // CredentialManifest represents a Credential Manifest object as defined in 28 // https://identity.foundation/credential-manifest/#credential-manifest-2. 29 type CredentialManifest struct { 30 ID string `json:"id,omitempty"` // mandatory property 31 Issuer Issuer `json:"issuer,omitempty"` // mandatory property 32 OutputDescriptors []*OutputDescriptor `json:"output_descriptors,omitempty"` // mandatory property 33 Format *presexch.Format `json:"format,omitempty"` 34 PresentationDefinition *presexch.PresentationDefinition `json:"presentation_definition,omitempty"` 35 } 36 37 // Issuer represents the issuer object defined in https://identity.foundation/credential-manifest/#general-composition. 38 type Issuer struct { 39 ID string `json:"id,omitempty"` // mandatory, must be a valid URI 40 Name string `json:"name,omitempty"` 41 Styles *Styles `json:"styles,omitempty"` 42 } 43 44 // Styles represents an Entity Styles object as defined in 45 // https://identity.foundation/wallet-rendering/#entity-styles. 46 type Styles struct { 47 Thumbnail *ImageURIWithAltText `json:"thumbnail,omitempty"` 48 Hero *ImageURIWithAltText `json:"hero,omitempty"` 49 Background *Color `json:"background,omitempty"` 50 Text *Color `json:"text,omitempty"` 51 } 52 53 // Color represents a single color in RGB hex code format. 54 type Color struct { 55 Color string `json:"color"` // RGB hex code 56 } 57 58 // OutputDescriptor represents an Output Descriptor object as defined in 59 // https://identity.foundation/credential-manifest/#output-descriptor. 60 type OutputDescriptor struct { 61 ID string `json:"id,omitempty"` // mandatory property 62 Schema string `json:"schema,omitempty"` // mandatory property 63 Name string `json:"name,omitempty"` 64 Description string `json:"description,omitempty"` 65 Display *DataDisplayDescriptor `json:"display,omitempty"` 66 Styles *Styles `json:"styles,omitempty"` 67 } 68 69 // ImageURIWithAltText represents a URI that points to an image along with the alt text for it. 70 type ImageURIWithAltText struct { 71 URI string `json:"uri,omitempty"` // mandatory property 72 Alt string `json:"alt,omitempty"` 73 } 74 75 // DataDisplayDescriptor represents a Data Display Descriptor as defined in 76 // https://identity.foundation/credential-manifest/wallet-rendering/#data-display. 77 type DataDisplayDescriptor struct { 78 Title *DisplayMappingObject `json:"title,omitempty"` 79 Subtitle *DisplayMappingObject `json:"subtitle,omitempty"` 80 Description *DisplayMappingObject `json:"description,omitempty"` 81 Properties []*LabeledDisplayMappingObject `json:"properties,omitempty"` 82 } 83 84 // DisplayMappingObject represents a Display Mapping Object as defined in 85 // https://identity.foundation/wallet-rendering/#display-mapping-object 86 // There are two possibilities here: 87 // 1. If the text field is used, schema is not required. The text field will contain display 88 // information about the target Claim. 89 // 2. If the path field is used, schema is required. Data will be pulled from the target Claim using the path. 90 // TODO (#3045) Support for JSONPath bracket notation. 91 type DisplayMappingObject struct { 92 Text string `json:"text,omitempty"` 93 Paths []string `json:"path,omitempty"` 94 Schema Schema `json:"schema,omitempty"` 95 Fallback string `json:"fallback,omitempty"` 96 } 97 98 // LabeledDisplayMappingObject is a DisplayMappingObject with an additional Label field. 99 // They are used for the dynamic Properties array in a DataDisplayDescriptor. 100 type LabeledDisplayMappingObject struct { 101 DisplayMappingObject 102 Label string `json:"label,omitempty"` // mandatory property 103 } 104 105 // Schema represents Type and (optional) Format information for a DisplayMappingObject that uses the Paths field, 106 // as defined in https://identity.foundation/wallet-rendering/#using-path. 107 type Schema struct { 108 Type string `json:"type"` // MUST be here 109 Format string `json:"format,omitempty"` // MAY be here if the Type is "string". 110 ContentMediaType string `json:"contentMediaType,omitempty"` // MAY be here if the Type is "string". 111 ContentEncoding string `json:"contentEncoding,omitempty"` // MAY be here if the Type is "string". 112 } 113 114 type staticDisplayMappingObjects struct { 115 title string 116 subtitle string 117 description string 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 CredentialManifest object per the specification. 122 func (cm *CredentialManifest) UnmarshalJSON(data []byte) error { 123 err := cm.standardUnmarshal(data) 124 if err != nil { 125 return err 126 } 127 128 err = cm.Validate() 129 if err != nil { 130 return fmt.Errorf("invalid credential manifest: %w", err) 131 } 132 133 return nil 134 } 135 136 // Validate ensures that this CredentialManifest is valid as per the spec. 137 // Note that this function is automatically called when unmarshalling a []byte into a CredentialManifest. 138 func (cm *CredentialManifest) Validate() error { 139 if cm.ID == "" { 140 return errors.New("ID missing") 141 } 142 143 err := validateIssuer(cm.Issuer) 144 if err != nil { 145 return err 146 } 147 148 if len(cm.OutputDescriptors) == 0 { 149 return errors.New("no output descriptors found") 150 } 151 152 err = ValidateOutputDescriptors(cm.OutputDescriptors) 153 if err != nil { 154 return err 155 } 156 157 return nil 158 } 159 160 func validateIssuer(issuer Issuer) error { 161 if issuer.ID == "" { 162 return errors.New("issuer ID missing") 163 } 164 165 if issuer.Styles != nil { 166 return validateStyles(*issuer.Styles) 167 } 168 169 return nil 170 } 171 172 func validateStyles(styles Styles) error { 173 if styles.Thumbnail != nil { 174 return validateImage(*styles.Thumbnail) 175 } 176 177 if styles.Hero != nil { 178 return validateImage(*styles.Hero) 179 } 180 181 return nil 182 } 183 184 func validateImage(image ImageURIWithAltText) error { 185 if image.URI == "" { 186 return errors.New("uri missing for image") 187 } 188 189 return nil 190 } 191 192 // ValidateOutputDescriptors checks the given slice of OutputDescriptors to ensure that they are valid (per the spec) 193 // when placed together within a single Credential Manifest. 194 // To pass validation, the following two conditions must be satisfied: 195 // 1. Each OutputDescriptor must have a unique ID. 196 // 2. Each OutputDescriptor must also have valid contents. See the validateOutputDescriptorDisplay function for details. 197 func ValidateOutputDescriptors(descriptors []*OutputDescriptor) error { 198 allOutputDescriptorIDs := make(map[string]struct{}) 199 200 for i := range descriptors { 201 if descriptors[i].ID == "" { 202 return fmt.Errorf("missing ID for output descriptor at index %d", i) 203 } 204 205 _, foundDuplicateID := allOutputDescriptorIDs[descriptors[i].ID] 206 if foundDuplicateID { 207 return fmt.Errorf("the ID %s appears in multiple output descriptors", descriptors[i].ID) 208 } 209 210 allOutputDescriptorIDs[descriptors[i].ID] = struct{}{} 211 212 if descriptors[i].Schema == "" { 213 return fmt.Errorf("missing schema for output descriptor at index %d", i) 214 } 215 216 err := validateOutputDescriptorDisplay(descriptors[i], i) 217 if err != nil { 218 return err 219 } 220 221 if descriptors[i].Styles != nil { 222 err = validateStyles(*descriptors[i].Styles) 223 if err != nil { 224 return fmt.Errorf("%w at index %d", err, i) 225 } 226 } 227 } 228 229 return nil 230 } 231 232 func (cm *CredentialManifest) hasFormat() bool { 233 if cm.Format == nil { 234 return false 235 } 236 237 return hasAnyAlgorithmsOrProofTypes(*cm.Format) 238 } 239 240 func (cm *CredentialManifest) standardUnmarshal(data []byte) error { 241 // The type alias below is used as to allow the standard json.Unmarshal to be called within a custom unmarshal 242 // function without causing infinite recursion. See https://stackoverflow.com/a/43178272 for more information. 243 type credentialManifestAliasWithoutMethods *CredentialManifest 244 245 err := json.Unmarshal(data, credentialManifestAliasWithoutMethods(cm)) 246 if err != nil { 247 return err 248 } 249 250 return nil 251 } 252 253 func validateOutputDescriptorDisplay(outputDescriptor *OutputDescriptor, outputDescriptorIndex int) error { 254 if outputDescriptor.Display == nil { 255 return nil 256 } 257 258 if outputDescriptor.Display.Title != nil { 259 err := validateDisplayMappingObject(outputDescriptor.Display.Title) 260 if err != nil { 261 return fmt.Errorf("display title for output descriptor at index %d is invalid: %w", 262 outputDescriptorIndex, err) 263 } 264 } 265 266 if outputDescriptor.Display.Subtitle != nil { 267 err := validateDisplayMappingObject(outputDescriptor.Display.Subtitle) 268 if err != nil { 269 return fmt.Errorf("display subtitle for output descriptor at index %d is invalid: %w", 270 outputDescriptorIndex, err) 271 } 272 } 273 274 if outputDescriptor.Display.Description != nil { 275 err := validateDisplayMappingObject(outputDescriptor.Display.Description) 276 if err != nil { 277 return fmt.Errorf("display description for output descriptor at index %d is invalid: %w", 278 outputDescriptorIndex, err) 279 } 280 } 281 282 for i := range outputDescriptor.Display.Properties { 283 err := validateDisplayMappingObject(&outputDescriptor.Display.Properties[i].DisplayMappingObject) 284 if err != nil { 285 return fmt.Errorf("display property at index %d for output descriptor at index %d is invalid: %w", 286 outputDescriptorIndex, i, err) 287 } 288 } 289 290 return nil 291 } 292 293 func validateDisplayMappingObject(displayMappingObject *DisplayMappingObject) error { 294 if len(displayMappingObject.Paths) > 0 { 295 for i, path := range displayMappingObject.Paths { 296 _, err := jsonpath.New(path) // Just using this to ValidateOutputDescriptors the JSONPath. 297 if err != nil { 298 return fmt.Errorf(`path "%s" at index %d is not a valid JSONPath: %w`, path, i, err) 299 } 300 } 301 302 return validateSchema(displayMappingObject) 303 } else if displayMappingObject.Text == "" { 304 return fmt.Errorf(`display mapping object must contain either a paths or a text property`) 305 } 306 307 return nil 308 } 309 310 func validateSchema(displayMappingObject *DisplayMappingObject) error { 311 schemaType := displayMappingObject.Schema.Type 312 313 if schemaType == "string" { 314 if schemaFormatIsValid(displayMappingObject.Schema.Format) { 315 return nil 316 } 317 318 return fmt.Errorf("%s is not a valid string schema format", displayMappingObject.Schema.Format) 319 } 320 321 if schemaType == "boolean" || schemaType == "number" || schemaType == "integer" { 322 return nil 323 } 324 325 return fmt.Errorf("%s is not a valid schema type", schemaType) 326 } 327 328 // Implemented per http://localhost:3000/wallet-rendering/#type-specific-configuration. 329 // This is only checked when the schema type is set to "string". In that case, format is optional (hence the "" check 330 // below). 331 func schemaFormatIsValid(format string) bool { 332 validFormats := []string{ 333 "", "date-time", "time", "date", "email", "idn-email", "hostname", "idn-hostname", 334 "ipv4", "ipv6", "uri", "uri-reference", "iri", "iri-reference", 335 } 336 337 var isValidFormat bool 338 339 for _, validFormat := range validFormats { 340 if format == validFormat { 341 isValidFormat = true 342 break 343 } 344 } 345 346 return isValidFormat 347 } 348 349 func mapDescriptors(manifest *CredentialManifest) map[string]*OutputDescriptor { 350 result := make(map[string]*OutputDescriptor, len(manifest.OutputDescriptors)) 351 352 for _, outputDescr := range manifest.OutputDescriptors { 353 result[outputDescr.ID] = outputDescr 354 } 355 356 return result 357 } 358 359 func selectVCByPath(builder gval.Language, vp interface{}, jsonPath string) (map[string]interface{}, error) { 360 path, err := builder.NewEvaluable(jsonPath) 361 if err != nil { 362 return nil, fmt.Errorf("failed to build new json path evaluator: %w", err) 363 } 364 365 cred, err := path(context.TODO(), vp) 366 if err != nil { 367 return nil, fmt.Errorf("failed to evaluate json path [%s]: %w", jsonPath, err) 368 } 369 370 if credMap, ok := cred.(map[string]interface{}); ok { 371 return credMap, nil 372 } 373 374 return nil, fmt.Errorf("unexpected credential evaluation result") 375 }