Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
18270ff
test(network): require fresh resolution authority before planning
seonghobae Aug 10, 2026
1fcc3b3
test(network): format fresh resolution planning contract
seonghobae Aug 10, 2026
a8da35a
test(network): isolate fresh-authority production RED
seonghobae Aug 10, 2026
238770a
feat(destination): expose fresh snapshot for transport composition
seonghobae Aug 10, 2026
18d3b19
feat(network): add fresh resolution connection plan
seonghobae Aug 10, 2026
f3981a4
fix(network): expose fresh resolution connection plan
seonghobae Aug 10, 2026
8e8944e
test(network): exercise fresh connection authority
seonghobae Aug 10, 2026
0d32685
test(network): satisfy strict lint contract without panic
seonghobae Aug 10, 2026
435f736
docs(changelog): record fresh connection authority
seonghobae Aug 10, 2026
d23ff4d
test(network): preserve strict error handling in freshness integration
seonghobae Aug 10, 2026
1b0915c
test(network): forbid stale public planning authority
seonghobae Aug 10, 2026
7ade598
fix(network): hide untimed direct planner
seonghobae Aug 10, 2026
5ac4041
test(tls): consume fresh socket authority in deadline integration
seonghobae Aug 10, 2026
c1378b8
test(tls): consume fresh socket authority in validity integration
seonghobae Aug 10, 2026
69162ee
test(tls): consume fresh socket authority in handshake integration
seonghobae Aug 10, 2026
1c9742f
style(tls): restore rustfmt newline
seonghobae Aug 10, 2026
a249405
style(tls): restore validity test newline
seonghobae Aug 10, 2026
f8b43bc
style(tls): restore handshake test newline
seonghobae Aug 10, 2026
135851a
merge: align fresh socket planning with current prerequisite
seonghobae Aug 15, 2026
b1fc175
chore(network): realign freshness planning with current destination head
seonghobae Aug 16, 2026
3f7fe10
docs(network): preserve current parent changelog during stack realign…
seonghobae Aug 16, 2026
0393829
test(network): reproduce stale connection plan use
seonghobae Aug 21, 2026
9038e64
fix(network): recheck resolution freshness at socket use
seonghobae Aug 21, 2026
3c29eec
test(tls): supply socket-use freshness time
seonghobae Aug 21, 2026
20178a2
test(tls): revalidate resolution at connect time
seonghobae Aug 21, 2026
0c54ac3
test(tls): restore complete integration coverage
seonghobae Aug 21, 2026
1a9de6b
test(tls): carry trusted time through socket use
seonghobae Aug 21, 2026
70b5577
fix(network): preserve compatibility while rechecking socket freshness
seonghobae Aug 21, 2026
01f9088
test(network): cover stale replay and time regression
seonghobae Aug 21, 2026
bf8281f
test(tls): use explicit socket freshness authority
seonghobae Aug 21, 2026
8535d18
fix(network): require explicit socket-use freshness time
seonghobae Aug 21, 2026
dabae67
test(network): require explicit fresh time at socket use
seonghobae Aug 21, 2026
4a7eb6b
test(tls): use explicit connect-time freshness
seonghobae Aug 21, 2026
2434f85
chore(stack): merge current resolution freshness prerequisite
seonghobae Aug 25, 2026
ecd7884
docs(network): preserve freshness adapter changelog after stack conve…
seonghobae Aug 25, 2026
20687be
chore(network): realign freshness stack on current prerequisite
seonghobae Aug 25, 2026
e6f74b5
Merge branch 'main' into feat/network-consume-resolution-freshness
opencode-agent[bot] Aug 27, 2026
8695d40
chore(network): adopt current protected main without rewriting history
seonghobae Sep 3, 2026
44c9fef
chore(network): adopt protected main #280 without rewriting history
seonghobae Sep 3, 2026
ae21163
chore(network): adopt current protected main
seonghobae Sep 4, 2026
30d032b
Merge remote-tracking branch 'origin/main' into codex/adopt-main-pr50
seonghobae Sep 4, 2026
ddbefc9
fix(network): bind direct sockets to origin ports
seonghobae Sep 5, 2026
2bd8518
docs(network): align public freshness contract
seonghobae Sep 5, 2026
e981ac4
test(network): cover default origin port binding
seonghobae Sep 5, 2026
ad87cfe
style(network): format default-port regression contract
seonghobae Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to OriginWeave are documented in this file. The format follo

## [Unreleased]

