Chio/Docs

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

Revoking a passport or a capability is not the same as deleting it. The signed artifact continues to exist, and verifiers that already pinned it can still see what it once asserted. What changes is the lifecycle record the operator publishes and the revocation store the kernel consults at admission time. Plan your rotations before you need them.

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_at on 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_by pointing 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.

SurfaceWhat it authorizesWhere state lives
Operator signing keySigns capability tokens, issues reputation credentials, authenticates trust-control writesAuthority seed file plus TrustAuthorityStatus.rotated_at
Agent passportCarries portable reputation credentials bound to a did:chio subjectPassportLifecycleRecord in the passport-statuses registry
Capability tokenAuthorizes a specific tool invocation under scope, budget, and TTLRevocationRecord 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.

rust
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

In 0.1.0 the 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

A rotation only changes which key future signatures use. Capabilities and credentials already signed by the previous key remain valid until they expire or are revoked. If you want the old key to stop counting, revoke the capabilities it issued or wait for them to expire against their TTL.

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.

rust
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.

bash
# 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-leak

Once 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.

rust
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.

bash
# 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-123

The 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.

bash
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.

rust
/// 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. SqliteRevocationStore persists RevocationRecord rows on disk and exposes list_revocations_after for cursor-based replication. Appropriate for single-node kernels and offline proofs.
  • Trust-control service. Run chio trust serve with a revocation database and every node that points at --control-url sees 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 an updatedAt so consumers can distinguish current active state from fail-closed stale state against the advertised cacheTtlSecs.

To query capability revocation status from the CLI, use chio trust status. To query passport lifecycle, use chio passport status resolve.

bash
# 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.com

If 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 with requireActiveLifecycle: true now 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-file target and restarting the trust-control service, or by calling POST /v1/authority on the trust-control HTTP endpoint. The new public key appears on TrustAuthorityStatus so every replica can pin it. A chio trust authority rotate CLI wrapper is planned.
  • 4. Republish downstream policy. Reissue signed verifier policies and reputation credentials under the new key so that issuer_allowlist entries 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.
bash
# 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

Capability tokens have a TTL, and an unrevoked token will expire on its own. In an incident this is far too slow. Revocation is what gives you a sub-second stop, TTL is just the backstop.

Summary

ActionCommandState it writes
Rotate authority keyPOST /v1/authority (CLI wrapper planned)TrustAuthorityStatus.rotated_at
Revoke a passportchio passport status revokePassportLifecycleRecord.status = Revoked
Supersede a passportchio passport status publishsuperseded_by = <new-id>
Revoke a capabilitychio trust revoke --capability-id ...RevocationRecord in the revocation store
Check capability statuschio trust status --capability-id ...Read-only query
Check passport lifecyclechio passport status resolveReturns 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 serve so revocations propagate cluster-wide