Skip to content

Commit

Permalink
generate presentation submission with path_nested (#2563)
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst authored Nov 3, 2023
1 parent 5306cf3 commit caa4913
Show file tree
Hide file tree
Showing 10 changed files with 371 additions and 199 deletions.
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

0 comments on commit caa4913

Please sign in to comment.