Skip to main content

rustls_native_certs/
lib.rs

1//! rustls-native-certs allows rustls to use the platform's native certificate
2//! store when operating as a TLS client.
3//!
4//! It provides a single function [`load_native_certs()`], which returns a
5//! collection of certificates found by reading the platform-native
6//! certificate store.
7//!
8//! If the SSL_CERT_FILE environment variable is set, certificates (in PEM
9//! format) are read from that file instead.
10//!
11//! If you want to load these certificates into a `rustls::RootCertStore`,
12//! you'll likely want to do something like this:
13//!
14//! ```no_run
15//! let mut roots = rustls::RootCertStore::empty();
16//! for cert in rustls_native_certs::load_native_certs().expect("could not load platform certs") {
17//!     roots.add(cert).unwrap();
18//! }
19//! ```
20
21// Enable documentation for all features on docs.rs
22#![cfg_attr(rustls_native_certs_docsrs, feature(doc_cfg))]
23
24use std::error::Error as StdError;
25use std::path::{Path, PathBuf};
26use std::{env, fmt, fs, io};
27
28use pki_types::pem::{self, PemObject};
29use pki_types::CertificateDer;
30
31#[cfg(all(unix, not(target_os = "macos")))]
32mod unix;
33#[cfg(all(unix, not(target_os = "macos")))]
34use unix as platform;
35
36#[cfg(windows)]
37mod windows;
38#[cfg(windows)]
39use windows as platform;
40
41#[cfg(target_os = "macos")]
42mod macos;
43#[cfg(target_os = "macos")]
44use macos as platform;
45
46/// Load root certificates found in the platform's native certificate store.
47///
48/// ## Environment Variables
49///
50/// | Env. Var.     | Description                                                                           |
51/// |---------------|---------------------------------------------------------------------------------------|
52/// | SSL_CERT_FILE | File containing an arbitrary number of certificates in PEM format.                    |
53/// | SSL_CERT_DIR  | Colon separated list of directories containing certificate files.                     |
54///
55/// If **either** (or **both**) are set, certificates are only loaded from
56/// the locations specified via environment variables and not the platform-
57/// native certificate store.
58///
59/// ## Certificate Validity
60///
61/// All certificates are expected to be in PEM format. A file may contain
62/// multiple certificates.
63///
64/// Example:
65///
66/// ```text
67/// -----BEGIN CERTIFICATE-----
68/// MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
69/// CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
70/// R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
71/// MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
72/// ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
73/// EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
74/// +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
75/// ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
76/// AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
77/// zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
78/// tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
79/// /q4AaOeMSQ+2b1tbFfLn
80/// -----END CERTIFICATE-----
81/// -----BEGIN CERTIFICATE-----
82/// MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
83/// MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
84/// Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
85/// A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
86/// Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
87/// ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
88/// QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
89/// ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
90/// BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
91/// YyRIHN8wfdVoOw==
92/// -----END CERTIFICATE-----
93///
94/// ```
95///
96/// For reasons of compatibility, an attempt is made to skip invalid sections
97/// of a certificate file but this means it's also possible for a malformed
98/// certificate to be skipped.
99///
100/// If a certificate isn't loaded, and no error is reported, check if:
101///
102/// 1. the certificate is in PEM format (see example above)
103/// 2. *BEGIN CERTIFICATE* line starts with exactly five hyphens (`'-'`)
104/// 3. *END CERTIFICATE* line ends with exactly five hyphens (`'-'`)
105/// 4. there is a line break after the certificate.
106///
107/// ## Errors
108///
109/// This function fails in a platform-specific way, expressed in a `std::io::Error`.
110///
111/// ## Caveats
112///
113/// This function can be expensive: on some platforms it involves loading
114/// and parsing a ~300KB disk file.  It's therefore prudent to call
115/// this sparingly.
116///
117/// [c_rehash]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
118pub fn load_native_certs() -> CertificateResult {
119    let paths = CertPaths::from_env();
120    match (&paths.dirs, &paths.file) {
121        (v, _) if !v.is_empty() => paths.load(),
122        (_, Some(_)) => paths.load(),
123        _ => platform::load_native_certs(),
124    }
125}
126
127/// Results from trying to load certificates from the platform's native store.
128#[non_exhaustive]
129#[derive(Debug, Default)]
130pub struct CertificateResult {
131    /// Any certificates that were successfully loaded.
132    pub certs: Vec<CertificateDer<'static>>,
133    /// Any errors encountered while loading certificates.
134    pub errors: Vec<Error>,
135}
136
137impl CertificateResult {
138    /// Return the found certificates if no error occurred, otherwise panic.
139    #[track_caller]
140    pub fn expect(self, msg: &str) -> Vec<CertificateDer<'static>> {
141        match self.errors.is_empty() {
142            true => self.certs,
143            false => panic!("{msg}: {:?}", self.errors),
144        }
145    }
146
147    /// Return the found certificates if no error occurred, otherwise panic.
148    #[track_caller]
149    pub fn unwrap(self) -> Vec<CertificateDer<'static>> {
150        match self.errors.is_empty() {
151            true => self.certs,
152            false => panic!(
153                "errors occurred while loading certificates: {:?}",
154                self.errors
155            ),
156        }
157    }
158
159    fn pem_error(&mut self, err: pem::Error, path: &Path) {
160        self.errors.push(Error {
161            context: "failed to read PEM from file",
162            kind: match err {
163                pem::Error::Io(err) => ErrorKind::Io {
164                    inner: err,
165                    path: path.to_owned(),
166                },
167                _ => ErrorKind::Pem(err),
168            },
169        });
170    }
171
172    fn io_error(&mut self, err: io::Error, path: &Path, context: &'static str) {
173        self.errors.push(Error {
174            context,
175            kind: ErrorKind::Io {
176                inner: err,
177                path: path.to_owned(),
178            },
179        });
180    }
181
182    #[cfg(any(windows, target_os = "macos"))]
183    fn os_error(&mut self, err: Box<dyn StdError + Send + Sync + 'static>, context: &'static str) {
184        self.errors.push(Error {
185            context,
186            kind: ErrorKind::Os(err),
187        });
188    }
189}
190
191/// Certificate paths from `SSL_CERT_FILE` and/or `SSL_CERT_DIR`.
192struct CertPaths {
193    file: Option<PathBuf>,
194    dirs: Vec<PathBuf>,
195}
196
197impl CertPaths {
198    fn from_env() -> Self {
199        Self {
200            file: env::var_os(ENV_CERT_FILE).map(PathBuf::from),
201            // Read `SSL_CERT_DIR`, split it on the platform delimiter (`:` on Unix, `;` on Windows),
202            // and return the entries as `PathBuf`s.
203            //
204            // See <https://docs.openssl.org/3.5/man1/openssl-rehash/#options>
205            dirs: match env::var_os(ENV_CERT_DIR) {
206                Some(dirs) => env::split_paths(&dirs)
207                    .filter(|p| !p.as_os_str().is_empty())
208                    .collect(),
209                None => Vec::new(),
210            },
211        }
212    }
213
214    /// Load certificates from the paths.
215    ///
216    /// See [`load_certs_from_paths()`].
217    fn load(&self) -> CertificateResult {
218        load_certs_from_paths_internal(self.file.as_deref(), &self.dirs)
219    }
220}
221
222/// Load certificates from the given paths.
223///
224/// If both are `None`, returns an empty [`CertificateResult`].
225///
226/// If `file` is `Some`, it is always used, so it must be a path to an existing,
227/// accessible file from which certificates can be loaded successfully. While parsing,
228/// the rustls-pki-types PEM parser will ignore parts of the file which are
229/// not considered part of a certificate. Certificates which are not in the right
230/// format (PEM) or are otherwise corrupted may get ignored silently.
231///
232/// If `dir` is defined, a directory must exist at this path, and all files
233/// contained in it must be loaded successfully, subject to the rules outlined above for `file`.
234/// The directory is not scanned recursively and may be empty.
235///
236/// On Unix, each file is read at most once: directory entries that resolve to a file
237/// already read (for example the symlinks created by `openssl rehash`, or a link to
238/// `file`) are skipped.
239pub fn load_certs_from_paths(file: Option<&Path>, dir: Option<&Path>) -> CertificateResult {
240    let dir = match dir {
241        Some(d) => vec![d],
242        None => Vec::new(),
243    };
244
245    load_certs_from_paths_internal(file, dir.as_ref())
246}
247
248fn load_certs_from_paths_internal(
249    file: Option<&Path>,
250    dir: &[impl AsRef<Path>],
251) -> CertificateResult {
252    let mut out = CertificateResult::default();
253    if file.is_none() && dir.is_empty() {
254        return out;
255    }
256
257    let mut seen = SeenFiles::default();
258    if let Some(cert_file) = file {
259        if let Ok(metadata) = fs::metadata(cert_file) {
260            seen.first_visit(&metadata);
261        }
262        load_pem_certs(cert_file, &mut out);
263    }
264
265    for cert_dir in dir.iter() {
266        load_pem_certs_from_dir(cert_dir.as_ref(), &mut out, &mut seen);
267    }
268
269    out.certs
270        .sort_unstable_by(|a, b| a.cmp(b));
271    out.certs.dedup();
272    out
273}
274
275/// Files already read, by identity.
276///
277/// Certificate directories typically hold several symlinks to each certificate
278/// (hash names, readable names) and often a copy of the bundle file too.
279#[derive(Default)]
280struct SeenFiles {
281    /// `(device, inode)` of each file read.
282    #[cfg(unix)]
283    ids: std::collections::HashSet<(u64, u64)>,
284}
285
286impl SeenFiles {
287    /// Records the file and returns whether it was not seen before.
288    ///
289    /// Always `true` where std exposes no stable file identity.
290    fn first_visit(&mut self, metadata: &fs::Metadata) -> bool {
291        #[cfg(unix)]
292        {
293            use std::os::unix::fs::MetadataExt;
294            self.ids
295                .insert((metadata.dev(), metadata.ino()))
296        }
297        #[cfg(not(unix))]
298        {
299            let _ = metadata;
300            true
301        }
302    }
303}
304
305/// Load certificate from certificate directory (what OpenSSL calls CAdir)
306fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult, seen: &mut SeenFiles) {
307    let dir_reader = match fs::read_dir(dir) {
308        Ok(reader) => reader,
309        Err(err) => {
310            out.io_error(err, dir, "opening directory");
311            return;
312        }
313    };
314
315    for entry in dir_reader {
316        let entry = match entry {
317            Ok(entry) => entry,
318            Err(err) => {
319                out.io_error(err, dir, "reading directory entries");
320                continue;
321            }
322        };
323
324        let path = entry.path();
325
326        // `openssl rehash` used to create this directory uses symlinks. So,
327        // make sure we resolve them.
328        let metadata = match fs::metadata(&path) {
329            Ok(metadata) => metadata,
330            Err(e) if e.kind() == io::ErrorKind::NotFound => {
331                // Dangling symlink
332                continue;
333            }
334            Err(e) => {
335                out.io_error(e, &path, "failed to open file");
336                continue;
337            }
338        };
339
340        if metadata.is_file() && seen.first_visit(&metadata) {
341            load_pem_certs(&path, out);
342        }
343    }
344}
345
346fn load_pem_certs(path: &Path, out: &mut CertificateResult) {
347    let iter = match CertificateDer::pem_file_iter(path) {
348        Ok(iter) => iter,
349        Err(err) => {
350            out.pem_error(err, path);
351            return;
352        }
353    };
354
355    for result in iter {
356        match result {
357            Ok(cert) => out.certs.push(cert),
358            Err(err) => out.pem_error(err, path),
359        }
360    }
361}
362
363#[derive(Debug)]
364pub struct Error {
365    pub context: &'static str,
366    pub kind: ErrorKind,
367}
368
369impl StdError for Error {
370    fn source(&self) -> Option<&(dyn StdError + 'static)> {
371        Some(match &self.kind {
372            ErrorKind::Io { inner, .. } => inner,
373            ErrorKind::Os(err) => &**err,
374            ErrorKind::Pem(err) => err,
375        })
376    }
377}
378
379impl fmt::Display for Error {
380    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381        f.write_str(self.context)?;
382        f.write_str(": ")?;
383        match &self.kind {
384            ErrorKind::Io { inner, path } => {
385                write!(f, "{inner} at '{}'", path.display())
386            }
387            ErrorKind::Os(err) => err.fmt(f),
388            ErrorKind::Pem(err) => err.fmt(f),
389        }
390    }
391}
392
393#[non_exhaustive]
394#[derive(Debug)]
395pub enum ErrorKind {
396    Io { inner: io::Error, path: PathBuf },
397    Os(Box<dyn StdError + Send + Sync + 'static>),
398    Pem(pem::Error),
399}
400
401const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
402const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    use std::fs::File;
409    #[cfg(unix)]
410    use std::fs::Permissions;
411    use std::io::Write;
412    #[cfg(unix)]
413    use std::os::unix::fs::PermissionsExt;
414
415    #[test]
416    fn deduplication() {
417        let temp_dir = tempfile::TempDir::new().unwrap();
418        let cert1 = include_str!("../tests/badssl-com-chain.pem");
419        let cert2 = include_str!("../integration-tests/one-existing-ca.pem");
420        let file_path = temp_dir
421            .path()
422            .join("ca-certificates.crt");
423        let dir_path = temp_dir.path().to_path_buf();
424
425        {
426            let mut file = File::create(&file_path).unwrap();
427            write!(file, "{}", cert1).unwrap();
428            write!(file, "{}", cert2).unwrap();
429        }
430
431        {
432            // Duplicate (already in `file_path`)
433            let mut file = File::create(dir_path.join("71f3bb26.0")).unwrap();
434            write!(file, "{}", cert1).unwrap();
435        }
436
437        {
438            // Duplicate (already in `file_path`)
439            let mut file = File::create(dir_path.join("912e7cd5.0")).unwrap();
440            write!(file, "{}", cert2).unwrap();
441        }
442
443        let result = CertPaths {
444            file: Some(file_path.clone()),
445            dirs: vec![],
446        }
447        .load();
448        assert_eq!(result.certs.len(), 2);
449
450        let result = CertPaths {
451            file: None,
452            dirs: vec![dir_path.clone()],
453        }
454        .load();
455        assert_eq!(result.certs.len(), 2);
456
457        let result = CertPaths {
458            file: Some(file_path),
459            dirs: vec![dir_path],
460        }
461        .load();
462        assert_eq!(result.certs.len(), 2);
463    }
464
465    /// Each read of this file records one PEM error, which makes reads countable.
466    #[cfg(unix)]
467    const MALFORMED_PEM: &str = "-----BEGIN CERTIFICATE-----\n!!!\n-----END CERTIFICATE-----\n";
468
469    #[test]
470    #[cfg(unix)]
471    fn dir_symlinks_to_same_file_read_once() {
472        use std::os::unix::fs::symlink;
473
474        let temp_dir = tempfile::TempDir::new().unwrap();
475        let dir = temp_dir.path();
476        fs::write(dir.join("Example_Root.pem"), MALFORMED_PEM).unwrap();
477        symlink("Example_Root.pem", dir.join("01234567.0")).unwrap();
478        symlink(dir.join("Example_Root.pem"), dir.join("89abcdef.0")).unwrap();
479
480        let result = load_certs_from_paths_internal(None, &[dir]);
481        assert_eq!(result.errors.len(), 1, "{:?}", result.errors);
482    }
483
484    #[test]
485    #[cfg(unix)]
486    fn dir_entry_for_cert_file_skipped() {
487        use std::os::unix::fs::symlink;
488
489        let file_dir = tempfile::TempDir::new().unwrap();
490        let cert_dir = tempfile::TempDir::new().unwrap();
491        let file = file_dir.path().join("ca-bundle.crt");
492        fs::write(&file, MALFORMED_PEM).unwrap();
493        symlink(&file, cert_dir.path().join("ca-bundle.crt")).unwrap();
494
495        let result = load_certs_from_paths_internal(Some(&file), &[cert_dir.path()]);
496        assert_eq!(result.errors.len(), 1, "{:?}", result.errors);
497    }
498
499    #[test]
500    #[cfg(unix)]
501    fn dir_copies_with_same_content_all_read() {
502        let temp_dir = tempfile::TempDir::new().unwrap();
503        fs::write(temp_dir.path().join("a.pem"), MALFORMED_PEM).unwrap();
504        fs::write(temp_dir.path().join("b.pem"), MALFORMED_PEM).unwrap();
505
506        let result = load_certs_from_paths_internal(None, &[temp_dir.path()]);
507        assert_eq!(result.errors.len(), 2, "{:?}", result.errors);
508    }
509
510    #[test]
511    #[cfg(unix)]
512    fn dir_symlinks_still_load_certs() {
513        use std::os::unix::fs::symlink;
514
515        let temp_dir = tempfile::TempDir::new().unwrap();
516        let dir = temp_dir.path();
517        fs::write(
518            dir.join("chain.pem"),
519            include_str!("../tests/badssl-com-chain.pem"),
520        )
521        .unwrap();
522        symlink("chain.pem", dir.join("71f3bb26.0")).unwrap();
523
524        let result = load_certs_from_paths_internal(None, &[dir]);
525        assert!(result.errors.is_empty(), "{:?}", result.errors);
526        assert_eq!(result.certs.len(), 1);
527    }
528
529    #[test]
530    fn malformed_file_from_env() {
531        // Certificate parser tries to extract certs from file ignoring
532        // invalid sections.
533        let mut result = CertificateResult::default();
534        load_pem_certs(Path::new(file!()), &mut result);
535        assert_eq!(result.certs.len(), 0);
536        assert!(result.errors.is_empty());
537    }
538
539    #[test]
540    fn from_env_missing_file() {
541        let mut result = CertificateResult::default();
542        load_pem_certs(Path::new("no/such/file"), &mut result);
543        match &first_error(&result).kind {
544            ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
545            _ => panic!("unexpected error {:?}", result.errors),
546        }
547    }
548
549    #[test]
550    fn from_env_missing_dir() {
551        let mut result = CertificateResult::default();
552        load_pem_certs_from_dir(
553            Path::new("no/such/directory"),
554            &mut result,
555            &mut SeenFiles::default(),
556        );
557        match &first_error(&result).kind {
558            ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
559            _ => panic!("unexpected error {:?}", result.errors),
560        }
561    }
562
563    #[test]
564    #[cfg(unix)]
565    fn from_env_with_non_regular_and_empty_file() {
566        let mut result = CertificateResult::default();
567        load_pem_certs(Path::new("/dev/null"), &mut result);
568        assert_eq!(result.certs.len(), 0);
569        assert!(result.errors.is_empty());
570    }
571
572    #[test]
573    #[cfg(unix)]
574    fn from_env_bad_dir_perms() {
575        // Create a temp dir that we can't read from.
576        let temp_dir = tempfile::TempDir::new().unwrap();
577        fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o000)).unwrap();
578
579        test_cert_paths_bad_perms(CertPaths {
580            file: None,
581            dirs: vec![temp_dir.path().into()],
582        })
583    }
584
585    #[test]
586    #[cfg(unix)]
587    fn from_env_bad_file_perms() {
588        // Create a tmp dir with a file inside that we can't read from.
589        let temp_dir = tempfile::TempDir::new().unwrap();
590        let file_path = temp_dir.path().join("unreadable.pem");
591        let cert_file = File::create(&file_path).unwrap();
592        cert_file
593            .set_permissions(Permissions::from_mode(0o000))
594            .unwrap();
595
596        test_cert_paths_bad_perms(CertPaths {
597            file: Some(file_path.clone()),
598            dirs: vec![],
599        });
600    }
601
602    #[cfg(unix)]
603    fn test_cert_paths_bad_perms(cert_paths: CertPaths) {
604        let result = cert_paths.load();
605
606        if let (None, true) = (cert_paths.file, cert_paths.dirs.is_empty()) {
607            panic!("only one of file or dir should be set");
608        };
609
610        let error = first_error(&result);
611        match &error.kind {
612            ErrorKind::Io { inner, .. } => {
613                assert_eq!(inner.kind(), io::ErrorKind::PermissionDenied);
614                inner
615            }
616            _ => panic!("unexpected error {:?}", result.errors),
617        };
618    }
619
620    fn first_error(result: &CertificateResult) -> &Error {
621        result.errors.first().unwrap()
622    }
623}