Chio/Docs
LOGIN · JOIN

BuildIdentity

Rotate Keys & Revoke

Rotate signing keys and revoke Agent Passports, capability tokens, and affected delegated capabilities.

Revocation is visible history

Revoking a passport or a capability is not the same as deleting it. The signed record 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.

Prerequisites

  • A revocation store the kernel reads. Either a local sqlite file named by --revocation-db, or a trust-control service reached through --control-url. Every command here targets one or the other.
  • An admin control token for the service path. Revoking and rotating go through --control-token, which the CLI sends as an Authorization: Bearer header; reading a passport lifecycle record does not need one.
  • A passport statuses registry if you publish passports: a JSON file named by --passport-statuses-file, or the same registry hosted by trust-control. A record has to be published before it can be revoked.
  • An authority backend on the service if you rotate operator keys. Start chio trust serve with --authority-db: on that backend the status carries a generation counter and a rotatedAt stamp. With --authority-seed-file the rotation still happens but both come back null, and with neither the rotate call answers 409.

When to Rotate

Chio has three independent lifecycles, each with its own rotation cadence and emergency revocation path. Rotate on a schedule to limit the lifetime of a leaked key; revoke on demand after a compromise.

  • Planned rotation. Operator authority keys should roll on a fixed cadence (typically every 90 days) to limit the lifetime of a leaked seed. 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 passport ID. This is a lifecycle transition that relying parties honor, not a security revocation.

These actions leave persistent records. 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.


Key and credential lifecycles

Before you run any command, understand which lifecycle you are touching. Treat each lifecycle independently.

ObjectWhat it authorizesWhere state lives
Operator signing keySigns capability tokens, issues reputation credentials, authenticates trust-control writesAn authority seed file, or the service authority database. Only the database backend stamps TrustAuthorityStatus.rotated_at and a generation counter
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 lifecycle does not affect another. 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. Revoke each affected credential type separately.


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 all replicas observe the rotation. The service exposes POST /v1/authority, which performs the same key rotation and returns a TrustAuthorityStatus carrying the new public key. GET /v1/authority reads the same record back. Rotation is a trust-plane operation on shared state, so it runs through the service or through the seed file the service reads; there is no per-process CLI verb that would let one replica rotate out from under the others.

Which backend the service runs on decides what the status can tell a verifier. On --authority-db the response carries a monotonic generation and a rotatedAt stamp. On --authority-seed-file both come back null, so a replica has no rotation marker to compare against and has to diff the key itself.

After rotation, confirm that the new public key is visible to each verifier. Verifiers should query 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 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 an Agent Passport

An Agent Passport is a bundle of signed credentials. Revocation operates on the published PassportLifecycleRecord, not on the signed passport. The record moves from Active to Revoked, stamps revoked_at, and optionally carries a revoked_reason string returned by later resolutions.

