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
| 1 | use crate::srv::{ |
| 2 | cfg::{ |
| 3 | AcmeConfig, |
| 4 | ServerConfig, |
| 5 | VhostConfig, |
| 6 | }, |
| 7 | constant, |
| 8 | }; |
| 9 | |
| 10 | use oxedyne_fe2o3_core::{ |
| 11 | prelude::*, |
| 12 | file as core_file, |
| 13 | path::{ |
| 14 | NormalPath, |
| 15 | NormPathBuf, |
| 16 | }, |
| 17 | }; |
| 18 | use oxedyne_fe2o3_net::tls; |
| 19 | use 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 | |
| 31 | use 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 | |
| 54 | use 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 | |
| 68 | use 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 | |
| 80 | const ACME_TLS_ALPN_NAME: &[u8] = b"acme-tls/1"; |
| 81 | |
| 82 | #[derive(Debug)] |
| 83 | pub 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 | |
| 89 | impl 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 | |
| 116 | impl 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 | |
| 149 | impl 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 | |
| 169 | pub 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 | |
| 185 | pub struct AcmeRenewer { |
| 186 | client: AcmeClient, |
| 187 | cache: AcmeDiskCache, |
| 188 | resolver: Arc<SteelCertResolver>, |
| 189 | dns_names: Vec<String>, |
| 190 | } |
| 191 | |
| 192 | impl 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 | |
| 288 | const RENEWAL_POLL_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); |
| 289 | |
| 290 | const RENEWAL_LEAD_SECS: i64 = 30 * 24 * 60 * 60; |
| 291 | |
| 292 | |
| 293 | // ┌───────────────────────────────────────────────────────────────────────────┐ |
| 294 | // │ CERTIFICATE │ |
| 295 | // └───────────────────────────────────────────────────────────────────────────┘ |
| 296 | |
| 297 | pub struct Certificate; |
| 298 | |
| 299 | impl 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 | |
| 633 | fn 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 | |
| 659 | fn 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 | } |