huskies: merge 728_story_cryptographic_peer_handshake_with_trusted_keys_gating

This commit is contained in:
dave
2026-04-27 19:17:05 +00:00
parent ded8c6fd66
commit aa7b26a24a
6 changed files with 640 additions and 139 deletions
+72 -4
View File
@@ -11,7 +11,7 @@ use crate::slog_warn;
use super::auth;
use super::dispatch::{handle_incoming_binary, handle_incoming_text};
use super::wire::{AuthMessage, ChallengeMessage, SyncMessage};
use super::wire::{AuthMessage, ChallengeMessage, HelloMessage, ServerAuthMessage, SyncMessage};
use super::{AUTH_TIMEOUT_SECS, PING_INTERVAL_SECS, PONG_TIMEOUT_SECS};
#[allow(unused_imports)]
@@ -86,11 +86,79 @@ pub(crate) async fn connect_and_sync(url: &str, token: Option<&str>) -> Result<(
let (mut sink, mut stream) = ws_stream.split();
slog!("[crdt-sync] Connected to rendezvous peer, awaiting challenge");
slog!("[crdt-sync] Connected to rendezvous peer, starting mutual-auth handshake");
// ── Step 1: Receive challenge from listener ───────────────────
use tokio_tungstenite::tungstenite::Message as TungsteniteMsg;
// ── Step 1: Send hello with a fresh client nonce ──────────────
let client_nonce = crate::node_identity::generate_challenge();
let hello = HelloMessage {
r#type: "hello".to_string(),
nonce: client_nonce.clone(),
};
let hello_json = serde_json::to_string(&hello).map_err(|e| format!("Serialize hello: {e}"))?;
sink.send(TungsteniteMsg::Text(hello_json.into()))
.await
.map_err(|e| format!("Send hello failed: {e}"))?;
slog!("[crdt-sync] Hello sent, awaiting server_auth");
// ── Step 2: Receive server_auth from the responding node ──────
let server_auth_frame = tokio::time::timeout(
std::time::Duration::from_secs(AUTH_TIMEOUT_SECS),
stream.next(),
)
.await
.map_err(|_| "Auth timeout waiting for server_auth".to_string())?
.ok_or_else(|| "Connection closed before server_auth".to_string())?
.map_err(|e| format!("WebSocket read error: {e}"))?;
let server_auth_text = match server_auth_frame {
TungsteniteMsg::Text(t) => t.to_string(),
_ => return Err("Expected text frame for server_auth".to_string()),
};
let server_auth: ServerAuthMessage = serde_json::from_str(&server_auth_text)
.map_err(|e| format!("Invalid server_auth message: {e}"))?;
if server_auth.r#type != "server_auth" {
return Err(format!(
"Expected server_auth message, got type={}",
server_auth.r#type
));
}
// ── Step 3: Verify server's signature and check trusted-key list ─
let versioned_challenge = format!("huskies-v1:{client_nonce}");
let server_sig_valid = crate::node_identity::verify_message_strict(
&server_auth.pubkey_hex,
versioned_challenge.as_bytes(),
&server_auth.signature_hex,
);
let server_key_trusted = auth::trusted_keys()
.iter()
.any(|k| k == &server_auth.pubkey_hex);
if !server_sig_valid || !server_key_trusted {
slog!(
"[crdt-sync] Server auth failed \
(sig_valid={server_sig_valid}, key_trusted={server_key_trusted}, \
server_pubkey={})",
server_auth.pubkey_hex
);
return Err(format!(
"Server auth rejected: sig_valid={server_sig_valid}, \
key_trusted={server_key_trusted}, server_pubkey={}",
server_auth.pubkey_hex
));
}
slog!(
"[crdt-sync] Server authenticated: {:.12}…",
&server_auth.pubkey_hex
);
// ── Step 4: Receive challenge from the responding node ────────
let challenge_frame = tokio::time::timeout(
std::time::Duration::from_secs(AUTH_TIMEOUT_SECS),
stream.next(),
@@ -115,7 +183,7 @@ pub(crate) async fn connect_and_sync(url: &str, token: Option<&str>) -> Result<(
));
}
// ── Step 2: Sign challenge and send auth reply ────────────────
// ── Step 5: Sign challenge and send auth reply ────────────────
let (pubkey_hex, signature_hex) = crdt_state::sign_challenge(&challenge_msg.nonce)
.ok_or_else(|| "CRDT not initialised — cannot sign challenge".to_string())?;