From d34c528d5cbc06403c36feea97147b4f0cf2d262 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:44:39 -0700 Subject: [PATCH 1/6] test(core): require exact BiDi socket peer verification --- ...webdriver_bidi_socket_peer_verification.rs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs b/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs new file mode 100644 index 000000000..dd20a4ef1 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs @@ -0,0 +1,107 @@ +use std::{error::Error, net::SocketAddr}; + +use originweave_core::{ + CorrelatedWebDriverBiDiWebSocketEndpoint, WebDriverBiDiSocketPeerVerificationError, + WebDriverBiDiWebSocketEndpoint, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +fn connect_target(endpoint: &str) -> originweave_core::WebDriverBiDiWebSocketConnectTarget { + let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); + assert!(admitted.is_ok(), "{admitted:?}"); + let Ok(admitted) = admitted else { + unreachable!("asserted valid endpoint") + }; + + let correlated: Result = + 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 literal loopback target") + }; + target +} + +#[test] +fn exact_connected_peer_becomes_verified_transport_metadata() { + let endpoint = format!("wss://127.0.0.1:9443/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let peer = SocketAddr::from(([127, 0, 0, 1], 9443)); + + let verified = target.verify_connected_peer(peer); + assert!(verified.is_ok(), "{verified:?}"); + let Ok(verified) = verified else { + return; + }; + + assert_eq!(verified.socket_addr(), peer); + assert!(verified.requires_tls()); + assert_eq!(verified.session_id(), SESSION_ID); +} + +#[test] +fn connected_peer_with_wrong_port_fails_closed() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9516)); + + let result = target.verify_connected_peer(actual); + assert_eq!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { + expected: SocketAddr::from(([127, 0, 0, 1], 9515)), + actual, + }) + ); +} + +#[test] +fn connected_peer_with_different_address_fails_closed() { + let endpoint = format!("ws://[::1]:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9515)); + + let result = target.verify_connected_peer(actual); + assert!(matches!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { .. }) + )); +} + +#[test] +fn non_loopback_observed_peer_cannot_inherit_approved_loopback_authority() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([192, 0, 2, 10], 9515)); + + let result = target.verify_connected_peer(actual); + assert!(matches!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { .. }) + )); +} + +#[test] +fn peer_mismatch_error_is_deterministic_and_source_free() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9516)); + + let result = target.verify_connected_peer(actual); + let Err(error) = result else { + return; + }; + + assert_eq!( + error.to_string(), + "connected WebDriver BiDi socket peer does not match the approved destination" + ); + assert!(error.source().is_none()); +} From d7cf226ba92f404262d4bab02c3fa622e676f459 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:49:13 -0700 Subject: [PATCH 2/6] feat(core): verify exact BiDi socket peer --- ...webdriver_bidi_websocket_connect_target.rs | 89 ++++++++++++++++++- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs index 085335f45..5002731db 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -4,8 +4,9 @@ //! It deliberately refuses `localhost` so a later connector cannot silently inherit ambient DNS //! authority from an admitted WebDriver endpoint. When explicit trusted name resolution is needed, //! the typed error preserves the correlated endpoint instead of discarding its session evidence. -//! The resulting value does not open a socket, authenticate a peer, negotiate TLS, perform a -//! WebSocket handshake, or grant Agent authority. +//! A separately observed connected peer must also match the approved socket destination exactly +//! before it becomes verified transport metadata. These values do not open a socket, authenticate +//! a process, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. use std::{ fmt, @@ -18,8 +19,9 @@ use crate::CorrelatedWebDriverBiDiWebSocketEndpoint; /// /// The destination is inert connection metadata. It proves only that the already-admitted endpoint /// named a literal loopback IP address, retained an explicit nonzero port, and was correlated to the -/// expected WebDriver session id. A runtime connector must independently enforce peer identity, -/// TLS, WebSocket, process, policy, and browser authority before using transport I/O. +/// expected WebDriver session id. A runtime connector must independently establish a connection and +/// verify its observed peer before treating that transport as the approved destination. TLS, +/// WebSocket, process, policy, and browser authority remain separate boundaries. #[derive(Debug, Clone, PartialEq, Eq)] pub struct WebDriverBiDiWebSocketConnectTarget { socket_addr: SocketAddr, @@ -45,8 +47,87 @@ impl WebDriverBiDiWebSocketConnectTarget { pub fn session_id(&self) -> &str { &self.session_id } + + /// Consume this approved destination and verify one observed connected socket peer exactly. + /// + /// Matching requires the complete [`SocketAddr`]—IP address and port—to equal the approved + /// no-DNS destination. A mismatch consumes the target and fails closed, preventing a connector + /// from accidentally reusing the same authority after observing a different peer. Success + /// produces inert verified-peer metadata only; it does not authenticate an OS process, + /// negotiate TLS, perform a WebSocket handshake, or grant browser/Agent authority. + pub fn verify_connected_peer( + self, + observed_peer: SocketAddr, + ) -> Result { + let expected = self.socket_addr; + if observed_peer != expected { + return Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { + expected, + actual: observed_peer, + }); + } + + Ok(VerifiedWebDriverBiDiSocketPeer { + connect_target: self, + }) + } +} + +/// Inert metadata proving that a connected peer exactly matched the approved BiDi destination. +/// +/// This value carries only the destination, TLS requirement, and correlated WebDriver session id +/// already established by preceding boundaries. It does not prove process identity, TLS peer +/// identity, WebSocket protocol state, browser authenticity, policy authorization, or Agent action +/// authority. +#[derive(Debug, PartialEq, Eq)] +pub struct VerifiedWebDriverBiDiSocketPeer { + connect_target: WebDriverBiDiWebSocketConnectTarget, +} + +impl VerifiedWebDriverBiDiSocketPeer { + /// Return the exact approved and observed socket peer address. + #[must_use] + pub const fn socket_addr(&self) -> SocketAddr { + self.connect_target.socket_addr() + } + + /// Return whether the correlated endpoint still requires TLS before WebSocket use. + #[must_use] + pub const fn requires_tls(&self) -> bool { + self.connect_target.requires_tls() + } + + /// Return the exact correlated WebDriver session id. + #[must_use] + pub fn session_id(&self) -> &str { + self.connect_target.session_id() + } +} + +/// Fail-closed errors while verifying an observed BiDi socket peer. +#[derive(Debug, PartialEq, Eq)] +pub enum WebDriverBiDiSocketPeerVerificationError { + /// The connected peer differed from the exact destination approved before connection. + PeerMismatch { + /// Exact socket address that the connector was authorized to reach. + expected: SocketAddr, + /// Socket peer address observed after connection. + actual: SocketAddr, + }, } +impl fmt::Display for WebDriverBiDiSocketPeerVerificationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PeerMismatch { .. } => formatter.write_str( + "connected WebDriver BiDi socket peer does not match the approved destination", + ), + } + } +} + +impl std::error::Error for WebDriverBiDiSocketPeerVerificationError {} + impl CorrelatedWebDriverBiDiWebSocketEndpoint { /// Consume this correlated endpoint and derive one exact no-DNS loopback socket destination. /// From 89fc9c3d5524c2bd76ef4cb4c27398acf5146b32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:49:45 -0700 Subject: [PATCH 3/6] feat(core): export verified BiDi peer contract --- crates/originweave-core/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index eaeecd064..b9d69c8b0 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -90,6 +90,7 @@ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; pub use webdriver_bidi_websocket_connect_target::{ + VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiSocketPeerVerificationError, WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketConnectTargetError, }; pub use webdriver_bidi_websocket_endpoint::{ From f62d20496b1d2ec4aa257ef28944b2edc2d57ff2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:03:34 -0700 Subject: [PATCH 4/6] test(core): require BiDi socket-peer release evidence --- tests/test_webdriver_bidi_connect_target_governance.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_webdriver_bidi_connect_target_governance.py b/tests/test_webdriver_bidi_connect_target_governance.py index b593727ff..de7949478 100644 --- a/tests/test_webdriver_bidi_connect_target_governance.py +++ b/tests/test_webdriver_bidi_connect_target_governance.py @@ -22,6 +22,14 @@ def test_changelog_records_explicit_no_dns_connect_target_boundary(self) -> None self.assertIn("no socket I/O", changelog) self.assertIn("no Agent authority", changelog) + def test_changelog_records_exact_connected_peer_verification_boundary(self) -> None: + """Verified socket-peer metadata must be visible without overstating transport trust.""" + changelog = CHANGELOG.read_text(encoding="utf-8") + self.assertIn("Exact WebDriver BiDi socket-peer verification", changelog) + self.assertIn("IP address and port", changelog) + self.assertIn("does not authenticate an OS process", changelog) + self.assertIn("does not negotiate TLS", changelog) + if __name__ == "__main__": unittest.main() From d52cdfefb8336df4edf4fb49d57ca99e1a6947e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:06:09 -0700 Subject: [PATCH 5/6] docs(changelog): record BiDi socket-peer verification --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64946b209..b8a348bce 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 +- Exact WebDriver BiDi socket-peer verification that consumes an approved no-DNS connect target, requires the observed IP address and port to match exactly, preserves the TLS requirement and exact correlated session id, and remains inert metadata that does not authenticate an OS process, does not negotiate TLS, perform a WebSocket handshake, or grant Agent authority. - Explicit no-DNS WebDriver BiDi loopback connection targets that derive exact IPv4/IPv6 loopback `SocketAddr` metadata from a session-correlated endpoint, reject `localhost` as requiring separately trusted name resolution, preserve the TLS requirement and exact session id, perform no socket I/O, and grant no Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Versioned browser-protocol adapter metadata that distinguishes WebDriver BiDi from pinned CDP, binds bounded adapter/browser revision tokens to an explicit duplicate-free capability set, normalizes capability-set identity independently of caller ordering, and exposes typed fail-closed capability requirements without granting browser, action, network, or secret authority by protocol kind alone. @@ -77,7 +78,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. - Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. -- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. +- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. - TLS accepts only an already verified direct stream, never a hostname or new socket, and requires the TLS origin to match the transport-authority origin exactly. From fa510a0e180b08f38f0e92920b16b874815504c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:08:26 -0700 Subject: [PATCH 6/6] fix(changelog): preserve direct TCP release wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a348bce..7ca383831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,7 +78,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. - Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. -- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. +- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. - TLS accepts only an already verified direct stream, never a hostname or new socket, and requires the TLS origin to match the transport-authority origin exactly.