Tutorial: Verification flow¶
Implement the complete issuer-holder-verifier credential flow.
Time: 15 minutes
Level: Beginner
Sample: samples/SdJwt.Net.Samples/01-Beginner/04-VerificationFlow.cs
What you will learn¶
- Complete end-to-end SD-JWT workflow
- Best practices for each actor
- Error handling and validation
Simple explanation¶
This tutorial walks through the complete cycle: an issuer creates a credential, a holder selects what to share, and a verifier checks everything. This is the end-to-end flow that all real systems implement.
Packages used¶
| Package | Purpose |
|---|---|
SdJwt.Net |
Issuance, presentation, and verification |
Where this fits¶
flowchart LR
A["Issuer"] -->|"SD-JWT"| B["Holder"]
B -->|"Presentation"| C["Verifier"]
style A fill:#2a6478,color:#fff
style B fill:#2a6478,color:#fff
style C fill:#2a6478,color:#fff
The complete flow¶
sequenceDiagram
participant Issuer
participant Holder
participant Verifier
Issuer->>Holder: 1. Issue credential with SD claims
Holder->>Holder: 2. Store credential
Holder->>Holder: 3. Select disclosures
Holder->>Verifier: 4. Create and send presentation
Verifier->>Verifier: 5. Verify signatures and claims
Phase 1: Issuer creates credential¶
// Setup issuer infrastructure
using var issuerEcdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var issuerKey = new ECDsaSecurityKey(issuerEcdsa) { KeyId = "issuer-2024" };
var issuer = new SdIssuer(issuerKey, SecurityAlgorithms.EcdsaSha256);
// Define credential claims
var claims = new JwtPayload
{
["iss"] = "https://university.example.edu",
["sub"] = "student-12345",
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["exp"] = DateTimeOffset.UtcNow.AddYears(4).ToUnixTimeSeconds(),
["given_name"] = "Alice",
["family_name"] = "Johnson",
["degree"] = "Computer Science",
["graduation_year"] = 2025,
["gpa"] = 3.85
};
// Configure selective disclosure
var options = new SdIssuanceOptions
{
DisclosureStructure = new
{
given_name = true,
family_name = true,
gpa = true // Sensitive - can be hidden
}
};
// Issue with holder binding
var issuance = issuer.Issue(claims, options, holderJwk);
Phase 2: Holder stores and prepares¶
// Holder receives and stores the credential
var holder = new SdJwtHolder(issuance.Issuance);
// Holder can inspect available disclosures
Console.WriteLine("Available claims to disclose:");
foreach (var disclosure in holder.AllDisclosures)
{
Console.WriteLine($" - {disclosure.ClaimName}");
}
Phase 3: Verifier requests credential¶
The verifier sends a request with:
- Expected audience (their identifier)
- Fresh nonce (prevents replay)
- Required claims (what they need)
// Verifier's request
var verifierRequest = new
{
Audience = "https://employer.example.com",
Nonce = Guid.NewGuid().ToString(),
RequiredClaims = new[] { "given_name", "family_name", "degree" }
};
Phase 4: Holder creates presentation¶
// Holder decides what to disclose based on request
var presentation = holder.CreatePresentation(
disclosure =>
disclosure.ClaimName == "given_name" ||
disclosure.ClaimName == "family_name",
// Note: NOT disclosing GPA
kbJwtPayload: new JwtPayload
{
["aud"] = verifierRequest.Audience,
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["nonce"] = verifierRequest.Nonce
},
kbJwtSigningKey: holderPrivateKey,
kbJwtSigningAlgorithm: SecurityAlgorithms.EcdsaSha256
);
Phase 5: Verifier validates¶
// Create verifier with issuer key resolver
var verifier = new SdVerifier(async jwt =>
{
// In production: resolve key from issuer metadata or trust registry
var issuer = jwt.Issuer;
if (issuer == "https://university.example.edu")
return issuerPublicKey;
throw new SecurityTokenException($"Unknown issuer: {issuer}");
});
// Configure validation
var sdJwtParams = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = new[] { "https://university.example.edu" },
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
var kbJwtParams = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = true,
ValidAudience = verifierRequest.Audience,
ValidateLifetime = false
};
// Verify
var result = await verifier.VerifyAsync(
presentation,
sdJwtParams,
kbJwtParams,
expectedKbJwtNonce: verifierRequest.Nonce
);
// Process verified claims
if (result.KeyBindingVerified)
{
Console.WriteLine("Verification successful!");
Console.WriteLine("Disclosed claims:");
foreach (var claim in result.ClaimsPrincipal.Claims)
{
Console.WriteLine($" {claim.Type}: {claim.Value}");
}
}
Error handling¶
try
{
var result = await verifier.VerifyAsync(presentation, sdJwtParams);
}
catch (SecurityTokenExpiredException)
{
Console.WriteLine("Credential has expired");
}
catch (SecurityTokenInvalidSignatureException)
{
Console.WriteLine("Invalid signature - credential may be tampered");
}
catch (SecurityTokenException ex)
{
Console.WriteLine($"Verification failed: {ex.Message}");
}
Best practices¶
For issuers¶
- Use strong algorithms (ES256, ES384, EdDSA)
- Set appropriate expiration times
- Include holder binding for sensitive credentials
For holders¶
- Store credentials securely
- Only disclose minimum necessary claims
- Use fresh nonces for each presentation
For verifiers¶
- Always validate issuer signatures
- Verify key binding for high-value credentials
- Check nonce freshness
- Validate expected audience
Run the sample¶
cd samples/SdJwt.Net.Samples
dotnet run -- 1.4
Important: Non-disclosed claims are not deleted or removed. They exist as SHA-256 digests in the JWT payload. The verifier cannot recover the original values without the corresponding disclosure.
Expected output¶
Issuer: SD-JWT created with 4 disclosures
Holder: Presenting 2 of 4 claims
Verifier: Signature valid
Verifier: Disclosed claims: given_name, email
Verifier: Non-disclosed claims are not visible
Demo vs production¶
This example uses a single process for all three roles. In production, each role runs on a separate system and credentials are exchanged via protocols like OID4VCI and OID4VP.
Common mistakes¶
- Forgetting to validate the issuer's public key against a trust list
- Assuming non-disclosed claims are deleted (they exist as digests in the JWT; the verifier just cannot see the values)
Next steps¶
Continue to intermediate tutorials:
Summary¶
| Phase | Actor | Action |
|---|---|---|
| 1 | Issuer | Creates SD-JWT with selective claims |
| 2 | Holder | Stores credential |
| 3 | Verifier | Sends request with nonce |
| 4 | Holder | Creates selective presentation |
| 5 | Verifier | Validates signatures and claims |