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 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 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 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 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 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 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 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 pub fn start_renewal_task(
714 self: Arc<Self>,
715 sni_resolver: Arc<SniCertResolver>,
716 ) -> tokio::task::JoinHandle<()> {
717 tokio::spawn(async move {
718 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 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 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 tokio::time::sleep(Duration::from_secs(10)).await;
764 }
765 }
766 })
767 }
768
769 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 fn account_path(&self) -> PathBuf {
808 self.storage_path.join("account.json")
809 }
810
811 fn credentials_path(&self) -> PathBuf {
813 self.storage_path.join("account_credentials.json")
814 }
815
816 pub async fn load_account(&self) -> Option<AcmeAccount> {
824 if !self.credentials_path().exists() {
826 return None;
827 }
828 self.load_account_metadata().await
829 }
830
831 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 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 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 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 *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 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 pub async fn get_or_create_account(
974 &self,
975 ) -> Result<AcmeAccount, Box<dyn std::error::Error + Send + Sync>> {
976 {
978 let account = self.account.read().await;
979 if let Some(ref acc) = *account {
980 return Ok(acc.clone());
981 }
982 }
983
984 if let Some(account) = self.load_account().await {
986 *self.account.write().await = Some(account.clone());
987 return Ok(account);
988 }
989
990 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 async fn get_or_create_acme_account(
1008 &self,
1009 ) -> Result<Account, Box<dyn std::error::Error + Send + Sync>> {
1010 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 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 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 let account_meta = AcmeAccount {
1057 account_url: account.id().to_string(),
1058 account_key_pem: String::new(), contact: vec![contact],
1060 created_at: Utc::now(),
1061 };
1062
1063 self.save_account_metadata(&account_meta).await?;
1065 self.save_credentials(&credentials).await?;
1066
1067 *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 pub async fn get_account(&self) -> Option<AcmeAccount> {
1083 self.account.read().await.clone()
1084 }
1085
1086 pub async fn has_account(&self) -> bool {
1088 {
1090 let account = self.account.read().await;
1091 if account.is_some() {
1092 return true;
1093 }
1094 }
1095
1096 self.account_path().exists()
1098 }
1099
1100 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 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 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 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 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 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 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 assert!(manager.has_cert("test.example.com").await);
1246
1247 manager.clear_cache().await;
1249 assert!(manager.has_cert("test.example.com").await);
1250
1251 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 manager.store_challenge(token, domain, key_auth);
1281 assert_eq!(manager.challenge_count(), 1);
1282
1283 let response = manager.get_challenge_response(token);
1285 assert_eq!(response, Some(key_auth.to_string()));
1286
1287 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 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 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 manager.clear_challenges_for_domain("domain1.com");
1325 assert_eq!(manager.challenge_count(), 1);
1326
1327 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 manager.store_challenge("token1", "domain.com", "auth1");
1341 assert_eq!(manager.challenge_count(), 1);
1342
1343 manager.cleanup_expired_challenges();
1345 assert_eq!(manager.challenge_count(), 1);
1346
1347 }
1350
1351 #[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 assert!(!manager.has_account().await);
1364 assert!(manager.get_account().await.is_none());
1365
1366 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 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 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 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 let credentials_path = dir.path().join("account_credentials.json");
1409 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 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 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 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 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1451 .await
1452 .unwrap();
1453
1454 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 let account_path = dir.path().join("account.json");
1470 std::fs::write(&account_path, "{ invalid json }").unwrap();
1471
1472 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1474 .await
1475 .unwrap();
1476
1477 assert!(manager.get_account().await.is_none());
1480
1481 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 {
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 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 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 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 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 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 manager.save_cert_metadata(&meta).await.unwrap();
1575
1576 let meta_path = dir.path().join("test.example.com.meta.json");
1578 assert!(meta_path.exists());
1579
1580 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 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 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 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 let (cert_pem, key_pem) = generate_test_cert();
1642
1643 manager
1645 .store_cert("metadata.example.com", &cert_pem, &key_pem)
1646 .await
1647 .unwrap();
1648
1649 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 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 assert!(manager.has_cert("invalid.example.com").await);
1677
1678 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 let (cert_pem, _) = generate_test_cert();
1687
1688 let fp1 = CertManager::compute_cert_fingerprint(&cert_pem);
1690 let fp2 = CertManager::compute_cert_fingerprint(&cert_pem);
1691 assert_eq!(fp1, fp2);
1692
1693 let (other_cert, _) = generate_test_cert();
1695 let fp3 = CertManager::compute_cert_fingerprint(&other_cert);
1696 assert_ne!(fp1, fp3);
1697 }
1698
1699 #[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 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1749 .await
1750 .unwrap();
1751
1752 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 #[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 let handle = manager.clone().start_renewal_task(sni_resolver);
1806
1807 assert!(!handle.is_finished());
1809
1810 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 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 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 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 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}