Materialize child MITM CA bundles

This commit is contained in:
Winston Howes
2026-06-03 22:19:17 -07:00
parent 130450a9fa
commit f39f0f4300
6 changed files with 787 additions and 11 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -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]]

View File

@@ -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"] }

View File

@@ -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() {

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;

View 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());
}

View File

@@ -1,6 +1,7 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]
mod certs;
mod child_ca;
mod config;
mod connect_policy;
mod http_proxy;