Skip to main content

zlayer_proxy/
acme.rs

1//! ACME certificate manager for automatic TLS
2//!
3//! This module provides automatic TLS certificate provisioning and management
4//! using the ACME protocol (Let's Encrypt compatible).
5//!
6//! Implements the full ACME protocol using instant-acme for HTTP-01 challenges.
7
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13use chrono::{DateTime, TimeDelta, Utc};
14use dashmap::DashMap;
15use instant_acme::{
16    Account, AccountCredentials, AuthorizationStatus, ChallengeType, Identifier, NewAccount,
17    NewOrder, OrderStatus,
18};
19use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair};
20use serde::{Deserialize, Serialize};
21use sha2::{Digest, Sha256};
22use tokio::sync::RwLock;
23use x509_parser::pem::parse_x509_pem;
24
25use crate::sni_resolver::SniCertResolver;
26
27/// Challenge expiration time (5 minutes)
28const CHALLENGE_EXPIRATION: Duration = Duration::from_secs(5 * 60);
29
30/// Default renewal threshold (30 days before expiry)
31const RENEWAL_THRESHOLD_DAYS: i64 = 30;
32
33/// Maximum time to wait for ACME validation (120 seconds)
34const ACME_VALIDATION_TIMEOUT: Duration = Duration::from_secs(120);
35
36/// Default ACME directory URL (Let's Encrypt production)
37pub const LETS_ENCRYPT_PRODUCTION: &str = "https://acme-v02.api.letsencrypt.org/directory";
38
39/// Let's Encrypt staging directory URL (for testing)
40pub const LETS_ENCRYPT_STAGING: &str = "https://acme-staging-v02.api.letsencrypt.org/directory";
41
42/// ACME HTTP-01 challenge token for domain validation
43#[derive(Debug, Clone)]
44pub struct ChallengeToken {
45    /// The challenge token from ACME server
46    pub token: String,
47    /// The key authorization response (token.thumbprint)
48    pub key_authorization: String,
49    /// The domain being validated
50    pub domain: String,
51    /// When this challenge was created
52    pub created_at: Instant,
53}
54
55/// ACME account credentials for persistent account management
56///
57/// This struct stores the account information needed to authenticate
58/// with an ACME server (e.g., Let's Encrypt) across restarts.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct AcmeAccount {
61    /// The account URL returned by the ACME server after registration
62    pub account_url: String,
63    /// P-256 ECDSA private key in PEM format
64    pub account_key_pem: String,
65    /// Contact email addresses for the account
66    pub contact: Vec<String>,
67    /// When the account was created
68    pub created_at: DateTime<Utc>,
69}
70
71/// Metadata about a stored certificate for tracking expiry and renewal
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CertMetadata {
74    /// The domain this certificate is for
75    pub domain: String,
76    /// Certificate validity start time
77    pub not_before: DateTime<Utc>,
78    /// Certificate expiry time
79    pub not_after: DateTime<Utc>,
80    /// When this certificate was provisioned/stored
81    pub provisioned_at: DateTime<Utc>,
82    /// SHA256 fingerprint of the certificate
83    pub fingerprint: String,
84}
85
86/// Certificate manager for TLS certificate provisioning and caching
87///
88/// The `CertManager` handles:
89/// - Loading existing certificates from disk
90/// - Caching certificates in memory
91/// - Provisioning new certificates via ACME
92pub struct CertManager {
93    /// Path to certificate storage directory
94    storage_path: PathBuf,
95    /// Optional ACME account email
96    acme_email: Option<String>,
97    /// ACME directory URL (e.g., Let's Encrypt)
98    acme_directory: String,
99    /// Certificate cache (domain -> (`cert_pem`, `key_pem`))
100    cache: RwLock<HashMap<String, (String, String)>>,
101    /// ACME HTTP-01 challenge tokens (token -> `ChallengeToken`)
102    challenges: DashMap<String, ChallengeToken>,
103    /// Cached ACME account metadata (for display/persistence)
104    account: RwLock<Option<AcmeAccount>>,
105}
106
107impl CertManager {
108    /// Create a new certificate manager
109    ///
110    /// # Arguments
111    /// * `storage_path` - Directory to store certificates
112    /// * `acme_email` - Optional email for ACME account registration
113    ///
114    /// # Errors
115    ///
116    /// Returns an error if the storage directory cannot be created or
117    /// if loading an existing account fails.
118    pub async fn new(
119        storage_path: String,
120        acme_email: Option<String>,
121    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
122        Self::with_directory(
123            storage_path,
124            acme_email,
125            LETS_ENCRYPT_PRODUCTION.to_string(),
126        )
127        .await
128    }
129
130    /// Create a new certificate manager with a custom ACME directory
131    ///
132    /// # Arguments
133    /// * `storage_path` - Directory to store certificates
134    /// * `acme_email` - Optional email for ACME account registration
135    /// * `acme_directory` - ACME directory URL (e.g., Let's Encrypt production/staging)
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the storage directory cannot be created.
140    pub async fn with_directory(
141        storage_path: String,
142        acme_email: Option<String>,
143        acme_directory: String,
144    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
145        let storage_path = PathBuf::from(storage_path);
146
147        // Create storage directory if it doesn't exist
148        if !storage_path.exists() {
149            tokio::fs::create_dir_all(&storage_path).await?;
150        }
151
152        let manager = Self {
153            storage_path,
154            acme_email,
155            acme_directory,
156            cache: RwLock::new(HashMap::new()),
157            challenges: DashMap::new(),
158            account: RwLock::new(None),
159        };
160
161        // Try to load existing account from disk
162        if let Some(account) = manager.load_account().await {
163            tracing::info!(
164                account_url = %account.account_url,
165                "Loaded existing ACME account from disk"
166            );
167            *manager.account.write().await = Some(account);
168        }
169
170        Ok(manager)
171    }
172
173    /// Get the ACME directory URL
174    pub fn acme_directory(&self) -> &str {
175        &self.acme_directory
176    }
177
178    /// Get a certificate for a domain
179    ///
180    /// This method:
181    /// 1. Checks the memory cache
182    /// 2. Checks disk storage
183    /// 3. Provisions via ACME if not found (future)
184    ///
185    /// # Arguments
186    /// * `domain` - The domain to get a certificate for
187    ///
188    /// # Returns
189    /// Tuple of (`certificate_pem`, `private_key_pem`)
190    ///
191    /// # Errors
192    ///
193    /// Returns an error if the certificate is not found and ACME
194    /// provisioning fails.
195    pub async fn get_cert(
196        &self,
197        domain: &str,
198    ) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
199        // Check memory cache
200        {
201            let cache = self.cache.read().await;
202            if let Some(cached) = cache.get(domain) {
203                return Ok(cached.clone());
204            }
205        }
206
207        // Check disk storage
208        let cert_path = self.storage_path.join(format!("{domain}.crt"));
209        let key_path = self.storage_path.join(format!("{domain}.key"));
210
211        if cert_path.exists() && key_path.exists() {
212            let cert = tokio::fs::read_to_string(&cert_path).await?;
213            let key = tokio::fs::read_to_string(&key_path).await?;
214
215            // Cache for future use
216            {
217                let mut cache = self.cache.write().await;
218                cache.insert(domain.to_string(), (cert.clone(), key.clone()));
219            }
220
221            return Ok((cert, key));
222        }
223
224        // Provision via ACME (not yet implemented)
225        self.provision_cert(domain).await
226    }
227
228    /// Store a certificate
229    ///
230    /// This method stores the certificate and key to disk, updates the memory cache,
231    /// and extracts/saves certificate metadata for renewal tracking.
232    ///
233    /// # Arguments
234    /// * `domain` - The domain
235    /// * `cert` - Certificate PEM content
236    /// * `key` - Private key PEM content
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if writing the certificate or key files to disk fails.
241    pub async fn store_cert(
242        &self,
243        domain: &str,
244        cert: &str,
245        key: &str,
246    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
247        let cert_path = self.storage_path.join(format!("{domain}.crt"));
248        let key_path = self.storage_path.join(format!("{domain}.key"));
249
250        // Write to disk
251        tokio::fs::write(&cert_path, cert).await?;
252        tokio::fs::write(&key_path, key).await?;
253
254        // Update cache
255        {
256            let mut cache = self.cache.write().await;
257            cache.insert(domain.to_string(), (cert.to_string(), key.to_string()));
258        }
259
260        // Extract and save certificate metadata for renewal tracking
261        if let Ok((not_before, not_after)) = Self::parse_cert_expiry(cert) {
262            let fingerprint = Self::compute_cert_fingerprint(cert);
263            let metadata = CertMetadata {
264                domain: domain.to_string(),
265                not_before,
266                not_after,
267                provisioned_at: Utc::now(),
268                fingerprint,
269            };
270            if let Err(e) = self.save_cert_metadata(&metadata).await {
271                tracing::warn!(
272                    domain = %domain,
273                    error = %e,
274                    "Failed to save certificate metadata"
275                );
276            }
277        } else {
278            tracing::debug!(
279                domain = %domain,
280                "Could not parse certificate expiry dates for metadata"
281            );
282        }
283
284        tracing::info!(domain = %domain, "Stored certificate");
285        Ok(())
286    }
287
288    /// Check if a certificate exists for a domain
289    pub async fn has_cert(&self, domain: &str) -> bool {
290        // Check cache first
291        {
292            let cache = self.cache.read().await;
293            if cache.contains_key(domain) {
294                return true;
295            }
296        }
297
298        // Check disk
299        let cert_path = self.storage_path.join(format!("{domain}.crt"));
300        let key_path = self.storage_path.join(format!("{domain}.key"));
301
302        cert_path.exists() && key_path.exists()
303    }
304
305    /// Get the ACME email (if configured)
306    pub fn acme_email(&self) -> Option<&str> {
307        self.acme_email.as_deref()
308    }
309
310    /// Get the storage path
311    pub fn storage_path(&self) -> &PathBuf {
312        &self.storage_path
313    }
314
315    /// Build a CSR for `domain` whose subject Distinguished Name carries the
316    /// real domain as its `CommonName`, with the domain also present as a
317    /// `DnsName` SAN (set by `CertificateParams::new`).
318    ///
319    /// This is required for ACME: rcgen's default DN uses the placeholder
320    /// `CommonName` "rcgen self signed cert", which Let's Encrypt reads as a
321    /// requested identifier and rejects with `rejectedIdentifier`
322    /// ("Domain name contains an invalid character"). Overriding the CN with
323    /// the actual domain removes the placeholder so the order finalizes.
324    fn build_csr(
325        domain: &str,
326        key_pair: &KeyPair,
327    ) -> Result<rcgen::CertificateSigningRequest, rcgen::Error> {
328        // `CertificateParams::new(vec![domain])` already registers the domain
329        // as a `SanType::DnsName` subject alternative name.
330        let mut params = CertificateParams::new(vec![domain.to_string()])?;
331        let mut dn = DistinguishedName::new();
332        dn.push(DnType::CommonName, domain);
333        params.distinguished_name = dn;
334        params.serialize_request(key_pair)
335    }
336
337    /// Provision a certificate via ACME
338    ///
339    /// This method implements the full ACME protocol using HTTP-01 challenges:
340    /// 1. Gets or creates an ACME account
341    /// 2. Creates a new order for the domain
342    /// 3. Processes HTTP-01 challenges
343    /// 4. Waits for validation
344    /// 5. Generates a keypair and CSR
345    /// 6. Finalizes the order
346    /// 7. Retrieves and stores the certificate
347    ///
348    /// # Arguments
349    /// * `domain` - The domain to provision a certificate for
350    ///
351    /// # Returns
352    /// Tuple of (`certificate_pem`, `private_key_pem`)
353    #[allow(clippy::too_many_lines)]
354    async fn provision_cert(
355        &self,
356        domain: &str,
357    ) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
358        tracing::info!(domain = %domain, "Starting ACME certificate provisioning");
359
360        // An ACME email is optional (RFC 8555 §7.3 allows a contact-less account),
361        // so provisioning proceeds whether or not one is configured.
362
363        // Step 1: Get or create ACME account
364        let account = self.get_or_create_acme_account().await?;
365
366        // Step 2: Create new order
367        let identifiers = [Identifier::Dns(domain.to_string())];
368        let new_order = NewOrder {
369            identifiers: &identifiers,
370        };
371        let mut order = account.new_order(&new_order).await.map_err(|e| {
372            tracing::error!(domain = %domain, error = %e, "Failed to create ACME order");
373            format!("Failed to create ACME order for '{domain}': {e}")
374        })?;
375
376        tracing::debug!(domain = %domain, "Created ACME order");
377
378        // Step 3: Get authorizations and process HTTP-01 challenges
379        let authorizations = order.authorizations().await.map_err(|e| {
380            tracing::error!(domain = %domain, error = %e, "Failed to get authorizations");
381            format!("Failed to get authorizations for '{domain}': {e}")
382        })?;
383
384        for auth in authorizations {
385            tracing::debug!(
386                domain = %domain,
387                status = ?auth.status,
388                "Processing authorization"
389            );
390
391            // Skip already valid authorizations
392            if auth.status == AuthorizationStatus::Valid {
393                continue;
394            }
395
396            // Find HTTP-01 challenge
397            let challenge = auth
398                .challenges
399                .iter()
400                .find(|c| c.r#type == ChallengeType::Http01)
401                .ok_or_else(|| {
402                    format!(
403                        "No HTTP-01 challenge available for '{}'. Available types: {:?}",
404                        domain,
405                        auth.challenges
406                            .iter()
407                            .map(|c| &c.r#type)
408                            .collect::<Vec<_>>()
409                    )
410                })?;
411
412            // Store challenge for our HTTP server to serve
413            let key_auth = order.key_authorization(challenge);
414            self.store_challenge(&challenge.token, domain, key_auth.as_str());
415
416            tracing::info!(
417                domain = %domain,
418                token = %challenge.token,
419                "Stored HTTP-01 challenge, notifying ACME server"
420            );
421
422            // Tell ACME server we're ready
423            order
424                .set_challenge_ready(&challenge.url)
425                .await
426                .map_err(|e| {
427                    tracing::error!(
428                        domain = %domain,
429                        error = %e,
430                        "Failed to set challenge ready"
431                    );
432                    format!("Failed to set challenge ready for '{domain}': {e}")
433                })?;
434        }
435
436        // Step 4: Wait for order to be ready (with timeout)
437        let start_time = Instant::now();
438        loop {
439            if start_time.elapsed() > ACME_VALIDATION_TIMEOUT {
440                self.clear_challenges_for_domain(domain);
441                return Err(format!(
442                    "ACME validation timeout for '{}'. Validation did not complete within {} seconds. \
443                     Ensure the domain is accessible at http://{}/.well-known/acme-challenge/",
444                    domain,
445                    ACME_VALIDATION_TIMEOUT.as_secs(),
446                    domain
447                )
448                .into());
449            }
450
451            order.refresh().await.map_err(|e| {
452                tracing::error!(domain = %domain, error = %e, "Failed to refresh order status");
453                format!("Failed to refresh order status for '{domain}': {e}")
454            })?;
455
456            let status = order.state().status;
457            tracing::debug!(domain = %domain, status = ?status, "Order status");
458
459            match status {
460                OrderStatus::Ready => {
461                    tracing::info!(domain = %domain, "Order is ready for finalization");
462                    break;
463                }
464                OrderStatus::Invalid => {
465                    self.clear_challenges_for_domain(domain);
466                    let error_msg = order
467                        .state()
468                        .error
469                        .as_ref()
470                        .map_or_else(|| "Unknown error".to_string(), |e| format!("{e:?}"));
471                    return Err(format!(
472                        "ACME order became invalid for '{domain}': {error_msg}. \
473                         This usually means the HTTP-01 challenge failed. \
474                         Ensure the domain is accessible at http://{domain}/.well-known/acme-challenge/"
475                    )
476                    .into());
477                }
478                OrderStatus::Valid => {
479                    // Already valid, can proceed to get certificate
480                    tracing::info!(domain = %domain, "Order is already valid");
481                    break;
482                }
483                OrderStatus::Pending | OrderStatus::Processing => {
484                    // Still waiting, sleep and retry
485                    tokio::time::sleep(Duration::from_secs(2)).await;
486                }
487            }
488        }
489
490        // Step 5: Generate keypair and CSR with rcgen
491        let key_pair = KeyPair::generate().map_err(|e| {
492            tracing::error!(domain = %domain, error = %e, "Failed to generate key pair");
493            format!("Failed to generate key pair for '{domain}': {e}")
494        })?;
495
496        let csr = Self::build_csr(domain, &key_pair).map_err(|e| {
497            tracing::error!(domain = %domain, error = %e, "Failed to create CSR");
498            format!("Failed to create CSR for '{domain}': {e}")
499        })?;
500
501        tracing::debug!(domain = %domain, "Generated CSR");
502
503        // Step 6: Finalize order with CSR (only if not already valid)
504        if order.state().status != OrderStatus::Valid {
505            order.finalize(csr.der()).await.map_err(|e| {
506                tracing::error!(domain = %domain, error = %e, "Failed to finalize order");
507                format!("Failed to finalize order for '{domain}': {e}")
508            })?;
509
510            tracing::info!(domain = %domain, "Order finalized, waiting for certificate");
511        }
512
513        // Step 7: Get certificate (with timeout)
514        let cert_chain = loop {
515            if start_time.elapsed() > ACME_VALIDATION_TIMEOUT {
516                self.clear_challenges_for_domain(domain);
517                return Err(format!(
518                    "Timeout waiting for certificate for '{}'. \
519                     Certificate was not issued within {} seconds.",
520                    domain,
521                    ACME_VALIDATION_TIMEOUT.as_secs()
522                )
523                .into());
524            }
525
526            match order.certificate().await {
527                Ok(Some(cert)) => break cert,
528                Ok(None) => {
529                    tracing::debug!(domain = %domain, "Certificate not yet available, waiting...");
530                    tokio::time::sleep(Duration::from_secs(1)).await;
531                }
532                Err(e) => {
533                    self.clear_challenges_for_domain(domain);
534                    return Err(format!("Failed to get certificate for '{domain}': {e}").into());
535                }
536            }
537        };
538
539        // Step 8: Store and return certificate
540        let key_pem = key_pair.serialize_pem();
541        self.store_cert(domain, &cert_chain, &key_pem).await?;
542        self.clear_challenges_for_domain(domain);
543
544        tracing::info!(
545            domain = %domain,
546            "Successfully provisioned certificate via ACME"
547        );
548
549        Ok((cert_chain, key_pem))
550    }
551
552    /// Clear the certificate cache
553    pub async fn clear_cache(&self) {
554        let mut cache = self.cache.write().await;
555        cache.clear();
556    }
557
558    /// Get cached certificate count
559    pub async fn cached_count(&self) -> usize {
560        let cache = self.cache.read().await;
561        cache.len()
562    }
563
564    /// Return the list of domain names currently held in the certificate cache.
565    pub async fn list_cached_domains(&self) -> Vec<String> {
566        let cache = self.cache.read().await;
567        cache.keys().cloned().collect()
568    }
569
570    /// Build a `rustls::ServerConfig` from the cert manager's current cache.
571    ///
572    /// Creates a fresh `SniCertResolver`, loads every certificate the manager
573    /// currently has cached into it, and wraps the resolver in a
574    /// `ServerConfig` with no client auth. Intended for callers (e.g. the
575    /// daemon API listener) that want to terminate TLS using the same cert
576    /// pool the proxy serves.
577    ///
578    /// Hot-reload semantics: this is a one-shot snapshot — certs renewed
579    /// after this call are NOT picked up by the returned config. Callers
580    /// that need hot-reload should keep an `Arc<SniCertResolver>` shared
581    /// with the proxy renewal task instead. For the join-token verifying-key
582    /// listener, restarting the daemon on renewal is acceptable since
583    /// certificates renew every ~60-90 days.
584    ///
585    /// # Errors
586    ///
587    /// Returns an error if loading any cached certificate into the SNI
588    /// resolver fails (malformed PEM, key/cert mismatch, etc.).
589    pub async fn build_server_config(
590        &self,
591    ) -> Result<Arc<rustls::ServerConfig>, Box<dyn std::error::Error + Send + Sync>> {
592        let resolver = Arc::new(SniCertResolver::new());
593
594        // Pull cached domains and load each into the resolver. We hold the
595        // read lock for the snapshot and release it before calling load_cert
596        // (which takes its own DashMap write lock on the resolver) to avoid
597        // holding two locks at once.
598        let cached: Vec<(String, String, String)> = {
599            let cache = self.cache.read().await;
600            cache
601                .iter()
602                .map(|(domain, (cert, key))| (domain.clone(), cert.clone(), key.clone()))
603                .collect()
604        };
605
606        for (domain, cert_pem, key_pem) in cached {
607            resolver
608                .load_cert(&domain, &cert_pem, &key_pem)
609                .map_err(|e| {
610                    format!(
611                        "Failed to load cached certificate for '{domain}' into SNI resolver: {e}"
612                    )
613                })?;
614        }
615
616        let server_config = rustls::ServerConfig::builder()
617            .with_no_client_auth()
618            .with_cert_resolver(resolver);
619
620        Ok(Arc::new(server_config))
621    }
622
623    // =========================================================================
624    // Certificate Metadata Management
625    // =========================================================================
626
627    /// Parse certificate expiry dates from a PEM-encoded certificate
628    ///
629    /// # Arguments
630    /// * `cert_pem` - The PEM-encoded certificate string
631    ///
632    /// # Returns
633    /// Tuple of (`not_before`, `not_after`) as `DateTime`<Utc>
634    ///
635    /// # Errors
636    ///
637    /// Returns an error if the PEM cannot be parsed or the X.509
638    /// certificate contains invalid timestamps.
639    pub fn parse_cert_expiry(
640        cert_pem: &str,
641    ) -> Result<(DateTime<Utc>, DateTime<Utc>), Box<dyn std::error::Error + Send + Sync>> {
642        let (_, pem) =
643            parse_x509_pem(cert_pem.as_bytes()).map_err(|e| format!("Failed to parse PEM: {e}"))?;
644
645        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents)
646            .map_err(|e| format!("Failed to parse X.509 certificate: {e}"))?;
647
648        let validity = cert.validity();
649
650        // Convert ASN1Time to DateTime<Utc>
651        let not_before = DateTime::from_timestamp(validity.not_before.timestamp(), 0)
652            .ok_or("Invalid not_before timestamp")?;
653
654        let not_after = DateTime::from_timestamp(validity.not_after.timestamp(), 0)
655            .ok_or("Invalid not_after timestamp")?;
656
657        Ok((not_before, not_after))
658    }
659
660    /// Compute SHA256 fingerprint of a PEM-encoded certificate
661    fn compute_cert_fingerprint(cert_pem: &str) -> String {
662        let mut hasher = Sha256::new();
663        hasher.update(cert_pem.as_bytes());
664        let result = hasher.finalize();
665        hex::encode(result)
666    }
667
668    /// Save certificate metadata to disk
669    ///
670    /// # Arguments
671    /// * `meta` - The certificate metadata to save
672    async fn save_cert_metadata(
673        &self,
674        meta: &CertMetadata,
675    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
676        let meta_path = self.storage_path.join(format!("{}.meta.json", meta.domain));
677        let json = serde_json::to_string_pretty(meta)?;
678        tokio::fs::write(&meta_path, json).await?;
679        tracing::debug!(domain = %meta.domain, "Saved certificate metadata");
680        Ok(())
681    }
682
683    /// Load certificate metadata from disk
684    ///
685    /// # Arguments
686    /// * `domain` - The domain to load metadata for
687    ///
688    /// # Returns
689    /// The certificate metadata if it exists
690    pub async fn load_cert_metadata(&self, domain: &str) -> Option<CertMetadata> {
691        let meta_path = self.storage_path.join(format!("{domain}.meta.json"));
692        if !meta_path.exists() {
693            return None;
694        }
695
696        match tokio::fs::read_to_string(&meta_path).await {
697            Ok(json) => match serde_json::from_str(&json) {
698                Ok(meta) => Some(meta),
699                Err(e) => {
700                    tracing::warn!(
701                        domain = %domain,
702                        error = %e,
703                        "Failed to parse certificate metadata"
704                    );
705                    None
706                }
707            },
708            Err(e) => {
709                tracing::warn!(
710                    domain = %domain,
711                    error = %e,
712                    "Failed to read certificate metadata file"
713                );
714                None
715            }
716        }
717    }
718
719    /// Get domains with certificates expiring within a threshold
720    ///
721    /// Returns domains with certificates that expire within 30 days (`RENEWAL_THRESHOLD_DAYS`).
722    ///
723    /// # Returns
724    /// Vector of domain names with certificates needing renewal
725    pub async fn get_domains_needing_renewal(&self) -> Vec<String> {
726        let threshold = Utc::now() + TimeDelta::days(RENEWAL_THRESHOLD_DAYS);
727        let mut domains_needing_renewal = Vec::new();
728
729        // Read directory for .meta.json files
730        let mut entries = match tokio::fs::read_dir(&self.storage_path).await {
731            Ok(entries) => entries,
732            Err(e) => {
733                tracing::warn!(error = %e, "Failed to read certificate storage directory");
734                return domains_needing_renewal;
735            }
736        };
737
738        while let Ok(Some(entry)) = entries.next_entry().await {
739            let path = entry.path();
740            if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
741                if filename.ends_with(".meta.json") {
742                    let domain = filename.trim_end_matches(".meta.json");
743                    if let Some(meta) = self.load_cert_metadata(domain).await {
744                        if meta.not_after <= threshold {
745                            tracing::debug!(
746                                domain = %domain,
747                                expires = %meta.not_after,
748                                "Certificate needs renewal"
749                            );
750                            domains_needing_renewal.push(domain.to_string());
751                        }
752                    }
753                }
754            }
755        }
756
757        domains_needing_renewal
758    }
759
760    // =========================================================================
761    // Automatic Certificate Renewal
762    // =========================================================================
763
764    /// Start background certificate renewal task
765    ///
766    /// This spawns a tokio task that checks certificates every 12 hours
767    /// and renews any that expire within 30 days.
768    ///
769    /// # Arguments
770    /// * `sni_resolver` - The SNI resolver to update with renewed certificates
771    ///
772    /// # Returns
773    /// A `JoinHandle` for the spawned renewal task
774    pub fn start_renewal_task(
775        self: Arc<Self>,
776        sni_resolver: Arc<SniCertResolver>,
777    ) -> tokio::task::JoinHandle<()> {
778        tokio::spawn(async move {
779            // Check every 12 hours
780            let mut interval = tokio::time::interval(Duration::from_secs(43200));
781
782            loop {
783                interval.tick().await;
784
785                tracing::info!("Starting certificate renewal check");
786
787                // Get domains needing renewal
788                let domains = self.get_domains_needing_renewal().await;
789
790                if domains.is_empty() {
791                    tracing::debug!("No certificates need renewal");
792                    continue;
793                }
794
795                tracing::info!(count = domains.len(), "Certificates need renewal");
796
797                for domain in domains {
798                    tracing::info!(domain = %domain, "Attempting certificate renewal");
799
800                    match self.provision_cert(&domain).await {
801                        Ok((cert_pem, key_pem)) => {
802                            tracing::info!(domain = %domain, "Certificate renewed successfully");
803
804                            // Update SNI resolver with new certificate
805                            if let Err(e) = sni_resolver.refresh_cert(&domain, &cert_pem, &key_pem)
806                            {
807                                tracing::error!(
808                                    domain = %domain,
809                                    error = %e,
810                                    "Failed to update SNI resolver with renewed cert"
811                                );
812                            }
813                        }
814                        Err(e) => {
815                            tracing::error!(
816                                domain = %domain,
817                                error = %e,
818                                "Certificate renewal failed"
819                            );
820                        }
821                    }
822
823                    // Small delay between renewals to avoid rate limiting
824                    tokio::time::sleep(Duration::from_secs(10)).await;
825                }
826            }
827        })
828    }
829
830    /// Run a single renewal check (for testing or on-demand renewal)
831    ///
832    /// This method checks for certificates needing renewal and attempts to renew them.
833    /// Unlike `start_renewal_task`, this runs once and returns immediately.
834    ///
835    /// # Arguments
836    /// * `sni_resolver` - The SNI resolver to update with renewed certificates
837    ///
838    /// # Returns
839    /// Vector of domain names that were successfully renewed
840    pub async fn run_renewal_check(&self, sni_resolver: &SniCertResolver) -> Vec<String> {
841        let domains = self.get_domains_needing_renewal().await;
842        let mut renewed = Vec::new();
843
844        for domain in domains {
845            match self.provision_cert(&domain).await {
846                Ok((cert_pem, key_pem)) => {
847                    if sni_resolver
848                        .refresh_cert(&domain, &cert_pem, &key_pem)
849                        .is_ok()
850                    {
851                        renewed.push(domain);
852                    }
853                }
854                Err(e) => {
855                    tracing::warn!(domain = %domain, error = %e, "Renewal failed");
856                }
857            }
858        }
859
860        renewed
861    }
862
863    // =========================================================================
864    // ACME Account Management
865    // =========================================================================
866
867    /// Get the path to the account metadata storage file
868    fn account_path(&self) -> PathBuf {
869        self.storage_path.join("account.json")
870    }
871
872    /// Get the path to the account credentials storage file
873    fn credentials_path(&self) -> PathBuf {
874        self.storage_path.join("account_credentials.json")
875    }
876
877    /// Load an existing ACME account from disk
878    ///
879    /// This loads the account metadata. Use `load_credentials()` to load the
880    /// actual credentials needed for ACME operations.
881    ///
882    /// # Returns
883    /// The account if it exists and is valid, None otherwise
884    pub async fn load_account(&self) -> Option<AcmeAccount> {
885        // Check if both files exist (need credentials to be usable)
886        if !self.credentials_path().exists() {
887            return None;
888        }
889        self.load_account_metadata().await
890    }
891
892    /// Load account metadata from disk
893    async fn load_account_metadata(&self) -> Option<AcmeAccount> {
894        let account_path = self.account_path();
895
896        if !account_path.exists() {
897            tracing::debug!(path = %account_path.display(), "No ACME account file found");
898            return None;
899        }
900
901        match tokio::fs::read_to_string(&account_path).await {
902            Ok(content) => match serde_json::from_str::<AcmeAccount>(&content) {
903                Ok(account) => {
904                    tracing::debug!(
905                        account_url = %account.account_url,
906                        created_at = %account.created_at,
907                        "Loaded ACME account from disk"
908                    );
909                    Some(account)
910                }
911                Err(e) => {
912                    tracing::warn!(
913                        error = %e,
914                        path = %account_path.display(),
915                        "Failed to parse ACME account file"
916                    );
917                    None
918                }
919            },
920            Err(e) => {
921                tracing::warn!(
922                    error = %e,
923                    path = %account_path.display(),
924                    "Failed to read ACME account file"
925                );
926                None
927            }
928        }
929    }
930
931    /// Load ACME credentials from disk
932    ///
933    /// Since `AccountCredentials` doesn't implement Clone, we load it fresh
934    /// each time it's needed.
935    async fn load_credentials(&self) -> Option<AccountCredentials> {
936        let credentials_path = self.credentials_path();
937
938        if !credentials_path.exists() {
939            tracing::debug!(path = %credentials_path.display(), "No ACME credentials file found");
940            return None;
941        }
942
943        match tokio::fs::read_to_string(&credentials_path).await {
944            Ok(content) => match serde_json::from_str::<AccountCredentials>(&content) {
945                Ok(credentials) => {
946                    tracing::debug!("Loaded ACME credentials from disk");
947                    Some(credentials)
948                }
949                Err(e) => {
950                    tracing::warn!(
951                        error = %e,
952                        path = %credentials_path.display(),
953                        "Failed to parse ACME credentials file"
954                    );
955                    None
956                }
957            },
958            Err(e) => {
959                tracing::warn!(
960                    error = %e,
961                    path = %credentials_path.display(),
962                    "Failed to read ACME credentials file"
963                );
964                None
965            }
966        }
967    }
968
969    /// Save an ACME account to disk (metadata only)
970    ///
971    /// # Arguments
972    /// * `account` - The account to save
973    ///
974    /// # Errors
975    ///
976    /// Returns an error if serialization or writing to disk fails.
977    pub async fn save_account(
978        &self,
979        account: &AcmeAccount,
980    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
981        self.save_account_metadata(account).await
982    }
983
984    /// Save account metadata to disk
985    async fn save_account_metadata(
986        &self,
987        account: &AcmeAccount,
988    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
989        let account_path = self.account_path();
990
991        let content = serde_json::to_string_pretty(account)?;
992        tokio::fs::write(&account_path, content).await?;
993
994        // Update the cached account
995        *self.account.write().await = Some(account.clone());
996
997        tracing::info!(
998            account_url = %account.account_url,
999            path = %account_path.display(),
1000            "Saved ACME account metadata to disk"
1001        );
1002
1003        Ok(())
1004    }
1005
1006    /// Save ACME credentials to disk
1007    async fn save_credentials(
1008        &self,
1009        credentials: &AccountCredentials,
1010    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1011        let credentials_path = self.credentials_path();
1012
1013        let content = serde_json::to_string_pretty(credentials)?;
1014        tokio::fs::write(&credentials_path, content).await?;
1015
1016        tracing::info!(
1017            path = %credentials_path.display(),
1018            "Saved ACME credentials to disk"
1019        );
1020
1021        Ok(())
1022    }
1023
1024    /// Get or create an ACME account (returns our metadata struct)
1025    ///
1026    /// This method:
1027    /// 1. Returns the cached account if available
1028    /// 2. Loads the account from disk if it exists
1029    /// 3. Creates a new account via ACME
1030    ///
1031    /// # Errors
1032    ///
1033    /// Returns an error if ACME account creation or restoration fails.
1034    pub async fn get_or_create_account(
1035        &self,
1036    ) -> Result<AcmeAccount, Box<dyn std::error::Error + Send + Sync>> {
1037        // Check cached account first
1038        {
1039            let account = self.account.read().await;
1040            if let Some(ref acc) = *account {
1041                return Ok(acc.clone());
1042            }
1043        }
1044
1045        // Try to load from disk
1046        if let Some(account) = self.load_account().await {
1047            *self.account.write().await = Some(account.clone());
1048            return Ok(account);
1049        }
1050
1051        // Create a new account (this will also cache it)
1052        let _account = self.get_or_create_acme_account().await?;
1053        let account_meta = self
1054            .account
1055            .read()
1056            .await
1057            .clone()
1058            .ok_or("Account was created but metadata not cached - this is a bug")?;
1059
1060        Ok(account_meta)
1061    }
1062
1063    /// Get or create an instant-acme Account object
1064    ///
1065    /// This is the internal method that returns the actual instant-acme Account
1066    /// needed for ACME operations. Since `AccountCredentials` doesn't implement Clone,
1067    /// we load credentials from disk each time.
1068    async fn get_or_create_acme_account(
1069        &self,
1070    ) -> Result<Account, Box<dyn std::error::Error + Send + Sync>> {
1071        // Try to load credentials from disk
1072        if let Some(credentials) = self.load_credentials().await {
1073            tracing::debug!("Restoring ACME account from saved credentials");
1074
1075            let account = Account::from_credentials(credentials)
1076                .await
1077                .map_err(|e| format!("Failed to restore account from saved credentials: {e}"))?;
1078
1079            // Ensure account metadata is cached
1080            if self.account.read().await.is_none() {
1081                if let Some(account_meta) = self.load_account_metadata().await {
1082                    *self.account.write().await = Some(account_meta);
1083                }
1084            }
1085
1086            return Ok(account);
1087        }
1088
1089        // Create a new account.
1090        //
1091        // RFC 8555 §7.3 permits an ACME account with no contact information, so
1092        // the email is optional. When configured we register a `mailto:` contact;
1093        // when absent we create a contact-less account.
1094        let contact = self
1095            .acme_email
1096            .as_ref()
1097            .map(|email| format!("mailto:{email}"));
1098
1099        if let Some(c) = contact.as_deref() {
1100            tracing::info!(
1101                contact = %c,
1102                directory = %self.acme_directory,
1103                "Creating new ACME account"
1104            );
1105        } else {
1106            tracing::info!(
1107                directory = %self.acme_directory,
1108                "Creating new ACME account with no contact email configured"
1109            );
1110        }
1111
1112        // `NewAccount.contact` is a `&[&str]`; an empty slice registers no contact.
1113        let contact_vec: Vec<&str> = contact.iter().map(String::as_str).collect();
1114        let new_account = NewAccount {
1115            contact: &contact_vec,
1116            terms_of_service_agreed: true,
1117            only_return_existing: false,
1118        };
1119
1120        let (account, credentials) = Account::create(&new_account, &self.acme_directory, None)
1121            .await
1122            .map_err(|e| {
1123                tracing::error!(error = %e, "Failed to create ACME account");
1124                format!("Failed to create ACME account: {e}")
1125            })?;
1126
1127        // Create our metadata struct
1128        let account_meta = AcmeAccount {
1129            account_url: account.id().to_string(),
1130            account_key_pem: String::new(), // We store credentials separately
1131            contact: contact.into_iter().collect(),
1132            created_at: Utc::now(),
1133        };
1134
1135        // Save account and credentials to disk
1136        self.save_account_metadata(&account_meta).await?;
1137        self.save_credentials(&credentials).await?;
1138
1139        // Cache the metadata
1140        *self.account.write().await = Some(account_meta.clone());
1141
1142        tracing::info!(
1143            account_url = %account_meta.account_url,
1144            "Successfully created ACME account"
1145        );
1146
1147        Ok(account)
1148    }
1149
1150    /// Get the cached ACME account (if any)
1151    ///
1152    /// # Returns
1153    /// The cached account, or None if not loaded
1154    pub async fn get_account(&self) -> Option<AcmeAccount> {
1155        self.account.read().await.clone()
1156    }
1157
1158    /// Check if an ACME account exists (either cached or on disk)
1159    pub async fn has_account(&self) -> bool {
1160        // Check cache first
1161        {
1162            let account = self.account.read().await;
1163            if account.is_some() {
1164                return true;
1165            }
1166        }
1167
1168        // Check disk
1169        self.account_path().exists()
1170    }
1171
1172    // =========================================================================
1173    // ACME HTTP-01 Challenge Management
1174    // =========================================================================
1175
1176    /// Store an ACME HTTP-01 challenge token
1177    ///
1178    /// This stores the challenge token that will be served at
1179    /// `/.well-known/acme-challenge/{token}` for domain validation.
1180    ///
1181    /// # Arguments
1182    /// * `token` - The challenge token from the ACME server
1183    /// * `domain` - The domain being validated
1184    /// * `key_authorization` - The key authorization response (token.thumbprint)
1185    pub fn store_challenge(&self, token: &str, domain: &str, key_authorization: &str) {
1186        let challenge = ChallengeToken {
1187            token: token.to_string(),
1188            key_authorization: key_authorization.to_string(),
1189            domain: domain.to_string(),
1190            created_at: Instant::now(),
1191        };
1192        self.challenges.insert(token.to_string(), challenge);
1193        tracing::debug!(
1194            token = %token,
1195            domain = %domain,
1196            "Stored ACME challenge token"
1197        );
1198    }
1199
1200    /// Get the key authorization response for a challenge token
1201    ///
1202    /// # Arguments
1203    /// * `token` - The challenge token from the request path
1204    ///
1205    /// # Returns
1206    /// The key authorization string if the token exists and hasn't expired
1207    pub fn get_challenge_response(&self, token: &str) -> Option<String> {
1208        self.challenges
1209            .get(token)
1210            .filter(|challenge| challenge.created_at.elapsed() < CHALLENGE_EXPIRATION)
1211            .map(|challenge| challenge.key_authorization.clone())
1212    }
1213
1214    /// Remove a challenge token after validation completes
1215    ///
1216    /// # Arguments
1217    /// * `token` - The challenge token to remove
1218    pub fn remove_challenge(&self, token: &str) {
1219        if self.challenges.remove(token).is_some() {
1220            tracing::debug!(token = %token, "Removed ACME challenge token");
1221        }
1222    }
1223
1224    /// Clear all challenge tokens for a specific domain
1225    ///
1226    /// Useful when certificate issuance completes or fails for a domain.
1227    ///
1228    /// # Arguments
1229    /// * `domain` - The domain to clear challenges for
1230    pub fn clear_challenges_for_domain(&self, domain: &str) {
1231        let tokens_to_remove: Vec<String> = self
1232            .challenges
1233            .iter()
1234            .filter(|entry| entry.value().domain == domain)
1235            .map(|entry| entry.key().clone())
1236            .collect();
1237
1238        for token in &tokens_to_remove {
1239            self.challenges.remove(token);
1240        }
1241
1242        if !tokens_to_remove.is_empty() {
1243            tracing::debug!(
1244                domain = %domain,
1245                count = tokens_to_remove.len(),
1246                "Cleared ACME challenge tokens for domain"
1247            );
1248        }
1249    }
1250
1251    /// Clean up expired challenge tokens
1252    ///
1253    /// Removes all challenge tokens older than 5 minutes.
1254    /// This should be called periodically to prevent memory leaks.
1255    pub fn cleanup_expired_challenges(&self) {
1256        let expired_tokens: Vec<String> = self
1257            .challenges
1258            .iter()
1259            .filter(|entry| entry.value().created_at.elapsed() >= CHALLENGE_EXPIRATION)
1260            .map(|entry| entry.key().clone())
1261            .collect();
1262
1263        for token in &expired_tokens {
1264            self.challenges.remove(token);
1265        }
1266
1267        if !expired_tokens.is_empty() {
1268            tracing::debug!(
1269                count = expired_tokens.len(),
1270                "Cleaned up expired ACME challenge tokens"
1271            );
1272        }
1273    }
1274
1275    /// Get the number of active challenge tokens
1276    pub fn challenge_count(&self) -> usize {
1277        self.challenges.len()
1278    }
1279}
1280
1281#[cfg(test)]
1282mod tests {
1283    use super::*;
1284    use tempfile::tempdir;
1285
1286    /// Idempotently install a default `rustls::CryptoProvider` for tests that
1287    /// reach the ACME network/TLS path (`Account::create` builds a rustls client
1288    /// and panics otherwise, because the workspace compiles in both `aws-lc-rs`
1289    /// and `ring`). The workspace pins `aws-lc-rs` as the rustls default, so we
1290    /// install that. Safe to call repeatedly — the second install returns `Err`,
1291    /// which we discard.
1292    fn ensure_crypto_provider() {
1293        use std::sync::Once;
1294        static INSTALLED: Once = Once::new();
1295        INSTALLED.call_once(|| {
1296            let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
1297        });
1298    }
1299
1300    #[tokio::test]
1301    async fn test_cert_manager_creation() {
1302        let dir = tempdir().unwrap();
1303        let manager = CertManager::new(
1304            dir.path().to_string_lossy().to_string(),
1305            Some("test@example.com".to_string()),
1306        )
1307        .await
1308        .unwrap();
1309
1310        assert_eq!(manager.acme_email(), Some("test@example.com"));
1311        assert!(manager.storage_path().exists());
1312    }
1313
1314    #[tokio::test]
1315    async fn test_store_and_get_cert() {
1316        let dir = tempdir().unwrap();
1317        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1318            .await
1319            .unwrap();
1320
1321        // Store a test certificate
1322        let cert = "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----";
1323        let key = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
1324
1325        manager
1326            .store_cert("test.example.com", cert, key)
1327            .await
1328            .unwrap();
1329
1330        // Should be in cache
1331        assert!(manager.has_cert("test.example.com").await);
1332
1333        // Clear cache and verify disk read
1334        manager.clear_cache().await;
1335        assert!(manager.has_cert("test.example.com").await);
1336
1337        // Get the certificate
1338        let (retrieved_cert, retrieved_key) = manager.get_cert("test.example.com").await.unwrap();
1339        assert_eq!(retrieved_cert, cert);
1340        assert_eq!(retrieved_key, key);
1341    }
1342
1343    #[tokio::test]
1344    async fn test_cert_not_found() {
1345        ensure_crypto_provider();
1346        let dir = tempdir().unwrap();
1347        // Point at an unreachable ACME directory: provisioning is now attempted
1348        // even without an email, so the failure comes from the network rather
1349        // than a missing-email gate (and we avoid touching a real ACME server).
1350        let manager = CertManager::with_directory(
1351            dir.path().to_string_lossy().to_string(),
1352            None,
1353            "https://127.0.0.1:1/directory".to_string(),
1354        )
1355        .await
1356        .unwrap();
1357
1358        let result = manager.get_cert("nonexistent.example.com").await;
1359        assert!(result.is_err());
1360    }
1361
1362    #[tokio::test]
1363    async fn test_store_and_get_challenge() {
1364        let dir = tempdir().unwrap();
1365        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1366            .await
1367            .unwrap();
1368
1369        let token = "abc123";
1370        let domain = "test.example.com";
1371        let key_auth = "abc123.thumbprint";
1372
1373        // Store a challenge
1374        manager.store_challenge(token, domain, key_auth);
1375        assert_eq!(manager.challenge_count(), 1);
1376
1377        // Retrieve it
1378        let response = manager.get_challenge_response(token);
1379        assert_eq!(response, Some(key_auth.to_string()));
1380
1381        // Non-existent token returns None
1382        let missing = manager.get_challenge_response("nonexistent");
1383        assert!(missing.is_none());
1384    }
1385
1386    #[tokio::test]
1387    async fn test_remove_challenge() {
1388        let dir = tempdir().unwrap();
1389        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1390            .await
1391            .unwrap();
1392
1393        manager.store_challenge("token1", "domain.com", "auth1");
1394        assert_eq!(manager.challenge_count(), 1);
1395
1396        manager.remove_challenge("token1");
1397        assert_eq!(manager.challenge_count(), 0);
1398
1399        // Removing non-existent token is a no-op
1400        manager.remove_challenge("nonexistent");
1401        assert_eq!(manager.challenge_count(), 0);
1402    }
1403
1404    #[tokio::test]
1405    async fn test_clear_challenges_for_domain() {
1406        let dir = tempdir().unwrap();
1407        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1408            .await
1409            .unwrap();
1410
1411        // Store challenges for multiple domains
1412        manager.store_challenge("token1", "domain1.com", "auth1");
1413        manager.store_challenge("token2", "domain1.com", "auth2");
1414        manager.store_challenge("token3", "domain2.com", "auth3");
1415        assert_eq!(manager.challenge_count(), 3);
1416
1417        // Clear only domain1.com challenges
1418        manager.clear_challenges_for_domain("domain1.com");
1419        assert_eq!(manager.challenge_count(), 1);
1420
1421        // domain2.com challenge should still exist
1422        let response = manager.get_challenge_response("token3");
1423        assert!(response.is_some());
1424    }
1425
1426    #[tokio::test]
1427    async fn test_cleanup_expired_challenges() {
1428        let dir = tempdir().unwrap();
1429        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1430            .await
1431            .unwrap();
1432
1433        // Store a challenge
1434        manager.store_challenge("token1", "domain.com", "auth1");
1435        assert_eq!(manager.challenge_count(), 1);
1436
1437        // Cleanup should not remove fresh challenges
1438        manager.cleanup_expired_challenges();
1439        assert_eq!(manager.challenge_count(), 1);
1440
1441        // Note: We can't easily test expiration without mocking time,
1442        // but the cleanup logic is tested by verifying it doesn't remove fresh tokens
1443    }
1444
1445    // =========================================================================
1446    // ACME Account Persistence Tests
1447    // =========================================================================
1448
1449    #[tokio::test]
1450    async fn test_save_and_load_account() {
1451        let dir = tempdir().unwrap();
1452        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1453            .await
1454            .unwrap();
1455
1456        // Initially no account
1457        assert!(!manager.has_account().await);
1458        assert!(manager.get_account().await.is_none());
1459
1460        // Create and save an account
1461        let account = AcmeAccount {
1462            account_url: "https://acme.example.com/acct/12345".to_string(),
1463            account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1464                .to_string(),
1465            contact: vec!["mailto:test@example.com".to_string()],
1466            created_at: Utc::now(),
1467        };
1468
1469        manager.save_account(&account).await.unwrap();
1470
1471        // Should be cached
1472        assert!(manager.has_account().await);
1473        let cached = manager.get_account().await.unwrap();
1474        assert_eq!(cached.account_url, account.account_url);
1475        assert_eq!(cached.contact, account.contact);
1476
1477        // Verify file exists on disk
1478        let account_path = dir.path().join("account.json");
1479        assert!(account_path.exists());
1480    }
1481
1482    #[tokio::test]
1483    async fn test_load_account_from_disk() {
1484        let dir = tempdir().unwrap();
1485
1486        // Create an account file manually
1487        let account = AcmeAccount {
1488            account_url: "https://acme.example.com/acct/67890".to_string(),
1489            account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1490                .to_string(),
1491            contact: vec!["mailto:admin@example.com".to_string()],
1492            created_at: Utc::now(),
1493        };
1494
1495        let account_path = dir.path().join("account.json");
1496        let content = serde_json::to_string_pretty(&account).unwrap();
1497        std::fs::write(&account_path, content).unwrap();
1498
1499        // Also need to create a mock credentials file for load_account to work
1500        // The credentials file must be valid JSON that deserializes to AccountCredentials
1501        // We use a minimal valid credentials structure here for testing
1502        let credentials_path = dir.path().join("account_credentials.json");
1503        // This is a minimal valid AccountCredentials JSON (the exact format is opaque but serializable)
1504        let mock_credentials = r#"{"id":"https://acme.example.com/acct/67890","key_pkcs8":"base64data","urls":{"newNonce":"https://acme.example.com/acme/new-nonce","newAccount":"https://acme.example.com/acme/new-account","newOrder":"https://acme.example.com/acme/new-order"}}"#;
1505        std::fs::write(&credentials_path, mock_credentials).unwrap();
1506
1507        // Create manager - should load the account automatically
1508        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1509            .await
1510            .unwrap();
1511
1512        assert!(manager.has_account().await);
1513        let loaded = manager.get_account().await.unwrap();
1514        assert_eq!(loaded.account_url, account.account_url);
1515        assert_eq!(loaded.contact, account.contact);
1516    }
1517
1518    #[tokio::test]
1519    async fn test_get_or_create_account_returns_cached() {
1520        let dir = tempdir().unwrap();
1521        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1522            .await
1523            .unwrap();
1524
1525        // Save an account first
1526        let account = AcmeAccount {
1527            account_url: "https://acme.example.com/acct/11111".to_string(),
1528            account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1529                .to_string(),
1530            contact: vec!["mailto:cached@example.com".to_string()],
1531            created_at: Utc::now(),
1532        };
1533        manager.save_account(&account).await.unwrap();
1534
1535        // get_or_create should return the cached account
1536        let result = manager.get_or_create_account().await.unwrap();
1537        assert_eq!(result.account_url, account.account_url);
1538    }
1539
1540    #[tokio::test]
1541    async fn test_get_or_create_account_without_existing() {
1542        ensure_crypto_provider();
1543        let dir = tempdir().unwrap();
1544        // No email configured. RFC 8555 §7.3 allows a contact-less ACME account,
1545        // so account creation must NOT be gated on a missing email — it proceeds
1546        // to talk to the ACME directory. We point at an unreachable directory so
1547        // the test fails fast on the network instead of registering against a real
1548        // ACME server, and assert it does NOT fail with the old email gate.
1549        let manager = CertManager::with_directory(
1550            dir.path().to_string_lossy().to_string(),
1551            None,
1552            "https://127.0.0.1:1/directory".to_string(),
1553        )
1554        .await
1555        .unwrap();
1556
1557        let err = manager
1558            .get_or_create_account()
1559            .await
1560            .expect_err("account creation must fail against an unreachable ACME directory")
1561            .to_string();
1562        assert!(
1563            !err.contains("ACME email is required"),
1564            "Account creation must not be gated on a missing email, got: {err}",
1565        );
1566    }
1567
1568    #[tokio::test]
1569    async fn test_load_invalid_account_file() {
1570        let dir = tempdir().unwrap();
1571
1572        // Create an invalid JSON file
1573        let account_path = dir.path().join("account.json");
1574        std::fs::write(&account_path, "{ invalid json }").unwrap();
1575
1576        // Manager should still be created, but no valid account should be loaded
1577        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1578            .await
1579            .unwrap();
1580
1581        // The file exists on disk, so has_account returns true (checking file existence)
1582        // But get_account returns None because the invalid JSON could not be
1583        // parsed into a cached account.
1584        assert!(manager.get_account().await.is_none());
1585
1586        // load_account must reject the corrupt file rather than returning a
1587        // bogus account. (We deliberately do NOT call get_or_create_account
1588        // here: account creation now proceeds even without an email — RFC 8555
1589        // allows a contact-less account — so it would hit the real ACME server
1590        // instead of failing locally.)
1591        assert!(manager.load_account().await.is_none());
1592    }
1593
1594    #[tokio::test]
1595    async fn test_account_persistence_across_instances() {
1596        let dir = tempdir().unwrap();
1597
1598        // Create first manager and save account with credentials
1599        {
1600            let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1601                .await
1602                .unwrap();
1603
1604            let account = AcmeAccount {
1605                account_url: "https://acme.example.com/acct/persist".to_string(),
1606                account_key_pem:
1607                    "-----BEGIN EC PRIVATE KEY-----\npersist\n-----END EC PRIVATE KEY-----"
1608                        .to_string(),
1609                contact: vec!["mailto:persist@example.com".to_string()],
1610                created_at: Utc::now(),
1611            };
1612            manager.save_account(&account).await.unwrap();
1613
1614            // Also need to create a mock credentials file for load_account to work
1615            let credentials_path = dir.path().join("account_credentials.json");
1616            let mock_credentials = r#"{"id":"https://acme.example.com/acct/persist","key_pkcs8":"base64data","urls":{"newNonce":"https://acme.example.com/acme/new-nonce","newAccount":"https://acme.example.com/acme/new-account","newOrder":"https://acme.example.com/acme/new-order"}}"#;
1617            std::fs::write(&credentials_path, mock_credentials).unwrap();
1618        }
1619
1620        // Create second manager - should load the persisted account
1621        let manager2 = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1622            .await
1623            .unwrap();
1624
1625        assert!(manager2.has_account().await);
1626        let loaded = manager2.get_account().await.unwrap();
1627        assert_eq!(loaded.account_url, "https://acme.example.com/acct/persist");
1628    }
1629
1630    // =========================================================================
1631    // Certificate Metadata Tests
1632    // =========================================================================
1633
1634    /// Generate a valid test certificate using rcgen
1635    fn generate_test_cert() -> (String, String) {
1636        use rcgen::{CertificateParams, KeyPair};
1637
1638        let key_pair = KeyPair::generate().unwrap();
1639        let params = CertificateParams::new(vec!["test.example.com".to_string()]).unwrap();
1640        let cert = params.self_signed(&key_pair).unwrap();
1641        (cert.pem(), key_pair.serialize_pem())
1642    }
1643
1644    /// Regression test for the ACME `rejectedIdentifier` finalize failure:
1645    /// the CSR subject must carry the real domain as its `CommonName`, NOT
1646    /// rcgen's default placeholder "rcgen self signed cert" (which Let's
1647    /// Encrypt rejected as an invalid requested identifier).
1648    #[test]
1649    fn test_build_csr_subject_uses_domain_not_placeholder() {
1650        use rcgen::KeyPair;
1651        use x509_parser::prelude::{FromDer, X509CertificationRequest};
1652
1653        let key_pair = KeyPair::generate().unwrap();
1654        let domain = "example.com";
1655
1656        let csr = CertManager::build_csr(domain, &key_pair).expect("build_csr should succeed");
1657
1658        // Parse the exact DER that would be sent to `order.finalize`.
1659        let (_, parsed) =
1660            X509CertificationRequest::from_der(csr.der()).expect("CSR DER should parse");
1661        let subject = parsed.certification_request_info.subject.to_string();
1662
1663        // The rcgen placeholder CN must be gone.
1664        assert!(
1665            !subject.contains("rcgen self signed cert"),
1666            "CSR subject still contains rcgen placeholder CN: {subject}"
1667        );
1668        // The real domain must be the CommonName.
1669        assert!(
1670            subject.contains(&format!("CN={domain}")),
1671            "CSR subject does not carry the domain as CommonName: {subject}"
1672        );
1673    }
1674
1675    /// The domain must also be present as a SAN (`DnsName`) on the params used
1676    /// to build the CSR — `CertificateParams::new` is responsible for this.
1677    #[test]
1678    fn test_build_csr_params_carry_domain_san_and_cn() {
1679        use rcgen::{CertificateParams, DnType, DnValue, SanType};
1680
1681        let domain = "service.example.org";
1682
1683        let mut params = CertificateParams::new(vec![domain.to_string()]).unwrap();
1684        let mut dn = rcgen::DistinguishedName::new();
1685        dn.push(DnType::CommonName, domain);
1686        params.distinguished_name = dn;
1687
1688        // CommonName in the DN is the real domain (UTF-8 string value).
1689        assert_eq!(
1690            params.distinguished_name.get(&DnType::CommonName),
1691            Some(&DnValue::Utf8String(domain.to_string())),
1692            "distinguished_name CommonName should equal the domain"
1693        );
1694
1695        // The SAN list carries the domain as a DnsName entry.
1696        let has_san = params
1697            .subject_alt_names
1698            .iter()
1699            .any(|san| matches!(san, SanType::DnsName(name) if name.as_str() == domain));
1700        assert!(
1701            has_san,
1702            "subject_alt_names should contain the domain as a DnsName SAN: {:?}",
1703            params.subject_alt_names
1704        );
1705    }
1706
1707    #[tokio::test]
1708    async fn test_parse_cert_expiry() {
1709        // Generate a valid test certificate
1710        let (cert_pem, _key_pem) = generate_test_cert();
1711
1712        let result = CertManager::parse_cert_expiry(&cert_pem);
1713        assert!(result.is_ok(), "Failed to parse cert: {:?}", result.err());
1714
1715        let (not_before, not_after) = result.unwrap();
1716        // The certificate should have valid timestamps
1717        assert!(not_before < not_after);
1718    }
1719
1720    #[tokio::test]
1721    async fn test_parse_cert_expiry_invalid_pem() {
1722        let result = CertManager::parse_cert_expiry("not a valid pem");
1723        assert!(result.is_err());
1724    }
1725
1726    #[tokio::test]
1727    async fn test_cert_metadata_save_load() {
1728        let dir = tempdir().unwrap();
1729        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1730            .await
1731            .unwrap();
1732
1733        let meta = CertMetadata {
1734            domain: "test.example.com".to_string(),
1735            not_before: Utc::now(),
1736            not_after: Utc::now() + TimeDelta::days(90),
1737            provisioned_at: Utc::now(),
1738            fingerprint: "abc123def456".to_string(),
1739        };
1740
1741        // Save metadata
1742        manager.save_cert_metadata(&meta).await.unwrap();
1743
1744        // Verify file exists
1745        let meta_path = dir.path().join("test.example.com.meta.json");
1746        assert!(meta_path.exists());
1747
1748        // Load metadata
1749        let loaded = manager.load_cert_metadata("test.example.com").await;
1750        assert!(loaded.is_some());
1751
1752        let loaded = loaded.unwrap();
1753        assert_eq!(loaded.domain, "test.example.com");
1754        assert_eq!(loaded.fingerprint, "abc123def456");
1755    }
1756
1757    #[tokio::test]
1758    async fn test_load_nonexistent_metadata() {
1759        let dir = tempdir().unwrap();
1760        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1761            .await
1762            .unwrap();
1763
1764        let loaded = manager.load_cert_metadata("nonexistent.example.com").await;
1765        assert!(loaded.is_none());
1766    }
1767
1768    #[tokio::test]
1769    async fn test_get_domains_needing_renewal() {
1770        let dir = tempdir().unwrap();
1771        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1772            .await
1773            .unwrap();
1774
1775        // Create a certificate that expires in 10 days (needs renewal)
1776        let expiring_meta = CertMetadata {
1777            domain: "expiring.example.com".to_string(),
1778            not_before: Utc::now() - TimeDelta::days(80),
1779            not_after: Utc::now() + TimeDelta::days(10),
1780            provisioned_at: Utc::now() - TimeDelta::days(80),
1781            fingerprint: "expiring_fingerprint".to_string(),
1782        };
1783        manager.save_cert_metadata(&expiring_meta).await.unwrap();
1784
1785        // Create a certificate that expires in 60 days (does not need renewal)
1786        let valid_meta = CertMetadata {
1787            domain: "valid.example.com".to_string(),
1788            not_before: Utc::now() - TimeDelta::days(30),
1789            not_after: Utc::now() + TimeDelta::days(60),
1790            provisioned_at: Utc::now() - TimeDelta::days(30),
1791            fingerprint: "valid_fingerprint".to_string(),
1792        };
1793        manager.save_cert_metadata(&valid_meta).await.unwrap();
1794
1795        // Check renewal list
1796        let needs_renewal = manager.get_domains_needing_renewal().await;
1797        assert_eq!(needs_renewal.len(), 1);
1798        assert!(needs_renewal.contains(&"expiring.example.com".to_string()));
1799    }
1800
1801    #[tokio::test]
1802    async fn test_store_cert_with_valid_pem_saves_metadata() {
1803        let dir = tempdir().unwrap();
1804        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1805            .await
1806            .unwrap();
1807
1808        // Generate a valid test certificate
1809        let (cert_pem, key_pem) = generate_test_cert();
1810
1811        // Store a valid certificate (this should also save metadata)
1812        manager
1813            .store_cert("metadata.example.com", &cert_pem, &key_pem)
1814            .await
1815            .unwrap();
1816
1817        // Verify metadata was saved
1818        let meta = manager.load_cert_metadata("metadata.example.com").await;
1819        assert!(meta.is_some(), "Metadata was not saved");
1820
1821        let meta = meta.unwrap();
1822        assert_eq!(meta.domain, "metadata.example.com");
1823        assert!(!meta.fingerprint.is_empty());
1824        assert!(meta.not_before < meta.not_after);
1825    }
1826
1827    #[tokio::test]
1828    async fn test_store_cert_with_invalid_pem_still_stores_cert() {
1829        let dir = tempdir().unwrap();
1830        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1831            .await
1832            .unwrap();
1833
1834        // Store an invalid certificate (should still store cert, just skip metadata)
1835        let invalid_cert = "-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----";
1836        let key = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
1837
1838        manager
1839            .store_cert("invalid.example.com", invalid_cert, key)
1840            .await
1841            .unwrap();
1842
1843        // Certificate should still be stored
1844        assert!(manager.has_cert("invalid.example.com").await);
1845
1846        // Metadata should not exist (parsing failed)
1847        let meta = manager.load_cert_metadata("invalid.example.com").await;
1848        assert!(meta.is_none());
1849    }
1850
1851    #[tokio::test]
1852    async fn test_cert_fingerprint_is_consistent() {
1853        // Generate a test certificate
1854        let (cert_pem, _) = generate_test_cert();
1855
1856        // Compute fingerprint twice, should be identical
1857        let fp1 = CertManager::compute_cert_fingerprint(&cert_pem);
1858        let fp2 = CertManager::compute_cert_fingerprint(&cert_pem);
1859        assert_eq!(fp1, fp2);
1860
1861        // Different cert should have different fingerprint
1862        let (other_cert, _) = generate_test_cert();
1863        let fp3 = CertManager::compute_cert_fingerprint(&other_cert);
1864        assert_ne!(fp1, fp3);
1865    }
1866
1867    // =========================================================================
1868    // ACME Provisioning Tests
1869    // =========================================================================
1870
1871    #[tokio::test]
1872    async fn test_with_directory_staging() {
1873        let dir = tempdir().unwrap();
1874        let manager = CertManager::with_directory(
1875            dir.path().to_string_lossy().to_string(),
1876            Some("test@example.com".to_string()),
1877            super::LETS_ENCRYPT_STAGING.to_string(),
1878        )
1879        .await
1880        .unwrap();
1881
1882        assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_STAGING);
1883    }
1884
1885    #[tokio::test]
1886    async fn test_with_directory_production() {
1887        let dir = tempdir().unwrap();
1888        let manager = CertManager::with_directory(
1889            dir.path().to_string_lossy().to_string(),
1890            Some("test@example.com".to_string()),
1891            super::LETS_ENCRYPT_PRODUCTION.to_string(),
1892        )
1893        .await
1894        .unwrap();
1895
1896        assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_PRODUCTION);
1897    }
1898
1899    #[tokio::test]
1900    async fn test_default_uses_production() {
1901        let dir = tempdir().unwrap();
1902        let manager = CertManager::new(
1903            dir.path().to_string_lossy().to_string(),
1904            Some("test@example.com".to_string()),
1905        )
1906        .await
1907        .unwrap();
1908
1909        assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_PRODUCTION);
1910    }
1911
1912    #[tokio::test]
1913    async fn test_provision_cert_attempted_without_email() {
1914        ensure_crypto_provider();
1915        let dir = tempdir().unwrap();
1916        // No email configured. An ACME email is optional (RFC 8555 §7.3), so
1917        // provisioning must be ATTEMPTED rather than rejected up-front. Point at
1918        // an unreachable directory so provisioning fails fast on the network
1919        // rather than hitting a real ACME server.
1920        let manager = CertManager::with_directory(
1921            dir.path().to_string_lossy().to_string(),
1922            None,
1923            "https://127.0.0.1:1/directory".to_string(),
1924        )
1925        .await
1926        .unwrap();
1927
1928        assert_eq!(manager.acme_email(), None);
1929
1930        // Try to get a cert that doesn't exist (will trigger provisioning). It
1931        // fails on the network here, but must NOT fail with the old
1932        // "ACME email is required" gate.
1933        let err = manager
1934            .get_cert("test.example.com")
1935            .await
1936            .expect_err("provisioning must fail against an unreachable ACME directory")
1937            .to_string();
1938        assert!(
1939            !err.contains("ACME email is required"),
1940            "Provisioning must not be gated on a missing email, got: {err}",
1941        );
1942    }
1943
1944    #[tokio::test]
1945    async fn test_acme_email_optional_contact_shape() {
1946        let dir = tempdir().unwrap();
1947
1948        // With an email configured, the manager exposes it and a `mailto:`
1949        // contact is registered for the account.
1950        let with_email = CertManager::with_directory(
1951            dir.path().join("with").to_string_lossy().to_string(),
1952            Some("ops@example.com".to_string()),
1953            super::LETS_ENCRYPT_STAGING.to_string(),
1954        )
1955        .await
1956        .unwrap();
1957        assert_eq!(with_email.acme_email(), Some("ops@example.com"));
1958        let contact: Option<String> = with_email
1959            .acme_email()
1960            .map(|email| format!("mailto:{email}"));
1961        assert_eq!(contact.as_deref(), Some("mailto:ops@example.com"));
1962
1963        // Without an email, no contact is registered (empty list) and the
1964        // accessor reports None.
1965        let without_email = CertManager::with_directory(
1966            dir.path().join("without").to_string_lossy().to_string(),
1967            None,
1968            super::LETS_ENCRYPT_STAGING.to_string(),
1969        )
1970        .await
1971        .unwrap();
1972        assert_eq!(without_email.acme_email(), None);
1973        let contact: Vec<String> = without_email
1974            .acme_email()
1975            .map(|email| format!("mailto:{email}"))
1976            .into_iter()
1977            .collect();
1978        assert!(
1979            contact.is_empty(),
1980            "no email must yield an empty contact list, got: {contact:?}",
1981        );
1982    }
1983
1984    #[tokio::test]
1985    async fn test_credentials_path() {
1986        let dir = tempdir().unwrap();
1987        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1988            .await
1989            .unwrap();
1990
1991        let credentials_path = manager.credentials_path();
1992        assert_eq!(
1993            credentials_path,
1994            dir.path().join("account_credentials.json")
1995        );
1996    }
1997
1998    #[tokio::test]
1999    async fn test_load_credentials_nonexistent() {
2000        let dir = tempdir().unwrap();
2001        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
2002            .await
2003            .unwrap();
2004
2005        let credentials = manager.load_credentials().await;
2006        assert!(credentials.is_none());
2007    }
2008
2009    // =========================================================================
2010    // Automatic Renewal Tests
2011    // =========================================================================
2012
2013    #[tokio::test]
2014    async fn test_start_renewal_task_spawns() {
2015        use crate::sni_resolver::SniCertResolver;
2016
2017        let dir = tempdir().unwrap();
2018        let manager = Arc::new(
2019            CertManager::new(dir.path().to_string_lossy().to_string(), None)
2020                .await
2021                .unwrap(),
2022        );
2023        let sni_resolver = Arc::new(SniCertResolver::new());
2024
2025        // Start the renewal task
2026        let handle = manager.clone().start_renewal_task(sni_resolver);
2027
2028        // The task should be running (not immediately finished)
2029        assert!(!handle.is_finished());
2030
2031        // Abort the task to clean up
2032        handle.abort();
2033    }
2034
2035    #[tokio::test]
2036    async fn test_run_renewal_check_no_certs() {
2037        use crate::sni_resolver::SniCertResolver;
2038
2039        let dir = tempdir().unwrap();
2040        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
2041            .await
2042            .unwrap();
2043        let sni_resolver = SniCertResolver::new();
2044
2045        // No certificates stored, should return empty
2046        let renewed = manager.run_renewal_check(&sni_resolver).await;
2047        assert!(renewed.is_empty());
2048    }
2049
2050    #[tokio::test]
2051    async fn test_run_renewal_check_with_fresh_cert() {
2052        use crate::sni_resolver::SniCertResolver;
2053
2054        let dir = tempdir().unwrap();
2055        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
2056            .await
2057            .unwrap();
2058        let sni_resolver = SniCertResolver::new();
2059
2060        // Create a certificate that does NOT need renewal (expires in 60 days)
2061        let meta = CertMetadata {
2062            domain: "fresh.example.com".to_string(),
2063            not_before: Utc::now() - TimeDelta::days(30),
2064            not_after: Utc::now() + TimeDelta::days(60),
2065            provisioned_at: Utc::now() - TimeDelta::days(30),
2066            fingerprint: "fresh_fingerprint".to_string(),
2067        };
2068        manager.save_cert_metadata(&meta).await.unwrap();
2069
2070        // Also store the actual cert so it exists
2071        let (cert_pem, key_pem) = generate_test_cert();
2072        manager
2073            .store_cert("fresh.example.com", &cert_pem, &key_pem)
2074            .await
2075            .unwrap();
2076
2077        // Run renewal check - should not attempt renewal since cert is fresh
2078        let renewed = manager.run_renewal_check(&sni_resolver).await;
2079        assert!(
2080            renewed.is_empty(),
2081            "Expected no renewals for fresh cert, got: {renewed:?}",
2082        );
2083    }
2084}