crates/trust/chio-credentials/src/passport.rs88-107rust
pub struct PassportLifecycleRecord {
    pub passport_id: String,
    pub subject: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub issuers: Vec<String>,
    pub issuer_count: usize,
    pub published_at: u64,
    #[serde(default)]
    pub updated_at: u64,
    pub status: PassportLifecycleState,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub superseded_by: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub revoked_at: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub revoked_reason: Option<String>,
    #[serde(default, skip_serializing_if = "PassportStatusDistribution::is_empty")]
    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 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

Against a local registry file the same verb prints the transition it wrote:

revocation-runbook · revoke-passporttranscript
$ chio passport status revoke \
    --passport-id "$(cat ./passport-id.txt)" \
    --passport-statuses-file ./passport-statuses.json \
    --reason compromised
passport revoked
passport_id:   57191b1ef425678d13e2080b8447bddba4ae8363585ff0d3a69efdac414cf4cc
state:         revoked
revoked_at:    1788609711
revoked_reason:compromised
exit 0

The trust-control variant takes no --passport-statuses-file: the service owns the registry, and the record it returns carries the same fields under camelCase keys.

Once committed, later resolutions return a PassportLifecycleResolution with state = Revoked. If a verifier policy has requireActiveLifecycle: true, the passport stops evaluating successfully the moment that resolution is available.

revocation-runbook · resolvetranscript
$ chio passport status resolve \
    --passport-id "$(cat ./passport-id.txt)" \
    --passport-statuses-file ./passport-statuses.json
passport_id:   57191b1ef425678d13e2080b8447bddba4ae8363585ff0d3a69efdac414cf4cc
state:         revoked
subject:       did:chio:d02f9b5f38ce7746e2ccbd8fc93f6d1d9dd4ddbe02a3e206830a6950b3752590
updated_at:    1788609711
source:        registry:./passport-statuses.json
revoked_at:    1788609711
revoked_reason:compromised
exit 0

An empty reason poisons the registry file

PassportLifecycleRecord validation rejects an entry with an empty revoked_reason, but the CLI does not run that validation before it writes. Passing --reason '' succeeds, exits zero, and stores the empty string. Every later read of that registry then fails, including the revoke that would have repaired it, so the file can only be fixed by hand. Always supply a non-empty string, or omit the flag.
revocation-runbook · revoke-empty-reasontranscript
$ chio passport status revoke \
    --passport-id "$(cat ./passport-id.txt)" \
    --passport-statuses-file ./poisoned-statuses.json \
    --reason ''
passport revoked
passport_id:   57191b1ef425678d13e2080b8447bddba4ae8363585ff0d3a69efdac414cf4cc
state:         revoked
revoked_at:    1788609711
revoked_reason:
exit 0
revocation-runbook · resolve-poisonedtranscript
$ chio passport status resolve \
    --passport-id "$(cat ./passport-id.txt)" \
    --passport-statuses-file ./poisoned-statuses.json
error [urn:chio:error:policy:decision-denied]: invalid passport lifecycle contract: passport lifecycle entry `57191b1ef425678d13e2080b8447bddba4ae8363585ff0d3a69efdac414cf4cc` cannot include an empty revoked_reason
context: {"domain":"policy","severity":"error","stability":"stable","string_code":"CHIO-CLI-POLICY"}
suggested fix: Inspect the policy rule path and update the request or policy inputs before retrying.
exit 1

Revoking a Capability Token

Capability tokens are revoked at admission. Each admission through ChioKernel consults a RevocationStore. Flipping one row stops that token from authorizing the next admission at each kernel that can read the store.

crates/kernel/chio-kernel/src/revocation_runtime.rs16-39rust
pub trait RevocationStore: Send + Sync {
    /// 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(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;

    fn observe_revocation(
        &self,
        capability_id: &str,
    ) -> Result<RevocationObservation, RevocationStoreError> {
        Ok(RevocationObservation {
            revoked: self.is_revoked(capability_id)?,
            commit: None,
        })
    }

