SD-JWT VC Validator
Decode an SD-JWT verifiable credential, expand its disclosures and verify that each one binds to the signed digests.
Free and informational, not a qualified validation service under eIDAS. Each mode below states what happens to your input.
What this checks
- Decodes the Issuer-signed JWT header and payload.
- Decodes each disclosure and recomputes its base64url SHA-256 digest.
- Confirms every disclosure binds to a digest, in an
_sdarray or an array element, in the signed payload or inside another disclosure. - Rejects these cases from RFC 9901 section 7.1: the
nonealgorithm, a reused digest, a reserved claim name, a disclosure that does not fit where its digest sits and a disclosed name that already exists. - Reconstructs the disclosed claim set.
- In local mode, verifies the issuer signature against a public JWK you provide.
- In trust mode, sends the credential to the Tessio verifier service, which finds the issuer key itself and checks
typ(accepting the oldervc+sd-jwttoo),vctand the validity window. - In trust mode, also checks the issuer name against the tool's demo allowlist. This is not an EU trusted list.
How to decode and check an SD-JWT
An SD-JWT is a signed JWT whose issuer can hide some claims and let the holder reveal any subset of them. The signed part carries only digests of the hidden claims. The values travel separately, after the JWT, as disclosures. A plain JWT decoder shows you the digests and none of the values, which is why an SD-JWT needs its own decoder.
How to decode an SD-JWT
Paste the whole string. Its compact form is the Issuer-signed JWT, then each disclosure, each followed by
a tilde: <JWT>~<D.1>~<D.2>~. When the holder adds a key binding JWT it takes
the place of the empty last part (RFC 9901 section 4). The tool splits the string at the tildes, decodes
the JWT header and payload, decodes every disclosure and rebuilds the claims as the holder disclosed
them. In local mode all of this happens in your browser.
Load example switches to local mode and builds a credential in your browser with two hidden claims (given_name
and age_over_18) and fills in the public key that signed it. It
reads as valid: both disclosures bind and the issuer signature verifies. Change a character inside a disclosure and run it again: the result turns
invalid, because the disclosure either no longer decodes or no longer matches its digest.
What a disclosure is
A disclosure is a JSON array, base64url encoded. For a claim in an object it holds three elements: a
salt, the claim name and the value (section 4.2.1). For an element of an array it holds two: a salt and
the value (section 4.2.2). The issuer hashes each disclosure and puts the digest in the signed payload,
in an _sd array for object claims or as {"...": digest} in place of an array
element. The salt is what stops anyone from guessing a hidden value by hashing likely candidates
(section 9.3).
An issuer may also add decoy digests that match no disclosure, which makes it harder for a verifier to count the hidden claims (section 4.2.5). A digest with no disclosure behind it is therefore normal, and the tool ignores it as section 7.1 requires. The reverse is not normal: a disclosure that no digest points to makes the whole SD-JWT invalid.
Why a disclosure does not bind
The digest is computed over the US-ASCII bytes of the base64url string exactly as it appears in the SD-JWT, not over the decoded JSON. The result is base64url
encoded rather than written as hex (section 4.2.3). The algorithm is the one named in _sd_alg. When that claim is absent it is
SHA-256 (section 4.1.1). This tool checks SHA-256 and rejects any other value. Anything that changes the string changes the digest. Encoding the decoded JSON again with
different spacing or key order does, as do padding and line breaks. A disclosure taken from another
credential fails for a different reason: its digest is not in this payload.
Disclosures can also nest. A disclosed object can carry its own _sd digests, which point to
further disclosures (section 4.2.6). A nested disclosure presented without the one that contains it has nothing to bind to and reads as unbound.
Beyond binding, the tool rejects these cases from section 7.1: the none algorithm, a
digest used twice, a disclosure named _sd or ..., a disclosure whose shape does
not fit where its digest sits and a disclosed name that already exists at the same level. The last one matters: without it a disclosure could replace a claim the issuer signed in the clear.
Both modes also reject an SD-JWT whose last part after a tilde is neither empty nor a key binding JWT (section 4). That is usually a missing final tilde, which would otherwise turn the last disclosure into something read as a key binding JWT.
SD-JWT and SD-JWT VC
SD-JWT VC is the verifiable credential format built on SD-JWT. EU Digital Identity Wallets support it alongside the ISO mdoc format (ARF v3.0.0,
section 5.4). It is still an IETF draft. In draft 19 the issuer must set the header
typ to dc+sd-jwt (section 2.2.1) and the vct claim, which names the
credential type, is required (section 2.2.2.3). Local mode shows both but does not check the VC layer: not vct and not type
metadata. It also does not check validity claims such as exp, nbf and
aud, which RFC 9901 section 7.1 step 6 asks a verifier to check when present. Trust mode does
more: there Tessio.Verifier requires typ to be dc+sd-jwt or the older
vc+sd-jwt, requires a plain vct and rejects a
credential past exp or before nbf, with five minutes of
clock skew. Neither mode fetches the credential's status
list.
Key binding and issuer trust
A key binding JWT is a signature by the holder's private key over the SD-JWT as presented,
made for one verifier. It has the type kb+jwt and carries an sd_hash over the
Issuer-signed JWT and the disclosures presented (sections 3.3, 4.3 and 4.3.1). Local mode decodes and shows it but does not verify its signature or its
sd_hash. Trust mode removes it before verifying, because it checks the credential as
issued. Neither shows that the presenter holds the credential.
In local mode the issuer signature is verified only against a public JWK you paste in, and no
key is looked up from iss or from a certificate in the header. Tick check issuer
trust to switch to trust mode, where the pasted JWK is not used. The Tessio verifier service takes the key from the certificate in the header (x5c) or from the issuer's
metadata when the issuer is on the tool's demo allowlist. It also checks the issuer name against that allowlist. The
allowlist is not an EU trusted list, and no certificate is chained to a root.
Sources
- RFC 9901, Selective Disclosure for JSON Web Tokens (November 2025): sections 3.3, 4, 4.1.1, 4.2, 4.3, 7.1 and 9.3
- draft-ietf-oauth-sd-jwt-vc-19, SD-JWT-based Verifiable Digital Credentials (31 August 2026): sections 2.2.1 and 2.2.2.3
- EUDI Wallet Architecture and Reference Framework v3.0.0, section 5.4
Frequently asked
What is an SD-JWT verifiable credential?
SD-JWT VC is one of the two credential formats every EU Digital Identity Wallet supports, alongside the ISO mdoc format. It is a signed JWT whose claims can be selectively disclosed: the issuer signs digests of salted claims, and the holder reveals only the disclosures they choose. This tool decodes the credential and checks that each disclosure binds to a signed digest.
Does this send my credential anywhere?
Not in local mode. By default the tool runs entirely in your browser and nothing you paste leaves your device. If you tick "check issuer trust", the credential is sent to the Tessio verifier service, processed in memory and not stored.
How is this different from a JWT decoder?
A JWT decoder shows the header and payload. This tool also decodes the disclosures, recomputes their SHA-256 digests and verifies that each one binds to a digest in the credential. Optionally it verifies the issuer signature against a public key you provide.
Does it check whether the issuer is trusted?
Only against a demo allowlist. Tick "check issuer trust" and the Tessio verifier service checks the issuer name against the tool's demo allowlist, using Tessio.Verifier, the engine behind Tessio.Cloud. No EU trusted list is consulted, and a certificate the credential carries is not chained to any root. Local mode does not check issuer trust.