Chio/Docs

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.

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 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 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 with the new public key and a rotated_at timestamp.

CLI commands are 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 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.

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

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

rust
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.
    /// Takes &self: implementations use interior mutability.
    fn revoke(&self, capability_id: &str) -> Result<bool, RevocationStoreError>;

    /// Whether this store loses its revocation set on process restart.
    /// The default is the safe (loud) assumption; durable stores (sqlite,
    /// trust-control) override this to false.
    fn is_ephemeral(&self) -> bool {
        true
    }
}

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

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

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.

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(())
}

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. 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.
  • 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 kernel does not allow new authority beneath a revoked parent.
  • Reissuance requires a live ancestor. Reissue from a non-revoked node to restore a subtree. TheRevocationStore trait has no unrevocation operation.

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 a revocation database; nodes that point 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 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; 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. 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 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 (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, revoke the token so the next admission fails; expiration supplements revocation.

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 capability-token authorization revoked by this procedure
  • Trust Control Plane · how to run chio trust serve so revocations propagate cluster-wide