Skip to content

Presentation Exchange Deep Dive (DIF PE)

Audience Developers building verifiers that define credential requirements, and wallet developers implementing credential selection.
Purpose Explain the DIF Presentation Exchange query language - presentation definitions, input descriptors, submission requirements, and credential matching - with working SdJwt.Net.PresentationExchange code examples.
Scope Presentation definitions, input descriptors with field constraints, filter expressions, submission requirements (all/pick), JSONPath matching, and presentation submission responses. Out of scope: protocol transport (see OID4VP Deep Dive), DCQL (covered in OID4VP).
Success criteria Reader can create a presentation definition with field constraints and submission requirements, implement wallet-side credential matching, and validate presentation submissions.

Prerequisites

Before reading this document, you should understand:

Prerequisite Why Needed Resource
SD-JWT basics PE queries SD-JWT credentials SD-JWT Deep Dive
Verifiable Credentials PE selects from VCs VC Deep Dive
OID4VP basics PE is used within OID4VP OID4VP Deep Dive

Glossary

Term Definition
Presentation Definition Complete specification of what credentials/claims a Verifier needs
Input Descriptor Single credential requirement within a definition
Constraints Rules a credential must satisfy (fields, formats)
Field Single claim requirement with JSONPath and optional filter
Submission Requirement Grouping logic for multiple descriptors (all, pick, count)
Presentation Submission Wallet's response mapping credentials to descriptors
Descriptor Map Maps each input descriptor ID to presented credential
JSONPath Query syntax for locating claims within credential JSON

Why Presentation Exchange Matters

Problem: Without a standard query language:

  • Verifiers invent custom request formats
  • Wallets need custom code for each verifier
  • Users cannot understand what is being requested

Solution: PEX provides:

  • Declarative requirement specification
  • Deterministic wallet-side matching
  • Clear mapping of what satisfies each requirement
  • User-friendly metadata for consent screens

Core Structures

flowchart TD
    PD[PresentationDefinition] --> ID1[InputDescriptor 1]
    PD --> ID2[InputDescriptor 2]
    PD --> SR[SubmissionRequirements]
    ID1 --> C1[Constraints]
    C1 --> F1[Field 1]
    C1 --> F2[Field 2]
    F1 --> Filter1[Filter]
    SR --> Rule[Rule: all / pick]
Structure Purpose Required
PresentationDefinition Container for all requirements Yes
InputDescriptor One credential pattern requirement At least one
Constraints Claims the credential must contain No (but usually present)
Field Single claim path + optional filter No
SubmissionRequirement How to combine descriptors No
PresentationSubmission Response mapping from wallet Response only

Presentation Definition Examples

Simple: Single Credential, Specific Claims

Verify employment status with position and department:

{
  "id": "employment_verification",
  "name": "Employment Verification",
  "purpose": "Verify your current employment status",
  "input_descriptors": [
    {
      "id": "employment_credential",
      "name": "Employment Credential",
      "purpose": "Proof of current employment",
      "format": {
        "dc+sd-jwt": {}
      },
      "constraints": {
        "fields": [
          {
            "path": ["$.vct"],
            "filter": {
              "type": "string",
              "pattern": ".*employment.*"
            }
          },
          {
            "path": ["$.position"]
          },
          {
            "path": ["$.department"],
            "optional": true
          }
        ]
      }
    }
  ]
}

Breakdown:

Component Value Meaning
format dc+sd-jwt Only accepts SD-JWT VC format
path: ["$.vct"] JSONPath Credential type must match pattern
path: ["$.position"] JSONPath position claim is required
optional: true Field flag department is nice-to-have

Multiple Credentials: Background Check

Verify both employment and education:

{
  "id": "background_check",
  "name": "Background Check",
  "purpose": "Complete background verification for employment",
  "input_descriptors": [
    {
      "id": "employment_input",
      "name": "Employment History",
      "group": ["verification"],
      "constraints": {
        "fields": [
          { "path": ["$.vct"], "filter": { "pattern": ".*employment.*" } },
          { "path": ["$.employer"] },
          { "path": ["$.start_date"] }
        ]
      }
    },
    {
      "id": "education_input",
      "name": "Education Record",
      "group": ["verification"],
      "constraints": {
        "fields": [
          { "path": ["$.vct"], "filter": { "pattern": ".*degree.*" } },
          { "path": ["$.degree"] },
          { "path": ["$.institution"] }
        ]
      }
    }
  ],
  "submission_requirements": [
    {
      "name": "Background Verification",
      "rule": "all",
      "from": "verification"
    }
  ]
}

