Theo Zourzouvillys

Field Note 49current

Verify by computation, not lookup; store revocations, not issuances

By
Theo Zourzouvillys
Published
Tags
securityarchitectureauthscalability

TL;DR

Where you can, make validity checkable from the value itself — no store on the verify path:

  • HMAC or sign the claims plus an expiry. Any node holding the key verifies locally; issuance writes nothing. Hash immutable content so the name proves the bytes.
  • Pass by value, not by reference. Carry the claims the verifier needs, not an id that must be dereferenced against a database that is now everyone’s availability ceiling.
  • Bound every lifetime. Expiry is what makes the whole scheme work — an unbounded credential is an unbounded revocation problem.
  • When you need revocation and it’s rarer than issuance, invert the state. Keep a denylist of the revoked few — each entry retained only for the remaining lifetime of the thing it revokes — instead of a row for every grant ever issued. The set stays small, self-cleans, and is cheap enough to replicate to every verifier, so the lookup you removed never sneaks back.

Context

The default shape in most codebases: generate a random id, insert a row, look the row up on every use. Sessions, API keys, download links, email-verification and unsubscribe tokens, invite codes — all pass-by-reference. It feels safe because the database is the truth. The cost is structural: every verification anywhere becomes a read against one store, so that store’s availability, latency, and scale ceiling silently become every service’s. And the busiest table in the system ends up being one that stores no business data at all.

The alternative is old and settled. HMAC the data plus an expiryDos and Don'ts of Client Authentication on the Web (USENIX Security 2001)Fu, Sit, Smith & Feamster's classic study of web authentication schemes. Its constructive core is the stateless authenticator: HMAC over the data plus an expiry, verifiable by any server holding the key — the pattern under signed cookies, links, and tokens ever since.pdos.csail.mit.edu ↗ and the value carries its own proof — this is 2001-era art, and it’s what presigned URLs, signed cookies, and self-contained tokens all are. MacaroonsMacaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud (NDSS 2014)Google's design for bearer credentials built from chained HMACs. A holder can attenuate a macaroon — add caveats narrowing where, when, and how it may be used — entirely offline, showing how far pass-by-value authority can go without a database.static.googleusercontent.com ↗ showed how far it stretches: holders can attenuate a credential offline, adding caveats without any issuer round-trip. Content addressing is the same move for data: the hash is the identity, so integrity is a computation and the object is cacheable forever (ZFN-21Field Note · currentZFN-21 — Cache only immutable objects; treat caches as tech debtUse caches sparingly, only for immutable addressed objects — never for mutable DB results, where invalidation bugs and stale reads live; use projections instead. A cache in the data path is usually a patch over an architectural gap that trades correctness for performance.Open ZFN-21 →).

The objection is always revocation — and the standard failure mode is to answer it by reintroducing full state: a “valid tokens” table consulted on every request. Now you pay both costs: a write per issuance and a read per verification, plus the key management of the signature you’re no longer really relying on.

The web PKI already ran this experiment at scale. OCSP — a live query to the issuer on every verification — failed on availability and privacy so thoroughly that browsers soft-failed it into meaninglessness, and the largest CA has shut it downLet's Encrypt: Ending OCSP Support in 2025The largest CA's announcement that it is shutting down OCSP — the design where every verification is a live query to the issuer — in favour of CRLs, citing privacy and reliability. The web PKI's two-decade verdict on lookup-per-verification.letsencrypt.org ↗. What replaced it is the inversion this note recommends: CRLiteCRLite: A Scalable System for Pushing All TLS Revocations to All Browsers (IEEE S&P 2017)Compresses the entire web PKI's revocation state into a few megabytes of layered Bloom filters pushed to every browser, making revocation checking a fast local computation. The existence proof that even internet-scale revocation fits in a distributable set.cbw.sh ↗ compresses every revocation in the web PKI into a few megabytes pushed to every browser, because certificates expire and revocation is rare — so the outstanding-revocations set is small even at internet scale.

Recommendation

