diff --git a/pkg/doc/presexch/api_test.go b/pkg/doc/presexch/api_test.go index d944a8d64..1868974cc 100644 --- a/pkg/doc/presexch/api_test.go +++ b/pkg/doc/presexch/api_test.go @@ -31,6 +31,8 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/vdr/fingerprint" ) +const testCredID = "http://test.credential.com/123456" + func TestPresentationDefinition_Match(t *testing.T) { t.Run("match one credential", func(t *testing.T) { uri := randomURI() @@ -72,7 +74,7 @@ func TestPresentationDefinition_Match(t *testing.T) { expectedNested := newVC([]string{uri}) expectedNested.Types = append(expectedNested.Types, customType) - expectedNested.ID = "http://test.credential.com/123456" + expectedNested.ID = testCredID expected := newVCWithCustomFld([]string{uri}, "nestedVC", expectedNested) expected.Types = append(expected.Types, customType) @@ -117,7 +119,7 @@ func TestPresentationDefinition_Match(t *testing.T) { expected := newVCWithCustomFld([]string{uri}, "nestedVC", expectedNested) expected.Types = append(expected.Types, customType) - expected.ID = "http://test.credential.com/123456" + expected.ID = testCredID defs := &PresentationDefinition{ InputDescriptors: []*InputDescriptor{{ @@ -192,6 +194,92 @@ func TestPresentationDefinition_Match(t *testing.T) { require.Equal(t, expected.ID, result.ID) }) + t.Run("match one nested sd-jwt credential", func(t *testing.T) { + uri := randomURI() + contextLoader := createTestDocumentLoader(t, uri) + agent := newAgent(t) + + customType := "CustomType" + + expectedNested := newSignedSDJWTVC(t, agent, []string{uri}) + + expected := newVCWithCustomFld([]string{uri}, "nestedVC", expectedNested) + expected.Types = append(expected.Types, customType) + expected.ID = testCredID + + defs := &PresentationDefinition{ + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + }}, + } + + docLoader := createTestDocumentLoader(t, uri, customType) + + matched, err := defs.Match(newVP(t, + &PresentationSubmission{DescriptorMap: []*InputDescriptorMapping{{ + ID: defs.InputDescriptors[0].ID, + Path: "$.verifiableCredential[0]", + PathNested: &InputDescriptorMapping{ + ID: defs.InputDescriptors[0].ID, + Path: "$.nestedVC", + }, + }}}, + expected, + ), docLoader, + WithCredentialOptions( + verifiable.WithJSONLDDocumentLoader(contextLoader), + verifiable.WithPublicKeyFetcher(verifiable.NewVDRKeyResolver(agent.VDRegistry()).PublicKeyFetcher()), + ), + ) + require.NoError(t, err) + require.Len(t, matched, 1) + result, ok := matched[defs.InputDescriptors[0].ID] + require.True(t, ok) + require.Equal(t, expectedNested.ID, result.ID) + }) + + t.Run("match with self referencing - sdjwt", func(t *testing.T) { + uri := randomURI() + contextLoader := createTestDocumentLoader(t, uri) + agent := newAgent(t) + + expected := newSignedSDJWTVC(t, agent, []string{uri}) + + defs := &PresentationDefinition{ + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + }}, + } + + matched, err := defs.Match(newVP(t, + &PresentationSubmission{DescriptorMap: []*InputDescriptorMapping{{ + ID: defs.InputDescriptors[0].ID, + Path: "$", + PathNested: &InputDescriptorMapping{ + ID: defs.InputDescriptors[0].ID, + Path: "$.verifiableCredential[0]", + }, + }}}, + expected, + ), contextLoader, + WithCredentialOptions( + verifiable.WithJSONLDDocumentLoader(contextLoader), + verifiable.WithPublicKeyFetcher(verifiable.NewVDRKeyResolver(agent.VDRegistry()).PublicKeyFetcher()), + ), + ) + require.NoError(t, err) + require.Len(t, matched, 1) + result, ok := matched[defs.InputDescriptors[0].ID] + require.True(t, ok) + require.Equal(t, expected.ID, result.ID) + }) + t.Run("match one signed credential", func(t *testing.T) { uri := randomURI() contextLoader := createTestDocumentLoader(t, uri) @@ -612,6 +700,41 @@ func newSignedJWTVC(t *testing.T, return vc } +func newSignedSDJWTVC(t *testing.T, + agent *context.Provider, ctx []string) *verifiable.Credential { + t.Helper() + + vc := getTestVCWithContext(ctx) + + keyID, kh, err := agent.KMS().Create(kms.ED25519Type) + require.NoError(t, err) + + signer := suite.NewCryptoSigner(agent.Crypto(), kh) + + pubKey, kt, err := agent.KMS().ExportPubKeyBytes(keyID) + require.NoError(t, err) + require.Equal(t, kms.ED25519Type, kt) + + issuer, verMethod := fingerprint.CreateDIDKeyByCode(fingerprint.ED25519PubKeyMultiCodec, pubKey) + + vc.Issuer = verifiable.Issuer{ID: issuer} + + jwsAlgo, err := verifiable.KeyTypeToJWSAlgo(kms.ED25519Type) + require.NoError(t, err) + + algName, err := jwsAlgo.Name() + require.NoError(t, err) + + combinedFormatForIssuance, err := vc.MakeSDJWT(verifiable.GetJWTSigner(signer, algName), verMethod) + require.NoError(t, err) + + parsed, err := verifiable.ParseCredential([]byte(combinedFormatForIssuance), + verifiable.WithPublicKeyFetcher(holderPublicKeyFetcher(pubKey))) + require.NoError(t, err) + + return parsed +} + func newVP(t *testing.T, submission *PresentationSubmission, vcs ...*verifiable.Credential) *verifiable.Presentation { vp, err := verifiable.NewPresentation(verifiable.WithCredentials(vcs...)) require.NoError(t, err) diff --git a/pkg/doc/presexch/definition.go b/pkg/doc/presexch/definition.go index 132b7d4c3..6fdda6d9e 100644 --- a/pkg/doc/presexch/definition.go +++ b/pkg/doc/presexch/definition.go @@ -25,6 +25,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/common/log" "github.com/hyperledger/aries-framework-go/pkg/doc/jose" "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" ) @@ -581,15 +582,29 @@ func filterConstraints(constraints *Constraints, creds []*verifiable.Credential, // if credential.JWT is set, credential will marshal to a JSON string. // temporarily clear credential.JWT to avoid this. + var err error + credJWT := credential.JWT - credential.JWT = "" - credentialSrc, err := json.Marshal(credential) + credentialWithFieldValues := credential + + if credential.SDJWTHashAlg != "" { + credentialWithFieldValues, err = credential.CreateDisplayCredential(verifiable.DisplayAllDisclosures()) + if err != nil { + continue + } + } + + // if credential.JWT is set, credential will marshal to a JSON string. + // temporarily clear credential.JWT to avoid this. + credentialWithFieldValues.JWT = "" + + credentialSrc, err := json.Marshal(credentialWithFieldValues) if err != nil { continue } - credential.JWT = credJWT + credentialWithFieldValues.JWT = credJWT var credentialMap map[string]interface{} @@ -623,7 +638,7 @@ func filterConstraints(constraints *Constraints, creds []*verifiable.Credential, continue } - if constraints.LimitDisclosure.isRequired() || predicate { + if (constraints.LimitDisclosure.isRequired() || predicate) && credential.SDJWTHashAlg == "" { template := credentialSrc var contexts []interface{} @@ -658,12 +673,91 @@ func filterConstraints(constraints *Constraints, creds []*verifiable.Credential, credential.ID = tmpID(credential.ID) } + if constraints.LimitDisclosure.isRequired() && credential.SDJWTHashAlg != "" { + limitedDisclosures, err := getLimitedDisclosures(constraints, credentialSrc, credential) + if err != nil { + return nil, err + } + + credential.SDJWTDisclosures = limitedDisclosures + } + result = append(result, credential) } return result, nil } +// nolint: gocyclo,funlen,gocognit +func getLimitedDisclosures(constraints *Constraints, displaySrc []byte, credential *verifiable.Credential) ([]*common.DisclosureClaim, error) { // nolint:lll + hash, err := common.GetCryptoHash(credential.SDJWTHashAlg) + if err != nil { + return nil, err + } + + vcJWT := credential.JWT + credential.JWT = "" + + credentialSrc, err := json.Marshal(credential) + if err != nil { + return nil, err + } + + // revert JWT to original value + credential.JWT = vcJWT + + var limitedDisclosures []*common.DisclosureClaim + + for _, f := range constraints.Fields { + jPaths, err := getJSONPaths(f.Path, displaySrc) + if err != nil { + return nil, err + } + + for _, path := range jPaths { + if strings.Contains(path[0], credentialSchema) { + continue + } + + parentPath := "" + + key := path[1] + + pathParts := strings.Split(path[1], ".") + if len(pathParts) > 1 { + parentPath = strings.Join(pathParts[:len(pathParts)-1], ".") + key = pathParts[len(pathParts)-1] + } + + parentObj, ok := gjson.GetBytes(credentialSrc, parentPath).Value().(map[string]interface{}) + if !ok { + // no selective disclosures at this level, so nothing to add to limited disclosures + continue + } + + digests, err := common.GetDisclosureDigests(parentObj) + if err != nil { + return nil, err + } + + for _, dc := range credential.SDJWTDisclosures { + if dc.Name == key { + digest, err := common.GetHash(hash, dc.Disclosure) + if err != nil { + return nil, err + } + + if _, ok := digests[digest]; ok { + limitedDisclosures = append(limitedDisclosures, dc) + } + } + } + } + } + + return limitedDisclosures, nil +} + func frameCreds(frame map[string]interface{}, creds []*verifiable.Credential, opts ...verifiable.CredentialOpt) ([]*verifiable.Credential, error) { if frame == nil { @@ -716,29 +810,11 @@ func createNewCredential(constraints *Constraints, src, limitedCred []byte, ) for _, f := range constraints.Fields { - paths, err := jsonpathkeys.ParsePaths(f.Path...) + jPaths, err := getJSONPaths(f.Path, src) if err != nil { return nil, err } - eval, err := jsonpathkeys.EvalPathsInReader(bytes.NewReader(src), paths) - if err != nil { - return nil, err - } - - var jPaths [][2]string - - set := map[string]int{} - - for { - result, ok := eval.Next() - if !ok { - break - } - - jPaths = append(jPaths, getPath(result.Keys, set)) - } - for _, path := range jPaths { if strings.Contains(path[0], credentialSchema) { continue @@ -785,6 +861,33 @@ func createNewCredential(constraints *Constraints, src, limitedCred []byte, return credential.GenerateBBSSelectiveDisclosure(doc, []byte(uuid.New().String()), opts...) } +func getJSONPaths(keys []string, src []byte) ([][2]string, error) { + paths, err := jsonpathkeys.ParsePaths(keys...) + if err != nil { + return nil, err + } + + eval, err := jsonpathkeys.EvalPathsInReader(bytes.NewReader(src), paths) + if err != nil { + return nil, err + } + + var jPaths [][2]string + + set := map[string]int{} + + for { + result, ok := eval.Next() + if !ok { + break + } + + jPaths = append(jPaths, getPath(result.Keys, set)) + } + + return jPaths, nil +} + func enhanceRevealDoc(explicitPaths map[string]bool, limitedCred, vcBytes []byte) ([]byte, error) { var err error diff --git a/pkg/doc/presexch/definition_test.go b/pkg/doc/presexch/definition_test.go index 0b06860b3..4961b3e7d 100644 --- a/pkg/doc/presexch/definition_test.go +++ b/pkg/doc/presexch/definition_test.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: Apache-2.0 package presexch_test import ( + "bytes" "context" "crypto/sha256" "encoding/json" @@ -29,6 +30,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/signature/jsonld" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/bbsblssignature2020" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" "github.com/hyperledger/aries-framework-go/pkg/doc/util" "github.com/hyperledger/aries-framework-go/pkg/doc/util/signature" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" @@ -38,6 +40,7 @@ import ( mockkms "github.com/hyperledger/aries-framework-go/pkg/mock/kms" "github.com/hyperledger/aries-framework-go/pkg/mock/storage" "github.com/hyperledger/aries-framework-go/pkg/secretlock/noop" + "github.com/hyperledger/aries-framework-go/pkg/vdr/fingerprint" ) const errMsgSchema = "credentials do not satisfy requirements" @@ -477,6 +480,441 @@ func TestPresentationDefinition_CreateVP(t *testing.T) { checkVP(t, vp) }) + t.Run("SD-JWT: Limit Disclosure + SD Claim paths", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{{ + Path: []string{ + "$.credentialSubject.family_name", + "$.credentialSubject.given_name", + "$.credentialSubject.address.country", + }, + }}, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.NoError(t, err) + require.NotNil(t, vp) + require.Equal(t, 1, len(vp.Credentials())) + + vc, ok := vp.Credentials()[0].(*verifiable.Credential) + require.True(t, ok) + + require.Len(t, vc.SDJWTDisclosures, 3) + + require.Len(t, vc.Subject.([]verifiable.Subject)[0].CustomFields["_sd"].([]interface{}), 7) + require.NotNil(t, vc.Subject.([]verifiable.Subject)[0].CustomFields["address"]) + + _, ok = vc.Subject.([]verifiable.Subject)[0].CustomFields["email"] + require.False(t, ok) + + displayVC, err := vc.CreateDisplayCredential(verifiable.DisplayAllDisclosures()) + require.NoError(t, err) + + printObject(t, "Display VC - Limited", displayVC) + + require.Equal(t, "John", displayVC.Subject.([]verifiable.Subject)[0].CustomFields["given_name"]) + require.Equal(t, "Doe", displayVC.Subject.([]verifiable.Subject)[0].CustomFields["family_name"]) + + checkSubmission(t, vp, pd) + checkVP(t, vp) + }) + + t.Run("SD-JWT: Limit Disclosure + SD Claim paths + additional filter", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{ + { + Path: []string{ + "$.credentialSubject.family_name", + "$.credentialSubject.given_name", + "$.credentialSubject.address.country", + }, + }, + { + Path: []string{ + "$.credentialSchema[0].id", + }, + Filter: &Filter{ + Type: &strFilterType, + Const: "https://www.w3.org/TR/vc-data-model/2.0/#types", + }, + }, + }, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.NoError(t, err) + require.NotNil(t, vp) + require.Equal(t, 1, len(vp.Credentials())) + + vc, ok := vp.Credentials()[0].(*verifiable.Credential) + require.True(t, ok) + + require.Len(t, vc.SDJWTDisclosures, 3) + + require.Len(t, vc.Subject.([]verifiable.Subject)[0].CustomFields["_sd"].([]interface{}), 7) + require.NotNil(t, vc.Subject.([]verifiable.Subject)[0].CustomFields["address"]) + + _, ok = vc.Subject.([]verifiable.Subject)[0].CustomFields["email"] + require.False(t, ok) + + displayVC, err := vc.CreateDisplayCredential(verifiable.DisplayAllDisclosures()) + require.NoError(t, err) + + printObject(t, "Display VC", displayVC) + + require.Equal(t, "John", displayVC.Subject.([]verifiable.Subject)[0].CustomFields["given_name"]) + require.Equal(t, "Doe", displayVC.Subject.([]verifiable.Subject)[0].CustomFields["family_name"]) + + checkSubmission(t, vp, pd) + checkVP(t, vp) + }) + + t.Run("SD-JWT: Limit Disclosure + non-SD claim path", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{{ + Path: []string{ + "$.id", + }, + }}, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.NoError(t, err) + require.NotNil(t, vp) + require.Equal(t, 1, len(vp.Credentials())) + + vc, ok := vp.Credentials()[0].(*verifiable.Credential) + require.True(t, ok) + + // there is only one non-SD claim path is in the fields array - hence no selective disclosures + require.Len(t, vc.SDJWTDisclosures, 0) + + require.Len(t, vc.Subject.([]verifiable.Subject)[0].CustomFields["_sd"].([]interface{}), 7) + + displayVC, err := vc.CreateDisplayCredential(verifiable.DisplayAllDisclosures()) + require.NoError(t, err) + + printObject(t, "Display VC - No Selective Disclosures", displayVC) + + require.Nil(t, displayVC.Subject.([]verifiable.Subject)[0].CustomFields["given_name"]) + require.Nil(t, displayVC.Subject.([]verifiable.Subject)[0].CustomFields["email"]) + + checkSubmission(t, vp, pd) + checkVP(t, vp) + }) + + t.Run("SD-JWT: No Limit Disclosure + Predicate Satisfied", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + Fields: []*Field{{ + Path: []string{ + "$.credentialSubject.family_name", + }, + Predicate: &required, + Filter: &Filter{Type: &strFilterType}, + }}, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.NoError(t, err) + require.NotNil(t, vp) + require.Equal(t, 1, len(vp.Credentials())) + + vc, ok := vp.Credentials()[0].(*verifiable.Credential) + require.True(t, ok) + + require.Len(t, vc.SDJWTDisclosures, 11) + + require.Len(t, vc.Subject.([]verifiable.Subject)[0].CustomFields["_sd"].([]interface{}), 7) + require.NotNil(t, vc.Subject.([]verifiable.Subject)[0].CustomFields["address"]) + + _, ok = vc.Subject.([]verifiable.Subject)[0].CustomFields["email"] + require.False(t, ok) + + displayVC, err := vc.CreateDisplayCredential(verifiable.DisplayAllDisclosures()) + require.NoError(t, err) + + printObject(t, "Display VC - No Limit Disclosure (all fields displayed)", displayVC) + + require.Equal(t, "John", displayVC.Subject.([]verifiable.Subject)[0].CustomFields["given_name"]) + require.Equal(t, "Doe", displayVC.Subject.([]verifiable.Subject)[0].CustomFields["family_name"]) + require.Equal(t, "johndoe@example.com", displayVC.Subject.([]verifiable.Subject)[0].CustomFields["email"]) + + checkSubmission(t, vp, pd) + checkVP(t, vp) + }) + + t.Run("SD-JWT: hash algorithm not supported", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{{ + Path: []string{ + "$.credentialSubject.given_name", + }, + }}, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + sdJwtVC.SDJWTHashAlg = "sha-128" + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.Error(t, err) + require.Nil(t, vp) + require.Contains(t, err.Error(), "_sd_alg 'sha-128' not supported") + }) + + t.Run("SD-JWT: invalid JSON path ", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{{ + Path: []string{ + "123", + }, + }}, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.Error(t, err) + require.Nil(t, vp) + require.Contains(t, err.Error(), "Expected $ or @ at start of path instead of U+0031") + }) + + t.Run("SD-JWT: Limit Disclosure (credentials don't meet requirement)", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{{ + Path: []string{ + "$.credentialSubject.family_name", + "$.credentialSubject.given_name", + "$.credentialSubject.address.country", + }, + Predicate: &required, + Filter: &Filter{Type: &arrFilterType}, + }}, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.Error(t, err) + require.Nil(t, vp) + require.Contains(t, err.Error(), "credentials do not satisfy requirements") + }) + + t.Run("SD-JWT: No Limit Disclosure (credentials don't meet requirement)", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + Fields: []*Field{{ + Path: []string{ + "$.credentialSubject.family_name", + "$.credentialSubject.given_name", + "$.credentialSubject.address.country", + }, + Predicate: &required, + Filter: &Filter{Type: &arrFilterType}, + }}, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.Error(t, err) + require.Nil(t, vp) + require.Contains(t, err.Error(), "credentials do not satisfy requirements") + }) + + t.Run("SD-JWT: Limit Disclosure with invalid field (credentials don't meet requirement)", func(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: uuid.New().String(), + InputDescriptors: []*InputDescriptor{{ + ID: uuid.New().String(), + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{{ + Path: []string{ + "$.credentialSubject.invalid", + }, + }}, + }, + }}, + } + + testVC := getTestVC() + + ed25519Signer, err := newCryptoSigner(kms.ED25519Type) + require.NoError(t, err) + + sdJwtVC := newSdJwtVC(t, testVC, ed25519Signer) + + vp, err := pd.CreateVP([]*verifiable.Credential{sdJwtVC}, + lddl, verifiable.WithJSONLDDocumentLoader(createTestJSONLDDocumentLoader(t))) + + require.Error(t, err) + require.Nil(t, vp) + require.Contains(t, err.Error(), "credentials do not satisfy requirements") + }) + t.Run("Limit disclosure BBS+", func(t *testing.T) { required := Required @@ -1533,6 +1971,85 @@ func createEdDSAJWS(t *testing.T, cred *verifiable.Credential, signer verifiable return vcJWT } +func getTestVCWithContext(ctx []string) *verifiable.Credential { + subject := map[string]interface{}{ + "id": uuid.New().String(), + "sub": "john_doe_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "birthdate": "1940-01-01", + "address": map[string]interface{}{ + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US", + }, + } + + vc := verifiable.Credential{ + Context: []string{verifiable.ContextURI}, + Types: []string{verifiable.VCType}, + ID: "http://example.edu/credentials/1872", + Issued: &util.TimeWrapper{ + Time: time.Now(), + }, + Issuer: verifiable.Issuer{ + ID: "did:example:76e12ec712ebc6f1c221ebfeb1f", + }, + Schemas: []verifiable.TypedID{{ + ID: "https://www.w3.org/TR/vc-data-model/2.0/#types", + Type: "JsonSchemaValidator2018", + }}, + Subject: subject, + } + + if ctx != nil { + vc.Context = append(vc.Context, ctx...) + } + + return &vc +} + +func getTestVC() *verifiable.Credential { + return getTestVCWithContext(nil) +} + +func newSdJwtVC(t *testing.T, vc *verifiable.Credential, signer signature.Signer) *verifiable.Credential { + t.Helper() + + pubKey := signer.PublicKeyBytes() + + issuer, verMethod := fingerprint.CreateDIDKeyByCode(fingerprint.ED25519PubKeyMultiCodec, pubKey) + + vc.Issuer = verifiable.Issuer{ID: issuer} + + jwsAlgo, err := verifiable.KeyTypeToJWSAlgo(kms.ED25519Type) + require.NoError(t, err) + + algName, err := jwsAlgo.Name() + require.NoError(t, err) + + combinedFormatForIssuance, err := vc.MakeSDJWT(verifiable.GetJWTSigner(signer, algName), verMethod) + require.NoError(t, err) + + parsed, err := verifiable.ParseCredential([]byte(combinedFormatForIssuance), + verifiable.WithPublicKeyFetcher(holderPublicKeyFetcher(pubKey))) + require.NoError(t, err) + + return parsed +} + +func holderPublicKeyFetcher(pubKeyBytes []byte) verifiable.PublicKeyFetcher { + return func(issuerID, keyID string) (*verifier.PublicKey, error) { + return &verifier.PublicKey{ + Type: kms.RSARS256, + Value: pubKeyBytes, + }, nil + } +} + func createKMS() (*localkms.LocalKMS, error) { p, err := mockkms.NewProviderForKMS(storage.NewMockStoreProvider(), &noop.NoLock{}) if err != nil { @@ -1542,7 +2059,7 @@ func createKMS() (*localkms.LocalKMS, error) { return localkms.New("local-lock://custom/master/key/", p) } -func newCryptoSigner(keyType kms.KeyType) (signature.Signer, error) { +func newCryptoSigner(keyType kms.KeyType) (signature.Signer, error) { // nolint:unparam localKMS, err := createKMS() if err != nil { return nil, err @@ -1662,3 +2179,27 @@ func createTestJSONLDDocumentLoader(t *testing.T) *ld.DocumentLoader { return loader } + +func prettyPrint(msg []byte) (string, error) { + var prettyJSON bytes.Buffer + + err := json.Indent(&prettyJSON, msg, "", "\t") + if err != nil { + return "", err + } + + return prettyJSON.String(), nil +} + +func printObject(t *testing.T, name string, obj interface{}) { + t.Helper() + + objBytes, err := json.Marshal(obj) + require.NoError(t, err) + + prettyJSON, err := prettyPrint(objBytes) + require.NoError(t, err) + + fmt.Println(name + ":") + fmt.Println(prettyJSON) +} diff --git a/pkg/doc/verifiable/jws.go b/pkg/doc/verifiable/jws.go index fc3985d15..279a1d75f 100644 --- a/pkg/doc/verifiable/jws.go +++ b/pkg/doc/verifiable/jws.go @@ -18,25 +18,28 @@ type Signer interface { Alg() string } -// jwtSigner implement jose.Signer interface. -type jwtSigner struct { +// JwtSigner implement jose.Signer interface. +type JwtSigner struct { signer Signer headers map[string]interface{} } -func getJWTSigner(signer Signer, algorithm string) *jwtSigner { +// GetJWTSigner returns JWT Signer. +func GetJWTSigner(signer Signer, algorithm string) *JwtSigner { headers := map[string]interface{}{ jose.HeaderAlgorithm: algorithm, } - return &jwtSigner{signer: signer, headers: headers} + return &JwtSigner{signer: signer, headers: headers} } -func (s jwtSigner) Sign(data []byte) ([]byte, error) { +// Sign returns signature. +func (s JwtSigner) Sign(data []byte) ([]byte, error) { return s.signer.Sign(data) } -func (s jwtSigner) Headers() jose.Headers { +// Headers returns headers. +func (s JwtSigner) Headers() jose.Headers { return s.headers } @@ -59,7 +62,7 @@ func marshalJWS(jwtClaims interface{}, signatureAlg JWSAlgorithm, signer Signer, jose.HeaderKeyID: keyID, } - token, err := jwt.NewSigned(jwtClaims, headers, getJWTSigner(signer, algName)) + token, err := jwt.NewSigned(jwtClaims, headers, GetJWTSigner(signer, algName)) if err != nil { return "", err }