diff --git a/Cargo.lock b/Cargo.lock index 158700ff..aaa587e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -82,6 +97,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "bzip2" version = "0.6.1" @@ -109,6 +130,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cmake" version = "0.1.57" @@ -118,6 +153,12 @@ dependencies = [ "cc", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "data-encoding" version = "2.10.0" @@ -164,12 +205,30 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "fs_extra" version = "1.3.0" @@ -199,6 +258,47 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "itoa" version = "1.0.18" @@ -215,6 +315,16 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -233,6 +343,24 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "limbo-harness-support" +version = "0.1.0" +source = "git+https://github.com/C2SP/x509-limbo?rev=9c7359242f16265a5154bc5989eca91822ef5ed2#9c7359242f16265a5154bc5989eca91822ef5ed2" +dependencies = [ + "chrono", + "regress 0.11.1", + "serde", + "serde_json", + "typify", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.8.0" @@ -347,6 +475,26 @@ dependencies = [ "yasna", ] +[[package]] +name = "regress" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" +dependencies = [ + "hashbrown", + "memchr", +] + +[[package]] +name = "regress" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158a764437582235e3501f683b93a0a6f8d825d04a789dbe5ed30b8799b8908a" +dependencies = [ + "hashbrown", + "memchr", +] + [[package]] name = "ring" version = "0.17.14" @@ -423,6 +571,8 @@ dependencies = [ "base64", "bencher", "bzip2", + "chrono", + "limbo-harness-support", "once_cell", "rcgen", "rustls-aws-lc-rs", @@ -434,6 +584,46 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde" version = "1.0.228" @@ -464,6 +654,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_json" version = "1.0.149" @@ -477,6 +678,18 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_tokenstream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c49585c52c01f13c5c2ebb333f14f6885d76daa768d8a037d28017ec538c69" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", +] + [[package]] name = "shlex" version = "1.3.0" @@ -562,6 +775,53 @@ dependencies = [ "time-core", ] +[[package]] +name = "typify" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b715573a376585888b742ead9be5f4826105e622169180662e2c81bed4a149c3" +dependencies = [ + "typify-impl", + "typify-macro", +] + +[[package]] +name = "typify-impl" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fd0d27608a466d063d23b97cf2d26c25d838f01b4f7d5ff406a7446f16b6e3" +dependencies = [ + "heck", + "log", + "proc-macro2", + "quote", + "regress 0.10.5", + "schemars", + "semver", + "serde", + "serde_json", + "syn", + "thiserror", + "unicode-ident", +] + +[[package]] +name = "typify-macro" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd04bb1207cd4e250941cc1641f4c4815f7eaa2145f45c09dd49cb0a3691710a" +dependencies = [ + "proc-macro2", + "quote", + "schemars", + "semver", + "serde", + "serde_json", + "serde_tokenstream", + "syn", + "typify-impl", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -595,6 +855,110 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 014f8223..be1ea21d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,8 @@ untrusted = "0.9" base64 = "0.22" bencher = "0.1.5" bzip2 = "0.6" +chrono = "0.4" +limbo-harness-support = { git = "https://github.com/C2SP/x509-limbo", rev = "9c7359242f16265a5154bc5989eca91822ef5ed2" } once_cell = "1.17.2" rcgen = { version = "0.14.2", default-features = false, features = ["aws_lc_rs"] } rustls-aws-lc-rs = { version = "0.1.0-dev.0" } diff --git a/deny.toml b/deny.toml index 667b8d11..8e157c65 100644 --- a/deny.toml +++ b/deny.toml @@ -24,7 +24,11 @@ license-files = [ [bans] wildcards = "deny" +# Allow git/path dev-dependencies (like limbo-harness-support) without version specs +allow-wildcard-paths = true [sources] unknown-registry = "deny" unknown-git = "deny" +# Allow git source for x509-limbo test harness (dev dependency only) +allow-git = ["https://github.com/C2SP/x509-limbo"] diff --git a/tests/better_tls.rs b/tests/better_tls.rs deleted file mode 100644 index 49e811c2..00000000 --- a/tests/better_tls.rs +++ /dev/null @@ -1,161 +0,0 @@ -use core::time::Duration; -use std::collections::HashMap; -use std::fs::File; - -use base64::{Engine as _, engine::general_purpose}; -use bzip2::read::BzDecoder; -use pki_types::{ - CertificateDer, ServerName, SignatureVerificationAlgorithm, TrustAnchor, UnixTime, -}; -use serde::Deserialize; - -use webpki::{ExtendedKeyUsage, anchor_from_trusted_cert}; - -// All of the BetterTLS testcases use P256 keys. -static ALGS: &[&dyn SignatureVerificationAlgorithm] = &[ - rustls_ring::ECDSA_P256_SHA256, - rustls_aws_lc_rs::ECDSA_P256_SHA256, -]; - -#[ignore] // Runs slower than other unit tests - opt-in with `cargo test -- --include-ignored` -#[test] -fn path_building() { - let better_tls = testdata(); - let root_der = &better_tls.root_der(); - let root_der = CertificateDer::from(root_der.as_slice()); - let roots = &[anchor_from_trusted_cert(&root_der).expect("invalid trust anchor")]; - - let suite = "pathbuilding"; - run_testsuite( - suite, - better_tls - .suites - .get(suite) - .unwrap_or_else(|| panic!("missing {suite} suite")), - roots, - ); -} - -#[ignore] // Runs slower than other unit tests - opt-in with `cargo test -- --include-ignored` -#[test] -fn name_constraints() { - let better_tls = testdata(); - let root_der = &better_tls.root_der(); - let root_der = CertificateDer::from(root_der.as_slice()); - let roots = &[anchor_from_trusted_cert(&root_der).expect("invalid trust anchor")]; - - let suite = "nameconstraints"; - run_testsuite( - suite, - better_tls - .suites - .get(suite) - .unwrap_or_else(|| panic!("missing {suite} suite")), - roots, - ); -} - -fn run_testsuite(suite_name: &str, suite: &BetterTlsSuite, roots: &[TrustAnchor<'_>]) { - for testcase in &suite.test_cases { - println!("Testing {suite_name} test case {}", testcase.id); - - let certs_der = testcase.certs_der(); - let ee_der = CertificateDer::from(certs_der[0].as_slice()); - let intermediates = &certs_der[1..] - .iter() - .map(|cert| CertificateDer::from(cert.as_slice())) - .collect::>(); - - let ee_cert = webpki::EndEntityCert::try_from(&ee_der).expect("invalid end entity cert"); - - // Set the time to the time of test case generation. This ensures that the test case - // certificates won't expire. - let now = UnixTime::since_unix_epoch(Duration::from_secs(1_691_788_832)); - - let result = ee_cert - .verify_for_usage( - ALGS, - roots, - intermediates, - now, - &ExtendedKeyUsage::server_auth(), - None, - None, - ) - .and_then(|_| { - ee_cert.verify_is_valid_for_subject_name( - &ServerName::try_from(testcase.hostname.as_str()) - .expect("invalid testcase hostname"), - ) - }); - - match testcase.expected { - ExpectedResult::Accept => assert!(result.is_ok(), "expected success, got {result:?}"), - ExpectedResult::Reject => { - assert!(result.is_err(), "expected failure, got {result:?}") - } - } - } -} - -fn testdata() -> BetterTls { - let mut data_file = File::open("third-party/bettertls/bettertls.tests.json.bz2") - .expect("failed to open data file"); - let decompressor = BzDecoder::new(&mut data_file); - - let better_tls: BetterTls = serde_json::from_reader(decompressor).expect("invalid test JSON"); - println!("Testing BetterTLS revision {:?}", better_tls.revision); - - better_tls -} - -#[derive(Deserialize, Debug)] -struct BetterTls { - #[serde(rename(deserialize = "betterTlsRevision"))] - revision: String, - #[serde(rename(deserialize = "trustRoot"))] - root: String, - suites: HashMap, -} - -impl BetterTls { - fn root_der(&self) -> Vec { - general_purpose::STANDARD - .decode(&self.root) - .expect("invalid trust anchor base64") - } -} - -#[derive(Deserialize, Debug)] -struct BetterTlsSuite { - #[serde(rename(deserialize = "testCases"))] - test_cases: Vec, -} - -#[derive(Deserialize, Debug)] -struct BetterTlsTest { - id: u32, - certificates: Vec, - hostname: String, - expected: ExpectedResult, -} - -impl BetterTlsTest { - fn certs_der(&self) -> Vec> { - self.certificates - .iter() - .map(|cert| { - general_purpose::STANDARD - .decode(cert) - .expect("invalid cert base64") - }) - .collect() - } -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "UPPERCASE")] -enum ExpectedResult { - Accept, - Reject, -} diff --git a/tests/x509_limbo.rs b/tests/x509_limbo.rs new file mode 100644 index 00000000..e3160280 --- /dev/null +++ b/tests/x509_limbo.rs @@ -0,0 +1,262 @@ +#![cfg(feature = "alloc")] + +use std::collections::HashMap; +use std::fs::File; + +use bzip2::read::BzDecoder; +use chrono::{DateTime, Utc}; +use limbo_harness_support::models::{ExpectedResult, Feature, Limbo, Testcase, ValidationKind}; +use serde::{Deserialize, Serialize}; + +use pki_types::pem::PemObject; +use pki_types::{CertificateDer, CertificateRevocationListDer, ServerName, UnixTime}; +use webpki::{ + EndEntityCert, ExpirationPolicy, ExtendedKeyUsage, OwnedCertRevocationList, + RevocationCheckDepth, RevocationOptionsBuilder, UnknownStatusPolicy, anchor_from_trusted_cert, +}; + +#[ignore] // Runs slower than other unit tests - opt-in with `cargo test -- --include-ignored` +#[test] +fn x509_limbo() { + let mut data_file = + File::open("third-party/x509-limbo/limbo.json.bz2").expect("failed to open data file"); + + let limbo: Limbo = + serde_json::from_reader(BzDecoder::new(&mut data_file)).expect("invalid test JSON"); + + let exceptions = serde_json::from_reader( + File::open("third-party/x509-limbo/exceptions.json") + .expect("failed to open exceptions fiel"), + ) + .expect("invalid exceptions JSON"); + + let mut summary = Summary::default(); + for testcase in &limbo.testcases { + let id = testcase.id.to_string(); + + match evaluate_testcase(testcase, &exceptions) { + Outcome::Pass => summary.passed.push(id), + Outcome::Skip(reason) => summary.skipped.push((id, reason)), + Outcome::KnownDivergence => summary.known_divergences.push(id), + Outcome::UnexpectedFailure(err) => summary.unexpected_failures.push((id, err)), + Outcome::UnexpectedSuccess => summary.unexpected_successes.push(id), + } + } + + summary.print(); + + if summary.has_failures() { + panic!( + "x509-limbo: {} unexpected failures, {} unexpected successes", + summary.unexpected_failures.len(), + summary.unexpected_successes.len() + ); + } +} + +fn evaluate_testcase(tc: &Testcase, exceptions: &HashMap) -> Outcome { + // Check for skipped features first + if tc.features.contains(&Feature::MaxChainDepth) { + return Outcome::Skip("max-chain-depth testcases are not supported by this API".into()); + } + + if !matches!(tc.validation_kind, ValidationKind::Server) { + return Outcome::Skip("non-SERVER testcases not supported yet".into()); + } + + if !tc.signature_algorithms.is_empty() { + return Outcome::Skip("signature_algorithms not supported yet".into()); + } + + if !tc.key_usage.is_empty() { + return Outcome::Skip("key_usage not supported yet".into()); + } + + let validation_result = run_validation(tc); + let actual_success = validation_result.is_ok(); + let expected_success = matches!(tc.expected_result, ExpectedResult::Success); + + if let Some(exception) = exceptions.get(tc.id.as_str()) { + if actual_success == (exception.actual == "SUCCESS") { + return Outcome::KnownDivergence; + } + // If the exception no longer applies (behavior changed), fall through to normal comparison + } + + // Compare actual vs expected + match (expected_success, validation_result) { + (true, Ok(())) | (false, Err(_)) => Outcome::Pass, + (true, Err(err)) => Outcome::UnexpectedFailure(err), + (false, Ok(())) => Outcome::UnexpectedSuccess, + } +} + +/// Run validation and return Ok(()) on success, or an error message on failure +fn run_validation(tc: &Testcase) -> Result<(), String> { + let leaf_der = cert_der_from_pem(&tc.peer_certificate); + let leaf = + EndEntityCert::try_from(&leaf_der).map_err(|e| format!("leaf cert parse failed: {e}"))?; + + let intermediates: Vec<_> = tc + .untrusted_intermediates + .iter() + .map(|ic| cert_der_from_pem(ic)) + .collect(); + + let trust_anchor_ders: Vec<_> = tc + .trusted_certs + .iter() + .map(|ta| cert_der_from_pem(ta)) + .collect(); + + let trust_anchors: Vec<_> = trust_anchor_ders + .iter() + .filter_map(|der| anchor_from_trusted_cert(der).ok()) + .collect(); + + if trust_anchors.is_empty() && !trust_anchor_ders.is_empty() { + return Err("trust anchor extraction failed".into()); + } + + let validation_time = UnixTime::since_unix_epoch( + (tc.validation_time.unwrap_or_else(Utc::now) - DateTime::UNIX_EPOCH) + .to_std() + .expect("invalid validation time!"), + ); + + let sig_algs = rustls_aws_lc_rs::ALL_VERIFICATION_ALGS; + + let crls: Vec<_> = tc + .crls + .iter() + .map(|pem| { + OwnedCertRevocationList::from_der( + CertificateRevocationListDer::from_pem_slice(pem.as_bytes()) + .expect("CRL PEM parse failed") + .as_ref(), + ) + .expect("CRL DER parse failed") + .into() + }) + .collect(); + let crls: Vec<_> = crls.iter().collect(); + + let revocation_options = if !crls.is_empty() { + let opts = RevocationOptionsBuilder::new(crls.as_slice()).unwrap(); + opts.with_depth(RevocationCheckDepth::Chain); + opts.with_status_policy(UnknownStatusPolicy::Deny); + opts.with_expiration_policy(ExpirationPolicy::Enforce); + Some(opts.build()) + } else { + None + }; + + leaf.verify_for_usage( + sig_algs, + &trust_anchors, + &intermediates[..], + validation_time, + &ExtendedKeyUsage::server_auth(), + revocation_options, + None, + ) + .map_err(|e| e.to_string())?; + + // Verify subject name if expected + if let Some(peer_name) = tc.expected_peer_name.as_ref() { + let subject_name = ServerName::try_from(peer_name.value.as_str()) + .map_err(|_| format!("invalid expected peer name: {:?}", peer_name))?; + + leaf.verify_is_valid_for_subject_name(&subject_name) + .map_err(|_| "subject name validation failed")?; + } + + Ok(()) +} + +fn cert_der_from_pem>(bytes: B) -> CertificateDer<'static> { + CertificateDer::from_pem_slice(bytes.as_ref()) + .expect("cert PEM parse failed") + .into_owned() +} + +/// An exception entry for a testcase where webpki intentionally/knowingly diverges from the expected result. +#[derive(Debug, Deserialize, Serialize)] +struct Exception { + /// The expected result according to x509-limbo + expected: String, + /// What webpki actually produces + actual: String, + /// Why webpki diverges from the expected result + reason: String, +} + +/// Outcome of evaluating a single testcase +#[derive(Debug)] +enum Outcome { + /// Test passed - actual result matches expected + Pass, + /// Test was skipped (unsupported feature) + Skip(String), + /// Known divergence - in exceptions file + KnownDivergence, + /// Unexpected failure - expected SUCCESS but got FAILURE + UnexpectedFailure(String), + /// Unexpected success - expected FAILURE but got SUCCESS + UnexpectedSuccess, +} + +/// Summary of test run +#[derive(Debug, Default)] +struct Summary { + passed: Vec, + skipped: Vec<(String, String)>, + known_divergences: Vec, + unexpected_failures: Vec<(String, String)>, + unexpected_successes: Vec, +} + +impl Summary { + fn print(&self) { + println!("\nx509-limbo: {} tests", self.total()); + println!(" {} passed (match expected)", self.passed.len()); + println!(" {} skipped (unsupported features)", self.skipped.len()); + println!( + " {} known divergences (see exceptions.json)", + self.known_divergences.len() + ); + + if !self.unexpected_failures.is_empty() { + println!( + "\nUNEXPECTED FAILURES ({}):", + self.unexpected_failures.len() + ); + for (id, err) in &self.unexpected_failures { + println!(" - {id}: {err}"); + } + } + + if !self.unexpected_successes.is_empty() { + println!( + "\nUNEXPECTED SUCCESSES ({}):", + self.unexpected_successes.len() + ); + println!(" (expected FAILURE but got SUCCESS - may indicate a bug in webpki)"); + for id in &self.unexpected_successes { + println!(" - {id}"); + } + } + } + + fn total(&self) -> usize { + self.passed.len() + + self.skipped.len() + + self.known_divergences.len() + + self.unexpected_failures.len() + + self.unexpected_successes.len() + } + + fn has_failures(&self) -> bool { + !self.unexpected_failures.is_empty() || !self.unexpected_successes.is_empty() + } +} diff --git a/third-party/bettertls/LICENSE b/third-party/bettertls/LICENSE deleted file mode 100644 index 4947287f..00000000 --- a/third-party/bettertls/LICENSE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/third-party/bettertls/README.md b/third-party/bettertls/README.md deleted file mode 100644 index 8ace1153..00000000 --- a/third-party/bettertls/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# BetterTLS Test Suite - -Generated using the Netflix [bettertls] project. - -[bettertls]: https://github.com/Netflix/bettertls - -## Test Data - -To regenerate vendored test data: - -1. Install Go -2. Generate the JSON testdata export: - -```bash -GOBIN=$PWD go install github.com/Netflix/bettertls/test-suites/cmd/bettertls@latest -./bettertls export-tests --out ./bettertls.tests.json -``` - -3. Bzip2 compress it: - -```bash -bzip2 ./bettertls.tests.json -``` diff --git a/third-party/bettertls/bettertls.tests.json.bz2 b/third-party/bettertls/bettertls.tests.json.bz2 deleted file mode 100644 index db066c86..00000000 Binary files a/third-party/bettertls/bettertls.tests.json.bz2 and /dev/null differ diff --git a/third-party/x509-limbo/exceptions.json b/third-party/x509-limbo/exceptions.json new file mode 100644 index 00000000..60314193 --- /dev/null +++ b/third-party/x509-limbo/exceptions.json @@ -0,0 +1,292 @@ +{ + "pathlen::validation-ignores-pathlen-in-leaf": { + "expected": "SUCCESS", + "actual": "FAILURE", + "reason": "webpki intentionally rejects CA certificates in leaf position (CaUsedAsEndEntity)" + }, + "pathlen::self-issued-certs-pathlen": { + "expected": "SUCCESS", + "actual": "FAILURE", + "reason": "webpki does not support self-signed certificates" + }, + "rfc5280::nc::permitted-dn-match": { + "expected": "SUCCESS", + "actual": "FAILURE", + "reason": "webpki does not support DirectoryName name constraints" + }, + "rfc5280::nc::permitted-self-issued": { + "expected": "SUCCESS", + "actual": "FAILURE", + "reason": "webpki does not support self-signed certificates" + }, + "rfc5280::ca-as-leaf": { + "expected": "SUCCESS", + "actual": "FAILURE", + "reason": "webpki intentionally rejects CA certificates in leaf position (CaUsedAsEndEntity)" + }, + "crl::crlnumber-missing": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement for CRLNumber extension presence" + }, + "crl::crlnumber-critical": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement that CRLNumber be non-critical" + }, + "crl::issuer-missing-crlsign": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce cRLSign key usage requirement for CRL issuers" + }, + "rfc5280::aki::critical-aki": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement that AKI be non-critical" + }, + "rfc5280::aki::leaf-missing-aki": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement for AKI presence" + }, + "rfc5280::aki::intermediate-missing-aki": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement for AKI presence" + }, + "rfc5280::aki::cross-signed-root-missing-aki": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement for AKI presence" + }, + "rfc5280::nc::permitted-dns-match-noncritical": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement that NameConstraints be critical" + }, + "rfc5280::nc::invalid-dnsname-leading-period": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki accepts leading period in DNS name constraints" + }, + "rfc5280::nc::not-allowed-in-ee-noncritical": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not reject NameConstraints extension in EE certificates" + }, + "rfc5280::nc::not-allowed-in-ee-critical": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not reject NameConstraints extension in EE certificates" + }, + "rfc5280::pc::ica-noncritical-pc": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement that PolicyConstraints be critical" + }, + "rfc5280::san::noncritical-with-empty-subject": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement that SAN be critical when subject is empty" + }, + "rfc5280::san::underscore-dns": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki accepts underscores in DNS names" + }, + "rfc5280::serial::too-long": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 serial number length limit" + }, + "rfc5280::serial::zero": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement that serial numbers be positive for compatibility reasons" + }, + "rfc5280::ski::critical-ski": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement that SKI be non-critical" + }, + "rfc5280::ski::root-missing-ski": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement for SKI presence in CA certs" + }, + "rfc5280::ski::intermediate-missing-ski": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement for SKI presence in CA certs" + }, + "rfc5280::validity::expired-root": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not check validity period of trust anchors" + }, + "rfc5280::ca-empty-subject": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce RFC 5280 requirement that CA subjects be non-empty" + }, + "rfc5280::unknown-critical-extension-root": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not reject trust anchors with unknown critical extensions" + }, + "rfc5280::root-missing-basic-constraints": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce BasicConstraints presence in trust anchors" + }, + "rfc5280::root-non-critical-basic-constraints": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce BasicConstraints criticality in trust anchors" + }, + "rfc5280::root-inconsistent-ca-extensions": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce consistency between BasicConstraints.cA and KeyUsage.keyCertSign in trust anchors" + }, + "rfc5280::leaf-ku-keycertsign": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not reject EE certs with keyCertSign in KeyUsage" + }, + "webpki::aki::root-with-aki-missing-keyidentifier": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF requirement for keyIdentifier in root AKI" + }, + "webpki::aki::root-with-aki-authoritycertissuer": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF prohibition on authorityCertIssuer in root AKI" + }, + "webpki::aki::root-with-aki-authoritycertserialnumber": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF prohibition on authorityCertSerialNumber in root AKI" + }, + "webpki::aki::root-with-aki-all-fields": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF AKI field requirements for roots" + }, + "webpki::aki::root-with-aki-ski-mismatch": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF requirement that root AKI match SKI" + }, + "webpki::cn::ipv4-hex-mismatch": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::cn::ipv4-leading-zeros-mismatch": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::cn::ipv6-uppercase-mismatch": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::cn::ipv6-uncompressed-mismatch": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::cn::ipv6-non-rfc5952-mismatch": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::cn::punycode-not-in-san": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::cn::utf8-vs-punycode-mismatch": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::cn::not-in-san": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::cn::case-mismatch": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not honor CN" + }, + "webpki::eku::ee-anyeku": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF prohibition on anyExtendedKeyUsage in EE certs" + }, + "webpki::eku::ee-critical-eku": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF requirement that EKU be non-critical" + }, + "webpki::eku::ee-without-eku": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF requirement for EKU presence in EE certs" + }, + "webpki::eku::root-has-eku": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF prohibition on EKU in root certs" + }, + "webpki::nc::intermediate-permitted-excluded-subtrees-both-null": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF NameConstraints encoding requirements" + }, + "webpki::nc::intermediate-permitted-excluded-subtrees-both-empty-sequences": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF NameConstraints encoding requirements" + }, + "webpki::san::san-critical-with-nonempty-subject": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF requirement that SAN not be critical when subject is non-empty" + }, + "webpki::malformed-aia": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not validate AIA extension contents" + }, + "webpki::forbidden-p192-leaf": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF key algorithm restrictions (P-192 accepted)" + }, + "webpki::forbidden-dsa-leaf": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF key algorithm restrictions (DSA accepted)" + }, + "webpki::forbidden-weak-rsa-in-leaf": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF RSA key size minimum (1024-bit accepted)" + }, + "webpki::forbidden-rsa-not-divisable-by-8-in-root": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF requirement that RSA key size be divisible by 8" + }, + "webpki::forbidden-rsa-key-not-divisable-by-8-in-leaf": { + "expected": "FAILURE", + "actual": "SUCCESS", + "reason": "webpki does not enforce CABF requirement that RSA key size be divisible by 8" + } +} diff --git a/third-party/x509-limbo/limbo.json.bz2 b/third-party/x509-limbo/limbo.json.bz2 new file mode 100644 index 00000000..0ff94b19 Binary files /dev/null and b/third-party/x509-limbo/limbo.json.bz2 differ