0043. Projection integrity and the distribution-key hierarchy
Context
Everything Vör trusts besides the token itself is a signed, edge-cached projection: the JWKS (ADR-0023), the status list (ADR-0005), later the SAML signing certificate (ADR-0017). ADR-0032 hardened one of these — the status list gained a monotonic counter, an issued-at, and a verifier-enforced max-age — but left two holes the design review surfaced.
First, the JWKS has no replay protection. ADR-0036 makes absence of a kid
from the JWKS the emergency kill switch for a compromised key; a network attacker
who can serve an edge a signed-but-stale JWKS re-animates the removed kid —
exactly the attack ADR-0032 closed for revocation, still open for keys.
Second, no record says what key signs the projections. If the status list or JWKS were signed by a token-signing key, a compromised token key could forge a "clean" list or a stale key set and defeat both of ADR-0036's kill paths — the compromised key would countersign its own pardon.
Third, ADR-0036 needs a wire form for kid-scoped revocation: the IETF Token
Status List is per-token-index bits, and "every token under this kid" is not
expressible in a bitstring.
Decision
-
A distribution key, distinct from token-signing keys, signs every projection. The JWKS document, the status list, and every future edge-consumed projection are signed by a dedicated distribution key held in
KeyCustody(ADR-0003) with its ownkidlifecycle (rotation per ADR-0023's current/next/retiring discipline). Token-signing keys sign tokens; the distribution key signs the artifacts that describe token-signing keys. A token-key compromise therefore cannot forge the projections that revoke it, and the two ADR-0036 kill paths stay independent of the compromised material. -
Every signed projection carries the ADR-0032 freshness discipline. A signed monotonic counter and an issued-at timestamp, under the signature; a verifier enforces max-age and per-projection monotonicity (older counter = replay, rejected; equal = same artifact, accepted), keeps its high-water marks as locally-persisted edge state, and fails closed past max-age — uniformly, for the JWKS exactly as for the status list. ADR-0032 owns the status-list case; this record generalizes it to the projection class.
-
The status list carries a key-revocation section. Alongside the per-token bitstring, the list carries a set of revoked
kids — the wire form of ADR-0036's emergencykid-scoped revocation. A verifier rejects any token whosekidis in the set, independent of the token's own status bit and of whether the JWKS refresh has landed. The section is part of the v0 status-list spec (specs/), reserved from the first published list. -
Distribution-key compromise has its own containment story. The distribution key can lie about liveness (replay within max-age, forge a clean list) but cannot mint tokens. Containment: rotate the distribution key via
KeyCustody, re-publish projections under the successor, and rely on max-age to bound how long a forged projection survives — the bound is registered in ADR-0039. The distribution key's trust anchor is deployment configuration (the edge is provisioned with the distribution-key set or its issuer), not the JWKS it signs — no self-certification loop. -
Budget registration. The JWKS document max-age and the edge JWKS-cache staleness bound are entries in the ADR-0039 registry, with the invariant that both stay ≤ the ADR-0036 containment budget. The startup gate validates them like every other budget.
Consequences
- The stale-projection replay class is closed uniformly: no projection an edge trusts can be older than its stated max-age, and none is signed by material whose compromise it would need to report.
- Vör's input set (ADR-0010) is unchanged in kind — token, public material, clock, cached projections — but every projection now arrives with verifiable freshness, and the verifier carries persistent high-water marks per projection (ADR-0032 decision 4).
- ADR-0036's "two independent kill paths" claim becomes true under a hostile
network, and its
kid-scoped revocation gains a concrete wire form (decision 3). - One more key in custody and one more rotation schedule to operate — the price of breaking the self-certification loop. The dev profile ships it as a second software key, invisible to the walking skeleton's user.
- Refines ADR-0023 (JWKS publication), ADR-0032 (extends its discipline to all projections), ADR-0036 (kill-path wire form and independence), and ADR-0039 (new registry entries); the projection-publication component itself is the CQRS path of ADR-0022.