1#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
23
24use std::error::Error as StdError;
25use std::ffi::OsStr;
26use std::path::{Path, PathBuf};
27use std::{env, fmt, fs, io};
28
29use pki_types::pem::{self, PemObject};
30use pki_types::CertificateDer;
31
32#[cfg(all(unix, not(target_os = "macos")))]
33mod unix;
34#[cfg(all(unix, not(target_os = "macos")))]
35use unix as platform;
36
37#[cfg(windows)]
38mod windows;
39#[cfg(windows)]
40use windows as platform;
41
42#[cfg(target_os = "macos")]
43mod macos;
44#[cfg(target_os = "macos")]
45use macos as platform;
46
47pub fn load_native_certs() -> CertificateResult {
120 let paths = CertPaths::from_env();
121 match (&paths.dir, &paths.file) {
122 (Some(_), _) | (_, Some(_)) => paths.load(),
123 (None, None) => 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 pub fn expect(self, msg: &str) -> Vec<CertificateDer<'static>> {
140 match self.errors.is_empty() {
141 true => self.certs,
142 false => panic!("{msg}: {:?}", self.errors),
143 }
144 }
145
146 pub fn unwrap(self) -> Vec<CertificateDer<'static>> {
148 match self.errors.is_empty() {
149 true => self.certs,
150 false => panic!(
151 "errors occurred while loading certificates: {:?}",
152 self.errors
153 ),
154 }
155 }
156
157 fn pem_error(&mut self, err: pem::Error, path: &Path) {
158 self.errors.push(Error {
159 context: "failed to read PEM from file",
160 kind: match err {
161 pem::Error::Io(err) => ErrorKind::Io {
162 inner: err,
163 path: path.to_owned(),
164 },
165 _ => ErrorKind::Pem(err),
166 },
167 });
168 }
169
170 fn io_error(&mut self, err: io::Error, path: &Path, context: &'static str) {
171 self.errors.push(Error {
172 context,
173 kind: ErrorKind::Io {
174 inner: err,
175 path: path.to_owned(),
176 },
177 });
178 }
179
180 #[cfg(any(windows, target_os = "macos"))]
181 fn os_error(&mut self, err: Box<dyn StdError + Send + Sync + 'static>, context: &'static str) {
182 self.errors.push(Error {
183 context,
184 kind: ErrorKind::Os(err),
185 });
186 }
187}
188
189struct CertPaths {
191 file: Option<PathBuf>,
192 dir: Option<PathBuf>,
193}
194
195impl CertPaths {
196 fn from_env() -> Self {
197 Self {
198 file: env::var_os(ENV_CERT_FILE).map(PathBuf::from),
199 dir: env::var_os(ENV_CERT_DIR).map(PathBuf::from),
200 }
201 }
202
203 fn load(&self) -> CertificateResult {
218 let mut out = CertificateResult::default();
219 if self.file.is_none() && self.dir.is_none() {
220 return out;
221 }
222
223 if let Some(cert_file) = &self.file {
224 load_pem_certs(cert_file, &mut out);
225 }
226
227 if let Some(cert_dir) = &self.dir {
228 load_pem_certs_from_dir(cert_dir, &mut out);
229 }
230
231 out.certs
232 .sort_unstable_by(|a, b| a.cmp(b));
233 out.certs.dedup();
234 out
235 }
236}
237
238fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult) {
246 let dir_reader = match fs::read_dir(dir) {
247 Ok(reader) => reader,
248 Err(err) => {
249 out.io_error(err, dir, "opening directory");
250 return;
251 }
252 };
253
254 for entry in dir_reader {
255 let entry = match entry {
256 Ok(entry) => entry,
257 Err(err) => {
258 out.io_error(err, dir, "reading directory entries");
259 continue;
260 }
261 };
262
263 let path = entry.path();
264 let file_name = path
265 .file_name()
266 .expect("dir entry with no name");
270
271 let metadata = match fs::metadata(&path) {
274 Ok(metadata) => metadata,
275 Err(e) if e.kind() == io::ErrorKind::NotFound => {
276 continue;
278 }
279 Err(e) => {
280 out.io_error(e, &path, "failed to open file");
281 continue;
282 }
283 };
284
285 if metadata.is_file() && is_hash_file_name(file_name) {
286 load_pem_certs(&path, out);
287 }
288 }
289}
290
291fn load_pem_certs(path: &Path, out: &mut CertificateResult) {
292 let iter = match CertificateDer::pem_file_iter(path) {
293 Ok(iter) => iter,
294 Err(err) => {
295 out.pem_error(err, path);
296 return;
297 }
298 };
299
300 for result in iter {
301 match result {
302 Ok(cert) => out.certs.push(cert),
303 Err(err) => out.pem_error(err, path),
304 }
305 }
306}
307
308fn is_hash_file_name(file_name: &OsStr) -> bool {
321 let file_name = match file_name.to_str() {
322 Some(file_name) => file_name,
323 None => return false, };
325
326 if file_name.len() != 10 {
327 return false;
328 }
329 let mut iter = file_name.chars();
330 let iter = iter.by_ref();
331 iter.take(8)
332 .all(|c| c.is_ascii_hexdigit())
333 && iter.next() == Some('.')
334 && matches!(iter.next(), Some(c) if c.is_ascii_digit())
335}
336
337#[derive(Debug)]
338pub struct Error {
339 pub context: &'static str,
340 pub kind: ErrorKind,
341}
342
343impl StdError for Error {
344 fn source(&self) -> Option<&(dyn StdError + 'static)> {
345 Some(match &self.kind {
346 ErrorKind::Io { inner, .. } => inner,
347 ErrorKind::Os(err) => &**err,
348 ErrorKind::Pem(err) => err,
349 })
350 }
351}
352
353impl fmt::Display for Error {
354 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355 f.write_str(self.context)?;
356 f.write_str(": ")?;
357 match &self.kind {
358 ErrorKind::Io { inner, path } => {
359 write!(f, "{inner} at '{}'", path.display())
360 }
361 ErrorKind::Os(err) => err.fmt(f),
362 ErrorKind::Pem(err) => err.fmt(f),
363 }
364 }
365}
366
367#[non_exhaustive]
368#[derive(Debug)]
369pub enum ErrorKind {
370 Io { inner: io::Error, path: PathBuf },
371 Os(Box<dyn StdError + Send + Sync + 'static>),
372 Pem(pem::Error),
373}
374
375const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
376const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 use std::fs::File;
383 #[cfg(unix)]
384 use std::fs::Permissions;
385 use std::io::Write;
386 #[cfg(unix)]
387 use std::os::unix::fs::PermissionsExt;
388
389 #[test]
390 fn valid_hash_file_name() {
391 let valid_names = [
392 "f3377b1b.0",
393 "e73d606e.1",
394 "01234567.2",
395 "89abcdef.3",
396 "ABCDEF00.9",
397 ];
398 for name in valid_names {
399 assert!(is_hash_file_name(OsStr::new(name)));
400 }
401 }
402
403 #[test]
404 fn invalid_hash_file_name() {
405 let valid_names = [
406 "f3377b1b.a",
407 "e73d606g.1",
408 "0123457.2",
409 "89abcdef0.3",
410 "name.pem",
411 ];
412 for name in valid_names {
413 assert!(!is_hash_file_name(OsStr::new(name)));
414 }
415 }
416
417 #[test]
418 fn deduplication() {
419 let temp_dir = tempfile::TempDir::new().unwrap();
420 let cert1 = include_str!("../tests/badssl-com-chain.pem");
421 let cert2 = include_str!("../integration-tests/one-existing-ca.pem");
422 let file_path = temp_dir
423 .path()
424 .join("ca-certificates.crt");
425 let dir_path = temp_dir.path().to_path_buf();
426
427 {
428 let mut file = File::create(&file_path).unwrap();
429 write!(file, "{}", &cert1).unwrap();
430 write!(file, "{}", &cert2).unwrap();
431 }
432
433 {
434 let mut file = File::create(dir_path.join("71f3bb26.0")).unwrap();
436 write!(file, "{}", &cert1).unwrap();
437 }
438
439 {
440 let mut file = File::create(dir_path.join("912e7cd5.0")).unwrap();
442 write!(file, "{}", &cert2).unwrap();
443 }
444
445 let result = CertPaths {
446 file: Some(file_path.clone()),
447 dir: None,
448 }
449 .load();
450 assert_eq!(result.certs.len(), 2);
451
452 let result = CertPaths {
453 file: None,
454 dir: Some(dir_path.clone()),
455 }
456 .load();
457 assert_eq!(result.certs.len(), 2);
458
459 let result = CertPaths {
460 file: Some(file_path),
461 dir: Some(dir_path),
462 }
463 .load();
464 assert_eq!(result.certs.len(), 2);
465 }
466
467 #[test]
468 fn malformed_file_from_env() {
469 let mut result = CertificateResult::default();
472 load_pem_certs(Path::new(file!()), &mut result);
473 assert_eq!(result.certs.len(), 0);
474 assert!(result.errors.is_empty());
475 }
476
477 #[test]
478 fn from_env_missing_file() {
479 let mut result = CertificateResult::default();
480 load_pem_certs(Path::new("no/such/file"), &mut result);
481 match &first_error(&result).kind {
482 ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
483 _ => panic!("unexpected error {:?}", result.errors),
484 }
485 }
486
487 #[test]
488 fn from_env_missing_dir() {
489 let mut result = CertificateResult::default();
490 load_pem_certs_from_dir(Path::new("no/such/directory"), &mut result);
491 match &first_error(&result).kind {
492 ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
493 _ => panic!("unexpected error {:?}", result.errors),
494 }
495 }
496
497 #[test]
498 #[cfg(unix)]
499 fn from_env_with_non_regular_and_empty_file() {
500 let mut result = CertificateResult::default();
501 load_pem_certs(Path::new("/dev/null"), &mut result);
502 assert_eq!(result.certs.len(), 0);
503 assert!(result.errors.is_empty());
504 }
505
506 #[test]
507 #[cfg(unix)]
508 fn from_env_bad_dir_perms() {
509 let temp_dir = tempfile::TempDir::new().unwrap();
511 fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o000)).unwrap();
512
513 test_cert_paths_bad_perms(CertPaths {
514 file: None,
515 dir: Some(temp_dir.path().into()),
516 })
517 }
518
519 #[test]
520 #[cfg(unix)]
521 fn from_env_bad_file_perms() {
522 let temp_dir = tempfile::TempDir::new().unwrap();
524 let file_path = temp_dir.path().join("unreadable.pem");
525 let cert_file = File::create(&file_path).unwrap();
526 cert_file
527 .set_permissions(Permissions::from_mode(0o000))
528 .unwrap();
529
530 test_cert_paths_bad_perms(CertPaths {
531 file: Some(file_path.clone()),
532 dir: None,
533 });
534 }
535
536 #[cfg(unix)]
537 fn test_cert_paths_bad_perms(cert_paths: CertPaths) {
538 let result = cert_paths.load();
539
540 if let (None, None) = (cert_paths.file, cert_paths.dir) {
541 panic!("only one of file or dir should be set");
542 };
543
544 let error = first_error(&result);
545 match &error.kind {
546 ErrorKind::Io { inner, .. } => {
547 assert_eq!(inner.kind(), io::ErrorKind::PermissionDenied);
548 inner
549 }
550 _ => panic!("unexpected error {:?}", result.errors),
551 };
552 }
553
554 fn first_error(result: &CertificateResult) -> &Error {
555 result.errors.first().unwrap()
556 }
557}