1use 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
27const CHALLENGE_EXPIRATION: Duration = Duration::from_secs(5 * 60);
29
30const RENEWAL_THRESHOLD_DAYS: i64 = 30;
32
33const ACME_VALIDATION_TIMEOUT: Duration = Duration::from_secs(120);
35
36pub const LETS_ENCRYPT_PRODUCTION: &str = "https://acme-v02.api.letsencrypt.org/directory";
38
39pub const LETS_ENCRYPT_STAGING: &str = "https://acme-staging-v02.api.letsencrypt.org/directory";
41
42#[derive(Debug, Clone)]
44pub struct ChallengeToken {
45 pub token: String,
47 pub key_authorization: String,
49 pub domain: String,
51 pub created_at: Instant,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct AcmeAccount {
61 pub account_url: String,
63 pub account_key_pem: String,
65 pub contact: Vec<String>,
67 pub created_at: DateTime<Utc>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CertMetadata {
74 pub domain: String,
76 pub not_before: DateTime<Utc>,
78 pub not_after: DateTime<Utc>,
80 pub provisioned_at: DateTime<Utc>,
82 pub fingerprint: String,
84}
85
86pub struct CertManager {
93 storage_path: PathBuf,
95 acme_email: Option<String>,
97 acme_directory: String,
99 cache: RwLock<HashMap<String, (String, String)>>,
101 challenges: DashMap<String, ChallengeToken>,
103 account: RwLock<Option<AcmeAccount>>,
105}
106
107impl CertManager {
108 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 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 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 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 pub fn acme_directory(&self) -> &str {
175 &self.acme_directory
176 }
177
178 pub async fn get_cert(
196 &self,
197 domain: &str,
198 ) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
199 {
201 let cache = self.cache.read().await;
202 if let Some(cached) = cache.get(domain) {
203 return Ok(cached.clone());
204 }
205 }
206
207 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 {
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 self.provision_cert(domain).await
226 }
227
228 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 tokio::fs::write(&cert_path, cert).await?;
252 tokio::fs::write(&key_path, key).await?;
253
254 {
256 let mut cache = self.cache.write().await;
257 cache.insert(domain.to_string(), (cert.to_string(), key.to_string()));
258 }
259
260 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 pub async fn has_cert(&self, domain: &str) -> bool {
290 {
292 let cache = self.cache.read().await;
293 if cache.contains_key(domain) {
294 return true;
295 }
296 }
297
298 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 pub fn acme_email(&self) -> Option<&str> {
307 self.acme_email.as_deref()
308 }
309
310 pub fn storage_path(&self) -> &PathBuf {
312 &self.storage_path
313 }
314
315 #[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 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 let account = self.get_or_create_acme_account().await?;
352
353 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 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 if auth.status == AuthorizationStatus::Valid {
380 continue;
381 }
382
383 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 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 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 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 tracing::info!(domain = %domain, "Order is already valid");
468 break;
469 }
470 OrderStatus::Pending | OrderStatus::Processing => {
471 tokio::time::sleep(Duration::from_secs(2)).await;
473 }
474 }
475 }
476
477 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 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 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 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 pub async fn clear_cache(&self) {
546 let mut cache = self.cache.write().await;
547 cache.clear();
548 }
549
550 pub async fn cached_count(&self) -> usize {
552 let cache = self.cache.read().await;
553 cache.len()
554 }
555
556 pub async fn list_cached_domains(&self) -> Vec<String> {
558 let cache = self.cache.read().await;
559 cache.keys().cloned().collect()
560 }
561
562 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 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 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 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 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 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 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 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 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 pub fn start_renewal_task(
767 self: Arc<Self>,
768 sni_resolver: Arc<SniCertResolver>,
769 ) -> tokio::task::JoinHandle<()> {
770 tokio::spawn(async move {
771 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 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 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 tokio::time::sleep(Duration::from_secs(10)).await;
817 }
818 }
819 })
820 }
821
822 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 fn account_path(&self) -> PathBuf {
861 self.storage_path.join("account.json")
862 }
863
864 fn credentials_path(&self) -> PathBuf {
866 self.storage_path.join("account_credentials.json")
867 }
868
869 pub async fn load_account(&self) -> Option<AcmeAccount> {
877 if !self.credentials_path().exists() {
879 return None;
880 }
881 self.load_account_metadata().await
882 }
883
884 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 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 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 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 *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 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 pub async fn get_or_create_account(
1027 &self,
1028 ) -> Result<AcmeAccount, Box<dyn std::error::Error + Send + Sync>> {
1029 {
1031 let account = self.account.read().await;
1032 if let Some(ref acc) = *account {
1033 return Ok(acc.clone());
1034 }
1035 }
1036
1037 if let Some(account) = self.load_account().await {
1039 *self.account.write().await = Some(account.clone());
1040 return Ok(account);
1041 }
1042
1043 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 async fn get_or_create_acme_account(
1061 &self,
1062 ) -> Result<Account, Box<dyn std::error::Error + Send + Sync>> {
1063 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 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 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 let account_meta = AcmeAccount {
1110 account_url: account.id().to_string(),
1111 account_key_pem: String::new(), contact: vec![contact],
1113 created_at: Utc::now(),
1114 };
1115
1116 self.save_account_metadata(&account_meta).await?;
1118 self.save_credentials(&credentials).await?;
1119
1120 *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 pub async fn get_account(&self) -> Option<AcmeAccount> {
1136 self.account.read().await.clone()
1137 }
1138
1139 pub async fn has_account(&self) -> bool {
1141 {
1143 let account = self.account.read().await;
1144 if account.is_some() {
1145 return true;
1146 }
1147 }
1148
1149 self.account_path().exists()
1151 }
1152
1153 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 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 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 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 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 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 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 assert!(manager.has_cert("test.example.com").await);
1299
1300 manager.clear_cache().await;
1302 assert!(manager.has_cert("test.example.com").await);
1303
1304 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 manager.store_challenge(token, domain, key_auth);
1334 assert_eq!(manager.challenge_count(), 1);
1335
1336 let response = manager.get_challenge_response(token);
1338 assert_eq!(response, Some(key_auth.to_string()));
1339
1340 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 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 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 manager.clear_challenges_for_domain("domain1.com");
1378 assert_eq!(manager.challenge_count(), 1);
1379
1380 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 manager.store_challenge("token1", "domain.com", "auth1");
1394 assert_eq!(manager.challenge_count(), 1);
1395
1396 manager.cleanup_expired_challenges();
1398 assert_eq!(manager.challenge_count(), 1);
1399
1400 }
1403
1404 #[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 assert!(!manager.has_account().await);
1417 assert!(manager.get_account().await.is_none());
1418
1419 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 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 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 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 let credentials_path = dir.path().join("account_credentials.json");
1462 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 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 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 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 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1504 .await
1505 .unwrap();
1506
1507 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 let account_path = dir.path().join("account.json");
1523 std::fs::write(&account_path, "{ invalid json }").unwrap();
1524
1525 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1527 .await
1528 .unwrap();
1529
1530 assert!(manager.get_account().await.is_none());
1533
1534 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 {
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 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 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 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 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 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 manager.save_cert_metadata(&meta).await.unwrap();
1628
1629 let meta_path = dir.path().join("test.example.com.meta.json");
1631 assert!(meta_path.exists());
1632
1633 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 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 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 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 let (cert_pem, key_pem) = generate_test_cert();
1695
1696 manager
1698 .store_cert("metadata.example.com", &cert_pem, &key_pem)
1699 .await
1700 .unwrap();
1701
1702 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 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 assert!(manager.has_cert("invalid.example.com").await);
1730
1731 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 let (cert_pem, _) = generate_test_cert();
1740
1741 let fp1 = CertManager::compute_cert_fingerprint(&cert_pem);
1743 let fp2 = CertManager::compute_cert_fingerprint(&cert_pem);
1744 assert_eq!(fp1, fp2);
1745
1746 let (other_cert, _) = generate_test_cert();
1748 let fp3 = CertManager::compute_cert_fingerprint(&other_cert);
1749 assert_ne!(fp1, fp3);
1750 }
1751
1752 #[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 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1802 .await
1803 .unwrap();
1804
1805 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 #[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 let handle = manager.clone().start_renewal_task(sni_resolver);
1859
1860 assert!(!handle.is_finished());
1862
1863 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 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 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 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 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}