github.com/hyperledger/aries-framework-go@v0.3.2/pkg/doc/sdjwt/issuer/issuer.go (about) 1 /* 2 Copyright SecureKey Technologies Inc. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 /* 8 Package issuer enables the Issuer: An entity that creates SD-JWTs. 9 10 An SD-JWT is a digitally signed document containing digests over the claims 11 (per claim: a random salt, the claim name and the claim value). 12 It MAY further contain clear-text claims that are always disclosed to the Verifier. 13 It MUST be digitally signed using the Issuer's private key. 14 15 SD-JWT-DOC = (METADATA, SD-CLAIMS, NON-SD-CLAIMS) 16 SD-JWT = SD-JWT-DOC | SIG(SD-JWT-DOC, ISSUER-PRIV-KEY) 17 18 SD-CLAIMS is an array of digest values that ensure the integrity of 19 and map to the respective Disclosures. Digest values are calculated 20 over the Disclosures, each of which contains the claim name (CLAIM-NAME), 21 the claim value (CLAIM-VALUE), and a random salt (SALT). 22 Digests are calculated using a hash function: 23 24 SD-CLAIMS = ( 25 HASH(SALT, CLAIM-NAME, CLAIM-VALUE) 26 )* 27 28 SD-CLAIMS can also be nested deeper to capture more complex objects. 29 30 The Issuer further creates a set of Disclosures for all claims in the 31 SD-JWT. The Disclosures are sent to the Holder together with the SD-JWT: 32 33 DISCLOSURES = ( 34 (SALT, CLAIM-NAME, CLAIM-VALUE) 35 )* 36 37 The SD-JWT and the Disclosures are sent to the Holder by the Issuer: 38 39 COMBINED-ISSUANCE = SD-JWT | DISCLOSURES 40 */ 41 package issuer 42 43 import ( 44 "crypto" 45 "crypto/rand" 46 "encoding/base64" 47 "encoding/json" 48 "errors" 49 "fmt" 50 mathrand "math/rand" 51 "strings" 52 "time" 53 54 "github.com/go-jose/go-jose/v3/jwt" 55 56 "github.com/hyperledger/aries-framework-go/pkg/common/utils" 57 "github.com/hyperledger/aries-framework-go/pkg/doc/jose" 58 "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" 59 afgjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" 60 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" 61 jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" 62 ) 63 64 const ( 65 defaultHash = crypto.SHA256 66 defaultSaltSize = 128 / 8 67 68 decoyMinElements = 1 69 decoyMaxElements = 4 70 71 credentialSubjectKey = "credentialSubject" 72 vcKey = "vc" 73 ) 74 75 var mr = mathrand.New(mathrand.NewSource(time.Now().Unix())) // nolint:gochecknoglobals 76 77 // Claims defines JSON Web Token Claims (https://tools.ietf.org/html/rfc7519#section-4) 78 type Claims jwt.Claims 79 80 // newOpts holds options for creating new SD-JWT. 81 type newOpts struct { 82 Subject string 83 Audience string 84 JTI string 85 ID string 86 87 Expiry *jwt.NumericDate 88 NotBefore *jwt.NumericDate 89 IssuedAt *jwt.NumericDate 90 91 HolderPublicKey *jwk.JWK 92 93 HashAlg crypto.Hash 94 95 jsonMarshal func(v interface{}) ([]byte, error) 96 getSalt func() (string, error) 97 98 addDecoyDigests bool 99 structuredClaims bool 100 101 nonSDClaimsMap map[string]bool 102 } 103 104 // NewOpt is the SD-JWT New option. 105 type NewOpt func(opts *newOpts) 106 107 // WithJSONMarshaller is option is for marshalling disclosure. 108 func WithJSONMarshaller(jsonMarshal func(v interface{}) ([]byte, error)) NewOpt { 109 return func(opts *newOpts) { 110 opts.jsonMarshal = jsonMarshal 111 } 112 } 113 114 // WithSaltFnc is an option for generating salt. Mostly used for testing. 115 // A new salt MUST be chosen for each claim independently of other salts. 116 // The RECOMMENDED minimum length of the randomly-generated portion of the salt is 128 bits. 117 // It is RECOMMENDED to base64url-encode the salt value, producing a string. 118 func WithSaltFnc(fnc func() (string, error)) NewOpt { 119 return func(opts *newOpts) { 120 opts.getSalt = fnc 121 } 122 } 123 124 // WithIssuedAt is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. 125 func WithIssuedAt(issuedAt *jwt.NumericDate) NewOpt { 126 return func(opts *newOpts) { 127 opts.IssuedAt = issuedAt 128 } 129 } 130 131 // WithAudience is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. 132 func WithAudience(audience string) NewOpt { 133 return func(opts *newOpts) { 134 opts.Audience = audience 135 } 136 } 137 138 // WithExpiry is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. 139 func WithExpiry(expiry *jwt.NumericDate) NewOpt { 140 return func(opts *newOpts) { 141 opts.Expiry = expiry 142 } 143 } 144 145 // WithNotBefore is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. 146 func WithNotBefore(notBefore *jwt.NumericDate) NewOpt { 147 return func(opts *newOpts) { 148 opts.NotBefore = notBefore 149 } 150 } 151 152 // WithSubject is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. 153 func WithSubject(subject string) NewOpt { 154 return func(opts *newOpts) { 155 opts.Subject = subject 156 } 157 } 158 159 // WithJTI is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. 160 func WithJTI(jti string) NewOpt { 161 return func(opts *newOpts) { 162 opts.JTI = jti 163 } 164 } 165 166 // WithID is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. 167 func WithID(id string) NewOpt { 168 return func(opts *newOpts) { 169 opts.ID = id 170 } 171 } 172 173 // WithHolderPublicKey is an option for SD-JWT payload. 174 // The Holder can prove legitimate possession of an SD-JWT by proving control over the same private key during 175 // the issuance and presentation. An SD-JWT with Holder Binding contains a public key or a reference to a public key 176 // that matches to the private key controlled by the Holder. 177 // The "cnf" claim value MUST represent only a single proof-of-possession key. This implementation is using CNF "jwk". 178 func WithHolderPublicKey(jwk *jwk.JWK) NewOpt { 179 return func(opts *newOpts) { 180 opts.HolderPublicKey = jwk 181 } 182 } 183 184 // WithHashAlgorithm is an option for hashing disclosures. 185 func WithHashAlgorithm(alg crypto.Hash) NewOpt { 186 return func(opts *newOpts) { 187 opts.HashAlg = alg 188 } 189 } 190 191 // WithDecoyDigests is an option for adding decoy digests(default is false). 192 func WithDecoyDigests(flag bool) NewOpt { 193 return func(opts *newOpts) { 194 opts.addDecoyDigests = flag 195 } 196 } 197 198 // WithStructuredClaims is an option for handling structured claims(default is false). 199 func WithStructuredClaims(flag bool) NewOpt { 200 return func(opts *newOpts) { 201 opts.structuredClaims = flag 202 } 203 } 204 205 // WithNonSelectivelyDisclosableClaims is an option for provide claim names that should be ignored when creating 206 // selectively disclosable claims. 207 // For example if you would like to not selectively disclose id and degree type from the following claims: 208 // { 209 // 210 // "degree": { 211 // "degree": "MIT", 212 // "type": "BachelorDegree", 213 // }, 214 // "name": "Jayden Doe", 215 // "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", 216 // } 217 // 218 // you should specify the following array: []string{"id", "degree.type"}. 219 func WithNonSelectivelyDisclosableClaims(nonSDClaims []string) NewOpt { 220 return func(opts *newOpts) { 221 opts.nonSDClaimsMap = common.SliceToMap(nonSDClaims) 222 } 223 } 224 225 // New creates new signed Selective Disclosure JWT based on input claims. 226 // The Issuer MUST create a Disclosure for each selectively disclosable claim as follows: 227 // Create an array of three elements in this order: 228 // 229 // A salt value. Generated by the system, the salt value MUST be unique for each claim that is to be selectively 230 // disclosed. 231 // The claim name, or key, as it would be used in a regular JWT body. This MUST be a string. 232 // The claim's value, as it would be used in a regular JWT body. The value MAY be of any type that is allowed in JSON, 233 // including numbers, strings, booleans, arrays, and objects. 234 // 235 // Then JSON-encode the array such that an UTF-8 string is produced. 236 // Then base64url-encode the byte representation of the UTF-8 string to create the Disclosure. 237 func New(issuer string, claims interface{}, headers jose.Headers, 238 signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) { 239 nOpts := &newOpts{ 240 jsonMarshal: json.Marshal, 241 getSalt: generateSalt, 242 HashAlg: defaultHash, 243 nonSDClaimsMap: make(map[string]bool), 244 } 245 246 for _, opt := range opts { 247 opt(nOpts) 248 } 249 250 claimsMap, err := afgjwt.PayloadToMap(claims) 251 if err != nil { 252 return nil, fmt.Errorf("convert payload to map: %w", err) 253 } 254 255 // check for the presence of the _sd claim in claims map 256 found := common.KeyExistsInMap(common.SDKey, claimsMap) 257 if found { 258 return nil, fmt.Errorf("key '%s' cannot be present in the claims", common.SDKey) 259 } 260 261 disclosures, digests, err := createDisclosuresAndDigests("", claimsMap, nOpts) 262 if err != nil { 263 return nil, err 264 } 265 266 payload, err := jsonutil.MergeCustomFields(createPayload(issuer, nOpts), digests) 267 if err != nil { 268 return nil, fmt.Errorf("failed to merge payload and digests: %w", err) 269 } 270 271 signedJWT, err := afgjwt.NewSigned(payload, headers, signer) 272 if err != nil { 273 return nil, fmt.Errorf("failed to create SD-JWT from payload[%+v]: %w", payload, err) 274 } 275 276 return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil 277 } 278 279 /* 280 NewFromVC creates new signed Selective Disclosure JWT based on Verifiable Credential. 281 282 Algorithm: 283 - extract credential subject map from verifiable credential 284 - create un-signed SD-JWT plus Disclosures with credential subject map 285 - decode claims from SD-JWT to get credential subject map with selective disclosures 286 - replace VC credential subject with newly created credential subject with selective disclosures 287 - create signed SD-JWT based on VC 288 - return signed SD-JWT plus Disclosures 289 */ 290 func NewFromVC(vc map[string]interface{}, headers jose.Headers, 291 signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) { 292 csObj, ok := common.GetKeyFromVC(credentialSubjectKey, vc) 293 if !ok { 294 return nil, fmt.Errorf("credential subject not found") 295 } 296 297 cs, ok := csObj.(map[string]interface{}) 298 if !ok { 299 return nil, fmt.Errorf("credential subject must be an object") 300 } 301 302 token, err := New("", cs, nil, &unsecuredJWTSigner{}, opts...) 303 if err != nil { 304 return nil, err 305 } 306 307 selectiveCredentialSubject := utils.CopyMap(token.SignedJWT.Payload) 308 // move _sd_alg key from credential subject to vc as per example 4 in spec 309 vc[vcKey].(map[string]interface{})[common.SDAlgorithmKey] = selectiveCredentialSubject[common.SDAlgorithmKey] 310 delete(selectiveCredentialSubject, common.SDAlgorithmKey) 311 312 // move cnf key from credential subject to vc as per example 4 in spec 313 cnfObj, ok := selectiveCredentialSubject[common.CNFKey] 314 if ok { 315 vc[vcKey].(map[string]interface{})[common.CNFKey] = cnfObj 316 delete(selectiveCredentialSubject, common.CNFKey) 317 } 318 319 // update VC with 'selective' credential subject 320 vc[vcKey].(map[string]interface{})[credentialSubjectKey] = selectiveCredentialSubject 321 322 // sign VC with 'selective' credential subject 323 signedJWT, err := afgjwt.NewSigned(vc, headers, signer) 324 if err != nil { 325 return nil, err 326 } 327 328 sdJWT := &SelectiveDisclosureJWT{Disclosures: token.Disclosures, SignedJWT: signedJWT} 329 330 return sdJWT, nil 331 } 332 333 func createPayload(issuer string, nOpts *newOpts) *payload { 334 var cnf map[string]interface{} 335 if nOpts.HolderPublicKey != nil { 336 cnf = make(map[string]interface{}) 337 cnf["jwk"] = nOpts.HolderPublicKey 338 } 339 340 payload := &payload{ 341 Issuer: issuer, 342 JTI: nOpts.JTI, 343 ID: nOpts.ID, 344 Subject: nOpts.Subject, 345 Audience: nOpts.Audience, 346 IssuedAt: nOpts.IssuedAt, 347 Expiry: nOpts.Expiry, 348 NotBefore: nOpts.NotBefore, 349 CNF: cnf, 350 SDAlg: strings.ToLower(nOpts.HashAlg.String()), 351 } 352 353 return payload 354 } 355 356 func createDigests(disclosures []string, nOpts *newOpts) ([]string, error) { 357 var digests []string 358 359 for _, disclosure := range disclosures { 360 digest, inErr := common.GetHash(nOpts.HashAlg, disclosure) 361 if inErr != nil { 362 return nil, fmt.Errorf("hash disclosure: %w", inErr) 363 } 364 365 digests = append(digests, digest) 366 } 367 368 mr.Shuffle(len(digests), func(i, j int) { 369 digests[i], digests[j] = digests[j], digests[i] 370 }) 371 372 return digests, nil 373 } 374 375 func createDecoyDisclosures(opts *newOpts) ([]string, error) { 376 if !opts.addDecoyDigests { 377 return nil, nil 378 } 379 380 n := mr.Intn(decoyMaxElements-decoyMinElements+1) + decoyMinElements 381 382 var decoyDisclosures []string 383 384 for i := 0; i < n; i++ { 385 salt, err := opts.getSalt() 386 if err != nil { 387 return nil, err 388 } 389 390 decoyDisclosures = append(decoyDisclosures, salt) 391 } 392 393 return decoyDisclosures, nil 394 } 395 396 // SelectiveDisclosureJWT defines Selective Disclosure JSON Web Token (https://tools.ietf.org/html/rfc7519) 397 type SelectiveDisclosureJWT struct { 398 SignedJWT *afgjwt.JSONWebToken 399 Disclosures []string 400 } 401 402 // DecodeClaims fills input c with claims of a token. 403 func (j *SelectiveDisclosureJWT) DecodeClaims(c interface{}) error { 404 return j.SignedJWT.DecodeClaims(c) 405 } 406 407 // LookupStringHeader makes look up of particular header with string value. 408 func (j *SelectiveDisclosureJWT) LookupStringHeader(name string) string { 409 return j.SignedJWT.LookupStringHeader(name) 410 } 411 412 // Serialize makes (compact) serialization of token. 413 func (j *SelectiveDisclosureJWT) Serialize(detached bool) (string, error) { 414 if j.SignedJWT == nil { 415 return "", errors.New("JWS serialization is supported only") 416 } 417 418 signedJWT, err := j.SignedJWT.Serialize(detached) 419 if err != nil { 420 return "", err 421 } 422 423 cf := common.CombinedFormatForIssuance{ 424 SDJWT: signedJWT, 425 Disclosures: j.Disclosures, 426 } 427 428 return cf.Serialize(), nil 429 } 430 431 func createDisclosuresAndDigests(path string, claims map[string]interface{}, opts *newOpts) ([]string, map[string]interface{}, error) { // nolint:lll 432 var disclosures []string 433 434 var levelDisclosures []string 435 436 digestsMap := make(map[string]interface{}) 437 438 decoyDisclosures, err := createDecoyDisclosures(opts) 439 if err != nil { 440 return nil, nil, fmt.Errorf("failed to create decoy disclosures: %w", err) 441 } 442 443 for key, value := range claims { 444 curPath := key 445 if path != "" { 446 curPath = path + "." + key 447 } 448 449 if obj, ok := value.(map[string]interface{}); ok && opts.structuredClaims { 450 nestedDisclosures, nestedDigestsMap, e := createDisclosuresAndDigests(curPath, obj, opts) 451 if e != nil { 452 return nil, nil, e 453 } 454 455 digestsMap[key] = nestedDigestsMap 456 457 disclosures = append(disclosures, nestedDisclosures...) 458 } else { 459 if _, ok := opts.nonSDClaimsMap[curPath]; ok { 460 digestsMap[key] = value 461 462 continue 463 } 464 465 disclosure, e := createDisclosure(key, value, opts) 466 if e != nil { 467 return nil, nil, fmt.Errorf("create disclosure: %w", e) 468 } 469 470 levelDisclosures = append(levelDisclosures, disclosure) 471 } 472 } 473 474 disclosures = append(disclosures, levelDisclosures...) 475 476 digests, err := createDigests(append(levelDisclosures, decoyDisclosures...), opts) 477 if err != nil { 478 return nil, nil, err 479 } 480 481 digestsMap[common.SDKey] = digests 482 483 return disclosures, digestsMap, nil 484 } 485 486 func createDisclosure(key string, value interface{}, opts *newOpts) (string, error) { 487 salt, err := opts.getSalt() 488 if err != nil { 489 return "", fmt.Errorf("generate salt: %w", err) 490 } 491 492 disclosure := []interface{}{salt, key, value} 493 494 disclosureBytes, err := opts.jsonMarshal(disclosure) 495 if err != nil { 496 return "", fmt.Errorf("marshal disclosure: %w", err) 497 } 498 499 return base64.RawURLEncoding.EncodeToString(disclosureBytes), nil 500 } 501 502 func generateSalt() (string, error) { 503 salt := make([]byte, defaultSaltSize) 504 505 _, err := rand.Read(salt) 506 if err != nil { 507 return "", err 508 } 509 510 // it is RECOMMENDED to base64url-encode the salt value, producing a string. 511 return base64.RawURLEncoding.EncodeToString(salt), nil 512 } 513 514 // payload represents SD-JWT payload. 515 type payload struct { 516 // registered claim names 517 Issuer string `json:"iss,omitempty"` 518 Subject string `json:"sub,omitempty"` 519 Audience string `json:"aud,omitempty"` 520 JTI string `json:"jti,omitempty"` 521 Expiry *jwt.NumericDate `json:"exp,omitempty"` 522 NotBefore *jwt.NumericDate `json:"nbf,omitempty"` 523 IssuedAt *jwt.NumericDate `json:"iat,omitempty"` 524 525 // non-registered name that can be used for claims based holder binding 526 ID string `json:"id,omitempty"` 527 528 // SD-JWT specific 529 CNF map[string]interface{} `json:"cnf,omitempty"` 530 SDAlg string `json:"_sd_alg,omitempty"` 531 } 532 533 type unsecuredJWTSigner struct{} 534 535 func (s unsecuredJWTSigner) Sign(_ []byte) ([]byte, error) { 536 return []byte(""), nil 537 } 538 539 func (s unsecuredJWTSigner) Headers() jose.Headers { 540 return map[string]interface{}{ 541 jose.HeaderAlgorithm: afgjwt.AlgorithmNone, 542 } 543 }