Skip to content

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

Closed

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

Activity

  1. added 3 commits that reference this issue on Mar 24, 2026
  2. stevenvegt commented on Mar 25, 2026

    @stevenvegt
    MemberAuthor

    Additional acceptance criterion: ErrNoCredentials soft-fail

    During PR review (#4098), we identified that the CredentialSelector contract needs to handle ErrNoCredentials as a soft failure in matchConstraints. Without this, a custom selector returning ErrNoCredentials would hard-fail the entire match, even when submission requirements (e.g., pick rules with min: 0) would accept zero fulfilled descriptors.

    This is important for the DCQL selector (#4089), which will return ErrNoCredentials when no credential matches a query — PDs with optional groups must still work.

    Additional acceptance criterion

    • matchConstraints treats ErrNoCredentials from the selector as a soft failure (nil selection), allowing submission requirements to evaluate. ErrMultipleCredentials and other errors remain hard failures.
    • Test: MatchWithSelector with ErrNoCredentials-returning selector behaves identically to Match when PD has min: 0

    This has been implemented and tested in #4098.

  3. added 2 commits that reference this issue on Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions