mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Keep managed MITM CA private keys in proxy memory (#29013)
## Why The managed MITM trust bundle must be readable by sandboxed commands. Persisting its sibling CA private key under `$CODEX_HOME/proxy` therefore requires a deny-read sandbox rule, but the Windows unelevated backend rejects deny-read paths and WSL1's legacy Landlock path cannot enforce that rule. A persistent OS credential store also does not provide the same cross-platform boundary from other processes running as the same user. Keeping the signer inside the network proxy process avoids both problems: ordinary sandbox setup stays independent of CA-key state, and no private signing key is exposed through the filesystem or a persistent credential record. ## What - generate one managed CA per proxy process and retain its private signer only in proxy memory - emit only content-addressed public CA certificates and trust bundles under `$CODEX_HOME/proxy` - hold a cross-process lease for each active public certificate and prune artifacts from inactive proxy processes - keep all CA ownership in `codex-network-proxy`; no `codex-core` or sandbox-policy changes - validate generated trust-bundle paths by their content hash - keep the public bundle readable by sandboxed commands on Windows, WSL1, macOS, and Linux The independent startup custom-CA follow-up is #29014. ## Validation - `CODEX_HOME=/private/tmp/codex-test-home-network-proxy just test -p codex-network-proxy` (179 tests) - `just bazel-lock-check` - `just fix -p codex-network-proxy` - `just fmt` --------- Co-authored-by: viyatb-oai <viyatb@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
354807920e
commit
c5a9a95ab6
@@ -34,9 +34,9 @@ allow_upstream_proxy = true
|
||||
dangerously_allow_non_loopback_proxy = false
|
||||
mode = "full" # default when unset; use "limited" for read-only mode
|
||||
# HTTPS MITM is enabled automatically when `mode = "limited"` or when MITM hooks are configured.
|
||||
# CA cert/key are managed internally under $CODEX_HOME/proxy/ (ca.pem + ca.key).
|
||||
# When MITM is active, spawned commands receive CA bundle env vars pointing at
|
||||
# immutable bundles under $CODEX_HOME/proxy/ so common HTTPS clients trust the managed CA.
|
||||
# The CA private key remains in proxy memory. When MITM is active, spawned commands receive CA
|
||||
# bundle env vars pointing at immutable public files under $CODEX_HOME/proxy/ so common HTTPS
|
||||
# clients trust the managed CA.
|
||||
|
||||
# If false, local/private networking is rejected. Explicit allowlisting of local IP literals
|
||||
# (or `localhost`) is required to permit them.
|
||||
|
||||
+341
-154
@@ -32,6 +32,8 @@ use std::net::IpAddr;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::LazyLock;
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tracing::info;
|
||||
@@ -39,15 +41,60 @@ use tracing::warn;
|
||||
|
||||
pub(super) struct ManagedMitmCa {
|
||||
issuer: Issuer<'static, KeyPair>,
|
||||
certificate_path: PathBuf,
|
||||
_artifact_lease: File,
|
||||
}
|
||||
|
||||
static MANAGED_MITM_CAS: LazyLock<Mutex<HashMap<PathBuf, Arc<ManagedMitmCa>>>> =
|
||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||
|
||||
impl ManagedMitmCa {
|
||||
pub(super) fn load_or_create() -> Result<Self> {
|
||||
let (ca_cert_pem, ca_key_pem) = load_or_create_ca()?;
|
||||
let ca_key = KeyPair::from_pem(&ca_key_pem).context("failed to parse CA key")?;
|
||||
let issuer: Issuer<'static, KeyPair> =
|
||||
Issuer::from_ca_cert_pem(&ca_cert_pem, ca_key).context("failed to parse CA cert")?;
|
||||
Ok(Self { issuer })
|
||||
pub(super) fn load_or_create() -> Result<Arc<Self>> {
|
||||
let proxy_dir = managed_ca_dir()?;
|
||||
let mut managed_cas = MANAGED_MITM_CAS
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if let Some(ca) = managed_cas.get(&proxy_dir) {
|
||||
return Ok(ca.clone());
|
||||
}
|
||||
|
||||
let ca = Arc::new(Self::create(&proxy_dir)?);
|
||||
managed_cas.insert(proxy_dir, ca.clone());
|
||||
Ok(ca)
|
||||
}
|
||||
|
||||
fn create(proxy_dir: &Path) -> Result<Self> {
|
||||
fs::create_dir_all(proxy_dir)
|
||||
.with_context(|| format!("failed to create {}", proxy_dir.display()))?;
|
||||
|
||||
let (certificate_pem, private_key) = generate_ca()?;
|
||||
let artifact_lock = match lock_managed_ca_artifacts(proxy_dir) {
|
||||
Ok(lock) => Some(lock),
|
||||
Err(err) => {
|
||||
warn!("failed to lock managed MITM CA artifacts; skipping pruning: {err}");
|
||||
None
|
||||
}
|
||||
};
|
||||
let certificate_path = persist_managed_ca_certificate(proxy_dir, &certificate_pem)?;
|
||||
let issuer = Issuer::from_ca_cert_pem(&certificate_pem, private_key)
|
||||
.context("failed to parse managed MITM CA certificate")?;
|
||||
let artifact_lease = lock_managed_ca_certificate(&certificate_path)?;
|
||||
if artifact_lock.is_some() {
|
||||
prune_managed_ca_artifacts(proxy_dir);
|
||||
}
|
||||
info!(
|
||||
cert_path = %certificate_path.display(),
|
||||
"generated process-local MITM CA"
|
||||
);
|
||||
Ok(Self {
|
||||
issuer,
|
||||
certificate_path,
|
||||
_artifact_lease: artifact_lease,
|
||||
})
|
||||
}
|
||||
|
||||
fn certificate_path(&self) -> &Path {
|
||||
&self.certificate_path
|
||||
}
|
||||
|
||||
pub(super) fn tls_acceptor_data_for_host(&self, host: &str) -> Result<TlsAcceptorData> {
|
||||
@@ -100,8 +147,8 @@ fn issue_host_certificate_pem(
|
||||
}
|
||||
|
||||
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_ARTIFACT_LOCK: &str = ".artifacts.lock";
|
||||
const MANAGED_MITM_CA_CERT_PREFIX: &str = "ca";
|
||||
const MANAGED_MITM_CA_TRUST_BUNDLE_PREFIX: &str = "ca-bundle";
|
||||
pub(crate) const SSL_CERT_DIR_ENV_KEY: &str = "SSL_CERT_DIR";
|
||||
|
||||
@@ -135,22 +182,17 @@ pub(crate) struct ManagedMitmCaTrustBundle {
|
||||
pub(crate) startup_env_values: HashMap<&'static str, String>,
|
||||
}
|
||||
|
||||
fn managed_ca_paths() -> Result<(PathBuf, PathBuf)> {
|
||||
fn managed_ca_dir() -> Result<PathBuf> {
|
||||
let codex_home =
|
||||
find_codex_home().context("failed to resolve CODEX_HOME for managed MITM CA")?;
|
||||
let proxy_dir = codex_home.join(MANAGED_MITM_CA_DIR);
|
||||
Ok((
|
||||
proxy_dir.join(MANAGED_MITM_CA_CERT).to_path_buf(),
|
||||
proxy_dir.join(MANAGED_MITM_CA_KEY).to_path_buf(),
|
||||
))
|
||||
Ok(codex_home.join(MANAGED_MITM_CA_DIR).to_path_buf())
|
||||
}
|
||||
|
||||
pub(crate) fn managed_ca_trust_bundle(
|
||||
env: &HashMap<&'static str, String>,
|
||||
) -> Result<ManagedMitmCaTrustBundle> {
|
||||
load_or_create_ca()?;
|
||||
let (cert_path, _) = managed_ca_paths()?;
|
||||
managed_ca_trust_bundle_for_cert_path(&cert_path, env)
|
||||
let ca = ManagedMitmCa::load_or_create()?;
|
||||
managed_ca_trust_bundle_for_cert_path(ca.certificate_path(), env)
|
||||
}
|
||||
|
||||
fn managed_ca_trust_bundle_for_cert_path(
|
||||
@@ -175,8 +217,8 @@ fn managed_ca_trust_bundle_for_cert_path(
|
||||
pub(crate) fn upstream_tls_root_store(
|
||||
env: &HashMap<&'static str, String>,
|
||||
) -> Result<Arc<rustls::RootCertStore>> {
|
||||
let (managed_ca_cert_path, _) = managed_ca_paths()?;
|
||||
upstream_tls_root_store_for_cert_path(&managed_ca_cert_path, env)
|
||||
let ca = ManagedMitmCa::load_or_create()?;
|
||||
upstream_tls_root_store_for_cert_path(ca.certificate_path(), env)
|
||||
}
|
||||
|
||||
pub(crate) fn upstream_tls_root_store_for_cert_path(
|
||||
@@ -360,6 +402,9 @@ 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;
|
||||
};
|
||||
if is_generated_trust_bundle_path(path, proxy_dir) {
|
||||
return true;
|
||||
}
|
||||
let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) else {
|
||||
return false;
|
||||
};
|
||||
@@ -381,12 +426,39 @@ fn is_current_generated_trust_bundle_path(path: &Path, managed_ca_cert_path: &Pa
|
||||
.any(|window| window == managed_ca_cert)
|
||||
}
|
||||
|
||||
/// Returns whether `path` points at a current Codex-generated MITM CA bundle.
|
||||
pub fn is_managed_mitm_ca_trust_bundle_path(path: &str) -> bool {
|
||||
let Ok((managed_ca_cert_path, _)) = managed_ca_paths() else {
|
||||
fn is_generated_trust_bundle_path(path: &Path, proxy_dir: &Path) -> bool {
|
||||
is_generated_managed_ca_artifact_path(path, proxy_dir, MANAGED_MITM_CA_TRUST_BUNDLE_PREFIX)
|
||||
}
|
||||
|
||||
fn is_generated_managed_ca_artifact_path(path: &Path, proxy_dir: &Path, prefix: &str) -> bool {
|
||||
let Some(file_name) = path.file_name().and_then(|file_name| file_name.to_str()) else {
|
||||
return false;
|
||||
};
|
||||
is_current_generated_trust_bundle_path(Path::new(path), &managed_ca_cert_path)
|
||||
let Some(expected_hash) = file_name
|
||||
.strip_prefix(prefix)
|
||||
.and_then(|suffix| suffix.strip_prefix('-'))
|
||||
.and_then(|suffix| suffix.strip_suffix(".pem"))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if path.parent() != Some(proxy_dir)
|
||||
|| expected_hash.len() != 64
|
||||
|| !expected_hash.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let Ok(trust_bundle) = fs::read(path) else {
|
||||
return false;
|
||||
};
|
||||
format!("{:x}", Sha256::digest(trust_bundle)) == expected_hash
|
||||
}
|
||||
|
||||
/// Returns whether `path` points at a current Codex-generated MITM CA bundle.
|
||||
pub fn is_managed_mitm_ca_trust_bundle_path(path: &str) -> bool {
|
||||
let Ok(proxy_dir) = managed_ca_dir() else {
|
||||
return false;
|
||||
};
|
||||
is_generated_trust_bundle_path(Path::new(path), &proxy_dir)
|
||||
}
|
||||
|
||||
fn persist_managed_ca_trust_bundle(
|
||||
@@ -439,56 +511,160 @@ fn push_certificate_pem(bundle: &mut String, der: &[u8]) {
|
||||
bundle.push_str("-----END CERTIFICATE-----\n");
|
||||
}
|
||||
|
||||
fn load_or_create_ca() -> Result<(String, String)> {
|
||||
let (cert_path, key_path) = managed_ca_paths()?;
|
||||
|
||||
if cert_path.exists() || key_path.exists() {
|
||||
if !cert_path.exists() || !key_path.exists() {
|
||||
return Err(anyhow!(
|
||||
"both managed MITM CA files must exist (cert={}, key={})",
|
||||
cert_path.display(),
|
||||
key_path.display()
|
||||
));
|
||||
}
|
||||
validate_existing_ca_key_file(&key_path)?;
|
||||
let cert_pem = fs::read_to_string(&cert_path)
|
||||
.with_context(|| format!("failed to read CA cert {}", cert_path.display()))?;
|
||||
let key_pem = fs::read_to_string(&key_path)
|
||||
.with_context(|| format!("failed to read CA key {}", key_path.display()))?;
|
||||
return Ok((cert_pem, key_pem));
|
||||
}
|
||||
|
||||
if let Some(parent) = cert_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
if let Some(parent) = key_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
|
||||
let (cert_pem, key_pem) = generate_ca()?;
|
||||
// The CA key is a high-value secret. Create it atomically with restrictive permissions.
|
||||
// The cert can be world-readable, but we still write it atomically to avoid partial writes.
|
||||
//
|
||||
// We intentionally use create-new semantics: if a key already exists, we should not overwrite
|
||||
// it silently (that would invalidate previously-trusted cert chains).
|
||||
write_atomic_create_new(&key_path, key_pem.as_bytes(), /*mode*/ 0o600)
|
||||
.with_context(|| format!("failed to persist CA key {}", key_path.display()))?;
|
||||
if let Err(err) = write_atomic_create_new(&cert_path, cert_pem.as_bytes(), /*mode*/ 0o644)
|
||||
.with_context(|| format!("failed to persist CA cert {}", cert_path.display()))
|
||||
{
|
||||
// Avoid leaving a partially-created CA around (cert missing) if the second write fails.
|
||||
let _ = fs::remove_file(&key_path);
|
||||
return Err(err);
|
||||
}
|
||||
let cert_path = cert_path.display();
|
||||
let key_path = key_path.display();
|
||||
info!("generated MITM CA (cert_path={cert_path}, key_path={key_path})");
|
||||
Ok((cert_pem, key_pem))
|
||||
fn persist_managed_ca_certificate(proxy_dir: &Path, cert_pem: &str) -> Result<PathBuf> {
|
||||
let hash = Sha256::digest(cert_pem.as_bytes());
|
||||
let cert_path = proxy_dir.join(format!("{MANAGED_MITM_CA_CERT_PREFIX}-{hash:x}.pem"));
|
||||
write_atomic_create_new_or_reuse(&cert_path, cert_pem.as_bytes(), /*mode*/ 0o644)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to persist managed MITM CA certificate {}",
|
||||
cert_path.display()
|
||||
)
|
||||
})?;
|
||||
Ok(cert_path)
|
||||
}
|
||||
|
||||
fn generate_ca() -> Result<(String, String)> {
|
||||
fn lock_managed_ca_certificate(certificate_path: &Path) -> Result<File> {
|
||||
let lock_path = managed_ca_certificate_lock_path(certificate_path)
|
||||
.ok_or_else(|| anyhow!("managed MITM CA certificate path is missing a file name"))?;
|
||||
let file = open_managed_ca_lock(&lock_path)?;
|
||||
file.lock_shared()
|
||||
.with_context(|| format!("failed to lock {}", lock_path.display()))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn lock_managed_ca_artifacts(proxy_dir: &Path) -> Result<File> {
|
||||
let lock_path = proxy_dir.join(MANAGED_MITM_CA_ARTIFACT_LOCK);
|
||||
let file = open_managed_ca_lock(&lock_path)?;
|
||||
file.lock()
|
||||
.with_context(|| format!("failed to lock {}", lock_path.display()))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn managed_ca_certificate_lock_path(certificate_path: &Path) -> Option<PathBuf> {
|
||||
let file_name = certificate_path.file_name()?.to_string_lossy();
|
||||
Some(certificate_path.with_file_name(format!(".{file_name}.lock")))
|
||||
}
|
||||
|
||||
fn open_managed_ca_lock(path: &Path) -> Result<File> {
|
||||
if fs::symlink_metadata(path)
|
||||
.ok()
|
||||
.is_some_and(|metadata| metadata.file_type().is_symlink())
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"refusing to use symlink lock file {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
let mut options = OpenOptions::new();
|
||||
options.read(true).write(true).create(true).truncate(false);
|
||||
#[cfg(unix)]
|
||||
options.mode(0o600);
|
||||
options
|
||||
.open(path)
|
||||
.with_context(|| format!("failed to open {}", path.display()))
|
||||
}
|
||||
|
||||
fn prune_managed_ca_artifacts(proxy_dir: &Path) {
|
||||
for certificate_path in
|
||||
generated_managed_ca_artifact_paths(proxy_dir, MANAGED_MITM_CA_CERT_PREFIX)
|
||||
{
|
||||
remove_inactive_managed_ca_certificate(&certificate_path);
|
||||
}
|
||||
|
||||
let remaining_certificates =
|
||||
generated_managed_ca_artifact_paths(proxy_dir, MANAGED_MITM_CA_CERT_PREFIX)
|
||||
.into_iter()
|
||||
.filter_map(|path| fs::read(path).ok())
|
||||
.filter(|certificate| !certificate.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
let bundle_paths =
|
||||
generated_managed_ca_artifact_paths(proxy_dir, MANAGED_MITM_CA_TRUST_BUNDLE_PREFIX);
|
||||
for bundle_path in bundle_paths {
|
||||
let Ok(contents) = fs::read(&bundle_path) else {
|
||||
continue;
|
||||
};
|
||||
if remaining_certificates.iter().any(|certificate| {
|
||||
contents
|
||||
.windows(certificate.len())
|
||||
.any(|window| window == certificate)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
if let Err(err) = fs::remove_file(&bundle_path)
|
||||
&& err.kind() != std::io::ErrorKind::NotFound
|
||||
{
|
||||
warn!(
|
||||
path = %bundle_path.display(),
|
||||
"failed to prune stale managed MITM CA trust bundle: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generated_managed_ca_artifact_paths(proxy_dir: &Path, prefix: &str) -> Vec<PathBuf> {
|
||||
let Ok(entries) = fs::read_dir(proxy_dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
entries
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
if !is_generated_managed_ca_artifact_path(&path, proxy_dir, prefix) {
|
||||
return None;
|
||||
}
|
||||
Some(path)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn remove_inactive_managed_ca_certificate(certificate_path: &Path) {
|
||||
let Some(lock_path) = managed_ca_certificate_lock_path(certificate_path) else {
|
||||
return;
|
||||
};
|
||||
let Ok(lock_file) = open_managed_ca_lock(&lock_path) else {
|
||||
return;
|
||||
};
|
||||
match lock_file.try_lock() {
|
||||
Ok(()) => {}
|
||||
Err(std::fs::TryLockError::WouldBlock) => return,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
path = %lock_path.display(),
|
||||
"failed to inspect managed MITM CA artifact lease: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let removed = match fs::remove_file(certificate_path) {
|
||||
Ok(()) => true,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => true,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
path = %certificate_path.display(),
|
||||
"failed to prune stale managed MITM CA certificate: {err}"
|
||||
);
|
||||
false
|
||||
}
|
||||
};
|
||||
drop(lock_file);
|
||||
if removed
|
||||
&& let Err(err) = fs::remove_file(&lock_path)
|
||||
&& err.kind() != std::io::ErrorKind::NotFound
|
||||
{
|
||||
warn!(
|
||||
path = %lock_path.display(),
|
||||
"failed to prune stale managed MITM CA artifact lease: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_ca() -> Result<(String, KeyPair)> {
|
||||
let mut params = CertificateParams::default();
|
||||
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||
params.key_usages = vec![
|
||||
@@ -505,7 +681,7 @@ fn generate_ca() -> Result<(String, String)> {
|
||||
let cert = params
|
||||
.self_signed(&key_pair)
|
||||
.map_err(|err| anyhow!("failed to generate CA cert: {err}"))?;
|
||||
Ok((cert.pem(), key_pair.serialize_pem()))
|
||||
Ok((cert.pem(), key_pair))
|
||||
}
|
||||
|
||||
fn write_atomic_create_new(path: &Path, contents: &[u8], mode: u32) -> Result<()> {
|
||||
@@ -604,41 +780,6 @@ fn write_atomic_create_new_or_reuse(path: &Path, contents: &[u8], mode: u32) ->
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn validate_existing_ca_key_file(path: &Path) -> Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let metadata = fs::symlink_metadata(path)
|
||||
.with_context(|| format!("failed to stat CA key {}", path.display()))?;
|
||||
if metadata.file_type().is_symlink() {
|
||||
return Err(anyhow!(
|
||||
"refusing to use symlink for managed MITM CA key {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
if !metadata.is_file() {
|
||||
return Err(anyhow!(
|
||||
"managed MITM CA key is not a regular file: {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let mode = metadata.permissions().mode() & 0o777;
|
||||
if mode & 0o077 != 0 {
|
||||
return Err(anyhow!(
|
||||
"managed MITM CA key {} must not be group/world accessible (mode={mode:o}; expected <= 600)",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn validate_existing_ca_key_file(_path: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn open_create_new_with_mode(path: &Path, mode: u32) -> Result<File> {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
@@ -664,12 +805,83 @@ fn open_create_new_with_mode(path: &Path, _mode: u32) -> Result<File> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn managed_ca_private_key_is_not_persisted() {
|
||||
ensure_rustls_crypto_provider();
|
||||
let dir = tempdir().unwrap();
|
||||
let ca = ManagedMitmCa::create(dir.path()).unwrap();
|
||||
ca.tls_acceptor_data_for_host("example.com").unwrap();
|
||||
let mut persisted_files = fs::read_dir(dir.path())
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.collect::<Vec<_>>();
|
||||
persisted_files.sort();
|
||||
let mut expected_files = vec![
|
||||
ca.certificate_path().to_path_buf(),
|
||||
managed_ca_certificate_lock_path(ca.certificate_path()).unwrap(),
|
||||
dir.path().join(MANAGED_MITM_CA_ARTIFACT_LOCK),
|
||||
];
|
||||
expected_files.sort();
|
||||
|
||||
assert_eq!(persisted_files, expected_files);
|
||||
assert_eq!(
|
||||
fs::read(managed_ca_certificate_lock_path(ca.certificate_path()).unwrap()).unwrap(),
|
||||
Vec::<u8>::new()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_ca_artifact_pruning_preserves_only_active_certificates() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut artifacts = Vec::new();
|
||||
let mut active_lease = None;
|
||||
for index in 0..3 {
|
||||
let certificate = format!("certificate {index}\n");
|
||||
let certificate_path =
|
||||
persist_managed_ca_certificate(dir.path(), &certificate).unwrap();
|
||||
let lease = lock_managed_ca_certificate(&certificate_path).unwrap();
|
||||
if index == 0 {
|
||||
active_lease = Some(lease);
|
||||
} else {
|
||||
drop(lease);
|
||||
}
|
||||
let bundle_path = persist_managed_ca_trust_bundle(
|
||||
&certificate_path,
|
||||
&format!("roots\n{certificate}"),
|
||||
)
|
||||
.unwrap();
|
||||
artifacts.push((certificate_path, bundle_path));
|
||||
}
|
||||
let unrelated_path = dir.path().join("ca-user.pem");
|
||||
fs::write(&unrelated_path, "user managed").unwrap();
|
||||
|
||||
prune_managed_ca_artifacts(dir.path());
|
||||
|
||||
let remaining_certificate_count =
|
||||
generated_managed_ca_artifact_paths(dir.path(), MANAGED_MITM_CA_CERT_PREFIX).len();
|
||||
assert_eq!(remaining_certificate_count, 1);
|
||||
assert!(artifacts[0].0.exists());
|
||||
assert!(artifacts[0].1.exists());
|
||||
assert!(!artifacts[1].0.exists());
|
||||
assert!(!artifacts[1].1.exists());
|
||||
assert!(!artifacts[2].0.exists());
|
||||
assert!(!artifacts[2].1.exists());
|
||||
assert!(unrelated_path.exists());
|
||||
|
||||
drop(active_lease.take());
|
||||
prune_managed_ca_artifacts(dir.path());
|
||||
|
||||
let remaining_certificates =
|
||||
generated_managed_ca_artifact_paths(dir.path(), MANAGED_MITM_CA_CERT_PREFIX);
|
||||
assert!(remaining_certificates.is_empty());
|
||||
assert!(!artifacts[0].0.exists());
|
||||
assert!(!artifacts[0].1.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_generated_trust_bundle_path_rejects_stale_bundle() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -683,6 +895,24 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_trust_bundle_path_requires_matching_content_hash() {
|
||||
let dir = tempdir().unwrap();
|
||||
let managed_ca_cert_path = dir.path().join("ca.pem");
|
||||
let trust_bundle_path =
|
||||
persist_managed_ca_trust_bundle(&managed_ca_cert_path, "trusted roots").unwrap();
|
||||
|
||||
assert!(is_generated_trust_bundle_path(
|
||||
&trust_bundle_path,
|
||||
dir.path()
|
||||
));
|
||||
fs::write(&trust_bundle_path, "tampered roots").unwrap();
|
||||
assert!(!is_generated_trust_bundle_path(
|
||||
&trust_bundle_path,
|
||||
dir.path()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_ca_trust_bundle_appends_startup_file_and_directory_certificates() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -691,6 +921,7 @@ mod tests {
|
||||
let startup_ca_dir = dir.path().join("startup-certs");
|
||||
let (managed_ca_cert, _) = generate_ca().unwrap();
|
||||
let (startup_ca_cert, startup_ca_key) = generate_ca().unwrap();
|
||||
let startup_ca_key = startup_ca_key.serialize_pem();
|
||||
let (directory_ca_cert, _) = generate_ca().unwrap();
|
||||
let mut trusted_ca_der = CertificateDer::from_pem_slice(startup_ca_cert.as_bytes())
|
||||
.unwrap()
|
||||
@@ -760,50 +991,6 @@ mod tests {
|
||||
assert_eq!(baseline_bundle.matches(&managed_ca_cert).count(), 1);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn validate_existing_ca_key_file_rejects_group_world_permissions() {
|
||||
let dir = tempdir().unwrap();
|
||||
let key_path = dir.path().join("ca.key");
|
||||
fs::write(&key_path, "key").unwrap();
|
||||
fs::set_permissions(&key_path, fs::Permissions::from_mode(0o644)).unwrap();
|
||||
|
||||
let err = validate_existing_ca_key_file(&key_path).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("group/world accessible"),
|
||||
"unexpected error: {err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn validate_existing_ca_key_file_rejects_symlink() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let target = dir.path().join("real.key");
|
||||
let link = dir.path().join("ca.key");
|
||||
fs::write(&target, "key").unwrap();
|
||||
symlink(&target, &link).unwrap();
|
||||
|
||||
let err = validate_existing_ca_key_file(&link).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("symlink"),
|
||||
"unexpected error: {err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn validate_existing_ca_key_file_allows_private_permissions() {
|
||||
let dir = tempdir().unwrap();
|
||||
let key_path = dir.path().join("ca.key");
|
||||
fs::write(&key_path, "key").unwrap();
|
||||
fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600)).unwrap();
|
||||
|
||||
validate_existing_ca_key_file(&key_path).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn write_atomic_create_new_or_reuse_rejects_matching_symlink_target() {
|
||||
|
||||
@@ -53,7 +53,7 @@ use tracing::warn;
|
||||
|
||||
/// State needed to terminate a CONNECT tunnel and enforce policy on inner HTTPS requests.
|
||||
pub struct MitmState {
|
||||
ca: ManagedMitmCa,
|
||||
ca: Arc<ManagedMitmCa>,
|
||||
upstream: UpstreamClient,
|
||||
inspect: bool,
|
||||
max_body_bytes: usize,
|
||||
@@ -104,7 +104,7 @@ impl MitmState {
|
||||
|
||||
// MITM exists when HTTPS policy depends on the inner request: limited-mode method clamps
|
||||
// and host-specific hooks both need visibility after CONNECT is established. We
|
||||
// generate/load a local CA and issue per-host leaf certs so we can terminate TLS and
|
||||
// generate a process-local CA and issue per-host leaf certs so we can terminate TLS and
|
||||
// apply policy.
|
||||
let ca = ManagedMitmCa::load_or_create()?;
|
||||
let upstream_tls_root_store =
|
||||
|
||||
Reference in New Issue
Block a user