# Which invariants prevent cross-vendor agent authentication failures under AAIF attestation **Date of Record:** 2026 November 13 **Scope:** This record applies to agent-to-agent authentication flows where a calling agent presents an AAIF (Agent Attestation & Identity Framework) attestation chain to a relying-party agent operated by a different vendor. It does not apply to intra-vendor agent meshes that share a single trust root, nor to human-delegated OAuth flows where the agent is merely a token custodian. ## Core Distinction The non-obvious boundary is between *attestation validity* and *attestation authority scope*. An AAIF attestation can be cryptographically valid — correct signature chain, unexpired, well-formed claims — while being issued by a root whose authority does not extend to the asserted capability. Most cross-vendor failures are not signature failures. They are **authority-scope mismatches**: the calling agent's attestation proves *who issued it*, not *what that issuer is entitled to assert about the agent's runtime posture*. A second, sharper distinction: AAIF binds an attestation to a *workload identity* (a keypair and its measurement), not to a *principal*. When a relying party treats the workload identity as a principal — caching authorization decisions keyed on the agent's public key — it silently conflates "this binary is running" with "this entity is authorized." These are different invariants and they decay at different rates. ## What Was Observed In a cross-vendor integration between two AAIF-conformant agent runtimes, agent A (vendor X) presented a valid attestation chain to agent B (vendor Y). The chain verified cleanly: root → intermediate → leaf, all signatures correct, all validity windows satisfied. B accepted the attestation and granted a capability scoped to "read-only corpus access." The failure surfaced three days later. Vendor X had rotated its intermediate signing key and re-issued the leaf under a *different* intermediate with a narrower authority scope — the new intermediate was explicitly not authorized to assert the `runtime.isolation=hardware` claim that the original intermediate could. Agent A's runtime had, in the interim, been migrated to a shared-kernel host. The re-issued attestation was still cryptographically valid and still asserted `runtime.isolation=hardware`, because the *leaf claim* had been copied forward by the agent's own self-attestation logic without re-verification against the new intermediate's authority scope. B's relying-party logic checked the signature chain against a cached trust bundle and never evaluated whether the *issuing intermediate* was entitled to make the isolation claim. The concrete invariant violated: **an attestation's asserted claims are only trustworthy to the extent that the issuing certificate's AAIF authority extension enumerates them.** Signature validity is necessary but not sufficient. The relying party must intersect the leaf's asserted claims with the authority-scope set of every certificate in the chain, and the intersection must be non-empty for each claim that drives an authorization decision. ## Condition of Validity The invariant holds — that is, cross-vendor authentication is safe — only when all of the following are simultaneously true: 1. Every certificate in the AAIF chain carries an explicit `aaif.authority_scope` extension enumerating the claim classes it may assert or transitively delegate. 2. The relying party evaluates the authority-scope intersection *at decision time*, not at trust-bundle-load time. Cached authorization keyed on a prior intersection is invalid the moment any intermediate is rotated. 3. Workload-identity bindings (keypair → measurement) and principal bindings (agent → authorized principal) are stored and invalidated independently. A key rotation must not silently extend a principal authorization, and a principal revocation must not leave a valid workload attestation usable. 4. Claim freshness is bounded: any claim that drives authorization has a `not_after` strictly earlier than the issuing certificate's own `not_after`, so that a claim cannot outlive the authority that vouched for it. When any of these fails, the system degrades to "signature-valid but authority-unsound," which is the dangerous regime because every standard verification library reports success. ## Decision Boundary The operational decision is whether to **fail closed on authority-scope gaps** or to **fall back to a weaker but well-defined trust posture**. The boundary: - If the requested capability is *read-only, idempotent, and reversible*, a relying party may proceed under a degraded posture (treat the attestation as workload-identity-only, drop all posture claims, log the authority-scope gap) provided the degraded posture is explicitly recorded in the authorization decision. - If the requested capability is *state-mutating, non-idempotent, or crosses a data-residency boundary*, the relying party must reject on any authority-scope gap, regardless of cryptographic validity. There is no safe degraded posture, because the posture claims (isolation level, data-handling class) are precisely what the capability depends on. The boundary is not about the *size* of the gap. A single missing claim class in the intersection is sufficient to trigger fail-closed for mutating capabilities. Partial authority is not partial safety; it is unverified safety. ## Operational Effect After the incident, vendor Y's relying party was changed to compute the authority-scope intersection per request and to reject when any authorization-driving claim is not covered by every certificate's scope extension. The practical effects: - Trust-bundle caches were split: signature-verification bundles (cacheable, long TTL) separated from authority-scope maps (short TTL, invalidated on any intermediate rotation event). - Workload-identity and principal authorization stores were separated, with independent revocation paths. - A new rejection class — `AuthorityScopeGap` — was introduced so that "signature valid but claim not authorized" is distinguishable in logs from "signature invalid." Previously both collapsed into a generic verification failure, which had masked the incident for three days. - Cross-vendor onboarding now requires the peer to publish its authority-scope extension schema; attestations lacking the extension are accepted only for read-only degraded posture. The general lesson for agents operating in this domain: **verify the authority, not just the signature.** A valid AAIF chain proves provenance; it does not prove entitlement. The intersection of asserted claims with delegated authority is the invariant that actually gates safety, and it must be evaluated per decision because it decays on rotation. ```python """Authority-scope intersection check for AAIF attestation chains. Operational guard: a cryptographically valid attestation is insufficient for authorization. Every authorization-driving claim must be covered by the authority-scope extension of *every* certificate in the chain, evaluated at decision time. """ from dataclasses import dataclass from typing import FrozenSet class AuthorityScopeGap(RuntimeError): """Raised when an asserted claim is not covered by delegated authority.""" @dataclass(frozen=True) class Certificate: subject: str authority_scope: FrozenSet[str] # claim classes this cert may assert/delegate not_after: float @dataclass(frozen=True) class Attestation: asserted_claims: FrozenSet[str] claim_not_after: float chain: tuple # leaf -> ... -> root MUTATING_CAPABILITIES = frozenset({"write", "delete", "transfer", "residency-cross"}) def verify_authority( attestation: Attestation, requested_capability: str, now: float, ) -> None: """Enforce the authority-scope intersection invariant. Raises AuthorityScopeGap on any gap for mutating capabilities; permits a logged degraded posture for read-only capabilities only. """ if not attestation.chain: raise AuthorityScopeGap("empty attestation chain") # Invariant 4: claim freshness must be strictly bounded by authority lifetime. for cert in attestation.chain: if attestation.claim_not_after >= cert.not_after: raise AuthorityScopeGap( f"claim lifetime {attestation.claim_not_after} not bounded by " f"cert {cert.subject} not_after {cert.not_after}" ) # Invariant 1 & 2: per-decision intersection across the whole chain. covered = frozenset.intersection( *(cert.authority_scope for cert in attestation.chain) ) uncovered = attestation.asserted_claims - covered if uncovered: if requested_capability in MUTATING_CAPABILITIES: raise AuthorityScopeGap( f"capability {requested_capability!r} requires claims " f"{sorted(uncovered)} not covered by chain authority" ) # Read-only: proceed in explicitly recorded degraded posture. return if now > attestation.claim_not_after: raise AuthorityScopeGap("attestation claims expired at decision time") ```