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,
16};
17use tokio::sync::RwLock;
18use tracing::{debug, error, info, trace, warn};
19
20use sentinel_config::server::AcmeConfig;
21
22use super::error::AcmeError;
23use super::storage::{CertificateStorage, StoredAccountCredentials};
24
25const LETSENCRYPT_PRODUCTION: &str = "https://acme-v02.api.letsencrypt.org/directory";
27const LETSENCRYPT_STAGING: &str = "https://acme-staging-v02.api.letsencrypt.org/directory";
29
30const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
32const CHALLENGE_TIMEOUT: Duration = Duration::from_secs(120);
34
35pub struct AcmeClient {
40 account: Arc<RwLock<Option<Account>>>,
42 config: AcmeConfig,
44 storage: Arc<CertificateStorage>,
46}
47
48impl AcmeClient {
49 pub fn new(config: AcmeConfig, storage: Arc<CertificateStorage>) -> Self {
56 Self {
57 account: Arc::new(RwLock::new(None)),
58 config,
59 storage,
60 }
61 }
62
63 pub fn config(&self) -> &AcmeConfig {
65 &self.config
66 }
67
68 pub fn storage(&self) -> &CertificateStorage {
70 &self.storage
71 }
72
73 fn directory_url(&self) -> &str {
75 if self.config.staging {
76 LETSENCRYPT_STAGING
77 } else {
78 LETSENCRYPT_PRODUCTION
79 }
80 }
81
82 pub async fn init_account(&self) -> Result<(), AcmeError> {
91 if let Some(creds_json) = self.storage.load_credentials_json()? {
93 info!("Loading existing ACME account from storage");
94
95 let credentials: instant_acme::AccountCredentials =
97 serde_json::from_str(&creds_json).map_err(|e| {
98 AcmeError::AccountCreation(format!("Failed to deserialize credentials: {}", e))
99 })?;
100
101 let account = Account::from_credentials(credentials)
103 .await
104 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?;
105
106 *self.account.write().await = Some(account);
107 info!("ACME account loaded successfully");
108 return Ok(());
109 }
110
111 info!(
113 email = %self.config.email,
114 staging = self.config.staging,
115 "Creating new ACME account"
116 );
117
118 let directory = if self.config.staging {
119 LetsEncrypt::Staging
120 } else {
121 LetsEncrypt::Production
122 };
123
124 let (account, credentials) = Account::create(
125 &NewAccount {
126 contact: &[&format!("mailto:{}", self.config.email)],
127 terms_of_service_agreed: true,
128 only_return_existing: false,
129 },
130 directory.url(),
131 None,
132 )
133 .await
134 .map_err(|e| AcmeError::AccountCreation(e.to_string()))?;
135
136 let creds_json = serde_json::to_string_pretty(&credentials).map_err(|e| {
138 AcmeError::AccountCreation(format!("Failed to serialize credentials: {}", e))
139 })?;
140 self.storage.save_credentials_json(&creds_json)?;
141
142 *self.account.write().await = Some(account);
143 info!("ACME account created successfully");
144
145 Ok(())
146 }
147
148 pub async fn create_order(&self) -> Result<(Order, Vec<ChallengeInfo>), AcmeError> {
158 let account_guard = self.account.read().await;
159 let account = account_guard
160 .as_ref()
161 .ok_or(AcmeError::NoAccount)?;
162
163 let identifiers: Vec<Identifier> = self
165 .config
166 .domains
167 .iter()
168 .map(|d: &String| Identifier::Dns(d.clone()))
169 .collect();
170
171 info!(domains = ?self.config.domains, "Creating certificate order");
172
173 let mut order = account
175 .new_order(&NewOrder {
176 identifiers: &identifiers,
177 })
178 .await
179 .map_err(|e| AcmeError::OrderCreation(e.to_string()))?;
180
181 let authorizations = order
183 .authorizations()
184 .await
185 .map_err(|e| AcmeError::OrderCreation(format!("Failed to get authorizations: {}", e)))?;
186
187 let mut challenges = Vec::new();
188
189 for auth in authorizations {
190 let domain = match &auth.identifier {
191 Identifier::Dns(domain) => domain.clone(),
192 };
193
194 debug!(domain = %domain, status = ?auth.status, "Processing authorization");
195
196 if auth.status == AuthorizationStatus::Valid {
198 debug!(domain = %domain, "Authorization already valid");
199 continue;
200 }
201
202 let http01_challenge = auth
204 .challenges
205 .iter()
206 .find(|c| c.r#type == ChallengeType::Http01)
207 .ok_or_else(|| AcmeError::NoHttp01Challenge(domain.clone()))?;
208
209 let key_authorization = order.key_authorization(http01_challenge);
210
211 challenges.push(ChallengeInfo {
212 domain,
213 token: http01_challenge.token.clone(),
214 key_authorization: key_authorization.as_str().to_string(),
215 url: http01_challenge.url.clone(),
216 });
217 }
218
219 Ok((order, challenges))
220 }
221
222 pub async fn validate_challenge(
229 &self,
230 order: &mut Order,
231 challenge_url: &str,
232 ) -> Result<(), AcmeError> {
233 debug!(challenge_url = %challenge_url, "Setting challenge ready");
234
235 order
236 .set_challenge_ready(challenge_url)
237 .await
238 .map_err(|e| AcmeError::ChallengeValidation {
239 domain: "unknown".to_string(),
240 message: e.to_string(),
241 })?;
242
243 Ok(())
244 }
245
246 pub async fn wait_for_order_ready(&self, order: &mut Order) -> Result<(), AcmeError> {
250 let deadline = tokio::time::Instant::now() + CHALLENGE_TIMEOUT;
251
252 loop {
253 let state = order
254 .refresh()
255 .await
256 .map_err(|e| AcmeError::OrderCreation(format!("Failed to refresh order: {}", e)))?;
257
258 match state.status {
259 OrderStatus::Ready => {
260 info!("Order is ready for finalization");
261 return Ok(());
262 }
263 OrderStatus::Invalid => {
264 error!("Order became invalid");
265 return Err(AcmeError::OrderCreation("Order became invalid".to_string()));
266 }
267 OrderStatus::Valid => {
268 info!("Order is already valid (certificate issued)");
269 return Ok(());
270 }
271 OrderStatus::Pending | OrderStatus::Processing => {
272 if tokio::time::Instant::now() > deadline {
273 return Err(AcmeError::Timeout(
274 "Timed out waiting for order to become ready".to_string(),
275 ));
276 }
277 trace!(status = ?state.status, "Order not ready yet, waiting...");
278 tokio::time::sleep(Duration::from_secs(2)).await;
279 }
280 }
281 }
282 }
283
284 pub async fn finalize_order(
293 &self,
294 order: &mut Order,
295 ) -> Result<(String, String, DateTime<Utc>), AcmeError> {
296 info!("Finalizing certificate order");
297
298 let cert_key = rcgen::KeyPair::generate()
300 .map_err(|e| AcmeError::Finalization(format!("Failed to generate key: {}", e)))?;
301
302 let params = rcgen::CertificateParams::new(self.config.domains.clone())
304 .map_err(|e| AcmeError::Finalization(format!("Failed to create CSR params: {}", e)))?;
305
306 let csr_request = params
308 .serialize_request(&cert_key)
309 .map_err(|e| AcmeError::Finalization(format!("Failed to serialize CSR: {}", e)))?;
310 let csr = csr_request.der().to_vec();
311
312 order
314 .finalize(&csr)
315 .await
316 .map_err(|e| AcmeError::Finalization(format!("Failed to finalize order: {}", e)))?;
317
318 let deadline = tokio::time::Instant::now() + DEFAULT_TIMEOUT;
320 let cert_chain = loop {
321 let state = order.refresh().await.map_err(|e| {
322 AcmeError::Finalization(format!("Failed to refresh order: {}", e))
323 })?;
324
325 match state.status {
326 OrderStatus::Valid => {
327 let cert_chain = order.certificate().await.map_err(|e| {
328 AcmeError::Finalization(format!("Failed to get certificate: {}", e))
329 })?;
330 break cert_chain.ok_or_else(|| {
331 AcmeError::Finalization("No certificate in response".to_string())
332 })?;
333 }
334 OrderStatus::Invalid => {
335 return Err(AcmeError::Finalization("Order became invalid".to_string()));
336 }
337 _ => {
338 if tokio::time::Instant::now() > deadline {
339 return Err(AcmeError::Timeout(
340 "Timed out waiting for certificate".to_string(),
341 ));
342 }
343 tokio::time::sleep(Duration::from_secs(1)).await;
344 }
345 }
346 };
347
348 let key_pem = cert_key.serialize_pem();
350
351 let expiry = parse_certificate_expiry(&cert_chain)?;
353
354 info!(
355 domains = ?self.config.domains,
356 expires = %expiry,
357 "Certificate issued successfully"
358 );
359
360 Ok((cert_chain, key_pem, expiry))
361 }
362
363 pub fn needs_renewal(&self, domain: &str) -> Result<bool, AcmeError> {
365 Ok(self
366 .storage
367 .needs_renewal(domain, self.config.renew_before_days)?)
368 }
369}
370
371#[derive(Debug, Clone)]
373pub struct ChallengeInfo {
374 pub domain: String,
376 pub token: String,
378 pub key_authorization: String,
380 pub url: String,
382}
383
384fn parse_certificate_expiry(cert_pem: &str) -> Result<DateTime<Utc>, AcmeError> {
386 use x509_parser::prelude::*;
387
388 let (_, pem) = pem::parse_x509_pem(cert_pem.as_bytes())
390 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse PEM: {}", e)))?;
391
392 let (_, cert) = X509Certificate::from_der(&pem.contents)
394 .map_err(|e| AcmeError::CertificateParse(format!("Failed to parse certificate: {}", e)))?;
395
396 let not_after = cert.validity().not_after;
398 let timestamp = not_after.timestamp();
399
400 DateTime::from_timestamp(timestamp, 0)
401 .ok_or_else(|| AcmeError::CertificateParse("Invalid expiry timestamp".to_string()))
402}
403
404impl std::fmt::Debug for AcmeClient {
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 f.debug_struct("AcmeClient")
407 .field("config", &self.config)
408 .field("has_account", &self.account.try_read().map(|a| a.is_some()).unwrap_or(false))
409 .finish()
410 }
411}