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
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");
}
VCT Best Practices
Use Resolvable URIs
// 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