From d21f63185d648ac1aa5982377d6d19190899717f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 18:45:59 +0900 Subject: [PATCH 01/10] test(destination): require bounded resolution freshness authority --- .../tests/resolution_freshness.rs | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 crates/originweave-destination/tests/resolution_freshness.rs diff --git a/crates/originweave-destination/tests/resolution_freshness.rs b/crates/originweave-destination/tests/resolution_freshness.rs new file mode 100644 index 000000000..f0053859f --- /dev/null +++ b/crates/originweave-destination/tests/resolution_freshness.rs @@ -0,0 +1,145 @@ +#![allow(clippy::expect_used)] + +use std::net::{IpAddr, Ipv4Addr}; +use std::time::Duration; + +use originweave_core::Origin; +use originweave_destination::{ + DestinationError, DestinationPolicy, FreshResolutionSnapshot, MAX_RESOLUTION_VALIDITY, +}; + +fn origin(value: &str) -> Origin { + Origin::parse(value).expect("test origin must parse") +} + +fn ipv4(a: u8, b: u8, c: u8, d: u8) -> IpAddr { + IpAddr::V4(Ipv4Addr::new(a, b, c, d)) +} + +#[test] +fn fresh_resolution_authority_is_half_open_and_bound_to_pinned_addresses() { + let approved_at = Duration::from_secs(100); + let validity = Duration::from_secs(5); + let approved = ipv4(8, 8, 8, 8); + let snapshot = FreshResolutionSnapshot::approve( + origin("https://example.com"), + [approved], + &DestinationPolicy::public_web(), + approved_at, + validity, + ) + .expect("bounded fresh resolution"); + + assert_eq!(snapshot.approved_at(), approved_at); + assert_eq!(snapshot.validity(), validity); + assert_eq!(snapshot.valid_until(), Duration::from_secs(105)); + + let evidence = snapshot + .authorize_connection(approved, approved_at) + .expect("authority begins at approval time"); + assert_eq!(evidence.resolution_approved_at(), approved_at); + assert_eq!(evidence.resolution_valid_until(), Duration::from_secs(105)); + assert_eq!(evidence.authorized_at(), approved_at); + + snapshot + .authorize_connection(approved, Duration::from_secs(104)) + .expect("authority remains valid before the exclusive deadline"); + + assert_eq!( + snapshot.authorize_connection(approved, Duration::from_secs(99)), + Err(DestinationError::ResolutionUseBeforeApproval { + approved_at, + current_time: Duration::from_secs(99), + }) + ); + assert_eq!( + snapshot.authorize_connection(approved, Duration::from_secs(105)), + Err(DestinationError::ResolutionApprovalExpired { + valid_until: Duration::from_secs(105), + current_time: Duration::from_secs(105), + }) + ); + assert_eq!( + snapshot.authorize_connection(ipv4(9, 9, 9, 9), approved_at), + Err(DestinationError::UnapprovedConnectionAddress { + address: ipv4(9, 9, 9, 9), + }) + ); +} + +#[test] +fn fresh_resolution_rejects_invalid_or_overflowing_validity() { + let target = origin("https://example.com"); + let address = ipv4(8, 8, 8, 8); + let policy = DestinationPolicy::public_web(); + + for validity in [Duration::ZERO, MAX_RESOLUTION_VALIDITY + Duration::from_nanos(1)] { + assert_eq!( + FreshResolutionSnapshot::approve( + target.clone(), + [address], + &policy, + Duration::from_secs(1), + validity, + ), + Err(DestinationError::InvalidResolutionValidity { + validity, + maximum_validity: MAX_RESOLUTION_VALIDITY, + }) + ); + } + + assert_eq!( + FreshResolutionSnapshot::approve( + target, + [address], + &policy, + Duration::MAX, + Duration::from_nanos(1), + ), + Err(DestinationError::ResolutionValidityOverflow { + approved_at: Duration::MAX, + validity: Duration::from_nanos(1), + }) + ); +} + +#[test] +fn fresh_revalidation_preserves_the_budget_and_resets_approval_time() { + let first = ipv4(8, 8, 8, 8); + let second = ipv4(1, 1, 1, 1); + let policy = DestinationPolicy::public_web(); + let snapshot = FreshResolutionSnapshot::approve( + origin("https://example.com"), + [first, second], + &policy, + Duration::from_secs(10), + Duration::from_secs(4), + ) + .expect("initial fresh resolution"); + + let refreshed = snapshot + .revalidate([second], &policy, Duration::from_secs(13)) + .expect("a fresh non-expanding answer renews the bounded window"); + assert_eq!(refreshed.addresses(), &std::collections::BTreeSet::from([second])); + assert_eq!(refreshed.approved_at(), Duration::from_secs(13)); + assert_eq!(refreshed.validity(), Duration::from_secs(4)); + assert_eq!(refreshed.valid_until(), Duration::from_secs(17)); + refreshed + .authorize_connection(second, Duration::from_secs(16)) + .expect("refreshed authority is usable before its new deadline"); + + assert_eq!( + snapshot.revalidate([second], &policy, Duration::from_secs(9)), + Err(DestinationError::ResolutionUseBeforeApproval { + approved_at: Duration::from_secs(10), + current_time: Duration::from_secs(9), + }) + ); + assert_eq!( + snapshot.revalidate([first, ipv4(9, 9, 9, 9)], &policy, Duration::from_secs(11)), + Err(DestinationError::ResolutionSetExpanded { + address: ipv4(9, 9, 9, 9), + }) + ); +} From f8cb43492fd48eb8634406a3c3a2065930fbef55 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 18:48:23 +0900 Subject: [PATCH 02/10] test(destination): format freshness contract before RED --- .../tests/resolution_freshness.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/originweave-destination/tests/resolution_freshness.rs b/crates/originweave-destination/tests/resolution_freshness.rs index f0053859f..027ac42b5 100644 --- a/crates/originweave-destination/tests/resolution_freshness.rs +++ b/crates/originweave-destination/tests/resolution_freshness.rs @@ -73,7 +73,10 @@ fn fresh_resolution_rejects_invalid_or_overflowing_validity() { let address = ipv4(8, 8, 8, 8); let policy = DestinationPolicy::public_web(); - for validity in [Duration::ZERO, MAX_RESOLUTION_VALIDITY + Duration::from_nanos(1)] { + for validity in [ + Duration::ZERO, + MAX_RESOLUTION_VALIDITY + Duration::from_nanos(1), + ] { assert_eq!( FreshResolutionSnapshot::approve( target.clone(), @@ -121,7 +124,10 @@ fn fresh_revalidation_preserves_the_budget_and_resets_approval_time() { let refreshed = snapshot .revalidate([second], &policy, Duration::from_secs(13)) .expect("a fresh non-expanding answer renews the bounded window"); - assert_eq!(refreshed.addresses(), &std::collections::BTreeSet::from([second])); + assert_eq!( + refreshed.addresses(), + &std::collections::BTreeSet::from([second]) + ); assert_eq!(refreshed.approved_at(), Duration::from_secs(13)); assert_eq!(refreshed.validity(), Duration::from_secs(4)); assert_eq!(refreshed.valid_until(), Duration::from_secs(17)); From 734da192cdfec465c50583b14ef59d6f1fd0789f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 18:50:50 +0900 Subject: [PATCH 03/10] feat(destination): add bounded resolution freshness authority --- .../originweave-destination/src/resolution.rs | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/crates/originweave-destination/src/resolution.rs b/crates/originweave-destination/src/resolution.rs index f55d1722b..45620e6cd 100644 --- a/crates/originweave-destination/src/resolution.rs +++ b/crates/originweave-destination/src/resolution.rs @@ -1,6 +1,7 @@ use std::collections::BTreeSet; use std::fmt; use std::net::IpAddr; +use std::time::Duration; use originweave_core::Origin; @@ -9,6 +10,13 @@ use crate::{AddressClass, ClassifiedAddress, classify_address}; /// The largest resolver answer accepted by one resolution snapshot. pub const MAX_RESOLUTION_ADDRESS_COUNT: usize = 256; +/// The largest freshness interval accepted for one resolution approval. +/// +/// This is an OriginWeave product safety budget, not a DNS protocol validity +/// rule. Callers may choose any smaller non-zero interval appropriate to their +/// resolver and network adapter. +pub const MAX_RESOLUTION_VALIDITY: Duration = Duration::from_secs(30); + /// A fail-closed allow-list of destination address classes. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DestinationPolicy { @@ -103,6 +111,34 @@ pub enum DestinationError { /// The newly introduced canonical address. address: IpAddr, }, + /// A freshness interval was zero or exceeded [`MAX_RESOLUTION_VALIDITY`]. + InvalidResolutionValidity { + /// The rejected freshness interval. + validity: Duration, + /// The largest accepted freshness interval. + maximum_validity: Duration, + }, + /// Adding the freshness interval to the approval time overflowed. + ResolutionValidityOverflow { + /// The trusted monotonic time at which the answer was approved. + approved_at: Duration, + /// The requested freshness interval. + validity: Duration, + }, + /// A caller supplied a monotonic time earlier than the recorded approval. + ResolutionUseBeforeApproval { + /// The recorded approval time. + approved_at: Duration, + /// The caller-supplied current time. + current_time: Duration, + }, + /// A bounded resolution approval reached its exclusive validity deadline. + ResolutionApprovalExpired { + /// The exclusive upper bound of the approval interval. + valid_until: Duration, + /// The caller-supplied current time. + current_time: Duration, + }, } impl fmt::Display for DestinationError { @@ -142,6 +178,34 @@ impl fmt::Display for DestinationError { formatter, "refreshed DNS answer introduced unapproved address {address}", ), + Self::InvalidResolutionValidity { + validity, + maximum_validity, + } => write!( + formatter, + "resolution validity {validity:?} is outside 1ns..={maximum_validity:?}", + ), + Self::ResolutionValidityOverflow { + approved_at, + validity, + } => write!( + formatter, + "resolution validity {validity:?} overflows approval time {approved_at:?}", + ), + Self::ResolutionUseBeforeApproval { + approved_at, + current_time, + } => write!( + formatter, + "resolution use time {current_time:?} precedes approval time {approved_at:?}", + ), + Self::ResolutionApprovalExpired { + valid_until, + current_time, + } => write!( + formatter, + "resolution approval expired at {valid_until:?}; current time is {current_time:?}", + ), } } } @@ -254,6 +318,143 @@ impl ResolutionSnapshot { } } +/// A resolution snapshot bound to one explicit trusted monotonic validity window. +/// +/// The time values are opaque durations from one caller-owned monotonic clock +/// domain. This type never reads a wall clock itself. Constructing a new fresh +/// snapshot always reruns the same destination validation used by +/// [`ResolutionSnapshot`], so callers cannot renew authority without presenting +/// another policy-valid answer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FreshResolutionSnapshot { + snapshot: ResolutionSnapshot, + approved_at: Duration, + validity: Duration, + valid_until: Duration, +} + +impl FreshResolutionSnapshot { + /// Validate addresses and bind the resulting snapshot to a bounded lifetime. + pub fn approve( + origin: Origin, + addresses: impl IntoIterator, + policy: &DestinationPolicy, + approved_at: Duration, + validity: Duration, + ) -> Result { + let snapshot = ResolutionSnapshot::approve(origin, addresses, policy)?; + Self::from_snapshot(snapshot, approved_at, validity) + } + + fn from_snapshot( + snapshot: ResolutionSnapshot, + approved_at: Duration, + validity: Duration, + ) -> Result { + if validity.is_zero() || validity > MAX_RESOLUTION_VALIDITY { + return Err(DestinationError::InvalidResolutionValidity { + validity, + maximum_validity: MAX_RESOLUTION_VALIDITY, + }); + } + let Some(valid_until) = approved_at.checked_add(validity) else { + return Err(DestinationError::ResolutionValidityOverflow { + approved_at, + validity, + }); + }; + Ok(Self { + snapshot, + approved_at, + validity, + valid_until, + }) + } + + /// Return the logical origin whose DNS answer was approved. + #[must_use] + pub const fn origin(&self) -> &Origin { + self.snapshot.origin() + } + + /// Return the canonical addresses pinned for this fresh snapshot. + #[must_use] + pub const fn addresses(&self) -> &BTreeSet { + self.snapshot.addresses() + } + + /// Return the trusted monotonic approval time. + #[must_use] + pub const fn approved_at(&self) -> Duration { + self.approved_at + } + + /// Return the configured non-zero validity budget. + #[must_use] + pub const fn validity(&self) -> Duration { + self.validity + } + + /// Return the exclusive upper bound of the approval interval. + #[must_use] + pub const fn valid_until(&self) -> Duration { + self.valid_until + } + + /// Authorize one pinned address only while the freshness window is valid. + pub fn authorize_connection( + &self, + address: IpAddr, + current_time: Duration, + ) -> Result { + self.validate_current_time(current_time)?; + let connection = self.snapshot.authorize_connection(address)?; + Ok(FreshConnectionEvidence { + connection, + resolution_approved_at: self.approved_at, + resolution_valid_until: self.valid_until, + authorized_at: current_time, + }) + } + + /// Revalidate a fresh answer and renew the same bounded validity budget. + /// + /// `revalidated_at` must come from the same monotonic clock domain and may + /// not precede this snapshot's approval time. Expansion of the pinned set + /// remains fail-closed under [`ResolutionSnapshot::revalidate`]. + pub fn revalidate( + &self, + addresses: impl IntoIterator, + policy: &DestinationPolicy, + revalidated_at: Duration, + ) -> Result { + if revalidated_at < self.approved_at { + return Err(DestinationError::ResolutionUseBeforeApproval { + approved_at: self.approved_at, + current_time: revalidated_at, + }); + } + let snapshot = self.snapshot.revalidate(addresses, policy)?; + Self::from_snapshot(snapshot, revalidated_at, self.validity) + } + + fn validate_current_time(&self, current_time: Duration) -> Result<(), DestinationError> { + if current_time < self.approved_at { + return Err(DestinationError::ResolutionUseBeforeApproval { + approved_at: self.approved_at, + current_time, + }); + } + if current_time >= self.valid_until { + return Err(DestinationError::ResolutionApprovalExpired { + valid_until: self.valid_until, + current_time, + }); + } + Ok(()) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum OriginHostConstraint { Domain, @@ -344,3 +545,38 @@ impl ConnectionEvidence { self.address_class } } + +/// Credential-free evidence that a pinned connection address was used while fresh. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FreshConnectionEvidence { + connection: ConnectionEvidence, + resolution_approved_at: Duration, + resolution_valid_until: Duration, + authorized_at: Duration, +} + +impl FreshConnectionEvidence { + /// Return the underlying canonical destination/connection evidence. + #[must_use] + pub const fn connection_evidence(&self) -> &ConnectionEvidence { + &self.connection + } + + /// Return the trusted monotonic time at which the answer was approved. + #[must_use] + pub const fn resolution_approved_at(&self) -> Duration { + self.resolution_approved_at + } + + /// Return the exclusive upper bound of the resolution approval interval. + #[must_use] + pub const fn resolution_valid_until(&self) -> Duration { + self.resolution_valid_until + } + + /// Return the trusted monotonic time used for this authorization decision. + #[must_use] + pub const fn authorized_at(&self) -> Duration { + self.authorized_at + } +} From cbe5f87ce9107f697c2031fd63ad57093473cb1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 18:51:06 +0900 Subject: [PATCH 04/10] feat(destination): export freshness authority contract --- crates/originweave-destination/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-destination/src/lib.rs b/crates/originweave-destination/src/lib.rs index 5fdf2d363..0b27014ba 100644 --- a/crates/originweave-destination/src/lib.rs +++ b/crates/originweave-destination/src/lib.rs @@ -24,6 +24,7 @@ pub use redirect::{ RedirectTargetDigestError, }; pub use resolution::{ - ConnectionEvidence, DestinationError, DestinationPolicy, MAX_RESOLUTION_ADDRESS_COUNT, + ConnectionEvidence, DestinationError, DestinationPolicy, FreshConnectionEvidence, + FreshResolutionSnapshot, MAX_RESOLUTION_ADDRESS_COUNT, MAX_RESOLUTION_VALIDITY, ResolutionSnapshot, -}; +}; \ No newline at end of file From 34873d1cfc083493f626d8aef2c16ac618069d9a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 18:51:35 +0900 Subject: [PATCH 05/10] test(destination): cover freshness evidence and errors --- .../tests/resolution_freshness.rs | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/originweave-destination/tests/resolution_freshness.rs b/crates/originweave-destination/tests/resolution_freshness.rs index 027ac42b5..d47fd8821 100644 --- a/crates/originweave-destination/tests/resolution_freshness.rs +++ b/crates/originweave-destination/tests/resolution_freshness.rs @@ -5,7 +5,8 @@ use std::time::Duration; use originweave_core::Origin; use originweave_destination::{ - DestinationError, DestinationPolicy, FreshResolutionSnapshot, MAX_RESOLUTION_VALIDITY, + AddressClass, DestinationError, DestinationPolicy, FreshResolutionSnapshot, + MAX_RESOLUTION_VALIDITY, }; fn origin(value: &str) -> Origin { @@ -20,9 +21,10 @@ fn ipv4(a: u8, b: u8, c: u8, d: u8) -> IpAddr { fn fresh_resolution_authority_is_half_open_and_bound_to_pinned_addresses() { let approved_at = Duration::from_secs(100); let validity = Duration::from_secs(5); + let target = origin("https://example.com"); let approved = ipv4(8, 8, 8, 8); let snapshot = FreshResolutionSnapshot::approve( - origin("https://example.com"), + target.clone(), [approved], &DestinationPolicy::public_web(), approved_at, @@ -30,6 +32,7 @@ fn fresh_resolution_authority_is_half_open_and_bound_to_pinned_addresses() { ) .expect("bounded fresh resolution"); + assert_eq!(snapshot.origin(), &target); assert_eq!(snapshot.approved_at(), approved_at); assert_eq!(snapshot.validity(), validity); assert_eq!(snapshot.valid_until(), Duration::from_secs(105)); @@ -37,6 +40,11 @@ fn fresh_resolution_authority_is_half_open_and_bound_to_pinned_addresses() { let evidence = snapshot .authorize_connection(approved, approved_at) .expect("authority begins at approval time"); + let connection = evidence.connection_evidence(); + assert_eq!(connection.origin(), &target); + assert_eq!(connection.requested_address(), approved); + assert_eq!(connection.canonical_address(), approved); + assert_eq!(connection.address_class(), AddressClass::Public); assert_eq!(evidence.resolution_approved_at(), approved_at); assert_eq!(evidence.resolution_valid_until(), Duration::from_secs(105)); assert_eq!(evidence.authorized_at(), approved_at); @@ -149,3 +157,39 @@ fn fresh_revalidation_preserves_the_budget_and_resets_approval_time() { }) ); } + +#[test] +fn freshness_errors_have_deterministic_bounded_messages() { + let invalid = DestinationError::InvalidResolutionValidity { + validity: Duration::ZERO, + maximum_validity: MAX_RESOLUTION_VALIDITY, + }; + assert_eq!( + invalid.to_string(), + "resolution validity 0ns is outside 1ns..=30s" + ); + + let overflow = DestinationError::ResolutionValidityOverflow { + approved_at: Duration::MAX, + validity: Duration::from_nanos(1), + }; + assert!(overflow.to_string().contains("overflows approval time")); + + let before = DestinationError::ResolutionUseBeforeApproval { + approved_at: Duration::from_secs(10), + current_time: Duration::from_secs(9), + }; + assert_eq!( + before.to_string(), + "resolution use time 9s precedes approval time 10s" + ); + + let expired = DestinationError::ResolutionApprovalExpired { + valid_until: Duration::from_secs(15), + current_time: Duration::from_secs(15), + }; + assert_eq!( + expired.to_string(), + "resolution approval expired at 15s; current time is 15s" + ); +} From 41ded6f27883d6c35443d0443d7131207fe56987 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 19:18:16 +0900 Subject: [PATCH 06/10] style(destination): restore canonical rustfmt newline --- crates/originweave-destination/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-destination/src/lib.rs b/crates/originweave-destination/src/lib.rs index 0b27014ba..774ba9ee9 100644 --- a/crates/originweave-destination/src/lib.rs +++ b/crates/originweave-destination/src/lib.rs @@ -27,4 +27,4 @@ pub use resolution::{ ConnectionEvidence, DestinationError, DestinationPolicy, FreshConnectionEvidence, FreshResolutionSnapshot, MAX_RESOLUTION_ADDRESS_COUNT, MAX_RESOLUTION_VALIDITY, ResolutionSnapshot, -}; \ No newline at end of file +}; From e03836f5e2e23eeb4cb89a1b08b46da70923718b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 19:20:10 +0900 Subject: [PATCH 07/10] test(destination): reject denied addresses before freshness authority --- .../tests/resolution_freshness.rs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/originweave-destination/tests/resolution_freshness.rs b/crates/originweave-destination/tests/resolution_freshness.rs index d47fd8821..79349d6dd 100644 --- a/crates/originweave-destination/tests/resolution_freshness.rs +++ b/crates/originweave-destination/tests/resolution_freshness.rs @@ -115,6 +115,39 @@ fn fresh_resolution_rejects_invalid_or_overflowing_validity() { ); } +#[test] +fn fresh_resolution_rejects_denied_addresses_before_granting_time_authority() { + let target = origin("https://example.com"); + let denied = ipv4(127, 0, 0, 1); + let public = ipv4(8, 8, 8, 8); + let policy = DestinationPolicy::public_web(); + let expected = Err(DestinationError::AddressClassDenied { + address: denied, + address_class: AddressClass::Loopback, + }); + + assert_eq!( + FreshResolutionSnapshot::approve( + target.clone(), + [denied], + &policy, + Duration::from_secs(1), + Duration::from_secs(1), + ), + expected.clone() + ); + assert_eq!( + FreshResolutionSnapshot::approve( + target, + [denied, public], + &policy, + Duration::from_secs(1), + Duration::from_secs(1), + ), + expected + ); +} + #[test] fn fresh_revalidation_preserves_the_budget_and_resets_approval_time() { let first = ipv4(8, 8, 8, 8); From 6b5ed4dcea281b505f67db6180bb14c3bc95b392 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 20:07:59 +0900 Subject: [PATCH 08/10] test(destination): cover single-address rebinding rejection --- .../tests/resolution_freshness.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/originweave-destination/tests/resolution_freshness.rs b/crates/originweave-destination/tests/resolution_freshness.rs index 79349d6dd..2df264563 100644 --- a/crates/originweave-destination/tests/resolution_freshness.rs +++ b/crates/originweave-destination/tests/resolution_freshness.rs @@ -152,6 +152,7 @@ fn fresh_resolution_rejects_denied_addresses_before_granting_time_authority() { fn fresh_revalidation_preserves_the_budget_and_resets_approval_time() { let first = ipv4(8, 8, 8, 8); let second = ipv4(1, 1, 1, 1); + let unexpected = ipv4(9, 9, 9, 9); let policy = DestinationPolicy::public_web(); let snapshot = FreshResolutionSnapshot::approve( origin("https://example.com"), @@ -184,9 +185,15 @@ fn fresh_revalidation_preserves_the_budget_and_resets_approval_time() { }) ); assert_eq!( - snapshot.revalidate([first, ipv4(9, 9, 9, 9)], &policy, Duration::from_secs(11)), + snapshot.revalidate([unexpected], &policy, Duration::from_secs(11)), Err(DestinationError::ResolutionSetExpanded { - address: ipv4(9, 9, 9, 9), + address: unexpected, + }) + ); + assert_eq!( + snapshot.revalidate([first, unexpected], &policy, Duration::from_secs(11)), + Err(DestinationError::ResolutionSetExpanded { + address: unexpected, }) ); } From 71a6fd9ea08fe1f628f8d9d6d6aaf4b4d2fec985 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 06:46:43 -0700 Subject: [PATCH 09/10] docs(destination): record bounded resolution freshness authority --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 116c02fe6..8935a05d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Active PR #168 adds deterministic MCP `2026-07-28` stateless tool-routing foundations with bounded names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. This is active-PR evidence only; the complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned until separately integrated on protected `main`. - 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. +- 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. - 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. From be893e96909745e358e68e4716e98a9d11a65fdf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 13:27:28 -0700 Subject: [PATCH 10/10] test(destination): prove post-expiry revalidation authority --- .../resolution_post_expiry_revalidation.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 crates/originweave-destination/tests/resolution_post_expiry_revalidation.rs diff --git a/crates/originweave-destination/tests/resolution_post_expiry_revalidation.rs b/crates/originweave-destination/tests/resolution_post_expiry_revalidation.rs new file mode 100644 index 000000000..3c8443554 --- /dev/null +++ b/crates/originweave-destination/tests/resolution_post_expiry_revalidation.rs @@ -0,0 +1,78 @@ +#![allow(clippy::expect_used)] + +use std::net::{IpAddr, Ipv4Addr}; +use std::time::Duration; + +use originweave_core::Origin; +use originweave_destination::{DestinationError, DestinationPolicy, FreshResolutionSnapshot}; + +fn origin() -> Origin { + Origin::parse("https://example.com").expect("test origin must parse") +} + +fn ipv4(a: u8, b: u8, c: u8, d: u8) -> IpAddr { + IpAddr::V4(Ipv4Addr::new(a, b, c, d)) +} + +#[test] +fn post_expiry_revalidation_establishes_new_authority_without_reviving_the_old_snapshot() { + let first = ipv4(8, 8, 8, 8); + let second = ipv4(1, 1, 1, 1); + let policy = DestinationPolicy::public_web(); + let snapshot = FreshResolutionSnapshot::approve( + origin(), + [first, second], + &policy, + Duration::from_secs(10), + Duration::from_secs(4), + ) + .expect("initial bounded freshness authority"); + + let expiry = Duration::from_secs(14); + assert_eq!( + snapshot.authorize_connection(first, expiry), + Err(DestinationError::ResolutionApprovalExpired { + valid_until: expiry, + current_time: expiry, + }) + ); + + let refreshed = snapshot + .revalidate([second], &policy, expiry) + .expect("fresh non-expanding validation may establish a new bounded snapshot"); + assert_eq!(refreshed.approved_at(), expiry); + assert_eq!(refreshed.valid_until(), Duration::from_secs(18)); + refreshed + .authorize_connection(second, expiry) + .expect("the newly validated snapshot has independent current authority"); + + assert_eq!( + snapshot.authorize_connection(second, expiry), + Err(DestinationError::ResolutionApprovalExpired { + valid_until: expiry, + current_time: expiry, + }) + ); +} + +#[test] +fn post_expiry_revalidation_still_rejects_address_set_expansion() { + let approved = ipv4(8, 8, 8, 8); + let unexpected = ipv4(9, 9, 9, 9); + let policy = DestinationPolicy::public_web(); + let snapshot = FreshResolutionSnapshot::approve( + origin(), + [approved], + &policy, + Duration::from_secs(10), + Duration::from_secs(4), + ) + .expect("initial bounded freshness authority"); + + assert_eq!( + snapshot.revalidate([approved, unexpected], &policy, Duration::from_secs(14)), + Err(DestinationError::ResolutionSetExpanded { + address: unexpected, + }) + ); +}