Verify Receipts Offline
A chio receipt is a self-contained proof that a tool call was evaluated by a kernel. You do not need to phone home to trust it. This guide walks you through fetching a signed receipt, verifying its Ed25519 signature over canonical JSON, proving Merkle inclusion against a signed checkpoint root, and catching the failure modes that matter.
One command, three checks
chio evidence verify --input ./package runs canonical hashing, receipt signature verification, checkpoint transparency validation, and inclusion-proof checks against a captured evidence package. Everything on this page is what that command does under the hood, so you can wire it into your own auditor or downstream verifier.Why Offline Verification
Chio is a trust-control plane, not a trust anchor. Operators, auditors, compliance teams, and downstream agents all need to confirm that a recorded tool call really happened, and that the kernel's decision was not rewritten after the fact. Phoning home to the kernel for that answer defeats the point: the kernel itself is the thing under review.
Offline verification flips the threat model. You start from three static inputs: a signed receipt, a signed checkpoint, and the signer public keys you have independently pinned. You end with a boolean. No live service, no auth token, no round trip.
- Auditors can replay evidence months later without the kernel still running.
- Compliance teams archive receipts as long-lived proof for regulatory review.
- Downstream verifiers confirm upstream behavior without needing access to the upstream kernel.
- Forensics survive the kernel going offline or a private key being rotated.
What You Need
Three artifacts and one out-of-band fact. The artifacts travel together in an evidence package. The fact lives in your trust store.
| Artifact | Where It Comes From | What It Proves |
|---|---|---|
| Signed receipt | receipts.ndjson in the evidence package | A specific kernel decision, signed by a specific kernel key |
| Kernel checkpoint | checkpoints.ndjson in the evidence package | A Merkle root that commits a batch of receipts at a point in time |
| Inclusion proof | inclusion-proofs.ndjson in the evidence package | The receipt's canonical bytes are a leaf under the checkpoint root |
| Pinned kernel public key | Your trust store, out of band | The checkpoint signer is the kernel you expect |
The fourth item is load-bearing. If you accept whatever kernel_key the receipt tells you to accept, you are verifying against an attacker's key. Pin kernel signing keys the same way you pin TLS roots.
# Evidence package layout produced by `chio evidence export`
package/
manifest.json # schema: chio.evidence_export_manifest.v1
receipts.ndjson # one ChioReceipt per line
child-receipts.ndjson # optional nested-flow receipts
checkpoints.ndjson # one KernelCheckpoint per line
inclusion-proofs.ndjson # one ReceiptInclusionProof per line
capability-lineage.ndjson # capability issuance chain
retention.json # live DB size, oldest receipt timestamp
query.json # the export query
README.txt # human-readable summaryAnatomy of a Receipt
An ChioReceipt is the signed record that a kernel evaluated one tool call. Every field below except signature is part of the signed body.
pub struct ChioReceipt {
pub id: String, // UUIDv7, unique per receipt
pub timestamp: u64, // Unix seconds when the receipt was created
pub capability_id: String, // capability token that was exercised
pub tool_server: String, // server that handled the invocation
pub tool_name: String, // tool that was invoked
pub action: ToolCallAction, // parameters + parameter_hash
pub decision: Decision, // Allow | Deny | Cancelled | Incomplete
pub content_hash: String, // SHA-256 of evaluated content
pub policy_hash: String, // SHA-256 of the applied policy
pub evidence: Vec<GuardEvidence>,
pub metadata: Option<Value>, // attribution, financial, etc.
pub trust_level: TrustLevel, // defaults to Mediated
pub tenant_id: Option<String>,
pub kernel_key: PublicKey, // the kernel's public key (for verification)
pub algorithm: Option<SigningAlgorithm>,
pub signature: Signature, // Ed25519 over canonical JSON of the body
}The ChioReceiptBody type mirrors ChioReceipt field for field, minus signature and algorithm. It is the thing you re-serialize to canonical JSON and verify against.
Canonical JSON here means RFC 8785 (JSON Canonicalization Scheme). Two serializers anywhere in the world, fed the same value, produce byte-identical output. That property is what makes the signature portable: you do not have to replay the kernel's serializer to re-derive the signed bytes.
{
"seq": 1,
"receipt": {
"id": "rcpt-019d921b-9ac5-7153-90b4-18718979644a",
"timestamp": 1776272775,
"capability_id": "cap-019d921b-9aa3-7220-a3e9-436bde9e2fc3",
"tool_server": "*",
"tool_name": "read_file",
"action": {
"parameters": { "path": "README.md" },
"parameter_hash": "7d6441497d2a000b8143602a7817c90abe7db88e139f89c062a1c36cfe0ad9d6"
},
"decision": { "verdict": "allow" },
"content_hash": "975ad5a7b597cc8abccc578c3be3a05c364b0a82ec9a58a1cc3ba7bcb25fb9aa",
"policy_hash": "21a96ebe251caf95e78c68fcf397433a07c66e8e75eee419aabb1f910be79854",
"metadata": {
"attribution": {
"delegation_depth": 0,
"grant_index": 0,
"issuer_key": "f257309eaa20cf974672c6049fef56c3a6a5f29c043067899d1bb2fe75ed6ed0",
"subject_key": "49bea292a1f73ca3d54d5b6377d60565a6483ee87ec532cdd97aff4ebe9ea4db"
}
},
"kernel_key": "f257309eaa20cf974672c6049fef56c3a6a5f29c043067899d1bb2fe75ed6ed0",
"signature": "3d86c9f952939a231e0bb8ee9f1109024cb7997061c24f8205ee20a7639ca671ce23bbc6b908925dbb0b1e32f0f4634d3dd4d6d51026e99d9830690afde3e107"
}
}Step 1: Verify the Signature
The receipt signature is Ed25519 over the canonical JSON bytes of the receipt body. The recipe is exactly:
- Take every field except
signatureandalgorithm. - Serialize to canonical JSON per RFC 8785 (sorted keys, minimal escaping, JCS number form).
- Verify the signature with the embedded
kernel_key, but only after you have confirmed that key is one you trust.
The Rust reference implementation in chio-core-types is a tight round trip. The verifier never trusts the wire bytes; it re-canonicalizes the body and hands that to Ed25519.
impl ChioReceipt {
/// Verify the receipt signature against the embedded kernel key.
pub fn verify_signature(&self) -> Result<bool> {
let body = self.body();
self.kernel_key.verify_canonical(&body, &self.signature)
}
}
// verify_canonical is:
pub fn verify_canonical<T: Serialize>(
&self,
value: &T,
signature: &Signature,
) -> Result<bool> {
let bytes = canonical_json_bytes(value)?; // RFC 8785
Ok(self.verify(&bytes, signature)) // Ed25519
}The high-level helper verify_receipt in chio-bindings-core checks both the signature and the parameter hash in one call, and reports the decision kind so you do not re-parse the enum yourself.
use chio_bindings_core::receipt::{verify_receipt, verify_receipt_json};
let verification = verify_receipt(&receipt)?;
assert!(verification.signature_valid);
assert!(verification.parameter_hash_valid);
match verification.decision {
ReceiptDecisionKind::Allow => { /* happy path */ }
ReceiptDecisionKind::Deny => { /* denied, still signed */ }
ReceiptDecisionKind::Cancelled | ReceiptDecisionKind::Incomplete => { /* terminal */ }
}
// Or operate directly on the JSON bytes:
let verification = verify_receipt_json(json_line)?;Pin the kernel key
verify_signature checks the signature against receipt.kernel_key, which is part of the receipt body. An attacker who fabricates a receipt can sign it with their own key and put the matching public key in the body. Your verifier must separately confirm that receipt.kernel_key matches the kernel you expect, either from a trust store, a federation policy, or the checkpoint signer chain.Step 2: Verify Merkle Inclusion
A valid signature proves the kernel signed the bytes. It does not prove the kernel committed those bytes to its log. For that, you need the Merkle inclusion proof: a short audit path that reconstructs the checkpoint root from the receipt's canonical bytes.
Chio uses RFC 6962-style hashing (the Certificate Transparency variant) for the receipt log:
LeafHash(leaf_bytes) = SHA256(0x00 || leaf_bytes)NodeHash(left, right) = SHA256(0x01 || left || right)- Odd-last-node semantics are append-only: the last node carries upward unchanged, not duplicated.
A ReceiptInclusionProof contains the leaf index, the tree size at the time of commitment, and the audit path of sibling hashes from leaf to root. Verification is deterministic and requires no kernel interaction:
pub struct ReceiptInclusionProof {
pub checkpoint_seq: u64,
pub receipt_seq: u64,
pub leaf_index: usize,
pub merkle_root: Hash,
pub proof: MerkleProof, // tree_size, leaf_index, audit_path
}
impl ReceiptInclusionProof {
pub fn verify(
&self,
receipt_canonical_bytes: &[u8],
expected_root: &Hash,
) -> bool {
self.proof.verify(receipt_canonical_bytes, expected_root)
}
}The receipt_canonical_bytes you feed in must be the exact RFC 8785 serialization of the whole ChioReceipt (the signed envelope, not just the body). Chio evidence export guarantees this invariant: the same bytes the kernel committed are the bytes on disk. If you build leaves yourself, you must canonicalize with the same serializer.
The expected root must come from a signed checkpoint body (the next section). Do not accept a root out of the air; the whole point of the proof is that it bottoms out at something the kernel put its name on.
Step 3: Verify the Checkpoint Signature
A kernel checkpoint is a signed statement of the form these receipts committed to this Merkle root at this time. The schema tag is chio.checkpoint_statement.v1.
pub struct KernelCheckpointBody {
pub schema: String, // "chio.checkpoint_statement.v1"
pub checkpoint_seq: u64, // monotonic counter
pub batch_start_seq: u64, // first receipt seq in this batch
pub batch_end_seq: u64, // last receipt seq in this batch
pub tree_size: usize, // number of leaves in the Merkle tree
pub merkle_root: Hash, // root over the batch
pub issued_at: u64, // Unix seconds at issuance
pub kernel_key: PublicKey, // the signer
pub previous_checkpoint_sha256: Option<String>, // continuity link
}
pub struct KernelCheckpoint {
pub body: KernelCheckpointBody,
pub signature: Signature, // Ed25519 over canonical JSON of body
}Checkpoint signature verification is structurally identical to receipt signature verification: canonical JSON of the body, then Ed25519 against the embedded kernel_key.
use chio_kernel::checkpoint::{validate_checkpoint, verify_checkpoint_signature};
// Just the signature check.
let ok = verify_checkpoint_signature(&checkpoint)?;
assert!(ok, "checkpoint signature must verify");
// Full structural validation: schema tag, non-zero sequences,
// tree_size matches batch_end_seq - batch_start_seq + 1, and signature.
validate_checkpoint(&checkpoint)?;When you have more than one checkpoint from the same kernel, you can chain them. Each checkpoint after the first carries previous_checkpoint_sha256, the SHA-256 digest of the previous body in canonical form. Contiguous checkpoint_seq values plus matching digests give you tamper-evident continuity: splicing or re-ordering breaks the chain.
use chio_kernel::checkpoint::{
validate_checkpoint_transparency,
verify_checkpoint_consistency_proof,
};
// Validate an entire checkpoint set: derives publications, witnesses,
// consistency proofs, and fails closed on equivocation.
let transparency = validate_checkpoint_transparency(&checkpoints)?;
assert!(transparency.equivocations.is_empty());
// Optionally verify a prefix-growth proof independently.
let proof = chio_kernel::checkpoint::build_checkpoint_consistency_proof(
&previous,
¤t,
)?;
assert!(verify_checkpoint_consistency_proof(&previous, ¤t, &proof)?);Forks are equivocations
checkpoint_seq, the same cumulative tree size, or the same predecessor digest with different content, chio reports a CheckpointEquivocation andvalidate_checkpoint_transparency refuses to return a clean summary. Treat any equivocation as a compromise signal and quarantine the log.Full Example: chio evidence verify
The CLI is the fastest path to a verified evidence package. Point it at a directory produced by chio evidence export and it runs every check on this page: manifest SHA-256 hashes, receipt signatures, checkpoint signatures, transparency chain, publication state, and inclusion proofs.
# Verify a captured evidence package.
chio evidence verify --input ./package --json > verify.json
# Human-readable output:
chio evidence verify --input ./package
# evidence package verified
# tool_receipts: 1
# child_receipts: 0
# checkpoints: 0
# checkpoint_publications: 0
# checkpoint_witnesses: 0
# checkpoint_consistency_proofs: 0
# checkpoint_equivocations: 0
# capability_lineage: 1
# inclusion_proofs: 0
# uncheckpointed_receipts: 1
# verified_files: 8
# child_receipt_scope: FullQueryWindow
# publication_state: publication_previewThe shape of that JSON is stable and machine-readable. Wire it into CI, a compliance pipeline, or a release gate. Any mismatch, tampered file, missing signature, bogus inclusion proof, conflicting checkpoint, fails the command with a non-zero exit.
# Tamper check: overwrite one file in the package and re-verify.
echo '{"tampered":true}' > package/query.json
chio evidence verify --input ./package --json
# exits non-zero with code CHIO-CLI-OTHER and
# message containing "hash mismatch" for query.jsonEmbedding Verification in a Service
If you need verification inside a Node.js, Bun, or Python service (for example an audit dashboard or a CI gate), shell out to chio evidence verify against a captured evidence package and parse the JSON result. The CLI bundles canonical hashing, receipt-signature verification, checkpoint-transparency checks, and inclusion-proof verification in a single fail-closed command.
import { spawnSync } from "node:child_process";
type VerifyResult = {
tool_receipts: number;
child_receipts: number;
checkpoints: number;
checkpoint_equivocations: number;
verified_files: number;
// ... plus publication/witness counters
};
export function verifyEvidencePackage(path: string): VerifyResult {
const proc = spawnSync(
"chio",
["evidence", "verify", "--input", path, "--format", "json"],
{ encoding: "utf8" },
);
if (proc.status !== 0) {
throw new Error(
`chio evidence verify failed (exit ${proc.status}): ${proc.stderr}`,
);
}
return JSON.parse(proc.stdout) as VerifyResult;
}
const result = verifyEvidencePackage("./package");
if (result.checkpoint_equivocations > 0) {
throw new Error("kernel log has forked; quarantine and investigate");
}Native TypeScript verifier is planned
chio evidence verify.Common Failure Modes and What They Mean
Offline verification fails in a small number of well-defined ways. Each one points at a different class of problem.
| Symptom | What It Means | What To Do |
|---|---|---|
| Receipt signature invalid | The body has been modified, or you canonicalized it differently than the kernel did | Confirm you are using an RFC 8785 serializer; dump the canonical bytes and diff against a known-good fixture |
| Inclusion proof fails | The receipt bytes you hashed are not the same bytes the kernel committed | Verify you are canonicalizing the whole ChioReceipt (including signature), not just the body |
| Checkpoint signature invalid | The checkpoint body was altered or the wrong key was used | Use validate_checkpoint; confirm checkpoint.body.kernel_key matches a pinned key |
| Manifest hash mismatch | A file in the evidence package was modified after export | Re-fetch the package from the authoritative source; do not trust the tampered copy |
| Checkpoint equivocation | Two checkpoints contradict each other on seq, tree size, or predecessor | The log has forked. Quarantine, open an incident, and investigate key compromise |
| Uncheckpointed receipt | The receipt is signed but has no checkpoint yet | Expected for very recent receipts. Re-export later or require require_checkpoint_coverage in your verifier policy |
| Unknown signer key | The signature is cryptographically valid but the key is not in your trust store | Decide whether to extend the trust store (federation) or reject; never auto-extend |
Canonical JSON is unforgiving
skip_serializing_if, an extra whitespace byte, or a different number serialization will break the signature even when the data is semantically identical. If you implement canonicalization yourself instead of using the chio primitives, test against the hello-receipt-verify fixture before you ship.Summary
| Check | Primitive | What It Tells You |
|---|---|---|
| Receipt signature | Ed25519 over RFC 8785 body bytes | The kernel signed these exact fields |
| Parameter hash | action.verify_hash() | The recorded parameters match their hash |
| Inclusion proof | RFC 6962 audit path | The receipt is a leaf under the checkpoint root |
| Checkpoint signature | Ed25519 over RFC 8785 checkpoint body | The kernel committed this batch at this time |
| Checkpoint continuity | previous_checkpoint_sha256 chain | No receipts were retroactively inserted or re-ordered |
| Trust store lookup | Out of band | The signing key is a kernel you actually trust |
Next Steps
- Receipts · the full receipt model, decisions, evidence, and metadata
- Query & Audit Receipts · the live counterpart when you do have kernel access
- CLI Reference ·
chio evidence exportandchio evidence verify - Agent Passport · how checkpoint roots become portable reputation evidence