Selective Disclosure for JWTs (SD-JWT), draft-ietf-oauth-selective-disclosure-jwt.
An SD-JWT lets an issuer sign a set of claims where individual claims are
selectively disclosable: the signed JWT carries only the SHA-256 digest of
each hidden claim (in the _sd array, or as an array element {"...": digest}),
and the cleartext claim travels separately as a Disclosure. The holder
chooses which Disclosures to release when presenting the credential, so a
verifier learns only the disclosed subset while the issuer's single signature
still covers all of them.
The wire form is a ~-separated list:
<Issuer-signed JWT>~<Disclosure 1>~...~<Disclosure N>~<optional Key Binding JWT>A Disclosure is base64url(UTF-8(JSON(array))) with no padding, where the
array is [salt, claim_name, claim_value] for an object property or
[salt, value] for an array element. Its digest is
base64url(SHA-256(ASCII(disclosure))) — computed over the exact disclosure
string, so interop does not depend on how any party serialises JSON.
This module is the base SD-JWT mechanism. Attesto.SdJwtVc layers the
IETF SD-JWT VC profile (vct, cnf, vc+sd-jwt typing) on top.
What is implemented
issue/2— sign an SD-JWT making chosen top-level claims selectively disclosable. (Nested/recursive and array-element issuance is a planned extension; the wire format it would produce is already a superset this module can verify.)verify/3— the full recursive processing model from the spec: verify the issuer signature, then resolve_sddigests and array{"...": digest}elements at every depth, rejecting a presentation whose Disclosures do not all reference a digest (spec §7.3) or whose digests collide.verify_key_binding/3— verify a holder Key Binding JWT over a verified presentation (nonce/aud/sd_hash), for the OID4VP verifier path.
Like the rest of attesto core this module is conn-free and fail-closed.
Summary
Functions
The Disclosure string for an array element ([salt, value]).
The digest of a Disclosure under sd_alg (default sha-256):
base64url(SHA-256(ASCII(disclosure))).
A fresh salt for a Disclosure: 128-bit CSPRNG value, base64url, no padding.
Issue an SD-JWT, making the chosen top-level claims selectively disclosable.
The Disclosure string for an object property ([salt, name, value]).
Verify an SD-JWT presentation and reconstruct the disclosed claims.
Verify a holder Key Binding JWT (kb+jwt) over an already-verified
presentation.
Types
@type verified() :: %{ claims: map(), key_binding_jwt: String.t() | nil, issuer_jwt: String.t(), disclosures: [String.t()] }
A parsed, verified SD-JWT presentation.
@type verify_error() ::
:malformed
| :invalid_signature
| :unsupported_alg
| :invalid_typ
| :invalid_disclosure
| :unused_disclosure
| :duplicate_digest
| :unsupported_sd_alg
| :reserved_claim_name
Functions
The Disclosure string for an array element ([salt, value]).
The digest of a Disclosure under sd_alg (default sha-256):
base64url(SHA-256(ASCII(disclosure))).
@spec generate_salt() :: String.t()
A fresh salt for a Disclosure: 128-bit CSPRNG value, base64url, no padding.
Issue an SD-JWT, making the chosen top-level claims selectively disclosable.
claims is the full claim set. Options:
:disclosable- the list of top-level claim names to hide behind_sddigests (each becomes a Disclosure). Every other claim is signed in the clear. Defaults to[](a plain, fully-visible JWT with an empty_sd).:pem- the issuer signing key (PEM). REQUIRED.:alg- the JWS algorithm; inferred from the key when omitted.:typ- the JOSEtypheader (e.g."vc+sd-jwt"); omitted when nil.:kid- the JOSEkidheader; omitted when nil.:sd_alg- the hashing algorithm name for_sd_alg. Defaultsha-256.
Returns the combined Issuance string (<JWT>~<D1>~...~<DN>~, trailing
separator, no Key Binding JWT).
The Disclosure string for an object property ([salt, name, value]).
name must be a string; value is any JSON-encodable term. Returns the
base64url(no-pad) of the UTF-8 JSON array.
@spec verify(String.t(), map() | [map()] | list(), keyword()) :: {:ok, verified()} | {:error, verify_error()}
Verify an SD-JWT presentation and reconstruct the disclosed claims.
combined is the ~-separated presentation. jwks is the issuer's JWK Set
(%{"keys" => [...]}, a single JWK map, or a list) used to verify the
Issuer-signed JWT. Options:
:accepted_algs- JWS algorithms accepted for the issuer signature. Defaults toAttesto.SigningAlg.fapi_algs/0.
Returns {:ok, %{claims:, key_binding_jwt:, issuer_jwt:}} where claims is
the payload with every _sd/array digest resolved from the presented
Disclosures (and the _sd/_sd_alg machinery removed). The Key Binding JWT,
if present, is returned UNVERIFIED (see verify_key_binding/3) - reconstructing
claims and checking holder binding are separate steps. Rejects a presentation
in which any Disclosure is unused (spec §7.3) or any digest is duplicated.
@spec verify_key_binding(key_binding_input(), map(), keyword()) :: :ok | {:error, atom()}
Verify a holder Key Binding JWT (kb+jwt) over an already-verified
presentation.
verified is the map verify/3 returned; holder_jwk is the key the issuer
bound the credential to (the cnf key). Options:
:nonce- the expectednonce(REQUIRED).:audience- the expectedaud(REQUIRED) - this verifier's identifier.:now/:max_age_seconds- freshness bounds oniat(default 300s).
Verifies the KB-JWT signature under holder_jwk, its typ (kb+jwt), the
nonce/aud, and that sd_hash equals base64url(SHA-256(<Issuer JWT and presented Disclosures,~-joined, trailing~>)) (spec §4.3) - so the
holder signed over exactly the presentation the verifier reconstructed claims
from.