Skip to content
10 changes: 8 additions & 2 deletions crates/originweave-network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
//! without hostname resolution or proxy inheritance, verifies operating-system
//! peers before exposing transport I/O, and emits credential-free evidence.
//! It also bridges a session-correlated WebDriver BiDi loopback target from
//! `originweave-core` into one bounded exact TCP connection without granting
//! browser, WebSocket, TLS, policy, or Agent authority.
//! `originweave-core` into one bounded exact TCP connection and can bind an inert
//! RFC 6455 opening request to an already-verified plain BiDi stream without
//! granting browser, WebSocket, TLS, policy, or Agent authority.

#![forbid(unsafe_code)]
#![deny(missing_docs)]

mod connection;
mod webdriver_bidi_connection;
mod webdriver_bidi_websocket_handshake;

pub use connection::{
ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS,
Expand All @@ -21,3 +23,7 @@ pub use webdriver_bidi_connection::{
WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError,
WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan,
};
pub use webdriver_bidi_websocket_handshake::{
WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakeError,
WebDriverBiDiWebSocketHandshakePlan,
};
155 changes: 155 additions & 0 deletions crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use std::fmt;

use originweave_core::VerifiedWebDriverBiDiSocketPeer;

use crate::WebDriverBiDiTcpConnection;

const WEBSOCKET_CLIENT_KEY_LENGTH: usize = 24;
const REDACTED_WEBSOCKET_CLIENT_NONCE: &str = "<redacted WebSocket client nonce>";

fn is_base64_data_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/')
}

fn is_canonical_16_byte_base64(value: &str) -> bool {
let bytes = value.as_bytes();
bytes.len() == WEBSOCKET_CLIENT_KEY_LENGTH
&& bytes[..22].iter().copied().all(is_base64_data_byte)
&& matches!(bytes[21], b'A' | b'Q' | b'g' | b'w')
&& bytes[22] == b'='
&& bytes[23] == b'='
}
Comment thread
seonghobae marked this conversation as resolved.

/// Deterministic failures while preparing one WebDriver BiDi RFC 6455 opening request.
#[derive(Debug, Eq, PartialEq)]
pub enum WebDriverBiDiWebSocketHandshakeError {
/// The supplied client key was not the canonical base64 representation of exactly 16 bytes.
InvalidClientKey,
/// The verified WebDriver BiDi target requires TLS before a WebSocket opening request is sent.
TlsRequired,
}

impl fmt::Display for WebDriverBiDiWebSocketHandshakeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidClientKey => formatter.write_str(
"WebDriver BiDi WebSocket client key is not canonical base64 for exactly 16 bytes",
),
Self::TlsRequired => formatter.write_str(
"WebDriver BiDi WebSocket target requires authenticated TLS before the opening request",
),
}
}
}

impl std::error::Error for WebDriverBiDiWebSocketHandshakeError {}

/// Canonical RFC 6455 client key for one WebDriver BiDi opening handshake.
///
/// RFC 6455 requires `Sec-WebSocket-Key` to be a nonce of 16 bytes encoded with base64. This type
/// validates only the canonical wire representation, including zero padding bits. It does not
/// generate entropy: callers remain responsible for supplying a fresh, unpredictable 16-byte nonce
/// for each connection attempt. Its [`fmt::Debug`] representation deliberately redacts the nonce so
/// diagnostic output cannot disclose handshake material.
#[derive(Eq, PartialEq)]
pub struct WebDriverBiDiWebSocketClientKey(String);

impl fmt::Debug for WebDriverBiDiWebSocketClientKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("WebDriverBiDiWebSocketClientKey")
.field(&REDACTED_WEBSOCKET_CLIENT_NONCE)
.finish()
}
}

impl WebDriverBiDiWebSocketClientKey {
/// Admit one canonical base64 client key representing exactly 16 bytes.
pub fn new(value: &str) -> Result<Self, WebDriverBiDiWebSocketHandshakeError> {
if !is_canonical_16_byte_base64(value) {
return Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey);
}
Ok(Self(value.to_owned()))
}

/// Borrow the exact canonical value for `Sec-WebSocket-Key` serialization.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}

