Tutorial: Verifiable credentials¶
Implement SD-JWT Verifiable Credentials per draft-ietf-oauth-sd-jwt-vc.
Time: 15 minutes
Level: Intermediate
Sample: samples/SdJwt.Net.Samples/02-Intermediate/01-VerifiableCredentials.cs
What you will learn¶
- SD-JWT VC structure and required claims
- VCT (Verifiable Credential Type) identifiers
- Credential metadata and status
Simple explanation¶
A raw SD-JWT gives you privacy through selective disclosure. SD-JWT VC adds meaning: a credential type, an issuer identity, validity dates, and status checking. This tutorial shows how SdJwt.Net.Vc builds on SdJwt.Net to create real credentials.
Packages used¶
| Package | Purpose |
|---|---|
SdJwt.Net |
Base SD-JWT token format |
SdJwt.Net.Vc |
SD-JWT VC credential profile |
Where this fits¶
flowchart LR
A["SD-JWT (format)"] --> B["SD-JWT VC (credential)"]
B --> C["OID4VCI (issue)"]
B --> D["OID4VP (present)"]
style A fill:#555,color:#fff
style B fill:#2a6478,color:#fff
SD-JWT VC vs base SD-JWT¶
| Feature | Base SD-JWT | SD-JWT VC |
|---|---|---|
| vct claim | Optional | Required |
| iss claim | Optional | Required |
| Credential type | Free-form | Standardized |
| Status support | Manual | Built-in |
Step 1: Create VC issuer¶
using SdJwt.Net.Vc.Issuer;
using SdJwt.Net.Vc.Models;
var vcIssuer = new SdJwtVcIssuer(issuerKey, SecurityAlgorithms.EcdsaSha256);
Step 2: Define credential payload¶
var payload = new SdJwtVcPayload
{
Issuer = "https://university.example.edu",
Subject = "did:example:student123",
IssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExpiresAt = DateTimeOffset.UtcNow.AddYears(4).ToUnixTimeSeconds(),
AdditionalData = new Dictionary<string, object>
{
["given_name"] = "Alice",
["family_name"] = "Smith",
["degree_type"] = "Bachelor of Science",
["field_of_study"] = "Computer Science",
["graduation_date"] = "2025-05-15",
["gpa"] = 3.85,
["honors"] = "Magna Cum Laude"
}
};
Step 3: Configure selective disclosure¶
var options = new SdIssuanceOptions
{
DisclosureStructure = new
{
given_name = true,
family_name = true,
gpa = true, // Sensitive
honors = true // Optional to reveal
}
};
Step 4: Issue with VCT¶
// VCT identifies the credential type
var vctIdentifier = "https://credentials.example.edu/UniversityDegree";
var credential = vcIssuer.Issue(
vctIdentifier,
payload,
options,
holderPublicKey
);
Credential structure¶
The issued SD-JWT VC contains:
{
"vct": "https://credentials.example.edu/UniversityDegree",
"iss": "https://university.example.edu",
"sub": "did:example:student123",
"iat": 1700000000,
"exp": 1830000000,
"degree_type": "Bachelor of Science",
"field_of_study": "Computer Science",
"_sd": ["hash1", "hash2", "hash3"],
"cnf": { "jwk": { ... } }
}
Note: given_name, family_name, gpa, and honors are replaced with digests in _sd.
Step 5: Holder presentation¶
var holder = new SdJwtHolder(credential.Issuance);
// Present degree without GPA
var presentation = holder.CreatePresentation(
d => d.ClaimName is "given_name" or "family_name" or "honors",
kbJwtPayload: new JwtPayload
{
["aud"] = "https://employer.example.com",
["nonce"] = "job-application-nonce"
},
kbJwtSigningKey: holderKey,
kbJwtSigningAlgorithm: SecurityAlgorithms.EcdsaSha256
);
Step 6: Verifier validation¶
var verifier = new SdVerifier(_ => Task.FromResult<SecurityKey>(issuerPublicKey));
var result = await verifier.VerifyAsync(presentation, sdJwtParams, kbJwtParams);
// Check credential type
var vct = result.ClaimsPrincipal.FindFirst("vct")?.Value;
if (vct != "https://credentials.example.edu/UniversityDegree")
{
throw new Exception("Unexpected credential type");
}
Expected output¶
VC issued with vct: IdentityCredential
Status reference: https://issuer.example.com/status/1#42
Credential valid: True
Demo vs production¶
This tutorial uses in-memory keys and no real status endpoint. In production, host a publicly accessible status list and use a key management service.
Common mistakes¶
- Omitting the
vctclaim (required by SD-JWT VC draft-16) - Confusing
SdJwt.Net.Vc(IETFdc+sd-jwt) withSdJwt.Net.VcDm(W3C VCDM 2.0) - they implement different specifications
## VCT best practices
### Use resolvable URIs
```csharp
// Good: Resolvable URL with schema
var vct = "https://credentials.example.edu/schemas/UniversityDegree/v1";
// Acceptable: URN for private credentials
var vct = "urn:example:credentials:employee-badge:v1";
Version your types¶
// Include version in VCT
var vct = "https://creds.example/DriverLicense/v2";
Run the sample¶
cd samples/SdJwt.Net.Samples
dotnet run -- 2.1
Next steps¶
- Status List - Add revocation support
- OpenID4VCI - Issuance protocol
Key takeaways¶
- SD-JWT VC adds standardized structure to SD-JWTs
- VCT identifies the credential type
- Use
SdJwtVcIssuerandSdJwtVcPayloadfor VC issuance - Certain claims (vct, iss) cannot be selectively disclosed