From 819dbab7dfebcee4e91ea705c96ff96f870e5ff4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 28 Sep 2026 19:40:37 +0900 Subject: [PATCH 1/3] test(destination): lock exact proxy, redirect, and freshness bounds Destination policy now has executable evidence for the maximum proxy and PAC authority sets, a full walk of the maximum redirect hop budget, and freshness limits that stay fail-closed at the published ceilings. --- CHANGELOG.md | 1 + .../tests/error_contract.rs | 36 +++++++++++++++++- .../tests/proxy_route_policy.rs | 37 +++++++++++++++++++ .../tests/proxy_server_identity.rs | 4 ++ .../tests/redirect_policy.rs | 28 ++++++++++++++ .../tests/resolution_freshness.rs | 14 +++++++ 6 files changed, 118 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f747adeae..b95d02924 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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 +- Destination policy now has executable evidence for the exact proxy and PAC authority ceilings, the maximum redirect hop walk, and the freshness bounds that stay fail-closed at the published limits. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. - Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, an organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge. diff --git a/crates/originweave-destination/tests/error_contract.rs b/crates/originweave-destination/tests/error_contract.rs index a5eaea131..7912b9c8f 100644 --- a/crates/originweave-destination/tests/error_contract.rs +++ b/crates/originweave-destination/tests/error_contract.rs @@ -2,11 +2,12 @@ use std::error::Error; use std::net::{IpAddr, Ipv4Addr}; +use std::time::Duration; use originweave_core::Origin; use originweave_destination::{ - AddressClass, DestinationError, MAX_REDIRECT_HOPS, MAX_RESOLUTION_ADDRESS_COUNT, RedirectError, - RedirectTargetDigest, RedirectTargetDigestError, + AddressClass, DestinationError, MAX_REDIRECT_HOPS, MAX_RESOLUTION_ADDRESS_COUNT, + MAX_RESOLUTION_VALIDITY, RedirectError, RedirectTargetDigest, RedirectTargetDigestError, }; const DIGEST: &str = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -128,6 +129,37 @@ fn destination_errors_implement_display_and_error() { DestinationError::ResolutionSetExpanded { address: other }, format!("refreshed DNS answer introduced unapproved address {other}"), ), + ( + DestinationError::InvalidResolutionValidity { + validity: Duration::ZERO, + maximum_validity: MAX_RESOLUTION_VALIDITY, + }, + format!("resolution validity 0ns is outside 1ns..={MAX_RESOLUTION_VALIDITY:?}"), + ), + ( + DestinationError::ResolutionValidityOverflow { + approved_at: Duration::MAX, + validity: Duration::from_nanos(1), + }, + format!( + "resolution validity 1ns overflows approval time {:?}", + Duration::MAX + ), + ), + ( + DestinationError::ResolutionUseBeforeApproval { + approved_at: Duration::from_secs(10), + current_time: Duration::from_secs(9), + }, + "resolution use time 9s precedes approval time 10s".to_owned(), + ), + ( + DestinationError::ResolutionApprovalExpired { + valid_until: Duration::from_secs(15), + current_time: Duration::from_secs(15), + }, + "resolution approval expired at 15s; current time is 15s".to_owned(), + ), ]; for (error, expected) in cases { diff --git a/crates/originweave-destination/tests/proxy_route_policy.rs b/crates/originweave-destination/tests/proxy_route_policy.rs index 1cfdeb347..cc47e8015 100644 --- a/crates/originweave-destination/tests/proxy_route_policy.rs +++ b/crates/originweave-destination/tests/proxy_route_policy.rs @@ -154,6 +154,43 @@ fn pac_selected_direct_requires_pac_and_direct_authority() { ); } +#[test] +fn policy_accepts_exact_maximum_proxy_and_pac_authority_sets() { + let proxies = (0..MAX_PROXY_SERVER_COUNT) + .map(|index| proxy(&format!("http://proxy-{index}.example:8080"))) + .collect::>(); + let pacs = (0..MAX_PAC_ORIGIN_COUNT) + .map(|index| origin(&format!("https://pac-{index}.example"))) + .collect::>(); + let first_proxy = proxies + .first() + .cloned() + .expect("boundary set must contain proxies"); + + let policy = ProxyRoutePolicy::new(true, proxies, pacs) + .expect("exact maximum proxy and PAC authority sets are accepted"); + let target = origin("https://target.example"); + let first_pac = origin("https://pac-0.example"); + let proxy_route = policy + .authorize( + &target, + &ProxyRoute::ExplicitProxy { + proxy_server: first_proxy, + }, + ) + .expect("authorized boundary proxy must route"); + assert_eq!(proxy_route.route_kind(), ProxyRouteKind::ExplicitProxy); + let pac_route = policy + .authorize( + &target, + &ProxyRoute::PacDirect { + pac_origin: first_pac, + }, + ) + .expect("authorized boundary PAC origin must route"); + assert_eq!(pac_route.route_kind(), ProxyRouteKind::PacDirect); +} + #[test] fn policy_rejects_unbounded_authority_sets_before_authorization() { let proxies = (0..=MAX_PROXY_SERVER_COUNT) diff --git a/crates/originweave-destination/tests/proxy_server_identity.rs b/crates/originweave-destination/tests/proxy_server_identity.rs index 82ee05ea0..c7077b5e6 100644 --- a/crates/originweave-destination/tests/proxy_server_identity.rs +++ b/crates/originweave-destination/tests/proxy_server_identity.rs @@ -70,6 +70,10 @@ fn ordinary_http_and_ipv6_proxies_are_not_forced_through_web_origin_policy() { .expect("canonical IPv6 proxy must be representable"); assert_eq!(ipv6.as_str(), "socks5://[2001:db8::1]"); + let ipv6_https = ProxyServer::parse("https://[2001:db8::1]:443") + .expect("HTTPS default-port IPv6 proxy must be representable"); + assert_eq!(ipv6_https.as_str(), "https://[2001:db8::1]"); + let ipv6_nondefault = ProxyServer::parse("https://[2001:db8::1]:8443") .expect("non-default IPv6 proxy port must be preserved"); assert_eq!(ipv6_nondefault.as_str(), "https://[2001:db8::1]:8443"); diff --git a/crates/originweave-destination/tests/redirect_policy.rs b/crates/originweave-destination/tests/redirect_policy.rs index 91d4f9342..7012f727a 100644 --- a/crates/originweave-destination/tests/redirect_policy.rs +++ b/crates/originweave-destination/tests/redirect_policy.rs @@ -190,6 +190,34 @@ fn redirect_guard_rejects_https_downgrade_cycles_and_excess_hops() { ); } +#[test] +fn redirect_guard_accepts_the_exact_maximum_and_walks_every_hop() { + let initial = origin("https://start.example"); + let target = origin("https://target.example"); + let resolution = public_resolution(&target, [8, 8, 8, 8]); + let grants = BTreeSet::from([target.clone()]); + + let mut guard = RedirectGuard::new(initial, digest(DIGEST_A), MAX_REDIRECT_HOPS) + .expect("exact maximum redirect bound is accepted"); + assert_eq!(guard.maximum_hops(), MAX_REDIRECT_HOPS); + + for hop_number in 1..=MAX_REDIRECT_HOPS { + let hop_digest = digest(&format!("sha256:{hop_number:064x}")); + let evidence = guard + .authorize_redirect(target.clone(), hop_digest, &resolution, &grants) + .expect("every hop within the exact maximum must authorize"); + assert_eq!(evidence.hop_number(), hop_number); + } + assert_eq!(guard.hop_count(), MAX_REDIRECT_HOPS); + + let final_digest = digest(&format!("sha256:{:064x}", MAX_REDIRECT_HOPS + 1)); + assert_eq!( + guard.authorize_redirect(target, final_digest, &resolution, &grants), + Err(RedirectError::RedirectLimitExceeded) + ); + assert_eq!(guard.hop_count(), MAX_REDIRECT_HOPS); +} + #[test] fn explicitly_managed_http_loopback_redirects_do_not_trigger_downgrade_logic() { let initial = origin("http://localhost"); diff --git a/crates/originweave-destination/tests/resolution_freshness.rs b/crates/originweave-destination/tests/resolution_freshness.rs index 2df264563..99cc1b13b 100644 --- a/crates/originweave-destination/tests/resolution_freshness.rs +++ b/crates/originweave-destination/tests/resolution_freshness.rs @@ -81,6 +81,20 @@ fn fresh_resolution_rejects_invalid_or_overflowing_validity() { let address = ipv4(8, 8, 8, 8); let policy = DestinationPolicy::public_web(); + let exact_maximum = FreshResolutionSnapshot::approve( + target.clone(), + [address], + &policy, + Duration::from_secs(1), + MAX_RESOLUTION_VALIDITY, + ) + .expect("the documented maximum freshness interval is accepted"); + assert_eq!(exact_maximum.validity(), MAX_RESOLUTION_VALIDITY); + assert_eq!( + exact_maximum.valid_until(), + Duration::from_secs(1) + MAX_RESOLUTION_VALIDITY + ); + for validity in [ Duration::ZERO, MAX_RESOLUTION_VALIDITY + Duration::from_nanos(1), From 9d0cd1fe706852f5645277fe24ff352be5529bd5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 2 Oct 2026 23:48:09 +0900 Subject: [PATCH 2/3] test(destination): pin literal proxy and PAC authority bounds --- CHANGELOG.md | 1 + .../tests/proxy_authority_literal_bounds.rs | 105 ++++++++++++++++++ docs/TEST_STRATEGY.md | 2 + 3 files changed, 108 insertions(+) create mode 100644 crates/originweave-destination/tests/proxy_authority_literal_bounds.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b95d02924..ccaff7c69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Destination policy now has executable evidence for the exact proxy and PAC authority ceilings, the maximum redirect hop walk, and the freshness bounds that stay fail-closed at the published limits. +- Added independent literal proxy/PAC limit regressions that retain and authorize all 32 proxy servers and 16 PAC origins, preserve route metadata, and reject unlisted entries and adjacent overflow. Isolated mutations verify sensitivity; production route policy and network authority are unchanged. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. - Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, an organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge. diff --git a/crates/originweave-destination/tests/proxy_authority_literal_bounds.rs b/crates/originweave-destination/tests/proxy_authority_literal_bounds.rs new file mode 100644 index 000000000..92b1c64ce --- /dev/null +++ b/crates/originweave-destination/tests/proxy_authority_literal_bounds.rs @@ -0,0 +1,105 @@ +#![allow(clippy::expect_used)] + +use originweave_core::Origin; + +fn origin(value: &str) -> Origin { + Origin::parse(value).expect("test origin must parse") +} + +fn proxy(value: &str) -> ProxyServer { + ProxyServer::parse(value).expect("test proxy must parse") +} +use originweave_destination::{ + MAX_PAC_ORIGIN_COUNT, MAX_PROXY_SERVER_COUNT, ProxyRoute, ProxyRouteError, ProxyRouteKind, + ProxyRoutePolicy, ProxyServer, +}; + +#[test] +fn all_32_proxy_authorities_are_retained_and_the_33rd_is_rejected() -> Result<(), ProxyRouteError> { + assert_eq!(MAX_PROXY_SERVER_COUNT, 32); + let target = origin("https://target.example"); + let proxies = (0..32) + .map(|index| proxy(&format!("http://proxy-{index}.example:8080"))) + .collect::>(); + let policy = ProxyRoutePolicy::new(false, proxies.clone(), Vec::new())?; + assert!(!policy.allows_direct()); + for proxy_server in &proxies { + let evidence = policy.authorize( + &target, + &ProxyRoute::ExplicitProxy { + proxy_server: proxy_server.clone(), + }, + )?; + assert_eq!(evidence.route_kind(), ProxyRouteKind::ExplicitProxy); + assert_eq!(evidence.target_origin(), &target); + assert_eq!(evidence.proxy_server(), Some(proxy_server)); + assert_eq!(evidence.pac_origin(), None); + } + let unlisted = proxy("http://proxy-32.example:8080"); + assert_eq!( + policy.authorize( + &target, + &ProxyRoute::ExplicitProxy { + proxy_server: unlisted.clone(), + }, + ), + Err(ProxyRouteError::ProxyServerDenied { + server: unlisted.clone(), + }) + ); + let mut oversized = proxies; + oversized.push(unlisted); + assert_eq!( + ProxyRoutePolicy::new(false, oversized, Vec::new()), + Err(ProxyRouteError::TooManyProxyServers { + count: 33, + maximum: 32, + }) + ); + Ok(()) +} + +#[test] +fn all_16_pac_authorities_are_retained_and_the_17th_is_rejected() -> Result<(), ProxyRouteError> { + assert_eq!(MAX_PAC_ORIGIN_COUNT, 16); + let target = origin("https://target.example"); + let pacs = (0..16) + .map(|index| origin(&format!("https://pac-{index}.example"))) + .collect::>(); + let policy = ProxyRoutePolicy::new(true, Vec::new(), pacs.clone())?; + assert!(policy.allows_direct()); + for pac_origin in &pacs { + let evidence = policy.authorize( + &target, + &ProxyRoute::PacDirect { + pac_origin: pac_origin.clone(), + }, + )?; + assert_eq!(evidence.route_kind(), ProxyRouteKind::PacDirect); + assert_eq!(evidence.target_origin(), &target); + assert_eq!(evidence.proxy_server(), None); + assert_eq!(evidence.pac_origin(), Some(pac_origin)); + } + let unlisted = origin("https://pac-16.example"); + assert_eq!( + policy.authorize( + &target, + &ProxyRoute::PacDirect { + pac_origin: unlisted.clone(), + }, + ), + Err(ProxyRouteError::PacOriginDenied { + origin: unlisted.clone(), + }) + ); + let mut oversized = pacs; + oversized.push(unlisted); + assert_eq!( + ProxyRoutePolicy::new(true, Vec::new(), oversized), + Err(ProxyRouteError::TooManyPacOrigins { + count: 17, + maximum: 16, + }) + ); + Ok(()) +} diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index ba3c31624..8ff691f7c 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -101,6 +101,8 @@ Include: Include direct-only default, unauthorized proxy/PAC origin, PAC-selected DIRECT vs proxy authority, exact address set membership, port/timeout/attempt bounds, permission/input/address errors, transient retry allow-list, exact peer mismatch and single-use plan replay. +Proxy/PAC route-policy regressions independently assert the fixed limits of 32 proxy servers and 16 PAC origins. They authorize every retained entry, preserve the target and selected route metadata, reject an unlisted entry, and reject adjacent 33-server/17-origin input with exact count errors. Isolated limit-drift and later-entry omission mutations must fail the regressions. These tests verify already-correct route-policy admission; they do not resolve a destination, evaluate PAC, open a socket, or grant TCP/TLS authority. + ### 4.3 TLS Use real loopback certificates/roots for: From aa5cdf6cb8e322a4b7648886c7c6d9c48171858b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 4 Oct 2026 11:03:47 +0900 Subject: [PATCH 3/3] test(destination): pin independent twenty-hop redirect bounds --- CHANGELOG.md | 1 + .../tests/redirect_policy.rs | 22 +++++++++---------- docs/TEST_STRATEGY.md | 2 ++ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccaff7c69..55919b108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Destination policy now has executable evidence for the exact proxy and PAC authority ceilings, the maximum redirect hop walk, and the freshness bounds that stay fail-closed at the published limits. - Added independent literal proxy/PAC limit regressions that retain and authorize all 32 proxy servers and 16 PAC origins, preserve route metadata, and reject unlisted entries and adjacent overflow. Isolated mutations verify sensitivity; production route policy and network authority are unchanged. +- Pinned redirect regression inputs and expected state to the documented 20-hop ceiling, with typed rejection of a 21-hop configuration and the next redirect after 20 authorized hops. Isolated 19/21 ceiling mutations demonstrate the prior constant-derived oracle gap; production redirect policy is unchanged. - Corrected the 2026-08-26 product-gap snapshot with current #229 presentation-identity evidence, stacked-only #205 integration evidence, current base/head pairs, the 126-PR queue count, explicit root-versus-child merge ordering, and the active GitHub counted-approval gate. - Refreshed the product and technical gap baseline onto the 2026-08-26 live inventory: 126 open pull requests (54 ready, 72 draft), protected-main promotion of #168/#194/#196/#216/#151, a verified maintenance-loop record (supersession closure of #153, conflict reconciliations on #37/#149/#152/#173/#175, issue #212 option-(b) authorization on #43, Strix vuln-0001 homoglyph remediation on #124), provider-rerun outcome evidence, an organization review-pipeline congestion record, and refreshed merge-order queue guidance. Documentation evidence contracts were aligned to the same snapshot so the baseline, its dated markers, and the pinned exact-head rows cannot silently diverge. diff --git a/crates/originweave-destination/tests/redirect_policy.rs b/crates/originweave-destination/tests/redirect_policy.rs index 7012f727a..c8ffc4d0e 100644 --- a/crates/originweave-destination/tests/redirect_policy.rs +++ b/crates/originweave-destination/tests/redirect_policy.rs @@ -5,8 +5,8 @@ use std::net::{IpAddr, Ipv4Addr}; use originweave_core::Origin; use originweave_destination::{ - AddressClass, DestinationPolicy, MAX_REDIRECT_HOPS, RedirectError, RedirectGuard, - RedirectTargetDigest, RedirectTargetDigestError, ResolutionSnapshot, + AddressClass, DestinationPolicy, RedirectError, RedirectGuard, RedirectTargetDigest, + RedirectTargetDigestError, ResolutionSnapshot, }; const DIGEST_A: &str = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -62,10 +62,8 @@ fn redirect_guard_validates_bounds_and_reports_state() { Err(RedirectError::InvalidMaximumHops { maximum_hops: 0 }) ); assert_eq!( - RedirectGuard::new(initial.clone(), digest(DIGEST_A), MAX_REDIRECT_HOPS + 1,), - Err(RedirectError::InvalidMaximumHops { - maximum_hops: MAX_REDIRECT_HOPS + 1, - }) + RedirectGuard::new(initial.clone(), digest(DIGEST_A), 21), + Err(RedirectError::InvalidMaximumHops { maximum_hops: 21 }) ); let guard = @@ -197,25 +195,25 @@ fn redirect_guard_accepts_the_exact_maximum_and_walks_every_hop() { let resolution = public_resolution(&target, [8, 8, 8, 8]); let grants = BTreeSet::from([target.clone()]); - let mut guard = RedirectGuard::new(initial, digest(DIGEST_A), MAX_REDIRECT_HOPS) + let mut guard = RedirectGuard::new(initial, digest(DIGEST_A), 20) .expect("exact maximum redirect bound is accepted"); - assert_eq!(guard.maximum_hops(), MAX_REDIRECT_HOPS); + assert_eq!(guard.maximum_hops(), 20); - for hop_number in 1..=MAX_REDIRECT_HOPS { + for hop_number in 1..=20 { let hop_digest = digest(&format!("sha256:{hop_number:064x}")); let evidence = guard .authorize_redirect(target.clone(), hop_digest, &resolution, &grants) .expect("every hop within the exact maximum must authorize"); assert_eq!(evidence.hop_number(), hop_number); } - assert_eq!(guard.hop_count(), MAX_REDIRECT_HOPS); + assert_eq!(guard.hop_count(), 20); - let final_digest = digest(&format!("sha256:{:064x}", MAX_REDIRECT_HOPS + 1)); + let final_digest = digest(&format!("sha256:{:064x}", 21)); assert_eq!( guard.authorize_redirect(target, final_digest, &resolution, &grants), Err(RedirectError::RedirectLimitExceeded) ); - assert_eq!(guard.hop_count(), MAX_REDIRECT_HOPS); + assert_eq!(guard.hop_count(), 20); } #[test] diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 8ff691f7c..33bbe62eb 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -97,6 +97,8 @@ Include: - DNS contraction vs expansion/rebinding; - redirect downgrade, cycle, hop limit and new destination authorization. +The redirect regression independently pins the documented 20-hop maximum: it admits exactly 20 authorized hops, retains the count, rejects the next redirect, and rejects a configured maximum of 21. Its fixture and expectations do not derive the boundary from `MAX_REDIRECT_HOPS`. Isolated 19/21 constant mutations must fail the relevant authorization assertion; otherwise-valid target, grant, digest, and resolution inputs remain. This verifies redirect-guard admission only, not HTTP or network redirects. + ### 4.2 Route / TCP Include direct-only default, unauthorized proxy/PAC origin, PAC-selected DIRECT vs proxy authority, exact address set membership, port/timeout/attempt bounds, permission/input/address errors, transient retry allow-list, exact peer mismatch and single-use plan replay.