Rotate Keys & Revoke
Trust material is not static. Operator signing keys age, agent passports get superseded, and capability tokens have to die the moment you lose confidence in the subject they authorize. This guide is the operator runbook for rotating and revoking each of those surfaces safely, with real commands and real state transitions from chio-credentials, chio-kernel, chio-cli, and chio-control-plane.
Revocation is visible history
When to Rotate
Chio gives you three independent lifecycle surfaces. Each has its own rotation cadence and its own emergency revocation path. You rotate on a schedule so that compromise is bounded, and you revoke on demand so that compromise is contained.
- Planned rotation. Operator authority keys should roll on a fixed cadence (typically every 90 days) so that a leaked seed has a hard expiration window. The trust-control service exposes
rotated_aton its authority status so verifiers can confirm the latest rotation. - Reactive revocation. A compromised subject, a bad deployment, or a failed policy evaluation is reason to revoke immediately. For capability tokens this flips one row in the revocation store. For passports this transitions the lifecycle record to
Revoked. - Superseding publication. A new passport for the same subject replaces the old one by publishing a lifecycle record with
superseded_bypointing at the new artifact id. This is not a security event, but it is a state transition that relying parties honor.
Every one of these actions leaves a persistent record. Rotation metadata lives on the authority status; revocation records live in the sqlite revocation store or the trust-control cluster; passport lifecycle records live in the passport statuses registry.
The Three Things You Rotate
Before you run any command, understand which lifecycle you are touching. Conflating these is the most common operator error.
| Surface | What it authorizes | Where state lives |
|---|---|---|
Operator signing key | Signs capability tokens, issues reputation credentials, authenticates trust-control writes | Authority seed file plus TrustAuthorityStatus.rotated_at |
Agent passport | Carries portable reputation credentials bound to a did:chio subject | PassportLifecycleRecord in the passport-statuses registry |
Capability token | Authorizes a specific tool invocation under scope, budget, and TTL | RevocationRecord in SqliteRevocationStore or trust-control |
A rotation in one surface does not cascade to another by accident. Rotating an operator key does not revoke outstanding capabilities signed by the previous key. Revoking a passport does not revoke the capability tokens the subject already holds. If you want the bigger effect, you have to run the bigger procedure, which this guide covers.
Rotating an Operator Signing Key
An operator signing key is an Ed25519 keypair held on disk as a seed file. Chio reads it with load_or_create_authority_keypair and rolls it with rotate_authority_keypair, both exported from chio-control-plane. The rotation is atomic: a new Ed25519 keypair is generated, written to the seed path, and the new public key is returned.
pub fn rotate_authority_keypair(path: &Path) -> Result<chio_core::PublicKey, CliError> {
let keypair = Keypair::generate();
write_authority_seed_file(path, &keypair)?;
Ok(keypair.public_key())
}In production you should rotate through the trust-control service so every replica observes the rotation together. The service exposes POST /v1/authority which calls into the same primitive and returns a TrustAuthorityStatus with the new public key and a rotated_at timestamp.
CLI surface is planned
chio trust authority rotate and chio trust authority status subcommands are not yet wired through the CLI. Today, operators roll the authority seed on disk (regenerate the --authority-seed-file target atomically) or call into the trust-control POST /v1/authority and GET /v1/authority endpoints directly. The CLI wrappers shown in the emergency runbook below are on the roadmap.After rotation, confirm the new public key is visible to every downstream that will verify signatures. Readers should hit the authority endpoint and pin the new key in their local policy before the old key actually stops signing new capabilities.
Rotation does not invalidate old signatures
Revoking a Passport
A passport is a bundle of signed credentials. Revocation operates on the published PassportLifecycleRecord, not on the signed artifact. The record moves from Active to Revoked, stamps revoked_at, and optionally carries a revoked_reason string that appears in every subsequent resolution.
pub enum PassportLifecycleState {
Active,
Stale,
Superseded,
Revoked,
NotFound,
}
pub struct PassportLifecycleRecord {
pub passport_id: String,
pub subject: String,
pub issuers: Vec<String>,
pub issuer_count: usize,
pub published_at: u64,
pub updated_at: u64,
pub status: PassportLifecycleState,
pub superseded_by: Option<String>,
pub revoked_at: Option<u64>,
pub revoked_reason: Option<String>,
pub distribution: PassportStatusDistribution,
pub valid_until: String,
}To revoke, call passport status revoke against the same registry file or trust-control service where the passport was originally published. The record must already be published: the registry refuses to transition an entry that does not exist.
# Revoke in a local lifecycle registry
chio passport status revoke \
--passport-id <passport-artifact-id> \
--passport-statuses-file passport-statuses.json \
--reason compromised
# Revoke via trust-control (all replicas observe it)
chio \
--control-url https://trust.example.com \
--control-token operator-admin-token \
passport status revoke \
--passport-id <passport-artifact-id> \
--reason key-leakOnce revocation is committed, every subsequent resolution returns a PassportLifecycleResolution with state = Revoked. If a verifier policy has requireActiveLifecycle: true, the passport stops evaluating successfully the moment that resolution is available.
Empty reason is rejected
PassportLifecycleRecord validation fails closed if you publish an entry with an empty revoked_reason. Supply a non-empty string or omit the flag entirely. Operators reviewing the audit trail later will thank you for using the reason field.Revoking a Capability Token
Capability tokens are the tightest revocation surface in chio. Every admission through ChioKernel consults a RevocationStore. Flipping one row stops a specific token from authorizing anything, on the next admission, everywhere the store is visible.
pub trait RevocationStore: Send {
/// Check if a capability ID has been revoked.
fn is_revoked(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;
/// Revoke a capability. Returns true if it was newly revoked.
fn revoke(&mut self, capability_id: &str) -> Result<bool, RevocationStoreError>;
}
pub struct RevocationRecord {
pub capability_id: String,
pub revoked_at: i64,
}The CLI surface is chio trust revoke. It targets either a local sqlite revocation store (via --revocation-db) or a remote trust-control service (via --control-url). Both paths end in the same primitive.
# Local single-node kernel, persistent sqlite revocation store
chio \
--revocation-db revocations.sqlite3 \
trust revoke \
--capability-id cap-test-123
# Trust-control service, cluster-wide visibility
chio \
--control-url https://trust.example.com \
--control-token operator-admin-token \
trust revoke \
--capability-id cap-test-123The command prints whether the capability was newly revoked and which backend committed the write. In JSON mode you get a machine readable result suitable for automated runbooks.
chio --json \
--revocation-db revocations.sqlite3 \
trust revoke \
--capability-id cap-test-123
# {
# "capability_id": "cap-test-123",
# "revoked": true,
# "newly_revoked": true,
# "revocation_backend": "revocations.sqlite3"
# }Revocation Cascade
Revocation is not scoped to a single token. The kernel revokes a capability and every descendant in its delegation subtree. When you revoke a root capability, every capability whose delegation_chain contains the revoked id is rejected on presentation.
/// Revoke a capability and all descendants in its delegation subtree.
///
/// When a root capability is revoked, every capability whose
/// `delegation_chain` contains the revoked ID will also be rejected
/// on presentation (the kernel checks all chain entries against the
/// revocation store).
pub fn revoke_capability(&self, capability_id: &CapabilityId) -> Result<(), KernelError> {
info!(capability_id = %capability_id, "revoking capability");
let _ = self.with_revocation_store(|store| Ok(store.revoke(capability_id)?))?;
Ok(())
}The mechanism is deliberately simple. At admission time, the kernel walks the presented capability's delegation chain. For each link it asks the revocation store whether that id has been revoked. If any ancestor is revoked, admission fails with delegation chain revoked at ancestor. The remediation surfaced in the error is to inspect the capability lineage and reissue the chain from a non-revoked ancestor.
This has three practical implications:
- Revoke the root, not the leaf. If an agent hands out ten attenuated children and one of those children misbehaves, revoke the child. If the agent itself is compromised, revoke the parent and every child dies with it.
- Chains with missing ancestors also fail. If a delegation chain references an ancestor that was revoked before this capability was issued, admission refuses the chain. The system does not let you mint new authority under a dead parent.
- Reissuance requires a live ancestor. To bring a subtree back to life you have to reissue from a non-revoked node. Simply unrevoking is not part of the trait, revocation is one-way.
Publishing & Checking Revocation Status
Revocation is only useful if the verifiers that care about it can see it. Chio gives you two publishing paths for capability revocations and one for passport lifecycle.
- Local sqlite.
SqliteRevocationStorepersistsRevocationRecordrows on disk and exposeslist_revocations_afterfor cursor-based replication. Appropriate for single-node kernels and offline proofs. - Trust-control service. Run
chio trust servewith a revocation database and every node that points at--control-urlsees the same revocation set. The service exposes a revocation query API with cursor pagination and a revoke admin endpoint. - Passport status distribution. Passport revocations are published into the passport-statuses registry and resolved through
/v1/public/passport/statuses/resolve. Each resolution includes anupdatedAtso consumers can distinguish currentactivestate from fail-closedstalestate against the advertisedcacheTtlSecs.
To query capability revocation status from the CLI, use chio trust status. To query passport lifecycle, use chio passport status resolve.
# Capability status
chio --json \
--revocation-db revocations.sqlite3 \
trust status \
--capability-id cap-test-123
# Passport lifecycle
chio passport status resolve \
--passport-id <passport-artifact-id> \
--passport-statuses-file passport-statuses.json
# Passport lifecycle via the public holder route (read-only, no admin token)
chio passport status resolve \
--passport-id <passport-artifact-id> \
--control-url https://trust.example.comIf you start trust-control with --advertise-url, published passport records inherit https://.../v1/public/passport/statuses/resolve as the default holder resolution endpoint. Public resolution is advertised only when the distribution also carries an explicit cacheTtlSecs. You can also advertise the resolve URL through the subject DID document via chio did resolve --passport-status-url ..., which emits an ChioPassportStatusService DID service entry.
Emergency Response Runbook
When a seed file leaks or an agent is confirmed compromised, stop thinking about rotation cadence and run this procedure in order. Every step is idempotent; repeat a step if a partial failure leaves you uncertain.
- 1. Revoke outstanding capabilities. Identify the root capability ids the compromised key or subject controls. For each one, run
chio trust revoke --capability-id <id>against the trust-control service. The cascade handles descendants automatically: every chain containing a revoked ancestor is now rejected on admission. - 2. Revoke the passport. If the compromised subject carried a passport, transition its lifecycle record with
chio passport status revoke --passport-id <id> --reason compromised. Any verifier policy withrequireActiveLifecycle: truenow fails closed for that subject. - 3. Rotate the operator key. If the leaked material was the authority seed, regenerate the keypair atomically by overwriting the
--authority-seed-filetarget and restarting the trust-control service, or by callingPOST /v1/authorityon the trust-control HTTP endpoint. The new public key appears onTrustAuthorityStatusso every replica can pin it. Achio trust authority rotateCLI wrapper is planned. - 4. Republish downstream policy. Reissue signed verifier policies and reputation credentials under the new key so that
issuer_allowlistentries keep matching. Stale policy artifacts still verify against their signer, but they no longer name the current authority. - 5. Audit the receipt log. Pull receipts for the compromised subject window and reconcile what the revoked capabilities did before the revocation landed. The receipt log is append-only; revocation does not rewrite it, it just stops new admissions.
# Emergency: one agent compromised, one operator key leaked
export CHIO_CONTROL=https://trust.example.com
export CHIO_TOKEN=operator-admin-token
# 1. Kill the agent's capability subtree
chio --control-url $CHIO_CONTROL --control-token $CHIO_TOKEN \
trust revoke --capability-id cap-root-7b0f6f63
# 2. Revoke the passport
chio --control-url $CHIO_CONTROL --control-token $CHIO_TOKEN \
passport status revoke \
--passport-id passport-7b0f6f63-v3 \
--reason compromised
# 3. Rotate the authority key (CLI wrapper planned;
# until then, POST /v1/authority on trust-control)
curl -X POST "$CHIO_CONTROL/v1/authority" \
-H "Authorization: Bearer $CHIO_TOKEN"
# 4. Confirm the new authority public key
curl "$CHIO_CONTROL/v1/authority" \
-H "Authorization: Bearer $CHIO_TOKEN"Do not wait for TTL
Summary
| Action | Command | State it writes |
|---|---|---|
| Rotate authority key | POST /v1/authority (CLI wrapper planned) | TrustAuthorityStatus.rotated_at |
| Revoke a passport | chio passport status revoke | PassportLifecycleRecord.status = Revoked |
| Supersede a passport | chio passport status publish | superseded_by = <new-id> |
| Revoke a capability | chio trust revoke --capability-id ... | RevocationRecord in the revocation store |
| Check capability status | chio trust status --capability-id ... | Read-only query |
| Check passport lifecycle | chio passport status resolve | Returns PassportLifecycleResolution |
Next Steps
- Agent Passport · how passports are issued, verified, and what lifecycle states mean to a relying party
- Delegate Between Agents · how delegation chains are built, which informs how revocation cascades through them
- Capabilities · the authorization primitive that revocation shuts down
- Trust Control Plane · how to run
chio trust serveso revocations propagate cluster-wide