Chio/Docs
LOGIN · JOIN

Verify Receipts Offline

Check a signed decision from your application against a key you chose, then verify the evidence package around it.

Guide chio@5f63e6e

On this page

A receipt from your application can be checked on any machine, long after the kernel that signed it has stopped. The check needs the signed record and a public key you selected yourself.

An auditor replays the evidence months later, and a compliance team archives the receipts as signed records for regulatory review. A downstream operator inspects upstream decisions with no access to the upstream kernel, and the record outlives that kernel going offline or rotating its key. Checkpoints and inclusion proofs add evidence that a record belongs to a committed log.

Start
Evidence flowChoose a stage.
ProducerRecipientProducerRecipient

The recipient chooses whom to trust

A package names the key that signed it, never whether to believe that key. The recipient pins its signers independently, and the verifier accepts signatures only from them. Verify receipts offline.

trusted signersthe recipient’s choice

The public keys the recipient accepts, chosen independently of the package: a partner card pinned with --partner, or a key passed with --trusted-kernel-pubkey. The output states “signatures verified against pinned keys”.

The recipient accepts a valid signature only from a signer it pins.

verifierchio evidence verify

Checks the file hashes, the signatures the package carries, and its inclusion proofs, with no access to the producer’s kernel. The receipt-verification project repeats the receipt checks in Python, JavaScript, and Go.

It computes integrity and inclusion from bytes. Whether an external effect happened is outside what a receipt can show.

kernel keyEd25519 by default

One keypair signs both receipts and checkpoints, and each signed body carries its public half as kernel_key. FIPS builds sign with P-256 or P-384.

The key identifies the signer. The recipient decides independently whether to trust it.

In Rust · checkpoint.rs:139
/// A signed kernel checkpoint statement.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KernelCheckpoint {
    /// The signed body.
    pub body: KernelCheckpointBody,
    /// Ed25519 signature over canonical JSON of `body`.
    pub signature: Signature,
}
The stages in one deployment: one kernel key signs receipts and checkpoints, both stay in the receipt store, and the export reads them from there. The outside commitment is optional. The verifier computes integrity and inclusion from bytes, and the recipient pins its trusted signers independently of the package.Verifiable evidence

1. Verify an allowed receipt against your key

Run the pricing tools and enter the run directory it prints. Its project includes verify-receipt, a small Rust verifier that reads the full receipt JSON and a public-key file your host wrote before it served any request.

Verify your first quotebash
cargo run --release --locked --bin verify-receipt -- \
  call-1-receipt.json kernel-public-key.txt
Output
{
  "authorized": true,
  "crypto_error": null,
  "crypto_floor": "allow_classical",
  "decision": {
    "verdict": "allow"
  },
  "ok": true,
  "parameter_hash_valid": true,
  "receipt_id": "0e38e3e7634b522fcac76d74b5d605085820a655a55859f4a7c47f4f651cee16",
  "receipt_id_valid": true,
  "signature_valid": true,
  "signer_trusted": true
}

You should see four checks pass: the signature verifies, the body matches its content-derived ID, the parameters match their hash, and the signer matches the key you supplied. ok means a valid record from that trusted signer. authorized additionally requires a recorded allow decision.

Here your host chose the key and wrote kernel-public-key.txt. For another operator’s record, take the expected key from your own trusted configuration or an authenticated channel; a key copied out of the receipt under review would check the receipt against itself. The complete verifier source selects the classical signature floor, allow_classical, that this Ed25519 host uses.

2. Confirm that a refusal verifies as a refusal

Run the same verifier on the call the grant refused.

Verify the refused refundbash
cargo run --release --locked --bin verify-receipt -- \
  outside-grant-receipt.json kernel-public-key.txt
Output
{
  "authorized": false,
  "crypto_error": null,
  "crypto_floor": "allow_classical",
  "decision": {
    "guard": "kernel",
    "reason": "requested tool refund on server pricing is not in capability scope",
    "verdict": "deny"
  },
  "ok": true,
  "parameter_hash_valid": true,
  "receipt_id": "0c8dc3e4b6ebe95b7cca1d994c77dbd62e293e9cb1f091a6ac3a5a82eb00b020",
  "receipt_id_valid": true,
  "signature_valid": true,
  "signer_trusted": true
}

You should see a successful exit with ok: true and authorized: false. The refusal is authentic, and refund stays outside the capability: verification confirms what the kernel decided and grants nothing. The same application run produced all four outcomes below.

InputResultMeaning
Allowed record, host keyok: true; authorized: trueA verified allow from the selected signer
Denied record, host keyok: true; authorized: falseA verified refusal from the selected signer
Allowed record, a different host keysigner_trusted: false; exit 1The signature verifies under a signer you did not select
policy_hash edited after signingID and signature invalid; exit 1The body no longer matches the signed record

