From f935762b23b5ed8fe790b5ee05875c04bad7260a Mon Sep 17 00:00:00 2001 From: Winston Howes Date: Wed, 27 May 2026 03:29:03 -0700 Subject: [PATCH] Address remaining MITM CA trust feedback --- codex-rs/Cargo.lock | 3 + codex-rs/Cargo.toml | 3 + .../linux-sandbox/src/linux_run_main_tests.rs | 1 + codex-rs/network-proxy/Cargo.toml | 9 + codex-rs/network-proxy/src/certs.rs | 234 +++++++++++++++++- codex-rs/network-proxy/src/proxy.rs | 15 +- 6 files changed, 262 insertions(+), 3 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index ca7df9916a..6ea9042999 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3330,6 +3330,7 @@ dependencies = [ "codex-utils-home-dir", "codex-utils-rustls-provider", "globset", + "openssl-probe 0.2.1", "pretty_assertions", "rama-core", "rama-http", @@ -3340,6 +3341,8 @@ dependencies = [ "rama-tls-rustls", "rama-unix", "rustls-native-certs", + "schannel", + "security-framework 3.5.1", "serde", "serde_json", "tempfile", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 830e244082..76342db286 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -315,6 +315,7 @@ multimap = "0.10.0" notify = "8.2.0" nucleo = { git = "https://github.com/helix-editor/nucleo.git", rev = "4253de9faabb4e5c6d81d946a5e35a90f87347ee" } once_cell = "1.20.2" +openssl-probe = "0.2.1" openssl-sys = "*" opentelemetry = "0.31.0" opentelemetry-appender-tracing = "0.31.0" @@ -348,8 +349,10 @@ rustls = { version = "0.23", default-features = false, features = [ ] } rustls-native-certs = "0.8.3" rustls-pki-types = "1.14.0" +schannel = "0.1.28" schemars = "0.8.22" seccompiler = "0.5.0" +security-framework = "3.5.1" semver = "1.0" sentry = "0.46.0" serde = "1" diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index 4441af7809..a5491e3fa1 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -268,6 +268,7 @@ fn managed_proxy_preflight_argv_is_wrapped_for_full_access_policy() { Path::new("/"), &FileSystemSandboxPolicy::unrestricted(), mode, + /*mitm_ca_cert_path*/ None, ) .expect("build preflight argv") .args; diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index e097269f1a..06ebdaf673 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -44,3 +44,12 @@ tempfile = { workspace = true } [target.'cfg(target_family = "unix")'.dependencies] rama-unix = { version = "=0.3.0-alpha.4" } + +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +openssl-probe = { workspace = true } + +[target.'cfg(target_os = "macos")'.dependencies] +security-framework = { workspace = true } + +[target.'cfg(windows)'.dependencies] +schannel = { workspace = true } diff --git a/codex-rs/network-proxy/src/certs.rs b/codex-rs/network-proxy/src/certs.rs index 46366228d0..22241c00ac 100644 --- a/codex-rs/network-proxy/src/certs.rs +++ b/codex-rs/network-proxy/src/certs.rs @@ -20,7 +20,19 @@ use rama_tls_rustls::dep::rcgen::PKCS_ECDSA_P256_SHA256; use rama_tls_rustls::dep::rcgen::SanType; use rama_tls_rustls::dep::rustls; use rama_tls_rustls::server::TlsAcceptorData; +#[cfg(windows)] +use schannel::cert_context::ValidUses; +#[cfg(windows)] +use schannel::cert_store::CertStore; +#[cfg(target_os = "macos")] +use security_framework::trust_settings::Domain; +#[cfg(target_os = "macos")] +use security_framework::trust_settings::TrustSettings; +#[cfg(target_os = "macos")] +use security_framework::trust_settings::TrustSettingsForCertificate; use std::collections::HashMap; +#[cfg(any(target_os = "macos", windows))] +use std::error::Error as StdError; use std::fs; use std::fs::File; use std::fs::OpenOptions; @@ -156,8 +168,7 @@ fn build_managed_ca_trust_bundle( env: &HashMap, ) -> Result { let mut trust_bundle = String::new(); - let rustls_native_certs::CertificateResult { certs, errors, .. } = - rustls_native_certs::load_native_certs(); + let rustls_native_certs::CertificateResult { certs, errors, .. } = load_platform_native_certs(); if !errors.is_empty() { warn!( native_root_error_count = errors.len(), @@ -194,6 +205,205 @@ fn build_managed_ca_trust_bundle( Ok(trust_bundle) } +// Match rustls-native-certs' platform loaders without honoring SSL_CERT_FILE / SSL_CERT_DIR. +// Those inherited values are child custom roots, so append their bundle paths separately below. +fn load_platform_native_certs() -> rustls_native_certs::CertificateResult { + #[cfg(all(unix, not(target_os = "macos")))] + { + let cert_file = NATIVE_CERTIFICATE_FILE_NAMES + .iter() + .copied() + .find_map(|path| Path::new(path).exists().then(|| PathBuf::from(path))); + let mut result = rustls_native_certs::load_certs_from_paths(cert_file.as_deref(), None); + for certs_dir in openssl_probe::candidate_cert_dirs() { + let dir_result = rustls_native_certs::load_certs_from_paths(None, Some(certs_dir)); + result.certs.extend(dir_result.certs); + result.errors.extend(dir_result.errors); + } + result.certs.sort_unstable_by(|left, right| left.cmp(right)); + result.certs.dedup(); + return result; + } + + #[cfg(target_os = "macos")] + { + let mut result = rustls_native_certs::CertificateResult::default(); + let mut all_certs = HashMap::new(); + for domain in &[Domain::User, Domain::Admin, Domain::System] { + let trust_settings = TrustSettings::new(*domain); + let iter = match trust_settings.iter() { + Ok(iter) => iter, + Err(err) => { + push_native_cert_os_error( + &mut result, + err.into(), + match domain { + Domain::User => "failed to load user trust settings", + Domain::Admin => "failed to load admin trust settings", + Domain::System => "failed to load system trust settings", + }, + ); + continue; + } + }; + + for cert in iter { + let der = cert.to_der(); + let trusted = match trust_settings.tls_trust_settings_for_certificate(&cert) { + Ok(trusted) => trusted.unwrap_or(TrustSettingsForCertificate::TrustRoot), + Err(err) => { + push_native_cert_os_error( + &mut result, + err.into(), + "certificate not trusted", + ); + continue; + } + }; + all_certs.entry(der).or_insert(trusted); + } + } + + for (der, trusted) in all_certs { + if matches!( + trusted, + TrustSettingsForCertificate::TrustRoot | TrustSettingsForCertificate::TrustAsRoot + ) { + result.certs.push(CertificateDer::from(der)); + } + } + return result; + } + + #[cfg(windows)] + { + let mut result = rustls_native_certs::CertificateResult::default(); + let current_user_store = match CertStore::open_current_user("ROOT") { + Ok(store) => store, + Err(err) => { + push_native_cert_os_error( + &mut result, + err.into(), + "failed to open current user certificate store", + ); + return result; + } + }; + + for cert in current_user_store.certs() { + let valid_uses = match cert.valid_uses() { + Ok(valid_uses) => valid_uses, + Err(err) => { + push_native_cert_os_error( + &mut result, + err.into(), + "failed to read certificate valid uses", + ); + continue; + } + }; + let is_time_valid = match cert.is_time_valid() { + Ok(is_time_valid) => is_time_valid, + Err(err) => { + push_native_cert_os_error( + &mut result, + err.into(), + "failed to read certificate validity", + ); + continue; + } + }; + if usable_for_rustls(valid_uses) && is_time_valid { + result + .certs + .push(CertificateDer::from(cert.to_der().to_vec())); + } + } + return result; + } + + #[allow(unreachable_code)] + rustls_native_certs::load_native_certs() +} + +#[cfg(any(target_os = "macos", windows))] +fn push_native_cert_os_error( + result: &mut rustls_native_certs::CertificateResult, + err: Box, + context: &'static str, +) { + result.errors.push(rustls_native_certs::Error { + context, + kind: rustls_native_certs::ErrorKind::Os(err), + }); +} + +#[cfg(windows)] +fn usable_for_rustls(uses: ValidUses) -> bool { + match uses { + ValidUses::All => true, + ValidUses::Oids(oids) => oids.iter().any(|oid| oid == PKIX_SERVER_AUTH), + } +} + +#[cfg(windows)] +const PKIX_SERVER_AUTH: &str = "1.3.6.1.5.5.7.3.1"; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "linux"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = &[ + "/etc/ssl/certs/ca-certificates.crt", + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", + "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/ca-bundle.pem", + "/etc/pki/tls/cacert.pem", + "/etc/ssl/cert.pem", + "/opt/etc/ssl/certs/ca-certificates.crt", + "/etc/ssl/certs/cacert.pem", +]; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "freebsd"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = &["/usr/local/etc/ssl/cert.pem"]; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "dragonfly"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = &["/usr/local/share/certs/ca-root-nss.crt"]; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "netbsd"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = &["/etc/openssl/certs/ca-certificates.crt"]; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "openbsd"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = &["/etc/ssl/cert.pem"]; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "solaris"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = &["/etc/certs/ca-certificates.crt"]; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "illumos"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = + &["/etc/ssl/cacert.pem", "/etc/certs/ca-certificates.crt"]; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "android"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = + &["/data/data/com.termux/files/usr/etc/tls/cert.pem"]; + +#[cfg(all(unix, not(target_os = "macos"), target_os = "haiku"))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = &["/boot/system/data/ssl/CARootCertificates.pem"]; + +#[cfg(all( + unix, + not(target_os = "macos"), + not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "solaris", + target_os = "illumos", + target_os = "android", + target_os = "haiku", + )) +))] +const NATIVE_CERTIFICATE_FILE_NAMES: &[&str] = &["/etc/ssl/certs/ca-certificates.crt"]; + fn append_pem_file(bundle: &mut String, path: &Path) -> Result<()> { if !bundle.ends_with('\n') { bundle.push('\n'); @@ -500,6 +710,26 @@ mod tests { assert!(!trust_bundle.contains("stale managed bundle")); } + #[test] + fn build_managed_ca_trust_bundle_appends_inherited_bundle() { + let dir = tempdir().unwrap(); + let managed_ca_cert_path = dir.path().join("ca.pem"); + let trust_bundle_path = dir.path().join("ca-bundle.pem"); + let inherited_bundle_path = dir.path().join("inherited.pem"); + fs::write(&managed_ca_cert_path, "managed ca\n").unwrap(); + fs::write(&inherited_bundle_path, "inherited ca\n").unwrap(); + let env = HashMap::from([( + "SSL_CERT_FILE".to_string(), + inherited_bundle_path.display().to_string(), + )]); + + let trust_bundle = + build_managed_ca_trust_bundle(&managed_ca_cert_path, &trust_bundle_path, &env).unwrap(); + + assert!(trust_bundle.contains("inherited ca")); + assert!(trust_bundle.contains("managed ca")); + } + #[cfg(unix)] #[test] fn validate_existing_ca_key_file_rejects_group_world_permissions() { diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index 04b717fe61..746d57f9ba 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -627,6 +627,19 @@ impl NetworkProxy { pub fn apply_to_env(&self, env: &mut HashMap) { let runtime_settings = self.runtime_settings(); + // Fold command-level CA overrides into our replacement bundle before overwriting them. + let mitm_ca_trust_bundle_path = + runtime_settings + .mitm_ca_trust_bundle_path + .as_ref() + .map(|fallback_path| { + crate::certs::managed_ca_trust_bundle_path(env).unwrap_or_else(|err| { + warn!( + "failed to refresh managed MITM CA trust bundle from child env; using startup bundle: {err}" + ); + fallback_path.clone() + }) + }); // Enforce proxying for child processes. We intentionally override existing values so // command-level environment cannot bypass the managed proxy endpoint. apply_proxy_env_overrides( @@ -635,7 +648,7 @@ impl NetworkProxy { self.socks_addr, self.socks_enabled, runtime_settings.allow_local_binding, - runtime_settings.mitm_ca_trust_bundle_path.as_deref(), + mitm_ca_trust_bundle_path.as_deref(), ); }