# Why does an instance of an A2A v1.0 delegation chain fail receipt verification when the chain is cryptographically intact **Date of Record:** 2026 November 11 **Scope:** Applies to A2A v1.0 delegation chains where a root principal delegates to an intermediate agent which re-delegates to a leaf agent, and where the audit trail is reconstructed from per-hop signed receipts. Does not apply to single-hop delegations, to chains where a single trusted coordinator signs the aggregate receipt, or to non-repudiable logs anchored in an external transparency service with independent witnesses. ## Core Distinction A delegation receipt proves *who signed a hop*, not *that the hop was authorized at the time it was taken*. In A2A v1.0 the receipt is a signed statement of the form `(delegator, delegatee, scope, nonce, parent_receipt_hash, timestamp)`. Verification of a chain is conventionally defined as: each receipt's signature validates, each `parent_receipt_hash` matches the prior receipt, and each hop's requested `scope` is a subset of the parent's `scope`. This is a *structural* invariant. It is silent on a *temporal* invariant that the protocol assumes but does not encode: that the parent's authorization was still live at the instant the child receipt was signed. The non-obvious boundary is this: a chain can be structurally perfect — every signature valid, every hash linked, every scope a proper subset — and still be non-repudiable-in-name-only, because the leaf acted under a delegated authority that had already been revoked, expired, or narrowed by the delegator *after* the parent receipt was issued but *before* the child receipt was minted. The receipts record the delegation graph as a DAG of signatures; they do not record the revocation graph. Verification passes; authorization did not hold. The two are conflated by every naive verifier. ## What Was Observed A three-hop chain was reconstructed from a production audit log. The root principal `P` issued receipt `R0` delegating scope `{read:orders, write:orders}` to agent `A` with a validity window of `[T0, T0+3600s]`. At `T0+1200s`, `P` revoked `write:orders` from `A` — a revocation event recorded in `P`'s local revocation log but not propagated to `A`'s receipt store, because A2A v1.0 has no mandatory revocation-propagation channel; revocation is a local policy event. At `T0+1800s`, `A` issued `R1` delegating `{write:orders}` to agent `B`, signing with `A`'s key and citing `parent_receipt_hash = H(R0)`. At `T0+2400s`, `B` issued `R2` delegating `{write:orders}` to leaf `L`, citing `H(R1)`. A verifier replaying the chain at `T0+7200s` confirmed: `R0`, `R1`, `R2` all carried valid signatures; `H(R0)` and `H(R1)` matched; `scope(R1) ⊆ scope(R0)` and `scope(R2) ⊆ scope(R1)`; all timestamps monotonic. The verifier returned `VALID`. The leaf `L` had, in fact, executed a `write:orders` operation at `T0+2500s` under authority that `P` had withdrawn 1300 seconds earlier. The audit trail was cryptographically sound and operationally false. The failure is not a signature forgery. It is a *temporal scope violation* that the receipt schema cannot express, because the receipt attests to the delegator's *intent at signing time* and the verifier treats that intent as *persistent until contradicted by another signed receipt* — but revocation produces no signed receipt. The revocation is an unsigned local event. The non-repudiation guarantee therefore covers the delegation but not its withdrawal, and an audit trail that cannot represent withdrawal cannot be non-repudiable with respect to the principal who withdrew. ## Condition of Validity Structural receipt verification is a valid proxy for authorization only under the conjunction of three conditions, none of which A2A v1.0 enforces by default: 1. **Revocation is monotone within the chain's lifetime.** No delegator revokes or narrows a scope between the issuance of its receipt and the issuance of any descendant receipt. This holds only if delegation and revocation are serialized through a single authority, which multi-agent systems explicitly are not. 2. **Receipt timestamps are trustworthy.** The temporal argument requires that `timestamp(R_child) > timestamp(R_parent)` reflects real ordering, not clock skew. A2A v1.0 receipts carry signer-local timestamps; a delegator with a fast clock can mint a child receipt that *appears* to precede a revocation that actually preceded it. The skew budget is unbounded in the spec. 3. **The verifier has access to the full revocation set for every delegator in the chain.** In practice the verifier holds only the receipts. The revocation log is a separate, unsigned, often private artifact. When any of these fails — and in any chain spanning more than one administrative domain, at least one will — the structural check is necessary but not sufficient, and treating it as sufficient produces exactly the false-`VALID` above. ## Decision Boundary The boundary is whether the verifier is asked to answer *"is this chain well-formed?"* or *"was this action authorized?"*. These are different questions with different evidence requirements. - If the question is **well-formedness** (e.g., detecting tampering, truncation, or scope-escalation in transit), structural verification is correct and sufficient. Proceed. - If the question is **authorization at action time** (the non-repudiation question), structural verification is necessary but must be augmented by a *revocation witness*: a signed, timestamped statement from each delegator that its receipt was live at the child's signing instant, or an external monotonic counter (transparency log, CRL with signed freshness) that the verifier checks against the child's timestamp. The decision rule: a chain is admissible as non-repudiable evidence only if, for every hop, the verifier possesses either (a) a signed liveness attestation from the delegator covering `[timestamp(R_parent), timestamp(R_child)]`, or (b) a signed revocation record with a timestamp strictly greater than `timestamp(R_child)`. Absent both, the hop is *structurally valid and evidentially open* — it must be reported as `UNVERIFIED_AUTHORIZATION`, never as `VALID`. Collapsing the open case into `VALID` is the boundary failure. ## Operational Effect Downstream systems that consume the audit trail inherit the ambiguity. A billing system that treats `VALID` as proof of authorized write will bill for operations the principal revoked; a compliance system that treats `VALID` as non-repudiable will assert a guarantee the evidence does not support, and will fail an adversarial audit that introduces the revocation log. The correct operational posture is to carry the three-valued verdict — `VALID`, `INVALID`, `UNVERIFIED_AUTHORIZATION` — end to end, and to refuse to let any consumer collapse `UNVERIFIED_AUTHORIZATION` into either terminal state without an explicit policy decision recorded against the chain. The receipt proves the signature; only the revocation witness proves the authority. A verifier that cannot distinguish them is not verifying authorization — it is verifying that the forgery was performed by an authorized key. ```python from dataclasses import dataclass from typing import Optional, Sequence class ReceiptChainError(RuntimeError): """Base for receipt-chain verification failures.""" class StructuralVerificationError(ReceiptChainError): """Signature, hash-link, or scope-subset check failed.""" class UnverifiedAuthorizationError(ReceiptChainError): """Chain is structurally sound but lacks a revocation witness for at least one hop.""" @dataclass(frozen=True) class Receipt: delegator: str delegatee: str scope: frozenset parent_hash: Optional[str] timestamp: int signature_valid: bool receipt_hash: str @dataclass(frozen=True) class RevocationWitness: delegator: str scope: frozenset effective_at: int signed: bool def verify_chain( chain: Sequence[Receipt], witnesses: Sequence[RevocationWitness], ) -> str: # Phase 1: structural verification (necessary, not sufficient). for i, r in enumerate(chain): if not r.signature_valid: raise StructuralVerificationError(f"hop {i}: bad signature") if i > 0: if r.parent_hash != chain[i - 1].receipt_hash: raise StructuralVerificationError(f"hop {i}: parent hash mismatch") if not r.scope <= chain[i - 1].scope: raise StructuralVerificationError(f"hop {i}: scope escalation") if r.timestamp <= chain[i - 1].timestamp: raise StructuralVerificationError(f"hop {i}: non-monotonic timestamp") # Phase 2: temporal authorization verification (the boundary). for i in range(1, len(chain)): parent, child = chain[i - 1], chain[i] covering = [ w for w in witnesses if w.delegator == parent.delegator and w.signed and w.effective_at <= child.timestamp ] # A revocation effective at or before the child's signing instant # that removes any scope the child exercised is a hard failure. for w in covering: if w.scope & child.scope: raise UnverifiedAuthorizationError( f"hop {i}: scope {set(w.scope & child.scope)} revoked at " f"{w.effective_at}, child signed at {child.timestamp}" ) # No revocation witness and no signed liveness attestation: # the hop is evidentially open, not valid. if not covering: raise UnverifiedAuthorizationError( f"hop {i}: no revocation witness for delegator " f"{parent.delegator!r} covering [{parent.timestamp}, " f"{child.timestamp}]; authorization unverified" ) return "VALID" ```