These cases establish the record’s integrity and its signer under your key policy. A receipt records the kernel’s decision; the tool’s external effect is observed by the host and the resource owner, as the mediation boundary describes.

3. Overwrite one file in a copy of the package

Audit the pricing tools exported this run to a package directory. Copy it, overwrite query.json in the copy, and verify the copy.

Verify a package copy with a changed query.jsontranscript
$ cp -R ./package ./package-tampered
$ echo '{"tampered":true}' > ./package-tampered/query.json
$ chio evidence verify --input ./package-tampered
error[CHIO-ATTEST-PROVENANCE-MISSING]: evidence package file hash mismatch for query.json
  help: query.json changed after the package was exported; verify the package as exported, or export it again
  code: urn:chio:error:attest:provenance-missing
  docs: https://chio.computer/docs/reference/errors#error-attest-provenance-missing
exit 1

You should see exit status 1 and a file hash mismatch for query.json. The manifest carries a SHA-256 for every declared file, so any changed byte fails before a signature is read, and the error names the file a CI gate should report. The manifest is a hash list, and it makes no signed statement that the package is complete.

The same failure in JSON mode
receipt-audit · tamper-jsontranscript
$ chio evidence verify --input ./package-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"}
exit 1

In --json mode the failure is one typed object with the same exit status. It arrives on stderr, and stdout stays empty, so a wrapper that reads only stdout falls back to the exit code.

4. Require checkpoint coverage before you hand the package on

A signature shows one record is intact. A checkpoint adds a signed Merkle root over a batch of receipts, and an inclusion proof shows a receipt is a leaf under that root.

Merkle inclusion proof for record r3level 0level 1level 2level 3s0s1h1h2s2h3Rr0r1r2r3r4r5r6r7r3 = {"record":"r3"}select a leaf; click its bytes to alter them
h0=H(00 ∥ r3)\htmlClass{role-call}{h_0} = H(\texttt{00} \,\|\, \htmlClass{role-call}{r_3})
301c68ca6d160458…
h1=H(01 ∥ s0 ∥ h0)\htmlClass{role-call}{h_1} = H(\texttt{01} \,\|\, \htmlClass{cor-parent}{s_0} \,\|\, \htmlClass{role-call}{h_0})
11d3c283a2c924ab…
h2=H(01 ∥ s1 ∥ h1)\htmlClass{role-call}{h_2} = H(\texttt{01} \,\|\, \htmlClass{cor-parent}{s_1} \,\|\, \htmlClass{role-call}{h_1})
aafaadd7ead0cd55…
h3=H(01 ∥ h2 ∥ s2)=R\htmlClass{role-call}{h_3} = H(\texttt{01} \,\|\, \htmlClass{role-call}{h_2} \,\|\, \htmlClass{cor-parent}{s_2}) = \htmlClass{role-seal}{R}
ec790fe79b59e416…
matches the checkpoint root
Merkle inclusion proof for record r3s0s1h1h2s2h3Rr0r1r2r3r4r5r6r7r3 = {"record":"r3"}select a leaf; click its bytes to alter them

r3 and its three sibling hashes rebuild the checkpoint root, so the proof holds.

h0=H(00 ∥ r3)h1=H(01 ∥ s0 ∥ h0)h2=H(01 ∥ s1 ∥ h1)h3=H(01 ∥ h2 ∥ s2)h3=R\begin{aligned} \htmlClass{role-call}{h_0} &= H(\texttt{00} \,\|\, \htmlClass{role-call}{r_3}) \\ \htmlClass{role-call}{h_1} &= H(\texttt{01} \,\|\, \htmlClass{cor-parent}{s_0} \,\|\, \htmlClass{role-call}{h_0}) \\ \htmlClass{role-call}{h_2} &= H(\texttt{01} \,\|\, \htmlClass{cor-parent}{s_1} \,\|\, \htmlClass{role-call}{h_1}) \\ \htmlClass{role-call}{h_3} &= H(\texttt{01} \,\|\, \htmlClass{role-call}{h_2} \,\|\, \htmlClass{cor-parent}{s_2}) \\[4pt] \htmlClass{role-call}{h_3} &= \htmlClass{role-seal}{R} \end{aligned}
In Rust · merkle.rs:433
pub fn verify(&self, leaf_bytes: &[u8], expected_root: &Hash) -> bool {
    match self.compute_root(leaf_bytes) {
        Ok(root) => &root == expected_root,
        Err(_) => false,
    }
}
Eight illustrative record bodies, hashed as in merkle.rs: a leaf is H(0x00 ∥ record) and a node is H(0x01 ∥ left ∥ right). An inclusion proof shows that a record sits under a root. It is not a signature check and says nothing about an external effect.Receipt records and evidence

