Skip to content

How to Verify Presentations

Audience Developers building relying-party (verifier) services using ASP.NET Core.
Purpose Walk through creating a Presentation Exchange request, receiving wallet responses via OID4VP, and performing three-layer verification (cryptographic, PEX constraint, business rule).
Scope Verifier DI setup, Presentation Exchange definition, authorization request generation, callback verification, and trust considerations. Out of scope: issuance (see Issuing Credentials), trust chain setup (see Establishing Trust).
Success criteria Reader can build a PEX-based credential request, verify the SD-JWT presentation response, and extract matched claims for business logic.

Some snippets are architecture-level pseudocode. For concrete APIs, see samples/SdJwt.Net.Samples.


Key Decisions

Decision Options Guidance
Trust model? Static allow-list or Federation Federation for 3+ issuers
Status check failure behavior? Reject or step-up Reject for high-risk flows
Cache TTL for status/trust? Minutes to hours Shorter for critical flows
Nonce binding? Required or optional Always required for production
HAIP enforcement? None, Level 1, 2, 3 Match issuer HAIP level

Prerequisites

Ensure your project references the necessary NuGet packages:

dotnet add package SdJwt.Net.Oid4Vp
dotnet add package SdJwt.Net.PresentationExchange
dotnet add package SdJwt.Net.HAIP

1. Configure the Verifier Service

Register verifier-related services in your dependency injection container.

using SdJwt.Net.PresentationExchange;

var builder = WebApplication.CreateBuilder(args);

// Register services used by your verifier pipeline.
// Presentation Exchange provides a concrete registration helper:
builder.Services.AddPresentationExchange();

var app = builder.Build();

2. Request Data (Presentation Exchange)

When a user clicks "Login" or "Verify Age" on your site, you must formulate a request telling their wallet exactly what data you need. We use the DIF Presentation Exchange v2.1.1 format for this.

using SdJwt.Net.Oid4Vp;

app.MapPost("/request-verification", async (/* your verifier service */ verifier) =>
{
    // Define precisely what you need from the user's wallet
    var definition = new PresentationDefinition
    {
        Id = Guid.NewGuid().ToString(),
        InputDescriptors = new[]
        {
            new InputDescriptor
            {
                Id = "university_degree_requirement",
                Constraints = new Constraints
                {
                    Fields = new[]
                    {
                        // 1. Must be a University Degree
                        new Field
                        {
                            Path = new[] { "$.vc.type" },
                            Filter = new { contains = new { @const = "UniversityDegreeCredential" } }
                        },
                        // 2. We specifically require them to reveal their GPA, and it must be >= 3.0
                        new Field
                        {
                            Path = new[] { "$.vc.credentialSubject.gpa" },
                            Filter = new { type = "number", minimum = 3.0 }
                        }
                    }
                }
            }
        }
    };

    // Generate the OpenID4VP Authorization Request URI
    var requestUri = await verifier.CreatePresentationRequestAsync(new PresentationRequestOptions
    {
        PresentationDefinition = definition,
        CallbackUri = "https://verifier.example.com/api/callback", // Where the wallet POSTs the token
        Nonce = Guid.NewGuid().ToString() // Crucial for preventing replay attacks!
    });

    // Return the URI to the frontend to render as a QR code or universal link
    return Results.Ok(new { request_uri = requestUri });
});

3. Verify the Submission

Once the user approves the request in their wallet, the wallet will POST the massive SD-JWT string back to your CallbackUri.

app.MapPost("/api/callback", async (
    PresentationResponse response,
    /* your verifier service */ verifier,
    IPresentationExchangeService peService) =>
{
    try
    {
        // 1. Core verification:
        // - Fetches Issuer Public Keys (e.g. via .well-known/jwks.json or Federation)
        // - Verifies the cryptographic signature
        // - Hashes the presented disclosures and ensures they match the payload
        // - Evaluates the Key Binding JWT against the Nonce and Audience
        var sdJwtResult = await verifier.VerifyPresentationAsync(response);

        if (!sdJwtResult.IsValid)
        {
            return Results.BadRequest($"Invalid Token: {sdJwtResult.ErrorMessage}");
        }

        // 2. Logic verification:
        // - Did the user actually provide the GPA we asked for?
        // - Was the GPA >= 3.0?
        var peResult = await peService.EvaluatePresentationAsync(
            sdJwtResult.Claims, // Only the safely revealed claims!
            expectedDefinition // Retrieve the definition we saved in Step 2
        );

        if (!peResult.IsValid)
        {
            return Results.BadRequest($"Presentation Exchange Failed: {peResult.ErrorMessage}");
        }

        // 3. Success! Consume the data.
        var gpa = peResult.MatchedClaims["university_degree_requirement"]["gpa"];
        return Results.Ok($"Successfully hired candidate with GPA: {gpa}");
    }
    catch (Exception ex)
    {
        // Invalid signatures, expired tokens, HAIP policy failures...
        return Results.Unauthorized();
    }
});

Security Note on Trust

In the above code, verifier.VerifyPresentationAsync will inherently trust any issuer that has a valid public key. In production, you must combine this with OpenID Federation Trust Chains to ensure the Issuer is actually authorized to issue University Degrees!