1use std::sync::Arc;
11
12use async_trait::async_trait;
13use base64::Engine;
14use instant_acme::{
15 Account, AccountCredentials, AuthorizationStatus, ChallengeType, ExternalAccountKey,
16 Identifier, NewAccount, NewOrder, OrderStatus, RetryPolicy,
17};
18use tracing::{debug, info};
19
20use super::storage::CertStorage;
21use super::AcmeError;
22use crate::config::AcmeConfig;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct PendingChallenge {
26 pub domain: String,
27 pub key_auth: String,
29}
30
31#[async_trait]
32pub trait AcmeOrder: Send {
33 async fn pending_challenges(&mut self) -> Result<Vec<PendingChallenge>, AcmeError>;
35 async fn mark_ready(&mut self, domain: &str) -> Result<(), AcmeError>;
37 async fn wait_ready(&mut self) -> Result<(), AcmeError>;
39 async fn finalize(&mut self, csr_der: &[u8]) -> Result<String, AcmeError>;
41}
42
43#[async_trait]
44pub trait AcmeClient: Send + Sync {
45 async fn new_order(&self, domains: &[String]) -> Result<Box<dyn AcmeOrder>, AcmeError>;
46 async fn renewal_window(&self, leaf_der: &[u8]) -> Result<Option<(i64, i64)>, AcmeError>;
48}
49
50#[async_trait]
51pub trait AcmeClientFactory: Send + Sync {
52 async fn connect(&self) -> Result<Arc<dyn AcmeClient>, AcmeError>;
53}
54
55pub fn decode_hmac_key(s: &str) -> Result<Vec<u8>, AcmeError> {
57 let s = s.trim();
58 base64::engine::general_purpose::URL_SAFE_NO_PAD
59 .decode(s)
60 .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(s))
61 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(s))
62 .map_err(|e| AcmeError::Config(format!("acme.eab.hmac_key is not base64: {e}")))
63}
64
65fn proto(e: instant_acme::Error) -> AcmeError {
66 AcmeError::Protocol(e.to_string())
67}
68
69pub struct InstantAcmeFactory {
70 cfg: AcmeConfig,
71 storage: Arc<dyn CertStorage>,
72}
73
74impl InstantAcmeFactory {
75 pub fn new(cfg: AcmeConfig, storage: Arc<dyn CertStorage>) -> Self {
76 Self { cfg, storage }
77 }
78
79 fn builder(&self) -> Result<instant_acme::AccountBuilder, AcmeError> {
80 match &self.cfg.directory_ca_path {
81 Some(path) => Account::builder_with_root(path).map_err(proto),
82 None => Account::builder().map_err(proto),
83 }
84 }
85}
86
87#[async_trait]
88impl AcmeClientFactory for InstantAcmeFactory {
89 async fn connect(&self) -> Result<Arc<dyn AcmeClient>, AcmeError> {
91 if let Some(bytes) = self.storage.load_account().await? {
92 let creds: AccountCredentials = serde_json::from_slice(&bytes).map_err(|e| {
93 AcmeError::Storage(format!("stored account credentials are corrupt: {e}"))
94 })?;
95 let account = self
96 .builder()?
97 .from_credentials(creds)
98 .await
99 .map_err(proto)?;
100 debug!("acme: using stored account {}", account.id());
101 return Ok(Arc::new(InstantAcmeClient { account }));
102 }
103 let contacts: Vec<&str> = self.cfg.contact.iter().map(String::as_str).collect();
104 let eab = match &self.cfg.eab {
105 Some(e) => Some(ExternalAccountKey::new(
106 e.key_id.clone(),
107 &decode_hmac_key(&e.hmac_key)?,
108 )),
109 None => None,
110 };
111 let (account, creds) = self
112 .builder()?
113 .create(
114 &NewAccount {
115 contact: &contacts,
116 terms_of_service_agreed: self.cfg.terms_of_service_agreed,
117 only_return_existing: false,
118 },
119 self.cfg.directory_url.clone(),
120 eab.as_ref(),
121 )
122 .await
123 .map_err(proto)?;
124 let bytes = serde_json::to_vec(&creds)
125 .map_err(|e| AcmeError::Storage(format!("serialize account credentials: {e}")))?;
126 self.storage.save_account(&bytes).await?;
127 info!(
128 "acme: registered account {} at {}",
129 account.id(),
130 self.cfg.directory_url
131 );
132 Ok(Arc::new(InstantAcmeClient { account }))
133 }
134}
135
136pub struct InstantAcmeClient {
137 account: Account,
138}
139
140#[async_trait]
141impl AcmeClient for InstantAcmeClient {
142 async fn new_order(&self, domains: &[String]) -> Result<Box<dyn AcmeOrder>, AcmeError> {
143 let identifiers: Vec<Identifier> = domains.iter().cloned().map(Identifier::Dns).collect();
144 let order = self
145 .account
146 .new_order(&NewOrder::new(&identifiers))
147 .await
148 .map_err(proto)?;
149 Ok(Box::new(InstantAcmeOrder { order }))
150 }
151
152 async fn renewal_window(&self, leaf_der: &[u8]) -> Result<Option<(i64, i64)>, AcmeError> {
153 let der = rustls::pki_types::CertificateDer::from(leaf_der.to_vec());
154 let id = match instant_acme::CertificateIdentifier::try_from(&der) {
155 Ok(id) => id,
156 Err(e) => {
157 debug!("acme: no ARI identifier for certificate: {e}");
158 return Ok(None);
159 }
160 };
161 match self.account.renewal_info(&id).await {
162 Ok((info, _retry_after)) => Ok(Some((
163 info.suggested_window.start.unix_timestamp(),
164 info.suggested_window.end.unix_timestamp(),
165 ))),
166 Err(e) => {
169 debug!("acme: renewal_info unavailable: {e}");
170 Ok(None)
171 }
172 }
173 }
174}
175
176struct InstantAcmeOrder {
177 order: instant_acme::Order,
178}
179
180fn dns_name(ident: &Identifier) -> Result<String, AcmeError> {
181 match ident {
182 Identifier::Dns(d) => Ok(d.clone()),
183 other => Err(AcmeError::Protocol(format!(
184 "unsupported identifier {other:?}"
185 ))),
186 }
187}
188
189#[async_trait]
190impl AcmeOrder for InstantAcmeOrder {
191 async fn pending_challenges(&mut self) -> Result<Vec<PendingChallenge>, AcmeError> {
192 let mut out = Vec::new();
193 let mut authorizations = self.order.authorizations();
194 while let Some(result) = authorizations.next().await {
195 let mut authz = result.map_err(proto)?;
196 let domain = dns_name(authz.identifier().identifier)?;
197 match authz.status {
198 AuthorizationStatus::Pending => {}
199 AuthorizationStatus::Valid => continue,
200 other => {
201 return Err(AcmeError::Protocol(format!(
202 "authorization for {domain} is {other:?}"
203 )))
204 }
205 }
206 let challenge = authz.challenge(ChallengeType::TlsAlpn01).ok_or_else(|| {
207 AcmeError::Protocol(format!("CA offers no tls-alpn-01 challenge for {domain}"))
208 })?;
209 out.push(PendingChallenge {
210 domain,
211 key_auth: challenge.key_authorization().as_str().to_string(),
212 });
213 }
214 Ok(out)
215 }
216
217 async fn mark_ready(&mut self, domain: &str) -> Result<(), AcmeError> {
218 let mut authorizations = self.order.authorizations();
219 while let Some(result) = authorizations.next().await {
220 let mut authz = result.map_err(proto)?;
221 if dns_name(authz.identifier().identifier)? != domain {
222 continue;
223 }
224 let mut challenge = authz.challenge(ChallengeType::TlsAlpn01).ok_or_else(|| {
225 AcmeError::Protocol(format!("CA offers no tls-alpn-01 challenge for {domain}"))
226 })?;
227 return challenge.set_ready().await.map_err(proto);
228 }
229 Err(AcmeError::Protocol(format!(
230 "order has no authorization for {domain}"
231 )))
232 }
233
234 async fn wait_ready(&mut self) -> Result<(), AcmeError> {
235 let status = self
236 .order
237 .poll_ready(&RetryPolicy::default())
238 .await
239 .map_err(proto)?;
240 if matches!(status, OrderStatus::Ready | OrderStatus::Valid) {
241 Ok(())
242 } else {
243 let detail = self
244 .order
245 .state()
246 .error
247 .as_ref()
248 .map(|p| p.to_string())
249 .unwrap_or_default();
250 Err(AcmeError::Protocol(format!(
251 "order is {status:?}: {detail}"
252 )))
253 }
254 }
255
256 async fn finalize(&mut self, csr_der: &[u8]) -> Result<String, AcmeError> {
257 self.order.finalize_csr(csr_der).await.map_err(proto)?;
258 self.order
259 .poll_certificate(&RetryPolicy::default())
260 .await
261 .map_err(proto)
262 }
263}
264
265#[cfg(test)]
270pub(crate) mod mock {
271 use super::*;
272 use std::sync::atomic::{AtomicUsize, Ordering};
273 use std::sync::Mutex;
274
275 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
276 pub enum MockStep {
277 NewOrder,
278 WaitReady,
279 Finalize,
280 }
281
282 #[derive(Debug, Clone)]
283 pub struct MockBehavior {
284 pub fail_step: Option<MockStep>,
285 pub ari: Option<(i64, i64)>,
286 pub wrong_key_chain: bool,
288 pub extra_pending_domain: Option<String>,
291 pub validity_secs: i64,
292 }
293
294 impl Default for MockBehavior {
295 fn default() -> Self {
296 Self {
297 fail_step: None,
298 ari: None,
299 wrong_key_chain: false,
300 extra_pending_domain: None,
301 validity_secs: 90 * 86_400,
302 }
303 }
304 }
305
306 pub struct MockAcmeClient {
307 ca_params: rcgen::CertificateParams,
308 ca_key: rcgen::KeyPair,
309 ca_pem: String,
310 pub behavior: Mutex<MockBehavior>,
311 orders: AtomicUsize,
312 }
313
314 impl MockAcmeClient {
315 pub fn new(behavior: MockBehavior) -> Arc<Self> {
316 let mut ca_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
317 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
318 ca_params
319 .distinguished_name
320 .push(rcgen::DnType::CommonName, "Mock ACME CA");
321 let ca_key = rcgen::KeyPair::generate().unwrap();
322 let ca_pem = ca_params.self_signed(&ca_key).unwrap().pem();
323 Arc::new(Self {
324 ca_params,
325 ca_key,
326 ca_pem,
327 behavior: Mutex::new(behavior),
328 orders: AtomicUsize::new(0),
329 })
330 }
331
332 pub fn orders(&self) -> usize {
333 self.orders.load(Ordering::SeqCst)
334 }
335
336 pub fn set_behavior(&self, b: MockBehavior) {
337 *self.behavior.lock().unwrap() = b;
338 }
339
340 fn behavior(&self) -> MockBehavior {
341 self.behavior.lock().unwrap().clone()
342 }
343
344 fn sign(&self, csr_der: &[u8], domains: &[String]) -> Result<String, AcmeError> {
345 let b = self.behavior();
346 let csr = rcgen::CertificateSigningRequestParams::from_der(
347 &rustls::pki_types::CertificateSigningRequestDer::from(csr_der.to_vec()),
348 )
349 .map_err(|e| AcmeError::Protocol(format!("mock: bad CSR: {e}")))?;
350 let mut params = rcgen::CertificateParams::new(domains.to_vec()).unwrap();
351 let now = time::OffsetDateTime::now_utc();
352 params.not_before = now - time::Duration::minutes(1);
353 params.not_after = now + time::Duration::seconds(b.validity_secs);
354 let issuer = rcgen::Issuer::from_params(&self.ca_params, &self.ca_key);
355 let leaf = if b.wrong_key_chain {
356 let other = rcgen::KeyPair::generate().unwrap();
357 params.signed_by(&other, &issuer).unwrap()
358 } else {
359 rcgen::CertificateSigningRequestParams {
361 params,
362 public_key: csr.public_key,
363 }
364 .signed_by(&issuer)
365 .unwrap()
366 };
367 Ok(format!("{}{}", leaf.pem(), self.ca_pem))
368 }
369 }
370
371 pub struct MockOrder {
372 client: Arc<MockAcmeClient>,
373 domains: Vec<String>,
374 ready: Vec<String>,
375 }
376
377 #[async_trait]
380 impl AcmeClient for Arc<MockAcmeClient> {
381 async fn new_order(&self, domains: &[String]) -> Result<Box<dyn AcmeOrder>, AcmeError> {
382 self.orders.fetch_add(1, Ordering::SeqCst);
383 if self.behavior().fail_step == Some(MockStep::NewOrder) {
384 return Err(AcmeError::Protocol(
385 "mock: newOrder rejected (rateLimited)".into(),
386 ));
387 }
388 Ok(Box::new(MockOrder {
389 client: self.clone(),
390 domains: domains.to_vec(),
391 ready: Vec::new(),
392 }))
393 }
394
395 async fn renewal_window(&self, _leaf_der: &[u8]) -> Result<Option<(i64, i64)>, AcmeError> {
396 Ok(self.behavior().ari)
397 }
398 }
399
400 #[async_trait]
401 impl AcmeOrder for MockOrder {
402 async fn pending_challenges(&mut self) -> Result<Vec<PendingChallenge>, AcmeError> {
403 let mut out: Vec<PendingChallenge> = self
404 .domains
405 .iter()
406 .map(|d| PendingChallenge {
407 domain: d.clone(),
408 key_auth: format!("tok-{d}.mockthumb"),
409 })
410 .collect();
411 if let Some(extra) = self.client.behavior().extra_pending_domain {
412 out.push(PendingChallenge {
413 key_auth: format!("tok-{extra}.mockthumb"),
414 domain: extra,
415 });
416 }
417 Ok(out)
418 }
419
420 async fn mark_ready(&mut self, domain: &str) -> Result<(), AcmeError> {
421 if !self.domains.iter().any(|d| d == domain) {
422 return Err(AcmeError::Protocol(format!(
423 "mock: no authorization for {domain}"
424 )));
425 }
426 self.ready.push(domain.to_string());
427 Ok(())
428 }
429
430 async fn wait_ready(&mut self) -> Result<(), AcmeError> {
431 if self.client.behavior().fail_step == Some(MockStep::WaitReady) {
432 return Err(AcmeError::Protocol(
433 "mock: order is Invalid: challenge failed".into(),
434 ));
435 }
436 if self.domains.iter().all(|d| self.ready.contains(d)) {
437 Ok(())
438 } else {
439 Err(AcmeError::Protocol(
440 "mock: order is Invalid: authorization pending".into(),
441 ))
442 }
443 }
444
445 async fn finalize(&mut self, csr_der: &[u8]) -> Result<String, AcmeError> {
446 if self.client.behavior().fail_step == Some(MockStep::Finalize) {
447 return Err(AcmeError::Protocol(
448 "mock: finalize rejected (badCSR)".into(),
449 ));
450 }
451 self.client.sign(csr_der, &self.domains)
452 }
453 }
454
455 pub struct MockFactory {
457 pub client: Arc<MockAcmeClient>,
458 pub fail_connects: AtomicUsize,
459 }
460
461 impl MockFactory {
462 pub fn new(client: Arc<MockAcmeClient>) -> Arc<Self> {
463 Arc::new(Self {
464 client,
465 fail_connects: AtomicUsize::new(0),
466 })
467 }
468 }
469
470 #[async_trait]
471 impl AcmeClientFactory for MockFactory {
472 async fn connect(&self) -> Result<Arc<dyn AcmeClient>, AcmeError> {
473 if self.fail_connects.load(Ordering::SeqCst) > 0 {
474 self.fail_connects.fetch_sub(1, Ordering::SeqCst);
475 return Err(AcmeError::Protocol("mock: directory unreachable".into()));
476 }
477 Ok(Arc::new(self.client.clone()) as Arc<dyn AcmeClient>)
478 }
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn decode_hmac_key_accepts_url_safe_and_standard_base64() {
488 assert_eq!(decode_hmac_key("AQID").unwrap(), vec![1, 2, 3]);
489 assert_eq!(decode_hmac_key("_-8").unwrap(), vec![0xff, 0xef]);
490 assert_eq!(decode_hmac_key("/+8=").unwrap(), vec![0xff, 0xef]);
491 assert!(decode_hmac_key("not base64!").is_err());
492 }
493
494 #[tokio::test]
495 async fn mock_client_issues_a_chain_for_the_csr_key() {
496 let client = mock::MockAcmeClient::new(mock::MockBehavior::default());
497 let mut order = client
498 .new_order(&["a.example.com".into(), "b.example.com".into()])
499 .await
500 .unwrap();
501 let pending = order.pending_challenges().await.unwrap();
502 assert_eq!(pending.len(), 2);
503 assert!(
504 order.wait_ready().await.is_err(),
505 "not all challenges marked ready"
506 );
507 for p in &pending {
508 order.mark_ready(&p.domain).await.unwrap();
509 }
510 order.wait_ready().await.unwrap();
511 let key = rcgen::KeyPair::generate().unwrap();
512 let csr = rcgen::CertificateParams::new(vec![
513 "a.example.com".to_string(),
514 "b.example.com".to_string(),
515 ])
516 .unwrap()
517 .serialize_request(&key)
518 .unwrap();
519 let chain = order.finalize(csr.der()).await.unwrap();
520 let (ck, leaf) = crate::acme::load_certified_key(&chain, &key.serialize_pem()).unwrap();
521 assert!(ck.end_entity_cert().is_ok());
522 let mut sans = crate::acme::leaf_dns_sans(&leaf).unwrap();
523 sans.sort();
524 assert_eq!(
525 sans,
526 vec!["a.example.com".to_string(), "b.example.com".to_string()]
527 );
528 assert!(crate::acme::parse_cert_meta(&leaf)
529 .unwrap()
530 .issuer
531 .contains("Mock ACME CA"));
532 assert_eq!(client.orders(), 1);
533 }
534
535 #[tokio::test]
536 async fn mock_client_honors_failure_script_and_ari() {
537 let client = mock::MockAcmeClient::new(mock::MockBehavior {
538 fail_step: Some(mock::MockStep::Finalize),
539 ari: Some((100, 200)),
540 ..Default::default()
541 });
542 let mut order = client.new_order(&["a.example.com".into()]).await.unwrap();
543 for p in order.pending_challenges().await.unwrap() {
544 order.mark_ready(&p.domain).await.unwrap();
545 }
546 order.wait_ready().await.unwrap();
547 assert!(order.finalize(b"irrelevant").await.is_err());
548 assert_eq!(
549 client.renewal_window(b"any").await.unwrap(),
550 Some((100, 200))
551 );
552 }
553}