Verify a Pipelock Receipt: Copy-Paste Demo

Receipts or it didn't happen. Pull a signed receipt from the public conformance corpus, run the verifier, watch it pass. Tamper one byte, watch it fail.

Ready to protect your own setup?

Pipelock writes a signed receipt every time it adjudicates an outbound agent action. Receipts compose into a hash-chained file an auditor or CI runner can read without trusting any vendor dashboard. This page is the shortest path from zero to “I ran the verifier, it passed, I tampered one byte, it failed.”

The fixtures here come from the public conformance corpus that verifier authors and Audit Packet producers can test against. There is no vendor-only verification path: every command below runs on public fixtures with a public test key.

The verifier commands

The Pipelock verifier is available as the built-in CLI, a standalone Go binary, and TypeScript, Rust, and Python ports. They expose the same receipt and chain verification surface, with matching exit-code semantics (0 valid, 1 invalid, 2 runtime error, 64 usage error).

BinarySourceWhat it covers
pipelock verify-receiptBuilt into the main Pipelock binarySingle receipt JSON, single evidence.jsonl, or a recorder session directory via --chain
pipelock-verifier (Go)cmd/pipelock-verifier, shipped as its own GoReleaser archiveSubcommands receipt, chain, audit-packet. No proxy, no scanner, no network surface.
pipelock-verifier-tssdk/verifiers/tsNode/Bun port via @noble/ed25519 and ajv
pipelock-verifier-rssdk/verifiers/rustRust port with embedded schema
pipelock-verifypipelock-verify-pythonPython port for receipt chains and Python audit scripts

Core Go invocations:

# Verify a single receipt with the main Pipelock binary
pipelock verify-receipt receipt.json

# Verify a single receipt with the standalone Go verifier
pipelock-verifier receipt receipt.json

# Verify a full receipt chain (one jsonl file or a session directory)
pipelock-verifier chain evidence.jsonl

# Verify a complete Audit Packet bundle
pipelock-verifier audit-packet ./audit-packet/

The receipt and chain commands accept --key HEX_OR_FILE to pin a signer key for trusted verification. The Go, TypeScript, and Rust CLIs also accept --json for machine-readable output. The standalone Go verifier additionally accepts --offline, --allow-self-consistent-only, --no-trust-required, and --expect-sha256 on audit-packet for stricter or more permissive policies depending on the relying party.

Walk the seven-case quick path

The full public conformance corpus ships 28 fixtures (8 golden, 8 edge, 12 malicious) covering every reject_reason in the closed enum. A verifier author runs the whole corpus. A first-time reader can grasp the format in seven hand-picked cases:

#FixtureCategoryWhat it shows
1golden/01-allow-clean-getgoldenA clean allowed GET request. The basic happy-path signed receipt.
2golden/03-block-dlp-aws-keygoldenA blocked DLP hit still produces a signed receipt. Blocked is not silent.
3malicious/m01-forged-signature-wrong-keymaliciousReject reason: signature_invalid. Attacker rewrote signer_key to claim the pinned key; the foreign signature fails.
4malicious/m03-expired-timestampmaliciousReject reason: expired. Receipt is past the verifier’s max_age window.
5malicious/m08-tampered-chain-prev-hashmaliciousReject reason: chain_break. A receipt’s chain_prev_hash does not match the SHA-256 of the prior receipt.
6malicious/m11-body-tampered-after-signmaliciousReject reason: body_tampered. Signature was valid for the original body; action_record.target was rewritten post-signing.
7edge/e04-edge-of-freshness-windowedgeA receipt right at the freshness boundary. A correct verifier still accepts; off-by-one verifiers do not.

Each .json fixture has a sibling .expect.json describing the verdict any correct verifier MUST emit, the reject_reason enum value, the expected signer key, and notes for verifier authors. Drop your verifier in, run it across the corpus, compare line by line.

Try it locally

Clone the bench, pin the test key, run the verifier on a golden receipt:

git clone https://github.com/luckyPipewrench/agent-egress-bench.git
cd agent-egress-bench/receipts/v0/conformance

# Public test key (committed for reproducibility; production deployments pin their own)
TEST_KEY=$(jq -r .public_key_hex _generator/test-key.json)

# Golden case: accept
pipelock-verifier receipt golden/01-allow-clean-get.json --key "$TEST_KEY"
echo "exit: $?"     # 0 (valid)

# Malicious case: reject with signature_invalid
pipelock-verifier receipt malicious/m01-forged-signature-wrong-key.json --key "$TEST_KEY"
echo "exit: $?"     # 1 (invalid)

# Chain case: reject with chain_break
pipelock-verifier chain malicious/m08-tampered-chain-prev-hash.jsonl --key "$TEST_KEY"
echo "exit: $?"     # 1 (invalid)

