diff --git a/vcr/pe/presentation_definition.go b/vcr/pe/presentation_definition.go index 19705eadbe..9774e06e63 100644 --- a/vcr/pe/presentation_definition.go +++ b/vcr/pe/presentation_definition.go @@ -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 + } + _, 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 @@ -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 } @@ -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 @@ -298,26 +329,31 @@ 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 @@ -325,23 +361,23 @@ func matchField(field Field, credential map[string]interface{}) (bool, error) { // 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++ @@ -349,9 +385,9 @@ func matchField(field Field, credential map[string]interface{}) (bool, error) { // 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 diff --git a/vcr/pe/presentation_definition_test.go b/vcr/pe/presentation_definition_test.go index eef3cb75db..431d264d6a 100644 --- a/vcr/pe/presentation_definition_test.go +++ b/vcr/pe/presentation_definition_test.go @@ -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) }) } @@ -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) { @@ -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) }) }) @@ -645,6 +670,53 @@ 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, err := definition.ResolveConstraintsFields(credentialMap) + + require.NoError(t, err) + assert.Empty(t, fieldValues) + }) +} + func credentialToJSONLD(credential vc.VerifiableCredential) vc.VerifiableCredential { bytes, err := credential.MarshalJSON() if err != nil { diff --git a/vcr/pe/test/pd_jsonld_jwt.json b/vcr/pe/test/pd_jsonld_jwt.json index 45f77f383a..5c95549874 100644 --- a/vcr/pe/test/pd_jsonld_jwt.json +++ b/vcr/pe/test/pd_jsonld_jwt.json @@ -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" @@ -15,6 +17,7 @@ } }, { + "id": "credentialsubject_organization_name", "path": [ "$.credentialSubject.organization.name", "$.credentialSubject[0].organization.name"