1use std::path::Path;
38use std::sync::Arc;
39use std::time::Duration;
40
41use rcgen::{CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose};
42use time::OffsetDateTime;
43use tokio_rustls::TlsAcceptor;
44use tracing::{info, warn};
45use url::{Host, Url};
46
47use crate::config::{ServerConfig, TlsConfig};
48use crate::pemfile;
49
50const SELF_SIGNED_VALIDITY_DAYS: i64 = 3653;
56
57const CLOCK_SKEW_ALLOWANCE: time::Duration = time::Duration::hours(1);
60
61pub fn from_config(cfg: &ServerConfig) -> anyhow::Result<Option<TlsAcceptor>> {
68 if !cfg.tls.enabled {
69 warn!(
70 event = "tls_disabled",
71 outcome = "advisory",
72 "serving ACME in cleartext: RFC 8555 §6.1 expects HTTPS, so either \
73 enable server.tls or terminate TLS in front of this server"
74 );
75 return Ok(None);
76 }
77
78 if cfg.base_url.starts_with("http://") {
83 warn!(event = "tls_base_url_mismatch", outcome = "advisory", base_url = ?cfg.base_url,
84 "server.base_url names http:// while TLS is enabled: signed requests \
85 will be refused until it names https://");
86 }
87
88 Ok(Some(acceptor_from(
89 &cfg.tls,
90 &cfg.base_url,
91 &cfg.bind_address,
92 "acme",
93 )?))
94}
95
96pub fn admin_from_config(cfg: &crate::config::AdminConfig) -> anyhow::Result<Option<TlsAcceptor>> {
106 if !cfg.enabled || !cfg.tls.enabled {
107 return Ok(None);
108 }
109
110 let tls = TlsConfig {
114 enabled: cfg.tls.enabled,
115 cert_path: cfg.tls.cert_path.clone(),
116 key_path: cfg.tls.key_path.clone(),
117 handshake_timeout_ms: cfg.tls.handshake_timeout_ms,
118 };
119 Ok(Some(acceptor_from(
120 &tls,
121 &cfg.base_url,
122 &cfg.bind_address,
123 "admin",
124 )?))
125}
126
127fn acceptor_from(
136 cfg: &TlsConfig,
137 host_url: &str,
138 bind_address: &str,
139 listener: &'static str,
140) -> anyhow::Result<TlsAcceptor> {
141 let cert_path = Path::new(&cfg.cert_path);
142 let key_path = Path::new(&cfg.key_path);
143
144 if cert_path.exists() && key_path.exists() {
145 pemfile::warn_if_key_is_readable("tls_key_permissive", key_path);
146 info!(event = "tls_cert_loaded", outcome = "success", listener = listener, cert_path = ?cfg.cert_path);
147 } else {
148 let (cert_pem, key_pem) = generate_self_signed(host_url)?;
149 std::fs::write(cert_path, &cert_pem)
150 .map_err(|error| anyhow::anyhow!("{}: {error}", cert_path.display()))?;
151 pemfile::write_private_key(key_path, &key_pem)?;
152 info!(event = "tls_cert_generated", outcome = "success", listener = listener, cert_path = ?cfg.cert_path, key_path = ?cfg.key_path);
153 }
154
155 let chain = pemfile::read_certificates(cert_path)?;
159 let key = pemfile::read_private_key(key_path)?;
160
161 let provider = rustls::crypto::ring::default_provider();
162 let mut config = rustls::ServerConfig::builder_with_provider(Arc::new(provider))
163 .with_safe_default_protocol_versions()
164 .map_err(|error| anyhow::anyhow!("building the TLS server configuration: {error}"))?
165 .with_no_client_auth()
166 .with_single_cert(chain, key)
167 .map_err(|error| {
168 anyhow::anyhow!(
169 "{} and {} are not a usable certificate/key pair: {error}",
170 cert_path.display(),
171 key_path.display()
172 )
173 })?;
174 config.alpn_protocols = vec![b"http/1.1".to_vec()];
177
178 info!(event = "tls_enabled", outcome = "success", listener = listener, bind_address = ?bind_address, cert_path = ?cfg.cert_path);
179 Ok(TlsAcceptor::from(Arc::new(config)))
180}
181
182fn generate_self_signed(base_url: &str) -> anyhow::Result<(String, String)> {
189 let url = Url::parse(base_url)
190 .map_err(|error| anyhow::anyhow!("server.base_url is not a URL: {error}"))?;
191 let host = match url
192 .host()
193 .ok_or_else(|| anyhow::anyhow!("server.base_url has no host: {base_url}"))?
194 {
195 Host::Domain(name) => name.to_string(),
196 Host::Ipv4(address) => address.to_string(),
199 Host::Ipv6(address) => address.to_string(),
200 };
201
202 let key_pair = KeyPair::generate()?;
203 let mut params = CertificateParams::new(vec![host.clone()])?;
206 params
207 .distinguished_name
208 .push(DnType::CommonName, host.clone());
209 params.is_ca = IsCa::NoCa;
210 params.key_usages = vec![
211 KeyUsagePurpose::DigitalSignature,
212 KeyUsagePurpose::KeyEncipherment,
213 ];
214 params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
215
216 let now = OffsetDateTime::now_utc();
217 params.not_before = now - CLOCK_SKEW_ALLOWANCE;
218 params.not_after = now + time::Duration::days(SELF_SIGNED_VALIDITY_DAYS);
219 let certificate = params.self_signed(&key_pair)?;
224 Ok((certificate.pem(), key_pair.serialize_pem()))
225}
226
227#[derive(Clone)]
239pub struct TlsSettings {
240 pub acceptor: TlsAcceptor,
241 pub handshake_timeout: Duration,
242}
243
244impl TlsSettings {
245 #[must_use]
246 pub fn new(acceptor: TlsAcceptor, handshake_timeout: Duration) -> Self {
247 Self {
248 acceptor,
249 handshake_timeout,
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::config::TlsConfig;
258 use crate::testutil::TempDir;
259 use std::fs;
260
261 fn tls_config(dir: &TempDir, base_url: &str) -> ServerConfig {
265 ServerConfig {
266 bind_address: "127.0.0.1:0".to_string(),
267 base_url: base_url.to_string(),
268 tls: TlsConfig {
269 enabled: true,
270 cert_path: dir.join("server.pem").display().to_string(),
271 key_path: dir.join("server.key").display().to_string(),
272 handshake_timeout_ms: 5_000,
273 },
274 ..ServerConfig::default()
275 }
276 }
277
278 fn startup_error(result: anyhow::Result<Option<TlsAcceptor>>) -> String {
280 match result {
281 Err(error) => error.to_string(),
282 Ok(_) => panic!("this configuration must not build"),
283 }
284 }
285
286 fn admin_config(dir: &TempDir, base_url: &str) -> crate::config::AdminConfig {
288 crate::config::AdminConfig {
289 enabled: true,
290 bind_address: "127.0.0.1:0".to_string(),
291 base_url: base_url.to_string(),
292 tls: crate::config::AdminTlsConfig {
293 enabled: true,
294 cert_path: dir.join("admin.pem").display().to_string(),
295 key_path: dir.join("admin.key").display().to_string(),
296 handshake_timeout_ms: 5_000,
297 },
298 ..crate::config::AdminConfig::default()
299 }
300 }
301
302 #[test]
305 fn the_admin_acceptor_is_absent_when_the_panel_or_its_tls_is_off() {
306 for (panel, tls) in [(false, true), (true, false), (false, false)] {
307 let dir = TempDir::new("tls-admin");
308 let mut cfg = admin_config(&dir, "https://localhost:3001");
309 cfg.enabled = panel;
310 cfg.tls.enabled = tls;
311
312 assert!(
313 admin_from_config(&cfg).unwrap().is_none(),
314 "enabled={panel} tls={tls} must build no acceptor"
315 );
316 assert!(!Path::new(&cfg.tls.cert_path).exists());
317 assert!(!Path::new(&cfg.tls.key_path).exists());
318 }
319 }
320
321 #[test]
324 fn the_admin_certificate_is_generated_reloaded_and_names_its_own_host() {
325 use x509_parser::prelude::*;
326
327 let dir = TempDir::new("tls-admin");
328 let cfg = admin_config(&dir, "https://panel.example.test:3001");
329
330 assert!(admin_from_config(&cfg).unwrap().is_some());
331 let generated = fs::read(&cfg.tls.cert_path).unwrap();
332
333 #[cfg(unix)]
334 {
335 use std::os::unix::fs::PermissionsExt;
336 let mode = fs::metadata(&cfg.tls.key_path)
337 .unwrap()
338 .permissions()
339 .mode()
340 & 0o777;
341 assert_eq!(mode, 0o600, "the generated key was {mode:o}");
342 }
343
344 assert!(admin_from_config(&cfg).unwrap().is_some());
346 assert_eq!(fs::read(&cfg.tls.cert_path).unwrap(), generated);
347
348 let chain = pemfile::read_certificates(Path::new(&cfg.tls.cert_path)).unwrap();
349 let (_, certificate) = X509Certificate::from_der(&chain[0]).unwrap();
350 let names: Vec<_> = certificate
351 .subject_alternative_name()
352 .unwrap()
353 .unwrap()
354 .value
355 .general_names
356 .iter()
357 .map(|name| format!("{name:?}"))
358 .collect();
359 assert!(names[0].contains("panel.example.test"), "{names:?}");
360 }
361
362 #[test]
364 fn a_mismatched_admin_pair_is_a_startup_error() {
365 let acme_dir = TempDir::new("tls-acme");
366 let admin_dir = TempDir::new("tls-admin");
367
368 let acme = tls_config(&acme_dir, "https://localhost:3000");
370 from_config(&acme).unwrap();
371 let cfg = admin_config(&admin_dir, "https://localhost:3001");
372 admin_from_config(&cfg).unwrap();
373
374 let crossed = crate::config::AdminConfig {
375 tls: crate::config::AdminTlsConfig {
376 key_path: acme.tls.key_path.clone(),
377 ..cfg.tls.clone()
378 },
379 ..cfg
380 };
381 let error = startup_error(admin_from_config(&crossed));
382 assert!(
383 error.contains("not a usable certificate/key pair"),
384 "got: {error}"
385 );
386 }
387
388 #[test]
391 fn the_two_listeners_provision_independent_certificates() {
392 let dir = TempDir::new("tls-both");
393 let acme = ServerConfig {
394 bind_address: "127.0.0.1:0".to_string(),
395 base_url: "https://acme.example.test".to_string(),
396 tls: TlsConfig {
397 enabled: true,
398 cert_path: dir.join("server.pem").display().to_string(),
399 key_path: dir.join("server.key").display().to_string(),
400 handshake_timeout_ms: 5_000,
401 },
402 ..ServerConfig::default()
403 };
404 let admin = admin_config(&dir, "https://panel.example.test");
405
406 from_config(&acme).unwrap();
407 admin_from_config(&admin).unwrap();
408
409 assert_ne!(
410 fs::read(&acme.tls.cert_path).unwrap(),
411 fs::read(&admin.tls.cert_path).unwrap(),
412 "the two listeners answer to different names and must not share a certificate"
413 );
414 }
415
416 #[test]
419 fn disabled_builds_nothing_and_touches_no_disk() {
420 let dir = TempDir::new("tls");
421 let mut cfg = tls_config(&dir, "http://localhost:3000");
422 cfg.tls.enabled = false;
423
424 assert!(from_config(&cfg).unwrap().is_none());
425 assert!(!Path::new(&cfg.tls.cert_path).exists());
426 assert!(!Path::new(&cfg.tls.key_path).exists());
427 }
428
429 #[test]
432 fn a_missing_certificate_is_generated_then_reloaded() {
433 let dir = TempDir::new("tls");
434 let cfg = tls_config(&dir, "https://localhost:3000");
435
436 assert!(from_config(&cfg).unwrap().is_some());
437 let generated = fs::read(&cfg.tls.cert_path).unwrap();
438
439 #[cfg(unix)]
440 {
441 use std::os::unix::fs::PermissionsExt;
442 let mode = fs::metadata(&cfg.tls.key_path)
443 .unwrap()
444 .permissions()
445 .mode()
446 & 0o777;
447 assert_eq!(mode, 0o600, "the generated key was {mode:o}");
448 }
449
450 assert!(from_config(&cfg).unwrap().is_some());
451 assert_eq!(fs::read(&cfg.tls.cert_path).unwrap(), generated);
452 }
453
454 #[test]
457 fn the_generated_certificate_names_the_base_url_host() {
458 use x509_parser::prelude::*;
459
460 let dir = TempDir::new("tls");
461 let cfg = tls_config(&dir, "https://acme.example.test:8443");
462 from_config(&cfg).unwrap();
463
464 let chain = pemfile::read_certificates(Path::new(&cfg.tls.cert_path)).unwrap();
465 let (_, certificate) = X509Certificate::from_der(&chain[0]).unwrap();
466 let names: Vec<_> = certificate
467 .subject_alternative_name()
468 .unwrap()
469 .unwrap()
470 .value
471 .general_names
472 .iter()
473 .map(|name| format!("{name:?}"))
474 .collect();
475 assert_eq!(names.len(), 1, "{names:?}");
476 assert!(names[0].contains("acme.example.test"), "{names:?}");
477 assert!(!names[0].contains("8443"), "{names:?}");
479 }
480
481 #[test]
484 fn an_ip_base_url_yields_an_ip_san() {
485 use x509_parser::prelude::*;
486
487 let dir = TempDir::new("tls");
488 let cfg = tls_config(&dir, "https://127.0.0.1:3000");
489 from_config(&cfg).unwrap();
490
491 let chain = pemfile::read_certificates(Path::new(&cfg.tls.cert_path)).unwrap();
492 let (_, certificate) = X509Certificate::from_der(&chain[0]).unwrap();
493 let general_names = certificate.subject_alternative_name().unwrap().unwrap();
494 assert!(
495 general_names
496 .value
497 .general_names
498 .iter()
499 .any(|name| matches!(name, GeneralName::IPAddress(_))),
500 "{:?}",
501 general_names.value.general_names
502 );
503 }
504
505 #[test]
506 fn a_base_url_without_a_host_is_a_startup_error() {
507 let dir = TempDir::new("tls");
508 let cfg = tls_config(&dir, "not-a-url");
509
510 let error = startup_error(from_config(&cfg));
511 assert!(error.contains("server.base_url"), "{error}");
512 }
513
514 #[test]
517 fn a_mismatched_pair_is_a_startup_error() {
518 let dir = TempDir::new("tls");
519 let cfg = tls_config(&dir, "https://localhost:3000");
520
521 let (cert_pem, _) = generate_self_signed("https://localhost").unwrap();
522 let (_, key_pem) = generate_self_signed("https://localhost").unwrap();
523 fs::write(&cfg.tls.cert_path, cert_pem).unwrap();
524 fs::write(&cfg.tls.key_path, key_pem).unwrap();
525
526 let error = startup_error(from_config(&cfg));
527 assert!(
528 error.contains("not a usable certificate/key pair"),
529 "{error}"
530 );
531 }
532
533 #[test]
534 fn a_certificate_file_without_a_certificate_is_a_startup_error() {
535 let dir = TempDir::new("tls");
536 let cfg = tls_config(&dir, "https://localhost:3000");
537
538 let (_, key_pem) = generate_self_signed("https://localhost").unwrap();
539 fs::write(&cfg.tls.cert_path, "nothing useful here\n").unwrap();
540 fs::write(&cfg.tls.key_path, key_pem).unwrap();
541
542 let error = startup_error(from_config(&cfg));
543 assert!(error.contains("no CERTIFICATE block"), "{error}");
544 }
545}