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

generate presentation submission with path_nested #2563

Merged
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 18 additions & 25 deletions auth/api/iam/openid4vp.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/vcr/credential"
"github.com/nuts-foundation/nuts-node/vcr/holder"
"github.com/nuts-foundation/nuts-node/vcr/pe"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -137,22 +136,18 @@ func (r *Wrapper) handlePresentationRequest(params map[string]string, session *S
}
}

// TODO: https://github.com/nuts-foundation/nuts-node/issues/2359
presentationContext := pe.PresentationContext{
Index: 0,
PresentationSubmission: &pe.PresentationSubmission{
Id: uuid.NewString(),
DefinitionId: presentationDefinition.Id,
},
}
matchingCredentials, err := presentationDefinition.Match(presentationContext, credentials)
submissionBuilder := presentationDefinition.Builder()
submissionBuilder.AddWallet(session.OwnDID, ownCredentials)
_, signInstructions, err := submissionBuilder.Build("ldp_vp")
if err != nil {
return nil, fmt.Errorf("unable to match presentation definition: %w", err)
}
var credentialIDs []string
for _, matchingCredential := range matchingCredentials {
templateParams.Credentials = append(templateParams.Credentials, makeCredentialInfo(matchingCredential))
credentialIDs = append(credentialIDs, matchingCredential.ID.String())
for _, signInstruction := range signInstructions {
for _, matchingCredential := range signInstruction.VerifiableCredentials {
templateParams.Credentials = append(templateParams.Credentials, makeCredentialInfo(matchingCredential))
credentialIDs = append(credentialIDs, matchingCredential.ID.String())
}
}
session.ServerState["openid4vp_credentials"] = credentialIDs

Expand Down Expand Up @@ -211,23 +206,21 @@ func (r *Wrapper) handlePresentationRequestAccept(c echo.Context) error {
if presentationDefinition == nil {
return fmt.Errorf("unsupported scope for presentation exchange: %s", session.Scope)
}
// TODO: Options
// TODO: Options (including format)
resultParams := map[string]string{}
presentationContext := pe.PresentationContext{
Index: 0,
PresentationSubmission: &pe.PresentationSubmission{
Id: uuid.NewString(),
DefinitionId: presentationDefinition.Id,
},
}
credentials, err = presentationDefinition.Match(presentationContext, credentials)
submissionBuilder := presentationDefinition.Builder()
submissionBuilder.AddWallet(session.OwnDID, credentials)
submission, signInstructions, err := submissionBuilder.Build("ldp_vp")
if err != nil {
// Matched earlier, shouldn't happen
return err
}
presentationSubmissionJSON, _ := json.Marshal(presentationContext.PresentationSubmission)
presentationSubmissionJSON, _ := json.Marshal(submission)
resultParams[presentationSubmissionParam] = string(presentationSubmissionJSON)
verifiablePresentation, err := r.vcr.Wallet().BuildPresentation(c.Request().Context(), credentials, holder.PresentationOptions{}, &session.OwnDID, false)
if len(signInstructions) != 1 {
// todo support multiple wallets (org + user)
return errors.New("expected to create exactly one presentation")
}
verifiablePresentation, err := r.vcr.Wallet().BuildPresentation(c.Request().Context(), signInstructions[0].VerifiableCredentials, holder.PresentationOptions{}, &signInstructions[0].Holder, false)
if err != nil {
return err
}
Expand Down
42 changes: 14 additions & 28 deletions vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,39 +57,24 @@ type PresentationContext struct {
// It assumes this method is used for OpenID4VP since other envelopes require different nesting.
// ErrUnsupportedFilter is returned when a filter uses unsupported features.
// Other errors can be returned for faulty JSON paths or regex patterns.
func (presentationDefinition PresentationDefinition) Match(context PresentationContext, vcs []vc.VerifiableCredential) ([]vc.VerifiableCredential, error) {
if context.PresentationSubmission == nil {
return nil, errors.New("presentationContext.PresentationSubmission must be set")
}
func (presentationDefinition PresentationDefinition) Match(vcs []vc.VerifiableCredential) ([]vc.VerifiableCredential, []InputDescriptorMappingObject, error) {
var selectedVCs []vc.VerifiableCredential
var descriptorMaps []InputDescriptorMappingObject
var err error
if len(presentationDefinition.SubmissionRequirements) > 0 {
if descriptorMaps, selectedVCs, err = presentationDefinition.matchSubmissionRequirements(vcs); err != nil {
return nil, err
return nil, nil, err
}
} else if descriptorMaps, selectedVCs, err = presentationDefinition.matchBasic(vcs); err != nil {
return nil, err
}

// wrap each InputDescriptorMappingObject for the outer VP
for _, descriptorMap := range descriptorMaps {
nestedDescriptorMap := InputDescriptorMappingObject{
Id: descriptorMap.Id,
Format: "ldp_vp", // todo support jwt_vp
Path: fmt.Sprintf("$[%d]", context.Index), // todo is this path correct for the outer envelope?
PathNested: []InputDescriptorMappingObject{
descriptorMap,
},
}
context.PresentationSubmission.DescriptorMap = append(context.PresentationSubmission.DescriptorMap, nestedDescriptorMap)
return nil, nil, err
}

return selectedVCs, nil
return selectedVCs, descriptorMaps, nil
}

func (presentationDefinition PresentationDefinition) matchConstraints(vcs []vc.VerifiableCredential) ([]Candidate, error) {
var candidates []Candidate

for _, inputDescriptor := range presentationDefinition.InputDescriptors {
match := Candidate{
InputDescriptor: *inputDescriptor,
Expand All @@ -106,6 +91,7 @@ func (presentationDefinition PresentationDefinition) matchConstraints(vcs []vc.V
}
candidates = append(candidates, match)
}

return candidates, nil
}

Expand All @@ -115,24 +101,24 @@ func (presentationDefinition PresentationDefinition) matchBasic(vcs []vc.Verifia
// for each constraint in descriptor.constraints:
// for each field in constraint.fields:
// a vc must match the field
matches, err := presentationDefinition.matchConstraints(vcs)
candidates, err := presentationDefinition.matchConstraints(vcs)
if err != nil {
return nil, nil, err
}
var index int
matchingCredentials := make([]vc.VerifiableCredential, len(matches))
matchingCredentials := make([]vc.VerifiableCredential, len(candidates))
var descriptors []InputDescriptorMappingObject
for _, match := range matches {
if match.VC == nil {
var index int
for i, candidate := range candidates {
if candidate.VC == nil {
return nil, []vc.VerifiableCredential{}, nil
}
mapping := InputDescriptorMappingObject{
Id: match.InputDescriptor.Id,
Format: match.VC.Format(),
Id: candidate.InputDescriptor.Id,
Format: candidate.VC.Format(),
Path: fmt.Sprintf("$.verifiableCredential[%d]", index),
}
descriptors = append(descriptors, mapping)
matchingCredentials[index] = *match.VC
matchingCredentials[i] = *candidate.VC
index++
}

Expand Down
73 changes: 28 additions & 45 deletions vcr/pe/presentation_definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,63 +109,38 @@ func TestMatch(t *testing.T) {
_ = json.Unmarshal(vcJSON, &verifiableCredential)

t.Run("Happy flow", func(t *testing.T) {
context := PresentationContext{
Index: 0,
PresentationSubmission: &PresentationSubmission{},
}
vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{verifiableCredential})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{verifiableCredential})

require.NoError(t, err)
assert.Len(t, vcs, 1)
require.Len(t, context.PresentationSubmission.DescriptorMap, 1)
assert.Equal(t, "$[0]", context.PresentationSubmission.DescriptorMap[0].Path)
require.Len(t, context.PresentationSubmission.DescriptorMap[0].PathNested, 1)
assert.Equal(t, "$.verifiableCredential[0]", context.PresentationSubmission.DescriptorMap[0].PathNested[0].Path)
require.Len(t, mappingObjects, 1)
assert.Equal(t, "$.verifiableCredential[0]", mappingObjects[0].Path)
})
t.Run("Only second VC matches", func(t *testing.T) {
context := PresentationContext{
Index: 0,
PresentationSubmission: &PresentationSubmission{},
}
vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{{Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential")}}, verifiableCredential})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{{Type: []ssi.URI{ssi.MustParseURI("VerifiableCredential")}}, verifiableCredential})

require.NoError(t, err)
assert.Len(t, vcs, 1)
})
t.Run("Correct index on envelope", func(t *testing.T) {
context := PresentationContext{
Index: 1,
PresentationSubmission: &PresentationSubmission{},
}
vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{verifiableCredential})

require.NoError(t, err)
assert.Len(t, vcs, 1)
require.Len(t, context.PresentationSubmission.DescriptorMap, 1)
assert.Equal(t, "$[1]", context.PresentationSubmission.DescriptorMap[0].Path)
assert.Len(t, mappingObjects, 1)
})
})
t.Run("Submission requirement feature", func(t *testing.T) {
context := PresentationContext{
Index: 0,
PresentationSubmission: &PresentationSubmission{},
}

t.Run("Pick", func(t *testing.T) {
t.Run("Pick 1", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickOne), &presentationDefinition)

vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 1)
assert.Len(t, mappingObjects, 1)
})
t.Run("error", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickOne), &presentationDefinition)

_, err := presentationDefinition.Match(context, []vc.VerifiableCredential{})
_, _, err := presentationDefinition.Match([]vc.VerifiableCredential{})

require.Error(t, err)
assert.EqualError(t, err, "submission requirement (Pick 1 matcher) has less credentials (0) than required (1)")
Expand All @@ -176,16 +151,17 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickMinMax), &presentationDefinition)

vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.Len(t, mappingObjects, 2)
})
t.Run("error", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickMinMax), &presentationDefinition)

