From 90f63c1f58dc77328bf314e23edda938f2c5ea12 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Thu, 26 Oct 2023 16:20:25 +0200 Subject: [PATCH] PEX: Validate presentation submission --- vcr/pe/presentation_definition.go | 2 +- vcr/pe/presentation_submission.go | 45 ++++++++ vcr/pe/presentation_submission_test.go | 138 +++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 1 deletion(-) diff --git a/vcr/pe/presentation_definition.go b/vcr/pe/presentation_definition.go index 4b1261641c..80a5748e39 100644 --- a/vcr/pe/presentation_definition.go +++ b/vcr/pe/presentation_definition.go @@ -146,7 +146,7 @@ func (presentationDefinition PresentationDefinition) matchSubmissionRequirements } for _, group := range presentationDefinition.groups() { if _, ok := availableGroups[group.Name]; !ok { - return nil, 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) } } diff --git a/vcr/pe/presentation_submission.go b/vcr/pe/presentation_submission.go index 5771e4443f..1244af0d72 100644 --- a/vcr/pe/presentation_submission.go +++ b/vcr/pe/presentation_submission.go @@ -20,11 +20,14 @@ package pe import ( "encoding/json" + "errors" "fmt" "github.com/google/uuid" "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" + "github.com/nuts-foundation/nuts-node/vcr/log" v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2" + "reflect" ) // ParsePresentationSubmission validates the given JSON and parses it into a PresentationSubmission. @@ -137,3 +140,45 @@ func (b *PresentationSubmissionBuilder) Build(format string) (PresentationSubmis return presentationSubmission, signInstructions, nil } + +// Validate validates the Presentation Submission to the Verifiable Presentation and Presentation Definition and returns the mapped credentials. +// The credentials will be returned as map with the InputDescriptor.Id as key. +// It assumes credentials of the presentation only map in 1 way to the input descriptors. +func (s PresentationSubmission) Validate(presentation vc.VerifiablePresentation, definition PresentationDefinition) (map[string]vc.VerifiableCredential, error) { + expectedCredentials, expectedDescriptorMap, err := definition.Match(presentation.VerifiableCredential) + if err != nil { + return nil, fmt.Errorf("credential submission is invalid: %w", err) + } + if len(expectedCredentials) == 0 { + return nil, errors.New("credential submission is invalid, credentials does not match the presentation definition") + } + // Marshal, then unmarshal descriptor mappings into interface{}, to make sure ordering and zero-handling is the same for both. + // Then, they can simply be compared to check that the submission + var expected interface{} + if err := remarshal(expectedDescriptorMap, &expected); err != nil { + return nil, err + } + var actual interface{} + if err := remarshal(s.DescriptorMap, &actual); err != nil { + return nil, err + } + if !reflect.DeepEqual(expected, actual) { + expectedJSON, _ := json.Marshal(expected) + actualJSON, _ := json.Marshal(actual) + log.Logger().Infof("Input descriptor mapping seems incorrect.\n Got: %s\n Expected: %s", string(actualJSON), string(expectedJSON)) + return nil, fmt.Errorf("credential submission is invalid, input descriptor mapping looks invalid") + } + credentialMap := make(map[string]vc.VerifiableCredential, len(s.DescriptorMap)) + for i, inputDescriptor := range s.DescriptorMap { + credentialMap[inputDescriptor.Id] = expectedCredentials[i] + } + return credentialMap, nil +} + +func remarshal(v interface{}, target any) error { + result, err := json.Marshal(v) + if err != nil { + return err + } + return json.Unmarshal(result, target) +} diff --git a/vcr/pe/presentation_submission_test.go b/vcr/pe/presentation_submission_test.go index 6c9f31c5e2..69e08f3b68 100644 --- a/vcr/pe/presentation_submission_test.go +++ b/vcr/pe/presentation_submission_test.go @@ -86,3 +86,141 @@ func TestPresentationSubmissionBuilder_Build(t *testing.T) { assert.Equal(t, "$.verifiableCredential[0]", submission.DescriptorMap[1].PathNested[0].Path) }) } + +func TestPresentationSubmission_Validate(t *testing.T) { + vcID := ssi.MustParseURI("first-vc") + vp := vc.VerifiablePresentation{ + VerifiableCredential: []vc.VerifiableCredential{ + {ID: &vcID}, + }, + } + + t.Run("ok", func(t *testing.T) { + constant := vcID.String() + definition := PresentationDefinition{ + InputDescriptors: []*InputDescriptor{ + { + Id: "1", + Constraints: &Constraints{ + Fields: []Field{ + { + Path: []string{"$.id"}, + Filter: &Filter{ + Type: "string", + Const: &constant, + }, + }, + }, + }, + }, + }, + } + submission := PresentationSubmission{ + DescriptorMap: []InputDescriptorMappingObject{ + { + Id: "1", + Path: "$.verifiableCredential[0]", + }, + }, + } + + credentials, err := submission.Validate(vp, definition) + + assert.NoError(t, err, "credential submission is invalid, credentials does not match the presentation definition") + assert.Len(t, credentials, 1) + assert.Equal(t, vcID.String(), credentials["1"].ID.String()) + }) + t.Run("credentials don't match input descriptors", func(t *testing.T) { + constant := "incorrect ID" + definition := PresentationDefinition{ + InputDescriptors: []*InputDescriptor{ + { + Id: "1", + Constraints: &Constraints{ + Fields: []Field{ + { + Path: []string{"$.id"}, + Filter: &Filter{ + Type: "string", + Const: &constant, + }, + }, + }, + }, + }, + }, + } + submission := PresentationSubmission{ + DescriptorMap: []InputDescriptorMappingObject{ + { + Id: "1", + Path: "$.verifiableCredential[0].id", + }, + }, + } + + credentials, err := submission.Validate(vp, definition) + + assert.EqualError(t, err, "credential submission is invalid, credentials does not match the presentation definition") + assert.Empty(t, credentials) + }) + t.Run("credentials match wrong input descriptors", func(t *testing.T) { + incorrectID := "incorrect ID" + correctID := vcID.String() + count := 1 + definition := PresentationDefinition{ + SubmissionRequirements: []*SubmissionRequirement{ + { + Count: &count, + From: "any", + Rule: "pick", + }, + }, + InputDescriptors: []*InputDescriptor{ + { + Id: "1", + Group: []string{"any"}, + Constraints: &Constraints{ + Fields: []Field{ + { + Path: []string{"$.id"}, + Filter: &Filter{ + Type: "string", + Const: &incorrectID, + }, + }, + }, + }, + }, + { + Id: "2", + Group: []string{"any"}, + Constraints: &Constraints{ + Fields: []Field{ + { + Path: []string{"$.id"}, + Filter: &Filter{ + Type: "string", + Const: &correctID, + }, + }, + }, + }, + }, + }, + } + submission := PresentationSubmission{ + DescriptorMap: []InputDescriptorMappingObject{ + { + Id: "1", // actually maps to input descriptor 2, so should cause an error + Path: "$.verifiableCredential[0]", + }, + }, + } + + credentials, err := submission.Validate(vp, definition) + + assert.EqualError(t, err, "credential submission is invalid, input descriptor mapping looks invalid") + assert.Empty(t, credentials) + }) +}