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 signz-base58btcSigned ReceiptReceipt 1prev_hash: nullSHA-256 of canonical form ──▶Receipt 2prev_hash: sha256:a3f1…SHA-256 of canonical form ──▶Receipt 3prev_hash: sha256:b4e2…

For hashing and signing, receipts must be serialized using the JSON Canonicalization Scheme (RFC 8785) with the proof field removed before hashing. This approach aligns with the W3C Verifiable Credentials Data Integrity specification, though the signing procedure defined here is intentionally simplified.

The issuer signs the canonical receipt (proof field excluded) with its Ed25519 private key. The signature is encoded as a multibase string (z-prefixed base58btc) and placed in proof.proofValue.

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.
    • 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.
  4. Confirm R(0)‘s chain.previous_receipt_hash is null.

If any step fails, the chain is broken at that point. Receipts before the break may still be individually valid; receipts after are suspect.

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.

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.