Verify Receipts Offline
Check a signed decision from your application against a key you chose, then verify the evidence package around it.
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.
StartThe 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.
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,
}
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.
cargo run --release --locked --bin verify-receipt -- \
call-1-receipt.json kernel-public-key.txt{
"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.
cargo run --release --locked --bin verify-receipt -- \
outside-grant-receipt.json kernel-public-key.txt{
"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.
| Input | Result | Meaning |
|---|---|---|
| Allowed record, host key | ok: true; authorized: true | A verified allow from the selected signer |
| Denied record, host key | ok: true; authorized: false | A verified refusal from the selected signer |
| Allowed record, a different host key | signer_trusted: false; exit 1 | The signature verifies under a signer you did not select |
policy_hash edited after signing | ID and signature invalid; exit 1 | The 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.
$ cp -R ./package ./package-tampered
$ echo '{"tampered":true}' > ./package-tampered/query.json
$ chio evidence verify --input ./package-tamperederror[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
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
$ 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"}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.
r3 and its three sibling hashes rebuild the checkpoint root, so the proof holds.
merkle.rs:433pub fn verify(&self, leaf_bytes: &[u8], expected_root: &Hash) -> bool {
match self.compute_root(leaf_bytes) {
Ok(root) => &root == expected_root,
Err(_) => false,
}
}
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 evidenceAsk the export to refuse a package whose receipts lack that coverage.
chio --receipt-db receipts.db evidence export --admin-all \
--require-proofs --output checkpointed-packageerror[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.
chio --format json evidence verify --input package{
"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.
chio evidence verify --input package \
--trusted-kernel-pubkey kernel-public-key.txtverified 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
Search for inclusion, checkpoint_seq, RFC 8785, equivocation, or exit codes.
Verification checks
| Check | Procedure | What 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. | 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_ chain | The supplied checkpoints agree with the checked predecessor commitments |
Trust store lookup | Out of band, or pinned with --partner or --trusted- | 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.
| 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. in the evidence package | A signed commitment to a batch root and a claimed issuance time |
Inclusion proof | inclusion- 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.
$ chio --receipt-db ./receipts.db evidence export --admin-all --output ./package
$ ls -1 packageREADME.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
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.
| Field | Type | In body | In id | Omitted 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_ | Option<Observation | 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_ | yes | yes | none |
trust_level | TrustLevel | yes | yes | always present |
tenant_id | Option<String> | yes | yes | none |
bbs_ | Option<String> | yes | yes | none |
kernel_key | PublicKey | yes | yes | always present |
bbs_signature | Option<Bbs | no | no | none |
algorithm | Option<Signing | no | no | is_ |
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.
{
"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:
- 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, 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.
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:
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:
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.
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:
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.
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.
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.
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, ¤t, &chain_leaf_hashes)?;
assert!(verify_checkpoint_consistency_proof(&previous, ¤t, &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.
$ chio evidence verify --input ./packageevidence 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
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.
$ 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
}
}
}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.
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)}`);
}import json
from chio.invariants import verify_receipt_with_trusted_signers
import sys
from pathlib import Path
receipt = json.loads(Path(sys.argv[1]).read_text())
trusted_key = Path(sys.argv[2]).read_text().strip()
report = verify_receipt_with_trusted_signers(receipt, [trusted_key])
print(json.dumps(report, indent=2))
if not report["ok"]:
raise SystemExit(f"receipt rejected: {report}")func VerifyReceiptWithTrustedSigners(
receipt map[string]any,
trustedSigners []string,
) (ReceiptVerification, error)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.
{
"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:
{
"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:
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");import json
import subprocess
def verify_evidence_package(path: str) -> dict:
proc = subprocess.run(
["chio", "--format", "json", "evidence", "verify", "--input", path],
capture_output=True,
text=True,
)
if proc.returncode != 0:
raise RuntimeError(
f"chio evidence verify failed (exit {proc.returncode}): {proc.stdout or proc.stderr}"
)
return json.loads(proc.stdout)
result = verify_evidence_package("./package")
if result["checkpointEquivocations"] > 0:
raise SystemExit("kernel log has forked; quarantine and investigate")
print(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.v1bundles with nochio-kerneldependency. 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. Its exit codes are stable:
0verified,10verdict drift,20signature mismatch,30parse error,40schema mismatch,50redaction mismatch. With--json,--expect-root, and--from-teeit 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_checkpointand confirmcheckpoint.body.kernel_keymatches 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 derivesuncheckpointedReceiptsfrom 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.