BuildAudit
Verify Receipts Offline
Verify a Chio-mediated tool call offline with Ed25519, Merkle inclusion, and RFC 8785 canonical JSON.
Verify an evidence package
chio evidence verify --input ./package runs canonical hashing, receipt signature verification, checkpoint transparency validation, and inclusion-proof checks against a captured evidence package. The sections below describe those checks so you can implement or review a 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. Querying the kernel cannot establish that independently because the kernel is under review.
Offline verification uses three static inputs: a signed receipt, a signed checkpoint, and independently pinned signer public keys. It returns a verification result without a live service or auth token.
- Auditors can replay evidence months later without the kernel still running.
- Compliance teams archive receipts as signed records 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
A receipt evidence package contains three records and a public key that your trust store supplies out of band.
| Record | Where It Comes From | What It Establishes |
|---|---|---|
| Signed receipt | receipts.ndjson in the evidence package | A kernel decision signed by a specified 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 pinned key establishes who signed the checkpoint. 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, algorithm, and bbs_signature is part of the signed body.
pub struct ChioReceipt {
pub id: String, // content-addressed, from the canonical body
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: Option<Decision>, // present only for mediated decisions
pub receipt_kind: ReceiptKind, // MediatedDecision | TraceObservation | AdvisoryEvaluation
pub boundary_class: BoundaryClass,
pub observation_outcome: Option<ObservationOutcome>,
pub tool_origin: ToolOrigin,
pub redaction_mode: RedactionMode,
pub actor_chain: Vec<ActorRef>, // signed actor attribution chain
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 bbs_projection_version: Option<String>,
pub kernel_key: PublicKey, // the kernel's public key (for verification)
pub bbs_signature: Option<BbsReceiptSignature>, // optional selective-disclosure material
pub algorithm: Option<SigningAlgorithm>,
pub signature: Signature, // Ed25519 over canonical JSON of the body
}The ChioReceiptBody type mirrors ChioReceipt minus signature, algorithm, and bbs_signature. It is the input the kernel recomputes the content-addressed id from and then wraps into the signing envelope described in Step 1; the signature is over that envelope, not the flat body. A verifier that omits any of the signed fields above (the receipt-kind, boundary, origin, redaction, and actor-chain fields are all signed) will fail signature verification against current receipts.
Canonical JSON here means RFC 8785 (JSON Canonicalization Scheme). Two RFC 8785 serializers given the same value produce identical bytes, so a verifier can re-create the signed bytes without the kernel's serializer.
{
"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 of a signing envelope that wraps the body, not over the bare body. Verify it as follows:
- Take the body: every field except
signature,algorithm, andbbs_signature. - Validate signable semantics and the BBS binding; reject if either fails.
- Recompute the content-addressed id over the body and confirm it equals
receipt.id. - Wrap into
ChioReceiptSigningBody, shaped{ id, body, bbs_signature }, wherebodyisChioReceiptIdInput(the body minus itsid). - Serialize that wrapper to canonical JSON per RFC 8785 (sorted keys, minimal escaping, JCS number form) and verify the signature with the embedded
kernel_key, but only after you have confirmed that key is one you trust.
The Rust implementation in chio-core-types re-derives the signed bytes. It recomputes the wire id, wraps the body into the signing envelope, re-canonicalizes that, and hands it to Ed25519.
impl ChioReceipt {
/// Verify the receipt signature against the embedded kernel key.
pub fn verify_signature(&self) -> Result<bool> {
let body = self.body();
// Preconditions: signable semantics and BBS binding must hold.
if body.validate_signable_semantics().is_err() {
return Ok(false);
}
if validate_bbs_receipt_binding(&body, self.bbs_signature.as_ref()).is_err() {
return Ok(false);
}
// The content-addressed id must match the recomputed body id.
if chio_receipt_id(&body)? != self.id {
return Ok(false);
}
// Sign/verify the wrapping envelope, not the flat body.
let signing_body =
ChioReceiptSigningBody::from_body_and_bbs(&body, self.bbs_signature.as_ref());
self.kernel_key.verify_canonical(&signing_body, &self.signature)
}
}
// The signing envelope wraps the body; `body` here is the ChioReceiptBody
// minus its id field (ChioReceiptIdInput).
pub struct ChioReceiptSigningBody {
pub id: String,
pub body: ChioReceiptIdInput,
pub bbs_signature: Option<BbsReceiptSignature>,
}
// 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-binding-helpers 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_binding_helpers::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 */ }
ReceiptDecisionKind::None => { /* no decision: trace or advisory receipt */ }
}
// 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 needs 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 inclusion proof must terminate at a checkpoint the kernel signed.
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 the simple case: verify_checkpoint_signature canonicalizes the bare KernelCheckpointBody and Ed25519-verifies it against the embedded kernel_key, with no wrapping envelope. Receipts are not structurally identical: they add the ChioReceiptSigningBody wrapper plus the id-recomputation and BBS-binding checks from Step 1. A checkpoint has none of those.
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.CLI Example: chio evidence verify
Point the CLI at a directory produced by chio evidence export to verify 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
# authorized_receipts: 1
# trace_observations: 0
# advisory_evaluations: 0
# verified_files: 8
# child_receipt_scope: FullQueryWindow
# transparency_preview_logs: 0
# publication_state: publication_previewThe JSON format is stable and machine-readable, so it can feed CI, a compliance check, or a release gate. A mismatch, tampered file, missing signature, invalid inclusion proof, or conflicting checkpoint causes a nonzero 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.Related Offline Verifiers
chio evidence verify is the general path for a captured evidence package. Three tools cover adjacent offline-verification needs.
- chio-eval-receipt is a reference verifier with no
chio-kerneldependency forchio.eval-report.bundle.v1bundles. It validates the closed field set, confirms the wrapped corpus identity, recomputes each receipt's SHA-256 and embeddedChioReceiptsignature, and verifies the outer bundle signature over the RFC 8785 canonical payload. It ships achio-eval-receiptbinary (verify,verify-fixture,verify-memo) and a PyO3 binding,chio_eval_receipt_py.verify_bundle_json. - chio replay <LOG> is a batch regression verifier that re-checks every Ed25519 signature and recomputes the Merkle root incrementally. It has a stable exit-code registry (
0verified,10verdict drift,20signature mismatch,30parse error,40schema mismatch,50redaction mismatch) plus--json,--expect-root, and--from-teeflags: a natural fit for a CI gate. - chio-attest-verify handles Sigstore supply-chain and TEE-quote offline verification (
chio attest supply-chain verify,chio attest runtime-quote verify), binding a TEE quote to a kernel signing key and receipt root.
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 | Extend the trust store only through an approved federation process, or reject the key. |
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 | Procedure | 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 · receipt fields, 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