Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ 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.
- 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.

Expand Down
9 changes: 9 additions & 0 deletions crates/originweave-tls/src/handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
165 changes: 165 additions & 0 deletions crates/originweave-tls/tests/policy_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<Vec<u8>> = (0_u8..5)
.map(|index| vec![b'a' + index; MAX_ALPN_PROTOCOL_LENGTH])
.collect();
Expand All @@ -153,6 +190,134 @@ 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<Vec<u8>> = (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 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<Vec<u8>> = (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::<std::collections::BTreeSet<_>>()
.len(),
256
);
let encoded_bytes: usize = roots.iter().map(Vec::len).sum();
let boundary_bundle = TrustRootBundle::new(
TrustBundleIdentifier::parse("boundary_roots:v1").expect("identifier"),
roots.clone(),
)
.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]
fn canonical_https_origins_produce_dns_or_ip_reference_identities() {
let cases = [
Expand Down
93 changes: 93 additions & 0 deletions crates/originweave-tls/tests/tls_policy_literal_bounds.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>>) -> Result<TlsClientPolicy, TlsError> {
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<Vec<u8>> = (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::<Vec<_>>()
);
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<Vec<u8>> = (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::<usize>(), 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::<Vec<_>>()
);
exact_total[4].push(b'e');
assert!(matches!(
policy(timeout, exact_total),
Err(TlsError::InvalidAlpnBytes {
byte_count: 1_025,
maximum_bytes: 1_024
})
));
}
2 changes: 2 additions & 0 deletions docs/TEST_STRATEGY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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

When protected-main HTTP capability exists, tests include:
Expand Down
Loading