Chio/Docs

Receipt Verification

An auditor with a saved evidence package and the chio CLI can verify a kernel decision offline. No live trust-control. No app surface. The package is a directory of NDJSON files plus a manifest, and chio evidence verify re-checks every signed receipt and Merkle inclusion proof against the manifest.

Where the code lives

examples/hello-receipt-verify/. Run with ./smoke.sh. The example ships a checked-in fixture under fixtures/minimal-evidence/; no live services are needed.

What It Shows

  • Receipt verification from a static captured package, with no live kernel and no network calls.
  • Local lineage inspection of the capability that produced the receipt.
  • Tamper detection: an attacker who edits any file under the package directory breaks verification at the manifest hash check.

Stops at offline verification

This example does not call chio evidence import. Import is intentionally stricter and requires a signed bilateral federation policy, so it belongs in a federation-focused example.

Files

text
examples/hello-receipt-verify/
  README.md
  fixtures/minimal-evidence/
    README.txt
    capability-lineage.ndjson   one line per capability on the chain
    checkpoints.ndjson          merkle checkpoint(s)
    child-receipts.ndjson       child-request receipts
    inclusion-proofs.ndjson     merkle inclusion proofs per receipt
    manifest.json               SHA-256 over every file in the package
    query.json                  bundle metadata
    receipts.ndjson             one line per signed tool receipt
    retention.json              retention policy fingerprint
  smoke.sh

Run It

bash
# From the chio workspace root
cargo build --bin chio
cd examples/hello-receipt-verify
./smoke.sh

On a successful run the smoke prints:

text
hello-receipt-verify smoke passed
artifacts: .../examples/hello-receipt-verify/.artifacts/<timestamp>
receipt id: <id>

Phase 1: Load the Package

The smoke copies fixtures/minimal-evidence/ into two scratch directories under the run's artifact root: one untouched (input-package/) and one we will mutate later (tampered-package/).

examples/hello-receipt-verify/smoke.sh
cp -R "${EXAMPLE_ROOT}/fixtures/minimal-evidence" "${INPUT_DIR}"
cp -R "${EXAMPLE_ROOT}/fixtures/minimal-evidence" "${TAMPERED_DIR}"

The manifest is the trust anchor for the package. It pins SHA-256 over every file plus the export-time counts the verifier asserts:

fixtures/minimal-evidence/manifest.json
{
  "schema": "chio.evidence_export_manifest.v1",
  "exportedAt": 1776272775,
  "query": {},
  "counts": {
    "toolReceipts": 1,
    "childReceipts": 0,
    "checkpoints": 0,
    "capabilityLineage": 1,
    "inclusionProofs": 0,
    "uncheckpointedReceipts": 1
  },
  "files": [
    { "path": "receipts.ndjson",          "sha256": "a8f22454...", "bytes": 968 },
    { "path": "capability-lineage.ndjson","sha256": "789ca683...", "bytes": 427 },
    { "path": "checkpoints.ndjson",       "sha256": "e3b0c442...", "bytes": 0 },
    { "path": "child-receipts.ndjson",    "sha256": "e3b0c442...", "bytes": 0 },
    { "path": "inclusion-proofs.ndjson",  "sha256": "e3b0c442...", "bytes": 0 },
    { "path": "query.json",               "sha256": "44136fa3...", "bytes": 2 },
    { "path": "retention.json",           "sha256": "f92db6f4...", "bytes": 75 },
    { "path": "README.txt",               "sha256": "e32367c0...", "bytes": 374 }
  ]
}

Phase 2: Verify Offline

chio evidence verify opens the package directory, parses the manifest, rehashes every listed file, parses every receipt out of receipts.ndjson, verifies each signature against the kernel public key embedded in the package, and confirms every Merkle inclusion proof in inclusion-proofs.ndjson.

smoke.sh
"${CHIO_BIN}" evidence verify --input "${INPUT_DIR}" --json \
  > "${ARTIFACT_ROOT}/verify.json"

Expected stdout from the verifier on the untouched fixture:

verify.json (success)
{
  "verifiedFiles": 8,
  "toolReceipts": 1,
  "childReceipts": 0,
  "checkpoints": 0,
  "capabilityLineage": 1,
  "inclusionProofs": 0
}

The smoke parses the result and asserts the expected counts match the fixture: one tool receipt, one capability lineage record.

smoke.sh (inline assertion)
verify = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
receipt_record = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8").splitlines()[0])
lineage_record = json.loads(Path(sys.argv[3]).read_text(encoding="utf-8").splitlines()[0])

assert verify["toolReceipts"] == 1, verify
assert verify["capabilityLineage"] == 1, verify

Phase 3: Inspect the Receipt

Once verified, the smoke pulls a few load-bearing fields out of the receipt and the lineage record and writes them into summary.json:

