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(docsrs, feature(doc_cfg, doc_auto_cfg))]
23
24use std::error::Error as StdError;
25use std::ffi::OsStr;
26use std::path::{Path, PathBuf};
27use std::{env, fmt, fs, io};
28
29use pki_types::pem::{self, PemObject};
30use pki_types::CertificateDer;
31
32#[cfg(all(unix, not(target_os = "macos")))]
33mod unix;
34#[cfg(all(unix, not(target_os = "macos")))]
35use unix as platform;
36
37#[cfg(windows)]
38mod windows;
39#[cfg(windows)]
40use windows as platform;
41
42#[cfg(target_os = "macos")]
43mod macos;
44#[cfg(target_os = "macos")]
45use macos as platform;
46
47/// Load root certificates found in the platform's native certificate store.
48///
49/// ## Environment Variables
50///
51/// | Env. Var.      | Description                                                                           |
52/// |----------------|---------------------------------------------------------------------------------------|
53/// | SSL_CERT_FILE  | File containing an arbitrary number of certificates in PEM format.                    |
54/// | SSL_CERT_DIR   | Directory utilizing the hierarchy and naming convention used by OpenSSL's [c_rehash]. |
55///
56/// If **either** (or **both**) are set, certificates are only loaded from
57/// the locations specified via environment variables and not the platform-
58/// native certificate store.
59///
60/// ## Certificate Validity
61///
62/// All certificates are expected to be in PEM format. A file may contain
63/// multiple certificates.
64///
65/// Example:
66///
67/// ```text
68/// -----BEGIN CERTIFICATE-----
69/// MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
70/// CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
71/// R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
72/// MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
73/// ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
74/// EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
75/// +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
76/// ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
77/// AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
78/// zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
79/// tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
80/// /q4AaOeMSQ+2b1tbFfLn
81/// -----END CERTIFICATE-----
82/// -----BEGIN CERTIFICATE-----
83/// MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5
84/// MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g
85/// Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG
86/// A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg
87/// Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl
88/// ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j
89/// QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr
90/// ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr
91/// BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM
92/// YyRIHN8wfdVoOw==
93/// -----END CERTIFICATE-----
94///
95/// ```
96///
97/// For reasons of compatibility, an attempt is made to skip invalid sections
98/// of a certificate file but this means it's also possible for a malformed
99/// certificate to be skipped.
100///
101/// If a certificate isn't loaded, and no error is reported, check if:
102///
103/// 1. the certificate is in PEM format (see example above)
104/// 2. *BEGIN CERTIFICATE* line starts with exactly five hyphens (`'-'`)
105/// 3. *END CERTIFICATE* line ends with exactly five hyphens (`'-'`)
106/// 4. there is a line break after the certificate.
107///
108/// ## Errors
109///
110/// This function fails in a platform-specific way, expressed in a `std::io::Error`.
111///
112/// ## Caveats
113///
114/// This function can be expensive: on some platforms it involves loading
115/// and parsing a ~300KB disk file.  It's therefore prudent to call
116/// this sparingly.
117///
118/// [c_rehash]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
119pub fn load_native_certs() -> CertificateResult {
120    let paths = CertPaths::from_env();
121    match (&paths.dir, &paths.file) {
122        (Some(_), _) | (_, Some(_)) => paths.load(),
123        (None, None) => 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    pub fn expect(self, msg: &str) -> Vec<CertificateDer<'static>> {
140        match self.errors.is_empty() {
141            true => self.certs,
142            false => panic!("{msg}: {:?}", self.errors),
143        }
144    }
145
146    /// Return the found certificates if no error occurred, otherwise panic.
147    pub fn unwrap(self) -> Vec<CertificateDer<'static>> {
148        match self.errors.is_empty() {
149            true => self.certs,
150            false => panic!(
151                "errors occurred while loading certificates: {:?}",
152                self.errors
153            ),
154        }
155    }
156
157    fn pem_error(&mut self, err: pem::Error, path: &Path) {
158        self.errors.push(Error {
159            context: "failed to read PEM from file",
160            kind: match err {
161                pem::Error::Io(err) => ErrorKind::Io {
162                    inner: err,
163                    path: path.to_owned(),
164                },
165                _ => ErrorKind::Pem(err),
166            },
167        });
168    }
169
170    fn io_error(&mut self, err: io::Error, path: &Path, context: &'static str) {
171        self.errors.push(Error {
172            context,
173            kind: ErrorKind::Io {
174                inner: err,
175                path: path.to_owned(),
176            },
177        });
178    }
179
180    #[cfg(any(windows, target_os = "macos"))]
181    fn os_error(&mut self, err: Box<dyn StdError + Send + Sync + 'static>, context: &'static str) {
182        self.errors.push(Error {
183            context,
184            kind: ErrorKind::Os(err),
185        });
186    }
187}
188
189/// Certificate paths from `SSL_CERT_FILE` and/or `SSL_CERT_DIR`.
190struct CertPaths {
191    file: Option<PathBuf>,
192    dir: Option<PathBuf>,
193}
194
195impl CertPaths {
196    fn from_env() -> Self {
197        Self {
198            file: env::var_os(ENV_CERT_FILE).map(PathBuf::from),
199            dir: env::var_os(ENV_CERT_DIR).map(PathBuf::from),
200        }
201    }
202
203    /// Load certificates from the paths.
204    ///
205    /// If both are `None`, return `Ok(None)`.
206    ///
207    /// If `self.file` is `Some`, it is always used, so it must be a path to an existing,
208    /// accessible file from which certificates can be loaded successfully. While parsing,
209    /// the rustls-pki-types PEM parser will ignore parts of the file which are
210    /// not considered part of a certificate. Certificates which are not in the right
211    /// format (PEM) or are otherwise corrupted may get ignored silently.
212    ///
213    /// If `self.dir` is defined, a directory must exist at this path, and all
214    /// [hash files](`is_hash_file_name()`) contained in it must be loaded successfully,
215    /// subject to the rules outlined above for `self.file`. The directory is not
216    /// scanned recursively and may be empty.
217    fn load(&self) -> CertificateResult {
218        let mut out = CertificateResult::default();
219        if self.file.is_none() && self.dir.is_none() {
220            return out;
221        }
222
223        if let Some(cert_file) = &self.file {
224            load_pem_certs(cert_file, &mut out);
225        }
226
227        if let Some(cert_dir) = &self.dir {
228            load_pem_certs_from_dir(cert_dir, &mut out);
229        }
230
231        out.certs
232            .sort_unstable_by(|a, b| a.cmp(b));
233        out.certs.dedup();
234        out
235    }
236}
237
238/// Load certificate from certificate directory (what OpenSSL calls CAdir)
239///
240/// This directory can contain other files and directories. CAfile tends
241/// to be in here too. To avoid loading something twice or something that
242/// isn't a valid certificate, we limit ourselves to loading those files
243/// that have a hash-based file name matching the pattern used by OpenSSL.
244/// The hash is not verified, however.
245fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult) {
246    let dir_reader = match fs::read_dir(dir) {
247        Ok(reader) => reader,
248        Err(err) => {
249            out.io_error(err, dir, "opening directory");
250            return;
251        }
252    };
253
254    for entry in dir_reader {
255        let entry = match entry {
256            Ok(entry) => entry,
257            Err(err) => {
258                out.io_error(err, dir, "reading directory entries");
259                continue;
260            }
261        };
262
263        let path = entry.path();
264        let file_name = path
265            .file_name()
266            // We are looping over directory entries. Directory entries
267            // always have a name (except "." and ".." which the iterator
268            // never yields).
269            .expect("dir entry with no name");
270
271        // `openssl rehash` used to create this directory uses symlinks. So,
272        // make sure we 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() && is_hash_file_name(file_name) {
286            load_pem_certs(&path, out);
287        }
288    }
289}
290
291fn load_pem_certs(path: &Path, out: &mut CertificateResult) {
292    let iter = match CertificateDer::pem_file_iter(path) {
293        Ok(iter) => iter,
294        Err(err) => {
295            out.pem_error(err, path);
296            return;
297        }
298    };
299
300    for result in iter {
301        match result {
302            Ok(cert) => out.certs.push(cert),
303            Err(err) => out.pem_error(err, path),
304        }
305    }
306}
307
308/// Check if this is a hash-based file name for a certificate
309///
310/// According to the [c_rehash man page][]:
311///
312/// > The links created are of the form HHHHHHHH.D, where each H is a hexadecimal
313/// > character and D is a single decimal digit.
314///
315/// `c_rehash` generates lower-case hex digits but this is not clearly documented.
316/// Because of this, and because it could lead to issues on case-insensitive file
317/// systems, upper-case hex digits are accepted too.
318///
319/// [c_rehash man page]: https://www.openssl.org/docs/manmaster/man1/c_rehash.html
320fn is_hash_file_name(file_name: &OsStr) -> bool {
321    let file_name = match file_name.to_str() {
322        Some(file_name) => file_name,
323        None => return false, // non-UTF8 can't be hex digits
324    };
325
326    if file_name.len() != 10 {
327        return false;
328    }
329    let mut iter = file_name.chars();
330    let iter = iter.by_ref();
331    iter.take(8)
332        .all(|c| c.is_ascii_hexdigit())
333        && iter.next() == Some('.')
334        && matches!(iter.next(), Some(c) if c.is_ascii_digit())
335}
336
337#[derive(Debug)]
338pub struct Error {
339    pub context: &'static str,
340    pub kind: ErrorKind,
341}
342
343impl StdError for Error {
344    fn source(&self) -> Option<&(dyn StdError + 'static)> {
345        Some(match &self.kind {
346            ErrorKind::Io { inner, .. } => inner,
347            ErrorKind::Os(err) => &**err,
348            ErrorKind::Pem(err) => err,
349        })
350    }
351}
352
353impl fmt::Display for Error {
354    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355        f.write_str(self.context)?;
356        f.write_str(": ")?;
357        match &self.kind {
358            ErrorKind::Io { inner, path } => {
359                write!(f, "{inner} at '{}'", path.display())
360            }
361            ErrorKind::Os(err) => err.fmt(f),
362            ErrorKind::Pem(err) => err.fmt(f),
363        }
364    }
365}
366
367#[non_exhaustive]
368#[derive(Debug)]
369pub enum ErrorKind {
370    Io { inner: io::Error, path: PathBuf },
371    Os(Box<dyn StdError + Send + Sync + 'static>),
372    Pem(pem::Error),
373}
374
375const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
376const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    use std::fs::File;
383    #[cfg(unix)]
384    use std::fs::Permissions;
385    use std::io::Write;
386    #[cfg(unix)]
387    use std::os::unix::fs::PermissionsExt;
388
389    #[test]
390    fn valid_hash_file_name() {
391        let valid_names = [
392            "f3377b1b.0",
393            "e73d606e.1",
394            "01234567.2",
395            "89abcdef.3",
396            "ABCDEF00.9",
397        ];
398        for name in valid_names {
399            assert!(is_hash_file_name(OsStr::new(name)));
400        }
401    }
402
403    #[test]
404    fn invalid_hash_file_name() {
405        let valid_names = [
406            "f3377b1b.a",
407            "e73d606g.1",
408            "0123457.2",
409            "89abcdef0.3",
410            "name.pem",
411        ];
412        for name in valid_names {
413            assert!(!is_hash_file_name(OsStr::new(name)));
414        }
415    }
416
417    #[test]
418    fn deduplication() {
419        let temp_dir = tempfile::TempDir::new().unwrap();
420        let cert1 = include_str!("../tests/badssl-com-chain.pem");
421        let cert2 = include_str!("../integration-tests/one-existing-ca.pem");
422        let file_path = temp_dir
423            .path()
424            .join("ca-certificates.crt");
425        let dir_path = temp_dir.path().to_path_buf();
426
427        {
428            let mut file = File::create(&file_path).unwrap();
429            write!(file, "{}", &cert1).unwrap();
430            write!(file, "{}", &cert2).unwrap();
431        }
432
433        {
434            // Duplicate (already in `file_path`)
435            let mut file = File::create(dir_path.join("71f3bb26.0")).unwrap();
436            write!(file, "{}", &cert1).unwrap();
437        }
438
439        {
440            // Duplicate (already in `file_path`)
441            let mut file = File::create(dir_path.join("912e7cd5.0")).unwrap();
442            write!(file, "{}", &cert2).unwrap();
443        }
444
445        let result = CertPaths {
446            file: Some(file_path.clone()),
447            dir: None,
448        }
449        .load();
450        assert_eq!(result.certs.len(), 2);
451
452        let result = CertPaths {
453            file: None,
454            dir: Some(dir_path.clone()),
455        }
456        .load();
457        assert_eq!(result.certs.len(), 2);
458
459        let result = CertPaths {
460            file: Some(file_path),
461            dir: Some(dir_path),
462        }
463        .load();
464        assert_eq!(result.certs.len(), 2);
465    }
466
467    #[test]
468    fn malformed_file_from_env() {
469        // Certificate parser tries to extract certs from file ignoring
470        // invalid sections.
471        let mut result = CertificateResult::default();
472        load_pem_certs(Path::new(file!()), &mut result);
473        assert_eq!(result.certs.len(), 0);
474        assert!(result.errors.is_empty());
475    }
476
477    #[test]
478    fn from_env_missing_file() {
479        let mut result = CertificateResult::default();
480        load_pem_certs(Path::new("no/such/file"), &mut result);
481        match &first_error(&result).kind {
482            ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
483            _ => panic!("unexpected error {:?}", result.errors),
484        }
485    }
486
487    #[test]
488    fn from_env_missing_dir() {
489        let mut result = CertificateResult::default();
490        load_pem_certs_from_dir(Path::new("no/such/directory"), &mut result);
491        match &first_error(&result).kind {
492            ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
493            _ => panic!("unexpected error {:?}", result.errors),
494        }
495    }
496
497    #[test]
498    #[cfg(unix)]
499    fn from_env_with_non_regular_and_empty_file() {
500        let mut result = CertificateResult::default();
501        load_pem_certs(Path::new("/dev/null"), &mut result);
502        assert_eq!(result.certs.len(), 0);
503        assert!(result.errors.is_empty());
504    }
505
506    #[test]
507    #[cfg(unix)]
508    fn from_env_bad_dir_perms() {
509        // Create a temp dir that we can't read from.
510        let temp_dir = tempfile::TempDir::new().unwrap();
511        fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o000)).unwrap();
512
513        test_cert_paths_bad_perms(CertPaths {
514            file: None,
515            dir: Some(temp_dir.path().into()),
516        })
517    }
518
519    #[test]
520    #[cfg(unix)]
521    fn from_env_bad_file_perms() {
522        // Create a tmp dir with a file inside that we can't read from.
523        let temp_dir = tempfile::TempDir::new().unwrap();
524        let file_path = temp_dir.path().join("unreadable.pem");
525        let cert_file = File::create(&file_path).unwrap();
526        cert_file
527            .set_permissions(Permissions::from_mode(0o000))
528            .unwrap();
529
530        test_cert_paths_bad_perms(CertPaths {
531            file: Some(file_path.clone()),
532            dir: None,
533        });
534    }
535
536    #[cfg(unix)]
537    fn test_cert_paths_bad_perms(cert_paths: CertPaths) {
538        let result = cert_paths.load();
539
540        if let (None, None) = (cert_paths.file, cert_paths.dir) {
541            panic!("only one of file or dir should be set");
542        };
543
544        let error = first_error(&result);
545        match &error.kind {
546            ErrorKind::Io { inner, .. } => {
547                assert_eq!(inner.kind(), io::ErrorKind::PermissionDenied);
548                inner
549            }
550            _ => panic!("unexpected error {:?}", result.errors),
551        };
552    }
553
554    fn first_error(result: &CertificateResult) -> &Error {
555        result.errors.first().unwrap()
556    }
557}