Verify security evidence
Check a receipt, a process response, or an exported package under keys you pinned, and state exactly which claim it supports.
On this page
A reviewer can check a security record without trusting the system that produced it. Verification takes the original signed bytes and a signer key from your own records, and each further input turns that signature into a larger claim about an operation, a log, a key, or an effect.
The steps freeze an incident's inputs, verify its receipts, bind a process response to the request that caused it, check an exported package against the signer's key history, and break one binding on a copy to watch the verifier refuse.
Start1. Name the claim before you choose a verifier
Write down the conclusion you intend to rely on. A reviewer asks whether a receipt belongs to the submitted request; an operator asks whether a rotated key was authorized at signing time; a recovery worker needs an authenticated provider outcome before it finalizes an operation.
The response belongs to the request you froze
chio receipt verify-process-response joins the signature result to the request frozen before dispatch and to the host's runtime_id, process_id, and capability_id. A valid receipt for another request fails the binding. Process attestation.
- This key signed this receipt. The original envelope and a trusted signer pin. A receipt ID or a display summary carries no signature.
- This response belongs to this operation. The frozen request, the trusted host context, and the result commitment. A valid receipt for an unrelated request fails the binding.
- This receipt is in this log. An inclusion path, the tree size, and a trusted checkpoint. A signature alone locates the record nowhere.
- This key was authorized then. Verified key history, the activation record, the signing epoch, and time evidence. The key active at review time says nothing about the interval in which the receipt was signed.
- This tool launched confined. The fully enforced launch record bound at dispatch as
native_launchorconfinement_launch. A compiled cage profile states intent only. - This external effect completed. Executor or provider evidence bound to the original effect. A durable capture or a timeout leaves the outcome unknown, and an unknown outcome is never re-dispatched.
A passing branch supplies none of another branch's inputs. Replaying a response is a seventh claim with its own input: the exact guarded release checkpoint, where a persisted raw output is insufficient.
2. Freeze the original inputs
Collect request.json before the executor receives it, take context.json from the independently selected runtime context, keep response.json exactly as received, and copy the key file from trusted configuration. Inputs reconstructed from the response let an attacker substitute a self-consistent operation.
import hashlib
import json
from pathlib import Path
root = Path("incident")
names = ("request.json", "context.json", "response.json", "trusted-kernel.pub")
records = []
for name in names:
data = (root / name).read_bytes()
records.append({
"file": name,
"bytes": len(data),
"sha256": hashlib.sha256(data).hexdigest(),
})
with (root / "input-digests.json").open("x", encoding="utf-8") as output:
json.dump(records, output, indent=2)
output.write("\n")Run it from the parent directory of incident/. You should see a new incident/input-digests.json with the byte length and SHA-256 digest of each file; the script opens the manifest with mode x, so it refuses to overwrite an existing one.
The manifest is a local inventory, separate from any Chio signed schema. A digest detects a changed input; who supplied it and why its signer is trusted come from the pin. Keep the manifest beside the verifier version, the exact invocations, stdout, stderr, and exit status.
3. Verify receipts under a pin from your own records
Take the expected public key from the deployment's trusted configuration or verified key history. A key extracted from the receipt under test only proves the receipt agrees with itself.
chio receipt verify \
--input incident/receipts.jsonl \
--trusted-kernel-pubkey trusted-kernel.pubYou should see each original receipt line pass the supported signed schema, the signature, the signer pin, and the action-parameter hash. The input is the original export, one JSON object per line, never the jq projection from an investigation. The check leaves policy unevaluated and says nothing about whether the log holds every receipt.
4. Bind a response to the request you froze
A writer sends report-proof with operation report-proof/send/v1. Give the verifier the request retained before dispatch, the owner-issued context holding runtime_id, process_id, and capability_id, the original response, and the owner's kernel pin.
chio receipt verify-process-response \
--response ./incident/response.json \
--request ./incident/request.json \
--context ./incident/verification-context.json \
--trusted-kernel-pubkey ./incident/kernel.pub \
--json{
"checks": [
"signature",
"signer_pin",
"action_parameter_hash",
"runtime",
"process",
"capability",
"operation_key",
"recovery_policy",
"tool",
"arguments",
"request_id",
"verdict",
"reason",
"terminal_state",
"output_shape",
"returned_output_content"
],
"receipt_verified": true,
"response_bound": true,
"schema": "chio.process.response-verification.v1",
"unchecked_fields": [
"execution_nonce_json"
]
}You should see receipt_verified and response_bound both true. The checks cover the signature and pins, the frozen operation key and recovery policy, the tool and its arguments, the presenting context, the returned verdict, and the output content. execution_nonce_json is listed in unchecked_fields: execution-nonce verification is a separate contract.
Retaining the four inputs from the Python SDK
The Python SDK's chio-process-invoke command, or its invoke_recorded function, writes the retained directory. It normalizes known_outcome_only before retaining the request, keeps the original receipt string, records transport uncertainty without an automatic retry, and runs this CLI verification. The caller's host connection and kernel key are issued by the owner before the operation.
5. Check log membership and the signer's history
An exported evidence package carries receipts, checkpoints, and inclusion proofs. Verify it against the partner card you pinned, which carries the partner's authority key history.
chio evidence verify --input audit --partner partnerverified 61 receipts from 2 kernels, offline
You should see one line counting the receipts and kernels verified against pinned keys. Receipts signed before a rotation still verify, because the card lists every key the partner's kernel signed with, and witness signatures are checked where witnesses are configured.
An inclusion proof places a receipt under one checkpoint. Whether that checkpoint is the newest acceptable one, whether the log forked, and whether every expected operation was recorded are answered by consistency proofs, witnesses, freshness, and your completeness policy. For signing authority, key transparency verifies the event history, the checkpoint, independent witness signatures, the activation commit, and artifact-time authority, and rejects an old key backdated into an interval it never owned.
6. Change one binding on a copy and watch it refuse
Verify the valid set first. Then alter one property at a time on separate copies (the operation key, the runtime, the returned output, the signer) and keep the originals byte for byte.
python3 verify_evidence.py \
--chio "$(command -v chio)" \
--original ./incident \
--cases ./verification-cases \
--unrelated-key ./other-owner.pubVERIFY original: signature and exact request/context/output bindings REFUSE changed-operation: process response binding mismatch: operation_key REFUSE wrong-runtime: process response binding mismatch: runtime_id REFUSE changed-output: process response binding mismatch: output content hash REFUSE wrong-signer: receipt verification failed: receipt signer is not in the trusted kernel key pin set RETAIN original inputs unchanged; separate nonce and history claims remain unchecked
You should see the original verify and each altered copy refuse at its own binding: a substituted request at operation_key, a substituted context at runtime_id, a changed mailbox sequence at the output content hash, and an unrelated key at the signer pin. The driver keeps each case's stdout, stderr, and exit, and records the originals' SHA-256 digests after confirming they are unchanged.
The complete verification and altered-copy driver
"""Verify original process evidence and alter only copies for negative controls."""
import argparse
import hashlib
import json
import os
import subprocess
from pathlib import Path
def compare_response(chio, original, cases, unrelated_key):
"""The original context/key come from owner configuration before invocation."""
files = ("request.json", "verification-context.json", "response.json", "kernel.pub")
retained = {name: (original / name).read_bytes() for name in files}
wrong_key = unrelated_key.read_bytes()
if wrong_key.strip() == retained["kernel.pub"].strip():
raise ValueError("negative signer control needs a genuinely different key")
cases.mkdir(mode=0o700) # Refuse reuse of an earlier comparison directory.
def check(name, changes, expected_success):
directory = cases / name
directory.mkdir(mode=0o700)
for filename, data in {**retained, **changes}.items():
with (directory / filename).open("xb") as output:
output.write(data)
command = [str(chio), "receipt", "verify-process-response",
"--response", str(directory / "response.json"),
"--request", str(directory / "request.json"),
"--context", str(directory / "verification-context.json"),
"--trusted-kernel-pubkey", str(directory / "kernel.pub"), "--json"]
result = subprocess.run(command, capture_output=True, timeout=90)
for filename, data in (("stdout", result.stdout), ("stderr", result.stderr),
("exit", f"{result.returncode}\n".encode())):
with (directory / filename).open("xb") as output:
output.write(data)
if expected_success:
report = json.loads(result.stdout)
assert result.returncode == 0 and report["receipt_verified"] and report["response_bound"]
assert report["unchecked_fields"] == ["execution_nonce_json"]
print("VERIFY original: signature and exact request/context/output bindings")
else:
assert result.returncode != 0, f"{name} was unexpectedly accepted"
raw_error = result.stderr.decode().strip()
try:
error = json.loads(raw_error)["message"]
except (json.JSONDecodeError, KeyError, TypeError):
error = raw_error
assert "binding mismatch" in error or "signature" in error or "signer" in error, error
print(f"REFUSE {name}: {error}")
encode = lambda value: (json.dumps(value, allow_nan=False) + "\n").encode()
check("original", {}, True)
request = json.loads(retained["request.json"])
request["operation_key"] += "/substituted"
check("changed-operation", {"request.json": encode(request)}, False)
context = json.loads(retained["verification-context.json"])
context["runtime_id"] += "-substituted"
check("wrong-runtime", {"verification-context.json": encode(context)}, False)
response = json.loads(retained["response.json"])
value = response["output"]["value"]
assert response["output"]["kind"] == "value" and value["status"] == "sent"
original_sequence = value["sequence"]
assert isinstance(original_sequence, str) and original_sequence.isdigit()
value["sequence"] = "2" if original_sequence == "1" else "1"
assert value["sequence"] != original_sequence
check("changed-output", {"response.json": encode(response)}, False)
check("wrong-signer", {"kernel.pub": wrong_key}, False)
assert all((original / name).read_bytes() == data for name, data in retained.items())
digests = {name: hashlib.sha256(data).hexdigest() for name, data in retained.items()}
with (cases / "original-sha256.json").open("x") as output:
json.dump(digests, output, indent=2)
output.write("\n")
print("RETAIN original inputs unchanged; separate nonce and history claims remain unchecked")
if __name__ == "__main__":
os.umask(0o077)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--chio", type=Path, required=True)
parser.add_argument("--original", type=Path, required=True)
parser.add_argument("--cases", type=Path, required=True)
parser.add_argument("--unrelated-key", type=Path, required=True)
args = parser.parse_args()
compare_response(args.chio.resolve(strict=True), args.original, args.cases, args.unrelated_key)A negative control counts only when the valid control passes and the failure reaches the intended check. The unrelated pin must be another valid public key: an unreadable file or a malformed fixture fails every command without exercising a signature or a binding. Changing the process or capability ID, or supplying only a projected summary, exercises the same bindings and the requirement for the complete artifact.
A failed check names the input to fix
Classify a failure at the edge where it occurred: an untrusted signer, an invalid signature, a changed request, a mismatched context, stale history, or insufficient effect evidence. Each names the next input to obtain or the artifact to reject.
Keep invalid and insufficient apart. A changed signature is an invalid artifact; reject it and recover the original. A valid record without the history a claim needs is insufficient; obtain the missing trusted input. Record which checks passed and which fields stayed unchecked, and never merge the two.
Read each record at its own scope. A signed trace observation can report a failure authentically while the execution outcome stays unknown. A cage launch receipt establishes the launch boundary, and target death and call completion need their own records. A response rollback receipt must agree with the reversible contributions that remain.
Read more in Investigate a session, which orders verified records beside host observations, and Formal evidence.
Next: the records and claims behind your evidence
Supply chain, release integrity, and evidence capture authority cover where the shipped binaries and their evidence came from.