1#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_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).collect(),
207 None => Vec::new(),
208 },
209 }
210 }
211
212 fn load(&self) -> CertificateResult {
216 load_certs_from_paths_internal(self.file.as_deref(), &self.dirs)
217 }
218}
219
220pub fn load_certs_from_paths(file: Option<&Path>, dir: Option<&Path>) -> CertificateResult {
234 let dir = match dir {
235 Some(d) => vec![d],
236 None => Vec::new(),
237 };
238
239 load_certs_from_paths_internal(file, dir.as_ref())
240}
241
242fn load_certs_from_paths_internal(
243 file: Option<&Path>,
244 dir: &[impl AsRef<Path>],
245) -> CertificateResult {
246 let mut out = CertificateResult::default();
247 if file.is_none() && dir.is_empty() {
248 return out;
249 }
250
251 if let Some(cert_file) = file {
252 load_pem_certs(cert_file, &mut out);
253 }
254
255 for cert_dir in dir.iter() {
256 load_pem_certs_from_dir(cert_dir.as_ref(), &mut out);
257 }
258
259 out.certs
260 .sort_unstable_by(|a, b| a.cmp(b));
261 out.certs.dedup();
262 out
263}
264
265fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult) {
267 let dir_reader = match fs::read_dir(dir) {
268 Ok(reader) => reader,
269 Err(err) => {
270 out.io_error(err, dir, "opening directory");
271 return;
272 }
273 };
274
275 for entry in dir_reader {
276 let entry = match entry {
277 Ok(entry) => entry,
278 Err(err) => {
279 out.io_error(err, dir, "reading directory entries");
280 continue;
281 }
282 };
283
284 let path = entry.path();
285
286 let metadata = match fs::metadata(&path) {
289 Ok(metadata) => metadata,
290 Err(e) if e.kind() == io::ErrorKind::NotFound => {
291 continue;
293 }
294 Err(e) => {
295 out.io_error(e, &path, "failed to open file");
296 continue;
297 }
298 };
299
300 if metadata.is_file() {
301 load_pem_certs(&path, out);
302 }
303 }
304}
305
306fn load_pem_certs(path: &Path, out: &mut CertificateResult) {
307 let iter = match CertificateDer::pem_file_iter(path) {
308 Ok(iter) => iter,
309 Err(err) => {
310 out.pem_error(err, path);
311 return;
312 }
313 };
314
315 for result in iter {
316 match result {
317 Ok(cert) => out.certs.push(cert),
318 Err(err) => out.pem_error(err, path),
319 }
320 }
321}
322
323#[derive(Debug)]
324pub struct Error {
325 pub context: &'static str,
326 pub kind: ErrorKind,
327}
328
329impl StdError for Error {
330 fn source(&self) -> Option<&(dyn StdError + 'static)> {
331 Some(match &self.kind {
332 ErrorKind::Io { inner, .. } => inner,
333 ErrorKind::Os(err) => &**err,
334 ErrorKind::Pem(err) => err,
335 })
336 }
337}
338
339impl fmt::Display for Error {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 f.write_str(self.context)?;
342 f.write_str(": ")?;
343 match &self.kind {
344 ErrorKind::Io { inner, path } => {
345 write!(f, "{inner} at '{}'", path.display())
346 }
347 ErrorKind::Os(err) => err.fmt(f),
348 ErrorKind::Pem(err) => err.fmt(f),
349 }
350 }
351}
352
353#[non_exhaustive]
354#[derive(Debug)]
355pub enum ErrorKind {
356 Io { inner: io::Error, path: PathBuf },
357 Os(Box<dyn StdError + Send + Sync + 'static>),
358 Pem(pem::Error),
359}
360
361const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
362const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 use std::fs::File;
369 #[cfg(unix)]
370 use std::fs::Permissions;
371 use std::io::Write;
372 #[cfg(unix)]
373 use std::os::unix::fs::PermissionsExt;
374
375 #[test]
376 fn deduplication() {
377 let temp_dir = tempfile::TempDir::new().unwrap();
378 let cert1 = include_str!("../tests/badssl-com-chain.pem");
379 let cert2 = include_str!("../integration-tests/one-existing-ca.pem");
380 let file_path = temp_dir
381 .path()
382 .join("ca-certificates.crt");
383 let dir_path = temp_dir.path().to_path_buf();
384
385 {
386 let mut file = File::create(&file_path).unwrap();
387 write!(file, "{}", &cert1).unwrap();
388 write!(file, "{}", &cert2).unwrap();
389 }
390
391 {
392 let mut file = File::create(dir_path.join("71f3bb26.0")).unwrap();
394 write!(file, "{}", &cert1).unwrap();
395 }
396
397 {
398 let mut file = File::create(dir_path.join("912e7cd5.0")).unwrap();
400 write!(file, "{}", &cert2).unwrap();
401 }
402
403 let result = CertPaths {
404 file: Some(file_path.clone()),
405 dirs: vec![],
406 }
407 .load();
408 assert_eq!(result.certs.len(), 2);
409
410 let result = CertPaths {
411 file: None,
412 dirs: vec![dir_path.clone()],
413 }
414 .load();
415 assert_eq!(result.certs.len(), 2);
416
417 let result = CertPaths {
418 file: Some(file_path),
419 dirs: vec![dir_path],
420 }
421 .load();
422 assert_eq!(result.certs.len(), 2);
423 }
424
425 #[test]
426 fn malformed_file_from_env() {
427 let mut result = CertificateResult::default();
430 load_pem_certs(Path::new(file!()), &mut result);
431 assert_eq!(result.certs.len(), 0);
432 assert!(result.errors.is_empty());
433 }
434
435 #[test]
436 fn from_env_missing_file() {
437 let mut result = CertificateResult::default();
438 load_pem_certs(Path::new("no/such/file"), &mut result);
439 match &first_error(&result).kind {
440 ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
441 _ => panic!("unexpected error {:?}", result.errors),
442 }
443 }
444
445 #[test]
446 fn from_env_missing_dir() {
447 let mut result = CertificateResult::default();
448 load_pem_certs_from_dir(Path::new("no/such/directory"), &mut result);
449 match &first_error(&result).kind {
450 ErrorKind::Io { inner, .. } => assert_eq!(inner.kind(), io::ErrorKind::NotFound),
451 _ => panic!("unexpected error {:?}", result.errors),
452 }
453 }
454
455 #[test]
456 #[cfg(unix)]
457 fn from_env_with_non_regular_and_empty_file() {
458 let mut result = CertificateResult::default();
459 load_pem_certs(Path::new("/dev/null"), &mut result);
460 assert_eq!(result.certs.len(), 0);
461 assert!(result.errors.is_empty());
462 }
463
464 #[test]
465 #[cfg(unix)]
466 fn from_env_bad_dir_perms() {
467 let temp_dir = tempfile::TempDir::new().unwrap();
469 fs::set_permissions(temp_dir.path(), Permissions::from_mode(0o000)).unwrap();
470
471 test_cert_paths_bad_perms(CertPaths {
472 file: None,
473 dirs: vec![temp_dir.path().into()],
474 })
475 }
476
477 #[test]
478 #[cfg(unix)]
479 fn from_env_bad_file_perms() {
480 let temp_dir = tempfile::TempDir::new().unwrap();
482 let file_path = temp_dir.path().join("unreadable.pem");
483 let cert_file = File::create(&file_path).unwrap();
484 cert_file
485 .set_permissions(Permissions::from_mode(0o000))
486 .unwrap();
487
488 test_cert_paths_bad_perms(CertPaths {
489 file: Some(file_path.clone()),
490 dirs: vec![],
491 });
492 }
493
494 #[cfg(unix)]
495 fn test_cert_paths_bad_perms(cert_paths: CertPaths) {
496 let result = cert_paths.load();
497
498 if let (None, true) = (cert_paths.file, cert_paths.dirs.is_empty()) {
499 panic!("only one of file or dir should be set");
500 };
501
502 let error = first_error(&result);
503 match &error.kind {
504 ErrorKind::Io { inner, .. } => {
505 assert_eq!(inner.kind(), io::ErrorKind::PermissionDenied);
506 inner
507 }
508 _ => panic!("unexpected error {:?}", result.errors),
509 };
510 }
511
512 fn first_error(result: &CertificateResult) -> &Error {
513 result.errors.first().unwrap()
514 }
515}