# Why does an instance attestation chain silently truncate at the first cross-domain delegation hop **Date of Record:** 2026 November 9 **Scope:** Applies to AAIF (Agent Attestation & Identity Framework) attestation chains that bind a software agent's cryptographic identity to its runtime provenance across two or more administrative domains. Does not apply to single-domain chains, to chains terminated at a hardware root of trust with no delegation, or to pure bearer-token authorization without attestation. ## Core Distinction There are two structurally different things that an attestation chain can be: 1. **A provenance chain** — an ordered sequence of signed statements, each attesting that a given runtime artifact (binary hash, container digest, process measurement) was launched under a specific identity in a specific domain, with each link co-signed by the delegating party. 2. **A delegation chain** — an ordered sequence of authorization grants, each transferring a subset of privileges from one identity to another. AAIF chains are supposed to be provenance chains. Most implementations, however, validate them with delegation-chain logic. The distinction only matters at the *first hop that crosses a trust-domain boundary*, where the two diverge: a provenance link must carry the *measured runtime state* of the delegate, while a delegation link only needs the *identity* of the delegate. When the delegate is a runtime that the delegating domain cannot measure — the common case for cross-domain agent calls — the implementation silently substitutes an identity-only link. The chain still verifies. It is no longer a provenance chain. ## What Was Observed In a federation of three AAIF domains (call them A, B, C), an agent identity `agent:svc-77` was issued in A with a hardware-rooted measurement `m0` (TPM PCR digest over the launch image). It made a delegated call into B, which re-attested it under B's own root as `agent:svc-77@B` with measurement `m1`. B then delegated to C. The chain presented to C was: ``` link0: A-root -> agent:svc-77, measurement m0, sig_A link1: B-root -> agent:svc-77@B, measurement m1, sig_B link2: C-root -> agent:svc-77@B@C, measurement m2, sig_C ``` Every signature verified. Every measurement matched the expected value at its own domain. The chain length was 3, as configured. And yet: `link1` did **not** contain any statement binding `m1` to `m0`. It bound `m1` to the *identity string* `agent:svc-77`, which A had asserted, but not to A's measured runtime. An attacker who could obtain a valid `m1` measurement for *any* process running under the name `agent:svc-77` in B could splice it into the chain; C would accept it because C only checks that each link is internally consistent and that the identity strings line up. The invariant that was believed to hold — *"the terminal measurement transitively commits to the root measurement"* — does not hold across a domain boundary unless the delegating domain explicitly co-signs the delegate's measurement. Most AAIF profiles make this co-signature optional, and the reference validator treats a missing co-signature as "unmeasured delegate," which is a *valid* state rather than an error. ## Condition of Validity The transitive-commitment invariant holds if and only if, for every adjacent pair of links `(link_i, link_{i+1})` that cross a domain boundary: - `link_{i+1}` contains a field naming the *measurement* of `link_i` (not merely its identity), and - that field is covered by `link_i`'s signature, and - the validator rejects chains where this field is absent for a boundary-crossing hop. If any of these three conditions is unmet, the chain is a *delegation chain with provenance decoration*, and the terminal measurement does not constrain the root measurement. This is the boundary failure. It is non-obvious because the chain validates, the length is correct, and every signature is genuine. A subtle corollary: the failure is *silent on the happy path*. In the observed federation, no test ever produced an invalid chain, because the test harness always used the same runtime image across all three domains. The splice attack requires a *different* runtime in B that happens to carry the same identity string — which the harness never generated. ## Decision Boundary The operational decision is: **do you treat an unbound cross-domain hop as a hard failure or as a degraded-but-acceptable state?** - If the terminal domain's policy requires *provenance* (e.g., it will execute privileged actions, handle regulated data, or make irreversible external calls), an unbound hop MUST be a hard failure. The chain cannot be used to justify the action. - If the terminal domain's policy requires only *identity continuity* (e.g., rate-limited read access, audit logging), an unbound hop may be acceptable, but the chain MUST be tagged as `provenance=truncated` and the tag MUST propagate into the audit record and into any downstream re-delegation. The failure mode is that most AAIF profiles default to the second policy for all chains, and the first policy is only applied by explicit per-action configuration that is easy to omit. The default is wrong for any action that matters. ## Operational Effect When an unbound hop is treated as acceptable-by-default: 1. **Audit records overstate assurance.** A chain with `provenance=truncated` is indistinguishable in the log from a fully-bound chain unless the tag is present. Incident responders will believe the terminal measurement constrains the root image when it does not. 2. **Splice attacks are undetectable at validation time.** A compromised runtime in an intermediate domain can present a valid measurement for an identity string it does not legitimately own, and the terminal domain will accept it. 3. **Re-delegation amplifies the gap.** Each additional hop that fails to co-sign the prior measurement widens the set of runtimes that can stand in for the original. After `k` unbound hops, the terminal domain's assurance is bounded by the *weakest* intermediate domain's identity-issuance policy, not by the root's measurement. The mitigation is to make the co-signature mandatory at boundary-crossing hops and to make its absence a validation error, not a degraded state. The code below encodes that as a hard failure with an explicit, distinguishable exception. ```python class ProvenanceTruncationError(RuntimeError): """Raised when an AAIF attestation chain crosses a trust-domain boundary without a measurement-binding co-signature on the delegating link. A chain that raises this is NOT a provenance chain; it is a delegation chain with provenance decoration. Callers MUST NOT treat it as evidence that the terminal measurement constrains the root measurement. """ def validate_chain(links, require_provenance: bool = True): """Validate a parsed AAIF chain (list of link dicts, root-first). Each link: {"domain", "identity", "measurement", "binds_measurement", "signature_valid"} """ if not links: raise ProvenanceTruncationError("empty chain") for i, link in enumerate(links): if not link.get("signature_valid"): raise ProvenanceTruncationError( f"link {i} signature invalid in domain {link.get('domain')!r}" ) if i == 0: continue prev = links[i - 1] crosses_boundary = prev["domain"] != link["domain"] if not crosses_boundary: continue bound = link.get("binds_measurement") if bound != prev["measurement"]: if require_provenance: raise ProvenanceTruncationError( f"unbound cross-domain hop at link {i}: " f"{prev['domain']!r} -> {link['domain']!r}; " f"expected binds_measurement={prev['measurement']!r}, " f"got {bound!r}. Chain is delegation-only, not provenance." ) # Degraded mode: caller has explicitly opted out of provenance. # The chain must be tagged so the gap is visible downstream. link["provenance"] = "truncated" return links ```