/// Inert RFC 6455 opening request bound to one already-verified plain BiDi TCP connection.
///
/// The plan consumes the verified TCP connection so the opening request cannot be detached from the
/// socket peer/session evidence that authorized its exact loopback destination. It serializes only
/// the fixed WebSocket version-13 request required for the admitted `/session/<session-id>` resource
/// and retains the exact client key required to validate a later `Sec-WebSocket-Accept` response.
/// Secure `wss` targets fail closed here and require a separate authenticated TLS transport boundary
/// before any WebSocket bytes may be written. Its [`fmt::Debug`] representation omits the serialized
/// request and redacts the client nonce because the request embeds that nonce in `Sec-WebSocket-Key`.
///
/// Construction performs no socket write, TLS operation, response parsing, `Sec-WebSocket-Accept`
/// validation, WebSocket framing, Chromium/ChromeDriver process authentication, browser action, or
/// Agent-authority grant.
pub struct WebDriverBiDiWebSocketHandshakePlan {
connection: WebDriverBiDiTcpConnection,
client_key: WebDriverBiDiWebSocketClientKey,
request: Vec<u8>,
}

impl fmt::Debug for WebDriverBiDiWebSocketHandshakePlan {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("WebDriverBiDiWebSocketHandshakePlan")
.field("verified_peer", self.connection.verified_peer())
.field("client_nonce", &REDACTED_WEBSOCKET_CLIENT_NONCE)
.field("request_byte_count", &self.request.len())
.finish()
}
}

impl WebDriverBiDiWebSocketHandshakePlan {
/// Bind one canonical opening request to an already-verified plain BiDi TCP connection.
pub fn new(
connection: WebDriverBiDiTcpConnection,
client_key: WebDriverBiDiWebSocketClientKey,
) -> Result<Self, WebDriverBiDiWebSocketHandshakeError> {
if connection.verified_peer().requires_tls() {
return Err(WebDriverBiDiWebSocketHandshakeError::TlsRequired);
}

let peer = connection.verified_peer();
let request = format!(
"GET /session/{} HTTP/1.1\r\nHost: {}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {}\r\nSec-WebSocket-Version: 13\r\n\r\n",
peer.session_id(),
peer.socket_addr(),
client_key.as_str(),
)
.into_bytes();
Comment thread
seonghobae marked this conversation as resolved.

Ok(Self {
connection,
client_key,
request,
})
}

/// Borrow the exact serialized RFC 6455 opening-request bytes.
#[must_use]
pub fn request_bytes(&self) -> &[u8] {
&self.request
}

/// Borrow the exact client key that a later server-handshake validator must correlate.
#[must_use]
pub const fn client_key(&self) -> &WebDriverBiDiWebSocketClientKey {
&self.client_key
}

/// Borrow the exact peer/session evidence already verified before request construction.
#[must_use]
pub const fn verified_peer(&self) -> &VerifiedWebDriverBiDiSocketPeer {
self.connection.verified_peer()
}
}
202 changes: 202 additions & 0 deletions crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use std::{net::TcpListener, thread, time::Duration};

use originweave_core::WebDriverBiDiWebSocketEndpoint;
use originweave_network::{
WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey,
WebDriverBiDiWebSocketHandshakeError, WebDriverBiDiWebSocketHandshakePlan,
};

const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef";
const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ==";
const REDACTED_CLIENT_KEY: &str = "<redacted WebSocket client nonce>";

fn connect(endpoint: &str) -> originweave_network::WebDriverBiDiTcpConnection {
let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint);
assert!(admitted.is_ok(), "{admitted:?}");
let Ok(admitted) = admitted else {
unreachable!("asserted valid endpoint")
};
let correlated = admitted.correlate_session_id(SESSION_ID);
assert!(correlated.is_ok(), "{correlated:?}");
let Ok(correlated) = correlated else {
unreachable!("asserted correlated endpoint")
};
let target = correlated.into_explicit_connect_target();
assert!(target.is_ok(), "{target:?}");
let Ok(target) = target else {
unreachable!("asserted explicit target")
};
let plan = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1);
assert!(plan.is_ok(), "{plan:?}");
let Ok(plan) = plan else {
unreachable!("asserted connection plan")
};
let connection = plan.connect();
assert!(connection.is_ok(), "{connection:?}");
let Ok(connection) = connection else {
unreachable!("asserted loopback connection")
};
connection
}

#[test]
fn client_key_debug_redacts_websocket_nonce() {
let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY);
assert!(key.is_ok(), "{key:?}");
let Ok(key) = key else {
return;
};

let debug = format!("{key:?}");
assert!(debug.contains(REDACTED_CLIENT_KEY));
assert!(!debug.contains(RFC6455_SAMPLE_KEY));
}

