1use std::sync::Arc;
10use std::time::Duration;
11
12use base64::engine::general_purpose::URL_SAFE_NO_PAD;
13use base64::Engine;
14use chrono::{DateTime, Utc};
15use instant_acme::{
16 Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder,
17 Order, OrderStatus, RetryPolicy,
18};
19use tokio::sync::RwLock;
20use tracing::{debug, error, info, trace, warn};
21
22use zentinel_config::server::AcmeConfig;
23
24use super::dns::challenge::{create_challenge_info, Dns01ChallengeInfo};
25use super::error::{is_retryable_acme_error, AcmeError, ACME_RETRY_BACKOFF, ACME_RETRY_MAX};
26use super::storage::{CertificateStorage, StoredAccountCredentials};
27
28const LETSENCRYPT_PRODUCTION: &str = "https://acme-v02.api.letsencrypt.org/directory";
30const LETSENCRYPT_STAGING: &str = "https://acme-staging-v02.api.letsencrypt.org/directory";
32
33const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
35const CHALLENGE_TIMEOUT: Duration = Duration::from_secs(120);
37
38async fn retry_acme<F, Fut, T>(mut op: F) -> Result<T, AcmeError>
45where
46 F: FnMut() -> Fut,
47 Fut: std::future::Future<Output = Result<T, AcmeError>>,
48{
49 let mut backoff = ACME_RETRY_BACKOFF;
50 for attempt in 0..ACME_RETRY_MAX {
51 match op().await {
52 Ok(v) => return Ok(v),
53 Err(e) if is_retryable_acme_error(&e) && attempt + 1 < ACME_RETRY_MAX => {
54 tracing::info!(
55 attempt = attempt + 1,
56 max_retries = ACME_RETRY_MAX,
57 backoff_secs = backoff.as_secs(),
58 error = %e,
59 "ACME transient failure, retrying"
60 );
61 tokio::time::sleep(backoff).await;
62 backoff = backoff.saturating_mul(2);
63 }
64 Err(e) => return Err(e),
65 }
66 }
67 unreachable!("retry loop always returns")
68}
69
70pub struct AcmeClient {
75 account: Arc<RwLock<Option<Account>>>,
77 config: AcmeConfig,
79 storage: Arc<CertificateStorage>,
81}
82
83impl AcmeClient {
84 pub fn new(config: AcmeConfig, storage: Arc<CertificateStorage>) -> Self {
91 Self {
92 account: Arc::new(RwLock::new(None)),
93 config,
94 storage,
95 }
96 }
97
98 pub fn config(&self) -> &AcmeConfig {
100 &self.config
101 }
102
103 pub fn storage(&self) -> &CertificateStorage {
105 &self.storage
106 }
107
108 fn directory_url(&self) -> &str {
110 if let Some(ref url) = self.config.server_url {
111 url
112 } else if self.config.staging {
113 LETSENCRYPT_STAGING
114 } else {
115 LETSENCRYPT_PRODUCTION
116 }
117 }
118
119 pub async fn init_account(&self) -> Result<(), AcmeError> {
128 retry_acme(|| async { self.init_account_once().await }).await
129 }
130
131 pub async fn ensure_account(&self) -> Result<(), AcmeError> {
137 if self.account.read().await.is_some() {
138 return Ok(());
139 }
140 self.init_account().await
141 }
142
143 async fn init_account_once(&self) -> Result<(), AcmeError> {
144 if let Some(creds_json) = self.storage.load_credentials_json()? {
146 info!("Loading existing ACME account from storage");
147
148 let credentials: instant_acme::AccountCredentials = serde_json::from_str(&creds_json)
150 .map_err(|e| {
151 AcmeError::AccountCreation(format!("Failed to deserialize credentials: {}", e))
152 })?;
153
154 let account = Account::builder()
156 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?
157 .from_credentials(credentials)
158 .await
159 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?;
160
161 *self.account.write().await = Some(account);
162 info!("ACME account loaded successfully");
163 return Ok(());
164 }
165
166 info!(
168 email = %self.config.email,
169 server_url = %self.directory_url(),
170 key_type = ?self.config.key_type,
171 "Creating new ACME account"
172 );
173
174 let eab = if let Some(ref eab_config) = self.config.eab {
175 let hmac_key = URL_SAFE_NO_PAD.decode(&eab_config.hmac_key).map_err(|e| {
176 AcmeError::AccountCreation(format!("Invalid EAB HMAC key (base64url): {}", e))
177 })?;
178 Some(instant_acme::ExternalAccountKey::new(
179 eab_config.kid.clone(),
180 &hmac_key,
181 ))
182 } else {
183 None
184 };
185
186 let (account, credentials) = Account::builder()
187 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?
188 .create(
189 &NewAccount {
190 contact: &[&format!("mailto:{}", self.config.email)],
191 terms_of_service_agreed: true,
192 only_return_existing: false,
193 },
194 self.directory_url().to_owned(),
195 eab.as_ref(),
196 )
197 .await
198 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?;
199
200 let creds_json = serde_json::to_string_pretty(&credentials).map_err(|e| {
202 AcmeError::AccountCreation(format!("Failed to serialize credentials: {}", e))
203 })?;
204 self.storage.save_credentials_json(&creds_json)?;
205
206 *self.account.write().await = Some(account);
207 info!("ACME account created successfully");
208
209 Ok(())
210 }
211
212 pub async fn create_order(&self) -> Result<(Order, Vec<ChallengeInfo>), AcmeError> {
222 retry_acme(|| async { self.create_order_once().await }).await
223 }
224
225 async fn create_order_once(&self) -> Result<(Order, Vec<ChallengeInfo>), AcmeError> {
226 let account_guard = self.account.read().await;
227 let account = account_guard.as_ref().ok_or(AcmeError::NoAccount)?;
228
229 let identifiers: Vec<Identifier> = self
231 .config
232 .domains
233 .iter()
234 .map(|d: &String| Identifier::Dns(d.clone()))
235 .collect();
236
237 info!(domains = ?self.config.domains, "Creating certificate order");
238
239 let mut order = account
241 .new_order(&NewOrder::new(&identifiers))
242 .await
243 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
244
245 let mut authorizations = order.authorizations();
247 let mut challenges = Vec::new();
248
249 while let Some(result) = authorizations.next().await {
250 let mut authz = result.map_err(|e| {
251 AcmeError::OrderCreation(format!("Failed to get authorization: {}", e))
252 })?;
253
254 let identifier = authz.identifier();
255 let domain = match &identifier.identifier {
256 Identifier::Dns(domain) => domain.clone(),
257 _ => continue,
258 };
259
260 debug!(domain = %domain, status = ?authz.status, "Processing authorization");
261
262 if authz.status == AuthorizationStatus::Valid {
264 debug!(domain = %domain, "Authorization already valid");
265 continue;
266 }
267
268 let http01_challenge = authz
270 .challenge(ChallengeType::Http01)
271 .ok_or_else(|| AcmeError::NoHttp01Challenge(domain.clone()))?;
272
273 let key_authorization = http01_challenge.key_authorization();
274
275 challenges.push(ChallengeInfo {
276 domain,
277 token: http01_challenge.token.clone(),
278 key_authorization: key_authorization.as_str().to_string(),
279 url: http01_challenge.url.clone(),
280 });
281 }
282
283 Ok((order, challenges))
284 }
285
286 pub async fn create_order_dns01(&self) -> Result<(Order, Vec<Dns01ChallengeInfo>), AcmeError> {
296 retry_acme(|| async { self.create_order_dns01_once().await }).await
297 }
298
299 async fn create_order_dns01_once(&self) -> Result<(Order, Vec<Dns01ChallengeInfo>), AcmeError> {
300 let account_guard = self.account.read().await;
301 let account = account_guard.as_ref().ok_or(AcmeError::NoAccount)?;
302
303 let identifiers: Vec<Identifier> = self
305 .config
306 .domains
307 .iter()
308 .map(|d: &String| Identifier::Dns(d.clone()))
309 .collect();
310
311 info!(domains = ?self.config.domains, "Creating certificate order with DNS-01 challenges");
312
313 let mut order = account
315 .new_order(&NewOrder::new(&identifiers))
316 .await
317 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
318
319 let mut authorizations = order.authorizations();
321 let mut challenges = Vec::new();
322
323 while let Some(result) = authorizations.next().await {
324 let mut authz = result.map_err(|e| {
325 AcmeError::OrderCreation(format!("Failed to get authorization: {}", e))
326 })?;
327
328 let identifier = authz.identifier();
329 let domain = match &identifier.identifier {
330 Identifier::Dns(domain) => domain.clone(),
331 _ => continue,
332 };
333
334 debug!(domain = %domain, status = ?authz.status, "Processing DNS-01 authorization");
335
336 if authz.status == AuthorizationStatus::Valid {
338 debug!(domain = %domain, "Authorization already valid");
339 continue;
340 }
341
342 let dns01_challenge = authz
344 .challenge(ChallengeType::Dns01)
345 .ok_or_else(|| AcmeError::NoDns01Challenge(domain.clone()))?;
346
347 let key_authorization = dns01_challenge.key_authorization();
348
349 let challenge_info =
351 create_challenge_info(&domain, key_authorization.as_str(), &dns01_challenge.url);
352
353 challenges.push(challenge_info);
354 }
355
356 Ok((order, challenges))
357 }
358
359 pub async fn validate_challenge(
369 &self,
370 order: &mut Order,
371 challenge_url: &str,
372 ) -> Result<(), AcmeError> {
373 debug!(challenge_url = %challenge_url, "Setting challenge ready");
374
375 let mut authorizations = order.authorizations();
377 while let Some(result) = authorizations.next().await {
378 let mut authz = result.map_err(|e| AcmeError::ChallengeValidation {
379 domain: "unknown".to_string(),
380 message: format!("Failed to get authorization: {}", e),
381 })?;
382
383 let matching_type = authz
385 .challenges
386 .iter()
387 .find(|c| c.url == challenge_url)
388 .map(|c| c.r#type.clone());
389
390 if let Some(challenge_type) = matching_type {
391 if let Some(mut challenge) = authz.challenge(challenge_type) {
392 challenge
393 .set_ready()
394 .await
395 .map_err(|e| AcmeError::ChallengeValidation {
396 domain: "unknown".to_string(),
397 message: e.to_string(),
398 })?;
399 return Ok(());
400 }
401 }
402 }
403
404 Err(AcmeError::ChallengeValidation {
405 domain: "unknown".to_string(),
406 message: format!("Challenge not found for URL: {}", challenge_url),
407 })
408 }
409
410 pub async fn wait_for_order_ready(&self, order: &mut Order) -> Result<(), AcmeError> {
414 let deadline = tokio::time::Instant::now() + CHALLENGE_TIMEOUT;
415
416 loop {
417 let state = order
418 .refresh()
419 .await
420 .map_err(|e| AcmeError::OrderCreation(format!("Failed to refresh order: {}", e)))?;
421
422 match state.status {
423 OrderStatus::Ready => {
424 info!("Order is ready for finalization");
425 return Ok(());
426 }
427 OrderStatus::Invalid => {
428 error!("Order became invalid");
429 return Err(AcmeError::OrderCreation("Order became invalid".to_string()));
430 }
431 OrderStatus::Valid => {
432 info!("Order is already valid (certificate issued)");
433 return Ok(());
434 }
435 OrderStatus::Pending | OrderStatus::Processing => {
436 if tokio::time::Instant::now() > deadline {
437 return Err(AcmeError::Timeout(
438 "Timed out waiting for order to become ready".to_string(),
439 ));
440 }
441 trace!(status = ?state.status, "Order not ready yet, waiting...");
442 tokio::time::sleep(Duration::from_secs(2)).await;
443 }
444 }
445 }
446 }
447
448 pub async fn finalize_order(
457 &self,
458 order: &mut Order,
459 ) -> Result<(String, String, DateTime<Utc>), AcmeError> {
460 let mut backoff = ACME_RETRY_BACKOFF;
465 for attempt in 0..ACME_RETRY_MAX {
466 match self.finalize_order_once(order).await {
467 Ok(v) => return Ok(v),
468 Err(e) if is_retryable_acme_error(&e) && attempt + 1 < ACME_RETRY_MAX => {
469 tracing::info!(
470 attempt = attempt + 1,
471 max_retries = ACME_RETRY_MAX,
472 backoff_secs = backoff.as_secs(),
473 error = %e,
474 "ACME transient failure, retrying"
475 );
476 tokio::time::sleep(backoff).await;
477 backoff = backoff.saturating_mul(2);
478 }
479 Err(e) => return Err(e),
480 }
481 }
482 unreachable!("retry loop always returns")
483 }
484
485 async fn finalize_order_once(
486 &self,
487 order: &mut Order,
488 ) -> Result<(String, String, DateTime<Utc>), AcmeError> {
489 info!("Finalizing certificate order");
490
491 use zentinel_config::server::AcmeKeyType;
493 let algo = match self.config.key_type {
494 AcmeKeyType::EcdsaP256 => &rcgen::PKCS_ECDSA_P256_SHA256,
495 AcmeKeyType::EcdsaP384 => &rcgen::PKCS_ECDSA_P384_SHA384,
496 };
497
498 let cert_key = rcgen::KeyPair::generate_for(algo)
500 .map_err(|e| AcmeError::Finalization(format!("Failed to generate key: {}", e)))?;
501
502 let mut params = rcgen::CertificateParams::new(self.config.domains.clone())
504 .map_err(|e| AcmeError::Finalization(format!("Failed to create CSR params: {}", e)))?;
505
506 let mut dn = rcgen::DistinguishedName::new();
509 dn.push(rcgen::DnType::CommonName, self.config.domains[0].clone());
510 params.distinguished_name = dn;
511
512 let csr_request = params
514 .serialize_request(&cert_key)
515 .map_err(|e| AcmeError::Finalization(format!("Failed to serialize CSR: {}", e)))?;
516 let csr = csr_request.der().to_vec();
517
518 order
520 .finalize_csr(&csr)
521 .await
522 .map_err(|e| AcmeError::Finalization(format!("Failed to finalize order: {}", e)))?;
523
524 let deadline = tokio::time::Instant::now() + DEFAULT_TIMEOUT;
526 let cert_chain = loop {
527 let state = order
528 .refresh()
529 .await
530 .map_err(|e| AcmeError::Finalization(format!("Failed to refresh order: {}", e)))?;
531
532 match state.status {
533 OrderStatus::Valid => {
534 let cert_chain = order.certificate().await.map_err(|e| {
535 AcmeError::Finalization(format!("Failed to get certificate: {}", e))
536 })?;
537 break cert_chain.ok_or_else(|| {
538 AcmeError::Finalization("No certificate in response".to_string())
539 })?;
540 }
541 OrderStatus::Invalid => {
542 return Err(AcmeError::Finalization("Order became invalid".to_string()));
543 }
544 _ => {
545 if tokio::time::Instant::now() > deadline {
546 return Err(AcmeError::Timeout(
547 "Timed out waiting for certificate".to_string(),
548 ));
549 }
550 tokio::time::sleep(ACME_RETRY_BACKOFF).await;
551 }
552 }
553 };
554
555 let key_pem = cert_key.serialize_pem();
557
558 let expiry = parse_certificate_expiry(&cert_chain)?;
560
561 info!(
562 domains = ?self.config.domains,
563 expires = %expiry,
564 "Certificate issued successfully"
565 );
566
567 Ok((cert_chain, key_pem, expiry))
568 }
569
570 pub fn needs_renewal(&self, domain: &str) -> Result<bool, AcmeError> {
572 Ok(self
573 .storage
574 .needs_renewal(domain, self.config.renew_before_days)?)
575 }
576}
577
578#[derive(Debug, Clone)]
580pub struct ChallengeInfo {
581 pub domain: String,
583 pub token: String,
585 pub key_authorization: String,
587 pub url: String,
589}
590
591fn parse_certificate_expiry(cert_pem: &str) -> Result<DateTime<Utc>, AcmeError> {
593 use x509_parser::prelude::*;
594
595 let (_, pem) = pem::parse_x509_pem(cert_pem.as_bytes())
597 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse PEM: {}", e)))?;
598
599 let (_, cert) = X509Certificate::from_der(&pem.contents)
601 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse certificate: {}", e)))?;
602
603 let not_after = cert.validity().not_after;
605 let timestamp = not_after.timestamp();
606
607 DateTime::from_timestamp(timestamp, 0)
608 .ok_or_else(|| AcmeError::CertificateParse("Invalid expiry timestamp".to_string()))
609}
610
611impl std::fmt::Debug for AcmeClient {
612 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613 f.debug_struct("AcmeClient")
614 .field("config", &self.config)
615 .field(
616 "has_account",
617 &self
618 .account
619 .try_read()
620 .map(|a| a.is_some())
621 .unwrap_or(false),
622 )
623 .finish()
624 }
625}