1use std::path::PathBuf;
2use std::sync::Arc;
3
4use rustls::pki_types::{CertificateDer, PrivateKeyDer};
5use thiserror::Error;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum ClientAuthMode {
17 #[default]
20 Disabled,
21 Optional,
25 Required,
28}
29
30#[derive(Debug, Clone)]
31pub struct TlsConfig {
32 pub cert_chain_path: PathBuf,
33 pub private_key_path: PathBuf,
34 pub trust_roots_path: Option<PathBuf>,
38 pub client_ca_path: Option<PathBuf>,
43 pub client_auth: ClientAuthMode,
46}
47
48#[derive(Debug, Error)]
49pub enum TlsError {
50 #[error("io: {0}")]
51 Io(#[from] std::io::Error),
52 #[error("rustls: {0}")]
53 Rustls(#[from] rustls::Error),
54 #[error("no private key in {0}")]
55 NoPrivateKey(PathBuf),
56 #[error("no certificates in {0}")]
57 NoCerts(PathBuf),
58 #[error("client_auth is enabled but no client_ca_path configured")]
61 MissingClientCa,
62 #[error("client cert verifier build failed: {0}")]
66 VerifierBuild(String),
67}
68
69impl TlsConfig {
70 pub fn build_server_config(&self) -> Result<Arc<rustls::ServerConfig>, TlsError> {
71 let certs = load_certs(&self.cert_chain_path)?;
72 let key = load_private_key(&self.private_key_path)?;
73 let builder = rustls::ServerConfig::builder();
74 let cfg = match self.client_auth {
75 ClientAuthMode::Disabled => {
76 builder.with_no_client_auth().with_single_cert(certs, key)?
77 }
78 ClientAuthMode::Optional | ClientAuthMode::Required => {
79 let ca_path = self
80 .client_ca_path
81 .as_ref()
82 .ok_or(TlsError::MissingClientCa)?;
83 let mut roots = rustls::RootCertStore::empty();
84 for cert in load_certs(ca_path)? {
85 roots.add(cert)?;
86 }
87 let verifier_builder =
88 rustls::server::WebPkiClientVerifier::builder(Arc::new(roots));
89 let verifier = match self.client_auth {
90 ClientAuthMode::Optional => verifier_builder.allow_unauthenticated().build(),
91 ClientAuthMode::Required => verifier_builder.build(),
92 ClientAuthMode::Disabled => unreachable!(),
93 }
94 .map_err(|e| TlsError::VerifierBuild(e.to_string()))?;
95 builder
96 .with_client_cert_verifier(verifier)
97 .with_single_cert(certs, key)?
98 }
99 };
100 Ok(Arc::new(cfg))
101 }
102
103 pub fn build_client_config(&self) -> Result<Arc<rustls::ClientConfig>, TlsError> {
104 let mut roots = rustls::RootCertStore::empty();
105 if let Some(path) = &self.trust_roots_path {
106 for cert in load_certs(path)? {
107 roots.add(cert)?;
108 }
109 }
110 let cfg = rustls::ClientConfig::builder()
111 .with_root_certificates(roots)
112 .with_no_client_auth();
113 Ok(Arc::new(cfg))
114 }
115
116 pub fn build_client_config_with_identity(&self) -> Result<Arc<rustls::ClientConfig>, TlsError> {
125 let mut roots = rustls::RootCertStore::empty();
126 if let Some(path) = &self.trust_roots_path {
127 for cert in load_certs(path)? {
128 roots.add(cert)?;
129 }
130 }
131 let certs = load_certs(&self.cert_chain_path)?;
132 let key = load_private_key(&self.private_key_path)?;
133 let cfg = rustls::ClientConfig::builder()
134 .with_root_certificates(roots)
135 .with_client_auth_cert(certs, key)
136 .map_err(TlsError::Rustls)?;
137 Ok(Arc::new(cfg))
138 }
139}
140
141fn load_certs(path: &PathBuf) -> Result<Vec<CertificateDer<'static>>, TlsError> {
142 use rustls::pki_types::pem::PemObject;
143 let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(path)
144 .map_err(|e| TlsError::Io(std::io::Error::other(e.to_string())))?
145 .collect::<Result<Vec<_>, _>>()
146 .map_err(|e| TlsError::Io(std::io::Error::other(e.to_string())))?;
147 if certs.is_empty() {
148 return Err(TlsError::NoCerts(path.clone()));
149 }
150 Ok(certs)
151}
152
153fn load_private_key(path: &PathBuf) -> Result<PrivateKeyDer<'static>, TlsError> {
154 use rustls::pki_types::pem::PemObject;
155 PrivateKeyDer::from_pem_file(path).map_err(|_| TlsError::NoPrivateKey(path.clone()))
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use assert2::assert;
162 use std::fs::File;
163 use std::io::Write;
164
165 fn install_provider() {
166 let _ = rustls::crypto::ring::default_provider().install_default();
169 }
170
171 fn write_self_signed(dir: &std::path::Path) -> (PathBuf, PathBuf) {
172 let cert_pem = include_str!("../tests/fixtures/dev_cert.pem");
177 let key_pem = include_str!("../tests/fixtures/dev_key.pem");
178 let cert_path = dir.join("cert.pem");
179 let key_path = dir.join("key.pem");
180 File::create(&cert_path)
181 .unwrap()
182 .write_all(cert_pem.as_bytes())
183 .unwrap();
184 File::create(&key_path)
185 .unwrap()
186 .write_all(key_pem.as_bytes())
187 .unwrap();
188 (cert_path, key_path)
189 }
190
191 fn write_client_ca(dir: &std::path::Path) -> PathBuf {
192 let pem = include_str!("../tests/fixtures/dev_client_ca.pem");
197 let p = dir.join("client_ca.pem");
198 File::create(&p).unwrap().write_all(pem.as_bytes()).unwrap();
199 p
200 }
201
202 #[test]
203 fn valid_cert_and_key_loads() {
204 install_provider();
205 let dir = tempfile::tempdir().unwrap();
206 let (cert_path, key_path) = write_self_signed(dir.path());
207 let cfg = TlsConfig {
208 cert_chain_path: cert_path,
209 private_key_path: key_path,
210 trust_roots_path: None,
211 client_ca_path: None,
212 client_auth: ClientAuthMode::Disabled,
213 };
214 cfg.build_server_config().expect("build server cfg");
215 }
216
217 #[test]
218 fn missing_cert_errors() {
219 let cfg = TlsConfig {
220 cert_chain_path: PathBuf::from("/nonexistent/cert.pem"),
221 private_key_path: PathBuf::from("/nonexistent/key.pem"),
222 trust_roots_path: None,
223 client_ca_path: None,
224 client_auth: ClientAuthMode::Disabled,
225 };
226 assert!(cfg.build_server_config().is_err());
227 }
228
229 #[test]
230 fn client_auth_required_without_ca_errors() {
231 install_provider();
232 let dir = tempfile::tempdir().unwrap();
233 let (cert_path, key_path) = write_self_signed(dir.path());
234 let cfg = TlsConfig {
235 cert_chain_path: cert_path,
236 private_key_path: key_path,
237 trust_roots_path: None,
238 client_ca_path: None,
239 client_auth: ClientAuthMode::Required,
240 };
241 let err = cfg.build_server_config().unwrap_err();
242 assert!(
243 matches!(err, TlsError::MissingClientCa),
244 "expected MissingClientCa, got {err:?}"
245 );
246 }
247
248 #[test]
249 fn client_auth_required_with_ca_builds() {
250 install_provider();
251 let dir = tempfile::tempdir().unwrap();
252 let (cert_path, key_path) = write_self_signed(dir.path());
253 let ca_path = write_client_ca(dir.path());
254 let cfg = TlsConfig {
255 cert_chain_path: cert_path,
256 private_key_path: key_path,
257 trust_roots_path: None,
258 client_ca_path: Some(ca_path),
259 client_auth: ClientAuthMode::Required,
260 };
261 cfg.build_server_config()
262 .expect("build with client cert verifier");
263 }
264
265 #[test]
266 fn client_auth_optional_with_ca_builds() {
267 install_provider();
268 let dir = tempfile::tempdir().unwrap();
269 let (cert_path, key_path) = write_self_signed(dir.path());
270 let ca_path = write_client_ca(dir.path());
271 let cfg = TlsConfig {
272 cert_chain_path: cert_path,
273 private_key_path: key_path,
274 trust_roots_path: None,
275 client_ca_path: Some(ca_path),
276 client_auth: ClientAuthMode::Optional,
277 };
278 cfg.build_server_config()
279 .expect("build with optional client cert verifier");
280 }
281
282 #[test]
283 fn client_config_with_identity_builds() {
284 install_provider();
285 let dir = tempfile::tempdir().unwrap();
286 let ca = crate::ca::generate_clients_ca("p4-ca", 365).expect("ca");
287 let leaf = crate::ca::issue_user_cert(&ca.cert_pem, &ca.key_pem, "gw", 365).expect("leaf");
288 let cert_path = dir.path().join("c.pem");
289 let key_path = dir.path().join("k.pem");
290 let ca_path = dir.path().join("ca.pem");
291 File::create(&cert_path)
292 .unwrap()
293 .write_all(leaf.cert_pem.as_bytes())
294 .unwrap();
295 File::create(&key_path)
296 .unwrap()
297 .write_all(leaf.key_pem.as_bytes())
298 .unwrap();
299 File::create(&ca_path)
300 .unwrap()
301 .write_all(ca.cert_pem.as_bytes())
302 .unwrap();
303 let cfg = TlsConfig {
304 cert_chain_path: cert_path,
305 private_key_path: key_path,
306 trust_roots_path: Some(ca_path),
307 client_ca_path: None,
308 client_auth: ClientAuthMode::Disabled,
309 };
310 cfg.build_client_config_with_identity()
311 .expect("client cfg with identity");
312 }
313}