github.com/hyperledger/aries-framework-go@v0.3.2/pkg/doc/verifiable/credential_sdjwt_test.go (about) 1 /* 2 Copyright SecureKey Technologies Inc. All Rights Reserved. 3 SPDX-License-Identifier: Apache-2.0 4 */ 5 6 package verifiable 7 8 import ( 9 "crypto" 10 "crypto/ed25519" 11 "crypto/rand" 12 "encoding/base64" 13 "fmt" 14 "sort" 15 "testing" 16 17 "github.com/go-jose/go-jose/v3/jwt" 18 "github.com/stretchr/testify/require" 19 20 "github.com/hyperledger/aries-framework-go/pkg/doc/jose" 21 22 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder" 23 24 afgojwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" 25 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" 26 "github.com/hyperledger/aries-framework-go/pkg/kms" 27 ) 28 29 func TestParseSDJWT(t *testing.T) { 30 pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) 31 require.NoError(t, err) 32 33 sdJWTString, issuerID := createTestSDJWTCred(t, privKey) 34 35 t.Run("success", func(t *testing.T) { 36 newVC, e := ParseCredential([]byte(sdJWTString), 37 WithPublicKeyFetcher(createDIDKeyFetcher(t, pubKey, issuerID))) 38 require.NoError(t, e) 39 require.NotNil(t, newVC) 40 }) 41 42 t.Run("success with sd alg in subject", func(t *testing.T) { 43 vc, e := ParseCredential([]byte(sdJWTString), WithDisabledProofCheck()) 44 require.NoError(t, e) 45 46 claims, e := vc.JWTClaims(false) 47 require.NoError(t, e) 48 49 claims.VC["credentialSubject"].(map[string]interface{})["_sd_alg"] = claims.VC["_sd_alg"] 50 delete(claims.VC, "_sd_alg") 51 52 ed25519Signer, e := newCryptoSigner(kms.ED25519Type) 53 require.NoError(t, e) 54 55 vc.JWT, e = claims.MarshalJWS(EdDSA, ed25519Signer, issuerID+"#keys-1") 56 require.NoError(t, e) 57 58 modifiedCred, e := vc.MarshalWithDisclosure(DiscloseAll()) 59 require.NoError(t, e) 60 61 newVC, e := ParseCredential([]byte(modifiedCred), 62 WithPublicKeyFetcher(createDIDKeyFetcher(t, ed25519Signer.PublicKeyBytes(), issuerID))) 63 require.NoError(t, e) 64 require.NotNil(t, newVC) 65 }) 66 67 t.Run("success with mock holder binding", func(t *testing.T) { 68 mockHolderBinding := "e30.e30.mockHolderBinding" 69 70 newVC, e := ParseCredential([]byte(sdJWTString+common.CombinedFormatSeparator+mockHolderBinding), 71 WithPublicKeyFetcher(createDIDKeyFetcher(t, pubKey, issuerID))) 72 require.NoError(t, e) 73 require.Equal(t, mockHolderBinding, newVC.SDHolderBinding) 74 }) 75 76 t.Run("invalid SDJWT disclosures", func(t *testing.T) { 77 sdJWTWithUnknownDisclosure := sdJWTString + 78 common.CombinedFormatSeparator + base64.RawURLEncoding.EncodeToString([]byte("blah blah")) 79 80 newVC, e := ParseCredential([]byte(sdJWTWithUnknownDisclosure), WithDisabledProofCheck()) 81 require.Error(t, e) 82 require.Nil(t, newVC) 83 require.Contains(t, e.Error(), "invalid SDJWT disclosures") 84 }) 85 } 86 87 func TestMarshalWithDisclosure(t *testing.T) { 88 _, privKey, err := ed25519.GenerateKey(rand.Reader) 89 require.NoError(t, err) 90 91 sourceCred, _ := createTestSDJWTCred(t, privKey) 92 93 t.Run("success", func(t *testing.T) { 94 newVC, e2 := ParseCredential([]byte(sourceCred), WithDisabledProofCheck()) 95 require.NoError(t, e2) 96 97 t.Run("disclose all with holder binding", func(t *testing.T) { 98 _, privKey, err := ed25519.GenerateKey(rand.Reader) 99 require.NoError(t, err) 100 101 var iat jwt.NumericDate = 0 102 103 resultCred, err := newVC.MarshalWithDisclosure(DiscloseAll(), DisclosureHolderBinding(&holder.BindingInfo{ 104 Payload: holder.BindingPayload{ 105 Nonce: "abc123", 106 Audience: "foo", 107 IssuedAt: &iat, 108 }, 109 Signer: afgojwt.NewEd25519Signer(privKey), 110 })) 111 require.NoError(t, err) 112 113 src := common.ParseCombinedFormatForPresentation(sourceCred + common.CombinedFormatSeparator) 114 res := common.ParseCombinedFormatForPresentation(resultCred) 115 116 require.Equal(t, src.SDJWT, res.SDJWT) 117 118 sort.Slice(src.Disclosures, func(i, j int) bool { 119 return src.Disclosures[i] < src.Disclosures[j] 120 }) 121 122 sort.Slice(res.Disclosures, func(i, j int) bool { 123 return res.Disclosures[i] < res.Disclosures[j] 124 }) 125 126 require.Equal(t, src.Disclosures, res.Disclosures) 127 require.NotEmpty(t, res.HolderBinding) 128 }) 129 130 t.Run("disclose required and some if-available claims", func(t *testing.T) { 131 resultCred, err := newVC.MarshalWithDisclosure( 132 DiscloseGivenRequired([]string{"type"}), 133 DiscloseGivenIfAvailable([]string{"university", "favourite-animal"})) 134 require.NoError(t, err) 135 136 res := common.ParseCombinedFormatForPresentation(resultCred) 137 require.Len(t, res.Disclosures, 2) 138 }) 139 140 t.Run("disclose selected claims by creating SD-JWT from vc", func(t *testing.T) { 141 _, privKey, err := ed25519.GenerateKey(rand.Reader) 142 require.NoError(t, err) 143 144 vc, err := parseTestCredential(t, []byte(jwtTestCredential)) 145 require.NoError(t, err) 146 147 var iat jwt.NumericDate = 0 148 149 resultCred, err := vc.MarshalWithDisclosure( 150 DiscloseGivenRequired([]string{"university"}), 151 DisclosureSigner(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1"), 152 DisclosureHolderBinding(&holder.BindingInfo{ 153 Payload: holder.BindingPayload{ 154 Nonce: "abc123", 155 Audience: "foo", 156 IssuedAt: &iat, 157 }, 158 Signer: afgojwt.NewEd25519Signer(privKey), 159 })) 160 require.NoError(t, err) 161 162 res := common.ParseCombinedFormatForPresentation(resultCred) 163 require.Len(t, res.Disclosures, 1) 164 require.NotEmpty(t, res.HolderBinding) 165 }) 166 }) 167 168 t.Run("failure", func(t *testing.T) { 169 newVC, e2 := ParseCredential([]byte(sourceCred), WithDisabledProofCheck()) 170 require.NoError(t, e2) 171 172 t.Run("incompatible options", func(t *testing.T) { 173 resultCred, err := newVC.MarshalWithDisclosure( 174 DiscloseAll(), 175 DiscloseGivenIfAvailable([]string{"university", "favourite-animal"})) 176 require.Error(t, err) 177 require.Empty(t, resultCred) 178 require.Contains(t, err.Error(), "incompatible options provided") 179 180 resultCred, err = newVC.MarshalWithDisclosure( 181 DiscloseGivenRequired([]string{"id"}), 182 DiscloseAll()) 183 require.Error(t, err) 184 require.Empty(t, resultCred) 185 require.Contains(t, err.Error(), "incompatible options provided") 186 }) 187 188 t.Run("missing required claim", func(t *testing.T) { 189 t.Run("not in disclosure list", func(t *testing.T) { 190 resultCred, err := newVC.MarshalWithDisclosure(DiscloseGivenRequired([]string{"favourite-animal"})) 191 require.Error(t, err) 192 require.Empty(t, resultCred) 193 require.Contains(t, err.Error(), "disclosure list missing required claim") 194 }) 195 196 t.Run("disclosure list empty", func(t *testing.T) { 197 badVC, err := ParseCredential([]byte(sourceCred), WithDisabledProofCheck()) 198 require.NoError(t, err) 199 200 // disclosure list empty 201 badVC.SDJWTDisclosures = nil 202 203 resultCred, err := badVC.MarshalWithDisclosure(DiscloseGivenRequired([]string{"id"})) 204 require.Error(t, err) 205 require.Empty(t, resultCred) 206 require.Contains(t, err.Error(), "disclosure list missing required claim") 207 }) 208 209 t.Run("created sdjwt but claim not in VC", func(t *testing.T) { 210 _, privKey, err := ed25519.GenerateKey(rand.Reader) 211 require.NoError(t, err) 212 213 vc, err := parseTestCredential(t, []byte(jwtTestCredential)) 214 require.NoError(t, err) 215 216 resultCred, err := vc.MarshalWithDisclosure( 217 DiscloseGivenRequired([]string{"favourite-animal"}), 218 DisclosureSigner(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1"), 219 ) 220 require.Error(t, err) 221 require.Empty(t, resultCred) 222 require.Contains(t, err.Error(), "disclosure list missing required claim") 223 }) 224 }) 225 226 t.Run("holder binding error", func(t *testing.T) { 227 expectErr := fmt.Errorf("expected error") 228 229 resultCred, err := newVC.MarshalWithDisclosure(DiscloseAll(), DisclosureHolderBinding(&holder.BindingInfo{ 230 Payload: holder.BindingPayload{ 231 Nonce: "abc123", 232 Audience: "foo", 233 }, 234 Signer: &mockSigner{signErr: expectErr}, 235 })) 236 require.Error(t, err) 237 require.Empty(t, resultCred) 238 require.ErrorIs(t, err, expectErr) 239 require.Contains(t, err.Error(), "failed to create holder binding") 240 }) 241 242 t.Run("missing signer when creating fresh SD-JWT credential", func(t *testing.T) { 243 vc, err := parseTestCredential(t, []byte(jwtTestCredential)) 244 require.NoError(t, err) 245 246 resultCred, err := vc.MarshalWithDisclosure(DiscloseAll()) 247 require.Error(t, err) 248 require.Empty(t, resultCred) 249 require.Contains(t, err.Error(), "credential needs signer") 250 }) 251 252 t.Run("signer error creating fresh SD-JWT credential", func(t *testing.T) { 253 expectErr := fmt.Errorf("expected error") 254 255 vc, err := parseTestCredential(t, []byte(jwtTestCredential)) 256 require.NoError(t, err) 257 258 resultCred, err := vc.MarshalWithDisclosure( 259 DiscloseAll(), 260 DisclosureSigner(&mockSigner{signErr: expectErr}, "")) 261 require.Error(t, err) 262 require.Empty(t, resultCred) 263 require.ErrorIs(t, err, expectErr) 264 require.Contains(t, err.Error(), "creating SD-JWT from Credential") 265 }) 266 267 t.Run("holder binding error - when creating fresh SD-JWT credential", func(t *testing.T) { 268 expectErr := fmt.Errorf("expected error") 269 _, privKey, err := ed25519.GenerateKey(rand.Reader) 270 require.NoError(t, err) 271 272 vc, err := parseTestCredential(t, []byte(jwtTestCredential)) 273 require.NoError(t, err) 274 275 resultCred, err := vc.MarshalWithDisclosure( 276 DiscloseAll(), 277 DisclosureSigner(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1"), 278 DisclosureHolderBinding(&holder.BindingInfo{ 279 Payload: holder.BindingPayload{ 280 Nonce: "abc123", 281 Audience: "foo", 282 }, 283 Signer: &mockSigner{signErr: expectErr}, 284 })) 285 require.Error(t, err) 286 require.Empty(t, resultCred) 287 require.ErrorIs(t, err, expectErr) 288 require.Contains(t, err.Error(), "create SD-JWT presentation") 289 }) 290 }) 291 } 292 293 func TestMakeSDJWT(t *testing.T) { 294 pubKey, privKey, e := ed25519.GenerateKey(rand.Reader) 295 require.NoError(t, e) 296 297 testCred := []byte(jwtTestCredential) 298 299 vc, e := parseTestCredential(t, testCred) 300 require.NoError(t, e) 301 302 t.Run("success", func(t *testing.T) { 303 t.Run("with default hash", func(t *testing.T) { 304 sdjwt, err := vc.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1") 305 require.NoError(t, err) 306 307 _, err = ParseCredential([]byte(sdjwt), WithPublicKeyFetcher(holderPublicKeyFetcher(pubKey))) 308 require.NoError(t, err) 309 }) 310 311 t.Run("with hash option", func(t *testing.T) { 312 sdjwt, err := vc.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1", 313 MakeSDJWTWithHash(crypto.SHA512)) 314 require.NoError(t, err) 315 316 _, err = ParseCredential([]byte(sdjwt), WithPublicKeyFetcher(holderPublicKeyFetcher(pubKey))) 317 require.NoError(t, err) 318 }) 319 }) 320 321 t.Run("failure", func(t *testing.T) { 322 t.Run("prepare claims", func(t *testing.T) { 323 badVC := &Credential{} 324 325 sdjwt, err := badVC.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), "did:example:abc123#key-1") 326 require.Error(t, err) 327 require.Empty(t, sdjwt) 328 require.Contains(t, err.Error(), "constructing VC JWT claims") 329 }) 330 331 t.Run("creating SD-JWT", func(t *testing.T) { 332 expectErr := fmt.Errorf("expected error") 333 334 sdjwt, err := vc.MakeSDJWT(&mockSigner{signErr: expectErr}, "did:example:abc123#key-1") 335 require.Error(t, err) 336 require.Empty(t, sdjwt) 337 require.ErrorIs(t, err, expectErr) 338 require.Contains(t, err.Error(), "creating SD-JWT from VC") 339 }) 340 }) 341 } 342 343 func TestCreateDisplayCredential(t *testing.T) { 344 ed25519Signer, e := newCryptoSigner(kms.ED25519Type) 345 require.NoError(t, e) 346 347 _, privKey, err := ed25519.GenerateKey(rand.Reader) 348 require.NoError(t, err) 349 350 sourceCred, _ := createTestSDJWTCred(t, privKey) 351 352 t.Run("success", func(t *testing.T) { 353 vc, e2 := ParseCredential([]byte(sourceCred), WithDisabledProofCheck()) 354 require.NoError(t, e2) 355 356 t.Run("not a SD-JWT credential", func(t *testing.T) { 357 vc2, err := parseTestCredential(t, []byte(jwtTestCredential)) 358 require.NoError(t, err) 359 360 displayVC, err := vc2.CreateDisplayCredential(DisplayAllDisclosures()) 361 require.NoError(t, err) 362 require.Equal(t, vc2, displayVC) 363 }) 364 365 t.Run("display all claims", func(t *testing.T) { 366 displayVC, err := vc.CreateDisplayCredential(DisplayAllDisclosures()) 367 require.NoError(t, err) 368 369 subj, ok := displayVC.Subject.([]Subject) 370 require.True(t, ok) 371 372 require.Len(t, subj, 1) 373 require.NotEmpty(t, subj[0].ID) 374 375 expectedFields := CustomFields{"degree": map[string]interface{}{ 376 "type": "BachelorDegree", 377 "university": "MIT", 378 }} 379 require.Equal(t, expectedFields, subj[0].CustomFields) 380 }) 381 382 t.Run("not a SD-JWT credential map", func(t *testing.T) { 383 vc2, err := parseTestCredential(t, []byte(jwtTestCredential)) 384 require.NoError(t, err) 385 386 displayVC, err := vc2.CreateDisplayCredentialMap(DisplayAllDisclosures()) 387 require.NoError(t, err) 388 require.NotEmpty(t, displayVC) 389 }) 390 391 t.Run("display all claims map", func(t *testing.T) { 392 displayVC, err := vc.CreateDisplayCredentialMap(DisplayAllDisclosures()) 393 require.NoError(t, err) 394 require.NotEmpty(t, displayVC) 395 }) 396 397 t.Run("display no claims", func(t *testing.T) { 398 displayVC, err := vc.CreateDisplayCredential() 399 require.NoError(t, err) 400 401 subj, ok := displayVC.Subject.([]Subject) 402 require.True(t, ok) 403 404 require.Len(t, subj, 1) 405 require.NotEmpty(t, subj[0].ID) 406 require.Empty(t, subj[0].CustomFields) 407 }) 408 409 t.Run("display subset of claims", func(t *testing.T) { 410 displayVC, err := vc.CreateDisplayCredential(DisplayGivenDisclosures([]string{"id", "type"})) 411 require.NoError(t, err) 412 413 subj, ok := displayVC.Subject.([]Subject) 414 require.True(t, ok) 415 416 require.Len(t, subj, 1) 417 require.NotEmpty(t, subj[0].ID) 418 419 expectedFields := CustomFields{"degree": map[string]interface{}{ 420 "type": "BachelorDegree", 421 }} 422 require.Equal(t, expectedFields, subj[0].CustomFields) 423 }) 424 }) 425 426 t.Run("failure", func(t *testing.T) { 427 t.Run("incompatible options", func(t *testing.T) { 428 vc, err := ParseCredential([]byte(sourceCred), WithDisabledProofCheck()) 429 require.NoError(t, err) 430 431 displayVC, err := vc.CreateDisplayCredential( 432 DisplayAllDisclosures(), 433 DisplayGivenDisclosures([]string{"name"}), 434 ) 435 require.Error(t, err) 436 require.Nil(t, displayVC) 437 require.Contains(t, err.Error(), "incompatible options provided") 438 }) 439 440 t.Run("parsing malformed JWT VC", func(t *testing.T) { 441 badVC := &Credential{ 442 JWT: "blah blah blahblah blah", 443 SDJWTHashAlg: "blah, blahblah", 444 } 445 446 displayVC, err := badVC.CreateDisplayCredential(DisplayAllDisclosures()) 447 require.Error(t, err) 448 require.Nil(t, displayVC) 449 require.Contains(t, err.Error(), "unmarshal VC JWT claims") 450 }) 451 452 t.Run("adding claims back to VC", func(t *testing.T) { 453 vc, err := ParseCredential([]byte(sourceCred), WithDisabledProofCheck()) 454 require.NoError(t, err) 455 456 subj, ok := vc.Subject.([]Subject) 457 require.True(t, ok) 458 require.Len(t, subj, 1) 459 460 testCases := []interface{}{ 461 "foo", // sd field not slice 462 []interface{}{"foo", 5}, // not all elements are strings 463 } 464 465 for _, testCase := range testCases { 466 subj[0].CustomFields["_sd"] = testCase 467 468 claims, err := vc.JWTClaims(false) 469 require.NoError(t, err) 470 471 badJWS, err := claims.MarshalJWS(EdDSA, ed25519Signer, "did:foo:bar#key-1") 472 require.NoError(t, err) 473 474 vc.JWT = badJWS 475 476 displayVC, err := vc.CreateDisplayCredential(DisplayAllDisclosures()) 477 require.Error(t, err) 478 require.Nil(t, displayVC) 479 require.Contains(t, err.Error(), "assembling disclosed claims into vc") 480 } 481 }) 482 483 t.Run("result credential invalid", func(t *testing.T) { 484 vc, err := ParseCredential([]byte(sourceCred), WithDisabledProofCheck()) 485 require.NoError(t, err) 486 487 claims, err := vc.JWTClaims(false) 488 require.NoError(t, err) 489 490 claims.VC["@context"] = 5 491 492 vc.JWT, err = claims.MarshalJWS(EdDSA, ed25519Signer, "did:foo:bar#key-1") 493 require.NoError(t, err) 494 495 displayVC, err := vc.CreateDisplayCredential(DisplayAllDisclosures()) 496 require.Error(t, err) 497 require.Nil(t, displayVC) 498 require.Contains(t, err.Error(), "parsing new VC from JSON") 499 }) 500 }) 501 } 502 503 type mockSigner struct { 504 signErr error 505 } 506 507 func (m *mockSigner) Sign([]byte) ([]byte, error) { 508 return nil, m.signErr 509 } 510 511 func (m *mockSigner) Headers() jose.Headers { 512 return jose.Headers{"alg": "foo"} 513 } 514 515 func createTestSDJWTCred(t *testing.T, privKey ed25519.PrivateKey) (sdJWTCred string, issuerID string) { 516 t.Helper() 517 518 testCred := []byte(jwtTestCredential) 519 520 srcVC, err := parseTestCredential(t, testCred) 521 require.NoError(t, err) 522 523 sdjwt, err := srcVC.MakeSDJWT(afgojwt.NewEd25519Signer(privKey), srcVC.Issuer.ID+"#keys-1") 524 require.NoError(t, err) 525 526 return sdjwt, srcVC.Issuer.ID 527 }