How to build an SD-JWT VC issuer service¶
| Field | Value |
|---|---|
| Package maturity | Stable (SdJwt.Net.Vc) + Spec-tracking (SdJwt.Net.Oid4Vci) |
| Code status | Mixed -- runnable package APIs with illustrative service wiring |
| Related concept | Verifiable Credentials, SD-JWT |
| Related tutorial | Tutorials |
| Audience | Developers building issuer services using ASP.NET Core. |
| Purpose | Walk through configuring an issuer endpoint, building a selectively disclosable VC payload, adding holder binding and status references, and returning OID4VCI-compliant responses. |
| Scope | Issuer DI setup, key configuration, SdJwtVcIssuer and SdJwtVcPayload usage, and under-the-hood mechanics. Out of scope: verification (see Verifying Presentations), revocation (see Managing Revocation). |
| Success criteria | Reader can stand up an issuer endpoint that signs SD-JWT VCs with selective claims and OID4VCI response formatting. |
What your application still owns¶
This guide does not provide: production key custody, user authentication, authorization policy, trust onboarding, certified wallet functionality, UX and consent screens, durable audit storage, monitoring and incident response, or legal/regulatory compliance review.
Key decisions¶
| Decision | Options | Guidance |
|---|---|---|
| Which claims to make selectively disclosable? | Any PII or sensitive data | Minimize always-visible claims for privacy |
| Credential validity period? | Hours to years | Shorter for high-risk credentials |
| Revocation support? | Yes/No | Yes for long-lived credentials |
| Key storage? | Software, HSM, cloud KMS | HSM for production environments |
| HAIP Final profile? | None, OID4VCI, OID4VP, DC API, SD-JWT VC, mdoc | Optional hardening; add after core flow works |
Prerequisites¶
Ensure your project references the necessary NuGet packages:
dotnet add package SdJwt.Net.Oid4Vci
dotnet add package SdJwt.Net.Vc
1. Configure the issuer service¶
Register the Issuer service in your Dependency Injection container (Program.cs or Startup.cs). This requires setting up your cryptographic signing keys and defining the credential types your service supports.
using SdJwt.Net.Vc.Issuer;
using SdJwt.Net.Vc.Models;
using SdJwt.Net.Issuer;
using Microsoft.IdentityModel.Tokens;
using System.Security.Cryptography;
var builder = WebApplication.CreateBuilder(args);
// Generate or load your private signing key (e.g., from Azure Key Vault)
var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var issuerSigningKey = new ECDsaSecurityKey(ecdsa);
// Create the issuer instance directly (or register in DI as a singleton)
var vcIssuer = new SdJwtVcIssuer(issuerSigningKey, SecurityAlgorithms.EcdsaSha256);
var app = builder.Build();
2. Issue a credential¶
Once OID4VCI handles the OAuth 2.0 component (e.g., the wallet exchanging an authorization code for an access token), your business logic needs to build the actual credential payload.
Use SdJwtVcIssuer to construct the payload and issue the credential, explicitly deciding which claims are always visible and which are selectively disclosable.
app.MapPost("/issue-degree", (CredentialRequest request) =>
{
// 1. Build the SD-JWT VC payload
var vcPayload = new SdJwtVcPayload
{
Issuer = "https://issuer.example.com",
Subject = $"did:example:student:{request.UserId}",
IssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExpiresAt = DateTimeOffset.UtcNow.AddYears(5).ToUnixTimeSeconds(),
// Domain-specific claims via AdditionalData
AdditionalData = new Dictionary<string, object>
{
// Public information (always visible to any verifier)
["degreeName"] = "Bachelor of Science in Computer Science",
["university"] = "Example University",
// Private information (holder must explicitly consent to reveal these)
["gpa"] = "3.8",
["graduationDate"] = "2023-05-15",
["honors"] = "Summa Cum Laude"
}
};
// 2. Configure selective disclosure: true = selectively disclosable
var options = new SdIssuanceOptions
{
DisclosureStructure = new
{
gpa = true,
graduationDate = true,
honors = true
// degreeName and university stay always visible
}
};
// 3. Issue the SD-JWT VC
var result = vcIssuer.Issue(
"UniversityDegreeCredential",
vcPayload,
options,
holderPublicJwk); // optional holder binding key
// 4. Return the standard OID4VCI response to the wallet
return Results.Ok(new
{
credential = result.Issuance, // The {JWT}~{disc1}~{disc2} string
format = "dc+sd-jwt"
});
});
What happens under the hood?¶
When vcIssuer.Issue() runs:
- Salting and hashing: For
gpa,graduationDate, andhonors, theSdJwt.Netcore generates high-entropy salts, creates disclosure strings, hashes them via SHA-256, and places the hashes into the JWT's_sdarray. - Decoys: Decoy hashes are injected to prevent observers from guessing how many claims the user has.
- Signing: The core JWT is signed using your ECDSA key.
Optional: HAIP Final profile validation¶
If your deployment requires HAIP compliance, add the HAIP package and validate before accepting traffic:
dotnet add package SdJwt.Net.HAIP
using SdJwt.Net.HAIP;
using SdJwt.Net.HAIP.Validators;
var haipOptions = new HaipProfileOptions();
haipOptions.Flows.Add(HaipFlow.Oid4VciIssuance);
haipOptions.CredentialProfiles.Add(HaipCredentialProfile.SdJwtVc);
haipOptions.SupportedCredentialFormats.Add(HaipConstants.SdJwtVcFormat);
haipOptions.SupportedJoseAlgorithms.Add(HaipConstants.RequiredJoseAlgorithm);
haipOptions.SupportedHashAlgorithms.Add(HaipConstants.RequiredHashAlgorithm);
haipOptions.SupportsAuthorizationCodeFlow = true;
haipOptions.EnforcesPkceS256 = true;
haipOptions.SupportsPushedAuthorizationRequests = true;
haipOptions.SupportsDpop = true;
haipOptions.SupportsDpopNonce = true;
haipOptions.ValidatesWalletAttestation = true;
haipOptions.ValidatesKeyAttestation = true;
haipOptions.SupportsSdJwtVcCompactSerialization = true;
haipOptions.UsesCnfJwkForSdJwtVcHolderBinding = true;
haipOptions.RequiresKbJwtForHolderBoundSdJwtVc = true;
haipOptions.SupportsStatusListClaim = true;
haipOptions.SupportsSdJwtVcIssuerX5c = true;
var haipResult = new HaipProfileValidator().Validate(haipOptions);
if (!haipResult.IsCompliant)
{
throw new InvalidOperationException(
"Issuer configuration does not meet the selected HAIP Final profile.");
}
Next steps¶
Now that the wallet holds the credential, learn how a Relying Party verifies it in the Verifying Presentations Guide.