Thoryn

2 May 2026 · Mark Bakker

Selective disclosure isn't a feature. It's a protocol.

Most identity systems enforce privacy by policy. SD-JWT enforces it by mathematics — the verifier can't over-disclose because the data isn't there.

There are two ways an identity system can protect a user's privacy.

One is to tell the verifier "please don't look at the claims you don't need." This is a policy. Policies work until they don't — until a log ingest, a debugging session, or a rogue employee treats undisclosed claims as ordinary data.

The other is to make it mathematically impossible for the verifier to see undisclosed claims. That is what SD-JWT VC does, and it is why we chose it as the credential format for the Thoryn wallet broker.

How SD-JWT commitments work

When an issuer creates a credential, they do something counterintuitive: instead of signing the claim values directly, they sign a list of hashes. Each hash is a SHA-256 of the claim plus a random salt:

claim: "given_name" = "Jane"
salt:  "M9tRNk7…"
hash:  sha256(["M9tRNk7…", "given_name", "Jane"])

The full SD-JWT has two parts:

  1. The JWT, signed by the issuer, containing the list of hashes.
  2. The disclosures — the raw claim + salt values, one per claim, kept separately.

When the holder wants to present the credential, they send:

  • The signed JWT (always — this proves the issuer blessed these hashes)
  • Only the disclosures for the claims they want to share

The verifier recomputes the hash for each received disclosure and checks it against the list in the JWT. If it matches, the claim is proven genuine. If a claim is undisclosed, its hash is still in the JWT, but the disclosure isn't provided — so the verifier has the hash, but no way to recover the original value.

It's not that the verifier politely declines to look. The data literally isn't there.

What this looks like in practice

The nightclub demo in the wallet-broker ships with an age_over_18 credential that has five claims: given_name, family_name, birth_date, age_over_18, and document_number. The bouncer's presentation definition asks for one field: age_over_18.

The wallet constructs a presentation with:

JWT signed by issuer (contains 5 hashes)
Disclosures:
  - [salt_4, "age_over_18", true]

That's all the broker sees. It reads age_over_18: true, verifies the hash matches one of the five in the JWT, and approves entry. The holder's name, birth date, and document number are in the JWT as hashes, but those hashes are cryptographically one-way. The broker cannot reconstruct them. If the broker's database is leaked, if the logs are dumped, if a rogue admin reads the audit trail — none of them find the birth date. It was never there.

Why this matters for regulated data

If you're building anything that handles regulated personal data — healthcare, finance, identity proofing, age gates — the question "what data does your verifier see?" keeps coming up in security reviews. The honest answer is almost always "more than we need, but we promise not to log it." That promise is brittle.

With SD-JWT, the answer changes. "The verifier sees exactly the claims the user chose to disclose. Everything else is a cryptographic hash we cannot invert." That's a different conversation with auditors. It's a different conversation with users, too — you can show them the spec and prove the point.

What we give up

SD-JWT is not free. Every credential issuance requires the issuer to commit to a fixed set of disclosable claims up front — you can't retroactively decide a claim is disclosable. Hash recomputation adds CPU cost per disclosure (small, but not zero). And the salt-per-claim data adds size. A typical SD-JWT with 10 disclosures is maybe 2 KB vs. 1 KB for a plain JWT.

For our use cases — wallet-based Verifiable Credentials where the user is in front of a QR code — those trade-offs are trivially worth it. For token-based API authentication, a plain JWT still makes sense. The rule of thumb: use SD-JWT when the subject is the one choosing what to reveal.

Further reading

If you want to see selective disclosure in action without writing a line of code, request access and run the nightclub demo locally. You'll watch the bouncer verify age_over_18 and literally not see anything else.

Selective disclosure isn't a feature. It's a protocol. — Thoryn