Offline verification
How a credential is checked with no network — a signature check against the cached issuer public key, plus a revocation check against the cached status list — and why offline is permissive while online is strict.
In plain terms. What is actually checked when there is no network, and why the offline answer is deliberately more forgiving than the online one — a verifier at a rural clinic should not have to turn someone away because a server is unreachable.
"Offline-verifiable" means a verifier can confirm a credential is genuine and valid with no network connection at verification time — it never phones home to the issuer. This page is the mechanics: exactly what gets checked, and how the online and offline paths differ.
Why not just call the issuer?
A traditional state ID is checked by querying a central database ("is #12345 valid?"). That needs live connectivity, the database to be up, and it leaks every check to the centre. A Verifiable Credential removes all three: the credential is self-contained and signed, and the verifier checks it against data it cached earlier — the two trust artifacts.
The two local checks
Offline verification is two computations, both local — no request leaves the device:
1. Signature — against the cached issuer public key
The credential carries its claims and the issuer's signature. To verify the signature you need
the issuer's public key, which the verifier fetched once while online (from the DID document
at /.well-known/did.json) and cached. Verifying a signature against a key you already hold
is pure local math.
- The trust list holds a list of keys (current first, retired after), so a credential signed
before a key rotation still verifies. The credential's header names a
kid, so the verifier tries the right key first. - No matching key for the issuer →
UNTRUSTED_ISSUER. Bad signature →BAD_SIGNATURE. Expired →EXPIRED. All decided locally.
2. Revocation — against the cached status list
The credential's credentialStatus names a status list URL and a statusListIndex (a
position). Instead of asking the issuer "is this revoked?", the verifier caches the whole status
list — a compact Bitstring Status List, synced when last online — and checks the bit at that
index locally. Bit set → REVOKED. Bit clear → still valid.
The sync-then-verify lifecycle
While online, the verifier fetches two things once: the DID document (keys) and the status list (revocation bitstring). After that it verifies any number of credentials offline. The credential comes to the field (a QR, or a wallet); the trust artifacts are already cached.
WHILE ONLINE (once): fetch /.well-known/did.json → cache issuer keys
fetch /.well-known/status/ng.json → cache status bitstring
OFFLINE (all day): scan credential (QR / wallet)
1. signature valid against a cached key? (local)
2. not expired? (local)
3. bit at statusListIndex clear? (local, vs cached list)
→ accept / reject, no networkExample: a rural clinic syncs the keys + status list over the morning's signal, then verifies residents' QR credentials all day with zero internet — never contacting OpenResidency again.
Offline is permissive; online presentation is strict
The revocation posture deliberately changes with context — this is a security invariant, not an accident:
Offline credential check (VcVerifier) | Online presentation (VpVerifier) | |
|---|---|---|
| If revocation can be checked | enforced (revoked → reject) | enforced (revoked → reject) |
| If revocation can't be checked | permissive: valid: true, checkedRevocation: false | fails closed: refuse (REVOCATION_UNCHECKABLE) |
| Why | a disconnected field officer must still get a useful answer against the last sync | a connected server has no excuse to skip revocation; "accept but flag" means a revoked credential slips past every RP that ignores the flag |
The verifier always returns checkedRevocation, so a relying party knows whether revocation was
actually confirmed or merely couldn't be reached. See "What not to break".
What offline verification can and can't establish
| Property | Offline? | How |
|---|---|---|
| Genuine (issuer signed it) | ✅ | signature vs cached key |
| Unexpired | ✅ | local time vs validUntil |
| Not revoked | ✅ as of last sync | bit vs cached status list |
| Held by this person (holder binding) | ❌ | needs a presentation — interactive, see below |
A credential verified offline is proven genuine, but not that the person presenting it is its holder — that's a presentation check (holder binding + nonce + audience), which is inherently interactive and online. Offline you're trusting possession; the presentation path adds proof of the holder.
In the code
- The two local checks + the permissive-when-uncached behavior:
VcVerifier.verify(jwt, { offline }) - The bitstring itself:
StatusList - The fail-closed online path:
VpVerifier→REVOCATION_UNCHECKABLE - The cached artifacts are published by
WellKnownController.
Credentials & trust
Trust artifacts, offline verification, the two verifiers, and did:key vs did:web — how a credential is trusted without calling home.
What not to break
The security and privacy invariants that are the whole point of OpenResidency — do not relax them to make something work; narrow the config instead.