- Restored pinned Rust formatting for the default HTTP/HTTPS port regression while preserving all accepted-port and mismatched-port assertions and the existing freshness policy.
- Corrected the direct-connection ADR and research notes to describe the public freshness-checked API, its trusted monotonic clock contract, and revalidation before socket I/O.
- Refreshed the product-gap queue to 126 open pull requests (54 ready, 72 draft) after #190, #188, #185, #192, #182, #184, #115, #181, #116, #117, #118, #183, #114, #127, #112, #109, #186, #110, #108, #111, #174, and #113 were merged into their immediate stacked prerequisites. PRs #147, #146, #145, #144, #143, #142, #141, #139, #136, #132, #129, and #128 moved to ready after exact-head checks and thread review; these are queue-consolidation results, not protected-main shipment.

### Added
Expand All @@ -25,6 +27,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.
- 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.
- Deterministic TLS revocation-material freshness authority with a strict signed `thisUpdate`→`nextUpdate` half-open window and typed invalid-window, not-yet-valid, and stale failures, without claiming OCSP/CRL acquisition, cryptographic validation, or certificate revocation status.
Expand Down Expand Up @@ -70,6 +73,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Security

- Bound every direct TCP socket port to the effective port of its approved logical origin, preventing an origin-approved IP address from becoming authority for another service on the same host.
- Explicit proxy server identifiers require ASCII decimal port tokens before numeric range parsing, preventing Rust-specific leading-plus spellings from widening proxy authority.
- Raw page content cannot become a trusted instruction.
- Raw secrets are rejected and secret-capable actions require an opaque broker handle.
Expand Down Expand Up @@ -102,4 +106,4 @@ All notable changes to OriginWeave are documented in this file. The format follo
- The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it.
- The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels.

[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD
[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD
13 changes: 12 additions & 1 deletion crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use std::net::{Ipv4Addr, Ipv6Addr};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Origin {
canonical: String,
port: u16,
}

impl Origin {
Expand Down Expand Up @@ -54,11 +55,15 @@ impl Origin {
return Err(OriginError::InsecureRemoteOrigin);
}
let normalized_port = normalize_default_port(&scheme, port);
let effective_port = normalized_port.unwrap_or(if scheme == "https" { 443 } else { 80 });
let canonical = match normalized_port {
Some(port_number) => format!("{scheme}://{host}:{port_number}"),
None => format!("{scheme}://{host}"),
};
Ok(Self { canonical })
Ok(Self {
canonical,
port: effective_port,
})
}

/// Return the normalized origin string.
Expand Down Expand Up @@ -90,6 +95,12 @@ impl Origin {
};
&authority[host_start..host_end]
}

/// Return the effective origin port, including the scheme default.
#[must_use]
pub const fn port(&self) -> u16 {
self.port
}
}

impl fmt::Display for Origin {
Expand Down
4 changes: 4 additions & 0 deletions crates/originweave-core/tests/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ fn origin_accepts_secure_and_loopback_origins() {
assert_eq!(secure_ipv6.as_str(), "https://[2001:db8::1]");
assert_eq!(secure.scheme(), "https");
assert_eq!(secure.host(), "example.com");
assert_eq!(secure.port(), 443);
assert_eq!(secure_custom.port(), 8443);
assert_eq!(localhost.scheme(), "http");
assert_eq!(localhost.host(), "localhost");
assert_eq!(localhost.port(), 80);
assert_eq!(localhost_custom.port(), 8080);
assert_eq!(ipv4.host(), "127.0.0.1");
assert_eq!(ipv6.host(), "::1");
assert_eq!(secure_ipv6.host(), "2001:db8::1");
Expand Down
11 changes: 11 additions & 0 deletions crates/originweave-destination/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,17 @@ impl FreshResolutionSnapshot {
})
}

/// Return the validated untimed snapshot underlying this freshness authority.
///
/// Callers that use this view must still enforce freshness separately. It is
/// exposed so a transport planner can reuse the existing canonical socket
/// validation only after [`FreshResolutionSnapshot::authorize_connection`]
/// succeeds for the same operation.
#[must_use]
pub const fn resolution_snapshot(&self) -> &ResolutionSnapshot {
&self.snapshot
}
Comment thread
seonghobae marked this conversation as resolved.

