BuildOperations
Receipt Verification
An auditor can use a saved evidence package and the chio CLI to verify a kernel decision offline without a trust-control connection.
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
chio evidence import. Import is intentionally stricter and requires a signed bilateral federation policy, so it belongs in a federation-focused example.Files
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 read-boundary metadata
receipts.ndjson one line per signed tool receipt
retention.json retention policy fingerprint
smoke.sh verify, tamper, then run verify_artifacts.py
verify_artifacts.py deep artifact assertions (--write-summary)
test_verify_artifacts.py unit tests for the assertionsRun It
# From the chio workspace root
cargo build --bin chio
cd examples/hello-receipt-verify
./smoke.shOn a successful run the smoke prints:
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 output root: one untouched (input-package/) and one we will mutate later (tampered-package/).
cp -R "${EXAMPLE_ROOT}/fixtures/minimal-evidence" "${INPUT_DIR}"
cp -R "${EXAMPLE_ROOT}/fixtures/minimal-evidence" "${TAMPERED_DIR}"The manifest records SHA-256 hashes over every file, the export-time counts the verifier asserts, and the proof-coverage, receipt-semantics, and child-receipt-scope summaries the verifier re-checks:
{
"schema": "chio.evidence_export_manifest.v1",
"exportedAt": 1776272775,
"query": { "readBoundary": { "kind": "admin_all" } },
"counts": {
"toolReceipts": 1,
"childReceipts": 0,
"checkpoints": 0,
"capabilityLineage": 1,
"inclusionProofs": 0,
"uncheckpointedReceipts": 1
},
"proofCoverage": {
"checkpointedReceipts": 0,
"uncheckpointedReceipts": 1
},
"receiptSemantics": {
"mediatedDecisions": 1,
"traceObservations": 0,
"advisoryEvaluations": 0,
"prevent": 1,
"detectOnly": 0,
"advisoryOnly": 0,
"cannotSee": 0,
"authorized": 1
},
"childReceiptScope": "full_query_window",
"files": [
{ "path": "query.json", "sha256": "389b6d7a...", "bytes": 37 },
{ "path": "receipts.ndjson", "sha256": "5954824c...", "bytes": 1207 },
{ "path": "child-receipts.ndjson", "sha256": "e3b0c442...", "bytes": 0 },
{ "path": "checkpoints.ndjson", "sha256": "e3b0c442...", "bytes": 0 },
{ "path": "capability-lineage.ndjson", "sha256": "953e149f...", "bytes": 427 },
{ "path": "inclusion-proofs.ndjson", "sha256": "e3b0c442...", "bytes": 0 },
{ "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.
"${CHIO_BIN}" evidence verify --input "${INPUT_DIR}" --json \
> "${ARTIFACT_ROOT}/verify.json"Expected stdout from the verifier on the untouched fixture:
{
"schema": "chio.evidence_export_manifest.v1",
"verifiedFiles": 8,
"toolReceipts": 1,
"childReceipts": 0,
"checkpoints": 0,
"capabilityLineage": 1,
"inclusionProofs": 0,
"uncheckpointedReceipts": 1,
"childReceiptScope": "full_query_window",
"receiptSemantics": {
"mediatedDecisions": 1,
"traceObservations": 0,
"advisoryEvaluations": 0,
"prevent": 1,
"detectOnly": 0,
"advisoryOnly": 0,
"cannotSee": 0,
"authorized": 1
},
"claimBoundary": { "schema": "chio.evidence_transparency_claims.v1" }
}After the two chio evidence verify calls, the smoke passes the output directory to verify_artifacts.py, which re-checks the verifier output against the fixture: the schema, the counts, the child-receipt scope, the receipt-semantics tallies, and the transparency claimBoundary schema.
verify = load_json(root / "verify.json")
require(verify.get("schema") == "chio.evidence_export_manifest.v1", "verify.json schema drifted")
require(verify.get("toolReceipts") == 1, "verify.json toolReceipts drifted")
require(verify.get("capabilityLineage") == 1, "verify.json capabilityLineage drifted")
require(verify.get("uncheckpointedReceipts") == 1, "verify.json uncheckpointedReceipts drifted")
require(verify.get("verifiedFiles") == manifest_file_count, "verify.json verifiedFiles drifted")
require(verify.get("childReceiptScope") == "full_query_window", "child receipt scope drifted")
claim_boundary = require_object(verify, "claimBoundary", "verify.json")
require(claim_boundary.get("schema") == "chio.evidence_transparency_claims.v1", "claim boundary schema drifted")Phase 3: Inspect the Receipt
Once verified, verify_artifacts.py extracts the relevant fields from the receipt and lineage record and writes them into summary.json:
summary = {
"example": "hello-receipt-verify",
"receipt_id": receipt_id,
"capability_id": capability_id,
"tool_name": "read_file",
"subject_key": subject_key,
"issuer_key": issuer_key,
"read_boundary": "admin_all",
"verified": True,
}Before writing the summary, the script checks the receipt fields: tool_server == "*", receipt_kind == "mediated_decision", boundary_class == "prevent", and a nested decision.verdict == "allow". It parses the lineage record's grants_json and asserts it equals the single read_file/invoke grant, then cross-references receipt.metadata.attribution.subject_key and issuer_key against the lineage record's own subject_key and issuer_key. That last check links the receipt attribution keys to the capability lineage record and 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:
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
fiverify_artifacts.py then asserts the structured CLI error is the manifest hash mismatch, routed through the attest error registry. The code is the stable error URN and there is no context.detail field — the file name lives in the message:
payload = load_json(root / "tamper-err.json")
require(payload.get("code") == "urn:chio:error:attest:provenance-missing", "tamper error code drifted")
message = require_string(payload, "message", "tamper-err.json")
require("hash mismatch" in message and "query.json" in message, "tamper error message drifted")
context = require_object(payload, "context", "tamper-err.json")
require(context.get("domain") == "attest", "tamper error domain drifted")
require(context.get("severity") == "error", "tamper error severity drifted")
require(context.get("string_code") == "CHIO-ATTEST-PROVENANCE-MISSING", "tamper error string_code drifted")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:
cp -R fixtures/minimal-evidence /tmp/tampered
echo '{"tampered":true}' > /tmp/tampered/query.json
chio evidence verify --input /tmp/tampered --json
# stderr:
# {
# "code": "urn:chio:error:attest:provenance-missing",
# "message": "evidence package file hash mismatch for query.json",
# "context": {
# "domain": "attest",
# "severity": "error",
# "string_code": "CHIO-ATTEST-PROVENANCE-MISSING",
# "stability": "unstable"
# },
# "suggested_fix": "Regenerate the evidence bundle and include provenance before submitting the operation."
# }
# exit 1What the manifest verifies
manifest.json contains SHA-256 of every file in the package. The kernel pre-signs the receipts. The manifest detects a file replacement even when each individual receipt still has a valid signature.Auditor Workflow
An auditor can pull a captured evidence package from 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:
# 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 Run Output
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 (stdout is empty on failure)
cat tamper-err.json # attest:provenance-missing, "... hash mismatch for query.json"
# Inputs we verified against
ls input-package/
ls tampered-package/Smoke Assertions
smoke.sh runs the two verify calls and the tamper check, then shells out to verify_artifacts.py, which contains the assertions. A companion test_verify_artifacts.py unit-tests the checker itself.
"${CHIO_BIN}" evidence verify --input "${INPUT_DIR}" --json \
> "${ARTIFACT_ROOT}/verify.json"
# ... write '{"tampered":true}' into query.json, expect verify to fail ...
python3 "${EXAMPLE_ROOT}/verify_artifacts.py" \
"${ARTIFACT_ROOT}" \
--write-summary \
> "${ARTIFACT_ROOT}/artifact-validation.json"verify_artifacts.py checks more than the counts. It re-hashes each manifest file, checks the receipt shape ( tool_server == "*", receipt_kind == "mediated_decision", boundary_class == "prevent", and a nested decision.verdict == "allow"), asserts the lineage grants_json is structurally equal to the single read_file/invoke grant, cross-references the receipt's attribution keys against the lineage record's subject_key and issuer_key, and confirms the tamper run failed closed with the attest provenance-missing error.
Decision rule
Where to read more