Selective Disclosure for JWTs (SD-JWT)

an issuer signs a JWT whose claims are replaced by salted hashes, so the holder can later reveal only the claims a verifier needs, optionally proving it holds the bound key.

Where it stands
Published RFC
Official text
RFC 9901
Documented support
0
Activity
6 signals in 90 days

Read the RFC side by sideExplainer source on GitHub

When you reach for it

  • A credential is issued once and presented many times: a JWT with many claims goes to a holder, and each verifier gets only the disclosures it asks for while the issuer's signature still verifies (Sections 1, 3.1, 3.2).
  • A verifier must know the presenter is the legitimate holder: the issuer puts the holder's public key in cnf, and the holder signs a Key Binding JWT over the presentation that carries a nonce and the verifier's aud (Sections 3.3, 4.1.2, 4.3).
  • Verifiers must not learn how many claims the credential hides: decoy digests pad the _sd array so the count of real claims is not visible (Sections 4.2.5, 10.4).
  • A credential must not become a tracking token across verifiers: unlinkability depends on fresh salts and Key Binding keys per credential, with batch issuance so the holder can use a different credential per verifier (Section 10.1).
Diagram
flowchart LR
    A["Issuer signs a JWT with all of a user's claims and hands it to the holder"] -->|"plain JWT presented to a verifier"| B["Every claim is revealed, or the signature breaks when some are removed"]
    A -->|"SD-JWT: claims replaced by salted digests, disclosures carried separately"| C["Holder sends the issuer-signed JWT plus only the disclosures the verifier needs, and optionally a KB-JWT"]

The problem it solves

A JWT is signed as a whole, so a holder who wants to show a verifier only a date of birth from a credential that also carries name and address has two bad options: show everything, or go back to the issuer for a new token. SD-JWT is built on salted hashes: the issuer puts a digest of each selectively disclosable claim into the signed payload, and the claim's actual salt, name and value travel in a separate Disclosure (Sections 1, 3.1). Disclosures can be omitted without breaking the signature, and modifications to them can be detected (Section 3.1).

The spec defines two formats. An SD-JWT is the Issuer-signed JWT plus zero or more Disclosures. An SD-JWT+KB adds a Key Binding JWT so the verifier can check that the presenter controls the key the issuer bound to the credential (Section 1.1).

How it works

Diagram
flowchart TB
    SD["SD-JWT+KB = Issuer-signed JWT ~ Disclosure 1 ~ ... ~ Disclosure N ~ KB-JWT"]
    SD --> I["Issuer-signed JWT: _sd array of digests, _sd_alg, cnf with holder key, plus always-visible claims"]
    SD --> D["Disclosure: base64url of salt, claim name, claim value"]
    SD --> K["KB-JWT: typ kb+jwt, iat, aud, nonce, sd_hash, signed with the holder key"]
    D -->|"hash of the US-ASCII bytes of the Disclosure"| I
    K -->|"sd_hash over Issuer-signed JWT ~ selected Disclosures ~"| SD

The Issuer-signed JWT must be signed with the issuer's private key (Section 4.1). For each selectively disclosable object property the issuer creates a Disclosure, a base64url-encoded JSON array of a salt, the claim name and the claim value; array elements use a two-element array of salt and value (Sections 4.2.1, 4.2.2). The digest is computed over the US-ASCII bytes of the base64url-encoded Disclosure (Section 4.2.3) and placed in an _sd array whose order the issuer must hide (Section 4.2.4.1). _sd_alg names the hash algorithm from the Named Information Hash Algorithm Registry, defaulting to sha-256 (Section 4.1.1). The holder's public key, or a reference to it, sits in cnf (Section 4.1.2). The compact format joins everything with ~, and a bare SD-JWT ends with a trailing ~, which is how the two formats are told apart (Section 4). The KB-JWT carries typ of kb+jwt and the required claims iat, aud, nonce and sd_hash, where sd_hash is the digest, using the same algorithm as the Disclosures, over the Issuer-signed JWT and the selected Disclosures (Sections 4.3, 4.3.1).

