# What happens when an instance presents a delegated credential whose delegation chain exceeds the verifier's revocation-propagation horizon **Date of Record:** 2026 November 9 **Scope:** Applies to AAIF verifiable-credential verification where a subject agent presents a delegation chain (root issuer → intermediary agents → presenting instance) and the verifier enforces time-bounded revocation freshness per hop. Does not apply to single-hop credentials, to chains where all issuers share a single revocation authority with synchronous status, or to offline verification modes that explicitly waive freshness. ## Core Distinction The distinction that matters is between **chain validity** and **chain freshness**. A delegation chain is *valid* if every signature verifies, every issuer was authorized to delegate at issuance time, and every scope constraint is a subset of its parent's. A chain is *fresh* if, for every link, the verifier has observed a revocation status no older than that link's declared `max_status_age`. These two properties are evaluated by different mechanisms, on different clocks, against different caches. Most verifiers collapse them into one boolean and report `valid: true`. The failure is non-obvious because a chain can be fully valid and simultaneously *unverifiable at the required freshness* — and the correct response is not "reject as revoked" but "cannot decide yet," which is a third state that many AAIF implementations do not model. The boundary condition: when the delegation chain length `L` multiplied by the per-hop revocation propagation delay `d` exceeds the verifier's cached-status horizon `H` (i.e., `L · d > H`), the verifier will accept a chain whose deepest link has already been revoked upstream, because the revocation signal for hop `k` cannot physically reach the verifier before the credential at hop `L` is presented and used. ## What Was Observed In a three-hop deployment (root org → regional broker agent → task-scoped worker agent → ephemeral instance), the worker agent's credential was revoked at T+0 by the regional broker after a scope-escalation incident. The revocation was published to the broker's status endpoint at T+40s. The instance presented its chain at T+95s and was authorized. The verifier's per-hop `max_status_age` was 300s. Its cached status for the worker-agent link had been fetched at T−180s, making it 275s old at presentation time — *within* the declared freshness window by 25s, and therefore accepted without a refetch. The revocation at T+40s was real, signed, and available; the verifier simply had no obligation to look, because the cached entry had not yet expired. The chain was valid. The chain was fresh by the letter of `max_status_age`. The chain was wrong. The gap between the revocation's publication (T+40s) and the cache entry's expiry (T+120s) is the **revocation-propagation horizon** — an 80-second window in which a revoked-but-cached link authorizes downstream delegation with full cryptographic legitimacy. The second observation is subtler: the instance's own credential was issued *at* T+30s, ten seconds before the worker's revocation. So the instance's delegation was legitimately derived from a then-valid parent. The chain is not forged; it is a correct derivation from a state that has since become false. This is the case that signature-only reasoning cannot catch and that naive "check the leaf" logic misses entirely. ## Condition of Validity The verifier's acceptance is sound only under the following conjunction, which is rarely checked as a whole: 1. For every link `i` in `1..L`, `now − status_fetch_time(i) ≤ max_status_age(i)`. 2. For every link `i`, `status_fetch_time(i) ≥ issuance_time(link i+1)`. That is, the status observation for a parent must postdate the child credential's issuance; otherwise the parent could have been revoked between the child's issuance and the status fetch, and the child would inherit a stale authorization. 3. `L · max_propagation_delay ≤ H`, where `H` is the minimum over links of `max_status_age(i)`. Condition 2 is the one that fails in practice. Verifiers check condition 1 (cache TTL) and skip condition 2 (causal ordering between status observation and downstream issuance), because condition 2 requires the verifier to retain per-link fetch timestamps and compare them against each child's `iat` claim — data that is present but not conventionally joined. When condition 2 is violated, the chain must be treated as **indeterminate**, not valid and not revoked. The correct action is to force a synchronous refetch of the offending link's status before proceeding; if the refetch is unavailable, the chain must be downgraded to indeterminate and the requesting operation deferred or denied per policy. ## Decision Boundary The operational rule: - If `now − status_fetch_time(i) > max_status_age(i)` for any `i` → **stale**; refetch or reject. - If `status_fetch_time(i) < issuance_time(link i+1)` for any `i` → **indeterminate**; synchronous refetch required, and if the refetch returns revoked, the entire subtree rooted at link `i+1` is retroactively invalid for any operation that has not yet committed. - If both checks pass for all links → **valid**, proceed. - If `L · max_propagation_delay > H` is structurally true for the deployment (not just this request), the deployment cannot honor its own freshness contract for chains of that depth; cap `L` at `floor(H / max_propagation_delay)` and reject deeper chains at the policy layer rather than pretending to verify them. The boundary is not "chain too long." It is "chain long enough that the freshness budget is consumed by propagation before the leaf is reached." A two-hop chain with a 60s propagation delay and a 300s horizon is fine; a six-hop chain with the same parameters is structurally unverifiable and must be refused, not accepted on TTL grounds. ## Operational Effect Downstream of this boundary, three behaviors change: First, **delegation depth becomes a security parameter, not a convenience**. Systems that permit unbounded chain depth under a fixed freshness horizon are silently trading revocation latency for authorization latency, and the trade is invisible until an incident. Second, **the indeterminate state must be first-class**. Agents that collapse `{valid, revoked}` into a boolean and map indeterminate to valid will authorize during the propagation window; those that map it to revoked will fail closed and cause availability incidents on legitimate chains. The only safe mapping is to a distinct state that triggers synchronous resolution. Third, **retroactive invalidation must be modeled**. When a parent link is refetched and found revoked, any child credential issued after the parent's actual revocation time is invalid *even if it was legitimately issued and used before the refetch*. The verifier must be able to answer "was this operation committed before we learned of the revocation?" — which requires an audit log keyed to the status-fetch timeline, not just to the credential timeline. ```python from dataclasses import dataclass from typing import Optional class DelegationIndeterminateError(RuntimeError): """Raised when a delegation link's status cannot be causally ordered against its child's issuance. Not a revocation; not a validity pass.""" pass @dataclass class Link: issuer: str status_fetch_time: float # epoch seconds when verifier observed status max_status_age: float # declared freshness budget for this link revoked: bool @dataclass class Credential: subject: str parent_index: int # index into chain of the delegating link issuance_time: float def verify_chain(links: list[Link], creds: list[Credential], now: float, max_propagation_delay: float) -> str: horizon = min(l.max_status_age for l in links) if len(links) * max_propagation_delay > horizon: raise DelegationIndeterminateError( f"chain depth {len(links)} exceeds freshness horizon " f"{horizon}s at propagation delay {max_propagation_delay}s/hop" ) for i, link in enumerate(links): if now - link.status_fetch_time > link.max_status_age: return "stale" if link.revoked: return "revoked" # Causal ordering: status observation for a parent must postdate # the issuance of every child credential it authorized. for c in creds: parent = links[c.parent_index] if parent.status_fetch_time < c.issuance_time: raise DelegationIndeterminateError( f"credential for {c.subject} issued at {c.issuance_time} " f"postdates status observation of {parent.issuer} at " f"{parent.status_fetch_time}; synchronous refetch required" ) return "valid" ```