Skip to main content

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}