1use std::{io, path::PathBuf, sync::Arc};
2
3use rustls::{
4 pki_types::{CertificateDer, PrivateKeyDer},
5 server::WebPkiClientVerifier,
6};
7use thiserror::Error;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum ClientAuthMode {
18 #[default]
21 Disabled,
22 Optional,
26 Required,
29}
30
31#[derive(Debug, Clone)]
32pub struct TlsConfig {
33 pub cert_chain_path: PathBuf,
34 pub private_key_path: PathBuf,
35 pub trust_roots_path: Option<PathBuf>,
39 pub client_ca_path: Option<PathBuf>,
44 pub client_auth: ClientAuthMode,
47}
48
49#[derive(Debug, Error)]
50pub enum TlsError {
51 #[error("io: {0}")]
52 Io(#[from] io::Error),
53 #[error("rustls: {0}")]
54 Rustls(#[from] rustls::Error),
55 #[error("PEM: {0}")]
56 Pem(String),
57 #[error("no private key in {0}")]
58 NoPrivateKey(PathBuf),
59 #[error("no certificates in {0}")]
60 NoCerts(PathBuf),
61 #[error("client_auth is enabled but no client_ca_path configured")]
64 MissingClientCa,
65 #[error("client cert verifier build failed: {0}")]
69 VerifierBuild(String),
70}
71
72impl TlsConfig {
73 pub fn build_client_config_from_pem(
80 cert_chain_pem: &[u8],
81 private_key_pem: &[u8],
82 trust_roots_pem: &[u8],
83 ) -> Result<Arc<rustls::ClientConfig>, TlsError> {
84 use rustls::pki_types::pem::PemObject;
85
86 let mut roots = rustls::RootCertStore::empty();
87 for cert in CertificateDer::pem_slice_iter(trust_roots_pem) {
88 roots.add(cert.map_err(|error| TlsError::Pem(error.to_string()))?)?;
89 }
90 let certs = CertificateDer::pem_slice_iter(cert_chain_pem)
91 .collect::<Result<Vec<_>, _>>()
92 .map_err(|error| TlsError::Pem(error.to_string()))?;
93 if certs.is_empty() {
94 return Err(TlsError::NoCerts(PathBuf::from("<memory>")));
95 }
96 let key = PrivateKeyDer::from_pem_slice(private_key_pem)
97 .map_err(|error| TlsError::Pem(error.to_string()))?;
98 Ok(Arc::new(
99 rustls::ClientConfig::builder()
100 .with_root_certificates(roots)
101 .with_client_auth_cert(certs, key)?,
102 ))
103 }
104
105 #[tracing::instrument(level = "info", skip_all, fields(client_auth = ?self.client_auth), err)]
109 pub fn build_server_config(&self) -> Result<Arc<rustls::ServerConfig>, TlsError> {
112 let certs = load_certs(&self.cert_chain_path)?;
113 let key = load_private_key(&self.private_key_path)?;
114 let builder = rustls::ServerConfig::builder();
115 #[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
116 let mut cfg = match self.client_auth {
117 ClientAuthMode::Disabled => {
118 builder.with_no_client_auth().with_single_cert(certs, key)?
119 }
120 ClientAuthMode::Optional | ClientAuthMode::Required => {
121 let ca_path = self
122 .client_ca_path
123 .as_ref()
124 .ok_or(TlsError::MissingClientCa)?;
125 let mut roots = rustls::RootCertStore::empty();
126 for cert in load_certs(ca_path)? {
127 roots.add(cert)?;
128 }
129 let verifier_builder = WebPkiClientVerifier::builder(Arc::new(roots));
130 let verifier = match self.client_auth {
131 ClientAuthMode::Optional => verifier_builder.allow_unauthenticated().build(),
132 ClientAuthMode::Required => verifier_builder.build(),
133 ClientAuthMode::Disabled => unreachable!(),
134 }
135 .map_err(|e| TlsError::VerifierBuild(e.to_string()))?;
136 builder
137 .with_client_cert_verifier(verifier)
138 .with_single_cert(certs, key)?
139 }
140 };
141 #[cfg(target_os = "linux")]
149 {
150 cfg.enable_secret_extraction = true;
151 }
152 Ok(Arc::new(cfg))
153 }
154
155 #[tracing::instrument(level = "info", skip_all, err)]
159 pub fn build_client_config(&self) -> Result<Arc<rustls::ClientConfig>, TlsError> {
162 let mut roots = rustls::RootCertStore::empty();
163 if let Some(path) = &self.trust_roots_path {
164 for cert in load_certs(path)? {
165 roots.add(cert)?;
166 }
167 }
168 let cfg = rustls::ClientConfig::builder()
169 .with_root_certificates(roots)
170 .with_no_client_auth();
171 Ok(Arc::new(cfg))
172 }
173
174 #[tracing::instrument(level = "info", skip_all, err)]
188 pub fn build_client_config_with_identity(&self) -> Result<Arc<rustls::ClientConfig>, TlsError> {
189 let mut roots = rustls::RootCertStore::empty();
190 if let Some(path) = &self.trust_roots_path {
191 for cert in load_certs(path)? {
192 roots.add(cert)?;
193 }
194 }
195 let certs = load_certs(&self.cert_chain_path)?;
196 let key = load_private_key(&self.private_key_path)?;
197 let cfg = rustls::ClientConfig::builder()
198 .with_root_certificates(roots)
199 .with_client_auth_cert(certs, key)
200 .map_err(TlsError::Rustls)?;
201 Ok(Arc::new(cfg))
202 }
203}
204
205fn load_certs(path: &PathBuf) -> Result<Vec<CertificateDer<'static>>, TlsError> {
206 use rustls::pki_types::pem::PemObject;
207 let certs: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(path)
208 .map_err(|e| TlsError::Io(io::Error::other(e.to_string())))?
209 .collect::<Result<Vec<_>, _>>()
210 .map_err(|e| TlsError::Io(io::Error::other(e.to_string())))?;
211 if certs.is_empty() {
212 return Err(TlsError::NoCerts(path.clone()));
213 }
214 Ok(certs)
215}
216
217fn load_private_key(path: &PathBuf) -> Result<PrivateKeyDer<'static>, TlsError> {
218 use rustls::pki_types::pem::PemObject;
219 PrivateKeyDer::from_pem_file(path).map_err(|_| TlsError::NoPrivateKey(path.clone()))
220}
221
222#[cfg(test)]
223mod tests {
224 use std::{fs::File, io::Write, path::Path};
225
226 use rustls::crypto::ring::default_provider;
227
228 use super::*;
229 use crate::ca::{generate_clients_ca, issue_user_cert};
230
231 fn install_provider() {
232 let _ = default_provider().install_default();
235 }
236
237 fn write_self_signed(dir: &Path) -> (PathBuf, PathBuf) {
238 let cert_pem = include_str!("../tests/fixtures/dev_cert.pem");
243 let key_pem = include_str!("../tests/fixtures/dev_key.pem");
244 let cert_path = dir.join("cert.pem");
245 let key_path = dir.join("key.pem");
246 File::create(&cert_path)
247 .unwrap()
248 .write_all(cert_pem.as_bytes())
249 .unwrap();
250 File::create(&key_path)
251 .unwrap()
252 .write_all(key_pem.as_bytes())
253 .unwrap();
254 (cert_path, key_path)
255 }
256
257 fn write_client_ca(dir: &Path) -> PathBuf {
258 let pem = include_str!("../tests/fixtures/dev_client_ca.pem");
263 let p = dir.join("client_ca.pem");
264 File::create(&p).unwrap().write_all(pem.as_bytes()).unwrap();
265 p
266 }
267
268 #[test]
269 fn valid_cert_and_key_loads() {
270 install_provider();
271 let dir = tempfile::tempdir().unwrap();
272 let (cert_path, key_path) = write_self_signed(dir.path());
273 let cfg = TlsConfig {
274 cert_chain_path: cert_path,
275 private_key_path: key_path,
276 trust_roots_path: None,
277 client_ca_path: None,
278 client_auth: ClientAuthMode::Disabled,
279 };
280 cfg.build_server_config().expect("build server cfg");
281 }
282
283 #[test]
284 fn missing_cert_errors() {
285 let cfg = TlsConfig {
286 cert_chain_path: PathBuf::from("/nonexistent/cert.pem"),
287 private_key_path: PathBuf::from("/nonexistent/key.pem"),
288 trust_roots_path: None,
289 client_ca_path: None,
290 client_auth: ClientAuthMode::Disabled,
291 };
292 assert2::assert!(cfg.build_server_config().is_err());
293 }
294
295 #[test]
296 fn client_auth_required_without_ca_errors() {
297 install_provider();
298 let dir = tempfile::tempdir().unwrap();
299 let (cert_path, key_path) = write_self_signed(dir.path());
300 let cfg = TlsConfig {
301 cert_chain_path: cert_path,
302 private_key_path: key_path,
303 trust_roots_path: None,
304 client_ca_path: None,
305 client_auth: ClientAuthMode::Required,
306 };
307 let err = cfg.build_server_config().unwrap_err();
308 assert2::assert!(matches!(err, TlsError::MissingClientCa));
309 }
310
311 #[test]
312 fn client_auth_required_with_ca_builds() {
313 install_provider();
314 let dir = tempfile::tempdir().unwrap();
315 let (cert_path, key_path) = write_self_signed(dir.path());
316 let ca_path = write_client_ca(dir.path());
317 let cfg = TlsConfig {
318 cert_chain_path: cert_path,
319 private_key_path: key_path,
320 trust_roots_path: None,
321 client_ca_path: Some(ca_path),
322 client_auth: ClientAuthMode::Required,
323 };
324 cfg.build_server_config()
325 .expect("build with client cert verifier");
326 }
327
328 #[test]
329 fn client_auth_optional_with_ca_builds() {
330 install_provider();
331 let dir = tempfile::tempdir().unwrap();
332 let (cert_path, key_path) = write_self_signed(dir.path());
333 let ca_path = write_client_ca(dir.path());
334 let cfg = TlsConfig {
335 cert_chain_path: cert_path,
336 private_key_path: key_path,
337 trust_roots_path: None,
338 client_ca_path: Some(ca_path),
339 client_auth: ClientAuthMode::Optional,
340 };
341 cfg.build_server_config()
342 .expect("build with optional client cert verifier");
343 }
344
345 #[test]
346 fn client_config_with_identity_builds() {
347 install_provider();
348 let dir = tempfile::tempdir().unwrap();
349 let ca = generate_clients_ca("p4-ca", 365).expect("ca");
350 let leaf = issue_user_cert(&ca.cert_pem, &ca.key_pem, "gw", 365).expect("leaf");
351 let cert_path = dir.path().join("c.pem");
352 let key_path = dir.path().join("k.pem");
353 let ca_path = dir.path().join("ca.pem");
354 File::create(&cert_path)
355 .unwrap()
356 .write_all(leaf.cert_pem.as_bytes())
357 .unwrap();
358 File::create(&key_path)
359 .unwrap()
360 .write_all(leaf.key_pem.as_bytes())
361 .unwrap();
362 File::create(&ca_path)
363 .unwrap()
364 .write_all(ca.cert_pem.as_bytes())
365 .unwrap();
366 let cfg = TlsConfig {
367 cert_chain_path: cert_path,
368 private_key_path: key_path,
369 trust_roots_path: Some(ca_path),
370 client_ca_path: None,
371 client_auth: ClientAuthMode::Disabled,
372 };
373 cfg.build_client_config_with_identity()
374 .expect("client cfg with identity");
375 }
376}