    /// Whether this store loses its revocation set on process restart. The
    /// default is the safe (loud) assumption so an unknown store is treated as
    /// ephemeral; durable and remote stores override to `false`.
    fn is_ephemeral(&self) -> bool {
        true
    }
}

Four methods, and every one takes &self, so a store is shared behind an Arc and mutates through interior mutability. observe_revocation is the richer read: it returns the boolean plus the commit metadata a distributed store attaches, and its default body answers from is_revoked with no commit. The row the sqlite store persists is two fields:

crates/kernel/chio-kernel/src/revocation_store.rsrust
pub struct RevocationRecord {
    pub capability_id: String,
    pub revoked_at: i64,
}

The CLI command 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 call the same revocation operation.

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
revocation-runbook · revoketranscript
$ chio --revocation-db ./revocations.sqlite3 \
    trust revoke --capability-id cap-test-123
capability_id: cap-test-123
revoked:       true
newly_revoked: true
backend:       ./revocations.sqlite3
exit 0

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.

revocation-runbook · revoke-jsontranscript
$ chio --json --revocation-db ./revocations.sqlite3 \
    trust revoke --capability-id cap-test-123
{
  "capability_id": "cap-test-123",
  "newly_revoked": false,
  "revocation_backend": "./revocations.sqlite3",
  "revoked": true
}
exit 0

newly_revoked distinguishes the first write from a repeat, and the transcript above is the second run against the same store: revoked is still true and newly_revoked has gone false. That is what makes the step safe to re-run in a runbook. The alphabetical key order is the canonical JSON the command-result envelope serializes in, not a tidied listing.


Revocation Cascade

Revoking a capability also blocks its delegated descendants. When you revoke a root capability, any capability whose delegation_chain contains the revoked id is rejected on presentation.

crates/kernel/chio-kernel/src/kernel/validation/revocation_trace.rs10-29rust
pub fn revoke_capability(&self, capability_id: &CapabilityId) -> Result<(), KernelError> {
    info!(capability_id = %capability_id, "revoking capability");
    let trace_transition = self.lock_runtime_trace_transition()?;
    let newly_revoked = self.with_revocation_store(|store| Ok(store.revoke(capability_id)?))?;
    let trace_event = if self.runtime_trace_observer.is_some() {
        Some(RuntimeTraceEvent::RevocationCommitted {
            source_sequence: self.allocate_runtime_trace_source_sequence()?,
            capability_id: capability_id.clone(),
            newly_revoked,
            delegation_depth_limit: self.config.max_delegation_depth,
        })
    } else {
        None
    };
    drop(trace_transition);
    if let Some(event) = trace_event {
        self.observe_runtime_trace(event);
    }
    Ok(())
}

The write itself is one row. The rest of the function is the runtime trace: the revocation is committed inside a trace transition, and the commit metadata becomes a RevocationCommitted event only when the store actually changed something.

Admission is where the subtree is enforced. The kernel checks the presented capability first, then walks its delegation chain link by link, asking the revocation store about each id:

crates/kernel/chio-kernel/src/kernel/validation.rs718-730rust
pub(crate) fn check_revocation(&self, cap: &CapabilityToken) -> Result<(), KernelError> {
    if self.with_revocation_store(|store| Ok(store.is_revoked(&cap.id)?))? {
        return Err(KernelError::CapabilityRevoked(cap.id.clone()));
    }
    for link in &cap.delegation_chain {
        if self.with_revocation_store(|store| Ok(store.is_revoked(&link.capability_id)?))? {
            return Err(KernelError::DelegationChainRevoked(
                link.capability_id.clone(),
            ));
        }
    }
    Ok(())
}

The two failures are distinguishable. A revoked leaf raises capability has been revoked; a revoked ancestor raises delegation chain revoked at ancestor <id>. Inspect the capability lineage and reissue the chain from a non-revoked ancestor.

This has three practical implications:

