Chio/Docs
LOGIN · JOIN

Key transparency

Check which key held signing authority when any artifact was signed, against key history you retain yourself.

Concept chio@5f63e6e

On this page

A verifier can establish which key held an authority's signing power at the moment an artifact was signed, long after that key has rotated out. Chio records every key change in a durable key log, commits the log in operator checkpoints that independent witnesses sign, and makes a change usable only through an activation commit.

Each artifact carries the signing epoch it was produced in, and verification follows the key history from a view the verifier already holds to that epoch.

Key lifecycleChoose a stage.
key historykey logwitnessesreceiptscandidateσ₁σ₂4f1c…a2e9epoch 1epoch 29b07…c41dauthorizesproves possessionerotation eventccheckpointw₁w₂w₃witnesses · 2 of 3 signv₁v₂auditors · both confirmactivationevent + checkpoint + epoch 2verifiesverifiesρ₁signed in epoch 1ρ₂signed in epoch 2keyskey logsignersreceiptscandidateσ₁σ₂4f1c…a2e9epoch 1epoch 29b07…c41dauthorizeseeventccheckpointw₁w₂w₃witnesses2 of 3 signv₁v₂auditorsboth confirmactivationverifiesverifiesρ₁epoch 1ρ₂epoch 2

Old receipts verify against the key that signed them

A verifier carries the authority key history, from the partner card or its own pinned copy. ρ₁, signed in epoch 1, verifies against σ₁ after the rotation, and ρ₂ verifies against σ₂. Historical receipts keep their original bytes and signatures. Verify security evidence.

One authority's key history on a time axis: a rotation from σ₁ to σ₂ through the key log, three witnesses, and two auditors. Times and fingerprints are illustrative; artifact-time roots, recovery roots, and equivocation evidence are left out.Witnessed rotation

Old evidence verifies against the epoch that signed it

Long-lived evidence outlasts the key that signed it. A verifier reading an old receipt needs the key that was authorized then; a serving process needs the key that may sign now. A current public-key endpoint answers only the second question.

The retained key history answers the first: signed key events, checkpoints, witness signatures, activation commits, trusted time anchors, and rotation receipts form one record. Every transition is an explicit event under a checkpoint, and an artifact's signing epoch names the activation it falls under. A partner publishes its authority key history on its partner card, and a verifier can also hold its own pinned copy. A host's wall clock cannot place an old artifact on that timeline; the trusted artifact-time evidence, the signing epoch, and the activation history do.

A key changes in two stages. The old key authorizes a proposal that the new key proves it holds, and the change activates only after a strict majority of witnesses signs the checkpoint containing it and both auditors confirm. Receipts signed under the old key keep their original bytes and signatures throughout.

Read more in Witnessed rotation and Signing authority.

A checkpoint commits the log to one position and one root

The operator signs a checkpoint naming the log, its sequence number, the tree size, the Merkle root, the previous checkpoint's hash, and the issue time. Each witness checks the operator signature and the synchronization against its own retained view before it signs those same bytes.

The checkpoint body in Rust
crates/security/chio-keyring/src/checkpoint.rs25–34rust
pub struct KeyLogCheckpointBody {
    pub schema: String,
    pub log_id: LogId,
    pub checkpoint_sequence: u64,
    pub tree_size: u64,
    pub root_hash: Hash,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub previous_checkpoint_hash: Option<Hash>,
    pub issued_at: u64,
}

A verifier confirms that the required independent signatures cover the same checkpoint hash; signatures attached to unrelated roots add nothing, and witnesses holding their own views catch a service that shows different histories to different consumers. The operator store holds a lifetime operating-system lock for its single writer, and read-only auditors open an observer view without that writer fence, so extending history and checking it stay separate roles.

A signed checkpoint is current only against the view you retained

A valid signature establishes who signed a checkpoint's bytes. Whether it is the newest acceptable view, and whether it extends the history you accepted before, depends on the trusted roots and checkpoint you retained yourself. An old valid prefix looks cryptographically sound in isolation.

crates/security/chio-keyring/src/checkpoint.rs61–68rust
pub struct KeyLogCheckpointExpectation<'a> {
    pub log_id: &'a LogId,
    pub sequence: u64,
    pub tree_size: u64,
    pub root: &'a Hash,
    pub previous_checkpoint_hash: Option<&'a Hash>,
    pub last_issued_at: Option<u64>,
}

Checkpoint validation takes explicit expectations. Populate them from your independently established view and its verified extension, then run three checks that answer three questions:

  1. Is this the history I expected? validate compares the log identity, sequence, tree size, root, predecessor, and last accepted issue time. The tree extension or inclusion evidence that justified those expectations is checked on its own.
  2. Did the operator sign these bytes? verify_operator checks the signature under the independently pinned operator key, which says nothing about witness agreement or freshness.
  3. Did the required witnesses agree? verify_witnesses checks signatures under the policy-owned witness map. Independent service topology, auditor confirmation, and the exact activation history remain to confirm.

Count independence as well as signatures: two signatures from aliased roots are one observation. The event's inclusion evidence and the witness signatures must both refer to the activated checkpoint, because individually valid artifacts from different log positions never combine into an activation.

Activation binds one witnessed event to a signing epoch

