diff --git a/e2e-tests/oauth-flow/rfc021/node-A/nuts.yaml b/e2e-tests/oauth-flow/rfc021/node-A/nuts.yaml index 28558468f..32ed85518 100644 --- a/e2e-tests/oauth-flow/rfc021/node-A/nuts.yaml +++ b/e2e-tests/oauth-flow/rfc021/node-A/nuts.yaml @@ -22,7 +22,7 @@ discovery: directory: /nuts/discovery server: ids: e2e-test -vdr: - didmethods: - - web +#vdr: +# didmethods: +# - web diff --git a/e2e-tests/oauth-flow/rfc021/node-B/nuts.yaml b/e2e-tests/oauth-flow/rfc021/node-B/nuts.yaml index d4cf72c7b..fb78e0ee5 100644 --- a/e2e-tests/oauth-flow/rfc021/node-B/nuts.yaml +++ b/e2e-tests/oauth-flow/rfc021/node-B/nuts.yaml @@ -19,6 +19,6 @@ tls: truststorefile: /opt/nuts/truststore.pem certfile: /opt/nuts/certificate-and-key.pem certkeyfile: /opt/nuts/certificate-and-key.pem -vdr: - didmethods: - - web +#vdr: +# didmethods: +# - web diff --git a/vcr/holder/presenter.go b/vcr/holder/presenter.go index 397470835..182f434d4 100644 --- a/vcr/holder/presenter.go +++ b/vcr/holder/presenter.go @@ -72,31 +72,10 @@ func (p presenter) buildSubmission(ctx context.Context, credentials map[did.DID] if format == "" { return nil, nil, errors.New("requester, verifier (authorization server metadata) and presentation definition don't share a supported VP format") } - presentationSubmission, signInstructions, err := builder.Build(format) + presentationSubmission, signInstruction, err := builder.Build(format) if err != nil { - return nil, nil, fmt.Errorf("failed to build presentation submission: %w", err) - } - if signInstructions.Empty() { - // add empty sign instruction - // TODO: If the verifier doesn't require any credentials, it also can't signal which DID methods it supports/requires? - var holderDID did.DID - for holder := range credentials { - holderDID = holder - break - } - signInstructions = append(signInstructions, pe.SignInstruction{Holder: holderDID}) - presentationSubmission = pe.PresentationSubmission{ - Id: uuid.NewString(), - DefinitionId: presentationDefinition.Id, - DescriptorMap: make([]pe.InputDescriptorMappingObject, 0), - } - } - - if len(signInstructions) > 1 { - // todo: support multiple wallets - return nil, nil, errors.New("multiple sign instructions not supported") + return nil, nil, err } - signInstruction := signInstructions[0] holderDID := signInstruction.Holder.URI() vp, err := p.buildPresentation(ctx, &signInstruction.Holder, signInstruction.VerifiableCredentials, PresentationOptions{ diff --git a/vcr/pe/presentation_submission.go b/vcr/pe/presentation_submission.go index 5ef11e9d0..1d78ed137 100644 --- a/vcr/pe/presentation_submission.go +++ b/vcr/pe/presentation_submission.go @@ -100,88 +100,54 @@ func (signInstructions SignInstructions) Empty() bool { // Build creates a PresentationSubmission from the added wallets. // The VP format is determined by the given format. -func (b *PresentationSubmissionBuilder) Build(format string) (PresentationSubmission, SignInstructions, error) { - presentationSubmission := PresentationSubmission{ - Id: uuid.New().String(), - DefinitionId: b.presentationDefinition.Id, - } +func (b *PresentationSubmissionBuilder) Build(format string) (PresentationSubmission, SignInstruction, error) { + // we try to match per wallet + var loopErrs []error + var selectedVCs []vc.VerifiableCredential + var inputDescriptorMappingObjects []InputDescriptorMappingObject + var selectedDID *did.DID - // first we need to select the VCs from all wallets that match the presentation definition - allVCs := make([]vc.VerifiableCredential, 0) - for _, vcs := range b.wallets { - allVCs = append(allVCs, vcs...) - } - - selectedVCs, inputDescriptorMappingObjects, err := b.presentationDefinition.Match(allVCs) - if err != nil { - return presentationSubmission, nil, fmt.Errorf("failed to match presentation definition: %w", err) + for i, walletVCs := range b.wallets { + vcs, mappingObjects, err := b.presentationDefinition.Match(walletVCs) + if err == nil { + selectedVCs = vcs + inputDescriptorMappingObjects = mappingObjects + selectedDID = &b.holders[i] + break + } + loopErrs = append(loopErrs, fmt.Errorf("failed to match presentation definition for %s: %w", b.holders[i].String(), err)) } - // next we need to map the selected VCs to the correct wallet - // loop over all selected VCs and find the wallet that contains the VC - signInstructions := make([]SignInstruction, len(b.wallets)) - walletCredentialIndex := map[did.DID]int{} - for j := range selectedVCs { - for i, walletVCs := range b.wallets { - for _, walletVC := range walletVCs { - // do a JSON equality check - if selectedVCs[j].Raw() == walletVC.Raw() { - signInstructions[i].Holder = b.holders[i] - signInstructions[i].VerifiableCredentials = append(signInstructions[i].VerifiableCredentials, selectedVCs[j]) - // remap the path to the correct wallet index - mapping := inputDescriptorMappingObjects[j] - mapping.Format = selectedVCs[j].Format() - mapping.Path = fmt.Sprintf("$.verifiableCredential[%d]", walletCredentialIndex[b.holders[i]]) - signInstructions[i].Mappings = append(signInstructions[i].Mappings, mapping) - walletCredentialIndex[b.holders[i]]++ - } - } + if selectedDID == nil { + if b.presentationDefinition.CredentialsRequired() { + return PresentationSubmission{}, SignInstruction{}, errors.Join(loopErrs...) } + // add empty sign instruction + return PresentationSubmission{Id: uuid.New().String(), DefinitionId: b.presentationDefinition.Id}, SignInstruction{Holder: b.holders[0]}, nil } - // filter out empty sign instructions - nonEmptySignInstructions := make([]SignInstruction, 0) - for _, signInstruction := range signInstructions { - if !signInstruction.Empty() { - nonEmptySignInstructions = append(nonEmptySignInstructions, signInstruction) - } + signInstruction := SignInstruction{ + Holder: *selectedDID, + VerifiableCredentials: selectedVCs, + Mappings: inputDescriptorMappingObjects, } // the verifiableCredential property in Verifiable Presentations can be a single VC or an array of VCs when represented in JSON. // go-did always marshals a single VC as a single VC for JSON-LD VPs. So we might need to fix the mapping paths. // todo the check below actually depends on the format of the credential and not the format of the VP - for _, signInstruction := range nonEmptySignInstructions { - if len(signInstruction.Mappings) == 1 { - signInstruction.Mappings[0].Path = "$.verifiableCredential" - } + if len(signInstruction.Mappings) == 1 { + signInstruction.Mappings[0].Path = "$.verifiableCredential" } - index := 0 - // last we create the descriptor map for the presentation submission - // If there's only one sign instruction the Path will be $. - // If there are multiple sign instructions (each yielding a VP) the Path will be $[0], $[1], etc. - for _, signInstruction := range nonEmptySignInstructions { - if len(signInstruction.Mappings) > 0 { - for _, inputDescriptorMapping := range signInstruction.Mappings { - // If we have multiple VPs in the resulting submission, wrap each in a nested descriptor map (see path_nested in PEX specification). - if len(nonEmptySignInstructions) > 1 { - presentationSubmission.DescriptorMap = append(presentationSubmission.DescriptorMap, InputDescriptorMappingObject{ - Id: inputDescriptorMapping.Id, - Format: format, - Path: fmt.Sprintf("$[%d]", index), - PathNested: &inputDescriptorMapping, - }) - } else { - // Just 1 VP, no nesting needed - presentationSubmission.DescriptorMap = append(presentationSubmission.DescriptorMap, inputDescriptorMapping) - } - } - index++ - } + // Just 1 VP, no nesting needed + presentationSubmission := PresentationSubmission{ + Id: uuid.New().String(), + DefinitionId: b.presentationDefinition.Id, + DescriptorMap: inputDescriptorMappingObjects, } - return presentationSubmission, nonEmptySignInstructions, nil + return presentationSubmission, signInstruction, nil } // Resolve returns a map where each of the input descriptors is mapped to the corresponding VerifiableCredential. @@ -264,9 +230,16 @@ func (s PresentationSubmission) Validate(envelope Envelope, definition Presentat if err != nil { return nil, fmt.Errorf("resolve credentials from presentation submission: %w", err) } + if len(envelope.Presentations) == 0 { + if definition.CredentialsRequired() { + return nil, errors.New("presentation submission doesn't match presentation definition") + } + // empty is OK. No need to Build + return map[string]vc.VerifiableCredential{}, nil + } - // Create a new presentation submission: the submission being validated should have the same input descriptor mapping. submissionBuilder := definition.PresentationSubmissionBuilder() + // Create a new presentation submission: the submission being validated should have the same input descriptor mapping. for _, presentation := range envelope.Presentations { signer, err := credential.PresentationSigner(presentation) if err != nil { @@ -274,19 +247,14 @@ func (s PresentationSubmission) Validate(envelope Envelope, definition Presentat } submissionBuilder.AddWallet(*signer, presentation.VerifiableCredential) } - _, signInstructions, err := submissionBuilder.Build("") + _, signInstruction, err := submissionBuilder.Build("") if err != nil { return nil, err } - if len(signInstructions) == 0 && definition.CredentialsRequired() { - return nil, errors.New("presentation submission doesn't match presentation definition") - } // Build a input descriptor -> credential map for comparison expectedCredentials := make(map[string]vc.VerifiableCredential) - for _, signInstruction := range signInstructions { - for i, mapping := range signInstruction.Mappings { - expectedCredentials[mapping.Id] = signInstruction.VerifiableCredentials[i] - } + for i, mapping := range signInstruction.Mappings { + expectedCredentials[mapping.Id] = signInstruction.VerifiableCredentials[i] } if len(actualCredentials) != len(expectedCredentials) { return nil, fmt.Errorf("expected %d credentials, got %d", len(expectedCredentials), len(actualCredentials)) diff --git a/vcr/pe/presentation_submission_test.go b/vcr/pe/presentation_submission_test.go index 22f08f635..b34ae8491 100644 --- a/vcr/pe/presentation_submission_test.go +++ b/vcr/pe/presentation_submission_test.go @@ -55,6 +55,28 @@ func TestPresentationSubmissionBuilder_Build(t *testing.T) { t.Run("ldp_vp", func(t *testing.T) { + t.Run("1 presentation without credentials", func(t *testing.T) { + expectedJSON := ` + { + "id": "for-test", + "definition_id": "empty" + }` + presentationDefinition := PresentationDefinition{} + _ = json.Unmarshal([]byte(test.Empty), &presentationDefinition) + builder := presentationDefinition.PresentationSubmissionBuilder() + builder.AddWallet(holder1, []vc.VerifiableCredential{}) + + submission, signInstruction, err := builder.Build("ldp_vp") + + require.NoError(t, err) + assert.Len(t, signInstruction.VerifiableCredentials, 0) + require.Len(t, submission.DescriptorMap, 0) + + submission.Id = "for-test" // easier assertion + actualJSON, _ := json.MarshalIndent(submission, "", " ") + assert.JSONEq(t, expectedJSON, string(actualJSON)) + }) + t.Run("1 presentation with 1 credential", func(t *testing.T) { expectedJSON := ` { @@ -73,11 +95,10 @@ func TestPresentationSubmissionBuilder_Build(t *testing.T) { builder := presentationDefinition.PresentationSubmissionBuilder() builder.AddWallet(holder1, []vc.VerifiableCredential{vc1, vc2}) - submission, signInstructions, err := builder.Build("ldp_vp") + submission, signInstruction, err := builder.Build("ldp_vp") require.NoError(t, err) - require.NotNil(t, signInstructions) - assert.Len(t, signInstructions, 1) + assert.Len(t, signInstruction.VerifiableCredentials, 1) require.Len(t, submission.DescriptorMap, 1) assert.Equal(t, "$.verifiableCredential", submission.DescriptorMap[0].Path) @@ -109,11 +130,10 @@ func TestPresentationSubmissionBuilder_Build(t *testing.T) { builder := presentationDefinition.PresentationSubmissionBuilder() builder.AddWallet(holder1, []vc.VerifiableCredential{vc1, vc2}) - submission, signInstructions, err := builder.Build("ldp_vp") + submission, signInstruction, err := builder.Build("ldp_vp") require.NoError(t, err) - require.NotNil(t, signInstructions) - assert.Len(t, signInstructions, 1) + assert.Len(t, signInstruction.VerifiableCredentials, 2) require.Len(t, submission.DescriptorMap, 2) submission.Id = "for-test" // easier assertion @@ -121,52 +141,6 @@ func TestPresentationSubmissionBuilder_Build(t *testing.T) { println(string(actualJSON)) assert.JSONEq(t, expectedJSON, string(actualJSON)) }) - t.Run("2 presentations", func(t *testing.T) { - expectedJSON := ` -{ - "id": "for-test", - "definition_id": "", - "descriptor_map": [ - { - "format": "ldp_vp", - "id": "Match ID=1", - "path": "$[0]", - "path_nested": { - "format": "ldp_vc", - "id": "Match ID=1", - "path": "$.verifiableCredential" - } - }, - { - "format": "ldp_vp", - "id": "Match ID=2", - "path": "$[1]", - "path_nested": { - "format": "ldp_vc", - "id": "Match ID=2", - "path": "$.verifiableCredential" - } - } - ] -} -` - presentationDefinition := PresentationDefinition{} - _ = json.Unmarshal([]byte(test.All), &presentationDefinition) - builder := presentationDefinition.PresentationSubmissionBuilder() - builder.AddWallet(holder1, []vc.VerifiableCredential{vc1}) - builder.AddWallet(holder2, []vc.VerifiableCredential{vc2}) - - submission, signInstructions, err := builder.Build("ldp_vp") - - require.NoError(t, err) - require.NotNil(t, signInstructions) - assert.Len(t, signInstructions, 2) - assert.Len(t, submission.DescriptorMap, 2) - - submission.Id = "for-test" // easier assertion - actualJSON, _ := json.MarshalIndent(submission, "", " ") - assert.JSONEq(t, expectedJSON, string(actualJSON)) - }) t.Run("2 wallets, but 1 VP", func(t *testing.T) { expectedJSON := ` { @@ -191,11 +165,10 @@ func TestPresentationSubmissionBuilder_Build(t *testing.T) { builder.AddWallet(holder1, []vc.VerifiableCredential{vc1, vc2}) builder.AddWallet(holder2, []vc.VerifiableCredential{vc3}) - submission, signInstructions, err := builder.Build("ldp_vp") + submission, signInstruction, err := builder.Build("ldp_vp") require.NoError(t, err) - require.NotNil(t, signInstructions) - assert.Len(t, signInstructions, 1) + assert.Len(t, signInstruction.VerifiableCredentials, 2) assert.Len(t, submission.DescriptorMap, 2) submission.Id = "for-test" // easier assertion @@ -210,11 +183,11 @@ func TestPresentationSubmissionBuilder_Build(t *testing.T) { builder := presentationDefinition.PresentationSubmissionBuilder() builder.AddWallet(holder1, []vc.VerifiableCredential{vc1, vc2}) - submission, signInstructions, err := builder.Build("jwt_vp") + submission, signInstruction, err := builder.Build("jwt_vp") require.NoError(t, err) - require.NotNil(t, signInstructions) - assert.Len(t, signInstructions, 1) + assert.Len(t, signInstruction.VerifiableCredentials, 1) + assert.Equal(t, holder1, signInstruction.Holder) require.Len(t, submission.DescriptorMap, 1) assert.Equal(t, "$.verifiableCredential", submission.DescriptorMap[0].Path) }) @@ -491,82 +464,6 @@ func TestPresentationSubmission_Validate(t *testing.T) { 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]", - Format: "ldp_vp", - PathNested: &InputDescriptorMappingObject{ - Id: "1", - Path: "$.verifiableCredential", - Format: "ldp_vc", - }, - }, - { - Id: "2", - Path: "$[1]", - Format: "ldp_vp", - PathNested: &InputDescriptorMappingObject{ - Id: "2", - Path: "$.verifiableCredential", - Format: "ldp_vc", - }, - }, - }, - } - - credentials, err := submission.Validate(toEnvelope(t, []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("submission mappings don't match definition input descriptors", func(t *testing.T) { constant := "incorrect ID" definition := PresentationDefinition{ @@ -590,7 +487,7 @@ func TestPresentationSubmission_Validate(t *testing.T) { credentials, err := PresentationSubmission{}.Validate(toEnvelope(t, []vc.VerifiablePresentation{vp}), definition) - assert.EqualError(t, err, "failed to match presentation definition: missing credentials\nconstraints not matched: no VC for InputDescriptor (1)") + assert.EqualError(t, err, "failed to match presentation definition for did:example:123: missing credentials\nconstraints not matched: no VC for InputDescriptor (1)") assert.Empty(t, credentials) }) t.Run("credentials match wrong input descriptors", func(t *testing.T) { diff --git a/vcr/pe/test/test_definitions.go b/vcr/pe/test/test_definitions.go index 587a3d9e5..214df2771 100644 --- a/vcr/pe/test/test_definitions.go +++ b/vcr/pe/test/test_definitions.go @@ -18,6 +18,59 @@ package test +const Empty = ` +{ + "id": "empty", + "submission_requirements": [ + { + "name": "Pick 0 matcher", + "rule": "pick", + "min": 0, + "max": 1, + "from": "A" + } + ], + "input_descriptors": [ + { + "id": "Match ID=1", + "name": "Pick 1", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": [ + "$.id" + ], + "filter": { + "type": "string", + "const": "1" + } + } + ] + } + }, + { + "id": "Match ID=2", + "name": "Pick 2", + "group": ["A"], + "constraints": { + "fields": [ + { + "path": [ + "$.id" + ], + "filter": { + "type": "string", + "const": "2" + } + } + ] + } + } + ] +} +` + const PickOne = ` { "submission_requirements": [ diff --git a/vcr/pe/types.go b/vcr/pe/types.go index c6efa68fc..6224a4deb 100644 --- a/vcr/pe/types.go +++ b/vcr/pe/types.go @@ -33,7 +33,7 @@ type PresentationSubmission struct { // DefinitionId is the id of the presentation definition that this submission is for DefinitionId string `json:"definition_id"` // DescriptorMap is a list of mappings from input descriptors to VCs - DescriptorMap []InputDescriptorMappingObject `json:"descriptor_map"` + DescriptorMap []InputDescriptorMappingObject `json:"descriptor_map,omitempty"` } // InputDescriptorMappingObject