Alternative Credentials: Age Verification

Accept either government ID OR driver's license:

{
  "id": "age_verification",
  "input_descriptors": [
    {
      "id": "govt_id",
      "group": ["age_proof"],
      "constraints": {
        "fields": [
          { "path": ["$.vct"], "filter": { "pattern": ".*government_id.*" } },
          { "path": ["$.age_over_21"] }
        ]
      }
    },
    {
      "id": "drivers_license",
      "group": ["age_proof"],
      "constraints": {
        "fields": [
          { "path": ["$.vct"], "filter": { "pattern": ".*drivers_license.*" } },
          { "path": ["$.age_over_21"] }
        ]
      }
    }
  ],
  "submission_requirements": [
    {
      "rule": "pick",
      "count": 1,
      "from": "age_proof"
    }
  ]
}

Submission Requirements Rules

Rule Meaning Example
all All descriptors in group required "Need both employment AND education"
pick + count Exactly N from group "Any 1 of these 3 ID types"
pick + min At least N from group "At least 2 references"
pick + max At most N from group "Up to 3 supporting documents"
pick + min + max Between N and M "2 to 4 credentials"

Presentation Submission Response

When the wallet finds matching credentials:

{
  "id": "submission-abc123",
  "definition_id": "background_check",
  "descriptor_map": [
    {
      "id": "employment_input",
      "format": "dc+sd-jwt",
      "path": "$[0]"
    },
    {
      "id": "education_input",
      "format": "dc+sd-jwt",
      "path": "$[1]"
    }
  ]
}
Field Purpose
definition_id Links to original request
descriptor_map Array mapping each satisfied descriptor to credential
format Format of the presented credential
path JSONPath to credential in vp_token (array or single)

Code Example: Verifier Creating Definition

using SdJwt.Net.PresentationExchange.Models;

// Build presentation definition programmatically
var definition = new PresentationDefinition
{
    Id = "employment_verification",
    Name = "Employment Verification",
    Purpose = "Verify current employment for loan application",
    InputDescriptors = new[]
    {
        new InputDescriptor
        {
            Id = "employment_credential",
            Name = "Employment Credential",
            Purpose = "Proof of stable employment",
            Format = new FormatConstraints
            {
                DcSdJwt = new FormatOptions()  // Accept dc+sd-jwt format
            },
            Constraints = new Constraints
            {
                Fields = new[]
                {
                    new Field
                    {
                        Path = new[] { "$.vct" },
                        Filter = new Filter
                        {
                            Type = "string",
                            Pattern = ".*employment.*"
                        }
                    },
                    new Field
                    {
                        Path = new[] { "$.employer" }
                    },
                    new Field
                    {
                        Path = new[] { "$.position" }
                    },
                    new Field
                    {
                        Path = new[] { "$.salary_range" },
                        Optional = true
                    }
                }
            }
        }
    }
};

// Validate before sending
definition.Validate();

// Serialize for transport
string json = JsonSerializer.Serialize(definition);

Code Example: Wallet Selecting Credentials

using SdJwt.Net.PresentationExchange.Engine;
using SdJwt.Net.PresentationExchange.Models;
using SdJwt.Net.PresentationExchange.Services;
using Microsoft.Extensions.Logging;

// Setup the engine (typically via DI)
var engine = new PresentationExchangeEngine(
    logger: loggerFactory.CreateLogger<PresentationExchangeEngine>(),
    constraintEvaluator: new ConstraintEvaluator(constraintLogger),
    submissionRequirementEvaluator: new SubmissionRequirementEvaluator(reqLogger),
    formatDetector: new CredentialFormatDetector()
);

// Parse incoming definition
var definition = JsonSerializer.Deserialize<PresentationDefinition>(definitionJson);

// Select from wallet credentials
var result = await engine.SelectCredentialsAsync(
    presentationDefinition: definition,
    credentialWallet: walletCredentials,
    options: new CredentialSelectionOptions
    {
        MaxCredentialsToEvaluate = 100,
        PreferNewerCredentials = true
    }
);

