Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

generate presentation submission with path_nested #2563

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 18 additions & 11 deletions auth/api/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,18 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S
}
}

// TODO: https://github.com/nuts-foundation/nuts-node/issues/2359
// TODO: What if multiple credentials of the same type match?
_, matchingCredentials, err := presentationDefinition.Match(credentials)
submissionBuilder := presentationDefinition.Builder()
submissionBuilder.AddWallet(session.OwnDID, ownCredentials)
_, signInstructions, err := submissionBuilder.Build("ldp_vp")
if err != nil {
return nil, fmt.Errorf("unable to match presentation definition: %w", err)
}
var credentialIDs []string
for _, matchingCredential := range matchingCredentials {
templateParams.Credentials = append(templateParams.Credentials, makeCredentialInfo(matchingCredential))
credentialIDs = append(credentialIDs, matchingCredential.ID.String())
for _, signInstruction := range signInstructions {
for _, matchingCredential := range signInstruction.VerifiableCredentials {
templateParams.Credentials = append(templateParams.Credentials, makeCredentialInfo(matchingCredential))
credentialIDs = append(credentialIDs, matchingCredential.ID.String())
}
}
session.ServerState["openid4vp_credentials"] = credentialIDs

Expand Down Expand Up @@ -204,16 +206,21 @@ func (r *Wrapper) handlePresentationRequestAccept(c echo.Context) error {
if presentationDefinition == nil {
return fmt.Errorf("unsupported scope for presentation exchange: %s", session.Scope)
}
// TODO: Options
// TODO: Options (including format)
resultParams := map[string]string{}
presentationSubmission, credentials, err := presentationDefinition.Match(credentials)
submissionBuilder := presentationDefinition.Builder()
submissionBuilder.AddWallet(session.OwnDID, credentials)
submission, signInstructions, err := submissionBuilder.Build("ldp_vp")
if err != nil {
// Matched earlier, shouldn't happen
return err
}
presentationSubmissionJSON, _ := json.Marshal(presentationSubmission)
presentationSubmissionJSON, _ := json.Marshal(submission)
resultParams[presentationSubmissionParam] = string(presentationSubmissionJSON)
verifiablePresentation, err := r.vcr.Wallet().BuildPresentation(c.Request().Context(), credentials, holder.PresentationOptions{}, &session.OwnDID, false)
if len(signInstructions) != 1 {
// todo support multiple wallets (org + user)
return errors.New("expected to create exactly one presentation")
}
verifiablePresentation, err := r.vcr.Wallet().BuildPresentation(c.Request().Context(), signInstructions[0].VerifiableCredentials, holder.PresentationOptions{}, &signInstructions[0].Holder, false)
if err != nil {
return err
}
Expand Down
80 changes: 44 additions & 36 deletions vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/PaesslerAG/jsonpath"
"github.com/dlclark/regexp2"
"github.com/google/uuid"
"github.com/nuts-foundation/go-did/vc"
"strings"
)

// ErrUnsupportedFilter is returned when a filter uses unsupported features.
Expand All @@ -39,6 +39,12 @@ type Candidate struct {
VC *vc.VerifiableCredential
}

// PresentationContext is a helper struct to keep track of the index of the VP in the nested paths of a PresentationSubmission.
type PresentationContext struct {
Index int
PresentationSubmission *PresentationSubmission
}

// Match matches the VCs against the presentation definition.
// It implements §5 of the Presentation Exchange specification (v2.x.x pre-Draft, 2023-07-29) (https://identity.foundation/presentation-exchange/#presentation-definition)
// It supports the following:
Expand All @@ -47,20 +53,28 @@ type Candidate struct {
// - number, boolean, array and string JSON schema types
// - Submission Requirements Feature
// It doesn't do the credential search, this should be done before calling this function.
// The PresentationDefinition.Format should be altered/set if an envelope defines the supported format before calling.
// The resulting PresentationSubmission has paths that are relative to the matching VCs.
// The PresentationSubmission needs to be altered so the paths use "path_nested"s that are relative to the created VP.
// The given PresentationContext is used to set the correct vp index in the nested paths and to alter the given PresentationSubmission.
// It assumes this method is used for OpenID4VP since other envelopes require different nesting.
// ErrUnsupportedFilter is returned when a filter uses unsupported features.
// Other errors can be returned for faulty JSON paths or regex patterns.
func (presentationDefinition PresentationDefinition) Match(vcs []vc.VerifiableCredential) (PresentationSubmission, []vc.VerifiableCredential, error) {
func (presentationDefinition PresentationDefinition) Match(vcs []vc.VerifiableCredential) ([]vc.VerifiableCredential, []InputDescriptorMappingObject, error) {
var selectedVCs []vc.VerifiableCredential
var descriptorMaps []InputDescriptorMappingObject
var err error
if len(presentationDefinition.SubmissionRequirements) > 0 {
return presentationDefinition.matchSubmissionRequirements(vcs)
if descriptorMaps, selectedVCs, err = presentationDefinition.matchSubmissionRequirements(vcs); err != nil {
return nil, nil, err
}
} else if descriptorMaps, selectedVCs, err = presentationDefinition.matchBasic(vcs); err != nil {
return nil, nil, err
}
return presentationDefinition.matchBasic(vcs)

return selectedVCs, descriptorMaps, nil
}

func (presentationDefinition PresentationDefinition) matchConstraints(vcs []vc.VerifiableCredential) ([]Candidate, error) {
var candidates []Candidate

for _, inputDescriptor := range presentationDefinition.InputDescriptors {
match := Candidate{
InputDescriptor: *inputDescriptor,
Expand All @@ -77,47 +91,45 @@ func (presentationDefinition PresentationDefinition) matchConstraints(vcs []vc.V
}
candidates = append(candidates, match)
}

return candidates, nil
}

func (presentationDefinition PresentationDefinition) matchBasic(vcs []vc.VerifiableCredential) (PresentationSubmission, []vc.VerifiableCredential, error) {
func (presentationDefinition PresentationDefinition) matchBasic(vcs []vc.VerifiableCredential) ([]InputDescriptorMappingObject, []vc.VerifiableCredential, error) {
// for each VC in vcs:
// for each descriptor in presentation_definition.descriptors:
// for each constraint in descriptor.constraints:
// for each field in constraint.fields:
// a vc must match the field
presentationSubmission := PresentationSubmission{
Id: uuid.New().String(),
DefinitionId: presentationDefinition.Id,
}
matches, err := presentationDefinition.matchConstraints(vcs)
candidates, err := presentationDefinition.matchConstraints(vcs)
if err != nil {
return PresentationSubmission{}, nil, err
return nil, nil, err
}
matchingCredentials := make([]vc.VerifiableCredential, len(candidates))
var descriptors []InputDescriptorMappingObject
var index int
matchingCredentials := make([]vc.VerifiableCredential, len(matches))
for _, match := range matches {
if match.VC == nil {
return PresentationSubmission{}, []vc.VerifiableCredential{}, nil
for i, candidate := range candidates {
if candidate.VC == nil {
return nil, []vc.VerifiableCredential{}, nil
}
mapping := InputDescriptorMappingObject{
Id: match.InputDescriptor.Id,
Format: match.VC.Format(),
Id: candidate.InputDescriptor.Id,
Format: candidate.VC.Format(),
Path: fmt.Sprintf("$.verifiableCredential[%d]", index),
}
presentationSubmission.DescriptorMap = append(presentationSubmission.DescriptorMap, mapping)
matchingCredentials[index] = *match.VC
descriptors = append(descriptors, mapping)
matchingCredentials[i] = *candidate.VC
index++
}

return presentationSubmission, matchingCredentials, nil
return descriptors, matchingCredentials, nil
}

func (presentationDefinition PresentationDefinition) matchSubmissionRequirements(vcs []vc.VerifiableCredential) (PresentationSubmission, []vc.VerifiableCredential, error) {
func (presentationDefinition PresentationDefinition) matchSubmissionRequirements(vcs []vc.VerifiableCredential) ([]InputDescriptorMappingObject, []vc.VerifiableCredential, error) {
// first we use the constraint matching algorithm to get the matching credentials
candidates, err := presentationDefinition.matchConstraints(vcs)
if err != nil {
return PresentationSubmission{}, nil, err
return nil, nil, err
}

// then we check the group constraints
Expand All @@ -132,7 +144,7 @@ func (presentationDefinition PresentationDefinition) matchSubmissionRequirements
}
for _, group := range presentationDefinition.groups() {
if _, ok := availableGroups[group.Name]; !ok {
return PresentationSubmission{}, nil, fmt.Errorf("group %s is required but not available", group.Name)
return nil, nil, fmt.Errorf("group %s is required but not available", group.Name)
}
}

Expand All @@ -146,19 +158,14 @@ func (presentationDefinition PresentationDefinition) matchSubmissionRequirements
}
}

presentationSubmission := PresentationSubmission{
Id: uuid.New().String(),
DefinitionId: presentationDefinition.Id,
}
var selectedVCs []vc.VerifiableCredential

// for each submission requirement:
// we select the credentials that match the requirement
// then we apply the rules and save the resulting credentials
var selectedVCs []vc.VerifiableCredential
for _, submissionRequirement := range presentationDefinition.SubmissionRequirements {
submissionRequirementVCs, err := submissionRequirement.match(availableGroups)
if err != nil {
return PresentationSubmission{}, nil, err
return nil, nil, err
}
selectedVCs = append(selectedVCs, submissionRequirementVCs...)
}
Expand All @@ -167,6 +174,7 @@ func (presentationDefinition PresentationDefinition) matchSubmissionRequirements

// now we have the selected VCs, we can create the presentation submission
var index int
var descriptors []InputDescriptorMappingObject
outer:
for _, uniqueVC := range uniqueVCs {
for _, candidate := range candidates {
Expand All @@ -176,14 +184,14 @@ outer:
Format: candidate.VC.Format(),
Path: fmt.Sprintf("$.verifiableCredential[%d]", index),
}
presentationSubmission.DescriptorMap = append(presentationSubmission.DescriptorMap, mapping)
descriptors = append(descriptors, mapping)
index++
continue outer
}
}
}

return presentationSubmission, uniqueVCs, nil
return descriptors, uniqueVCs, nil
}

// groups returns all the Matches with input descriptors and matching VCs.
Expand Down
52 changes: 24 additions & 28 deletions vcr/pe/presentation_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,19 @@ func TestMatch(t *testing.T) {
_ = json.Unmarshal(vcJSON, &verifiableCredential)

t.Run("Happy flow", func(t *testing.T) {
presentationSubmission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{verifiableCredential})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{verifiableCredential})

require.NoError(t, err)
assert.Len(t, vcs, 1)
require.Len(t, presentationSubmission.DescriptorMap, 1)
assert.Equal(t, "$.verifiableCredential[0]", presentationSubmission.DescriptorMap[0].Path)
require.Len(t, mappingObjects, 1)
assert.Equal(t, "$.verifiableCredential[0]", mappingObjects[0].Path)
})
t.Run("Only second VC matches", func(t *testing.T) {
presentationSubmission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{{Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential")}}, verifiableCredential})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{{Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential")}}, verifiableCredential})

require.NoError(t, err)
assert.Len(t, vcs, 1)
require.Len(t, presentationSubmission.DescriptorMap, 1)
assert.Equal(t, "$.verifiableCredential[0]", presentationSubmission.DescriptorMap[0].Path)
assert.Len(t, mappingObjects, 1)
})
})
t.Run("Submission requirement feature", func(t *testing.T) {
Expand All @@ -131,13 +130,11 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickOne), &presentationDefinition)

submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 1)
require.NotNil(t, submission)
require.Len(t, submission.DescriptorMap, 1)
assert.Equal(t, "$.verifiableCredential[0]", submission.DescriptorMap[0].Path)
assert.Len(t, mappingObjects, 1)
})
t.Run("error", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
Expand All @@ -154,11 +151,11 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickMinMax), &presentationDefinition)

submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.NotNil(t, submission)
assert.Len(t, mappingObjects, 2)
})
t.Run("error", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
Expand All @@ -174,58 +171,57 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickOnePerGroup), &presentationDefinition)

submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.NotNil(t, submission)
assert.Len(t, mappingObjects, 2)
})
t.Run("Pick all", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.All), &presentationDefinition)

submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.NotNil(t, submission)
assert.Len(t, mappingObjects, 2)
})
t.Run("Deduplicate", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.DeduplicationRequired), &presentationDefinition)

submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1})

require.NoError(t, err)
assert.Len(t, vcs, 1)
assert.NotNil(t, submission)
assert.Len(t, mappingObjects, 1)
})
t.Run("Pick 1 from nested", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickOneFromNested), &presentationDefinition)

t.Run("all from group A or all from group B", func(t *testing.T) {
t.Run("all A", func(t *testing.T) {
submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.NotNil(t, submission)
assert.Len(t, mappingObjects, 2)
})
t.Run("all B", func(t *testing.T) {
submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc3, vc4})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc3, vc4})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.Len(t, mappingObjects, 2)
assert.Equal(t, "3", vcs[0].ID.String())
assert.NotNil(t, submission)
})
t.Run("no match", func(t *testing.T) {
submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc3})
vcs, _, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc3})

require.Error(t, err)
assert.Len(t, vcs, 0)
assert.NotNil(t, submission)
})
})
})
Expand All @@ -234,11 +230,11 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.AllFromNested), &presentationDefinition)

submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.NotNil(t, submission)
assert.Len(t, mappingObjects, 2)
})
t.Run("error", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
Expand All @@ -255,11 +251,11 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickMinMaxFromNested), &presentationDefinition)

submission, vcs, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2, vc3})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2, vc3})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.NotNil(t, submission)
assert.Len(t, mappingObjects, 2)
})
t.Run("error", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
Expand Down
Loading
Loading