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}