  • Revoke a delegated-capability root when its issuer is compromised. 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; its children will also be rejected.
  • A chain whose ancestors the kernel cannot find also fails, for a different reason. Delegation admission requires a stored capability snapshot for every ancestor, and refuses the chain with delegation admission failed: missing capability snapshot for delegation ancestor when one is absent, or when the stored ancestor's depth or parent linkage disagrees with the presented chain. That is a lineage failure, not a revocation one, and it fires whether or not anything was ever revoked.
  • Reissuance requires a live ancestor. Reissue from a non-revoked node to restore a subtree. TheRevocationStore trait has no unrevocation operation.

A Revoked Capability, End to End

The transcript below is one capability across four steps: it authorizes a tool call, an operator revokes it, the same call is refused, and the refusal is itself a signed receipt. Every labelled block is output captured from that run.

Two processes. The trust-control service owns the revocation set, and the MCP edge reads it over --control-url, which is what makes a revoke on one host visible to the kernel on another. Start the service first:

terminal 1: trust-controlbash
chio trust serve \
  --listen 127.0.0.1:8787 \
  --service-token operator-admin-token \
  --receipt-db ./receipts.sqlite3 \
  --authority-db ./authority.sqlite3 \
  --session-db ./trust-session.sqlite3

Then the edge, pointed at that service. It wraps a tool server that exposes one tool, hello_world, under a policy that grants it. This is the project chio init scaffolds.

terminal 2: the governed edgebash
chio --session-db ./session.sqlite3 \
  --control-url http://127.0.0.1:8787 \
  --control-token operator-admin-token \
  mcp serve --policy policy.yaml --server-id hello \
  -- ./target/debug/hello_server

1. The capability authorizes the call

An MCP client sends tools/call. The kernel admits it and the wrapped server runs.

MCP tool call response, before the revokejson
{
  "id": 3,
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "text": "Hello, Ada! This call was mediated by Chio.",
        "type": "text"
      }
    ],
    "isError": false,
    "structuredContent": {
      "greeting": "Hello, Ada! This call was mediated by Chio."
    }
  }
}

The receipt that call wrote names the capability the operator is about to revoke. Read it back through the same control service and take the capability_id. Receipts come back in ascending sequence order, so take the last line rather than --limit 1, which would hand you the oldest receipt in the store:

bash
chio --control-url http://127.0.0.1:8787 \
  --control-token operator-admin-token \
  receipt list --limit 200 \
  | tail -n 1 | jq -r '.capability_id'
jq stdoutbash
cap-019fbe3d-bcc9-7263-9752-68702b83f369

2. The operator revokes it

bash
chio --control-url http://127.0.0.1:8787 \
  --control-token operator-admin-token \
  trust revoke \
  --capability-id cap-019fbe3d-bcc9-7263-9752-68702b83f369
chio trust revoke stdout (trust-control backend)bash
capability_id: cap-019fbe3d-bcc9-7263-9752-68702b83f369
revoked:       true
newly_revoked: true
backend:       http://127.0.0.1:8787

backend reports the service URL rather than a file path, which is how you confirm the write landed in shared state and not in a local sqlite file only this process reads.

3. The same call is refused

No restart, no new session. The client repeats the identical tools/call on the live edge.

MCP tool call response, after the revokejson
{
  "id": 4,
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "text": "capability has been revoked: cap-019fbe3d-bcc9-7263-9752-68702b83f369",
        "type": "text"
      }
    ],
    "isError": true
  }
}

The wrapped tool server was never reached. The kernel raised KernelError::CapabilityRevoked during admission, and the edge surfaced it as an MCP tool error with isError: true.

4. The refusal is on the record

A denial writes a signed receipt on the same schema as an allow. Query the deny lane:

bash
chio --control-url http://127.0.0.1:8787 \
  --control-token operator-admin-token \
  receipt list --outcome deny --limit 5
