use crate::error::{ProxyError, Result};
use std::path::{Path, PathBuf};
use tracing::{debug, warn};
pub struct BundleInputs<'a> {
pub dir: &'a Path,
pub filename: &'a str,
pub parent_ssl_cert_file: Option<&'a [u8]>,
pub ephemeral_ca_pem: &'a str,
}
pub fn write_bundle(inputs: BundleInputs<'_>) -> Result<PathBuf> {
let mut pem = String::new();
if let Some(parent_pem) = inputs.parent_ssl_cert_file {
match std::str::from_utf8(parent_pem) {
Ok(s) => {
debug!(
"tls_intercept: merging parent SSL_CERT_FILE contents \
({} bytes) into trust bundle",
s.len()
);
pem.push_str(s);
if !pem.ends_with('\n') {
pem.push('\n');
}
}
Err(_) => {
warn!(
"tls_intercept: parent SSL_CERT_FILE contents are not valid UTF-8; \
skipping merge — corporate CAs configured on the host may not be \
trusted by the sandboxed child"
);
}
}
}
let system_certs = rustls_native_certs::load_native_certs();
if !system_certs.errors.is_empty() {
debug!(
"tls_intercept: rustls-native-certs reported {} non-fatal errors while \
loading system trust store",
system_certs.errors.len()
);
}
if system_certs.certs.is_empty() && inputs.parent_ssl_cert_file.is_none() {
return Err(ProxyError::Config(
"tls_intercept: failed to load any system trust roots; \
refusing to write a bundle that would strip the agent's TLS trust"
.to_string(),
));
}
debug!(
"tls_intercept: appending {} certs from the system trust store to bundle",
system_certs.certs.len()
);
for cert in system_certs.certs {
pem.push_str("-----BEGIN CERTIFICATE-----\n");
pem.push_str(&base64_chunked(cert.as_ref()));
pem.push_str("-----END CERTIFICATE-----\n");
}
if !inputs.ephemeral_ca_pem.contains("BEGIN CERTIFICATE") {
return Err(ProxyError::Config(
"tls_intercept: ephemeral CA PEM is not in the expected format".to_string(),
));
}
pem.push_str(inputs.ephemeral_ca_pem);
if !pem.ends_with('\n') {
pem.push('\n');
}
let path = inputs.dir.join(inputs.filename);
write_with_restrictive_perms(&path, pem.as_bytes())?;
debug!(
"tls_intercept: wrote trust bundle ({} bytes) to {}",
pem.len(),
path.display()
);
Ok(path)
}
fn write_with_restrictive_perms(path: &Path, contents: &[u8]) -> Result<()> {
use std::io::Write;
if path.exists() {
std::fs::remove_file(path).map_err(|e| {
ProxyError::Config(format!(
"tls_intercept: cannot remove stale bundle '{}': {}",
path.display(),
e
))
})?;
}
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o400)
.open(path)
.map_err(|e| {
ProxyError::Config(format!(
"tls_intercept: cannot create bundle '{}': {}",
path.display(),
e
))
})?;
file.write_all(contents).map_err(|e| {
ProxyError::Config(format!(
"tls_intercept: cannot write bundle '{}': {}",
path.display(),
e
))
})?;
file.flush().ok();
}
#[cfg(not(unix))]
{
std::fs::write(path, contents).map_err(|e| {
ProxyError::Config(format!(
"tls_intercept: cannot write bundle '{}': {}",
path.display(),
e
))
})?;
}
Ok(())
}
fn base64_chunked(bytes: &[u8]) -> String {
use base64::engine::{general_purpose::STANDARD, Engine};
let encoded = STANDARD.encode(bytes);
let mut out = String::with_capacity(encoded.len() + encoded.len() / 64 + 1);
for chunk in encoded.as_bytes().chunks(64) {
out.push_str(std::str::from_utf8(chunk).unwrap_or(""));
out.push('\n');
}
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::tls_intercept::ca::EphemeralCa;
#[test]
fn bundle_contains_ephemeral_and_system_roots() {
let dir = tempfile::tempdir().unwrap();
let ca = EphemeralCa::generate().unwrap();
let path = write_bundle(BundleInputs {
dir: dir.path(),
filename: "intercept-ca.pem",
parent_ssl_cert_file: None,
ephemeral_ca_pem: ca.cert_pem(),
})
.unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
let cert_count = contents.matches("BEGIN CERTIFICATE").count();
assert!(
cert_count >= 2,
"bundle should contain at least one system root + the ephemeral CA, got {}",
cert_count
);
assert!(
contents.contains(ca.cert_pem().trim()),
"ephemeral CA PEM must appear verbatim in bundle"
);
}
#[test]
fn bundle_merges_parent_file() {
let dir = tempfile::tempdir().unwrap();
let ca = EphemeralCa::generate().unwrap();
let parent = b"# corporate roots\n-----BEGIN CERTIFICATE-----\nMIIBcorpfake\n-----END CERTIFICATE-----\n";
let path = write_bundle(BundleInputs {
dir: dir.path(),
filename: "intercept-ca.pem",
parent_ssl_cert_file: Some(parent),
ephemeral_ca_pem: ca.cert_pem(),
})
.unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.contains("MIIBcorpfake"));
}
#[test]
fn bundle_rejects_malformed_ephemeral_pem() {
let dir = tempfile::tempdir().unwrap();
let result = write_bundle(BundleInputs {
dir: dir.path(),
filename: "intercept-ca.pem",
parent_ssl_cert_file: None,
ephemeral_ca_pem: "not a certificate",
});
assert!(result.is_err());
}
#[test]
#[cfg(unix)]
fn bundle_file_has_restrictive_permissions() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let ca = EphemeralCa::generate().unwrap();
let path = write_bundle(BundleInputs {
dir: dir.path(),
filename: "intercept-ca.pem",
parent_ssl_cert_file: None,
ephemeral_ca_pem: ca.cert_pem(),
})
.unwrap();
let metadata = std::fs::metadata(&path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(mode, 0o400, "bundle must be owner-read-only");
}
}