Ask the export to refuse a package whose receipts lack that coverage.

Require checkpoint coverage at exportbash
chio --receipt-db receipts.db evidence export --admin-all \
  --require-proofs --output checkpointed-package
stderr
error[CHIO-ATTEST-PROVENANCE-MISSING]: evidence export requires checkpoint coverage, but 4 receipt(s) are uncheckpointed: 0e38e3e7634b522fcac76d74b5d605085820a655a55859f4a7c47f4f651cee16@1, 981c6c1501577a5aced9db41ae1fd414c2febee1370677ce28119b5c5d0f7af1@2, 0c8dc3e4b6ebe95b7cca1d994c77dbd62e293e9cb1f091a6ac3a5a82eb00b020@3, c49ede251b54ac69db5e49881b1f97d938db1fa3234da3376739f2c932a2a57e@4
  help: export again once a checkpoint covers these receipts, or drop --require-proofs
  code: urn:chio:error:attest:provenance-missing
  docs: https://chio.computer/docs/reference/errors#error-attest-provenance-missing

You should see the export stop with an error naming four uncheckpointed receipts. This short-lived host exited before its first checkpoint batch. A long-running kernel signs a checkpoint every checkpoint_batch_size receipts, 100 by default, and the same command then writes a package with an inclusion proof for each receipt.

5. Verify the original package against your pinned key

Verify the untouched package from the same directory.

Verify your exported packagebash
chio --format json evidence verify --input package
Coverage projection
{
  "toolReceipts": 4,
  "checkpoints": 0,
  "inclusionProofs": 0,
  "uncheckpointedReceipts": 4,
  "receiptSemantics": {
    "mediatedDecisions": 4,
    "traceObservations": 0,
    "advisoryEvaluations": 0,
    "prevent": 4,
    "detectOnly": 0,
    "advisoryOnly": 0,
    "cannotSee": 0,
    "authorized": 2
  },
  "verifiedFiles": 12,
  "publicationState": "transparency_preview"
}

You should see twelve verified files and four signed records, two of them authorized allows, with zero checkpoints and four uncheckpointed receipts. The command reports that coverage and succeeds; a review that needs checkpoints enforces it at export with --require-proofs.

Pin the signer so the package check attributes every signature, as step 1 did for one receipt. --trusted-kernel-pubkey takes the key file from step 1. --partner takes the name of a partner card you added with chio partner add.

Terminal · pinned to your host keybash
chio evidence verify --input package \
  --trusted-kernel-pubkey kernel-public-key.txt
Output
verified   4 receipts from 1 kernel, signatures verified against pinned keys

You should see the four receipts verified against your pinned key (illustrative line). A record signed under any other key fails this check, even when its signature is valid. The package proves integrity and attribution; completeness comes from checkpoints and their publication, covered in Verifiable evidence.

Verifier reference

Verification checks

CheckProcedureWhat it tells you
Receipt signature
A signature over the RFC 8785 bytes of the signing envelope
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
A signature over the RFC 8785 bytes of the bare checkpoint body
The signer attests to this batch root and claimed issuance time
Checkpoint continuity
previous_checkpoint_sha256 chain
The supplied checkpoints agree with the checked predecessor commitments
Trust store lookup
Out of band, or pinned with --partner or --trusted-kernel-pubkey
The signing key is a kernel you trust

Checkpoint verification inputs

Checkpoint-backed verification uses four records. Package files are empty when their evidence is unavailable, so read the coverage report. Trusted keys always arrive separately from the package.

RecordWhere it comes fromWhat 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 signed commitment to a batch root and a claimed issuance 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 receipt or checkpoint signer matches a key your policy accepts

A self-signed receipt can be internally valid under an unknown key, so compare its key with your policy before accepting its statement. chio evidence export writes thirteen files unconditionally: twelve artifacts plus the manifest that hashes them. The capture below comes from a separate three-receipt package.

receipt-audit · exporttranscript
$ chio --receipt-db ./receipts.db evidence export --admin-all --output ./package
$ ls -1 package
README.txt
capability-lineage.ndjson
checkpoint-consistency-proofs.ndjson
checkpoint-equivocations.ndjson
checkpoint-publications.ndjson
checkpoint-witnesses.ndjson
checkpoints.ndjson
child-receipts.ndjson
inclusion-proofs.ndjson
manifest.json
query.json
receipts.ndjson
retention.json
exit 0

