Skip to content

Commit

Permalink
Add submission requirement feature to presentation definition matching (
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst authored Oct 31, 2023
1 parent 72b788c commit 5306cf3
Show file tree
Hide file tree
Showing 8 changed files with 1,152 additions and 95 deletions.
42 changes: 0 additions & 42 deletions vcr/pe/fields.go

This file was deleted.

192 changes: 174 additions & 18 deletions vcr/pe/matcher.go → vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,55 @@ import (
// ErrUnsupportedFilter is returned when a filter uses unsupported features.
var ErrUnsupportedFilter = errors.New("unsupported filter")

// Candidate is a struct that holds the result of a match between an input descriptor and a VC
// A non-matching VC also leads to a Candidate, but without a VC.
type Candidate struct {
InputDescriptor InputDescriptor
VC *vc.VerifiableCredential
}

// 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 only supports the following:
// It supports the following:
// - ldp_vc format
// - pattern, const and enum only on string fields
// - 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.
// 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) {
if len(presentationDefinition.SubmissionRequirements) > 0 {
return presentationDefinition.matchSubmissionRequirements(vcs)
}
return presentationDefinition.matchBasic(vcs)
}

func (presentationDefinition PresentationDefinition) matchConstraints(vcs []vc.VerifiableCredential) ([]Candidate, error) {
var candidates []Candidate
for _, inputDescriptor := range presentationDefinition.InputDescriptors {
match := Candidate{
InputDescriptor: *inputDescriptor,
}
for _, credential := range vcs {
isMatch, err := matchCredential(*inputDescriptor, credential)
if err != nil {
return nil, err
}
if isMatch && matchFormat(presentationDefinition.Format, credential) {
match.VC = &credential
break
}
}
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:
Expand All @@ -54,30 +90,123 @@ func (presentationDefinition PresentationDefinition) Match(vcs []vc.VerifiableCr
Id: uuid.New().String(),
DefinitionId: presentationDefinition.Id,
}
var matchingCredentials []vc.VerifiableCredential
matches, err := presentationDefinition.matchConstraints(vcs)
if err != nil {
return PresentationSubmission{}, nil, err
}
var index int
for _, inputDescriptor := range presentationDefinition.InputDescriptors {
var mapping *InputDescriptorMappingObject
var err error
for _, credential := range vcs {
mapping, err = matchDescriptor(*inputDescriptor, credential)
if err != nil {
return PresentationSubmission{}, nil, err
matchingCredentials := make([]vc.VerifiableCredential, len(matches))
for _, match := range matches {
if match.VC == nil {
return PresentationSubmission{}, []vc.VerifiableCredential{}, nil
}
mapping := InputDescriptorMappingObject{
Id: match.InputDescriptor.Id,
Format: match.VC.Format(),
Path: fmt.Sprintf("$.verifiableCredential[%d]", index),
}
presentationSubmission.DescriptorMap = append(presentationSubmission.DescriptorMap, mapping)
matchingCredentials[index] = *match.VC
index++
}

return presentationSubmission, matchingCredentials, nil
}

func (presentationDefinition PresentationDefinition) matchSubmissionRequirements(vcs []vc.VerifiableCredential) (PresentationSubmission, []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
}

// 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)
for _, submissionRequirement := range presentationDefinition.SubmissionRequirements {
for _, group := range submissionRequirement.Groups() {
availableGroups[group] = GroupCandidates{
Name: group,
}
if mapping != nil && matchFormat(presentationDefinition.Format, credential) {
mapping.Path = fmt.Sprintf("$.verifiableCredential[%d]", index)
presentationSubmission.DescriptorMap = append(presentationSubmission.DescriptorMap, *mapping)
matchingCredentials = append(matchingCredentials, credential)
}
}
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)
}
}

// now we know there are no missing groups, we can start matching the submission requirements
// now we add each specific match to the correct group(s)
for _, match := range candidates {
for _, group := range match.InputDescriptor.Group {
current := availableGroups[group]
current.Candidates = append(current.Candidates, match)
availableGroups[group] = current
}
}

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
for _, submissionRequirement := range presentationDefinition.SubmissionRequirements {
submissionRequirementVCs, err := submissionRequirement.match(availableGroups)
if err != nil {
return PresentationSubmission{}, nil, err
}
selectedVCs = append(selectedVCs, submissionRequirementVCs...)
}

uniqueVCs := deduplicate(selectedVCs)

// now we have the selected VCs, we can create the presentation submission
var index int
outer:
for _, uniqueVC := range uniqueVCs {
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)
index++
break
continue outer
}
}
if mapping == nil {
return PresentationSubmission{}, []vc.VerifiableCredential{}, nil
}
}

return presentationSubmission, matchingCredentials, nil
return presentationSubmission, 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)
for _, inputDescriptor := range presentationDefinition.InputDescriptors {
for _, group := range inputDescriptor.Group {
existing, ok := groups[group]
if !ok {
existing = GroupCandidates{
Name: group,
}
}
existing.Candidates = append(existing.Candidates, Candidate{InputDescriptor: *inputDescriptor})
groups[group] = existing
}
}
var result []GroupCandidates
for _, group := range groups {
result = append(result, group)
}
return result
}

// matchFormat checks if the credential matches the Format from the presentationDefinition.
Expand Down Expand Up @@ -290,3 +419,30 @@ func matchFilter(filter Filter, value interface{}) (bool, error) {
// if we get here, no pattern, enum or const is requested just the type.
return true, nil
}

// deduplicate removes duplicate VCs from the slice.
// It uses JSON marshalling to determine if two VCs are equal.
func deduplicate(vcs []vc.VerifiableCredential) []vc.VerifiableCredential {
var result []vc.VerifiableCredential
for _, vc := range vcs {
found := false
for _, existing := range result {
if vcEqual(existing, vc) {
found = true
break
}
}
if !found {
result = append(result, vc)
}
}
return result
}

// vcEqual checks if two VCs are equal.
// It uses JSON marshalling to determine if two VCs are equal.
func vcEqual(a, b vc.VerifiableCredential) bool {
aJSON, _ := json.Marshal(a)
bJSON, _ := json.Marshal(b)
return string(aJSON) == string(bJSON)
}
Loading

0 comments on commit 5306cf3

Please sign in to comment.