This commit is contained in:
mbecker20
2025-09-27 14:21:12 -07:00
parent 3d455f5142
commit 2d83105500
8 changed files with 155 additions and 248 deletions
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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::{
+1 -1
View File
@@ -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 _,
+1 -1
View File
@@ -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::{
+150 -58
View File
@@ -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=<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)
}
-184
View File
@@ -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=<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)
}
+1 -1
View File
@@ -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.
-1
View File
@@ -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;