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 all 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.PresentationSubmissionBuilder()
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.PresentationSubmissionBuilder()
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
121 changes: 66 additions & 55 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,21 +53,31 @@ 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 {
// we create an empty Candidate. If a VC matches, it'll be attached to the Candidate.
// if no VC matches, the Candidate will have an nil VC which is detected later on for SubmissionRequirement rules.
match := Candidate{
InputDescriptor: *inputDescriptor,
}
Expand All @@ -77,66 +93,64 @@ 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) {
// 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)
func (presentationDefinition PresentationDefinition) matchBasic(vcs []vc.VerifiableCredential) ([]InputDescriptorMappingObject, []vc.VerifiableCredential, error) {
// do the constraints check
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 {
// a constraint is not matched, return early
// we do not raise an error here since SubmissionRequirements might specify a "pick" rule
if candidate.VC == nil {
return nil, []vc.VerifiableCredential{}, nil
}
// create the InputDescriptorMappingObject with the relative path
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
// for each 'group' in input_descriptor there must be a matching 'from' field in a submission requirement
availableGroups := make(map[string]GroupCandidates)
// This is the "all groups must be present" check
availableGroups := make(map[string]groupCandidates)
for _, submissionRequirement := range presentationDefinition.SubmissionRequirements {
for _, group := range submissionRequirement.Groups() {
availableGroups[group] = GroupCandidates{
for _, group := range submissionRequirement.groups() {
availableGroups[group] = groupCandidates{
Name: group,
}
}
}
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)
}
}

// now we know there are no missing groups, we can start matching the submission requirements
// now we know there are no missing groups, we can start matching the SubmissionRequirements
// now we add each specific match to the correct group(s)
for _, match := range candidates {
for _, group := range match.InputDescriptor.Group {
Expand All @@ -146,63 +160,60 @@ func (presentationDefinition PresentationDefinition) matchSubmissionRequirements
}
}

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

// for each submission requirement:
// for each SubmissionRequirement:
// 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...)
}

uniqueVCs := deduplicate(selectedVCs)

// now we have the selected VCs, we can create the presentation submission
// now we have the selected VCs, we can create the PresentationSubmission
var index int
var descriptors []InputDescriptorMappingObject
outer:
for _, uniqueVC := range uniqueVCs {
// we loop over the candidate VCs and find the one that matches the unique VC
// for each match we create a InputDescriptorMappingObject which links the VC to the InputDescriptor from the PresentationDefinition
for _, candidate := range candidates {
if candidate.VC != nil && vcEqual(uniqueVC, *candidate.VC) {
mapping := InputDescriptorMappingObject{
Id: candidate.InputDescriptor.Id,
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.
// If no VC matches the input descriptor, the match is still returned.
func (presentationDefinition PresentationDefinition) groups() []GroupCandidates {
groups := make(map[string]GroupCandidates)
// groups returns all the groupCandidates with input descriptors and matching VCs.
func (presentationDefinition PresentationDefinition) groups() []groupCandidates {
groups := make(map[string]groupCandidates)
for _, inputDescriptor := range presentationDefinition.InputDescriptors {
for _, group := range inputDescriptor.Group {
existing, ok := groups[group]
if !ok {
existing = GroupCandidates{
existing = groupCandidates{
Name: group,
}
}
existing.Candidates = append(existing.Candidates, Candidate{InputDescriptor: *inputDescriptor})
groups[group] = existing
}
}
var result []GroupCandidates
var result []groupCandidates
for _, group := range groups {
result = append(result, group)
}
Expand Down Expand Up @@ -260,7 +271,7 @@ func matchDescriptor(descriptor InputDescriptor, credential vc.VerifiableCredent

return &InputDescriptorMappingObject{
Id: descriptor.Id,
Format: "ldp_vc", // todo: hardcoded for now, must be derived from the VC, but we don't support other VC types yet
Format: credential.Format(),
}, nil
}

Expand Down Expand Up @@ -351,7 +362,7 @@ func getValueAtPath(path string, vcAsInterface interface{}) (interface{}, error)
// Supported schema types: string, number, boolean, array, enum.
// Supported schema properties: const, enum, pattern. These only work for strings.
// Supported go value types: string, float64, int, bool and array.
// 'null' values are also not supported.
// 'null' values are not supported.
// It returns an error on unsupported features or when the regex pattern fails.
func matchFilter(filter Filter, value interface{}) (bool, error) {
// first we check if it's an enum, so we can recursively call matchFilter for each value
Expand Down
Loading
Loading