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  }