Coverage Report

Created: 2026-06-28 08:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/rustls-native-certs-0.8.4/src/lib.rs
Line
Count
Source
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
24
use std::error::Error as StdError;
25
use std::path::{Path, PathBuf};
26
use std::{env, fmt, fs, io};
27
28
use pki_types::pem::{self, PemObject};
29
use pki_types::CertificateDer;
30
31
#[cfg(all(unix, not(target_os = "macos")))]
32
mod unix;
33
#[cfg(all(unix, not(target_os = "macos")))]
34
use unix as platform;
35
36
#[cfg(windows)]
37
mod windows;
38
#[cfg(windows)]
39
use windows as platform;
40
41
#[cfg(target_os = "macos")]
42
mod macos;
43
#[cfg(target_os = "macos")]
44
use 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
118
1
pub fn load_native_certs() -> CertificateResult {
119
1
    let paths = CertPaths::from_env();
120
1
    match (&paths.dirs, &paths.file) {
121
1
        (v, _) if !v.is_empty() => paths.load(),
122
0
        (_, Some(_)) => paths.load(),
123
1
        _ => platform::load_native_certs(),
124
    }
125
1
}
126
127
/// Results from trying to load certificates from the platform's native store.
128
#[non_exhaustive]
129
#[derive(Debug, Default)]
130
pub 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
137
impl CertificateResult {
138
    /// Return the found certificates if no error occurred, otherwise panic.
139
    #[track_caller]
140
0
    pub fn expect(self, msg: &str) -> Vec<CertificateDer<'static>> {
141
0
        match self.errors.is_empty() {
142
0
            true => self.certs,
143
0
            false => panic!("{msg}: {:?}", self.errors),
144
        }
145
0
    }
146
147
    /// Return the found certificates if no error occurred, otherwise panic.
148
    #[track_caller]
149
0
    pub fn unwrap(self) -> Vec<CertificateDer<'static>> {
150
0
        match self.errors.is_empty() {
151
0
            true => self.certs,
152
0
            false => panic!(
153
0
                "errors occurred while loading certificates: {:?}",
154
                self.errors
155
            ),
156
        }
157
0
    }
158
159
0
    fn pem_error(&mut self, err: pem::Error, path: &Path) {
160
0
        self.errors.push(Error {
161
            context: "failed to read PEM from file",
162
0
            kind: match err {
163
0
                pem::Error::Io(err) => ErrorKind::Io {
164
0
                    inner: err,
165
0
                    path: path.to_owned(),
166
0
                },
167
0
                _ => ErrorKind::Pem(err),
168
            },
169
        });
170
0
    }
