github.com/hyperledger/aries-framework-go@v0.3.2/pkg/doc/sdjwt/integration_test.go (about) 1 /* 2 Copyright SecureKey Technologies Inc. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package sdjwt 8 9 import ( 10 "bytes" 11 "crypto" 12 "crypto/ed25519" 13 "crypto/rand" 14 "encoding/json" 15 "fmt" 16 "testing" 17 "time" 18 19 "github.com/go-jose/go-jose/v3/jwt" 20 "github.com/stretchr/testify/require" 21 22 "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport" 23 afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" 24 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder" 25 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer" 26 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/verifier" 27 ) 28 29 const ( 30 testIssuer = "https://example.com/issuer" 31 32 year = 365 * 24 * 60 * time.Minute 33 ) 34 35 func TestSDJWTFlow(t *testing.T) { 36 r := require.New(t) 37 38 issuerPublicKey, issuerPrivateKey, e := ed25519.GenerateKey(rand.Reader) 39 r.NoError(e) 40 41 signer := afjwt.NewEd25519Signer(issuerPrivateKey) 42 43 signatureVerifier, e := afjwt.NewEd25519Verifier(issuerPublicKey) 44 r.NoError(e) 45 46 claims := map[string]interface{}{ 47 "given_name": "Albert", 48 "last_name": "Smith", 49 } 50 51 now := time.Now() 52 53 var timeOpts []issuer.NewOpt 54 timeOpts = append(timeOpts, 55 issuer.WithNotBefore(jwt.NewNumericDate(now)), 56 issuer.WithIssuedAt(jwt.NewNumericDate(now)), 57 issuer.WithExpiry(jwt.NewNumericDate(now.Add(year)))) 58 59 t.Run("success - simple claims (flat option)", func(t *testing.T) { 60 // Issuer will issue SD-JWT for specified claims. 61 token, err := issuer.New(testIssuer, claims, nil, signer, timeOpts...) 62 r.NoError(err) 63 64 var simpleClaimsFlatOption map[string]interface{} 65 err = token.DecodeClaims(&simpleClaimsFlatOption) 66 r.NoError(err) 67 68 printObject(t, "Simple Claims:", simpleClaimsFlatOption) 69 70 combinedFormatForIssuance, err := token.Serialize(false) 71 r.NoError(err) 72 73 fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance)) 74 75 // Holder will parse combined format for issuance and hold on to that 76 // combined format for issuance and the claims that can be selected. 77 claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) 78 r.NoError(err) 79 80 // expected disclosures given_name and last_name 81 r.Equal(2, len(claims)) 82 83 selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name"}, claims) 84 85 // Holder will disclose only sub-set of claims to verifier. 86 combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures) 87 r.NoError(err) 88 89 fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation)) 90 91 // Verifier will validate combined format for presentation and create verified claims. 92 verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, 93 verifier.WithSignatureVerifier(signatureVerifier)) 94 r.NoError(err) 95 r.NotNil(verifiedClaims) 96 97 printObject(t, "Verified Claims", verifiedClaims) 98 99 // expected claims iss, exp, iat, nbf, given_name; last_name was not disclosed 100 r.Equal(5, len(verifiedClaims)) 101 }) 102 103 t.Run("success - with holder binding", func(t *testing.T) { 104 holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader) 105 r.NoError(err) 106 107 holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey) 108 require.NoError(t, err) 109 110 // Issuer will issue SD-JWT for specified claims and holder public key. 111 token, err := issuer.New(testIssuer, claims, nil, signer, 112 issuer.WithHashAlgorithm(crypto.SHA512), 113 issuer.WithNotBefore(jwt.NewNumericDate(now)), 114 issuer.WithIssuedAt(jwt.NewNumericDate(now)), 115 issuer.WithExpiry(jwt.NewNumericDate(now.Add(year))), 116 issuer.WithHolderPublicKey(holderPublicJWK)) 117 r.NoError(err) 118 119 combinedFormatForIssuance, err := token.Serialize(false) 120 r.NoError(err) 121 122 fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance)) 123 124 // Holder will parse combined format for issuance and hold on to that 125 // combined format for issuance and the claims that can be selected. 126 claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) 127 r.NoError(err) 128 129 // expected disclosures given_name and last_name 130 r.Equal(2, len(claims)) 131 132 holderSigner := afjwt.NewEd25519Signer(holderPrivateKey) 133 134 const testAudience = "https://test.com/verifier" 135 const testNonce = "nonce" 136 137 selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name"}, claims) 138 139 // Holder will disclose only sub-set of claims to verifier and add holder binding. 140 combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures, 141 holder.WithHolderBinding(&holder.BindingInfo{ 142 Payload: holder.BindingPayload{ 143 Nonce: testNonce, 144 Audience: testAudience, 145 IssuedAt: jwt.NewNumericDate(time.Now()), 146 }, 147 Signer: holderSigner, 148 })) 149 r.NoError(err) 150 151 fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation)) 152 153 // Verifier will validate combined format for presentation and create verified claims. 154 verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, 155 verifier.WithSignatureVerifier(signatureVerifier), 156 verifier.WithHolderBindingRequired(true), 157 verifier.WithExpectedAudienceForHolderBinding(testAudience), 158 verifier.WithExpectedNonceForHolderBinding(testNonce)) 159 r.NoError(err) 160 161 printObject(t, "Verified Claims", verifiedClaims) 162 163 // expected claims cnf, iss, given_name, iat, nbf, exp; last_name was not disclosed 164 r.Equal(6, len(verifiedClaims)) 165 }) 166 167 t.Run("success - complex claims object with structured claims option", func(t *testing.T) { 168 complexClaims := createComplexClaims() 169 170 // Issuer will issue SD-JWT for specified claims. We will use structured(nested) claims in this test. 171 token, err := issuer.New(testIssuer, complexClaims, nil, signer, 172 issuer.WithStructuredClaims(true)) 173 r.NoError(err) 174 175 var structuredClaims map[string]interface{} 176 err = token.DecodeClaims(&structuredClaims) 177 r.NoError(err) 178 179 printObject(t, "Complex Claims(Structured Option) :", structuredClaims) 180 181 combinedFormatForIssuance, err := token.Serialize(false) 182 r.NoError(err) 183 184 fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance)) 185 186 // Holder will parse combined format for issuance and hold on to that 187 // combined format for issuance and the claims that can be selected. 188 claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) 189 r.NoError(err) 190 191 printObject(t, "Holder Claims", claims) 192 193 r.Equal(10, len(claims)) 194 195 selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name", "email", "street_address"}, claims) 196 197 // Holder will disclose only sub-set of claims to verifier. 198 combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures) 199 r.NoError(err) 200 201 fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation)) 202 203 // Verifier will validate combined format for presentation and create verified claims. 204 verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, 205 verifier.WithSignatureVerifier(signatureVerifier)) 206 r.NoError(err) 207 208 // expected claims iss, given_name, email, street_address; time options not provided 209 r.Equal(4, len(verifiedClaims)) 210 211 printObject(t, "Verified Claims", verifiedClaims) 212 }) 213 214 t.Run("success - complex claims object with flat claims option", func(t *testing.T) { 215 complexClaims := createComplexClaims() 216 217 // Issuer will issue SD-JWT for specified claims. We will use structured(nested) claims in this test. 218 token, err := issuer.New(testIssuer, complexClaims, nil, signer, 219 issuer.WithHashAlgorithm(crypto.SHA384)) 220 r.NoError(err) 221 222 var flatClaims map[string]interface{} 223 err = token.DecodeClaims(&flatClaims) 224 r.NoError(err) 225 226 printObject(t, "Complex Claims (Flat Option)", flatClaims) 227 228 combinedFormatForIssuance, err := token.Serialize(false) 229 r.NoError(err) 230 231 fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", combinedFormatForIssuance)) 232 233 // Holder will parse combined format for issuance and hold on to that 234 // combined format for issuance and the claims that can be selected. 235 claims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) 236 r.NoError(err) 237 238 printObject(t, "Holder Claims", claims) 239 240 r.Equal(7, len(claims)) 241 242 selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name", "email", "address"}, claims) 243 244 // Holder will disclose only sub-set of claims to verifier. 245 combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures) 246 r.NoError(err) 247 248 fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation)) 249 250 // Verifier will validate combined format for presentation and create verified claims. 251 verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, 252 verifier.WithSignatureVerifier(signatureVerifier)) 253 r.NoError(err) 254 255 // expected claims iss, given_name, email, street_address; time options not provided 256 r.Equal(4, len(verifiedClaims)) 257 258 printObject(t, "Verified Claims", verifiedClaims) 259 }) 260 261 t.Run("success - NewFromVC API", func(t *testing.T) { 262 holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader) 263 r.NoError(err) 264 265 holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey) 266 require.NoError(t, err) 267 268 // create VC - we will use template here 269 var vc map[string]interface{} 270 err = json.Unmarshal([]byte(sampleVCFull), &vc) 271 r.NoError(err) 272 273 token, err := issuer.NewFromVC(vc, nil, signer, 274 issuer.WithHolderPublicKey(holderPublicJWK), 275 issuer.WithStructuredClaims(true), 276 issuer.WithNonSelectivelyDisclosableClaims([]string{"id", "degree.type"}), 277 ) 278 r.NoError(err) 279 280 var decoded map[string]interface{} 281 282 err = token.DecodeClaims(&decoded) 283 require.NoError(t, err) 284 285 printObject(t, "SD-JWT Payload", decoded) 286 287 vcCombinedFormatForIssuance, err := token.Serialize(false) 288 r.NoError(err) 289 290 fmt.Println(fmt.Sprintf("issuer SD-JWT: %s", vcCombinedFormatForIssuance)) 291 292 claims, err := holder.Parse(vcCombinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) 293 r.NoError(err) 294 295 printObject(t, "Holder Claims", claims) 296 297 r.Equal(4, len(claims)) 298 299 const testAudience = "https://test.com/verifier" 300 const testNonce = "nonce" 301 302 holderSigner := afjwt.NewEd25519Signer(holderPrivateKey) 303 304 selectedDisclosures := getDisclosuresFromClaimNames([]string{"degree", "id", "name"}, claims) 305 306 // Holder will disclose only sub-set of claims to verifier. 307 combinedFormatForPresentation, err := holder.CreatePresentation(vcCombinedFormatForIssuance, selectedDisclosures, 308 holder.WithHolderBinding(&holder.BindingInfo{ 309 Payload: holder.BindingPayload{ 310 Nonce: testNonce, 311 Audience: testAudience, 312 IssuedAt: jwt.NewNumericDate(time.Now()), 313 }, 314 Signer: holderSigner, 315 })) 316 r.NoError(err) 317 318 fmt.Println(fmt.Sprintf("holder SD-JWT: %s", combinedFormatForPresentation)) 319 320 // Verifier will validate combined format for presentation and create verified claims. 321 // In this case it will be VC since VC was passed in. 322 verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, 323 verifier.WithSignatureVerifier(signatureVerifier)) 324 r.NoError(err) 325 326 printObject(t, "Verified Claims", verifiedClaims) 327 328 r.Equal(len(vc), len(verifiedClaims)) 329 }) 330 } 331 332 func createComplexClaims() map[string]interface{} { 333 claims := map[string]interface{}{ 334 "sub": "john_doe_42", 335 "given_name": "John", 336 "family_name": "Doe", 337 "email": "johndoe@example.com", 338 "phone_number": "+1-202-555-0101", 339 "birthdate": "1940-01-01", 340 "address": map[string]interface{}{ 341 "street_address": "123 Main St", 342 "locality": "Anytown", 343 "region": "Anystate", 344 "country": "US", 345 }, 346 } 347 348 return claims 349 } 350 351 func getDisclosuresFromClaimNames(selectedClaimNames []string, claims []*holder.Claim) []string { 352 var disclosures []string 353 354 for _, c := range claims { 355 if contains(selectedClaimNames, c.Name) { 356 disclosures = append(disclosures, c.Disclosure) 357 } 358 } 359 360 return disclosures 361 } 362 363 func contains(values []string, val string) bool { 364 for _, v := range values { 365 if v == val { 366 return true 367 } 368 } 369 370 return false 371 } 372 373 func printObject(t *testing.T, name string, obj interface{}) { 374 t.Helper() 375 376 objBytes, err := json.Marshal(obj) 377 require.NoError(t, err) 378 379 prettyJSON, err := prettyPrint(objBytes) 380 require.NoError(t, err) 381 382 fmt.Println(name + ":") 383 fmt.Println(prettyJSON) 384 } 385 386 func prettyPrint(msg []byte) (string, error) { 387 var prettyJSON bytes.Buffer 388 389 err := json.Indent(&prettyJSON, msg, "", "\t") 390 if err != nil { 391 return "", err 392 } 393 394 return prettyJSON.String(), nil 395 } 396 397 const sampleVCFull = ` 398 { 399 "iat": 1673987547, 400 "iss": "did:example:76e12ec712ebc6f1c221ebfeb1f", 401 "jti": "http://example.edu/credentials/1872", 402 "nbf": 1673987547, 403 "sub": "did:example:ebfeb1f712ebc6f1c276e12ec21", 404 "vc": { 405 "@context": [ 406 "https://www.w3.org/2018/credentials/v1" 407 ], 408 "credentialSubject": { 409 "degree": { 410 "degree": "MIT", 411 "type": "BachelorDegree", 412 "id": "some-id" 413 }, 414 "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", 415 "name": "Jayden Doe", 416 "spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1" 417 }, 418 "first_name": "First name", 419 "id": "http://example.edu/credentials/1872", 420 "info": "Info", 421 "issuanceDate": "2023-01-17T22:32:27.468109817+02:00", 422 "issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f", 423 "last_name": "Last name", 424 "type": "VerifiableCredential" 425 } 426 }`