A correct verifier emits the reject reason on stderr or in the --json output. The exit code is the relying party’s contract: CI runners gate on 0; auditors investigate any 1; 2 and 64 indicate the verifier itself is misconfigured.

Same corpus, three other languages

Each language port exposes the same receipt and chain verification surface. Use the public corpus to confirm whether a verifier agrees with the expected verdicts before trusting it on production receipts.

# TypeScript
cd sdk/verifiers/ts && npm install && npm run build
node dist/src/cli.js receipt path/to/golden/01-allow-clean-get.json --key "$TEST_KEY"

# Rust
cd sdk/verifiers/rust && cargo build --release
./target/release/pipelock-verifier-rs receipt path/to/golden/01-allow-clean-get.json --key "$TEST_KEY"

# Python (separate repo)
pip install pipelock-verify
pipelock-verify path/to/golden/01-allow-clean-get.json

If a verifier disagrees with the others on any fixture, the disagreement itself is the bug report. The corpus is the contract.

What a passing verification actually proves

Three things, in order, every time:

  1. Signature math. The signature is a well-formed Ed25519 signature over SHA-256(canonical-JSON(action_record)), and it verifies against the embedded signer_key. A wrong-key forgery fails here.
  2. Body integrity. The verifier recomputes the canonical-JSON hash from the current bytes of action_record. If a single byte changed after signing, the recomputed hash disagrees with the one the signature was produced over. A post-sign edit fails here.
  3. Chain linkage. When verifying a chain, each receipt’s chain_prev_hash is checked against the SHA-256 of the previous receipt’s bytes. Cutting one receipt out, reordering, or splicing in a foreign receipt breaks the chain.

Add the verifier-side policy checks on top: pinned signer key, max age, replay window, schema version, strict JSON parsing. The conformance README documents the exact policy assumptions every conformance run uses.

Where to go next

Receipts or it didn’t happen. The corpus is public, the verifiers are public, the test key is public. The only thing that varies between a trustworthy run and an untrustworthy one is which public key the relying party chose to pin.

Frequently asked questions

What does verifying a receipt actually prove?
A passing receipt verification proves three things at once. First, the receipt’s body was produced by the holder of a specific Ed25519 private key (signature math). Second, the body has not changed since signing (canonical-JSON hash recomputed from the current bytes). Third, when checked against a chain, the receipt sits where it claims in a tamper-evident sequence (each receipt’s chain_prev_hash matches the SHA-256 of the prior one). A correctly written verifier does all three offline, with no network and no vendor dashboard.
What's the difference between `pipelock verify-receipt` and `pipelock-verifier`?
pipelock verify-receipt is built into the main Pipelock binary; an operator who already runs Pipelock has it without installing anything else. pipelock-verifier is a separate binary with no network surface, no proxy, no scanner, and no config reload. It exists so a CI runner, an auditor’s laptop, or a third party can drop in just the verifier without provisioning the full firewall stack. Same verification logic. The standalone verifier adds three subcommands for receipt, chain, and audit-packet, and matches exit codes with the TypeScript and Rust ports.
Why are there four verifiers in four languages?
Defense in depth for the verification step itself. If a relying party only trusts implementations written in their own ecosystem’s language, the receipt format has to support that. The Go, TypeScript, Rust, and Python implementations expose the same receipt and chain verification surface, so a CI pipeline in Node, a Rust-based auditor tool, a Python compliance script, or the canonical Go binary can check the same public fixtures against the same expected verdicts.
Where do receipts come from in production?
Pipelock emits a signed receipt for every mediated action: every HTTP request, every MCP tools/call invocation, every WebSocket frame the proxy adjudicates. Receipts append to a hash-chained flight recorder file named evidence.jsonl. The pipelock-agent-egress-action GitHub Action bundles the chain with policy metadata into an Audit Packet v0 directory containing packet.json, summary.md, evidence.jsonl, and verifier.txt. The Audit Packet is the procurement-evidence artifact; the receipt chain is the underlying signed input.
Can a malicious actor with the test key forge receipts that the verifier accepts?
The conformance corpus is signed with a test key whose private half is committed in _generator/test-key.json for reproducibility. That key is for conformance testing only. Production verifiers pin a different public key per deployment; the corpus is structured so a verifier configured to pin the test key will accept the golden fixtures and reject the malicious ones. If a production verifier is misconfigured to trust the test key, that is a deployment mistake, not a property of the format.
What happens if the corpus changes upstream?
Each fixture has a stable fixture_id and an .expect.json that pins the exact verdict and reject_reason. Adding new fixtures does not change the verdict on existing ones. The corpus is versioned at receipts/v0/; if the receipt envelope itself ever changes in a way that breaks v0 verifiers, the new corpus will live at receipts/v1/ next to v0, not on top of it.

Ready to protect your own setup?