receipts.ndjson holds one ChioReceipt per line, wrapped as {seq, receipt}; checkpoints.ndjson one KernelCheckpoint per line; inclusion-proofs.ndjson one ReceiptInclusionProof. The four checkpoint-*.ndjson files carry the transparency material: publications, witnesses, consistency proofs, and any equivocations found.

retention.json records the live database size and the oldest receipt timestamp, query.json the export query, and manifest.json declares schema chio.evidence_export_manifest.v1. Two more files appear when you pass --policy-file: policy/source.yaml and policy/metadata.json.

Receipt field contract

A ChioReceipt is the signed record that a kernel evaluated one tool call. It has 24 fields. 21 form the receipt body; the 3 outside that body are bbs_signature, algorithm and signature. Of the body fields, 20 also feed the content-addressed id: every body field except the id itself. A field with skip_serializing_if is absent from the JSON when its condition holds, and a field without it is always present.

FieldTypeIn bodyIn idOmitted when
id
String
yes
no
always present
timestamp
u64
yes
yes
always present
capability_id
String
yes
yes
always present
tool_server
String
yes
yes
always present
tool_name
String
yes
yes
always present
action
ToolCallAction
yes
yes
always present
decision
Option<Decision>
yes
yes
none
receipt_kind
ReceiptKind
yes
yes
always present
boundary_class
BoundaryClass
yes
yes
always present
observation_outcome
Option<ObservationOutcome>
yes
yes
none
tool_origin
ToolOrigin
yes
yes
always present
redaction_mode
RedactionMode
yes
yes
always present
actor_chain
Vec<ActorRef>
yes
yes
empty
content_hash
String
yes
yes
always present
policy_hash
String
yes
yes
always present
evidence
Vec<GuardEvidence>
yes
yes
empty
metadata
Option<serde_json::Value>
yes
yes
none
trust_level
TrustLevel
yes
yes
always present
tenant_id
Option<String>
yes
yes
none
bbs_projection_version
Option<String>
yes
yes
none
kernel_key
PublicKey
yes
yes
always present
bbs_signature
Option<BbsReceiptSignature>
no
no
none
algorithm
Option<SigningAlgorithm>
no
no
is_default_optional_algorithm
signature
Signature
no
no
always present

Five always-present fields are the ones a hand-written example tends to drop: receipt_kind, boundary_class, tool_origin, redaction_mode and trust_level. None carries a serde default, so the reference verifiers raise on a missing key. Signing validation requires trust_level to agree with receipt_kind: mediated with mediated, trace with verified, advisory with advisory.

The ChioReceiptBody type mirrors ChioReceipt minus signature, algorithm, and bbs_signature. The kernel recomputes the content-addressed id from it and wraps it into the signing envelope described under the receipt signature. The signature covers that envelope, which carries bbs_signature back in, so the BBS material is signed although it sits outside the body. A verifier that omits any signed field fails signature verification.

Canonical JSON here means RFC 8785 (JSON Canonicalization Scheme). Two RFC 8785 serializers given the same value produce identical bytes, so a verifier re-creates the signed bytes without the kernel’s serializer. The complete receipt below is the cross-language conformance vector allow_receipt in tests/bindings/vectors/receipt/v1.json; use it when checking a custom parser.

tests/bindings/vectors/receipt/v1.json (case allow_receipt)json
{
  "action": {
    "parameter_hash": "035350b0e6a021a4c924a149111944945e94d12f6d8a5e24e4461e7009ebf929",
    "parameters": {
      "mode": "read",
      "path": "/workspace/docs/roadmap.md"
    }
  },
  "boundary_class": "prevent",
  "capability_id": "cap-bindings-001",
  "content_hash": "4062edaf750fb8074e7e83e0c9028c94e32468a8b6f1614774328ef045150f93",
  "decision": {
    "verdict": "allow"
  },
  "evidence": [
    {
      "details": "path allowed",
      "guard_name": "ForbiddenPathGuard",
      "verdict": true
    },
    {
      "details": "no secrets detected",
      "guard_name": "SecretLeakGuard",
      "verdict": true
    }
  ],
  "id": "c9909f733d5fb367922293f0167d06db715393be114f212096cf2866f29433ea",
  "kernel_key": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c",
  "metadata": {
    "surface": "bindings-vectors",
    "version": 1
  },
  "policy_hash": "policy-bindings-v1",
  "receipt_kind": "mediated_decision",
  "redaction_mode": "none",
  "signature": "276a4c59dbc5e0c4fbd47beefa33515d207e8f516aca296afee2cb95a443bb18d31031cae0b69943e7a930be03b2e0a60156e5a26e732e888e32d4653d059f03",
  "timestamp": 1710000200,
  "tool_name": "file_read",
  "tool_origin": "caller_executed",
  "tool_server": "srv-files",
  "trust_level": "mediated"
}

