Skip to content

Commit

Permalink
PEX: Resolve values mapped by Input Descriptor constraint fields
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Dec 9, 2023
1 parent 961dbc7 commit ec5d519
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 30 deletions.
61 changes: 47 additions & 14 deletions vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,32 @@ func (presentationDefinition PresentationDefinition) Match(vcs []vc.VerifiableCr
return selectedVCs, descriptorMaps, nil
}

// ResolveConstraintsFields returns a map where each of the InputDescriptor constraints field is mapped,
// to the corresponding value from the Verifiable Credentials that map to the InputDescriptor.
// The credentialMap is a map with the InputDescriptor.Id as key and the VerifiableCredential as value.
// Constraints that contain no ID are ignored.
func (presentationDefinition PresentationDefinition) ResolveConstraintsFields(credentialMap map[string]vc.VerifiableCredential) (map[string]interface{}, error) {
result := make(map[string]interface{})
for inputDescriptorID, cred := range credentialMap {
// Find the input descriptor
var inputDescriptor InputDescriptor
for _, curr := range presentationDefinition.InputDescriptors {
if curr.Id == inputDescriptorID {
inputDescriptor = *curr
break
}
}
if inputDescriptor.Constraints == nil {
continue
}
_, values, _ := matchConstraint(inputDescriptor.Constraints, cred)
for key, value := range values {
result[key] = value
}
}
return result, nil
}

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

Expand Down Expand Up @@ -275,7 +301,8 @@ func matchCredential(descriptor InputDescriptor, credential vc.VerifiableCredent
// for each constraint in descriptor.constraints:
// a vc must match the constraint
if descriptor.Constraints != nil {
return matchConstraint(descriptor.Constraints, credential)
matches, _, err := matchConstraint(descriptor.Constraints, credential)
return matches, err
}
return true, nil
}
Expand All @@ -284,7 +311,8 @@ func matchCredential(descriptor InputDescriptor, credential vc.VerifiableCredent
// All Fields need to match according to the Field rules.
// IsHolder, SameSubject, SubjectIsIssuer, Statuses are not supported for now.
// LimitDisclosure is not supported for now.
func matchConstraint(constraint *Constraints, credential vc.VerifiableCredential) (bool, error) {
// If the constraint matches, it returns true and a map containing constraint field IDs and matched values.
func matchConstraint(constraint *Constraints, credential vc.VerifiableCredential) (bool, map[string]interface{}, error) {
// jsonpath works on interfaces, so convert the VC to an interface
var credentialAsMap map[string]interface{}
var err error
Expand All @@ -298,60 +326,65 @@ func matchConstraint(constraint *Constraints, credential vc.VerifiableCredential
credentialAsMap, err = remarshalToMap(credential)
}
if err != nil {
return false, err
return false, nil, err
}

// for each field in constraint.fields:
// a vc must match the field
values := make(map[string]interface{})
for _, field := range constraint.Fields {
match, err := matchField(field, credentialAsMap)
match, value, err := matchField(field, credentialAsMap)
if err != nil {
return false, err
return false, nil, err
}
if !match {
return false, nil
return false, nil, nil
}
if field.Id != nil {
values[*field.Id] = value
}
}
return true, nil
return true, values, nil
}

// matchField matches the field against the VC.
// If the field matches, it returns true and the matched value. The matched value can be nil if the field is optional.
// All fields need to match unless optional is set to true and no values are found for all the paths.
func matchField(field Field, credential map[string]interface{}) (bool, error) {
func matchField(field Field, credential map[string]interface{}) (bool, interface{}, error) {
// for each path in field.paths:
// a vc must match one of the path
var optionalInvalid int
for _, path := range field.Path {
// if path is not found continue
value, err := getValueAtPath(path, credential)
if err != nil {
return false, err
return false, nil, err
}
if value == nil {
continue
}

if field.Filter == nil {
return true, nil
return true, value, nil
}

// if filter at path matches return true
match, err := matchFilter(*field.Filter, value)
if err != nil {
return false, err
return false, nil, err
}
if match {
return true, nil
return true, value, nil
}
// if filter at path does not match continue and set optionalInvalid
optionalInvalid++
}
// no matches, check optional. Optional is only valid if all paths returned no results
// not if a filter did not match
if field.Optional != nil && *field.Optional && optionalInvalid == 0 {
return true, nil
return true, nil, nil
}
return false, nil
return false, nil, nil
}

// getValueAtPath uses the JSON path expression to get the value from the VC
Expand Down
100 changes: 84 additions & 16 deletions vcr/pe/presentation_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"crypto/rand"
"embed"
"encoding/json"
"fmt"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"testing"

Expand Down Expand Up @@ -443,39 +444,54 @@ func Test_matchConstraint(t *testing.T) {
testCredential := vc.VerifiableCredential{}
_ = json.Unmarshal([]byte(testCredentialString), &testCredential)

credSubjectFieldID := "credential_subject_field"
typeVal := "VerifiableCredential"
f1True := Field{Path: []string{"$.credentialSubject.field"}}
f1True := Field{Id: &credSubjectFieldID, Path: []string{"$.credentialSubject.field"}}
f1TrueWithoutID := Field{Path: []string{"$.credentialSubject.field"}}
f2True := Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &typeVal}}
f3False := Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Const: &typeVal}}
fieldMap := map[string]interface{}{credSubjectFieldID: "value"}