if (result.IsSuccessful)
{
    // Show user which credentials match
    foreach (var selected in result.SelectedCredentials)
    {
        Console.WriteLine($"Matched: {selected.DescriptorId} -> {selected.CredentialId}");
    }

    // Get the presentation submission for response
    var submission = result.PresentationSubmission;
}
else
{
    Console.WriteLine($"No match: {result.ErrorCode} - {result.ErrorMessage}");
}

JSONPath Quick Reference

Pattern Meaning Example
$.claim Root-level claim $.position
$.nested.claim Nested claim $.address.city
$['claim'] Bracket notation $['given_name']
$.array[0] Array index $.degrees[0]
$.array[*] All array elements $.employers[*]

Filter Expressions

Filters constrain the values that satisfy a field:

{
  "path": ["$.vct"],
  "filter": {
    "type": "string",
    "pattern": ".*UniversityDegree.*"
  }
}
Filter Property Purpose Example
type JSON Schema type "string", "number", "boolean"
pattern Regex for strings ".*employment.*"
enum Allowed values ["BachelorDegree", "MasterDegree"]
minimum Numeric minimum 18 (for age)
maximum Numeric maximum 65
const Exact value true

Implementation References

Component File Description
Engine PresentationExchangeEngine.cs Main credential selection
Definition model PresentationDefinition.cs Definition structure
Descriptor model InputDescriptor.cs Descriptor structure
Requirement model SubmissionRequirement.cs Submission requirements
Constraint evaluator ConstraintEvaluator.cs Field matching
Submission evaluator SubmissionRequirementEvaluator.cs Rule application
Package overview README.md Quick start
Sample code PresentationExchangeExample.cs Working examples

Beginner Pitfalls to Avoid

1. Requesting More Than Needed

Wrong: Requesting all available claims "just in case."

Right: Request only what you need for your use case.

// WRONG - over-requesting
{
  "fields": [
    { "path": ["$.given_name"] },
    { "path": ["$.family_name"] },
    { "path": ["$.birth_date"] },
    { "path": ["$.social_security_number"] },
    { "path": ["$.address"] },
    { "path": ["$.phone"] }
  ]
}

// RIGHT - minimal disclosure for age check
{
  "fields": [
    { "path": ["$.age_over_21"] }
  ]
}

2. Forgetting Format Constraints

Wrong: Not specifying format, accepting any credential format.

Right: Specify the format you can process.

{
  "id": "my_descriptor",
  "format": {
    "dc+sd-jwt": {}
  },
  "constraints": { ... }
}

3. Ignoring Submission Mapping Validation

Wrong: Only validating the credentials themselves.

Right: Validate that descriptor_map correctly maps to the definition.

// Verify submission matches definition
if (submission.DefinitionId != definition.Id)
{
    throw new ValidationException("Submission does not match definition");
}

foreach (var mapping in submission.DescriptorMap)
{
    if (!definition.InputDescriptors.Any(d => d.Id == mapping.Id))
    {
        throw new ValidationException($"Unknown descriptor: {mapping.Id}");
    }
}

4. Conflicting Submission Requirements

Wrong: Creating requirements that cannot be satisfied.

{
  "submission_requirements": [
    { "rule": "all", "from": "group_a" },
    { "rule": "pick", "count": 1, "from": "group_a" }
  ]
}

Right: Requirements should be logically consistent.

Frequently Asked Questions

Q: What is the difference between PE and DCQL?

A: PE (Presentation Exchange) is feature-rich with groups, submission requirements, and complex filters. DCQL (Digital Credentials Query Language) is simpler for basic single-credential queries. Use PE when you need complex logic; use DCQL for straightforward requests.

Q: How do I request a claim that might be at different paths?

A: Use array syntax in the path field:

{
  "path": ["$.given_name", "$.first_name", "$.name.given"]
}

The wallet will try each path and use the first match.

Q: Can I require a specific issuer?

A: Yes, add a field constraint on $.iss:

{
  "path": ["$.iss"],
  "filter": {
    "type": "string",
    "enum": [
      "https://trusted-issuer.example.com",
      "https://another-issuer.example.com"
    ]
  }
}

Q: How does the wallet handle multiple matching credentials?

A: The engine can be configured to prefer certain credentials (newest, specific issuers). By default, it returns the first match for each descriptor.

Q: What happens if a required field is missing from a credential?

A: The credential does not match that input descriptor. If no credentials match, the selection fails with an error indicating which descriptors could not be satisfied.