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::AcmeError;
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
38pub struct AcmeClient {
43 account: Arc<RwLock<Option<Account>>>,
45 config: AcmeConfig,
47 storage: Arc<CertificateStorage>,
49}
50
51impl AcmeClient {
52 pub fn new(config: AcmeConfig, storage: Arc<CertificateStorage>) -> Self {
59 Self {
60 account: Arc::new(RwLock::new(None)),
61 config,
62 storage,
63 }
64 }
65
66 pub fn config(&self) -> &AcmeConfig {
68 &self.config
69 }
70
71 pub fn storage(&self) -> &CertificateStorage {
73 &self.storage
74 }
75
76 fn directory_url(&self) -> &str {
78 if let Some(ref url) = self.config.server_url {
79 url
80 } else if self.config.staging {
81 LETSENCRYPT_STAGING
82 } else {
83 LETSENCRYPT_PRODUCTION
84 }
85 }
86
87 pub async fn init_account(&self) -> Result<(), AcmeError> {
96 if let Some(creds_json) = self.storage.load_credentials_json()? {
98 info!("Loading existing ACME account from storage");
99
100 let credentials: instant_acme::AccountCredentials = serde_json::from_str(&creds_json)
102 .map_err(|e| {
103 AcmeError::AccountCreation(format!("Failed to deserialize credentials: {}", e))
104 })?;
105
106 let account = Account::builder()
108 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?
109 .from_credentials(credentials)
110 .await
111 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?;
112
113 *self.account.write().await = Some(account);
114 info!("ACME account loaded successfully");
115 return Ok(());
116 }
117
118 info!(
120 email = %self.config.email,
121 server_url = %self.directory_url(),
122 key_type = ?self.config.key_type,
123 "Creating new ACME account"
124 );
125
126 let eab = if let Some(ref eab_config) = self.config.eab {
127 let hmac_key = URL_SAFE_NO_PAD.decode(&eab_config.hmac_key).map_err(|e| {
128 AcmeError::AccountCreation(format!("Invalid EAB HMAC key (base64url): {}", e))
129 })?;
130 Some(instant_acme::ExternalAccountKey::new(
131 eab_config.kid.clone(),
132 &hmac_key,
133 ))
134 } else {
135 None
136 };
137
138 let (account, credentials) = Account::builder()
139 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?
140 .create(
141 &NewAccount {
142 contact: &[&format!("mailto:{}", self.config.email)],
143 terms_of_service_agreed: true,
144 only_return_existing: false,
145 },
146 self.directory_url().to_owned(),
147 eab.as_ref(),
148 )
149 .await
150 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?;
151
152 let creds_json = serde_json::to_string_pretty(&credentials).map_err(|e| {
154 AcmeError::AccountCreation(format!("Failed to serialize credentials: {}", e))
155 })?;
156 self.storage.save_credentials_json(&creds_json)?;
157
158 *self.account.write().await = Some(account);
159 info!("ACME account created successfully");
160
161 Ok(())
162 }
163
164 pub async fn create_order(&self) -> Result<(Order, Vec<ChallengeInfo>), AcmeError> {
174 let account_guard = self.account.read().await;
175 let account = account_guard.as_ref().ok_or(AcmeError::NoAccount)?;
176
177 let identifiers: Vec<Identifier> = self
179 .config
180 .domains
181 .iter()
182 .map(|d: &String| Identifier::Dns(d.clone()))
183 .collect();
184
185 info!(domains = ?self.config.domains, "Creating certificate order");
186
187 let mut order = account
189 .new_order(&NewOrder::new(&identifiers))
190 .await
191 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
192
193 let mut authorizations = order.authorizations();
195 let mut challenges = Vec::new();
196
197 while let Some(result) = authorizations.next().await {
198 let mut authz = result.map_err(|e| {
199 AcmeError::OrderCreation(format!("Failed to get authorization: {}", e))
200 })?;
201
202 let identifier = authz.identifier();
203 let domain = match &identifier.identifier {
204 Identifier::Dns(domain) => domain.clone(),
205 _ => continue,
206 };
207
208 debug!(domain = %domain, status = ?authz.status, "Processing authorization");
209
210 if authz.status == AuthorizationStatus::Valid {
212 debug!(domain = %domain, "Authorization already valid");
213 continue;
214 }
215
216 let http01_challenge = authz
218 .challenge(ChallengeType::Http01)
219 .ok_or_else(|| AcmeError::NoHttp01Challenge(domain.clone()))?;
220
221 let key_authorization = http01_challenge.key_authorization();
222
223 challenges.push(ChallengeInfo {
224 domain,
225 token: http01_challenge.token.clone(),
226 key_authorization: key_authorization.as_str().to_string(),
227 url: http01_challenge.url.clone(),
228 });
229 }
230
231 Ok((order, challenges))
232 }
233
234 pub async fn create_order_dns01(&self) -> Result<(Order, Vec<Dns01ChallengeInfo>), AcmeError> {
244 let account_guard = self.account.read().await;
245 let account = account_guard.as_ref().ok_or(AcmeError::NoAccount)?;
246
247 let identifiers: Vec<Identifier> = self
249 .config
250 .domains
251 .iter()
252 .map(|d: &String| Identifier::Dns(d.clone()))
253 .collect();
254
255 info!(domains = ?self.config.domains, "Creating certificate order with DNS-01 challenges");
256
257 let mut order = account
259 .new_order(&NewOrder::new(&identifiers))
260 .await
261 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
262
263 let mut authorizations = order.authorizations();
265 let mut challenges = Vec::new();
266
267 while let Some(result) = authorizations.next().await {
268 let mut authz = result.map_err(|e| {
269 AcmeError::OrderCreation(format!("Failed to get authorization: {}", e))
270 })?;
271
272 let identifier = authz.identifier();
273 let domain = match &identifier.identifier {
274 Identifier::Dns(domain) => domain.clone(),
275 _ => continue,
276 };
277
278 debug!(domain = %domain, status = ?authz.status, "Processing DNS-01 authorization");
279
280 if authz.status == AuthorizationStatus::Valid {
282 debug!(domain = %domain, "Authorization already valid");
283 continue;
284 }
285
286 let dns01_challenge = authz
288 .challenge(ChallengeType::Dns01)
289 .ok_or_else(|| AcmeError::NoDns01Challenge(domain.clone()))?;
290
291 let key_authorization = dns01_challenge.key_authorization();
292
293 let challenge_info =
295 create_challenge_info(&domain, key_authorization.as_str(), &dns01_challenge.url);
296
297 challenges.push(challenge_info);
298 }
299
300 Ok((order, challenges))
301 }
302
303 pub async fn validate_challenge(
313 &self,
314 order: &mut Order,
315 challenge_url: &str,
316 ) -> Result<(), AcmeError> {
317 debug!(challenge_url = %challenge_url, "Setting challenge ready");
318
319 let mut authorizations = order.authorizations();
321 while let Some(result) = authorizations.next().await {
322 let mut authz = result.map_err(|e| AcmeError::ChallengeValidation {
323 domain: "unknown".to_string(),
324 message: format!("Failed to get authorization: {}", e),
325 })?;
326
327 let matching_type = authz
329 .challenges
330 .iter()
331 .find(|c| c.url == challenge_url)
332 .map(|c| c.r#type.clone());
333
334 if let Some(challenge_type) = matching_type {
335 if let Some(mut challenge) = authz.challenge(challenge_type) {
336 challenge
337 .set_ready()
338 .await
339 .map_err(|e| AcmeError::ChallengeValidation {
340 domain: "unknown".to_string(),
341 message: e.to_string(),
342 })?;
343 return Ok(());
344 }
345 }
346 }
347
348 Err(AcmeError::ChallengeValidation {
349 domain: "unknown".to_string(),
350 message: format!("Challenge not found for URL: {}", challenge_url),
351 })
352 }
353
354 pub async fn wait_for_order_ready(&self, order: &mut Order) -> Result<(), AcmeError> {
358 let deadline = tokio::time::Instant::now() + CHALLENGE_TIMEOUT;
359
360 loop {
361 let state = order
362 .refresh()
363 .await
364 .map_err(|e| AcmeError::OrderCreation(format!("Failed to refresh order: {}", e)))?;
365
366 match state.status {
367 OrderStatus::Ready => {
368 info!("Order is ready for finalization");
369 return Ok(());
370 }
371 OrderStatus::Invalid => {
372 error!("Order became invalid");
373 return Err(AcmeError::OrderCreation("Order became invalid".to_string()));
374 }
375 OrderStatus::Valid => {
376 info!("Order is already valid (certificate issued)");
377 return Ok(());
378 }
379 OrderStatus::Pending | OrderStatus::Processing => {
380 if tokio::time::Instant::now() > deadline {
381 return Err(AcmeError::Timeout(
382 "Timed out waiting for order to become ready".to_string(),
383 ));
384 }
385 trace!(status = ?state.status, "Order not ready yet, waiting...");
386 tokio::time::sleep(Duration::from_secs(2)).await;
387 }
388 }
389 }
390 }
391
392 pub async fn finalize_order(
401 &self,
402 order: &mut Order,
403 ) -> Result<(String, String, DateTime<Utc>), AcmeError> {
404 info!("Finalizing certificate order");
405
406 use zentinel_config::server::AcmeKeyType;
408 let algo = match self.config.key_type {
409 AcmeKeyType::EcdsaP256 => &rcgen::PKCS_ECDSA_P256_SHA256,
410 AcmeKeyType::EcdsaP384 => &rcgen::PKCS_ECDSA_P384_SHA384,
411 };
412
413 let cert_key = rcgen::KeyPair::generate_for(algo)
415 .map_err(|e| AcmeError::Finalization(format!("Failed to generate key: {}", e)))?;
416
417 let mut params = rcgen::CertificateParams::new(self.config.domains.clone())
419 .map_err(|e| AcmeError::Finalization(format!("Failed to create CSR params: {}", e)))?;
420
421 let mut dn = rcgen::DistinguishedName::new();
424 dn.push(rcgen::DnType::CommonName, self.config.domains[0].clone());
425 params.distinguished_name = dn;
426
427 let csr_request = params
429 .serialize_request(&cert_key)
430 .map_err(|e| AcmeError::Finalization(format!("Failed to serialize CSR: {}", e)))?;
431 let csr = csr_request.der().to_vec();
432
433 order
435 .finalize_csr(&csr)
436 .await
437 .map_err(|e| AcmeError::Finalization(format!("Failed to finalize order: {}", e)))?;
438
439 let deadline = tokio::time::Instant::now() + DEFAULT_TIMEOUT;
441 let cert_chain = loop {
442 let state = order
443 .refresh()
444 .await
445 .map_err(|e| AcmeError::Finalization(format!("Failed to refresh order: {}", e)))?;
446
447 match state.status {
448 OrderStatus::Valid => {
449 let cert_chain = order.certificate().await.map_err(|e| {
450 AcmeError::Finalization(format!("Failed to get certificate: {}", e))
451 })?;
452 break cert_chain.ok_or_else(|| {
453 AcmeError::Finalization("No certificate in response".to_string())
454 })?;
455 }
456 OrderStatus::Invalid => {
457 return Err(AcmeError::Finalization("Order became invalid".to_string()));
458 }
459 _ => {
460 if tokio::time::Instant::now() > deadline {
461 return Err(AcmeError::Timeout(
462 "Timed out waiting for certificate".to_string(),
463 ));
464 }
465 tokio::time::sleep(Duration::from_secs(1)).await;
466 }
467 }
468 };
469
470 let key_pem = cert_key.serialize_pem();
472
473 let expiry = parse_certificate_expiry(&cert_chain)?;
475
476 info!(
477 domains = ?self.config.domains,
478 expires = %expiry,
479 "Certificate issued successfully"
480 );
481
482 Ok((cert_chain, key_pem, expiry))
483 }
484
485 pub fn needs_renewal(&self, domain: &str) -> Result<bool, AcmeError> {
487 Ok(self
488 .storage
489 .needs_renewal(domain, self.config.renew_before_days)?)
490 }
491}
492
493#[derive(Debug, Clone)]
495pub struct ChallengeInfo {
496 pub domain: String,
498 pub token: String,
500 pub key_authorization: String,
502 pub url: String,
504}
505
506fn parse_certificate_expiry(cert_pem: &str) -> Result<DateTime<Utc>, AcmeError> {
508 use x509_parser::prelude::*;
509
510 let (_, pem) = pem::parse_x509_pem(cert_pem.as_bytes())
512 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse PEM: {}", e)))?;
513
514 let (_, cert) = X509Certificate::from_der(&pem.contents)
516 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse certificate: {}", e)))?;
517
518 let not_after = cert.validity().not_after;
520 let timestamp = not_after.timestamp();
521
522 DateTime::from_timestamp(timestamp, 0)
523 .ok_or_else(|| AcmeError::CertificateParse("Invalid expiry timestamp".to_string()))
524}
525
526impl std::fmt::Debug for AcmeClient {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 f.debug_struct("AcmeClient")
529 .field("config", &self.config)
530 .field(
531 "has_account",
532 &self
533 .account
534 .try_read()
535 .map(|a| a.is_some())
536 .unwrap_or(false),
537 )
538 .finish()
539 }
540}