How to Issue Verifiable Credentials
| Audience | Developers building issuer services using ASP.NET Core. |
| Purpose | Walk through configuring an issuer endpoint, building a selectively disclosable VC payload, and returning OID4VCI-compliant responses with HAIP enforcement. |
| Scope | Issuer DI setup, key configuration, VerifiableCredentialBuilder usage, HAIP policy enforcement, 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, HAIP-level algorithm enforcement, and OID4VCI response formatting. |
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 level? | None, Level 1, 2, 3 | Level 2 for most regulated deployments |
Prerequisites
Ensure your project references the necessary NuGet packages:
dotnet add package SdJwt.Net.Oid4Vci
dotnet add package SdJwt.Net.Vc
dotnet add package SdJwt.Net.HAIP
1. Configure the Issuer Service
First, 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.Oid4Vci;
using SdJwt.Net.HAIP;
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);
builder.Services.AddSdJwtIssuer(options =>
{
options.IssuerUrl = "https://issuer.example.com";
options.SigningKey = issuerSigningKey;
// Define the JSON schema types this issuer supports
options.SupportedCredentialTypes = new[]
{
"UniversityDegreeCredential",
"EmployeeIdCredential"
};
// We are issuing the 'vc+sd-jwt' format defined in OpenID4VCI
options.SupportedFormats = new[] { "vc+sd-jwt" };
// Apply HAIP Level 2 Security Policy (e.g., forces DPoP, strictly ES384+)
options.UseHaipProfile(HaipLevel.Level2_VeryHigh);
});
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 the VerifiableCredentialBuilder to construct the W3C payload, explicitly deciding which claims are plain-text and which are SelectiveDisclosure (hidden behind a salt/hash).
using SdJwt.Net.Vc;
app.MapPost("/issue-degree", async (
CredentialRequest request,
ISdJwtIssuerService issuer) =>
{
// 1. Build the W3C Verifiable Credential Payload
var vcBuilder = new VerifiableCredentialBuilder()
.WithType("UniversityDegreeCredential")
.WithIssuer("https://issuer.example.com")
.WithSubject($"did:example:student:{request.UserId}")
.WithIssuanceDate(DateTimeOffset.UtcNow)
.WithExpirationDate(DateTimeOffset.UtcNow.AddYears(5))
// Public information (visible to any verifier)
.WithCredentialSubject("degreeName", "Bachelor of Science in Computer Science")
.WithCredentialSubject("university", "Example University")
// Private information (Holder must explicitly consent to reveal these)
.WithSelectiveCredentialSubject("gpa", "3.8")
.WithSelectiveCredentialSubject("graduationDate", "2023-05-15")
.WithSelectiveCredentialSubject("honors", "Summa Cum Laude");
// 2. The ISdJwtIssuerService handles the complex SD-JWT hashing and signing logic automatically
// It also enforces the configured HAIP policy.
var credentialResult = await issuer.CreateCredentialAsync(vcBuilder);
// 3. Return the standard OID4VCI response to the wallet
return Results.Ok(new
{
credential = credentialResult.SdJwt, // The massive {JWT}~{disc1}~{disc2} string
format = "vc+sd-jwt"
});
});
What happens under the hood?
When issuer.CreateCredentialAsync() runs:
- HAIP Validation: HAIP intercepts the call to ensure your
issuerSigningKeymeets the requirements forLevel2_VeryHigh(e.g., throwing an error if you accidentally passed in an insecure RSA 1024-bit key). - Salting & 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.
Next Steps
Now that the wallet holds the credential, learn how a Relying Party verifies it in the Verifying Presentations Guide.