tachyon_web/tls/acme.rs
1//! ACME (Automatic Certificate Management Environment) native manager for Let's Encrypt.
2//!
3//! This module orchestrates the full TLS certificate lifecycle automatically:
4//!
5//! - **Account management**: Creates or reuses a cached Let's Encrypt account per environment
6//! (staging vs production). Account credentials are serialized to disk so that re-running
7//! the server never creates duplicate accounts and avoids hitting ACME registration limits.
8//!
9//! - **Certificate provisioning**: Performs the HTTP-01 challenge flow entirely in-process —
10//! no external CLI tools required. Challenge tokens are served via the built-in HTTP redirect
11//! listener used by [`crate::server::Server::serve_all_acme`].
12//!
13//! - **Hot-reload**: The [`AcmeResolver`] implements [`rustls::server::ResolvesServerCert`],
14//! meaning the TLS stack picks up renewed certificates without any downtime or restart.
15//!
16//! - **Automatic renewal**: A background task wakes up every 24 hours and renews certificates
17//! that expire within 30 days. Renewal uses exponential backoff on failure to avoid
18//! hammering the Let's Encrypt rate-limit window.
19//!
20//! - **Rate-limit safety**: Certificates and account credentials are cached to disk. On startup
21//! the cached cert is loaded and validated before ever contacting Let's Encrypt.
22
23use std::collections::HashMap;
24use std::fs;
25use std::path::PathBuf;
26use std::sync::{Arc, OnceLock, RwLock};
27use std::time::{Duration, SystemTime};
28
29use instant_acme::{
30 Account, AccountCredentials, ChallengeType, Identifier, NewAccount, NewOrder, OrderStatus,
31};
32use rcgen::{CertificateParams, KeyPair, PKCS_ECDSA_P256_SHA256};
33use rustls::pki_types::{CertificateDer, PrivateKeyDer};
34use rustls::server::{ClientHello, ResolvesServerCert};
35use rustls::sign::CertifiedKey;
36use tracing::{error, info, warn};
37use webpki::EndEntityCert;
38
39/// Writes `contents` to `path` with owner-only read/write access (`0600` on Unix)
40/// from the moment the file is created — never leaving a window where the file
41/// briefly exists with the process's default (often world/group-readable) umask
42/// permissions, unlike a `write()` followed by a separate `chmod()`.
43///
44/// Used for private keys and ACME account credentials, both of which are
45/// sensitive enough that even a brief on-disk exposure to other local users is
46/// worth closing.
47fn write_private_file(path: &std::path::Path, contents: &[u8]) -> std::io::Result<()> {
48 use std::io::Write;
49 #[cfg(unix)]
50 let mut file = {
51 use std::os::unix::fs::OpenOptionsExt;
52 fs::OpenOptions::new()
53 .write(true)
54 .create(true)
55 .truncate(true)
56 .mode(0o600)
57 .open(path)?
58 };
59 #[cfg(not(unix))]
60 let mut file = fs::File::create(path)?;
61 file.write_all(contents)
62}
63
64// ─── Global challenge store ──────────────────────────────────────────────────
65
66/// Global map of active HTTP-01 challenges: `token → key_authorization`.
67///
68/// Uses an `RwLock`-protected `HashMap` so that many concurrent HTTPS requests
69/// can read challenge responses lock-free during the brief provisioning window.
70static ACTIVE_CHALLENGES: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
71
72#[inline]
73fn challenges() -> &'static RwLock<HashMap<String, String>> {
74 ACTIVE_CHALLENGES.get_or_init(|| RwLock::new(HashMap::new()))
75}
76
77/// Registers a temporary ACME HTTP-01 challenge response in the global store.
78///
79/// The challenge will be served by the HTTP listener until [`unregister_challenge`] is called.
80pub fn register_challenge(token: String, key_authorization: String) {
81 if let Ok(mut map) = challenges().write() {
82 let _ = map.insert(token, key_authorization);
83 } else {
84 error!("[acme] Failed to acquire write lock for challenge registration");
85 }
86}
87
88/// Removes a challenge token from the global store once the ACME server has validated it.
89pub fn unregister_challenge(token: &str) {
90 if let Ok(mut map) = challenges().write() {
91 let _ = map.remove(token);
92 }
93}
94
95/// Looks up the key authorization for a given challenge token.
96///
97/// Returns `Some(key_authorization)` if the token is active, or `None` otherwise.
98/// Callers on the hot HTTP path acquire only a read lock.
99#[inline]
100#[must_use]
101pub fn get_challenge(token: &str) -> Option<String> {
102 challenges()
103 .read()
104 .ok()
105 .and_then(|map| map.get(token).cloned())
106}
107
108// ─── AcmeResolver ────────────────────────────────────────────────────────────
109
110/// Dynamic `rustls` certificate resolver that serves the most recently provisioned
111/// certificate during every TLS handshake.
112///
113/// This allows zero-downtime certificate hot-swap: simply call [`AcmeResolver::update_cert`]
114/// with the new [`CertifiedKey`] and all subsequent connections will use it immediately,
115/// without restarting the listener.
116///
117/// # Thread safety
118/// All accesses are protected by an inner [`RwLock`]; reads (handshakes) never block
119/// each other, and writes (certificate renewals) happen at most once every 24 hours.
120#[derive(Debug)]
121pub struct AcmeResolver {
122 current_key: RwLock<Option<Arc<CertifiedKey>>>,
123}
124
125impl AcmeResolver {
126 /// Creates a new resolver with no initial certificate loaded.
127 ///
128 /// The resolver will return `None` from [`ResolvesServerCert::resolve`] until
129 /// [`update_cert`][Self::update_cert] is called with a valid certificate.
130 #[must_use]
131 pub const fn new() -> Self {
132 Self {
133 current_key: RwLock::new(None),
134 }
135 }
136
137 /// Atomically swaps in a new certificate for all future TLS handshakes.
138 ///
139 /// Old connections keep using whatever certificate was negotiated at handshake
140 /// time; only new connections will see the updated certificate.
141 pub fn update_cert(&self, certified_key: CertifiedKey) {
142 match self.current_key.write() {
143 Ok(mut lock) => {
144 *lock = Some(Arc::new(certified_key));
145 info!("[acme] Certificate hot-swapped into TLS resolver");
146 }
147 Err(e) => error!("[acme] Failed to update certificate in resolver: {e}"),
148 }
149 }
150
151 /// Returns `true` if a certificate has been loaded into this resolver.
152 pub fn has_certificate(&self) -> bool {
153 self.current_key
154 .read()
155 .ok()
156 .and_then(|g| g.as_ref().map(|_| ()))
157 .is_some()
158 }
159}
160
161impl Default for AcmeResolver {
162 fn default() -> Self {
163 Self::new()
164 }
165}
166
167impl ResolvesServerCert for AcmeResolver {
168 /// Called by `rustls` on every TLS handshake. Acquires a read-lock and clones
169 /// the `Arc` — this is a very cheap operation (two atomic increments).
170 fn resolve(&self, _client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
171 self.current_key.read().ok()?.clone()
172 }
173}
174
175// ─── AcmeError ───────────────────────────────────────────────────────────────
176
177/// Errors that can occur during ACME certificate management.
178#[derive(Debug)]
179pub enum AcmeError {
180 /// An I/O error reading or writing the certificate/key cache.
181 Io(std::io::Error),
182 /// An ACME protocol error returned by the CA.
183 Acme(instant_acme::Error),
184 /// A certificate generation error from `rcgen`.
185 CertGen(rcgen::Error),
186 /// JSON serialization / deserialization error for stored credentials.
187 Json(serde_json::Error),
188 /// The ACME order was rejected by the CA (challenge failed).
189 OrderInvalid,
190 /// No private key was found in the PEM data on disk.
191 MissingPrivateKey,
192 /// Certificate parsing failed (x509-parser error).
193 CertParse(String),
194 /// TLS signing key could not be loaded from the private key.
195 TlsKeyLoad(String),
196}
197
198impl std::fmt::Display for AcmeError {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 match self {
201 Self::Io(e) => write!(f, "I/O error: {e}"),
202 Self::Acme(e) => write!(f, "ACME protocol error: {e}"),
203 Self::CertGen(e) => write!(f, "Certificate generation error: {e}"),
204 Self::Json(e) => write!(f, "JSON error: {e}"),
205 Self::OrderInvalid => write!(f, "ACME order was rejected by CA"),
206 Self::MissingPrivateKey => write!(f, "No private key found in PEM data"),
207 Self::CertParse(s) => write!(f, "Certificate parse error: {s}"),
208 Self::TlsKeyLoad(s) => write!(f, "TLS signing key load failed: {s}"),
209 }
210 }
211}
212
213impl std::error::Error for AcmeError {}
214
215impl From<std::io::Error> for AcmeError {
216 fn from(e: std::io::Error) -> Self {
217 Self::Io(e)
218 }
219}
220impl From<instant_acme::Error> for AcmeError {
221 fn from(e: instant_acme::Error) -> Self {
222 Self::Acme(e)
223 }
224}
225impl From<rcgen::Error> for AcmeError {
226 fn from(e: rcgen::Error) -> Self {
227 Self::CertGen(e)
228 }
229}
230impl From<serde_json::Error> for AcmeError {
231 fn from(e: serde_json::Error) -> Self {
232 Self::Json(e)
233 }
234}
235
236// ─── AcmeManager ─────────────────────────────────────────────────────────────
237
238/// The central orchestrator for automatic Let's Encrypt TLS certificate management.
239///
240/// # Usage
241///
242/// ```rust,no_run
243/// use tachyon_web::tls::acme::AcmeManager;
244///
245/// # async fn example() {
246/// // 1. Create the manager for your domains.
247/// let acme = AcmeManager::new(
248/// "/var/cache/tachyon/certs", // persistent cache dir (survives restarts)
249/// vec!["example.com".to_string(), "www.example.com".to_string()],
250/// "admin@example.com".to_string(),
251/// false, // false = production Let's Encrypt
252/// );
253///
254/// // 2. Get the TLS resolver to wire into the server.
255/// let resolver = acme.resolver();
256///
257/// // 3. Launch the background renewal loop.
258/// acme.start();
259/// # }
260/// ```
261///
262/// # Rate-limit safety
263///
264/// On every restart the manager first tries to load a valid certificate from
265/// `<cache_dir>/domain.crt` and `<cache_dir>/domain.key`. A new ACME order is
266/// only placed if:
267/// - No cached certificate exists, or
268/// - The cached certificate expires within 30 days.
269///
270/// Account credentials are cached in `<cache_dir>/account-{staging|prod}.json`
271/// and reused across runs, so only one account registration per environment
272/// ever happens.
273///
274/// On provisioning failure the background loop retries with exponential backoff
275/// (starting at 5 minutes, capped at 6 hours) to stay well within the
276/// [Let's Encrypt rate limits](https://letsencrypt.org/docs/rate-limits/).
277#[derive(Debug)]
278pub struct AcmeManager {
279 domains: Vec<String>,
280 email: String,
281 cache_dir: PathBuf,
282 is_staging: bool,
283 resolver: Arc<AcmeResolver>,
284 /// Guard to prevent concurrent provisioning runs.
285 /// Uses `tokio::sync::Mutex` so the guard is `Send` across async await points.
286 provisioning: tokio::sync::Mutex<()>,
287}
288
289/// Minimum time remaining before renewal is triggered.
290const RENEW_THRESHOLD: Duration = Duration::from_hours(30 * 24); // 30 days
291/// How often the background loop wakes up to check cert validity.
292const CHECK_INTERVAL: Duration = Duration::from_hours(24);
293/// Initial backoff delay on provisioning failure.
294const BACKOFF_INITIAL: Duration = Duration::from_mins(5);
295/// Maximum backoff delay on repeated provisioning failures.
296const BACKOFF_MAX: Duration = Duration::from_hours(6);
297
298impl AcmeManager {
299 /// Creates a new `AcmeManager` and ensures the cache directory exists.
300 ///
301 /// # Arguments
302 /// - `cache_dir`: Directory used for storing account credentials and the certificate/key pair.
303 /// Must be writable by the process. Survives across server restarts.
304 /// - `domains`: The domain names to include in the certificate as Subject Alternative
305 /// Names. The issued certificate has no Subject/CN set — TLS clients validate against
306 /// the SAN list, not the (legacy, deprecated) CN field.
307 /// - `email`: Contact address sent to Let's Encrypt. Used for expiry warnings.
308 /// - `is_staging`: If `true`, targets `acme-staging-v02.api.letsencrypt.org` instead of
309 /// production. Staging issues untrusted certificates but has much more lenient rate limits.
310 ///
311 /// # Returns
312 /// An `Arc<AcmeManager>` so it can be cheaply shared between the background renewal
313 /// task and the calling code that needs the [`resolver`][Self::resolver].
314 pub fn new(
315 cache_dir: impl Into<PathBuf>,
316 domains: Vec<String>,
317 email: String,
318 is_staging: bool,
319 ) -> Arc<Self> {
320 let cache_dir = cache_dir.into();
321 if let Err(e) = fs::create_dir_all(&cache_dir) {
322 error!(
323 "[acme] Failed to create cache directory {:?}: {e}",
324 cache_dir
325 );
326 }
327 Arc::new(Self {
328 domains,
329 email,
330 cache_dir,
331 is_staging,
332 resolver: Arc::new(AcmeResolver::new()),
333 provisioning: tokio::sync::Mutex::new(()),
334 })
335 }
336
337 /// Returns the [`AcmeResolver`] that should be passed to [`rustls::ServerConfig`].
338 ///
339 /// Wire this into your TLS configuration:
340 /// ```rust,no_run
341 /// use std::sync::Arc;
342 /// use tachyon_web::tls::acme::AcmeManager;
343 ///
344 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
345 /// let acme = AcmeManager::new("/tmp/certs", vec!["example.com".into()], "admin@example.com".into(), true);
346 /// let resolver = acme.resolver();
347 ///
348 /// let tls_config = rustls::ServerConfig::builder()
349 /// .with_no_client_auth()
350 /// .with_cert_resolver(resolver);
351 /// # Ok(())
352 /// # }
353 /// ```
354 pub fn resolver(&self) -> Arc<AcmeResolver> {
355 self.resolver.clone()
356 }
357
358 /// Spawns the background certificate management loop as a Tokio task.
359 ///
360 /// The loop runs indefinitely:
361 /// 1. Checks whether a valid cached certificate exists and loads it.
362 /// 2. If the certificate is missing or expiring soon, provisions a new one from ACME.
363 /// 3. Sleeps for 24 hours, then repeats.
364 ///
365 /// Provisioning failures use exponential backoff instead of immediately retrying
366 /// to respect Let's Encrypt rate limits.
367 ///
368 /// # Panics
369 /// Never panics. All errors are logged via [`tracing`].
370 pub fn start(self: Arc<Self>) {
371 drop(tokio::spawn(async move {
372 self.run_loop().await;
373 }));
374 }
375
376 /// Internal background loop. Runs forever with controlled sleep intervals.
377 async fn run_loop(&self) {
378 let mut backoff = BACKOFF_INITIAL;
379
380 loop {
381 let needs_provisioning = match self.load_and_activate_cached_cert() {
382 Ok(true) => {
383 // Valid cert loaded and activated — reset backoff for next cycle.
384 backoff = BACKOFF_INITIAL;
385 false
386 }
387 Ok(false) => {
388 info!("[acme] No valid cached certificate — provisioning new one");
389 true
390 }
391 Err(e) => {
392 warn!("[acme] Error loading cached certificate: {e}");
393 true
394 }
395 };
396
397 if needs_provisioning {
398 match self.provision_cert().await {
399 Ok((certs, key)) => {
400 info!("[acme] Successfully provisioned new certificate from Let's Encrypt");
401 match Self::build_certified_key(certs, key) {
402 Ok(certified_key) => {
403 self.resolver.update_cert(certified_key);
404 backoff = BACKOFF_INITIAL; // success — reset backoff
405 }
406 Err(e) => {
407 error!(
408 "[acme] Failed to build TLS signing key: {e}. Retrying in {:?}",
409 backoff
410 );
411 tokio::time::sleep(backoff).await;
412 backoff = (backoff * 2).min(BACKOFF_MAX);
413 continue;
414 }
415 }
416 }
417 Err(e) => {
418 error!(
419 "[acme] Certificate provisioning failed: {e}. Retrying in {:?}",
420 backoff
421 );
422 tokio::time::sleep(backoff).await;
423 // Exponential backoff, capped at BACKOFF_MAX.
424 backoff = (backoff * 2).min(BACKOFF_MAX);
425 continue; // Skip the CHECK_INTERVAL sleep on failure.
426 }
427 }
428 }
429
430 tokio::time::sleep(CHECK_INTERVAL).await;
431 }
432 }
433
434 /// Loads the cached certificate from disk and activates it in the resolver.
435 ///
436 /// Returns `Ok(true)` if a valid (non-expiring) certificate was loaded,
437 /// `Ok(false)` if the certificate is missing or about to expire,
438 /// or `Err` if reading/parsing failed with an unexpected error.
439 fn load_and_activate_cached_cert(&self) -> Result<bool, AcmeError> {
440 let Ok((certs, key)) = self.load_cached_certs_and_key() else {
441 return Ok(false); // Cache miss — silently request provisioning.
442 };
443
444 let Some(expiry) = Self::check_cert_expiry(&certs) else {
445 return Ok(false);
446 };
447
448 if !Self::cert_matches_domains(&certs, &self.domains) {
449 warn!(
450 "[acme] Cached certificate in {:?} does not cover the configured domain set {:?} \
451 — discarding stale cache and re-provisioning",
452 self.cache_dir, self.domains
453 );
454 return Ok(false);
455 }
456
457 let now = SystemTime::now();
458 let time_remaining = expiry.duration_since(now).unwrap_or(Duration::ZERO);
459
460 if expiry <= now || time_remaining <= RENEW_THRESHOLD {
461 warn!(
462 "[acme] Cached certificate expires in {:.1} days — triggering renewal",
463 time_remaining.as_secs_f64() / 86400.0
464 );
465 return Ok(false);
466 }
467
468 info!(
469 "[acme] Loaded cached certificate (expires in {:.1} days)",
470 time_remaining.as_secs_f64() / 86400.0
471 );
472
473 let certified_key = Self::build_certified_key(certs, key)?;
474 self.resolver.update_cert(certified_key);
475 Ok(true)
476 }
477
478 /// Parses the `notAfter` field from the first DER certificate in the chain.
479 ///
480 /// Uses the small hand-rolled DER walker in [`min_der`] rather than a general-purpose
481 /// X.509 parsing crate — see that module's docs for why.
482 fn check_cert_expiry(certs: &[CertificateDer<'static>]) -> Option<SystemTime> {
483 let first = certs.first()?;
484 min_der::parse_not_after(first.as_ref())
485 .inspect_err(|e| warn!("[acme] Failed to parse cached certificate: {e}"))
486 .ok()
487 }
488
489 /// Verifies that every domain this manager is configured for is covered by the
490 /// certificate's Subject Alternative Names.
491 ///
492 /// A cached certificate is only safe to reuse if it was actually issued for the
493 /// domain set this instance is managing — an expiry check alone isn't enough: a
494 /// still-valid cert left behind by a previous configuration (different domains
495 /// pointed at the same `cache_dir`) would otherwise be silently activated for the
496 /// wrong hostname.
497 fn cert_matches_domains(certs: &[CertificateDer<'static>], domains: &[String]) -> bool {
498 let Some(first) = certs.first() else {
499 return false;
500 };
501 let Ok(cert) = EndEntityCert::try_from(first) else {
502 return false;
503 };
504 let san_names: Vec<String> = cert
505 .valid_dns_names()
506 .map(str::to_ascii_lowercase)
507 .collect();
508 !san_names.is_empty()
509 && domains
510 .iter()
511 .all(|d| san_names.contains(&d.to_ascii_lowercase()))
512 }
513
514 /// Reads PEM-encoded cert and key from `<cache_dir>/domain.crt` and `domain.key`.
515 fn load_cached_certs_and_key(
516 &self,
517 ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), AcmeError> {
518 let cert_path = self.cache_dir.join("domain.crt");
519 let key_path = self.cache_dir.join("domain.key");
520
521 let cert_pem = fs::read_to_string(cert_path)?;
522 let key_pem = fs::read_to_string(key_path)?;
523
524 let mut cert_reader = std::io::BufReader::new(cert_pem.as_bytes());
525 let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
526 .filter_map(std::result::Result::ok)
527 .collect();
528
529 let mut key_reader = std::io::BufReader::new(key_pem.as_bytes());
530 let key =
531 rustls_pemfile::private_key(&mut key_reader)?.ok_or(AcmeError::MissingPrivateKey)?;
532
533 Ok((certs, key))
534 }
535
536 /// Atomically writes the PEM cert chain and private key to the cache directory.
537 ///
538 /// Both files are written independently; if the key write fails the cert file is
539 /// still present. On next startup the loader will fail to parse the key and
540 /// re-provision — no data corruption risk.
541 ///
542 /// The private key is written with owner-only permissions (`0600` on Unix) so it
543 /// is never left world- or group-readable on disk.
544 fn save_certs_and_key(&self, cert_pem: &str, key_pem: &str) -> Result<(), AcmeError> {
545 fs::write(self.cache_dir.join("domain.crt"), cert_pem)?;
546 let key_path = self.cache_dir.join("domain.key");
547 write_private_file(&key_path, key_pem.as_bytes())?;
548 Ok(())
549 }
550
551 /// Constructs a `rustls` [`CertifiedKey`] from DER-encoded certificates and a private key.
552 fn build_certified_key(
553 certs: Vec<CertificateDer<'static>>,
554 key: PrivateKeyDer<'static>,
555 ) -> Result<CertifiedKey, AcmeError> {
556 let provider = rustls::crypto::aws_lc_rs::default_provider();
557 let signing_key = provider
558 .key_provider
559 .load_private_key(key)
560 .map_err(|e| AcmeError::TlsKeyLoad(e.to_string()))?;
561 Ok(CertifiedKey::new(certs, signing_key))
562 }
563
564 /// Runs the full ACME HTTP-01 challenge flow and returns the new certificate chain + key.
565 ///
566 /// A mutex guard prevents two concurrent `provision_cert` calls (both driven by
567 /// [`run_loop`][Self::run_loop] today) from racing each other — e.g. two overlapping
568 /// HTTP-01 challenge flows stomping on each other's [`ACTIVE_CHALLENGES`] entries.
569 async fn provision_cert(
570 &self,
571 ) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), AcmeError> {
572 // Prevent concurrent provisioning attempts. tokio::sync::Mutex is used here
573 // because std::sync::MutexGuard is not Send across .await points.
574 let _guard = self.provisioning.lock().await;
575
576 let directory_url = if self.is_staging {
577 "https://acme-staging-v02.api.letsencrypt.org/directory"
578 } else {
579 "https://acme-v02.api.letsencrypt.org/directory"
580 };
581
582 // 1. Load or create an ACME account (scoped to staging vs production).
583 let account = self.get_or_create_account(directory_url).await?;
584
585 // 2. Create a new order for all configured domains.
586 let identifiers: Vec<Identifier> = self
587 .domains
588 .iter()
589 .map(|d| Identifier::Dns(d.clone()))
590 .collect();
591 let new_order = NewOrder::new(&identifiers);
592 let mut order = account.new_order(&new_order).await?;
593
594 // 3. Complete all HTTP-01 authorizations.
595 let mut tokens_to_unregister: Vec<String> = Vec::new();
596 {
597 let mut auths = order.authorizations();
598 while let Some(auth_res) = auths.next().await {
599 let mut auth = auth_res?;
600 let mut challenge = auth.challenge(ChallengeType::Http01).ok_or_else(|| {
601 AcmeError::Io(std::io::Error::other(
602 "No HTTP-01 challenge offered by CA — ensure port 80 is reachable",
603 ))
604 })?;
605
606 let key_auth = challenge.key_authorization().as_str().to_string();
607 let token = challenge.token.clone();
608
609 register_challenge(token.clone(), key_auth);
610 tokens_to_unregister.push(token);
611
612 // Signal ACME server that it may now probe the challenge endpoint.
613 challenge.set_ready().await?;
614 }
615 }
616
617 // 4. Poll until the order is valid (or failed).
618 let status = order
619 .poll_ready(&instant_acme::RetryPolicy::default())
620 .await?;
621
622 // Unregister all challenge tokens regardless of outcome.
623 for token in &tokens_to_unregister {
624 unregister_challenge(token);
625 }
626
627 if status == OrderStatus::Invalid {
628 return Err(AcmeError::OrderInvalid);
629 }
630
631 // 5. Generate a new ECDSA P-256 key pair and CSR for the certificate.
632 let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
633 let cert_params = CertificateParams::new(self.domains.clone())?;
634 let csr = cert_params.serialize_request(&key_pair)?;
635
636 // 6. Finalize the order by submitting the CSR.
637 order.finalize_csr(csr.der().as_ref()).await?;
638
639 // 7. Download the signed certificate chain from the CA.
640 let cert_chain_pem = order
641 .poll_certificate(&instant_acme::RetryPolicy::default())
642 .await?;
643 let private_key_pem = key_pair.serialize_pem();
644
645 // 8. Persist to disk for future restarts.
646 if let Err(e) = self.save_certs_and_key(&cert_chain_pem, &private_key_pem) {
647 error!("[acme] Failed to persist certificate to cache directory: {e}");
648 }
649
650 // 9. Parse PEM → DER for immediate use in rustls.
651 let mut cert_reader = std::io::BufReader::new(cert_chain_pem.as_bytes());
652 let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut cert_reader)
653 .filter_map(std::result::Result::ok)
654 .collect();
655
656 let mut key_reader = std::io::BufReader::new(private_key_pem.as_bytes());
657 let key =
658 rustls_pemfile::private_key(&mut key_reader)?.ok_or(AcmeError::MissingPrivateKey)?;
659
660 Ok((certs, key))
661 }
662
663 /// Loads existing ACME account credentials from `<cache_dir>/account-{staging|prod}.json`
664 /// or creates a new account and caches the credentials.
665 ///
666 /// The file name is scoped by environment so that staging and production accounts
667 /// can coexist in the same cache directory without interfering.
668 async fn get_or_create_account(&self, directory_url: &str) -> Result<Account, AcmeError> {
669 // Use separate credential files per environment to avoid mixing staging/prod accounts.
670 let env_suffix = if self.is_staging { "staging" } else { "prod" };
671 let account_path = self.cache_dir.join(format!("account-{env_suffix}.json"));
672
673 if account_path.exists() {
674 match fs::read(&account_path) {
675 Ok(creds_bytes) => {
676 match serde_json::from_slice::<AccountCredentials>(&creds_bytes) {
677 Ok(creds) => {
678 let builder = Account::builder()?;
679 match builder.from_credentials(creds).await {
680 Ok(account) => {
681 info!("[acme] Reusing cached ACME account ({env_suffix})");
682 return Ok(account);
683 }
684 Err(e) => {
685 warn!(
686 "[acme] Cached account credentials invalid, creating new: {e}"
687 );
688 }
689 }
690 }
691 Err(e) => warn!("[acme] Failed to parse cached account credentials: {e}"),
692 }
693 }
694 Err(e) => warn!("[acme] Failed to read account credentials file: {e}"),
695 }
696 }
697
698 // Create a new ACME account.
699 info!("[acme] Registering new ACME account with Let's Encrypt ({env_suffix})");
700 let contact = [format!("mailto:{}", self.email)];
701 let contact_refs: Vec<&str> = contact.iter().map(String::as_str).collect();
702 let builder = Account::builder()?;
703 let (account, creds) = builder
704 .create(
705 &NewAccount {
706 contact: &contact_refs,
707 terms_of_service_agreed: true,
708 only_return_existing: false,
709 },
710 directory_url.to_string(),
711 None,
712 )
713 .await?;
714
715 // Persist credentials. If this fails, warn but don't fail the overall flow —
716 // provisioning can still succeed; we'll just re-register next restart.
717 let creds_bytes = serde_json::to_vec(&creds)?;
718 if let Err(e) = write_private_file(&account_path, &creds_bytes) {
719 warn!("[acme] Failed to cache account credentials: {e}");
720 }
721
722 Ok(account)
723 }
724}
725
726/// Minimal, purpose-built DER reader for extracting a certificate's `notAfter`
727/// timestamp — nothing else.
728///
729/// # Why this exists instead of a general X.509-parsing crate
730///
731/// The only certificate this module ever parses is one this process itself wrote to
732/// `<cache_dir>/domain.crt` after a successful ACME order (see
733/// [`AcmeManager::provision_cert`]) — never arbitrary, unauthenticated network input.
734/// Pulling in a full X.509/ASN.1 parsing stack (`x509-parser`, plus its own
735/// `der-parser`/`asn1-rs`/`nom`/`oid-registry`/`num-bigint` dependency tree — over a
736/// dozen extra crates) just to read one timestamp field out of our own
737/// previously-issued certificate was disproportionate. This walks the handful of DER
738/// TLVs needed to reach `TBSCertificate.validity.notAfter` and nothing else; SAN
739/// parsing (a genuinely more involved structure) is instead delegated to
740/// [`cert_matches_domains`] via `rustls-webpki`'s public, already-linked-through-`rustls`
741/// `EndEntityCert::valid_dns_names()`.
742///
743/// X.509 certificates are always definite-length DER (never indefinite-length BER),
744/// so this only needs to handle short- and long-form DER lengths — no indefinite
745/// length, no BER quirks.
746mod min_der {
747 use std::time::{Duration, SystemTime};
748
749 /// Reads one DER TLV starting at `pos`, returning `(tag, content, end)` where
750 /// `end` is the offset in `buf` just past the whole TLV (header + content).
751 fn read_tlv(buf: &[u8], pos: usize) -> Result<(u8, &[u8], usize), &'static str> {
752 let tag = *buf.get(pos).ok_or("truncated DER: missing tag")?;
753 let len_byte = *buf.get(pos + 1).ok_or("truncated DER: missing length")?;
754 let (len, header_len) = if len_byte & 0x80 == 0 {
755 (usize::from(len_byte), 2usize)
756 } else {
757 // Long form: low 7 bits count the number of following length bytes.
758 // Real certificates never need more than a couple of these (a cert
759 // would have to be >16 MiB to need a 3rd byte); cap at 4 bytes (up
760 // to a 4 GiB length) purely as a sanity bound against malformed input.
761 let n = usize::from(len_byte & 0x7f);
762 if n == 0 || n > 4 {
763 return Err("unsupported DER length encoding");
764 }
765 let start = pos + 2;
766 let bytes = buf
767 .get(start..start + n)
768 .ok_or("truncated DER: missing length bytes")?;
769 let mut len = 0usize;
770 for &b in bytes {
771 len = len
772 .checked_shl(8)
773 .and_then(|v| v.checked_add(usize::from(b)))
774 .ok_or("DER length overflow")?;
775 }
776 (len, 2 + n)
777 };
778 let content_start = pos + header_len;
779 let content_end = content_start
780 .checked_add(len)
781 .ok_or("DER length overflow")?;
782 let content = buf
783 .get(content_start..content_end)
784 .ok_or("truncated DER: content shorter than declared length")?;
785 Ok((tag, content, content_end))
786 }
787
788 const TAG_SEQUENCE: u8 = 0x30;
789 const TAG_INTEGER: u8 = 0x02;
790 const TAG_CONTEXT_0: u8 = 0xA0;
791 const TAG_UTC_TIME: u8 = 0x17;
792 const TAG_GENERALIZED_TIME: u8 = 0x18;
793
794 /// Extracts `TBSCertificate.validity.notAfter` from a DER-encoded X.509 certificate.
795 pub(super) fn parse_not_after(cert_der: &[u8]) -> Result<SystemTime, &'static str> {
796 // Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
797 let (tag, cert_content, _) = read_tlv(cert_der, 0)?;
798 if tag != TAG_SEQUENCE {
799 return Err("not a DER SEQUENCE (Certificate)");
800 }
801 // TBSCertificate ::= SEQUENCE { version?, serialNumber, signature, issuer, validity, ... }
802 let (tag, tbs, _) = read_tlv(cert_content, 0)?;
803 if tag != TAG_SEQUENCE {
804 return Err("not a DER SEQUENCE (TBSCertificate)");
805 }
806
807 // Optional `[0] EXPLICIT Version` — present on v3 certs, absent on v1.
808 let (tag, _, next) = read_tlv(tbs, 0)?;
809 let pos = if tag == TAG_CONTEXT_0 { next } else { 0 };
810
811 // serialNumber INTEGER
812 let (tag, _, pos) = read_tlv(tbs, pos)?;
813 if tag != TAG_INTEGER {
814 return Err("expected serialNumber INTEGER");
815 }
816 // signature AlgorithmIdentifier ::= SEQUENCE
817 let (tag, _, pos) = read_tlv(tbs, pos)?;
818 if tag != TAG_SEQUENCE {
819 return Err("expected signature AlgorithmIdentifier SEQUENCE");
820 }
821 // issuer Name ::= SEQUENCE
822 let (tag, _, pos) = read_tlv(tbs, pos)?;
823 if tag != TAG_SEQUENCE {
824 return Err("expected issuer Name SEQUENCE");
825 }
826 // validity Validity ::= SEQUENCE { notBefore, notAfter }
827 let (tag, validity, _) = read_tlv(tbs, pos)?;
828 if tag != TAG_SEQUENCE {
829 return Err("expected validity SEQUENCE");
830 }
831
832 // notBefore Time — skip.
833 let (_, _, pos) = read_tlv(validity, 0)?;
834 // notAfter Time — decode.
835 let (tag, time, _) = read_tlv(validity, pos)?;
836 match tag {
837 TAG_UTC_TIME => parse_utc_time(time),
838 TAG_GENERALIZED_TIME => parse_generalized_time(time),
839 _ => Err("notAfter is neither UTCTime nor GeneralizedTime"),
840 }
841 }
842
843 fn parse_utc_time(b: &[u8]) -> Result<SystemTime, &'static str> {
844 // UTCTime, RFC 5280 profile: `YYMMDDHHMMSSZ` — always UTC, always seconds, always `Z`.
845 if b.len() != 13 || b[12] != b'Z' {
846 return Err("malformed UTCTime");
847 }
848 // RFC 5280's Y2K pivot rule: YY >= 50 means 19YY, otherwise 20YY.
849 let yy = two_digits(&b[0..2])?;
850 let year = i64::from(if yy >= 50 { 1900 + yy } else { 2000 + yy });
851 ymdhms_to_system_time(
852 year,
853 two_digits(&b[2..4])?,
854 two_digits(&b[4..6])?,
855 two_digits(&b[6..8])?,
856 two_digits(&b[8..10])?,
857 two_digits(&b[10..12])?,
858 )
859 }
860
861 fn parse_generalized_time(b: &[u8]) -> Result<SystemTime, &'static str> {
862 // GeneralizedTime, RFC 5280 profile: `YYYYMMDDHHMMSSZ` — no fractional seconds.
863 if b.len() != 15 || b[14] != b'Z' {
864 return Err("malformed GeneralizedTime");
865 }
866 let year = i64::from(two_digits(&b[0..2])?) * 100 + i64::from(two_digits(&b[2..4])?);
867 ymdhms_to_system_time(
868 year,
869 two_digits(&b[4..6])?,
870 two_digits(&b[6..8])?,
871 two_digits(&b[8..10])?,
872 two_digits(&b[10..12])?,
873 two_digits(&b[12..14])?,
874 )
875 }
876
877 fn two_digits(b: &[u8]) -> Result<u32, &'static str> {
878 let [hi, lo] = *b else {
879 return Err("expected two ASCII digits");
880 };
881 if !hi.is_ascii_digit() || !lo.is_ascii_digit() {
882 return Err("expected two ASCII digits");
883 }
884 Ok(u32::from(hi - b'0') * 10 + u32::from(lo - b'0'))
885 }
886
887 /// Converts a UTC calendar date/time (as decoded from DER) into a `SystemTime`,
888 /// using the standard proleptic-Gregorian civil-calendar-to-days-since-epoch
889 /// formula (Howard Hinnant's `days_from_civil`, a widely published public-domain
890 /// algorithm — not copied from any particular implementation).
891 fn ymdhms_to_system_time(
892 year: i64,
893 month: u32,
894 day: u32,
895 hour: u32,
896 minute: u32,
897 second: u32,
898 ) -> Result<SystemTime, &'static str> {
899 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
900 return Err("month/day out of range");
901 }
902 if hour > 23 || minute > 59 || second > 60 {
903 return Err("time-of-day out of range");
904 }
905 let days = days_from_civil(year, i64::from(month), i64::from(day));
906 let secs_of_day = i64::from(hour) * 3600 + i64::from(minute) * 60 + i64::from(second);
907 let total_secs = days
908 .checked_mul(86_400)
909 .and_then(|d| d.checked_add(secs_of_day))
910 .ok_or("date arithmetic overflow")?;
911 // Certificates with a notAfter before 1970 aren't something we can (or need
912 // to) support: we only ever compare this against `SystemTime::now()`.
913 let total_secs = u64::try_from(total_secs).map_err(|_| "date before the Unix epoch")?;
914 Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(total_secs))
915 }
916
917 /// Days since 1970-01-01 for a given proleptic-Gregorian civil date.
918 const fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
919 let y = if m <= 2 { y - 1 } else { y };
920 let era = (if y >= 0 { y } else { y - 399 }) / 400;
921 let yoe = y - era * 400; // [0, 399]
922 let mp = (m + 9) % 12; // [0, 11], Mar=0 .. Feb=11
923 let doy = (153 * mp + 2) / 5 + d - 1; // [0, 365]
924 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
925 era * 146_097 + doe - 719_468
926 }
927
928 #[cfg(test)]
929 mod tests {
930 #![allow(clippy::unwrap_used)]
931 use super::*;
932
933 #[test]
934 fn epoch_day_zero() {
935 assert_eq!(days_from_civil(1970, 1, 1), 0);
936 }
937
938 #[test]
939 fn known_dates() {
940 // 2024-01-01 is 19723 days after the epoch.
941 assert_eq!(days_from_civil(2024, 1, 1), 19723);
942 // Leap-day handling: 2024 is a leap year, so 2024-02-29 exists.
943 assert_eq!(
944 days_from_civil(2024, 3, 1) - days_from_civil(2024, 2, 29),
945 1
946 );
947 }
948
949 #[test]
950 fn utc_time_roundtrip() {
951 let t = parse_utc_time(b"991231235959Z").unwrap();
952 let secs = t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
953 // 1999-12-31 23:59:59 UTC
954 assert_eq!(secs, 946_684_799);
955 }
956
957 #[test]
958 fn utc_time_y2k_pivot() {
959 // "49" -> 2049 (post-epoch, decodable); "50" -> 1950 (pre-epoch, rejected
960 // by design — see `ymdhms_to_system_time`).
961 assert!(parse_utc_time(b"490101000000Z").is_ok());
962 assert!(parse_utc_time(b"500101000000Z").is_err());
963 }
964
965 #[test]
966 fn generalized_time_roundtrip() {
967 let t = parse_generalized_time(b"20991231235959Z").unwrap();
968 let secs = t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
969 assert_eq!(secs, 4_102_444_799);
970 }
971
972 #[test]
973 fn rejects_malformed_input() {
974 assert!(parse_utc_time(b"not-a-time!!!").is_err());
975 assert!(parse_generalized_time(b"short").is_err());
976 assert!(parse_not_after(b"").is_err());
977 assert!(parse_not_after(&[0x30, 0x00]).is_err());
978 }
979
980 /// End-to-end: generate a real cert with `rcgen` (already a dependency of
981 /// the `cert-gen` feature that `lets-encrypt` requires) and confirm the
982 /// full DER walk (`SEQUENCE` -> `TBSCertificate` -> ... -> `Validity` ->
983 /// `notAfter`) lands on a sane result.
984 #[test]
985 #[cfg(feature = "cert-gen")]
986 fn parses_notafter_from_a_real_certificate() {
987 use rcgen::{CertificateParams, KeyPair, PKCS_ECDSA_P256_SHA256};
988
989 let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
990 let params = CertificateParams::new(vec!["example.com".to_string()]).unwrap();
991 let cert = params.self_signed(&key_pair).unwrap();
992
993 // rcgen's default `not_after` is far in the future (year 4096) — check
994 // we land somewhere plausible rather than pinning the exact instant, so
995 // this test isn't fragile to rcgen ever changing its default.
996 let parsed = parse_not_after(cert.der().as_ref()).unwrap();
997 let year_2170 = SystemTime::UNIX_EPOCH + Duration::from_hours(24 * 365 * 200);
998 assert!(
999 parsed > year_2170,
1000 "expected a far-future notAfter, got {parsed:?}"
1001 );
1002 }
1003 }
1004}