From ff897d725b6a162688074eb49a5f917914749d11 Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Fri, 11 Sep 2026 07:52:12 -0700 Subject: [PATCH] Use Windows platform trust for voice connections --- MODULE.bazel.lock | 2 + codex-rs/Cargo.lock | 28 +++++++ .../endpoint/realtime_websocket/methods.rs | 11 +++ codex-rs/http-client/Cargo.toml | 1 + codex-rs/http-client/src/custom_ca.rs | 4 + .../http-client/src/custom_ca_tls_tests.rs | 79 +++++++++++++++++++ codex-rs/http-client/src/lib.rs | 6 ++ codex-rs/http-client/src/windows_tls.rs | 22 ++++++ codex-rs/http-client/src/windows_tls_tests.rs | 58 ++++++++++++++ 9 files changed, 211 insertions(+) create mode 100644 codex-rs/http-client/src/custom_ca_tls_tests.rs create mode 100644 codex-rs/http-client/src/windows_tls.rs create mode 100644 codex-rs/http-client/src/windows_tls_tests.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 39aa0f9507..7d66ecffd3 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1663,6 +1663,8 @@ "rustls-native-certs_0.8.3": "{\"dependencies\":[{\"name\":\"openssl-probe\",\"req\":\"^0.2\",\"target\":\"cfg(all(unix, not(target_os = \\\"macos\\\")))\"},{\"features\":[\"std\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.10\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"kind\":\"dev\",\"name\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"security-framework\",\"req\":\"^3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"kind\":\"dev\",\"name\":\"serial_test\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.5\"},{\"kind\":\"dev\",\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18\"}],\"features\":{}}", "rustls-pki-types_1.14.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", "rustls-pki-types_1.14.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"=0.1.9\",\"target\":\"cfg(all(target_os = \\\"linux\\\", target_arch = \\\"x86_64\\\"))\"},{\"name\":\"web-time\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"dep:zeroize\"],\"default\":[\"alloc\"],\"std\":[\"alloc\"],\"web\":[\"web-time\"]}}", + "rustls-platform-verifier-android_0.1.1": "{\"dependencies\":[],\"features\":{}}", + "rustls-platform-verifier_0.7.0": "{\"dependencies\":[{\"name\":\"android_logger\",\"optional\":true,\"req\":\"^0.15\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"core-foundation\",\"req\":\"^0.10\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"name\":\"core-foundation-sys\",\"req\":\"^0.8\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"jni\",\"req\":\"^0.22\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"default_features\":false,\"name\":\"jni\",\"optional\":true,\"req\":\"^0.22.4\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1.9\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.9\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"req\":\"^0.23.27\"},{\"default_features\":false,\"features\":[\"ring\"],\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"name\":\"rustls-native-certs\",\"req\":\"^0.8\",\"target\":\"cfg(all(unix, not(target_os = \\\"android\\\"), not(target_vendor = \\\"apple\\\"), not(target_arch = \\\"wasm32\\\")))\"},{\"name\":\"rustls-platform-verifier-android\",\"req\":\"^0.1.0\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"security-framework\",\"req\":\"^3.5.0\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"name\":\"security-framework-sys\",\"req\":\"^2.15\",\"target\":\"cfg(any(target_vendor = \\\"apple\\\"))\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(all(unix, not(target_os = \\\"android\\\"), not(target_vendor = \\\"apple\\\"), not(target_arch = \\\"wasm32\\\")))\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"name\":\"webpki-root-certs\",\"req\":\"^1\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"webpki-root-certs\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"Win32_Foundation\",\"Win32_Security_Cryptography\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.62.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"cert-logging\":[\"base64\"],\"dbg\":[],\"docsrs\":[\"jni\",\"once_cell\"],\"ffi-testing\":[\"android_logger\",\"rustls/ring\"]}}", "rustls-webpki_0.103.13": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"kind\":\"dev\",\"name\":\"bzip2\",\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1.17.2\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.2\"},{\"default_features\":false,\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.18.1\"}],\"features\":{\"alloc\":[\"ring?/alloc\",\"pki-types/alloc\"],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"dep:aws-lc-rs\",\"aws-lc-rs/fips\"],\"aws-lc-rs-unstable\":[\"aws-lc-rs\",\"aws-lc-rs/unstable\"],\"default\":[\"std\"],\"ring\":[\"dep:ring\"],\"std\":[\"alloc\",\"pki-types/std\"]}}", "rustls_0.23.36": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"macro_rules_attribute\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"features\":[\"alloc\",\"race\"],\"name\":\"once_cell\",\"req\":\"^1.16\"},{\"features\":[\"alloc\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"pem\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.6\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103.5\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17\"},{\"name\":\"zeroize\",\"req\":\"^1.8\"},{\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.5\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"dep:aws-lc-rs\",\"webpki/aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"brotli\":[\"dep:brotli\",\"dep:brotli-decompressor\",\"std\"],\"custom-provider\":[],\"default\":[\"aws_lc_rs\",\"logging\",\"prefer-post-quantum\",\"std\",\"tls12\"],\"fips\":[\"aws_lc_rs\",\"aws-lc-rs?/fips\",\"webpki/aws-lc-rs-fips\"],\"logging\":[\"log\"],\"prefer-post-quantum\":[\"aws_lc_rs\"],\"read_buf\":[\"rustversion\",\"std\"],\"ring\":[\"dep:ring\",\"webpki/ring\"],\"std\":[\"webpki/std\",\"pki-types/std\",\"once_cell/std\"],\"tls12\":[],\"zlib\":[\"dep:zlib-rs\"]}}", "rustls_0.23.40": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.14\"},{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"brotli\",\"optional\":true,\"req\":\"^8\"},{\"name\":\"brotli-decompressor\",\"optional\":true,\"req\":\"^5.0.0\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4.8\"},{\"kind\":\"dev\",\"name\":\"macro_rules_attribute\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"features\":[\"alloc\",\"race\"],\"name\":\"once_cell\",\"req\":\"^1.16\"},{\"features\":[\"alloc\"],\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"pem\",\"aws_lc_rs\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"optional\":true,\"req\":\"^1.0.6\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103.5\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17\"},{\"name\":\"zeroize\",\"req\":\"^1.8\"},{\"name\":\"zlib-rs\",\"optional\":true,\"req\":\"^0.6\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"dep:aws-lc-rs\",\"webpki/aws-lc-rs\",\"aws-lc-rs/aws-lc-sys\",\"aws-lc-rs/prebuilt-nasm\"],\"brotli\":[\"dep:brotli\",\"dep:brotli-decompressor\",\"std\"],\"custom-provider\":[],\"default\":[\"aws_lc_rs\",\"logging\",\"prefer-post-quantum\",\"std\",\"tls12\"],\"fips\":[\"aws_lc_rs\",\"aws-lc-rs?/fips\",\"webpki/aws-lc-rs-fips\"],\"logging\":[\"log\"],\"prefer-post-quantum\":[\"aws_lc_rs\"],\"read_buf\":[\"rustversion\",\"std\"],\"ring\":[\"dep:ring\",\"webpki/ring\"],\"std\":[\"webpki/std\",\"pki-types/std\",\"once_cell/std\"],\"tls12\":[],\"zlib\":[\"dep:zlib-rs\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 572ffcd05b..104bf31923 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3699,6 +3699,7 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-pki-types", + "rustls-platform-verifier", "serde", "serde_json", "sha2 0.10.9", @@ -13427,6 +13428,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs index 293305ea53..fe31a1dbef 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs @@ -965,6 +965,17 @@ impl RealtimeWebsocketClient { let connector = maybe_build_rustls_client_config_with_custom_ca() .map_err(|err| ApiError::Stream(format!("failed to configure websocket TLS: {err}")))? .map(tokio_tungstenite::Connector::Rustls); + // A fresh Windows install may not have downloaded the server's trusted root yet. + // Use platform validation only for system trust, preserving custom CA semantics. + #[cfg(windows)] + let connector = match connector { + Some(connector) => Some(connector), + None => Some(tokio_tungstenite::Connector::Rustls( + codex_http_client::build_windows_platform_tls_config().map_err(|err| { + ApiError::Stream(format!("failed to configure websocket TLS: {err}")) + })?, + )), + }; let (stream, response) = tokio_tungstenite::connect_async_tls_with_config( request, Some(websocket_config()), diff --git a/codex-rs/http-client/Cargo.toml b/codex-rs/http-client/Cargo.toml index 2d54ca7158..a390c46ab2 100644 --- a/codex-rs/http-client/Cargo.toml +++ b/codex-rs/http-client/Cargo.toml @@ -28,6 +28,7 @@ zstd = { workspace = true } system-configuration = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] +rustls-platform-verifier = "0.7.0" windows-sys = { version = "0.52", features = [ "Win32_Foundation", "Win32_Networking_WinHttp", diff --git a/codex-rs/http-client/src/custom_ca.rs b/codex-rs/http-client/src/custom_ca.rs index 5a2e51c320..6ca705d0ab 100644 --- a/codex-rs/http-client/src/custom_ca.rs +++ b/codex-rs/http-client/src/custom_ca.rs @@ -818,3 +818,7 @@ mod tests { )); } } + +#[cfg(test)] +#[path = "custom_ca_tls_tests.rs"] +mod tls_tests; diff --git a/codex-rs/http-client/src/custom_ca_tls_tests.rs b/codex-rs/http-client/src/custom_ca_tls_tests.rs new file mode 100644 index 0000000000..fb3c78fe14 --- /dev/null +++ b/codex-rs/http-client/src/custom_ca_tls_tests.rs @@ -0,0 +1,79 @@ +//! Handshake coverage for the custom trust path retained by Windows realtime connections. + +use super::CODEX_CA_CERT_ENV; +use super::ConfiguredCaBundle; +use super::build_rustls_client_config; +use pretty_assertions::assert_eq; +use rcgen::BasicConstraints; +use rcgen::CertificateParams; +use rcgen::CertifiedIssuer; +use rcgen::IsCa; +use rcgen::KeyPair; +use std::sync::Arc; + +#[test] +fn custom_intermediate_trust_preserves_hostname_validation() { + let mut params = CertificateParams::default(); + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + let root = CertifiedIssuer::self_signed(params.clone(), KeyPair::generate().unwrap()).unwrap(); + let intermediate = + CertifiedIssuer::signed_by(params, KeyPair::generate().unwrap(), &root).unwrap(); + let leaf_key = KeyPair::generate().unwrap(); + let leaf = CertificateParams::new(vec!["localhost".to_string()]) + .unwrap() + .signed_by(&leaf_key, &intermediate) + .unwrap(); + let temp = tempfile::TempDir::new().unwrap(); + let path = temp.path().join("intermediate.pem"); + std::fs::write(&path, intermediate.pem()).unwrap(); + let config = build_rustls_client_config(Some(&ConfiguredCaBundle { + source_env: CODEX_CA_CERT_ENV, + path, + })) + .unwrap(); + let server = Arc::new( + rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(vec![leaf.der().clone()], leaf_key.into()) + .unwrap(), + ); + + assert_eq!( + handshake(config.clone(), server.clone(), "localhost"), + Ok(()) + ); + let error = handshake(config, server, "wrong.example").unwrap_err(); + assert!( + matches!( + error, + rustls::Error::InvalidCertificate( + rustls::CertificateError::NotValidForName + | rustls::CertificateError::NotValidForNameContext { .. } + ) + ), + "{error:?}" + ); +} + +fn handshake( + config: Arc, + server: Arc, + hostname: &'static str, +) -> Result<(), rustls::Error> { + let mut client = rustls::ClientConnection::new(config, hostname.try_into().unwrap()).unwrap(); + let mut server = rustls::ServerConnection::new(server).unwrap(); + for _ in 0..10 { + let mut bytes = Vec::new(); + client.write_tls(&mut bytes).unwrap(); + server.read_tls(&mut bytes.as_slice()).unwrap(); + server.process_new_packets()?; + bytes.clear(); + server.write_tls(&mut bytes).unwrap(); + client.read_tls(&mut bytes.as_slice()).unwrap(); + client.process_new_packets()?; + if !client.is_handshaking() { + return Ok(()); + } + } + panic!("handshake did not complete"); +} diff --git a/codex-rs/http-client/src/lib.rs b/codex-rs/http-client/src/lib.rs index cb7f1209e2..d3e8ac85a6 100644 --- a/codex-rs/http-client/src/lib.rs +++ b/codex-rs/http-client/src/lib.rs @@ -57,3 +57,9 @@ pub use crate::transport::ByteStream; pub use crate::transport::HttpTransport; pub use crate::transport::ReqwestTransport; pub use crate::transport::StreamResponse; + +#[cfg(windows)] +mod windows_tls; + +#[cfg(windows)] +pub use crate::windows_tls::build_windows_platform_tls_config; diff --git a/codex-rs/http-client/src/windows_tls.rs b/codex-rs/http-client/src/windows_tls.rs new file mode 100644 index 0000000000..5b42b37fc3 --- /dev/null +++ b/codex-rs/http-client/src/windows_tls.rs @@ -0,0 +1,22 @@ +//! Windows certificate validation for callers without a custom CA bundle. +//! +//! Unlike a snapshot of installed roots, Windows can retrieve missing trusted roots on demand. +//! Callers must retain the custom-CA configuration path when a bundle is configured. + +use std::sync::Arc; + +use codex_utils_rustls_provider::ensure_rustls_crypto_provider; +use rustls::ClientConfig; +use rustls_platform_verifier::ConfigVerifierExt; + +/// Builds a TLS configuration that validates server certificates using Windows trust policy. +/// +/// This does not load Codex custom CA settings; callers must check those before selecting it. +pub fn build_windows_platform_tls_config() -> Result, rustls::Error> { + ensure_rustls_crypto_provider(); + ClientConfig::with_platform_verifier().map(Arc::new) +} + +#[cfg(test)] +#[path = "windows_tls_tests.rs"] +mod tests; diff --git a/codex-rs/http-client/src/windows_tls_tests.rs b/codex-rs/http-client/src/windows_tls_tests.rs new file mode 100644 index 0000000000..6ddf3b50f7 --- /dev/null +++ b/codex-rs/http-client/src/windows_tls_tests.rs @@ -0,0 +1,58 @@ +//! Certificate rejection coverage for Windows platform TLS. + +use super::build_windows_platform_tls_config; +use std::sync::Arc; + +#[test] +fn platform_tls_rejects_a_self_signed_server() { + let client_config = + build_windows_platform_tls_config().expect("configure platform verification"); + let certified_key = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]) + .expect("generate untrusted certificate"); + let server_config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + vec![certified_key.cert.der().clone()], + certified_key.signing_key.into(), + ) + .expect("configure test server"); + let mut client = rustls::ClientConnection::new( + client_config, + "localhost".try_into().expect("valid server name"), + ) + .expect("create client"); + let mut server = rustls::ServerConnection::new(Arc::new(server_config)).expect("create server"); + + // Drive the real handshake without opening ports or changing system trust. + for _ in 0..10 { + let mut outbound = Vec::new(); + client + .write_tls(&mut outbound) + .expect("write client records"); + server + .read_tls(&mut outbound.as_slice()) + .expect("read client records"); + server + .process_new_packets() + .expect("process client records"); + outbound.clear(); + server + .write_tls(&mut outbound) + .expect("write server records"); + client + .read_tls(&mut outbound.as_slice()) + .expect("read server records"); + if let Err(error) = client.process_new_packets() { + assert!( + matches!(error, rustls::Error::InvalidCertificate(_)), + "{error:?}" + ); + return; + } + assert!( + client.is_handshaking(), + "untrusted server must not complete TLS" + ); + } + panic!("expected certificate rejection during handshake"); +}