Receipt signature

The receipt signature covers the canonical JSON of a signing envelope that wraps the body. Verify it in five steps:

  1. Take the body: every field except signature, algorithm, and bbs_signature.
  2. Validate signable semantics and the BBS binding; reject if either fails.
  3. Recompute the content-addressed id over the body and confirm it equals receipt.id.
  4. Wrap into ChioReceiptSigningBody, shaped { id, body, bbs_signature }, where body is ChioReceiptIdInput (the body minus its id).
  5. 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, once 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 the signature verifier.

crates/core/chio-core-types/src/receipt/body.rs479-494rust
pub fn verify_signature(&self) -> Result<bool> {
    let body = self.body();
    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);
    }
    if chio_receipt_id(&body)? != self.id {
        return Ok(false);
    }
    let signing_body =
        ChioReceiptSigningBody::from_body_and_bbs(&body, self.bbs_signature.as_ref());
    self.kernel_key
        .verify_canonical(&signing_body, &self.signature)
}

The envelope carries the recomputed id, the body minus that id, and the BBS material:

crates/core/chio-core-types/src/receipt/signing.rs99-104rust
pub struct ChioReceiptSigningBody {
    pub id: String,
    pub body: ChioReceiptIdInput,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub bbs_signature: Option<BbsReceiptSignature>,
}

The canonicalize-then-verify step at the end:

crates/core/chio-core-types/src/crypto.rs537-540rust
pub fn verify_canonical<T: Serialize>(&self, value: &T, signature: &Signature) -> Result<bool> {
    let canonical = canonical_json_shared_bytes(value)?;
    Ok(self.verify_shared_canonical(&canonical, signature))
}

verify dispatches on the signature’s own self-describing encoding, so one call site handles Ed25519, P-256, P-384 and the hybrid post-quantum algorithm. A downstream verifier hard-coded to Ed25519 rejects valid receipts signed with another algorithm. Algorithm support and the minimum algorithm your trust policy accepts are separate choices.

The high-level helper verify_receipt in chio-binding-helpers checks the signature and parameter hash and reports the decision kind. It works on a parsed receipt and leaves the trusted-key policy to the caller, as the application verifier in step 1 shows.

Rust · chio-binding-helpersrust
use chio_binding_helpers::receipt::{
    verify_receipt, verify_receipt_json, ReceiptDecisionKind,
};

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)?;

verify_signature checks the signature against receipt.kernel_key, which is part of the receipt body. A fabricated receipt can be signed with the forger’s own key and carry the matching public key, so confirm that receipt.kernel_key matches the kernel you expect, from a trust store, a federation policy, or the checkpoint signer chain.

Merkle inclusion

A valid signature proves the kernel signed the bytes. The Merkle inclusion proof shows the kernel committed those bytes to its log: 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, never 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:

crates/kernel/chio-kernel/src/checkpoint.rs151-162rust
pub struct ReceiptInclusionProof {
    /// Which checkpoint this proof is for.
    pub checkpoint_seq: u64,
    /// The seq of the receipt being proved.
    pub receipt_seq: u64,
    /// Index of this receipt in the Merkle leaf array.
    pub leaf_index: usize,
    /// The Merkle root this proof is against.
    pub merkle_root: Hash,
    /// The audit path proof.
    pub proof: MerkleProof,
}

MerkleProof carries tree_size, leaf_index and audit_path. ReceiptInclusionProof::verify checks that its own leaf_index agrees with the proof’s before it walks the path, so a record whose two indices disagree is rejected without hashing anything.

The receipt_canonical_bytes you feed in are the exact RFC 8785 serialization of the whole ChioReceipt, including its signature, algorithm, and optional BBS material. The exporter writes ordinary serde JSON in declaration order, wrapped as {seq, receipt}, so a verifier parses each line back into a receipt and re-canonicalizes before hashing, as chio evidence verify does. Hashing the line as it sits on disk fails. The expected root comes from a signed checkpoint body: every inclusion proof terminates at a checkpoint the kernel signed.

Checkpoint signature and continuity

A kernel checkpoint is a signed statement that these receipts committed to this Merkle root at this time. The timestamp is the signer’s claim; an independent clock attestation is a separate record. New checkpoints are issued under chio.checkpoint_statement.v2. chio.checkpoint_statement.v1 tags a body with no checkpoint-chain commitment, and the validator accepts either.