/// Return the logical origin whose DNS answer was approved.
#[must_use]
pub const fn origin(&self) -> &Origin {
Expand Down
66 changes: 51 additions & 15 deletions crates/originweave-network/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ impl ConnectionPlan {
if socket_address.port() == 0 {
return Err(NetworkError::InvalidPort);
}
let origin_port = resolution.origin().port();
if socket_address.port() != origin_port {
return Err(NetworkError::OriginPortMismatch {
socket_port: socket_address.port(),
origin_port,
});
}
if connect_timeout.is_zero() || connect_timeout > MAX_CONNECT_TIMEOUT {
return Err(NetworkError::InvalidConnectTimeout {
connect_timeout,
Expand Down Expand Up @@ -273,6 +280,13 @@ impl SocketConnectionEvidence {
pub enum NetworkError {
/// The requested destination port was zero.
InvalidPort,
/// The requested socket port did not match the approved logical origin.
OriginPortMismatch {
/// The rejected socket port.
socket_port: u16,
/// The effective port bound to the logical origin.
origin_port: u16,
},
/// The timeout was zero or exceeded [`MAX_CONNECT_TIMEOUT`].
InvalidConnectTimeout {
/// The rejected timeout.
Expand Down Expand Up @@ -351,6 +365,7 @@ impl NetworkError {
Self::PeerInspectionFailed { attempt_number, .. }
| Self::PeerMismatch { attempt_number, .. } => Some(*attempt_number),
Self::InvalidPort
| Self::OriginPortMismatch { .. }
| Self::InvalidConnectTimeout { .. }
| Self::InvalidAttemptCount { .. }
| Self::DestinationNotApproved { .. }
Expand All @@ -363,6 +378,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 {
socket_port,
origin_port,
} => write!(
formatter,
"connection port {socket_port} does not match origin port {origin_port}",
),
Self::InvalidConnectTimeout {
connect_timeout,
maximum_timeout,
Expand Down Expand Up @@ -436,6 +458,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 { .. }
Expand Down Expand Up @@ -533,18 +556,18 @@ mod tests {
client
}

fn loopback_snapshot() -> ResolutionSnapshot {
fn loopback_snapshot(port: u16) -> ResolutionSnapshot {
ResolutionSnapshot::approve(
Origin::parse("http://localhost").expect("loopback origin"),
Origin::parse(&format!("http://localhost:{port}")).expect("loopback origin"),
[IpAddr::V4(Ipv4Addr::LOCALHOST)],
&DestinationPolicy::from_allowed_classes([AddressClass::Loopback]),
)
.expect("managed loopback snapshot")
}

fn ipv6_loopback_snapshot() -> ResolutionSnapshot {
fn ipv6_loopback_snapshot(port: u16) -> ResolutionSnapshot {
ResolutionSnapshot::approve(
Origin::parse("http://[::1]").expect("IPv6 loopback origin"),
Origin::parse(&format!("http://[::1]:{port}")).expect("IPv6 loopback origin"),
[IpAddr::V6(Ipv6Addr::LOCALHOST)],
&DestinationPolicy::from_allowed_classes([AddressClass::Loopback]),
)
Expand All @@ -553,7 +576,7 @@ mod tests {

fn plan(maximum_attempts: u8) -> ConnectionPlan {
ConnectionPlan::new(
&loopback_snapshot(),
&loopback_snapshot(requested_socket().port()),
requested_socket(),
Duration::from_secs(2),
maximum_attempts,
Expand Down Expand Up @@ -705,7 +728,7 @@ mod tests {

#[test]
fn validation_errors_cover_every_public_contract() {
let snapshot = loopback_snapshot();
let snapshot = loopback_snapshot(80);
let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 80);
let validation_errors = [
ConnectionPlan::new(
Expand Down Expand Up @@ -733,14 +756,21 @@ mod tests {
MAX_CONNECTION_ATTEMPTS + 1,
)
.expect_err("excessive attempts must fail"),
ConnectionPlan::new(
&snapshot,
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 81),
Duration::from_secs(1),
1,
)
.expect_err("origin-port mismatch must fail"),
];
for error in validation_errors {
assert!(!error.to_string().is_empty());
assert!(error.source().is_none());
assert_eq!(error.attempt_count(), None);
}

let denied_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 443);
let denied_socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 80);
let denied = ConnectionPlan::new(&snapshot, denied_socket, Duration::from_secs(1), 1)
.expect_err("address absent from snapshot must fail");
assert!(denied.to_string().contains("not approved"));
Expand All @@ -750,7 +780,7 @@ mod tests {
let mapped = Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0x7f00, 1);
let noncanonical = ConnectionPlan::new(
&snapshot,
SocketAddr::new(IpAddr::V6(mapped), 443),
SocketAddr::new(IpAddr::V6(mapped), 80),
Duration::from_secs(1),
1,
)
Expand All @@ -759,7 +789,7 @@ mod tests {
assert!(noncanonical.source().is_none());
assert_eq!(noncanonical.attempt_count(), None);

let ipv6_snapshot = ipv6_loopback_snapshot();
let ipv6_snapshot = ipv6_loopback_snapshot(443);
let canonical_ipv6_socket =
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0));
assert!(
Expand Down Expand Up @@ -815,8 +845,9 @@ mod tests {
stream.write_all(b"ok").expect("server response must write");
});

let origin = Origin::parse("http://localhost").expect("loopback origin");
let snapshot = loopback_snapshot();
let origin =
Origin::parse(&format!("http://localhost:{}", socket.port())).expect("loopback origin");
let snapshot = loopback_snapshot(socket.port());
let connection = ConnectionPlan::new(&snapshot, socket, Duration::from_secs(1), 1)
.expect("plan must validate")
.connect()
Expand Down Expand Up @@ -853,10 +884,15 @@ mod tests {
let socket = listener.local_addr().expect("reserved address");
drop(listener);

let error = ConnectionPlan::new(&loopback_snapshot(), socket, Duration::from_secs(1), 3)
.expect("plan must validate")
.connect()
.expect_err("closed loopback port must fail");
let error = ConnectionPlan::new(
&loopback_snapshot(socket.port()),
socket,
Duration::from_secs(1),
3,
)
.expect("plan must validate")
.connect()
.expect_err("closed loopback port must fail");

assert_eq!(error.attempt_count(), Some(3));
assert!(error.source().is_some());
Expand Down
98 changes: 98 additions & 0 deletions crates/originweave-network/src/fresh_connection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::net::SocketAddr;
use std::time::Duration;

use originweave_destination::{DestinationError, FreshResolutionSnapshot};

use crate::connection::{ConnectionPlan, DirectTcpConnection, NetworkError};

/// A single-use direct connection plan authorized by a fresh resolution window.
///
/// This adapter composes the destination crate's monotonic freshness authority
/// with the existing exact-socket connection planner. It performs no DNS lookup,
/// wall-clock read, proxy selection, TLS, HTTP, browser control, or persistence.
#[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,
}

impl FreshConnectionPlan {
/// Validate freshness and one exact direct-connection request without I/O.
pub fn new(
resolution: &FreshResolutionSnapshot,
current_time: Duration,
socket_address: SocketAddr,
connect_timeout: Duration,
maximum_attempts: u8,
) -> Result<Self, NetworkError> {
let fresh_evidence = resolution
.authorize_connection(socket_address.ip(), current_time)
.map_err(|source| NetworkError::DestinationNotApproved {
socket_address,
source,
})?;
let connection_plan = ConnectionPlan::new(
resolution.resolution_snapshot(),
socket_address,
connect_timeout,
maximum_attempts,
)?;
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(),
})
}

/// Return the trusted monotonic time at which resolution was approved.
#[must_use]
pub const fn resolution_approved_at(&self) -> Duration {
self.resolution_approved_at
}

/// Return the exclusive end of the resolution authority window.
#[must_use]
pub const fn resolution_valid_until(&self) -> Duration {
self.resolution_valid_until
}

/// Return the trusted monotonic time used to authorize this plan.
#[must_use]
pub const fn resolution_authorized_at(&self) -> Duration {
self.resolution_authorized_at
}

/// Open the exact approved socket only while resolution authority is still fresh.
///
/// `current_time` must come from the same caller-owned 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
/// replayed after expiry. A supplied time earlier than the plan's own authorization
/// checkpoint fails closed instead of permitting clock regression. The plan remains
/// single-use because this method consumes `self`.
pub fn connect(self, current_time: Duration) -> Result<DirectTcpConnection, NetworkError> {
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 {
socket_address: self.socket_address,
source,
})?;
self.connection_plan.connect()
Comment thread
seonghobae marked this conversation as resolved.
}
}
16 changes: 14 additions & 2 deletions crates/originweave-network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,25 @@
//! The crate consumes a validated connection plan, opens one exact socket
//! address without hostname resolution or proxy inheritance, verifies the
//! operating-system peer, and emits credential-free evidence.
//!
//! Direct planning from an untimed resolution snapshot is intentionally not a
//! public capability. External callers must cross the fresh-resolution boundary
//! before they can obtain socket authority.
//!
//! ```compile_fail
//! use originweave_network::ConnectionPlan;
//!
//! fn stale_resolution_bypass(_: Option<ConnectionPlan>) {}
//! ```

#![forbid(unsafe_code)]
#![deny(missing_docs)]

mod connection;
mod fresh_connection;

pub use connection::{
ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS,
NetworkError, SocketConnectionEvidence,
DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, NetworkError,
SocketConnectionEvidence,
};
pub use fresh_connection::FreshConnectionPlan;
Loading
Loading