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