Chio/Docs

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.

ArtifactWhere It Comes FromWhat It Proves
Signed receiptreceipts.ndjson in the evidence packageA specific kernel decision, signed by a specific kernel key
Kernel checkpointcheckpoints.ndjson in the evidence packageA Merkle root that commits a batch of receipts at a point in time
Inclusion proofinclusion-proofs.ndjson in the evidence packageThe receipt's canonical bytes are a leaf under the checkpoint root
Pinned kernel public keyYour trust store, out of bandThe 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.

bash
# 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 summary

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

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

json
{
  "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:

  1. Take every field except signature and algorithm.
  2. Serialize to canonical JSON per RFC 8785 (sorted keys, minimal escaping, JCS number form).
  3. 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.

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

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

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

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

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

rust
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,
    &current,
)?;
assert!(verify_checkpoint_consistency_proof(&previous, &current, &proof)?);

Forks are equivocations

If two checkpoints claim the same 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.

bash
# 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_preview

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

bash
# 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.json

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

typescript
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

A pure TypeScript offline verifier (canonical JSON via RFC 8785, Ed25519 signature verification, and RFC 6962 Merkle audit-path checks in one importable module) is on the roadmap. Until it ships, drive the CLI from your host language and treat the JSON result as the verification contract. The primitives are stable: every check described in this guide is covered by 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.

SymptomWhat It MeansWhat To Do
Receipt signature invalidThe body has been modified, or you canonicalized it differently than the kernel didConfirm you are using an RFC 8785 serializer; dump the canonical bytes and diff against a known-good fixture
Inclusion proof failsThe receipt bytes you hashed are not the same bytes the kernel committedVerify you are canonicalizing the whole ChioReceipt (including signature), not just the body
Checkpoint signature invalidThe checkpoint body was altered or the wrong key was usedUse validate_checkpoint; confirm checkpoint.body.kernel_key matches a pinned key
Manifest hash mismatchA file in the evidence package was modified after exportRe-fetch the package from the authoritative source; do not trust the tampered copy
Checkpoint equivocationTwo checkpoints contradict each other on seq, tree size, or predecessorThe log has forked. Quarantine, open an incident, and investigate key compromise
Uncheckpointed receiptThe receipt is signed but has no checkpoint yetExpected for very recent receipts. Re-export later or require require_checkpoint_coverage in your verifier policy
Unknown signer keyThe signature is cryptographically valid but the key is not in your trust storeDecide whether to extend the trust store (federation) or reject; never auto-extend

Canonical JSON is unforgiving

A missing 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

CheckPrimitiveWhat It Tells You
Receipt signatureEd25519 over RFC 8785 body bytesThe kernel signed these exact fields
Parameter hashaction.verify_hash()The recorded parameters match their hash
Inclusion proofRFC 6962 audit pathThe receipt is a leaf under the checkpoint root
Checkpoint signatureEd25519 over RFC 8785 checkpoint bodyThe kernel committed this batch at this time
Checkpoint continuityprevious_checkpoint_sha256 chainNo receipts were retroactively inserted or re-ordered
Trust store lookupOut of bandThe signing key is a kernel you actually trust

Next Steps

Verify Receipts Offline · Chio Docs