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::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).collect(),
207                None => Vec::new(),
208            },
209        }
210    }
211
212    /// Load certificates from the paths.
213    ///
214    /// See [`load_certs_from_paths()`].
215    fn load(&self) -> CertificateResult {
216        load_certs_from_paths_internal(self.file.as_deref(), &self.dirs)
217    }
218}
219
220/// Load certificates from the given paths.
221///
222/// If both are `None`, returns an empty [`CertificateResult`].
223///
224/// If `file` is `Some`, it is always used, so it must be a path to an existing,
225/// accessible file from which certificates can be loaded successfully. While parsing,
226/// the rustls-pki-types PEM parser will ignore parts of the file which are
227/// not considered part of a certificate. Certificates which are not in the right
228/// format (PEM) or are otherwise corrupted may get ignored silently.
229///
230/// If `dir` is defined, a directory must exist at this path, and all files
231/// contained in it must be loaded successfully, subject to the rules outlined above for `file`.
232/// The directory is not scanned recursively and may be empty.
233pub fn load_certs_from_paths(file: Option<&Path>, dir: Option<&Path>) -> CertificateResult {
234    let dir = match dir {
235        Some(d) => vec![d],
236        None => Vec::new(),
237    };
238
239    load_certs_from_paths_internal(file, dir.as_ref())
240}
241
242fn load_certs_from_paths_internal(
243    file: Option<&Path>,
244    dir: &[impl AsRef<Path>],
245) -> CertificateResult {
246    let mut out = CertificateResult::default();
247    if file.is_none() && dir.is_empty() {
248        return out;
249    }
250
251    if let Some(cert_file) = file {
252        load_pem_certs(cert_file, &mut out);
253    }
254
255    for cert_dir in dir.iter() {
256        load_pem_certs_from_dir(cert_dir.as_ref(), &mut out);
257    }
258
259    out.certs
260        .sort_unstable_by(|a, b| a.cmp(b));
261    out.certs.dedup();
262    out
263}
264
265/// Load certificate from certificate directory (what OpenSSL calls CAdir)
266fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult) {
267    let dir_reader = match fs::read_dir(dir) {
268        Ok(reader) => reader,
269        Err(err) => {
270            out.io_error(err, dir, "opening directory");
271            return;
272        }
273    };
274
275    for entry in dir_reader {
276        let entry = match entry {
277            Ok(entry) => entry,
278            Err(err) => {
279                out.io_error(err, dir, "reading directory entries");
280                continue;
281            }
282        };
283
284        let path = entry.path();
285
286        // `openssl rehash` used to create this directory uses symlinks. So,
287        // make sure we resolve them.
288        let metadata = match fs::metadata(&path) {
289            Ok(metadata) => metadata,
290            Err(e) if e.kind() == io::ErrorKind::NotFound => {
291                // Dangling symlink
292                continue;
293            }
294            Err(e) => {
295                out.io_error(e, &path, "failed to open file");
296                continue;
297            }
298        };
299
300        if metadata.is_file() {
301            load_pem_certs(&path, out);
302        }
303    }
304}
305
306fn load_pem_certs(path: &Path, out: &mut CertificateResult) {
307    let iter = match CertificateDer::pem_file_iter(path) {
308        Ok(iter) => iter,
309        Err(err) => {
310            out.pem_error(err, path);
311            return;
312        }
313    };
314
315    for result in iter {
316        match result {
317            Ok(cert) => out.certs.push(cert),
318            Err(err) => out.pem_error(err, path),
319        }
320    }
321}
322
323#[derive(Debug)]
324pub struct Error {
325    pub context: &'static str,
326    pub kind: ErrorKind,
327}
328
329impl StdError for Error {
330    fn source(&self) -> Option<&(dyn StdError + 'static)> {
331        Some(match &self.kind {
332            ErrorKind::Io { inner, .. } => inner,
333            ErrorKind::Os(err) => &**err,
334            ErrorKind::Pem(err) => err,
335        })
336    }
337}
338
339impl fmt::Display for Error {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        f.write_str(self.context)?;
342        f.write_str(": ")?;
343        match &self.kind {
344            ErrorKind::Io { inner, path } => {
345                write!(f, "{inner} at '{}'", path.display())
346            }
347            ErrorKind::Os(err) => err.fmt(f),
348            ErrorKind::Pem(err) => err.fmt(f),
349        }
350    }
351}
352
353#[non_exhaustive]
354#[derive(Debug)]
355pub enum ErrorKind {
356    Io { inner: io::Error, path: PathBuf },
357    Os(Box<dyn StdError + Send + Sync + 'static>),
358    Pem(pem::Error),
359}
360
361const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
362const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    use std::fs::File;
369    #[cfg(unix)]
370    use std::fs::Permissions;
371    use std::io::Write;
372    #[cfg(unix)]
373    use std::os::unix::fs::PermissionsExt;
374
375    #[test]
376    fn deduplication() {
377        let temp_dir = tempfile::TempDir::new().unwrap();
378        let cert1 = include_str!("../tests/badssl-com-chain.pem");
379        let cert2 = include_str!("../integration-tests/one-existing-ca.pem");
380        let file_path = temp_dir
381            .path()
382            .join("ca-certificates.crt");
383        let dir_path = temp_dir.path().to_path_buf();
384
385        {
386            let mut file = File::create(&file_path).unwrap();
387            write!(file, "{}", &cert1).unwrap();
388            write!(file, "{}", &cert2).unwrap();
389        }
390
391        {
392            // Duplicate (already in `file_path`)
393            let mut file = File::create(dir_path.join("71f3bb26.0")).unwrap();
394            write!(file, "{}", &cert1).unwrap();
395        }
396
397        {
398            // Duplicate (already in `file_path`)
399            let mut file = File::create(dir_path.join("912e7cd5.0")).unwrap();
400            write!(file, "{}", &cert2).unwrap();
401        }
402
403        let result = CertPaths {
404            file: Some(file_path.clone()),
405            dirs: vec![],
406        }
407        .load();
408        assert_eq!(result.certs.len(), 2);
409
410        let result = CertPaths {
411            file: None,
412            dirs: vec![dir_path.clone()],
413        }
414        .load();
415        assert_eq!(result.certs.len(), 2);
416
417        let result = CertPaths {
418            file: Some(file_path),
419            dirs: vec![dir_path],
420        }
421        .load();
422        assert_eq!(result.certs.len(), 2);
423    }
424
425    #[test]
426    fn malformed_file_from_env() {
427        // Certificate parser tries to extract certs from file ignoring
428        // invalid sections.
429        let mut result = CertificateResult::default();
430        load_pem_certs(Path::new(file!()), &mut result);
431        assert_eq!(result.certs.len(), 0);
432        assert!(result.errors.is_empty());
433    }
434
435    #[test]
436    fn from_env_missing_file() {
437        let mut result = CertificateResult::default();
438        load_pem_certs(Path::new("no/such/file"), &mut result);
439        match &first_error(&result).kind {
440            ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
441            _ => panic!("unexpected error {:?}", result.errors),
442        }
443    }
444
445    #[test]
446    fn from_env_missing_dir() {
447        let mut result = CertificateResult::default();
448        load_pem_certs_from_dir(Path::new("no/such/directory"), &mut result);
449        match &first_error(&result).kind {
450            ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
451            _ => panic!("unexpected error {:?}", result.errors),
452        }
453    }
454
455    #[test]
456    #[cfg(unix)]
457    fn from_env_with_non_regular_and_empty_file() {
458        let mut result = CertificateResult::default();
459        load_pem_certs(Path::new("/dev/null"), &mut result);
460        assert_eq!(result.certs.len(), 0);
461        assert!(result.errors.is_empty());
462    }
463
464    #[test]
465    #[cfg(unix)]
466    fn from_env_bad_dir_perms() {
467        // Create a temp dir that we can't read from.
468        let temp_dir = tempfile::TempDir::new().unwrap();
469        fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o000)).unwrap();
470
471        test_cert_paths_bad_perms(CertPaths {
472            file: None,
473            dirs: vec![temp_dir.path().into()],
474        })
475    }
476
477    #[test]
478    #[cfg(unix)]
479    fn from_env_bad_file_perms() {
480        // Create a tmp dir with a file inside that we can't read from.
481        let temp_dir = tempfile::TempDir::new().unwrap();
482        let file_path = temp_dir.path().join("unreadable.pem");
483        let cert_file = File::create(&file_path).unwrap();
484        cert_file
485            .set_permissions(Permissions::from_mode(0o000))
486            .unwrap();
487
488        test_cert_paths_bad_perms(CertPaths {
489            file: Some(file_path.clone()),
490            dirs: vec![],
491        });
492    }
493
494    #[cfg(unix)]
495    fn test_cert_paths_bad_perms(cert_paths: CertPaths) {
496        let result = cert_paths.load();
497
498        if let (None, true) = (cert_paths.file, cert_paths.dirs.is_empty()) {
499            panic!("only one of file or dir should be set");
500        };
501
502        let error = first_error(&result);
503        match &error.kind {
504            ErrorKind::Io { inner, .. } => {
505                assert_eq!(inner.kind(), io::ErrorKind::PermissionDenied);
506                inner
507            }
508            _ => panic!("unexpected error {:?}", result.errors),
509        };
510    }
511
512    fn first_error(result: &CertificateResult) -> &Error {
513        result.errors.first().unwrap()
514    }
515}