From ae95cbf2e37de5c9d9f0ebbe06e4dd2666212fc0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 30 Sep 2026 11:59:00 +0900 Subject: [PATCH 1/3] test(tls): cover exact peer certificate limits Co-Authored-By: Claude Code --- CHANGELOG.md | 1 + crates/originweave-tls/src/handshake.rs | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f747adeae..313c44399 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 +- Added regression checks that accept TLS peer-certificate chains at the exact count and byte limits while rejecting oversized chains. - 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-tls/src/handshake.rs b/crates/originweave-tls/src/handshake.rs index 921a33232..53f37800a 100644 --- a/crates/originweave-tls/src/handshake.rs +++ b/crates/originweave-tls/src/handshake.rs @@ -956,6 +956,15 @@ mod tests { fn certificate_bounds_are_fail_closed() { let valid = vec![CertificateDer::from(vec![1_u8])]; validate_certificate_bounds(&valid).expect("bounded certificate"); + let exact_count = vec![CertificateDer::from(vec![1_u8]); MAX_SERVER_CERTIFICATE_COUNT]; + validate_certificate_bounds(&exact_count) + .expect("exact certificate count maximum must be accepted"); + let exact_bytes = vec![CertificateDer::from(vec![ + 1_u8; + MAX_SERVER_CERTIFICATE_BYTES + ])]; + validate_certificate_bounds(&exact_bytes) + .expect("exact certificate byte maximum must be accepted"); let missing = validate_certificate_bounds(&[]).expect_err("missing certificate"); assert_error_variant(&missing, &TlsError::MissingPeerCertificates); From 8b012bed253cc122211176693b36ae885b04c018 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 2 Oct 2026 23:51:29 +0900 Subject: [PATCH 2/3] test(tls): pin literal policy and ALPN authority bounds --- CHANGELOG.md | 1 + .../originweave-tls/tests/policy_contract.rs | 116 ++++++++++++++++++ .../tests/tls_policy_literal_bounds.rs | 93 ++++++++++++++ docs/TEST_STRATEGY.md | 2 + 4 files changed, 212 insertions(+) create mode 100644 crates/originweave-tls/tests/tls_policy_literal_bounds.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 313c44399..170c695d7 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 +- Added independent literal TLS policy regressions for the 30-second handshake deadline, seven-day leaf horizon, eight ALPN identifiers, 255-byte identifiers, and 1,024 total bytes, retaining exact inputs and rejecting each adjacent overflow without changing production policy. - Added regression checks that accept TLS peer-certificate chains at the exact count and byte limits while rejecting oversized chains. - 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-tls/tests/policy_contract.rs b/crates/originweave-tls/tests/policy_contract.rs index 4fad353b3..68280c3c5 100644 --- a/crates/originweave-tls/tests/policy_contract.rs +++ b/crates/originweave-tls/tests/policy_contract.rs @@ -35,6 +35,21 @@ fn trust_bundle_identifier_is_bounded_and_ascii() { TrustBundleIdentifier::parse(&"a".repeat(129)), Err(TlsError::InvalidTrustBundleIdentifier) )); + + let exact_maximum = "a".repeat(128); + assert_eq!( + TrustBundleIdentifier::parse(&exact_maximum) + .expect("128-byte identifier is the accepted maximum") + .as_str(), + exact_maximum + ); + let full_alphabet = "Az09._:-az09._:-"; + assert_eq!( + TrustBundleIdentifier::parse(full_alphabet) + .expect("every admitted identifier byte is accepted") + .as_str(), + full_alphabet + ); } #[test] @@ -73,6 +88,14 @@ fn trust_root_bundle_is_nonempty_bounded_deduplicated_and_hashed() { ), Err(TlsError::InvalidTrustRootBytes { .. }) )); + let exact_maximum_bytes = TrustRootBundle::new( + TrustBundleIdentifier::parse("at_limit:v1").expect("identifier"), + vec![vec![0_u8; MAX_TRUST_ROOT_BYTES]], + ); + assert!( + matches!(exact_maximum_bytes, Err(TlsError::InvalidTrustRoot { .. })), + "exactly MAX_TRUST_ROOT_BYTES must pass the byte-count gate and fail later at DER parsing", + ); assert!(matches!( TrustRootBundle::new( TrustBundleIdentifier::parse("malformed:v1").expect("identifier"), @@ -100,6 +123,13 @@ fn tls_policy_bounds_timeouts_and_alpn() { [b"h2".as_slice(), b"http/1.1".as_slice()] ); assert_eq!(policy.alpn_requirement(), AlpnRequirement::Required); + assert_eq!(policy.minimum_leaf_validity(), Duration::ZERO); + + let horizon = Duration::from_secs(3_600); + let policy_with_horizon = policy + .with_minimum_leaf_validity(horizon) + .expect("valid delegated-task leaf horizon"); + assert_eq!(policy_with_horizon.minimum_leaf_validity(), horizon); assert!(matches!( TlsClientPolicy::new( @@ -129,6 +159,13 @@ fn tls_policy_bounds_timeouts_and_alpn() { )); } + assert!(matches!( + policy_with_horizon.with_minimum_leaf_validity( + originweave_tls::MAX_MINIMUM_LEAF_VALIDITY + Duration::from_nanos(1), + ), + Err(TlsError::InvalidMinimumLeafValidity { .. }) + )); + let cumulative_overflow: Vec> = (0_u8..5) .map(|index| vec![b'a' + index; MAX_ALPN_PROTOCOL_LENGTH]) .collect(); @@ -153,6 +190,85 @@ fn tls_policy_bounds_timeouts_and_alpn() { } } +#[test] +fn tls_policy_accepts_every_exact_maximum_bound() { + let trusted_time = UnixTime::since_unix_epoch(Duration::from_secs(1_800_000_000)); + + let boundary_timeout = TlsClientPolicy::new( + trusted_time, + MAX_TLS_HANDSHAKE_TIMEOUT, + vec![b"h2".to_vec()], + AlpnRequirement::Required, + ) + .expect("maximum handshake timeout is accepted"); + assert_eq!( + boundary_timeout.handshake_timeout(), + MAX_TLS_HANDSHAKE_TIMEOUT + ); + + let optional_without_alpn = TlsClientPolicy::new( + trusted_time, + Duration::from_secs(1), + Vec::new(), + AlpnRequirement::Optional, + ) + .expect("optional ALPN admits an empty allow-list"); + assert!(optional_without_alpn.alpn_protocols().is_empty()); + + let maximum_protocol_count: Vec> = (0..MAX_ALPN_PROTOCOL_COUNT) + .map(|index| format!("p{index}").into_bytes()) + .collect(); + TlsClientPolicy::new( + trusted_time, + Duration::from_secs(1), + maximum_protocol_count, + AlpnRequirement::Required, + ) + .expect("maximum distinct ALPN protocol count is accepted"); + + TlsClientPolicy::new( + trusted_time, + Duration::from_secs(1), + vec![vec![b'a'; MAX_ALPN_PROTOCOL_LENGTH]], + AlpnRequirement::Required, + ) + .expect("maximum ALPN protocol length is accepted"); + + let mut maximum_total_bytes = vec![ + vec![b'a'; MAX_ALPN_PROTOCOL_LENGTH], + vec![b'b'; MAX_ALPN_PROTOCOL_LENGTH], + vec![b'c'; MAX_ALPN_PROTOCOL_LENGTH], + vec![b'd'; MAX_ALPN_PROTOCOL_LENGTH], + ]; + maximum_total_bytes.push(vec![ + b'e'; + MAX_ALPN_TOTAL_BYTES - 4 * MAX_ALPN_PROTOCOL_LENGTH + ]); + TlsClientPolicy::new( + trusted_time, + Duration::from_secs(1), + maximum_total_bytes, + AlpnRequirement::Required, + ) + .expect("exact maximum ALPN total bytes is accepted"); + + let boundary_horizon = boundary_timeout + .with_minimum_leaf_validity(originweave_tls::MAX_MINIMUM_LEAF_VALIDITY) + .expect("maximum leaf validity horizon is accepted"); + assert_eq!( + boundary_horizon.minimum_leaf_validity(), + originweave_tls::MAX_MINIMUM_LEAF_VALIDITY + ); + + let root = root_der(); + let boundary_bundle = TrustRootBundle::new( + TrustBundleIdentifier::parse("boundary_roots:v1").expect("identifier"), + std::iter::repeat_n(root, MAX_TRUST_ROOT_COUNT).collect(), + ) + .expect("maximum trust root count is accepted before canonical deduplication"); + assert_eq!(boundary_bundle.root_count(), 1); +} + #[test] fn canonical_https_origins_produce_dns_or_ip_reference_identities() { let cases = [ diff --git a/crates/originweave-tls/tests/tls_policy_literal_bounds.rs b/crates/originweave-tls/tests/tls_policy_literal_bounds.rs new file mode 100644 index 000000000..590e84982 --- /dev/null +++ b/crates/originweave-tls/tests/tls_policy_literal_bounds.rs @@ -0,0 +1,93 @@ +#![allow(clippy::expect_used)] + +use std::time::Duration; + +use originweave_tls::{AlpnRequirement, TlsClientPolicy, TlsError}; +use rustls::pki_types::UnixTime; + +fn policy(timeout: Duration, protocols: Vec>) -> Result { + TlsClientPolicy::new( + UnixTime::since_unix_epoch(Duration::from_secs(1_800_000_000)), + timeout, + protocols, + AlpnRequirement::Required, + ) +} + +#[test] +fn literal_handshake_deadline_and_leaf_horizon_preserve_exact_limits() { + let maximum = policy(Duration::from_secs(30), vec![b"h2".to_vec()]) + .expect("the documented 30-second handshake ceiling is admitted"); + assert_eq!(maximum.handshake_timeout(), Duration::from_secs(30)); + let excessive_timeout = Duration::from_secs(30) + Duration::from_nanos(1); + assert!(matches!( + policy(excessive_timeout, vec![b"h2".to_vec()]), + Err(TlsError::InvalidHandshakeTimeout { timeout, maximum_timeout }) + if timeout == excessive_timeout && maximum_timeout == Duration::from_secs(30) + )); + let horizon = maximum + .with_minimum_leaf_validity(Duration::from_secs(604_800)) + .expect("the documented seven-day leaf horizon is admitted"); + assert_eq!( + horizon.minimum_leaf_validity(), + Duration::from_secs(604_800) + ); + let excessive_horizon = Duration::from_secs(604_800) + Duration::from_nanos(1); + assert!(matches!( + horizon.with_minimum_leaf_validity(excessive_horizon), + Err(TlsError::InvalidMinimumLeafValidity { minimum_validity, maximum_validity }) + if minimum_validity == excessive_horizon && maximum_validity == Duration::from_secs(604_800) + )); +} + +#[test] +fn literal_alpn_count_length_and_total_limits_preserve_every_identifier() { + let timeout = Duration::from_secs(1); + let eight: Vec> = (0..8) + .map(|index| format!("p{index}").into_bytes()) + .collect(); + let count_policy = policy(timeout, eight.clone()).expect("eight distinct ALPN identifiers"); + assert_eq!( + count_policy.alpn_protocols(), + eight.iter().map(Vec::as_slice).collect::>() + ); + let mut nine = eight; + nine.push(b"p8".to_vec()); + assert!(matches!( + policy(timeout, nine), + Err(TlsError::InvalidAlpnCount { + protocol_count: 9, + maximum_count: 8 + }) + )); + + let longest = vec![0x80; 255]; + let length_policy = policy(timeout, vec![longest.clone()]) + .expect("255 opaque ALPN bytes, with no ASCII reinterpretation"); + assert_eq!(length_policy.alpn_protocols(), [longest.as_slice()]); + assert!(matches!( + policy(timeout, vec![vec![0x80; 256]]), + Err(TlsError::InvalidAlpnIdentifier { + protocol_index: 0, + protocol_length: 256, + maximum_length: 255, + }) + )); + + let mut exact_total: Vec> = (b'a'..=b'd').map(|value| vec![value; 255]).collect(); + exact_total.push(vec![b'e'; 4]); + assert_eq!(exact_total.iter().map(Vec::len).sum::(), 1_024); + let total_policy = policy(timeout, exact_total.clone()).expect("1,024 total ALPN bytes"); + assert_eq!( + total_policy.alpn_protocols(), + exact_total.iter().map(Vec::as_slice).collect::>() + ); + exact_total[4].push(b'e'); + assert!(matches!( + policy(timeout, exact_total), + Err(TlsError::InvalidAlpnBytes { + byte_count: 1_025, + maximum_bytes: 1_024 + }) + )); +} diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index ba3c31624..fb8f450a8 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -117,6 +117,8 @@ Use real loopback certificates/roots for: - certificate/trust/ALPN/deadline bounds; - task-horizon safety policy if shipped. +Pure TLS policy regressions also use independent literal limits: a 30-second handshake deadline, 604,800-second leaf horizon, eight ALPN identifiers, 255 bytes per identifier, and 1,024 total ALPN bytes. Each exact boundary is admitted and its adjacent overflow is rejected with the corresponding error and reported limit. Tests compare every retained identifier, including opaque non-ASCII ALPN bytes, without network access or certificate-policy changes. Isolated reductions of these five production constants demonstrate regression sensitivity; mutation results are not production-defect or TLS-handshake acceptance evidence. + ### 4.4 HTTP When protected-main HTTP capability exists, tests include: From 6dbd71a58503e546f3a27d2d1f7b295661fb4a95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 3 Oct 2026 11:45:13 +0900 Subject: [PATCH 3/3] test(tls): retain distinct trust roots at literal count limit --- CHANGELOG.md | 1 + .../originweave-tls/tests/policy_contract.rs | 55 ++++++++++++++++++- docs/TEST_STRATEGY.md | 2 +- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 170c695d7..aafeaf171 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 +- Strengthened the trust-root boundary regression with 256 independently generated distinct certificates, canonical retained count and byte assertions, order-independent hashing, and literal 257-root rejection, while preserving the separate duplicate-input case. Production TLS behavior is unchanged. - Added independent literal TLS policy regressions for the 30-second handshake deadline, seven-day leaf horizon, eight ALPN identifiers, 255-byte identifiers, and 1,024 total bytes, retaining exact inputs and rejecting each adjacent overflow without changing production policy. - Added regression checks that accept TLS peer-certificate chains at the exact count and byte limits while rejecting oversized chains. - 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. diff --git a/crates/originweave-tls/tests/policy_contract.rs b/crates/originweave-tls/tests/policy_contract.rs index 68280c3c5..043f7014d 100644 --- a/crates/originweave-tls/tests/policy_contract.rs +++ b/crates/originweave-tls/tests/policy_contract.rs @@ -261,12 +261,61 @@ fn tls_policy_accepts_every_exact_maximum_bound() { ); let root = root_der(); + let duplicate_bundle = TrustRootBundle::new( + TrustBundleIdentifier::parse("duplicate_roots:v1").expect("identifier"), + std::iter::repeat_n(root, 256).collect(), + ) + .expect("256 input roots are accepted before canonical deduplication"); + assert_eq!(duplicate_bundle.root_count(), 1); + + let roots: Vec> = (0..256) + .map(|index| { + rcgen::generate_simple_self_signed(vec![format!("root-{index}.example")]) + .expect("distinct test root generation") + .cert + .der() + .to_vec() + }) + .collect(); + assert_eq!( + roots + .iter() + .collect::>() + .len(), + 256 + ); + let encoded_bytes: usize = roots.iter().map(Vec::len).sum(); let boundary_bundle = TrustRootBundle::new( TrustBundleIdentifier::parse("boundary_roots:v1").expect("identifier"), - std::iter::repeat_n(root, MAX_TRUST_ROOT_COUNT).collect(), + roots.clone(), ) - .expect("maximum trust root count is accepted before canonical deduplication"); - assert_eq!(boundary_bundle.root_count(), 1); + .expect("256 distinct roots are retained at the literal count limit"); + assert_eq!(boundary_bundle.root_count(), 256); + assert_eq!(boundary_bundle.encoded_byte_count(), encoded_bytes); + + let mut reversed_roots = roots.clone(); + reversed_roots.reverse(); + let reversed_bundle = TrustRootBundle::new( + TrustBundleIdentifier::parse("boundary_roots:v1").expect("identifier"), + reversed_roots, + ) + .expect("reversed root input retains the same canonical bundle"); + assert_eq!(reversed_bundle.root_count(), 256); + assert_eq!(reversed_bundle.encoded_byte_count(), encoded_bytes); + assert_eq!(reversed_bundle.bundle_hash(), boundary_bundle.bundle_hash()); + + let mut oversized_roots = roots; + oversized_roots.push(root_der()); + assert!(matches!( + TrustRootBundle::new( + TrustBundleIdentifier::parse("overflow_roots:v1").expect("identifier"), + oversized_roots, + ), + Err(TlsError::InvalidTrustRootCount { + root_count: 257, + maximum_count: 256, + }) + )); } #[test] diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index fb8f450a8..6b57a5f35 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -117,7 +117,7 @@ Use real loopback certificates/roots for: - certificate/trust/ALPN/deadline bounds; - task-horizon safety policy if shipped. -Pure TLS policy regressions also use independent literal limits: a 30-second handshake deadline, 604,800-second leaf horizon, eight ALPN identifiers, 255 bytes per identifier, and 1,024 total ALPN bytes. Each exact boundary is admitted and its adjacent overflow is rejected with the corresponding error and reported limit. Tests compare every retained identifier, including opaque non-ASCII ALPN bytes, without network access or certificate-policy changes. Isolated reductions of these five production constants demonstrate regression sensitivity; mutation results are not production-defect or TLS-handshake acceptance evidence. +Pure TLS policy regressions also use independent literal limits: a 30-second handshake deadline, 604,800-second leaf horizon, eight ALPN identifiers, 255 bytes per identifier, and 1,024 total ALPN bytes. Each exact boundary is admitted and its adjacent overflow is rejected with the corresponding error and reported limit. Tests compare every retained identifier, including opaque non-ASCII ALPN bytes, without network access or certificate-policy changes. Trust-root count coverage additionally distinguishes 256 duplicate inputs (one canonical root) from 256 independently generated certificates retained after canonicalization, checks their exact total encoded bytes and order-independent bundle hash, and rejects 257 inputs. Isolated reductions of the policy constants demonstrate regression sensitivity; mutation results are not production-defect or TLS-handshake acceptance evidence. ### 4.4 HTTP