github.com/hyperledger/aries-framework-go@v0.3.2/pkg/doc/sdjwt/holder/holder_test.go (about) 1 /* 2 Copyright SecureKey Technologies Inc. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package holder 8 9 import ( 10 "crypto/ed25519" 11 "crypto/rand" 12 "encoding/base64" 13 "encoding/json" 14 "fmt" 15 "strings" 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" 23 afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" 24 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" 25 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer" 26 ) 27 28 const ( 29 testIssuer = "https://example.com/issuer" 30 ) 31 32 func TestParse(t *testing.T) { 33 r := require.New(t) 34 35 pubKey, privKey, e := ed25519.GenerateKey(rand.Reader) 36 r.NoError(e) 37 38 signer := afjwt.NewEd25519Signer(privKey) 39 claims := map[string]interface{}{"given_name": "Albert"} 40 41 token, e := issuer.New(testIssuer, claims, nil, signer) 42 r.NoError(e) 43 combinedFormatForIssuance, e := token.Serialize(false) 44 r.NoError(e) 45 46 verifier, e := afjwt.NewEd25519Verifier(pubKey) 47 r.NoError(e) 48 49 t.Run("success", func(t *testing.T) { 50 claims, err := Parse(combinedFormatForIssuance, WithSignatureVerifier(verifier)) 51 r.NoError(err) 52 r.NotNil(claims) 53 r.Equal(1, len(claims)) 54 r.Equal("given_name", claims[0].Name) 55 r.Equal("Albert", claims[0].Value) 56 }) 57 58 t.Run("success - default is no signature verifier", func(t *testing.T) { 59 claims, err := Parse(combinedFormatForIssuance) 60 r.NoError(err) 61 r.Equal(1, len(claims)) 62 r.Equal("given_name", claims[0].Name) 63 r.Equal("Albert", claims[0].Value) 64 }) 65 66 t.Run("success - spec SD-JWT", func(t *testing.T) { 67 claims, err := Parse(specSDJWT, WithSignatureVerifier(&NoopSignatureVerifier{})) 68 r.NoError(err) 69 require.NotNil(t, claims) 70 require.Equal(t, 7, len(claims)) 71 }) 72 73 t.Run("success - VC example", func(t *testing.T) { 74 claims, err := Parse(vcCombinedFormatForIssuance, WithSignatureVerifier(&NoopSignatureVerifier{})) 75 r.NoError(err) 76 require.NotNil(t, claims) 77 require.Equal(t, 4, len(claims)) 78 }) 79 80 t.Run("success - complex claims", func(t *testing.T) { 81 complexClaims := createComplexClaims() 82 83 token, e := issuer.New(testIssuer, complexClaims, nil, signer, 84 issuer.WithStructuredClaims(true)) 85 r.NoError(e) 86 cfi, e := token.Serialize(false) 87 r.NoError(e) 88 89 claims, err := Parse(cfi, WithSignatureVerifier(verifier)) 90 r.NoError(err) 91 r.NotNil(claims) 92 r.Equal(10, len(claims)) 93 }) 94 95 t.Run("error - additional disclosure", func(t *testing.T) { 96 claims, err := Parse(fmt.Sprintf("%s~%s", combinedFormatForIssuance, additionalDisclosure), 97 WithSignatureVerifier(verifier)) 98 r.Error(err) 99 r.Nil(claims) 100 r.Contains(err.Error(), 101 "disclosure digest 'qqvcqnczAMgYx7EykI6wwtspyvyvK790ge7MBbQ-Nus' not found in SD-JWT disclosure digests") 102 }) 103 104 t.Run("success - with detached payload", func(t *testing.T) { 105 jwsParts := strings.Split(combinedFormatForIssuance, ".") 106 jwsDetached := fmt.Sprintf("%s..%s", jwsParts[0], jwsParts[2]) 107 108 jwsPayload, err := base64.RawURLEncoding.DecodeString(jwsParts[1]) 109 require.NoError(t, err) 110 111 sdJWT, err := Parse(jwsDetached, 112 WithSignatureVerifier(verifier), WithJWTDetachedPayload(jwsPayload)) 113 r.NoError(err) 114 r.NotNil(r, sdJWT) 115 }) 116 117 t.Run("error - invalid claims", func(t *testing.T) { 118 // claims is not JSON 119 sdJWTSerialized, err := buildJWS(signer, "not JSON") 120 r.NoError(err) 121 122 claims, err := Parse(sdJWTSerialized, WithSignatureVerifier(verifier)) 123 r.Error(err) 124 r.Nil(claims) 125 r.Contains(err.Error(), "read JWT claims from JWS payload") 126 }) 127 } 128 129 func TestDiscloseClaims(t *testing.T) { 130 r := require.New(t) 131 132 _, privKey, e := ed25519.GenerateKey(rand.Reader) 133 r.NoError(e) 134 135 signer := afjwt.NewEd25519Signer(privKey) 136 claims := map[string]interface{}{"given_name": "Albert"} 137 138 token, e := issuer.New(testIssuer, claims, nil, signer) 139 r.NoError(e) 140 combinedFormatForIssuance, e := token.Serialize(false) 141 r.NoError(e) 142 143 cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) 144 145 claimsToDisclose := []string{cfi.Disclosures[0]} 146 147 t.Run("success", func(t *testing.T) { 148 combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, claimsToDisclose) 149 r.NoError(err) 150 require.NotNil(t, combinedFormatForPresentation) 151 require.Equal(t, combinedFormatForIssuance+common.CombinedFormatSeparator, combinedFormatForPresentation) 152 }) 153 154 t.Run("success - with holder binding", func(t *testing.T) { 155 _, holderPrivKey, e := ed25519.GenerateKey(rand.Reader) 156 r.NoError(e) 157 158 holderSigner := afjwt.NewEd25519Signer(holderPrivKey) 159 160 combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, claimsToDisclose, 161 WithHolderBinding(&BindingInfo{ 162 Payload: BindingPayload{ 163 Audience: "https://example.com/verifier", 164 Nonce: "nonce", 165 IssuedAt: jwt.NewNumericDate(time.Now()), 166 }, 167 Signer: holderSigner, 168 })) 169 r.NoError(err) 170 r.NotEmpty(combinedFormatForPresentation) 171 r.Contains(combinedFormatForPresentation, combinedFormatForIssuance+common.CombinedFormatSeparator) 172 }) 173 174 t.Run("error - failed to create holder binding due to signing error", func(t *testing.T) { 175 combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, claimsToDisclose, 176 WithHolderBinding(&BindingInfo{ 177 Payload: BindingPayload{}, 178 Signer: &mockSigner{Err: fmt.Errorf("signing error")}, 179 })) 180 181 r.Error(err) 182 r.Empty(combinedFormatForPresentation) 183 184 r.Contains(err.Error(), 185 "failed to create holder binding: create JWS: sign JWS: sign JWS verification data: signing error") 186 }) 187 188 t.Run("error - no disclosure(s)", func(t *testing.T) { 189 cfi := common.ParseCombinedFormatForIssuance(combinedFormatForIssuance) 190 191 combinedFormatForPresentation, err := CreatePresentation(cfi.SDJWT, claimsToDisclose) 192 r.Error(err) 193 r.Empty(combinedFormatForPresentation) 194 r.Contains(err.Error(), "no disclosures found in SD-JWT") 195 }) 196 197 t.Run("error - disclosure not found", func(t *testing.T) { 198 combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, 199 []string{"non_existent"}) 200 r.Error(err) 201 r.Empty(combinedFormatForPresentation) 202 r.Contains(err.Error(), "disclosure 'non_existent' not found") 203 }) 204 } 205 206 func TestGetClaims(t *testing.T) { 207 r := require.New(t) 208 209 t.Run("success", func(t *testing.T) { 210 claims, err := getClaims([]string{additionalDisclosure}) 211 r.NoError(err) 212 r.Len(claims, 1) 213 }) 214 215 t.Run("error - not base64 encoded ", func(t *testing.T) { 216 claims, err := getClaims([]string{"!!!"}) 217 r.Error(err) 218 r.Nil(claims) 219 r.Contains(err.Error(), "failed to decode disclosure") 220 }) 221 } 222 223 func TestWithJWTDetachedPayload(t *testing.T) { 224 detachedPayloadOpt := WithJWTDetachedPayload([]byte("payload")) 225 require.NotNil(t, detachedPayloadOpt) 226 227 opts := &parseOpts{} 228 detachedPayloadOpt(opts) 229 require.Equal(t, []byte("payload"), opts.detachedPayload) 230 } 231 232 func buildJWS(signer jose.Signer, claims interface{}) (string, error) { 233 claimsBytes, err := json.Marshal(claims) 234 if err != nil { 235 return "", err 236 } 237 238 jws, err := jose.NewJWS(nil, nil, claimsBytes, signer) 239 if err != nil { 240 return "", err 241 } 242 243 return jws.SerializeCompact(false) 244 } 245 246 // Signer defines JWS Signer interface. It makes signing of data and provides custom JWS headers relevant to the signer. 247 type mockSigner struct { 248 Err error 249 } 250 251 // Sign signs. 252 func (m *mockSigner) Sign(_ []byte) ([]byte, error) { 253 if m.Err != nil { 254 return nil, m.Err 255 } 256 257 return nil, nil 258 } 259 260 // Headers provides JWS headers. 261 func (m *mockSigner) Headers() jose.Headers { 262 headers := make(jose.Headers) 263 headers["alg"] = "EdDSA" 264 265 return headers 266 } 267 268 func createComplexClaims() map[string]interface{} { 269 claims := map[string]interface{}{ 270 "sub": "john_doe_42", 271 "given_name": "John", 272 "family_name": "Doe", 273 "email": "johndoe@example.com", 274 "phone_number": "+1-202-555-0101", 275 "birthdate": "1940-01-01", 276 "address": map[string]interface{}{ 277 "street_address": "123 Main St", 278 "locality": "Anytown", 279 "region": "Anystate", 280 "country": "US", 281 }, 282 } 283 284 return claims 285 } 286 287 // nolint: lll 288 const additionalDisclosure = `WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd` 289 290 // nolint: lll 291 const specSDJWT = `eyJhbGciOiAiUlMyNTYiLCAia2lkIjogImNBRUlVcUowY21MekQxa3pHemhlaUJhZzBZUkF6VmRsZnhOMjgwTmdIYUEifQ.eyJfc2QiOiBbIk5ZQ29TUktFWXdYZHBlNXlkdUpYQ3h4aHluRVU4ei1iNFR5TmlhcDc3VVkiLCAiU1k4bjJCYmtYOWxyWTNleEhsU3dQUkZYb0QwOUdGOGE5Q1BPLUc4ajIwOCIsICJUUHNHTlBZQTQ2d21CeGZ2MnpuT0poZmRvTjVZMUdrZXpicGFHWkNUMWFjIiwgIlprU0p4eGVHbHVJZFlCYjdDcWtaYkpWbTB3MlY1VXJSZU5UekFRQ1lCanciLCAibDlxSUo5SlRRd0xHN09MRUlDVEZCVnhtQXJ3OFBqeTY1ZEQ2bXRRVkc1YyIsICJvMVNBc0ozM1lNaW9POXBYNVZlQU0xbHh1SEY2aFpXMmtHZGtLS0JuVmxvIiwgInFxdmNxbmN6QU1nWXg3RXlrSTZ3d3RzcHl2eXZLNzkwZ2U3TUJiUS1OdXMiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNTE2MjM5MDIyLCAiZXhwIjogMTUxNjI0NzAyMiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIlJTQSIsICJuIjogInBtNGJPSEJnLW9ZaEF5UFd6UjU2QVdYM3JVSVhwMTFfSUNEa0dnUzZXM1pXTHRzLWh6d0kzeDY1NjU5a2c0aFZvOWRiR29DSkUzWkdGX2VhZXRFMzBVaEJVRWdwR3dyRHJRaUo5enFwcm1jRmZyM3F2dmtHanR0aDhaZ2wxZU0yYkpjT3dFN1BDQkhXVEtXWXMxNTJSN2c2SmcyT1ZwaC1hOHJxLXE3OU1oS0c1UW9XX21UejEwUVRfNkg0YzdQaldHMWZqaDhocFdObmJQX3B2NmQxelN3WmZjNWZsNnlWUkwwRFYwVjNsR0hLZTJXcWZfZU5HakJyQkxWa2xEVGs4LXN0WF9NV0xjUi1FR21YQU92MFVCV2l0U19kWEpLSnUtdlhKeXcxNG5IU0d1eFRJSzJoeDFwdHRNZnQ5Q3N2cWltWEtlRFRVMTRxUUwxZUU3aWhjdyIsICJlIjogIkFRQUIifX19.xqgKrDO6dK_oBL3fiqdcq_elaIGxM6Z-RyuysglGyddR1O1IiE3mIk8kCpoqcRLR88opkVWN2392K_XYfAuAmeT9kJVisD8ZcgNcv-MQlWW9s8WaViXxBRe7EZWkWRQcQVR6jf95XZ5H2-_KA54POq3L42xjk0y5vDr8yc08Reak6vvJVvjXpp-Wk6uxsdEEAKFspt_EYIvISFJhfTuQqyhCjnaW13X312MSQBPwjbHn74ylUqVLljDvqcemxeqjh42KWJq4C3RqNJ7anA2i3FU1kB4-KNZWsijY7-op49iL7BrnIBxdlAMrbHEkoGTbFWdl7Ki17GHtDxxa1jaxQg~WyJkcVR2WE14UzBHYTNEb2FHbmU5eDBRIiwgInN1YiIsICJqb2huX2RvZV80MiJd~WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJxUVdtakpsMXMxUjRscWhFTkxScnJ3IiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyJLVXhTNWhFX1hiVmFjckdBYzdFRnd3IiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyIzcXZWSjFCQURwSERTUzkzOVEtUml3IiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyIweEd6bjNNaXFzY3RaSV9PcERsQWJRIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJFUktNMENOZUZKa2FENW1UWFZfWDh3IiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0` 292 293 // nolint: lll 294 const vcCombinedFormatForIssuance = `eyJhbGciOiJFZERTQSJ9.eyJpYXQiOjEuNjczOTg3NTQ3ZSswOSwiaXNzIjoiZGlkOmV4YW1wbGU6NzZlMTJlYzcxMmViYzZmMWMyMjFlYmZlYjFmIiwianRpIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzE4NzIiLCJuYmYiOjEuNjczOTg3NTQ3ZSswOSwic3ViIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIiwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiX3NkX2FsZyI6InNoYS0yNTYiLCJjbmYiOnsiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiZDlYemtRbVJMQncxSXpfeHVGUmVLMUItRmpCdTdjT0N3RTlOR2F1d251SSJ9fSwiY3JlZGVudGlhbFN1YmplY3QiOnsiX3NkIjpbInBBdjJUMU10YmRXNGttUUdxT1VVRUpjQmdTZi1mSFRHV2xQVUV4aWlIbVEiLCI2dDlBRUJCQnEzalZwckJ3bGljOGhFWnNNSmxXSXhRdUw5c3ExMzJZTnYwIl0sImRlZ3JlZSI6eyJfc2QiOlsibzZzV2h4RjcxWHBvZ1cxVUxCbU90bjR1SXFGdjJ3ODF6emRuelJXdlpqYyIsIi1yRklXbU1YR3ZXX0FIYVEtODhpMy11ZzRUVjhLUTg5TjdmZmtneFc2X2MiXX0sImlkIjoiZGlkOmV4YW1wbGU6ZWJmZWIxZjcxMmViYzZmMWMyNzZlMTJlYzIxIn0sImZpcnN0X25hbWUiOiJGaXJzdCBuYW1lIiwiaWQiOiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMTg3MiIsImluZm8iOiJJbmZvIiwiaXNzdWFuY2VEYXRlIjoiMjAyMy0wMS0xN1QyMjozMjoyNy40NjgxMDk4MTcrMDI6MDAiLCJpc3N1ZXIiOiJkaWQ6ZXhhbXBsZTo3NmUxMmVjNzEyZWJjNmYxYzIyMWViZmViMWYiLCJsYXN0X25hbWUiOiJMYXN0IG5hbWUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwifX0.GcfSA6NkONxdsm5Lxj9-988eWx1ZvMz5vJ1uh2x8UK1iKIeQLmhsWpA_34RbtAm2HnuoxW4_ZGeiHBzQ1GLTDQ~WyJFWkVDRVZ1YWVJOXhZWmlWb3VMQldBIiwidHlwZSIsIkJhY2hlbG9yRGVncmVlIl0~WyJyMno1UzZMa25FRTR3TWwteFB0VEx3IiwiZGVncmVlIiwiTUlUIl0~WyJ2VkhfaGhNQy1aSUt5WFdtdDUyOWpnIiwic3BvdXNlIiwiZGlkOmV4YW1wbGU6YzI3NmUxMmVjMjFlYmZlYjFmNzEyZWJjNmYxIl0~WyJrVzh0WVVwbVl1VmRoZktFT050TnFnIiwibmFtZSIsIkpheWRlbiBEb2UiXQ`