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  }