crates/kernel/chio-kernel/src/checkpoint.rs101-137rust
pub struct KernelCheckpointBody {
    /// Schema identifier for new checkpoint issuance.
    pub schema: String,
    /// Monotonic checkpoint counter.
    pub checkpoint_seq: u64,
    /// First receipt seq in this batch.
    pub batch_start_seq: u64,
    /// Last receipt seq in this batch.
    pub batch_end_seq: u64,
    /// Number of leaves in the Merkle tree.
    pub tree_size: usize,
    /// Root from MerkleTree::from_leaves.
    pub merkle_root: Hash,
    /// Unix timestamp (seconds) when the checkpoint was issued.
    pub issued_at: u64,
    /// The kernel's signing key (public).
    pub kernel_key: PublicKey,
    /// Hash of the immediately preceding checkpoint body when this checkpoint extends a prior batch.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_non_null_option"
    )]
    pub previous_checkpoint_sha256: Option<String>,
    /// RFC 6962 root over the checkpoint-chain leaves for checkpoint_seq 1
    /// through this checkpoint, one leaf per checkpoint binding its sequence,
    /// entry range, and batch root (see [`checkpoint_chain_leaf_hash`]). This
    /// is the commitment that consistency proofs verify against. Absent on
    /// v1 checkpoints and on detached v2 checkpoints built without chain
    /// context.
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_non_null_option"
    )]
    pub chain_root: Option<Hash>,
}

chain_root is what the v2 tag adds: the commitment a consistency proof verifies against. The enclosing KernelCheckpoint is that body plus a signature over its canonical JSON. Both types carry deny_unknown_fields, so an extra key is a parse failure.

verify_checkpoint_signature canonicalizes the bare KernelCheckpointBody and verifies it against the embedded kernel_key, with no wrapping envelope. Receipts add the ChioReceiptSigningBody wrapper plus the id-recomputation and BBS-binding checks; a checkpoint has none of those.

Rust · chio-kernel checkpoint APIrust
use chio_kernel::checkpoint::{validate_checkpoint, verify_checkpoint_signature};

// The signature check alone.
let ok = verify_checkpoint_signature(&checkpoint)?;
assert!(ok, "checkpoint signature must verify");

// Full structural validation, then the signature.
validate_checkpoint(&checkpoint)?;

validate_checkpoint rejects a checkpoint whose schema tag is neither supported version, whose tree_size or issued_at is zero, whose batch_end_seq precedes its batch_start_seq, whose tree_size differs from batch_end_seq - batch_start_seq + 1, whose previous_checkpoint_sha256 is something other than 64 lowercase hex, whose v1 body carries a chain_root, or whose first v2 checkpoint lacks one or commits to the wrong chain leaf. Then it checks the signature.

Several checkpoints from one kernel chain. 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 tamper-evident continuity: altering those committed bodies or their order breaks the checked chain. The check covers the supplied checkpoint set; whether every event was recorded and every later checkpoint supplied is a question for publication.

Rust · checkpoint transparencyrust
use chio_kernel::checkpoint::{
    build_checkpoint_consistency_proof,
    build_checkpoint_transparency,
    validate_checkpoint_transparency,
    verify_checkpoint_consistency_proof,
};

// Validate an entire checkpoint set: derives publications, witnesses and
// consistency proofs, and returns Err on the first equivocation. The
// question mark IS the equivocation check; nothing is left to assert.
let transparency = validate_checkpoint_transparency(&checkpoints)?;

// To inspect equivocations rather than fail on them, build the summary
// instead and read its equivocations field.
let summary = build_checkpoint_transparency(&checkpoints)?;

// Optionally verify a prefix-growth proof independently. The builder also
// takes the chain leaf hashes the proof commits to.
let proof = build_checkpoint_consistency_proof(&previous, &current, &chain_leaf_hashes)?;
assert!(verify_checkpoint_consistency_proof(&previous, &current, &proof)?);

Two checkpoints that claim the same checkpoint_seq, the same cumulative tree size for one log, or the same predecessor digest, with bodies that hash differently, form a CheckpointEquivocation. validate_checkpoint_transparency returns an error on the first one. Treat any equivocation as a compromise signal and quarantine the log.

chio evidence verify output

Point the CLI at a directory produced by chio evidence export to verify manifest SHA-256 hashes, receipt signatures, checkpoint signatures, the transparency chain, publication state, and inclusion proofs. The capture comes from the three-receipt package above, built from three chio check calls, one allowed and two denied.

receipt-audit · verifytranscript
$ chio evidence verify --input ./package
evidence package verified
tool_receipts:          3
child_receipts:         0
checkpoints:            0
checkpoint_publications: 0
checkpoint_witnesses:   0
checkpoint_consistency_proofs: 0
checkpoint_equivocations: 0
capability_lineage:     3
inclusion_proofs:       0
uncheckpointed_receipts: 3
authorized_receipts:     1
trace_observations:      0
advisory_evaluations:    0
verified_files:         12
child_receipt_scope:    FullQueryWindow
transparency_preview_logs: 0
publication_state:      transparency_preview
exit 0

