Skip to main content

rama_crypto/native_certs/
mod.rs

1//! Load the platform's native certificate store (system trust chain) in a
2//! tls-implementation agnostic way, as [`pki_types`] certificates.
3//!
4//! The certificates returned here can be fed into any tls backend (e.g.
5//! `rustls` or `boring`), which is why this lives in `rama-crypto` rather than
6//! in one of the tls backend crates.
7//!
8//! The main entry points are:
9//!
10//! - [`shared_native_trust_anchors`]: the cached, process-wide default trust
11//!   anchors used by rama tls clients. Loads the native store once; if nothing
12//!   is found it warns and falls back to the bundled webpki roots.
13//! - [`load_native_certs`]: a one-shot (uncached) read of the platform store,
14//!   for callers that want to manage caching/merging themselves.
15//! - [`bundled_root_certs`]: the bundled Mozilla (CCADB) root certificates used
16//!   as the fallback.
17//!
18//! # Attribution
19//!
20//! The platform readers and `SSL_CERT_FILE`/`SSL_CERT_DIR` handling are an
21//! adapted fork of [`rustls-native-certs`] (Apache-2.0 OR ISC OR MIT), with the
22//! pending [permission-skip fix][pr228] folded in and the public surface
23//! reshaped around rama's [`pki_types`] re-export, error and tracing
24//! conventions.
25//!
26//! [`pki_types`]: crate::pki_types
27//! [`rustls-native-certs`]: https://github.com/rustls/rustls-native-certs
28//! [pr228]: https://github.com/rustls/rustls-native-certs/pull/228
29
30use 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
55/// Returns the cached, process-wide default trust anchors used by rama tls
56/// clients (both the `rustls` and `boring` backends consume these).
57///
58/// On first call this loads the platform's native certificate store via
59/// [`load_native_certs`] (honoring `SSL_CERT_FILE`/`SSL_CERT_DIR`). If the
60/// native store yields no certificates, a warning is logged and the bundled
61/// webpki roots ([`bundled_root_certs`]) are used instead so that clients on
62/// minimal systems (e.g. distroless containers) still have a sane default.
63///
64/// The result is cached for the lifetime of the process: the (potentially
65/// expensive) native read happens at most once.
66pub 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
92/// The bundled Mozilla (CCADB) root certificates, used as the fallback by
93/// [`shared_native_trust_anchors`] and available for explicit use.
94///
95/// This is a re-export of the data shipped by the [`webpki-root-certs`] crate.
96///
97/// [`webpki-root-certs`]: https://docs.rs/webpki-root-certs
98pub fn bundled_root_certs() -> &'static [CertificateDer<'static>] {
99    webpki_root_certs::TLS_SERVER_ROOT_CERTS
100}
101
102/// Load root certificates found in the platform's native certificate store.
103///
104/// ## Environment Variables
105///
106/// | Env. Var.     | Description                                                       |
107/// |---------------|-------------------------------------------------------------------|
108/// | SSL_CERT_FILE | File containing an arbitrary number of certificates in PEM format.|
109/// | SSL_CERT_DIR  | `:`/`;` separated list of directories containing certificate files.|
110///
111/// If **either** (or **both**) are set, certificates are only loaded from the
112/// locations specified via environment variables and not the platform-native
113/// certificate store.
114///
115/// ## Caveats
116///
117/// This function can be expensive: on some platforms it involves loading and
118/// parsing a ~300KB disk file, or querying the OS keychain. Prefer
119/// [`shared_native_trust_anchors`] which caches the result.
120pub 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/// Results from trying to load certificates from the platform's native store.
132#[non_exhaustive]
133#[derive(Debug, Default)]
134pub struct CertificateResult {
135    /// Any certificates that were successfully loaded.
136    pub certs: Vec<CertificateDer<'static>>,
137    /// Any errors encountered while loading certificates.
138    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
174/// Certificate paths from `SSL_CERT_FILE` and/or `SSL_CERT_DIR`.
175struct 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            // Read `SSL_CERT_DIR`, split it on the platform delimiter (`:` on
185            // unix, `;` on windows), ignoring empty entries.
186            //
187            // See <https://docs.openssl.org/3.5/man1/openssl-rehash/#options>
188            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
206/// Load certificates from the given paths.
207///
208/// If both are `None`, returns an empty [`CertificateResult`].
209///
210/// If `file` is `Some`, it must be a path to an existing, accessible file from
211/// which certificates can be loaded. The PEM parser ignores parts of the file
212/// which are not considered part of a certificate; malformed certificates may
213/// be silently skipped.
214///
215/// If `dir` is defined, a directory must exist at this path. The directory is
216/// not scanned recursively and may be empty; entries that are not readable
217/// (e.g. root-only files) are skipped.
218pub 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        // An explicit file is expected to exist and be readable: surface errors.
238        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
250/// Load certificates from a certificate directory (what OpenSSL calls CAdir).
251fn 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        // `openssl rehash` used to create this directory uses symlinks, so
272        // resolve them.
273        let metadata = match fs::metadata(&path) {
274            Ok(metadata) => metadata,
275            Err(e) if e.kind() == io::ErrorKind::NotFound => {
276                // Dangling symlink
277                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            // When scanning a directory, skip over files that are not readable
287            // (usually `chown root` or `chmod -r`), rather than failing the
288            // whole load. See <https://github.com/rustls/rustls-native-certs/pull/228>.
289            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/// An error encountered while loading certificates from the platform store.
318#[derive(Debug)]
319pub struct Error {
320    /// Human-readable context describing what was being attempted.
321    pub context: &'static str,
322    /// The underlying error kind.
323    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/// The kinds of errors that can occur while loading native certificates.
349#[non_exhaustive]
350#[derive(Debug)]
351pub enum ErrorKind {
352    /// An I/O error while reading a certificate file or directory.
353    Io {
354        /// The underlying I/O error.
355        inner: io::Error,
356        /// The path being read.
357        path: PathBuf,
358    },
359    /// A platform (OS keychain / cert store) error.
360    Os(Box<dyn StdError + Send + Sync + 'static>),
361    /// A PEM parsing error.
362    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}