1use rustls::RootCertStore;
11use rustls::pki_types::{CertificateDer, pem::PemObject};
12
13#[derive(Debug, thiserror::Error)]
15pub enum CaLoadError {
16 #[error("read CA file {path}: {source}")]
18 Read {
19 path: String,
21 #[source]
23 source: rustls::pki_types::pem::Error,
24 },
25 #[error("parse CA certificate in {path}: {source}")]
27 Parse {
28 path: String,
30 #[source]
32 source: rustls::pki_types::pem::Error,
33 },
34 #[error("add CA certificate from {path} to root store: {source}")]
37 Reject {
38 path: String,
40 #[source]
42 source: rustls::Error,
43 },
44 #[error("CA file {path} contained no certificates")]
47 Empty {
48 path: String,
50 },
51}
52
53pub fn load_ca_into(roots: &mut RootCertStore, path: &str) -> Result<(), CaLoadError> {
67 let mut added = 0usize;
68 for cert in CertificateDer::pem_file_iter(path).map_err(|source| CaLoadError::Read {
69 path: path.to_owned(),
70 source,
71 })? {
72 let cert = cert.map_err(|source| CaLoadError::Parse {
73 path: path.to_owned(),
74 source,
75 })?;
76 roots.add(cert).map_err(|source| CaLoadError::Reject {
77 path: path.to_owned(),
78 source,
79 })?;
80 added += 1;
81 }
82 if added == 0 {
83 return Err(CaLoadError::Empty {
84 path: path.to_owned(),
85 });
86 }
87 Ok(())
88}
89
90pub fn load_ca_pem_into(
102 roots: &mut RootCertStore,
103 label: &str,
104 pem: &[u8],
105) -> Result<(), CaLoadError> {
106 let mut added = 0usize;
107 for cert in CertificateDer::pem_slice_iter(pem) {
108 let cert = cert.map_err(|source| CaLoadError::Parse {
109 path: label.to_owned(),
110 source,
111 })?;
112 roots.add(cert).map_err(|source| CaLoadError::Reject {
113 path: label.to_owned(),
114 source,
115 })?;
116 added += 1;
117 }
118 if added == 0 {
119 return Err(CaLoadError::Empty {
120 path: label.to_owned(),
121 });
122 }
123 Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128 #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
129 use super::*;
130
131 #[test]
132 fn missing_file_is_a_read_error() {
133 let mut roots = RootCertStore::empty();
134 let err = load_ca_into(&mut roots, "/nonexistent/ca.pem").unwrap_err();
135 assert!(matches!(err, CaLoadError::Read { .. }), "got {err:?}");
136 assert!(err.to_string().contains("ca.pem"), "got: {err}");
137 }
138
139 #[test]
140 fn empty_file_is_an_empty_error() {
141 let dir = std::env::temp_dir();
142 let path = dir.join(format!("polyc-crypto-tls-test-{}.pem", std::process::id()));
143 std::fs::write(&path, b"").unwrap();
144 let mut roots = RootCertStore::empty();
145 let err = load_ca_into(&mut roots, path.to_str().unwrap()).unwrap_err();
146 std::fs::remove_file(&path).ok();
147 assert!(matches!(err, CaLoadError::Empty { .. }), "got {err:?}");
148 }
149
150 #[test]
151 fn empty_pem_bytes_is_an_empty_error() {
152 let mut roots = RootCertStore::empty();
153 let err = load_ca_pem_into(&mut roots, "test CA", b"").unwrap_err();
154 assert!(matches!(err, CaLoadError::Empty { .. }), "got {err:?}");
155 assert!(err.to_string().contains("test CA"), "got: {err}");
156 }
157}