t.Run("single constraint match", func(t *testing.T) {
match, err := matchConstraint(&Constraints{Fields: []Field{f1True}}, testCredential)
match, value, err := matchConstraint(&Constraints{Fields: []Field{f1True}}, testCredential)

require.NoError(t, err)
assert.Equal(t, fieldMap, value)
assert.True(t, match)
})
t.Run("field match without ID is not included in values map", func(t *testing.T) {
match, values, err := matchConstraint(&Constraints{Fields: []Field{f1TrueWithoutID}}, testCredential)

require.NoError(t, err)
assert.Empty(t, values)
assert.True(t, match)
})
t.Run("single constraint mismatch", func(t *testing.T) {
match, err := matchConstraint(&Constraints{Fields: []Field{f3False}}, testCredential)
match, values, err := matchConstraint(&Constraints{Fields: []Field{f3False}}, testCredential)

require.NoError(t, err)
assert.Nil(t, values)
assert.False(t, match)
})
t.Run("multi constraint match", func(t *testing.T) {
match, err := matchConstraint(&Constraints{Fields: []Field{f1True, f2True}}, testCredential)
match, values, err := matchConstraint(&Constraints{Fields: []Field{f1True, f2True}}, testCredential)

require.NoError(t, err)
assert.Equal(t, fieldMap, values)
assert.True(t, match)
})
t.Run("multi constraint, single mismatch", func(t *testing.T) {
match, err := matchConstraint(&Constraints{Fields: []Field{f1True, f3False}}, testCredential)
match, values, err := matchConstraint(&Constraints{Fields: []Field{f1True, f3False}}, testCredential)

require.NoError(t, err)
assert.Nil(t, values)
assert.False(t, match)
})
t.Run("error", func(t *testing.T) {
match, err := matchConstraint(&Constraints{Fields: []Field{{Path: []string{"$$"}}}}, testCredential)
match, values, err := matchConstraint(&Constraints{Fields: []Field{{Path: []string{"$$"}}}}, testCredential)

require.Error(t, err)
assert.Nil(t, values)
assert.False(t, match)
})
}
Expand All @@ -486,50 +502,57 @@ func Test_matchField(t *testing.T) {
testCredentialMap, _ := remarshalToMap(testCredential)

t.Run("single path match", func(t *testing.T) {
match, err := matchField(Field{Path: []string{"$.credentialSubject.field"}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.credentialSubject.field"}}, testCredentialMap)

require.NoError(t, err)
assert.Equal(t, "value", value)
assert.True(t, match)
})
t.Run("multi path match", func(t *testing.T) {
match, err := matchField(Field{Path: []string{"$.other", "$.credentialSubject.field"}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.other", "$.credentialSubject.field"}}, testCredentialMap)

require.NoError(t, err)
assert.Equal(t, "value", value)
assert.True(t, match)
})
t.Run("no match", func(t *testing.T) {
match, err := matchField(Field{Path: []string{"$.foo", "$.bar"}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.foo", "$.bar"}}, testCredentialMap)

require.NoError(t, err)
assert.Nil(t, value)
assert.False(t, match)
})
t.Run("no match, but optional", func(t *testing.T) {
trueVal := true
match, err := matchField(Field{Path: []string{"$.foo", "$.bar"}, Optional: &trueVal}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.foo", "$.bar"}, Optional: &trueVal}, testCredentialMap)

require.NoError(t, err)
assert.Nil(t, value)
assert.True(t, match)
})
t.Run("invalid match and optional", func(t *testing.T) {
trueVal := true
stringVal := "bar"
match, err := matchField(Field{Path: []string{"$.credentialSubject.field", "$.foo"}, Optional: &trueVal, Filter: &Filter{Const: &stringVal}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.credentialSubject.field", "$.foo"}, Optional: &trueVal, Filter: &Filter{Const: &stringVal}}, testCredentialMap)

require.NoError(t, err)
assert.Nil(t, value)
assert.False(t, match)
})
t.Run("valid match with Filter", func(t *testing.T) {
stringVal := "value"
match, err := matchField(Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap)

require.NoError(t, err)
assert.Equal(t, stringVal, value)
assert.True(t, match)
})
t.Run("match on type", func(t *testing.T) {
stringVal := "VerifiableCredential"
match, err := matchField(Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap)

require.NoError(t, err)
assert.Equal(t, stringVal, value)
assert.True(t, match)
})
t.Run("match on type array", func(t *testing.T) {
Expand All @@ -542,24 +565,27 @@ func Test_matchField(t *testing.T) {
}`
_ = json.Unmarshal([]byte(testCredentialString), &testCredentialMap)
stringVal := "VerifiableCredential"
match, err := matchField(Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.type"}, Filter: &Filter{Type: "string", Const: &stringVal}}, testCredentialMap)

require.NoError(t, err)
assert.Equal(t, []interface{}{"VerifiableCredential"}, value)
assert.True(t, match)
})

t.Run("errors", func(t *testing.T) {
t.Run("invalid path", func(t *testing.T) {
match, err := matchField(Field{Path: []string{"$$"}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$$"}}, testCredentialMap)

require.Error(t, err)
assert.Nil(t, value)
assert.False(t, match)
})
t.Run("invalid pattern", func(t *testing.T) {
pattern := "["
match, err := matchField(Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Pattern: &pattern}}, testCredentialMap)
match, value, err := matchField(Field{Path: []string{"$.credentialSubject.field"}, Filter: &Filter{Type: "string", Pattern: &pattern}}, testCredentialMap)

require.Error(t, err)
assert.Nil(t, value)
assert.False(t, match)
})
})
Expand Down Expand Up @@ -657,3 +683,45 @@ func credentialToJSONLD(credential vc.VerifiableCredential) vc.VerifiableCredent
}
return result
}

func TestPresentationDefinition_ResolveConstraintsFields(t *testing.T) {
type fields struct {
Format *PresentationDefinitionClaimFormatDesignations
Frame *Frame
Id string
InputDescriptors []*InputDescriptor
Name string
Purpose *string
SubmissionRequirements []*SubmissionRequirement
}
type args struct {
credentialMap map[string]vc.VerifiableCredential
}
tests := []struct {
name string
fields fields
args args
want map[string]interface{}
wantErr assert.ErrorAssertionFunc
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
presentationDefinition := PresentationDefinition{
Format: tt.fields.Format,
Frame: tt.fields.Frame,
Id: tt.fields.Id,
InputDescriptors: tt.fields.InputDescriptors,
Name: tt.fields.Name,
Purpose: tt.fields.Purpose,
SubmissionRequirements: tt.fields.SubmissionRequirements,
}
got, err := presentationDefinition.ResolveConstraintsFields(tt.args.credentialMap)
if !tt.wantErr(t, err, fmt.Sprintf("ResolveConstraintsFields(%v)", tt.args.credentialMap)) {
return
}
assert.Equalf(t, tt.want, got, "ResolveConstraintsFields(%v)", tt.args.credentialMap)
})
}
}

0 comments on commit ec5d519

Please sign in to comment.