From 2d831055009661ae4056dbd8fc459dbc6299119e Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sat, 20 Sep 2025 15:46:48 -0700 Subject: [PATCH] clean up --- bin/periphery/src/connection/client.rs | 2 +- bin/periphery/src/connection/server.rs | 2 +- client/periphery/rs/src/connection/client.rs | 2 +- client/periphery/rs/src/connection/server.rs | 2 +- lib/transport/src/auth.rs | 208 +++++++++++++------ lib/transport/src/auth2.rs | 184 ---------------- lib/transport/src/bytes.rs | 2 +- lib/transport/src/lib.rs | 1 - 8 files changed, 155 insertions(+), 248 deletions(-) delete mode 100644 lib/transport/src/auth2.rs diff --git a/bin/periphery/src/connection/client.rs b/bin/periphery/src/connection/client.rs index c8f664d75..172e31c4e 100644 --- a/bin/periphery/src/connection/client.rs +++ b/bin/periphery/src/connection/client.rs @@ -4,7 +4,7 @@ use anyhow::Context; use axum::http::HeaderValue; use bytes::Bytes; use transport::{ - auth2::{ConnectionIdentifiers, handle_client_side_login}, + auth::{ConnectionIdentifiers, handle_client_side_login}, fix_ws_address, websocket::{ WebsocketMessage, WebsocketReceiver, WebsocketSender, diff --git a/bin/periphery/src/connection/server.rs b/bin/periphery/src/connection/server.rs index b4ccd812f..caa7a7933 100644 --- a/bin/periphery/src/connection/server.rs +++ b/bin/periphery/src/connection/server.rs @@ -17,7 +17,7 @@ use axum_server::tls_rustls::RustlsConfig; use bytes::Bytes; use serror::{AddStatusCode, AddStatusCodeError}; use transport::{ - auth2::{ + auth::{ ConnectionIdentifiers, compute_accept, handle_server_side_login, }, websocket::{ diff --git a/client/periphery/rs/src/connection/client.rs b/client/periphery/rs/src/connection/client.rs index 8bdcacb16..f07c4f6a7 100644 --- a/client/periphery/rs/src/connection/client.rs +++ b/client/periphery/rs/src/connection/client.rs @@ -8,7 +8,7 @@ use rustls::{ClientConfig, client::danger::ServerCertVerifier}; use tokio_tungstenite::Connector; use tracing::{info, warn}; use transport::{ - auth2::{ConnectionIdentifiers, handle_client_side_login}, + auth::{ConnectionIdentifiers, handle_client_side_login}, fix_ws_address, websocket::{ WebsocketMessage, WebsocketReceiver as _, WebsocketSender as _, diff --git a/client/periphery/rs/src/connection/server.rs b/client/periphery/rs/src/connection/server.rs index d01b39bc1..005ac550b 100644 --- a/client/periphery/rs/src/connection/server.rs +++ b/client/periphery/rs/src/connection/server.rs @@ -8,7 +8,7 @@ use bytes::Bytes; use serror::AddStatusCode; use tracing::{error, info, warn}; use transport::{ - auth2::{ + auth::{ ConnectionIdentifiers, compute_accept, handle_server_side_login, }, websocket::{ diff --git a/lib/transport/src/auth.rs b/lib/transport/src/auth.rs index eddadc2ef..ec7da4c17 100644 --- a/lib/transport/src/auth.rs +++ b/lib/transport/src/auth.rs @@ -1,88 +1,154 @@ -use anyhow::{Context, anyhow}; +//! Implementes both sides of Noise handshake +//! using asymmetric private-public key authentication. +//! +//! TODO: Revisit +//! Note. Relies on Server being behind trusted TLS connection. +//! This is trivial for Periphery -> Core connection, but presents a challenge +//! for Core -> Periphery, where untrusted TLS certs are being used. + +use anyhow::Context; +use base64::{Engine, prelude::BASE64_STANDARD}; use bytes::Bytes; use rand::RngCore; -use tracing::{info, warn}; +use sha2::{Digest, Sha256}; -use crate::{ - MessageState, - websocket::{Websocket, WebsocketMessage}, +use crate::websocket::{ + Websocket, axum::AxumWebsocket, tungstenite::TungsteniteWebsocket, }; +pub struct ConnectionIdentifiers<'a> { + /// Server hostname + pub host: &'a [u8], + /// Query: 'server=' + pub query: &'a [u8], + /// Sec-Websocket-Accept, unique for each connection + pub accept: &'a [u8], +} + #[derive(Debug, Clone, Copy)] pub enum AuthType { Passkey = 0, Noise = 1, } +const NOISE_XX_PARAMS: &str = "Noise_XX_25519_ChaChaPoly_BLAKE2s"; + pub async fn handle_server_side_login( - socket: &mut impl Websocket, - validate_credentials: impl Fn(&[u8]) -> bool, + socket: &mut AxumWebsocket, + id: ConnectionIdentifiers<'_>, + private_key: &[u8], ) -> anyhow::Result<()> { // Server generates random nonce and sends to client let nonce = nonce(); + socket + .send(Bytes::from_owner(nonce)) + .await + .context("Failed to send connection nonce")?; - socket.send(Bytes::copy_from_slice(&nonce)).await?; + // Build the handshake using the unique prologue hash. + // The prologue must be the same on both sides of connection. + let mut handshake = snow::Builder::new(NOISE_XX_PARAMS.parse()?) + .local_private_key(private_key)? + .prologue(&id.hash(&nonce))? + .build_responder()?; - let credentials = match socket.recv().await? { - WebsocketMessage::Binary(bytes) => bytes, - WebsocketMessage::Close(frame) => { - return Err(anyhow!( - "Websocket close frame received during login | frame: {frame:?}" - )); - } - WebsocketMessage::Closed => { - return Err(anyhow!("Websocket closed during login")); - } - }; - if validate_credentials(&credentials) { - // Send login confirmation - // TODO: remove / edit logs - info!("Client logged in"); - socket.send(MessageState::Successful.into()).await?; - return Ok(()); - } else { - // Send login failure - warn!("Client failed to log in"); - socket.send(MessageState::Failed.into()).await?; - let _ = socket.close(None).await; - return Err(anyhow!("Received invalid credentials")); - } + // Receive and read handshake_m1 + let handshake_m1 = socket + .recv_bytes() + .await + .context("Failed to get handshake_m1")?; + handshake + .read_message(&handshake_m1, &mut []) + .context("Failed to read handshake_m1")?; + + // Send handshake_m2 + let mut handshake_m2 = [0u8; 1024]; + let written = handshake + .write_message(&[], &mut handshake_m2) + .context("Failed to write handshake_m2")?; + socket + .send(Bytes::copy_from_slice(&handshake_m2[..written])) + .await + .context("Failed to send handshake_m2")?; + + // Receive and read handshake_m3 + let handshake_m3 = socket + .recv_bytes() + .await + .context("Failed to get handshake_m3")?; + handshake + .read_message(&handshake_m3, &mut []) + .context("Failed to read handshake_m3")?; + + // Server now has client public key + let client_public_key = handshake + .get_remote_static() + .context("Failed to get remote public key")?; + println!( + "Server got client public key: {}", + BASE64_STANDARD.encode(client_public_key) + ); + + Ok(()) } pub async fn handle_client_side_login( - socket: &mut impl Websocket, - credentials: Bytes, + socket: &mut TungsteniteWebsocket, + id: ConnectionIdentifiers<'_>, + private_key: &[u8], ) -> anyhow::Result<()> { + // Receive nonce from server let nonce = socket .recv_bytes() .await .context("Failed to receive connection nonce")?; - socket - .send(credentials) - .await - .context("Failed to send login credentials")?; + // Build the handshake using the unique prologue hash. + // The prologue must be the same on both sides of connection. + let mut handshake = snow::Builder::new(NOISE_XX_PARAMS.parse()?) + .local_private_key(private_key)? + .prologue(&id.hash(&nonce))? + .build_initiator()?; - let response = match socket.recv().await? { - WebsocketMessage::Binary(bytes) => bytes, - WebsocketMessage::Close(frame) => { - return Err(anyhow!( - "Websocket close frame received during login | frame: {frame:?}" - )); - } - WebsocketMessage::Closed => { - return Err(anyhow!("Websocket closed during login")); - } - }; - let state = response - .first() - .map(|b| MessageState::from_byte(*b)) - .context("Login response is empty")?; - if matches!(state, MessageState::Successful) { - return Ok(()); - } else { - return Err(anyhow!("Failed to login | Invalid credentails")); - } + // Send handshake_m1 + let mut handshake_m1 = [0u8; 1024]; + let written = handshake + .write_message(&[], &mut handshake_m1) + .context("Failed to write handshake_m1")?; + socket + .send(Bytes::copy_from_slice(&handshake_m1[..written])) + .await + .context("Failed to send handshake_m1")?; + + // Receive and read handshake_m2 + let handshake_m2 = socket + .recv_bytes() + .await + .context("Failed to get handshake_m2")?; + handshake + .read_message(&handshake_m2, &mut []) + .context("Failed to read handshake_m2")?; + + // Client now has server public key + let server_public_key = handshake + .get_remote_static() + .context("Failed to get remote public key")?; + println!( + "Client got server public key: {}", + BASE64_STANDARD.encode(server_public_key) + ); + + // Send handshake_m3 + let mut handshake_m3 = [0u8; 1024]; + let written = handshake + .write_message(&[], &mut handshake_m3) + .context("Failed to write handshake_m3")?; + socket + .send(Bytes::copy_from_slice(&handshake_m3[..written])) + .await + .context("Failed to send handshake_m3")?; + + Ok(()) } fn nonce() -> [u8; 32] { @@ -90,3 +156,29 @@ fn nonce() -> [u8; 32] { rand::rng().fill_bytes(&mut out); out } + +impl ConnectionIdentifiers<'_> { + /// nonce: Server computed random connection nonce, sent to client before auth handshake + pub fn hash(&self, nonce: &[u8]) -> [u8; 32] { + let mut hash = Sha256::new(); + hash.update(b"noise-wss-v1|"); + hash.update(self.host); + hash.update(b"|"); + hash.update(self.query); + hash.update(b"|"); + hash.update(self.accept); + hash.update(b"|"); + hash.update(nonce); + hash.finalize().into() + } +} + +pub fn compute_accept(sec_websocket_key: &[u8]) -> String { + // This is standard GUID to compute Sec-Websocket-Accept + const GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + let mut sha1 = sha1::Sha1::new(); + sha1.update(sec_websocket_key); + sha1.update(GUID); + let digest = sha1.finalize(); + BASE64_STANDARD.encode(digest) +} diff --git a/lib/transport/src/auth2.rs b/lib/transport/src/auth2.rs deleted file mode 100644 index ec7da4c17..000000000 --- a/lib/transport/src/auth2.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Implementes both sides of Noise handshake -//! using asymmetric private-public key authentication. -//! -//! TODO: Revisit -//! Note. Relies on Server being behind trusted TLS connection. -//! This is trivial for Periphery -> Core connection, but presents a challenge -//! for Core -> Periphery, where untrusted TLS certs are being used. - -use anyhow::Context; -use base64::{Engine, prelude::BASE64_STANDARD}; -use bytes::Bytes; -use rand::RngCore; -use sha2::{Digest, Sha256}; - -use crate::websocket::{ - Websocket, axum::AxumWebsocket, tungstenite::TungsteniteWebsocket, -}; - -pub struct ConnectionIdentifiers<'a> { - /// Server hostname - pub host: &'a [u8], - /// Query: 'server=' - pub query: &'a [u8], - /// Sec-Websocket-Accept, unique for each connection - pub accept: &'a [u8], -} - -#[derive(Debug, Clone, Copy)] -pub enum AuthType { - Passkey = 0, - Noise = 1, -} - -const NOISE_XX_PARAMS: &str = "Noise_XX_25519_ChaChaPoly_BLAKE2s"; - -pub async fn handle_server_side_login( - socket: &mut AxumWebsocket, - id: ConnectionIdentifiers<'_>, - private_key: &[u8], -) -> anyhow::Result<()> { - // Server generates random nonce and sends to client - let nonce = nonce(); - socket - .send(Bytes::from_owner(nonce)) - .await - .context("Failed to send connection nonce")?; - - // Build the handshake using the unique prologue hash. - // The prologue must be the same on both sides of connection. - let mut handshake = snow::Builder::new(NOISE_XX_PARAMS.parse()?) - .local_private_key(private_key)? - .prologue(&id.hash(&nonce))? - .build_responder()?; - - // Receive and read handshake_m1 - let handshake_m1 = socket - .recv_bytes() - .await - .context("Failed to get handshake_m1")?; - handshake - .read_message(&handshake_m1, &mut []) - .context("Failed to read handshake_m1")?; - - // Send handshake_m2 - let mut handshake_m2 = [0u8; 1024]; - let written = handshake - .write_message(&[], &mut handshake_m2) - .context("Failed to write handshake_m2")?; - socket - .send(Bytes::copy_from_slice(&handshake_m2[..written])) - .await - .context("Failed to send handshake_m2")?; - - // Receive and read handshake_m3 - let handshake_m3 = socket - .recv_bytes() - .await - .context("Failed to get handshake_m3")?; - handshake - .read_message(&handshake_m3, &mut []) - .context("Failed to read handshake_m3")?; - - // Server now has client public key - let client_public_key = handshake - .get_remote_static() - .context("Failed to get remote public key")?; - println!( - "Server got client public key: {}", - BASE64_STANDARD.encode(client_public_key) - ); - - Ok(()) -} - -pub async fn handle_client_side_login( - socket: &mut TungsteniteWebsocket, - id: ConnectionIdentifiers<'_>, - private_key: &[u8], -) -> anyhow::Result<()> { - // Receive nonce from server - let nonce = socket - .recv_bytes() - .await - .context("Failed to receive connection nonce")?; - - // Build the handshake using the unique prologue hash. - // The prologue must be the same on both sides of connection. - let mut handshake = snow::Builder::new(NOISE_XX_PARAMS.parse()?) - .local_private_key(private_key)? - .prologue(&id.hash(&nonce))? - .build_initiator()?; - - // Send handshake_m1 - let mut handshake_m1 = [0u8; 1024]; - let written = handshake - .write_message(&[], &mut handshake_m1) - .context("Failed to write handshake_m1")?; - socket - .send(Bytes::copy_from_slice(&handshake_m1[..written])) - .await - .context("Failed to send handshake_m1")?; - - // Receive and read handshake_m2 - let handshake_m2 = socket - .recv_bytes() - .await - .context("Failed to get handshake_m2")?; - handshake - .read_message(&handshake_m2, &mut []) - .context("Failed to read handshake_m2")?; - - // Client now has server public key - let server_public_key = handshake - .get_remote_static() - .context("Failed to get remote public key")?; - println!( - "Client got server public key: {}", - BASE64_STANDARD.encode(server_public_key) - ); - - // Send handshake_m3 - let mut handshake_m3 = [0u8; 1024]; - let written = handshake - .write_message(&[], &mut handshake_m3) - .context("Failed to write handshake_m3")?; - socket - .send(Bytes::copy_from_slice(&handshake_m3[..written])) - .await - .context("Failed to send handshake_m3")?; - - Ok(()) -} - -fn nonce() -> [u8; 32] { - let mut out = [0u8; 32]; - rand::rng().fill_bytes(&mut out); - out -} - -impl ConnectionIdentifiers<'_> { - /// nonce: Server computed random connection nonce, sent to client before auth handshake - pub fn hash(&self, nonce: &[u8]) -> [u8; 32] { - let mut hash = Sha256::new(); - hash.update(b"noise-wss-v1|"); - hash.update(self.host); - hash.update(b"|"); - hash.update(self.query); - hash.update(b"|"); - hash.update(self.accept); - hash.update(b"|"); - hash.update(nonce); - hash.finalize().into() - } -} - -pub fn compute_accept(sec_websocket_key: &[u8]) -> String { - // This is standard GUID to compute Sec-Websocket-Accept - const GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - let mut sha1 = sha1::Sha1::new(); - sha1.update(sec_websocket_key); - sha1.update(GUID); - let digest = sha1.finalize(); - BASE64_STANDARD.encode(digest) -} diff --git a/lib/transport/src/bytes.rs b/lib/transport/src/bytes.rs index 4bf70fa60..e0459560d 100644 --- a/lib/transport/src/bytes.rs +++ b/lib/transport/src/bytes.rs @@ -2,7 +2,7 @@ use anyhow::{Context, anyhow}; use bytes::Bytes; use uuid::Uuid; -use crate::{MessageState, auth2::AuthType}; +use crate::{MessageState, auth::AuthType}; /// Serializes data + channel id + state to byte vec. /// The last byte is the State, and the 16 before that is the Uuid. diff --git a/lib/transport/src/lib.rs b/lib/transport/src/lib.rs index dc5ab22b5..1ba9538cd 100644 --- a/lib/transport/src/lib.rs +++ b/lib/transport/src/lib.rs @@ -2,7 +2,6 @@ use ::bytes::Bytes; use serde::{Deserialize, Serialize}; pub mod auth; -pub mod auth2; pub mod bytes; pub mod channel; pub mod websocket;