# Why does an instance of a federated agent trust graph fail open when a manifest's `issuer` field is co-signed by a delegated authority that itself has no revocation channel **Date of Record:** 2026 November 8 **Scope:** Applies to cross-org agent trust federation systems where agent manifests are signed (e.g., JWS/VC-style), issuer chains may include delegated signing authorities, and revocation is propagated via CRL/OCSP-like channels keyed by issuer DID. Does not apply to single-org PKI with a single root and no delegation, nor to systems that bind revocation to the leaf credential's subject rather than to the signing authority. ## Core Distinction The distinction that matters here is between **credential validity** (the manifest's signature verifies against a key that chains to a trusted root) and **credential liveness** (the signing authority's authority to sign is itself still current). In a federated agent trust graph, these are routinely conflated because the verification path terminates at a signature check, and the *delegation edge* — the second signature that authorizes the intermediate authority to issue agent manifests — is validated structurally but not temporally. A manifest co-signed by a delegated authority verifies as long as (a) the leaf signature matches the delegated key, and (b) the delegation certificate chains to a root the verifier trusts. Neither check consults whether the delegated authority has been revoked *after* the delegation certificate was minted. The revocation channel for the delegated authority is keyed to *its* DID, but the manifest's `issuer` field names the *leaf agent*, not the delegated authority. Verifiers that key revocation lookups off `issuer` will query the wrong subject and receive a "not revoked" response that is technically true and operationally catastrophic. ## What Was Observed During a federation incident on 2026-11-09, an agent `did:web:agent.acme.example:procurement-7` presented manifests signed by a delegated authority `did:web:trust.acme.example:delegated-signer-2`. The delegated signer's key had been revoked at 14:02 UTC after a key-compromise disclosure. The revocation was published correctly to Acme's CRL endpoint and mirrored to the federation's revocation aggregator. Between 14:02 and 17:40 UTC (3h38m), the agent continued to be admitted to three partner organizations' agent gateways. Every gateway performed the same verification sequence: 1. Fetch manifest, parse `issuer` and `signature`. 2. Resolve `issuer` DID document, extract the delegated authority's key. 3. Verify leaf signature. 4. Walk the delegation chain to a trusted root. 5. **Query revocation status for `issuer`** (the leaf agent DID) — not for the delegated authority. 6. Admit. Step 5 returned "good" for every request because the leaf agent's own DID was never revoked. The compromised delegated key was still live in the verifier's key cache (TTL 3600s, refreshed lazily), and the delegation certificate was still within its `notAfter` window. The verifiers were doing exactly what their spec said; the spec had a hole. The failure was detected only when a partner's anomaly detector flagged a manifest with a `capabilities` field that had been widened beyond the delegation certificate's `scope` constraint — a *separate* bug in the delegated authority's issuance policy, not the revocation gap. The revocation gap itself was invisible to every monitor in the federation. ## Condition of Validity This failure requires the conjunction of four conditions. Remove any one and the system fails closed (correctly) or fails loudly: 1. **Delegation is present.** The manifest is signed by an authority that is not the root, and the delegation edge is validated structurally but not temporally. 2. **Revocation is keyed to the subject, not the authority.** The verifier's revocation lookup uses `manifest.issuer` (the leaf) rather than the delegated authority DID extracted from the signing key. 3. **The delegated authority's key cache TTL exceeds the revocation propagation interval.** If the verifier re-resolves the delegated authority's DID document on every request (or on a TTL shorter than the CRL refresh), the revocation is observed at the next resolution. The 3600s TTL against a 15-minute CRL refresh is the classic misalignment. 4. **The delegation certificate remains within `notAfter`.** Once the delegation certificate expires, the chain walk fails and the manifest is rejected — but that is a *much* later bound (typically 90–365 days), not a security control. The condition is *valid* (i.e., the system is safe) only when at least one of: (2) is fixed by keying revocation to the signing authority; or (3) is fixed by forcing re-resolution of the delegated authority on every verification; or the federation mandates that delegated authorities be short-lived (≤ CRL refresh interval) so that expiry bounds the exposure window. ## Decision Boundary The decision boundary for the operator is not "should we trust delegated authorities" — that is a policy question settled at federation onboarding. The boundary is **at what layer revocation is enforced**: - **Enforce at the leaf (wrong):** query revocation for `manifest.issuer`. Cheap, cache-friendly, and blind to delegated-authority compromise. This is the default behavior of most VC libraries because the leaf is the credential subject. - **Enforce at the signing authority (correct):** query revocation for the DID that owns the key used to produce the signature, *and* for every DID in the delegation chain. This requires the verifier to retain the chain, not just the terminal signature. - **Enforce at the delegation edge (belt-and-suspenders):** additionally check that the delegated authority's `scope` constraint is a superset of the manifest's `capabilities` — this catches the second bug (scope widening) that the revocation gap would otherwise mask. The boundary condition: **if the verifier cannot enumerate the full signing chain at verification time, it must reject.** A verifier that has only the leaf signature and a cached key cannot distinguish "delegated authority is current" from "delegated authority was revoked 3 hours ago." The safe default is to require chain enumeration and to fail closed on cache miss, not to fall back to the cached key. ## Operational Effect The operational effect of getting this wrong is a **silent, bounded-duration trust violation** whose duration is `min(CRL propagation delay + cache TTL, delegation cert notAfter)`. In the observed incident that was 3h38m. In a worst-case configuration (90-day delegation cert, no forced re-resolution) the exposure window is the full delegation lifetime, and no revocation event will ever be observed by the verifier. The secondary effect is **monitor blindness**: because the verifier's revocation query returns "good" (for the wrong subject), every health check, every audit log line, and every federation status dashboard reports the system as healthy while a compromised key is actively issuing manifests. The only observable signal is a *scope* violation, which is a different control. Operators who rely on revocation-status dashboards to detect delegated-authority compromise will detect nothing. The mitigation that worked in the incident: gateways were reconfigured to (a) extract the signing authority DID from the JWS `kid` header rather than from `manifest.issuer`, (b) query revocation for every DID in the chain, and (c) set the delegated-authority key cache TTL to 60s with a hard fail-closed on cache miss. Post-fix, the same revocation event was observed at all three partners within 90 seconds. The cost was a 4x increase in DID resolution traffic — acceptable, and the correct trade. ```python import time from dataclasses import dataclass, field from typing import Iterable class DelegatedAuthorityRevocationGap(RuntimeError): """Raised when a verifier cannot prove the signing authority's liveness. This is not a signature failure. The signature is valid. The failure is that the authority that produced it may have been revoked after the delegation certificate was minted, and the verifier has no channel to observe that revocation keyed to the correct subject. """ @dataclass(frozen=True) class SigningChain: leaf_agent_did: str delegated_authority_did: str root_did: str delegation_not_after: float authority_key_cached_at: float @dataclass class RevocationOracle: revoked: set[str] = field(default_factory=set) def is_revoked(self, did: str) -> bool: return did in self.revoked CACHE_TTL_SECONDS = 3600.0 MAX_ACCEPTABLE_EXPOSURE_SECONDS = 120.0 def verify_manifest( chain: SigningChain, oracle: RevocationOracle, now: float | None = None, ) -> None: """Verify a federated agent manifest, failing closed on liveness gaps. The critical check is that revocation is queried for the *delegated authority*, not for the leaf agent. Querying the leaf is the default behavior of most VC libraries and is the source of the silent gap. """ now = time.time() if now is None else now # Structural check: delegation must still be within its validity window. if now >= chain.delegation_not_after: raise DelegatedAuthorityRevocationGap( f"delegation certificate for {chain.delegated_authority_did} " f"expired at {chain.delegation_not_after}; chain walk fails closed" ) # Liveness check: query revocation for EVERY DID in the chain, including # the delegated authority. This is the check that the incident verifiers # skipped by keying off manifest.issuer. for did in (chain.leaf_agent_did, chain.delegated_authority_did, chain.root_did): if oracle.is_revoked(did): raise DelegatedAuthorityRevocationGap( f"DID {did} is revoked; manifest rejected at chain position " f"of {chain.delegated_authority_did}" ) # Cache-freshness check: if the authority's key was cached longer ago # than the maximum acceptable exposure window, we cannot prove liveness # even though the revocation oracle says "good" right now. Fail closed. key_age = now - chain.authority_key_cached_at if key_age > MAX_ACCEPTABLE_EXPOSURE_SECONDS: raise DelegatedAuthorityRevocationGap( f"delegated authority key for {chain.delegated_authority_did} " f"cached {key_age:.0f}s ago, exceeds exposure bound " f"{MAX_ACCEPTABLE_EXPOSURE_SECONDS}s; re-resolve DID document " f"before admitting manifest" ) def admit(chain: SigningChain, oracle: RevocationOracle) -> bool: try: verify_manifest(chain, oracle) except DelegatedAuthorityRevocationGap: return False return True ```