_, err := presentationDefinition.Match(context, []vc.VerifiableCredential{})
_, _, err := presentationDefinition.Match([]vc.VerifiableCredential{})

require.Error(t, err)
assert.EqualError(t, err, "submission requirement (Pick 1 matcher) has less matches (0) than minimal required (1)")
Expand All @@ -195,49 +171,54 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickOnePerGroup), &presentationDefinition)

vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.Len(t, mappingObjects, 2)
})
t.Run("Pick all", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.All), &presentationDefinition)

vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.Len(t, mappingObjects, 2)
})
t.Run("Deduplicate", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.DeduplicationRequired), &presentationDefinition)

vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1})

require.NoError(t, err)
assert.Len(t, vcs, 1)
assert.Len(t, mappingObjects, 1)
})
t.Run("Pick 1 from nested", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickOneFromNested), &presentationDefinition)

t.Run("all from group A or all from group B", func(t *testing.T) {
t.Run("all A", func(t *testing.T) {
vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.Len(t, mappingObjects, 2)
})
t.Run("all B", func(t *testing.T) {
vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc3, vc4})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc3, vc4})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.Len(t, mappingObjects, 2)
assert.Equal(t, "3", vcs[0].ID.String())
})
t.Run("no match", func(t *testing.T) {
vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc3})
vcs, _, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc3})