171
172
0
    fn io_error(&mut self, err: io::Error, path: &Path, context: &'static str) {
173
0
        self.errors.push(Error {
174
0
            context,
175
0
            kind: ErrorKind::Io {
176
0
                inner: err,
177
0
                path: path.to_owned(),
178
0
            },
179
0
        });
180
0
    }
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`.
192
struct CertPaths {
193
    file: Option<PathBuf>,
194
    dirs: Vec<PathBuf>,
195
}
196
197
impl CertPaths {
198
1
    fn from_env() -> Self {
199
        Self {
200
1
            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
1
            dirs: match env::var_os(ENV_CERT_DIR) {
206
0
                Some(dirs) => env::split_paths(&dirs)
207
0
                    .filter(|p| !p.as_os_str().is_empty())
208
0
                    .collect(),
209
1
                None => Vec::new(),
210
            },
211
        }
212
1
    }
213
214
    /// Load certificates from the paths.
215
    ///
216
    /// See [`load_certs_from_paths()`].
217
1
    fn load(&self) -> CertificateResult {
218
1
        load_certs_from_paths_internal(self.file.as_deref(), &self.dirs)
219
1
    }
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.
235
0
pub fn load_certs_from_paths(file: Option<&Path>, dir: Option<&Path>) -> CertificateResult {
236
0
    let dir = match dir {
237
0
        Some(d) => vec![d],
238
0
        None => Vec::new(),
239
    };
240
241
0
    load_certs_from_paths_internal(file, dir.as_ref())
242
0
}
243
244
1
fn load_certs_from_paths_internal(
245
1
    file: Option<&Path>,
246
1
    dir: &[impl AsRef<Path>],
247
1
) -> CertificateResult {
248
1
    let mut out = CertificateResult::default();
249
1
    if file.is_none() && dir.is_empty() {
250
0
        return out;
251
1
    }
252
253
1
    if let Some(cert_file) = file {
254
1
        load_pem_certs(cert_file, &mut out);
255
1
    }
256
257
1
    for cert_dir in dir.iter() {
258
1
        load_pem_certs_from_dir(cert_dir.as_ref(), &mut out);
259
1
    }
260
261
1
    out.certs
262
5.59k
        .sort_unstable_by(|a, b| a.cmp(b));
rustls_native_certs::load_certs_from_paths_internal::<std::path::PathBuf>::{closure#0}
Line
Count
Source
262
5.59k
        .sort_unstable_by(|a, b| a.cmp(b));
Unexecuted instantiation: rustls_native_certs::load_certs_from_paths_internal::<&std::path::Path>::{closure#0}
263
1
    out.certs.dedup();
264
1
    out
265
1
}
rustls_native_certs::load_certs_from_paths_internal::<std::path::PathBuf>
Line
Count
Source
244
1
fn load_certs_from_paths_internal(
245
1
    file: Option<&Path>,
246
1
    dir: &[impl AsRef<Path>],
247
1
) -> CertificateResult {
248
1
    let mut out = CertificateResult::default();
249
1
    if file.is_none() && dir.is_empty() {
250
0
        return out;
251
1
    }
252
253
1
    if let Some(cert_file) = file {
254
1
        load_pem_certs(cert_file, &mut out);
255
1
    }
256
257
1
    for cert_dir in dir.iter() {
258
1
        load_pem_certs_from_dir(cert_dir.as_ref(), &mut out);
259
1
    }
260
261
1
    out.certs
262
1
        .sort_unstable_by(|a, b| a.cmp(b));
263
1
    out.certs.dedup();
264
1
    out
265
1
}
Unexecuted instantiation: rustls_native_certs::load_certs_from_paths_internal::<&std::path::Path>
266
267
/// Load certificate from certificate directory (what OpenSSL calls CAdir)
268
1
fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult) {
269
1
    let dir_reader = match fs::read_dir(dir) {
270
1
        Ok(reader) => reader,
271
0
        Err(err) => {
272
0
            out.io_error(err, dir, "opening directory");
273
0
            return;
274
        }
275
    };
276
277
294
    for entry in dir_reader {
278
293
        let entry = match entry {
279
293
            Ok(entry) => entry,
280
0
            Err(err) => {
281
0
                out.io_error(err, dir, "reading directory entries");
282
0
                continue;
283
            }
284
        };
285
286
293
        let path = entry.path();
287
288
        // `openssl rehash` used to create this directory uses symlinks. So,
289
        // make sure we resolve them.
290
293
        let metadata = match fs::metadata(&path) {
291
293
            Ok(metadata) => metadata,
292
0
            Err(e) if e.kind() == io::ErrorKind::NotFound => {
293
                // Dangling symlink
294
0
                continue;
295
            }
296
0
            Err(e) => {
297
0
                out.io_error(e, &path, "failed to open file");
298
0
                continue;
299
            }
300
        };
301
302
293
        if metadata.is_file() {
303
293
            load_pem_certs(&path, out);
304
293
        }
305
    }
306
1
}
307
308
294
fn load_pem_certs(path: &Path, out: &mut CertificateResult) {
309
294
    let iter = match CertificateDer::pem_file_iter(path) {
310
294
        Ok(iter) => iter,
311
0
        Err(err) => {
312
0
            out.pem_error(err, path);
313
0
            return;
314
        }
315
    };
316
317
878
    for result in iter {
318
584
        match result {
319
584
            Ok(cert) => out.certs.push(cert),
320
0
            Err(err) => out.pem_error(err, path),
321
        }
322
    }
323
294
}
324
325
#[derive(Debug)]
326
pub struct Error {
327
    pub context: &'static str,
328
    pub kind: ErrorKind,
329
}
330
331
impl StdError for Error {
332
0
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
333
0
        Some(match &self.kind {
334
0
            ErrorKind::Io { inner, .. } => inner,
335
0
            ErrorKind::Os(err) => &**err,
336
0
            ErrorKind::Pem(err) => err,
337
        })
338
0
    }
339
}
340
341
impl fmt::Display for Error {
342
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343
0
        f.write_str(self.context)?;
344
0
        f.write_str(": ")?;
345
0
        match &self.kind {
346
0
            ErrorKind::Io { inner, path } => {
347
0
                write!(f, "{inner} at '{}'", path.display())
348
            }
349
0
            ErrorKind::Os(err) => err.fmt(f),
350
0
            ErrorKind::Pem(err) => err.fmt(f),
351
        }
352
0
    }
353
}
354
355
#[non_exhaustive]
356
#[derive(Debug)]
357
pub enum ErrorKind {
358
    Io { inner: io::Error, path: PathBuf },
359
    Os(Box<dyn StdError + Send + Sync + 'static>),
360
    Pem(pem::Error),
361
}
362
363
const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
364
const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
365
366
#[cfg(test)]
367
mod 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
}