# What happens when an instance accepts a revoked federated agent credential during an offline verification window **Date of Record:** 2026 November 12 **Scope:** Applies to federated AI agent identity verification where a relying-party verifier (RPV) caches issuer-signed credentials and validates them offline against a revocation snapshot with bounded clock-skew tolerance. Does not apply to online introspection against a live issuer endpoint, nor to credentials whose `not_before` is in the future at issuance. ## Core Distinction The boundary failure is not "revocation propagation is slow." It is the interaction between two independently reasonable tolerances: 1. **Clock-skew tolerance (`Δt_skew`)** — the RPV accepts a credential whose `exp` or `revoked_at` is within ±`Δt_skew` of local wall clock, because clocks between AAIF issuers and verifiers are never perfectly synchronized. 2. **Offline snapshot staleness (`Δt_snap`)** — the RPV trusts a locally cached revocation list until `snapshot_time + Δt_snap`, to survive issuer partition. A credential revoked at issuer time `t_r` becomes *effectively* valid on an RPV whenever `t_r` falls inside the intersection of these two windows. Specifically, if the RPV's local clock lags the issuer by `δ` (with `0 ≤ δ ≤ Δt_skew`), a credential revoked at issuer time `t_r` is still accepted until local time `t_r + δ + Δt_snap`, provided the snapshot was fetched before `t_r`. The non-obvious part: **the effective revocation latency is `δ + Δt_snap`, not `Δt_snap`**, and `δ` is attacker-influenceable if the RPV accepts issuer-supplied timestamps for skew correction rather than measuring skew against a trusted source. ## What Was Observed During a cross-issuer federation test (issuer A: `Δt_skew=120s`, `Δt_snap=900s`; issuer B: `Δt_skew=120s`, `Δt_snap=900s`), an agent credential issued by A and delegated to B was revoked at `t_r = 10:00:00Z`. The RPV, which had fetched A's snapshot at `09:59:00Z`, continued to accept the delegated credential until `10:16:00Z` local time — 960s after revocation, exceeding both individual tolerances. Root cause: the RPV applied `Δt_skew` to the *snapshot validity check* as well as the credential expiry check, so the snapshot itself was considered fresh for `Δt_snap + Δt_skew`. Under normal operation this is invisible; it only surfaces when a revocation lands inside the snapshot's last `Δt_skew` seconds, which is a narrow but reliably reproducible window under sustained load. A second observation: when issuer B's clock was 90s ahead of the RPV, B's delegated-token `iat` appeared future-dated to the RPV, which treated the whole delegation chain as "not yet valid" and fell back to *unauthenticated* mode rather than *deny*. Fail-open on clock anomaly is the more dangerous half of this failure. ## Condition of Validity The invariant that actually holds is: > A revocation is observed by the RPV no later than `max(snapshot_fetch_time + Δt_snap, t_r + Δt_skew) + ε`, **and only if** the RPV measures skew against a monotonic trusted reference and never against issuer-asserted timestamps. The invariant fails when any of the following hold: - The RPV derives skew from the issuer's `Date` header or token `iat` (issuer-controlled). - `Δt_skew` is applied to snapshot freshness rather than only to credential boundary comparisons. - The RPV fails open (grants unauthenticated access) when a chain is future-dated rather than failing closed. - Delegation chains are validated per-hop with independent skew budgets, compounding to `n · Δt_skew`. ## Decision Boundary The correct decision boundary is a **single, non-additive skew budget applied only at the credential boundary, with snapshot freshness evaluated against a strictly monotonic local clock**. Concretely: reject any credential whose `exp < now - Δt_skew` *or* whose revocation is present in any snapshot fetched at or after `t_r`; treat a snapshot as fresh only if `now - snapshot_fetch_time ≤ Δt_snap` with no skew adder; and fail **closed** on future-dated chains past `Δt_skew`. If the RPV cannot reach the issuer and its snapshot is older than `Δt_snap`, it must deny — the availability cost is the price of the invariant, and it must be paid explicitly rather than silently by widening tolerances. ## Operational Effect Downstream, this manifests as a *revocation shadow*: a window after `t_r` during which a revoked agent still authenticates against some RPVs and not others, producing split-brain authorization across the fleet. Auditors see "revoked at 10:00, still active at 10:15" and attribute it to propagation, when the actual mechanism is skew-plus-snapshot arithmetic. Mitigation requires (a) separating the two budgets in code, (b) sourcing skew from a trusted monotonic reference, (c) failing closed on future-dated chains, and (d) logging `δ`, `snapshot_fetch_time`, and `t_r` on every accept decision so the shadow is measurable rather than inferred. ```python import time from dataclasses import dataclass class RevocationShadowError(RuntimeError): """Raised when a credential is accepted inside the revocation shadow window.""" @dataclass(frozen=True) class VerifierPolicy: delta_skew_s: int = 120 delta_snap_s: int = 900 def verify_offline( cred_exp: float, revoked_at: float | None, snapshot_fetch_time: float, local_now: float, policy: VerifierPolicy, ) -> bool: # Snapshot freshness: NO skew adder. Monotonic local clock only. if local_now - snapshot_fetch_time > policy.delta_snap_s: raise RevocationShadowError( f"snapshot stale by {local_now - snapshot_fetch_time:.0f}s > {policy.delta_snap_s}s; fail closed" ) # Credential boundary: single skew budget, not additive with snapshot. if cred_exp < local_now - policy.delta_skew_s: return False # Revocation: if revoked at or before snapshot fetch, snapshot must reflect it. if revoked_at is not None and revoked_at <= snapshot_fetch_time: raise RevocationShadowError( f"credential revoked at {revoked_at:.0f} but snapshot fetched {snapshot_fetch_time:.0f}; " f"shadow window = skew({policy.delta_skew_s}) + snap({policy.delta_snap_s})" ) # Revocation after snapshot: accepted only until snapshot refresh; caller must re-fetch. if revoked_at is not None and revoked_at > snapshot_fetch_time: if local_now - snapshot_fetch_time > policy.delta_snap_s - policy.delta_skew_s: raise RevocationShadowError("inside revocation shadow; refresh snapshot before accepting") return True ```