The activation commit names the key event, its checkpoint hash and body hash, the sequence, tree size, root, the event's leaf hash, the witness set and its signatures, the commit time, and the new signing epoch. The witness quorum, both auditors, and the final activation guard pass before the commit is written, and serving processes use a key only after it; a proposed key can sit in witnessed history with its activation still pending.

The activation commit in Rust
crates/security/chio-keyring/src/checkpoint.rs171–186rust
pub struct KeyActivationCommitBody {
    pub schema: String,
    pub log_id: LogId,
    pub event_id: crate::EventId,
    pub checkpoint_hash: Hash,
    pub checkpoint_body_hash: Hash,
    pub checkpoint_sequence: u64,
    pub tree_size: u64,
    pub root_hash: Hash,
    pub event_leaf_hash: Hash,
    pub witness_set_hash: Hash,
    #[serde(deserialize_with = "deserialize_witness_signatures")]
    pub witness_signatures: Vec<WitnessSignature>,
    pub committed_at: u64,
    pub signing_epoch: u64,
}

Checkpoints are signed by three witness services, each with its own identity, signing key, and storage, while two audit monitors compare what each witness saw. Two of the three witnesses are enough to sign a checkpoint. Losing either auditor blocks rotation activation and production readiness while checkpoint signing continues, and because startup needs signed readiness from all five witness and audit services, losing one witness also blocks a trust-control restart.

Read more in Key log witnesses, which runs the services and detects split views.

The checkpoint comparison accepts your view and refuses a substituted root

The comparison builds a populated two-event fixture with an independently retained predecessor and root, an independent operator pin, and the checkpoint API. Call compare_checkpoint_history() from a Rust host that depends on chio-keyring and chio-core-types, or download checkpoint_cases.rs.

security-host/checkpoint_cases.rsrust
use chio_core_types::{sha256, Ed25519Backend, Hash, Keypair};
use chio_keyring::{
    KeyLogCheckpointBody, KeyLogCheckpointExpectation, LogId, SignedKeyLogCheckpoint,
    KEY_LOG_CHECKPOINT_SCHEMA,
};

fn require_expected_history(
    checkpoint: &SignedKeyLogCheckpoint,
    log: &LogId,
    root: &Hash,
    previous: &Hash,
) -> chio_keyring::Result<()> {
    checkpoint.validate(KeyLogCheckpointExpectation {
        log_id: log,
        sequence: 2,
        tree_size: 2,
        root,
        previous_checkpoint_hash: Some(previous),
        last_issued_at: Some(1_800_000_000),
    })
}

/// A populated structural/signature fixture. The root stands for a verifier's
/// already verified tree; this example does not compute inclusion/consistency.
pub fn compare_checkpoint_history() -> Result<(), Box<dyn std::error::Error>> {
    let operator = Keypair::generate();
    let independent_operator_pin = operator.public_key();
    let log = LogId::new("larkfield-authority")?;
    let root = sha256(b"fixture: independently verified two-event tree");
    let previous = sha256(b"fixture: retained predecessor checkpoint");
    let checkpoint = SignedKeyLogCheckpoint::sign(
        KeyLogCheckpointBody {
            schema: KEY_LOG_CHECKPOINT_SCHEMA.into(),
            log_id: log.clone(),
            checkpoint_sequence: 2,
            tree_size: 2,
            root_hash: root,
            previous_checkpoint_hash: Some(previous),
            issued_at: 1_800_000_001,
        },
        &Ed25519Backend::new(operator),
    )?;
    require_expected_history(&checkpoint, &log, &root, &previous)?;
    checkpoint.verify_operator(&independent_operator_pin)?;
    println!("VERIFY expected populated history and independently pinned operator");
    let changed_root = sha256(b"fixture: substituted tree");
    assert!(require_expected_history(&checkpoint, &log, &changed_root, &previous).is_err());
    let mut old = checkpoint.clone();
    old.body.issued_at = 1_799_999_999;
    assert!(require_expected_history(&old, &log, &root, &previous).is_err());
    assert!(old.verify_operator(&independent_operator_pin).is_err());
    println!("REFUSE changed expected root and backward issue time; no self-pinning");
    Ok(())
}
Output
VERIFY expected populated history and independently pinned operator
REFUSE changed expected root and backward issue time; no self-pinning

You should see the expected history accepted, then refused under a different expected root and under an issue time before the last accepted one. The root and predecessor are fixtures standing for an already verified tree; the program computes no Merkle tree and proves no inclusion or consistency. A serving verifier takes those expectations from its retained view and a verified extension, never by copying the candidate's own fields back in.

A substituted root and two conflicting signed views are different failures. Only the second is equivocation evidence, and only a newer retained head shows that a valid old prefix is stale. The witness quorum comparison covers unique signers and same-root binding.

Trust control serves keyring custody as a single-node profile

The trust-control profile selects keyring custody with --authority-keyring-config, a persistent authority seed, a durable receipt store, and a distinct authority-workload token. The runtime verifies the complete topology before it opens its HTTP listener. The profile refuses --authority-db and cluster peers, because the keyring's selector lease sits outside the trust-control cluster's consensus.

Once loaded, the keyring owns the authority seed and supplies a generation-fenced signing backend. A route that would need a raw seed refuses the operation in place of signing directly. Administrative log synchronization uses POST /v1/authority/key-log/sync with the configured service authentication.

The operator, witnesses, auditors, and active signing authority run as separate services, so no service attests to its own observation.

Next: rotate a key through the log