receipt list --outcome deny stdout (one JSON Lines row, pretty-printed)json
{
  "action": {
    "parameter_hash": "88bab6d8f6dc68a877064d584cbb5b6c50e74f617ea50d81d3a53c2ee6ffbc4f",
    "parameters": {
      "name": "Ada"
    }
  },
  "boundary_class": "prevent",
  "capability_id": "cap-019fbe3d-bcc9-7263-9752-68702b83f369",
  "content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
  "decision": {
    "guard": "kernel",
    "reason": "capability has been revoked: cap-019fbe3d-bcc9-7263-9752-68702b83f369",
    "verdict": "deny"
  },
  "id": "110b30ea4829d0cda2b23fa65c117906532bd0ae45a60297c4fd76c00f14382b",
  "kernel_key": "c56228c41776e96d3b266053c0cfd3e2b8439cad5b83730b37a8d2d8ec4fa226",
  "metadata": {
    "attribution": {
      "delegation_depth": 0,
      "issuer_key": "617e2042e610c87a33ba9876e6cb6d750389db1dd74010b62372aa4e6e7eb73d",
      "subject_key": "ead3b427b2954a3373dce7976618a5af04d7d4e60eccfdeb11ccc32bfd163172"
    },
    "chio_receipt_signing_nonce": "rcpt-019fbe3d-bf7e-7ab2-89b0-f5a89725b5dc",
    "receipt_context": {
      "request_id": "mcp-edge-req-a26edfd6b5b14d6aa4d8b81697e975dd"
    }
  },
  "policy_hash": "69e943b96e9ce64d0264bb56bf8930e77bd4e68adf68fcf1395790dae03e6b55",
  "receipt_kind": "mediated_decision",
  "redaction_mode": "none",
  "signature": "a1ea2190da78070fea48d91464b3756f2110b7bba5e3e3f282df3bc2df7abcbeaac386bfb8c9b534e8a365e5f147132884fc4bceb39fc3e082b687cba7687a0d",
  "timestamp": 1785603145,
  "tool_name": "hello_world",
  "tool_origin": "caller_executed",
  "tool_server": "hello",
  "trust_level": "mediated"
}

Four fields carry the incident. decision.guard is kernel, which is what every kernel-built deny receipt carries: the field names the layer that refused, not the check that fired. Read decision.reason and, for a guard denial, evidence[].guard_name to tell one refusal from another. decision.reason repeats the revoked id. capability_id ties the denial to the allow that preceded it. And action.parameter_hash is byte identical to the allowed call's, which is what lets an auditor prove the two receipts describe the same request either side of the revoke.

The allow stays in the log

Revocation stops the next admission. It does not retract the receipt for work already done. The deny receipt above sits beside the allow it followed, both signed under the same kernel_key c56228c4.... That pair, not the deny alone, is what an incident review reads.

Publishing & Checking Revocation Status

Verifiers must be able to read a revocation before it affects their decisions. Chio provides two publication 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 either a joint authority database (--session-db, which supplies the revocation store along with budget state) or a standalone --revocation-db. The two are mutually exclusive, and with neither a revoke answers 409 naming the flag it wants. Every node pointed at --control-url then 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/{passport_id}. 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
revocation-runbook · statustranscript
$ chio --json --revocation-db ./revocations.sqlite3 \
    trust status --capability-id cap-test-123
{
  "capability_id": "cap-test-123",
  "revocation_backend": "./revocations.sqlite3",
  "revoked": true
}
exit 0

The same query against an id the store has never seen returns revoked: false rather than an error, so a status poll does not need to distinguish never issued from issued and still live.

revocation-runbook · status-unknowntranscript
$ chio --json --revocation-db ./revocations.sqlite3 \
    trust status --capability-id cap-never-issued
{
  "capability_id": "cap-never-issued",
  "revocation_backend": "./revocations.sqlite3",
  "revoked": false
}
exit 0

If you start trust-control with --advertise-url, published passport records inherit https://.../v1/public/passport/statuses/resolve as the default holder resolution endpoint, together with a cacheTtlSecs of 300. The TTL is a mandatory companion rather than a precondition: PassportStatusDistribution validation rejects a distribution that publishes resolve URLs without one, and rejects a TTL of zero. 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; run this procedure in order. Each step is idempotent; repeat one 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: chains containing a revoked ancestor are rejected at 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 each replica can pin it, and the previous key stays in trustedPublicKeys so signatures it already made keep verifying. Run the service on --authority-db if you want the rotation stamped: the seed-file backend returns a null generation and rotatedAt.
  • 4. Republish downstream policy. Reissue signed verifier policies and reputation credentials under the new key so that issuer_allowlist entries keep matching. Stale policies 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. Revocation leaves existing receipt entries unchanged and 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. Rotation is a trust-plane operation on
