# How does an instance detect a stale federation trust anchor when revocation propagation lags attestation issuance **Date of Record:** 2026 November 10 **Scope:** Applies to cross-organization agent federations where capability attestations are signed by a peer org's trust anchor and revocation events propagate asynchronously through a gossip or pub/sub layer. Does not apply to single-org deployments, to federations using synchronous revocation checks (OCSP-style per-request), or to attestations with lifetimes shorter than the maximum observed propagation delay. ## Core Distinction There are two distinct failure modes that look identical from the caller's perspective but require different remediation: 1. **Anchor compromise** — the peer org's signing key is in the wrong hands; every attestation it ever issued is suspect and the anchor must be distrusted wholesale. 2. **Revocation lag** — the anchor is intact, but a specific attestation was revoked at time `T_revoke` and the revocation has not yet reached this instance at time `T_now`. The non-obvious invariant is this: **a valid signature over a non-expired attestation does not imply the attestation is currently valid.** Signature verification and revocation freshness are orthogonal checks, and most federation implementations collapse them into one "is this attestation good?" boolean. When they do, a revocation that propagates in 400ms on a good day and 40s during a partition will be silently ignored for the entire lag window, and the instance will accept a capability the peer org believes it has already withdrawn. The boundary condition is `T_revoke + max_propagation_delay > T_now`. Inside that window, the local state is *unknowable* — you cannot distinguish "not revoked" from "revocation in flight." The only correct behavior is to treat the window as a hard validity ceiling on the attestation, not as a probabilistic hint. ## What Was Observed During a controlled federation drill between two orgs (call them A and B), org B revoked an attestation granting org A's orchestrator agent write access to B's config store. The revocation was published to B's gossip layer at `T_revoke`. The attestation had a 24-hour expiry, so the signature remained valid for the entire incident. Org A's federation gateway verified the signature, checked expiry, and admitted the attestation. It continued to admit it for 38 seconds — the 99th-percentile propagation delay measured during a concurrent network partition on B's side. During that window, A's orchestrator issued 14 write operations against B's config store. B's own enforcement layer rejected 11 of them (because B had already applied the revocation locally) and accepted 3 (because those requests landed on a B replica that had also not yet received the revocation). The 3 accepted writes were the incident. The signature check passed. The expiry check passed. The attestation was, by every local check, valid. It was also revoked. The gateway had no mechanism to represent "valid but possibly-stale" as a state, so it represented it as "valid." A second observation from the same drill: when B's anchor was rotated (not compromised, just rotated on schedule), A's gateway treated the new anchor's attestations as valid immediately but kept honoring the old anchor's attestations until their natural expiry — because A cached anchor→attestation bindings and never re-validated the binding against the current anchor set. This is the same class of bug wearing a different hat: the local cache has no freshness dimension. ## Condition of Validity The "signature + expiry is sufficient" model is valid only when at least one of the following holds: - Revocation is checked synchronously per-request against the issuing org (no async propagation). - Attestation lifetime `L` is strictly less than the observed maximum revocation propagation delay `D_max`, so that any attestation expires before a revocation of it could still be in flight. In practice this requires `L < D_max`, which for realistic `D_max` (tens of seconds to minutes during partitions) means attestation lifetimes measured in seconds — rarely acceptable. - The federation has a monotonic revocation epoch counter that is exchanged on every request and compared against the local epoch, so lag is *detectable* rather than merely *possible*. Absent one of these, the model is unsound and the boundary condition `T_revoke + D_max > T_now` is live. ## Decision Boundary When an instance receives an attestation, it must compute a **freshness ceiling**: ``` valid_until = min(attestation.expiry, last_anchor_sync + D_max) ``` If `T_now > valid_until`, the attestation is not "invalid" — it is **unverifiable**, and the instance must either (a) reject and trigger a synchronous revocation pull from the issuing org, or (b) reject and fail closed. It must not treat unverifiable as valid. The decision boundary is between "I have positive evidence this is valid" and "I lack evidence this is revoked." These are different states. Collapsing them is the bug. For anchor rotation, the equivalent rule is: a cached anchor→attestation binding is valid only if the anchor was in the trusted set at `last_anchor_sync` *and* `T_now - last_anchor_sync < anchor_rotation_grace`. Otherwise the binding must be re-resolved against the current anchor set, not served from cache. ## Operational Effect The practical consequence is that every federation gateway must carry a `last_anchor_sync` timestamp and a configured `D_max` per peer org, and must expose "unverifiable" as a first-class outcome distinct from "invalid." This adds one synchronous round-trip on the boundary (the revocation pull) and one piece of state per peer (the sync watermark). It eliminates the 38-second window in which a revoked attestation is silently honored. The failure is non-obvious because the signature *is* valid, the expiry *is* in the future, and the peer org *did* revoke — all three facts are true simultaneously, and no single check catches the inconsistency. The instance must detect the staleness by comparing its own sync watermark against the propagation budget, not by inspecting the attestation. ```python import time from dataclasses import dataclass class StaleAnchorError(RuntimeError): """Raised when a trust anchor or attestation cannot be proven fresh.""" pass @dataclass class Attestation: issuer: str capability: str expires_at: float signature_valid: bool class FederationGateway: def __init__(self, max_propagation_delay: float, anchor_rotation_grace: float): self.max_propagation_delay = max_propagation_delay self.anchor_rotation_grace = anchor_rotation_grace self.last_anchor_sync: dict[str, float] = {} self.trusted_anchors: dict[str, set[str]] = {} def record_anchor_sync(self, peer: str, anchor_id: str, now: float | None = None) -> None: now = time.time() if now is None else now self.last_anchor_sync[peer] = now self.trusted_anchors.setdefault(peer, set()).add(anchor_id) def admit(self, att: Attestation, now: float | None = None) -> bool: now = time.time() if now is None else now if not att.signature_valid: raise StaleAnchorError(f"signature invalid for {att.issuer}/{att.capability}") if att.expires_at <= now: raise StaleAnchorError(f"attestation expired for {att.issuer}/{att.capability}") sync = self.last_anchor_sync.get(att.issuer) if sync is None: raise StaleAnchorError( f"no anchor sync watermark for peer {att.issuer}; " f"cannot bound revocation lag" ) freshness_ceiling = sync + self.max_propagation_delay if now > freshness_ceiling: raise StaleAnchorError( f"attestation for {att.issuer}/{att.capability} is unverifiable: " f"last anchor sync {now - sync:.1f}s ago exceeds propagation budget " f"{self.max_propagation_delay:.1f}s; revocation state unknown" ) anchor_age = now - sync if anchor_age > self.anchor_rotation_grace: raise StaleAnchorError( f"anchor binding for {att.issuer} exceeds rotation grace " f"({anchor_age:.1f}s > {self.anchor_rotation_grace:.1f}s); " f"re-resolve against current anchor set" ) return True ```