Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_steel/src/srv/cert.rs

25.5 KiB, 181 runs

created by r1870400018:969, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::srv::{
2 cfg::{
3 AcmeConfig,
4 ServerConfig,
5 VhostConfig,
6 },
7 constant,
8};
9
10use oxedyne_fe2o3_core::{
11 prelude::*,
12 file as core_file,
13 path::{
14 NormalPath,
15 NormPathBuf,
16 },
17};
18use oxedyne_fe2o3_net::tls;
19use oxedyne_fe2o3_net::acme::{
20 cache::AcmeDiskCache,
21 challenge::ChallengeCert,
22 client::{
23 AcmeClient,
24 ChallengeInstaller,
25 IssuedCertificate,
26 },
27 jose::JwsSigner,
28 trust::letsencrypt_client_config,
29};
30
31use std::{
32 collections::HashMap,
33 fs::{
34 self,
35 File,
36 },
37 io::{
38 BufReader,
39 Write,
40 },
41 path::{
42 Path,
43 PathBuf,
44 },
45 sync::{
46 Arc,
47 RwLock,
48 },
49 time::{
50 Duration,
51 },
52};
53
54use rustls::{
55 self,
56 pki_types::{
57 CertificateDer,
58 PrivateKeyDer,
59 PrivatePkcs8KeyDer,
60 },
61 server::{
62 ClientHello,
63 ResolvesServerCert,
64 },
65 sign::CertifiedKey,
66};
67
68use rcgen;
69
70
71// ┌───────────────────────────────────────────────────────────────────────────┐
72// │ STEEL CERT RESOLVER │
73// │ │
74// │ Per-vhost cert resolver that looks up the right CertifiedKey by SNI. │
75// │ Also handles TLS-ALPN-01 challenge handshakes from an ACME CA by │
76// │ detecting the `acme-tls/1` ALPN and serving a separate challenge cert │
77// │ map on those connections. │
78// └───────────────────────────────────────────────────────────────────────────┘
79
80const ACME_TLS_ALPN_NAME: &[u8] = b"acme-tls/1";
81
82#[derive(Debug)]
83pub struct SteelCertResolver {
84 by_hostname: RwLock<HashMap<String, Arc<CertifiedKey>>>,
85 default_cert: RwLock<Option<Arc<CertifiedKey>>>,
86 challenge_certs: RwLock<HashMap<String, Arc<CertifiedKey>>>,
87}
88
89impl SteelCertResolver {
90 pub fn new() -> Self {
91 Self {
92 by_hostname: RwLock::new(HashMap::new()),
93 default_cert: RwLock::new(None),
94 challenge_certs: RwLock::new(HashMap::new()),
95 }
96 }
97
98 pub fn insert_vhost_cert(&self, hostnames: &[String], cert: Arc<CertifiedKey>) {
99 {
100 let mut default = lock_write_or_recover!(self.default_cert,
101 "SteelCertResolver.default_cert RwLock was poisoned; \
102 recovering.");
103 if default.is_none() {
104 *default = Some(cert.clone());
105 }
106 }
107 let mut map = lock_write_or_recover!(self.by_hostname,
108 "SteelCertResolver.by_hostname RwLock was poisoned; \
109 recovering.");
110 for host in hostnames {
111 map.insert(host.to_lowercase(), cert.clone());
112 }
113 }
114}
115
116impl ResolvesServerCert for SteelCertResolver {
117 fn resolve(&self, client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
118 // If the client's ALPN offer is exactly {"acme-tls/1"} this is an
119 // ACME challenge handshake from the CA; serve a challenge cert
120 // keyed on the SNI instead of the real vhost cert. The single-
121 // element equality test matches rustls-acme's own helper.
122 let is_acme_challenge = client_hello
123 .alpn()
124 .into_iter()
125 .flatten()
126 .eq([ACME_TLS_ALPN_NAME]);
127
128 if is_acme_challenge {
129 let name = match client_hello.server_name() {
130 Some(n) => n.to_lowercase(),
131 None => return None,
132 };
133 let map = lock_read_or_recover!(self.challenge_certs);
134 return map.get(&name).cloned();
135 }
136
137 // Regular handshake: SNI lookup then default cert fallback.
138 if let Some(name) = client_hello.server_name() {
139 let map = lock_read_or_recover!(self.by_hostname);
140 if let Some(cert) = map.get(&name.to_lowercase()) {
141 return Some(cert.clone());
142 }
143 }
144 let default = lock_read_or_recover!(self.default_cert);
145 default.clone()
146 }
147}
148
149impl ChallengeInstaller for SteelCertResolver {
150 fn install(&self, hostname: &str, cert: &ChallengeCert) -> Outcome<()> {
151 let certified = res!(der_to_certified_key(&cert.cert_der, &cert.key_der));
152 let mut map = lock_write_or_recover!(self.challenge_certs);
153 map.insert(hostname.to_lowercase(), Arc::new(certified));
154 Ok(())
155 }
156
157 fn remove(&self, hostname: &str) -> Outcome<()> {
158 let mut map = lock_write_or_recover!(self.challenge_certs);
159 map.remove(&hostname.to_lowercase());
160 Ok(())
161 }
162}
163
164
165// ┌───────────────────────────────────────────────────────────────────────────┐
166// │ LOADED TLS STATE │
167// └───────────────────────────────────────────────────────────────────────────┘
168
169pub struct LoadedTls {
170 pub server_config: rustls::server::ServerConfig,
171 pub acme_renewer: Option<AcmeRenewer>,
172}
173
174
175// ┌───────────────────────────────────────────────────────────────────────────┐
176// │ ACME RENEWER │
177// │ │
178// │ Drives the `fe2o3_net::acme::AcmeClient` issuance cycle on startup (if │
179// │ needed) and then periodically in a renewal loop. Holds an `Arc` to the │
180// │ shared `SteelCertResolver` so it can install challenge certs during │
181// │ `tls-alpn-01` validation and swap the issued cert into the vhost map │
182// │ once issuance completes. │
183// └───────────────────────────────────────────────────────────────────────────┘
184
185pub struct AcmeRenewer {
186 client: AcmeClient,
187 cache: AcmeDiskCache,
188 resolver: Arc<SteelCertResolver>,
189 dns_names: Vec<String>,
190}
191
192impl AcmeRenewer {
193
194 pub async fn run_forever(mut self) -> Outcome<()> {
195 // Initial issuance on startup if the cache is empty or its cert is
196 // older than the renewal threshold.
197 if res!(self.needs_renewal()) {
198 info!("ACME: initial issuance for {:?}", self.dns_names);
199 res!(self.issue_and_install().await);
200 } else {
201 info!("ACME: cached certificate for {:?} is still fresh.", self.dns_names);
202 }
203
204 // Renewal loop. 24-hour tick granularity is plenty -- LE issues
205 // 90-day certs, we renew at 60 days, and a one-day latency on
206 // detecting the rollover is fine.
207 loop {
208 tokio::time::sleep(RENEWAL_POLL_INTERVAL).await;
209 match self.needs_renewal() {
210 Ok(true) => {
211 info!("ACME: cached cert is due for renewal, issuing now.");
212 if let Err(e) = self.issue_and_install().await {
213 error!(err!(e,
214 "ACME: renewal attempt failed; will retry in \
215 24 hours.";
216 Init, Network));
217 }
218 },
219 Ok(false) => (),
220 Err(e) => error!(err!(e,
221 "ACME: failed to check cached cert age; will retry in \
222 24 hours.";
223 IO, File)),
224 }
225 }
226 }
227
228 fn needs_renewal(&self) -> Outcome<bool> {
229 let cert_path = self.cache.certificate_path();
230 if !cert_path.exists() {
231 return Ok(true);
232 }
233 let pem = match fs::read(&cert_path) {
234 Ok(b) => b,
235 Err(e) => {
236 warn!("Cached cert at {:?} could not be read ({}); renewing.",
237 cert_path, e);
238 return Ok(true);
239 }
240 };
241 if tls::certificate_expires_within(&pem, RENEWAL_LEAD_SECS) {
242 return Ok(true);
243 }
244 let covered = match tls::certificate_dns_names(&pem) {
245 Ok(names) => names,
246 Err(e) => {
247 warn!("Cached cert at {:?} could not be parsed ({}); renewing.",
248 cert_path, e);
249 return Ok(true);
250 }
251 };
252 let missing: Vec<&String> = self.dns_names.iter()
253 .filter(|want| !covered.iter().any(|got| got.eq_ignore_ascii_case(want)))
254 .collect();
255 if !missing.is_empty() {
256 info!("ACME: cached cert does not cover {:?}; reissuing for {:?}.",
257 missing, self.dns_names);
258 return Ok(true);
259 }
260 Ok(false)
261 }
262
263 async fn issue_and_install(&mut self) -> Outcome<()> {
264 let issued: IssuedCertificate = res!(self.client.issue_certificate(
265 &self.dns_names,
266 &*self.resolver,
267 ).await);
268
269 // Persist the PEM chain and the matching key to disk first, so a
270 // process crash between issuance and resolver swap still leaves a
271 // usable cached cert for the next restart.
272 res!(self.cache.store_certificate(&issued.cert_pem, &issued.key_pkcs8));
273
274 // Parse the PEM bytes into a CertifiedKey and swap it into the
275 // vhost map under every DNS name.
276 let certified = res!(pem_to_certified_key(&issued.cert_pem, &issued.key_pkcs8));
277 self.resolver.insert_vhost_cert(&self.dns_names, Arc::new(certified));
278 info!("ACME: issued and installed cert for {:?}.", self.dns_names);
279 Ok(())
280 }
281}
282
283
284// ┌───────────────────────────────────────────────────────────────────────────┐
285// │ CONSTANTS │
286// └───────────────────────────────────────────────────────────────────────────┘
287
288const RENEWAL_POLL_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
289
290const RENEWAL_LEAD_SECS: i64 = 30 * 24 * 60 * 60;
291
292
293// ┌───────────────────────────────────────────────────────────────────────────┐
294// │ CERTIFICATE │
295// └───────────────────────────────────────────────────────────────────────────┘
296
297pub struct Certificate;
298
299impl Certificate {
300
301 pub fn filepath(
302 root: &NormPathBuf,
303 dir_root: &String,
304 subdir: &str,
305 name: &str,
306 ext: &str,
307 )
308 -> PathBuf
309 {
310 let mut relpath = PathBuf::from(dir_root);
311 relpath.push(subdir);
312 relpath.push(name);
313 relpath.set_extension(ext);
314 let relpath = relpath.normalise().remove_relative();
315 root.clone().join(relpath).absolute().into_inner()
316 }
317
318 pub fn write_to_file<
319 P: AsRef<Path> + std::fmt::Debug,
320 >(
321 fname: P,
322 data: &[u8],
323 )
324 -> Outcome<()>
325 {
326 let fname = fname.as_ref();
327 let mut file = res!(File::create(fname));
328 res!(file.write_all(data));
329 info!("{:?} saved successfully.", fname);
330 Ok(())
331 }
332
333 pub fn load(
334 cfg: &ServerConfig,
335 root: &NormPathBuf,
336 dev_mode: bool,
337 )
338 -> Outcome<LoadedTls>
339 {
340 debug!("DEV_MODE = {}", dev_mode);
341 let vhosts = res!(cfg.get_vhosts());
342 let acme_cfg = res!(cfg.get_acme());
343
344 // ACME is orthogonal to dev/prod mode: if it's on, use it; if it's
345 // off, fall back to loading static certificates from disk.
346 if acme_cfg.enabled {
347 Self::load_acme(cfg, &vhosts, &acme_cfg, root)
348 } else {
349 Self::load_static(cfg, &vhosts, root, dev_mode)
350 }
351 }
352
353 fn load_static(
354 cfg: &ServerConfig,
355 vhosts: &[VhostConfig],
356 root: &NormPathBuf,
357 dev_mode: bool,
358 )
359 -> Outcome<LoadedTls>
360 {
361 let tls_subdir = if dev_mode {
362 constant::TLS_DIR_DEV
363 } else {
364 constant::TLS_DIR_PROD
365 };
366
367 let resolver = Arc::new(SteelCertResolver::new());
368
369 if dev_mode {
370 // In dev mode, all vhosts share the single self-signed dev cert.
371 let cert_path = Self::filepath(
372 root, &cfg.tls_dir_rel, tls_subdir, "fullchain", "pem",
373 );
374 let key_path = Self::filepath(
375 root, &cfg.tls_dir_rel, tls_subdir, "privkey", "pem",
376 );
377 info!("Loading dev certificate from {:?}", cert_path);
378 let certified = res!(Self::read_cert_and_key(&cert_path, &key_path));
379 let all_hostnames: Vec<String> = vhosts
380 .iter()
381 .flat_map(|v| v.hostnames.iter().cloned())
382 .collect();
383 resolver.insert_vhost_cert(&all_hostnames, Arc::new(certified));
384 } else {
385 // Production without ACME: one cert per vhost under
386 // {tls_dir_rel}/prod/{primary_hostname}/{fullchain,privkey}.pem
387 for vh in vhosts {
388 let primary = vh.primary_hostname();
389 let cert_path = Self::filepath(
390 root, &cfg.tls_dir_rel, tls_subdir, &fmt!("{}/fullchain", primary), "pem",
391 );
392 let key_path = Self::filepath(
393 root, &cfg.tls_dir_rel, tls_subdir, &fmt!("{}/privkey", primary), "pem",
394 );
395 info!("Loading cert for vhost '{}' from {:?}", primary, cert_path);
396 let certified = res!(Self::read_cert_and_key(&cert_path, &key_path));
397 resolver.insert_vhost_cert(&vh.hostnames, Arc::new(certified));
398 }
399 }
400
401 let mut server_config = rustls::server::ServerConfig::builder()
402 .with_no_client_auth()
403 .with_cert_resolver(resolver);
404 // See `load_acme` for the ALPN rationale; we advertise the same
405 // set so a cert in the static path can still be used in front of
406 // clients that expect `http/1.1` ALPN and so that toggling ACME
407 // on and off does not change the wire-level ALPN offering.
408 server_config.alpn_protocols.push(b"http/1.1".to_vec());
409
410 Ok(LoadedTls {
411 server_config,
412 acme_renewer: None,
413 })
414 }
415
416 fn load_acme(
417 cfg: &ServerConfig,
418 vhosts: &[VhostConfig],
419 acme_cfg: &AcmeConfig,
420 root: &NormPathBuf,
421 )
422 -> Outcome<LoadedTls>
423 {
424 if acme_cfg.contact_email.is_empty() {
425 return Err(err!(
426 "AcmeConfig: contact_email must be set when acme.enabled = true.";
427 Invalid, Input, Missing));
428 }
429 let cache_dir = res!(acme_cfg.get_cache_dir(root));
430
431 // Collect the names to certify, preserving order and ignoring
432 // duplicates (a mail hostname is commonly also a vhost).
433 let mut all_hostnames: Vec<String> = Vec::new();
434 let add = |h: &String, out: &mut Vec<String>| {
435 if !h.is_empty() && !out.iter().any(|x| x.eq_ignore_ascii_case(h)) {
436 out.push(h.clone());
437 }
438 };
439 for vh in vhosts {
440 for h in &vh.hostnames {
441 add(h, &mut all_hostnames);
442 }
443 }
444 // Steel's mail listeners share this resolver, so the greeting
445 // hostname must be certified or every IMAP and SMTP client will
446 // reject the connection on a name mismatch.
447 if let Some(mail_cfg) = res!(cfg.get_mail()) {
448 if mail_cfg.enabled {
449 add(&mail_cfg.hostname, &mut all_hostnames);
450 }
451 }
452 for d in &acme_cfg.extra_domains {
453 add(d, &mut all_hostnames);
454 }
455 if all_hostnames.is_empty() {
456 return Err(err!(
457 "AcmeConfig: no vhost hostnames configured to issue certs for.";
458 Invalid, Input, Missing));
459 }
460 info!("ACME: requesting certificates for {:?} via {}",
461 all_hostnames, acme_cfg.directory_url);
462
463 // Disk cache for account key + issued cert.
464 let cache = res!(AcmeDiskCache::new(&cache_dir));
465
466 // Load or generate the account key.
467 let signer = match res!(cache.load_account_key()) {
468 Some(s) => {
469 info!("ACME: loaded cached account key from {:?}.", cache.root());
470 s
471 },
472 None => {
473 info!("ACME: no cached account key; generating a fresh one.");
474 let s = res!(JwsSigner::new_es256());
475 res!(cache.store_account_key(&s));
476 s
477 },
478 };
479
480 // Build the trust store and ACME client.
481 let tls_client_config = res!(letsencrypt_client_config());
482 let client = AcmeClient::new(
483 acme_cfg.directory_url.clone(),
484 acme_cfg.contact_email.clone(),
485 tls_client_config,
486 signer,
487 );
488
489 // Build the resolver and pre-load any cached cert into it.
490 let resolver = Arc::new(SteelCertResolver::new());
491 if let Some((cert_pem, key_pkcs8)) = res!(cache.load_certificate()) {
492 match pem_to_certified_key(&cert_pem, &key_pkcs8) {
493 Ok(certified) => {
494 info!("ACME: pre-loaded cached cert for {:?} from {:?}.",
495 all_hostnames, cache.root());
496 resolver.insert_vhost_cert(
497 &all_hostnames, Arc::new(certified));
498 },
499 Err(e) => {
500 // A broken cache file should not stop startup -- we'll
501 // just issue a fresh cert on the renewer's first pass.
502 warn!("ACME: cached cert at {:?} failed to parse: {:?}. \
503 Will re-issue.", cache.root(), e);
504 }
505 }
506 }
507
508 // Build the ServerConfig around the resolver.
509 let mut server_config = rustls::server::ServerConfig::builder()
510 .with_no_client_auth()
511 .with_cert_resolver(resolver.clone());
512 // Advertise HTTP/1.1 for normal clients plus the ACME-specific
513 // "acme-tls/1" protocol so our resolver can serve the challenge
514 // cert when the CA connects. Rustls rejects any client whose
515 // ALPN offer does not intersect this list, so omitting http/1.1
516 // breaks every real request with NoApplicationProtocol. We
517 // deliberately do NOT advertise h2 because Steel's HTTP parser
518 // is HTTP/1.1 only; advertising h2 would cause HTTP/2-capable
519 // clients to send the `PRI * HTTP/2.0` connection preface,
520 // which Steel cannot parse.
521 server_config.alpn_protocols.push(b"http/1.1".to_vec());
522 server_config.alpn_protocols.push(b"acme-tls/1".to_vec());
523
524 let renewer = AcmeRenewer {
525 client,
526 cache,
527 resolver,
528 dns_names: all_hostnames,
529 };
530
531 Ok(LoadedTls {
532 server_config,
533 acme_renewer: Some(renewer),
534 })
535 }
536
537 fn read_cert_and_key(
538 cert_path: &Path,
539 key_path: &Path,
540 )
541 -> Outcome<CertifiedKey>
542 {
543 let cert_file = res!(File::open(cert_path));
544 let mut cert_reader = BufReader::new(cert_file);
545 let certs: Result<Vec<CertificateDer<'static>>, _> =
546 rustls_pemfile::certs(&mut cert_reader)
547 .map(|c| c.map_err(|e| err!(e,
548 "Error reading cert at {:?}.", cert_path; File)))
549 .collect();
550 let certs = res!(certs);
551
552 let key_file = res!(File::open(key_path));
553 let mut key_reader = BufReader::new(key_file);
554 let keys: Result<Vec<PrivatePkcs8KeyDer<'static>>, _> =
555 rustls_pemfile::pkcs8_private_keys(&mut key_reader)
556 .map(|k| k.map_err(|e| err!(e,
557 "Error reading private key at {:?}.", key_path; File)))
558 .collect();
559 let keys = res!(keys);
560 let key: PrivateKeyDer<'static> = match keys.into_iter().next() {
561 Some(k) => k.into(),
562 None => return Err(err!(
563 "No private keys found in {:?}.", key_path;
564 Missing, Input, File)),
565 };
566
567 let signing_key = res!(rustls::crypto::ring::sign::any_supported_type(&key)
568 .map_err(|e| err!("{:?}", e; Init, Invalid)));
569 Ok(CertifiedKey::new(certs, signing_key))
570 }
571
572 pub fn new_dev(
573 cfg: &ServerConfig,
574 root: &NormPathBuf,
575 )
576 -> Outcome<()>
577 {
578 let scheme = res!(rcgen::SignatureAlgorithm::from_oid(constant::PKCS_ECDSA_P256_SHA256));
579 let key_pair = res!(rcgen::KeyPair::generate(&scheme));
580 let der_encoding = key_pair.serialize_der();
581 let key_pair_copy = res!(rcgen::KeyPair::from_der_and_sign_algo(&der_encoding, &scheme));
582
583 let domains = vec![
584 fmt!("localhost"),
585 fmt!("127.0.0.1"),
586 ];
587 let mut params = rcgen::CertificateParams::new(domains);
588 params.alg = &scheme;
589 params.key_pair = Some(key_pair_copy);
590 params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
591 params.key_usages = vec![
592 rcgen::KeyUsagePurpose::DigitalSignature,
593 rcgen::KeyUsagePurpose::KeyEncipherment,
594 ];
595 params.extended_key_usages = vec![
596 rcgen::ExtendedKeyUsagePurpose::ServerAuth,
597 rcgen::ExtendedKeyUsagePurpose::ClientAuth,
598 ];
599
600 let cert = res!(rcgen::Certificate::from_params(params));
601
602 let cert_path = Self::filepath(
603 root, &cfg.tls_dir_rel, constant::TLS_DIR_DEV, "fullchain", "pem",
604 );
605 let dir_path = match cert_path.parent() {
606 Some(p) => p,
607 None => return Err(err!(
608 "Could not get parent directory from {:?}.", cert_path;
609 Path)),
610 };
611 // Key directory: 0700, not the default create mode.
612 res!(core_file::create_secret_dir(dir_path));
613
614 // The private key: 0600 whatever the umask, via the secret path
615 // rather than `write_to_file`, which the public chain below keeps.
616 res!(core_file::save_secret(
617 &Self::filepath(root, &cfg.tls_dir_rel, constant::TLS_DIR_DEV, "privkey", "pem"),
618 cert.serialize_private_key_pem().as_bytes(),
619 ));
620 res!(Self::write_to_file(
621 Self::filepath(root, &cfg.tls_dir_rel, constant::TLS_DIR_DEV, "fullchain", "pem"),
622 res!(cert.serialize_pem()).as_bytes(),
623 ));
624 Ok(())
625 }
626}
627
628
629// ┌───────────────────────────────────────────────────────────────────────────┐
630// │ PEM / DER DECODING │
631// └───────────────────────────────────────────────────────────────────────────┘
632
633fn pem_to_certified_key(
634 cert_pem: &[u8],
635 key_pkcs8: &[u8],
636)
637 -> Outcome<CertifiedKey>
638{
639 let mut reader = BufReader::new(cert_pem);
640 let certs: Result<Vec<CertificateDer<'static>>, _> =
641 rustls_pemfile::certs(&mut reader)
642 .map(|c| c.map_err(|e| err!(e,
643 "Error parsing ACME-issued cert PEM."; IO, Decode)))
644 .collect();
645 let certs = res!(certs);
646 if certs.is_empty() {
647 return Err(err!(
648 "ACME-issued cert PEM contained no certificates.";
649 IO, Decode, Missing));
650 }
651
652 let key = PrivatePkcs8KeyDer::from(key_pkcs8.to_vec());
653 let key_der: PrivateKeyDer<'static> = key.into();
654 let signing_key = res!(rustls::crypto::ring::sign::any_supported_type(&key_der)
655 .map_err(|e| err!("{:?}", e; Init, Invalid)));
656 Ok(CertifiedKey::new(certs, signing_key))
657}
658
659fn der_to_certified_key(
660 cert_der: &[u8],
661 key_pkcs8: &[u8],
662)
663 -> Outcome<CertifiedKey>
664{
665 let cert = CertificateDer::from(cert_der.to_vec());
666 let key = PrivatePkcs8KeyDer::from(key_pkcs8.to_vec());
667 let key_der: PrivateKeyDer<'static> = key.into();
668 let signing_key = res!(rustls::crypto::ring::sign::any_supported_type(&key_der)
669 .map_err(|e| err!("{:?}", e; Init, Invalid)));
670 Ok(CertifiedKey::new(vec![cert], signing_key))
671}