Secrets & Signing Keys
Choose where the kernel's signing seed lives, which issuers it trusts, and how each key set rotates without breaking verification.
On this page
A node that signs every receipt with a key the host protects, admits capability tokens only from issuers you list, and rotates either key set while every receipt it already signed still verifies.
The kernel signing key signs receipts and checkpoints, trusted issuer keys verify capability tokens, admin tokens open control routes, and partner keys verify a partner's evidence. Replacing a signer, removing an issuer, and revoking one capability have different effects, so name the credential before you change it.
StartEvery chain of trust ends at a pin
A verifier accepts a signing key from its own configuration or from a signed artifact it already trusts. Six chains cover the keys a deployment signs with:
- pinned
- Listed in the verifier’s own configuration: trusted issuers, approvers, receipt signers, directory issuers, an expected identity.
- signs
- The key signs the artifact, and the signature verifies against that key alone.
- named
- An accepted artifact names the next key: a capability’s subject, a directory’s passport keys, an endorsed oracle key.
In Rust · validation.rs:266
pub(crate) fn trusted_issuer_keys(&self) -> Vec<chio_core::PublicKey> {
let mut trusted = self.config.ca_public_keys.clone();
for authority_pk in self.capability_authority.trusted_public_keys() {
if !trusted.contains(&authority_pk) {
trusted.push(authority_pk);
}
}
let kernel_pk = self.config.keypair.public_key();
if !trusted.contains(&kernel_pk) {
trusted.push(kernel_pk);
}
trusted
}
1. Give the kernel one signing key that survives restarts
The kernel reads its signing key from the seed file named by the global --authority-seed-file flag, which every subcommand accepts; no environment variable sets it. A missing file is generated and written with mode 0600 through a same-directory atomic rename. Without the flag, each boot generates a fresh signer, so the node's key changes on every restart.
$ export CHIO_AUTH_TOKEN="$(openssl rand -hex 32)"
$ chio --authority-seed-file ./authority.seed \
--receipt-db ./receipts.sqlite mcp serve-http \
--policy ./policy.yaml --server-id pricing -- python mcp_server.py$ ls -l ./authority.seed-rw------- 1 chio chio 64 Oct 11 09:14 ./authority.seed
You should see -rw-------; owner and timestamp vary. The policy and server come from the pricing-service example (chio new pricing-service --example pricing-service). Reuse an existing identity's seed. Any process that can become the Chio user can read the file, so a plaintext seed file is for development.
2. Move the seed into custody the host protects
On a headless host the seed is a systemd credential. systemd-creds encrypt seals it to the host, LoadCredentialEncrypted= decrypts it into the service's private credentials directory at start, and the unit passes that path, %d, to --authority-seed-file.
$ sudo systemd-creds encrypt --name=chio-authority \
./authority.seed /etc/credstore.encrypted/chio-authority
$ shred -u ./authority.seed[Service]
User=chio
LoadCredentialEncrypted=chio-authority:/etc/credstore.encrypted/chio-authority
ExecStart=/usr/local/bin/chio --authority-seed-file %d/chio-authority \
--receipt-db /var/lib/chio/receipts.sqlite \
mcp serve-http --policy /etc/chio/policy.yaml --server-id pricing \
-- /usr/bin/python3 /opt/pricing-service/mcp_server.pyYou should see the service keep one kernel key across restarts; step 5 reads it back from a receipt. Desktops keep the key in Keychain or Secret Service through a keychain:<service>/<account> or secret-service:<name> reference from chio key init --key; orchestrators can mount it from a secret store. A running process keeps the key it loaded, so restart it after replacing the credential.
3. Hand signing to a remote signer or an enclave
A deployment that never holds the secret half implements SigningBackend from chio-core-types::crypto. The kernel asks the backend for a public key and for signatures over bytes, and never sees the seed.
pub trait SigningBackend: Send + Sync {
fn algorithm(&self) -> SigningAlgorithm;
fn public_key(&self) -> PublicKey;
fn sign_bytes(&self, message: &[u8]) -> Result<Signature>;
}The default Ed25519Backend wraps a local Keypair. A custom backend calls AWS KMS, Google Cloud KMS, Azure Key Vault, an HSM over PKCS#11, or any other remote signer, and sign_canonical_with_backend canonicalizes a serializable value and hands the bytes to it. Under the fips feature, P-256 and P-384 ECDSA backends route through aws-lc-rs; choose the algorithm at backend construction.
A canonical receipt body is about a kilobyte, and each checkpoint signs one Merkle root over a batch. Measure the backend's latency, quotas, and cost at your receipt rate and batch size. Hardware authenticators and enclave-bound keys are listed under Custody backends.
4. List the issuers whose tokens the kernel admits
The kernel admits a capability token only when its issuer is pinned to the issuer role in the trust file it reads at start: --trust, then CHIO_TRUST, then the project's .chio/trust.toml, then ~/.chio/trust.toml. chio key init pins the node's own authority. To admit tokens a second authority mints, pin its key by name.
$ chio trust pin issuer ./issuer-2026-09.pub --name issuer-2026-09pinned issuer-2026-09 issuer ed25519:3dc8…255f file ~/.chio/trust.toml
You should see the pin and the file it went to. After a restart, that issuer's tokens are admitted and any other signer is refused with UntrustedIssuer. A pin produces no receipt, so keep the trust file under change control; chio trust pins --role issuer lists the current set.
5. Read the signing key back from a receipt
chio receipt list prints one JSON receipt per line. Reads are scoped: --tenant <id> for one tenant, or --admin-all across every tenant as an administrative read. Unscoped, the command exits 1 with CHIO-KERNEL-RECEIPT-READ-CONTEXT-MISSING and a help line naming both flags.
$ chio --receipt-db ./chio.db receipt list --admin-all --outcome deny{"id":"rcpt-01b3…9c40",
"timestamp":1791820531,
"capability_id":"cap-01ab…77d1",
"tool_server":"pricing","tool_name":"refund",
"action":{"parameters":{"order":"order-77"},"parameter_hash":"4413…ff8a"},
"decision":{"verdict":"deny",
"reason":"requested tool refund on server pricing is not in capability scope",
"guard":"kernel"},
"receipt_kind":"mediated_decision","boundary_class":"prevent",
"tool_origin":"caller_executed","redaction_mode":"none",
"content_hash":"7423…e98a",
"policy_hash":"69e9…6b55",
"trust_level":"mediated",
"kernel_key":"4f1c…a2e9",
"signature":"516e…a508"}You should see kernel_key equal to your seed's public half, and the refused refund's reason and guard inside the signed body. Identifiers and hashes are shortened here. The line is one JSON object, wrapped here, with metadata (attribution keys, signing nonce, request id) omitted.
Filter by time window, outcome, or capability
# Use the database retained by the service being audited.
$ : "${RECEIPT_DB:?Set the receipt database path}"
# List receipts in a window; --since/--until are Unix seconds.
$ chio --receipt-db "$RECEIPT_DB" receipt list --since 1719792000 --until 1722470400 --admin-all
# Narrow by decision outcome, or by the capability that authorized the call.
$ chio --receipt-db "$RECEIPT_DB" receipt list --since 1719792000 --admin-all --outcome deny
$ : "${CAPABILITY_ID:?Set the capability ID from the audit record}"
$ chio --receipt-db "$RECEIPT_DB" receipt list --capability "$CAPABILITY_ID" --admin-allEvery filter is listed under Receipt list filters.
Rotating the kernel key leaves signed receipts verifiable
rotate_authority_keypair in chio-control-plane generates a fresh keypair and atomically replaces the seed file. The operator command is in Rotate Keys & Revoke.
- One signature per receipt. Each receipt is signed once, by the key current at signing time. Rotation changes the signer for later receipts and re-signs nothing, so no overlap window is needed.
- A stitched checkpoint chain. Each checkpoint statement records the previous checkpoint hash and the kernel public key in use, so a verifier can rebuild the key timeline across the rotation.
- An observable transition. The control plane advances the authority generation, stamps the rotation time, and keeps the prior key in the trusted-key history at
GET /v1/authority. The partner card carries the same authority key history, so partners keep verifying receipts signed before the rotation.
Read more in Authority & Rotation: the generation counter, the seedless snapshot, and the term fence.
Issuer rotation overlaps both keys, then retires one
The trust file loads at process start, so a restart applies a change. For zero downtime, pin the new key beside the old one, roll replacement processes, and shift traffic to them.
$ chio trust pin issuer ./issuer-2026-10.pub --name issuer-2026-10
# restart or reload the kernel, mint with the new key, then:
$ chio trust unpin issuer-2026-09pinned issuer-2026-10 issuer ed25519:9c3e…41b7 file ~/.chio/trust.toml unpinned issuer-2026-09 issuer
Keep both pins until producers have moved and the last old-key token's validity window has closed, or accept that unpinning the issuer invalidates those outstanding tokens. Roll the processes again after the unpin, and verify an old-key refusal and a new-key admission.
Each admin credential belongs to the service that exposes it
chio api protect reads CHIO_SIDECAR_CONTROL_TOKEN, with CHIO_API_PROTECT_CONTROL_TOKEN as an alias. chio trust serve reads CHIO_TRUST_SERVICE_TOKEN or --service-token, and the trust control plane authorizes each route for its own principal. The MCP edge's credentials are on Admin API.
$ : "${CHIO_TRUST_SERVICE_TOKEN:?Set the running trust service's token}"
$ curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer $CHIO_TRUST_SERVICE_TOKEN" \
http://127.0.0.1:8940/v1/authorityWith the standalone trust service on its default loopback address, you should see the authority's generation, current key, and trusted-key history; the response itself verifies no signature. Treat each token as a service-account credential: rotate it on a schedule, scope it per environment, and keep it out of source control. Without its token, an admin API refuses every request that requires authentication.
A lost or compromised key changes what relying parties trust
The seed file is lost
Rotate to a new keypair and re-anchor relying parties on its public key. Receipts already signed stay verifiable while you retain their public keys and trust history. A new signer can extend a retained checkpoint head; trust in that signer is established separately.
The kernel signing key is compromised
Remove the signer from admission, rotate under an independently trusted authority, and assess evidence against anchors set before the suspected compromise. A later checkpoint under the compromised key repairs no trust in forged receipts.
A trusted issuer is compromised
Unpin its key and restart; capabilities it minted stop verifying on the next request. Revoke known tokens from the compromise window in the trust-control revocation store, which also revokes single tokens from a still-trusted issuer.
A compromise is found late
The receipt log is append-only and signed. Replay retained evidence forward from the last independently trusted checkpoint and examine decisions signed in the affected window. A signed log alone cannot tell a forged receipt from an authentic one made by the same key. The replay tooling and the verdict-drift Loki dashboard are on Node Observability.
Keys, variables, and key formats
Try issuer, hybrid, crypto_floor, or --outcome.
Keys a node holds
| Key | Signs or verifies | Configured by |
|---|---|---|
Kernel signing key | Signs receipts and checkpoint statements. | --authority- seed (via load_), or a SigningBackend |
Trusted issuer keys | Verify capability tokens from upstream authorities. | issuer pins in the trust file (chio trust pin issuer) |
Sidecar control token | Authenticates admin calls to chio api protect. | CHIO_, alias CHIO_ |
Trust-service token | Authenticates the trust control plane API. | CHIO_, --service-token |
Partner kernel keys | Verify a partner's checkpoints, receipts, and revocation deltas. | Pinned by adding the partner card ( chio partner add), which writes receipt-kernel pins with source = "partner:halden" |
Partner pinning and bilateral evidence are on Bilateral Receipts.
Environment variables
| Variable | Read by | Value |
|---|---|---|
CHIO_TRUST | The kernel and every verifier, at start | Path of the trust file; its issuer pins admit capability tokens. |
CHIO_ | chio api protect | Bearer token for admin calls. |
CHIO_ | Adapter helpers | Alias for the sidecar control token. |
CHIO_ | chio trust serve | Service bearer token, distinct from the sidecar and MCP edge admin tokens. |
CHIO_TEE_MODE | chio-tee | Highest- verdict-only, shadow, enforce. |
CHIO_TEE_CONFIG | chio-tee | TOML path; default /etc/. |
CHIO_HOME | Every image | Persistent state root; /var/lib/chio in the shipped images. |
The kernel signing key has no environment variable. These variables are read in crates/products/chio-cli/src/cli/runtime.rs and crates/trust/chio-tee/src/config.rs; the key types are in crates/core/chio-core-types/src/crypto.rs, the control-plane rotation in crates/platform/chio-control-plane/src/lib.rs, and DPoP in crates/kernel/chio-kernel/src/dpop.rs.
Tool provider secrets stay behind the secret broker in Credential custody, and each service that signs security artifacts keeps its own authority key.
Key format
The primary signing algorithm is Ed25519. Keys round-trip through hex: the seed is 32 bytes as 64 hex characters, and the public key is 32 bytes as 64 hex characters.
impl Keypair {
pub fn from_seed(seed: &[u8; 32]) -> Self { ... }
pub fn from_seed_hex(hex_str: &str) -> Result<Self> {
// Strips optional 0x prefix; rejects anything but 32 bytes.
}
pub fn seed_hex(&self) -> String { ... }
pub fn public_key(&self) -> PublicKey { ... }
}Public keys for other algorithms carry a self-describing prefix: p256:<hex> and p384:<hex> for the P-256 and P-384 ECDSA backends under the fips feature, and hybrid:<hex> for a classical signature composed with ML-DSA-65. Plain hex means Ed25519. The prefix is part of the wire format and is set by the backend, never by hand.
Hybrid post-quantum signing
Under the pq feature, the kernel signs receipts and capability tokens with a hybrid classical plus ML-DSA-65 backend. The kernel crypto floor selects the configuration and mirrors the HushSpec field policy.crypto_floor: allow_classical (the default), allow_hybrid, or pq_required. The last two require a 32-byte ML-DSA-65 keygen seed, from which the kernel builds the hybrid backend at boot (HybridSigningConfig). Verifiers key off the hybrid: prefix, so relying parties that read the hybrid form can verify a post-quantum deployment.
Receipt list filters
chio receipt list filters by --capability, --tool-server, --tool-name, --outcome (allow, deny, cancelled, incomplete), --since and --until in Unix seconds, and --min-cost and --max-cost, and pages with --cursor and --limit. It has no free-form event-type filter. The field set is on Receipt Contract.
Custody backends
Hardware authenticators. The chio-custody-hw crate binds capability signing to a hardware or WebAuthn authenticator: it produces HybridBackend-signed capabilities, keeps a replay-resistant nonce store, and cascades revocation.
Enclaves. Generate or provision the key inside a protected environment and bind its public key to a verified quote. The relying party appraises the attestation root, the binding, and any required launch measurement. Choose a fresh, sealed, or externally provisioned identity deliberately, because the quote defines no key recovery. Confidential Node has the provisioning steps.
Orchestrator secret mounts
Project the seed from a Kubernetes Secret, the AWS Secrets Manager CSI driver, or the Azure Key Vault CSI provider. The Chio process sees an ordinary file path, and the orchestrator handles secret delivery and access auditing. Supply the complete service arguments and a configured SecretProviderClass before deploying this fragment.
# Kubernetes Secret + CSI projection sketch
apiVersion: v1
kind: Pod
spec:
containers:
- name: chio-edge
image: ghcr.io/backbay-labs/chio-sidecar:0.2.0
args:
- --authority-seed-file=/secrets/authority.seed
- mcp
- serve-http
volumeMounts:
- name: chio-keys
mountPath: /secrets
readOnly: true
volumes:
- name: chio-keys
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: chio-keys