mirror of
https://github.com/openai/codex.git
synced 2026-09-10 20:26:47 +00:00
Materialize child MITM CA bundles
This commit is contained in:
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -3363,6 +3363,7 @@ dependencies = [
|
||||
"codex-utils-home-dir",
|
||||
"codex-utils-rustls-provider",
|
||||
"globset",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"rama-core",
|
||||
"rama-http",
|
||||
@@ -3384,6 +3385,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -44,6 +44,7 @@ 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]
|
||||
@@ -51,3 +52,4 @@ security-framework = "3"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
schannel = "0.1"
|
||||
windows-sys = { version = "0.52", features = ["Win32_Storage_FileSystem"] }
|
||||
|
||||
@@ -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,6 +183,7 @@ fn managed_ca_trust_bundle_for_cert_path(
|
||||
Ok(ManagedMitmCaTrustBundle {
|
||||
path,
|
||||
startup_env_values,
|
||||
startup_cwd,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
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,36 @@ 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()),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -541,6 +872,50 @@ mod tests {
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn validate_existing_ca_key_file_rejects_group_world_permissions() {
|
||||
|
||||
187
codex-rs/network-proxy/src/child_ca.rs
Normal file
187
codex-rs/network-proxy/src/child_ca.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
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()
|
||||
{
|
||||
crate::certs::append_pem_contents(&mut custom_ca_bundle, ssl_cert_dir_contents);
|
||||
}
|
||||
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() => {
|
||||
crate::certs::append_pem_contents(&mut trust_bundle, &contents);
|
||||
}
|
||||
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;
|
||||
186
codex-rs/network-proxy/src/child_ca_tests.rs
Normal file
186
codex-rs/network-proxy/src/child_ca_tests.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
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()
|
||||
}
|
||||
|
||||
#[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 ssl_cert_dir_paths = [dir.path().join("certs-a"), dir.path().join("certs-b")];
|
||||
for (path, contents) in ssl_cert_dir_paths.iter().zip(["dir ca a\n", "dir ca b\n"]) {
|
||||
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 mut env = 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.to_string_lossy().into_owned(),
|
||||
),
|
||||
]);
|
||||
let mitm_ca_trust_bundle = ManagedMitmCaTrustBundle {
|
||||
path: mitm_ca_trust_bundle_path,
|
||||
startup_env_values: HashMap::from([(
|
||||
crate::certs::SSL_CERT_DIR_ENV_KEY,
|
||||
ssl_cert_dir.to_string_lossy().into_owned(),
|
||||
)]),
|
||||
startup_cwd: dir.path().to_path_buf(),
|
||||
};
|
||||
|
||||
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 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());
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
mod certs;
|
||||
mod child_ca;
|
||||
mod config;
|
||||
mod connect_policy;
|
||||
mod http_proxy;
|
||||
@@ -18,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;
|
||||
@@ -46,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;
|
||||
|
||||
@@ -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,7 @@ 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";
|
||||
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 +386,7 @@ 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,
|
||||
ALLOW_LOCAL_BINDING_ENV_KEY,
|
||||
ELECTRON_GET_USE_PROXY_ENV_KEY,
|
||||
NODE_USE_ENV_PROXY_ENV_KEY,
|
||||
@@ -571,24 +575,23 @@ 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();
|
||||
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)
|
||||
})
|
||||
.is_some_and(|value| value != &managed_path)
|
||||
{
|
||||
// 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.
|
||||
// Child-scoped overrides, including inherited startup values, need the
|
||||
// effective filesystem policy before we can combine them with the managed CA
|
||||
// bundle, so leave them for prepare_child_env().
|
||||
continue;
|
||||
}
|
||||
env.insert(key.to_string(), managed_path.clone());
|
||||
}
|
||||
} else {
|
||||
env.remove(MITM_CA_ENV_ACTIVE_ENV_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,17 +632,6 @@ impl NetworkProxy {
|
||||
self.runtime_settings().dangerously_allow_all_unix_sockets
|
||||
}
|
||||
|
||||
/// Returns the generated MITM CA bundle path child sandboxes should expose to TLS clients.
|
||||
pub fn managed_mitm_ca_trust_bundle_path(&self) -> Option<AbsolutePathBuf> {
|
||||
self.runtime_settings()
|
||||
.mitm_ca_trust_bundle
|
||||
.and_then(|bundle| {
|
||||
AbsolutePathBuf::from_absolute_path(bundle.path)
|
||||
.map_err(|err| warn!("managed MITM CA trust bundle path is invalid: {err}"))
|
||||
.ok()
|
||||
})
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -654,6 +646,51 @@ 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();
|
||||
let startup_ca_env_keys_present_in_child = runtime_settings
|
||||
.mitm_ca_trust_bundle
|
||||
.as_ref()
|
||||
.map_or_else(Vec::new, |mitm_ca_trust_bundle| {
|
||||
mitm_ca_trust_bundle
|
||||
.startup_env_values
|
||||
.iter()
|
||||
.filter_map(|(&key, startup_value)| {
|
||||
(env.get(key) == Some(startup_value)).then_some(key)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
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(),
|
||||
);
|
||||
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 +1147,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 +1167,21 @@ 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_preserves_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.clone(),
|
||||
)]),
|
||||
startup_cwd: Path::new("/tmp").to_path_buf(),
|
||||
};
|
||||
|
||||
apply_proxy_env_overrides(
|
||||
&mut env,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
|
||||
@@ -1150,7 +1191,7 @@ 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(&startup_ca_bundle_path));
|
||||
assert_eq!(
|
||||
env.get("SSL_CERT_FILE"),
|
||||
Some(&mitm_ca_trust_bundle_path.display().to_string())
|
||||
|
||||
Reference in New Issue
Block a user