diff --git a/vcr/pe/presentation_submission.go b/vcr/pe/presentation_submission.go index 8dcef7c917..0ed435b51a 100644 --- a/vcr/pe/presentation_submission.go +++ b/vcr/pe/presentation_submission.go @@ -25,6 +25,7 @@ import ( "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/credential" v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2" "strings" ) @@ -254,3 +255,44 @@ func resolveCredential(path []string, mapping InputDescriptorMappingObject, valu _ = json.Unmarshal(decodedValueJSON, &decodedValueMap) return resolveCredential(fullPath, *mapping.PathNested, decodedValueMap) } + +// Validate validates the Presentation Submission to the Verifiable Presentations 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 presentations only map in 1 way to the input descriptors. +func (s PresentationSubmission) Validate(presentations []vc.VerifiablePresentation, definition PresentationDefinition) (map[string]vc.VerifiableCredential, error) { + actualCredentials, err := s.Resolve(presentations) + if err != nil { + return nil, err + } + + // Create a new presentation submission and resolve the credentials using our own copy of the Presentation Definition. + // This is then used to validate the presentation submission. The Input Descriptor IDs should map to the same credentials. + submissionBuilder := definition.PresentationSubmissionBuilder() + for _, presentation := range presentations { + signer, err := credential.PresentationSigner(presentation) + if err != nil { + return nil, fmt.Errorf("unable to derive presentation signer: %w", err) + } + submissionBuilder.AddWallet(*signer, presentation.VerifiableCredential) + } + // TODO: What if there the presentations are of mixed format? + //expectedPresentationSubmission, _, err := submissionBuilder.Build(presentations[0].Format()) + expectedPresentationSubmission, _, err := submissionBuilder.Build("") + if err != nil { + return nil, err + } + expectedCredenials, err := expectedPresentationSubmission.Resolve(presentations) + if err != nil { + return nil, err + } + + if len(actualCredentials) != len(expectedCredenials) { + return nil, fmt.Errorf("expected %d credentials, got %d", len(expectedCredenials), len(actualCredentials)) + } + for inputDescriptorID, expectedCredential := range expectedCredenials { + if actualCredentials[inputDescriptorID].Raw() != expectedCredential.Raw() { + return nil, fmt.Errorf("incorrect mapping for input descriptor : %s", inputDescriptorID) + } + } + return expectedCredenials, nil +} diff --git a/vcr/pe/presentation_submission_test.go b/vcr/pe/presentation_submission_test.go index 4acbc873b3..7df6c05d08 100644 --- a/vcr/pe/presentation_submission_test.go +++ b/vcr/pe/presentation_submission_test.go @@ -24,6 +24,7 @@ import ( "github.com/nuts-foundation/go-did/did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/vcr/pe/test" + "github.com/nuts-foundation/nuts-node/vcr/signature/proof" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -383,3 +384,225 @@ func TestPresentationSubmission_Resolve(t *testing.T) { assert.Nil(t, credentials) }) } + +func credentialToJSONLD(credential vc.VerifiableCredential) vc.VerifiableCredential { + bytes, err := credential.MarshalJSON() + if err != nil { + panic(err) + } + var result vc.VerifiableCredential + err = json.Unmarshal(bytes, &result) + if err != nil { + panic(err) + } + return result +} + +func TestPresentationSubmission_Validate(t *testing.T) { + vcID := ssi.MustParseURI("did:example:123#first-vc") + vp := vc.VerifiablePresentation{ + VerifiableCredential: []vc.VerifiableCredential{ + {ID: &vcID}, + }, + Proof: []interface{}{ + proof.LDProof{VerificationMethod: vcID}, + }, + } + + t.Run("ok - 1 presentation", 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", + Format: "ldp_vc", + }, + }, + } + + credentials, err := submission.Validate([]vc.VerifiablePresentation{vp}, definition) + + require.NoError(t, err) + require.Len(t, credentials, 1) + assert.Equal(t, vcID.String(), credentials["1"].ID.String()) + }) + t.Run("ok - 2 presentations", func(t *testing.T) { + constant1 := vcID.String() + secondVCID := ssi.MustParseURI("did:example:123#second-vc") + constant2 := secondVCID.String() + secondVP := vc.VerifiablePresentation{ + VerifiableCredential: []vc.VerifiableCredential{ + {ID: &secondVCID}, + }, + Proof: []interface{}{ + proof.LDProof{VerificationMethod: vcID}, + }, + } + definition := PresentationDefinition{ + InputDescriptors: []*InputDescriptor{ + { + Id: "1", + Constraints: &Constraints{ + Fields: []Field{ + { + Path: []string{"$.id"}, + Filter: &Filter{ + Type: "string", + Const: &constant1, + }, + }, + }, + }, + }, + { + Id: "2", + Constraints: &Constraints{ + Fields: []Field{ + { + Path: []string{"$.id"}, + Filter: &Filter{ + Type: "string", + Const: &constant2, + }, + }, + }, + }, + }, + }, + } + submission := PresentationSubmission{ + DescriptorMap: []InputDescriptorMappingObject{ + { + Id: "1", + Path: "$[0]", + PathNested: &InputDescriptorMappingObject{ + Id: "1", + Path: "$.verifiableCredential[0]", + Format: "ldp_vc", + }, + }, + { + Id: "2", + Path: "$[0]", + PathNested: &InputDescriptorMappingObject{ + Id: "2", + Path: "$.verifiableCredential[0]", + Format: "ldp_vc", + }, + }, + }, + } + + credentials, err := submission.Validate([]vc.VerifiablePresentation{vp, secondVP}, definition) + + require.NoError(t, err) + require.Len(t, credentials, 2) + assert.Equal(t, vcID.String(), credentials["1"].ID.String()) + assert.Equal(t, secondVCID.String(), credentials["2"].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, + }, + }, + }, + }, + }, + }, + } + + credentials, err := PresentationSubmission{}.Validate([]vc.VerifiablePresentation{vp}, definition) + + assert.EqualError(t, err, "credential submission is invalid, credentials do 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]", + Format: "ldp_vc", + }, + }, + } + + credentials, err := submission.Validate([]vc.VerifiablePresentation{vp}, definition) + + assert.EqualError(t, err, "credential submission is invalid, input descriptor mapping looks invalid") + assert.Empty(t, credentials) + }) +}