SD-JWT Verifiable Credentials¶
Level: Beginner concept + implementation
Simple explanation¶
Raw SD-JWT gives you selective disclosure. SD-JWT VC adds credential meaning: what type of credential it is, who issued it, when it is valid, how to check status, and what trust policy a verifier should apply.
What you will learn¶
- What SD-JWT VC adds on top of raw SD-JWT
- How
vct,iss,status, andcnfclaims work together - How to issue, present, and verify credentials with
SdJwt.Net.Vc - How SD-JWT VC relates to OID4VCI and OID4VP
SD-JWT = privacy-preserving token format
SD-JWT VC = credential profile using that token format
OID4VCI = how it is issued
OID4VP = how it is presented
| Audience | Developers building credential issuance or verification services, and architects designing trust frameworks. |
| Purpose | Explain the semantic layer SD-JWT VC adds on top of raw SD-JWT - credential typing (vct), status references, and trust policies - and show how to issue, present, and verify credentials using SdJwt.Net.Vc. |
| Scope | VC payload structure, vct and status claims, issuance/presentation/verification lifecycle with code, and common use cases. Out of scope: base SD-JWT mechanics (see SD-JWT), protocol transport (see OID4VCI / OID4VP). |
| Success criteria | Reader can issue an SD-JWT VC with type metadata and status reference, create a selective presentation, and verify it with VC-layer checks including revocation checking. |
Prerequisites¶
Before reading this document, you should be familiar with:
- SD-JWT fundamentals from SD-JWT
- Basic concepts of digital credentials and identity verification
- JSON Web Tokens (JWT) structure
SD-JWT VC vs W3C VCDM 2.0 — two parallel specs¶
SD-JWT VC is NOT the same as W3C Verifiable Credentials Data Model 2.0. The two specifications come from different standards bodies (IETF vs W3C), target different securing mechanisms, and have incompatible data models.
| Aspect | SD-JWT VC (IETF) | W3C VCDM 2.0 |
|---|---|---|
| Spec body | IETF (draft-ietf-oauth-sd-jwt-vc) |
W3C Recommendation |
| OID4VCI format | dc+sd-jwt |
jwt_vc_json, ldp_vc |
| Type system | vct claim (collision-resistant URI) |
@context + type[] (JSON-LD) |
| Issuer claim | iss (string URL) |
issuer (string or object) |
| Dates | iat/exp/nbf (Unix seconds) |
validFrom/validUntil (ISO 8601) |
| Selective disclosure | _sd arrays (SD-JWT native) |
ecdsa-sd-2023 or bbs-2023 suites |
| Revocation | status.status_list (IETF Token Status List) |
credentialStatus.BitstringStatusListEntry |
| Package | SdJwt.Net.Vc |
SdJwt.Net.VcDm |
The IETF spec renamed vc+sd-jwt → dc+sd-jwt in late 2024 specifically to avoid confusion with W3C's vc media type namespace. The dc prefix stands for "Digital Credential", not "Data Model Compliant."
SdJwt.Net.Vc (this document) implements the IETF SD-JWT VC spec.
For W3C VCDM 2.0 (jwt_vc_json / ldp_vc formats), see W3C VCDM 2.0 and the SdJwt.Net.VcDm package.
Why SD-JWT VC exists¶
SD-JWT handles selective disclosure, but by itself it does not define credential semantics. A verifier still needs standard meanings for claims such as type, issuer, and lifecycle status.
Problem with raw SD-JWT:
{
"iss": "https://example.com",
"given_name": "Alice",
"family_name": "Smith"
}
Questions the verifier cannot answer:
- What type of credential is this?
- Is this a driver's license, university degree, or employment record?
- Has this credential been revoked?
- Can I trust this issuer for this credential type?
SD-JWT VC adds semantic layer:
{
"iss": "https://university.example.edu",
"vct": "https://credentials.example.com/UniversityDegree",
"status": {
"status_list": {
"idx": 42,
"uri": "https://university.example.edu/status/1"
}
},
"given_name": "Alice",
"degree": "Bachelor of Science"
}
Now the verifier knows the credential type, where to check status, and can apply type-specific trust policies.
Glossary¶
| Term | Definition |
|---|---|
| Verifiable Credential (VC) | A tamper-evident credential with cryptographically verifiable authorship |
| vct | Verifiable Credential Type - unique identifier for the credential schema |
| Status List | Compact mechanism to check if credential is valid, revoked, or suspended |
| Issuer | Entity that creates and signs the credential |
| Holder | Entity that stores and presents the credential |
| Verifier | Entity that validates the credential and makes trust decisions |
| cnf | Confirmation claim containing holder's public key for binding |
The credential lifecycle at a glance¶
flowchart LR
SdJwt["SD-JWT<br/>Token format"] --> SdJwtVc["SD-JWT VC<br/>Credential semantics"]
SdJwtVc --> OID4VCI["OID4VCI<br/>Issuance protocol"]
SdJwtVc --> OID4VP["OID4VP<br/>Presentation protocol"]
OID4VP --> Verifier["Verifier<br/>Trust decision"]
Each layer adds something:
| Layer | What it adds | SD-JWT .NET package |
|---|---|---|
| SD-JWT | Selective disclosure, hashes, key binding | SdJwt.Net |
| SD-JWT VC | Credential type, issuer, status, holder binding | SdJwt.Net.Vc |
| OID4VCI | Standard issuance protocol and metadata | SdJwt.Net.Oid4Vci |
| OID4VP | Standard presentation protocol and trust | SdJwt.Net.Oid4Vp |
What a verifier can decide from an SD-JWT VC¶
| Claim | Verifier question it answers |
|---|---|
vct |
What type of credential is this? |
iss |
Who issued it, and do I trust that issuer for this type? |
status |
Has this credential been revoked or suspended? |
cnf |
Is the presenter the legitimate holder? |
exp / nbf |
Is this credential within its validity period? |
| Disclosed claims | Do the specific facts meet my business requirements? |
SD-JWT VC structure¶
An SD-JWT VC is still an SD-JWT artifact, but its signed payload carries VC-specific semantics.
Header example¶
{
"alg": "ES256",
"typ": "dc+sd-jwt"
}
The typ header value dc+sd-jwt identifies this as a Digital Credential using SD-JWT format.
Payload example¶
{
"iss": "https://university.example.edu",
"iat": 1704067200,
"exp": 1735689600,
"vct": "https://credentials.example.com/UniversityDegree",
"vct#integrity": "sha256-WZO1qV8aVgE8H0wc8c5VpVqQjYK...",
"sub": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"cnf": {
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "TCAER19Zvu3OHF4j4W4vfSVoHIP1ILilDls7vCeGemc",
"y": "ZxjiWWbZMQGHVWKVQ4hbSIirsVfuecCE6t4jT9F2HZQ"
}
},
"status": {
"status_list": {
"idx": 42,
"uri": "https://university.example.edu/status/degrees"
}
},
"given_name": "Alice",
"family_name": "Smith",
"degree": "Bachelor of Science",
"graduation_year": 2024,
"_sd": ["JnPBS7TpL8...", "xF9bZ8cQ2Y..."],
"_sd_alg": "sha-256"
}
Claim reference¶
| Claim/Header | Location | Purpose |
|---|---|---|
typ = dc+sd-jwt |
Header | Identifies SD-JWT VC format (legacy: vc+sd-jwt) |
vct |
Payload | Verifiable Credential Type identifier |
vct#integrity |
Payload | Optional hash binding for external type metadata |
iss |
Payload | Credential issuer identifier (URL or DID) |
sub |
Payload | Subject identifier (policy-dependent) |
status |
Payload | Status list reference for revocation/suspension |
cnf |
Payload | Holder binding key material |
iat |
Payload | Issuance timestamp |
exp |
Payload | Expiration timestamp |
_sd, _sd_alg |
Payload | Selective disclosure digest structures |
Lifecycle¶
Phase 1: Issuance¶
The issuer creates a Verifiable Credential with proper semantics:
using SdJwt.Net.Vc.Issuer;
using SdJwt.Net.Vc.Models;
using SdJwt.Net.Issuer;
using Microsoft.IdentityModel.Tokens;
// Create VC issuer
var issuer = new SdJwtVcIssuer(issuerSigningKey, SecurityAlgorithms.EcdsaSha256);
// Build VC payload with required claims
var vcPayload = new SdJwtVcPayload
{
Issuer = "https://university.example.edu",
Subject = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
IssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
ExpiresAt = DateTimeOffset.UtcNow.AddYears(4).ToUnixTimeSeconds(),
// Domain-specific claims via AdditionalData
AdditionalData = new Dictionary<string, object>
{
["given_name"] = "Alice",
["family_name"] = "Smith",
["degree"] = "Bachelor of Science",
["graduation_year"] = 2024,
["gpa"] = 3.85
}
};
// Configure selective disclosure
var options = new SdIssuanceOptions
{
DisclosureStructure = new
{
given_name = true,
family_name = true,
gpa = true // GPA can be hidden
// degree and graduation_year always visible
}
};
// Issue the credential (vctIdentifier is the first parameter)
var result = issuer.Issue(
"https://credentials.example.com/UniversityDegree",
vcPayload,
options,
holderPublicJwk);
What the holder receives:
eyJhbGciOiJFUzI1NiIsInR5cCI6ImRjK3NkLWp3dCJ9.eyJpc3MiOiJodHRwczovL3VuaXZlcnNpdHkuZXhhbXBsZS5lZHUiLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLmV4YW1wbGUuY29tL1VuaXZlcnNpdHlEZWdyZWUiLCJkZWdyZWUiOiJCYWNoZWxvciBvZiBTY2llbmNlIiwiX3NkIjpbLi4uXX0.sig~disclosure1~disclosure2~disclosure3~
Phase 2: Holder Presentation¶
When a verifier requests proof, the holder selects which claims to disclose:
using SdJwt.Net.Holder;
// Holder creates selective presentation
var holder = new SdJwtHolder(result.Issuance);
// Employer only needs name and degree - NOT GPA
var presentation = holder.CreatePresentation(
disclosure => disclosure.ClaimName == "given_name" ||
disclosure.ClaimName == "family_name",
new JwtPayload
{
["aud"] = "https://employer.example.com",
["nonce"] = "employer-hiring-2024-abc123",
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
},
holderPrivateKey,
SecurityAlgorithms.EcdsaSha256
);
Phase 3: Verification¶
The verifier validates the credential with VC-layer checks (type, status, issuer, expiry) on top of base SD-JWT signature and disclosure verification:
using SdJwt.Net.Vc.Verifier;
using Microsoft.IdentityModel.Tokens;
var verifier = new SdJwtVcVerifier(
issuerKeyProvider: async jwt => await FetchIssuerKey(jwt));
var policy = new SdJwtVcVerificationPolicy
{
ExpectedVctType = "https://credentials.example.com/UniversityDegree",
RequireStatusCheck = true,
StatusValidator = new StatusListSdJwtVcStatusValidator(httpClient)
};
var validationParams = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = new[] { "https://university.example.edu" },
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
var kbJwtParams = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudiences = new[] { "https://employer.example.com" }
};
var result = await verifier.VerifyAsync(
presentation,
validationParams,
kbJwtValidationParameters: kbJwtParams,
expectedKbJwtNonce: "employer-hiring-2024-abc123",
expectedVctType: "https://credentials.example.com/UniversityDegree",
verificationPolicy: policy);
// Result is a record: SdJwtVcVerificationResult
// Properties: ClaimsPrincipal, KeyBindingVerified, VerifiableCredentialType, SdJwtVcPayload
var givenName = result.ClaimsPrincipal.FindFirst("given_name")?.Value;
var vcType = result.VerifiableCredentialType;
Verification flow diagram¶
sequenceDiagram
autonumber
participant Issuer
participant Wallet
participant Verifier
participant Status as Status Endpoint
Issuer->>Issuer: Build VC payload (vct, iss, claims)
Issuer->>Issuer: Add _sd digests + cnf + status reference
Issuer-->>Wallet: SD-JWT VC issuance
Wallet-->>Verifier: Selective presentation
Verifier->>Verifier: Verify SD-JWT signature
Verifier->>Verifier: Validate VC semantics (typ, vct, issuer trust)
Verifier->>Status: Fetch status list token
Status-->>Verifier: statuslist+jwt
Verifier->>Verifier: Check credential status at idx
Verifier-->>Verifier: Policy decision (accept/reject)
Common use cases¶
University degree credential¶
{
"vct": "https://credentials.example.com/UniversityDegree",
"given_name": "Alice",
"family_name": "Smith",
"degree": "Bachelor of Science",
"major": "Computer Science",
"graduation_year": 2024,
"gpa": 3.85
}
Selective disclosure scenarios:
- Job application: disclose name, degree, major (hide GPA)
- Graduate school: disclose all including GPA
- Professional network: disclose degree type only
Driver's license credential¶
{
"vct": "https://credentials.example.com/DriverLicense",
"given_name": "Bob",
"family_name": "Jones",
"birth_date": "1990-05-15",
"license_class": "C",
"address": {
"street": "123 Main St",
"city": "Springfield",
"state": "CA"
}
}
Selective disclosure scenarios:
- Age verification: derive "over 21" without revealing birth_date
- Car rental: disclose license_class only
- Address verification: disclose city/state only (hide street)
Employment credential¶
{
"vct": "https://credentials.example.com/Employment",
"given_name": "Carol",
"family_name": "Davis",
"employer": "Acme Corp",
"title": "Senior Engineer",
"start_date": "2020-03-01",
"salary": 150000
}
Selective disclosure scenarios:
- Background check: disclose employer, title, dates (hide salary)
- Mortgage application: disclose salary verification
- Professional network: disclose title only
Implementation references¶
| Component | File | Description |
|---|---|---|
| VC issuer | SdJwtVcIssuer.cs | Create SD-JWT VCs |
| VC payload model | VerifiableCredentialPayload.cs | Payload structure |
| VC verifier | SdJwtVcVerifier.cs | Validate VCs |
| Status validator | StatusListSdJwtVcStatusValidator.cs | Status list integration |
| Type metadata | TypeMetadataResolver.cs | Resolve vct metadata |
| Package overview | README.md | Quick start guide |
| Sample code | VerifiableCredentialsExample.cs | Working examples |
Beginner pitfalls to avoid¶
1. Signature validation alone is not enough¶
A valid signature does not mean you should trust the credential. You must also validate:
vctmatches an accepted credential type for your use caseissis a trusted issuer for this credential type- Status check passes (if
statusclaim present) - Key binding is valid (if required by policy)
// WRONG - signature only
if (jwt.SignatureValid) { Accept(); }
// RIGHT - full VC validation with policy and TokenValidationParameters
var result = await vcVerifier.VerifyAsync(
presentation,
validationParameters,
verificationPolicy: new SdJwtVcVerificationPolicy
{
RequireStatusCheck = true,
StatusValidator = statusValidator
});
2. Ignoring status checks¶
If a credential has a status claim, status checking should be part of your verification policy. A revoked credential may still have a valid signature.
3. Treating legacy type header as new standard¶
The type header changed from vc+sd-jwt to dc+sd-jwt. Accept both for compatibility but use dc+sd-jwt for new credentials.
4. Exposing high-sensitivity claims by default¶
Keep sensitive claims (SSN, medical data, financial info) selectively disclosable by default, not always visible in the base JWT payload.
Frequently asked questions¶
Q: What is the difference between SD-JWT VC and W3C VCDM 2.0?¶
A: They are parallel, independent specifications from different standards bodies. SD-JWT VC (IETF) uses vct and _sd arrays and has no JSON-LD dependency. W3C VCDM 2.0 uses @context, type[], and issuer/credentialSubject properties with JSON-LD semantics. In OID4VCI, dc+sd-jwt maps to SD-JWT VC (SdJwt.Net.Vc), while jwt_vc_json and ldp_vc map to VCDM 2.0 (SdJwt.Net.VcDm). See the W3C VCDM 2.0 for full details.
Q: What is the difference between SD-JWT and SD-JWT VC?¶
A: SD-JWT is the base format for selective disclosure. SD-JWT VC adds credential semantics on top - type identification (vct), status checking, and standardized claim names that allow verifiers to understand what the credential represents.
Q: Can I issue a credential without a status list?¶
A: Yes, the status claim is optional. However, without it you cannot revoke or suspend the credential after issuance. Consider your credential lifecycle requirements.
Q: How do I choose what to make selectively disclosable?¶
A: Credential type, issuer, and expiration should always be visible since verifiers need them. Personal data and sensitive information should be selectively disclosable. If a verifier might need a claim but not always, make it disclosable.
Q: What happens if the verifier does not support my credential type?¶
A: The verifier should reject credentials with unknown vct values. This prevents accepting credentials outside the trust framework.
Q: Can I have multiple credential types in one SD-JWT VC?¶
A: No, each SD-JWT VC has exactly one vct value. For multiple credential types, issue separate credentials.
What SD-JWT VC does not do¶
SD-JWT VC defines credential semantics on top of SD-JWT. It does not by itself define:
- How the credential is delivered to a wallet (see OID4VCI)
- How a verifier requests a presentation (see OID4VP)
- Which issuers are trusted (application-level trust policy)
- Which claims are required for a business decision (application policy)
- How status endpoints are operated (deployment responsibility; see Status List)
- How user consent is captured or displayed (wallet UX responsibility)
Related concepts¶
- SD-JWT - Base SD-JWT format
- Status List - Revocation and suspension
- OID4VCI - Credential issuance protocol
- OID4VP - Presentation protocol
- W3C VCDM 2.0 -
jwt_vc_json/ldp_vccredential model (SdJwt.Net.VcDm)