mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +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_Foundation", "Win32_Storage_FileSystem"] }
|
||||
|
||||
@@ -23,11 +23,25 @@ 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::io::AsRawHandle;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
@@ -101,6 +115,8 @@ 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 MAX_CUSTOM_CA_DIR_ENTRIES: usize = 256;
|
||||
const SSL_CERT_FILE_ENV_KEY: &str = "SSL_CERT_FILE";
|
||||
pub const SSL_CERT_DIR_ENV_KEY: &str = "SSL_CERT_DIR";
|
||||
|
||||
@@ -191,18 +207,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;
|
||||
};
|
||||
@@ -227,6 +248,133 @@ 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, &file, &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_index, entry) in fs::read_dir(&dir)
|
||||
.with_context(|| format!("failed to read CA directory {}", dir.display()))?
|
||||
.enumerate()
|
||||
{
|
||||
anyhow::ensure!(
|
||||
entry_index < MAX_CUSTOM_CA_DIR_ENTRIES,
|
||||
"CA directory exceeds {MAX_CUSTOM_CA_DIR_ENTRIES} entries"
|
||||
);
|
||||
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());
|
||||
@@ -247,19 +395,205 @@ 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,
|
||||
file: &File,
|
||||
metadata: &fs::Metadata,
|
||||
) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let _ = file;
|
||||
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 _ = metadata;
|
||||
let opened_path_file = File::open(opened_path).with_context(|| {
|
||||
format!(
|
||||
"failed to reopen opened CA bundle {}",
|
||||
opened_path.display()
|
||||
)
|
||||
})?;
|
||||
anyhow::ensure!(
|
||||
windows_file_identity(&opened_path_file)? == windows_file_identity(file)?,
|
||||
"CA bundle {} changed before it could be validated",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = path;
|
||||
let _ = opened_path;
|
||||
let _ = file;
|
||||
let _ = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_file_identity(file: &File) -> Result<(u32, u64)> {
|
||||
use windows_sys::Win32::Storage::FileSystem::BY_HANDLE_FILE_INFORMATION;
|
||||
use windows_sys::Win32::Storage::FileSystem::GetFileInformationByHandle;
|
||||
|
||||
let mut file_information = BY_HANDLE_FILE_INFORMATION::default();
|
||||
// SAFETY: `file` owns a live OS handle and `file_information` is writable.
|
||||
let result =
|
||||
unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut file_information) };
|
||||
anyhow::ensure!(
|
||||
result != 0,
|
||||
"failed to inspect opened CA bundle: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
let file_index = u64::from(file_information.nFileIndexHigh) << 32
|
||||
| u64::from(file_information.nFileIndexLow);
|
||||
Ok((file_information.dwVolumeSerialNumber, file_index))
|
||||
}
|
||||
|
||||
fn push_certificate_pem(bundle: &mut String, der: &[u8]) {
|
||||
bundle.push_str("-----BEGIN CERTIFICATE-----\n");
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(der);
|
||||
@@ -514,6 +848,19 @@ 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();
|
||||
@@ -554,6 +901,66 @@ 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()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_custom_ca_dir_rejects_too_many_entries() {
|
||||
let dir = tempdir().unwrap();
|
||||
for entry_index in 0..=MAX_CUSTOM_CA_DIR_ENTRIES {
|
||||
fs::write(dir.path().join(format!("ca-{entry_index}.pem")), "ca\n").unwrap();
|
||||
}
|
||||
|
||||
let err = read_custom_ca_dir(dir.path(), |_| true).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains(&format!(
|
||||
"CA directory exceeds {MAX_CUSTOM_CA_DIR_ENTRIES} entries"
|
||||
)),
|
||||
"unexpected error: {err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn validate_existing_ca_key_file_rejects_group_world_permissions() {
|
||||
|
||||
203
codex-rs/network-proxy/src/child_ca.rs
Normal file
203
codex-rs/network-proxy/src/child_ca.rs
Normal 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;
|
||||
161
codex-rs/network-proxy/src/child_ca_tests.rs
Normal file
161
codex-rs/network-proxy/src/child_ca_tests.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
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 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());
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||
|
||||
mod certs;
|
||||
mod child_ca;
|
||||
mod config;
|
||||
mod connect_policy;
|
||||
mod http_proxy;
|
||||
|
||||
Reference in New Issue
Block a user