Merge branch 'dev/winston/mitm-command-ca-bundles' into dev/winston/credentialed-routes-e2e

# Conflicts:
#	codex-rs/backend-client/src/client.rs
#	codex-rs/backend-client/src/lib.rs
This commit is contained in:
Winston Howes
2026-06-03 19:39:58 -07:00
329 changed files with 15716 additions and 5353 deletions
+8
View File
@@ -44,4 +44,12 @@ pretty_assertions = { workspace = true }
tempfile = { workspace = true }
[target.'cfg(target_family = "unix")'.dependencies]
libc = { workspace = true }
rama-unix = { version = "=0.3.0-alpha.4" }
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "3"
[target.'cfg(windows)'.dependencies]
schannel = "0.1"
windows-sys = { version = "0.52", features = ["Win32_Storage_FileSystem"] }
+410 -15
View File
@@ -23,11 +23,27 @@ use rama_tls_rustls::server::TlsAcceptorData;
use sha2::Digest as _;
use sha2::Sha256;
use std::collections::HashMap;
use std::ffi::OsStr;
#[cfg(windows)]
use std::ffi::OsString;
use std::fs;
use std::fs::File;
use std::fs::OpenOptions;
use std::io::Read;
use std::io::Write;
use std::net::IpAddr;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::os::fd::AsRawFd;
#[cfg(target_os = "macos")]
use std::os::unix::ffi::OsStrExt;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::ffi::OsStringExt;
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
#[cfg(windows)]
use std::os::windows::io::AsRawHandle;
use std::path::Path;
use std::path::PathBuf;
use std::time::SystemTime;
@@ -101,12 +117,15 @@ 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 MAX_CUSTOM_CA_BUNDLE_BYTES: u64 = 4 * 1024 * 1024;
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",
@@ -122,6 +141,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)> {
@@ -146,8 +166,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())
@@ -160,13 +183,14 @@ fn managed_ca_trust_bundle_for_cert_path(
Ok(ManagedMitmCaTrustBundle {
path,
startup_env_values,
startup_cwd,
})
}
fn build_managed_ca_trust_bundle(managed_ca_cert_path: &Path) -> Result<String> {
let mut trust_bundle = String::new();
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
crate::native_certs::load_platform_native_certs();
if !errors.is_empty() {
warn!(
native_root_error_count = errors.len(),
@@ -184,18 +208,23 @@ fn is_current_generated_trust_bundle_path(path: &Path, managed_ca_cert_path: &Pa
let Some(proxy_dir) = managed_ca_cert_path.parent() else {
return false;
};
let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) else {
return false;
};
if path.parent() != Some(proxy_dir)
|| !file_name.starts_with(MANAGED_MITM_CA_TRUST_BUNDLE_PREFIX)
|| !file_name.ends_with(".pem")
{
if !matches_generated_trust_bundle_path(path, proxy_dir) {
return false;
}
let Ok(trust_bundle) = fs::read(path) else {
return false;
};
let expected_hash = format!("{:x}", Sha256::digest(&trust_bundle));
if path
.file_stem()
.and_then(OsStr::to_str)
.and_then(|file_stem| {
file_stem.strip_prefix(&format!("{MANAGED_MITM_CA_TRUST_BUNDLE_PREFIX}-"))
})
.is_none_or(|hash| hash != expected_hash)
{
return false;
}
let Ok(managed_ca_cert) = fs::read(managed_ca_cert_path) else {
return false;
};
@@ -220,6 +249,128 @@ fn persist_managed_ca_trust_bundle(
let proxy_dir = managed_ca_cert_path
.parent()
.ok_or_else(|| anyhow!("managed MITM CA cert path is missing a parent"))?;
persist_ca_trust_bundle(proxy_dir, trust_bundle)
}
pub(crate) fn materialize_ca_trust_bundle_with_custom_ca(
managed_ca_trust_bundle: &ManagedMitmCaTrustBundle,
custom_ca_bundle: &str,
) -> Result<PathBuf> {
let proxy_dir = managed_ca_trust_bundle
.path
.parent()
.ok_or_else(|| anyhow!("managed MITM CA trust bundle path is missing a parent"))?;
anyhow::ensure!(
custom_ca_bundle.len() as u64 <= MAX_CUSTOM_CA_BUNDLE_BYTES,
"custom CA bundle exceeds {MAX_CUSTOM_CA_BUNDLE_BYTES} bytes"
);
let mut trust_bundle = String::new();
append_pem_contents(&mut trust_bundle, custom_ca_bundle);
append_pem_file(&mut trust_bundle, &managed_ca_trust_bundle.path)?;
persist_ca_trust_bundle(proxy_dir, &trust_bundle)
}
pub(crate) fn read_custom_ca_bundle<F>(path: &Path, can_read_path: F) -> Result<String>
where
F: Fn(&Path) -> bool,
{
anyhow::ensure!(
can_read_path(path),
"CA bundle {} is not readable by child policy",
path.display()
);
let path = path
.canonicalize()
.with_context(|| format!("failed to resolve CA bundle {}", path.display()))?;
anyhow::ensure!(
can_read_path(&path),
"CA bundle {} is not readable by child policy",
path.display()
);
let mut file = open_readonly_without_following_symlink(&path)?;
let metadata = file
.metadata()
.with_context(|| format!("failed to stat CA bundle {}", path.display()))?;
anyhow::ensure!(
metadata.is_file(),
"CA bundle {} must be a regular file",
path.display()
);
anyhow::ensure!(
metadata.len() <= MAX_CUSTOM_CA_BUNDLE_BYTES,
"CA bundle {} exceeds {MAX_CUSTOM_CA_BUNDLE_BYTES} bytes",
path.display()
);
let opened_path = opened_file_path(&path, &file)?;
anyhow::ensure!(
can_read_path(&opened_path),
"CA bundle {} is not readable by child policy",
opened_path.display()
);
validate_opened_file_path(&path, &opened_path, &metadata)?;
let mut bytes = Vec::with_capacity(metadata.len() as usize);
std::io::Read::by_ref(&mut file)
.take(MAX_CUSTOM_CA_BUNDLE_BYTES + 1)
.read_to_end(&mut bytes)
.with_context(|| format!("failed to read CA bundle {}", path.display()))?;
anyhow::ensure!(
bytes.len() as u64 <= MAX_CUSTOM_CA_BUNDLE_BYTES,
"CA bundle {} exceeds {MAX_CUSTOM_CA_BUNDLE_BYTES} bytes",
path.display()
);
String::from_utf8(bytes)
.with_context(|| format!("CA bundle {} must be valid UTF-8", path.display()))
}
pub(crate) fn read_custom_ca_dir<F>(dir: &Path, can_read_path: F) -> Result<String>
where
F: Fn(&Path) -> bool,
{
anyhow::ensure!(
can_read_path(dir),
"CA directory {} is not readable by child policy",
dir.display()
);
let dir = dir
.canonicalize()
.with_context(|| format!("failed to resolve CA directory {}", dir.display()))?;
anyhow::ensure!(
can_read_path(&dir),
"CA directory {} is not readable by child policy",
dir.display()
);
anyhow::ensure!(
dir.metadata()
.with_context(|| format!("failed to stat CA directory {}", dir.display()))?
.is_dir(),
"CA directory {} must be a directory",
dir.display()
);
let mut trust_bundle = String::new();
for entry in fs::read_dir(&dir)
.with_context(|| format!("failed to read CA directory {}", dir.display()))?
{
let entry = entry
.with_context(|| format!("failed to read CA directory entry in {}", dir.display()))?;
let path = entry.path();
match read_custom_ca_bundle(&path, &can_read_path) {
Ok(contents) => append_bounded_pem_contents(&mut trust_bundle, &contents)?,
Err(err) => {
warn!(
ca_bundle_path = %path.display(),
"failed to read CA directory entry; skipping it: {err}"
);
}
}
}
Ok(trust_bundle)
}
fn persist_ca_trust_bundle(proxy_dir: &Path, trust_bundle: &str) -> Result<PathBuf> {
fs::create_dir_all(proxy_dir)
.with_context(|| format!("failed to create {}", proxy_dir.display()))?;
let hash = Sha256::digest(trust_bundle.as_bytes());
@@ -240,16 +391,177 @@ fn persist_managed_ca_trust_bundle(
Ok(trust_bundle_path)
}
fn matches_generated_trust_bundle_path(path: &Path, proxy_dir: &Path) -> bool {
let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) else {
return false;
};
path.parent() == Some(proxy_dir)
&& file_name.starts_with(&format!("{MANAGED_MITM_CA_TRUST_BUNDLE_PREFIX}-"))
&& file_name.ends_with(".pem")
}
fn append_pem_file(bundle: &mut String, path: &Path) -> Result<()> {
if !bundle.ends_with('\n') {
bundle.push('\n');
}
let pem = fs::read_to_string(path)
.with_context(|| format!("failed to read CA bundle {}", path.display()))?;
bundle.push_str(&pem);
append_pem_contents(bundle, &pem);
Ok(())
}
pub(crate) fn append_pem_contents(bundle: &mut String, pem: &str) {
if !bundle.is_empty() && !bundle.ends_with('\n') {
bundle.push('\n');
}
bundle.push_str(pem);
if !bundle.ends_with('\n') {
bundle.push('\n');
}
}
pub(crate) fn append_bounded_pem_contents(bundle: &mut String, pem: &str) -> Result<()> {
let separator_len = usize::from(!bundle.is_empty() && !bundle.ends_with('\n'));
let trailing_newline_len = usize::from(!pem.ends_with('\n'));
anyhow::ensure!(
(bundle.len() + separator_len + pem.len() + trailing_newline_len) as u64
<= MAX_CUSTOM_CA_BUNDLE_BYTES,
"CA directory exceeds {MAX_CUSTOM_CA_BUNDLE_BYTES} bytes"
);
append_pem_contents(bundle, pem);
Ok(())
}
fn open_readonly_without_following_symlink(path: &Path) -> Result<File> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
options
.open(path)
.with_context(|| format!("failed to open CA bundle {}", path.display()))
}
fn opened_file_path(path: &Path, _file: &File) -> Result<PathBuf> {
#[cfg(target_os = "linux")]
{
let opened_path = fs::read_link(format!("/proc/self/fd/{}", _file.as_raw_fd()))
.with_context(|| format!("failed to resolve opened CA bundle {}", path.display()))?;
opened_path
.canonicalize()
.with_context(|| format!("failed to canonicalize opened CA bundle {}", path.display()))
}
#[cfg(target_os = "macos")]
{
let mut opened_path = vec![0_u8; libc::PATH_MAX as usize];
// SAFETY: fcntl writes at most PATH_MAX bytes into the provided writable buffer.
let result =
unsafe { libc::fcntl(_file.as_raw_fd(), libc::F_GETPATH, opened_path.as_mut_ptr()) };
anyhow::ensure!(
result != -1,
"failed to resolve opened CA bundle {}: {}",
path.display(),
std::io::Error::last_os_error()
);
let opened_path_len = opened_path
.iter()
.position(|byte| *byte == 0)
.unwrap_or(opened_path.len());
PathBuf::from(OsStr::from_bytes(&opened_path[..opened_path_len]))
.canonicalize()
.with_context(|| format!("failed to canonicalize opened CA bundle {}", path.display()))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
#[cfg(windows)]
{
use windows_sys::Win32::Storage::FileSystem::FILE_NAME_NORMALIZED;
use windows_sys::Win32::Storage::FileSystem::GetFinalPathNameByHandleW;
use windows_sys::Win32::Storage::FileSystem::VOLUME_NAME_DOS;
let mut opened_path = vec![0_u16; 512];
loop {
// SAFETY: `_file` owns a live OS handle and `opened_path` is writable for its
// declared capacity.
let length = unsafe {
GetFinalPathNameByHandleW(
_file.as_raw_handle() as _,
opened_path.as_mut_ptr(),
opened_path.len() as u32,
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS,
)
};
anyhow::ensure!(
length != 0,
"failed to resolve opened CA bundle {}: {}",
path.display(),
std::io::Error::last_os_error()
);
if length < opened_path.len() as u32 {
opened_path.truncate(length as usize);
break;
}
opened_path.resize(length as usize + 1, 0);
}
PathBuf::from(OsString::from_wide(&opened_path))
.canonicalize()
.with_context(|| {
format!("failed to canonicalize opened CA bundle {}", path.display())
})
}
#[cfg(not(windows))]
{
path.canonicalize()
.with_context(|| format!("failed to resolve CA bundle {}", path.display()))
}
}
}
fn validate_opened_file_path(
path: &Path,
opened_path: &Path,
metadata: &fs::Metadata,
) -> Result<()> {
#[cfg(unix)]
{
let opened_path_metadata = fs::metadata(opened_path).with_context(|| {
format!("failed to stat opened CA bundle {}", opened_path.display())
})?;
anyhow::ensure!(
metadata.dev() == opened_path_metadata.dev()
&& metadata.ino() == opened_path_metadata.ino(),
"CA bundle {} changed before it could be validated",
path.display()
);
}
#[cfg(not(unix))]
{
#[cfg(windows)]
{
let opened_path_metadata = fs::metadata(opened_path).with_context(|| {
format!("failed to stat opened CA bundle {}", opened_path.display())
})?;
anyhow::ensure!(
metadata.volume_serial_number() == opened_path_metadata.volume_serial_number()
&& metadata.file_index() == opened_path_metadata.file_index(),
"CA bundle {} changed before it could be validated",
path.display()
);
}
#[cfg(not(windows))]
{
let _ = path;
let _ = opened_path;
let _ = metadata;
}
}
Ok(())
}
@@ -507,17 +819,100 @@ mod tests {
));
}
#[test]
fn current_generated_trust_bundle_path_rejects_hash_mismatch() {
let dir = tempdir().unwrap();
let managed_ca_cert_path = dir.path().join("ca.pem");
let trust_bundle_path = dir.path().join("ca-bundle-123.pem");
fs::write(&managed_ca_cert_path, "managed ca\n").unwrap();
fs::write(&trust_bundle_path, "custom ca\nmanaged ca\n").unwrap();
assert!(!is_current_generated_trust_bundle_path(
&trust_bundle_path,
&managed_ca_cert_path,
));
}
#[test]
fn managed_ca_trust_bundle_records_startup_ca_env_values() {
let dir = tempdir().unwrap();
let managed_ca_cert_path = dir.path().join("ca.pem");
fs::write(&managed_ca_cert_path, "managed ca\n").unwrap();
let env = HashMap::from([("SSL_CERT_FILE", "/tmp/startup-ca.pem".to_string())]);
let env = HashMap::from([
("SSL_CERT_FILE", "/tmp/startup-ca.pem".to_string()),
(SSL_CERT_DIR_ENV_KEY, "/tmp/startup-certs".to_string()),
]);
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", "/tmp/startup-ca.pem".to_string())])
HashMap::from([
("SSL_CERT_FILE", "/tmp/startup-ca.pem".to_string()),
(SSL_CERT_DIR_ENV_KEY, "/tmp/startup-certs".to_string()),
])
);
}
#[test]
fn managed_ca_trust_bundle_does_not_append_startup_ca_override_to_baseline() {
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");
fs::write(&managed_ca_cert_path, "managed ca\n").unwrap();
fs::write(&startup_ca_bundle_path, "startup ca\n").unwrap();
let env = HashMap::from([(
"SSL_CERT_FILE",
startup_ca_bundle_path.display().to_string(),
)]);
let trust_bundle =
managed_ca_trust_bundle_for_cert_path(&managed_ca_cert_path, &env).unwrap();
let baseline_bundle = fs::read_to_string(trust_bundle.path).unwrap();
assert!(!baseline_bundle.contains("startup ca"));
assert!(baseline_bundle.contains("managed ca"));
}
#[test]
fn read_custom_ca_bundle_rejects_non_regular_file() {
let dir = tempdir().unwrap();
let err = read_custom_ca_bundle(dir.path(), |_| true).unwrap_err();
assert!(
err.to_string().contains("must be a regular file")
|| err.to_string().contains("failed to open CA bundle"),
"unexpected error: {err:#}"
);
}
#[cfg(unix)]
#[test]
fn read_custom_ca_bundle_reads_readable_symlink() {
let dir = tempdir().unwrap();
let ca_bundle_path = dir.path().join("ca.pem");
let symlink_path = dir.path().join("ca-link.pem");
fs::write(&ca_bundle_path, "custom ca\n").unwrap();
std::os::unix::fs::symlink(&ca_bundle_path, &symlink_path).unwrap();
assert_eq!(
read_custom_ca_bundle(&symlink_path, |_| true).unwrap(),
"custom ca\n"
);
}
#[cfg(windows)]
#[test]
fn opened_file_path_uses_windows_handle_target() {
let dir = tempdir().unwrap();
let checked_path = dir.path().join("checked.pem");
let opened_path = dir.path().join("opened.pem");
fs::write(&checked_path, "checked ca\n").unwrap();
fs::write(&opened_path, "opened ca\n").unwrap();
let file = File::open(&opened_path).unwrap();
assert_eq!(
opened_file_path(&checked_path, &file).unwrap(),
opened_path.canonicalize().unwrap()
);
}
+203
View File
@@ -0,0 +1,203 @@
use crate::certs::ManagedMitmCaTrustBundle;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::path::Path;
use tracing::warn;
pub(crate) fn prepare_mitm_ca_trust_bundle_env<F>(
mitm_ca_trust_bundle: &ManagedMitmCaTrustBundle,
env: &mut HashMap<String, String>,
cwd: &Path,
startup_ca_env_keys_present_in_child: &[&'static str],
can_read_path: F,
) -> Vec<AbsolutePathBuf>
where
F: Fn(&Path) -> bool,
{
let ssl_cert_dir_contents = read_child_ca_dir_contents(
mitm_ca_trust_bundle,
env,
cwd,
startup_ca_env_keys_present_in_child,
&can_read_path,
);
// Fold SSL_CERT_DIR into SSL_CERT_FILE so children cannot consult an
// unmaterialized CA directory after preparation.
env.remove(crate::certs::SSL_CERT_DIR_ENV_KEY);
let mut materialized_ca_trust_bundle_paths = Vec::new();
for key in crate::certs::CUSTOM_CA_ENV_KEYS {
let Some(value) = env.get(key).filter(|value| !value.is_empty()) else {
continue;
};
let mut custom_ca_bundle = read_child_ca_bundle_contents(
mitm_ca_trust_bundle,
key,
value,
cwd,
startup_ca_env_keys_present_in_child,
&can_read_path,
)
.unwrap_or_default();
if key == "SSL_CERT_FILE"
&& let Some(ssl_cert_dir_contents) = ssl_cert_dir_contents.as_deref()
&& let Err(err) = crate::certs::append_bounded_pem_contents(
&mut custom_ca_bundle,
ssl_cert_dir_contents,
)
{
warn!(
ca_env_key = key,
"failed to combine child MITM CA bundle; leaving current value unchanged: {err}"
);
continue;
}
if custom_ca_bundle.is_empty() {
continue;
}
match crate::certs::materialize_ca_trust_bundle_with_custom_ca(
mitm_ca_trust_bundle,
&custom_ca_bundle,
) {
Ok(path) => {
env.insert(key.to_string(), path.to_string_lossy().into_owned());
materialized_ca_trust_bundle_paths.push(path);
}
Err(err) => {
warn!(
ca_env_key = key,
"failed to materialize child MITM CA bundle; leaving current value unchanged: {err}"
);
}
}
}
managed_mitm_ca_trust_bundle_paths_for_env(
mitm_ca_trust_bundle,
env,
&materialized_ca_trust_bundle_paths,
)
}
fn resolve_ca_bundle_path(path: &str, cwd: &Path) -> std::path::PathBuf {
let path = Path::new(path);
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
}
fn read_child_ca_bundle_contents<F>(
mitm_ca_trust_bundle: &ManagedMitmCaTrustBundle,
key: &'static str,
value: &str,
cwd: &Path,
startup_ca_env_keys_present_in_child: &[&'static str],
can_read_path: &F,
) -> Option<String>
where
F: Fn(&Path) -> bool,
{
let value_path = Path::new(value);
let custom_ca_bundle_path = if startup_ca_env_keys_present_in_child.contains(&key) {
let startup_value = mitm_ca_trust_bundle.startup_env_values.get(key)?;
resolve_ca_bundle_path(startup_value, &mitm_ca_trust_bundle.startup_cwd)
} else if value_path == mitm_ca_trust_bundle.path {
return None;
} else {
resolve_ca_bundle_path(value, cwd)
};
match crate::certs::read_custom_ca_bundle(&custom_ca_bundle_path, can_read_path) {
Ok(contents) => Some(contents),
Err(err) => {
warn!(
ca_env_key = key,
ca_bundle_path = %custom_ca_bundle_path.display(),
"failed to read child MITM CA bundle; leaving current value unchanged: {err}"
);
None
}
}
}
fn read_child_ca_dir_contents<F>(
mitm_ca_trust_bundle: &ManagedMitmCaTrustBundle,
env: &HashMap<String, String>,
cwd: &Path,
startup_ca_env_keys_present_in_child: &[&'static str],
can_read_path: &F,
) -> Option<String>
where
F: Fn(&Path) -> bool,
{
let value = env
.get(crate::certs::SSL_CERT_DIR_ENV_KEY)
.filter(|value| !value.is_empty())?;
let ca_dir_cwd =
if startup_ca_env_keys_present_in_child.contains(&crate::certs::SSL_CERT_DIR_ENV_KEY) {
&mitm_ca_trust_bundle.startup_cwd
} else {
cwd
};
let mut trust_bundle = String::new();
for ca_dir_path in std::env::split_paths(value).map(|path| {
if path.is_absolute() {
path
} else {
ca_dir_cwd.join(path)
}
}) {
match crate::certs::read_custom_ca_dir(&ca_dir_path, can_read_path) {
Ok(contents) if !contents.is_empty() => {
if let Err(err) =
crate::certs::append_bounded_pem_contents(&mut trust_bundle, &contents)
{
warn!(
ca_bundle_path = %ca_dir_path.display(),
"failed to combine child MITM CA directories; ignoring SSL_CERT_DIR override: {err}"
);
return None;
}
}
Ok(_) => {}
Err(err) => {
warn!(
ca_bundle_path = %ca_dir_path.display(),
"failed to read child MITM CA directory; skipping it: {err}"
);
}
}
}
if trust_bundle.is_empty() {
None
} else {
Some(trust_bundle)
}
}
fn managed_mitm_ca_trust_bundle_paths_for_env(
mitm_ca_trust_bundle: &ManagedMitmCaTrustBundle,
env: &HashMap<String, String>,
materialized_ca_trust_bundle_paths: &[std::path::PathBuf],
) -> Vec<AbsolutePathBuf> {
let mut paths = crate::certs::CUSTOM_CA_ENV_KEYS
.into_iter()
.filter_map(|key| env.get(key))
.map(Path::new)
.filter(|path| {
*path == mitm_ca_trust_bundle.path
|| materialized_ca_trust_bundle_paths
.iter()
.any(|materialized_path| path == materialized_path)
})
.filter_map(|path| AbsolutePathBuf::from_absolute_path(path).ok())
.collect::<Vec<_>>();
paths.sort();
paths.dedup();
paths
}
#[cfg(test)]
#[path = "child_ca_tests.rs"]
mod tests;
@@ -0,0 +1,218 @@
use super::*;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::Path;
use tempfile::tempdir;
const REQUESTS_CA_BUNDLE_ENV_KEY: &str = "REQUESTS_CA_BUNDLE";
fn test_mitm_ca_trust_bundle(
dir: &tempfile::TempDir,
startup_env_values: HashMap<&'static str, String>,
) -> ManagedMitmCaTrustBundle {
let path = dir.path().join("ca-bundle.pem");
fs::write(&path, "managed ca\n").unwrap();
ManagedMitmCaTrustBundle {
path,
startup_env_values,
startup_cwd: dir.path().to_path_buf(),
}
}
fn requests_ca_bundle_env(value: impl Into<String>) -> HashMap<String, String> {
HashMap::from([(REQUESTS_CA_BUNDLE_ENV_KEY.to_string(), value.into())])
}
fn requests_ca_bundle_contents(env: &HashMap<String, String>) -> String {
fs::read_to_string(Path::new(
env.get(REQUESTS_CA_BUNDLE_ENV_KEY)
.expect("REQUESTS_CA_BUNDLE should be set"),
))
.unwrap()
}
fn ssl_cert_dir_env(
dir: &tempfile::TempDir,
contents: [String; 2],
) -> (HashMap<String, String>, ManagedMitmCaTrustBundle) {
let ssl_cert_dir_paths = [dir.path().join("certs-a"), dir.path().join("certs-b")];
for (path, contents) in ssl_cert_dir_paths.iter().zip(contents) {
fs::create_dir(path).unwrap();
fs::write(path.join("ordinary-ca.pem"), contents).unwrap();
}
let mitm_ca_trust_bundle_path = dir.path().join("ca-bundle.pem");
fs::write(&mitm_ca_trust_bundle_path, "managed ca\n").unwrap();
let ssl_cert_dir = std::env::join_paths(["certs-a", "certs-b"]).unwrap();
let ssl_cert_dir = ssl_cert_dir.to_string_lossy().into_owned();
(
HashMap::from([
(
"SSL_CERT_FILE".to_string(),
mitm_ca_trust_bundle_path.display().to_string(),
),
(
crate::certs::SSL_CERT_DIR_ENV_KEY.to_string(),
ssl_cert_dir.clone(),
),
]),
ManagedMitmCaTrustBundle {
path: mitm_ca_trust_bundle_path,
startup_env_values: HashMap::from([(crate::certs::SSL_CERT_DIR_ENV_KEY, ssl_cert_dir)]),
startup_cwd: dir.path().to_path_buf(),
},
)
}
#[test]
fn materializes_readable_startup_ca_override() {
let dir = tempdir().unwrap();
let startup_ca_bundle_path = dir.path().join("startup-ca.pem");
let command_cwd = dir.path().join("command-cwd");
fs::create_dir(&command_cwd).unwrap();
fs::write(&startup_ca_bundle_path, "startup ca\n").unwrap();
let mitm_ca_trust_bundle = test_mitm_ca_trust_bundle(
&dir,
HashMap::from([(REQUESTS_CA_BUNDLE_ENV_KEY, "startup-ca.pem".to_string())]),
);
let mut env = requests_ca_bundle_env("startup-ca.pem");
let bundle_paths = prepare_mitm_ca_trust_bundle_env(
&mitm_ca_trust_bundle,
&mut env,
&command_cwd,
&[REQUESTS_CA_BUNDLE_ENV_KEY],
|_| true,
);
assert_eq!(
requests_ca_bundle_contents(&env),
"startup ca\nmanaged ca\n"
);
assert_eq!(bundle_paths.len(), 1);
}
#[test]
fn does_not_restore_filtered_startup_override() {
let dir = tempdir().unwrap();
let mitm_ca_trust_bundle = test_mitm_ca_trust_bundle(
&dir,
HashMap::from([(REQUESTS_CA_BUNDLE_ENV_KEY, "startup-ca.pem".to_string())]),
);
let mut env = requests_ca_bundle_env(mitm_ca_trust_bundle.path.display().to_string());
let bundle_paths =
prepare_mitm_ca_trust_bundle_env(&mitm_ca_trust_bundle, &mut env, dir.path(), &[], |_| {
true
});
assert_eq!(
env.get(REQUESTS_CA_BUNDLE_ENV_KEY),
Some(&mitm_ca_trust_bundle.path.display().to_string())
);
assert_eq!(bundle_paths.len(), 1);
}
#[test]
fn materializes_readable_command_scoped_override() {
let dir = tempdir().unwrap();
let command_ca_bundle_path = dir.path().join("command-ca.pem");
fs::write(&command_ca_bundle_path, "command ca\n").unwrap();
let mut env = requests_ca_bundle_env("command-ca.pem");
let mitm_ca_trust_bundle = test_mitm_ca_trust_bundle(&dir, HashMap::new());
prepare_mitm_ca_trust_bundle_env(&mitm_ca_trust_bundle, &mut env, dir.path(), &[], |_| true);
assert_eq!(
requests_ca_bundle_contents(&env),
"command ca\nmanaged ca\n"
);
}
#[test]
fn materializes_readable_ssl_cert_dir() {
let dir = tempdir().unwrap();
let (mut env, mitm_ca_trust_bundle) =
ssl_cert_dir_env(&dir, ["dir ca a\n".to_string(), "dir ca b\n".to_string()]);
prepare_mitm_ca_trust_bundle_env(
&mitm_ca_trust_bundle,
&mut env,
dir.path(),
&[crate::certs::SSL_CERT_DIR_ENV_KEY],
|_| true,
);
let ssl_cert_file_path = Path::new(
env.get("SSL_CERT_FILE")
.expect("SSL_CERT_FILE should be set"),
);
assert_eq!(
fs::read_to_string(ssl_cert_file_path).unwrap(),
"dir ca a\ndir ca b\nmanaged ca\n"
);
assert_eq!(env.get(crate::certs::SSL_CERT_DIR_ENV_KEY), None);
}
#[test]
fn bounds_aggregate_ssl_cert_dir_contents() {
let dir = tempdir().unwrap();
let oversized_dir_contents = "a".repeat(2_200_000);
let (mut env, mitm_ca_trust_bundle) = ssl_cert_dir_env(
&dir,
[oversized_dir_contents.clone(), oversized_dir_contents],
);
prepare_mitm_ca_trust_bundle_env(
&mitm_ca_trust_bundle,
&mut env,
dir.path(),
&[crate::certs::SSL_CERT_DIR_ENV_KEY],
|_| true,
);
assert_eq!(
env.get("SSL_CERT_FILE"),
Some(&mitm_ca_trust_bundle.path.display().to_string())
);
assert_eq!(env.get(crate::certs::SSL_CERT_DIR_ENV_KEY), None);
}
#[test]
fn preserves_unreadable_command_scoped_override() {
let dir = tempdir().unwrap();
let command_ca_bundle_path = dir.path().join("command-ca.pem");
fs::write(&command_ca_bundle_path, "command ca\n").unwrap();
let mut env = requests_ca_bundle_env("command-ca.pem");
let mitm_ca_trust_bundle = test_mitm_ca_trust_bundle(&dir, HashMap::new());
let bundle_paths =
prepare_mitm_ca_trust_bundle_env(&mitm_ca_trust_bundle, &mut env, dir.path(), &[], |_| {
false
});
assert_eq!(
env.get(REQUESTS_CA_BUNDLE_ENV_KEY),
Some(&"command-ca.pem".to_string())
);
assert!(bundle_paths.is_empty());
}
#[test]
fn does_not_whitelist_existing_generated_bundle_override() {
let dir = tempdir().unwrap();
let generated_ca_bundle_path = dir.path().join("ca-bundle-handcrafted.pem");
fs::write(&generated_ca_bundle_path, "extra ca\nmanaged ca\n").unwrap();
let mut env = requests_ca_bundle_env(generated_ca_bundle_path.display().to_string());
let mitm_ca_trust_bundle = test_mitm_ca_trust_bundle(&dir, HashMap::new());
let bundle_paths =
prepare_mitm_ca_trust_bundle_env(&mitm_ca_trust_bundle, &mut env, dir.path(), &[], |_| {
false
});
assert_eq!(
env.get(REQUESTS_CA_BUNDLE_ENV_KEY),
Some(&generated_ca_bundle_path.display().to_string())
);
assert!(bundle_paths.is_empty());
}
+4
View File
@@ -1,11 +1,13 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]
mod certs;
mod child_ca;
mod config;
mod connect_policy;
mod http_proxy;
mod mitm;
mod mitm_hook;
mod native_certs;
mod network_policy;
mod policy;
mod proxy;
@@ -17,6 +19,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;
@@ -47,6 +50,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;
+260
View File
@@ -0,0 +1,260 @@
#[cfg(any(target_os = "macos", windows))]
use rama_tls_rustls::dep::pki_types::CertificateDer;
use rustls_native_certs::CertificateResult;
#[cfg(any(target_os = "macos", windows))]
use rustls_native_certs::Error;
#[cfg(any(target_os = "macos", windows))]
use rustls_native_certs::ErrorKind;
// `rustls_native_certs::load_native_certs()` first consults SSL_CERT_FILE and
// SSL_CERT_DIR. The managed baseline must contain platform roots only; child
// CA overrides are copied later after the child filesystem policy can be checked.
#[cfg(all(unix, not(target_os = "macos")))]
pub(crate) fn load_platform_native_certs() -> CertificateResult {
let mut result =
rustls_native_certs::load_certs_from_paths(platform_cert_file().as_deref(), None);
for cert_dir in platform_cert_dirs() {
extend_certificate_result(
&mut result,
rustls_native_certs::load_certs_from_paths(None, Some(&cert_dir)),
);
}
dedupe_certs(&mut result);
result
}
#[cfg(target_os = "macos")]
pub(crate) fn load_platform_native_certs() -> CertificateResult {
use security_framework::trust_settings::Domain;
use security_framework::trust_settings::TrustSettings;
use security_framework::trust_settings::TrustSettingsForCertificate;
use std::collections::BTreeMap;
let mut result = CertificateResult::default();
let mut all_certs = BTreeMap::new();
for domain in &[Domain::User, Domain::Admin, Domain::System] {
let ts = TrustSettings::new(*domain);
let iter = match ts.iter() {
Ok(iter) => iter,
Err(err) => {
result.errors.push(Error {
context: 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",
},
kind: ErrorKind::Os(err.into()),
});
continue;
}
};
for cert in iter {
let der = cert.to_der();
let trusted = match ts.tls_trust_settings_for_certificate(&cert) {
Ok(trusted) => trusted.unwrap_or(TrustSettingsForCertificate::TrustRoot),
Err(err) => {
result.errors.push(Error {
context: "certificate not trusted",
kind: ErrorKind::Os(err.into()),
});
continue;
}
};
all_certs.entry(der).or_insert(trusted);
}
}
for (der, trusted) in all_certs {
use TrustSettingsForCertificate::*;
if let TrustRoot | TrustAsRoot = trusted {
result.certs.push(CertificateDer::from(der));
}
}
result
}
#[cfg(windows)]
pub(crate) fn load_platform_native_certs() -> CertificateResult {
use schannel::cert_store::CertStore;
let mut result = CertificateResult::default();
let current_user_store = match CertStore::open_current_user("ROOT") {
Ok(store) => store,
Err(err) => {
result.errors.push(Error {
context: "failed to open current user certificate store",
kind: ErrorKind::Os(err.into()),
});
return result;
}
};
for cert in current_user_store.certs() {
let valid_uses = match cert.valid_uses() {
Ok(valid_uses) => valid_uses,
Err(err) => {
result.errors.push(Error {
context: "failed to inspect certificate valid uses",
kind: ErrorKind::Os(err.into()),
});
continue;
}
};
let is_time_valid = match cert.is_time_valid() {
Ok(is_time_valid) => is_time_valid,
Err(err) => {
result.errors.push(Error {
context: "failed to inspect certificate time validity",
kind: ErrorKind::Os(err.into()),
});
continue;
}
};
if usable_for_rustls(valid_uses) && is_time_valid {
result
.certs
.push(CertificateDer::from(cert.to_der().to_vec()));
}
}
result
}
#[cfg(not(any(all(unix, not(target_os = "macos")), target_os = "macos", windows)))]
pub(crate) fn load_platform_native_certs() -> CertificateResult {
rustls_native_certs::load_native_certs()
}
#[cfg(all(unix, not(target_os = "macos")))]
fn extend_certificate_result(result: &mut CertificateResult, extra: CertificateResult) {
result.certs.extend(extra.certs);
result.errors.extend(extra.errors);
}
#[cfg(all(unix, not(target_os = "macos")))]
fn dedupe_certs(result: &mut CertificateResult) {
result.certs.sort_unstable_by(|a, b| a.cmp(b));
result.certs.dedup();
}
#[cfg(all(unix, not(target_os = "macos")))]
fn platform_cert_file() -> Option<std::path::PathBuf> {
PLATFORM_CERTIFICATE_FILE_NAMES
.iter()
.map(std::path::Path::new)
.find(|path| path.exists())
.map(std::path::Path::to_path_buf)
}
#[cfg(all(unix, not(target_os = "macos")))]
fn platform_cert_dirs() -> impl Iterator<Item = std::path::PathBuf> {
PLATFORM_CERTIFICATE_DIRS
.iter()
.map(std::path::Path::new)
.filter(|path| path.exists())
.map(std::path::Path::to_path_buf)
}
#[cfg(all(unix, not(target_os = "macos"), target_os = "linux"))]
const PLATFORM_CERTIFICATE_DIRS: &[&str] = &[
"/etc/ssl/certs",
"/etc/pki/tls/certs",
"/etc/security/certificates",
];
#[cfg(all(unix, not(target_os = "macos"), target_os = "freebsd"))]
const PLATFORM_CERTIFICATE_DIRS: &[&str] = &["/etc/ssl/certs", "/usr/local/share/certs"];
#[cfg(all(
unix,
not(target_os = "macos"),
any(target_os = "illumos", target_os = "solaris")
))]
const PLATFORM_CERTIFICATE_DIRS: &[&str] = &["/etc/certs/CA"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "netbsd"))]
const PLATFORM_CERTIFICATE_DIRS: &[&str] = &["/etc/openssl/certs"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "aix"))]
const PLATFORM_CERTIFICATE_DIRS: &[&str] = &["/var/ssl/certs"];
#[cfg(all(
unix,
not(target_os = "macos"),
not(any(
target_os = "linux",
target_os = "freebsd",
target_os = "illumos",
target_os = "solaris",
target_os = "netbsd",
target_os = "aix"
))
))]
const PLATFORM_CERTIFICATE_DIRS: &[&str] = &["/etc/ssl/certs"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "linux"))]
const PLATFORM_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 PLATFORM_CERTIFICATE_FILE_NAMES: &[&str] = &["/usr/local/etc/ssl/cert.pem"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "dragonfly"))]
const PLATFORM_CERTIFICATE_FILE_NAMES: &[&str] = &["/usr/local/share/certs/ca-root-nss.crt"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "netbsd"))]
const PLATFORM_CERTIFICATE_FILE_NAMES: &[&str] = &["/etc/openssl/certs/ca-certificates.crt"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "openbsd"))]
const PLATFORM_CERTIFICATE_FILE_NAMES: &[&str] = &["/etc/ssl/cert.pem"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "solaris"))]
const PLATFORM_CERTIFICATE_FILE_NAMES: &[&str] = &["/etc/certs/ca-certificates.crt"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "illumos"))]
const PLATFORM_CERTIFICATE_FILE_NAMES: &[&str] =
&["/etc/ssl/cacert.pem", "/etc/certs/ca-certificates.crt"];
#[cfg(all(unix, not(target_os = "macos"), target_os = "android"))]
const PLATFORM_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 PLATFORM_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 PLATFORM_CERTIFICATE_FILE_NAMES: &[&str] = &["/etc/ssl/certs/ca-certificates.crt"];
#[cfg(windows)]
fn usable_for_rustls(uses: schannel::cert_context::ValidUses) -> bool {
match uses {
schannel::cert_context::ValidUses::All => true,
schannel::cert_context::ValidUses::Oids(strs) => strs.iter().any(|x| x == PKIX_SERVER_AUTH),
}
}
#[cfg(windows)]
const PKIX_SERVER_AUTH: &str = "1.3.6.1.5.5.7.3.1";
+111 -13
View File
@@ -13,6 +13,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::net::TcpListener as StdTcpListener;
use std::path::Path;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::RwLock;
@@ -308,6 +309,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)?)
@@ -376,6 +378,8 @@ 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";
@@ -383,6 +387,8 @@ const NODE_USE_ENV_PROXY_ENV_KEY: &str = "NODE_USE_ENV_PROXY";
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,
@@ -571,27 +577,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()
@@ -643,7 +677,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,
@@ -654,6 +688,43 @@ impl NetworkProxy {
);
}
/// Rewrites readable child-selected CA bundles into immutable managed MITM bundles.
pub fn prepare_child_env<F>(
&self,
env: &mut HashMap<String, String>,
cwd: &Path,
can_read_path: F,
) -> Vec<AbsolutePathBuf>
where
F: Fn(&Path) -> bool,
{
let runtime_settings = self.runtime_settings();
apply_proxy_env_overrides(
env,
self.http_addr,
self.socks_addr,
self.socks_enabled,
runtime_settings.allow_local_binding,
runtime_settings.mitm_ca_trust_bundle.as_ref(),
);
let startup_ca_env_keys_present_in_child = ca_env_keys()
.filter(|&key| is_tracked_startup_ca_env_key(env, key))
.collect::<Vec<_>>();
env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY);
runtime_settings.mitm_ca_trust_bundle.as_ref().map_or_else(
Vec::new,
|mitm_ca_trust_bundle| {
crate::child_ca::prepare_mitm_ca_trust_bundle_env(
mitm_ca_trust_bundle,
env,
cwd,
&startup_ca_env_keys_present_in_child,
can_read_path,
)
},
)
}
pub async fn replace_config_state(&self, new_state: ConfigState) -> Result<()> {
let current_cfg = self.state.current_cfg().await?;
anyhow::ensure!(
@@ -1110,6 +1181,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,
@@ -1129,18 +1201,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),
@@ -1150,11 +1222,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]