use std::collections::HashMap;
use std::sync::{Mutex, PoisonError};
use chio_core_types::canonical::canonical_json_bytes;
use chio_core_types::crypto::{Ed25519Backend, Keypair, PublicKey, Signature, SigningBackend};
use serde::{Deserialize, Serialize};
pub const FEDERATION_HANDSHAKE_SCHEMA: &str = "chio.federation-kernel-handshake.v1";
pub const DEFAULT_ROTATION_WINDOW_SECS: u64 = 12 * 60 * 60;
pub const DEFAULT_HANDSHAKE_MAX_SKEW_SECS: u64 = 5 * 60;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct FederationPeer {
pub kernel_id: String,
pub public_key: PublicKey,
pub established_at: u64,
pub rotation_due: u64,
}
impl FederationPeer {
pub fn is_fresh(&self, now: u64) -> bool {
now < self.rotation_due
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct HandshakeChallenge {
pub schema: String,
pub local_kernel_id: String,
pub remote_kernel_id: String,
pub nonce: String,
pub timestamp: u64,
}
impl HandshakeChallenge {
pub fn new(
local_kernel_id: impl Into<String>,
remote_kernel_id: impl Into<String>,
nonce: impl Into<String>,
timestamp: u64,
) -> Self {
Self {
schema: FEDERATION_HANDSHAKE_SCHEMA.to_string(),
local_kernel_id: local_kernel_id.into(),
remote_kernel_id: remote_kernel_id.into(),
nonce: nonce.into(),
timestamp,
}
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, PeerHandshakeError> {
canonical_json_bytes(self).map_err(|e| PeerHandshakeError::CanonicalJson(e.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PeerHandshakeEnvelope {
pub challenge: HandshakeChallenge,
pub declared_public_key: PublicKey,
pub signature: Signature,
}
impl PeerHandshakeEnvelope {
pub fn sign(
local_kernel_id: &str,
remote_kernel_id: &str,
nonce: &str,
timestamp: u64,
local_keypair: &Keypair,
) -> Result<Self, PeerHandshakeError> {
let challenge =
HandshakeChallenge::new(local_kernel_id, remote_kernel_id, nonce, timestamp);
let bytes = challenge.canonical_bytes()?;
let backend = Ed25519Backend::new(local_keypair.clone());
let signature = backend
.sign_bytes(&bytes)
.map_err(|e| PeerHandshakeError::SigningFailed(e.to_string()))?;
Ok(Self {
challenge,
declared_public_key: local_keypair.public_key(),
signature,
})
}
pub fn verify_signature(&self) -> Result<(), PeerHandshakeError> {
if self.challenge.schema != FEDERATION_HANDSHAKE_SCHEMA {
return Err(PeerHandshakeError::UnsupportedSchema(
self.challenge.schema.clone(),
));
}
let bytes = self.challenge.canonical_bytes()?;
if !self.declared_public_key.verify(&bytes, &self.signature) {
return Err(PeerHandshakeError::InvalidSignature);
}
Ok(())
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum PeerHandshakeError {
#[error("unsupported handshake schema: {0}")]
UnsupportedSchema(String),
#[error("canonical JSON encoding failed: {0}")]
CanonicalJson(String),
#[error("handshake signing failed: {0}")]
SigningFailed(String),
#[error("remote handshake signature is invalid")]
InvalidSignature,
#[error("remote envelope is addressed to kernel_id {addressed_to} but we are {actual}")]
AddressMismatch {
addressed_to: String,
actual: String,
},
#[error("remote envelope declares self as kernel_id {declared} but we expected {expected}")]
KernelIdMismatch { declared: String, expected: String },
#[error("remote envelope timestamp {envelope} drifts from local clock {local} beyond {skew}s")]
ClockSkewExceeded {
envelope: u64,
local: u64,
skew: u64,
},
#[error("peer {0} is not pinned; run a handshake before resolving")]
PeerNotPinned(String),
#[error("peer {0} is stale and must be re-handshaked before use")]
PeerStale(String),
#[error("peer {0} is not trusted for first contact; configure a trust anchor before accepting handshakes")]
MissingTrustAnchor(String),
#[error("peer {kernel_id} declared unexpected public key; expected {expected}, got {actual}")]
UnexpectedPeerKey {
kernel_id: String,
expected: String,
actual: String,
},
#[error("trust store is poisoned and cannot service requests")]
StorePoisoned,
}
impl<T> From<PoisonError<T>> for PeerHandshakeError {
fn from(_: PoisonError<T>) -> Self {
PeerHandshakeError::StorePoisoned
}
}
pub trait FederationPeerStore: Send + Sync {
fn insert(&self, peer: FederationPeer) -> Result<(), PeerHandshakeError>;
fn get(&self, kernel_id: &str) -> Result<Option<FederationPeer>, PeerHandshakeError>;
fn remove(&self, kernel_id: &str) -> Result<Option<FederationPeer>, PeerHandshakeError>;
fn snapshot(&self) -> Result<Vec<FederationPeer>, PeerHandshakeError>;
}
#[derive(Debug, Default)]
pub struct InMemoryPeerStore {
inner: Mutex<HashMap<String, FederationPeer>>,
}
impl InMemoryPeerStore {
pub fn new() -> Self {
Self::default()
}
}
impl FederationPeerStore for InMemoryPeerStore {
fn insert(&self, peer: FederationPeer) -> Result<(), PeerHandshakeError> {
let mut guard = self.inner.lock()?;
guard.insert(peer.kernel_id.clone(), peer);
Ok(())
}
fn get(&self, kernel_id: &str) -> Result<Option<FederationPeer>, PeerHandshakeError> {
let guard = self.inner.lock()?;
Ok(guard.get(kernel_id).cloned())
}
fn remove(&self, kernel_id: &str) -> Result<Option<FederationPeer>, PeerHandshakeError> {
let mut guard = self.inner.lock()?;
Ok(guard.remove(kernel_id))
}
fn snapshot(&self) -> Result<Vec<FederationPeer>, PeerHandshakeError> {
let guard = self.inner.lock()?;
Ok(guard.values().cloned().collect())
}
}
#[derive(Debug, Clone, Copy)]
pub struct KernelTrustExchangeConfig {
pub rotation_window_secs: u64,
pub max_handshake_skew_secs: u64,
}
impl Default for KernelTrustExchangeConfig {
fn default() -> Self {
Self {
rotation_window_secs: DEFAULT_ROTATION_WINDOW_SECS,
max_handshake_skew_secs: DEFAULT_HANDSHAKE_MAX_SKEW_SECS,
}
}
}
pub struct KernelTrustExchange {
local_kernel_id: String,
local_keypair: Keypair,
config: KernelTrustExchangeConfig,
store: Box<dyn FederationPeerStore>,
trusted_peers: HashMap<String, PublicKey>,
}
impl core::fmt::Debug for KernelTrustExchange {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("KernelTrustExchange")
.field("local_kernel_id", &self.local_kernel_id)
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl KernelTrustExchange {
pub fn new(local_kernel_id: impl Into<String>, local_keypair: Keypair) -> Self {
Self {
local_kernel_id: local_kernel_id.into(),
local_keypair,
config: KernelTrustExchangeConfig::default(),
store: Box::new(InMemoryPeerStore::new()),
trusted_peers: HashMap::new(),
}
}
pub fn with_config(mut self, config: KernelTrustExchangeConfig) -> Self {
self.config = config;
self
}
pub fn with_store(mut self, store: Box<dyn FederationPeerStore>) -> Self {
self.store = store;
self
}
pub fn with_trusted_peer(
mut self,
kernel_id: impl Into<String>,
public_key: PublicKey,
) -> Self {
self.trusted_peers.insert(kernel_id.into(), public_key);
self
}
pub fn local_kernel_id(&self) -> &str {
&self.local_kernel_id
}
pub fn local_public_key(&self) -> PublicKey {
self.local_keypair.public_key()
}
pub fn rotation_window_secs(&self) -> u64 {
self.config.rotation_window_secs
}
pub fn local_envelope(
&self,
remote_kernel_id: &str,
nonce: &str,
now: u64,
) -> Result<PeerHandshakeEnvelope, PeerHandshakeError> {
PeerHandshakeEnvelope::sign(
&self.local_kernel_id,
remote_kernel_id,
nonce,
now,
&self.local_keypair,
)
}
pub fn accept_envelope(
&self,
envelope: &PeerHandshakeEnvelope,
expected_remote_kernel_id: &str,
now: u64,
) -> Result<FederationPeer, PeerHandshakeError> {
envelope.verify_signature()?;
if envelope.challenge.remote_kernel_id != self.local_kernel_id {
return Err(PeerHandshakeError::AddressMismatch {
addressed_to: envelope.challenge.remote_kernel_id.clone(),
actual: self.local_kernel_id.clone(),
});
}
if envelope.challenge.local_kernel_id != expected_remote_kernel_id {
return Err(PeerHandshakeError::KernelIdMismatch {
declared: envelope.challenge.local_kernel_id.clone(),
expected: expected_remote_kernel_id.to_string(),
});
}
let envelope_ts = envelope.challenge.timestamp;
let skew = self.config.max_handshake_skew_secs;
let drift = envelope_ts.abs_diff(now);
if drift > skew {
return Err(PeerHandshakeError::ClockSkewExceeded {
envelope: envelope_ts,
local: now,
skew,
});
}
let pinned_peer = self.store.get(expected_remote_kernel_id)?;
let expected_public_key = self
.trusted_peers
.get(expected_remote_kernel_id)
.cloned()
.or_else(|| pinned_peer.as_ref().map(|peer| peer.public_key.clone()))
.ok_or_else(|| {
PeerHandshakeError::MissingTrustAnchor(expected_remote_kernel_id.to_string())
})?;
if envelope.declared_public_key != expected_public_key {
return Err(PeerHandshakeError::UnexpectedPeerKey {
kernel_id: expected_remote_kernel_id.to_string(),
expected: expected_public_key.to_hex(),
actual: envelope.declared_public_key.to_hex(),
});
}
let peer = FederationPeer {
kernel_id: expected_remote_kernel_id.to_string(),
public_key: envelope.declared_public_key.clone(),
established_at: now,
rotation_due: now.saturating_add(self.config.rotation_window_secs),
};
self.store.insert(peer.clone())?;
Ok(peer)
}
pub fn resolve(&self, kernel_id: &str, now: u64) -> Result<FederationPeer, PeerHandshakeError> {
let Some(peer) = self.store.get(kernel_id)? else {
return Err(PeerHandshakeError::PeerNotPinned(kernel_id.to_string()));
};
if !peer.is_fresh(now) {
return Err(PeerHandshakeError::PeerStale(kernel_id.to_string()));
}
Ok(peer)
}
pub fn forget(&self, kernel_id: &str) -> Result<Option<FederationPeer>, PeerHandshakeError> {
self.store.remove(kernel_id)
}
pub fn peers(&self) -> Result<Vec<FederationPeer>, PeerHandshakeError> {
self.store.snapshot()
}
}