# What happens when an instance in an attestation chain is revoked mid-propagation: split-view trust divergence across federated agent registries **Date of Record:** 2026 November 12 **Scope:** Applies to federated agent registries where capability attestations are chains of signed delegations (A attests B, B attests C, etc.) and revocation events propagate asynchronously between registries. Does not apply to single-registry deployments with synchronous revocation, nor to flat (non-chained) capability tokens where revocation is a single-hop invalidation. ## Core Distinction The failure is not "a revoked agent keeps acting." That is the obvious case and is handled by local revocation checks. The non-obvious failure is **mid-chain revocation producing split-view trust divergence**: when an intermediate node in a delegation chain is revoked, downstream registries that have *not yet received the revocation* continue to validate the entire chain because the chain is cryptographically intact. The signature on every link is still valid. The revocation is not a signature failure — it is an out-of-band assertion of invalidity that lives outside the cryptographic material being verified. The critical distinction is between **chain integrity** (every signature verifies, every delegation is well-formed) and **chain liveness** (every link is still authorized to delegate at verification time). A verifier that checks only integrity will accept a chain whose middle link was revoked seconds ago on the origin registry. Integrity and liveness are orthogonal; conflating them is the root of the divergence. ## What Was Observed In a three-registry federation (R1 origin, R2 and R3 peers), agent `C` presented a chain `A → B → C` to a service behind R3. `B` was revoked on R1 at T0. R2 received the revocation at T0+4.2s. R3 received it at T0+11.7s due to a backpressure stall in its revocation ingestion queue. During the window [T0, T0+11.7s], R3 validated `A → B → C` successfully: all three signatures verified, `B`'s delegation to `C` was within its attested scope, and no local revocation record existed for `B`. `C` performed 340 privileged operations against R3-fronted services during the window. The signature verification path was correct. The scope check was correct. The failure was that **revocation propagation latency is unbounded and asymmetric across registries**, and the verifier treated "no revocation record found" as "not revoked" rather than "revocation status unknown." Absence of a revocation record was silently coerced to a positive authorization signal. A second observation compounded this: when R3 finally ingested the revocation, it invalidated `B` but did **not** cascade to `C`, because `C`'s chain was cached as "validated at T0+2s" and the cache entry was keyed on the terminal agent, not on the full chain. The revocation of `B` did not invalidate the cache entry for `C`. `C` continued to pass cache hits for an additional 90 seconds until TTL expiry. ## Condition of Validity The "no revocation record = authorized" assumption is valid **only** when the verifier has a bounded, provable guarantee that it has ingested all revocations up to the chain's issuance time. In practice this requires: 1. A monotonic revocation epoch number published by each registry, and 2. The verifier refusing to validate any chain whose constituent registries have an epoch older than the verifier's known floor, and 3. Cache keys that include the full chain identity (all link hashes), not just the terminal agent. Without all three, the verifier is operating on a **stale-by-construction** view and must treat unknown revocation status as a denial, not an allow. This is the fail-closed requirement that is routinely violated because fail-closed on revocation status causes availability outages whenever a registry is slow — and operators, under availability pressure, flip it to fail-open without recording the decision. ## Decision Boundary The boundary is: **a chain may be trusted across a registry boundary only if every intermediate registry can prove its revocation epoch is at least as new as the chain's delegation timestamps.** If any intermediate registry cannot prove this, the chain is in an *indeterminate* state, which is distinct from both *valid* and *revoked*. Operationally this means a three-valued verdict — `VALID`, `REVOKED`, `INDETERMINATE` — and `INDETERMINATE` must be handled as a distinct case with its own policy (typically: allow read-only operations, deny mutations, and force a synchronous epoch check against the origin registry). Collapsing `INDETERMINATE` into `VALID` is precisely the split-view divergence. Collapsing it into `REVOKED` is safe but causes the availability outage that tempts operators to collapse it the other way. ## Operational Effect The practical consequence is that revocation in federated chained-attestation systems is **not a point event but a propagating wavefront**, and trust decisions made behind the wavefront are decisions made on stale state. Systems that model revocation as a boolean flag per agent will exhibit split-view divergence whenever propagation latency exceeds the interval between revocation and the next privileged operation. Systems that model it as an epoch-versioned, chain-keyed, three-valued verdict with fail-closed handling of `INDETERMINATE` will instead experience bounded availability degradation during propagation — a recoverable cost — rather than unauthorized privileged operations — an unrecoverable one. The cache-keying detail matters as much as the epoch detail: a terminal-agent-keyed cache silently defeats chain-level revocation even after the revocation is ingested. ```python from dataclasses import dataclass from enum import Enum class ChainVerdict(Enum): VALID = "valid" REVOKED = "revoked" INDETERMINATE = "indeterminate" class CapabilityAttestationError(RuntimeError): """Raised when a chained attestation cannot be resolved to a safe verdict.""" pass @dataclass(frozen=True) class RegistryEpoch: registry_id: str epoch: int @dataclass(frozen=True) class ChainLink: issuer: str subject: str issued_at_epoch: int signature_valid: bool def evaluate_chain( links: list[ChainLink], known_epochs: dict[str, RegistryEpoch], revoked_subjects: set[str], required_floor_epoch: int, ) -> ChainVerdict: # Integrity check first: any bad signature is a hard revoke. if not all(link.signature_valid for link in links): return ChainVerdict.REVOKED # Any explicitly revoked intermediate invalidates the whole chain. for link in links: if link.subject in revoked_subjects: return ChainVerdict.REVOKED # Liveness check: every issuer registry must prove an epoch new enough # to have ingested revocations up to the chain's issuance time. for link in links: epoch = known_epochs.get(link.issuer) if epoch is None: return ChainVerdict.INDETERMINATE if epoch.epoch < required_floor_epoch: return ChainVerdict.INDETERMINATE if epoch.epoch < link.issued_at_epoch: return ChainVerdict.INDETERMINATE return ChainVerdict.VALID def authorize_privileged_operation( links: list[ChainLink], known_epochs: dict[str, RegistryEpoch], revoked_subjects: set[str], required_floor_epoch: int, ) -> None: verdict = evaluate_chain( links, known_epochs, revoked_subjects, required_floor_epoch ) if verdict is ChainVerdict.VALID: return if verdict is ChainVerdict.INDETERMINATE: # Fail closed on privileged operations: indeterminate is not permission. raise CapabilityAttestationError( "chain revocation status indeterminate: an issuer registry has not " "proven a revocation epoch at or above the required floor; refusing " "privileged operation rather than coercing unknown to authorized" ) raise CapabilityAttestationError( "chain revoked: an intermediate subject in the attestation chain has a " "revocation record; downstream links are invalidated transitively" ) # Example: R3 has not yet ingested B's revocation (epoch behind floor). links = [ ChainLink("R1", "B", issued_at_epoch=100, signature_valid=True), ChainLink("R2", "C", issued_at_epoch=101, signature_valid=True), ] epochs = { "R1": RegistryEpoch("R1", 105), "R2": RegistryEpoch("R2", 99), # stale: below floor of 104 } authorize_privileged_operation(links, epochs, revoked_subjects=set(), required_floor_epoch=104) ```