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 }