rama_crypto/native_certs/
mod.rs1use std::error::Error as StdError;
31use std::path::{Path, PathBuf};
32use std::sync::{Arc, LazyLock};
33use std::{env, fmt, fs, io};
34
35use rama_core::telemetry::tracing::{debug, warn};
36
37use crate::pki_types::CertificateDer;
38use crate::pki_types::pem::{self, PemObject};
39
40#[cfg(all(unix, not(target_os = "macos")))]
41mod unix;
42#[cfg(all(unix, not(target_os = "macos")))]
43use unix as platform;
44
45#[cfg(windows)]
46mod windows;
47#[cfg(windows)]
48use windows as platform;
49
50#[cfg(target_os = "macos")]
51mod macos;
52#[cfg(target_os = "macos")]
53use macos as platform;
54
55pub fn shared_native_trust_anchors() -> Arc<[CertificateDer<'static>]> {
67 static ANCHORS: LazyLock<Arc<[CertificateDer<'static>]>> = LazyLock::new(|| {
68 let paths = CertPaths::from_env();
69 let result = load_native_certs_with_paths(&paths);
70 for err in &result.errors {
71 debug!(%err, "rama native-certs: error while loading native root certificate");
72 }
73
74 if result.certs.is_empty() && !paths.has_overrides() {
75 warn!(
76 native_cert_errors = result.errors.len(),
77 "rama native-certs: no native system root certificates found; \
78 falling back to the bundled webpki (Mozilla CCADB) root certificates"
79 );
80 bundled_root_certs().to_vec().into()
81 } else {
82 debug!(
83 native_cert_count = result.certs.len(),
84 "rama native-certs: loaded native system root certificates"
85 );
86 result.certs.into()
87 }
88 });
89 ANCHORS.clone()
90}
91
92pub fn bundled_root_certs() -> &'static [CertificateDer<'static>] {
99 webpki_root_certs::TLS_SERVER_ROOT_CERTS
100}
101
102pub fn load_native_certs() -> CertificateResult {
121 load_native_certs_with_paths(&CertPaths::from_env())
122}
123
124fn load_native_certs_with_paths(paths: &CertPaths) -> CertificateResult {
125 match paths.has_overrides() {
126 true => paths.load(),
127 _ => platform::load_native_certs(),
128 }
129}
130
131#[non_exhaustive]
133#[derive(Debug, Default)]
134pub struct CertificateResult {
135 pub certs: Vec<CertificateDer<'static>>,
137 pub errors: Vec<Error>,
139}
140
141impl CertificateResult {
142 fn pem_error(&mut self, err: pem::Error, path: &Path) {
143 self.errors.push(Error {
144 context: "failed to read PEM from file",
145 kind: match err {
146 pem::Error::Io(err) => ErrorKind::Io {
147 inner: err,
148 path: path.to_owned(),
149 },
150 _ => ErrorKind::Pem(err),
151 },
152 });
153 }
154
155 fn io_error(&mut self, err: io::Error, path: &Path, context: &'static str) {
156 self.errors.push(Error {
157 context,
158 kind: ErrorKind::Io {
159 inner: err,
160 path: path.to_owned(),
161 },
162 });
163 }
164
165 #[cfg(any(windows, target_os = "macos"))]
166 fn os_error(&mut self, err: Box<dyn StdError + Send + Sync + 'static>, context: &'static str) {
167 self.errors.push(Error {
168 context,
169 kind: ErrorKind::Os(err),
170 });
171 }
172}
173
174struct CertPaths {
176 file: Option<PathBuf>,
177 dirs: Vec<PathBuf>,
178}
179
180impl CertPaths {
181 fn from_env() -> Self {
182 Self {
183 file: env::var_os(ENV_CERT_FILE).map(PathBuf::from),
184 dirs: match env::var_os(ENV_CERT_DIR) {
189 Some(dirs) => env::split_paths(&dirs)
190 .filter(|p| !p.as_os_str().is_empty())
191 .collect(),
192 None => Vec::new(),
193 },
194 }
195 }
196
197 fn load(&self) -> CertificateResult {
198 load_certs_from_paths_internal(self.file.as_deref(), &self.dirs)
199 }
200
201 fn has_overrides(&self) -> bool {
202 self.file.is_some() || !self.dirs.is_empty()
203 }
204}
205
206pub fn load_certs_from_paths(file: Option<&Path>, dir: Option<&Path>) -> CertificateResult {
219 let dir = match dir {
220 Some(d) => vec![d],
221 None => Vec::new(),
222 };
223
224 load_certs_from_paths_internal(file, dir.as_ref())
225}
226
227fn load_certs_from_paths_internal(
228 file: Option<&Path>,
229 dir: &[impl AsRef<Path>],
230) -> CertificateResult {
231 let mut out = CertificateResult::default();
232 if file.is_none() && dir.is_empty() {
233 return out;
234 }
235
236 if let Some(cert_file) = file {
237 load_pem_certs(cert_file, &mut out, false);
239 }
240
241 for cert_dir in dir.iter() {
242 load_pem_certs_from_dir(cert_dir.as_ref(), &mut out);
243 }
244
245 out.certs.sort_unstable_by(|a, b| a.cmp(b));
246 out.certs.dedup();
247 out
248}
249
250fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult) {
252 let dir_reader = match fs::read_dir(dir) {
253 Ok(reader) => reader,
254 Err(err) => {
255 out.io_error(err, dir, "opening directory");
256 return;
257 }
258 };
259
260 for entry in dir_reader {
261 let entry = match entry {
262 Ok(entry) => entry,
263 Err(err) => {
264 out.io_error(err, dir, "reading directory entries");
265 continue;
266 }
267 };
268
269 let path = entry.path();
270
271 let metadata = match fs::metadata(&path) {
274 Ok(metadata) => metadata,
275 Err(e) if e.kind() == io::ErrorKind::NotFound => {
276 continue;
278 }
279 Err(e) => {
280 out.io_error(e, &path, "failed to open file");
281 continue;
282 }
283 };
284
285 if metadata.is_file() {
286 load_pem_certs(&path, out, true);
290 }
291 }
292}
293
294fn load_pem_certs(path: &Path, out: &mut CertificateResult, skip_eperm: bool) {
295 let iter = match CertificateDer::pem_file_iter(path) {
296 Ok(iter) => iter,
297 Err(err) => {
298 if skip_eperm
299 && let pem::Error::Io(io_error) = &err
300 && io_error.kind() == io::ErrorKind::PermissionDenied
301 {
302 return;
303 }
304 out.pem_error(err, path);
305 return;
306 }
307 };
308
309 for result in iter {
310 match result {
311 Ok(cert) => out.certs.push(cert),
312 Err(err) => out.pem_error(err, path),
313 }
314 }
315}
316
317#[derive(Debug)]
319pub struct Error {
320 pub context: &'static str,
322 pub kind: ErrorKind,
324}
325
326impl StdError for Error {
327 fn source(&self) -> Option<&(dyn StdError + 'static)> {
328 Some(match &self.kind {
329 ErrorKind::Io { inner, .. } => inner,
330 ErrorKind::Os(err) => &**err,
331 ErrorKind::Pem(err) => err,
332 })
333 }
334}
335
336impl fmt::Display for Error {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 f.write_str(self.context)?;
339 f.write_str(": ")?;
340 match &self.kind {
341 ErrorKind::Io { inner, path } => write!(f, "{inner} at '{}'", path.display()),
342 ErrorKind::Os(err) => err.fmt(f),
343 ErrorKind::Pem(err) => err.fmt(f),
344 }
345 }
346}
347
348#[non_exhaustive]
350#[derive(Debug)]
351pub enum ErrorKind {
352 Io {
354 inner: io::Error,
356 path: PathBuf,
358 },
359 Os(Box<dyn StdError + Send + Sync + 'static>),
361 Pem(pem::Error),
363}
364
365const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
366const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn bundled_root_certs_non_empty() {
374 assert!(
375 !bundled_root_certs().is_empty(),
376 "bundled webpki root certificates should not be empty"
377 );
378 }
379
380 #[test]
381 fn from_env_missing_file() {
382 let mut result = CertificateResult::default();
383 load_pem_certs(Path::new("no/such/file"), &mut result, false);
384 match &result.errors.first().unwrap().kind {
385 ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
386 other => panic!("unexpected error {other:?}"),
387 }
388 }
389
390 #[test]
391 fn from_env_missing_dir() {
392 let mut result = CertificateResult::default();
393 load_pem_certs_from_dir(Path::new("no/such/directory"), &mut result);
394 match &result.errors.first().unwrap().kind {
395 ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
396 other => panic!("unexpected error {other:?}"),
397 }
398 }
399
400 #[test]
401 fn cert_paths_detects_env_overrides() {
402 assert!(
403 !CertPaths {
404 file: None,
405 dirs: Vec::new()
406 }
407 .has_overrides()
408 );
409 assert!(
410 CertPaths {
411 file: Some(PathBuf::from("ca.pem")),
412 dirs: Vec::new()
413 }
414 .has_overrides()
415 );
416 assert!(
417 CertPaths {
418 file: None,
419 dirs: vec![PathBuf::from("certs")]
420 }
421 .has_overrides()
422 );
423 }
424
425 #[test]
426 #[cfg(unix)]
427 fn from_env_with_non_regular_and_empty_file() {
428 let mut result = CertificateResult::default();
429 load_pem_certs(Path::new("/dev/null"), &mut result, false);
430 assert_eq!(result.certs.len(), 0);
431 assert!(result.errors.is_empty());
432 }
433}