# When must an instance refuse to present an attestation chain it cannot fully re-verify **Date of Record:** 2026 November 7 **Scope:** Applies to cross-org agent trust establishment where a relying party (RP) accepts a presentation of an agent attestation chain (hardware-rooted or key-rooted) issued across organizational boundaries, and where any link in that chain may be delegated, re-signed, or counter-signed by an intermediate agent or broker. Does not apply to single-org intra-domain attestation where the RP and issuer share a trust anchor and a revocation channel, nor to opaque bearer tokens with no chain semantics. ## Core Distinction The failure is not "the chain is invalid." It is the distinction between **verifying a chain** and **being able to re-verify a chain at presentation time under the RP's own trust policy**. An agent that holds a previously-validated attestation bundle and re-presents it is performing *replay of a verification result*, not *verification*. Those are different operations with different preconditions, and the boundary where they diverge is subtle: - A chain that verified at issuance time T0 against trust anchor A may, at presentation time T1, be presented to an RP whose trust anchor set is A′ ≠ A, whose revocation view is staler or fresher than the presenter's, or whose policy requires a freshness binding the presenter never obtained. - A delegated link (agent X attests that agent Y holds capability C) is only meaningful if the RP can independently establish that X was itself attested for the *delegation* right, not merely for C. Presenters routinely conflate "X is trusted" with "X is trusted to delegate." The invariant: **a presenter must not assert a chain as verified unless every link is re-verifiable under the RP's declared policy at the moment of presentation.** If any link cannot be re-verified — unknown anchor, unresolvable revocation status, missing delegation scope, expired freshness binding — the correct behavior is refusal, not best-effort presentation. ## What Was Observed In a cross-org federation of three issuers (call them I1, I2, I3) and two relying parties (RP-A, RP-B), an agent runtime was configured to cache validated attestation bundles for 24 hours to avoid repeated hardware quote costs. The cache key was the agent identity, not the (identity, RP, policy-version) tuple. - RP-A accepted chains rooted at {I1, I2}. RP-B accepted chains rooted at {I2, I3}. - An agent attested under I1 successfully presented to RP-A; the bundle was cached. - The same agent subsequently presented the cached bundle to RP-B. The presenter's local check passed (it re-checked the cached *result*), but RP-B's anchor set excluded I1. RP-B rejected — correctly — but only because RP-B happened to enforce anchor membership. The presenter had asserted "verified" to its own orchestration layer, which had already begun provisioning cross-org resources on the strength of that assertion. - Separately, a delegated link from I2→broker→agent carried a `delegation_scope` extension that the presenter's verifier ignored (it validated signatures only). The broker was attested for `attest` but not for `delegate`. The chain was cryptographically sound and semantically void. This passed both RPs until RP-B's policy engine was upgraded. The second case is the dangerous one: no signature failed, no anchor was unknown, and every party's local check returned true. The invariant was violated silently. ## Condition of Validity The refusal obligation holds when **all** of the following are true: 1. The presentation crosses an organizational trust boundary (issuer org ≠ RP org). 2. The chain contains at least one delegated or brokered link, or the RP's policy is not byte-identical to the policy under which the bundle was originally validated. 3. The presenter cannot, at presentation time, obtain: (a) the RP's current anchor set, (b) a revocation status fresher than the RP's declared staleness bound, and (c) the delegation scope of every intermediate link. It does **not** hold when the presenter and RP share a trust anchor and revocation channel and the bundle carries a freshness binding (nonce or timestamp) within the RP's window — in that case replay of a verification result is equivalent to verification, and refusal would be spurious. ## Decision Boundary The boundary is drawn at *what the presenter can prove about the RP's policy*, not at *what the presenter believes about the chain*. Concretely: - If the presenter knows the RP's anchor set and can re-walk the chain to a member of it, and can resolve revocation to within the RP's freshness bound, and can read and check `delegation_scope` on every intermediate — present. - If any of those is unknown, stale, or unreadable — refuse, and surface the specific unmet precondition to the orchestration layer rather than a generic "attestation failed." - Never downgrade to "present with a warning." A warned presentation is consumed by downstream systems as a success. The asymmetry that makes this non-obvious: a *false refusal* costs a retry; a *false presentation* costs a cross-org trust violation that is invisible to every party's local checks. The boundary must therefore be biased toward refusal, and the refusal must be typed so the caller can distinguish "chain invalid" from "cannot re-verify under your policy." ## Operational Effect Caching must be keyed by `(agent_identity, rp_identity, policy_version, anchor_set_hash, revocation_epoch)`, not by identity alone. Delegation scope must be a first-class field in the verification result, and a chain whose intermediate links lack a readable scope must be treated as unverifiable rather than as scope-free. The orchestration layer must treat the typed refusal as a blocking condition, not a soft failure, because the entire value of cross-org attestation is that downstream provisioning can trust the presenter's assertion — and that assertion is only as good as the presenter's willingness to withhold it. ```python from __future__ import annotations from dataclasses import dataclass from typing import FrozenSet, Optional class AttestationRefusal(RuntimeError): """Raised when a chain cannot be re-verified under the relying party's policy. Distinct from ChainInvalidError: the chain may be cryptographically sound, but the presenter cannot establish the preconditions for asserting it. """ @dataclass(frozen=True) class RelyingPartyPolicy: anchors: FrozenSet[str] max_revocation_staleness_s: int requires_delegation_scope: bool @dataclass(frozen=True) class ChainLink: issuer: str signature_ok: bool delegation_scope: Optional[str] # None means scope not asserted revocation_age_s: Optional[int] # None means status unresolvable def present_or_refuse( chain: list[ChainLink], policy: RelyingPartyPolicy, rp_anchor_set_hash: str, cached_anchor_set_hash: str, ) -> None: if rp_anchor_set_hash != cached_anchor_set_hash: raise AttestationRefusal( "anchor set drift: cached bundle validated against a different " f"anchor set ({cached_anchor_set_hash}) than RP declares " f"({rp_anchor_set_hash}); cannot re-verify" ) for i, link in enumerate(chain): if not link.signature_ok: raise AttestationRefusal(f"link {i} ({link.issuer}): signature invalid") if link.issuer not in policy.anchors and i == len(chain) - 1: raise AttestationRefusal( f"link {i} ({link.issuer}): root not in RP anchor set " f"{sorted(policy.anchors)}" ) if link.revocation_age_s is None: raise AttestationRefusal( f"link {i} ({link.issuer}): revocation status unresolvable; " "refusing to present" ) if link.revocation_age_s > policy.max_revocation_staleness_s: raise AttestationRefusal( f"link {i} ({link.issuer}): revocation status stale " f"({link.revocation_age_s}s > {policy.max_revocation_staleness_s}s)" ) if policy.requires_delegation_scope and i < len(chain) - 1: if link.delegation_scope is None: raise AttestationRefusal( f"link {i} ({link.issuer}): intermediate link lacks " "delegation_scope; chain is semantically void for delegation" ) if "delegate" not in link.delegation_scope.split(","): raise AttestationRefusal( f"link {i} ({link.issuer}): attested for " f"{link.delegation_scope!r} but not for 'delegate'" ) # All preconditions established: presentation is equivalent to verification. return None ```