Make validity self-evident. The credential is the claims plus a MAC or signature over them: subject, audience/purpose, expiry, and a unique id. Shared-key HMAC is a fine start; move to asymmetric when verifiers shouldn’t be able to mint — the same progression as ZFN-5Field Note · currentZFN-5 — Make workload identity a platform-owned serviceWorkload identity belongs in shared platform infrastructure, not reimplemented per service. A small token service mints short-lived tokens any service verifies. Shared keys are a fine first step; asymmetric signing the better end-state — don't let 'no PKI' block it.Open ZFN-5 → and ZFN-7Field Note · currentZFN-7 — Sign the message, not just the session (HTTP Message Signatures)A bearer token proves nothing about the request it rides on. Sign the message itself (HTTP Message Signatures, RFC 9421) — request, and ideally response — so the recipient can prove who sent this exact message and not a byte changed. Shared keys first; asymmetric better.Open ZFN-7 →. Always include the unique id (a jti): it costs nothing at issuance and it is what makes precise revocation possible later.

Pass by value. Put the claims the verifier needs in the value instead of behind an id. The same principle shows up beyond auth: a version token the client carries beats a server-side session table for read-your-writes (ZFN-25Field Note · currentZFN-25 — Track the version a client has seen for read-your-writesFor read-your-writes across backends, track the latest version a client has seen — a token or vector clock. Return it on write; reads then go to a backend at least that fresh. Hold it client-side (a token they present) or server-side (a gateway tracks the session and routes).Open ZFN-25 →); a content hash beats an object-id-plus-lookup for anything immutable. Don’t put secrets in claims — the holder can read them; signed is not encrypted.

Bound every lifetime, short. Issue short and refresh to extend, the discipline of ZFN-9Field Note · currentZFN-9 — No long-lived cloud keys; workloads authenticate by federated identityNo static AWS or GCP keys anywhere — not in code, secret stores, or env. Workloads use their runtime's own identity and cross clouds by exchanging it (OIDC) for short-lived credentials via federation. Static keys are a documented carve-out only.Open ZFN-9 →. The refresh point is a natural, low-rate stateful checkpoint — the right place for policy and revocation checks — and short expiry is what caps how long any revocation must be remembered.