All three are tool_receipts; only the allowed one is an authorized_receipt. A receipt counts as authorized only when its recomputed id, its signature, and its action hash all check out and its decision is an allow, so a tampered allow drops out of the count without appearing as a denial. Without a pin the signatures are checked against the keys the package names; pin a key to attribute them.

receipt-audit · verify-jsontranscript
$ chio --format json evidence verify --input ./package | jq .
{
  "schema": "chio.evidence_export_manifest.v1",
  "verifiedAt": 1788610111,
  "toolReceipts": 3,
  "childReceipts": 0,
  "checkpoints": 0,
  "checkpointPublications": 0,
  "checkpointWitnesses": 0,
  "checkpointConsistencyProofs": 0,
  "checkpointEquivocations": 0,
  "capabilityLineage": 3,
  "inclusionProofs": 0,
  "uncheckpointedReceipts": 3,
  "receiptSemantics": {
    "mediatedDecisions": 3,
    "traceObservations": 0,
    "advisoryEvaluations": 0,
    "prevent": 3,
    "detectOnly": 0,
    "advisoryOnly": 0,
    "cannotSee": 0,
    "authorized": 1
  },
  "verifiedFiles": 12,
  "childReceiptScope": "full_query_window",
  "claimBoundary": {
    "schema": "chio.evidence_transparency_claims.v1",
    "publicationState": "transparency_preview",
    "audit": {
      "checkpointLogs": [],
      "signedCheckpoints": 0,
      "checkpointPublications": 0,
      "checkpointWitnesses": 0,
      "checkpointConsistencyProofs": 0,
      "inclusionProofs": 0,
      "capabilityLineageRecords": 3
    }
  }
}
exit 0

The JSON format is stable and machine-readable, for a CI step, a compliance check, or a release gate. schema in the verify result is the manifest’s own schema, copied through.

In-process receipt verifiers

The TypeScript, Python, and Go SDKs verify one receipt against a list of trusted signers. Install the SDK with its package manager: npm install @chio-protocol/sdk exposes the verifier through @chio-protocol/sdk/invariants, and pip install chio-sdk installs the chio import package. Run each example with a receipt path and a separate trusted-key path, the same files the Rust command in step 1 reads.

verify-receipt.mjsverify_receipt.pyGo · API signature
import { readFileSync } from "node:fs";
import { verifyReceiptWithTrustedSigners } from "@chio-protocol/sdk/invariants";

const receipt = JSON.parse(readFileSync(process.argv[2], "utf8"));
const trustedKey = readFileSync(process.argv[3], "utf8").trim();
const report = verifyReceiptWithTrustedSigners(receipt, [trustedKey]);
console.log(JSON.stringify(report, null, 2));

if (!report.ok) {
  throw new Error(`receipt rejected: ${JSON.stringify(report)}`);
}

The report identifies which check failed. The Python and TypeScript helpers return the same results as the Rust verifier on the application’s allow, denial, changed-body, and wrong-key cases. The two reports below come from the Python verifier on the conformance vectors, with the allow_receipt case’s kernel key pinned as the trusted signer.

verify_receipt_with_trusted_signers, allow_receiptjson
{
  "signature_valid": true,
  "parameter_hash_valid": true,
  "receipt_id_valid": true,
  "decision": "allow",
  "receipt_kind": "mediated_decision",
  "boundary_class": "prevent",
  "trust_level": "mediated",
  "result": "Authorized",
  "authorized": true,
  "signer_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c",
  "signer_trusted": true,
  "ok": true
}

The same call over tampered_receipt_signature, the vector whose policy_hash was edited after signing, with the same key pinned:

verify_receipt_with_trusted_signers, tampered_receipt_signaturejson
{
  "signature_valid": false,
  "parameter_hash_valid": true,
  "receipt_id_valid": false,
  "decision": "allow",
  "receipt_kind": "mediated_decision",
  "boundary_class": "prevent",
  "trust_level": "mediated",
  "result": "Authorized",
  "authorized": false,
  "signer_key_hex": "ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c",
  "signer_trusted": true,
  "ok": false
}

receipt_id_valid is false because the id is a hash of the body, so editing the body breaks the id before the signature is reached. parameter_hash_valid stays true, because that check rehashes only action.parameters and this vector left the action alone. result still reads Authorized: it labels what the receipt claims, while authorized, here false, records what the checks established.

