SD-JWT · Token Status List · base45
The answer is already inside the code
A credential that proves itself. Signature, expiry, revocation and claims, all checked without one request leaving the device.
TypeScript, zero runtime dependencies, WebCrypto only. Node, browsers and React Native, because the thing doing the verifying is usually a phone.
What this actually does
A credential is a statement somebody signed. “This person is over 18.” “This person may drive a car.” The signature is what makes it worth anything: it says an authority stands behind the statement, and that nobody edited it afterwards.
On paper, and in a PDF, one signature covers the whole document. To prove a single line of it you hand over all of it. Show a driving licence to prove your age and the other person also learns your address, your licence number and your exact date of birth. They never asked for any of that. They have it now anyway.
Selective disclosure breaks that trade. The issuer signs each fact separately, and the credential carries only a fingerprint of each one. You choose which facts to reveal, and the rest stay fingerprints that say nothing about their contents. The signature still checks out, because it was never a signature over one indivisible blob.
All of it fits inside the code the verifier scans: the signature, the fingerprints, and the facts you chose to share. Nothing is fetched while verifying, so a door, a bus, a rural clinic or an aeroplane at cruising altitude can check a credential with no network at all.
There is one last piece, and it is the one people forget. Anything you can scan, you can photograph. So the person presenting also has to sign a fresh challenge with a private key that never leaves their device. Without that step, a screenshot of somebody else's credential would pass. This library refuses any presentation that lacks it.
Built for the moment there is no signal
I built the eCNH, Brazil's digital driving licence, used by more than 40 million people. The part that taught me the most was not the app. It was the roadside: an officer scanning a licence on a highway with one bar of signal, or none, needing a yes or no in under a second.
Everything you need for that answer fits in the code. The signature proves the issuer, the claims are right there, and the only thing you need from outside is the issuer's public key, which changes so rarely that you can ship it and refresh it weekly.
Libraries for this exist. They are enterprise SDKs: heavy, tied to one country's profile, and written as though you already work in the identity industry. This is the version a product engineer can add on a Tuesday.
Three parties, and none of them is a server
Sign every claim once
The issuer decides which claims may later be withheld, and signs a digest for each of those.
Send only what is asked
The wallet drops the claims it is keeping. The issuer's signature still checks out on what is left.
Answer without asking anyone
Signature, expiry, revocation and every disclosure, checked against a pinned key on the device.
Prove you are over 18 without handing over your birthday
Age verification laws are arriving faster than the tooling. The usual implementation has the customer upload a photo of their ID to a third party, which is a privacy disaster and a breach waiting to happen.
Selective disclosure does it properly. The verifier cannot learn the birth date even if it wants to, because that value never left the wallet. The property is cryptographic, not a promise in a privacy policy.
// The credential holds name, address, birth date and document number.
// The bar gets one boolean.
const presentation = await present(credential, { disclose: ['over_18'] })
const result = await verify(presentation, { trust })
result.claims // { over_18: true }
result.claims.birth_date // undefined, and it was never transmitted
result.withheld // 4, and it cannot tell you which four
Real numbers, including the awkward one
A realistic driving licence, eight claims, five year expiry, status list pointer. Measured, not estimated:
| Credential | Characters | QR version |
|---|---|---|
| everything visible | ~740 | 18, scans fine |
| all eight claims disclosable | ~1590 | 27, too dense |
presenting only over_18 | ~1115 | 22, still dense |
fits() tell you where you stand before you print anything.
Don't take my word for it
The playground runs the whole library in your browser and fires eight real attacks at it. Each one prints the rejection code it expects, so you can check the library against its own claims instead of trusting a README.
Why this one, and when not
Five reasons to pick it, then the honest part.
- About 1,300 lines you can actually readSmall enough that one engineer reads all of it in an afternoon and knows what it does. Security you cannot read is security you are taking on faith.
- Zero dependenciesNothing from npm at runtime. There is no supply chain to audit but this one, and nothing that can change under you next week.
- It refuses what it cannot fully checkNo partial passes, no warning you are free to ignore. Every failure comes back as a named reason you can log and act on, never as a bare false.
- It runs where verification happensNode, Deno, Bun, browsers, React Native and edge workers. The same code everywhere, on the standard crypto already built into the platform.
- Tested three waysUnit tests, the official test vectors published with RFC 9901, and property tests that throw random malformed input at it looking for a case that slips through.
And when this is the wrong tool.
- You need ISO 18013-5 mobile driving licencesThat is a different encoding, CBOR and COSE, and a different standard. This library speaks SD-JWT, the format the European wallets and the OpenID specifications use. Related problem, different job.
- You want a whole walletThis verifies and presents. It does not store credentials, manage devices or handle onboarding. It is one piece, meant to sit inside your product.
- You need a third-party audit before a regulated rolloutThere is not one yet. The tests and the RFC vectors are public and so is every line, but for a regulated deployment you should budget for your own review.
Install
npm i qredential
Node 20 or newer, every current browser, React Native. About 1,300 lines of source with no runtime dependencies, specifically so that reading it before you trust it is realistic.
Standards, not inventions
- SD-JWT, RFC 9901selective disclosure and key binding, nested and recursive, the mechanism the European identity wallet uses
- SD-JWT VCthe credential shape
- Token Status Listrevocation that works from a cached copy
- base45, RFC 9285the QR envelope, chosen for scanner compatibility
Not ISO 18013-5 mDL, which is CBOR and COSE rather than JWT. It is on the roadmap, and a credential using something unsupported is refused rather than half understood. Claiming half of a compliance standard is worse than not claiming it.