#[test]
fn handshake_plan_debug_redacts_websocket_nonce() {
let listener = TcpListener::bind(("127.0.0.1", 0));
assert!(listener.is_ok(), "{listener:?}");
let Ok(listener) = listener else {
return;
};
let local_addr = listener.local_addr();
assert!(local_addr.is_ok(), "{local_addr:?}");
let Ok(local_addr) = local_addr else {
return;
};
let server = thread::spawn(move || listener.accept().map(|_| ()));

let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}");
let connection = connect(&endpoint);
let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY);
assert!(key.is_ok(), "{key:?}");
let Ok(key) = key else {
return;
};
let plan = WebDriverBiDiWebSocketHandshakePlan::new(connection, key);
assert!(plan.is_ok(), "{plan:?}");
let Ok(plan) = plan else {
return;
};

let debug = format!("{plan:?}");
assert!(debug.contains(REDACTED_CLIENT_KEY));
assert!(!debug.contains(RFC6455_SAMPLE_KEY));

let server_result = server.join();
assert!(server_result.is_ok(), "{server_result:?}");
if let Ok(accept_result) = server_result {
assert!(accept_result.is_ok(), "{accept_result:?}");
}
}

#[test]
fn plain_bidi_connection_serializes_exact_rfc6455_opening_request() {
let listener = TcpListener::bind(("127.0.0.1", 0));
assert!(listener.is_ok(), "{listener:?}");
let Ok(listener) = listener else {
return;
};
let local_addr = listener.local_addr();
assert!(local_addr.is_ok(), "{local_addr:?}");
let Ok(local_addr) = local_addr else {
return;
};
let server = thread::spawn(move || listener.accept().map(|_| ()));

let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}");
let connection = connect(&endpoint);
let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY);
assert!(key.is_ok(), "{key:?}");
let Ok(key) = key else {
return;
};
let plan = WebDriverBiDiWebSocketHandshakePlan::new(connection, key);
assert!(plan.is_ok(), "{plan:?}");
let Ok(plan) = plan else {
return;
};

let expected = format!(
"GET /session/{SESSION_ID} HTTP/1.1\r\nHost: {local_addr}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {RFC6455_SAMPLE_KEY}\r\nSec-WebSocket-Version: 13\r\n\r\n"
);
assert_eq!(plan.request_bytes(), expected.as_bytes());
assert_eq!(plan.client_key().as_str(), RFC6455_SAMPLE_KEY);
assert_eq!(plan.verified_peer().socket_addr(), local_addr);
assert_eq!(plan.verified_peer().session_id(), SESSION_ID);
assert!(!plan.verified_peer().requires_tls());

let server_result = server.join();
assert!(server_result.is_ok(), "{server_result:?}");
if let Ok(accept_result) = server_result {
assert!(accept_result.is_ok(), "{accept_result:?}");
}
}

#[test]
fn handshake_errors_render_actionable_fail_closed_messages() {
assert_eq!(
WebDriverBiDiWebSocketHandshakeError::InvalidClientKey.to_string(),
"WebDriver BiDi WebSocket client key is not canonical base64 for exactly 16 bytes"
);
assert_eq!(
WebDriverBiDiWebSocketHandshakeError::TlsRequired.to_string(),
"WebDriver BiDi WebSocket target requires authenticated TLS before the opening request"
);
}

#[test]
fn handshake_plan_rejects_tls_required_stream_and_noncanonical_client_keys() {
let invalid_length = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQ=");
assert!(matches!(
invalid_length,
Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey)
));
let invalid_character = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZ!==");
assert!(matches!(
invalid_character,
Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey)
));
let invalid_padding_bits = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZR==");
assert!(matches!(
invalid_padding_bits,
Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey)
));
let invalid_padding_character =
WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQA=");
assert!(matches!(
invalid_padding_character,
Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey)
));

let listener = TcpListener::bind(("127.0.0.1", 0));
assert!(listener.is_ok(), "{listener:?}");
let Ok(listener) = listener else {
return;
};
let local_addr = listener.local_addr();
assert!(local_addr.is_ok(), "{local_addr:?}");
let Ok(local_addr) = local_addr else {
return;
};
let server = thread::spawn(move || listener.accept().map(|_| ()));

let endpoint = format!("wss://{local_addr}/session/{SESSION_ID}");
let connection = connect(&endpoint);
let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY);
assert!(key.is_ok(), "{key:?}");
let Ok(key) = key else {
return;
};
let plan = WebDriverBiDiWebSocketHandshakePlan::new(connection, key);
assert!(matches!(
plan,
Err(WebDriverBiDiWebSocketHandshakeError::TlsRequired)
));

let server_result = server.join();
assert!(server_result.is_ok(), "{server_result:?}");
if let Ok(accept_result) = server_result {
assert!(accept_result.is_ok(), "{accept_result:?}");
}
}
Loading