Receipt Chain Verification
Agent Receipts are hash-chained into tamper-evident sequences. This page describes the canonical form, signing process, and verification algorithms.
Canonical form
Section titled “Canonical form”For hashing and signing, receipts must be serialized using the JSON Canonicalization Scheme (RFC 8785) with the proof field removed before hashing. This diverges from the W3C Verifiable Credentials Data Integrity default, which uses JSON-LD canonicalization (RDF Dataset Canonicalization). Implementations expecting JSON-LD processing will not be interoperable with Agent Receipt signatures — existing JSON-LD-based VC tooling cannot verify them without modification.
Signing
Section titled “Signing”The issuer signs the canonical receipt (proof field excluded) with its Ed25519 private key. The signature is encoded as a multibase string (u-prefixed base64url, no padding) and placed in proof.proofValue.
Chain integrity verification
Section titled “Chain integrity verification”Prerequisite. This algorithm assumes every input receipt has already been validated against the JSON Schema, per the end-to-end verification flow (spec §7.8 step 1). The schema enforces per-receipt invariants such as chain.sequence being an integer ≥ 1 and chain.previous_receipt_hash being either null or a sha256:-prefixed hex string. The chain algorithm below does not re-validate those constraints — chains containing schema-invalid receipts will produce undefined results.
To verify a receipt chain:
- Retrieve all receipts for the chain, ordered by
chain.sequence. Let n be the number of receipts. - For each receipt R(i) (0-based index):
- a. Verify the
proofsignature against the issuer’s public key atproof.verificationMethod. Note: resolution ofproof.verificationMethodURLs (particularlydid:agent:identifiers) is not specified in the current protocol — verifiers must obtain public keys through out-of-band means (e.g. the daemon’s published.pubfile). - b. Compute the hex-encoded SHA-256 digest of the RFC 8785 canonical form of R(i) with the
prooffield removed. - c. If i < n - 1, confirm R(i+1)‘s
chain.previous_receipt_hashequalssha256:concatenated with that hex digest.
- a. Verify the
- For each receipt R(i) where i > 0, confirm
chain.sequenceequals R(i-1)‘schain.sequence+ 1. Any gap is a hard verification failure, not a warning — partial chains cannot be silently accepted. - Confirm R(0)‘s
chain.previous_receipt_hashisnull. (The spec mandateschain.sequencestarts at1per §4.3.2, but the reference SDK verifiers only enforcesequence ≥ 1here and rely on the schema and issuer conformance for the exact starting value. Callers needing strict first-sequence enforcement should additionally validateR(0).chain.sequence == 1inline — schema validation alone is not sufficient because JSON Schema can only express the per-receipt≥ 1bound, not the position-dependent “first equals 1” rule.) - Confirm every receipt shares the same
chain.chain_idas R(0). Any mismatch fails verification immediately with achain_id mismatcherror identifying the offending index and both values. This check is unconditional and runs independently ofprevious_receipt_hashlinkage — an attacker who splices a forged hash link cannot mix receipts from two distinct chains under a single verification call. - If any receipt R(i) carries
chain.terminal: trueand a receipt R(i+1) exists in the input, verification fails immediately withreceipt after terminal. This check is unconditional — no caller parameter can suppress it.
If any step fails, the chain is broken at that point. Receipts before the break may still be individually valid; receipts after are suspect.
Truncation detection. Steps 1–6 detect inserted, modified, reordered, gapped, cross-chain-spliced, and post-terminal receipts. They do not detect tail truncation of an open chain — dropping the last N receipts from a non-terminal chain still produces Valid: true, because no in-chain field commits to the chain’s total length. Three mitigations are available:
- In-band terminal marker. When the final receipt carries
chain.terminal: true, the receipt-after-terminal check (step 6) prevents extension. Callers MAY additionally passRequireTerminalto fail verification when the final observed receipt is not explicitly terminal. The terminal receipt MAY also carrychain.status—"complete"for normal end-of-session,"interrupted"for best-effort terminators emitted on signal or known abort path. The verifier reports a"complete"/"interrupted"/"unknown"classification regardless ofRequireTerminal."unknown"is the verifier-derived label for chains with no terminal at all; issuers MUST NOT write"unknown"on the wire. - Out-of-band witness. Callers maintaining an external record of chain state (audit log, transparency log, signed checkpoint) MAY supply
ExpectedLengthand/orExpectedFinalHashtoVerifyChain. Verification fails when the observed chain does not match. - Floor. Tail truncation of an open (non-terminal) chain without any external witness cannot be detected by any mechanism in this specification.
Store trust. Chain verification proves the integrity of the receipts the verifier is given; it cannot prove the verifier was given every receipt the issuer wrote. An attacker who can both delete from the store AND re-sign downstream receipts with updated previous_receipt_hash values (which requires the agent’s signing key) can erase mid-chain history undetectably from chain verification alone. Operators who require detection of store-level tampering SHOULD layer at least one of: append-only storage (S3 Object Lock, WORM), external chain-state witnesses (per the bullet above), or periodic chain-head anchoring to a public log.
Reversal receipts
Section titled “Reversal receipts”Receipts are immutable once issued. To record that an action was reversed, issue a new receipt appended to the same chain with:
- The same
action.typeas the original receipt outcome.status:"success"if the reversal succeeded,"failure"if it did not- A
reversal_offield referencing theidof the original receipt
The chain is always append-only; the original receipt is never mutated or removed.
Chain issuer model
Section titled “Chain issuer model”A chain MUST have a single issuer. All receipts within a chain MUST have the same issuer.id. Delegated agents MUST create a new chain and link it to the parent chain via the delegation field.
Delegation verification
Section titled “Delegation verification”When a receipt chain includes a delegation field:
- Resolve
delegation.parent_chain_idand retrieve the parent chain. - Locate the receipt with
idmatchingdelegation.parent_receipt_idin the parent chain. - Confirm
delegation.delegator.idmatches theissuer.idof the parent chain’s receipts. - Confirm the
principalin the delegated chain matches theprincipalin the parent chain — the human on whose behalf actions are taken does not change across delegation.
If any step fails, the delegation link is unverifiable. The delegated chain’s receipts are still individually valid but cannot be traced back to the parent chain.
Trusted timestamp verification
Section titled “Trusted timestamp verification”When action.trusted_timestamp is present and non-null:
- Base64-decode the value to obtain the DER-encoded RFC 3161
TimeStampToken. - Verify the TSA’s signature on the
TimeStampTokenagainst the TSA’s certificate. - Extract the
MessageImprinthash from theTimeStampToken. - Confirm the
MessageImprinthash matches the SHA-256 digest of the receipt’s canonical form (proof field removed). - Confirm the TSA timestamp falls within a reasonable window of
action.timestamp(implementation-defined tolerance).
Trusted timestamps are optional. When present, they provide independent evidence of when the receipt was created, which cannot be backdated by the issuer.