#    shared state, so it runs through trust-control, not a per-process verb.
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"

Steps 3 and 4, against a live service started with --authority-db. Read the status first so you know which key you are replacing:

revocation-runbook · authority-statustranscript
$ curl -sS "$CHIO_CONTROL/v1/authority" \
    -H "Authorization: Bearer $CHIO_TOKEN" | jq .
{
  "configured": true,
  "backend": "sqlite",
  "publicKey": "d991d21af0635a2eacdca9e85966fd8f08bdb3580d28c72dc0fee4f334febf23",
  "generation": 1,
  "rotatedAt": 1788609712,
  "appliesToFutureSessionsOnly": true,
  "trustedPublicKeys": [
    "d991d21af0635a2eacdca9e85966fd8f08bdb3580d28c72dc0fee4f334febf23"
  ]
}
exit 0
revocation-runbook · authority-rotatetranscript
$ curl -sS -X POST "$CHIO_CONTROL/v1/authority" \
    -H "Authorization: Bearer $CHIO_TOKEN" | jq .
{
  "appliesToFutureSessionsOnly": true,
  "backend": "sqlite",
  "configured": true,
  "generation": 2,
  "publicKey": "4a3756f1650d76f57a8d7cc09a83aa95c4b14cfe339857e9db2fde282783a807",
  "rotatedAt": 1788609712,
  "trustedPublicKeys": [
    "d991d21af0635a2eacdca9e85966fd8f08bdb3580d28c72dc0fee4f334febf23",
    "4a3756f1650d76f57a8d7cc09a83aa95c4b14cfe339857e9db2fde282783a807"
  ]
}
exit 0

generation went from 1 to 2 and publicKey changed, which is the pair a replica compares. trustedPublicKeys now holds both keys: rotation adds, it does not retract, so a capability the old key signed still verifies until it expires or is revoked. appliesToFutureSessionsOnly says the same thing about sessions already open.

Do not wait for TTL

Capability tokens have a TTL, and an unrevoked token will expire on its own. In an incident, revoke the token so the next admission fails; expiration supplements revocation.

Failures and Recovery

What you seeWhat it meansWhat to do
missing or invalid control bearer token, HTTP 401The control token does not match the service tokenCheck --control-token against the service's --service-token. Reads of the public passport route need no token at all
trust control service requires --authority-seed-file or --authority-db, HTTP 409The service was started with no authority backend, so there is nothing to rotateRestart it with --authority-db and rotate again
trust control service requires --revocation-db, HTTP 409The service holds no revocation storeRestart with a joint authority database (--session-db) or a standalone --revocation-db. Passing both is refused
a joint authority database replaces separate budget and revocation databasesBoth --session-db and --revocation-db were passed to the serviceDrop one. The joint database already owns the revocation store
was not found in the lifecycle registryYou revoked a passport the registry never publishedPublish the record first with chio passport status publish, then revoke
cannot include an empty revoked_reasonA revoke with --reason '' already landed and every read of that registry now failsEdit the registry JSON by hand: the repair path runs the same failing load
delegation chain revoked at ancestorA capability presented under a revoked parentExpected after a cascade. Reissue the subtree from a live ancestor; there is no unrevoke
private directory ancestry must not be group or world writable unless stickyThe directory holding the sqlite files has permissive parentschmod 700 the working directory before starting the service
revocation-runbook · authority-unauthorizedtranscript
$ curl -sS -w '\nHTTP %{http_code}\n' "$CHIO_CONTROL/v1/authority" \
    -H "Authorization: Bearer wrong-token"
{"error":"missing or invalid control bearer token"}
HTTP 401
exit 0

Summary

ActionCommandState it writes
Rotate authority keyPOST /v1/authority on trust-controlTrustAuthorityStatus.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 capability-token authorization revoked by this procedure
  • Trust Control Plane · how to run chio trust serve so revocations propagate cluster-wide