Use ok for record validity under the selected signer policy, and require authorized when your task also needs an allow decision. Pass an empty trusted-signer list and signer_trusted is false, which drags ok and authorized false with it even for a valid receipt. That is the fail-closed default: a signature you cannot attribute to a key you chose shows only that some key signed the record.

Package verification inside a service

An audit dashboard or CI gate in Node.js, Bun, or Python verifies a package by running chio evidence verify and parsing its JSON result. The CLI bundles canonical hashing, receipt-signature verification, checkpoint-transparency checks, and checks for the inclusion proofs the package carries. Add --partner or --trusted-kernel-pubkey to apply your signer policy, and read the coverage counts against your review’s requirements.

--format json is a top-level flag, so it goes before the subcommand; placed after --input, the parser rejects the run. The JSON keys are camelCase:

verify-evidence.tsverify_evidence.py
import { spawnSync } from "node:child_process";

type VerifyResult = {
  toolReceipts: number;
  childReceipts: number;
  checkpoints: number;
  uncheckpointedReceipts: number;
  checkpointEquivocations: number;
  verifiedFiles: number;
  // ... plus publication/witness counters
};

export function verifyEvidencePackage(path: string): VerifyResult {
  const proc = spawnSync(
    "chio",
    ["--format", "json", "evidence", "verify", "--input", path],
    { encoding: "utf8" },
  );
  if (proc.status !== 0) {
    throw new Error(
      `chio evidence verify failed (exit ${proc.status}): ${proc.stdout || proc.stderr}`,
    );
  }
  return JSON.parse(proc.stdout) as VerifyResult;
}

const result = verifyEvidencePackage("./package");
if (result.checkpointEquivocations > 0) {
  throw new Error("kernel log has forked; quarantine and investigate");
}
console.log(result.toolReceipts, "tool receipts,", result.verifiedFiles, "files verified");

Against the application package, these wrappers report four tool receipts and twelve verified files. A package with no checkpoints passes, so read uncheckpointedReceipts when checkpoint coverage is required. The error branch reads both streams because the typed urn:chio:error:* body arrives on stderr with stdout empty.

Related offline verifiers

chio evidence verify is the general path for an exported evidence package. Three tools cover adjacent offline checks.

  • chio-eval-receipt verifies chio.eval-report.bundle.v1 bundles with no chio-kernel dependency. It validates the closed field set, confirms the wrapped corpus identity, recomputes each receipt’s SHA-256 and embedded ChioReceipt signature, and verifies the outer bundle signature over the RFC 8785 canonical payload. It ships a chio-eval-receipt binary (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. Its exit codes are stable: 0 verified, 10 verdict drift, 20 signature mismatch, 30 parse error, 40 schema mismatch, 50 redaction mismatch. With --json, --expect-root, and --from-tee it fits a CI gate.
  • chio-attest-verify verifies Sigstore supply-chain material and TEE quotes offline (chio attest supply-chain verify, chio attest runtime-quote verify), binding a TEE quote to a kernel signing key and receipt root.

Verification failures

Each of the seven offline verification failures points at a different class of problem.

  • Receipt signature invalid. The body was modified, or it was canonicalized differently from the kernel. Confirm the serializer follows RFC 8785, then dump the canonical bytes and diff them against a known-good fixture.
  • Inclusion proof fails. The hashed receipt bytes differ from the bytes the kernel committed. Canonicalize the whole ChioReceipt, signature included, rather than the body alone.
  • Checkpoint signature invalid. The checkpoint body was altered or signed with another key. Run validate_checkpoint and confirm checkpoint.body.kernel_key matches a pinned key.
  • Manifest hash mismatch. A package file changed after export. Fetch the package again from its authoritative source and discard the changed copy.
  • Checkpoint equivocation. Two checkpoints contradict each other on sequence, tree size, or predecessor, so the log has forked. Quarantine it, open an incident, and investigate key compromise.
  • Uncheckpointed receipt. The receipt is signed and has no checkpoint yet. Inspect checkpoint production, then export again with --require-proofs. Offline verification derives uncheckpointedReceipts from the tool proofs present; the operator report’s similarly named coverage count is computed differently.
  • Unknown signer key. The signature is valid under a key outside your trust store. Extend the trust store only through an approved federation process, such as adding a partner card, or reject the key.

Verifiers parse JSON and rebuild canonical bytes before checking a signature, so pretty-printed input still verifies, while an omitted signed field, an added value that should be absent, or different canonical bytes fail. A custom canonicalizer is tested against tests/bindings/vectors/receipt/v1.json: each of its 14 cases carries a receipt_body_canonical_json string, the bytes every SDK is held to.

Next: look up what each field records