diff --git a/AGENTS.md b/AGENTS.md index edaa9c0fd..cf54fcee4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,3 +120,4 @@ A release requires all current-head checks, complete coverage and docs, updated ## Reusable verification notes - RFC 6455 client-frame masks must come from the OS CSPRNG in production paths. Keep deterministic `WebDriverBiDiWebSocketMaskKey::new` values for fixtures only; exercise the live loopback write path with `cargo test -p originweave-network --test webdriver_bidi_websocket_masking_key_reuse --locked`. In this pinned dependency set, `getrandom 0.2` exposes `getrandom::getrandom` but its error does not implement `std::error::Error`; retain it in the typed frame error without manufacturing an error-chain source. +- RFC 6455 opening requests need a distinct unpredictable 16-byte `Sec-WebSocket-Key`. Use `WebDriverBiDiWebSocketClientKey::random` in production assembly and retain `new` for deterministic fixtures; unit-test the private deterministic filler for canonical base64, redaction, and entropy failure. diff --git a/CHANGELOG.md b/CHANGELOG.md index cae92c48e..e1f47c85a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- OS-CSPRNG-backed RFC 6455 WebDriver BiDi opening-request client-key generation for a fresh redacted 16-byte `Sec-WebSocket-Key` nonce; deterministic injected keys remain limited to fixture-oriented APIs. - OS-CSPRNG-backed WebDriver BiDi WebSocket text and Pong writes that acquire a fresh redacted RFC 6455 masking key before any frame bytes are emitted; entropy failure fails closed and deterministic injected keys remain limited to the existing fixture-oriented APIs. - Bounded RFC 6455 WebDriver BiDi opening-response validation on the exact peer-verified stream: it admits only HTTP/1.1 `101`, case-insensitive `Upgrade`/`Connection` tokens, and the client-key-correlated `Sec-WebSocket-Accept` value within monotonic time and header-size ceilings; it restores blocking mode and still does not implement WebSocket frames or grant browser/Agent authority. - Bounded WebDriver BiDi loopback TCP transport that consumes one exact no-DNS connect target, retries only explicitly recoverable local transport failures within repository timeout and attempt ceilings, exposes the stream only after operating-system peer inspection and exact peer verification, supports a consuming handoff of the original stream with typed credential-free peer/session/TLS and bounded-attempt evidence, preserves typed causal errors, and performs no DNS, proxy/PAC, process authentication, TLS, WebSocket, BiDi message, browser-action, or Agent-authority step. diff --git a/CLAUDE.md b/CLAUDE.md index db3824020..cb69dec15 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,3 +13,4 @@ Additional constraints: - Keep changes bounded to one product gap and preserve modular crate boundaries. - Never claim a test, benchmark, browser integration, TLS identity, GPU execution, release, or merge succeeded without current exact-head evidence. - For production WebSocket client frames, use the OS-CSPRNG helper on the established stream. Keep injected mask bytes only in deterministic fixtures; `getrandom 0.2` error values are not `std::error::Error` in this pinned build. +- For production RFC 6455 opening requests, use `WebDriverBiDiWebSocketClientKey::random`; retain `new` only for deterministic fixture nonces. diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs index f05443f99..7484ab413 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs @@ -76,9 +76,9 @@ impl 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 +/// validates the canonical wire representation, including zero padding bits. [`Self::random`] +/// obtains a fresh nonce from the operating-system CSPRNG; [`Self::new`] remains available for +/// deterministic fixtures. Its [`fmt::Debug`] representation deliberately redacts the nonce so /// diagnostic output cannot disclose handshake material. #[derive(Eq, PartialEq)] pub struct WebDriverBiDiWebSocketClientKey(String); @@ -101,6 +101,19 @@ impl WebDriverBiDiWebSocketClientKey { Ok(Self(value.to_owned())) } + /// Obtain one fresh canonical 16-byte handshake nonce from the operating-system CSPRNG. + pub fn random() -> Result { + Self::from_random_fill(getrandom::getrandom) + } + + fn from_random_fill( + fill_random_bytes: fn(&mut [u8]) -> Result<(), getrandom::Error>, + ) -> Result { + let mut nonce = [0_u8; 16]; + fill_random_bytes(&mut nonce)?; + Ok(Self(STANDARD.encode(nonce))) + } + /// Borrow the exact canonical value for `Sec-WebSocket-Key` serialization. #[must_use] pub fn as_str(&self) -> &str { @@ -1083,6 +1096,31 @@ mod opening_write_tests { .expect("test client key must be valid") } + #[test] + fn random_client_key_is_canonical_redacted_and_propagates_entropy_failure() { + fn fill_test_nonce(value: &mut [u8]) -> Result<(), getrandom::Error> { + value.copy_from_slice(&[0_u8; 16]); + Ok(()) + } + + fn reject_random_fill(_value: &mut [u8]) -> Result<(), getrandom::Error> { + Err(getrandom::Error::UNSUPPORTED) + } + + let key = WebDriverBiDiWebSocketClientKey::from_random_fill(fill_test_nonce) + .expect("test entropy source succeeds"); + assert_eq!(key.as_str(), "AAAAAAAAAAAAAAAAAAAAAA=="); + assert_eq!( + format!("{key:?}"), + "WebDriverBiDiWebSocketClientKey(\"\")" + ); + assert_eq!( + WebDriverBiDiWebSocketClientKey::from_random_fill(reject_random_fill), + Err(getrandom::Error::UNSUPPORTED) + ); + assert!(WebDriverBiDiWebSocketClientKey::random().is_ok()); + } + fn valid_response() -> Vec { b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n".to_vec() } diff --git a/docs/doctoring.md b/docs/doctoring.md index 4f4bee95e..945accdc1 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -14,7 +14,7 @@ WAI-ARIA 1.2 defines host-language `role` values as a token list: user agents sp UTS #39 Revision 32 is the current Unicode security-mechanisms standard and marks Default_Ignorable and bidirectional format characters as restricted in identifier profiles. UAX #9 defines the bidirectional format controls that can reorder displayed protocol text. UTR #36 Revision 15 remains a stabilized historical security-considerations report; its identifier recommendations are superseded by UTS #39 rather than cited as current normative profile rules. OriginWeave therefore rejects the reviewed format-character set in roles, shared identifiers, and registry external identifiers, and rejects those same characters inside accessible names while still allowing ordinary U+0020 spaces. -RFC 6455 carries the WebSocket opening handshake over HTTP/1.1, and RFC 9110 permits `obs-text` octets (`%x80-FF`) in field values while retaining ASCII field-name and delimiter syntax. RFC 6455 also specifies that unknown opening-handshake header fields are ignored. OriginWeave therefore treats unknown extension-field values as opaque compatibility data rather than requiring the entire opening response to be UTF-8, while keeping the authority-bearing `Upgrade`, `Connection`, and `Sec-WebSocket-Accept` checks fail closed: opaque replacement material cannot satisfy the reviewed ASCII token or exact accept-value contracts. Ignoring an unknown field never grants browser, network, secret, approval, or Agent authority. RFC 6455 section 5.3 also requires a fresh, unpredictable client masking key per frame. The verified stream therefore offers OS-CSPRNG-backed text and Pong writes while retaining caller-injected keys only for deterministic fixtures; entropy failure emits no frame and diagnostic rendering never exposes key bytes. +RFC 6455 carries the WebSocket opening handshake over HTTP/1.1, and RFC 9110 permits `obs-text` octets (`%x80-FF`) in field values while retaining ASCII field-name and delimiter syntax. RFC 6455 also specifies that unknown opening-handshake header fields are ignored. OriginWeave therefore treats unknown extension-field values as opaque compatibility data rather than requiring the entire opening response to be UTF-8, while keeping the authority-bearing `Upgrade`, `Connection`, and `Sec-WebSocket-Accept` checks fail closed: opaque replacement material cannot satisfy the reviewed ASCII token or exact accept-value contracts. Ignoring an unknown field never grants browser, network, secret, approval, or Agent authority. RFC 6455 section 4.1 requires an unpredictable 16-byte `Sec-WebSocket-Key` nonce, and section 5.3 requires a fresh, unpredictable client masking key per frame. The verified transport therefore offers OS-CSPRNG-backed key generators for the opening request and text/Pong writes while retaining caller-injected keys only for deterministic fixtures; entropy failure emits no frame or opening request and diagnostic rendering never exposes key bytes. ### Browser origin equivalence