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, DistinguishedName, DnType, KeyPair};
20use serde::{Deserialize, Serialize};
21use sha2::{Digest, Sha256};
22use tokio::sync::RwLock;
23use x509_parser::pem::parse_x509_pem;
24
25use crate::sni_resolver::SniCertResolver;
26
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 fn build_csr(
325 domain: &str,
326 key_pair: &KeyPair,
327 ) -> Result<rcgen::CertificateSigningRequest, rcgen::Error> {
328 let mut params = CertificateParams::new(vec![domain.to_string()])?;
331 let mut dn = DistinguishedName::new();
332 dn.push(DnType::CommonName, domain);
333 params.distinguished_name = dn;
334 params.serialize_request(key_pair)
335 }
336
337 #[allow(clippy::too_many_lines)]
354 async fn provision_cert(
355 &self,
356 domain: &str,
357 ) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
358 tracing::info!(domain = %domain, "Starting ACME certificate provisioning");
359
360 let account = self.get_or_create_acme_account().await?;
365
366 let identifiers = [Identifier::Dns(domain.to_string())];
368 let new_order = NewOrder {
369 identifiers: &identifiers,
370 };
371 let mut order = account.new_order(&new_order).await.map_err(|e| {
372 tracing::error!(domain = %domain, error = %e, "Failed to create ACME order");
373 format!("Failed to create ACME order for '{domain}': {e}")
374 })?;
375
376 tracing::debug!(domain = %domain, "Created ACME order");
377
378 let authorizations = order.authorizations().await.map_err(|e| {
380 tracing::error!(domain = %domain, error = %e, "Failed to get authorizations");
381 format!("Failed to get authorizations for '{domain}': {e}")
382 })?;
383
384 for auth in authorizations {
385 tracing::debug!(
386 domain = %domain,
387 status = ?auth.status,
388 "Processing authorization"
389 );
390
391 if auth.status == AuthorizationStatus::Valid {
393 continue;
394 }
395
396 let challenge = auth
398 .challenges
399 .iter()
400 .find(|c| c.r#type == ChallengeType::Http01)
401 .ok_or_else(|| {
402 format!(
403 "No HTTP-01 challenge available for '{}'. Available types: {:?}",
404 domain,
405 auth.challenges
406 .iter()
407 .map(|c| &c.r#type)
408 .collect::<Vec<_>>()
409 )
410 })?;
411
412 let key_auth = order.key_authorization(challenge);
414 self.store_challenge(&challenge.token, domain, key_auth.as_str());
415
416 tracing::info!(
417 domain = %domain,
418 token = %challenge.token,
419 "Stored HTTP-01 challenge, notifying ACME server"
420 );
421
422 order
424 .set_challenge_ready(&challenge.url)
425 .await
426 .map_err(|e| {
427 tracing::error!(
428 domain = %domain,
429 error = %e,
430 "Failed to set challenge ready"
431 );
432 format!("Failed to set challenge ready for '{domain}': {e}")
433 })?;
434 }
435
436 let start_time = Instant::now();
438 loop {
439 if start_time.elapsed() > ACME_VALIDATION_TIMEOUT {
440 self.clear_challenges_for_domain(domain);
441 return Err(format!(
442 "ACME validation timeout for '{}'. Validation did not complete within {} seconds. \
443 Ensure the domain is accessible at http://{}/.well-known/acme-challenge/",
444 domain,
445 ACME_VALIDATION_TIMEOUT.as_secs(),
446 domain
447 )
448 .into());
449 }
450
451 order.refresh().await.map_err(|e| {
452 tracing::error!(domain = %domain, error = %e, "Failed to refresh order status");
453 format!("Failed to refresh order status for '{domain}': {e}")
454 })?;
455
456 let status = order.state().status;
457 tracing::debug!(domain = %domain, status = ?status, "Order status");
458
459 match status {
460 OrderStatus::Ready => {
461 tracing::info!(domain = %domain, "Order is ready for finalization");
462 break;
463 }
464 OrderStatus::Invalid => {
465 self.clear_challenges_for_domain(domain);
466 let error_msg = order
467 .state()
468 .error
469 .as_ref()
470 .map_or_else(|| "Unknown error".to_string(), |e| format!("{e:?}"));
471 return Err(format!(
472 "ACME order became invalid for '{domain}': {error_msg}. \
473 This usually means the HTTP-01 challenge failed. \
474 Ensure the domain is accessible at http://{domain}/.well-known/acme-challenge/"
475 )
476 .into());
477 }
478 OrderStatus::Valid => {
479 tracing::info!(domain = %domain, "Order is already valid");
481 break;
482 }
483 OrderStatus::Pending | OrderStatus::Processing => {
484 tokio::time::sleep(Duration::from_secs(2)).await;
486 }
487 }
488 }
489
490 let key_pair = KeyPair::generate().map_err(|e| {
492 tracing::error!(domain = %domain, error = %e, "Failed to generate key pair");
493 format!("Failed to generate key pair for '{domain}': {e}")
494 })?;
495
496 let csr = Self::build_csr(domain, &key_pair).map_err(|e| {
497 tracing::error!(domain = %domain, error = %e, "Failed to create CSR");
498 format!("Failed to create CSR for '{domain}': {e}")
499 })?;
500
501 tracing::debug!(domain = %domain, "Generated CSR");
502
503 if order.state().status != OrderStatus::Valid {
505 order.finalize(csr.der()).await.map_err(|e| {
506 tracing::error!(domain = %domain, error = %e, "Failed to finalize order");
507 format!("Failed to finalize order for '{domain}': {e}")
508 })?;
509
510 tracing::info!(domain = %domain, "Order finalized, waiting for certificate");
511 }
512
513 let cert_chain = loop {
515 if start_time.elapsed() > ACME_VALIDATION_TIMEOUT {
516 self.clear_challenges_for_domain(domain);
517 return Err(format!(
518 "Timeout waiting for certificate for '{}'. \
519 Certificate was not issued within {} seconds.",
520 domain,
521 ACME_VALIDATION_TIMEOUT.as_secs()
522 )
523 .into());
524 }
525
526 match order.certificate().await {
527 Ok(Some(cert)) => break cert,
528 Ok(None) => {
529 tracing::debug!(domain = %domain, "Certificate not yet available, waiting...");
530 tokio::time::sleep(Duration::from_secs(1)).await;
531 }
532 Err(e) => {
533 self.clear_challenges_for_domain(domain);
534 return Err(format!("Failed to get certificate for '{domain}': {e}").into());
535 }
536 }
537 };
538
539 let key_pem = key_pair.serialize_pem();
541 self.store_cert(domain, &cert_chain, &key_pem).await?;
542 self.clear_challenges_for_domain(domain);
543
544 tracing::info!(
545 domain = %domain,
546 "Successfully provisioned certificate via ACME"
547 );
548
549 Ok((cert_chain, key_pem))
550 }
551
552 pub async fn clear_cache(&self) {
554 let mut cache = self.cache.write().await;
555 cache.clear();
556 }
557
558 pub async fn cached_count(&self) -> usize {
560 let cache = self.cache.read().await;
561 cache.len()
562 }
563
564 pub async fn list_cached_domains(&self) -> Vec<String> {
566 let cache = self.cache.read().await;
567 cache.keys().cloned().collect()
568 }
569
570 pub async fn build_server_config(
590 &self,
591 ) -> Result<Arc<rustls::ServerConfig>, Box<dyn std::error::Error + Send + Sync>> {
592 let resolver = Arc::new(SniCertResolver::new());
593
594 let cached: Vec<(String, String, String)> = {
599 let cache = self.cache.read().await;
600 cache
601 .iter()
602 .map(|(domain, (cert, key))| (domain.clone(), cert.clone(), key.clone()))
603 .collect()
604 };
605
606 for (domain, cert_pem, key_pem) in cached {
607 resolver
608 .load_cert(&domain, &cert_pem, &key_pem)
609 .map_err(|e| {
610 format!(
611 "Failed to load cached certificate for '{domain}' into SNI resolver: {e}"
612 )
613 })?;
614 }
615
616 let server_config = rustls::ServerConfig::builder()
617 .with_no_client_auth()
618 .with_cert_resolver(resolver);
619
620 Ok(Arc::new(server_config))
621 }
622
623 pub fn parse_cert_expiry(
640 cert_pem: &str,
641 ) -> Result<(DateTime<Utc>, DateTime<Utc>), Box<dyn std::error::Error + Send + Sync>> {
642 let (_, pem) =
643 parse_x509_pem(cert_pem.as_bytes()).map_err(|e| format!("Failed to parse PEM: {e}"))?;
644
645 let (_, cert) = x509_parser::parse_x509_certificate(&pem.contents)
646 .map_err(|e| format!("Failed to parse X.509 certificate: {e}"))?;
647
648 let validity = cert.validity();
649
650 let not_before = DateTime::from_timestamp(validity.not_before.timestamp(), 0)
652 .ok_or("Invalid not_before timestamp")?;
653
654 let not_after = DateTime::from_timestamp(validity.not_after.timestamp(), 0)
655 .ok_or("Invalid not_after timestamp")?;
656
657 Ok((not_before, not_after))
658 }
659
660 fn compute_cert_fingerprint(cert_pem: &str) -> String {
662 let mut hasher = Sha256::new();
663 hasher.update(cert_pem.as_bytes());
664 let result = hasher.finalize();
665 hex::encode(result)
666 }
667
668 async fn save_cert_metadata(
673 &self,
674 meta: &CertMetadata,
675 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
676 let meta_path = self.storage_path.join(format!("{}.meta.json", meta.domain));
677 let json = serde_json::to_string_pretty(meta)?;
678 tokio::fs::write(&meta_path, json).await?;
679 tracing::debug!(domain = %meta.domain, "Saved certificate metadata");
680 Ok(())
681 }
682
683 pub async fn load_cert_metadata(&self, domain: &str) -> Option<CertMetadata> {
691 let meta_path = self.storage_path.join(format!("{domain}.meta.json"));
692 if !meta_path.exists() {
693 return None;
694 }
695
696 match tokio::fs::read_to_string(&meta_path).await {
697 Ok(json) => match serde_json::from_str(&json) {
698 Ok(meta) => Some(meta),
699 Err(e) => {
700 tracing::warn!(
701 domain = %domain,
702 error = %e,
703 "Failed to parse certificate metadata"
704 );
705 None
706 }
707 },
708 Err(e) => {
709 tracing::warn!(
710 domain = %domain,
711 error = %e,
712 "Failed to read certificate metadata file"
713 );
714 None
715 }
716 }
717 }
718
719 pub async fn get_domains_needing_renewal(&self) -> Vec<String> {
726 let threshold = Utc::now() + TimeDelta::days(RENEWAL_THRESHOLD_DAYS);
727 let mut domains_needing_renewal = Vec::new();
728
729 let mut entries = match tokio::fs::read_dir(&self.storage_path).await {
731 Ok(entries) => entries,
732 Err(e) => {
733 tracing::warn!(error = %e, "Failed to read certificate storage directory");
734 return domains_needing_renewal;
735 }
736 };
737
738 while let Ok(Some(entry)) = entries.next_entry().await {
739 let path = entry.path();
740 if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
741 if filename.ends_with(".meta.json") {
742 let domain = filename.trim_end_matches(".meta.json");
743 if let Some(meta) = self.load_cert_metadata(domain).await {
744 if meta.not_after <= threshold {
745 tracing::debug!(
746 domain = %domain,
747 expires = %meta.not_after,
748 "Certificate needs renewal"
749 );
750 domains_needing_renewal.push(domain.to_string());
751 }
752 }
753 }
754 }
755 }
756
757 domains_needing_renewal
758 }
759
760 pub fn start_renewal_task(
775 self: Arc<Self>,
776 sni_resolver: Arc<SniCertResolver>,
777 ) -> tokio::task::JoinHandle<()> {
778 tokio::spawn(async move {
779 let mut interval = tokio::time::interval(Duration::from_secs(43200));
781
782 loop {
783 interval.tick().await;
784
785 tracing::info!("Starting certificate renewal check");
786
787 let domains = self.get_domains_needing_renewal().await;
789
790 if domains.is_empty() {
791 tracing::debug!("No certificates need renewal");
792 continue;
793 }
794
795 tracing::info!(count = domains.len(), "Certificates need renewal");
796
797 for domain in domains {
798 tracing::info!(domain = %domain, "Attempting certificate renewal");
799
800 match self.provision_cert(&domain).await {
801 Ok((cert_pem, key_pem)) => {
802 tracing::info!(domain = %domain, "Certificate renewed successfully");
803
804 if let Err(e) = sni_resolver.refresh_cert(&domain, &cert_pem, &key_pem)
806 {
807 tracing::error!(
808 domain = %domain,
809 error = %e,
810 "Failed to update SNI resolver with renewed cert"
811 );
812 }
813 }
814 Err(e) => {
815 tracing::error!(
816 domain = %domain,
817 error = %e,
818 "Certificate renewal failed"
819 );
820 }
821 }
822
823 tokio::time::sleep(Duration::from_secs(10)).await;
825 }
826 }
827 })
828 }
829
830 pub async fn run_renewal_check(&self, sni_resolver: &SniCertResolver) -> Vec<String> {
841 let domains = self.get_domains_needing_renewal().await;
842 let mut renewed = Vec::new();
843
844 for domain in domains {
845 match self.provision_cert(&domain).await {
846 Ok((cert_pem, key_pem)) => {
847 if sni_resolver
848 .refresh_cert(&domain, &cert_pem, &key_pem)
849 .is_ok()
850 {
851 renewed.push(domain);
852 }
853 }
854 Err(e) => {
855 tracing::warn!(domain = %domain, error = %e, "Renewal failed");
856 }
857 }
858 }
859
860 renewed
861 }
862
863 fn account_path(&self) -> PathBuf {
869 self.storage_path.join("account.json")
870 }
871
872 fn credentials_path(&self) -> PathBuf {
874 self.storage_path.join("account_credentials.json")
875 }
876
877 pub async fn load_account(&self) -> Option<AcmeAccount> {
885 if !self.credentials_path().exists() {
887 return None;
888 }
889 self.load_account_metadata().await
890 }
891
892 async fn load_account_metadata(&self) -> Option<AcmeAccount> {
894 let account_path = self.account_path();
895
896 if !account_path.exists() {
897 tracing::debug!(path = %account_path.display(), "No ACME account file found");
898 return None;
899 }
900
901 match tokio::fs::read_to_string(&account_path).await {
902 Ok(content) => match serde_json::from_str::<AcmeAccount>(&content) {
903 Ok(account) => {
904 tracing::debug!(
905 account_url = %account.account_url,
906 created_at = %account.created_at,
907 "Loaded ACME account from disk"
908 );
909 Some(account)
910 }
911 Err(e) => {
912 tracing::warn!(
913 error = %e,
914 path = %account_path.display(),
915 "Failed to parse ACME account file"
916 );
917 None
918 }
919 },
920 Err(e) => {
921 tracing::warn!(
922 error = %e,
923 path = %account_path.display(),
924 "Failed to read ACME account file"
925 );
926 None
927 }
928 }
929 }
930
931 async fn load_credentials(&self) -> Option<AccountCredentials> {
936 let credentials_path = self.credentials_path();
937
938 if !credentials_path.exists() {
939 tracing::debug!(path = %credentials_path.display(), "No ACME credentials file found");
940 return None;
941 }
942
943 match tokio::fs::read_to_string(&credentials_path).await {
944 Ok(content) => match serde_json::from_str::<AccountCredentials>(&content) {
945 Ok(credentials) => {
946 tracing::debug!("Loaded ACME credentials from disk");
947 Some(credentials)
948 }
949 Err(e) => {
950 tracing::warn!(
951 error = %e,
952 path = %credentials_path.display(),
953 "Failed to parse ACME credentials file"
954 );
955 None
956 }
957 },
958 Err(e) => {
959 tracing::warn!(
960 error = %e,
961 path = %credentials_path.display(),
962 "Failed to read ACME credentials file"
963 );
964 None
965 }
966 }
967 }
968
969 pub async fn save_account(
978 &self,
979 account: &AcmeAccount,
980 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
981 self.save_account_metadata(account).await
982 }
983
984 async fn save_account_metadata(
986 &self,
987 account: &AcmeAccount,
988 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
989 let account_path = self.account_path();
990
991 let content = serde_json::to_string_pretty(account)?;
992 tokio::fs::write(&account_path, content).await?;
993
994 *self.account.write().await = Some(account.clone());
996
997 tracing::info!(
998 account_url = %account.account_url,
999 path = %account_path.display(),
1000 "Saved ACME account metadata to disk"
1001 );
1002
1003 Ok(())
1004 }
1005
1006 async fn save_credentials(
1008 &self,
1009 credentials: &AccountCredentials,
1010 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1011 let credentials_path = self.credentials_path();
1012
1013 let content = serde_json::to_string_pretty(credentials)?;
1014 tokio::fs::write(&credentials_path, content).await?;
1015
1016 tracing::info!(
1017 path = %credentials_path.display(),
1018 "Saved ACME credentials to disk"
1019 );
1020
1021 Ok(())
1022 }
1023
1024 pub async fn get_or_create_account(
1035 &self,
1036 ) -> Result<AcmeAccount, Box<dyn std::error::Error + Send + Sync>> {
1037 {
1039 let account = self.account.read().await;
1040 if let Some(ref acc) = *account {
1041 return Ok(acc.clone());
1042 }
1043 }
1044
1045 if let Some(account) = self.load_account().await {
1047 *self.account.write().await = Some(account.clone());
1048 return Ok(account);
1049 }
1050
1051 let _account = self.get_or_create_acme_account().await?;
1053 let account_meta = self
1054 .account
1055 .read()
1056 .await
1057 .clone()
1058 .ok_or("Account was created but metadata not cached - this is a bug")?;
1059
1060 Ok(account_meta)
1061 }
1062
1063 async fn get_or_create_acme_account(
1069 &self,
1070 ) -> Result<Account, Box<dyn std::error::Error + Send + Sync>> {
1071 if let Some(credentials) = self.load_credentials().await {
1073 tracing::debug!("Restoring ACME account from saved credentials");
1074
1075 let account = Account::from_credentials(credentials)
1076 .await
1077 .map_err(|e| format!("Failed to restore account from saved credentials: {e}"))?;
1078
1079 if self.account.read().await.is_none() {
1081 if let Some(account_meta) = self.load_account_metadata().await {
1082 *self.account.write().await = Some(account_meta);
1083 }
1084 }
1085
1086 return Ok(account);
1087 }
1088
1089 let contact = self
1095 .acme_email
1096 .as_ref()
1097 .map(|email| format!("mailto:{email}"));
1098
1099 if let Some(c) = contact.as_deref() {
1100 tracing::info!(
1101 contact = %c,
1102 directory = %self.acme_directory,
1103 "Creating new ACME account"
1104 );
1105 } else {
1106 tracing::info!(
1107 directory = %self.acme_directory,
1108 "Creating new ACME account with no contact email configured"
1109 );
1110 }
1111
1112 let contact_vec: Vec<&str> = contact.iter().map(String::as_str).collect();
1114 let new_account = NewAccount {
1115 contact: &contact_vec,
1116 terms_of_service_agreed: true,
1117 only_return_existing: false,
1118 };
1119
1120 let (account, credentials) = Account::create(&new_account, &self.acme_directory, None)
1121 .await
1122 .map_err(|e| {
1123 tracing::error!(error = %e, "Failed to create ACME account");
1124 format!("Failed to create ACME account: {e}")
1125 })?;
1126
1127 let account_meta = AcmeAccount {
1129 account_url: account.id().to_string(),
1130 account_key_pem: String::new(), contact: contact.into_iter().collect(),
1132 created_at: Utc::now(),
1133 };
1134
1135 self.save_account_metadata(&account_meta).await?;
1137 self.save_credentials(&credentials).await?;
1138
1139 *self.account.write().await = Some(account_meta.clone());
1141
1142 tracing::info!(
1143 account_url = %account_meta.account_url,
1144 "Successfully created ACME account"
1145 );
1146
1147 Ok(account)
1148 }
1149
1150 pub async fn get_account(&self) -> Option<AcmeAccount> {
1155 self.account.read().await.clone()
1156 }
1157
1158 pub async fn has_account(&self) -> bool {
1160 {
1162 let account = self.account.read().await;
1163 if account.is_some() {
1164 return true;
1165 }
1166 }
1167
1168 self.account_path().exists()
1170 }
1171
1172 pub fn store_challenge(&self, token: &str, domain: &str, key_authorization: &str) {
1186 let challenge = ChallengeToken {
1187 token: token.to_string(),
1188 key_authorization: key_authorization.to_string(),
1189 domain: domain.to_string(),
1190 created_at: Instant::now(),
1191 };
1192 self.challenges.insert(token.to_string(), challenge);
1193 tracing::debug!(
1194 token = %token,
1195 domain = %domain,
1196 "Stored ACME challenge token"
1197 );
1198 }
1199
1200 pub fn get_challenge_response(&self, token: &str) -> Option<String> {
1208 self.challenges
1209 .get(token)
1210 .filter(|challenge| challenge.created_at.elapsed() < CHALLENGE_EXPIRATION)
1211 .map(|challenge| challenge.key_authorization.clone())
1212 }
1213
1214 pub fn remove_challenge(&self, token: &str) {
1219 if self.challenges.remove(token).is_some() {
1220 tracing::debug!(token = %token, "Removed ACME challenge token");
1221 }
1222 }
1223
1224 pub fn clear_challenges_for_domain(&self, domain: &str) {
1231 let tokens_to_remove: Vec<String> = self
1232 .challenges
1233 .iter()
1234 .filter(|entry| entry.value().domain == domain)
1235 .map(|entry| entry.key().clone())
1236 .collect();
1237
1238 for token in &tokens_to_remove {
1239 self.challenges.remove(token);
1240 }
1241
1242 if !tokens_to_remove.is_empty() {
1243 tracing::debug!(
1244 domain = %domain,
1245 count = tokens_to_remove.len(),
1246 "Cleared ACME challenge tokens for domain"
1247 );
1248 }
1249 }
1250
1251 pub fn cleanup_expired_challenges(&self) {
1256 let expired_tokens: Vec<String> = self
1257 .challenges
1258 .iter()
1259 .filter(|entry| entry.value().created_at.elapsed() >= CHALLENGE_EXPIRATION)
1260 .map(|entry| entry.key().clone())
1261 .collect();
1262
1263 for token in &expired_tokens {
1264 self.challenges.remove(token);
1265 }
1266
1267 if !expired_tokens.is_empty() {
1268 tracing::debug!(
1269 count = expired_tokens.len(),
1270 "Cleaned up expired ACME challenge tokens"
1271 );
1272 }
1273 }
1274
1275 pub fn challenge_count(&self) -> usize {
1277 self.challenges.len()
1278 }
1279}
1280
1281#[cfg(test)]
1282mod tests {
1283 use super::*;
1284 use tempfile::tempdir;
1285
1286 fn ensure_crypto_provider() {
1293 use std::sync::Once;
1294 static INSTALLED: Once = Once::new();
1295 INSTALLED.call_once(|| {
1296 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
1297 });
1298 }
1299
1300 #[tokio::test]
1301 async fn test_cert_manager_creation() {
1302 let dir = tempdir().unwrap();
1303 let manager = CertManager::new(
1304 dir.path().to_string_lossy().to_string(),
1305 Some("test@example.com".to_string()),
1306 )
1307 .await
1308 .unwrap();
1309
1310 assert_eq!(manager.acme_email(), Some("test@example.com"));
1311 assert!(manager.storage_path().exists());
1312 }
1313
1314 #[tokio::test]
1315 async fn test_store_and_get_cert() {
1316 let dir = tempdir().unwrap();
1317 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1318 .await
1319 .unwrap();
1320
1321 let cert = "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----";
1323 let key = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
1324
1325 manager
1326 .store_cert("test.example.com", cert, key)
1327 .await
1328 .unwrap();
1329
1330 assert!(manager.has_cert("test.example.com").await);
1332
1333 manager.clear_cache().await;
1335 assert!(manager.has_cert("test.example.com").await);
1336
1337 let (retrieved_cert, retrieved_key) = manager.get_cert("test.example.com").await.unwrap();
1339 assert_eq!(retrieved_cert, cert);
1340 assert_eq!(retrieved_key, key);
1341 }
1342
1343 #[tokio::test]
1344 async fn test_cert_not_found() {
1345 ensure_crypto_provider();
1346 let dir = tempdir().unwrap();
1347 let manager = CertManager::with_directory(
1351 dir.path().to_string_lossy().to_string(),
1352 None,
1353 "https://127.0.0.1:1/directory".to_string(),
1354 )
1355 .await
1356 .unwrap();
1357
1358 let result = manager.get_cert("nonexistent.example.com").await;
1359 assert!(result.is_err());
1360 }
1361
1362 #[tokio::test]
1363 async fn test_store_and_get_challenge() {
1364 let dir = tempdir().unwrap();
1365 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1366 .await
1367 .unwrap();
1368
1369 let token = "abc123";
1370 let domain = "test.example.com";
1371 let key_auth = "abc123.thumbprint";
1372
1373 manager.store_challenge(token, domain, key_auth);
1375 assert_eq!(manager.challenge_count(), 1);
1376
1377 let response = manager.get_challenge_response(token);
1379 assert_eq!(response, Some(key_auth.to_string()));
1380
1381 let missing = manager.get_challenge_response("nonexistent");
1383 assert!(missing.is_none());
1384 }
1385
1386 #[tokio::test]
1387 async fn test_remove_challenge() {
1388 let dir = tempdir().unwrap();
1389 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1390 .await
1391 .unwrap();
1392
1393 manager.store_challenge("token1", "domain.com", "auth1");
1394 assert_eq!(manager.challenge_count(), 1);
1395
1396 manager.remove_challenge("token1");
1397 assert_eq!(manager.challenge_count(), 0);
1398
1399 manager.remove_challenge("nonexistent");
1401 assert_eq!(manager.challenge_count(), 0);
1402 }
1403
1404 #[tokio::test]
1405 async fn test_clear_challenges_for_domain() {
1406 let dir = tempdir().unwrap();
1407 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1408 .await
1409 .unwrap();
1410
1411 manager.store_challenge("token1", "domain1.com", "auth1");
1413 manager.store_challenge("token2", "domain1.com", "auth2");
1414 manager.store_challenge("token3", "domain2.com", "auth3");
1415 assert_eq!(manager.challenge_count(), 3);
1416
1417 manager.clear_challenges_for_domain("domain1.com");
1419 assert_eq!(manager.challenge_count(), 1);
1420
1421 let response = manager.get_challenge_response("token3");
1423 assert!(response.is_some());
1424 }
1425
1426 #[tokio::test]
1427 async fn test_cleanup_expired_challenges() {
1428 let dir = tempdir().unwrap();
1429 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1430 .await
1431 .unwrap();
1432
1433 manager.store_challenge("token1", "domain.com", "auth1");
1435 assert_eq!(manager.challenge_count(), 1);
1436
1437 manager.cleanup_expired_challenges();
1439 assert_eq!(manager.challenge_count(), 1);
1440
1441 }
1444
1445 #[tokio::test]
1450 async fn test_save_and_load_account() {
1451 let dir = tempdir().unwrap();
1452 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1453 .await
1454 .unwrap();
1455
1456 assert!(!manager.has_account().await);
1458 assert!(manager.get_account().await.is_none());
1459
1460 let account = AcmeAccount {
1462 account_url: "https://acme.example.com/acct/12345".to_string(),
1463 account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1464 .to_string(),
1465 contact: vec!["mailto:test@example.com".to_string()],
1466 created_at: Utc::now(),
1467 };
1468
1469 manager.save_account(&account).await.unwrap();
1470
1471 assert!(manager.has_account().await);
1473 let cached = manager.get_account().await.unwrap();
1474 assert_eq!(cached.account_url, account.account_url);
1475 assert_eq!(cached.contact, account.contact);
1476
1477 let account_path = dir.path().join("account.json");
1479 assert!(account_path.exists());
1480 }
1481
1482 #[tokio::test]
1483 async fn test_load_account_from_disk() {
1484 let dir = tempdir().unwrap();
1485
1486 let account = AcmeAccount {
1488 account_url: "https://acme.example.com/acct/67890".to_string(),
1489 account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1490 .to_string(),
1491 contact: vec!["mailto:admin@example.com".to_string()],
1492 created_at: Utc::now(),
1493 };
1494
1495 let account_path = dir.path().join("account.json");
1496 let content = serde_json::to_string_pretty(&account).unwrap();
1497 std::fs::write(&account_path, content).unwrap();
1498
1499 let credentials_path = dir.path().join("account_credentials.json");
1503 let mock_credentials = r#"{"id":"https://acme.example.com/acct/67890","key_pkcs8":"base64data","urls":{"newNonce":"https://acme.example.com/acme/new-nonce","newAccount":"https://acme.example.com/acme/new-account","newOrder":"https://acme.example.com/acme/new-order"}}"#;
1505 std::fs::write(&credentials_path, mock_credentials).unwrap();
1506
1507 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1509 .await
1510 .unwrap();
1511
1512 assert!(manager.has_account().await);
1513 let loaded = manager.get_account().await.unwrap();
1514 assert_eq!(loaded.account_url, account.account_url);
1515 assert_eq!(loaded.contact, account.contact);
1516 }
1517
1518 #[tokio::test]
1519 async fn test_get_or_create_account_returns_cached() {
1520 let dir = tempdir().unwrap();
1521 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1522 .await
1523 .unwrap();
1524
1525 let account = AcmeAccount {
1527 account_url: "https://acme.example.com/acct/11111".to_string(),
1528 account_key_pem: "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----"
1529 .to_string(),
1530 contact: vec!["mailto:cached@example.com".to_string()],
1531 created_at: Utc::now(),
1532 };
1533 manager.save_account(&account).await.unwrap();
1534
1535 let result = manager.get_or_create_account().await.unwrap();
1537 assert_eq!(result.account_url, account.account_url);
1538 }
1539
1540 #[tokio::test]
1541 async fn test_get_or_create_account_without_existing() {
1542 ensure_crypto_provider();
1543 let dir = tempdir().unwrap();
1544 let manager = CertManager::with_directory(
1550 dir.path().to_string_lossy().to_string(),
1551 None,
1552 "https://127.0.0.1:1/directory".to_string(),
1553 )
1554 .await
1555 .unwrap();
1556
1557 let err = manager
1558 .get_or_create_account()
1559 .await
1560 .expect_err("account creation must fail against an unreachable ACME directory")
1561 .to_string();
1562 assert!(
1563 !err.contains("ACME email is required"),
1564 "Account creation must not be gated on a missing email, got: {err}",
1565 );
1566 }
1567
1568 #[tokio::test]
1569 async fn test_load_invalid_account_file() {
1570 let dir = tempdir().unwrap();
1571
1572 let account_path = dir.path().join("account.json");
1574 std::fs::write(&account_path, "{ invalid json }").unwrap();
1575
1576 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1578 .await
1579 .unwrap();
1580
1581 assert!(manager.get_account().await.is_none());
1585
1586 assert!(manager.load_account().await.is_none());
1592 }
1593
1594 #[tokio::test]
1595 async fn test_account_persistence_across_instances() {
1596 let dir = tempdir().unwrap();
1597
1598 {
1600 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1601 .await
1602 .unwrap();
1603
1604 let account = AcmeAccount {
1605 account_url: "https://acme.example.com/acct/persist".to_string(),
1606 account_key_pem:
1607 "-----BEGIN EC PRIVATE KEY-----\npersist\n-----END EC PRIVATE KEY-----"
1608 .to_string(),
1609 contact: vec!["mailto:persist@example.com".to_string()],
1610 created_at: Utc::now(),
1611 };
1612 manager.save_account(&account).await.unwrap();
1613
1614 let credentials_path = dir.path().join("account_credentials.json");
1616 let mock_credentials = r#"{"id":"https://acme.example.com/acct/persist","key_pkcs8":"base64data","urls":{"newNonce":"https://acme.example.com/acme/new-nonce","newAccount":"https://acme.example.com/acme/new-account","newOrder":"https://acme.example.com/acme/new-order"}}"#;
1617 std::fs::write(&credentials_path, mock_credentials).unwrap();
1618 }
1619
1620 let manager2 = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1622 .await
1623 .unwrap();
1624
1625 assert!(manager2.has_account().await);
1626 let loaded = manager2.get_account().await.unwrap();
1627 assert_eq!(loaded.account_url, "https://acme.example.com/acct/persist");
1628 }
1629
1630 fn generate_test_cert() -> (String, String) {
1636 use rcgen::{CertificateParams, KeyPair};
1637
1638 let key_pair = KeyPair::generate().unwrap();
1639 let params = CertificateParams::new(vec!["test.example.com".to_string()]).unwrap();
1640 let cert = params.self_signed(&key_pair).unwrap();
1641 (cert.pem(), key_pair.serialize_pem())
1642 }
1643
1644 #[test]
1649 fn test_build_csr_subject_uses_domain_not_placeholder() {
1650 use rcgen::KeyPair;
1651 use x509_parser::prelude::{FromDer, X509CertificationRequest};
1652
1653 let key_pair = KeyPair::generate().unwrap();
1654 let domain = "example.com";
1655
1656 let csr = CertManager::build_csr(domain, &key_pair).expect("build_csr should succeed");
1657
1658 let (_, parsed) =
1660 X509CertificationRequest::from_der(csr.der()).expect("CSR DER should parse");
1661 let subject = parsed.certification_request_info.subject.to_string();
1662
1663 assert!(
1665 !subject.contains("rcgen self signed cert"),
1666 "CSR subject still contains rcgen placeholder CN: {subject}"
1667 );
1668 assert!(
1670 subject.contains(&format!("CN={domain}")),
1671 "CSR subject does not carry the domain as CommonName: {subject}"
1672 );
1673 }
1674
1675 #[test]
1678 fn test_build_csr_params_carry_domain_san_and_cn() {
1679 use rcgen::{CertificateParams, DnType, DnValue, SanType};
1680
1681 let domain = "service.example.org";
1682
1683 let mut params = CertificateParams::new(vec![domain.to_string()]).unwrap();
1684 let mut dn = rcgen::DistinguishedName::new();
1685 dn.push(DnType::CommonName, domain);
1686 params.distinguished_name = dn;
1687
1688 assert_eq!(
1690 params.distinguished_name.get(&DnType::CommonName),
1691 Some(&DnValue::Utf8String(domain.to_string())),
1692 "distinguished_name CommonName should equal the domain"
1693 );
1694
1695 let has_san = params
1697 .subject_alt_names
1698 .iter()
1699 .any(|san| matches!(san, SanType::DnsName(name) if name.as_str() == domain));
1700 assert!(
1701 has_san,
1702 "subject_alt_names should contain the domain as a DnsName SAN: {:?}",
1703 params.subject_alt_names
1704 );
1705 }
1706
1707 #[tokio::test]
1708 async fn test_parse_cert_expiry() {
1709 let (cert_pem, _key_pem) = generate_test_cert();
1711
1712 let result = CertManager::parse_cert_expiry(&cert_pem);
1713 assert!(result.is_ok(), "Failed to parse cert: {:?}", result.err());
1714
1715 let (not_before, not_after) = result.unwrap();
1716 assert!(not_before < not_after);
1718 }
1719
1720 #[tokio::test]
1721 async fn test_parse_cert_expiry_invalid_pem() {
1722 let result = CertManager::parse_cert_expiry("not a valid pem");
1723 assert!(result.is_err());
1724 }
1725
1726 #[tokio::test]
1727 async fn test_cert_metadata_save_load() {
1728 let dir = tempdir().unwrap();
1729 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1730 .await
1731 .unwrap();
1732
1733 let meta = CertMetadata {
1734 domain: "test.example.com".to_string(),
1735 not_before: Utc::now(),
1736 not_after: Utc::now() + TimeDelta::days(90),
1737 provisioned_at: Utc::now(),
1738 fingerprint: "abc123def456".to_string(),
1739 };
1740
1741 manager.save_cert_metadata(&meta).await.unwrap();
1743
1744 let meta_path = dir.path().join("test.example.com.meta.json");
1746 assert!(meta_path.exists());
1747
1748 let loaded = manager.load_cert_metadata("test.example.com").await;
1750 assert!(loaded.is_some());
1751
1752 let loaded = loaded.unwrap();
1753 assert_eq!(loaded.domain, "test.example.com");
1754 assert_eq!(loaded.fingerprint, "abc123def456");
1755 }
1756
1757 #[tokio::test]
1758 async fn test_load_nonexistent_metadata() {
1759 let dir = tempdir().unwrap();
1760 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1761 .await
1762 .unwrap();
1763
1764 let loaded = manager.load_cert_metadata("nonexistent.example.com").await;
1765 assert!(loaded.is_none());
1766 }
1767
1768 #[tokio::test]
1769 async fn test_get_domains_needing_renewal() {
1770 let dir = tempdir().unwrap();
1771 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1772 .await
1773 .unwrap();
1774
1775 let expiring_meta = CertMetadata {
1777 domain: "expiring.example.com".to_string(),
1778 not_before: Utc::now() - TimeDelta::days(80),
1779 not_after: Utc::now() + TimeDelta::days(10),
1780 provisioned_at: Utc::now() - TimeDelta::days(80),
1781 fingerprint: "expiring_fingerprint".to_string(),
1782 };
1783 manager.save_cert_metadata(&expiring_meta).await.unwrap();
1784
1785 let valid_meta = CertMetadata {
1787 domain: "valid.example.com".to_string(),
1788 not_before: Utc::now() - TimeDelta::days(30),
1789 not_after: Utc::now() + TimeDelta::days(60),
1790 provisioned_at: Utc::now() - TimeDelta::days(30),
1791 fingerprint: "valid_fingerprint".to_string(),
1792 };
1793 manager.save_cert_metadata(&valid_meta).await.unwrap();
1794
1795 let needs_renewal = manager.get_domains_needing_renewal().await;
1797 assert_eq!(needs_renewal.len(), 1);
1798 assert!(needs_renewal.contains(&"expiring.example.com".to_string()));
1799 }
1800
1801 #[tokio::test]
1802 async fn test_store_cert_with_valid_pem_saves_metadata() {
1803 let dir = tempdir().unwrap();
1804 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1805 .await
1806 .unwrap();
1807
1808 let (cert_pem, key_pem) = generate_test_cert();
1810
1811 manager
1813 .store_cert("metadata.example.com", &cert_pem, &key_pem)
1814 .await
1815 .unwrap();
1816
1817 let meta = manager.load_cert_metadata("metadata.example.com").await;
1819 assert!(meta.is_some(), "Metadata was not saved");
1820
1821 let meta = meta.unwrap();
1822 assert_eq!(meta.domain, "metadata.example.com");
1823 assert!(!meta.fingerprint.is_empty());
1824 assert!(meta.not_before < meta.not_after);
1825 }
1826
1827 #[tokio::test]
1828 async fn test_store_cert_with_invalid_pem_still_stores_cert() {
1829 let dir = tempdir().unwrap();
1830 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1831 .await
1832 .unwrap();
1833
1834 let invalid_cert = "-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----";
1836 let key = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
1837
1838 manager
1839 .store_cert("invalid.example.com", invalid_cert, key)
1840 .await
1841 .unwrap();
1842
1843 assert!(manager.has_cert("invalid.example.com").await);
1845
1846 let meta = manager.load_cert_metadata("invalid.example.com").await;
1848 assert!(meta.is_none());
1849 }
1850
1851 #[tokio::test]
1852 async fn test_cert_fingerprint_is_consistent() {
1853 let (cert_pem, _) = generate_test_cert();
1855
1856 let fp1 = CertManager::compute_cert_fingerprint(&cert_pem);
1858 let fp2 = CertManager::compute_cert_fingerprint(&cert_pem);
1859 assert_eq!(fp1, fp2);
1860
1861 let (other_cert, _) = generate_test_cert();
1863 let fp3 = CertManager::compute_cert_fingerprint(&other_cert);
1864 assert_ne!(fp1, fp3);
1865 }
1866
1867 #[tokio::test]
1872 async fn test_with_directory_staging() {
1873 let dir = tempdir().unwrap();
1874 let manager = CertManager::with_directory(
1875 dir.path().to_string_lossy().to_string(),
1876 Some("test@example.com".to_string()),
1877 super::LETS_ENCRYPT_STAGING.to_string(),
1878 )
1879 .await
1880 .unwrap();
1881
1882 assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_STAGING);
1883 }
1884
1885 #[tokio::test]
1886 async fn test_with_directory_production() {
1887 let dir = tempdir().unwrap();
1888 let manager = CertManager::with_directory(
1889 dir.path().to_string_lossy().to_string(),
1890 Some("test@example.com".to_string()),
1891 super::LETS_ENCRYPT_PRODUCTION.to_string(),
1892 )
1893 .await
1894 .unwrap();
1895
1896 assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_PRODUCTION);
1897 }
1898
1899 #[tokio::test]
1900 async fn test_default_uses_production() {
1901 let dir = tempdir().unwrap();
1902 let manager = CertManager::new(
1903 dir.path().to_string_lossy().to_string(),
1904 Some("test@example.com".to_string()),
1905 )
1906 .await
1907 .unwrap();
1908
1909 assert_eq!(manager.acme_directory(), super::LETS_ENCRYPT_PRODUCTION);
1910 }
1911
1912 #[tokio::test]
1913 async fn test_provision_cert_attempted_without_email() {
1914 ensure_crypto_provider();
1915 let dir = tempdir().unwrap();
1916 let manager = CertManager::with_directory(
1921 dir.path().to_string_lossy().to_string(),
1922 None,
1923 "https://127.0.0.1:1/directory".to_string(),
1924 )
1925 .await
1926 .unwrap();
1927
1928 assert_eq!(manager.acme_email(), None);
1929
1930 let err = manager
1934 .get_cert("test.example.com")
1935 .await
1936 .expect_err("provisioning must fail against an unreachable ACME directory")
1937 .to_string();
1938 assert!(
1939 !err.contains("ACME email is required"),
1940 "Provisioning must not be gated on a missing email, got: {err}",
1941 );
1942 }
1943
1944 #[tokio::test]
1945 async fn test_acme_email_optional_contact_shape() {
1946 let dir = tempdir().unwrap();
1947
1948 let with_email = CertManager::with_directory(
1951 dir.path().join("with").to_string_lossy().to_string(),
1952 Some("ops@example.com".to_string()),
1953 super::LETS_ENCRYPT_STAGING.to_string(),
1954 )
1955 .await
1956 .unwrap();
1957 assert_eq!(with_email.acme_email(), Some("ops@example.com"));
1958 let contact: Option<String> = with_email
1959 .acme_email()
1960 .map(|email| format!("mailto:{email}"));
1961 assert_eq!(contact.as_deref(), Some("mailto:ops@example.com"));
1962
1963 let without_email = CertManager::with_directory(
1966 dir.path().join("without").to_string_lossy().to_string(),
1967 None,
1968 super::LETS_ENCRYPT_STAGING.to_string(),
1969 )
1970 .await
1971 .unwrap();
1972 assert_eq!(without_email.acme_email(), None);
1973 let contact: Vec<String> = without_email
1974 .acme_email()
1975 .map(|email| format!("mailto:{email}"))
1976 .into_iter()
1977 .collect();
1978 assert!(
1979 contact.is_empty(),
1980 "no email must yield an empty contact list, got: {contact:?}",
1981 );
1982 }
1983
1984 #[tokio::test]
1985 async fn test_credentials_path() {
1986 let dir = tempdir().unwrap();
1987 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
1988 .await
1989 .unwrap();
1990
1991 let credentials_path = manager.credentials_path();
1992 assert_eq!(
1993 credentials_path,
1994 dir.path().join("account_credentials.json")
1995 );
1996 }
1997
1998 #[tokio::test]
1999 async fn test_load_credentials_nonexistent() {
2000 let dir = tempdir().unwrap();
2001 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
2002 .await
2003 .unwrap();
2004
2005 let credentials = manager.load_credentials().await;
2006 assert!(credentials.is_none());
2007 }
2008
2009 #[tokio::test]
2014 async fn test_start_renewal_task_spawns() {
2015 use crate::sni_resolver::SniCertResolver;
2016
2017 let dir = tempdir().unwrap();
2018 let manager = Arc::new(
2019 CertManager::new(dir.path().to_string_lossy().to_string(), None)
2020 .await
2021 .unwrap(),
2022 );
2023 let sni_resolver = Arc::new(SniCertResolver::new());
2024
2025 let handle = manager.clone().start_renewal_task(sni_resolver);
2027
2028 assert!(!handle.is_finished());
2030
2031 handle.abort();
2033 }
2034
2035 #[tokio::test]
2036 async fn test_run_renewal_check_no_certs() {
2037 use crate::sni_resolver::SniCertResolver;
2038
2039 let dir = tempdir().unwrap();
2040 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
2041 .await
2042 .unwrap();
2043 let sni_resolver = SniCertResolver::new();
2044
2045 let renewed = manager.run_renewal_check(&sni_resolver).await;
2047 assert!(renewed.is_empty());
2048 }
2049
2050 #[tokio::test]
2051 async fn test_run_renewal_check_with_fresh_cert() {
2052 use crate::sni_resolver::SniCertResolver;
2053
2054 let dir = tempdir().unwrap();
2055 let manager = CertManager::new(dir.path().to_string_lossy().to_string(), None)
2056 .await
2057 .unwrap();
2058 let sni_resolver = SniCertResolver::new();
2059
2060 let meta = CertMetadata {
2062 domain: "fresh.example.com".to_string(),
2063 not_before: Utc::now() - TimeDelta::days(30),
2064 not_after: Utc::now() + TimeDelta::days(60),
2065 provisioned_at: Utc::now() - TimeDelta::days(30),
2066 fingerprint: "fresh_fingerprint".to_string(),
2067 };
2068 manager.save_cert_metadata(&meta).await.unwrap();
2069
2070 let (cert_pem, key_pem) = generate_test_cert();
2072 manager
2073 .store_cert("fresh.example.com", &cert_pem, &key_pem)
2074 .await
2075 .unwrap();
2076
2077 let renewed = manager.run_renewal_check(&sni_resolver).await;
2079 assert!(
2080 renewed.is_empty(),
2081 "Expected no renewals for fresh cert, got: {renewed:?}",
2082 );
2083 }
2084}