Peer Handshake
Prove that a partner's kernel holds the key its card pinned before your kernel records it as a peer.
On this page
Your kernel records a partner as a peer only after the partner's kernel signs a fresh challenge with the key its card pinned. The handshake runs that proof once in each direction and leaves a peer record with a conformance tier and a rotation deadline.
Adding a partner card with chio partner add pins the partner's kernel key. The handshake confirms it when the relationship opens and again before every rotation deadline, and a downloadable Rust program below runs one accepted handshake and three refusals.
The handshake answers to the pin
Each kernel signs a challenge that binds both kernel ids, a fresh nonce, and the time. The receiver verifies the response against the pinned key, then records the peer's conformance tier and its rotation deadline beside the pin. Peer handshake.
Each kernel signs a challenge the other checks against its pin
Each side builds a challenge under the schema chio.federation-kernel-handshake.v1 that binds its own kernel id, the remote kernel id, a fresh nonce, and the time, then signs it with its kernel key. The envelope it sends carries the challenge, the signature, and the declared public key.
- Signature. The signature must verify under the declared key, over the canonical JSON of the challenge.
- Address. The envelope must be addressed to this kernel, and its declared kernel id must be the peer the receiver expected.
- Freshness. The timestamp must sit within the accepted clock skew, five minutes by default.
- Pin. The declared key must match the trust anchor the partner card supplied or a peer already pinned. A different key stops with
UnexpectedPeerKey, before any peer state is written. - Terms. The peer's conformance tier must meet the receiver's floor, and both sides must share a protocol capability set. An optional signed ladder manifest reference names the governance ladder the peer enforces.
A handshake that passes every step records the peer as a FederationPeer: its kernel id, public key, accepted conformance tier, negotiated capabilities, ladder manifest reference, and rotation deadline. Keep that record with the pin; every later exchange with the partner resolves through it. chio partner status prints the record your kernel holds:
chio partner status partnerpartner did:chio:z6MkpA7…c2T, named partner kernel 9b07…d31c, pinned from partner-card.json handshake verified both ways, chio.federation-kernel-handshake.v1 tier silver, your floor bronze capabilities signals · revocation · co-sign ladder manifest 040f…bc01, signed by the partner rotation deadline in 11 h 52 min
Read more in Operator Identity & Key Bindings, which separates the kernel id, passport key, and endpoint the handshake and transport bind.
A pin lasts until its rotation deadline, and only a new handshake renews it
Every accepted handshake sets the peer's rotation deadline from the configured freshness window, twelve hours by default. After the deadline, resolving the peer fails closed with PeerStale until the two kernels run the handshake again. Nothing renews a pin silently. Run it before the deadline, and the record keeps its key and moves its deadline:
chio partner handshake partnerhandshake verified both ways with did:chio:z6MkpA7…c2T key 9b07…d31c, matches the pin tier silver, your floor bronze rotation deadline in 12 h, renewed
The handshake re-proves a key; it never introduces one. When the partner rotates its authority key, the new key reaches your trust store through a new card or the authority key history the card carries, and receipts signed under previous keys still verify offline.
Read more in Peer Pins & Trust Bundles, which covers key persistence and rotation.
The handshake carries one conformance tier derived from the peer's evidence
The signed challenge carries the peer's conformance tier. ConformanceEvidence derives exactly one tier from three measurements: threat coverage and mutation kill rate in basis points, and the count of crates with Kani trust-boundary harnesses. The receiver compares the tier with its QuorumPolicy.min_tier floor, which defaults to Bronze.
- Bronze. Schema-valid evidence is present, below the Silver thresholds.
- Silver. Threat coverage of at least 90 percent, mutation kill of at least 65 percent, and at least 4 crates with Kani trust-boundary harnesses.
- Gold. Threat coverage of 100 percent, mutation kill of at least 80 percent, and at least 8 crates with Kani trust-boundary harnesses.
These tiers describe a peer's published assurance evidence. Runtime Assurance Tiers are a separate ladder that a cluster operator assigns to its own nodes, and the two ladders are not interchangeable.
Read more in Federation Assurance, which covers the evidence behind each tier.
The prepared example pins one peer and refuses three handshakes
Establish a pinned peer relationship
Create two operator identities, verify the expected peer, and consume a receiver-owned challenge before using the relationship.
Complete the project setup, then run the program from federation-guide/. The download includes every constructor and input.
cargo run --locked --bin handshakepinned peer: accepted; challenge consumed same challenge: refused by receiving host same identity with another key: refused expired handshake: refused
The transport or host owns atomic challenge consumption and persistence. This local process demonstrates that boundary with an in-memory set; it does not provide a shared replay database.
Read the complete program 39 lines · complete file
use chio_core_types::Keypair;
use chio_federation::trust_establishment::KernelTrustExchange;
use std::{
collections::HashSet,
time::{SystemTime, UNIX_EPOCH},
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let alice_key = Keypair::generate();
let bob_key = Keypair::generate();
// Exchange these public keys through an authenticated operator channel.
let alice = KernelTrustExchange::new("operator.alice", alice_key.clone())
.with_trusted_peer("operator.bob", bob_key.public_key());
let bob = KernelTrustExchange::new("operator.bob", bob_key)
.with_trusted_peer("operator.alice", alice_key.public_key());
// A fresh challenge is owned by the receiving host, not chosen by the peer.
let challenge = Keypair::generate().public_key().to_hex();
let mut outstanding = HashSet::from([challenge.clone()]);
let envelope = alice.local_envelope("operator.bob", &challenge, now)?;
assert!(outstanding.contains(&envelope.challenge.nonce));
let peer = bob.accept_envelope(&envelope, "operator.alice", now)?;
assert!(peer.is_fresh(now));
assert!(outstanding.remove(&envelope.challenge.nonce));
println!("pinned peer: accepted; challenge consumed");
// The exchange validates signatures; this host supplies the replay boundary.
assert!(!outstanding.remove(&envelope.challenge.nonce));
println!("same challenge: refused by receiving host");
let impostor = KernelTrustExchange::new("operator.alice", Keypair::generate());
let forged = impostor.local_envelope("operator.bob", "new-challenge", now)?;
assert!(bob.accept_envelope(&forged, "operator.alice", now).is_err());
println!("same identity with another key: refused");
assert!(bob
.accept_envelope(&envelope, "operator.alice", now + 301)
.is_err());
println!("expired handshake: refused");
std::fs::write("handshake.json", serde_json::to_vec_pretty(&envelope)?)?;
Ok(())
}Open the source file. Each program asserts its expected refusals; keys, timestamps, and digests change between runs.
You should see four lines. The first pins the peer and consumes its challenge. The next three are refusals: the same challenge presented again, the same kernel id under another key, and a handshake past its freshness window.
Every handshake error fails closed and leaves the peer unpinned
A PeerHandshakeError names the step that failed. PeerNotPinned and PeerStale come later, when the relationship is used: the first before any handshake, the second after the rotation deadline.
| Variant | Condition | Repair |
|---|---|---|
UnsupportedSchema | The envelope names a schema this build rejects | Agree a schema version with the peer |
CanonicalJson | The envelope did not canonicalize | Re-sign from the canonical body |
SigningFailed | The local key could not sign | Check the local seed and key custody |
InvalidSignature | The signature fails under the declared key | Re-establish the expected key over an authenticated channel |
CapabilityNegotiation | No protocol capability set both sides accept | Enable a shared capability set |
AddressMismatch | The envelope is addressed to another kernel | Send the challenge to the intended peer |
KernelIdMismatch | The declared kernel id is not the expected one | Pin the peer you meant |
ClockSkewExceeded | The timestamp is outside the accepted skew | Synchronize clocks, then retry |
PeerNotPinned | The peer has no pinned entry on this side | Complete the handshake before use |
PeerStale | The pin passed its rotation deadline | Re-handshake the peer |
MissingTrustAnchor | No trust anchor exists for first contact | Add the partner's card first |
UnexpectedPeerKey | The peer declared a key other than the pinned one | Re-establish the key over an authenticated channel |
ConformanceTierBelowMinimum | The derived tier is below the quorum policy floor | Lower the floor deliberately, or wait for evidence |
InvalidConformanceEvidence | The conformance evidence fails validation | Have the peer republish valid evidence |
InvalidLadderManifestRef | The ladder manifest reference is malformed or expired | Have the peer sign and reference a current manifest |
StorePoisoned | The local trust store is poisoned | Restore the store from its durable backing |
Treat UnexpectedPeerKey as a question for the partner's operator, never as a key to adopt.
Read more in Federation Refusal Codes, which indexes every federation code.
The transport or host consumes each challenge exactly once
The nonce makes every envelope unique, and the signature binds it. Rejecting a re-presented envelope inside the freshness window belongs to the layer in front of the exchange: mutual TLS on the default HTTPS transport, or a replay store that the receiving host keeps across worker restarts. The rotation deadline caps how long any pin stays usable.
A host embedding the exchange supplies a durable FederationPeerStore; the default InMemoryPeerStore lasts for one process, as does the example's set of consumed nonces.
Source
KernelTrustExchange, FederationPeer, ConformanceTier, ConformanceEvidence, and PeerHandshakeError live in crates/trust/chio-federation/src/trust_establishment.rs in the Chio repository, with the defaults DEFAULT_ROTATION_WINDOW_SECS and DEFAULT_HANDSHAKE_MAX_SKEW_SECS.