Key transparency
Check which key held signing authority when any artifact was signed, against key history you retain yourself.
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.
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.
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
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.
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:
- Is this the history I expected?
validatecompares 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. - Did the operator sign these bytes?
verify_operatorchecks the signature under the independently pinned operator key, which says nothing about witness agreement or freshness. - Did the required witnesses agree?
verify_witnesseschecks 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
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.
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(())
}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.