When you must revoke, store the revocations. Keep a denylist keyed by token id or subject. Each entry’s TTL is the remaining lifetime of what it revokes — after that the expiry enforces itself, so the set is self-cleaning; like a lease (ZFN-37Field Note · currentZFN-37 — Every lock is a leaseA lock that can outlive its holder is a deadlock scheduled for later. Give every lock — including informal ones like claimed_by columns — a TTL, a named owner, and a heartbeat; make expiry automatic and server-side; fence the side effects so a stale holder can't corrupt anything.Open ZFN-37 →), cleanup is the passage of time, not a job. State is O(outstanding revocations), not O(issued): issuing a million links costs nothing, revoking three costs three entries for an hour. This inversion is only right when revocation is genuinely rarer than issuance — if you’re consulting per-grant state on most verifications anyway (per-request entitlements, usage metering), you want honest pass-by-reference, not a denylist bolted onto a token.

Replicate the denylist to the verifiers. Small and bounded means it fits in memory everywhere — a full set or a Bloom-filter cascade pushed to every node, CRLite-style. Verification stays a local computation even with revocation in the picture. A revocation now takes effect within your propagation interval; measure that interval, put a number on it, and get the number agreed as policy — “revocation takes effect within N seconds, and expiry caps the damage at M minutes” is a sentence a security review can accept or reject.

Keep the coarse levers. Rotating a signing key revokes everything it signed — the emergency brake. A per-subject not-before timestamp (“reject anything for this principal issued before T”) implements log out everywhere with one small record per affected subject rather than a hunt through per-token state. Between per-id, per-subject, and per-key, you can revoke at whatever granularity the incident calls for.

Consequences

Easier:

  • Verification scales with CPU, not with a datastore — no hot-path dependency, no cross-region read, nothing to fall over during a partition. Issuance is free, so granting a million narrow, short-lived credentials is an architectural non-event.
  • The blast radius of the credential store shrinks to the issuer; verifiers keep working through its outages, degraded only in revocation freshness.

Harder:

  • Revocation is no longer instantaneous: worst case is denylist propagation delay, capped by expiry. That’s a real policy decision to defend, not an implementation detail to hide.
  • The signing key is now the crown jewels. Rotation must actually be built — key ids on every credential, overlap windows, a rehearsed rotation runbook — not left as “we’d figure it out.”
  • You gave up the inventory: “how many active sessions?” has no cheap answer. If audit needs enumeration, log issuance asynchronously off the hot path (ZFN-48Field Note · currentZFN-48 — Emit async work into the WAL, not a job tableWhen a DB write should trigger async work, ride the WAL instead of dual-writing or polling a job table. pg_logical_emit_message emits the event transactionally — outbox semantics, no table. A WAL listener consumes it statefully and fans out, keeping load off the primary.Open ZFN-48 →) — don’t put a write back in front of verification.
  • Claims travel with every request — size on the wire, and visible to the holder.

New obligations:

  • Every issued credential carries a unique id and an expiry, even when you can’t imagine revoking it — retrofitting jti into credentials already in the wild is miserable.
  • The denylist propagation lag is monitored and alarmed; a stalled replica is a silent revocation failure.

References

  • ZFN-5Field Note · currentZFN-5 — Make workload identity a platform-owned serviceWorkload identity belongs in shared platform infrastructure, not reimplemented per service. A small token service mints short-lived tokens any service verifies. Shared keys are a fine first step; asymmetric signing the better end-state — don't let 'no PKI' block it.Open ZFN-5 → — short-lived platform-minted tokens, verified anywhere; the issuance side of this note.
  • ZFN-7Field Note · currentZFN-7 — Sign the message, not just the session (HTTP Message Signatures)A bearer token proves nothing about the request it rides on. Sign the message itself (HTTP Message Signatures, RFC 9421) — request, and ideally response — so the recipient can prove who sent this exact message and not a byte changed. Shared keys first; asymmetric better.Open ZFN-7 → — signing the message itself; shared keys first, asymmetric better.
  • ZFN-9Field Note · currentZFN-9 — No long-lived cloud keys; workloads authenticate by federated identityNo static AWS or GCP keys anywhere — not in code, secret stores, or env. Workloads use their runtime's own identity and cross clouds by exchanging it (OIDC) for short-lived credentials via federation. Static keys are a documented carve-out only.Open ZFN-9 → — short lifetimes and refresh as the base discipline.
  • ZFN-21Field Note · currentZFN-21 — Cache only immutable objects; treat caches as tech debtUse caches sparingly, only for immutable addressed objects — never for mutable DB results, where invalidation bugs and stale reads live; use projections instead. A cache in the data path is usually a patch over an architectural gap that trades correctness for performance.Open ZFN-21 → — content addressing: hashes as identity for immutable data.
  • ZFN-25Field Note · currentZFN-25 — Track the version a client has seen for read-your-writesFor read-your-writes across backends, track the latest version a client has seen — a token or vector clock. Return it on write; reads then go to a backend at least that fresh. Hold it client-side (a token they present) or server-side (a gateway tracks the session and routes).Open ZFN-25 → — pass-by-value client state for consistency.
  • ZFN-37Field Note · currentZFN-37 — Every lock is a leaseA lock that can outlive its holder is a deadlock scheduled for later. Give every lock — including informal ones like claimed_by columns — a TTL, a named owner, and a heartbeat; make expiry automatic and server-side; fence the side effects so a stale holder can't corrupt anything.Open ZFN-37 → — state that expires by itself; recovery as the passage of time.
  • Fu et al. — Dos and Don’ts of Client Authentication on the Web (2001) — the canonical stateless authenticator: HMAC over data plus expiry.
  • Birgisson et al. — Macaroons (NDSS 2014) — HMAC-chained bearer credentials attenuable offline.
  • Larisch et al. — CRLite (IEEE S&P 2017) — all of the web PKI’s revocations, compressed and pushed to every verifier.
  • Let’s Encrypt — Ending OCSP Support in 2025 — the ecosystem’s verdict on lookup-per-verification.

Changelog

  • 2026-07-07: First published as a Field Note.