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

PEX: Resolve values mapped by Input Descriptor constraint fields #2667

Merged
merged 4 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
64 changes: 50 additions & 14 deletions vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,35 @@ 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
}
gerardsn marked this conversation as resolved.
Show resolved Hide resolved
_, values, err := matchConstraint(inputDescriptor.Constraints, cred)
if err != nil {
return nil, fmt.Errorf("failed to match constraint for input descriptor '%s' and credential '%s': %w", inputDescriptorID, cred.ID, err)
}
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 +304,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 +314,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 +329,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
103 changes: 87 additions & 16 deletions vcr/pe/presentation_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,39 +443,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 +501,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 +564,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 @@ -645,6 +670,52 @@ func Test_matchFilter(t *testing.T) {
})
}

func TestPresentationDefinition_ResolveConstraintsFields(t *testing.T) {
jwtCredential := credential.JWTNutsOrganizationCredential(t)
jsonldCredential := credential.JWTNutsOrganizationCredential(t)
definition := definitions().JSONLDorJWT
t.Run("match JWT", func(t *testing.T) {
credentialMap := map[string]vc.VerifiableCredential{
"organization_credential": jwtCredential,
}

fieldValues, _ := definition.ResolveConstraintsFields(credentialMap)

require.Len(t, fieldValues, 2)
assert.Equal(t, "IJbergen", fieldValues["credentialsubject_organization_city"])
assert.Equal(t, "care", fieldValues["credentialsubject_organization_name"])
})
t.Run("match JSON-LD", func(t *testing.T) {
credentialMap := map[string]vc.VerifiableCredential{
"organization_credential": jsonldCredential,
}

fieldValues, _ := definition.ResolveConstraintsFields(credentialMap)

require.Len(t, fieldValues, 2)
assert.Equal(t, "IJbergen", fieldValues["credentialsubject_organization_city"])
assert.Equal(t, "care", fieldValues["credentialsubject_organization_name"])
})
t.Run("input descriptor without constraints", func(t *testing.T) {
format := PresentationDefinitionClaimFormatDesignations(map[string]map[string][]string{"jwt_vc": {"alg": {"ES256"}}})
definition := PresentationDefinition{
InputDescriptors: []*InputDescriptor{
{
Id: "any_credential",
Format: &format,
},
},
}
credentialMap := map[string]vc.VerifiableCredential{
"any_credential": jwtCredential,
}

fieldValues, _ := definition.ResolveConstraintsFields(credentialMap)
reinkrul marked this conversation as resolved.
Show resolved Hide resolved

assert.Empty(t, fieldValues)
})
}

func credentialToJSONLD(credential vc.VerifiableCredential) vc.VerifiableCredential {
bytes, err := credential.MarshalJSON()
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions vcr/pe/test/pd_jsonld_jwt.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
"id": "Definition requesting NutsOrganizationCredential",
"input_descriptors": [
{
"id": "organization_credential",
"constraints": {
"fields": [
{
"id": "credentialsubject_organization_city",
"path": [
"$.credentialSubject.organization.city",
"$.credentialSubject[0].organization.city"
Expand All @@ -15,6 +17,7 @@
}
},
{
"id": "credentialsubject_organization_name",
"path": [
"$.credentialSubject.organization.name",
"$.credentialSubject[0].organization.name"
Expand Down
Loading