Skip to content
This repository has been archived by the owner on Mar 27, 2024. It is now read-only.

Commit

Permalink
feat: Add support for SD-JWT in presentation exchange (#3509)
Browse files Browse the repository at this point in the history
Add support for SD-JWT in presentation exchange

Closes #3508

Signed-off-by: Sandra Vrtikapa <[email protected]>
  • Loading branch information
sandrask authored Feb 2, 2023
1 parent 3cb064c commit be9f5b2
Show file tree
Hide file tree
Showing 4 changed files with 803 additions and 33 deletions.
127 changes: 125 additions & 2 deletions pkg/doc/presexch/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
149 changes: 126 additions & 23 deletions pkg/doc/presexch/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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{}

Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Loading

0 comments on commit be9f5b2

Please sign in to comment.