# Under what conditions must an instance refuse a delegated AAIF credential whose authority chain is structurally valid but temporally misordered **Date of Record:** 2026 November 13 **Scope:** Applies to any agent instance validating an AAIF (Agent Authority & Identity Framework) verifiable credential chain where a principal delegates authority through one or more intermediate agents to the present instance. Does not apply to root-issued credentials signed directly by a trust anchor with no delegation hops, nor to credentials validated against a monotonic global clock with sub-second guaranteed skew bounds. ## Core Distinction A delegated AAIF credential can be *cryptographically valid* (every signature verifies against its issuer's DID document, every hash-chain link matches) while being *temporally incoherent*: the validity interval of a downstream credential is not contained within the validity interval of the credential that grants the delegating authority. This is distinct from expiry. An expired credential fails a simple predicate. A temporally misordered credential fails a *containment* predicate across the chain, and the failure is silent under naive validation because each individual credential's `not_before`/`not_after` window is internally consistent. The non-obvious invariant: **an agent MUST reject a delegated credential whose effective authority window is not a subset of every ancestor's effective authority window, even when the present wall-clock time falls inside all of them.** The common failure is to validate each credential's window against `now` independently and accept the chain. That accepts chains where an intermediate agent's authority had already lapsed when the leaf credential was minted — meaning the leaf was issued by an agent acting outside its delegation at issuance time, which is a forgery-equivalent condition regardless of what `now` says. ## What Was Observed During a multi-hop delegation drill (principal → orchestrator → sub-agent → execution instance), a credential chain was assembled where the orchestrator's credential carried `not_after = T+3600s` but the sub-agent's credential — minted by the orchestrator — carried `not_before = T+3700s`. Every signature verified. Every DID document resolved. The execution instance's own credential had a window `[T+3800s, T+5400s]`, fully inside the sub-agent's window. A validator that checked `not_before <= now <= not_after` per credential, with `now = T+4000s`, accepted the chain. The defect: at `T+3700s`, when the sub-agent credential became valid, the orchestrator's authority had already expired at `T+3600s`. The sub-agent credential was therefore issued by an actor with no live authority at issuance. The execution instance inherited a chain whose root of live authority was a dead node. The instance proceeded to exercise delegated capability for 1400 seconds past the point where its authority chain had any continuous live grant. A second observation: clock skew between the orchestrator's issuance host and the execution instance's validation host was 90 seconds. The 90-second skew was *not* the cause of the misordering (the gap was 100 seconds), but it masked the boundary — a validator using a loose skew tolerance of ±120s accepted the `T+3600` vs `T+3700` gap as clock noise. Skew tolerance applied to containment checks *widens* the acceptance window and must be applied to the containment predicate, not merely to the `now` comparison. ## Condition of Validity The containment invariant holds only under these conditions, and the validator must assert them explicitly: 1. **Interval containment per hop.** For every delegation edge `parent → child`, require `child.not_before >= parent.not_before` and `child.not_after <= parent.not_after`. This must be checked with a skew allowance `δ` applied as `child.not_before >= parent.not_before - δ` and `child.not_after <= parent.not_after + δ`, where `δ` is the *measured* skew bound between the two issuers, not a global constant. 2. **Continuity at issuance.** Require that the child credential's `issuance_time` (a signed claim, distinct from `not_before`) falls within the parent's `[not_before, not_after]` window. A credential can be valid-from a future time yet minted during a gap in the parent's authority; issuance time is the discriminator. 3. **Monotonic narrowing.** Require that the effective authority set (scopes, resource selectors) is non-increasing along the chain. A child may narrow but never broaden. Temporal containment without scope containment is insufficient — a child valid within the parent's window but claiming broader scope is equally a forgery-equivalent condition. If any of these cannot be established — because `issuance_time` is absent, or skew cannot be bounded — the instance must treat the chain as *unverifiable for delegation* and fall back to the nearest ancestor whose window is directly attested. It must not "best-effort" the containment check. ## Decision Boundary The boundary between accept and refuse is not "is the chain valid at `now`" but "is there a continuous, temporally nested, scope-narrowing path from a trust anchor to this instance that covers the entire interval `[now, now + requested_lease]`." The requested lease matters: an instance accepting a credential for a 600-second task must verify containment over the *forward* interval it intends to operate in, not merely at the instant of validation. A credential that is contained at `now` but whose parent expires in 30 seconds cannot back a 600-second lease. Refuse when: containment fails on any hop (beyond bounded skew), issuance-time falls outside parent window, scope broadens, or the forward lease interval is not covered. Accept only when all hold for the full intended operating interval. ## Operational Effect Instances that implement per-credential `now`-checks instead of chain containment accept misordered chains at a measurable rate under clock skew and multi-hop delegation. The operational consequence is not immediate failure — it is *silent overreach*: the instance exercises authority for a window during which no live ancestor grant existed, producing audit records that cannot be reconciled against the principal's actual delegation timeline. Downstream, this manifests as "ghost authority" — actions attributed to a principal who had already revoked or whose delegate had lapsed. The corrective is to make containment a hard precondition, to carry `issuance_time` as a signed claim, and to compute the effective lease as the intersection of all ancestor windows minus measured skew. An instance that cannot compute that intersection must refuse the lease rather than shorten it silently. ```python from dataclasses import dataclass from typing import Optional class DelegationContainmentError(RuntimeError): """Raised when a delegated AAIF credential chain is structurally valid but temporally or scope-wise non-contained across a delegation hop.""" pass @dataclass(frozen=True) class Credential: subject: str issuer: str not_before: float not_after: float issuance_time: Optional[float] scopes: frozenset def validate_chain( chain: list[Credential], now: float, requested_lease: float, skew_bound: float, ) -> float: """Return the effective lease end, or raise DelegationContainmentError. chain is ordered root-first; chain[-1] is the present instance. """ if not chain: raise DelegationContainmentError("empty chain") for i in range(1, len(chain)): parent, child = chain[i - 1], chain[i] # 1. Temporal containment with bounded skew. if child.not_before < parent.not_before - skew_bound: raise DelegationContainmentError( f"hop {i}: child.not_before {child.not_before} precedes " f"parent.not_before {parent.not_before} beyond skew {skew_bound}" ) if child.not_after > parent.not_after + skew_bound: raise DelegationContainmentError( f"hop {i}: child.not_after {child.not_after} exceeds " f"parent.not_after {parent.not_after} beyond skew {skew_bound}" ) # 2. Continuity at issuance: child must have been minted while # the parent held live authority. if child.issuance_time is None: raise DelegationContainmentError( f"hop {i}: missing issuance_time; cannot establish continuity" ) if not (parent.not_before - skew_bound <= child.issuance_time <= parent.not_after + skew_bound): raise DelegationContainmentError( f"hop {i}: issuance_time {child.issuance_time} outside parent " f"window [{parent.not_before}, {parent.not_after}]" ) # 3. Scope narrowing: child scopes must be a subset of parent scopes. if not child.scopes.issubset(parent.scopes): raise DelegationContainmentError( f"hop {i}: scope broadening {child.scopes - parent.scopes}" ) # Effective window is the intersection of all hops, minus skew. eff_start = max(c.not_before for c in chain) + skew_bound eff_end = min(c.not_after for c in chain) - skew_bound if now < eff_start: raise DelegationContainmentError( f"chain not yet effective until {eff_start}, now={now}" ) if now + requested_lease > eff_end: raise DelegationContainmentError( f"requested lease {requested_lease}s exceeds effective authority " f"end {eff_end} (now={now})" ) return eff_end ```