What the spec requires

  • The salt MUST be unique for each selectively disclosed claim, and the claim name MUST NOT be _sd, ..., or a name already present as a permanently disclosed claim in the same object (Section 4.2.1).
  • If _sd_alg is absent at the top level, sha-256 MUST be used (Section 4.1.1).
  • Any holder or verifier processing an SD-JWT MUST reject it if any digest value is encountered more than once, or if any Disclosure was not referenced by a digest; a digest with no matching Disclosure MUST be ignored; nbf, exp and aud are checked on the processed payload (Section 7.1).
  • A holder that receives an SD-JWT+KB MUST reject it (Section 7.2).
  • A verifier MUST decide whether Key Binding is required before verifying, and that decision MUST NOT depend on whether the holder provided a KB-JWT. If required and missing, the verifier MUST reject the presentation; the KB-JWT must then verify with the holder key, none MUST NOT be accepted, typ must be kb+jwt, iat within an acceptable window, nonce and aud validated, and sd_hash recomputed and matched (Sections 7.3, 9.5).
  • The JWS JSON Serialization form is OPTIONAL to support (Section 8).

Watch out

  • The JWT MUST be signed by the issuer; the verifier MUST check the Disclosures to ensure the claim values are correct (Sections 9.1, 9.2).
  • Each salt MUST be cryptographically random and sufficiently long. The RECOMMENDED minimum is 128 bits of randomness, and the hash function MUST be second-preimage resistant (Sections 9.3, 9.4).
  • An issuer MUST NOT make any claim selectively disclosable that is critical for evaluating the credential's validity (Section 9.7).
  • Without Key Binding, anyone holding an SD-JWT can forward it to any third party that does not enforce Key Binding, and the SD-JWT can be modified by adding or removing Disclosures while staying valid (Sections 9.9, 9.10).
  • Application profiles are RECOMMENDED to set an explicit typ on the Issuer-signed JWT with a media type of the form application/example+sd-jwt (Section 9.11).
  • The spec provides no confidentiality: when user privacy or correlation by passive observers is a concern, implementers MUST ensure the transport protocol provides it. New Key Binding keys and salts MUST be used for each credential in a batch (Sections 10.1, 10.3).

Sources

How it relates to other specs

Relations diagram
flowchart LR
  s_sd_jwt["Selective Disclosure for JWTs"]
  s_jwt["JSON Web Token"]
  s_sd_jwt_vc["SD-JWT-based Verifiable Credentials"]
  s_delegate_sd_jwt["Delegate SD-JWT"]
  style s_sd_jwt stroke-width:3px
  s_sd_jwt -->|Builds on| s_jwt
  s_sd_jwt_vc -->|Profiles| s_sd_jwt
  s_delegate_sd_jwt -->|Extends| s_sd_jwt

Documented support

