1use std::sync::Arc;
10use std::time::Duration;
11
12use chrono::{DateTime, Utc};
13use instant_acme::{
14 Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder,
15 Order, OrderStatus, RetryPolicy,
16};
17use tokio::sync::RwLock;
18use tracing::{debug, error, info, trace, warn};
19
20use grapsus_config::server::AcmeConfig;
21
22use super::dns::challenge::{create_challenge_info, Dns01ChallengeInfo};
23use super::error::AcmeError;
24use super::storage::{CertificateStorage, StoredAccountCredentials};
25
26const LETSENCRYPT_PRODUCTION: &str = "https://acme-v02.api.letsencrypt.org/directory";
28const LETSENCRYPT_STAGING: &str = "https://acme-staging-v02.api.letsencrypt.org/directory";
30
31const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
33const CHALLENGE_TIMEOUT: Duration = Duration::from_secs(120);
35
36pub struct AcmeClient {
41 account: Arc<RwLock<Option<Account>>>,
43 config: AcmeConfig,
45 storage: Arc<CertificateStorage>,
47}
48
49impl AcmeClient {
50 pub fn new(config: AcmeConfig, storage: Arc<CertificateStorage>) -> Self {
57 Self {
58 account: Arc::new(RwLock::new(None)),
59 config,
60 storage,
61 }
62 }
63
64 pub fn config(&self) -> &AcmeConfig {
66 &self.config
67 }
68
69 pub fn storage(&self) -> &CertificateStorage {
71 &self.storage
72 }
73
74 fn directory_url(&self) -> &str {
76 if self.config.staging {
77 LETSENCRYPT_STAGING
78 } else {
79 LETSENCRYPT_PRODUCTION
80 }
81 }
82
83 pub async fn init_account(&self) -> Result<(), AcmeError> {
92 if let Some(creds_json) = self.storage.load_credentials_json()? {
94 info!("Loading existing ACME account from storage");
95
96 let credentials: instant_acme::AccountCredentials = serde_json::from_str(&creds_json)
98 .map_err(|e| {
99 AcmeError::AccountCreation(format!("Failed to deserialize credentials: {}", e))
100 })?;
101
102 let account = Account::builder()
104 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?
105 .from_credentials(credentials)
106 .await
107 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?;
108
109 *self.account.write().await = Some(account);
110 info!("ACME account loaded successfully");
111 return Ok(());
112 }
113
114 info!(
116 email = %self.config.email,
117 staging = self.config.staging,
118 "Creating new ACME account"
119 );
120
121 let directory = if self.config.staging {
122 LetsEncrypt::Staging
123 } else {
124 LetsEncrypt::Production
125 };
126
127 let (account, credentials) = Account::builder()
128 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?
129 .create(
130 &NewAccount {
131 contact: &[&format!("mailto:{}", self.config.email)],
132 terms_of_service_agreed: true,
133 only_return_existing: false,
134 },
135 directory.url().to_owned(),
136 None,
137 )
138 .await
139 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?;
140
141 let creds_json = serde_json::to_string_pretty(&credentials).map_err(|e| {
143 AcmeError::AccountCreation(format!("Failed to serialize credentials: {}", e))
144 })?;
145 self.storage.save_credentials_json(&creds_json)?;
146
147 *self.account.write().await = Some(account);
148 info!("ACME account created successfully");
149
150 Ok(())
151 }
152
153 pub async fn create_order(&self) -> Result<(Order, Vec<ChallengeInfo>), AcmeError> {
163 let account_guard = self.account.read().await;
164 let account = account_guard.as_ref().ok_or(AcmeError::NoAccount)?;
165
166 let identifiers: Vec<Identifier> = self
168 .config
169 .domains
170 .iter()
171 .map(|d: &String| Identifier::Dns(d.clone()))
172 .collect();
173
174 info!(domains = ?self.config.domains, "Creating certificate order");
175
176 let mut order = account
178 .new_order(&NewOrder::new(&identifiers))
179 .await
180 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
181
182 let mut authorizations = order.authorizations();
184 let mut challenges = Vec::new();
185
186 while let Some(result) = authorizations.next().await {
187 let mut authz = result.map_err(|e| {
188 AcmeError::OrderCreation(format!("Failed to get authorization: {}", e))
189 })?;
190
191 let identifier = authz.identifier();
192 let domain = match &identifier.identifier {
193 Identifier::Dns(domain) => domain.clone(),
194 _ => continue,
195 };
196
197 debug!(domain = %domain, status = ?authz.status, "Processing authorization");
198
199 if authz.status == AuthorizationStatus::Valid {
201 debug!(domain = %domain, "Authorization already valid");
202 continue;
203 }
204
205 let http01_challenge = authz
207 .challenge(ChallengeType::Http01)
208 .ok_or_else(|| AcmeError::NoHttp01Challenge(domain.clone()))?;
209
210 let key_authorization = http01_challenge.key_authorization();
211
212 challenges.push(ChallengeInfo {
213 domain,
214 token: http01_challenge.token.clone(),
215 key_authorization: key_authorization.as_str().to_string(),
216 url: http01_challenge.url.clone(),
217 });
218 }
219
220 Ok((order, challenges))
221 }
222
223 pub async fn create_order_dns01(&self) -> Result<(Order, Vec<Dns01ChallengeInfo>), AcmeError> {
233 let account_guard = self.account.read().await;
234 let account = account_guard.as_ref().ok_or(AcmeError::NoAccount)?;
235
236 let identifiers: Vec<Identifier> = self
238 .config
239 .domains
240 .iter()
241 .map(|d: &String| Identifier::Dns(d.clone()))
242 .collect();
243
244 info!(domains = ?self.config.domains, "Creating certificate order with DNS-01 challenges");
245
246 let mut order = account
248 .new_order(&NewOrder::new(&identifiers))
249 .await
250 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
251
252 let mut authorizations = order.authorizations();
254 let mut challenges = Vec::new();
255
256 while let Some(result) = authorizations.next().await {
257 let mut authz = result.map_err(|e| {
258 AcmeError::OrderCreation(format!("Failed to get authorization: {}", e))
259 })?;
260
261 let identifier = authz.identifier();
262 let domain = match &identifier.identifier {
263 Identifier::Dns(domain) => domain.clone(),
264 _ => continue,
265 };
266
267 debug!(domain = %domain, status = ?authz.status, "Processing DNS-01 authorization");
268
269 if authz.status == AuthorizationStatus::Valid {
271 debug!(domain = %domain, "Authorization already valid");
272 continue;
273 }
274
275 let dns01_challenge = authz
277 .challenge(ChallengeType::Dns01)
278 .ok_or_else(|| AcmeError::NoDns01Challenge(domain.clone()))?;
279
280 let key_authorization = dns01_challenge.key_authorization();
281
282 let challenge_info =
284 create_challenge_info(&domain, key_authorization.as_str(), &dns01_challenge.url);
285
286 challenges.push(challenge_info);
287 }
288
289 Ok((order, challenges))
290 }
291
292 pub async fn validate_challenge(
302 &self,
303 order: &mut Order,
304 challenge_url: &str,
305 ) -> Result<(), AcmeError> {
306 debug!(challenge_url = %challenge_url, "Setting challenge ready");
307
308 let mut authorizations = order.authorizations();
310 while let Some(result) = authorizations.next().await {
311 let mut authz = result.map_err(|e| AcmeError::ChallengeValidation {
312 domain: "unknown".to_string(),
313 message: format!("Failed to get authorization: {}", e),
314 })?;
315
316 let matching_type = authz
318 .challenges
319 .iter()
320 .find(|c| c.url == challenge_url)
321 .map(|c| c.r#type.clone());
322
323 if let Some(challenge_type) = matching_type {
324 if let Some(mut challenge) = authz.challenge(challenge_type) {
325 challenge
326 .set_ready()
327 .await
328 .map_err(|e| AcmeError::ChallengeValidation {
329 domain: "unknown".to_string(),
330 message: e.to_string(),
331 })?;
332 return Ok(());
333 }
334 }
335 }
336
337 Err(AcmeError::ChallengeValidation {
338 domain: "unknown".to_string(),
339 message: format!("Challenge not found for URL: {}", challenge_url),
340 })
341 }
342
343 pub async fn wait_for_order_ready(&self, order: &mut Order) -> Result<(), AcmeError> {
347 let deadline = tokio::time::Instant::now() + CHALLENGE_TIMEOUT;
348
349 loop {
350 let state = order
351 .refresh()
352 .await
353 .map_err(|e| AcmeError::OrderCreation(format!("Failed to refresh order: {}", e)))?;
354
355 match state.status {
356 OrderStatus::Ready => {
357 info!("Order is ready for finalization");
358 return Ok(());
359 }
360 OrderStatus::Invalid => {
361 error!("Order became invalid");
362 return Err(AcmeError::OrderCreation("Order became invalid".to_string()));
363 }
364 OrderStatus::Valid => {
365 info!("Order is already valid (certificate issued)");
366 return Ok(());
367 }
368 OrderStatus::Pending | OrderStatus::Processing => {
369 if tokio::time::Instant::now() > deadline {
370 return Err(AcmeError::Timeout(
371 "Timed out waiting for order to become ready".to_string(),
372 ));
373 }
374 trace!(status = ?state.status, "Order not ready yet, waiting...");
375 tokio::time::sleep(Duration::from_secs(2)).await;
376 }
377 }
378 }
379 }
380
381 pub async fn finalize_order(
390 &self,
391 order: &mut Order,
392 ) -> Result<(String, String, DateTime<Utc>), AcmeError> {
393 info!("Finalizing certificate order");
394
395 let cert_key = rcgen::KeyPair::generate()
397 .map_err(|e| AcmeError::Finalization(format!("Failed to generate key: {}", e)))?;
398
399 let mut params = rcgen::CertificateParams::new(self.config.domains.clone())
401 .map_err(|e| AcmeError::Finalization(format!("Failed to create CSR params: {}", e)))?;
402
403 let mut dn = rcgen::DistinguishedName::new();
406 dn.push(rcgen::DnType::CommonName, self.config.domains[0].clone());
407 params.distinguished_name = dn;
408
409 let csr_request = params
411 .serialize_request(&cert_key)
412 .map_err(|e| AcmeError::Finalization(format!("Failed to serialize CSR: {}", e)))?;
413 let csr = csr_request.der().to_vec();
414
415 order
417 .finalize_csr(&csr)
418 .await
419 .map_err(|e| AcmeError::Finalization(format!("Failed to finalize order: {}", e)))?;
420
421 let deadline = tokio::time::Instant::now() + DEFAULT_TIMEOUT;
423 let cert_chain = loop {
424 let state = order
425 .refresh()
426 .await
427 .map_err(|e| AcmeError::Finalization(format!("Failed to refresh order: {}", e)))?;
428
429 match state.status {
430 OrderStatus::Valid => {
431 let cert_chain = order.certificate().await.map_err(|e| {
432 AcmeError::Finalization(format!("Failed to get certificate: {}", e))
433 })?;
434 break cert_chain.ok_or_else(|| {
435 AcmeError::Finalization("No certificate in response".to_string())
436 })?;
437 }
438 OrderStatus::Invalid => {
439 return Err(AcmeError::Finalization("Order became invalid".to_string()));
440 }
441 _ => {
442 if tokio::time::Instant::now() > deadline {
443 return Err(AcmeError::Timeout(
444 "Timed out waiting for certificate".to_string(),
445 ));
446 }
447 tokio::time::sleep(Duration::from_secs(1)).await;
448 }
449 }
450 };
451
452 let key_pem = cert_key.serialize_pem();
454
455 let expiry = parse_certificate_expiry(&cert_chain)?;
457
458 info!(
459 domains = ?self.config.domains,
460 expires = %expiry,
461 "Certificate issued successfully"
462 );
463
464 Ok((cert_chain, key_pem, expiry))
465 }
466
467 pub fn needs_renewal(&self, domain: &str) -> Result<bool, AcmeError> {
469 Ok(self
470 .storage
471 .needs_renewal(domain, self.config.renew_before_days)?)
472 }
473}
474
475#[derive(Debug, Clone)]
477pub struct ChallengeInfo {
478 pub domain: String,
480 pub token: String,
482 pub key_authorization: String,
484 pub url: String,
486}
487
488fn parse_certificate_expiry(cert_pem: &str) -> Result<DateTime<Utc>, AcmeError> {
490 use x509_parser::prelude::*;
491
492 let (_, pem) = pem::parse_x509_pem(cert_pem.as_bytes())
494 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse PEM: {}", e)))?;
495
496 let (_, cert) = X509Certificate::from_der(&pem.contents)
498 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse certificate: {}", e)))?;
499
500 let not_after = cert.validity().not_after;
502 let timestamp = not_after.timestamp();
503
504 DateTime::from_timestamp(timestamp, 0)
505 .ok_or_else(|| AcmeError::CertificateParse("Invalid expiry timestamp".to_string()))
506}
507
508impl std::fmt::Debug for AcmeClient {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 f.debug_struct("AcmeClient")
511 .field("config", &self.config)
512 .field(
513 "has_account",
514 &self
515 .account
516 .try_read()
517 .map(|a| a.is_some())
518 .unwrap_or(false),
519 )
520 .finish()
521 }
522}