require.Error(t, err)
assert.Len(t, vcs, 0)
Expand All @@ -249,16 +230,17 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.AllFromNested), &presentationDefinition)

vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc2})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.Len(t, mappingObjects, 2)
})
t.Run("error", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.AllFromNested), &presentationDefinition)

_, err := presentationDefinition.Match(context, []vc.VerifiableCredential{})
_, _, err := presentationDefinition.Match([]vc.VerifiableCredential{})

require.Error(t, err)
assert.EqualError(t, err, "submission requirement (All from nested) does not have all credentials from the group")
Expand All @@ -269,16 +251,17 @@ func TestMatch(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickMinMaxFromNested), &presentationDefinition)

vcs, err := presentationDefinition.Match(context, []vc.VerifiableCredential{vc1, vc2, vc3})
vcs, mappingObjects, err := presentationDefinition.Match([]vc.VerifiableCredential{vc1, vc2, vc3})

require.NoError(t, err)
assert.Len(t, vcs, 2)
assert.Len(t, mappingObjects, 2)
})
t.Run("error", func(t *testing.T) {
presentationDefinition := PresentationDefinition{}
_ = json.Unmarshal([]byte(test.PickMinMaxFromNested), &presentationDefinition)

_, err := presentationDefinition.Match(context, []vc.VerifiableCredential{})
_, _, err := presentationDefinition.Match([]vc.VerifiableCredential{})

require.Error(t, err)
assert.EqualError(t, err, "submission requirement (Pick 1 matcher) has less matches (0) than minimal required (1)")
Expand Down
Loading
Loading