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, 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    /// Provision a certificate via ACME
316    ///
317    /// This method implements the full ACME protocol using HTTP-01 challenges:
318    /// 1. Gets or creates an ACME account
319    /// 2. Creates a new order for the domain
320    /// 3. Processes HTTP-01 challenges
321    /// 4. Waits for validation
322    /// 5. Generates a keypair and CSR
323    /// 6. Finalizes the order
324    /// 7. Retrieves and stores the certificate
325    ///
326    /// # Arguments
327    /// * `domain` - The domain to provision a certificate for
328    ///
329    /// # Returns
330    /// Tuple of (`certificate_pem`, `private_key_pem`)
331    #[allow(clippy::too_many_lines)]
332    async fn provision_cert(
333        &self,
334        domain: &str,
335    ) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
336        tracing::info!(domain = %domain, "Starting ACME certificate provisioning");
337
338        // Require an email for ACME registration
339        if self.acme_email.is_none() {
340            return Err(format!(
341                "ACME email is required for certificate provisioning. \
342                 Certificate for '{}' not found. Please configure an ACME email \
343                 or manually provide certificates at {}",
344                domain,
345                self.storage_path.display()
346            )
347            .into());
348        }
349
350        // Step 1: Get or create ACME account
351        let account = self.get_or_create_acme_account().await?;
352
353        // Step 2: Create new order
354        let identifiers = [Identifier::Dns(domain.to_string())];
355        let new_order = NewOrder {
356            identifiers: &identifiers,
357        };
358        let mut order = account.new_order(&new_order).await.map_err(|e| {
359            tracing::error!(domain = %domain, error = %e, "Failed to create ACME order");
360            format!("Failed to create ACME order for '{domain}': {e}")
361        })?;
362
363        tracing::debug!(domain = %domain, "Created ACME order");
364
365        // Step 3: Get authorizations and process HTTP-01 challenges
366        let authorizations = order.authorizations().await.map_err(|e| {
367            tracing::error!(domain = %domain, error = %e, "Failed to get authorizations");
368            format!("Failed to get authorizations for '{domain}': {e}")
369        })?;
370
371        for auth in authorizations {
372            tracing::debug!(
373                domain = %domain,
374                status = ?auth.status,
375                "Processing authorization"
376            );
377
378            // Skip already valid authorizations
379            if auth.status == AuthorizationStatus::Valid {
380                continue;
381            }
382
383            // Find HTTP-01 challenge
384            let challenge = auth
385                .challenges
386                .iter()
387                .find(|c| c.r#type == ChallengeType::Http01)
388                .ok_or_else(|| {
389                    format!(
390                        "No HTTP-01 challenge available for '{}'. Available types: {:?}",
391                        domain,
392                        auth.challenges
393                            .iter()
394                            .map(|c| &c.r#type)
395                            .collect::<Vec<_>>()
396                    )
397                })?;
398
399            // Store challenge for our HTTP server to serve
400            let key_auth = order.key_authorization(challenge);
401            self.store_challenge(&challenge.token, domain, key_auth.as_str());
402
403            tracing::info!(
404                domain = %domain,
405                token = %challenge.token,
406                "Stored HTTP-01 challenge, notifying ACME server"
407            );
408
409            // Tell ACME server we're ready
410            order
411                .set_challenge_ready(&challenge.url)
412                .await
413                .map_err(|e| {
414                    tracing::error!(
415                        domain = %domain,
416                        error = %e,
417                        "Failed to set challenge ready"
418                    );
419                    format!("Failed to set challenge ready for '{domain}': {e}")
420                })?;
421        }
422
423        // Step 4: Wait for order to be ready (with timeout)
424        let start_time = Instant::now();
425        loop {
426            if start_time.elapsed() > ACME_VALIDATION_TIMEOUT {
427                self.clear_challenges_for_domain(domain);
428                return Err(format!(
429                    "ACME validation timeout for '{}'. Validation did not complete within {} seconds. \
430                     Ensure the domain is accessible at http://{}/.well-known/acme-challenge/",
431                    domain,
432                    ACME_VALIDATION_TIMEOUT.as_secs(),
433                    domain
434                )
435                .into());
436            }
437
438            order.refresh().await.map_err(|e| {
439                tracing::error!(domain = %domain, error = %e, "Failed to refresh order status");
440                format!("Failed to refresh order status for '{domain}': {e}")
441            })?;
442
443            let status = order.state().status;
444            tracing::debug!(domain = %domain, status = ?status, "Order status");
445
446            match status {
447                OrderStatus::Ready => {
448                    tracing::info!(domain = %domain, "Order is ready for finalization");
449                    break;
450                }
451                OrderStatus::Invalid => {
452                    self.clear_challenges_for_domain(domain);
453                    let error_msg = order
454                        .state()
455                        .error
456                        .as_ref()
457                        .map_or_else(|| "Unknown error".to_string(), |e| format!("{e:?}"));
458                    return Err(format!(
459                        "ACME order became invalid for '{domain}': {error_msg}. \
460                         This usually means the HTTP-01 challenge failed. \
461                         Ensure the domain is accessible at http://{domain}/.well-known/acme-challenge/"
462                    )
463                    .into());
464                }
465                OrderStatus::Valid => {
466                    // Already valid, can proceed to get certificate
467                    tracing::info!(domain = %domain, "Order is already valid");
468                    break;
469                }
470                OrderStatus::Pending | OrderStatus::Processing => {
471                    // Still waiting, sleep and retry
472                    tokio::time::sleep(Duration::from_secs(2)).await;
473                }
474            }
475        }
476
477        // Step 5: Generate keypair and CSR with rcgen
478        let key_pair = KeyPair::generate().map_err(|e| {
479            tracing::error!(domain = %domain, error = %e, "Failed to generate key pair");
480            format!("Failed to generate key pair for '{domain}': {e}")
481        })?;
482
483        let params = CertificateParams::new(vec![domain.to_string()]).map_err(|e| {
484            tracing::error!(domain = %domain, error = %e, "Failed to create certificate params");
485            format!("Failed to create certificate params for '{domain}': {e}")
486        })?;
487
488        let csr = params.serialize_request(&key_pair).map_err(|e| {
489            tracing::error!(domain = %domain, error = %e, "Failed to create CSR");
490            format!("Failed to create CSR for '{domain}': {e}")
491        })?;
492
493        tracing::debug!(domain = %domain, "Generated CSR");
494
495        // Step 6: Finalize order with CSR (only if not already valid)
496        if order.state().status != OrderStatus::Valid {
497            order.finalize(csr.der()).await.map_err(|e| {
498                tracing::error!(domain = %domain, error = %e, "Failed to finalize order");
499                format!("Failed to finalize order for '{domain}': {e}")
500            })?;
501
502            tracing::info!(domain = %domain, "Order finalized, waiting for certificate");
503        }
504
505        // Step 7: Get certificate (with timeout)
506        let cert_chain = loop {
507            if start_time.elapsed() > ACME_VALIDATION_TIMEOUT {
508                self.clear_challenges_for_domain(domain);
509                return Err(format!(
510                    "Timeout waiting for certificate for '{}'. \
511                     Certificate was not issued within {} seconds.",
512                    domain,
513                    ACME_VALIDATION_TIMEOUT.as_secs()
514                )
515                .into());
516            }
517
518            match order.certificate().await {
519                Ok(Some(cert)) => break cert,
520                Ok(None) => {
521                    tracing::debug!(domain = %domain, "Certificate not yet available, waiting...");
522                    tokio::time::sleep(Duration::from_secs(1)).await;
523                }
524                Err(e) => {
525                    self.clear_challenges_for_domain(domain);
526                    return Err(format!("Failed to get certificate for '{domain}': {e}").into());
527                }
528            }
529        };
530
531        // Step 8: Store and return certificate
532        let key_pem = key_pair.serialize_pem();
533        self.store_cert(domain, &cert_chain, &key_pem).await?;
534        self.clear_challenges_for_domain(domain);
535
536        tracing::info!(
537            domain = %domain,
538            "Successfully provisioned certificate via ACME"
539        );
540
541        Ok((cert_chain, key_pem))
542    }
543
544    /// Clear the certificate cache
545    pub async fn clear_cache(&self) {
546        let mut cache = self.cache.write().await;
547        cache.clear();
548    }
549
550    /// Get cached certificate count
551    pub async fn cached_count(&self) -> usize {
552        let cache = self.cache.read().await;
553        cache.len()
554    }
555
556    /// Return the list of domain names currently held in the certificate cache.
557    pub async fn list_cached_domains(&self) -> Vec<String> {
558        let cache = self.cache.read().await;
559        cache.keys().cloned().collect()
560    }
561
562    /// Build a `rustls::ServerConfig` from the cert manager's current cache.
563    ///
564    /// Creates a fresh `SniCertResolver`, loads every certificate the manager
565    /// currently has cached into it, and wraps the resolver in a
566    /// `ServerConfig` with no client auth. Intended for callers (e.g. the
567    /// daemon API listener) that want to terminate TLS using the same cert
568    /// pool the proxy serves.
569    ///
570    /// Hot-reload semantics: this is a one-shot snapshot — certs renewed
571    /// after this call are NOT picked up by the returned config. Callers
572    /// that need hot-reload should keep an `Arc<SniCertResolver>` shared
573    /// with the proxy renewal task instead. For the join-token verifying-key
574    /// listener, restarting the daemon on renewal is acceptable since
575    /// certificates renew every ~60-90 days.
576    ///
577    /// # Errors
578    ///
579    /// Returns an error if loading any cached certificate into the SNI
580    /// resolver fails (malformed PEM, key/cert mismatch, etc.).
581    pub async fn build_server_config(
582        &self,
583    ) -> Result<Arc<rustls::ServerConfig>, Box<dyn std::error::Error + Send + Sync>> {
584        let resolver = Arc::new(SniCertResolver::new());
585
586        // Pull cached domains and load each into the resolver. We hold the
587        // read lock for the snapshot and release it before calling load_cert
588        // (which takes its own DashMap write lock on the resolver) to avoid
589        // holding two locks at once.
590        let cached: Vec<(String, String, String)> = {
591            let cache = self.cache.read().await;
592            cache
593                .iter()
594                .map(|(domain, (cert, key))| (domain.clone(), cert.clone(), key.clone()))
595                .collect()
596        };
597
598        for (domain, cert_pem, key_pem) in cached {
599            resolver
600                .load_cert(&domain, &cert_pem, &key_pem)
601                .map_err(|e| {
602                    format!(
603                        "Failed to load cached certificate for '{domain}' into SNI resolver: {e}"
604                    )
605                })?;
606        }
607
608        let server_config = rustls::ServerConfig::builder()
609            .with_no_client_auth()
610            .with_cert_resolver(resolver);
611
612        Ok(Arc::new(server_config))
613    }
614
615    // =========================================================================
616    // Certificate Metadata Management
617    // =========================================================================
618
619    /// Parse certificate expiry dates from a PEM-encoded certificate
620    ///
621    /// # Arguments
622    /// * `cert_pem` - The PEM-encoded certificate string
623    ///
624    /// # Returns
625    /// Tuple of (`not_before`, `not_after`) as `DateTime`<Utc>
626    ///
627    /// # Errors
628    ///
629    /// Returns an error if the PEM cannot be parsed or the X.509
630    /// certificate contains invalid timestamps.
631    pub fn parse_cert_expiry(
632        cert_pem: &str,
633    ) -> Result<(DateTime<Utc>, DateTime<Utc>), Box<dyn std::error::Error + Send + Sync>> {
634        let (_, pem) =
635            parse_x509_pem(cert_pem.as_bytes()).map_err(|e| format!("Failed to parse PEM: {e}"))?;
636
637        let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents)
638            .map_err(|e| format!("Failed to parse X.509 certificate: {e}"))?;
639
640        let validity = cert.validity();
641
642        // Convert ASN1Time to DateTime<Utc>
643        let not_before = DateTime::from_timestamp(validity.not_before.timestamp(), 0)
644            .ok_or("Invalid not_before timestamp")?;
645
646        let not_after = DateTime::from_timestamp(validity.not_after.timestamp(), 0)
647            .ok_or("Invalid not_after timestamp")?;
648
649        Ok((not_before, not_after))
650    }
651
652    /// Compute SHA256 fingerprint of a PEM-encoded certificate
653    fn compute_cert_fingerprint(cert_pem: &str) -> String {
654        let mut hasher = Sha256::new();
655        hasher.update(cert_pem.as_bytes());
656        let result = hasher.finalize();
657        hex::encode(result)
658    }
659
660    /// Save certificate metadata to disk
661    ///
662    /// # Arguments
663    /// * `meta` - The certificate metadata to save
664    async fn save_cert_metadata(
665        &self,
666        meta: &CertMetadata,
667    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
668        let meta_path = self.storage_path.join(format!("{}.meta.json", meta.domain));
669        let json = serde_json::to_string_pretty(meta)?;
670        tokio::fs::write(&meta_path, json).await?;
671        tracing::debug!(domain = %meta.domain, "Saved certificate metadata");
672        Ok(())
673    }
674
675    /// Load certificate metadata from disk
676    ///
677    /// # Arguments
678    /// * `domain` - The domain to load metadata for
679    ///
680    /// # Returns
681    /// The certificate metadata if it exists
682    pub async fn load_cert_metadata(&self, domain: &str) -> Option<CertMetadata> {
683        let meta_path = self.storage_path.join(format!("{domain}.meta.json"));
684        if !meta_path.exists() {
685            return None;
686        }
687
688        match tokio::fs::read_to_string(&meta_path).await {
689            Ok(json) => match serde_json::from_str(&json) {
690                Ok(meta) => Some(meta),
691                Err(e) => {
692                    tracing::warn!(
693                        domain = %domain,
694                        error = %e,
695                        "Failed to parse certificate metadata"
696                    );
697                    None
698                }
699            },
700            Err(e) => {
701                tracing::warn!(
702                    domain = %domain,
703                    error = %e,
704                    "Failed to read certificate metadata file"
705                );
706                None
707            }
708        }
709    }
710
711    /// Get domains with certificates expiring within a threshold
712    ///
713    /// Returns domains with certificates that expire within 30 days (`RENEWAL_THRESHOLD_DAYS`).
714    ///
715    /// # Returns
716    /// Vector of domain names with certificates needing renewal
717    pub async fn get_domains_needing_renewal(&self) -> Vec<String> {
718        let threshold = Utc::now() + TimeDelta::days(RENEWAL_THRESHOLD_DAYS);
719        let mut domains_needing_renewal = Vec::new();
720
721        // Read directory for .meta.json files
722        let mut entries = match tokio::fs::read_dir(&self.storage_path).await {
723            Ok(entries) => entries,
724            Err(e) => {
725                tracing::warn!(error = %e, "Failed to read certificate storage directory");
726                return domains_needing_renewal;
727            }
728        };
729
730        while let Ok(Some(entry)) = entries.next_entry().await {
731            let path = entry.path();
732            if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
733                if filename.ends_with(".meta.json") {
734                    let domain = filename.trim_end_matches(".meta.json");
735                    if let Some(meta) = self.load_cert_metadata(domain).await {
736                        if meta.not_after <= threshold {
737                            tracing::debug!(
738                                domain = %domain,
739                                expires = %meta.not_after,
740                                "Certificate needs renewal"
741                            );
742                            domains_needing_renewal.push(domain.to_string());
743                        }
744                    }
745                }
746            }
747        }
748
749        domains_needing_renewal
750    }
751
752    // =========================================================================
753    // Automatic Certificate Renewal
754    // =========================================================================
755
756    /// Start background certificate renewal task
757    ///
758    /// This spawns a tokio task that checks certificates every 12 hours
759    /// and renews any that expire within 30 days.
760    ///
761    /// # Arguments
762    /// * `sni_resolver` - The SNI resolver to update with renewed certificates
763    ///
764    /// # Returns
765    /// A `JoinHandle` for the spawned renewal task
766    pub fn start_renewal_task(
767        self: Arc<Self>,
768        sni_resolver: Arc<SniCertResolver>,
769    ) -> tokio::task::JoinHandle<()> {
770        tokio::spawn(async move {
771            // Check every 12 hours
772            let mut interval = tokio::time::interval(Duration::from_secs(43200));
773
774            loop {
775                interval.tick().await;
776
777                tracing::info!("Starting certificate renewal check");
778
779                // Get domains needing renewal
780                let domains = self.get_domains_needing_renewal().await;
781
782                if domains.is_empty() {
783                    tracing::debug!("No certificates need renewal");
784                    continue;
785                }
786
787                tracing::info!(count = domains.len(), "Certificates need renewal");
788
789                for domain in domains {
790                    tracing::info!(domain = %domain, "Attempting certificate renewal");
791
792                    match self.provision_cert(&domain).await {
793                        Ok((cert_pem, key_pem)) => {
794                            tracing::info!(domain = %domain, "Certificate renewed successfully");
795
796                            // Update SNI resolver with new certificate
797                            if let Err(e) = sni_resolver.refresh_cert(&domain, &cert_pem, &key_pem)
798                            {
799                                tracing::error!(
800                                    domain = %domain,
801                                    error = %e,
802                                    "Failed to update SNI resolver with renewed cert"
803                                );
804                            }
805                        }
806                        Err(e) => {
807                            tracing::error!(
808                                domain = %domain,
809                                error = %e,
810                                "Certificate renewal failed"
811                            );
812                        }
813                    }
814
815                    // Small delay between renewals to avoid rate limiting
816                    tokio::time::sleep(Duration::from_secs(10)).await;
817                }
818            }
819        })
820    }
821
822    /// Run a single renewal check (for testing or on-demand renewal)
823    ///
824    /// This method checks for certificates needing renewal and attempts to renew them.
825    /// Unlike `start_renewal_task`, this runs once and returns immediately.
826    ///
827    /// # Arguments
828    /// * `sni_resolver` - The SNI resolver to update with renewed certificates
829    ///
830    /// # Returns
831    /// Vector of domain names that were successfully renewed
832    pub async fn run_renewal_check(&self, sni_resolver: &SniCertResolver) -> Vec<String> {
833        let domains = self.get_domains_needing_renewal().await;
834        let mut renewed = Vec::new();
835
836        for domain in domains {
837            match self.provision_cert(&domain).await {
838                Ok((cert_pem, key_pem)) => {
839                    if sni_resolver
840                        .refresh_cert(&domain, &cert_pem, &key_pem)
841                        .is_ok()
842                    {
843                        renewed.push(domain);
844                    }
845                }
846                Err(e) => {
847                    tracing::warn!(domain = %domain, error = %e, "Renewal failed");
848                }
849            }
850        }
851
852        renewed
853    }
854
855    // =========================================================================
856    // ACME Account Management
857    // =========================================================================
858
859    /// Get the path to the account metadata storage file
860    fn account_path(&self) -> PathBuf {
861        self.storage_path.join("account.json")
862    }
863
864    /// Get the path to the account credentials storage file
865    fn credentials_path(&self) -> PathBuf {
866        self.storage_path.join("account_credentials.json")
867    }
868
869    /// Load an existing ACME account from disk
870    ///
871    /// This loads the account metadata. Use `load_credentials()` to load the
872    /// actual credentials needed for ACME operations.
873    ///
874    /// # Returns
875    /// The account if it exists and is valid, None otherwise
876    pub async fn load_account(&self) -> Option<AcmeAccount> {
877        // Check if both files exist (need credentials to be usable)
878        if !self.credentials_path().exists() {
879            return None;
880        }
881        self.load_account_metadata().await
882    }
883
884    /// Load account metadata from disk
885    async fn load_account_metadata(&self) -> Option<AcmeAccount> {
886        let account_path = self.account_path();
887
888        if !account_path.exists() {
889            tracing::debug!(path = %account_path.display(), "No ACME account file found");
890            return None;
891        }
892
893        match tokio::fs::read_to_string(&account_path).await {
894            Ok(content) => match serde_json::from_str::<AcmeAccount>(&content) {
895                Ok(account) => {
896                    tracing::debug!(
897                        account_url = %account.account_url,
898                        created_at = %account.created_at,
899                        "Loaded ACME account from disk"
900                    );
901                    Some(account)
902                }
903                Err(e) => {
904                    tracing::warn!(
905                        error = %e,
906                        path = %account_path.display(),
907                        "Failed to parse ACME account file"
908                    );
909                    None
910                }
911            },
912            Err(e) => {
913                tracing::warn!(
914                    error = %e,
915                    path = %account_path.display(),
916                    "Failed to read ACME account file"
917                );
918                None
919            }
920        }
921    }
922
923    /// Load ACME credentials from disk
924    ///
925    /// Since `AccountCredentials` doesn't implement Clone, we load it fresh
926    /// each time it's needed.
927    async fn load_credentials(&self) -> Option<AccountCredentials> {
928        let credentials_path = self.credentials_path();
929
930        if !credentials_path.exists() {
931            tracing::debug!(path = %credentials_path.display(), "No ACME credentials file found");
932            return None;
933        }
934
935        match tokio::fs::read_to_string(&credentials_path).await {
936            Ok(content) => match serde_json::from_str::<AccountCredentials>(&content) {
937                Ok(credentials) => {
938                    tracing::debug!("Loaded ACME credentials from disk");
939                    Some(credentials)
940                }
941                Err(e) => {
942                    tracing::warn!(
943                        error = %e,
944                        path = %credentials_path.display(),
945                        "Failed to parse ACME credentials file"
946                    );
947                    None
948                }
949            },
950            Err(e) => {
951                tracing::warn!(
952                    error = %e,
953                    path = %credentials_path.display(),
954                    "Failed to read ACME credentials file"
955                );
956                None
957            }
958        }
959    }
960
961    /// Save an ACME account to disk (metadata only)
962    ///
963    /// # Arguments
964    /// * `account` - The account to save
965    ///
966    /// # Errors
967    ///
968    /// Returns an error if serialization or writing to disk fails.
969    pub async fn save_account(
970        &self,
971        account: &AcmeAccount,
972    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
973        self.save_account_metadata(account).await
974    }
975
976    /// Save account metadata to disk
977    async fn save_account_metadata(
978        &self,
979        account: &AcmeAccount,
980    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
981        let account_path = self.account_path();
982
983        let content = serde_json::to_string_pretty(account)?;
984        tokio::fs::write(&account_path, content).await?;
985
986        // Update the cached account
987        *self.account.write().await = Some(account.clone());
988
989        tracing::info!(
990            account_url = %account.account_url,
991            path = %account_path.display(),
992            "Saved ACME account metadata to disk"
993        );
994
995        Ok(())
996    }
997
998    /// Save ACME credentials to disk
999    async fn save_credentials(
1000        &self,
1001        credentials: &AccountCredentials,
1002    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1003        let credentials_path = self.credentials_path();
1004
1005        let content = serde_json::to_string_pretty(credentials)?;
1006        tokio::fs::write(&credentials_path, content).await?;
1007
1008        tracing::info!(
1009            path = %credentials_path.display(),
1010            "Saved ACME credentials to disk"
1011        );
1012
1013        Ok(())
1014    }
1015
1016    /// Get or create an ACME account (returns our metadata struct)
1017    ///
1018    /// This method:
1019    /// 1. Returns the cached account if available
1020    /// 2. Loads the account from disk if it exists
1021    /// 3. Creates a new account via ACME
1022    ///
1023    /// # Errors
1024    ///
1025    /// Returns an error if ACME account creation or restoration fails.
1026    pub async fn get_or_create_account(
1027        &self,
1028    ) -> Result<AcmeAccount, Box<dyn std::error::Error + Send + Sync>> {
1029        // Check cached account first
1030        {
1031            let account = self.account.read().await;
1032            if let Some(ref acc) = *account {
1033                return Ok(acc.clone());
1034            }
1035        }
1036
1037        // Try to load from disk
1038        if let Some(account) = self.load_account().await {
1039            *self.account.write().await = Some(account.clone());
1040            return Ok(account);
1041        }
1042
1043        // Create a new account (this will also cache it)
1044        let _account = self.get_or_create_acme_account().await?;
1045        let account_meta = self
1046            .account
1047            .read()
1048            .await
1049            .clone()
1050            .ok_or("Account was created but metadata not cached - this is a bug")?;
1051
1052        Ok(account_meta)
1053    }
1054
1055    /// Get or create an instant-acme Account object
1056    ///
1057    /// This is the internal method that returns the actual instant-acme Account
1058    /// needed for ACME operations. Since `AccountCredentials` doesn't implement Clone,
1059    /// we load credentials from disk each time.
1060    async fn get_or_create_acme_account(
1061        &self,
1062    ) -> Result<Account, Box<dyn std::error::Error + Send + Sync>> {
1063        // Try to load credentials from disk
1064        if let Some(credentials) = self.load_credentials().await {
1065            tracing::debug!("Restoring ACME account from saved credentials");
1066
1067            let account = Account::from_credentials(credentials)
1068                .await
1069                .map_err(|e| format!("Failed to restore account from saved credentials: {e}"))?;
1070
1071            // Ensure account metadata is cached
1072            if self.account.read().await.is_none() {
1073                if let Some(account_meta) = self.load_account_metadata().await {
1074                    *self.account.write().await = Some(account_meta);
1075                }
1076            }
1077
1078            return Ok(account);
1079        }
1080
1081        // Create a new account
1082        let email = self.acme_email.as_ref().ok_or(
1083            "ACME email is required to create a new account. \
1084             Please configure an email address for ACME registration.",
1085        )?;
1086
1087        tracing::info!(
1088            email = %email,
1089            directory = %self.acme_directory,
1090            "Creating new ACME account"
1091        );
1092
1093        let contact = format!("mailto:{email}");
1094        let contact_refs: &[&str] = &[&contact];
1095        let new_account = NewAccount {
1096            contact: contact_refs,
1097            terms_of_service_agreed: true,
1098            only_return_existing: false,
1099        };
1100
1101        let (account, credentials) = Account::create(&new_account, &self.acme_directory, None)
1102            .await
1103            .map_err(|e| {
1104                tracing::error!(error = %e, "Failed to create ACME account");
1105                format!("Failed to create ACME account: {e}")
1106            })?;
1107
1108        // Create our metadata struct
1109        let account_meta = AcmeAccount {
1110            account_url: account.id().to_string(),
1111            account_key_pem: String::new(), // We store credentials separately
1112            contact: vec![contact],
1113            created_at: Utc::now(),
1114        };
1115
1116        // Save account and credentials to disk
1117        self.save_account_metadata(&account_meta).await?;
1118        self.save_credentials(&credentials).await?;
1119
1120        // Cache the metadata
1121        *self.account.write().await = Some(account_meta.clone());
1122
1123        tracing::info!(
1124            account_url = %account_meta.account_url,
1125            "Successfully created ACME account"
1126        );
1127
1128        Ok(account)
1129    }
1130
1131    /// Get the cached ACME account (if any)
1132    ///
1133    /// # Returns
1134    /// The cached account, or None if not loaded
1135    pub async fn get_account(&self) -> Option<AcmeAccount> {
1136        self.account.read().await.clone()
1137    }
1138
1139    /// Check if an ACME account exists (either cached or on disk)
1140    pub async fn has_account(&self) -> bool {
1141        // Check cache first
1142        {
1143            let account = self.account.read().await;
1144            if account.is_some() {
1145                return true;
1146            }
1147        }
1148
1149        // Check disk
1150        self.account_path().exists()
1151    }
1152
1153    // =========================================================================
1154    // ACME HTTP-01 Challenge Management
1155    // =========================================================================
1156
1157    /// Store an ACME HTTP-01 challenge token
1158    ///
1159    /// This stores the challenge token that will be served at
1160    /// `/.well-known/acme-challenge/{token}` for domain validation.
1161    ///
1162    /// # Arguments
1163    /// * `token` - The challenge token from the ACME server
1164    /// * `domain` - The domain being validated
1165    /// * `key_authorization` - The key authorization response (token.thumbprint)
1166    pub fn store_challenge(&self, token: &str, domain: &str, key_authorization: &str) {
1167        let challenge = ChallengeToken {
1168            token: token.to_string(),
1169            key_authorization: key_authorization.to_string(),
1170            domain: domain.to_string(),
1171            created_at: Instant::now(),
1172        };
1173        self.challenges.insert(token.to_string(), challenge);
1174        tracing::debug!(
1175            token = %token,
1176            domain = %domain,
1177            "Stored ACME challenge token"
1178        );
1179    }
1180
1181    /// Get the key authorization response for a challenge token
1182    ///
1183    /// # Arguments
1184    /// * `token` - The challenge token from the request path
1185    ///
1186    /// # Returns
1187    /// The key authorization string if the token exists and hasn't expired
1188    pub fn get_challenge_response(&self, token: &str) -> Option<String> {
1189        self.challenges
1190            .get(token)
1191            .filter(|challenge| challenge.created_at.elapsed() < CHALLENGE_EXPIRATION)
1192            .map(|challenge| challenge.key_authorization.clone())
1193    }
1194
1195    /// Remove a challenge token after validation completes
1196    ///
1197    /// # Arguments
1198    /// * `token` - The challenge token to remove
1199    pub fn remove_challenge(&self, token: &str) {
1200        if self.challenges.remove(token).is_some() {
1201            tracing::debug!(token = %token, "Removed ACME challenge token");
1202        }
1203    }
1204
1205    /// Clear all challenge tokens for a specific domain
1206    ///
1207    /// Useful when certificate issuance completes or fails for a domain.
1208    ///
1209    /// # Arguments
1210    /// * `domain` - The domain to clear challenges for
1211    pub fn clear_challenges_for_domain(&self, domain: &str) {
1212        let tokens_to_remove: Vec<String> = self
1213            .challenges
1214            .iter()
1215            .filter(|entry| entry.value().domain == domain)
1216            .map(|entry| entry.key().clone())
1217            .collect();
1218
1219        for token in &tokens_to_remove {
1220            self.challenges.remove(token);
1221        }
1222
1223        if !tokens_to_remove.is_empty() {
1224            tracing::debug!(
1225                domain = %domain,
1226                count = tokens_to_remove.len(),
1227                "Cleared ACME challenge tokens for domain"
1228            );
1229        }
1230    }
1231
1232    /// Clean up expired challenge tokens
1233    ///
1234    /// Removes all challenge tokens older than 5 minutes.
1235    /// This should be called periodically to prevent memory leaks.
1236    pub fn cleanup_expired_challenges(&self) {
1237        let expired_tokens: Vec<String> = self
1238            .challenges
1239            .iter()
1240            .filter(|entry| entry.value().created_at.elapsed() >= CHALLENGE_EXPIRATION)
1241            .map(|entry| entry.key().clone())
1242            .collect();
1243
1244        for token in &expired_tokens {
1245            self.challenges.remove(token);
1246        }
1247
1248        if !expired_tokens.is_empty() {
1249            tracing::debug!(
1250                count = expired_tokens.len(),
1251                "Cleaned up expired ACME challenge tokens"
1252            );
1253        }
1254    }
1255
1256    /// Get the number of active challenge tokens
1257    pub fn challenge_count(&self) -> usize {
1258        self.challenges.len()
1259    }
1260}
1261
1262#[cfg(test)]
1263mod tests {
1264    use super::*;
1265    use tempfile::tempdir;
1266
1267    #[tokio::test]
1268    async fn test_cert_manager_creation() {
1269        let dir = tempdir().unwrap();
1270        let manager = CertManager::new(
1271            dir.path().to_string_lossy().to_string(),
1272            Some("test@example.com".to_string()),
1273        )
1274        .await
1275        .unwrap();
1276
1277        assert_eq!(manager.acme_email(), Some("test@example.com"));
1278        assert!(manager.storage_path().exists());
1279    }
1280
1281    #[tokio::test]
1282    async fn test_store_and_get_cert() {
1283        let dir = tempdir().unwrap();
1284        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1285            .await
1286            .unwrap();
1287
1288        // Store a test certificate
1289        let cert = "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----";
1290        let key = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
1291
1292        manager
1293            .store_cert("test.example.com", cert, key)
1294            .await
1295            .unwrap();
1296
1297        // Should be in cache
1298        assert!(manager.has_cert("test.example.com").await);
1299
1300        // Clear cache and verify disk read
1301        manager.clear_cache().await;
1302        assert!(manager.has_cert("test.example.com").await);
1303
1304        // Get the certificate
1305        let (retrieved_cert, retrieved_key) = manager.get_cert("test.example.com").await.unwrap();
1306        assert_eq!(retrieved_cert, cert);
1307        assert_eq!(retrieved_key, key);
1308    }
1309
1310    #[tokio::test]
1311    async fn test_cert_not_found() {
1312        let dir = tempdir().unwrap();
1313        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1314            .await
1315            .unwrap();
1316
1317        let result = manager.get_cert("nonexistent.example.com").await;
1318        assert!(result.is_err());
1319    }
1320
1321    #[tokio::test]
1322    async fn test_store_and_get_challenge() {
1323        let dir = tempdir().unwrap();
1324        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1325            .await
1326            .unwrap();
1327
1328        let token = "abc123";
1329        let domain = "test.example.com";
1330        let key_auth = "abc123.thumbprint";
1331
1332        // Store a challenge
1333        manager.store_challenge(token, domain, key_auth);
1334        assert_eq!(manager.challenge_count(), 1);
1335
1336        // Retrieve it
1337        let response = manager.get_challenge_response(token);
1338        assert_eq!(response, Some(key_auth.to_string()));
1339
1340        // Non-existent token returns None
1341        let missing = manager.get_challenge_response("nonexistent");
1342        assert!(missing.is_none());
1343    }
1344
1345    #[tokio::test]
1346    async fn test_remove_challenge() {
1347        let dir = tempdir().unwrap();
1348        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1349            .await
1350            .unwrap();
1351
1352        manager.store_challenge("token1", "domain.com", "auth1");
1353        assert_eq!(manager.challenge_count(), 1);
1354
1355        manager.remove_challenge("token1");
1356        assert_eq!(manager.challenge_count(), 0);
1357
1358        // Removing non-existent token is a no-op
1359        manager.remove_challenge("nonexistent");
1360        assert_eq!(manager.challenge_count(), 0);
1361    }
1362
1363    #[tokio::test]
1364    async fn test_clear_challenges_for_domain() {
1365        let dir = tempdir().unwrap();
1366        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1367            .await
1368            .unwrap();
1369
1370        // Store challenges for multiple domains
1371        manager.store_challenge("token1", "domain1.com", "auth1");
1372        manager.store_challenge("token2", "domain1.com", "auth2");
1373        manager.store_challenge("token3", "domain2.com", "auth3");
1374        assert_eq!(manager.challenge_count(), 3);
1375
1376        // Clear only domain1.com challenges
1377        manager.clear_challenges_for_domain("domain1.com");
1378        assert_eq!(manager.challenge_count(), 1);
1379
1380        // domain2.com challenge should still exist
1381        let response = manager.get_challenge_response("token3");
1382        assert!(response.is_some());
1383    }
1384
1385    #[tokio::test]
1386    async fn test_cleanup_expired_challenges() {
1387        let dir = tempdir().unwrap();
1388        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1389            .await
1390            .unwrap();
1391
1392        // Store a challenge
1393        manager.store_challenge("token1", "domain.com", "auth1");
1394        assert_eq!(manager.challenge_count(), 1);
1395
1396        // Cleanup should not remove fresh challenges
1397        manager.cleanup_expired_challenges();
1398        assert_eq!(manager.challenge_count(), 1);
1399
1400        // Note: We can't easily test expiration without mocking time,
1401        // but the cleanup logic is tested by verifying it doesn't remove fresh tokens
1402    }
1403
1404    // =========================================================================
1405    // ACME Account Persistence Tests
1406    // =========================================================================
1407
1408    #[tokio::test]
1409    async fn test_save_and_load_account() {
1410        let dir = tempdir().unwrap();
1411        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1412            .await
1413            .unwrap();
1414
1415        // Initially no account
1416        assert!(!manager.has_account().await);
1417        assert!(manager.get_account().await.is_none());
1418
1419        // Create and save an account
1420        let account = AcmeAccount {
1421            account_url: "https://acme.example.com/acct/12345".to_string(),
1422            account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1423                .to_string(),
1424            contact: vec!["mailto:test@example.com".to_string()],
1425            created_at: Utc::now(),
1426        };
1427
1428        manager.save_account(&account).await.unwrap();
1429
1430        // Should be cached
1431        assert!(manager.has_account().await);
1432        let cached = manager.get_account().await.unwrap();
1433        assert_eq!(cached.account_url, account.account_url);
1434        assert_eq!(cached.contact, account.contact);
1435
1436        // Verify file exists on disk
1437        let account_path = dir.path().join("account.json");
1438        assert!(account_path.exists());
1439    }
1440
1441    #[tokio::test]
1442    async fn test_load_account_from_disk() {
1443        let dir = tempdir().unwrap();
1444
1445        // Create an account file manually
1446        let account = AcmeAccount {
1447            account_url: "https://acme.example.com/acct/67890".to_string(),
1448            account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1449                .to_string(),
1450            contact: vec!["mailto:admin@example.com".to_string()],
1451            created_at: Utc::now(),
1452        };
1453
1454        let account_path = dir.path().join("account.json");
1455        let content = serde_json::to_string_pretty(&account).unwrap();
1456        std::fs::write(&account_path, content).unwrap();
1457
1458        // Also need to create a mock credentials file for load_account to work
1459        // The credentials file must be valid JSON that deserializes to AccountCredentials
1460        // We use a minimal valid credentials structure here for testing
1461        let credentials_path = dir.path().join("account_credentials.json");
1462        // This is a minimal valid AccountCredentials JSON (the exact format is opaque but serializable)
1463        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"}}"#;
1464        std::fs::write(&credentials_path, mock_credentials).unwrap();
1465
1466        // Create manager - should load the account automatically
1467        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1468            .await
1469            .unwrap();
1470
1471        assert!(manager.has_account().await);
1472        let loaded = manager.get_account().await.unwrap();
1473        assert_eq!(loaded.account_url, account.account_url);
1474        assert_eq!(loaded.contact, account.contact);
1475    }
1476
1477    #[tokio::test]
1478    async fn test_get_or_create_account_returns_cached() {
1479        let dir = tempdir().unwrap();
1480        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1481            .await
1482            .unwrap();
1483
1484        // Save an account first
1485        let account = AcmeAccount {
1486            account_url: "https://acme.example.com/acct/11111".to_string(),
1487            account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1488                .to_string(),
1489            contact: vec!["mailto:cached@example.com".to_string()],
1490            created_at: Utc::now(),
1491        };
1492        manager.save_account(&account).await.unwrap();
1493
1494        // get_or_create should return the cached account
1495        let result = manager.get_or_create_account().await.unwrap();
1496        assert_eq!(result.account_url, account.account_url);
1497    }
1498
1499    #[tokio::test]
1500    async fn test_get_or_create_account_without_existing() {
1501        let dir = tempdir().unwrap();
1502        // No email configured, so account creation will fail
1503        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1504            .await
1505            .unwrap();
1506
1507        // Should fail because no email is configured and no existing account
1508        let result = manager.get_or_create_account().await;
1509        assert!(result.is_err());
1510        let err = result.unwrap_err().to_string();
1511        assert!(
1512            err.contains("ACME email is required"),
1513            "Expected error about ACME email, got: {err}",
1514        );
1515    }
1516
1517    #[tokio::test]
1518    async fn test_load_invalid_account_file() {
1519        let dir = tempdir().unwrap();
1520
1521        // Create an invalid JSON file
1522        let account_path = dir.path().join("account.json");
1523        std::fs::write(&account_path, "{ invalid json }").unwrap();
1524
1525        // Manager should still be created, but no valid account should be loaded
1526        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1527            .await
1528            .unwrap();
1529
1530        // The file exists on disk, so has_account returns true (checking file existence)
1531        // But get_account returns None because no valid account was loaded into cache
1532        assert!(manager.get_account().await.is_none());
1533
1534        // get_or_create_account should fail because:
1535        // 1. No cached account
1536        // 2. load_account returns None (invalid JSON)
1537        // 3. Account creation not implemented
1538        let result = manager.get_or_create_account().await;
1539        assert!(result.is_err());
1540    }
1541
1542    #[tokio::test]
1543    async fn test_account_persistence_across_instances() {
1544        let dir = tempdir().unwrap();
1545
1546        // Create first manager and save account with credentials
1547        {
1548            let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1549                .await
1550                .unwrap();
1551
1552            let account = AcmeAccount {
1553                account_url: "https://acme.example.com/acct/persist".to_string(),
1554                account_key_pem:
1555                    "-----BEGIN EC PRIVATE KEY-----\npersist\n-----END EC PRIVATE KEY-----"
1556                        .to_string(),
1557                contact: vec!["mailto:persist@example.com".to_string()],
1558                created_at: Utc::now(),
1559            };
1560            manager.save_account(&account).await.unwrap();
1561
1562            // Also need to create a mock credentials file for load_account to work
1563            let credentials_path = dir.path().join("account_credentials.json");
1564            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"}}"#;
1565            std::fs::write(&credentials_path, mock_credentials).unwrap();
1566        }
1567
1568        // Create second manager - should load the persisted account
1569        let manager2 = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1570            .await
1571            .unwrap();
1572
1573        assert!(manager2.has_account().await);
1574        let loaded = manager2.get_account().await.unwrap();
1575        assert_eq!(loaded.account_url, "https://acme.example.com/acct/persist");
1576    }
1577
1578    // =========================================================================
1579    // Certificate Metadata Tests
1580    // =========================================================================
1581
1582    /// Generate a valid test certificate using rcgen
1583    fn generate_test_cert() -> (String, String) {
1584        use rcgen::{CertificateParams, KeyPair};
1585
1586        let key_pair = KeyPair::generate().unwrap();
1587        let params = CertificateParams::new(vec!["test.example.com".to_string()]).unwrap();
1588        let cert = params.self_signed(&key_pair).unwrap();
1589        (cert.pem(), key_pair.serialize_pem())
1590    }
1591
1592    #[tokio::test]
1593    async fn test_parse_cert_expiry() {
1594        // Generate a valid test certificate
1595        let (cert_pem, _key_pem) = generate_test_cert();
1596
1597        let result = CertManager::parse_cert_expiry(&cert_pem);
1598        assert!(result.is_ok(), "Failed to parse cert: {:?}", result.err());
1599
1600        let (not_before, not_after) = result.unwrap();
1601        // The certificate should have valid timestamps
1602        assert!(not_before < not_after);
1603    }
1604
1605    #[tokio::test]
1606    async fn test_parse_cert_expiry_invalid_pem() {
1607        let result = CertManager::parse_cert_expiry("not a valid pem");
1608        assert!(result.is_err());
1609    }
1610
1611    #[tokio::test]
1612    async fn test_cert_metadata_save_load() {
1613        let dir = tempdir().unwrap();
1614        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1615            .await
1616            .unwrap();
1617
1618        let meta = CertMetadata {
1619            domain: "test.example.com".to_string(),
1620            not_before: Utc::now(),
1621            not_after: Utc::now() + TimeDelta::days(90),
1622            provisioned_at: Utc::now(),
1623            fingerprint: "abc123def456".to_string(),
1624        };
1625
1626        // Save metadata
1627        manager.save_cert_metadata(&meta).await.unwrap();
1628
1629        // Verify file exists
1630        let meta_path = dir.path().join("test.example.com.meta.json");
1631        assert!(meta_path.exists());
1632
1633        // Load metadata
1634        let loaded = manager.load_cert_metadata("test.example.com").await;
1635        assert!(loaded.is_some());
1636
1637        let loaded = loaded.unwrap();
1638        assert_eq!(loaded.domain, "test.example.com");
1639        assert_eq!(loaded.fingerprint, "abc123def456");
1640    }
1641
1642    #[tokio::test]
1643    async fn test_load_nonexistent_metadata() {
1644        let dir = tempdir().unwrap();
1645        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1646            .await
1647            .unwrap();
1648
1649        let loaded = manager.load_cert_metadata("nonexistent.example.com").await;
1650        assert!(loaded.is_none());
1651    }
1652
1653    #[tokio::test]
1654    async fn test_get_domains_needing_renewal() {
1655        let dir = tempdir().unwrap();
1656        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1657            .await
1658            .unwrap();
1659
1660        // Create a certificate that expires in 10 days (needs renewal)
1661        let expiring_meta = CertMetadata {
1662            domain: "expiring.example.com".to_string(),
1663            not_before: Utc::now() - TimeDelta::days(80),
1664            not_after: Utc::now() + TimeDelta::days(10),
1665            provisioned_at: Utc::now() - TimeDelta::days(80),
1666            fingerprint: "expiring_fingerprint".to_string(),
1667        };
1668        manager.save_cert_metadata(&expiring_meta).await.unwrap();
1669
1670        // Create a certificate that expires in 60 days (does not need renewal)
1671        let valid_meta = CertMetadata {
1672            domain: "valid.example.com".to_string(),
1673            not_before: Utc::now() - TimeDelta::days(30),
1674            not_after: Utc::now() + TimeDelta::days(60),
1675            provisioned_at: Utc::now() - TimeDelta::days(30),
1676            fingerprint: "valid_fingerprint".to_string(),
1677        };
1678        manager.save_cert_metadata(&valid_meta).await.unwrap();
1679
1680        // Check renewal list
1681        let needs_renewal = manager.get_domains_needing_renewal().await;
1682        assert_eq!(needs_renewal.len(), 1);
1683        assert!(needs_renewal.contains(&"expiring.example.com".to_string()));
1684    }
1685
1686    #[tokio::test]
1687    async fn test_store_cert_with_valid_pem_saves_metadata() {
1688        let dir = tempdir().unwrap();
1689        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1690            .await
1691            .unwrap();
1692
1693        // Generate a valid test certificate
1694        let (cert_pem, key_pem) = generate_test_cert();
1695
1696        // Store a valid certificate (this should also save metadata)
1697        manager
1698            .store_cert("metadata.example.com", &cert_pem, &key_pem)
1699            .await
1700            .unwrap();
1701
1702        // Verify metadata was saved
1703        let meta = manager.load_cert_metadata("metadata.example.com").await;
1704        assert!(meta.is_some(), "Metadata was not saved");
1705
1706        let meta = meta.unwrap();
1707        assert_eq!(meta.domain, "metadata.example.com");
1708        assert!(!meta.fingerprint.is_empty());
1709        assert!(meta.not_before < meta.not_after);
1710    }
1711
1712    #[tokio::test]
1713    async fn test_store_cert_with_invalid_pem_still_stores_cert() {
1714        let dir = tempdir().unwrap();
1715        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1716            .await
1717            .unwrap();
1718
1719        // Store an invalid certificate (should still store cert, just skip metadata)
1720        let invalid_cert = "-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----";
1721        let key = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
1722
1723        manager
1724            .store_cert("invalid.example.com", invalid_cert, key)
1725            .await
1726            .unwrap();
1727
1728        // Certificate should still be stored
1729        assert!(manager.has_cert("invalid.example.com").await);
1730
1731        // Metadata should not exist (parsing failed)
1732        let meta = manager.load_cert_metadata("invalid.example.com").await;
1733        assert!(meta.is_none());
1734    }
1735
1736    #[tokio::test]
1737    async fn test_cert_fingerprint_is_consistent() {
1738        // Generate a test certificate
1739        let (cert_pem, _) = generate_test_cert();
1740
1741        // Compute fingerprint twice, should be identical
1742        let fp1 = CertManager::compute_cert_fingerprint(&cert_pem);
1743        let fp2 = CertManager::compute_cert_fingerprint(&cert_pem);
1744        assert_eq!(fp1, fp2);
1745
1746        // Different cert should have different fingerprint
1747        let (other_cert, _) = generate_test_cert();
1748        let fp3 = CertManager::compute_cert_fingerprint(&other_cert);
1749        assert_ne!(fp1, fp3);
1750    }
1751
1752    // =========================================================================
1753    // ACME Provisioning Tests
1754    // =========================================================================
1755
1756    #[tokio::test]
1757    async fn test_with_directory_staging() {
1758        let dir = tempdir().unwrap();
1759        let manager = CertManager::with_directory(
1760            dir.path().to_string_lossy().to_string(),
1761            Some("test@example.com".to_string()),
1762            super::LETS_ENCRYPT_STAGING.to_string(),
1763        )
1764        .await
1765        .unwrap();
1766
1767        assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_STAGING);
1768    }
1769
1770    #[tokio::test]
1771    async fn test_with_directory_production() {
1772        let dir = tempdir().unwrap();
1773        let manager = CertManager::with_directory(
1774            dir.path().to_string_lossy().to_string(),
1775            Some("test@example.com".to_string()),
1776            super::LETS_ENCRYPT_PRODUCTION.to_string(),
1777        )
1778        .await
1779        .unwrap();
1780
1781        assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_PRODUCTION);
1782    }
1783
1784    #[tokio::test]
1785    async fn test_default_uses_production() {
1786        let dir = tempdir().unwrap();
1787        let manager = CertManager::new(
1788            dir.path().to_string_lossy().to_string(),
1789            Some("test@example.com".to_string()),
1790        )
1791        .await
1792        .unwrap();
1793
1794        assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_PRODUCTION);
1795    }
1796
1797    #[tokio::test]
1798    async fn test_provision_cert_requires_email() {
1799        let dir = tempdir().unwrap();
1800        // No email configured
1801        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1802            .await
1803            .unwrap();
1804
1805        // Try to get a cert that doesn't exist (will trigger provisioning)
1806        let result = manager.get_cert("test.example.com").await;
1807        assert!(result.is_err());
1808
1809        let err = result.unwrap_err().to_string();
1810        assert!(
1811            err.contains("ACME email is required"),
1812            "Expected error about ACME email, got: {err}",
1813        );
1814    }
1815
1816    #[tokio::test]
1817    async fn test_credentials_path() {
1818        let dir = tempdir().unwrap();
1819        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1820            .await
1821            .unwrap();
1822
1823        let credentials_path = manager.credentials_path();
1824        assert_eq!(
1825            credentials_path,
1826            dir.path().join("account_credentials.json")
1827        );
1828    }
1829
1830    #[tokio::test]
1831    async fn test_load_credentials_nonexistent() {
1832        let dir = tempdir().unwrap();
1833        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1834            .await
1835            .unwrap();
1836
1837        let credentials = manager.load_credentials().await;
1838        assert!(credentials.is_none());
1839    }
1840
1841    // =========================================================================
1842    // Automatic Renewal Tests
1843    // =========================================================================
1844
1845    #[tokio::test]
1846    async fn test_start_renewal_task_spawns() {
1847        use crate::sni_resolver::SniCertResolver;
1848
1849        let dir = tempdir().unwrap();
1850        let manager = Arc::new(
1851            CertManager::new(dir.path().to_string_lossy().to_string(), None)
1852                .await
1853                .unwrap(),
1854        );
1855        let sni_resolver = Arc::new(SniCertResolver::new());
1856
1857        // Start the renewal task
1858        let handle = manager.clone().start_renewal_task(sni_resolver);
1859
1860        // The task should be running (not immediately finished)
1861        assert!(!handle.is_finished());
1862
1863        // Abort the task to clean up
1864        handle.abort();
1865    }
1866
1867    #[tokio::test]
1868    async fn test_run_renewal_check_no_certs() {
1869        use crate::sni_resolver::SniCertResolver;
1870
1871        let dir = tempdir().unwrap();
1872        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1873            .await
1874            .unwrap();
1875        let sni_resolver = SniCertResolver::new();
1876
1877        // No certificates stored, should return empty
1878        let renewed = manager.run_renewal_check(&sni_resolver).await;
1879        assert!(renewed.is_empty());
1880    }
1881
1882    #[tokio::test]
1883    async fn test_run_renewal_check_with_fresh_cert() {
1884        use crate::sni_resolver::SniCertResolver;
1885
1886        let dir = tempdir().unwrap();
1887        let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1888            .await
1889            .unwrap();
1890        let sni_resolver = SniCertResolver::new();
1891
1892        // Create a certificate that does NOT need renewal (expires in 60 days)
1893        let meta = CertMetadata {
1894            domain: "fresh.example.com".to_string(),
1895            not_before: Utc::now() - TimeDelta::days(30),
1896            not_after: Utc::now() + TimeDelta::days(60),
1897            provisioned_at: Utc::now() - TimeDelta::days(30),
1898            fingerprint: "fresh_fingerprint".to_string(),
1899        };
1900        manager.save_cert_metadata(&meta).await.unwrap();
1901
1902        // Also store the actual cert so it exists
1903        let (cert_pem, key_pem) = generate_test_cert();
1904        manager
1905            .store_cert("fresh.example.com", &cert_pem, &key_pem)
1906            .await
1907            .unwrap();
1908
1909        // Run renewal check - should not attempt renewal since cert is fresh
1910        let renewed = manager.run_renewal_check(&sni_resolver).await;
1911        assert!(
1912            renewed.is_empty(),
1913            "Expected no renewals for fresh cert, got: {renewed:?}",
1914        );
1915    }
1916}