From 25bcc848b008fef8d3f2bd98de38afafbd10b3b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:08:04 +0900 Subject: [PATCH 01/43] test(network): require freshness at socket use --- .../tests/fresh_resolution_plan.rs | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index 584252c66..836e3090b 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -41,7 +41,7 @@ fn connection_plan_requires_a_current_fresh_resolution_authority() -> Result<(), assert_eq!(plan.resolution_authorized_at(), Duration::from_secs(12)); let connection = plan - .connect() + .connect_at(Duration::from_secs(12)) .map_err(|error| format!("connect fresh loopback plan: {error}"))?; assert_eq!(connection.evidence().requested_socket(), socket); assert_eq!(connection.evidence().observed_peer(), socket); @@ -69,6 +69,58 @@ fn expired_resolution_cannot_create_a_connection_plan() -> Result<(), String> { Ok(()) } +#[test] +fn plan_must_still_be_fresh_at_actual_socket_use() -> Result<(), String> { + let snapshot = fresh_loopback_snapshot()?; + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); + let plan = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::from_secs(1), + 1, + ) + .map_err(|error| format!("authorize fresh connection plan: {error}"))?; + + let result = plan.connect_at(Duration::from_secs(15)); + assert!(matches!( + result, + Err(NetworkError::ResolutionAuthorityExpired { + authorized_at, + valid_until, + attempted_at, + }) if authorized_at == Duration::from_secs(12) + && valid_until == Duration::from_secs(15) + && attempted_at == Duration::from_secs(15) + )); + Ok(()) +} + +#[test] +fn socket_use_time_cannot_regress_before_plan_authorization() -> Result<(), String> { + let snapshot = fresh_loopback_snapshot()?; + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); + let plan = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::from_secs(1), + 1, + ) + .map_err(|error| format!("authorize fresh connection plan: {error}"))?; + + let result = plan.connect_at(Duration::from_secs(11)); + assert!(matches!( + result, + Err(NetworkError::ResolutionAuthorityTimeRegressed { + authorized_at, + attempted_at, + }) if authorized_at == Duration::from_secs(12) + && attempted_at == Duration::from_secs(11) + )); + Ok(()) +} + #[test] fn fresh_resolution_still_requires_valid_connection_parameters() -> Result<(), String> { let snapshot = fresh_loopback_snapshot()?; From 43ff003e7cb683c9737eedd9381447e7c79d70f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:12:33 +0900 Subject: [PATCH 02/43] fix(network): reauthorize resolution at socket use --- .../src/fresh_connection.rs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 6ca7b2074..870c40998 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -13,6 +13,8 @@ use crate::connection::{ConnectionPlan, DirectTcpConnection, NetworkError}; #[derive(Debug)] pub struct FreshConnectionPlan { connection_plan: ConnectionPlan, + resolution: FreshResolutionSnapshot, + socket_address: SocketAddr, resolution_approved_at: Duration, resolution_valid_until: Duration, resolution_authorized_at: Duration, @@ -41,6 +43,8 @@ impl FreshConnectionPlan { )?; Ok(Self { connection_plan, + resolution: resolution.clone(), + socket_address, resolution_approved_at: fresh_evidence.resolution_approved_at(), resolution_valid_until: fresh_evidence.resolution_valid_until(), resolution_authorized_at: fresh_evidence.authorized_at(), @@ -65,8 +69,20 @@ impl FreshConnectionPlan { self.resolution_authorized_at } - /// Open the exact approved socket and expose it only after peer verification. - pub fn connect(self) -> Result { + /// Open the exact approved socket only while resolution authority is still fresh. + /// + /// `current_time` must come from the same trusted monotonic clock domain used + /// when this plan was created. Freshness is re-authorized immediately before + /// socket I/O so a plan cannot be created inside the validity window and then + /// replayed after that authority expires or before its recorded authorization + /// time. The plan remains single-use because this method consumes `self`. + pub fn connect_at(self, current_time: Duration) -> Result { + self.resolution + .authorize_connection(self.socket_address.ip(), current_time) + .map_err(|source| NetworkError::DestinationNotApproved { + socket_address: self.socket_address, + source, + })?; self.connection_plan.connect() } } From 623e09815792e86d484989f4c45cbbc86171d03a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:13:09 +0900 Subject: [PATCH 03/43] test(network): assert destination freshness at socket use --- .../tests/fresh_resolution_plan.rs | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index 836e3090b..e077235b6 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -2,7 +2,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; use std::time::Duration; use originweave_core::Origin; -use originweave_destination::{AddressClass, DestinationPolicy, FreshResolutionSnapshot}; +use originweave_destination::{ + AddressClass, DestinationError, DestinationPolicy, FreshResolutionSnapshot, +}; use originweave_network::{FreshConnectionPlan, NetworkError}; fn fresh_loopback_snapshot() -> Result { @@ -63,8 +65,14 @@ fn expired_resolution_cannot_create_a_connection_plan() -> Result<(), String> { assert!(matches!( result, - Err(NetworkError::DestinationNotApproved { ref source, .. }) - if source.to_string().contains("expired") + Err(NetworkError::DestinationNotApproved { + source: DestinationError::ResolutionApprovalExpired { + valid_until, + current_time, + }, + .. + }) if valid_until == Duration::from_secs(15) + && current_time == Duration::from_secs(15) )); Ok(()) } @@ -85,19 +93,20 @@ fn plan_must_still_be_fresh_at_actual_socket_use() -> Result<(), String> { let result = plan.connect_at(Duration::from_secs(15)); assert!(matches!( result, - Err(NetworkError::ResolutionAuthorityExpired { - authorized_at, - valid_until, - attempted_at, - }) if authorized_at == Duration::from_secs(12) - && valid_until == Duration::from_secs(15) - && attempted_at == Duration::from_secs(15) + Err(NetworkError::DestinationNotApproved { + source: DestinationError::ResolutionApprovalExpired { + valid_until, + current_time, + }, + .. + }) if valid_until == Duration::from_secs(15) + && current_time == Duration::from_secs(15) )); Ok(()) } #[test] -fn socket_use_time_cannot_regress_before_plan_authorization() -> Result<(), String> { +fn socket_use_time_cannot_regress_before_resolution_approval() -> Result<(), String> { let snapshot = fresh_loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); let plan = FreshConnectionPlan::new( @@ -109,14 +118,17 @@ fn socket_use_time_cannot_regress_before_plan_authorization() -> Result<(), Stri ) .map_err(|error| format!("authorize fresh connection plan: {error}"))?; - let result = plan.connect_at(Duration::from_secs(11)); + let result = plan.connect_at(Duration::from_secs(9)); assert!(matches!( result, - Err(NetworkError::ResolutionAuthorityTimeRegressed { - authorized_at, - attempted_at, - }) if authorized_at == Duration::from_secs(12) - && attempted_at == Duration::from_secs(11) + Err(NetworkError::DestinationNotApproved { + source: DestinationError::ResolutionUseBeforeApproval { + approved_at, + current_time, + }, + .. + }) if approved_at == Duration::from_secs(10) + && current_time == Duration::from_secs(9) )); Ok(()) } From 2783d711f15fc1cc40199bf207ed806b45651f17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:14:42 +0900 Subject: [PATCH 04/43] fix(network): fail closed on delayed fresh-plan use --- .../src/fresh_connection.rs | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 870c40998..8dfc50bd8 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -1,7 +1,7 @@ use std::net::SocketAddr; -use std::time::Duration; +use std::time::{Duration, Instant}; -use originweave_destination::FreshResolutionSnapshot; +use originweave_destination::{DestinationError, FreshResolutionSnapshot}; use crate::connection::{ConnectionPlan, DirectTcpConnection, NetworkError}; @@ -18,6 +18,7 @@ pub struct FreshConnectionPlan { resolution_approved_at: Duration, resolution_valid_until: Duration, resolution_authorized_at: Duration, + authorized_instant: Instant, } impl FreshConnectionPlan { @@ -48,6 +49,7 @@ impl FreshConnectionPlan { resolution_approved_at: fresh_evidence.resolution_approved_at(), resolution_valid_until: fresh_evidence.resolution_valid_until(), resolution_authorized_at: fresh_evidence.authorized_at(), + authorized_instant: Instant::now(), }) } @@ -69,14 +71,39 @@ impl FreshConnectionPlan { self.resolution_authorized_at } + /// Open the exact approved socket using the elapsed monotonic time since plan authorization. + /// + /// This compatibility path anchors a process-local [`Instant`] when the + /// caller-supplied trusted resolution time is admitted. Actual elapsed time + /// is added to that authorization value before socket I/O, so callers that + /// do not supply a second timestamp cannot replay a plan indefinitely after + /// its freshness window expires. New authority-bearing call sites should use + /// [`FreshConnectionPlan::connect_at`] with their trusted monotonic clock. + pub fn connect(self) -> Result { + let current_time = self + .resolution_authorized_at + .saturating_add(self.authorized_instant.elapsed()); + self.connect_at(current_time) + } + /// Open the exact approved socket only while resolution authority is still fresh. /// /// `current_time` must come from the same trusted monotonic clock domain used /// when this plan was created. Freshness is re-authorized immediately before /// socket I/O so a plan cannot be created inside the validity window and then - /// replayed after that authority expires or before its recorded authorization - /// time. The plan remains single-use because this method consumes `self`. + /// replayed after that authority expires. A time earlier than the plan's own + /// authorization checkpoint fails closed. The plan remains single-use because + /// this method consumes `self`. pub fn connect_at(self, current_time: Duration) -> Result { + if current_time < self.resolution_authorized_at { + return Err(NetworkError::DestinationNotApproved { + socket_address: self.socket_address, + source: DestinationError::ResolutionUseBeforeApproval { + approved_at: self.resolution_authorized_at, + current_time, + }, + }); + } self.resolution .authorize_connection(self.socket_address.ip(), current_time) .map_err(|source| NetworkError::DestinationNotApproved { From 492f74c12e37f47a52275dc55d8c7b04e5ec57ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:15:16 +0900 Subject: [PATCH 05/43] test(network): prove all fresh-plan connect paths expire --- .../tests/fresh_resolution_plan.rs | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index e077235b6..3c723397f 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -1,4 +1,5 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; +use std::thread; use std::time::Duration; use originweave_core::Origin; @@ -106,7 +107,7 @@ fn plan_must_still_be_fresh_at_actual_socket_use() -> Result<(), String> { } #[test] -fn socket_use_time_cannot_regress_before_resolution_approval() -> Result<(), String> { +fn socket_use_time_cannot_regress_before_plan_authorization() -> Result<(), String> { let snapshot = fresh_loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); let plan = FreshConnectionPlan::new( @@ -118,7 +119,7 @@ fn socket_use_time_cannot_regress_before_resolution_approval() -> Result<(), Str ) .map_err(|error| format!("authorize fresh connection plan: {error}"))?; - let result = plan.connect_at(Duration::from_secs(9)); + let result = plan.connect_at(Duration::from_secs(11)); assert!(matches!( result, Err(NetworkError::DestinationNotApproved { @@ -127,8 +128,42 @@ fn socket_use_time_cannot_regress_before_resolution_approval() -> Result<(), Str current_time, }, .. - }) if approved_at == Duration::from_secs(10) - && current_time == Duration::from_secs(9) + }) if approved_at == Duration::from_secs(12) + && current_time == Duration::from_secs(11) + )); + Ok(()) +} + +#[test] +fn compatibility_connect_path_expires_from_real_monotonic_elapsed_time() -> Result<(), String> { + let origin = Origin::parse("http://localhost") + .map_err(|error| format!("loopback origin fixture is invalid: {error:?}"))?; + let snapshot = FreshResolutionSnapshot::approve( + origin, + [IpAddr::V4(Ipv4Addr::LOCALHOST)], + &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), + Duration::from_secs(10), + Duration::from_millis(1), + ) + .map_err(|error| format!("short-lived snapshot is invalid: {error}"))?; + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); + let plan = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(10), + socket, + Duration::from_secs(1), + 1, + ) + .map_err(|error| format!("authorize short-lived connection plan: {error}"))?; + + thread::sleep(Duration::from_millis(5)); + let result = plan.connect(); + assert!(matches!( + result, + Err(NetworkError::DestinationNotApproved { + source: DestinationError::ResolutionApprovalExpired { .. }, + .. + }) )); Ok(()) } From ec81031c537f2b662910c1ce78c7ae0e0bfc9c1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:17:31 +0900 Subject: [PATCH 06/43] docs(changelog): record socket-use freshness recheck --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28cb0d11d..3b69944b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. -- Fresh-resolution network adapter that consumes a validated `FreshResolutionSnapshot` plus caller-supplied trusted monotonic time before constructing the existing exact-socket `ConnectionPlan`, retains credential-free approval/validity/authorization timestamps, and preserves the original direct-connect validation and single-use stream boundary. +- Fresh-resolution network adapter that consumes a validated `FreshResolutionSnapshot` plus trusted monotonic authority, retains credential-free approval/validity/authorization timestamps, and reauthorizes the same exact address immediately before socket I/O. Explicit `connect_at` callers supply current trusted monotonic time; the compatibility `connect` path anchors a process-local monotonic instant at authorization so delayed plans also expire instead of replaying stale resolution authority. - Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves. - Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior and `NotConfigured` revocation evidence. - Credential-free TLS evidence containing canonical origin, requested and observed peer, DNS/IP reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. From 910f1cf766819f67c20ce308c156da1b8a1c3a73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 14:36:52 +0900 Subject: [PATCH 07/43] test(network): reject socket port origin drift --- .../tests/fresh_resolution_plan.rs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index 3c723397f..f481904b1 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -184,3 +184,46 @@ fn fresh_resolution_still_requires_valid_connection_parameters() -> Result<(), S assert!(matches!(result, Err(NetworkError::InvalidPort))); Ok(()) } + +#[test] +fn connection_plan_rejects_socket_port_that_changes_default_origin() -> Result<(), String> { + let snapshot = fresh_loopback_snapshot()?; + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080); + + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::from_secs(1), + 1, + ); + + assert!(result.is_err(), "socket port must remain bound to origin"); + Ok(()) +} + +#[test] +fn connection_plan_rejects_socket_port_that_changes_explicit_origin() -> Result<(), String> { + let origin = Origin::parse("http://localhost:8080") + .map_err(|error| format!("explicit-port origin fixture is invalid: {error:?}"))?; + let snapshot = FreshResolutionSnapshot::approve( + origin, + [IpAddr::V4(Ipv4Addr::LOCALHOST)], + &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), + Duration::from_secs(10), + Duration::from_secs(5), + ) + .map_err(|error| format!("fresh explicit-port snapshot is invalid: {error}"))?; + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081); + + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::from_secs(1), + 1, + ); + + assert!(result.is_err(), "socket port must remain bound to origin"); + Ok(()) +} From c7634c6735661b982ebc8b3a43eca26197c9eb01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 14:42:23 +0900 Subject: [PATCH 08/43] fix(network): bind fresh socket plan to origin port --- .../src/fresh_connection.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 8dfc50bd8..66a71349f 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -1,10 +1,31 @@ use std::net::SocketAddr; use std::time::{Duration, Instant}; +use originweave_core::Origin; use originweave_destination::{DestinationError, FreshResolutionSnapshot}; use crate::connection::{ConnectionPlan, DirectTcpConnection, NetworkError}; +fn effective_origin_port(origin: &Origin) -> u16 { + let default_port = match origin.scheme() { + "https" => 443, + _ => 80, + }; + let authority = &origin.as_str()[origin.scheme().len() + 3..]; + let explicit_port = if authority.starts_with('[') { + authority.rsplit_once("]:").map(|(_, port)| port) + } else { + authority.rsplit_once(':').map(|(_, port)| port) + }; + + match explicit_port { + Some(port) => port.bytes().fold(0_u16, |value, digit| { + value * 10 + u16::from(digit - b'0') + }), + None => default_port, + } +} + /// A single-use direct connection plan authorized by a fresh resolution window. /// /// This adapter composes the destination crate's monotonic freshness authority @@ -36,6 +57,9 @@ impl FreshConnectionPlan { socket_address, source, })?; + if socket_address.port() != effective_origin_port(resolution.origin()) { + return Err(NetworkError::InvalidPort); + } let connection_plan = ConnectionPlan::new( resolution.resolution_snapshot(), socket_address, @@ -113,3 +137,27 @@ impl FreshConnectionPlan { self.connection_plan.connect() } } + +#[cfg(test)] +mod tests { + use originweave_core::Origin; + + use super::effective_origin_port; + + #[test] + fn effective_origin_port_covers_default_and_explicit_authorities() { + let fixtures = [ + ("http://localhost", 80), + ("https://example.com", 443), + ("http://localhost:8080", 8080), + ("http://[::1]:8443", 8443), + ]; + + for (origin, expected_port) in fixtures { + let parsed = Origin::parse(origin).unwrap_or_else(|error| { + panic!("valid origin fixture {origin} must parse: {error:?}") + }); + assert_eq!(effective_origin_port(&parsed), expected_port); + } + } +} From cb57e2fa1a84cf4f3532d13b23cc4abc67a0f0ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 14:44:08 +0900 Subject: [PATCH 09/43] test(network): bind freshness fixtures to origin ports --- .../tests/fresh_resolution_plan.rs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index f481904b1..8f96a0345 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -8,8 +8,8 @@ use originweave_destination::{ }; use originweave_network::{FreshConnectionPlan, NetworkError}; -fn fresh_loopback_snapshot() -> Result { - let origin = Origin::parse("http://localhost") +fn fresh_loopback_snapshot_for_port(port: u16) -> Result { + let origin = Origin::parse(&format!("http://localhost:{port}")) .map_err(|error| format!("loopback origin fixture is invalid: {error:?}"))?; FreshResolutionSnapshot::approve( origin, @@ -21,14 +21,27 @@ fn fresh_loopback_snapshot() -> Result { .map_err(|error| format!("fresh loopback snapshot is invalid: {error}")) } +fn fresh_default_loopback_snapshot() -> Result { + let origin = Origin::parse("http://localhost") + .map_err(|error| format!("default loopback origin fixture is invalid: {error:?}"))?; + FreshResolutionSnapshot::approve( + origin, + [IpAddr::V4(Ipv4Addr::LOCALHOST)], + &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), + Duration::from_secs(10), + Duration::from_secs(5), + ) + .map_err(|error| format!("fresh default loopback snapshot is invalid: {error}")) +} + #[test] fn connection_plan_requires_a_current_fresh_resolution_authority() -> Result<(), String> { - let snapshot = fresh_loopback_snapshot()?; let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)) .map_err(|error| format!("bind loopback listener: {error}"))?; let socket = listener .local_addr() .map_err(|error| format!("read loopback listener address: {error}"))?; + let snapshot = fresh_loopback_snapshot_for_port(socket.port())?; let plan = FreshConnectionPlan::new( &snapshot, @@ -53,8 +66,8 @@ fn connection_plan_requires_a_current_fresh_resolution_authority() -> Result<(), #[test] fn expired_resolution_cannot_create_a_connection_plan() -> Result<(), String> { - let snapshot = fresh_loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080); + let snapshot = fresh_loopback_snapshot_for_port(socket.port())?; let result = FreshConnectionPlan::new( &snapshot, @@ -80,8 +93,8 @@ fn expired_resolution_cannot_create_a_connection_plan() -> Result<(), String> { #[test] fn plan_must_still_be_fresh_at_actual_socket_use() -> Result<(), String> { - let snapshot = fresh_loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); + let snapshot = fresh_loopback_snapshot_for_port(socket.port())?; let plan = FreshConnectionPlan::new( &snapshot, Duration::from_secs(12), @@ -108,8 +121,8 @@ fn plan_must_still_be_fresh_at_actual_socket_use() -> Result<(), String> { #[test] fn socket_use_time_cannot_regress_before_plan_authorization() -> Result<(), String> { - let snapshot = fresh_loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); + let snapshot = fresh_loopback_snapshot_for_port(socket.port())?; let plan = FreshConnectionPlan::new( &snapshot, Duration::from_secs(12), @@ -136,7 +149,8 @@ fn socket_use_time_cannot_regress_before_plan_authorization() -> Result<(), Stri #[test] fn compatibility_connect_path_expires_from_real_monotonic_elapsed_time() -> Result<(), String> { - let origin = Origin::parse("http://localhost") + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); + let origin = Origin::parse("http://localhost:9") .map_err(|error| format!("loopback origin fixture is invalid: {error:?}"))?; let snapshot = FreshResolutionSnapshot::approve( origin, @@ -146,7 +160,6 @@ fn compatibility_connect_path_expires_from_real_monotonic_elapsed_time() -> Resu Duration::from_millis(1), ) .map_err(|error| format!("short-lived snapshot is invalid: {error}"))?; - let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); let plan = FreshConnectionPlan::new( &snapshot, Duration::from_secs(10), @@ -170,7 +183,7 @@ fn compatibility_connect_path_expires_from_real_monotonic_elapsed_time() -> Resu #[test] fn fresh_resolution_still_requires_valid_connection_parameters() -> Result<(), String> { - let snapshot = fresh_loopback_snapshot()?; + let snapshot = fresh_default_loopback_snapshot()?; let invalid_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); let result = FreshConnectionPlan::new( @@ -187,7 +200,7 @@ fn fresh_resolution_still_requires_valid_connection_parameters() -> Result<(), S #[test] fn connection_plan_rejects_socket_port_that_changes_default_origin() -> Result<(), String> { - let snapshot = fresh_loopback_snapshot()?; + let snapshot = fresh_default_loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080); let result = FreshConnectionPlan::new( @@ -198,7 +211,7 @@ fn connection_plan_rejects_socket_port_that_changes_default_origin() -> Result<( 1, ); - assert!(result.is_err(), "socket port must remain bound to origin"); + assert!(matches!(result, Err(NetworkError::InvalidPort))); Ok(()) } @@ -224,6 +237,6 @@ fn connection_plan_rejects_socket_port_that_changes_explicit_origin() -> Result< 1, ); - assert!(result.is_err(), "socket port must remain bound to origin"); + assert!(matches!(result, Err(NetworkError::InvalidPort))); Ok(()) } From 4588380f3457c34842760ca73f28ccd0edbaf611 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 14:45:15 +0900 Subject: [PATCH 10/43] style(network): apply pinned rustfmt output --- crates/originweave-network/src/fresh_connection.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 66a71349f..02fc12b7e 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -19,9 +19,9 @@ fn effective_origin_port(origin: &Origin) -> u16 { }; match explicit_port { - Some(port) => port.bytes().fold(0_u16, |value, digit| { - value * 10 + u16::from(digit - b'0') - }), + Some(port) => port + .bytes() + .fold(0_u16, |value, digit| value * 10 + u16::from(digit - b'0')), None => default_port, } } From 6277c673202582ae1d534e906e84755f3857ba3c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 14:49:04 +0900 Subject: [PATCH 11/43] test(network): keep origin-port helper panic-free --- crates/originweave-network/src/fresh_connection.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 02fc12b7e..1c29e3c74 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -145,7 +145,7 @@ mod tests { use super::effective_origin_port; #[test] - fn effective_origin_port_covers_default_and_explicit_authorities() { + fn effective_origin_port_covers_default_and_explicit_authorities() -> Result<(), String> { let fixtures = [ ("http://localhost", 80), ("https://example.com", 443), @@ -154,10 +154,10 @@ mod tests { ]; for (origin, expected_port) in fixtures { - let parsed = Origin::parse(origin).unwrap_or_else(|error| { - panic!("valid origin fixture {origin} must parse: {error:?}") - }); + let parsed = Origin::parse(origin) + .map_err(|error| format!("valid origin fixture {origin} must parse: {error:?}"))?; assert_eq!(effective_origin_port(&parsed), expected_port); } + Ok(()) } } From 1f504ed6af8509883d3a6b077e12d29863042e1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 14:53:06 +0900 Subject: [PATCH 12/43] test(network): cover canonical-port parameter rejection --- .../tests/fresh_resolution_plan.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index 8f96a0345..0aa64a5f6 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -184,17 +184,17 @@ fn compatibility_connect_path_expires_from_real_monotonic_elapsed_time() -> Resu #[test] fn fresh_resolution_still_requires_valid_connection_parameters() -> Result<(), String> { let snapshot = fresh_default_loopback_snapshot()?; - let invalid_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80); - let result = FreshConnectionPlan::new( - &snapshot, - Duration::from_secs(12), - invalid_socket, - Duration::from_secs(1), - 1, - ); + let result = FreshConnectionPlan::new(&snapshot, Duration::from_secs(12), socket, Duration::ZERO, 1); - assert!(matches!(result, Err(NetworkError::InvalidPort))); + assert!(matches!( + result, + Err(NetworkError::InvalidConnectTimeout { + connect_timeout, + .. + }) if connect_timeout == Duration::ZERO + )); Ok(()) } From f8ef916d8d3b4dc19e8095adeaaf7b7aae57a03c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 14:55:07 +0900 Subject: [PATCH 13/43] test(network): cover origin-port helper without dead error closure --- crates/originweave-network/src/fresh_connection.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 1c29e3c74..a89929619 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -145,7 +145,7 @@ mod tests { use super::effective_origin_port; #[test] - fn effective_origin_port_covers_default_and_explicit_authorities() -> Result<(), String> { + fn effective_origin_port_covers_default_and_explicit_authorities() { let fixtures = [ ("http://localhost", 80), ("https://example.com", 443), @@ -154,10 +154,8 @@ mod tests { ]; for (origin, expected_port) in fixtures { - let parsed = Origin::parse(origin) - .map_err(|error| format!("valid origin fixture {origin} must parse: {error:?}"))?; - assert_eq!(effective_origin_port(&parsed), expected_port); + let actual_port = Origin::parse(origin).map(|parsed| effective_origin_port(&parsed)); + assert!(matches!(actual_port, Ok(port) if port == expected_port)); } - Ok(()) } } From 3f5f4a69af910b8289d5ba21be13f11a7cd201b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 15:13:29 +0900 Subject: [PATCH 14/43] style(network): format fresh resolution regression --- crates/originweave-network/tests/fresh_resolution_plan.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index 0aa64a5f6..dc05433c3 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -186,7 +186,13 @@ fn fresh_resolution_still_requires_valid_connection_parameters() -> Result<(), S let snapshot = fresh_default_loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80); - let result = FreshConnectionPlan::new(&snapshot, Duration::from_secs(12), socket, Duration::ZERO, 1); + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::ZERO, + 1, + ); assert!(matches!( result, From 1055ad07b8b0c1757e0b2776050f7c128ffad2aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 15:15:31 +0900 Subject: [PATCH 15/43] test(network): remove synthetic coverage branch --- crates/originweave-network/src/fresh_connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index a89929619..b1537f4f0 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -155,7 +155,7 @@ mod tests { for (origin, expected_port) in fixtures { let actual_port = Origin::parse(origin).map(|parsed| effective_origin_port(&parsed)); - assert!(matches!(actual_port, Ok(port) if port == expected_port)); + assert_eq!(actual_port.ok(), Some(expected_port)); } } } From c972d67536903da026c6e79330013a95071b742c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 15:20:21 +0900 Subject: [PATCH 16/43] docs(changelog): record origin-port socket authority --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b69944b5..8d0a6c469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. +- Fresh-resolution direct TCP plans require the requested socket port to equal the canonical origin's effective HTTP or HTTPS port, preventing same-IP authority from widening to a different service. - 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 5a410cb39ebcb9c0ea54f644a72349316c60c7d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 17:11:51 +0900 Subject: [PATCH 17/43] test(network): cover HTTPS IPv6 origin port authority --- .../tests/fresh_resolution_plan.rs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index dc05433c3..5103a159d 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -1,4 +1,4 @@ -use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener}; use std::thread; use std::time::Duration; @@ -221,6 +221,39 @@ fn connection_plan_rejects_socket_port_that_changes_default_origin() -> Result<( Ok(()) } +#[test] +fn connection_plan_enforces_default_https_port_for_ipv6_origin() -> Result<(), String> { + let origin = Origin::parse("https://[::1]") + .map_err(|error| format!("default HTTPS IPv6 origin fixture is invalid: {error:?}"))?; + let snapshot = FreshResolutionSnapshot::approve( + origin, + [IpAddr::V6(Ipv6Addr::LOCALHOST)], + &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), + Duration::from_secs(10), + Duration::from_secs(5), + ) + .map_err(|error| format!("fresh HTTPS IPv6 snapshot is invalid: {error}"))?; + + let authorized = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 443), + Duration::from_secs(1), + 1, + ); + assert!(authorized.is_ok()); + + let wrong_port = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 444), + Duration::from_secs(1), + 1, + ); + assert!(matches!(wrong_port, Err(NetworkError::InvalidPort))); + Ok(()) +} + #[test] fn connection_plan_rejects_socket_port_that_changes_explicit_origin() -> Result<(), String> { let origin = Origin::parse("http://localhost:8080") From 2b6495d24438d3b532502809e0d982c464a17ff2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 18:06:29 +0900 Subject: [PATCH 18/43] test(network): require typed origin port mismatch --- .../tests/fresh_resolution_plan.rs | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index 5103a159d..5c0726c7a 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -1,3 +1,4 @@ +use std::error::Error; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener}; use std::thread; use std::time::Duration; @@ -216,8 +217,23 @@ fn connection_plan_rejects_socket_port_that_changes_default_origin() -> Result<( Duration::from_secs(1), 1, ); + let error = match result { + Err(error) => error, + Ok(_plan) => return Err("origin-port drift unexpectedly produced a connection plan".into()), + }; - assert!(matches!(result, Err(NetworkError::InvalidPort))); + assert!(matches!( + &error, + NetworkError::OriginPortMismatch { + requested_port, + expected_port, + } if *requested_port == 8080 && *expected_port == 80 + )); + assert_eq!( + error.to_string(), + "socket port 8080 does not match canonical origin port 80" + ); + assert!(error.source().is_none()); Ok(()) } @@ -250,7 +266,13 @@ fn connection_plan_enforces_default_https_port_for_ipv6_origin() -> Result<(), S Duration::from_secs(1), 1, ); - assert!(matches!(wrong_port, Err(NetworkError::InvalidPort))); + assert!(matches!( + wrong_port, + Err(NetworkError::OriginPortMismatch { + requested_port: 444, + expected_port: 443, + }) + )); Ok(()) } @@ -276,6 +298,12 @@ fn connection_plan_rejects_socket_port_that_changes_explicit_origin() -> Result< 1, ); - assert!(matches!(result, Err(NetworkError::InvalidPort))); + assert!(matches!( + result, + Err(NetworkError::OriginPortMismatch { + requested_port: 8081, + expected_port: 8080, + }) + )); Ok(()) } From d5ac94949f2ceb9f140d0f6706c399cf23a05211 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 18:12:13 +0900 Subject: [PATCH 19/43] fix(network): distinguish origin port authority mismatch --- crates/originweave-network/src/connection.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/originweave-network/src/connection.rs b/crates/originweave-network/src/connection.rs index bbd34c5a7..1002b4f8e 100644 --- a/crates/originweave-network/src/connection.rs +++ b/crates/originweave-network/src/connection.rs @@ -273,6 +273,13 @@ impl SocketConnectionEvidence { pub enum NetworkError { /// The requested destination port was zero. InvalidPort, + /// The requested socket port did not match the canonical origin's effective port. + OriginPortMismatch { + /// The nonzero socket port requested by the caller. + requested_port: u16, + /// The HTTP or HTTPS port authorized by the canonical origin. + expected_port: u16, + }, /// The timeout was zero or exceeded [`MAX_CONNECT_TIMEOUT`]. InvalidConnectTimeout { /// The rejected timeout. @@ -351,6 +358,7 @@ impl NetworkError { Self::PeerInspectionFailed { attempt_number, .. } | Self::PeerMismatch { attempt_number, .. } => Some(*attempt_number), Self::InvalidPort + | Self::OriginPortMismatch { .. } | Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } | Self::DestinationNotApproved { .. } @@ -363,6 +371,13 @@ impl fmt::Display for NetworkError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::InvalidPort => formatter.write_str("connection port must be within 1..=65535"), + Self::OriginPortMismatch { + requested_port, + expected_port, + } => write!( + formatter, + "socket port {requested_port} does not match canonical origin port {expected_port}", + ), Self::InvalidConnectTimeout { connect_timeout, maximum_timeout, @@ -436,6 +451,7 @@ impl std::error::Error for NetworkError { | Self::ConnectionFailed { source, .. } | Self::PeerInspectionFailed { source, .. } => Some(source), Self::InvalidPort + | Self::OriginPortMismatch { .. } | Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } | Self::NonCanonicalSocketAddress { .. } From faa79327508f2a8facf2c01ecb99cbe65170863e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 18:12:57 +0900 Subject: [PATCH 20/43] fix(network): return typed origin port mismatch --- crates/originweave-network/src/fresh_connection.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index b1537f4f0..76d0e067d 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -57,8 +57,12 @@ impl FreshConnectionPlan { socket_address, source, })?; - if socket_address.port() != effective_origin_port(resolution.origin()) { - return Err(NetworkError::InvalidPort); + let expected_port = effective_origin_port(resolution.origin()); + if socket_address.port() != expected_port { + return Err(NetworkError::OriginPortMismatch { + requested_port: socket_address.port(), + expected_port, + }); } let connection_plan = ConnectionPlan::new( resolution.resolution_snapshot(), From 3739602e096d708974349fd4e4e9695957aa12e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 18:14:01 +0900 Subject: [PATCH 21/43] test(network): cover origin port mismatch error contract --- crates/originweave-network/tests/fresh_resolution_plan.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index 5c0726c7a..db1a6fd79 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -234,6 +234,7 @@ fn connection_plan_rejects_socket_port_that_changes_default_origin() -> Result<( "socket port 8080 does not match canonical origin port 80" ); assert!(error.source().is_none()); + assert_eq!(error.attempt_count(), None); Ok(()) } From 221bd05e6f73703151f8f2f454a5f060ffbf5b7a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 18:27:15 +0900 Subject: [PATCH 22/43] test(network): close exact coverage gaps --- .../tests/fresh_resolution_plan.rs | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/fresh_resolution_plan.rs b/crates/originweave-network/tests/fresh_resolution_plan.rs index db1a6fd79..93f38c063 100644 --- a/crates/originweave-network/tests/fresh_resolution_plan.rs +++ b/crates/originweave-network/tests/fresh_resolution_plan.rs @@ -7,7 +7,7 @@ use originweave_core::Origin; use originweave_destination::{ AddressClass, DestinationError, DestinationPolicy, FreshResolutionSnapshot, }; -use originweave_network::{FreshConnectionPlan, NetworkError}; +use originweave_network::{FreshConnectionPlan, MAX_CONNECTION_ATTEMPTS, NetworkError}; fn fresh_loopback_snapshot_for_port(port: u16) -> Result { let origin = Origin::parse(&format!("http://localhost:{port}")) @@ -187,21 +187,75 @@ fn fresh_resolution_still_requires_valid_connection_parameters() -> Result<(), S let snapshot = fresh_default_loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80); - let result = FreshConnectionPlan::new( + let zero_timeout = FreshConnectionPlan::new( &snapshot, Duration::from_secs(12), socket, Duration::ZERO, 1, ); - assert!(matches!( - result, + zero_timeout, Err(NetworkError::InvalidConnectTimeout { connect_timeout, .. }) if connect_timeout == Duration::ZERO )); + + let zero_attempts = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::from_secs(1), + 0, + ); + assert!(matches!( + zero_attempts, + Err(NetworkError::InvalidAttemptCount { + attempt_count: 0, + .. + }) + )); + + let excessive_attempts = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::from_secs(1), + MAX_CONNECTION_ATTEMPTS + 1, + ); + assert!(matches!( + excessive_attempts, + Err(NetworkError::InvalidAttemptCount { + attempt_count, + maximum_attempts: MAX_CONNECTION_ATTEMPTS, + }) if attempt_count == MAX_CONNECTION_ATTEMPTS + 1 + )); + Ok(()) +} + +#[test] +fn fresh_resolution_rejects_noncanonical_mapped_socket() -> Result<(), String> { + let snapshot = fresh_default_loopback_snapshot()?; + let mapped_loopback = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0x7f00, 1); + let socket = SocketAddr::new(IpAddr::V6(mapped_loopback), 80); + + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::from_secs(1), + 1, + ); + + assert!(matches!( + result, + Err(NetworkError::NonCanonicalSocketAddress { + socket_address, + canonical_address, + }) if socket_address == socket + && canonical_address == IpAddr::V4(Ipv4Addr::LOCALHOST) + )); Ok(()) } From 143394ac7a915d9c1f3d9f458b2862c994331466 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 19:07:53 +0900 Subject: [PATCH 23/43] test(network): cover origin port error contract in unit crate --- .../src/fresh_connection.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 76d0e067d..30946ff44 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -144,9 +144,11 @@ impl FreshConnectionPlan { #[cfg(test)] mod tests { + use std::error::Error; + use originweave_core::Origin; - use super::effective_origin_port; + use super::{NetworkError, effective_origin_port}; #[test] fn effective_origin_port_covers_default_and_explicit_authorities() { @@ -162,4 +164,19 @@ mod tests { assert_eq!(actual_port.ok(), Some(expected_port)); } } + + #[test] + fn origin_port_mismatch_error_is_deterministic_and_source_free() { + let error = NetworkError::OriginPortMismatch { + requested_port: 8080, + expected_port: 80, + }; + + assert_eq!( + error.to_string(), + "socket port 8080 does not match canonical origin port 80" + ); + assert!(error.source().is_none()); + assert_eq!(error.attempt_count(), None); + } } From 0d0fbba7fc42d31cc3b2b0d8f6f9d2eab742b968 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 18:39:13 +0900 Subject: [PATCH 24/43] test(network): preserve zero-port input error precedence --- .../tests/fresh_resolution_port_error.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 crates/originweave-network/tests/fresh_resolution_port_error.rs diff --git a/crates/originweave-network/tests/fresh_resolution_port_error.rs b/crates/originweave-network/tests/fresh_resolution_port_error.rs new file mode 100644 index 000000000..eb56d471f --- /dev/null +++ b/crates/originweave-network/tests/fresh_resolution_port_error.rs @@ -0,0 +1,32 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::time::Duration; + +use originweave_core::Origin; +use originweave_destination::{AddressClass, DestinationPolicy, FreshResolutionSnapshot}; +use originweave_network::{FreshConnectionPlan, NetworkError}; + +#[test] +fn zero_socket_port_remains_invalid_input_before_origin_mismatch() -> Result<(), String> { + let origin = Origin::parse("http://localhost") + .map_err(|error| format!("default loopback origin is invalid: {error:?}"))?; + let snapshot = FreshResolutionSnapshot::approve( + origin, + [IpAddr::V4(Ipv4Addr::LOCALHOST)], + &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), + Duration::from_secs(10), + Duration::from_secs(5), + ) + .map_err(|error| format!("fresh loopback snapshot is invalid: {error}"))?; + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); + + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + socket, + Duration::from_secs(1), + 1, + ); + + assert!(matches!(result, Err(NetworkError::InvalidPort))); + Ok(()) +} From 25315b24c2598a60580389651abd5f94d5c285e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 18:42:07 +0900 Subject: [PATCH 25/43] fix(network): reject zero port before origin binding --- crates/originweave-network/src/fresh_connection.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 30946ff44..6063d7275 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -51,6 +51,9 @@ impl FreshConnectionPlan { connect_timeout: Duration, maximum_attempts: u8, ) -> Result { + if socket_address.port() == 0 { + return Err(NetworkError::InvalidPort); + } let fresh_evidence = resolution .authorize_connection(socket_address.ip(), current_time) .map_err(|source| NetworkError::DestinationNotApproved { From 347cf6a6077103294afd52a1c5ad91e3b406bd8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 18:46:50 +0900 Subject: [PATCH 26/43] docs(changelog): preserve zero-port input classification --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0a6c469..817a0b9e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - Fresh-resolution direct TCP plans require the requested socket port to equal the canonical origin's effective HTTP or HTTPS port, preventing same-IP authority from widening to a different service. +- Fresh-resolution direct TCP plans reject socket port zero before consulting resolution or origin-port state, preserving `InvalidPort` for malformed input instead of misclassifying it as an authority mismatch. - 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 8d27a44b4e9f2cc206f45d31016f9853c2ce7f75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 18:49:12 +0900 Subject: [PATCH 27/43] test(network): reject malformed settings before authority checks --- .../tests/fresh_resolution_port_error.rs | 74 ++++++++++++++++++- 1 file changed, 70 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/fresh_resolution_port_error.rs b/crates/originweave-network/tests/fresh_resolution_port_error.rs index eb56d471f..bc07667bb 100644 --- a/crates/originweave-network/tests/fresh_resolution_port_error.rs +++ b/crates/originweave-network/tests/fresh_resolution_port_error.rs @@ -5,18 +5,22 @@ use originweave_core::Origin; use originweave_destination::{AddressClass, DestinationPolicy, FreshResolutionSnapshot}; use originweave_network::{FreshConnectionPlan, NetworkError}; -#[test] -fn zero_socket_port_remains_invalid_input_before_origin_mismatch() -> Result<(), String> { +fn loopback_snapshot() -> Result { let origin = Origin::parse("http://localhost") .map_err(|error| format!("default loopback origin is invalid: {error:?}"))?; - let snapshot = FreshResolutionSnapshot::approve( + FreshResolutionSnapshot::approve( origin, [IpAddr::V4(Ipv4Addr::LOCALHOST)], &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), Duration::from_secs(10), Duration::from_secs(5), ) - .map_err(|error| format!("fresh loopback snapshot is invalid: {error}"))?; + .map_err(|error| format!("fresh loopback snapshot is invalid: {error}")) +} + +#[test] +fn zero_socket_port_remains_invalid_input_before_origin_mismatch() -> Result<(), String> { + let snapshot = loopback_snapshot()?; let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0); let result = FreshConnectionPlan::new( @@ -30,3 +34,65 @@ fn zero_socket_port_remains_invalid_input_before_origin_mismatch() -> Result<(), assert!(matches!(result, Err(NetworkError::InvalidPort))); Ok(()) } + +#[test] +fn malformed_connection_settings_fail_before_origin_port_authority() -> Result<(), String> { + let snapshot = loopback_snapshot()?; + let wrong_port_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 81); + + let invalid_timeout = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + wrong_port_socket, + Duration::ZERO, + 1, + ); + assert!(matches!( + invalid_timeout, + Err(NetworkError::InvalidConnectTimeout { .. }) + )); + + let invalid_attempt_count = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + wrong_port_socket, + Duration::from_secs(1), + 0, + ); + assert!(matches!( + invalid_attempt_count, + Err(NetworkError::InvalidAttemptCount { .. }) + )); + Ok(()) +} + +#[test] +fn malformed_connection_settings_fail_before_resolution_membership() -> Result<(), String> { + let snapshot = loopback_snapshot()?; + let unapproved_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 80); + + let invalid_timeout = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + unapproved_socket, + Duration::ZERO, + 1, + ); + assert!(matches!( + invalid_timeout, + Err(NetworkError::InvalidConnectTimeout { .. }) + )); + + let invalid_attempt_count = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + unapproved_socket, + Duration::from_secs(1), + 0, + ); + assert!(matches!( + invalid_attempt_count, + Err(NetworkError::InvalidAttemptCount { .. }) + )); + Ok(()) +} From f1188339638cf168a81d3fe822b6b86e52fbd798 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 18:56:14 +0900 Subject: [PATCH 28/43] fix(network): validate settings before authority checks --- .../originweave-network/src/fresh_connection.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 6063d7275..666cec18d 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -4,7 +4,10 @@ use std::time::{Duration, Instant}; use originweave_core::Origin; use originweave_destination::{DestinationError, FreshResolutionSnapshot}; -use crate::connection::{ConnectionPlan, DirectTcpConnection, NetworkError}; +use crate::connection::{ + ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, + NetworkError, +}; fn effective_origin_port(origin: &Origin) -> u16 { let default_port = match origin.scheme() { @@ -54,6 +57,18 @@ impl FreshConnectionPlan { if socket_address.port() == 0 { return Err(NetworkError::InvalidPort); } + if connect_timeout.is_zero() || connect_timeout > MAX_CONNECT_TIMEOUT { + return Err(NetworkError::InvalidConnectTimeout { + connect_timeout, + maximum_timeout: MAX_CONNECT_TIMEOUT, + }); + } + if maximum_attempts == 0 || maximum_attempts > MAX_CONNECTION_ATTEMPTS { + return Err(NetworkError::InvalidAttemptCount { + attempt_count: maximum_attempts, + maximum_attempts: MAX_CONNECTION_ATTEMPTS, + }); + } let fresh_evidence = resolution .authorize_connection(socket_address.ip(), current_time) .map_err(|source| NetworkError::DestinationNotApproved { From 0504951d3a72630d105ceacaf0002bc4e5989226 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:04:02 +0900 Subject: [PATCH 29/43] style(network): apply canonical validation import formatting --- crates/originweave-network/src/fresh_connection.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 666cec18d..c9e25f877 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -5,8 +5,7 @@ use originweave_core::Origin; use originweave_destination::{DestinationError, FreshResolutionSnapshot}; use crate::connection::{ - ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, - NetworkError, + ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, NetworkError, }; fn effective_origin_port(origin: &Origin) -> u16 { From 63576fb8e6594b47db2b0de6244b262f0aebc401 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 19:10:03 +0900 Subject: [PATCH 30/43] test(network): cover fresh-plan validation upper bounds --- .../tests/fresh_resolution_port_error.rs | 102 ++++++++++-------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/crates/originweave-network/tests/fresh_resolution_port_error.rs b/crates/originweave-network/tests/fresh_resolution_port_error.rs index bc07667bb..1d38010c4 100644 --- a/crates/originweave-network/tests/fresh_resolution_port_error.rs +++ b/crates/originweave-network/tests/fresh_resolution_port_error.rs @@ -3,7 +3,9 @@ use std::time::Duration; use originweave_core::Origin; use originweave_destination::{AddressClass, DestinationPolicy, FreshResolutionSnapshot}; -use originweave_network::{FreshConnectionPlan, NetworkError}; +use originweave_network::{ + FreshConnectionPlan, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, NetworkError, +}; fn loopback_snapshot() -> Result { let origin = Origin::parse("http://localhost") @@ -40,29 +42,36 @@ fn malformed_connection_settings_fail_before_origin_port_authority() -> Result<( let snapshot = loopback_snapshot()?; let wrong_port_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 81); - let invalid_timeout = FreshConnectionPlan::new( - &snapshot, - Duration::from_secs(12), - wrong_port_socket, + for invalid_timeout in [ Duration::ZERO, - 1, - ); - assert!(matches!( - invalid_timeout, - Err(NetworkError::InvalidConnectTimeout { .. }) - )); + MAX_CONNECT_TIMEOUT.saturating_add(Duration::from_nanos(1)), + ] { + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + wrong_port_socket, + invalid_timeout, + 1, + ); + assert!(matches!( + result, + Err(NetworkError::InvalidConnectTimeout { .. }) + )); + } - let invalid_attempt_count = FreshConnectionPlan::new( - &snapshot, - Duration::from_secs(12), - wrong_port_socket, - Duration::from_secs(1), - 0, - ); - assert!(matches!( - invalid_attempt_count, - Err(NetworkError::InvalidAttemptCount { .. }) - )); + for invalid_attempt_count in [0, MAX_CONNECTION_ATTEMPTS + 1] { + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + wrong_port_socket, + Duration::from_secs(1), + invalid_attempt_count, + ); + assert!(matches!( + result, + Err(NetworkError::InvalidAttemptCount { .. }) + )); + } Ok(()) } @@ -71,28 +80,35 @@ fn malformed_connection_settings_fail_before_resolution_membership() -> Result<( let snapshot = loopback_snapshot()?; let unapproved_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), 80); - let invalid_timeout = FreshConnectionPlan::new( - &snapshot, - Duration::from_secs(12), - unapproved_socket, + for invalid_timeout in [ Duration::ZERO, - 1, - ); - assert!(matches!( - invalid_timeout, - Err(NetworkError::InvalidConnectTimeout { .. }) - )); + MAX_CONNECT_TIMEOUT.saturating_add(Duration::from_nanos(1)), + ] { + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + unapproved_socket, + invalid_timeout, + 1, + ); + assert!(matches!( + result, + Err(NetworkError::InvalidConnectTimeout { .. }) + )); + } - let invalid_attempt_count = FreshConnectionPlan::new( - &snapshot, - Duration::from_secs(12), - unapproved_socket, - Duration::from_secs(1), - 0, - ); - assert!(matches!( - invalid_attempt_count, - Err(NetworkError::InvalidAttemptCount { .. }) - )); + for invalid_attempt_count in [0, MAX_CONNECTION_ATTEMPTS + 1] { + let result = FreshConnectionPlan::new( + &snapshot, + Duration::from_secs(12), + unapproved_socket, + Duration::from_secs(1), + invalid_attempt_count, + ); + assert!(matches!( + result, + Err(NetworkError::InvalidAttemptCount { .. }) + )); + } Ok(()) } From 035488e2c8f7d99d19dc29151ff2f43ad3adfb73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 20:21:49 +0900 Subject: [PATCH 31/43] test(network): cover plan-admission freshness interval --- .../src/fresh_connection.rs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index c9e25f877..b5a1b49f1 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -162,10 +162,15 @@ impl FreshConnectionPlan { #[cfg(test)] mod tests { use std::error::Error; + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + use std::time::{Duration, Instant}; use originweave_core::Origin; + use originweave_destination::{ + AddressClass, DestinationError, DestinationPolicy, FreshResolutionSnapshot, + }; - use super::{NetworkError, effective_origin_port}; + use super::{FreshConnectionPlan, NetworkError, effective_origin_port}; #[test] fn effective_origin_port_covers_default_and_explicit_authorities() { @@ -182,6 +187,44 @@ mod tests { } } + #[test] + fn compatibility_anchor_includes_time_spent_before_plan_completion() -> Result<(), String> { + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); + let origin = Origin::parse("http://localhost:9") + .map_err(|error| format!("loopback origin fixture is invalid: {error:?}"))?; + let snapshot = FreshResolutionSnapshot::approve( + origin, + [IpAddr::V4(Ipv4Addr::LOCALHOST)], + &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), + Duration::from_secs(10), + Duration::from_millis(1), + ) + .map_err(|error| format!("short-lived snapshot is invalid: {error}"))?; + let authorization_started_at = Instant::now() + .checked_sub(Duration::from_millis(5)) + .ok_or_else(|| "process monotonic clock cannot represent the test interval".to_owned())?; + + let plan = FreshConnectionPlan::new_with_authorization_instant( + &snapshot, + Duration::from_secs(10), + socket, + Duration::from_secs(1), + 1, + authorization_started_at, + ) + .map_err(|error| format!("authorize short-lived connection plan: {error}"))?; + + let result = plan.connect(); + assert!(matches!( + result, + Err(NetworkError::DestinationNotApproved { + source: DestinationError::ResolutionApprovalExpired { .. }, + .. + }) + )); + Ok(()) + } + #[test] fn origin_port_mismatch_error_is_deterministic_and_source_free() { let error = NetworkError::OriginPortMismatch { From e2779575d916a709178af2008e8d5df484e6efde Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 20:23:17 +0900 Subject: [PATCH 32/43] style(network): apply canonical rustfmt to freshness regression --- crates/originweave-network/src/fresh_connection.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index b5a1b49f1..4f572eb06 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -202,7 +202,9 @@ mod tests { .map_err(|error| format!("short-lived snapshot is invalid: {error}"))?; let authorization_started_at = Instant::now() .checked_sub(Duration::from_millis(5)) - .ok_or_else(|| "process monotonic clock cannot represent the test interval".to_owned())?; + .ok_or_else(|| { + "process monotonic clock cannot represent the test interval".to_owned() + })?; let plan = FreshConnectionPlan::new_with_authorization_instant( &snapshot, From a332f08120a49bc8897dd92b3bd313bbc94ea1f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:19:42 +0900 Subject: [PATCH 33/43] fix(network): anchor freshness before plan construction --- .../src/fresh_connection.rs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 4f572eb06..14bca4300 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -52,6 +52,25 @@ impl FreshConnectionPlan { socket_address: SocketAddr, connect_timeout: Duration, maximum_attempts: u8, + ) -> Result { + let authorization_started_at = Instant::now(); + Self::new_with_authorization_instant( + resolution, + current_time, + socket_address, + connect_timeout, + maximum_attempts, + authorization_started_at, + ) + } + + fn new_with_authorization_instant( + resolution: &FreshResolutionSnapshot, + current_time: Duration, + socket_address: SocketAddr, + connect_timeout: Duration, + maximum_attempts: u8, + authorization_started_at: Instant, ) -> Result { if socket_address.port() == 0 { return Err(NetworkError::InvalidPort); @@ -94,7 +113,7 @@ impl FreshConnectionPlan { resolution_approved_at: fresh_evidence.resolution_approved_at(), resolution_valid_until: fresh_evidence.resolution_valid_until(), resolution_authorized_at: fresh_evidence.authorized_at(), - authorized_instant: Instant::now(), + authorized_instant: authorization_started_at, }) } From c3734fa1f5fb334ef6e8eed07df25d602980d674 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:22:08 +0900 Subject: [PATCH 34/43] fix(network): preserve current prerequisite while binding origin port --- CHANGELOG.md | 5 +- crates/originweave-core/src/lib.rs | 34 ++++- .../tests/extension_authority.rs | 119 +++++++++++++++++- docs/TRD.md | 2 +- .../0013-manifest-v3-extension-authority.md | 2 +- docs/doctoring.md | 12 ++ .../extension-authority-security.md | 6 + 7 files changed, 170 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 817a0b9e8..4ad462e6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,15 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. +- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. -- Fresh-resolution network adapter that consumes a validated `FreshResolutionSnapshot` plus trusted monotonic authority, retains credential-free approval/validity/authorization timestamps, and reauthorizes the same exact address immediately before socket I/O. Explicit `connect_at` callers supply current trusted monotonic time; the compatibility `connect` path anchors a process-local monotonic instant at authorization so delayed plans also expire instead of replaying stale resolution authority. +- Fresh-resolution network adapter that consumes a validated `FreshResolutionSnapshot` plus caller-supplied trusted monotonic time before constructing the existing exact-socket `ConnectionPlan`, retains credential-free approval/validity/authorization timestamps, reauthorizes the same exact address immediately before socket I/O, and anchors compatibility-path elapsed time at the start of plan admission so construction latency cannot widen the freshness window. - Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves. - Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior and `NotConfigured` revocation evidence. - Credential-free TLS evidence containing canonical origin, requested and observed peer, DNS/IP reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. @@ -61,6 +63,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - Fresh-resolution direct TCP plans require the requested socket port to equal the canonical origin's effective HTTP or HTTPS port, preventing same-IP authority from widening to a different service. - Fresh-resolution direct TCP plans reject socket port zero before consulting resolution or origin-port state, preserving `InvalidPort` for malformed input instead of misclassifying it as an authority mismatch. +- Compatibility fresh-resolution plans start their process-local elapsed-time anchor before validation and exact-socket plan construction, so admission work cannot silently extend a short-lived resolution authority window. - 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. diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 88dd2e586..b6ed55ff2 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -967,16 +967,20 @@ pub struct ExtensionAgentGrant { extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, + expires_at_epoch_seconds: u64, capabilities: BTreeSet, } impl ExtensionAgentGrant { - /// Build an exact extension-to-Agent grant for one browser session and context. + /// Build an exact extension-to-Agent grant for one session, context, origin, and exclusive expiry. #[must_use] pub fn new( extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, + expires_at_epoch_seconds: u64, capabilities: I, ) -> Self where @@ -986,6 +990,8 @@ impl ExtensionAgentGrant { extension_id, browser_session, browsing_context, + origin, + expires_at_epoch_seconds, capabilities: capabilities.into_iter().collect(), } } @@ -997,22 +1003,31 @@ pub struct ExtensionAccessRequest { extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, + now_epoch_seconds: u64, capability: ExtensionAgentCapability, } impl ExtensionAccessRequest { /// Build one exact extension capability request without granting authority. + /// + /// `now_epoch_seconds` must be trusted evaluation time supplied by the host, + /// not a page, extension, or model clock. #[must_use] pub const fn new( extension_id: ExtensionId, browser_session: BrowserSessionId, browsing_context: BrowsingContextId, + origin: Origin, + now_epoch_seconds: u64, capability: ExtensionAgentCapability, ) -> Self { Self { extension_id, browser_session, browsing_context, + origin, + now_epoch_seconds, capability, } } @@ -1021,7 +1036,7 @@ impl ExtensionAccessRequest { /// Result of evaluating an extension request against one explicit Agent grant. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ExtensionAccessDecision { - /// The exact extension, session, context, and capability are explicitly granted. + /// The exact extension, session, context, origin, unexpired grant, and capability are explicitly granted. Allow, /// No explicit extension-to-Agent grant was supplied. DenyMissingGrant, @@ -1031,6 +1046,10 @@ pub enum ExtensionAccessDecision { DenyBrowserSessionMismatch, /// The request belongs to a different independently navigable browser context. DenyBrowsingContextMismatch, + /// The request belongs to a different canonical origin than the grant. + DenyOriginMismatch, + /// Trusted evaluation time is at or after the grant's exclusive expiry. + DenyExpired, /// The extension grant does not contain the requested OriginWeave capability. DenyCapabilityNotGranted, } @@ -1039,8 +1058,9 @@ pub enum ExtensionAccessDecision { /// /// A Chrome extension permission, installation state, or page capability is never /// consulted here. A future Chromium adapter must construct a host-originated -/// [`ExtensionAgentGrant`] explicitly and re-evaluate the exact session/context -/// request at the boundary where Agent authority would otherwise cross. +/// [`ExtensionAgentGrant`] explicitly and re-evaluate the exact session, context, +/// canonical origin, and exclusive expiry at the boundary where Agent authority +/// would otherwise cross. #[must_use] pub fn evaluate_extension_access( request: &ExtensionAccessRequest, @@ -1058,6 +1078,12 @@ pub fn evaluate_extension_access( if request.browsing_context != grant.browsing_context { return ExtensionAccessDecision::DenyBrowsingContextMismatch; } + if request.origin != grant.origin { + return ExtensionAccessDecision::DenyOriginMismatch; + } + if request.now_epoch_seconds >= grant.expires_at_epoch_seconds { + return ExtensionAccessDecision::DenyExpired; + } if !grant.capabilities.contains(&request.capability) { return ExtensionAccessDecision::DenyCapabilityNotGranted; } diff --git a/crates/originweave-core/tests/extension_authority.rs b/crates/originweave-core/tests/extension_authority.rs index 82507a244..f34c30e9b 100644 --- a/crates/originweave-core/tests/extension_authority.rs +++ b/crates/originweave-core/tests/extension_authority.rs @@ -2,7 +2,7 @@ use originweave_core::{ BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest, - ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, evaluate_extension_access, + ExtensionAgentCapability, ExtensionAgentGrant, ExtensionId, Origin, evaluate_extension_access, }; fn extension_id(value: &str) -> ExtensionId { @@ -17,6 +17,13 @@ fn context(value: u64) -> BrowsingContextId { BrowsingContextId::new(value).expect("nonzero browsing context") } +fn origin(value: &str) -> Origin { + Origin::parse(value).expect("canonical origin") +} + +const UNEXPIRED_NOW_EPOCH_SECONDS: u64 = 1_700_000_000; +const UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS: u64 = 1_700_000_600; + #[test] fn extension_id_accepts_only_canonical_chromium_extension_ids() { let canonical = "abcdefghijklmnopabcdefghijklmnop"; @@ -43,10 +50,13 @@ fn extension_id_accepts_only_canonical_chromium_extension_ids() { fn extension_agent_access_requires_an_explicit_exact_grant() { let allowed_extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); let other_extension = extension_id("bcdefghijklmnopabcdefghijklmnopa"); + let granted_origin = origin("https://app.example"); let grant = ExtensionAgentGrant::new( allowed_extension.clone(), session(7), context(11), + granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ExtensionAgentCapability::ObserveCurrentContext], ); @@ -54,6 +64,8 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { allowed_extension.clone(), session(7), context(11), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -68,6 +80,8 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { other_extension, session(7), context(11), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -79,6 +93,8 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { allowed_extension.clone(), session(8), context(11), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( @@ -87,24 +103,55 @@ fn extension_agent_access_requires_an_explicit_exact_grant() { ); let wrong_context = ExtensionAccessRequest::new( - allowed_extension, + allowed_extension.clone(), session(7), context(12), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ObserveCurrentContext, ); assert_eq!( evaluate_extension_access(&wrong_context, Some(&grant)), ExtensionAccessDecision::DenyBrowsingContextMismatch ); + + let wrong_origin = ExtensionAccessRequest::new( + allowed_extension.clone(), + session(7), + context(11), + origin("https://other.example"), + UNEXPIRED_NOW_EPOCH_SECONDS, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&wrong_origin, Some(&grant)), + ExtensionAccessDecision::DenyOriginMismatch + ); + + let wrong_port = ExtensionAccessRequest::new( + allowed_extension, + session(7), + context(11), + origin("https://app.example:8443"), + UNEXPIRED_NOW_EPOCH_SECONDS, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&wrong_port, Some(&grant)), + ExtensionAccessDecision::DenyOriginMismatch + ); } #[test] fn chrome_permissions_never_imply_originweave_agent_capabilities() { let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("https://mail.example"); let grant = ExtensionAgentGrant::new( id.clone(), session(3), context(5), + granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ExtensionAgentCapability::ObserveCurrentContext], ); @@ -112,6 +159,8 @@ fn chrome_permissions_never_imply_originweave_agent_capabilities() { id, session(3), context(5), + granted_origin, + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ProposeTypedAction, ); assert_eq!( @@ -123,10 +172,13 @@ fn chrome_permissions_never_imply_originweave_agent_capabilities() { #[test] fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("http://127.0.0.1:8080"); let grant = ExtensionAgentGrant::new( id.clone(), session(13), context(17), + granted_origin.clone(), + UNEXPIRED_EXPIRES_AT_EPOCH_SECONDS, [ ExtensionAgentCapability::ObserveCurrentContext, ExtensionAgentCapability::ProposeTypedAction, @@ -137,10 +189,71 @@ fn explicit_grant_can_authorize_multiple_bounded_agent_capabilities() { ExtensionAgentCapability::ObserveCurrentContext, ExtensionAgentCapability::ProposeTypedAction, ] { - let request = ExtensionAccessRequest::new(id.clone(), session(13), context(17), capability); + let request = ExtensionAccessRequest::new( + id.clone(), + session(13), + context(17), + granted_origin.clone(), + UNEXPIRED_NOW_EPOCH_SECONDS, + capability, + ); assert_eq!( evaluate_extension_access(&request, Some(&grant)), ExtensionAccessDecision::Allow ); } } + +#[test] +fn expired_origin_bound_grant_cannot_be_reused_after_exclusive_deadline() { + let id = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let granted_origin = origin("https://billing.example"); + let expires_at_epoch_seconds = 1_700_000_100; + let grant = ExtensionAgentGrant::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds, + [ExtensionAgentCapability::ObserveCurrentContext], + ); + + let before_deadline = ExtensionAccessRequest::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds - 1, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&before_deadline, Some(&grant)), + ExtensionAccessDecision::Allow + ); + + let at_deadline = ExtensionAccessRequest::new( + id.clone(), + session(19), + context(23), + granted_origin.clone(), + expires_at_epoch_seconds, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&at_deadline, Some(&grant)), + ExtensionAccessDecision::DenyExpired + ); + + let after_deadline = ExtensionAccessRequest::new( + id, + session(19), + context(23), + granted_origin, + expires_at_epoch_seconds + 1, + ExtensionAgentCapability::ObserveCurrentContext, + ); + assert_eq!( + evaluate_extension_access(&after_deadline, Some(&grant)), + ExtensionAccessDecision::DenyExpired + ); +} diff --git a/docs/TRD.md b/docs/TRD.md index 3e8030012..0e60e5ca5 100644 --- a/docs/TRD.md +++ b/docs/TRD.md @@ -25,7 +25,7 @@ The current reusable Rust control plane is intentionally smaller than the final | Module / boundary | Current responsibility | Protected-main status | Active/non-shipped evidence | |---|---|---|---| -| `originweave-core` | Canonical origin, typed actions, purpose/mode, capabilities, risk, secret-delivery, approval, session/context/document/node authority values. | **Implemented** | PR #40 builds a protocol-ID registry on top of these values; it is not protected-main truth | +| `originweave-core` | Canonical origin, typed actions, purpose/mode, capabilities, risk, secret-delivery, approval, session/context/document/node authority values. | **Implemented** | Active origin-bound `ExtensionAgentGrant` evaluation adds canonical-origin matching and exclusive trusted-time expiry; it is not protected-main truth until merge | | `originweave-policy` | Pure fail-closed action policy including purpose-bound sensitive-data authority. | **Implemented** | Trusted broker/runtime lifecycle remains separate planned work under issue #10 | | `originweave-destination` | Resolved-address classification, origin-bound snapshots, route authority, connection pinning, rebinding and redirect authority. | **Implemented** | PAC evaluation/proxy transport/CONNECT are still Planned | | `originweave-network` | Direct single-address TCP connection plan and exact operating-system peer verification. | **Implemented** | — | diff --git a/docs/adr/0013-manifest-v3-extension-authority.md b/docs/adr/0013-manifest-v3-extension-authority.md index e620edf9d..8feacbf27 100644 --- a/docs/adr/0013-manifest-v3-extension-authority.md +++ b/docs/adr/0013-manifest-v3-extension-authority.md @@ -92,7 +92,7 @@ No persistent database migration is introduced. A release can roll back the Chro ## Open follow-ups -- Complete issue #27's compatibility matrix and production isolation acceptance. +- Complete issue #27's compatibility matrix and production isolation acceptance. Exclusive trusted-time expiry on origin-bound `ExtensionAgentGrant` evaluation is the next protected-main candidate; task identity binding remains open. - Define managed-extension identity/update semantics. - Implement the native-messaging allow-list/process boundary before claiming support. - Integrate the complete Agent Task browser vertical slice under issue #28. diff --git a/docs/doctoring.md b/docs/doctoring.md index 75c107ef0..f0133bb5d 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -14,6 +14,14 @@ The WHATWG URL host parser and Chromium canonicalizer classify shortened decimal The exact Chromium regression evidence is pinned to revision `446d05d21720f0b3505ec21057b3e9f909784262`. A mutable `HEAD` reference is not sufficient for a reproducible security contract. +### Extension-to-Agent grant origin binding + +RFC 6454 defines a web origin as the scheme, host, and port tuple that browsers use to isolate authority. An OriginWeave `extension_grant` that is bound only to extension identity, session, and browsing context would remain valid after the same context navigates to another origin. OriginWeave therefore requires the grant and the request to carry the same canonical origin. A host change or a non-default port change is a different origin and cannot reuse the grant. This is grant-scope isolation only; it does not install an extension, parse Chrome messages, or mint Agent capabilities from Manifest V3 permissions. + +### Extension-to-Agent grant exclusive expiry + +RFC 9700 is the current Best Current Practice for OAuth 2.0 security. It requires access tokens to be restricted in lifetime and treats long-lived bearer credentials as a standing authorization risk. An OriginWeave `extension_grant` that matches extension identity, session, browsing context, and canonical origin but has no exclusive expiry remains usable after the Agent Task window ends. OriginWeave therefore requires the grant to carry an exclusive `expires_at_epoch_seconds` deadline and the request to carry trusted `now_epoch_seconds`. Evaluation fails closed when `now >= expires_at`, matching the existing sensitive-handle exclusive-expiry rule. Page, extension, and model clocks are not trusted time. This slice does not bind task identity, install an extension, or mint Agent capabilities from Manifest V3 permissions. + ### Resolved destination and redirect safety Canonical origin identity is not a network-destination authorization. The IANA IPv4 and IPv6 Special-Purpose Address Space registries enumerate blocks whose source, destination, forwardability, globally reachable, and protocol-reserved properties differ. Both registries were last updated on 9 October 2025 and explicitly warn that registry presence does not guarantee routability in a particular local or global context. RFC 6890 established the common special-purpose registry fields, and RFC 8190 replaced the ambiguous `global` field with `globally reachable`. @@ -96,6 +104,8 @@ Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retriev Autio, C., Schwartz, R., Dunietz, J., Jain, S., Stanley, M., Tabassi, E., Hall, P., & Roberts, K. (2024). *Artificial intelligence risk management framework: Generative artificial intelligence profile* (NIST AI 600-1). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.AI.600-1 +Barth, A. (2011). *The web origin concept* (RFC 6454). Internet Engineering Task Force. https://doi.org/10.17487/RFC6454 + Bonica, R., Cotton, M., Haberman, B., & Vegoda, L. (2017). *Updates to the special-purpose IP address registries* (RFC 8190). Internet Engineering Task Force. https://doi.org/10.17487/RFC8190 Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chromium. https://chromium.googlesource.com/chromium/src/+/a3e71ebfa307d8760eb68b777e2998a869940092/net/docs/proxy.md @@ -128,6 +138,8 @@ International Organization for Standardization. (2017). *Information and documen Koster, M., Illyes, G., Zeller, H., & Sassman, L. (2022). *Robots Exclusion Protocol* (RFC 9309). Internet Engineering Task Force. https://doi.org/10.17487/RFC9309 +Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *OAuth 2.0 security best current practice* (RFC 9700). Internet Engineering Task Force. https://doi.org/10.17487/RFC9700 + Microsoft. (2025, July 25). *Azure IP address 168.63.129.16 overview*. Microsoft Learn. https://learn.microsoft.com/azure/virtual-network/what-is-ip-address-168-63-129-16 Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04388 diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index a36380a31..1c211f83d 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -50,6 +50,12 @@ Exact head `e83749acd1cf5a0b778ba38eb9d6ed5a9bd1e68f` deliberately keeps only th The exact head has successful CI, exact owned production coverage, Security Scan, SAST and CodeRabbit status and is Ready for review. It has no raw secret bytes and does not create approval evidence, a broker, browser-fill adapter, protected-value store, KMS path, authenticated workload identity, persistence owner, or release claim. +### Origin-bound extension grant evaluation + +**Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` + +The current origin-binding slice requires `ExtensionAgentGrant` and `ExtensionAccessRequest` to carry the same canonical origin. A same-session, same-context request for `https://other.example` or `https://app.example:8443` against a grant for `https://app.example` is `DenyOriginMismatch`. Exclusive trusted-time expiry is evaluated after that origin match: `now >= expires_at` is `DenyExpired`. This does not install an extension, parse Chrome messages, bind task identity, or mint Agent capabilities from Manifest V3 permissions. + ## 4. Security interpretation The executable authority chain is intentionally non-transitive: From 6f6c926867977c3db589c7b2fc5cd514e5740491 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:36:35 +0900 Subject: [PATCH 35/43] test(network): cover compatibility anchor without dead error closure --- crates/originweave-network/src/fresh_connection.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 14bca4300..2dd2c00e8 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -219,11 +219,8 @@ mod tests { Duration::from_millis(1), ) .map_err(|error| format!("short-lived snapshot is invalid: {error}"))?; - let authorization_started_at = Instant::now() - .checked_sub(Duration::from_millis(5)) - .ok_or_else(|| { - "process monotonic clock cannot represent the test interval".to_owned() - })?; + let authorization_started_at = Instant::now(); + std::thread::sleep(Duration::from_millis(5)); let plan = FreshConnectionPlan::new_with_authorization_instant( &snapshot, From c4a61075db15ca5853f74582c2a7e42f4137d1a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:40:39 +0900 Subject: [PATCH 36/43] test(network): remove uncovered setup closures from compatibility proof --- crates/originweave-network/src/fresh_connection.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 2dd2c00e8..346c85ab3 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -207,18 +207,17 @@ mod tests { } #[test] - fn compatibility_anchor_includes_time_spent_before_plan_completion() -> Result<(), String> { + fn compatibility_anchor_includes_time_spent_before_plan_completion( + ) -> Result<(), Box> { let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); - let origin = Origin::parse("http://localhost:9") - .map_err(|error| format!("loopback origin fixture is invalid: {error:?}"))?; + let origin = Origin::parse("http://localhost:9")?; let snapshot = FreshResolutionSnapshot::approve( origin, [IpAddr::V4(Ipv4Addr::LOCALHOST)], &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), Duration::from_secs(10), Duration::from_millis(1), - ) - .map_err(|error| format!("short-lived snapshot is invalid: {error}"))?; + )?; let authorization_started_at = Instant::now(); std::thread::sleep(Duration::from_millis(5)); @@ -229,8 +228,7 @@ mod tests { Duration::from_secs(1), 1, authorization_started_at, - ) - .map_err(|error| format!("authorize short-lived connection plan: {error}"))?; + )?; let result = plan.connect(); assert!(matches!( From d62435fec87db9cb35a4c9ca6d107cb3597769c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:44:02 +0900 Subject: [PATCH 37/43] style(network): apply canonical rustfmt to compatibility proof --- crates/originweave-network/src/fresh_connection.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 346c85ab3..e69a27c94 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -207,8 +207,8 @@ mod tests { } #[test] - fn compatibility_anchor_includes_time_spent_before_plan_completion( - ) -> Result<(), Box> { + fn compatibility_anchor_includes_time_spent_before_plan_completion() + -> Result<(), Box> { let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); let origin = Origin::parse("http://localhost:9")?; let snapshot = FreshResolutionSnapshot::approve( From 47742ca5c97463d944105142f911f1184c52f931 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:47:17 +0900 Subject: [PATCH 38/43] test(network): keep compatibility proof closure-free and fallible --- .../src/fresh_connection.rs | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index e69a27c94..e255ceb30 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -180,7 +180,6 @@ impl FreshConnectionPlan { #[cfg(test)] mod tests { - use std::error::Error; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::time::{Duration, Instant}; @@ -207,38 +206,49 @@ mod tests { } #[test] - fn compatibility_anchor_includes_time_spent_before_plan_completion() - -> Result<(), Box> { + fn compatibility_anchor_includes_time_spent_before_plan_completion() { let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9); - let origin = Origin::parse("http://localhost:9")?; - let snapshot = FreshResolutionSnapshot::approve( - origin, - [IpAddr::V4(Ipv4Addr::LOCALHOST)], - &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), - Duration::from_secs(10), - Duration::from_millis(1), - )?; - let authorization_started_at = Instant::now(); - std::thread::sleep(Duration::from_millis(5)); + let origin = Origin::parse("http://localhost:9"); + assert_eq!( + origin.as_ref().map(Origin::as_str), + Ok("http://localhost:9") + ); - let plan = FreshConnectionPlan::new_with_authorization_instant( - &snapshot, - Duration::from_secs(10), - socket, - Duration::from_secs(1), - 1, - authorization_started_at, - )?; + for origin in origin.into_iter() { + let snapshot = FreshResolutionSnapshot::approve( + origin, + [IpAddr::V4(Ipv4Addr::LOCALHOST)], + &DestinationPolicy::from_allowed_classes([AddressClass::Loopback]), + Duration::from_secs(10), + Duration::from_millis(1), + ); + assert!(snapshot.is_ok()); - let result = plan.connect(); - assert!(matches!( - result, - Err(NetworkError::DestinationNotApproved { - source: DestinationError::ResolutionApprovalExpired { .. }, - .. - }) - )); - Ok(()) + for snapshot in snapshot.into_iter() { + let authorization_started_at = Instant::now(); + std::thread::sleep(Duration::from_millis(5)); + let plan = FreshConnectionPlan::new_with_authorization_instant( + &snapshot, + Duration::from_secs(10), + socket, + Duration::from_secs(1), + 1, + authorization_started_at, + ); + assert!(plan.is_ok()); + + for plan in plan.into_iter() { + let result = plan.connect(); + assert!(matches!( + result, + Err(NetworkError::DestinationNotApproved { + source: DestinationError::ResolutionApprovalExpired { .. }, + .. + }) + )); + } + } + } } #[test] @@ -252,7 +262,7 @@ mod tests { error.to_string(), "socket port 8080 does not match canonical origin port 80" ); - assert!(error.source().is_none()); + assert!(std::error::Error::source(&error).is_none()); assert_eq!(error.attempt_count(), None); } } From 5f138df6734b380cced3bf2883450229f0b683ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:51:43 +0900 Subject: [PATCH 39/43] test(network): remove uncovered pattern assertion from freshness proof --- .../src/fresh_connection.rs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index e255ceb30..b8b6f9ff3 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -239,13 +239,27 @@ mod tests { for plan in plan.into_iter() { let result = plan.connect(); - assert!(matches!( - result, - Err(NetworkError::DestinationNotApproved { - source: DestinationError::ResolutionApprovalExpired { .. }, - .. - }) - )); + assert!(result.is_err()); + + for error in result.err().into_iter() { + let source = std::error::Error::source(&error); + assert!(source.is_some()); + for source in source { + let destination_error = source.downcast_ref::(); + assert!(destination_error.is_some()); + for destination_error in destination_error { + assert_eq!( + std::mem::discriminant(destination_error), + std::mem::discriminant( + &DestinationError::ResolutionApprovalExpired { + valid_until: Duration::ZERO, + current_time: Duration::ZERO, + } + ) + ); + } + } + } } } } From 76f94b094c4045b2401bb3f601364317c92e0b79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:06:28 +0900 Subject: [PATCH 40/43] test(network): satisfy strict Clippy in freshness proof --- .../src/fresh_connection.rs | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index b8b6f9ff3..2f9ed9535 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -242,23 +242,21 @@ mod tests { assert!(result.is_err()); for error in result.err().into_iter() { - let source = std::error::Error::source(&error); - assert!(source.is_some()); - for source in source { - let destination_error = source.downcast_ref::(); - assert!(destination_error.is_some()); - for destination_error in destination_error { - assert_eq!( - std::mem::discriminant(destination_error), - std::mem::discriminant( - &DestinationError::ResolutionApprovalExpired { - valid_until: Duration::ZERO, - current_time: Duration::ZERO, - } - ) - ); - } - } + let Some(source) = std::error::Error::source(&error) else { + panic!("expired compatibility path must retain its destination error source"); + }; + let Some(destination_error) = source.downcast_ref::() else { + panic!("expired compatibility path must preserve a destination error"); + }; + assert_eq!( + std::mem::discriminant(destination_error), + std::mem::discriminant( + &DestinationError::ResolutionApprovalExpired { + valid_until: Duration::ZERO, + current_time: Duration::ZERO, + } + ) + ); } } } From fb387a0cbedf71d0aad5f8fb4804c67dc3fbd406 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:08:21 +0900 Subject: [PATCH 41/43] style(network): apply canonical Rust formatting --- .../originweave-network/src/fresh_connection.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index 2f9ed9535..fe33721bd 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -243,19 +243,20 @@ mod tests { for error in result.err().into_iter() { let Some(source) = std::error::Error::source(&error) else { - panic!("expired compatibility path must retain its destination error source"); + panic!( + "expired compatibility path must retain its destination error source" + ); }; - let Some(destination_error) = source.downcast_ref::() else { + let Some(destination_error) = source.downcast_ref::() + else { panic!("expired compatibility path must preserve a destination error"); }; assert_eq!( std::mem::discriminant(destination_error), - std::mem::discriminant( - &DestinationError::ResolutionApprovalExpired { - valid_until: Duration::ZERO, - current_time: Duration::ZERO, - } - ) + std::mem::discriminant(&DestinationError::ResolutionApprovalExpired { + valid_until: Duration::ZERO, + current_time: Duration::ZERO, + }) ); } } From 1f839f2f7e1662c7760bac2b13c47f6d8e337d78 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 08:10:04 +0900 Subject: [PATCH 42/43] test(network): avoid forbidden panic in freshness regression --- .../src/fresh_connection.rs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/src/fresh_connection.rs b/crates/originweave-network/src/fresh_connection.rs index fe33721bd..7d1af709d 100644 --- a/crates/originweave-network/src/fresh_connection.rs +++ b/crates/originweave-network/src/fresh_connection.rs @@ -242,22 +242,16 @@ mod tests { assert!(result.is_err()); for error in result.err().into_iter() { - let Some(source) = std::error::Error::source(&error) else { - panic!( - "expired compatibility path must retain its destination error source" - ); - }; - let Some(destination_error) = source.downcast_ref::() - else { - panic!("expired compatibility path must preserve a destination error"); - }; - assert_eq!( - std::mem::discriminant(destination_error), - std::mem::discriminant(&DestinationError::ResolutionApprovalExpired { + let actual = std::error::Error::source(&error) + .and_then(|source| source.downcast_ref::()) + .map(std::mem::discriminant); + let expected = Some(std::mem::discriminant( + &DestinationError::ResolutionApprovalExpired { valid_until: Duration::ZERO, current_time: Duration::ZERO, - }) - ); + }, + )); + assert_eq!(actual, expected); } } } From c28b1499c99b0eac3bde100c4d6bed7a2af17c1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:12:58 +0900 Subject: [PATCH 43/43] test(tls): use explicit socket freshness time Restore the workspace TLS fixtures after the socket-use API split while preserving the explicit trusted-time authority path. Signed-off-by: Seongho Bae --- CHANGELOG.md | 2 +- crates/originweave-tls/tests/handshake_deadline.rs | 2 +- crates/originweave-tls/tests/handshake_integration.rs | 2 +- crates/originweave-tls/tests/validity_horizon_integration.rs | 2 +- docs/traceability/resolution-freshness-authority.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 605c4bcb7..315c2b81a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Bounded resolution-freshness authority with trusted monotonic approval time, capped non-zero validity, half-open use windows, non-expanding revalidation, and credential-free authorization timestamps. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. -- Fresh-resolution network adapter that consumes a validated `FreshResolutionSnapshot` plus caller-supplied trusted monotonic time before constructing the existing exact-socket `ConnectionPlan`, retains credential-free approval/validity/authorization timestamps, and preserves the original direct-connect validation and single-use stream boundary. +- Fresh-resolution network adapter that consumes a validated `FreshResolutionSnapshot` plus caller-supplied trusted monotonic time before constructing the existing exact-socket `ConnectionPlan`, rechecks freshness at actual socket use, retains credential-free approval/validity/authorization timestamps, and preserves the original direct-connect validation and single-use stream boundary. - Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves. - Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior and `NotConfigured` revocation evidence. - Credential-free TLS evidence containing canonical origin, TCP peers, reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. diff --git a/crates/originweave-tls/tests/handshake_deadline.rs b/crates/originweave-tls/tests/handshake_deadline.rs index bb2d9c722..f2f3b6d03 100644 --- a/crates/originweave-tls/tests/handshake_deadline.rs +++ b/crates/originweave-tls/tests/handshake_deadline.rs @@ -39,7 +39,7 @@ fn direct_connection( 1, ) .expect("fresh direct connection plan") - .connect(RESOLUTION_AUTHORIZED_AT) + .connect_at(RESOLUTION_AUTHORIZED_AT) .expect("loopback TCP connection") } diff --git a/crates/originweave-tls/tests/handshake_integration.rs b/crates/originweave-tls/tests/handshake_integration.rs index 4f8b6ceff..b275eac8b 100644 --- a/crates/originweave-tls/tests/handshake_integration.rs +++ b/crates/originweave-tls/tests/handshake_integration.rs @@ -153,7 +153,7 @@ fn direct_connection(origin: &Origin, socket_address: SocketAddr) -> DirectTcpCo 1, ) .expect("fresh direct connection plan") - .connect(RESOLUTION_AUTHORIZED_AT) + .connect_at(RESOLUTION_AUTHORIZED_AT) .expect("loopback TCP connection") } diff --git a/crates/originweave-tls/tests/validity_horizon_integration.rs b/crates/originweave-tls/tests/validity_horizon_integration.rs index f2049073e..35c725a42 100644 --- a/crates/originweave-tls/tests/validity_horizon_integration.rs +++ b/crates/originweave-tls/tests/validity_horizon_integration.rs @@ -123,7 +123,7 @@ fn direct_connection(origin: &Origin, socket_address: SocketAddr) -> DirectTcpCo 1, ) .expect("fresh direct connection plan") - .connect(RESOLUTION_AUTHORIZED_AT) + .connect_at(RESOLUTION_AUTHORIZED_AT) .expect("loopback TCP connection") } diff --git a/docs/traceability/resolution-freshness-authority.md b/docs/traceability/resolution-freshness-authority.md index edb91e4bb..ba37d7411 100644 --- a/docs/traceability/resolution-freshness-authority.md +++ b/docs/traceability/resolution-freshness-authority.md @@ -48,7 +48,7 @@ PR #54 follows #50 because a plan authorized within the resolution window could The accepted active-branch remedy keeps the admitted freshness snapshot with the non-cloneable single-use plan and revalidates it at the socket-use boundary. `connect_at(current_time)` is the explicit deterministic path and rejects both expiry and an authorization-time regression using the existing destination error taxonomy. The compatibility `connect()` path does not freeze the old authorization timestamp: it anchors a process-local monotonic `Instant` at plan construction, adds actual elapsed time to the admitted authorization time, and delegates to `connect_at`, so delayed legacy callers cannot replay stale authority indefinitely. -The regression suite proves explicit success, deadline expiry, trusted-time regression, unchanged connection-parameter validation, and expiry of the compatibility path with a deliberately short real monotonic interval. Current exact head `ec81031c537f2b662910c1ce78c7ae0e0bfc9c1e` passes CI run `31418337788`. This remains active-PR evidence and does not add DNS lookup, a wall-clock authority, proxy/PAC, or a resolver service. +The regression suite proves explicit success, deadline expiry, trusted-time regression, unchanged connection-parameter validation, and expiry of the compatibility path with a deliberately short real monotonic interval. A later stack convergence exposed three TLS integration fixtures that still called the compatibility method with an explicit time: exact-head CI run `32858865789` failed both workspace compilation and coverage on that signature mismatch. The fixtures now use the explicit trusted-time socket-use path, restoring the intended cross-crate authority contract. This remains active-PR evidence and does not add DNS lookup, a wall-clock authority, proxy/PAC, or a resolver service; the repaired head still requires fresh exact-head CI and coverage evidence. ## Deterministic authority contract