Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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, getrandom::Error> {
Self::from_random_fill(getrandom::getrandom)
}

fn from_random_fill(
fill_random_bytes: fn(&mut [u8]) -> Result<(), getrandom::Error>,
) -> Result<Self, getrandom::Error> {
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 {
Expand Down Expand Up @@ -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(\"<redacted WebSocket client nonce>\")"
);
assert_eq!(
WebDriverBiDiWebSocketClientKey::from_random_fill(reject_random_fill),
Err(getrandom::Error::UNSUPPORTED)
);
assert!(WebDriverBiDiWebSocketClientKey::random().is_ok());
}

fn valid_response() -> Vec<u8> {
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()
}
Expand Down
2 changes: 1 addition & 1 deletion docs/doctoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading