use super::*;
use crate::HttpComponent;
use crate::config::TlsConfig;
use crate::lock_ca_store_test_mutex;
use crate::tls_harness::*;
use camel_component_api::Component;
use camel_component_api::NoOpComponentContext;
#[test]
fn test_webpki_fallback_config_builds_client() {
let client = reqwest::Client::builder()
.tls_backend_preconfigured(webpki_root_client_config())
.build()
.expect("webpki-rooted client must build"); let rendered = format!("{client:?}");
assert!(
!rendered.is_empty(),
"client must render a debug representation"
);
}
fn fallback_test_ca() -> (tempfile::TempDir, String) {
let ca_key = rcgen::KeyPair::generate().expect("ca keygen"); let mut ca_params = rcgen::CertificateParams::default();
ca_params
.distinguished_name
.push(rcgen::DnType::CommonName, "Fallback test CA");
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let ca_cert = ca_params.self_signed(&ca_key).expect("ca self-sign"); let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("ca.pem");
std::fs::write(&ca_path, ca_cert.pem()).expect("write ca pem"); (dir, ca_path.display().to_string())
}
struct StrictMaterial {
dir: tempfile::TempDir,
ca: TestCa,
ca_path: std::path::PathBuf,
cert_path: std::path::PathBuf,
key_path: std::path::PathBuf,
}
fn strict_material_fixtures() -> StrictMaterial {
let ca = gen_test_ca();
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("ca.pem");
std::fs::write(&ca_path, ca.ca_pem.as_bytes()).expect("write ca pem"); let (client_cert_pem, client_key_pem) = gen_client_identity(&ca);
let cert_path = dir.path().join("client-cert.pem");
std::fs::write(&cert_path, client_cert_pem.as_bytes()).expect("write client cert"); let key_path = dir.path().join("client-key.pem");
std::fs::write(&key_path, client_key_pem.as_bytes()).expect("write client key"); StrictMaterial {
dir,
ca,
ca_path,
cert_path,
key_path,
}
}
fn assert_strict_fallback_error(err: CamelError, fragments: &[&str]) {
match err {
CamelError::EndpointCreationFailed(msg) => {
for fragment in fragments {
assert!(msg.contains(fragment), "message: {msg}");
}
}
other => panic!("expected EndpointCreationFailed, got {other:?}"),
}
}
#[test]
fn test_fallback_root_store_union_custom_and_mozilla() {
let (_dir, ca_path) = fallback_test_ca();
let store = fallback_root_store(Some(&ca_path), true).expect("union root store must build"); assert_eq!(
store.roots.len(),
webpki_roots::TLS_SERVER_ROOTS.len() + 1,
"custom CA must UNION with the Mozilla anchors, not replace them"
);
let ca_pem = std::fs::read_to_string(&ca_path).expect("read ca pem back"); let der = rustls_pemfile::certs(&mut std::io::Cursor::new(ca_pem.as_bytes()))
.next()
.expect("exactly one CERTIFICATE section in test CA") .expect("parse test CA pem"); let mut scratch = rustls::RootCertStore::empty();
let (added, _ignored) = scratch.add_parsable_certificates(vec![der]);
assert_eq!(added, 1, "scratch root store must accept the test CA");
let expected_anchor = scratch.roots[0].clone();
assert_eq!(
store.roots.first(),
Some(&expected_anchor),
"custom anchor must sit at index 0 — parity with the primary path's \
extra-roots-before-platform-roots order"
);
assert_eq!(
store.roots[1..],
*webpki_roots::TLS_SERVER_ROOTS,
"everything after the custom anchor must be exactly the bundled Mozilla anchors"
);
let first_custom = store
.roots
.iter()
.position(|anchor| anchor == &expected_anchor)
.expect("custom anchor present in store"); let first_mozilla = store
.roots
.iter()
.position(|anchor| webpki_roots::TLS_SERVER_ROOTS.contains(anchor))
.expect("Mozilla anchors present in store"); assert!(
first_custom < first_mozilla,
"first custom anchor (index {first_custom}) must precede the first \
Mozilla anchor (index {first_mozilla})"
);
}
#[test]
fn test_fallback_root_store_no_ca_is_mozilla_only() {
let store = fallback_root_store(None, true).expect("mozilla-only store must build"); assert_eq!(
store.roots.len(),
webpki_roots::TLS_SERVER_ROOTS.len(),
"no configured CA must leave the bundled Mozilla anchors untouched"
);
assert_eq!(
store.roots,
*webpki_roots::TLS_SERVER_ROOTS,
"with no configured CA the whole store IS the bundled Mozilla anchors, \
byte-for-byte"
);
}
#[test]
fn test_fallback_root_store_strict_unreadable_ca_fails_closed() {
let err = match fallback_root_store(Some("/nonexistent/ca-bundle.pem"), true) {
Err(e) => e,
Ok(_) => panic!("strict mode must fail closed on an unreadable CA file"),
};
assert_strict_fallback_error(err, &["tls.strict/webpki-fallback", "unreadable"]);
}
#[test]
fn test_fallback_root_store_strict_zero_pem_sections_fails_closed() {
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("key-only.pem");
std::fs::write(
&ca_path,
"-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n",
)
.expect("write key-only pem"); let path = ca_path.display().to_string();
let err = match fallback_root_store(Some(&path), true) {
Err(e) => e,
Ok(_) => panic!("strict mode must fail closed on a CA with no CERTIFICATE section"),
};
assert_strict_fallback_error(err, &["no parseable PEM CERTIFICATE"]);
}
#[test]
fn test_fallback_root_store_strict_zero_roots_accepted_fails_closed() {
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("garbage-cert.pem");
std::fs::write(
&ca_path,
"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n",
)
.expect("write garbage cert pem"); let path = ca_path.display().to_string();
let err = match fallback_root_store(Some(&path), true) {
Err(e) => e,
Ok(_) => panic!("strict mode must fail closed when the root store rejects the CA"),
};
assert_strict_fallback_error(err, &["rejected by the TLS root store"]);
}
#[test]
fn test_fallback_root_store_nonstrict_bad_ca_degrades_to_mozilla() {
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("garbage.pem");
std::fs::write(&ca_path, "this is not a certificate\n").expect("write garbage pem"); let path = ca_path.display().to_string();
let store = fallback_root_store(Some(&path), false)
.expect("non-strict garbage CA must degrade to Mozilla-only"); assert_eq!(
store.roots.len(),
webpki_roots::TLS_SERVER_ROOTS.len(),
"non-strict bad CA must degrade to the bundled Mozilla anchors"
);
}
#[test]
fn test_fallback_client_config_disabled_tls_returns_none() {
let tls = TlsConfig {
enabled: false,
..Default::default()
};
assert!(
fallback_client_config(&tls)
.expect("disabled TLS is infallible") .is_none()
);
}
#[test]
fn test_fallback_client_config_no_material_verifying_returns_none() {
let tls = TlsConfig {
enabled: true,
..Default::default()
};
assert!(
fallback_client_config(&tls)
.expect("plain verifying TLS is infallible") .is_none()
);
}
#[test]
fn test_fallback_client_config_strict_identity_garbage_key_fails_closed() {
let dir = tempfile::tempdir().expect("tempdir"); let client_key = rcgen::KeyPair::generate().expect("client keygen"); let client_params = rcgen::CertificateParams::default();
let client_cert = client_params
.self_signed(&client_key)
.expect("client cert self-sign"); let cert_path = dir.path().join("client.pem");
std::fs::write(&cert_path, client_cert.pem()).expect("write client pem"); let key_path = dir.path().join("key.pem");
std::fs::write(&key_path, "garbage not a key at all\n").expect("write garbage key"); let tls = TlsConfig {
enabled: true,
strict: true,
client_cert_path: Some(cert_path.display().to_string()),
client_key_path: Some(key_path.display().to_string()),
..Default::default()
};
let err = match fallback_client_config(&tls) {
Err(e) => e,
Ok(_) => panic!("strict mode must fail closed on a garbage client key"),
};
assert_strict_fallback_error(err, &["no parseable private key"]);
}
#[test]
fn test_fallback_client_config_strict_rustls_rejected_key_fails_closed() {
let dir = tempfile::tempdir().expect("tempdir"); let client_key = rcgen::KeyPair::generate().expect("client keygen"); let client_params = rcgen::CertificateParams::default();
let client_cert = client_params
.self_signed(&client_key)
.expect("client cert self-sign"); let cert_path = dir.path().join("client.pem");
std::fs::write(&cert_path, client_cert.pem()).expect("write client pem"); let key_path = dir.path().join("key.pem");
std::fs::write(
&key_path,
"-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n",
)
.expect("write pkcs8-framed garbage key"); let tls = TlsConfig {
enabled: true,
strict: true,
client_cert_path: Some(cert_path.display().to_string()),
client_key_path: Some(key_path.display().to_string()),
..Default::default()
};
let err = match fallback_client_config(&tls) {
Err(e) => e,
Ok(_) => panic!("strict mode must fail closed when rustls rejects the identity"),
};
assert_strict_fallback_error(err, &["rejected by the TLS backend"]);
}
#[test]
fn test_fallback_client_config_half_mtls_pair_strict_fails_nonstrict_no_auth() {
let dir = tempfile::tempdir().expect("tempdir"); let cert_path = dir.path().join("client.pem");
std::fs::write(&cert_path, "cert-only, key missing\n").expect("write placeholder pem"); let tls_strict = TlsConfig {
enabled: true,
strict: true,
client_cert_path: Some(cert_path.display().to_string()),
..Default::default()
};
let err = match fallback_client_config(&tls_strict) {
Err(e) => e,
Ok(_) => panic!("strict mode must fail closed on a half-configured mTLS pair"),
};
assert_strict_fallback_error(err, &["BOTH client_cert_path"]);
let tls_permissive = TlsConfig {
enabled: true,
client_cert_path: Some(cert_path.display().to_string()),
..Default::default()
};
assert!(
fallback_client_config(&tls_permissive)
.expect("non-strict half pair degrades to no-auth") .is_some()
);
}
#[test]
fn test_no_verify_verifier_accepts_any_certificate() {
use rustls::client::danger::ServerCertVerifier;
let verifier = NoVerifyServerCertVerifier;
let garbage = rustls::pki_types::CertificateDer::from(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let server_name =
rustls::pki_types::ServerName::try_from("localhost").expect("localhost dns name"); assert!(
verifier
.verify_server_cert(
&garbage,
&[],
&server_name,
&[],
rustls::pki_types::UnixTime::now()
)
.is_ok(),
"no-verify verifier must accept any certificate without validation"
);
}
#[cfg(not(target_vendor = "apple"))]
#[test]
fn test_build_client_falls_back_on_empty_platform_ca_store() {
let _ca_guard = lock_ca_store_test_mutex();
let empty_ca_dir = tempfile::tempdir().expect("tempdir"); let empty_ca_file = empty_ca_dir.path().join("empty-ca-bundle.pem");
std::fs::write(&empty_ca_file, b"").expect("write empty CA file");
let (prev_file, prev_dir) = unsafe {
let prev = (
std::env::var_os("SSL_CERT_FILE"),
std::env::var_os("SSL_CERT_DIR"),
);
std::env::set_var("SSL_CERT_FILE", &empty_ca_file);
std::env::set_var("SSL_CERT_DIR", empty_ca_dir.path());
prev
};
let result = std::panic::catch_unwind(|| {
let fallbacks_before = build_client_fallback_count();
let _client = build_client(&HttpConfig::default(), None).expect("client must build"); build_client_fallback_count() - fallbacks_before
});
unsafe {
match prev_file {
Some(v) => std::env::set_var("SSL_CERT_FILE", v),
None => std::env::remove_var("SSL_CERT_FILE"),
}
match prev_dir {
Some(v) => std::env::set_var("SSL_CERT_DIR", v),
None => std::env::remove_var("SSL_CERT_DIR"),
}
}
let fallbacks_taken = result.expect("build_client must not panic on an empty CA store"); assert_eq!(
fallbacks_taken, 1,
"an empty platform CA store must route exactly one build through \
the webpki fallback (primary build failed, fallback succeeded)"
);
}
const HERMETIC_TEST_ROOT_PEM: &str = "-----BEGIN CERTIFICATE-----\n\
MIIDVzCCAj+gAwIBAgIUQ+hB0JPFtHpUKaNwcTB8ijGqEW0wDQYJKoZIhvcNAQEL\n\
BQAwOjEdMBsGA1UEAwwUY2FtZWwtaHR0cCB0ZXN0IHJvb3QxGTAXBgNVBAoMEGNh\n\
bWVsLWh0dHAgdGVzdHMwIBcNMjYwOTIxMTEyMzUwWhgPMjEyNjA4MjgxMTIzNTBa\n\
MDoxHTAbBgNVBAMMFGNhbWVsLWh0dHAgdGVzdCByb290MRkwFwYDVQQKDBBjYW1l\n\
bC1odHRwIHRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAolnF\n\
SiT/bK8pMl9n50HTQj4oxPZGXK34Q/OSq8TMUEcmhUFzBjzwZKoFqvDIt/e0mSi/\n\
h8WQlnrHAvYOnsRlpDsfs4yder+lpEYE84OooJjQvj/kREj7ncK6WQKDY1NTGikx\n\
lF8kIaCrNHNIlUgoSWMR4E6UshLFwSu5lKtcWCeN6FNzGVQ9jxYJSFsmVk+wFyHI\n\
edvpPbnFkD3M1/GKNVuxCCR50sO+cQeB7w9FCyFdHvoTWWuwPV4Qq2LBM2n4eedG\n\
pCXkqmuARtFuOKjpYIRryGybic9u9ZW59FbTbgHSj6rcOXNtBUVV/KnWRDRWo7ZC\n\
cR5SmO2YwtfpISjbmwIDAQABo1MwUTAdBgNVHQ4EFgQUkhwU0Q5JYNLrs7yOqbXE\n\
szzn2/cwHwYDVR0jBBgwFoAUkhwU0Q5JYNLrs7yOqbXEszzn2/cwDwYDVR0TAQH/\n\
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAXLqN7poLyxlpxN9vY1jISs403P2I\n\
edwgX3eWyJgXQfc/+dhnig8Pi7SkJKNM9PM27qain3+e2/HeetNYGBD8xnzQo5ga\n\
1ScPvm7u2DWIWc5y0bpm5JuCPqXoDl/7kBcaelA2lZRrPJzQuu6uPnH1ubjUPFJj\n\
9SxWhR+QgJW3cFc17C179WSVSJSPLozxX1ODABSyxNXnz14Rq+MOJi2tEsDggyMm\n\
txh6GwgVbiSey9c9bvu3B28iPfNpULuxBzxSLHcRrSSoII+46foY0G63V/jx7huY\n\
Fc2DPwOcO2Ni8fLssMaAnEdWPZaIN/DdjmUFw9XgHE9Eld6aKEXBcRFPzA==\n\
-----END CERTIFICATE-----";
#[test]
fn test_build_client_primary_path_when_platform_store_non_empty() {
let _ca_guard = lock_ca_store_test_mutex();
let ca_dir = tempfile::tempdir().expect("tempdir"); let ca_file = ca_dir.path().join("hermetic-root.pem");
std::fs::write(&ca_file, HERMETIC_TEST_ROOT_PEM).expect("write fixture root");
let (prev_file, prev_dir) = unsafe {
let prev = (
std::env::var_os("SSL_CERT_FILE"),
std::env::var_os("SSL_CERT_DIR"),
);
std::env::set_var("SSL_CERT_FILE", &ca_file);
std::env::set_var("SSL_CERT_DIR", ca_dir.path());
prev
};
let fallbacks_before = build_client_fallback_count();
let _client = build_client(&HttpConfig::default(), None).expect("client must build"); let fallbacks_taken = build_client_fallback_count() - fallbacks_before;
unsafe {
match prev_file {
Some(v) => std::env::set_var("SSL_CERT_FILE", v),
None => std::env::remove_var("SSL_CERT_FILE"),
}
match prev_dir {
Some(v) => std::env::set_var("SSL_CERT_DIR", v),
None => std::env::remove_var("SSL_CERT_DIR"),
}
}
assert_eq!(
fallbacks_taken, 0,
"with at least one platform root loadable, build_client must \
use the platform-verifier primary path, never the webpki \
fallback"
);
}
#[test]
fn test_build_client_forced_fallback_strict_store_rejected_ca_returns_typed_error() {
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("garbage-cert.pem");
std::fs::write(
&ca_path,
"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n",
)
.expect("write garbage cert pem"); let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
ca_cert_path: Some(ca_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let (err, fallbacks_taken) = forced_fallback_env(|| {
let fallbacks_before = build_client_fallback_count();
let err = match build_client(&config, None) {
Err(e) => e,
Ok(_) => panic!("strict mode must fail closed when the root store rejects the CA"),
};
(err, build_client_fallback_count() - fallbacks_before)
});
assert_strict_fallback_error(
err,
&[
"tls.strict/webpki-fallback",
"rejected by the TLS root store",
],
);
assert_eq!(
fallbacks_taken, 1,
"the typed error must come from exactly one webpki fallback entry"
);
}
#[test]
fn test_build_client_forced_fallback_strict_material_rejection_classes() {
let dir = tempfile::tempdir().expect("tempdir");
let plain_text_ca = dir.path().join("plain-text.pem");
std::fs::write(&plain_text_ca, "this is not a certificate\n").expect("write plain text ca");
let client_key = rcgen::KeyPair::generate().expect("client keygen"); let client_cert = rcgen::CertificateParams::default()
.self_signed(&client_key)
.expect("client self-sign"); let cert_path = dir.path().join("client-cert.pem");
std::fs::write(&cert_path, client_cert.pem()).expect("write client cert"); let valid_key_path = dir.path().join("client-key.pem");
std::fs::write(&valid_key_path, client_key.serialize_pem()).expect("write client key"); let garbage_key_path = dir.path().join("garbage-key.pem");
std::fs::write(&garbage_key_path, "not a key\n").expect("write garbage key"); let rejected_key_path = dir.path().join("rejected-key.pem");
std::fs::write(
&rejected_key_path,
"-----BEGIN PRIVATE KEY-----\nAAAA\n-----END PRIVATE KEY-----\n",
)
.expect("write rejected key");
struct RejectionCase {
name: &'static str,
tls: TlsConfig,
fragment: &'static str,
}
let cases = [
RejectionCase {
name: "CA with zero PEM CERTIFICATE sections",
tls: TlsConfig {
enabled: true,
strict: true,
ca_cert_path: Some(plain_text_ca.display().to_string()),
..Default::default()
},
fragment: "no parseable PEM CERTIFICATE section",
},
RejectionCase {
name: "client key with no parseable section",
tls: TlsConfig {
enabled: true,
strict: true,
client_cert_path: Some(cert_path.display().to_string()),
client_key_path: Some(garbage_key_path.display().to_string()),
..Default::default()
},
fragment: "no parseable private key section",
},
RejectionCase {
name: "client key rejected by the TLS backend",
tls: TlsConfig {
enabled: true,
strict: true,
client_cert_path: Some(cert_path.display().to_string()),
client_key_path: Some(rejected_key_path.display().to_string()),
..Default::default()
},
fragment: "rejected by the TLS backend",
},
RejectionCase {
name: "unreadable client cert",
tls: TlsConfig {
enabled: true,
strict: true,
client_cert_path: Some("/nonexistent/client-cert.pem".to_string()),
client_key_path: Some(valid_key_path.display().to_string()),
..Default::default()
},
fragment: "unreadable",
},
];
for case in cases {
let config = HttpConfig {
tls: Some(case.tls),
..Default::default()
};
let err = force_webpki_fallback(|| match build_client(&config, None) {
Err(e) => e,
Ok(_) => panic!("strict case '{}' must fail closed", case.name),
});
assert_strict_fallback_error(err, &["tls.strict/webpki-fallback", case.fragment]);
}
}
#[test]
fn test_build_client_forced_fallback_nonstrict_bad_ca_still_builds() {
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("garbage.pem");
std::fs::write(&ca_path, "this is not a certificate\n").expect("write garbage pem"); let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: false,
ca_cert_path: Some(ca_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let (_client, fallbacks_taken) = force_webpki_fallback(|| {
let fallbacks_before = build_client_fallback_count();
let client = build_client(&config, None)
.expect("non-strict bad CA must degrade per-item and still build"); (client, build_client_fallback_count() - fallbacks_before)
});
assert_eq!(
fallbacks_taken, 1,
"permissive item downgrade still routes exactly one build \
through the webpki fallback"
);
}
#[test]
fn test_http_component_with_config_folds_fallback_error_without_panic() {
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("garbage-cert.pem");
std::fs::write(
&ca_path,
"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n",
)
.expect("write garbage cert pem"); let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
ca_cert_path: Some(ca_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let endpoint_err = forced_fallback_env(|| {
let component = HttpComponent::with_config(config);
component
.create_endpoint("http://h/p", &NoOpComponentContext)
.err()
});
let err = endpoint_err.expect("folded fallback error must fail endpoint creation"); assert!(
err.to_string().contains("rejected by the TLS root store"),
"folded error must carry the fallback diagnosis: {err}"
);
}
#[test]
fn test_http_component_new_no_panic_on_empty_ca_store() {
let endpoint = forced_fallback_env(|| {
let component = HttpComponent::new();
component
.create_endpoint("http://h/p", &NoOpComponentContext)
.ok()
});
let _endpoint = endpoint.expect("material-free webpki fallback must serve endpoints"); }
#[tracing_test::traced_test]
#[tokio::test]
async fn tls_handshake_strict_custom_ca_carried() {
let ca = gen_test_ca();
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("ca.pem");
std::fs::write(&ca_path, ca.ca_pem.as_bytes()).expect("write ca pem"); let (server_cert, server_key) =
gen_leaf_signed_by_ca(&ca, "127.0.0.1".parse().expect("loopback ip")); let (addr, server) = spawn_tls_server(&server_cert, &server_key, None).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
ca_cert_path: Some(ca_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let resp = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout") .expect("strict webpki fallback must carry the configured CA into the handshake"); assert_eq!(resp.status().as_u16(), 200);
let body = resp.text().await.expect("response body"); assert_eq!(body, "ok");
assert!(
logs_contain("carries configured TLS material"),
"strict webpki fallback with configured material must log the carry"
);
server.abort();
}
#[tracing_test::traced_test]
#[tokio::test]
async fn tls_handshake_strict_material_free_fails_same_server() {
let ca = gen_test_ca();
let (server_cert, server_key) =
gen_leaf_signed_by_ca(&ca, "127.0.0.1".parse().expect("loopback ip")); let (addr, server) = spawn_tls_server(&server_cert, &server_key, None).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let send_result = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout"); assert!(
send_result.is_err(),
"material-free strict fallback trusts only Mozilla roots, which do not \
trust the rcgen CA — the handshake must fail"
);
assert!(
logs_contain("HTTP client build failed on platform TLS roots"),
"positive control: this test's own fallback warn must be captured"
);
assert!(
!logs_contain("client certificate NOT used"),
"no identity was configured — the identity degrade warn must not fire"
);
assert!(
!logs_contain("falling back to bundled Mozilla roots"),
"no CA was configured — the CA degrade warn must not fire"
);
server.abort();
}
#[tracing_test::traced_test]
#[tokio::test]
async fn tls_handshake_strict_mtls_identity_carried() {
let ca = gen_test_ca();
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("ca.pem");
std::fs::write(&ca_path, ca.ca_pem.as_bytes()).expect("write ca pem"); let (client_cert_pem, client_key_pem) = gen_client_identity(&ca);
let client_cert_path = dir.path().join("client-cert.pem");
std::fs::write(&client_cert_path, client_cert_pem.as_bytes()).expect("write client cert"); let client_key_path = dir.path().join("client-key.pem");
std::fs::write(&client_key_path, client_key_pem.as_bytes()).expect("write client key"); let (server_cert, server_key) =
gen_leaf_signed_by_ca(&ca, "127.0.0.1".parse().expect("loopback ip")); let (addr, server) = spawn_tls_server(&server_cert, &server_key, Some(&ca.ca_pem)).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
ca_cert_path: Some(ca_path.display().to_string()),
client_cert_path: Some(client_cert_path.display().to_string()),
client_key_path: Some(client_key_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let resp = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout") .expect("strict webpki fallback must carry the mTLS identity into the handshake"); assert_eq!(resp.status().as_u16(), 200);
let body = resp.text().await.expect("response body"); assert_eq!(body, "ok");
assert!(
logs_contain("carries configured TLS material"),
"strict webpki fallback with configured material must log the carry"
);
server.abort();
}
#[tokio::test]
async fn tls_handshake_strict_mtls_less_client_rejected() {
let ca = gen_test_ca();
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("ca.pem");
std::fs::write(&ca_path, ca.ca_pem.as_bytes()).expect("write ca pem"); let (server_cert, server_key) =
gen_leaf_signed_by_ca(&ca, "127.0.0.1".parse().expect("loopback ip")); let (addr, server) = spawn_tls_server(&server_cert, &server_key, Some(&ca.ca_pem)).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
ca_cert_path: Some(ca_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let send_result = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout"); assert!(
send_result.is_err(),
"the server requires client certificates — an identity-less client \
must be rejected at the handshake"
);
server.abort();
}
#[tracing_test::traced_test]
#[tokio::test]
async fn tls_handshake_nonstrict_valid_ca_carried() {
let ca = gen_test_ca();
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("ca.pem");
std::fs::write(&ca_path, ca.ca_pem.as_bytes()).expect("write ca pem"); let (server_cert, server_key) =
gen_leaf_signed_by_ca(&ca, "127.0.0.1".parse().expect("loopback ip")); let (addr, server) = spawn_tls_server(&server_cert, &server_key, None).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: false,
ca_cert_path: Some(ca_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let resp = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout") .expect("non-strict webpki fallback must carry the valid configured CA into the handshake"); assert_eq!(resp.status().as_u16(), 200);
let body = resp.text().await.expect("response body"); assert_eq!(body, "ok");
assert!(
!logs_contain("client certificate NOT used"),
"no identity was configured — the identity degrade warn must not fire"
);
assert!(
!logs_contain("falling back to bundled Mozilla roots"),
"the CA loaded cleanly — the CA degrade warn must not fire"
);
server.abort();
}
#[cfg(not(target_vendor = "apple"))]
#[tracing_test::traced_test]
#[tokio::test]
async fn tls_handshake_mtls_identity_only_genuine_fallback_anchor() {
let ca = gen_test_ca();
let dir = tempfile::tempdir().expect("tempdir"); let (client_cert_pem, client_key_pem) = gen_client_identity(&ca);
let client_cert_path = dir.path().join("client-cert.pem");
std::fs::write(&client_cert_path, client_cert_pem.as_bytes()).expect("write client cert"); let client_key_path = dir.path().join("client-key.pem");
std::fs::write(&client_key_path, client_key_pem.as_bytes()).expect("write client key");
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
client_cert_path: Some(client_cert_path.display().to_string()),
client_key_path: Some(client_key_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let (client, fallbacks_taken) = {
let fallbacks_before = build_client_fallback_count();
let client = forced_fallback_env(|| {
build_client(&config, None).expect("client must build") });
(client, build_client_fallback_count() - fallbacks_before)
};
assert_eq!(
fallbacks_taken, 1,
"the empty-store env window must genuinely fail the platform-verifier primary"
);
drop(client);
assert!(
logs_contain("HTTP client build failed on platform TLS roots"),
"the genuine env-window trigger must emit the real platform warn"
);
}
#[tokio::test]
async fn tls_parity_insecure_self_signed_succeeds() {
let (server_cert, server_key) = gen_self_signed_server_cert();
let (addr, server) = spawn_tls_server(&server_cert, &server_key, None).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: false,
insecure: true,
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let resp = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout") .expect("verify-off (insecure) fallback must handshake with the self-signed server"); assert_eq!(resp.status().as_u16(), 200);
let body = resp.text().await.expect("response body"); assert_eq!(body, "ok");
server.abort();
}
#[tokio::test]
async fn tls_parity_verify_peer_false_self_signed_succeeds() {
let (server_cert, server_key) = gen_self_signed_server_cert();
let (addr, server) = spawn_tls_server(&server_cert, &server_key, None).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: false,
verify_peer: false,
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let resp = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout") .expect("verify_peer=false fallback must handshake with the self-signed server"); assert_eq!(resp.status().as_u16(), 200);
let body = resp.text().await.expect("response body"); assert_eq!(body, "ok");
server.abort();
}
#[tokio::test]
async fn tls_parity_insecure_strict_bad_material_still_fails_closed() {
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("rejected-ca.pem");
std::fs::write(
&ca_path,
b"-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n",
)
.expect("write rejected ca fixture");
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
insecure: true,
ca_cert_path: Some(ca_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let err = match force_webpki_fallback(|| build_client(&config, None)) {
Err(e) => e,
Ok(_) => panic!(
"strict mode must fail closed on store-rejected CA material \
even with insecure=true"
),
};
assert_strict_fallback_error(err, &["tls.strict/webpki-fallback"]);
}
#[test]
fn strict_rebuild_failure_returns_typed_error() {
let StrictMaterial {
dir: _dir,
ca_path,
cert_path,
key_path,
..
} = strict_material_fixtures();
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
ca_cert_path: Some(ca_path.display().to_string()),
client_cert_path: Some(cert_path.display().to_string()),
client_key_path: Some(key_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let fallbacks_before = build_client_fallback_count();
let err = match force_webpki_fallback_rebuild_failure(|| build_client(&config, None)) {
Err(e) => e,
Ok(_) => panic!(
"strict mode must fail closed when the webpki fallback client \
rebuild fails — never the material-free emergency client"
),
};
assert_eq!(
build_client_fallback_count() - fallbacks_before,
1,
"the rebuild-failure call must route through the webpki fallback exactly once"
);
assert_strict_fallback_error(
err,
&[
"tls.strict/webpki-fallback:",
"webpki fallback client rebuild failed",
"forced webpki fallback rebuild failure",
],
);
}
#[test]
fn non_strict_rebuild_failure_returns_emergency_client() {
let StrictMaterial {
dir: _dir,
ca_path,
cert_path,
key_path,
..
} = strict_material_fixtures();
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: false,
ca_cert_path: Some(ca_path.display().to_string()),
client_cert_path: Some(cert_path.display().to_string()),
client_key_path: Some(key_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let fallbacks_before = build_client_fallback_count();
let _client = force_webpki_fallback_rebuild_failure(|| build_client(&config, None))
.expect("non-strict rebuild failure must degrade to the emergency client"); assert_eq!(
build_client_fallback_count() - fallbacks_before,
1,
"the rebuild-failure call must route through the webpki fallback exactly once"
);
}
#[test]
fn strict_rebuild_failure_surfaces_at_endpoint_creation() {
let StrictMaterial {
dir: _dir,
ca_path,
cert_path,
key_path,
..
} = strict_material_fixtures();
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
ca_cert_path: Some(ca_path.display().to_string()),
client_cert_path: Some(cert_path.display().to_string()),
client_key_path: Some(key_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let endpoint_err = force_webpki_fallback_rebuild_failure(|| {
let component = HttpComponent::with_config(config);
component
.create_endpoint("http://h/p", &NoOpComponentContext)
.err()
});
let err = endpoint_err.expect("folded rebuild failure must fail endpoint creation"); assert_strict_fallback_error(
err,
&[
"tls.strict/webpki-fallback:",
"webpki fallback client rebuild failed",
],
);
}
#[tracing_test::traced_test]
#[tokio::test]
async fn non_strict_rebuild_failure_drops_custom_trust() {
let StrictMaterial {
dir: _dir,
ca,
ca_path,
..
} = strict_material_fixtures();
let (server_cert, server_key) =
gen_leaf_signed_by_ca(&ca, "127.0.0.1".parse().expect("loopback ip")); let (addr, server) = spawn_tls_server(&server_cert, &server_key, None).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: false,
ca_cert_path: Some(ca_path.display().to_string()),
..Default::default()
}),
..Default::default()
};
let control = force_webpki_fallback(|| {
build_client(&config, None).expect("control client must build") });
let client = force_webpki_fallback_rebuild_failure(|| {
build_client(&config, None).expect("emergency client must build") });
let control_resp = tokio::time::timeout(
Duration::from_secs(15),
control.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout") .expect("the control client carries the CA — the CA-signed handshake must succeed"); assert_eq!(control_resp.status().as_u16(), 200);
let send_result = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout"); assert!(
send_result.is_err(),
"the emergency client carries Mozilla roots only — the configured \
CA trust is provably dropped, so the CA-signed handshake must fail"
);
assert!(
logs_contain("webpki fallback client build failed — TLS stack broken process-wide"),
"the rebuild-failure terminal must log the system-broken diagnosis"
);
server.abort();
}
#[tracing_test::traced_test]
#[tokio::test]
async fn tls_parity_nonstrict_itemwise_valid_ca_bad_identity() {
let ca = gen_test_ca();
let dir = tempfile::tempdir().expect("tempdir"); let ca_path = dir.path().join("ca.pem");
std::fs::write(&ca_path, ca.ca_pem.as_bytes()).expect("write ca pem"); let (client_cert_pem, _client_key_pem) = gen_client_identity(&ca);
let client_cert_path = dir.path().join("client-cert.pem");
std::fs::write(&client_cert_path, client_cert_pem.as_bytes()).expect("write client cert"); let (server_cert, server_key) =
gen_leaf_signed_by_ca(&ca, "127.0.0.1".parse().expect("loopback ip")); let (addr, server) = spawn_tls_server(&server_cert, &server_key, None).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: false,
ca_cert_path: Some(ca_path.display().to_string()),
client_cert_path: Some(client_cert_path.display().to_string()),
client_key_path: Some("/nonexistent/key.pem".to_string()),
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let resp = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout") .expect("the valid CA item must be carried despite the unreadable key"); assert_eq!(resp.status().as_u16(), 200);
let body = resp.text().await.expect("response body"); assert_eq!(body, "ok");
assert!(
logs_contain("client certificate NOT used"),
"the unreadable key must degrade the identity item with the mTLS warn"
);
assert!(
!logs_contain("falling back to bundled Mozilla roots"),
"the CA loaded cleanly — the CA degrade warn must not fire"
);
server.abort();
}
#[tokio::test]
async fn tls_parity_verifying_client_rejects_self_signed_without_insecure() {
let (server_cert, server_key) = gen_self_signed_server_cert();
let (addr, server) = spawn_tls_server(&server_cert, &server_key, None).await;
let config = HttpConfig {
tls: Some(TlsConfig {
enabled: true,
strict: true,
..Default::default()
}),
..Default::default()
};
let client = build_under_seam(&config);
let send_result = tokio::time::timeout(
Duration::from_secs(15),
client.get(format!("https://{addr}/")).send(),
)
.await
.expect("request must complete within the harness timeout"); assert!(
send_result.is_err(),
"a verifying client must reject the self-signed server at the handshake"
);
server.abort();
}