Skip to content

Refactor PD matching to return all candidates per input descriptor #4088

@stevenvegt

Description

@stevenvegt

Parent PRD

#4067 (see PRD comment)

What to build

Inject a configurable credential selection strategy into the PD matcher, so that when multiple credentials match a single input descriptor, the selection logic determines which one is used — without changing the existing Match public API or return types.

Call stack

The credential selection flows through the following call stack:

API: RequestServiceAccessToken (auth/api/iam/api.go)
  → RequestRFC021AccessToken (auth/client/iam/openid4vp.go)
    → wallet.BuildSubmission (vcr/holder/sql_wallet.go)
      → presenter.buildSubmission (vcr/holder/presenter.go)
        → PresentationSubmissionBuilder (vcr/pe/presentation_submission.go)
          → builder.Build()
            → presentationDefinition.Match() (vcr/pe/presentation_definition.go)
              → matchBasic() or matchSubmissionRequirements()
                → matchConstraints()

Design

CredentialSelector type (vcr/pe/presentation_definition.go):

A function type that picks one credential from a list of candidates matching an input descriptor:

type CredentialSelector func(descriptor InputDescriptor, candidates []vc.VerifiableCredential) (*vc.VerifiableCredential, error)

FirstMatchSelector (vcr/pe/presentation_definition.go):

The default selector, extracted from the current matchConstraints logic. Picks the first matching credential, preserving existing behavior:

func FirstMatchSelector(_ InputDescriptor, candidates []vc.VerifiableCredential) (*vc.VerifiableCredential, error)

MatchWithSelector (vcr/pe/presentation_definition.go):

New method on PresentationDefinition that accepts a CredentialSelector. The existing Match method delegates to MatchWithSelector with FirstMatchSelector:

func (pd PresentationDefinition) Match(vcs []vc.VerifiableCredential) ([]vc.VerifiableCredential, []InputDescriptorMappingObject, error)
func (pd PresentationDefinition) MatchWithSelector(vcs []vc.VerifiableCredential, selector CredentialSelector) ([]vc.VerifiableCredential, []InputDescriptorMappingObject, error)

matchConstraints refactor (vcr/pe/presentation_definition.go):

Currently breaks on the first matching credential per input descriptor. Refactored to:

  1. Collect all matching VCs per input descriptor (remove the break)
  2. Call the CredentialSelector to pick one from the candidates
  3. Return the same Candidate struct — no change to callers

PresentationSubmissionBuilder (vcr/pe/presentation_submission.go):

Add SetCredentialSelector method to the builder. When set, Build passes the selector through to MatchWithSelector. When not set, falls back to FirstMatchSelector:

func (b *PresentationSubmissionBuilder) SetCredentialSelector(selector CredentialSelector) *PresentationSubmissionBuilder

This allows the presenter layer (vcr/holder/presenter.go) to configure the selector when credential_query is provided, without changing the builder's existing API.

Future: DCQL selector (#4089)

The DCQL selector (implemented in #4089) will be a CredentialSelector that:

  • Takes the credential_query array as configuration
  • For a given input descriptor, checks if a credential_query with matching ID exists
  • If yes: filters candidates using dcql.Match, errors on zero or multiple matches
  • If no: falls back to FirstMatchSelector

The presenter layer will call builder.SetCredentialSelector(dcqlSelector) when credential_query is provided.

Acceptance criteria

  • CredentialSelector function type defined and exported in vcr/pe/presentation_definition.go
  • FirstMatchSelector extracted as an exported function — picks the first matching credential
  • matchConstraints refactored to collect all matching VCs per input descriptor and call the selector
  • MatchWithSelector method added to PresentationDefinition
  • Match delegates to MatchWithSelector with FirstMatchSelector — no behavior change
  • SetCredentialSelector method added to PresentationSubmissionBuilder
  • Build uses the configured selector when set, falls back to FirstMatchSelector
  • Candidate struct unchanged — still holds one *vc.VerifiableCredential
  • Match public API return type unchanged
  • All existing PD matching tests pass unchanged (backward compatible)
  • All existing PresentationSubmissionBuilder tests pass unchanged
  • New test: when multiple VCs match an input descriptor, all candidates are passed to the selector
  • New test: custom selector can pick a non-first credential

Blocked by

None — can start immediately (independent of DCQL parser).

User stories addressed

  • User story 11: existing behavior preserved for input descriptors without credential_query

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions