A signed JWT is all or nothing: the holder must reveal every claim the issuer signed, even when the verifier only needs one. SD-JWT, published as RFC 9901 in November 2025, fixes that by letting an issuer sign salted digests of claims so the holder can later reveal only the claims a verifier asks for. SD-JWT VC, still an IETF Internet-Draft, builds a credential format on top of it, and it is one of the two mandatory formats for the EU Digital Identity Wallet and its Person Identification Data (PID). This guide explains how disclosures, digests and key binding work, what an SD-JWT VC adds, how verifiers must validate a presentation, and where implementations go wrong. It is written for developers and architects building issuers, wallets or verifiers, and for security teams reviewing them.
Key takeaways
- Selective disclosure is done with salted hashes, not new cryptography. The issuer signs a JWT that holds digests; the holder sends the matching cleartext disclosures only for the claims it chooses to reveal.
- Key binding is what stops replay. Without a Key Binding JWT carrying
nonce,audandsd_hash, anyone who copies a presentation can reuse it. The verifier decides whether key binding is required by policy, never by whether one happened to be sent. - SD-JWT VC adds the credential layer. It defines
typ: dc+sd-jwt, the requiredvcttype claim, issuer key discovery, Type Metadata and status checking, and lists the claims that must never be selectively disclosable. - Selective disclosure is not unlinkability. The same SD-JWT shown twice carries the same signature and digests, so verifiers can correlate it. Batch issuance of single-use credentials is the main mitigation.
- Status and drafts move. RFC 9901 is final, but SD-JWT VC (draft 19 at the time of writing) and Token Status List (in the RFC Editor queue) are not. Pin the versions your ecosystem profile names, such as OpenID4VC HAIP 1.0.
How SD-JWT selective disclosure works
RFC 9901 defines three objects: the Issuer-signed JWT, a set of Disclosures, and an optional Key Binding JWT (KB-JWT). The mechanism works in four steps.
- Build a Disclosure per claim: for an object property the issuer creates a JSON array of a salt, the claim name and the claim value, then base64url-encodes it (Section 4.2.1). For an array element the array has only a salt and the value (Section 4.2.2). The salt should carry 128 bits of cryptographically secure randomness, and the issuer must not reveal salts to anyone but the holder.
- Hash each Disclosure: the digest is computed over the US-ASCII bytes of the base64url-encoded Disclosure, not the decoded JSON, and is itself base64url-encoded (Section 4.2.3). The hash algorithm is named in the top-level
_sd_algclaim; when absent it is SHA-256, which every implementation must support. - Embed digests and sign: object property digests go into an
_sdarray, which the issuer should shuffle so claim order leaks nothing. Hidden array elements are replaced in place by{"...": "<digest>"}. The issuer may add decoy digests that match no Disclosure to hide how many claims exist, then signs the JWT. Thenonealgorithm is forbidden. - Present a subset: the holder sends
<Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure N>~with only the Disclosures it chooses, optionally followed by a KB-JWT after the last tilde. The trailing tilde is required when there is no KB-JWT.
Disclosures can be recursive: a Disclosure may itself contain digests of further Disclosures, so nested structures such as an address object can be hidden as a whole or field by field (Section 4.2.6). A Disclosure is valid only if it connects to the signed payload, directly or through other Disclosures. RFC 9901 also defines an optional JWS JSON serialisation (Section 8) with disclosures and kb_jwt unprotected header parameters, but compact serialisation is what most ecosystems require.
Terms you will meet
| Term | What it is | Where defined |
|---|---|---|
| Disclosure | base64url-encoded JSON array of salt, claim name (for objects) and value | RFC 9901 Section 4.2 |
_sd | Array of digests for selectively disclosable object properties | RFC 9901 Section 4.2.4.1 |
... | Key used in place of a hidden array element | RFC 9901 Section 4.2.4.2 |
_sd_alg | Hash algorithm for digests; SHA-256 by default | RFC 9901 Section 4.1.1 |
cnf | Holder public key confirmation claim (RFC 7800), usually a jwk | RFC 9901 Section 4.1.2 |
| KB-JWT | Holder-signed JWT with typ: kb+jwt, iat, aud, nonce, sd_hash | RFC 9901 Section 4.3 |
| SD-JWT+KB | An SD-JWT presented with a KB-JWT appended | RFC 9901 Section 4 |
vct | Verifiable credential type identifier, required in SD-JWT VC | SD-JWT VC draft |
Key binding: turning a token into a presentation
A bare SD-JWT is a bearer artefact: whoever holds the string can present it. Key binding ties the credential to a key the holder controls. The issuer places the holder's public key in cnf, and at presentation time the holder signs a KB-JWT with the matching private key.
- Header:
typmust bekb+jwtandalgmust not benone. - nonce and aud: the verifier supplies a fresh nonce and its own identifier; the KB-JWT repeats both so a captured presentation cannot be replayed to another verifier or later session.
- sd_hash: a hash over the SD-JWT exactly as presented, including the tildes and the selected Disclosures (Section 4.3.1). This stops an attacker adding or removing Disclosures after the holder signed.
- iat: the verifier checks it falls in an acceptable window, which limits the useful life of a captured presentation.
The single most important verifier rule is in Section 7.3: whether key binding is required is a matter of verifier policy. A verifier that accepts a presentation without a KB-JWT simply because none was sent has turned a holder-bound credential back into a bearer token. Where OpenID4VC HAIP 1.0 applies, a KB-JWT must always be present when a credential has cryptographic holder binding. In wallets the private key normally lives in secure hardware on the device; see the Wallet Unit Attestation and Device-Bound Keys Guide for how issuers learn that a key is hardware-protected.
What SD-JWT VC adds
RFC 9901 is a token format, not a credential model. The SD-JWT VC specification (draft-ietf-oauth-sd-jwt-vc-19, 31 August 2026, IESG state "Waiting for AD go-ahead" when checked on 27 October 2026) adds the pieces a credential ecosystem needs. Because it is a draft, check the current revision before relying on any detail below.
| Element | Requirement in SD-JWT VC | Why it matters |
|---|---|---|
Media type and typ | application/dc+sd-jwt; header typ: dc+sd-jwt | Explicit typing stops a credential being accepted as another kind of JWT. Earlier drafts used vc+sd-jwt until November 2024 |
vct | Required, case-sensitive, collision-resistant type identifier; aka_vcts lists additional types | Verifiers match on type; breaking changes should get a new vct |
| Never selectively disclosable | iss, nbf, exp, cnf, vct, vct#integrity, aka_vcts, status | Validity, binding and type must always be visible to the verifier |
| May be selectively disclosable | sub, iat | Lets issuers avoid a stable subject identifier where it is not needed |
| Issuer key discovery | JWT VC Issuer Metadata at /.well-known/jwt-vc-issuer for HTTPS iss, or an x5c chain validated against a policy-permitted trust anchor | If the key cannot be tied to the issuer, the credential must be rejected |
| Type Metadata | Optional document describing claims and display; vct#integrity carries its hash | Integrity hash allows safe caching and prevents silent changes |
| Status | status claim, usually pointing to a Token Status List entry; should be checked when present | Revocation and suspension without contacting the issuer per credential |
The draft also sets retrieval rules for anything a verifier dereferences: HTTPS only, limited redirects that stay on HTTPS, and time-bounded and size-bounded requests. Its Security Considerations treat iss and vct derived URLs as untrusted input and a vector for server-side request forgery, so verifiers must block loopback, link-local and private ranges, including on every redirect target. See the SSRF Prevention Guide.
Revocation with Token Status List
SD-JWT VCs are usually short to medium lived, but issuers still need to revoke or suspend them. The companion IETF draft Token Status List (draft-ietf-oauth-status-list-21, in the RFC Editor queue when checked) defines a compressed bit array published as a signed statuslist+jwt. Each credential carries status.status_list.idx and status.status_list.uri; the verifier fetches the list, checks the signature and reads the bits at its index. Values are 0x00 VALID, 0x01 INVALID and 0x02 SUSPENDED, with entries of 1, 2, 4 or 8 bits, and a ttl tells verifiers how long they may cache the list.
- Herd privacy: because many credentials share one list, the issuer cannot tell which credential a verifier is checking. That breaks if an issuer uses a list or URI per credential.
- Unpredictable indices: HAIP 1.0 requires each credential to get its own unpredictable index, and the draft recommends random indices and decoy entries.
- Expiry wins: an expired credential is invalid even if its status bit says VALID.
Verifying an SD-JWT VC presentation
Verification order matters. A verifier that decodes Disclosures before checking the signature, or that trusts iss before resolving its key through an approved mechanism, invites forgery. The sequence below follows RFC 9901 Section 7 and the SD-JWT VC verification rules.
- Parse and type-check: split on tildes, confirm the header
typisdc+sd-jwtand the algorithm is on your allowlist (for HAIP ecosystems at least ES256). - Resolve and validate the issuer key: use only the mechanisms your policy permits for that issuer: an
x5cchain to a trust anchor from your trusted list, or pinned JWT VC Issuer Metadata. Reject on failure. - Verify the issuer signature over the Issuer-signed JWT.
- Check
_sd_algis supported, then hash each received Disclosure and match it to exactly one digest. Reject duplicate digests, Disclosures that match nothing and Disclosures whose claim name collides with a plaintext claim. - Rebuild the payload by inserting disclosed claims and removing
_sd,_sd_algand unmatched array placeholders. - Validate time claims:
exp,nbfand, where used,iat. - Verify key binding when policy requires it: KB-JWT
typ, signature with thecnfkey,iatwindow, yournonce, youraud, andsd_hashover the presented string. - Check status through the Token Status List if
statusis present. - Apply business rules: confirm the
vctis one you accept and that the claims you requested are present. Discard anything you did not need.
OWASP ASVS 5.0 captures the token basics behind steps 1 to 7: V9.1.1 (validate signature before use), V9.1.2 (algorithm allowlist), V9.2.1 (validity time span) and V9.2.3 (audience). For general JWT pitfalls such as algorithm confusion, see the JWT Security Guide.
Privacy: selective disclosure is not unlinkability
SD-JWT hides values, not identity. Every presentation of the same SD-JWT carries the same issuer signature, the same digests and, if key-bound, the same cnf key. Two verifiers comparing notes, or a verifier colluding with the issuer, can link those presentations. RFC 9901 Section 10.1 covers unlinkability, and SD-JWT VC adds that its considerations apply especially to cnf.
| Privacy risk | Cause | Mitigation |
|---|---|---|
| Verifier to verifier linking | Same signature, digests and cnf key in every presentation | Issue a batch of credentials with distinct salts and keys and use each once; the EUDI ARF describes PID providers issuing batches and requiring a different one per presentation |
| Type leakage | vct and aka_vcts are always visible | Choose vct values that do not reveal nationality or other personal context |
| Issuer phone-home | Holder-specific iss values or remote cnf key references (x5u, jku, URL kid) make verifiers contact the issuer | Pin issuer identifiers, monitor identifier churn, do not resolve remote key references in cnf |
| Status tracking | Per-credential status list or URI | Shared lists, random indices, decoy entries |
| Over-collection | Verifier asks for full credential | Request named claims only; under OpenID4VP DCQL the wallet must not send claims that were not requested |
| Claim count inference | Number of digests reveals how many claims exist | Decoy digests made from random input |
For real zero-knowledge properties such as proving "over 18" without a stable value, SD-JWT alone is not enough: issuers typically issue a dedicated age_over_18 style boolean claim. Data minimisation still falls on the verifier under GDPR Article 5(1)(c). See the Privacy by Design Guide for Engineering Teams.
Choosing a format: SD-JWT VC, mdoc or W3C VCDM
The EUDI Architecture and Reference Framework (ARF 3.0.0, July 2026) lists three attestation formats. Wallet support for ISO/IEC 18013-5 mdoc and SD-JWT VC is mandatory; W3C VCDM 2.0 is optional.
| Factor | SD-JWT VC | ISO/IEC 18013-5 mdoc | W3C VCDM 2.0 |
|---|---|---|---|
| Encoding | JSON, JWT | CBOR | JSON-LD |
| Disclosure mechanism | Salted hashes | Salted hashes | Depends on securing mechanism |
| Main use in EUDI | Remote presentation via OpenID4VP and HAIP; not proximity | Proximity (NFC, BLE) and remote | Optional, needs profiling |
| Developer fit | Existing JOSE libraries and JWT skills | Requires CBOR and COSE tooling | Linked data processing |
| Choose when | Web and app relying parties, enterprise and attestation use cases | Driving licence and in-person checks | An ecosystem already standardised on VCDM |
Small relying parties should usually accept whichever format their national wallet profile mandates rather than choose. Large issuers often issue the same attestation in both SD-JWT VC and mdoc. See the Mobile Driving Licence (mDL) Guide and the Verifiable Credentials Guide for the wider model.
Implementation roadmap
- Pick a profile first: decide which ecosystem profile governs you (OpenID4VC HAIP 1.0, an EUDI attestation rulebook, a national scheme). HAIP 1.0 names specific pre-final versions of SD-JWT VC and Token Status List; pin those.
- Use a maintained library: do not hand-roll digest matching or tilde parsing. Run the library against the RFC 9901 examples and your profile's conformance tests.
- Design the credential: choose the
vct, decide per claim whether it is always disclosed, selectively disclosable, or split into derived claims (for exampleage_over_18), and decide on decoy counts. - Set up issuer keys: HSM-backed signing keys, an
x5cchain or JWT VC Issuer Metadata, and a published rotation process. See the JWKS Signing Key Rotation Guide. - Build the verifier in audit mode: verify and log results without gating the business decision, then compare failure rates by issuer and wallet.
- Turn on enforcement in rings: internal users, then a pilot cohort, then everyone. Keep a feature flag that falls back to your previous identity check, not to accepting unverified credentials.
- Add status checks and batch issuance once the core path is stable, and test revocation end to end.
Detection and measurement
Log verification outcomes without logging disclosed personal data. Record the vct, issuer, result and failure reason, not the claim values. See the JWT and Session Tokens in Code and Logs Guide.
- Verification failure rate by reason: signature, unknown issuer, digest mismatch, missing KB-JWT, nonce or audience mismatch, revoked. A spike in one reason is either a broken issuer or an attack.
- Nonce reuse attempts: any repeat should be zero; a non-zero count means replay.
- Presentations without key binding for credential types that carry
cnf: target zero. - Status check coverage: share of presentations with a
statusclaim that were actually checked against a fresh list. - Claims requested versus claims used: fields requested that no downstream process consumes indicate over-collection.
- Metadata fetch errors and blocked SSRF attempts on issuer and type metadata retrieval.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST CSF 2.0 | PR.AA-03 | Authenticating users and services with verified credentials |
| NIST CSF 2.0 | PR.DS-02 | Protecting credential data in transit |
| NIST SP 800-53 Rev. 5 | IA-5, SC-12, SC-13 | Authenticator management, issuer and holder key management, approved cryptography |
| NIST SP 800-53 Rev. 5 | PT-2 | Authority to process the personal data requested |
| OWASP ASVS 5.0 | V9.1.1, V9.1.2, V9.2.1, V9.2.3 | Signature, algorithm allowlist, validity and audience checks for self-contained tokens |
| ISO/IEC 27001:2022 Annex A | 5.34, 8.24 | Privacy and protection of PII; use of cryptography |
Common failure patterns
- Accepting presentations without a KB-JWT: the verifier checks key binding only when one is present. RFC 9901 says the decision is policy, so an attacker simply strips it.
- Trusting
issas a URL to fetch keys from: any attacker can host metadata. Keys must chain to a trust anchor or a pinned issuer your policy permits, and the fetch itself must be SSRF-safe. - Hashing the decoded Disclosure: digests are over the base64url string. Implementations that hash the JSON fail interop or, worse, accept mismatched Disclosures through lenient fallbacks.
- Making validity claims selectively disclosable: if
exporstatuscan be hidden, a holder can present an expired or revoked credential. SD-JWT VC forbids this; custom profiles sometimes reintroduce it. - Reusing one credential everywhere: a wallet that presents a single SD-JWT to every verifier gives them a correlation handle, defeating the privacy goal.
- Weak or reused salts: low-entropy salts let an attacker brute-force undisclosed values such as a birth date from the digest.
Practitioner checklist
- Name the profile and exact draft versions you implement (for example HAIP 1.0 and the SD-JWT VC and Token Status List versions it pins).
- Require
typ: dc+sd-jwt, an algorithm allowlist and rejection ofnone. - Resolve issuer keys only through policy-permitted mechanisms and trust anchors; reject otherwise.
- Make key binding mandatory by policy for every credential type that carries
cnf, with fresh nonces and your ownaud. - Verify
sd_hash, reject duplicate and unmatched Disclosures, and validateexpandnbf. - Check Token Status List entries and honour
ttl. - Use 128-bit random salts, decoy digests and shuffled
_sdarrays when issuing. - Issue batches of single-use credentials where unlinkability matters.
- Apply SSRF controls to every metadata and status fetch.
- Request only the claims a decision needs, and log outcomes without claim values.
Standards and references
- IETF: RFC 9901 Selective Disclosure for JSON Web Tokens (SD-JWT)
- IETF: SD-JWT-based Verifiable Digital Credentials (SD-JWT VC), Internet-Draft
- IETF: Token Status List, Internet-Draft
- OpenID Foundation: OpenID4VC High Assurance Interoperability Profile 1.0
- OpenID Foundation: OpenID for Verifiable Presentations 1.0
- European Commission: EUDI Wallet Architecture and Reference Framework
- OWASP: Application Security Verification Standard 5.0
Related NHI Mgmt Group resources: Verifiable Credentials Guide · OpenID for Verifiable Presentations Guide · OpenID for Verifiable Credential Issuance Guide · Digital Identity Wallets Guide · EUDI Wallet Architecture and Reference Framework Guide