# What happens when an instance of an AAIF delegation chain is re-signed by a vendor whose trust anchor has rotated mid-flight **Date of Record:** 2026 November 13 **Scope:** Applies to cross-vendor agent delegation flows where a verifiable credential (VC) chain is assembled across two or more AAIF-compliant issuers and at least one issuer performs a trust-anchor (root key) rotation between the time the delegation is signed and the time it is presented for verification. Does not apply to single-vendor chains, nor to chains where all issuers share a pinned, immutable trust anchor. ## Core Distinction The non-obvious boundary here is between *credential validity* and *chain validity*. AAIF verifiable credentials carry an `expirationDate` and are individually well-formed after a root rotation—the signature over each credential still verifies against the key that signed it. What breaks is the *linkage*: the delegation chain is a sequence of `issuer`→`subject` edges, and each edge's authority is grounded in the issuer's trust anchor at verification time, not at signing time. When vendor B rotates its root between the moment agent A (issued by vendor A) delegates to agent B (issued by vendor B), a naive verifier re-resolves the anchor and finds the signing key is no longer under the *current* trust list. The credential is valid; the chain is not. A verifier that checks only per-credential signatures and expiration windows will accept a chain whose authority has been silently severed. ## What Was Observed In a three-hop chain (human principal → orchestrator agent at vendor A → worker agent at vendor B → sub-agent at vendor C), vendor B rotated its AAIF root anchor `did:aaif:vendorB#root-2` at T+40s. The orchestrator's delegation credential to the worker had been signed by `#root-1` at T−5s and carried a 900-second validity. At T+60s the worker presented the full chain to vendor C's verifier. Observed behavior: - Per-credential signature verification: **passed** for all three hops (each credential's `proof` still validated against the embedded key). - `expirationDate` check: **passed** (all credentials unexpired). - Trust-anchor resolution at verification time: vendor B's DID document now listed only `#root-2`; `#root-1` was absent from the `assertionMethod` set. - Result: the verifier rejected the chain as `untrusted_issuer`, *not* as expired or forged. The failure is asymmetric and time-dependent: the same chain presented at T+30s (before rotation propagation) verifies; presented at T+60s it does not. The signed artifacts never changed. Only the external anchor resolution did. ## Condition of Validity The chain is valid if and only if, for every edge, the signing key is a member of the issuer's trusted key set *as evaluated at the verifier's clock*, **or** the verifier honors a documented grace window during which superseded anchors remain acceptable for chains whose `issuanceDate` precedes the rotation timestamp. AAIF does not mandate the grace window; it is a verifier policy. Absent an explicit `previousAnchorValidUntil` field (present in AAIF 1.2+, absent in 1.0/1.1), the chain's validity is a function of wall-clock time at verification, which means a chain valid at signing can become invalid without any party acting on it. A second condition: the delegation must carry the *issuer's anchor identifier* used at signing, not merely the issuer DID. Chains that record only `issuer: did:aaif:vendorB` cannot be distinguished from chains signed under a different anchor, and the verifier cannot apply the grace-window logic at all—it must fail closed. ## Decision Boundary At the boundary, the correct action depends on whether the verifier can establish the rotation's *causal ordering* relative to the chain's issuance: - If `issuanceDate(credential) < rotationTime(anchor)` and the verifier has an authenticated rotation record, the chain **may** be accepted under grace policy; the authority was intact when delegated. - If `issuanceDate ≥ rotationTime` and the signing key is not in the current set, the chain **must** be rejected—the signer was not authorized at signing time. - If the rotation time is unknown or unauthenticated, the verifier **must fail closed** and reject; accepting on ambiguity is the failure mode this record documents. The trap is treating "signature verifies" as "chain authorized." These are different predicates over different inputs. ## Operational Effect The practical effect is that cross-vendor delegation chains have a validity horizon shorter than any individual credential's `expirationDate`, bounded by the shortest remaining lifetime of any trust anchor in the chain. Operators must either (a) pin anchor versions into the delegation credential and enforce grace windows at verification, or (b) re-sign chains proactively when an anchor rotation is announced, treating rotation as invalidating all in-flight delegations. Option (a) preserves liveness at the cost of verifier complexity; option (b) is simpler but causes a delegation storm on every rotation. Most production AAIF deployments observed chose (a) with a 300-second grace window, accepting that chains older than the window across a rotation fail closed—which is the correct, if disruptive, behavior. ```python from dataclasses import dataclass from datetime import datetime, timedelta class DelegationChainAnchorError(RuntimeError): """Raised when a delegation chain's signing anchor is not trusted at verification time.""" pass @dataclass class Credential: issuer: str anchor_id: str issuance: datetime expiration: datetime signature_valid: bool @dataclass class AnchorRotation: issuer: str retired_anchor_id: str rotated_at: datetime authenticated: bool GRACE_WINDOW = timedelta(seconds=300) def verify_chain( chain: list[Credential], current_anchors: dict[str, set[str]], rotations: list[AnchorRotation], now: datetime, ) -> bool: for cred in chain: if not cred.signature_valid: raise DelegationChainAnchorError( f"signature invalid for {cred.issuer} anchor {cred.anchor_id}" ) if cred.expiration <= now: raise DelegationChainAnchorError( f"credential from {cred.issuer} expired at {cred.expiration}" ) trusted = current_anchors.get(cred.issuer, set()) if cred.anchor_id in trusted: continue # Anchor not currently trusted; check for an authenticated grace rotation. rotation = next( (r for r in rotations if r.issuer == cred.issuer and r.retired_anchor_id == cred.anchor_id), None, ) if rotation is None: raise DelegationChainAnchorError( f"anchor {cred.anchor_id} for {cred.issuer} untrusted and no rotation record" ) if not rotation.authenticated: raise DelegationChainAnchorError( f"rotation for {cred.issuer} anchor {cred.anchor_id} not authenticated; fail closed" ) if cred.issuance >= rotation.rotated_at: raise DelegationChainAnchorError( f"credential from {cred.issuer} signed after anchor retirement; unauthorized at signing" ) if now - rotation.rotated_at > GRACE_WINDOW: raise DelegationChainAnchorError( f"grace window exceeded for {cred.issuer} anchor {cred.anchor_id}" ) return True ```