Adoption lists only support stated in official documentation (the vendor's or project's own, or a standards body's page naming it), and a weekly check confirms the cited phrase is still on that page. Experimental, alpha and beta support is counted separately as preview.

No sourced support entries collected yet. This says nothing about how widely it is used.

Activity

Revisions from the Datatracker, explanations from this site's daily runs, and mailing-list subjects naming the draft.

Explanations on this site

  • Substantive changeOAuth WG

    In oauth-delegate-sd-jwt, cnf, exp, nbf and the other security claims move out of delegate_payload into the base KB-SD-JWT payload, with defined semantics.

    oauth-wg/oauth-delegate-sd-jwt#15: Move security claims to base KB-SD-JWT payload and define claim semantics

    Read the explanation

    PR #15, which fixes #3 and #10, moves the security and validity claims (cnf, exp, nbf and so on) out of delegate_payload into the base KB-SD-JWT payload. Standard JWT and SD-JWT libraries can then evaluate them without first unpacking the delegate payload. When several delegate_payload entries are present, they share the base KB-SD-JWT's security claims. The PR also adds a section defining how cnf, iat, exp, nbf, aud, nonce, sd_hash and issuer_jwt_hash are interpreted when a KB-SD-JWT is delegated to a new Delegate Holder, versus how subsequent Delegate Holders and Verifiers interpret them, and updates the Verification section to match. Only the PR description was available, not the diff.

    • A library implementer who wants existing SD-JWT KB verification to check cnf and expiry on a delegated presentation without delegate-specific unpacking.
    • An agent that re-delegates a credential to another holder and needs the audience and nonce semantics to be unambiguous.
  • Substantive changeOAuth WG

    This PR makes the types of an SD-JWT VC exactly those the Issuer asserts in vct and aka_vcts, and drops the extends-based matching.

    oauth-wg/oauth-sd-jwt-vc#457: Clarify that the types of an SD-JWT VC are those asserted in vct and aka_vcts

    Read the explanation

    Since -25, Section 2.2.2.2 says a credential is of a type if it is the vct value or listed in aka_vcts. The PR removes two leftovers in Section 5.4: a MAY that let a Consumer also match types through the extends property of Type Metadata, and a SHOULD that the vct type extend each type in aka_vcts, which cannot hold for unrelated types since a type can extend only one other. extends now only determines which Type Metadata is inherited. Section 5.4 points to the recommendation in 2.2.2.2 to list extended types in aka_vcts, and Section 7.7 now says aka_vcts types are asserted by the Issuer like the vct type, with the MUST NOT unchanged. The PR says there is no new requirement, that OpenID4VP 1.1 already matches vct_values on vct and aka_vcts, and that ISO/IEC 18013-5 will cite a dated draft at the end of November.

    • A verifier matching vct_values must not infer extra types from Type Metadata extends, so results do not depend on which side fetched metadata.
    • An Issuer whose credential type extends a parent type that verifiers request should list that parent in aka_vcts.
    • An ISO/IEC 18013-5 mdoc profile that delegates type matching to this document.
    • Related in OpenID: openid/OpenID4VP. The PR states OpenID4VP 1.1 already matches vct_values on vct and aka_vcts and treats type extension as out of band knowledge.
  • Substantive changeOpenID

    Openid/dchp saw 23 commits of spec edits: a field rename, credential_types as an array, and mdoc and SD-JWT VC wording fixes.

    openid/dchp: 23 new commits on the default branch

    Read the explanation

    Only the commit titles are available, and 8 of the 23 are not listed, so this is a partial picture. The titles show CredentialItem.format renamed to credential_format and the format annexes adapted to credential_types being an array. They also show the following. The wallet, not the mdoc, chooses issuer- or device-signed. A substitute data element may be returned where its definition allows. The mapping of support_no_cryptographic_binding to DocRequestInfo was dropped. SD-JWT VC path semantics were deferred to issue #52, and a KB-JWT suggestion was applied. The ISO 18013-5 Document type is no longer redefined as a byte string, the authority key identifier now uses the RFC 5280 name, and RFC 7515 was added as a normative reference. Several other commits are review fixes and housekeeping. Exact wire-format effects cannot be confirmed from titles alone.

    • A wallet developer tracking dchp needs to update for the renamed credential_format field and the array-valued credential_types.
    • An implementer of mdoc responses needs to know that the wallet selects issuer- or device-signed data.
  • EditorialOAuth WG

    The -25 revision drops the BCP 14 SHOULD on type matching, adds a status-check risk note and regenerates the examples.

    draft-ietf-oauth-sd-jwt-vc-25: SD-JWT-based Verifiable Digital Credentials (SD-JWT VC)

    Read the explanation

    The type-matching text no longer uses SHOULD: a Holder or Verifier now "treats" an SD-JWT VC as being of a given type if the type is the vct value or in aka_vcts. The status-check text keeps SHOULD but adds that a Verifier that does not check status cannot detect revocation or suspension and accepts that risk under its policy. The examples were rewritten: the typ in the example headers changed from vc+sd-jwt to dc+sd-jwt, the PID example now uses a fictional "Ændgard" issuer with a urn:example:eudi:pid:aendgard:1 vct and extra claims (sex, age_in_years, portrait and others), and the signatures and hashes were recomputed. The rest of the diff shows only table-of-contents page numbers and whitespace, and the material is truncated.

  • EditorialOAuth WG

    The -26 revision of SD-JWT VC only swaps an RFC 6890 reference for the IANA registries and drops a duplicate sentence.

    draft-ietf-oauth-sd-jwt-vc-26: SD-JWT-based Verifiable Digital Credentials (SD-JWT VC)

    Read the explanation

    Two textual changes, both tied to Éric Vyncke's review. The SSRF considerations now cite the IANA IPv4 and IPv6 Special-Purpose Address Registries instead of RFC 6890, and the RFC 6890 reference entry is removed. In Section 2.2.2.2 the sentence saying Holders and Verifiers can treat the SD-JWT VC as a credential of the vct and aka_vcts types is removed. The other diff hunks only change dates, expiry and whitespace in the document history. The history lists no change in requirements.

  • EditorialOAuth WG

    This PR removes a sentence in Section 2.2.2.2 that repeated the one before it.

    oauth-wg/oauth-sd-jwt-vc#456: Remove a duplicate sentence in Section 2.2.2.2

    Read the explanation

    After the type matching rule was added in #454, the older sentence saying Holders and Verifiers can treat the SD-JWT VC as a credential of these types only repeated the previous sentence. The PR removes it and says the meaning is unchanged. It is stacked on #455 so the history entry sits under the same -26 heading.