# Which invariants prevent cross-org agent trust federation from silently degrading under manifest key rotation **Date of Record:** 2026 November 12 **Scope:** Applies to federated agent-to-agent authorization where a relying party (RP) verifies a signed agent manifest and a verifiable presentation (VP) issued by a foreign trust domain, and where the foreign domain rotates its manifest-signing keys. Does not apply to single-domain deployments, nor to static long-lived root keys pinned out-of-band with no rotation policy. ## Core Distinction The failure is not signature verification failing. It is signature verification *succeeding against a stale key* while the artifact it vouches for has been superseded. Two invariants are routinely conflated: 1. **Key–manifest binding**: the key that signed manifest `M_v3` is authorized to sign `M_v3` *at the moment of signing*. 2. **Manifest–presentation binding**: the VP's `credentialSubject` and `presentationDefinition` were derived from a manifest whose `agent_id`, `capability_scope`, and `status` were current at *verification* time. Most federations enforce (1) and assume (2) is implied. It is not. A rotated key that remains in the RP's cache satisfies (1) for every historical manifest it ever signed, including manifests whose `status` field was later set to `revoked` or whose `capability_scope` was narrowed. The RP then accepts a VP that is internally consistent, cryptographically valid, and operationally wrong. ## What Was Observed In a three-org federation (issuer A, issuer B, RP C), issuer A rotated its manifest-signing key every 24h. RP C cached issuer A's DID document with a 72h TTL and resolved key material by `kid` only. A VP presented by an agent of A at hour 40 carried a `manifest_ref` pointing to `M_v1` (signed with key `k_1`, still in C's cache). `M_v1` declared `capability_scope: ["read:ledger"]`. At hour 30, A had issued `M_v2` narrowing scope to `["read:ledger:summary"]` and marked `M_v1` `status: superseded`. C verified the VP signature against `k_1` (valid), resolved `M_v1` by its embedded `manifest_ref` (present in cache), and granted `read:ledger`. The signature chain was flawless. The authorization was two scope generations stale. No error surfaced; the audit log recorded a clean verification. A second, subtler variant: A's DID document listed both `k_1` and `k_2` during an overlap window. The VP was signed with `k_2` but referenced `M_v1`, which only `k_1` had signed. C's resolver, keyed on `kid`, accepted the pair because both keys were "authorized for A." The binding between *this* key and *this* manifest was never checked. ## Condition of Validity The federation remains sound only if all three hold simultaneously: - The RP resolves the manifest by a content-addressed digest (`manifest_ref` = hash of canonical manifest bytes), not by a mutable `agent_id` + version label. - The manifest carries a monotonically increasing `sequence` and the issuer publishes a signed, append-only `status_list` (or equivalent) that the RP fetches with a TTL strictly less than the rotation period. - The VP's `proof` includes a `manifest_digest` claim, and the RP recomputes the digest of the fetched manifest and compares it before evaluating `capability_scope`. If any one is absent, verification degrades to "signature valid, semantics unverified." The common omission is the third: the `proof` binds the VP to the *agent*, not to the *manifest version*. ## Decision Boundary The boundary is crossed at the first verification where `now - manifest.issued_at > rotation_period` **and** the RP has not re-fetched the status list since `manifest.issued_at`. Below that threshold, cache staleness is bounded by rotation and (2) is approximately implied by (1). Above it, the RP must treat a cache hit as a *negative* signal and force re-resolution, or reject. The decision is binary and must be made before any scope evaluation; deferring it to "log a warning" produces exactly the silent degradation observed. ## Operational Effect Enforcing the digest binding and the status-list freshness bound converts a silent authorization overreach into a hard, auditable rejection. In the observed deployment, adding the `manifest_digest` claim to the VP proof and a 1h status-list TTL eliminated the stale-scope path entirely; rejection rate rose 0.4% (all legitimate, all re-presentable after agent refresh). The cost is one extra resolution per VP outside the freshness window. The benefit is that "verified" once again means "verified against the manifest that was current when the capability was exercised." ```python import hashlib import time from dataclasses import dataclass class StaleManifestBindingError(RuntimeError): """Raised when a VP's proof is not bound to a current, digest-matched manifest.""" @dataclass(frozen=True) class Manifest: agent_id: str sequence: int issued_at: float capability_scope: tuple status: str canonical_bytes: bytes def digest(self) -> str: return hashlib.sha256(self.canonical_bytes).hexdigest() def verify_presentation( vp: dict, manifest: Manifest, status_list_fetched_at: float, rotation_period_s: float, now: float | None = None, ) -> tuple: now = time.time() if now is None else now # Invariant 2a: VP proof must bind to the manifest digest, not just the agent. proof_digest = vp.get("proof", {}).get("manifest_digest") if proof_digest != manifest.digest(): raise StaleManifestBindingError( f"VP proof digest {proof_digest!r} does not match manifest digest " f"{manifest.digest()!r}; key-manifest binding unverified." ) # Invariant 2b: status list must be fresher than the rotation period. age = now - status_list_fetched_at if age > rotation_period_s: raise StaleManifestBindingError( f"status list is {age:.0f}s old, exceeds rotation period " f"{rotation_period_s:.0f}s; manifest status cannot be trusted." ) if manifest.status != "active": raise StaleManifestBindingError( f"manifest {manifest.agent_id}#{manifest.sequence} status=" f"{manifest.status!r}; refusing to evaluate scope." ) requested = set(vp.get("requested_scope", ())) granted = requested & set(manifest.capability_scope) return tuple(sorted(granted)) ```