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