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 {
236 let dir = match dir {
237 Some(d) => vec![d],
238 None => Vec::new(),
239 };
240
241 load_certs_from_paths_internal(file, dir.as_ref())
242}
243
244fn load_certs_from_paths_internal(
245 file: Option<&Path>,
246 dir: &[impl AsRef<Path>],
247) -> CertificateResult {
248 let mut out = CertificateResult::default();
249 if file.is_none() && dir.is_empty() {
250 return out;
251 }
252
253 if let Some(cert_file) = file {
254 load_pem_certs(cert_file, &mut out);
255 }
256
257 for cert_dir in dir.iter() {
258 load_pem_certs_from_dir(cert_dir.as_ref(), &mut out);
259 }
260
261 out.certs
262 .sort_unstable_by(|a, b| a.cmp(b));
263 out.certs.dedup();
264 out
265}
266
267fn load_pem_certs_from_dir(dir: &Path, out: &mut CertificateResult) {
269 let dir_reader = match fs::read_dir(dir) {
270 Ok(reader) => reader,
271 Err(err) => {
272 out.io_error(err, dir, "opening directory");
273 return;
274 }
275 };
276
277 for entry in dir_reader {
278 let entry = match entry {
279 Ok(entry) => entry,
280 Err(err) => {
281 out.io_error(err, dir, "reading directory entries");
282 continue;
283 }
284 };
285
286 let path = entry.path();
287
288 let metadata = match fs::metadata(&path) {
291 Ok(metadata) => metadata,
292 Err(e) if e.kind() == io::ErrorKind::NotFound => {
293 continue;
295 }
296 Err(e) => {
297 out.io_error(e, &path, "failed to open file");
298 continue;
299 }
300 };
301
302 if metadata.is_file() {
303 load_pem_certs(&path, out);
304 }
305 }
306}
307
308fn load_pem_certs(path: &Path, out: &mut CertificateResult) {
309 let iter = match CertificateDer::pem_file_iter(path) {
310 Ok(iter) => iter,
311 Err(err) => {
312 out.pem_error(err, path);
313 return;
314 }
315 };
316
317 for result in iter {
318 match result {
319 Ok(cert) => out.certs.push(cert),
320 Err(err) => out.pem_error(err, path),
321 }
322 }
323}
324
325#[derive(Debug)]
326pub struct Error {
327 pub context: &'static str,
328 pub kind: ErrorKind,
329}
330
331impl StdError for Error {
332 fn source(&self) -> Option<&(dyn StdError + 'static)> {
333 Some(match &self.kind {
334 ErrorKind::Io { inner, .. } => inner,
335 ErrorKind::Os(err) => &**err,
336 ErrorKind::Pem(err) => err,
337 })
338 }
339}
340
341impl fmt::Display for Error {
342 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343 f.write_str(self.context)?;
344 f.write_str(": ")?;
345 match &self.kind {
346 ErrorKind::Io { inner, path } => {
347 write!(f, "{inner} at '{}'", path.display())
348 }
349 ErrorKind::Os(err) => err.fmt(f),
350 ErrorKind::Pem(err) => err.fmt(f),
351 }
352 }
353}
354
355#[non_exhaustive]
356#[derive(Debug)]
357pub enum ErrorKind {
358 Io { inner: io::Error, path: PathBuf },
359 Os(Box<dyn StdError + Send + Sync + 'static>),
360 Pem(pem::Error),
361}
362
363const ENV_CERT_FILE: &str = "SSL_CERT_FILE";
364const ENV_CERT_DIR: &str = "SSL_CERT_DIR";
365
366#[cfg(test)]
367mod 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 let mut file = File::create(dir_path.join("71f3bb26.0")).unwrap();
396 write!(file, "{}", cert1).unwrap();
397 }
398
399 {
400 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 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 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 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}