Skip to content

Receipt Chain Verification

Agent Receipts are hash-chained into tamper-evident sequences. This page describes the canonical form, signing process, and verification algorithms.

SIGNING & CHAIN LINKINGReceipt(no proof yet)RFC 8785canonicalizeEd25519 signu-base64urlSigned ReceiptReceipt 1prev_hash: nullSHA-256 of canonical form ──▶Receipt 2prev_hash: sha256:a3f1…SHA-256 of canonical form ──▶Receipt 3prev_hash: sha256:b4e2…

no

yes

no

yes

fail

pass

yes

yes

no

fail

pass

fail

pass

no

Load chain ordered by sequence

R_0.chain.previous_receipt_hash is null?

Chain broken

For each receipt R_i

R_i.chain.chain_id == R_0.chain.chain_id?

Chain broken: chain_id mismatch

Verify Ed25519 signature

SHA-256 of canonical form — proof excluded

Not last receipt?

R_i.chain.terminal == true?

Chain broken: receipt after terminal

Check R_i+1.chain.previous_receipt_hash == sha256:digest

Check sequence increments by 1

Chain integrity verified ✓

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.

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.

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:

  1. Retrieve all receipts for the chain, ordered by chain.sequence. Let n be the number of receipts.
  2. For each receipt R(i) (0-based index):
    • a. Verify the proof signature against the issuer’s public key at proof.verificationMethod. Note: resolution of proof.verificationMethod URLs (particularly did: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 .pub file).
    • b. Compute the hex-encoded SHA-256 digest of the RFC 8785 canonical form of R(i) with the proof field removed.
    • c. If i < n - 1, confirm R(i+1)‘s chain.previous_receipt_hash equals sha256: concatenated with that hex digest.
  3. For each receipt R(i) where i > 0, confirm chain.sequence equals R(i-1)‘s chain.sequence + 1. Any gap is a hard verification failure, not a warning — partial chains cannot be silently accepted.
  4. Confirm R(0)‘s chain.previous_receipt_hash is null. (The spec mandates chain.sequence starts at 1 per §4.3.2, but the reference SDK verifiers only enforce sequence ≥ 1 here and rely on the schema and issuer conformance for the exact starting value. Callers needing strict first-sequence enforcement should additionally validate R(0).chain.sequence == 1 inline — schema validation alone is not sufficient because JSON Schema can only express the per-receipt ≥ 1 bound, not the position-dependent “first equals 1” rule.)
  5. Confirm every receipt shares the same chain.chain_id as R(0). Any mismatch fails verification immediately with a chain_id mismatch error identifying the offending index and both values. This check is unconditional and runs independently of previous_receipt_hash linkage — an attacker who splices a forged hash link cannot mix receipts from two distinct chains under a single verification call.
  6. If any receipt R(i) carries chain.terminal: true and a receipt R(i+1) exists in the input, verification fails immediately with receipt 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 pass RequireTerminal to fail verification when the final observed receipt is not explicitly terminal. The terminal receipt MAY also carry chain.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 of RequireTerminal. "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 ExpectedLength and/or ExpectedFinalHash to VerifyChain. 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.

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.type as the original receipt
  • outcome.status: "success" if the reversal succeeded, "failure" if it did not
  • A reversal_of field referencing the id of the original receipt

The chain is always append-only; the original receipt is never mutated or removed.

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.

When a receipt chain includes a delegation field:

  1. Resolve delegation.parent_chain_id and retrieve the parent chain.
  2. Locate the receipt with id matching delegation.parent_receipt_id in the parent chain.
  3. Confirm delegation.delegator.id matches the issuer.id of the parent chain’s receipts.
  4. Confirm the principal in the delegated chain matches the principal in 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.

not found

found

not found

found

no

yes

no

yes

Delegated chain receipt with delegation field

Resolve delegation.parent_chain_id

Delegation unverifiable

Find receipt matching delegation.parent_receipt_id

delegation.delegator.id == parent chain issuer.id?

principal unchanged across chains?

Delegation link verified ✓

When action.trusted_timestamp is present and non-null:

  1. Base64-decode the value to obtain the DER-encoded RFC 3161 TimeStampToken.
  2. Verify the TSA’s signature on the TimeStampToken against the TSA’s certificate.
  3. Extract the MessageImprint hash from the TimeStampToken.
  4. Confirm the MessageImprint hash matches the SHA-256 digest of the receipt’s canonical form (proof field removed).
  5. 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.