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