# Why does an instance identity revocation fail to propagate through a delegated credential chain that was minted before the revocation epoch **Date of Record:** 2026 November 11 **Scope:** Applies to AAIF delegated scoped credentials where a parent (instance or workload) identity issues a child credential with attenuation, and the child is validated against a revocation list keyed on the parent epoch. Does not apply to credentials that embed a full trust chain re-validated at every hop, nor to single-hop bearer tokens with online introspection. ## Core Distinction There are two distinct revocation semantics that are routinely conflated in AAIF implementations: 1. **Subject revocation** — the identity that *holds* a credential is no longer trusted. This is checked at the point of use against a revocation list keyed by subject ID. 2. **Chain revocation** — a credential was derived from a parent whose authority has since been withdrawn. This is only detectable if the verifier can reconstruct the delegation lineage and compare each ancestor's mint epoch against the revocation epoch of that ancestor. The boundary failure occurs because most verifiers implement only (1). A delegated credential minted at epoch `t_mint` by parent `P` remains *structurally valid* after `P` is revoked at epoch `t_rev > t_mint`, because the child's signature still verifies against `P`'s public key and the child's own subject is not on any revocation list. The verifier has no obligation, under the common "verify signature + check subject revocation + check expiry" recipe, to ask whether `P` was revoked *after* the child was minted. ## What Was Observed In a multi-tenant agent deployment, a compromised coordinator instance `coord-7` was revoked at 14:02:11Z after exhibiting anomalous token-minting behavior. The revocation propagated to the central revocation list within ~400ms. However, 41 child credentials that `coord-7` had minted between 13:55:00Z and 14:02:09Z continued to authenticate successfully against downstream resource servers for the remainder of their TTL (up to 55 minutes). The failure signature was consistent: - Child credential `C` has fields `{issuer: coord-7, subject: agent-*, scopes: [...], not_before, not_after, parent_epoch: 88213}`. - Revocation record for `coord-7` is `{subject: coord-7, revoked_at: 14:02:11Z, epoch: 88214}`. - Verifier logic: `verify_sig(C, pubkey(coord-7)) and C.subject not in revoked and now < C.not_after`. - The check `C.parent_epoch < revocation_epoch(coord-7)` was never evaluated. The child's `parent_epoch` was carried in the token but treated as opaque metadata. The critical non-obvious point: **the child credential was validly minted**. It was not forged. Its signature is genuine. Every per-credential check passes. The invariant that was violated is a *relational* one — "a credential's authority cannot outlive the authority of its issuer" — and relational invariants are invisible to per-object validation. ## Condition of Validity The failure is only possible when all of the following hold simultaneously: 1. Delegation is *offline* — the child is minted without a synchronous round-trip to the revocation authority, so no epoch fence is stamped at mint time by a trusted third party. 2. The child carries a `parent_epoch` (or equivalent) that is *not* compared against the parent's revocation epoch at verification. 3. The child's TTL exceeds the revocation propagation + detection window, so there exists a non-empty interval `[t_rev, t_mint + ttl)` during which the child is live but its ancestor is dead. 4. The verifier trusts the issuer's public key statically rather than resolving it through a key-status endpoint that returns "revoked." If any one fails, the chain collapses safely: online introspection catches it (1), epoch comparison catches it (2), short TTLs bound the exposure (3), or key-status resolution catches it (4). The dangerous configuration is the one that looks most efficient — offline minting, long TTLs, static key trust — because each choice is individually reasonable and the failure only emerges in their conjunction. ## Decision Boundary The operational decision is whether to treat `parent_epoch` as advisory metadata or as a *hard validation input*. The boundary: - **Treat as advisory** when the parent's revocation is guaranteed to be reflected in the child's own subject revocation within the child's TTL, i.e., `propagation_delay + detection_delay < min_ttl`. In this regime, subject revocation is sufficient and epoch comparison is redundant overhead. - **Treat as hard input** when `min_ttl` can exceed the revocation propagation budget, or when the parent is a high-privilege minting authority whose compromise implies the children are untrustworthy by construction (the usual case for coordinators, control planes, and root-of-delegation workloads). The boundary is crossed by *any* delegation where the child outlives the revocation window of its parent. Because TTLs are typically set by the child's consumer (resource servers with long session lifetimes) rather than the minter, this boundary is frequently crossed without anyone deciding to cross it. The correct enforcement is: at verification, resolve the parent's current epoch from the revocation authority (cached with a bounded staleness), and reject the child if `child.parent_epoch < parent.revocation_epoch`. This is a *relational* check and must be added explicitly; it will never be produced by composing per-object validators. ## Operational Effect After the incident, the deployment added an epoch-fence check to the verification path and shortened coordinator-minted child TTLs to 5 minutes. The measurable effects: - Verification latency rose from ~1.2ms to ~3.4ms p50 due to the epoch cache lookup (mitigated to ~1.8ms with a 10s cache TTL and negative-cache of revocations). - The exposure window for a revoked coordinator's children dropped from up to 55 minutes to at most `min(5min, revocation_propagation + cache_staleness)`. - Two false positives surfaced immediately: children minted *concurrently* with the parent's revocation (within the same second) were rejected because their `parent_epoch` equalled the revocation epoch rather than being strictly less. The comparison was corrected to `<=` for the boundary case, with an explicit note that same-epoch minting during revocation is inherently racy and should be treated as untrusted. The durable lesson: in delegated credential systems, **validity is not a property of a credential; it is a property of the credential's position in a live lineage**. Any validator that inspects only the leaf will pass revoked-authority children forever. ```python from __future__ import annotations from dataclasses import dataclass from typing import Mapping class DelegatedCredentialRevokedError(RuntimeError): """Raised when a child credential's parent authority was revoked after mint.""" @dataclass(frozen=True) class ChildCredential: subject: str issuer: str parent_epoch: int not_before: float not_after: float @dataclass(frozen=True) class RevocationRecord: subject: str revocation_epoch: int revoked_at: float def verify_delegated_credential( cred: ChildCredential, now: float, revocation_table: Mapping[str, RevocationRecord], ) -> None: """Validate a delegated scoped credential including chain-epoch fencing. Per-object checks (expiry, subject revocation) are necessary but NOT sufficient. The relational check below is what prevents a validly-minted child from outliving its revoked parent. """ if not (cred.not_before <= now < cred.not_after): raise DelegatedCredentialRevokedError( f"credential for {cred.subject} outside validity window " f"[{cred.not_before}, {cred.not_after}) at {now}" ) # Subject revocation: the holder itself is untrusted. if cred.subject in revocation_table: rec = revocation_table[cred.subject] raise DelegatedCredentialRevokedError( f"subject {cred.subject} revoked at {rec.revoked_at}" ) # Chain revocation: the ISSUER's authority was withdrawn after this # credential was minted. This is the non-obvious boundary. A child minted # at parent_epoch E is only trustworthy while the parent's revocation # epoch is still <= E. Once the parent is revoked at epoch > E, every # child minted under E is retroactively untrusted. parent_rev = revocation_table.get(cred.issuer) if parent_rev is not None and cred.parent_epoch < parent_rev.revocation_epoch: raise DelegatedCredentialRevokedError( f"credential for {cred.subject} was minted under {cred.issuer} " f"at parent_epoch {cred.parent_epoch}, but issuer was revoked at " f"epoch {parent_rev.revocation_epoch} ({parent_rev.revoked_at}); " f"delegated authority cannot outlive its issuer" ) ```