smoke.sh
summary = {
    "example": "hello-receipt-verify",
    "receipt_id": receipt_record["receipt"]["id"],
    "capability_id": receipt_record["receipt"]["capability_id"],
    "tool_name": receipt_record["receipt"]["tool_name"],
    "subject_key": lineage_record["subject_key"],
    "issuer_key": lineage_record["issuer_key"],
    "verified": True,
}

The receipt links to its capability through capability_id; the lineage record carries the subject and issuer keys. Cross-referencing them is what makes the package a complete evidence bundle: the auditor can prove that a specific capability signed a specific tool decision under a specific issuer key.


Phase 4: Tamper Check

The smoke writes a single rogue byte sequence into tampered-package/query.json and runs chio evidence verify against the modified directory. Verification fails closed:

smoke.sh
Path(sys.argv[1]).write_text('{"tampered":true}\n', encoding="utf-8")

if "${CHIO_BIN}" evidence verify --input "${TAMPERED_DIR}" --json \
    > "${ARTIFACT_ROOT}/tamper-out.json" \
    2> "${ARTIFACT_ROOT}/tamper-err.json"; then
  echo "expected tampered package verification to fail" >&2
  exit 1
fi

The smoke then asserts the structured CLI error includes the manifest hash mismatch, not a generic decode failure:

smoke.sh
payload = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
assert payload["code"] == "CHIO-CLI-OTHER", payload
assert "hash mismatch" in payload["message"] or "hash mismatch" in payload["context"]["detail"], payload

Reproduce the failure by hand: copy the fixture, write one byte into any file the manifest covers, run chio evidence verify again. The CLI exits non-zero with a structured error:

terminal
cp -R fixtures/minimal-evidence /tmp/tampered
echo '{"tampered":true}' > /tmp/tampered/query.json

chio evidence verify --input /tmp/tampered --json
# stderr:
# {
#   "code": "CHIO-CLI-OTHER",
#   "message": "evidence verification failed",
#   "context": {
#     "detail": "hash mismatch for query.json: expected 44136fa3..., got <new>"
#   }
# }
# exit 1

Manifest is the auditor's trust anchor

manifest.json contains SHA-256 of every file in the package. The kernel pre-signs the receipts, but the manifest is what stops a clever attacker from replacing one record with another while keeping every individual signature intact: the manifest hash chain catches the tamper.

Auditor Workflow

Real audits work the same way: pull a captured evidence package out of cold storage, run chio evidence verify, and read the receipts. No live kernel access, no trust-control connectivity, no shared secrets beyond the kernel's long-lived public key. A typical session:

bash
# Pull the package from cold storage
tar -xzf evidence-2026-04-15.tar.gz -C ./review

# Verify offline
chio evidence verify --input ./review/evidence-2026-04-15 --json | jq '.'

# Inspect the receipts
cat ./review/evidence-2026-04-15/receipts.ndjson | jq -c '{id: .receipt.id, tool: .receipt.tool_name, verdict: .receipt.verdict}'

# Inspect the capability that authorized them
cat ./review/evidence-2026-04-15/capability-lineage.ndjson | jq '.'

# Spot-check the manifest if you want: each entry is the SHA-256 the verifier rehashes
cat ./review/evidence-2026-04-15/manifest.json | jq '.files'

For audits that span multiple parties, both kernels can publish their own packages and the auditor verifies each independently. Bilateral Receipts covers the cross-pair check that confirms two packages commit to the same governed transaction.


Inspect the Artifacts

bash
cd .artifacts/<timestamp>

# Verifier output
cat verify.json          # toolReceipts: 1, capabilityLineage: 1
cat summary.json         # receipt_id, capability_id, tool_name, ...

# Tamper run
cat tamper-out.json      # empty / partial
cat tamper-err.json      # CHIO-CLI-OTHER, "hash mismatch ..."

# Inputs we verified against
ls input-package/
ls tampered-package/

Smoke Assertions

The smoke pulls four checks out of the verifier output and the fixture:

smoke.sh (assertions)
assert verify["toolReceipts"] == 1, verify
assert verify["capabilityLineage"] == 1, verify

# Tamper run must fail closed
assert payload["code"] == "CHIO-CLI-OTHER", payload
assert "hash mismatch" in payload["message"] \
    or "hash mismatch" in payload["context"]["detail"], payload

Decision rule

Use this example when an auditor needs offline verification of a captured evidence package, with no live trust-control connection. Pick trust-control when you also need to mint receipts and exercise the issue / revoke surface. Cross-package bilateral verification is out of scope here: see Bilateral Receipts for that.

Where to read more

Verify Receipts Offline for the full auditor playbook. Receipts for the receipt schema and signature shape. Compliance Certificates for how packages roll up into auditable certificates.
Receipt Verification Example · Chio Docs