1#![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
46pub 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#[non_exhaustive]
129#[derive(Debug, Default)]
130pub struct CertificateResult {
131 pub certs: Vec<CertificateDer<'static>>,
133 pub errors: Vec<Error>,
135}
136
137impl CertificateResult {
138 #[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 #[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
191struct 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 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 fn load(&self) -> CertificateResult {
218 load_certs_from_paths_internal(self.file.as_deref(), &self.dirs)
219 }
220}
221
222pub fn load_certs_from_paths(file: Option<&Path>, dir: Option<&Path>) -> CertificateResult {
240 let dir = match dir {
241 Some(d) => vec![d],
242 None => Vec::new(),
243 };
244
245 load_certs_from_paths_internal(file, dir.as_ref())
246}
247
248fn load_certs_from_paths_internal(
249 file: Option<&Path>,
250 dir: &[impl AsRef<Path>],
251) -> CertificateResult {
252 let mut out = CertificateResult::default();
253 if file.is_none() && dir.is_empty() {
254 return out;
255 }
256
257 let mut seen = SeenFiles::default();
258 if let Some(cert_file) = file {
259 if let Ok(metadata) = fs::metadata(cert_file) {
260 seen.first_visit(&metadata);
261 }
262 load_pem_certs(cert_file, &mut out);
263 }
264
265 for cert_dir in dir.iter() {
266 load_pem_certs_from_dir(cert_dir.as_ref(), &mut out, &mut seen);
267 }
268
269 out.certs
270 .sort_unstable_by(|a, b| a.cmp(b));
271 out.certs.dedup();
272 out
273}
274
275#[derive(Default)]
280struct SeenFiles {
281 #[cfg(unix)]
283 ids: std::collections::HashSet<(u64, u64)>,
284}
285
286impl SeenFiles {
287 fn first_visit(&mut self, metadata: &fs::Metadata) -> bool {
291 #[cfg(unix)]
292 {
293 use std::os::unix::fs::MetadataExt;
294 self.ids
295 .insert((metadata.dev(), metadata.ino()))
296 }
297 #[cfg(not(unix))]
298 {
299 let _ = metadata;
300 true
301 }
302 }
303}
304
305fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult, seen: &mut SeenFiles) {
307 let dir_reader = match fs::read_dir(dir) {
308 Ok(reader) => reader,
309 Err(err) => {
310 out.io_error(err, dir, "opening directory");
311 return;
312 }
313 };
314
315 for entry in dir_reader {
316 let entry = match entry {
317 Ok(entry) => entry,
318 Err(err) => {
319 out.io_error(err, dir, "reading directory entries");
320 continue;
321 }
322 };
323
324 let path = entry.path();
325
326 let metadata = match fs::metadata(&path) {
329 Ok(metadata) => metadata,
330 Err(e) if e.kind() == io::ErrorKind::NotFound => {
331 continue;
333 }
334 Err(e) => {
335 out.io_error(e, &path, "failed to open file");
336 continue;
337 }
338 };
339
340 if metadata.is_file() && seen.first_visit(&metadata) {
341 load_pem_certs(&path, out);
342 }
343 }
344}
345
346fn load_pem_certs(path: &Path, out: &mut CertificateResult) {
347 let iter = match CertificateDer::pem_file_iter(path) {
348 Ok(iter) => iter,
349 Err(err) => {
350 out.pem_error(err, path);
351 return;
352 }
353 };
354
355 for result in iter {
356 match result {
357 Ok(cert) => out.certs.push(cert),
358 Err(err) => out.pem_error(err, path),
359 }
360 }
361}
362
363#[derive(Debug)]
364pub struct Error {
365 pub context: &'static str,
366 pub kind: ErrorKind,
367}
368
369impl StdError for Error {
370 fn source(&self) -> Option<&(dyn StdError + 'static)> {
371 Some(match &self.kind {
372 ErrorKind::Io { inner, .. } => inner,
373 ErrorKind::Os(err) => &**err,
374 ErrorKind::Pem(err) => err,
375 })
376 }
377}
378
379impl fmt::Display for Error {
380 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
381 f.write_str(self.context)?;
382 f.write_str(": ")?;
383 match &self.kind {
384 ErrorKind::Io { inner, path } => {
385 write!(f, "{inner} at '{}'", path.display())
386 }
387 ErrorKind::Os(err) => err.fmt(f),
388 ErrorKind::Pem(err) => err.fmt(f),
389 }
390 }
391}
392
393#[non_exhaustive]
394#[derive(Debug)]
395pub enum ErrorKind {
396 Io { inner: io::Error, path: PathBuf },
397 Os(Box<dyn StdError + Send + Sync + 'static>),
398 Pem(pem::Error),
399}
400
401const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
402const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 use std::fs::File;
409 #[cfg(unix)]
410 use std::fs::Permissions;
411 use std::io::Write;
412 #[cfg(unix)]
413 use std::os::unix::fs::PermissionsExt;
414
415 #[test]
416 fn deduplication() {
417 let temp_dir = tempfile::TempDir::new().unwrap();
418 let cert1 = include_str!("../tests/badssl-com-chain.pem");
419 let cert2 = include_str!("../integration-tests/one-existing-ca.pem");
420 let file_path = temp_dir
421 .path()
422 .join("ca-certificates.crt");
423 let dir_path = temp_dir.path().to_path_buf();
424
425 {
426 let mut file = File::create(&file_path).unwrap();
427 write!(file, "{}", cert1).unwrap();
428 write!(file, "{}", cert2).unwrap();
429 }
430
431 {
432 let mut file = File::create(dir_path.join("71f3bb26.0")).unwrap();
434 write!(file, "{}", cert1).unwrap();
435 }
436
437 {
438 let mut file = File::create(dir_path.join("912e7cd5.0")).unwrap();
440 write!(file, "{}", cert2).unwrap();
441 }
442
443 let result = CertPaths {
444 file: Some(file_path.clone()),
445 dirs: vec![],
446 }
447 .load();
448 assert_eq!(result.certs.len(), 2);
449
450 let result = CertPaths {
451 file: None,
452 dirs: vec![dir_path.clone()],
453 }
454 .load();
455 assert_eq!(result.certs.len(), 2);
456
457 let result = CertPaths {
458 file: Some(file_path),
459 dirs: vec![dir_path],
460 }
461 .load();
462 assert_eq!(result.certs.len(), 2);
463 }
464
465 #[cfg(unix)]
467 const MALFORMED_PEM: &str = "-----BEGIN CERTIFICATE-----\n!!!\n-----END CERTIFICATE-----\n";
468
469 #[test]
470 #[cfg(unix)]
471 fn dir_symlinks_to_same_file_read_once() {
472 use std::os::unix::fs::symlink;
473
474 let temp_dir = tempfile::TempDir::new().unwrap();
475 let dir = temp_dir.path();
476 fs::write(dir.join("Example_Root.pem"), MALFORMED_PEM).unwrap();
477 symlink("Example_Root.pem", dir.join("01234567.0")).unwrap();
478 symlink(dir.join("Example_Root.pem"), dir.join("89abcdef.0")).unwrap();
479
480 let result = load_certs_from_paths_internal(None, &[dir]);
481 assert_eq!(result.errors.len(), 1, "{:?}", result.errors);
482 }
483
484 #[test]
485 #[cfg(unix)]
486 fn dir_entry_for_cert_file_skipped() {
487 use std::os::unix::fs::symlink;
488
489 let file_dir = tempfile::TempDir::new().unwrap();
490 let cert_dir = tempfile::TempDir::new().unwrap();
491 let file = file_dir.path().join("ca-bundle.crt");
492 fs::write(&file, MALFORMED_PEM).unwrap();
493 symlink(&file, cert_dir.path().join("ca-bundle.crt")).unwrap();
494
495 let result = load_certs_from_paths_internal(Some(&file), &[cert_dir.path()]);
496 assert_eq!(result.errors.len(), 1, "{:?}", result.errors);
497 }
498
499 #[test]
500 #[cfg(unix)]
501 fn dir_copies_with_same_content_all_read() {
502 let temp_dir = tempfile::TempDir::new().unwrap();
503 fs::write(temp_dir.path().join("a.pem"), MALFORMED_PEM).unwrap();
504 fs::write(temp_dir.path().join("b.pem"), MALFORMED_PEM).unwrap();
505
506 let result = load_certs_from_paths_internal(None, &[temp_dir.path()]);
507 assert_eq!(result.errors.len(), 2, "{:?}", result.errors);
508 }
509
510 #[test]
511 #[cfg(unix)]
512 fn dir_symlinks_still_load_certs() {
513 use std::os::unix::fs::symlink;
514
515 let temp_dir = tempfile::TempDir::new().unwrap();
516 let dir = temp_dir.path();
517 fs::write(
518 dir.join("chain.pem"),
519 include_str!("../tests/badssl-com-chain.pem"),
520 )
521 .unwrap();
522 symlink("chain.pem", dir.join("71f3bb26.0")).unwrap();
523
524 let result = load_certs_from_paths_internal(None, &[dir]);
525 assert!(result.errors.is_empty(), "{:?}", result.errors);
526 assert_eq!(result.certs.len(), 1);
527 }
528
529 #[test]
530 fn malformed_file_from_env() {
531 let mut result = CertificateResult::default();
534 load_pem_certs(Path::new(file!()), &mut result);
535 assert_eq!(result.certs.len(), 0);
536 assert!(result.errors.is_empty());
537 }
538
539 #[test]
540 fn from_env_missing_file() {
541 let mut result = CertificateResult::default();
542 load_pem_certs(Path::new("no/such/file"), &mut result);
543 match &first_error(&result).kind {
544 ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
545 _ => panic!("unexpected error {:?}", result.errors),
546 }
547 }
548
549 #[test]
550 fn from_env_missing_dir() {
551 let mut result = CertificateResult::default();
552 load_pem_certs_from_dir(
553 Path::new("no/such/directory"),
554 &mut result,
555 &mut SeenFiles::default(),
556 );
557 match &first_error(&result).kind {
558 ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
559 _ => panic!("unexpected error {:?}", result.errors),
560 }
561 }
562
563 #[test]
564 #[cfg(unix)]
565 fn from_env_with_non_regular_and_empty_file() {
566 let mut result = CertificateResult::default();
567 load_pem_certs(Path::new("/dev/null"), &mut result);
568 assert_eq!(result.certs.len(), 0);
569 assert!(result.errors.is_empty());
570 }
571
572 #[test]
573 #[cfg(unix)]
574 fn from_env_bad_dir_perms() {
575 let temp_dir = tempfile::TempDir::new().unwrap();
577 fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o000)).unwrap();
578
579 test_cert_paths_bad_perms(CertPaths {
580 file: None,
581 dirs: vec![temp_dir.path().into()],
582 })
583 }
584
585 #[test]
586 #[cfg(unix)]
587 fn from_env_bad_file_perms() {
588 let temp_dir = tempfile::TempDir::new().unwrap();
590 let file_path = temp_dir.path().join("unreadable.pem");
591 let cert_file = File::create(&file_path).unwrap();
592 cert_file
593 .set_permissions(Permissions::from_mode(0o000))
594 .unwrap();
595
596 test_cert_paths_bad_perms(CertPaths {
597 file: Some(file_path.clone()),
598 dirs: vec![],
599 });
600 }
601
602 #[cfg(unix)]
603 fn test_cert_paths_bad_perms(cert_paths: CertPaths) {
604 let result = cert_paths.load();
605
606 if let (None, true) = (cert_paths.file, cert_paths.dirs.is_empty()) {
607 panic!("only one of file or dir should be set");
608 };
609
610 let error = first_error(&result);
611 match &error.kind {
612 ErrorKind::Io { inner, .. } => {
613 assert_eq!(inner.kind(), io::ErrorKind::PermissionDenied);
614 inner
615 }
616 _ => panic!("unexpected error {:?}", result.errors),
617 };
618 }
619
620 fn first_error(result: &CertificateResult) -> &Error {
621 result.errors.first().unwrap()
622 }
623}