EconomyProof for Auditors
Compliance Certificates
A signed session-end certificate over receipt signatures, chain order, scope, budget, and guard evidence.
A compliance certificate collapses one Agent Client Protocol session into a single signed record: six checks over every receipt the session produced, plus the counts and timestamps that bound them. Generation, verification, and the certificate types are pure code in crates/protocol/chio-acp-proxy/src/compliance.rs, and chio cert is the only shipped caller. A certificate records results for one session under one configuration. It is not a legal, regulatory, or third-party certification.
What a certificate asserts
A certificate that reports every check passing asserts six things about the session it names:
- Every receipt in the session carries a signature its own signing key verifies.
- The receipt chain is continuous:
receipts[i].seq + 1 == receipts[i+1].seqholds across the whole session. - Every invocation named a tool inside the authorized scope set.
- The session stayed inside its invocation budget.
- Every required guard left evidence in every receipt.
- Every receipt was signed by the one kernel key the body names.
The body records the first five as typed booleans alongside an anomalies array. Generation is all-or-nothing: the first failed check raises a typed error and returns, so a certificate with a false boolean or a non-empty anomalies array is not something generate_compliance_certificate produces. Verification treats one anyway as a failure, which is what makes those fields worth checking on a certificate that arrived from somewhere else.
The signed body
The body names its own schema with COMPLIANCE_CERTIFICATE_SCHEMA, chio.compliance.certificate.v1:
pub struct ComplianceCertificateBody {
/// Schema identifier.
pub schema: String,
/// Session ID the certificate covers.
pub session_id: String,
/// Unix timestamp when the certificate was generated.
pub issued_at: u64,
/// Number of receipts examined.
pub receipt_count: u64,
/// First receipt timestamp in the session.
pub first_receipt_at: u64,
/// Last receipt timestamp in the session.
pub last_receipt_at: u64,
/// Whether all receipts passed signature verification.
pub all_signatures_valid: bool,
/// Whether the receipt chain is continuous (no gaps).
pub chain_continuous: bool,
/// Whether all receipts are within authorized scope.
pub scope_compliant: bool,
/// Whether the invocation budget was respected.
pub budget_compliant: bool,
/// Whether all required guards have evidence in every receipt.
pub guards_compliant: bool,
/// Summary of any anomalies detected (empty if fully compliant).
pub anomalies: Vec<String>,
/// The kernel public key that signed the session receipts.
pub kernel_key: PublicKey,
}The signed certificate wraps that body with the signer key and the detached signature:
pub struct ComplianceCertificate {
/// The unsigned body.
pub body: ComplianceCertificateBody,
/// Public key that signed the certificate.
pub signer_key: PublicKey,
/// Ed25519 signature over canonical JSON of `body`.
pub signature: Signature,
}Neither type carries a rename_all, so the field names above are the wire names: this schema is snake_case on the wire while most signed Chio artifacts are camelCase. The signature covers RFC 8785 canonical JSON of the body, so a verifier re-canonicalizes before checking it and a re-serialization that is not canonical fails. PublicKey and Signature serialize through to_hex: an Ed25519 key is a bare 64-character lowercase hex string and an Ed25519 signature a bare 128-character one, with no algorithm prefix. A P-256 key carries a p256: prefix and a P-384 key a p384: one.
Generation
generate_compliance_certificate walks the receipts it is handed in order. Any failed check returns a typed error, so there is no partial certificate.
pub fn generate_compliance_certificate(
session_id: &str,
receipts: &[ComplianceReceiptEntry],
config: &ComplianceConfig,
keypair: &Keypair,
) -> Result<ComplianceCertificate, ComplianceCertificateError>;The config is what the checks are run against:
pub struct ComplianceConfig {
/// Maximum number of invocations allowed (0 = unlimited).
pub budget_limit: u64,
/// Guard names that must appear in every receipt's evidence.
pub required_guards: Vec<String>,
/// Authorized resource scopes (path prefixes).
pub authorized_scopes: Vec<String>,
/// Expected tenant for all receipts. None disables tenant checking.
pub expected_tenant_id: Option<String>,
/// Trusted kernel keys allowed to sign receipts and certificates.
///
/// MUST be populated by the operator with the kernel public keys
/// that are authorized to sign receipts in this deployment. An
/// empty set is a misconfiguration: every receipt will be rejected
/// with `UntrustedKernelKey`, blocking both generation and
/// verification of compliance certificates. The first observed
/// empty-set evaluation logs a one-shot tracing warning to make
/// this contract visible to operators.
pub trusted_kernel_keys: std::collections::BTreeSet<String>,
}The checks run in this order:
- Empty session. No receipts returns
EmptySessionbefore anything else runs. - Per-receipt integrity.
validate_compliance_receiptchecks each receipt in turn: the signature, the content-addressed receipt id, the action parameter hash, the signing kernel key againsttrusted_kernel_keys, the session binding read from the receipt metadata, the tenant binding whenexpected_tenant_idis set, and that an allow decision actually carries authorization semantics. - Chain continuity. Consecutive sequence numbers, with the gap reported as the pair it expected and found.
- Scope. Skipped when
authorized_scopesis empty. Otherwise each receipt'stool_namemust start with at least one entry in the set. - Budget. Skipped when
budget_limitis zero. Otherwise the receipt count must not exceed it. The limit counts invocations, not money. - Guard evidence. Every name in
required_guardsmust appear in every receipt's evidence. - One kernel key. Every receipt after the first must carry the same
kernel_keyas the first. The body names a single key, so a session that mixed two kernels would certify under a key that misrepresents part of it. - Sign, then self-verify. The body is built, signed over its canonical bytes, and immediately run back through
verify_compliance_certificatein lightweight mode against a trust set augmented with the certificate's own signer and the receipts' kernel key. A certificate that would not verify is returned asSigninginstead of handed back.
An empty trust set rejects every receipt
ComplianceConfig derives Default, and the default trusted_kernel_keys set is empty. An empty set contains nothing, so every receipt fails the membership check with UntrustedKernelKey and neither generation nor verification can succeed. The first evaluation against an empty set emits one tracing::warn! naming the misconfiguration, then falls through to the ordinary rejection.Abort errors
ComplianceCertificateError has fifteen variants. The thirteen below are compliance aborts: each means the session is non-compliant and no certificate exists. The other two, Serialization and Signing, both wrap a String and report a failure during construction rather than a finding about the session.
| Variant | Fields | Message |
|---|---|---|
EmptySession | session_id | empty session: no receipts found for session {0} |
InvalidReceiptSignature | receipt_id | invalid receipt signature: receipt {receipt_id} failed verification |
InvalidReceiptId | receipt_id | invalid receipt id: receipt {receipt_id} does not match its canonical body |
InvalidActionHash | receipt_id | invalid receipt action hash: receipt {receipt_id} failed parameter hash verification |
SessionMismatch | receipt_id, session_id | session mismatch: receipt {receipt_id} is not bound to session {session_id} |
TenantMismatch | receipt_id, tenant_id | tenant mismatch: receipt {receipt_id} is not bound to tenant {tenant_id} |
NonAuthorizingReceipt | receipt_id | non-authorizing allow receipt: receipt {receipt_id} is not mediated/prevent/allow |
UntrustedKernelKey | receipt_id, kernel_key | untrusted kernel key: receipt {receipt_id} was signed by {kernel_key} |
ChainDiscontinuity | expected, found | chain discontinuity: expected seq {expected} but found {found} |
ScopeViolation | receipt_id, resource | scope violation: receipt {receipt_id} accesses {resource} outside authorized scope |
BudgetExceeded | used, limit | budget exceeded: {used} invocations against limit of {limit} |
GuardBypass | guard_name, receipt_id | guard bypass: guard {guard_name} has no evidence in receipt {receipt_id} |
KernelKeyMismatch | receipt_id | kernel key mismatch: receipt {receipt_id} signed by a different kernel than the session's first receipt |
Verification modes
verify_compliance_certificate takes the certificate, a VerificationMode, an optional receipt slice, and the same ComplianceConfig. It returns a result rather than an error, so a rejected certificate still says why.
pub enum VerificationMode {
Lightweight,
FullBundle,
}
pub struct CertificateVerificationResult {
pub certificate_signature_valid: bool,
pub body_consistent: bool,
pub receipts_reverified: u64,
pub receipt_failures: u64,
pub passed: bool,
pub summary: String,
}Lightweight
Lightweight verification takes the kernel's own assertions on trust and checks four things about the certificate itself: the signature over the canonical body, that the signer key is in trusted_kernel_keys, that signer_key equals body.kernel_key, and that the body is internally consistent, meaning the schema is the certificate schema, all five booleans are true, and anomalies is empty. It reads no receipts. Trusting the signer is a decision the caller makes by populating the trust set, and the check is enforced rather than advised: an untrusted signer fails whatever else holds.
Full bundle
Full-bundle verification does everything lightweight does and then re-runs the whole per-receipt integrity walk from generation against the receipt log, counting how many receipts it examined and how many failed. That walk is more than a signature check: it re-derives the content-addressed id, re-checks the action hash, re-checks the kernel key against the trust set, and re-checks the session and tenant bindings. A third party can therefore reach a verdict without trusting the issuing kernel. Passing full-bundle verification requires the lightweight conditions plus zero receipt failures.
The mode falls back rather than failing when it has nothing to work with: a FullBundle call with no receipt slice returns the lightweight result. The CLI closes that gap one level up by refusing --full without a receipt database.
A passing verification summarizes as lightweight verification passed or full-bundle verification passed (N receipts re-verified). A failing one reads verification failed: followed by every reason that applied, comma separated:
| Reason | What it means |
|---|---|
certificate signature invalid | The signer key did not verify the signature over the canonical body. |
certificate signer is not trusted | The signer key is outside the caller's trusted-kernel-key set. |
certificate signer does not match body kernel key | signer_key and body.kernel_key are different keys. |
body consistency check failed | A body boolean is false, the schema is not the certificate schema, or anomalies is non-empty. |
N receipt authority check(s) failed | Full-bundle mode only: N receipts failed the per-receipt integrity walk. |
A signer that does not match the body is reported by its own reason and never folded into the generic body-consistency line, so the two never appear together for the same underlying fault.
The chio cert commands
chio cert ships three subcommands. generate loads a session from a receipt database and signs a certificate with the capability-authority keypair. verify checks one. inspect prints one, with no cryptography at all. All three accept the global --json flag: verify then prints the whole CertificateVerificationResult instead of a verdict line, inspect prints the body instead of a field list, and generate prints the certificate to stdout, which it also does whenever no --output path is given.
# Generate a certificate for a session. --receipt-db is required.
chio cert generate --session-id <session-id> --receipt-db <path> \
[--budget-limit <n>] [--output <path>]
# Verify a certificate. --full needs --receipt-db alongside it.
chio cert verify --certificate <path> --trusted-kernel-pubkey <path> \
[--full] [--receipt-db <path>]
# Inspect a certificate without verifying anything.
chio cert inspect --certificate <path>generate signs with the capability-authority keypair at .chio-authority-seed unless the global --authority-seed-file names another path, and creates the seed when it is absent. It looks up receipts whose capability_id begins with acp-session:<session-id>, so a receipt store with no Agent Client Protocol traffic yields nothing for any session id. verify builds its trust set from the single key in --trusted-kernel-pubkey, which accepts raw 32-byte key material, bare hex, or an algorithm-prefixed hex string. Both a failed verification and an unusable input exit 1; an argument the parser rejects exits 2.
Expected output
The certificate below is a body assembled by hand, with a placeholder signature, to show what each command does with one. inspect renders every field and reaches no verdict, because it verifies nothing:
$ chio cert inspect --certificate ./cert.jsonSession ID: demo-session Schema: chio.compliance.certificate.v1 Issued at: 1756944000 Receipt count: 3 First receipt: 1756943000 Last receipt: 1756944000 Signatures: valid Chain: continuous Scope: compliant Budget: compliant Guards: compliant Signer key: 31debe55d37c722768b137131caa6087080b2e0b60b94bd785d14575cfa498bc Kernel key: 31debe55d37c722768b137131caa6087080b2e0b60b94bd785d14575cfa498bc
When it refuses
verify reaches the verdict inspect declined to. The placeholder signature does not check out, so the run fails on its first reason and exits 1. The signer is trusted here, because the same key was passed in --trusted-kernel-pubkey, which is why only one reason appears:
$ chio cert verify \
--certificate ./cert.json \
--trusted-kernel-pubkey ./kernel.pubFAIL: verification failed: certificate signature invalid
Full-bundle mode refuses before it reads anything when it has no receipt database to re-verify against:
$ chio cert verify \
--certificate ./cert.json \
--trusted-kernel-pubkey ./kernel.pub \
--fullerror [urn:chio:error:cli:other]: full-bundle verification requires --receipt-db
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.And generation refuses a session it cannot find receipts for, which is the shape a receipt store with no Agent Client Protocol history takes for every session id:
$ chio cert generate \
--session-id demo-session \
--receipt-db ./receipts.db \
--authority-seed-file ./authority.seederror [urn:chio:error:cli:other]: certificate generation failed: empty session: no receipts found for session demo-session
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.What a certificate does not prove
A compliance certificate is an attestation by the operator running the kernel, over the configuration that operator supplied:
- It is not a regulatory attestation. A regulator or accredited auditor can take the certificate as input. The finding is theirs to make.
- It says nothing about policy quality. It shows that the policies the operator declared were honored, not that those policies were sufficient for any particular regime.
- It says nothing about absent evidence. A guard that
required_guardsnever named is not checked for, so its absence from every receipt is not a finding. The same holds for a scope check with an emptyauthorized_scopesand a budget check withbudget_limitat zero: both are skipped, and both still reporttruein the body. - It does not transfer. Certificates are session-scoped. A clean certificate for one session says nothing about the next one.
Difference from the regulatory export
A certificate is a signed set of booleans for one session, and it carries no receipts. The regulatory receipt export in crates/platform/chio-http-core/src/regulatory_api.rs is the other direction: a read-only projection of the receipt store itself, filtered by agent and time window, capped at MAX_REGULATORY_EXPORT_LIMIT rows, and wrapped in a SignedExportEnvelope the kernel's receipt-signing keypair signs. One says a session held; the other hands over the underlying records to whoever wants to decide for themselves. A reviewer who has both can check one certificate signature instead of replaying a guard, and still fall back to the receipts when the answer matters.
See also
- Kernel · Receipts for the per-invocation record the certificate aggregates, including the sequence numbers chain continuity reads.
- Regulatory APIs for the receipt export, its query parameters, and its envelope.
- Reputation Scoring for the longitudinal signal compiled from receipts across many sessions, rather than one.
- Agent Passports for the portable credential bundle an agent carries between organizations.