Receipt Verification
Verify a saved decision without contacting its host. Select your trusted signer, inspect the result, and detect modified evidence.
On this page
A Chio receipt carries the decision, its argument binding, and the host’s signature, so anyone holding your public key can check it without reaching your kernel. Here Corbel Assurance verifies the package Larkfield exported, first with the Chio CLI and then with a short program of its own in Python, TypeScript, or Go.
Continues from Inspect Your Receipts · uses the evidence/ package from the fixer’s runs, 23 receipts signed by kernel 4f1c…a2e9
Verify the package as the auditor
If you no longer have the package, export it again from the project directory:
chio evidence export --admin-all --output ./evidenceCorbel trusts Larkfield’s kernel because Larkfield handed over its card out of band, never because the package names a key. Corbel pins the card once into a trust file it keeps for this engagement, then verifies:
chio trust pin receipt-kernel ./larkfield.card --trust ./corbel-trust.toml
chio evidence verify --input ./evidence --trust ./corbel-trust.tomlpinned larkfield receipt-kernel ed25519:4f1c…a2e9 file ./corbel-trust.toml verified 23 receipts, 0 checkpoints, kernel larkfield trust ./corbel-trust.toml
You should see all 23 receipts verified under the pinned key. The CLI checks every file hash in the manifest, every receipt signature, and the signed capability lineage behind each grant.
Verify each receipt from your own code
An auditor often wants the check inside its own tooling. Download the verification project, which holds the same verifier in three languages with locked dependencies. Write Larkfield’s kernel key into larkfield-signers.json as a one-element JSON array, then run the verifier over the package:
uv run --locked verify.py ./evidence/receipts.ndjson --trusted-signers larkfield-signers.jsonnpm ci
npm run verify -- ./evidence/receipts.ndjson larkfield-signers.jsongo run verify.go -trusted-signers larkfield-signers.json ./evidence/receipts.ndjsonEach program prints one result per receipt and exits zero only when all 23 pass. An empty file, a modified record, or a signer missing from larkfield-signers.json exits nonzero. For the fixer’s refused test write, the result reads:
{
"signature_valid": true,
"parameter_hash_valid": true,
"receipt_id_valid": true,
"signer_trusted": true,
"authorized": false,
"ok": true
}The record is genuine and correctly signed, so ok is true; it records a denial, so authorized is false. An auditor checking that the refusal really happened needs exactly this pair.
Read integrity and authority separately
| Field | What it establishes |
|---|---|
signature_valid | The signed body matches the recorded public key. |
parameter_ | The retained arguments match their signed hash. |
receipt_ | The content- |
signer_trusted | Your independent trust configuration accepts the signer. |
ok | All four verification checks pass. |
authorized | The record describes an authoritative allow. A valid denial can have ok: true and authorized: false. |
None of these fields shows that an external effect happened. A signature proves Larkfield’s kernel decided; the fixer’s run events and test results are the separate record of what ran.
Use the verifier in your program
The essential operation is a pure function over a receipt and an independently selected signer set. The complete programs add file handling, empty-input checks, readable output, and a meaningful exit code.
from chio.invariants import verify_receipt_with_trusted_signers
result = verify_receipt_with_trusted_signers(receipt, signers)import { verifyReceiptWithTrustedSigners } from "@chio-protocol/sdk/invariants";
const result = verifyReceiptWithTrustedSigners(receipt, signers);import "github.com/backbay-labs/chio/sdks/go/chio-go/invariants"
result, err := invariants.VerifyReceiptWithTrustedSigners(receipt, signers)Read verify.py 25 lines · complete file
"""Verify every exported receipt using separately selected trusted signers."""
import argparse
import json
from pathlib import Path
from chio.invariants import verify_receipt_with_trusted_signers
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('receipts', nargs='?', type=Path, default=Path('fixtures/minimal-evidence/receipts.ndjson'))
parser.add_argument('--trusted-signers', type=Path, default=Path('trusted-signers.json'))
args = parser.parse_args()
signers = json.loads(args.trusted_signers.read_text())
if not isinstance(signers, list) or not signers or not all(isinstance(key, str) for key in signers):
parser.error('trusted-signers must be a nonempty JSON array of public keys')
records = [json.loads(line) for line in args.receipts.read_text().splitlines() if line.strip()]
if not records:
parser.error('receipt file is empty')
results = [verify_receipt_with_trusted_signers(record.get('receipt', record), signers) for record in records]
print(json.dumps(results, indent=2))
return 0 if all(result['ok'] for result in results) else 1
if __name__ == '__main__':
raise SystemExit(main())Read verify.mjs 10 lines · complete file
import { readFileSync } from 'node:fs';
import { verifyReceiptWithTrustedSigners } from '@chio-protocol/sdk/invariants';
const [receipts = 'fixtures/minimal-evidence/receipts.ndjson', trust = 'trusted-signers.json'] = process.argv.slice(2);
const signers = JSON.parse(readFileSync(trust, 'utf8'));
if (!Array.isArray(signers) || !signers.length || signers.some(key => typeof key !== 'string')) throw Error('Trusted signers must be a nonempty JSON array of public keys');
const records = readFileSync(receipts, 'utf8').split('\n').filter(line => line.trim()).map(JSON.parse);
if (!records.length) throw Error('Receipt file is empty');
const results = records.map(record => verifyReceiptWithTrustedSigners(record.receipt ?? record, signers));
console.log(JSON.stringify(results, null, 2));
process.exitCode = results.every(result => result.ok) ? 0 : 1;Read verify.go 76 lines · complete file
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"github.com/backbay-labs/chio/sdks/go/chio-go/invariants"
"os"
)
func run() error {
trust := flag.String("trusted-signers", "trusted-signers.json", "JSON array of independently selected trusted public keys")
flag.Parse()
receiptPath := "fixtures/minimal-evidence/receipts.ndjson"
if flag.NArg() > 0 {
receiptPath = flag.Arg(0)
}
keys, err := os.ReadFile(*trust)
if err != nil {
return err
}
var signers []string
if err := json.Unmarshal(keys, &signers); err != nil {
return err
}
if len(signers) == 0 {
return fmt.Errorf("trusted signer set is empty")
}
f, err := os.Open(receiptPath)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 4096), 4*1024*1024)
count := 0
valid := true
for scanner.Scan() {
if len(scanner.Bytes()) == 0 {
continue
}
var record map[string]any
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
return err
}
receipt, ok := record["receipt"].(map[string]any)
if !ok {
receipt = record
}
result, err := invariants.VerifyReceiptWithTrustedSigners(receipt, signers)
if err != nil {
return err
}
out, err := json.MarshalIndent(result, "", " ")
if err != nil {
return err
}
fmt.Println(string(out))
valid = valid && result.Ok
count++
}
if err := scanner.Err(); err != nil {
return err
}
if count == 0 || !valid {
return fmt.Errorf("receipt verification failed")
}
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}Keep trust outside the package
Both verifiers take the signer set from a file the auditor controls: corbel-trust.toml for the CLI and larkfield-signers.json for the programs. Taking the key from the receipt being checked would let the sender choose its own trust. A package re-signed under another key passes every check inside it and still fails signer_trusted.
To give a recipient only part of the history, export one tenant with --tenant instead of --admin-all, as Export and Verify Evidence shows.
Read the complete verification report
Add --json for the report a pipeline keeps: counts of tool receipts, child receipts, checkpoints, and lineage records, the files the manifest covered, and the transparency boundary of the package.
chio evidence verify --input ./evidence --trust ./corbel-trust.toml --jsonKeep the language verifier’s signer check as a separate acceptance step beside the CLI report; each covers the other’s blind spot.
Make a controlled change
Copy the package, overwrite its query metadata in the copy, and verify the copy:
cp -R evidence tampered
printf '{"tampered":true}\n' > tampered/query.json
chio evidence verify --input ./tampered --json{"code":"urn:chio:error:attest:provenance-missing","message":"evidence package file hash mismatch for query.json","context":{"domain":"attest","severity":"error","stability":"unstable","string_code":"CHIO-ATTEST-PROVENANCE-MISSING"},"suggested_fix":"query.json changed after the package was exported; verify the package as exported, or export it again"}You should see exit status 1 and a hash mismatch naming query.json. The manifest check runs first, so the change is caught before any signature is read. To test signer trust instead, put a different key in larkfield-signers.json and rerun a language verifier: every signature stays valid while signer_trusted and ok become false.
An attacker who edits a record and also recomputes every manifest checksum gets past the hash check and fails on the signature. The project’s semantic checks rehash the manifest after each change to prove it:
Read check_rehashed.py · complete semantic tampering checks 73 lines · complete file
"""Require semantic rejection even after an attacker updates package checksums."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
def check(binary, original, destination):
destination.mkdir(parents=True, exist_ok=True)
subprocess.run(
[binary, "evidence", "verify", "--input", str(original), "--json"],
check=True,
capture_output=True,
timeout=60,
)
cases = []
for name in ("unsigned-lineage", "changed-signed-token", "changed-receipt"):
package = destination / name
shutil.copytree(original, package)
relative = "receipts.ndjson" if name == "changed-receipt" else "capability-lineage.ndjson"
path = package / relative
records = [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
if name == "unsigned-lineage":
records[0].pop("signed_capability")
records[0]["provenance"] = "legacy_projection"
elif name == "changed-signed-token":
records[0]["signed_capability"]["subject"] = "01" * 32
else:
records[0]["receipt"]["tool_name"] = "substituted_tool"
path.write_text("".join(json.dumps(record) + "\n" for record in records))
manifest_path = package / "manifest.json"
manifest = json.loads(manifest_path.read_text())
for entry in manifest["files"]:
payload = (package / entry["path"]).read_bytes()
entry["sha256"] = hashlib.sha256(payload).hexdigest()
entry["bytes"] = len(payload)
manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")
result = subprocess.run(
[binary, "evidence", "verify", "--input", str(package), "--json"],
capture_output=True,
text=True,
timeout=60,
)
(destination / (name + ".stdout.json")).write_text(result.stdout)
(destination / (name + ".stderr.txt")).write_text(result.stderr)
if result.returncode == 0:
raise ValueError("Rehashed malicious package was accepted: " + name)
# A verifier crash or CLI usage error is not a successful refusal.
output = json.loads(result.stderr)
if not output.get("code", "").startswith("urn:chio:error:attest:"):
raise ValueError("Expected a structured semantic verification failure: " + name)
cases.append(name)
return {"refused_rehashed_packages": cases}
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--chio", default=os.environ.get("CHIO_BIN") or shutil.which("chio"))
parser.add_argument("--output", type=Path)
args = parser.parse_args()
if not args.chio:
parser.error("Install the matching Chio CLI or set CHIO_BIN")
root = Path(__file__).resolve().parent
if args.output:
print(json.dumps(check(args.chio, root / "fixtures/minimal-evidence", args.output)))
else:
with tempfile.TemporaryDirectory(prefix="chio-rehashed-evidence-") as temporary:
print(json.dumps(check(args.chio, root / "fixtures/minimal-evidence", Path(temporary))))Find the record and its lineage
The package directory holds receipts.ndjson, capability-lineage.ndjson, the manifest, query metadata, retention metadata, and checkpoint-related files. The manifest covers every package file except itself. The trusted-signer file sits outside that package.
Each lineage record carries the original signed capability, its issuer, subject, and granted operation. A projection without that signed provenance is refused at the evidence boundary. The fixer’s receipts have no checkpoint witnesses or publication evidence until you publish checkpoints; Inspect Your Receipts reads individual records field by field.
What this project exercises
The CLI accepts a complete package and refuses a modified file, unsigned lineage, a changed signed capability, and a changed receipt; the last three hold even when every manifest checksum is recomputed. The language programs refuse an untrusted signer. The project’s source and dependency locks make each check inspectable.
Continue with scoped evidence export or query the application’s receipt store. Keep the operation, its record, and your trust configuration associated as the application grows.