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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Typed WebDriver BiDi response-envelope correlation that preserves success/error classification, rejects success responses with absent ids, treats nullable error ids as explicitly uncorrelatable, and prevents correlated error envelopes from becoming success correlation evidence without raw JSON parsing or browser/Agent authority grants.
- Bounded raw WebDriver BiDi response-document admission before JSON parsing, with a 65,536-byte product safety budget, exact wire-text retention, JSON-whitespace-aware top-level object-boundary checks, and typed fail-closed errors; this coarse boundary deliberately does not claim JSON validity, response correlation, browser authenticity, or Agent authority.
- One consuming bounded WebDriver BiDi response-document correlation boundary that parses the exact admitted JSON document and feeds only its typed response kind and protocol-range id into the existing exact command-correlation gate, preserving nested parser/correlation error sources without authenticating transport or granting browser/Agent authority.
- Consuming WebDriver BiDi WebSocket endpoint/session correlation that validates one caller-supplied canonical session UUID and rejects exact session mismatches before later transport use; the correlated type preserves only bounded endpoint metadata and does not authenticate Chromium, ChromeDriver, the caller, or the socket peer.
- Fail-closed WebDriver BiDi `script.NodeRemoteValue` admission that requires the exact remote type `node` and a non-empty `sharedId` within the same UTF-8 identifier budget as browser session and context identifiers, rejecting control and whitespace so an untrusted `locateNodes` item cannot be retained as a later typed-input handle without a usable shared node identity.
- Same-call `locateNodes` result admission that revalidates the exact current session, browsing context, canonical origin, and document epoch, rejects an over-budget or non-node result, and translates each admitted `sharedId` through the authority registry into an `ObservedNodeHandle` without performing browser I/O.
- Same-call QueryNodes admission that transfers a non-cloneable SemanticObservation protocol-use proof by ownership into `bind_current_nodes` before an untrusted `locateNodes` result can become current `ObservedNodeHandle` values, so Navigation-only or TypedInput-only proofs cannot mint observation handles.
Expand Down
5 changes: 3 additions & 2 deletions crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub use webdriver_bidi_result::{
ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError,
};
pub use webdriver_bidi_websocket_endpoint::{
MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint,
WebDriverBiDiWebSocketEndpointAdmissionError,
CorrelatedWebDriverBiDiWebSocketEndpoint, MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES,
WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointAdmissionError,
WebDriverBiDiWebSocketEndpointCorrelationError,
};
91 changes: 91 additions & 0 deletions crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ pub struct WebDriverBiDiWebSocketEndpoint {
session_id: String,
}

/// One admitted WebDriver BiDi WebSocket endpoint correlated to an expected session id.
///
/// Correlation proves only that the endpoint resource and the caller-supplied expected session id
/// contain the same canonical admitted session text. The expected session id must itself come from
/// a trusted session-creation boundary. This value does not authenticate Chromium, ChromeDriver,
/// the caller, the operating-system peer, TLS, policy, or Agent authority, and it does not establish
/// a socket.
#[derive(Debug, PartialEq, Eq)]
pub struct CorrelatedWebDriverBiDiWebSocketEndpoint {
endpoint: WebDriverBiDiWebSocketEndpoint,
}

impl WebDriverBiDiWebSocketEndpoint {
/// Admit one bounded canonical first-fixture WebDriver BiDi endpoint.
pub fn new(value: &str) -> Result<Self, WebDriverBiDiWebSocketEndpointAdmissionError> {
Expand Down Expand Up @@ -128,6 +140,28 @@ impl WebDriverBiDiWebSocketEndpoint {
})
}

/// Correlate this endpoint resource to one exact expected WebDriver session id.
///
/// The endpoint is consumed so downstream connection code can require the correlated type and
/// cannot accidentally retain an uncorrelated copy. This comparison does not establish that the
/// caller-supplied expected id is authentic; the caller must obtain that id from its trusted
/// session-creation boundary.
pub fn correlate_session_id(
self,
expected_session_id: &str,
) -> Result<
CorrelatedWebDriverBiDiWebSocketEndpoint,
WebDriverBiDiWebSocketEndpointCorrelationError,
> {
if !is_canonical_session_id(expected_session_id) {
return Err(WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId);
}
if self.session_id != expected_session_id {
return Err(WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch);
}
Comment thread
seonghobae marked this conversation as resolved.
Ok(CorrelatedWebDriverBiDiWebSocketEndpoint { endpoint: self })
}

/// Return the exact admitted endpoint text.
#[must_use]
pub fn as_str(&self) -> &str {
Expand Down Expand Up @@ -159,6 +193,38 @@ impl WebDriverBiDiWebSocketEndpoint {
}
}

