How it works: privacy by design

A status-list service sits between a credential's issuer and its verifiers, and sees things neither side normally sees about the other. Most of this service's design decisions exist to stop that vantage point from becoming a surveillance opportunity, for an outside observer and for the operator of this service itself.

The leak a shared status list can't fully avoid

Some things aren't avoidable: this service learns which issuer allocated which index in which list the moment it happens, the same way a certificate authority knows who it issued a certificate to. That mapping has to exist somewhere for revocation to work at all.

What is avoidable is that mapping becoming exploitable by anyone watching traffic instead of holding the database, including the operator watching which URLs get fetched. draft-ietf-oauth-status-list-21 publishes a status list as one raw bit-packed array, not a per-index lookup API. Every design decision below addresses what that format choice exposes and how to close it.

Randomized index allocation

A credential's index within its list is derived with a keyed format-preserving permutation (a small Feistel network over the list's bit-length, with cycle-walking to land back in range), not handed out sequentially. A sequential counter would leak issuance order and, over time, issuance volume: anyone watching a list fill up linearly could infer roughly how many credentials a given issuer population was producing and when. The permutation is bijective and keyed per list, so index order is indistinguishable from random to anyone without that list's key. It runs in O(1) time and space, with no stored shuffle array needed.

Issuer-blind list placement

The privacy goal covers more than the index inside a list; it also covers what the choice of list reveals. If a list were effectively dedicated to one issuer, or a handful of low-volume ones, an operator watching verifier requests would learn "this verifier just checked something from issuer X" without ever decoding the list's contents. Fetching that URL alone would be the leak.

Allocation instead maintains a pool of several concurrently-active lists and assigns each new credential using power-of-two-choices: pick two lists at random from the active pool, allocate into whichever has more remaining capacity. The selection function never sees issuer identity, only the current fill level of two randomly-sampled lists, which keeps the mixing unbiased. Over time each list's membership reflects overall issuance traffic rather than any single issuer's.

Anonymity-set-aware rotation

A list rotates out of active use once it fills up or hits a configured age limit — but a list that rotates out while still nearly empty is itself a small-anonymity- set problem: a handful of entries in a list is barely better than no mixing at all. Rotation is guarded against ending a list's active life while its membership is still too small, extending the window (up to an administrative ceiling) rather than rotating a near-empty list out on a timer alone.

Decoy noise: herd immunity for real revocations

Because allocation itself never writes anything (a fresh index simply reads as VALID until something changes it), every non-VALID byte in a published list used to be unambiguous: it could only mean one thing, a credential its issuer had revoked or suspended. Downloading a list and diffing it over time would let an observer count, and potentially correlate, real revocation events.

The service can inject camouflage noise into never-yet-allocated capacity: periodically flipping a random sample of unused indices to a real INVALID or SUSPENDED state too, much as chaff traffic obscures real traffic in an anonymity network. Decoy transitions deliberately mirror real ones: a suspended decoy can revert to valid, matching a real lifted suspension, and an invalid decoy never changes again, matching a real revocation's permanence. That makes a decoy statistically indistinguishable from a real entry. This is opt-in and off by default in every deployment today; see the limitations below.

What this doesn't solve

A few limits worth stating directly:

  • The issuer → list → index mapping this service records at allocation time is real and, by necessity, known to the service's own operator. Nothing above removes that. It only stops that knowledge from being exploitable by someone watching traffic instead of holding the database.
  • Decoy noise is opt-in, off by default, and has not yet run against real production traffic. It is a real, tested mechanism, not a placeholder, but its effectiveness at real-world scale is still an open question.
  • Network-level metadata (source IP, request timing, TLS fingerprinting) is outside this service's control. The mitigations here address what the application-layer data itself reveals, not network-layer traffic analysis; a CDN in front of the read path is meant to help with that separately.

This page summarizes the reasoning. The full design document, including the trade-offs considered and rejected along the way, is public at docs/design.md in the service's own repository.