Tessio Labs

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

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

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.