use std::error::Error as StdError;
use std::path::{Path, PathBuf};
use std::sync::{Arc, LazyLock};
use std::{env, fmt, fs, io};
use rama_core::telemetry::tracing::{debug, warn};
use crate::pki_types::CertificateDer;
use crate::pki_types::pem::{self, PemObject};
#[cfg(all(unix, not(target_os = "macos")))]
mod unix;
#[cfg(all(unix, not(target_os = "macos")))]
use unix as platform;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
use windows as platform;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos as platform;
pub fn shared_native_trust_anchors() -> Arc<[CertificateDer<'static>]> {
static ANCHORS: LazyLock<Arc<[CertificateDer<'static>]>> = LazyLock::new(|| {
let paths = CertPaths::from_env();
let result = load_native_certs_with_paths(&paths);
for err in &result.errors {
debug!(%err, "rama native-certs: error while loading native root certificate");
}
if result.certs.is_empty() && !paths.has_overrides() {
warn!(
native_cert_errors = result.errors.len(),
"rama native-certs: no native system root certificates found; \
falling back to the bundled webpki (Mozilla CCADB) root certificates"
);
bundled_root_certs().to_vec().into()
} else {
debug!(
native_cert_count = result.certs.len(),
"rama native-certs: loaded native system root certificates"
);
result.certs.into()
}
});
ANCHORS.clone()
}
pub fn bundled_root_certs() -> &'static [CertificateDer<'static>] {
webpki_root_certs::TLS_SERVER_ROOT_CERTS
}
pub fn load_native_certs() -> CertificateResult {
load_native_certs_with_paths(&CertPaths::from_env())
}
fn load_native_certs_with_paths(paths: &CertPaths) -> CertificateResult {
match paths.has_overrides() {
true => paths.load(),
_ => platform::load_native_certs(),
}
}
#[non_exhaustive]
#[derive(Debug, Default)]
pub struct CertificateResult {
pub certs: Vec<CertificateDer<'static>>,
pub errors: Vec<Error>,
}
impl CertificateResult {
fn pem_error(&mut self, err: pem::Error, path: &Path) {
self.errors.push(Error {
context: "failed to read PEM from file",
kind: match err {
pem::Error::Io(err) => ErrorKind::Io {
inner: err,
path: path.to_owned(),
},
_ => ErrorKind::Pem(err),
},
});
}
fn io_error(&mut self, err: io::Error, path: &Path, context: &'static str) {
self.errors.push(Error {
context,
kind: ErrorKind::Io {
inner: err,
path: path.to_owned(),
},
});
}
#[cfg(any(windows, target_os = "macos"))]
fn os_error(&mut self, err: Box<dyn StdError + Send + Sync + 'static>, context: &'static str) {
self.errors.push(Error {
context,
kind: ErrorKind::Os(err),
});
}
}
struct CertPaths {
file: Option<PathBuf>,
dirs: Vec<PathBuf>,
}
impl CertPaths {
fn from_env() -> Self {
Self {
file: env::var_os(ENV_CERT_FILE).map(PathBuf::from),
dirs: match env::var_os(ENV_CERT_DIR) {
Some(dirs) => env::split_paths(&dirs)
.filter(|p| !p.as_os_str().is_empty())
.collect(),
None => Vec::new(),
},
}
}
fn load(&self) -> CertificateResult {
load_certs_from_paths_internal(self.file.as_deref(), &self.dirs)
}
fn has_overrides(&self) -> bool {
self.file.is_some() || !self.dirs.is_empty()
}
}
pub fn load_certs_from_paths(file: Option<&Path>, dir: Option<&Path>) -> CertificateResult {
let dir = match dir {
Some(d) => vec![d],
None => Vec::new(),
};
load_certs_from_paths_internal(file, dir.as_ref())
}
fn load_certs_from_paths_internal(
file: Option<&Path>,
dir: &[impl AsRef<Path>],
) -> CertificateResult {
let mut out = CertificateResult::default();
if file.is_none() && dir.is_empty() {
return out;
}
if let Some(cert_file) = file {
load_pem_certs(cert_file, &mut out, false);
}
for cert_dir in dir.iter() {
load_pem_certs_from_dir(cert_dir.as_ref(), &mut out);
}
out.certs.sort_unstable_by(|a, b| a.cmp(b));
out.certs.dedup();
out
}
fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult) {
let dir_reader = match fs::read_dir(dir) {
Ok(reader) => reader,
Err(err) => {
out.io_error(err, dir, "opening directory");
return;
}
};
for entry in dir_reader {
let entry = match entry {
Ok(entry) => entry,
Err(err) => {
out.io_error(err, dir, "reading directory entries");
continue;
}
};
let path = entry.path();
let metadata = match fs::metadata(&path) {
Ok(metadata) => metadata,
Err(e) if e.kind() == io::ErrorKind::NotFound => {
continue;
}
Err(e) => {
out.io_error(e, &path, "failed to open file");
continue;
}
};
if metadata.is_file() {
load_pem_certs(&path, out, true);
}
}
}
fn load_pem_certs(path: &Path, out: &mut CertificateResult, skip_eperm: bool) {
let iter = match CertificateDer::pem_file_iter(path) {
Ok(iter) => iter,
Err(err) => {
if skip_eperm
&& let pem::Error::Io(io_error) = &err
&& io_error.kind() == io::ErrorKind::PermissionDenied
{
return;
}
out.pem_error(err, path);
return;
}
};
for result in iter {
match result {
Ok(cert) => out.certs.push(cert),
Err(err) => out.pem_error(err, path),
}
}
}
#[derive(Debug)]
pub struct Error {
pub context: &'static str,
pub kind: ErrorKind,
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(match &self.kind {
ErrorKind::Io { inner, .. } => inner,
ErrorKind::Os(err) => &**err,
ErrorKind::Pem(err) => err,
})
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.context)?;
f.write_str(": ")?;
match &self.kind {
ErrorKind::Io { inner, path } => write!(f, "{inner} at '{}'", path.display()),
ErrorKind::Os(err) => err.fmt(f),
ErrorKind::Pem(err) => err.fmt(f),
}
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum ErrorKind {
Io {
inner: io::Error,
path: PathBuf,
},
Os(Box<dyn StdError + Send + Sync + 'static>),
Pem(pem::Error),
}
const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bundled_root_certs_non_empty() {
assert!(
!bundled_root_certs().is_empty(),
"bundled webpki root certificates should not be empty"
);
}
#[test]
fn from_env_missing_file() {
let mut result = CertificateResult::default();
load_pem_certs(Path::new("no/such/file"), &mut result, false);
match &result.errors.first().unwrap().kind {
ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
other => panic!("unexpected error {other:?}"),
}
}
#[test]
fn from_env_missing_dir() {
let mut result = CertificateResult::default();
load_pem_certs_from_dir(Path::new("no/such/directory"), &mut result);
match &result.errors.first().unwrap().kind {
ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
other => panic!("unexpected error {other:?}"),
}
}
#[test]
fn cert_paths_detects_env_overrides() {
assert!(
!CertPaths {
file: None,
dirs: Vec::new()
}
.has_overrides()
);
assert!(
CertPaths {
file: Some(PathBuf::from("ca.pem")),
dirs: Vec::new()
}
.has_overrides()
);
assert!(
CertPaths {
file: None,
dirs: vec![PathBuf::from("certs")]
}
.has_overrides()
);
}
#[test]
#[cfg(unix)]
fn from_env_with_non_regular_and_empty_file() {
let mut result = CertificateResult::default();
load_pem_certs(Path::new("/dev/null"), &mut result, false);
assert_eq!(result.certs.len(), 0);
assert!(result.errors.is_empty());
}
}