use std::env;
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
use rustls::ClientConfig;
use rustls::RootCertStore;
use rustls_pki_types::CertificateDer;
use rustls_pki_types::pem::PemObject;
use rustls_pki_types::pem::SectionKind;
use rustls_pki_types::pem::{self};
use thiserror::Error;
use tracing::info;
use tracing::warn;
pub const CODEX_CA_CERT_ENV: &str = "CODEX_CA_CERTIFICATE";
pub const SSL_CERT_FILE_ENV: &str = "SSL_CERT_FILE";
const CA_CERT_HINT: &str = "If you set CODEX_CA_CERTIFICATE or SSL_CERT_FILE, ensure it points to a PEM file containing one or more CERTIFICATE blocks, or unset it to use system roots.";
type PemSection = (SectionKind, Vec<u8>);
#[derive(Debug, Error)]
pub enum BuildCustomCaTransportError {
#[error(
"Failed to read CA certificate file {} selected by {}: {source}. {hint}",
path.display(),
source_env,
hint = CA_CERT_HINT
)]
ReadCaFile {
source_env: &'static str,
path: PathBuf,
source: io::Error,
},
#[error(
"Failed to load CA certificates from {} selected by {}: {detail}. {hint}",
path.display(),
source_env,
hint = CA_CERT_HINT
)]
InvalidCaFile {
source_env: &'static str,
path: PathBuf,
detail: String,
},
#[error(
"Failed to parse certificate #{certificate_index} from {} selected by {}: {source}. {hint}",
path.display(),
source_env,
hint = CA_CERT_HINT
)]
RegisterCertificate {
source_env: &'static str,
path: PathBuf,
certificate_index: usize,
source: reqwest::Error,
},
#[error(
"Failed to build HTTP client while using CA bundle from {} ({}): {source}",
source_env,
path.display()
)]
BuildClientWithCustomCa {
source_env: &'static str,
path: PathBuf,
#[source]
source: reqwest::Error,
},
#[error("Failed to build HTTP client while using system root certificates: {0}")]
BuildClientWithSystemRoots(#[source] reqwest::Error),
#[error(
"Failed to register certificate #{certificate_index} from {} selected by {} in rustls root store: {source}. {hint}",
path.display(),
source_env,
hint = CA_CERT_HINT
)]
RegisterRustlsCertificate {
source_env: &'static str,
path: PathBuf,
certificate_index: usize,
source: rustls::Error,
},
}
impl From<BuildCustomCaTransportError> for io::Error {
fn from(error: BuildCustomCaTransportError) -> Self {
match error {
BuildCustomCaTransportError::ReadCaFile { ref source, .. } => {
io::Error::new(source.kind(), error)
}
BuildCustomCaTransportError::InvalidCaFile { .. }
| BuildCustomCaTransportError::RegisterCertificate { .. }
| BuildCustomCaTransportError::RegisterRustlsCertificate { .. } => {
io::Error::new(io::ErrorKind::InvalidData, error)
}
BuildCustomCaTransportError::BuildClientWithCustomCa { .. }
| BuildCustomCaTransportError::BuildClientWithSystemRoots(_) => io::Error::other(error),
}
}
}
pub fn build_reqwest_client_with_custom_ca(
builder: reqwest::ClientBuilder,
) -> Result<reqwest::Client, BuildCustomCaTransportError> {
build_reqwest_client_with_env(&ProcessEnv, builder)
}
pub fn maybe_build_rustls_client_config_with_custom_ca()
-> Result<Option<Arc<ClientConfig>>, BuildCustomCaTransportError> {
maybe_build_rustls_client_config_with_env(&ProcessEnv)
}
pub fn build_rustls_client_config_with_custom_ca()
-> Result<Arc<ClientConfig>, BuildCustomCaTransportError> {
build_rustls_client_config_with_env(&ProcessEnv)
}
pub fn build_reqwest_client_for_subprocess_tests(
builder: reqwest::ClientBuilder,
) -> Result<reqwest::Client, BuildCustomCaTransportError> {
build_reqwest_client_with_env(&ProcessEnv, builder.no_proxy())
}
fn maybe_build_rustls_client_config_with_env(
env_source: &dyn EnvSource,
) -> Result<Option<Arc<ClientConfig>>, BuildCustomCaTransportError> {
let Some(bundle) = env_source.configured_ca_bundle() else {
return Ok(None);
};
build_rustls_client_config(Some(&bundle)).map(Some)
}
fn build_rustls_client_config_with_env(
env_source: &dyn EnvSource,
) -> Result<Arc<ClientConfig>, BuildCustomCaTransportError> {
let bundle = env_source.configured_ca_bundle();
build_rustls_client_config(bundle.as_ref())
}
fn build_rustls_client_config(
bundle: Option<&ConfiguredCaBundle>,
) -> Result<Arc<ClientConfig>, BuildCustomCaTransportError> {
ensure_rustls_crypto_provider();
let mut root_store = RootCertStore::empty();
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
if !errors.is_empty() {
warn!(
native_root_error_count = errors.len(),
"encountered errors while loading native root certificates"
);
}
let _ = root_store.add_parsable_certificates(certs);
if let Some(bundle) = bundle {
let certificates = bundle.load_certificates()?;
for (idx, cert) in certificates.into_iter().enumerate() {
if let Err(source) = root_store.add(cert) {
warn!(
source_env = bundle.source_env,
ca_path = %bundle.path.display(),
certificate_index = idx + 1,
error = %source,
"failed to register CA certificate in rustls root store"
);
return Err(BuildCustomCaTransportError::RegisterRustlsCertificate {
source_env: bundle.source_env,
path: bundle.path.clone(),
certificate_index: idx + 1,
source,
});
}
}
}
Ok(Arc::new(
ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth(),
))
}
fn build_reqwest_client_with_env(
env_source: &dyn EnvSource,
mut builder: reqwest::ClientBuilder,
) -> Result<reqwest::Client, BuildCustomCaTransportError> {
if let Some(bundle) = env_source.configured_ca_bundle() {
ensure_rustls_crypto_provider();
info!(
source_env = bundle.source_env,
ca_path = %bundle.path.display(),
"building HTTP client with rustls backend for custom CA bundle"
);
builder = builder.use_rustls_tls();
let certificates = bundle.load_certificates()?;
for (idx, cert) in certificates.iter().enumerate() {
let certificate = match reqwest::Certificate::from_der(cert.as_ref()) {
Ok(certificate) => certificate,
Err(source) => {
warn!(
source_env = bundle.source_env,
ca_path = %bundle.path.display(),
certificate_index = idx + 1,
error = %source,
"failed to register CA certificate"
);
return Err(BuildCustomCaTransportError::RegisterCertificate {
source_env: bundle.source_env,
path: bundle.path.clone(),
certificate_index: idx + 1,
source,
});
}
};
builder = builder.add_root_certificate(certificate);
}
return match builder.build() {
Ok(client) => Ok(client),
Err(source) => {
warn!(
source_env = bundle.source_env,
ca_path = %bundle.path.display(),
error = %source,
"failed to build client after loading custom CA bundle"
);
Err(BuildCustomCaTransportError::BuildClientWithCustomCa {
source_env: bundle.source_env,
path: bundle.path.clone(),
source,
})
}
};
}
info!(
codex_ca_certificate_configured = false,
ssl_cert_file_configured = false,
"using system root certificates because no CA override environment variable was selected"
);
match builder.build() {
Ok(client) => Ok(client),
Err(source) => {
warn!(
error = %source,
"failed to build client while using system root certificates"
);
Err(BuildCustomCaTransportError::BuildClientWithSystemRoots(
source,
))
}
}
}
trait EnvSource {
fn var(&self, key: &str) -> Option<String>;
fn non_empty_path(&self, key: &str) -> Option<PathBuf> {
self.var(key)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
}
fn configured_ca_bundle(&self) -> Option<ConfiguredCaBundle> {
self.non_empty_path(CODEX_CA_CERT_ENV)
.map(|path| ConfiguredCaBundle {
source_env: CODEX_CA_CERT_ENV,
path,
})
.or_else(|| {
self.non_empty_path(SSL_CERT_FILE_ENV)
.map(|path| ConfiguredCaBundle {
source_env: SSL_CERT_FILE_ENV,
path,
})
})
}
}
struct ProcessEnv;
impl EnvSource for ProcessEnv {
fn var(&self, key: &str) -> Option<String> {
env::var(key).ok()
}
}
struct ConfiguredCaBundle {
source_env: &'static str,
path: PathBuf,
}
impl ConfiguredCaBundle {
fn load_certificates(
&self,
) -> Result<Vec<CertificateDer<'static>>, BuildCustomCaTransportError> {
match self.parse_certificates() {
Ok(certificates) => {
info!(
source_env = self.source_env,
ca_path = %self.path.display(),
certificate_count = certificates.len(),
"loaded certificates from custom CA bundle"
);
Ok(certificates)
}
Err(error) => {
warn!(
source_env = self.source_env,
ca_path = %self.path.display(),
error = %error,
"failed to load custom CA bundle"
);
Err(error)
}
}
}
fn parse_certificates(
&self,
) -> Result<Vec<CertificateDer<'static>>, BuildCustomCaTransportError> {
let pem_data = self.read_pem_data()?;
let normalized_pem = NormalizedPem::from_pem_data(self.source_env, &self.path, &pem_data);
let mut certificates = Vec::new();
let mut logged_crl_presence = false;
for section_result in normalized_pem.sections() {
let (section_kind, der) = match section_result {
Ok(section) => section,
Err(error) => return Err(self.pem_parse_error(&error)),
};
match section_kind {
SectionKind::Certificate => {
let cert_der = normalized_pem.certificate_der(&der).ok_or_else(|| {
self.invalid_ca_file(
"failed to extract certificate data from TRUSTED CERTIFICATE: invalid DER length",
)
})?;
certificates.push(CertificateDer::from(cert_der.to_vec()));
}
SectionKind::Crl if !logged_crl_presence => {
info!(
source_env = self.source_env,
ca_path = %self.path.display(),
"ignoring X509 CRL entries found in custom CA bundle"
);
logged_crl_presence = true;
}
_ => {}
}
}
if certificates.is_empty() {
return Err(self.pem_parse_error(&pem::Error::NoItemsFound));
}
Ok(certificates)
}
fn read_pem_data(&self) -> Result<Vec<u8>, BuildCustomCaTransportError> {
fs::read(&self.path).map_err(|source| BuildCustomCaTransportError::ReadCaFile {
source_env: self.source_env,
path: self.path.clone(),
source,
})
}
fn pem_parse_error(&self, error: &pem::Error) -> BuildCustomCaTransportError {
let detail = match error {
pem::Error::NoItemsFound => "no certificates found in PEM file".to_string(),
_ => format!("failed to parse PEM file: {error}"),
};
self.invalid_ca_file(detail)
}
fn invalid_ca_file(&self, detail: impl std::fmt::Display) -> BuildCustomCaTransportError {
BuildCustomCaTransportError::InvalidCaFile {
source_env: self.source_env,
path: self.path.clone(),
detail: detail.to_string(),
}
}
}
enum NormalizedPem {
Standard(String),
TrustedCertificate(String),
}
impl NormalizedPem {
fn from_pem_data(source_env: &'static str, path: &Path, pem_data: &[u8]) -> Self {
let pem = String::from_utf8_lossy(pem_data);
if pem.contains("TRUSTED CERTIFICATE") {
info!(
source_env,
ca_path = %path.display(),
"normalizing OpenSSL TRUSTED CERTIFICATE labels in custom CA bundle"
);
Self::TrustedCertificate(
pem.replace("BEGIN TRUSTED CERTIFICATE", "BEGIN CERTIFICATE")
.replace("END TRUSTED CERTIFICATE", "END CERTIFICATE"),
)
} else {
Self::Standard(pem.into_owned())
}
}
fn contents(&self) -> &str {
match self {
Self::Standard(contents) | Self::TrustedCertificate(contents) => contents,
}
}
fn sections(&self) -> impl Iterator<Item = Result<PemSection, pem::Error>> + '_ {
PemSection::pem_slice_iter(self.contents().as_bytes())
}
fn certificate_der<'a>(&self, der: &'a [u8]) -> Option<&'a [u8]> {
match self {
Self::Standard(_) => Some(der),
Self::TrustedCertificate(_) => first_der_item(der),
}
}
}
fn first_der_item(der: &[u8]) -> Option<&[u8]> {
der_item_length(der).map(|length| &der[..length])
}
fn der_item_length(der: &[u8]) -> Option<usize> {
let &length_octet = der.get(1)?;
if length_octet & 0x80 == 0 {
return Some(2 + usize::from(length_octet)).filter(|length| *length <= der.len());
}
let length_octets = usize::from(length_octet & 0x7f);
if length_octets == 0 {
return None;
}
let length_start = 2usize;
let length_end = length_start.checked_add(length_octets)?;
let length_bytes = der.get(length_start..length_end)?;
let mut content_length = 0usize;
for &byte in length_bytes {
content_length = content_length
.checked_mul(256)?
.checked_add(usize::from(byte))?;
}
length_end
.checked_add(content_length)
.filter(|length| *length <= der.len())
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use super::BuildCustomCaTransportError;
use super::CODEX_CA_CERT_ENV;
use super::EnvSource;
use super::SSL_CERT_FILE_ENV;
use super::maybe_build_rustls_client_config_with_env;
const TEST_CERT: &str = include_str!("../tests/fixtures/test-ca.pem");
struct MapEnv {
values: HashMap<String, String>,
}
impl EnvSource for MapEnv {
fn var(&self, key: &str) -> Option<String> {
self.values.get(key).cloned()
}
}
fn map_env(pairs: &[(&str, &str)]) -> MapEnv {
MapEnv {
values: pairs
.iter()
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
.collect(),
}
}
fn write_cert_file(temp_dir: &TempDir, name: &str, contents: &str) -> PathBuf {
let path = temp_dir.path().join(name);
fs::write(&path, contents).unwrap_or_else(|error| {
panic!("write cert fixture failed for {}: {error}", path.display())
});
path
}
#[test]
fn ca_path_prefers_codex_env() {
let env = map_env(&[
(CODEX_CA_CERT_ENV, "/tmp/codex.pem"),
(SSL_CERT_FILE_ENV, "/tmp/fallback.pem"),
]);
assert_eq!(
env.configured_ca_bundle().map(|bundle| bundle.path),
Some(PathBuf::from("/tmp/codex.pem"))
);
}
#[test]
fn ca_path_falls_back_to_ssl_cert_file() {
let env = map_env(&[(SSL_CERT_FILE_ENV, "/tmp/fallback.pem")]);
assert_eq!(
env.configured_ca_bundle().map(|bundle| bundle.path),
Some(PathBuf::from("/tmp/fallback.pem"))
);
}
#[test]
fn ca_path_ignores_empty_values() {
let env = map_env(&[
(CODEX_CA_CERT_ENV, ""),
(SSL_CERT_FILE_ENV, "/tmp/fallback.pem"),
]);
assert_eq!(
env.configured_ca_bundle().map(|bundle| bundle.path),
Some(PathBuf::from("/tmp/fallback.pem"))
);
}
#[test]
fn rustls_config_uses_custom_ca_bundle_when_configured() {
let temp_dir = TempDir::new().expect("tempdir");
let cert_path = write_cert_file(&temp_dir, "ca.pem", TEST_CERT);
let env = map_env(&[(CODEX_CA_CERT_ENV, cert_path.to_string_lossy().as_ref())]);
let config = maybe_build_rustls_client_config_with_env(&env)
.expect("rustls config")
.expect("custom CA config should be present");
assert!(config.enable_sni);
}
#[test]
fn rustls_config_reports_invalid_ca_file() {
let temp_dir = TempDir::new().expect("tempdir");
let cert_path = write_cert_file(&temp_dir, "empty.pem", "");
let env = map_env(&[(CODEX_CA_CERT_ENV, cert_path.to_string_lossy().as_ref())]);
let error = maybe_build_rustls_client_config_with_env(&env).expect_err("invalid CA");
assert!(matches!(
error,
BuildCustomCaTransportError::InvalidCaFile { .. }
));
}
}