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 sentinel_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 =
98 serde_json::from_str(&creds_json).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
165 .as_ref()
166 .ok_or(AcmeError::NoAccount)?;
167
168 let identifiers: Vec<Identifier> = self
170 .config
171 .domains
172 .iter()
173 .map(|d: &String| Identifier::Dns(d.clone()))
174 .collect();
175
176 info!(domains = ?self.config.domains, "Creating certificate order");
177
178 let mut order = account
180 .new_order(&NewOrder::new(&identifiers))
181 .await
182 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
183
184 let mut authorizations = order.authorizations();
186 let mut challenges = Vec::new();
187
188 while let Some(result) = authorizations.next().await {
189 let mut authz = result
190 .map_err(|e| AcmeError::OrderCreation(format!("Failed to get authorization: {}", e)))?;
191
192 let identifier = authz.identifier();
193 let domain = match &identifier.identifier {
194 Identifier::Dns(domain) => domain.clone(),
195 _ => continue,
196 };
197
198 debug!(domain = %domain, status = ?authz.status, "Processing authorization");
199
200 if authz.status == AuthorizationStatus::Valid {
202 debug!(domain = %domain, "Authorization already valid");
203 continue;
204 }
205
206 let http01_challenge = authz
208 .challenge(ChallengeType::Http01)
209 .ok_or_else(|| AcmeError::NoHttp01Challenge(domain.clone()))?;
210
211 let key_authorization = http01_challenge.key_authorization();
212
213 challenges.push(ChallengeInfo {
214 domain,
215 token: http01_challenge.token.clone(),
216 key_authorization: key_authorization.as_str().to_string(),
217 url: http01_challenge.url.clone(),
218 });
219 }
220
221 Ok((order, challenges))
222 }
223
224 pub async fn create_order_dns01(
234 &self,
235 ) -> Result<(Order, Vec<Dns01ChallengeInfo>), AcmeError> {
236 let account_guard = self.account.read().await;
237 let account = account_guard.as_ref().ok_or(AcmeError::NoAccount)?;
238
239 let identifiers: Vec<Identifier> = self
241 .config
242 .domains
243 .iter()
244 .map(|d: &String| Identifier::Dns(d.clone()))
245 .collect();
246
247 info!(domains = ?self.config.domains, "Creating certificate order with DNS-01 challenges");
248
249 let mut order = account
251 .new_order(&NewOrder::new(&identifiers))
252 .await
253 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
254
255 let mut authorizations = order.authorizations();
257 let mut challenges = Vec::new();
258
259 while let Some(result) = authorizations.next().await {
260 let mut authz = result
261 .map_err(|e| AcmeError::OrderCreation(format!("Failed to get authorization: {}", e)))?;
262
263 let identifier = authz.identifier();
264 let domain = match &identifier.identifier {
265 Identifier::Dns(domain) => domain.clone(),
266 _ => continue,
267 };
268
269 debug!(domain = %domain, status = ?authz.status, "Processing DNS-01 authorization");
270
271 if authz.status == AuthorizationStatus::Valid {
273 debug!(domain = %domain, "Authorization already valid");
274 continue;
275 }
276
277 let dns01_challenge = authz
279 .challenge(ChallengeType::Dns01)
280 .ok_or_else(|| AcmeError::NoDns01Challenge(domain.clone()))?;
281
282 let key_authorization = dns01_challenge.key_authorization();
283
284 let challenge_info = create_challenge_info(
286 &domain,
287 key_authorization.as_str(),
288 &dns01_challenge.url,
289 );
290
291 challenges.push(challenge_info);
292 }
293
294 Ok((order, challenges))
295 }
296
297 pub async fn validate_challenge(
307 &self,
308 order: &mut Order,
309 challenge_url: &str,
310 ) -> Result<(), AcmeError> {
311 debug!(challenge_url = %challenge_url, "Setting challenge ready");
312
313 let mut authorizations = order.authorizations();
315 while let Some(result) = authorizations.next().await {
316 let mut authz = result.map_err(|e| AcmeError::ChallengeValidation {
317 domain: "unknown".to_string(),
318 message: format!("Failed to get authorization: {}", e),
319 })?;
320
321 let matching_type = authz
323 .challenges
324 .iter()
325 .find(|c| c.url == challenge_url)
326 .map(|c| c.r#type.clone());
327
328 if let Some(challenge_type) = matching_type {
329 if let Some(mut challenge) = authz.challenge(challenge_type) {
330 challenge
331 .set_ready()
332 .await
333 .map_err(|e| AcmeError::ChallengeValidation {
334 domain: "unknown".to_string(),
335 message: e.to_string(),
336 })?;
337 return Ok(());
338 }
339 }
340 }
341
342 Err(AcmeError::ChallengeValidation {
343 domain: "unknown".to_string(),
344 message: format!("Challenge not found for URL: {}", challenge_url),
345 })
346 }
347
348 pub async fn wait_for_order_ready(&self, order: &mut Order) -> Result<(), AcmeError> {
352 let deadline = tokio::time::Instant::now() + CHALLENGE_TIMEOUT;
353
354 loop {
355 let state = order
356 .refresh()
357 .await
358 .map_err(|e| AcmeError::OrderCreation(format!("Failed to refresh order: {}", e)))?;
359
360 match state.status {
361 OrderStatus::Ready => {
362 info!("Order is ready for finalization");
363 return Ok(());
364 }
365 OrderStatus::Invalid => {
366 error!("Order became invalid");
367 return Err(AcmeError::OrderCreation("Order became invalid".to_string()));
368 }
369 OrderStatus::Valid => {
370 info!("Order is already valid (certificate issued)");
371 return Ok(());
372 }
373 OrderStatus::Pending | OrderStatus::Processing => {
374 if tokio::time::Instant::now() > deadline {
375 return Err(AcmeError::Timeout(
376 "Timed out waiting for order to become ready".to_string(),
377 ));
378 }
379 trace!(status = ?state.status, "Order not ready yet, waiting...");
380 tokio::time::sleep(Duration::from_secs(2)).await;
381 }
382 }
383 }
384 }
385
386 pub async fn finalize_order(
395 &self,
396 order: &mut Order,
397 ) -> Result<(String, String, DateTime<Utc>), AcmeError> {
398 info!("Finalizing certificate order");
399
400 let cert_key = rcgen::KeyPair::generate()
402 .map_err(|e| AcmeError::Finalization(format!("Failed to generate key: {}", e)))?;
403
404 let params = rcgen::CertificateParams::new(self.config.domains.clone())
406 .map_err(|e| AcmeError::Finalization(format!("Failed to create CSR params: {}", e)))?;
407
408 let csr_request = params
410 .serialize_request(&cert_key)
411 .map_err(|e| AcmeError::Finalization(format!("Failed to serialize CSR: {}", e)))?;
412 let csr = csr_request.der().to_vec();
413
414 order
416 .finalize_csr(&csr)
417 .await
418 .map_err(|e| AcmeError::Finalization(format!("Failed to finalize order: {}", e)))?;
419
420 let deadline = tokio::time::Instant::now() + DEFAULT_TIMEOUT;
422 let cert_chain = loop {
423 let state = order.refresh().await.map_err(|e| {
424 AcmeError::Finalization(format!("Failed to refresh order: {}", e))
425 })?;
426
427 match state.status {
428 OrderStatus::Valid => {
429 let cert_chain = order.certificate().await.map_err(|e| {
430 AcmeError::Finalization(format!("Failed to get certificate: {}", e))
431 })?;
432 break cert_chain.ok_or_else(|| {
433 AcmeError::Finalization("No certificate in response".to_string())
434 })?;
435 }
436 OrderStatus::Invalid => {
437 return Err(AcmeError::Finalization("Order became invalid".to_string()));
438 }
439 _ => {
440 if tokio::time::Instant::now() > deadline {
441 return Err(AcmeError::Timeout(
442 "Timed out waiting for certificate".to_string(),
443 ));
444 }
445 tokio::time::sleep(Duration::from_secs(1)).await;
446 }
447 }
448 };
449
450 let key_pem = cert_key.serialize_pem();
452
453 let expiry = parse_certificate_expiry(&cert_chain)?;
455
456 info!(
457 domains = ?self.config.domains,
458 expires = %expiry,
459 "Certificate issued successfully"
460 );
461
462 Ok((cert_chain, key_pem, expiry))
463 }
464
465 pub fn needs_renewal(&self, domain: &str) -> Result<bool, AcmeError> {
467 Ok(self
468 .storage
469 .needs_renewal(domain, self.config.renew_before_days)?)
470 }
471}
472
473#[derive(Debug, Clone)]
475pub struct ChallengeInfo {
476 pub domain: String,
478 pub token: String,
480 pub key_authorization: String,
482 pub url: String,
484}
485
486fn parse_certificate_expiry(cert_pem: &str) -> Result<DateTime<Utc>, AcmeError> {
488 use x509_parser::prelude::*;
489
490 let (_, pem) = pem::parse_x509_pem(cert_pem.as_bytes())
492 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse PEM: {}", e)))?;
493
494 let (_, cert) = X509Certificate::from_der(&pem.contents)
496 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse certificate: {}", e)))?;
497
498 let not_after = cert.validity().not_after;
500 let timestamp = not_after.timestamp();
501
502 DateTime::from_timestamp(timestamp, 0)
503 .ok_or_else(|| AcmeError::CertificateParse("Invalid expiry timestamp".to_string()))
504}
505
506impl std::fmt::Debug for AcmeClient {
507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
508 f.debug_struct("AcmeClient")
509 .field("config", &self.config)
510 .field("has_account", &self.account.try_read().map(|a| a.is_some()).unwrap_or(false))
511 .finish()
512 }
513}