Skip to content
This repository has been archived by the owner on Mar 27, 2024. It is now read-only.

feat: presentation definition can match array of presentations #3538

Merged
merged 1 commit into from
Mar 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
29 changes: 15 additions & 14 deletions pkg/doc/cm/credentialapplication.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ func UnmarshalAndValidateAgainstCredentialManifest(credentialApplicationBytes []
// ValidateCredentialApplication validates credential application presentation by validating
// the embedded Credential Application object against the given Credential Manifest.
// There are 3 requirements for the Credential Application to be valid against the Credential Manifest:
// 1. Credential Application's manifest ID must match the Credential Manifest's ID.
// 2. If the Credential Manifest has a format property, the Credential Application must also have a
// format property which is a subset of the Credential Manifest's.
// 3. If the Credential Manifest contains a presentation_definition property, the Credential Application
// must have a matching presentation_submission property.
// 1. Credential Application's manifest ID must match the Credential Manifest's ID.
// 2. If the Credential Manifest has a format property, the Credential Application must also have a
// format property which is a subset of the Credential Manifest's.
// 3. If the Credential Manifest contains a presentation_definition property, the Credential Application
// must have a matching presentation_submission property.
//
// Proof of all individual credentials can also be validated by using options.
// Refer to https://identity.foundation/credential-manifest/#credential-application for more info.
func ValidateCredentialApplication(application *verifiable.Presentation, cm *CredentialManifest,
Expand Down Expand Up @@ -111,7 +112,7 @@ func ValidateCredentialApplication(application *verifiable.Presentation, cm *Cre
return nil
}

_, err = cm.PresentationDefinition.Match(application, contextLoader, options...)
_, err = cm.PresentationDefinition.Match([]*verifiable.Presentation{application}, contextLoader, options...)

return err
}
Expand Down Expand Up @@ -260,14 +261,14 @@ func WithExistingPresentationForPresentCredentialApplication(
// "https://identity.foundation/presentation-exchange/submission/v1" context is found, it will be replaced with
// the "https://identity.foundation/credential-manifest/application/v1" context. Note that any existing proofs are
// not updated. Note also the following assumptions/limitations of this method:
// 1. The format of all claims in the Presentation Submission are assumed to be ldp_vp and will be set as such.
// 2. The format for the Credential Application object will be set to match the format from the Credential Manifest
// exactly. If a caller wants to use a smaller subset of the Credential Manifest's format, then they will have to
// set it manually.
// 3. The location of the Verifiable Credentials is assumed to be an array at the root under a field called
// "verifiableCredential".
// 4. The Verifiable Credentials in the presentation is assumed to be in the same order as the Output Descriptors in
// the Credential Manifest.
// 1. The format of all claims in the Presentation Submission are assumed to be ldp_vp and will be set as such.
// 2. The format for the Credential Application object will be set to match the format from the Credential Manifest
// exactly. If a caller wants to use a smaller subset of the Credential Manifest's format, then they will have to
// set it manually.
// 3. The location of the Verifiable Credentials is assumed to be an array at the root under a field called
// "verifiableCredential".
// 4. The Verifiable Credentials in the presentation is assumed to be in the same order as the Output Descriptors in
// the Credential Manifest.
func PresentCredentialApplication(credentialManifest *CredentialManifest,
opts ...PresentCredentialApplicationOpt) (*verifiable.Presentation, error) {
if credentialManifest == nil {
Expand Down
220 changes: 176 additions & 44 deletions pkg/doc/presexch/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
Expand Down Expand Up @@ -52,10 +54,18 @@ type InputDescriptorMapping struct {
PathNested *InputDescriptorMapping `json:"path_nested,omitempty"`
}

// MatchValue holds a matched credential from PresentationDefinition.Match, along with the ID of the
// presentation that held the matched credential.
type MatchValue struct {
PresentationID string
Credential *verifiable.Credential
}

// MatchOptions is a holder of options that can set when matching a submission against definitions.
type MatchOptions struct {
CredentialOptions []verifiable.CredentialOpt
DisableSchemaValidation bool
MergedSubmission *PresentationSubmission
}

// MatchOption is an option that sets an option for when matching.
Expand All @@ -75,73 +85,132 @@ func WithDisableSchemaValidation() MatchOption {
}
}

// WithMergedSubmission provides a presentation submission that's external to the Presentations being matched,
// which contains the descriptor mapping for each Presentation.
//
// If there are multiple Presentations, this merged submission should use the Presentation array as the JSON Path root
// when referencing the contained Presentations and the Credentials within.
func WithMergedSubmission(submission *PresentationSubmission) MatchOption {
return func(m *MatchOptions) {
m.MergedSubmission = submission
}
}

// Match returns the credentials matched against the InputDescriptors ids.
func (pd *PresentationDefinition) Match(vp *verifiable.Presentation, // nolint:gocyclo,funlen
contextLoader ld.DocumentLoader, options ...MatchOption) (map[string]*verifiable.Credential, error) {
func (pd *PresentationDefinition) Match(vpList []*verifiable.Presentation,
contextLoader ld.DocumentLoader, options ...MatchOption) (map[string]MatchValue, error) {
opts := &MatchOptions{}

for i := range options {
options[i](opts)
}

err := checkJSONLDContextType(vp)
result, err := getMatchedCreds(pd, vpList, contextLoader, opts)
if err != nil {
return nil, err
}

vpBits, err := vp.MarshalJSON()
err = pd.evalSubmissionRequirements(result)
if err != nil {
return nil, fmt.Errorf("failed to marshal vp: %w", err)
return nil, fmt.Errorf("failed submission requirements: %w", err)
}

typelessVP := interface{}(nil)
return result, nil
}

err = json.Unmarshal(vpBits, &typelessVP)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal vp: %w", err)
}
func getMatchedCreds( //nolint:gocyclo,funlen
pd *PresentationDefinition,
vpList []*verifiable.Presentation,
contextLoader ld.DocumentLoader,
opts *MatchOptions,
) (map[string]MatchValue, error) {
result := make(map[string]MatchValue)

descriptorIDs := descriptorIDs(pd.InputDescriptors)

descriptorMap, err := parseDescriptorMap(vp)
if err != nil {
return nil, fmt.Errorf("failed to parse descriptor map: %w", err)
useMergedSubmission := opts.MergedSubmission != nil

var mappingsByVPIndex map[int][]*InputDescriptorMapping

if useMergedSubmission {
mappingsByVPIndex = descriptorsByPresentationIndex(opts.MergedSubmission.DescriptorMap)
}

result := make(map[string]*verifiable.Credential)
rawVPs := make([]interface{}, len(vpList))

for i := range descriptorMap {
mapping := descriptorMap[i]
// The object MUST include an id property, and its value MUST be a string matching the id property of
// the Input Descriptor in the Presentation Definition the submission is related to.
if !stringsContain(descriptorIDs, mapping.ID) {
return nil, fmt.Errorf(
"an %s ID was found that did not match the `id` property of any input descriptor: %s",
descriptorMapProperty, mapping.ID)
for vpIdx, vp := range vpList {
err := checkJSONLDContextType(vp)
if err != nil {
return nil, err
}

vc, selectErr := selectVC(typelessVP, mapping, opts)
if selectErr != nil {
return nil, selectErr
vpBits, err := vp.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("failed to marshal vp: %w", err)
}

inputDescriptor := pd.inputDescriptor(mapping.ID)
typelessVP := interface{}(nil)

passed := filterSchema(inputDescriptor.Schema, []*verifiable.Credential{vc}, contextLoader)
if len(passed) == 0 && !opts.DisableSchemaValidation {
return nil, fmt.Errorf(
"input descriptor id [%s] requires schemas %+v which do not match vc with @context [%+v] and types [%+v] selected by path [%s]", // nolint:lll
inputDescriptor.ID, inputDescriptor.Schema, vc.Context, vc.Types, mapping.Path)
err = json.Unmarshal(vpBits, &typelessVP)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal vp: %w", err)
}

// TODO add support for constraints: https://github.com/hyperledger/aries-framework-go/issues/2108
rawVPs[vpIdx] = typelessVP

result[mapping.ID] = vc
}
var descriptorMap []*InputDescriptorMapping

err = pd.evalSubmissionRequirements(result)
if err != nil {
return nil, fmt.Errorf("failed submission requirements: %w", err)
if useMergedSubmission {
descriptorMap = mappingsByVPIndex[vpIdx]
} else {
descriptorMap, err = parseDescriptorMap(vp)
if err != nil {
return nil, fmt.Errorf("failed to parse descriptor map: %w", err)
}
}

for _, mapping := range descriptorMap {
// The object MUST include an id property, and its value MUST be a string matching the id property of
// the Input Descriptor in the Presentation Definition the submission is related to.
if _, ok := descriptorIDs[mapping.ID]; !ok {
return nil, fmt.Errorf(
"an %s ID was found that did not match the `id` property of any input descriptor: %s",
descriptorMapProperty, mapping.ID)
}

var (
vc *verifiable.Credential
selectErr error
)

if descriptorMappingExpectsVPList(mapping) {
vc, selectErr = selectVC(rawVPs, mapping, opts)
} else if len(vpList) == 1 || !useMergedSubmission {
vc, selectErr = selectVC(typelessVP, mapping, opts)
} else {
return nil, fmt.Errorf("presentation submission has invalid path for matching a list of presentations")
}

if selectErr != nil {
return nil, selectErr
}

inputDescriptor := pd.inputDescriptor(mapping.ID)

passed := filterSchema(inputDescriptor.Schema, []*verifiable.Credential{vc}, contextLoader)
if len(passed) == 0 && !opts.DisableSchemaValidation {
return nil, fmt.Errorf(
"input descriptor id [%s] requires schemas %+v which do not match vc with @context [%+v] and types [%+v] selected by path [%s]", // nolint:lll
inputDescriptor.ID, inputDescriptor.Schema, vc.Context, vc.Types, mapping.Path)
}

// TODO add support for constraints: https://github.com/hyperledger/aries-framework-go/issues/2108

result[mapping.ID] = MatchValue{
PresentationID: vp.ID,
Credential: vc,
}
}
}

return result, nil
Expand Down Expand Up @@ -185,14 +254,14 @@ func selectVC(typelessVerifiable interface{},
}

// Ensures the matched credentials meet the submission requirements.
func (pd *PresentationDefinition) evalSubmissionRequirements(matched map[string]*verifiable.Credential) error {
func (pd *PresentationDefinition) evalSubmissionRequirements(matched map[string]MatchValue) error {
// TODO support submission requirement rules: https://github.com/hyperledger/aries-framework-go/issues/2109
descriptorIDs := descriptorIDs(pd.InputDescriptors)

for i := range descriptorIDs {
_, found := matched[descriptorIDs[i]]
_, found := matched[i]
if !found {
return fmt.Errorf("no credential provided for input descriptor %s", descriptorIDs[i])
return fmt.Errorf("no credential provided for input descriptor %s", i)
}
}

Expand Down Expand Up @@ -251,16 +320,79 @@ func parseDescriptorMap(vp *verifiable.Presentation) ([]*InputDescriptorMapping,
return typedDescriptorMap, nil
}

func descriptorIDs(input []*InputDescriptor) []string {
ids := make([]string, len(input))
func descriptorIDs(input []*InputDescriptor) map[string]bool {
ids := make(map[string]bool)

for i := range input {
ids[i] = input[i].ID
for _, id := range input {
ids[id.ID] = true
}

return ids
}

func descriptorMappingExpectsVPList(idm *InputDescriptorMapping) bool {
if strings.HasPrefix(idm.Path, "$[") {
return true
}

if idm.PathNested == nil {
return false
}

return descriptorMappingExpectsVPList(idm.PathNested)
}

func descriptorsByPresentationIndex(idm []*InputDescriptorMapping) map[int][]*InputDescriptorMapping {
results := map[int][]*InputDescriptorMapping{}

for _, mapping := range idm {
idx := presentationIndex(mapping)

results[idx] = append(results[idx], mapping)
}

return results
}

func presentationIndex(idm *InputDescriptorMapping) int {
if idm == nil {
return 0
}

idx := rootIndex(idm.Path)
if idx != -1 {
return idx
}

// check the nested path, if the root path stays at root.
if idm.Path == "$" {
return presentationIndex(idm.PathNested)
}

return 0
}

// rootIndex takes a jsonpath, and if the path indexes the root as an array, this returns the index.
// Otherwise, this returns -1.
func rootIndex(jsonPathStr string) int {
if !strings.HasPrefix(jsonPathStr, "$[") {
return -1
}

split := strings.SplitN(jsonPathStr[2:], "]", 2)

if len(split) == 0 || split[0] == "" {
return -1
}

result, err := strconv.Atoi(split[0])
if err != nil {
return -1
}

return result
}

// [The Input Descriptor Mapping Object] MUST include a path property, and its value MUST be a JSONPath
// string expression that selects the credential to be submit in relation to the identified Input Descriptor
// identified, when executed against the top-level of the object the Presentation Submission is embedded within.
Expand Down
Loading