impl CorrelatedWebDriverBiDiWebSocketEndpoint {
/// Return the exact admitted endpoint text.
#[must_use]
pub fn as_str(&self) -> &str {
self.endpoint.as_str()
}

/// Return whether the endpoint uses `wss` rather than `ws`.
#[must_use]
pub const fn is_secure(&self) -> bool {
self.endpoint.is_secure()
}

/// Return the canonical loopback listener host without IPv6 brackets.
#[must_use]
pub fn host(&self) -> &str {
self.endpoint.host()
}

/// Return the explicit nonzero listener port.
#[must_use]
pub const fn port(&self) -> u16 {
self.endpoint.port()
}

/// Return the exact session id proven equal to the caller-supplied expected session id.
#[must_use]
pub fn session_id(&self) -> &str {
self.endpoint.session_id()
}
}

fn is_lowercase_hex(byte: u8) -> bool {
byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')
}
Expand Down Expand Up @@ -234,3 +300,28 @@ impl fmt::Display for WebDriverBiDiWebSocketEndpointAdmissionError {
}

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

/// Fail-closed errors while correlating an admitted endpoint with an expected session id.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebDriverBiDiWebSocketEndpointCorrelationError {
/// The expected session id is not an admitted canonical W3C/ChromeDriver representation.
InvalidExpectedSessionId,
/// The endpoint resource belongs to a different canonical session id.
SessionIdMismatch,
}

impl fmt::Display for WebDriverBiDiWebSocketEndpointCorrelationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::InvalidExpectedSessionId => {
"expected WebDriver session id is not an admitted canonical representation"
}
Self::SessionIdMismatch => {
"WebDriver BiDi WebSocket endpoint session id does not match the expected session"
}
};
f.write_str(message)
}
}

impl std::error::Error for WebDriverBiDiWebSocketEndpointCorrelationError {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use std::error::Error;

use originweave_core::{
WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointCorrelationError,
};

const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef";
const OTHER_SESSION_ID: &str = "11234567-89ab-cdef-0123-456789abcdef";
const CHROMEDRIVER_SESSION_ID: &str = "0123456789abcdef0123456789abcdef";

fn endpoint() -> WebDriverBiDiWebSocketEndpoint {
let result =
WebDriverBiDiWebSocketEndpoint::new(&format!("ws://127.0.0.1:9515/session/{SESSION_ID}"));
assert!(result.is_ok(), "{result:?}");
let Ok(endpoint) = result else {
unreachable!("asserted valid endpoint")
};
endpoint
}

#[test]
fn exact_session_identity_correlation_preserves_bounded_endpoint_metadata() {
let result = endpoint().correlate_session_id(SESSION_ID);
assert!(result.is_ok(), "{result:?}");
let Ok(correlated) = result else {
return;
};

assert_eq!(
correlated.as_str(),
format!("ws://127.0.0.1:9515/session/{SESSION_ID}")
);
assert!(!correlated.is_secure());
assert_eq!(correlated.host(), "127.0.0.1");
assert_eq!(correlated.port(), 9515);
assert_eq!(correlated.session_id(), SESSION_ID);
}

#[test]
fn chromedriver_session_identity_correlation_preserves_exact_session_evidence() {
let endpoint = WebDriverBiDiWebSocketEndpoint::new(&format!(
"ws://127.0.0.1:9515/session/{CHROMEDRIVER_SESSION_ID}"
));
assert!(endpoint.is_ok(), "{endpoint:?}");
let Ok(endpoint) = endpoint else {
return;
};

let result = endpoint.correlate_session_id(CHROMEDRIVER_SESSION_ID);
assert!(result.is_ok(), "{result:?}");
let Ok(correlated) = result else {
return;
};
assert_eq!(correlated.session_id(), CHROMEDRIVER_SESSION_ID);
}

#[test]
fn a_different_canonical_session_identity_fails_closed() {
assert!(matches!(
endpoint().correlate_session_id(OTHER_SESSION_ID),
Err(WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch)
));
}

#[test]
fn malformed_expected_session_identity_is_rejected_before_comparison() {
for expected in [
"",
"01234567-89ab-cdef-0123-456789abcdeF",
"0123456789ab-cdef-0123-456789abcdef",
"01234567-89ab-cdef-0123-456789abcdeg",
"01234567_89ab-cdef-0123-456789abcdef",
"0123456789abcdef0123456789abcdeF",
"0123456789abcdef0123456789abcdeg",
] {
assert!(matches!(
endpoint().correlate_session_id(expected),
Err(WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId)
));
}
}

#[test]
fn session_correlation_errors_are_deterministic_and_source_free() {
for error in [
WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId,
WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch,
] {
assert!(!error.to_string().is_empty());
assert!(error.source().is_none());
}
}
Loading