Chio/Docs

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

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                  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 assertions

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 output 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 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:

fixtures/minimal-evidence/manifest.json
{
  "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.

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)
{
  "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_artifacts.py (validate_verify_output)
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:

verify_artifacts.py (validate_package summary)
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:

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

verify_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:

verify_artifacts.py (validate_tamper_error)
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:

terminal
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 1

What 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:

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 Run Output

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 (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.

smoke.sh
"${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

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 auditor workflow. Receipts for the receipt schema and signature shape. Compliance Certificates for how packages support certificate records.