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

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  SPDX-License-Identifier: Apache-2.0
     4  */
     5  
     6  package didconfig
     7  
     8  import (
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"net/http"
    13  	"net/url"
    14  
    15  	jsonld "github.com/piprate/json-gold/ld"
    16  
    17  	"github.com/hyperledger/aries-framework-go/pkg/common/log"
    18  	"github.com/hyperledger/aries-framework-go/pkg/doc/did"
    19  	diddoc "github.com/hyperledger/aries-framework-go/pkg/doc/did"
    20  	"github.com/hyperledger/aries-framework-go/pkg/doc/jose"
    21  	"github.com/hyperledger/aries-framework-go/pkg/doc/jwt"
    22  	"github.com/hyperledger/aries-framework-go/pkg/doc/verifiable"
    23  	vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr"
    24  	"github.com/hyperledger/aries-framework-go/pkg/vdr"
    25  	"github.com/hyperledger/aries-framework-go/pkg/vdr/key"
    26  )
    27  
    28  var logger = log.New("aries-framework/doc/didconfig")
    29  
    30  const (
    31  	// ContextV0 is did configuration context version 0.
    32  	ContextV0 = "https://identity.foundation/.well-known/contexts/did-configuration-v0.0.jsonld"
    33  
    34  	// ContextV1 is did configuration context version 1.
    35  	ContextV1 = "https://identity.foundation/.well-known/did-configuration/v1"
    36  
    37  	domainLinkageCredentialType = "DomainLinkageCredential"
    38  
    39  	contextProperty    = "@context"
    40  	linkedDIDsProperty = "linked_dids"
    41  )
    42  
    43  type didResolver interface {
    44  	Resolve(did string, opts ...vdrapi.DIDMethodOption) (*did.DocResolution, error)
    45  }
    46  
    47  // didConfigOpts holds options for the DID Configuration decoding.
    48  type didConfigOpts struct {
    49  	jsonldDocumentLoader jsonld.DocumentLoader
    50  	didResolver          didResolver
    51  }
    52  
    53  // DIDConfigurationOpt is the DID Configuration decoding option.
    54  type DIDConfigurationOpt func(opts *didConfigOpts)
    55  
    56  // WithJSONLDDocumentLoader defines a JSON-LD document loader.
    57  func WithJSONLDDocumentLoader(documentLoader jsonld.DocumentLoader) DIDConfigurationOpt {
    58  	return func(opts *didConfigOpts) {
    59  		opts.jsonldDocumentLoader = documentLoader
    60  	}
    61  }
    62  
    63  // WithVDRegistry defines a vdr service.
    64  func WithVDRegistry(didResolver didResolver) DIDConfigurationOpt {
    65  	return func(opts *didConfigOpts) {
    66  		opts.didResolver = didResolver
    67  	}
    68  }
    69  
    70  type rawDoc struct {
    71  	Context    string        `json:"@context,omitempty"`
    72  	LinkedDIDs []interface{} `json:"linked_dids,omitempty"`
    73  }
    74  
    75  // VerifyDIDAndDomain will verify that there is valid domain linkage credential in did configuration
    76  // for specified did and domain.
    77  func VerifyDIDAndDomain(didConfig []byte, did, domain string, opts ...DIDConfigurationOpt) error {
    78  	// apply options
    79  	didCfgOpts := getDIDConfigurationOpts(opts)
    80  
    81  	// verify required and allowed properties in did configuration
    82  	err := verifyDidConfigurationProperties(didConfig)
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	raw := rawDoc{}
    88  
    89  	err = json.Unmarshal(didConfig, &raw)
    90  	if err != nil {
    91  		return fmt.Errorf("JSON unmarshalling of DID configuration bytes failed: %w", err)
    92  	}
    93  
    94  	credOpts := getParseCredentialOptions(true, didCfgOpts)
    95  
    96  	credentials, err := getCredentials(raw.LinkedDIDs, did, domain, credOpts...)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	logger.Debugf("found %d domain linkage credential(s) for DID[%s] and domain[%s]", len(credentials), did, domain)
   102  
   103  	for _, credBytes := range credentials {
   104  		credOpts := getParseCredentialOptions(false, didCfgOpts)
   105  
   106  		// this time we are parsing credential with proof check so DID will be resolved
   107  		// and public key from did will be used to verify proof
   108  		_, err := verifiable.ParseCredential(credBytes, credOpts...)
   109  		if err == nil {
   110  			// we found domain linkage credential with valid proof so all good
   111  			return nil
   112  		}
   113  
   114  		// failed to verify credential proof - log info and continue to next one
   115  		logger.Warnf("skipping domain linkage credential for DID[%s] and domain[%s] due to error: %s",
   116  			did, domain, err.Error())
   117  	}
   118  
   119  	return fmt.Errorf("domain linkage credential(s) with valid proof not found")
   120  }
   121  
   122  func getDIDConfigurationOpts(opts []DIDConfigurationOpt) *didConfigOpts {
   123  	didCfgOpts := &didConfigOpts{
   124  		jsonldDocumentLoader: jsonld.NewDefaultDocumentLoader(http.DefaultClient),
   125  		didResolver:          vdr.New(vdr.WithVDR(key.New())),
   126  	}
   127  
   128  	for _, opt := range opts {
   129  		opt(didCfgOpts)
   130  	}
   131  
   132  	return didCfgOpts
   133  }
   134  
   135  func verifyDidConfigurationProperties(data []byte) error {
   136  	requiredProperties := []string{contextProperty, linkedDIDsProperty}
   137  	allowedProperties := []string{contextProperty, linkedDIDsProperty}
   138  
   139  	var didCfgMap map[string]interface{}
   140  
   141  	err := json.Unmarshal(data, &didCfgMap)
   142  	if err != nil {
   143  		return fmt.Errorf("JSON unmarshalling of DID configuration bytes failed: %w", err)
   144  	} else if didCfgMap == nil {
   145  		return errors.New("DID configuration payload is not provided")
   146  	}
   147  
   148  	if err := verifyRequiredProperties(didCfgMap, requiredProperties); err != nil {
   149  		return fmt.Errorf("did configuration: %w ", err)
   150  	}
   151  
   152  	return verifyAllowedProperties(didCfgMap, allowedProperties)
   153  }
   154  
   155  func verifyRequiredProperties(values map[string]interface{}, requiredProperties []string) error {
   156  	for _, key := range requiredProperties {
   157  		if _, ok := values[key]; !ok {
   158  			return fmt.Errorf("property '%s' is required", key)
   159  		}
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  func verifyAllowedProperties(values map[string]interface{}, allowedProperty []string) error {
   166  	for key := range values {
   167  		if !contains(key, allowedProperty) {
   168  			return fmt.Errorf("property '%s' is not allowed", key)
   169  		}
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  func isValidDomainLinkageCredential(vc *verifiable.Credential, did, origin string) error {
   176  	// validate JWT format if credential has been parsed from JWT format
   177  	// https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format
   178  	if vc.JWT != "" {
   179  		return validateJWT(vc, did, origin)
   180  	}
   181  
   182  	// validate domain linkage credential rules:
   183  	// https://identity.foundation/.well-known/resources/did-configuration/#domain-linkage-credential
   184  	return validateDomainLinkageCredential(vc, did, origin)
   185  }
   186  
   187  func validateDomainLinkageCredential(vc *verifiable.Credential, did, origin string) error {
   188  	if !contains(domainLinkageCredentialType, vc.Types) {
   189  		return fmt.Errorf("credential is not of %s type", domainLinkageCredentialType)
   190  	}
   191  
   192  	if vc.ID != "" {
   193  		return fmt.Errorf("id MUST NOT be present")
   194  	}
   195  
   196  	if vc.Issued == nil {
   197  		return fmt.Errorf("issuance date MUST be present")
   198  	}
   199  
   200  	if vc.Expired == nil {
   201  		return fmt.Errorf("expiration date MUST be present")
   202  	}
   203  
   204  	if vc.Subject == nil {
   205  		return fmt.Errorf("subject MUST be present")
   206  	}
   207  
   208  	return validateSubject(vc.Subject, did, origin)
   209  }
   210  
   211  func validateJWT(vc *verifiable.Credential, did, origin string) error {
   212  	jsonWebToken, _, err := jwt.Parse(vc.JWT, jwt.WithSignatureVerifier(&noVerifier{}))
   213  	if err != nil {
   214  		return fmt.Errorf("parse JWT: %w", err)
   215  	}
   216  
   217  	if err := validateJWTHeader(jsonWebToken.Headers); err != nil {
   218  		return err
   219  	}
   220  
   221  	if err := validateJWTPayload(vc, jsonWebToken.Payload, did); err != nil {
   222  		return err
   223  	}
   224  
   225  	if err := validateDomainLinkageCredential(vc, did, origin); err != nil {
   226  		return err
   227  	}
   228  
   229  	// TODO: vc MUST be equal to the LD Proof Format without the proof property
   230  	// Having issues with time format being lost when parsing VC from JWT
   231  	return nil
   232  }
   233  
   234  func validateJWTHeader(headers jose.Headers) error {
   235  	_, ok := headers.Algorithm()
   236  	if !ok {
   237  		return fmt.Errorf("alg MUST be present in the JWT Header")
   238  	}
   239  
   240  	_, ok = headers.KeyID()
   241  	if !ok {
   242  		return fmt.Errorf("kid MUST be present in the JWT Header")
   243  	}
   244  
   245  	// relaxing rule 'typ MUST NOT be present in the JWT Header' due to interop
   246  	typ, ok := headers.Type()
   247  	if ok && typ != jwt.TypeJWT {
   248  		return fmt.Errorf("typ is not JWT")
   249  	}
   250  
   251  	allowed := []string{jose.HeaderAlgorithm, jose.HeaderKeyID, jose.HeaderType}
   252  
   253  	err := verifyAllowedProperties(headers, allowed)
   254  	if err != nil {
   255  		return fmt.Errorf("JWT Header: %w", err)
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func validateJWTPayload(vc *verifiable.Credential, payload map[string]interface{}, did string) error {
   262  	// iat added for interop
   263  	allowedProperties := []string{"exp", "iss", "nbf", "sub", "vc", "iat"}
   264  
   265  	err := verifyAllowedProperties(payload, allowedProperties)
   266  	if err != nil {
   267  		return fmt.Errorf("JWT Payload: %w", err)
   268  	}
   269  
   270  	return validateJWTClaims(vc, did)
   271  }
   272  
   273  func validateJWTClaims(vc *verifiable.Credential, did string) error {
   274  	jwtClaims, err := vc.JWTClaims(false)
   275  	if err != nil {
   276  		return err
   277  	}
   278  
   279  	if jwtClaims.Issuer != did {
   280  		return fmt.Errorf("iss MUST be equal to credentialSubject.id")
   281  	}
   282  
   283  	if jwtClaims.Subject != did {
   284  		return fmt.Errorf("sub MUST be equal to credentialSubject.id")
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  func contains(v string, values []string) bool {
   291  	for _, val := range values {
   292  		if v == val {
   293  			return true
   294  		}
   295  	}
   296  
   297  	return false
   298  }
   299  
   300  func validateSubject(subject interface{}, did, origin string) error {
   301  	switch s := subject.(type) {
   302  	case []verifiable.Subject:
   303  		if len(s) > 1 {
   304  			// TODO: Can we have more than one subject in this case
   305  			return fmt.Errorf("encountered multiple subjects")
   306  		}
   307  
   308  		subject := s[0]
   309  
   310  		if subject.ID == "" {
   311  			return fmt.Errorf("credentialSubject.id MUST be present")
   312  		}
   313  
   314  		_, err := diddoc.Parse(subject.ID)
   315  		if err != nil {
   316  			return fmt.Errorf("credentialSubject.id MUST be a DID: %w", err)
   317  		}
   318  
   319  		objOrigin, ok := subject.CustomFields["origin"]
   320  		if !ok {
   321  			return fmt.Errorf("credentialSubject.origin MUST be present")
   322  		}
   323  
   324  		sOrigin, ok := objOrigin.(string)
   325  		if !ok {
   326  			return fmt.Errorf("credentialSubject.origin MUST be string")
   327  		}
   328  
   329  		// domain linkage credential format is valid - now check did configuration resource verification rules
   330  		// https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification
   331  
   332  		// subject ID must equal requested DID
   333  		if subject.ID != did {
   334  			return fmt.Errorf("credential subject ID[%s] is different from requested DID[%s]", subject.ID, did)
   335  		}
   336  
   337  		// subject origin must match the origin the resource was requested from
   338  		err = validateOrigin(sOrigin, origin)
   339  		if err != nil {
   340  			return err
   341  		}
   342  
   343  	default:
   344  		return fmt.Errorf("unexpected interface[%T] for subject", subject)
   345  	}
   346  
   347  	return nil
   348  }
   349  
   350  func validateOrigin(origin1, origin2 string) error {
   351  	url1, err := url.Parse(origin1)
   352  	if err != nil {
   353  		return err
   354  	}
   355  
   356  	url2, err := url.Parse(origin2)
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	// Browsers define same origin based on the following pieces of data:
   362  	// The protocol (e.g., HTTP or HTTPS)
   363  	// The port, if available
   364  	// The host
   365  	if url1.Host != url2.Host || url1.Scheme != url2.Scheme || url1.Port() != url2.Port() {
   366  		return fmt.Errorf("origin[%s] and domain origin[%s] are different", origin1, origin2)
   367  	}
   368  
   369  	return nil
   370  }
   371  
   372  func getCredentials(linkedDIDs []interface{}, did, domain string, opts ...verifiable.CredentialOpt) ([][]byte, error) {
   373  	var credentialsForDIDAndDomain [][]byte
   374  
   375  	for _, linkedDID := range linkedDIDs {
   376  		var rawBytes []byte
   377  
   378  		var err error
   379  
   380  		switch linkedDID := linkedDID.(type) {
   381  		case string: // JWT
   382  			rawBytes = []byte(linkedDID)
   383  		case map[string]interface{}: // Linked Data
   384  			rawBytes, err = json.Marshal(linkedDID)
   385  			if err != nil {
   386  				return nil, err
   387  			}
   388  
   389  		default:
   390  			return nil, fmt.Errorf("unexpected interface[%T] for linked DID", linkedDID)
   391  		}
   392  
   393  		vc, err := verifiable.ParseCredential(rawBytes, opts...)
   394  		if err != nil {
   395  			// failed to parse credential - continue to next one
   396  			logger.Infof("skipping credential due to error: %s", string(rawBytes), err.Error())
   397  
   398  			continue
   399  		}
   400  
   401  		if vc.Issuer.ID != did {
   402  			logger.Infof("skipping credential since issuer[%s] is different from DID[%s]", vc.Issuer.ID, did)
   403  
   404  			continue
   405  		}
   406  
   407  		err = isValidDomainLinkageCredential(vc, did, domain)
   408  		if err != nil {
   409  			logger.Warnf("credential is not a valid domain linkage credential for DID[%s] and domain[%s]: %s",
   410  				did, domain, err.Error())
   411  
   412  			continue
   413  		}
   414  
   415  		credentialsForDIDAndDomain = append(credentialsForDIDAndDomain, rawBytes)
   416  	}
   417  
   418  	if len(credentialsForDIDAndDomain) == 0 {
   419  		return nil, fmt.Errorf("domain linkage credential(s) not found")
   420  	}
   421  
   422  	return credentialsForDIDAndDomain, nil
   423  }
   424  
   425  // noVerifier is used when no JWT signature verification is needed.
   426  // To be used with precaution.
   427  type noVerifier struct{}
   428  
   429  func (v noVerifier) Verify(_ jose.Headers, _, _, _ []byte) error {
   430  	return nil
   431  }
   432  
   433  func getParseCredentialOptions(disableProofCheck bool, opts *didConfigOpts) []verifiable.CredentialOpt {
   434  	var credOpts []verifiable.CredentialOpt
   435  
   436  	credOpts = append(credOpts,
   437  		verifiable.WithNoCustomSchemaCheck(),
   438  		verifiable.WithJSONLDDocumentLoader(opts.jsonldDocumentLoader),
   439  		verifiable.WithStrictValidation())
   440  
   441  	if disableProofCheck {
   442  		credOpts = append(credOpts, verifiable.WithDisabledProofCheck())
   443  	} else {
   444  		credOpts = append(credOpts,
   445  			verifiable.WithPublicKeyFetcher(verifiable.NewVDRKeyResolver(opts.didResolver).PublicKeyFetcher()))
   446  	}
   447  
   448  	return credOpts
   449  }