Skip to content
22 changes: 19 additions & 3 deletions codex-rs/network-proxy/src/certs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,14 @@ const MANAGED_MITM_CA_DIR: &str = "proxy";
const MANAGED_MITM_CA_CERT: &str = "ca.pem";
const MANAGED_MITM_CA_KEY: &str = "ca.key";
const MANAGED_MITM_CA_TRUST_BUNDLE_PREFIX: &str = "ca-bundle";
const SSL_CERT_FILE_ENV_KEY: &str = "SSL_CERT_FILE";
pub const SSL_CERT_DIR_ENV_KEY: &str = "SSL_CERT_DIR";

// Best-effort compatibility set for common child toolchains that accept a CA bundle path.
// This is intentionally curated rather than pretending to cover every TLS client.
pub const CUSTOM_CA_ENV_KEYS: [&str; 10] = [
"CODEX_CA_CERTIFICATE",
"SSL_CERT_FILE",
SSL_CERT_FILE_ENV_KEY,
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"NODE_EXTRA_CA_CERTS",
Expand All @@ -123,6 +125,7 @@ pub const CUSTOM_CA_ENV_KEYS: [&str; 10] = [
pub(crate) struct ManagedMitmCaTrustBundle {
pub(crate) path: PathBuf,
pub(crate) startup_env_values: HashMap<&'static str, String>,
pub(crate) startup_cwd: PathBuf,
}

fn managed_ca_paths() -> Result<(PathBuf, PathBuf)> {
Expand All @@ -147,8 +150,11 @@ fn managed_ca_trust_bundle_for_cert_path(
cert_path: &Path,
env: &HashMap<&'static str, String>,
) -> Result<ManagedMitmCaTrustBundle> {
let startup_cwd =
std::env::current_dir().context("failed to resolve startup cwd for managed MITM CA")?;
let startup_env_values = CUSTOM_CA_ENV_KEYS
.into_iter()
.chain(std::iter::once(SSL_CERT_DIR_ENV_KEY))
.filter_map(|key| {
env.get(key)
.filter(|value| !value.is_empty())
Expand All @@ -161,6 +167,7 @@ fn managed_ca_trust_bundle_for_cert_path(
Ok(ManagedMitmCaTrustBundle {
path,
startup_env_values,
startup_cwd,
})
}

Expand Down Expand Up @@ -526,15 +533,24 @@ mod tests {
let dir = tempdir().unwrap();
let managed_ca_cert_path = dir.path().join("ca.pem");
let startup_ca_bundle_path = dir.path().join("startup-ca.pem");
let startup_cert_dir = dir.path().join("startup-certs");
fs::write(&managed_ca_cert_path, "managed ca\n").unwrap();
fs::write(&startup_ca_bundle_path, "startup ca\n").unwrap();
fs::create_dir(&startup_cert_dir).unwrap();
let startup_ca_bundle_path = startup_ca_bundle_path.display().to_string();
let env = HashMap::from([("SSL_CERT_FILE", startup_ca_bundle_path.clone())]);
let startup_cert_dir = startup_cert_dir.display().to_string();
let env = HashMap::from([
("SSL_CERT_FILE", startup_ca_bundle_path.clone()),
(SSL_CERT_DIR_ENV_KEY, startup_cert_dir.clone()),
]);
let trust_bundle =
managed_ca_trust_bundle_for_cert_path(&managed_ca_cert_path, &env).unwrap();
assert_eq!(
trust_bundle.startup_env_values,
HashMap::from([("SSL_CERT_FILE", startup_ca_bundle_path)])
HashMap::from([
("SSL_CERT_FILE", startup_ca_bundle_path),
(SSL_CERT_DIR_ENV_KEY, startup_cert_dir),
])
);
}

Expand Down
2 changes: 2 additions & 0 deletions codex-rs/network-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod state;
mod upstream;

pub use certs::CUSTOM_CA_ENV_KEYS;
pub use certs::SSL_CERT_DIR_ENV_KEY;
pub use certs::is_managed_mitm_ca_trust_bundle_path;
pub use config::NetworkDomainPermission;
pub use config::NetworkDomainPermissionEntry;
Expand Down Expand Up @@ -47,6 +48,7 @@ pub use proxy::Args;
#[cfg(target_os = "macos")]
pub use proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER;
pub use proxy::DEFAULT_NO_PROXY_VALUE;
pub use proxy::MITM_CA_ENV_ACTIVE_ENV_KEY;
pub use proxy::NO_PROXY_ENV_KEYS;
pub use proxy::NetworkProxy;
pub use proxy::NetworkProxyBuilder;
Expand Down
86 changes: 73 additions & 13 deletions codex-rs/network-proxy/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ impl NetworkProxyRuntimeSettings {
let mitm_ca_trust_bundle = if config.network.mitm {
let env = crate::certs::CUSTOM_CA_ENV_KEYS
.into_iter()
.chain(std::iter::once(crate::certs::SSL_CERT_DIR_ENV_KEY))
.filter_map(|key| std::env::var(key).ok().map(|value| (key, value)))
.collect();
Some(crate::certs::managed_ca_trust_bundle(&env)?)
Expand Down Expand Up @@ -376,13 +377,17 @@ pub const PROXY_URL_ENV_KEYS: &[&str] = &[

pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"];
pub const PROXY_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_ACTIVE";
pub const MITM_CA_ENV_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_MITM_CA_ENV_ACTIVE";
const STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY: &str = "CODEX_NETWORK_PROXY_STARTUP_CA_ENV_KEYS_PRESENT";
pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING";
const ELECTRON_GET_USE_PROXY_ENV_KEY: &str = "ELECTRON_GET_USE_PROXY";
const NODE_USE_ENV_PROXY_ENV_KEY: &str = "NODE_USE_ENV_PROXY";
#[cfg(any(target_os = "macos", test))]
const GIT_SSH_COMMAND_ENV_KEY: &str = "GIT_SSH_COMMAND";
pub const PROXY_ENV_KEYS: &[&str] = &[
PROXY_ACTIVE_ENV_KEY,
MITM_CA_ENV_ACTIVE_ENV_KEY,
STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY,
ALLOW_LOCAL_BINDING_ENV_KEY,
ELECTRON_GET_USE_PROXY_ENV_KEY,
NODE_USE_ENV_PROXY_ENV_KEY,
Expand Down Expand Up @@ -571,27 +576,55 @@ fn apply_proxy_env_overrides(
}

if let Some(mitm_ca_trust_bundle) = mitm_ca_trust_bundle {
env.insert(MITM_CA_ENV_ACTIVE_ENV_KEY.to_string(), "1".to_string());
let managed_path = mitm_ca_trust_bundle.path.to_string_lossy().into_owned();
let startup_ca_env_keys_present_in_child = ca_env_keys()
.filter(|&key| {
env.get(key)
.filter(|value| !value.is_empty())
.is_some_and(|value| {
mitm_ca_trust_bundle.startup_env_values.get(key) == Some(value)
|| (value == &managed_path && is_tracked_startup_ca_env_key(env, key))
})
})
.collect::<Vec<_>>();
if startup_ca_env_keys_present_in_child.is_empty() {
env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY);
} else {
env.insert(
STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY.to_string(),
startup_ca_env_keys_present_in_child.join(","),
);
}
for key in crate::certs::CUSTOM_CA_ENV_KEYS {
if env
.get(key)
.filter(|value| !value.is_empty())
.is_some_and(|value| {
value != &managed_path
&& mitm_ca_trust_bundle.startup_env_values.get(key) != Some(value)
value != &managed_path && !startup_ca_env_keys_present_in_child.contains(&key)
})
{
// TODO(winston): Materialize policy-checked per-child bundles for readable
// startup and command-scoped CA overrides. For now startup overrides are
// replaced with the default bundle and later command-scoped overrides are
// preserved, either of which can make intercepted TLS fail.
continue;
}
env.insert(key.to_string(), managed_path.clone());
}
} else {
env.remove(MITM_CA_ENV_ACTIVE_ENV_KEY);
env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY);
}
}

fn ca_env_keys() -> impl Iterator<Item = &'static str> {
crate::certs::CUSTOM_CA_ENV_KEYS
.into_iter()
.chain(std::iter::once(crate::certs::SSL_CERT_DIR_ENV_KEY))
}

fn is_tracked_startup_ca_env_key(env: &HashMap<String, String>, key: &str) -> bool {
env.get(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY)
.is_some_and(|keys| keys.split(',').any(|tracked_key| tracked_key == key))
}

impl NetworkProxy {
pub fn builder() -> NetworkProxyBuilder {
NetworkProxyBuilder::default()
Expand Down Expand Up @@ -643,7 +676,7 @@ impl NetworkProxy {
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
let runtime_settings = self.runtime_settings();
// Enforce proxying for child processes. Proxy endpoint values are always rewritten;
// managed MITM CA vars preserve child-scoped overrides after proxy startup.
// managed MITM CA vars preserve command-scoped overrides after proxy startup.
apply_proxy_env_overrides(
env,
self.http_addr,
Expand Down Expand Up @@ -1110,6 +1143,7 @@ mod tests {
let mitm_ca_trust_bundle = crate::certs::ManagedMitmCaTrustBundle {
path: mitm_ca_trust_bundle_path.to_path_buf(),
startup_env_values: HashMap::new(),
startup_cwd: Path::new("/tmp").to_path_buf(),
};
apply_proxy_env_overrides(
&mut env,
Expand All @@ -1129,18 +1163,18 @@ mod tests {
}

#[test]
fn apply_proxy_env_overrides_preserves_command_scoped_mitm_ca_override() {
let command_ca_bundle_path = "/tmp/command-ca.pem".to_string();
fn apply_proxy_env_overrides_tracks_rewritten_startup_mitm_ca_override() {
let startup_ca_bundle_path = "/tmp/startup-ca.pem".to_string();
let mut env = HashMap::from([(
"REQUESTS_CA_BUNDLE".to_string(),
command_ca_bundle_path.clone(),
startup_ca_bundle_path.clone(),
)]);
let mitm_ca_trust_bundle_path = Path::new("/tmp/codex-proxy/ca-bundle.pem");
let mitm_ca_trust_bundle = crate::certs::ManagedMitmCaTrustBundle {
path: mitm_ca_trust_bundle_path.to_path_buf(),
startup_env_values: HashMap::new(),
startup_env_values: HashMap::from([("REQUESTS_CA_BUNDLE", startup_ca_bundle_path)]),
startup_cwd: Path::new("/tmp").to_path_buf(),
};

apply_proxy_env_overrides(
&mut env,
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
Expand All @@ -1150,11 +1184,37 @@ mod tests {
Some(&mitm_ca_trust_bundle),
);

assert_eq!(env.get("REQUESTS_CA_BUNDLE"), Some(&command_ca_bundle_path));
assert_eq!(
env.get("REQUESTS_CA_BUNDLE"),
Some(&mitm_ca_trust_bundle_path.display().to_string())
);
assert_eq!(
env.get(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY),
Some(&"REQUESTS_CA_BUNDLE".to_string())
);
assert_eq!(
env.get("SSL_CERT_FILE"),
Some(&mitm_ca_trust_bundle_path.display().to_string())
);

env.insert(
"REQUESTS_CA_BUNDLE".to_string(),
"/tmp/command-ca.pem".to_string(),
);
apply_proxy_env_overrides(
&mut env,
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
/*socks_enabled*/ true,
/*allow_local_binding*/ false,
Some(&mitm_ca_trust_bundle),
);

assert_eq!(
env.get("REQUESTS_CA_BUNDLE"),
Some(&"/tmp/command-ca.pem".to_string())
);
assert_eq!(env.get(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY), None);
}

#[test]
Expand Down
Loading