1use std::collections::HashMap;
12use std::sync::Arc;
13
14use arc_swap::ArcSwap;
15use rustls::pki_types::pem::PemObject;
16use rustls::pki_types::{CertificateDer, PrivateKeyDer};
17use rustls::sign::CertifiedKey;
18use serde::{Deserialize, Serialize};
19use tracing::{info, warn};
20
21use crate::config::{AcmeConfig, AcmeStorageConfig, TlsConfig};
22use crate::metrics::GatewayMetrics;
23use crate::plugins::resources::PluginResources;
24use challenge::TlsAlpnSolver;
25use manager::{ManagedSlot, Manager, ManagerConfig};
26use storage::CertStorage;
27
28pub mod challenge;
29pub mod client;
30#[cfg(test)]
31mod live_tests;
32pub mod manager;
33pub mod metrics;
34pub mod order;
35pub mod storage;
36
37#[derive(Debug, thiserror::Error)]
38pub enum AcmeError {
39 #[error("acme config: {0}")]
40 Config(String),
41 #[error("acme storage: {0}")]
42 Storage(String),
43 #[error("acme protocol: {0}")]
44 Protocol(String),
45 #[error("acme certificate: {0}")]
46 Certificate(String),
47 #[error("acme crypto: {0}")]
48 Crypto(String),
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash)]
53pub struct CertId(String);
54
55impl CertId {
56 pub fn from_domains(domains: &[String]) -> Result<(CertId, Vec<String>), AcmeError> {
59 let mut norm = domains
60 .iter()
61 .map(|d| crate::config::normalize_domain(d).map_err(AcmeError::Config))
62 .collect::<Result<Vec<_>, _>>()?;
63 norm.sort();
64 norm.dedup();
65 if norm.is_empty() {
66 return Err(AcmeError::Config(
67 "a certificate needs at least one domain".into(),
68 ));
69 }
70 Ok((CertId(norm.join(",")), norm))
71 }
72
73 pub fn as_str(&self) -> &str {
74 &self.0
75 }
76}
77
78impl std::fmt::Display for CertId {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.write_str(&self.0)
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
85#[serde(rename_all = "lowercase")]
86pub enum CertState {
87 Placeholder,
89 Issued,
90 Renewing,
91 Failed,
93}
94
95impl CertState {
96 pub const ALL: [CertState; 4] = [
97 CertState::Placeholder,
98 CertState::Issued,
99 CertState::Renewing,
100 CertState::Failed,
101 ];
102
103 pub fn as_str(&self) -> &'static str {
104 match self {
105 CertState::Placeholder => "placeholder",
106 CertState::Issued => "issued",
107 CertState::Renewing => "renewing",
108 CertState::Failed => "failed",
109 }
110 }
111}
112
113#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
115pub struct CertMeta {
116 pub not_before: i64,
117 pub not_after: i64,
118 pub issuer: String,
119 pub serial: String,
120 #[serde(default)]
121 pub next_renewal_at: Option<i64>,
122 #[serde(default)]
123 pub last_attempt_at: Option<i64>,
124 #[serde(default)]
125 pub last_error: Option<String>,
126}
127
128#[derive(Debug, Clone)]
130pub struct ManagedCert {
131 pub key: Arc<CertifiedKey>,
132 pub leaf_der: Vec<u8>,
133 pub state: CertState,
134 pub meta: CertMeta,
135 pub domains: Vec<String>,
136}
137
138pub type ManagedCerts = Arc<ArcSwap<HashMap<String, ManagedCert>>>;
140
141pub fn new_managed_certs() -> ManagedCerts {
142 Arc::new(ArcSwap::from_pointee(HashMap::new()))
143}
144
145pub fn publish(certs: &ManagedCerts, id: &CertId, cert: ManagedCert) {
148 let mut map: HashMap<String, ManagedCert> = (**certs.load()).clone();
149 map.insert(id.as_str().to_string(), cert);
150 certs.store(Arc::new(map));
151}
152
153pub fn update(certs: &ManagedCerts, id: &CertId, f: impl FnOnce(&mut ManagedCert)) {
155 let mut map: HashMap<String, ManagedCert> = (**certs.load()).clone();
156 if let Some(c) = map.get_mut(id.as_str()) {
157 f(c);
158 certs.store(Arc::new(map));
159 }
160}
161
162#[derive(Clone, PartialEq, Deserialize)]
171pub struct StoredCert {
172 pub chain_pem: String,
173 pub key_pem: String,
174 pub issued_at: i64,
175}
176
177impl std::fmt::Debug for StoredCert {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 f.debug_struct("StoredCert")
180 .field("chain_pem", &self.chain_pem)
181 .field("key_pem", &"<redacted>")
182 .field("issued_at", &self.issued_at)
183 .finish()
184 }
185}
186
187pub fn now_unix() -> i64 {
188 std::time::SystemTime::now()
189 .duration_since(std::time::UNIX_EPOCH)
190 .map(|d| d.as_secs() as i64)
191 .unwrap_or(0)
192}
193
194fn provider() -> rustls::crypto::CryptoProvider {
195 rustls::crypto::ring::default_provider()
196}
197
198pub fn placeholder_cert(domains: &[String]) -> Result<(Arc<CertifiedKey>, Vec<u8>), AcmeError> {
201 let first = domains
202 .first()
203 .ok_or_else(|| AcmeError::Config("placeholder needs a domain".into()))?;
204 let mut params = rcgen::CertificateParams::new(domains.to_vec())
205 .map_err(|e| AcmeError::Crypto(e.to_string()))?;
206 params
207 .distinguished_name
208 .push(rcgen::DnType::CommonName, first.as_str());
209 let now = time::OffsetDateTime::now_utc();
210 params.not_before = now - time::Duration::minutes(5);
211 params.not_after = now + time::Duration::hours(1);
212 let key = rcgen::KeyPair::generate().map_err(|e| AcmeError::Crypto(e.to_string()))?;
213 let cert = params
214 .self_signed(&key)
215 .map_err(|e| AcmeError::Crypto(e.to_string()))?;
216 load_certified_key(&cert.pem(), &key.serialize_pem())
217}
218
219pub fn load_certified_key(
222 chain_pem: &str,
223 key_pem: &str,
224) -> Result<(Arc<CertifiedKey>, Vec<u8>), AcmeError> {
225 let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(chain_pem.as_bytes())
226 .collect::<Result<Vec<_>, _>>()
227 .map_err(|e| AcmeError::Certificate(format!("chain PEM: {e}")))?;
228 if chain.is_empty() {
229 return Err(AcmeError::Certificate(
230 "chain PEM contains no certificates".into(),
231 ));
232 }
233 let key = PrivateKeyDer::from_pem_slice(key_pem.as_bytes())
234 .map_err(|e| AcmeError::Certificate(format!("key PEM: {e}")))?;
235 let leaf = chain[0].as_ref().to_vec();
236 let ck = CertifiedKey::from_der(chain, key, &provider())
237 .map_err(|e| AcmeError::Certificate(format!("certificate/key: {e}")))?;
238 ck.keys_match().map_err(|e| {
239 AcmeError::Certificate(format!("private key does not match certificate: {e}"))
240 })?;
241 Ok((Arc::new(ck), leaf))
242}
243
244fn parse_leaf(leaf_der: &[u8]) -> Result<x509_parser::prelude::X509Certificate<'_>, AcmeError> {
245 use x509_parser::prelude::*;
246 X509Certificate::from_der(leaf_der)
247 .map(|(_, c)| c)
248 .map_err(|e| AcmeError::Certificate(format!("leaf DER: {e}")))
249}
250
251pub fn parse_cert_meta(leaf_der: &[u8]) -> Result<CertMeta, AcmeError> {
252 let cert = parse_leaf(leaf_der)?;
253 Ok(CertMeta {
254 not_before: cert.validity().not_before.timestamp(),
255 not_after: cert.validity().not_after.timestamp(),
256 issuer: cert.issuer().to_string(),
257 serial: cert.raw_serial_as_string(),
258 next_renewal_at: None,
259 last_attempt_at: None,
260 last_error: None,
261 })
262}
263
264pub fn leaf_dns_sans(leaf_der: &[u8]) -> Result<Vec<String>, AcmeError> {
266 use x509_parser::prelude::*;
267 let cert = parse_leaf(leaf_der)?;
268 let mut out = Vec::new();
269 if let Ok(Some(san)) = cert.subject_alternative_name() {
270 for name in &san.value.general_names {
271 if let GeneralName::DNSName(d) = name {
272 out.push(d.to_ascii_lowercase());
273 }
274 }
275 }
276 Ok(out)
277}
278
279#[cfg(test)]
286pub fn leaf_spki(leaf_der: &[u8]) -> Result<Vec<u8>, AcmeError> {
287 let cert = parse_leaf(leaf_der)?;
288 Ok(cert.tbs_certificate.subject_pki.raw.to_vec())
289}
290
291pub struct AcmeRuntime {
293 pub certs: ManagedCerts,
294 pub solver: Arc<TlsAlpnSolver>,
295 pub manager: Arc<Manager>,
296 pub storage_label: String,
297}
298
299impl AcmeRuntime {
300 pub fn hooks(&self) -> crate::server::tls::AcmeHooks {
301 crate::server::tls::AcmeHooks {
302 certs: self.certs.clone(),
303 solver: self.solver.clone(),
304 }
305 }
306
307 pub fn placeholder_ids(&self) -> Vec<String> {
309 let mut v: Vec<String> = self
310 .certs
311 .load()
312 .iter()
313 .filter(|(_, c)| c.state == CertState::Placeholder)
314 .map(|(id, _)| id.clone())
315 .collect();
316 v.sort();
317 v
318 }
319}
320
321pub fn build_storage(
327 cfg: &AcmeConfig,
328 resources: &Arc<PluginResources>,
329) -> Result<Arc<dyn CertStorage>, AcmeError> {
330 match &cfg.storage {
331 AcmeStorageConfig::Filesystem { dir } => Ok(Arc::new(storage::fs::FsCertStorage::new(dir))),
332 #[cfg(feature = "redis-store")]
333 AcmeStorageConfig::Store {
334 store,
335 encryption_key,
336 } => {
337 let storage =
338 storage::redis::RedisCertStorage::new(resources.clone(), store, encryption_key);
339 storage
340 .client()
341 .map_err(|e| AcmeError::Config(e.to_string()))?;
342 Ok(Arc::new(storage))
343 }
344 #[cfg(not(feature = "redis-store"))]
345 AcmeStorageConfig::Store { .. } => {
346 let _ = resources;
347 Err(AcmeError::Config(
348 "acme.storage.type: store needs the redis-store feature".into(),
349 ))
350 }
351 }
352}
353
354pub async fn start(
358 cfg: &AcmeConfig,
359 tls: &TlsConfig,
360 resources: &Arc<PluginResources>,
361 metrics: &GatewayMetrics,
362) -> Result<Arc<AcmeRuntime>, AcmeError> {
363 let storage = build_storage(cfg, resources)?;
364 let solver = TlsAlpnSolver::new(storage.clone());
365 let certs = new_managed_certs();
366 let acme_metrics = metrics::AcmeMetrics::register(&metrics.registry)
367 .map_err(|e| AcmeError::Config(format!("acme metrics: {e}")))?;
368 let now = now_unix();
369
370 let mut slots = Vec::new();
371 for domains in tls.managed_domains() {
372 let (id, domains) = CertId::from_domains(&domains)?;
373 if slots.iter().any(|s: &ManagedSlot| s.id == id) {
374 continue; }
376 let adopted = match storage.load_cert(&id).await {
377 Ok(Some(stored)) => {
378 match load_certified_key(&stored.chain_pem, &stored.key_pem)
379 .and_then(|(key, leaf)| parse_cert_meta(&leaf).map(|meta| (key, leaf, meta)))
380 {
381 Ok((key, leaf, meta)) if meta.not_after > now => {
382 info!(
383 "acme: loaded stored certificate for {} (expires {})",
384 id, meta.not_after
385 );
386 Some(ManagedCert {
387 key,
388 leaf_der: leaf,
389 state: CertState::Issued,
390 meta,
391 domains: domains.clone(),
392 })
393 }
394 Ok(_) => {
395 warn!(
396 "acme: stored certificate for {} is expired; serving a placeholder",
397 id
398 );
399 None
400 }
401 Err(e) => {
402 warn!("acme: stored certificate for {} is unusable ({}); serving a placeholder", id, e);
403 None
404 }
405 }
406 }
407 Ok(None) => None,
408 Err(e) => {
409 warn!(
410 "acme: reading stored certificate for {} failed ({}); serving a placeholder",
411 id, e
412 );
413 None
414 }
415 };
416 let cert = match adopted {
417 Some(c) => c,
418 None => {
419 let (key, leaf) = placeholder_cert(&domains)?;
420 warn!("acme: {} has no certificate yet — serving a self-signed placeholder until issuance succeeds", id);
421 let meta = parse_cert_meta(&leaf).unwrap_or_default();
427 ManagedCert {
428 key,
429 leaf_der: leaf,
430 state: CertState::Placeholder,
431 meta,
432 domains: domains.clone(),
433 }
434 }
435 };
436 let seeded_not_after = if cert.state == CertState::Placeholder {
441 0
442 } else {
443 cert.meta.not_after
444 };
445 acme_metrics.observe(id.as_str(), cert.state, seeded_not_after);
446 publish(&certs, &id, cert);
447 slots.push(ManagedSlot { id, domains });
448 }
449
450 let manager_cfg = ManagerConfig {
451 key_type: order::KeyType::parse(&cfg.key_type)?,
452 renew_before: cfg.renew_before_duration().map_err(AcmeError::Config)?,
453 ..ManagerConfig::default()
454 };
455 let factory = Arc::new(client::InstantAcmeFactory::new(
456 cfg.clone(),
457 storage.clone(),
458 ));
459 let manager = Manager::new(
460 manager_cfg,
461 factory,
462 storage.clone(),
463 solver.clone(),
464 certs.clone(),
465 slots,
466 Some(acme_metrics),
467 );
468 tokio::spawn(manager.clone().run());
469
470 Ok(Arc::new(AcmeRuntime {
471 certs,
472 solver,
473 manager,
474 storage_label: storage.label(),
475 }))
476}
477
478#[cfg(test)]
481pub(crate) mod testing {
482 use super::*;
483 use client::mock::{MockAcmeClient, MockBehavior, MockFactory};
484
485 pub fn runtime_with(certs: ManagedCerts, slots: Vec<ManagedSlot>) -> Arc<AcmeRuntime> {
486 let dir = std::env::temp_dir().join(format!(
487 "fb_acme_rt_{}_{}",
488 std::process::id(),
489 uuid::Uuid::new_v4().simple()
490 ));
491 let storage: Arc<dyn CertStorage> = Arc::new(storage::fs::FsCertStorage::new(dir));
492 let solver = TlsAlpnSolver::new(storage.clone());
493 let factory = MockFactory::new(MockAcmeClient::new(MockBehavior::default()));
494 let manager = Manager::new(
495 ManagerConfig::default(),
496 factory,
497 storage,
498 solver.clone(),
499 certs.clone(),
500 slots,
501 None,
502 );
503 Arc::new(AcmeRuntime {
504 certs,
505 solver,
506 manager,
507 storage_label: "filesystem".into(),
508 })
509 }
510
511 fn seeded(domains: &[&str], state: CertState) -> Arc<AcmeRuntime> {
512 let domains: Vec<String> = domains.iter().map(|d| d.to_string()).collect();
513 let (id, domains) = CertId::from_domains(&domains).unwrap();
514 let certs = new_managed_certs();
515 let (key, leaf) = if state == CertState::Placeholder {
516 placeholder_cert(&domains).unwrap()
517 } else {
518 let c = rcgen::generate_simple_self_signed(domains.clone()).unwrap();
519 load_certified_key(&c.cert.pem(), &c.signing_key.serialize_pem()).unwrap()
520 };
521 let meta = parse_cert_meta(&leaf).unwrap();
522 publish(
523 &certs,
524 &id,
525 ManagedCert {
526 key,
527 leaf_der: leaf,
528 state,
529 meta,
530 domains: domains.clone(),
531 },
532 );
533 runtime_with(certs, vec![ManagedSlot { id, domains }])
534 }
535
536 pub fn placeholder_runtime(domains: &[&str]) -> Arc<AcmeRuntime> {
537 seeded(domains, CertState::Placeholder)
538 }
539
540 pub fn issued_runtime(domains: &[&str]) -> Arc<AcmeRuntime> {
541 seeded(domains, CertState::Issued)
542 }
543}
544
545#[cfg(test)]
546mod tests {
547 use super::*;
548
549 fn s(v: &[&str]) -> Vec<String> {
550 v.iter().map(|x| x.to_string()).collect()
551 }
552
553 #[test]
554 fn cert_id_normalizes_sorts_and_dedups() {
555 let (id, domains) =
556 CertId::from_domains(&s(&["B.example.com", "a.example.com", "a.EXAMPLE.com"])).unwrap();
557 assert_eq!(id.as_str(), "a.example.com,b.example.com");
558 assert_eq!(domains, s(&["a.example.com", "b.example.com"]));
559 assert!(CertId::from_domains(&[]).is_err());
560 assert!(CertId::from_domains(&s(&["*.example.com"])).is_err());
561 }
562
563 #[test]
564 fn placeholder_is_self_signed_for_domains_and_short_lived() {
565 let (key, leaf) = placeholder_cert(&s(&["api.example.com", "www.example.com"])).unwrap();
566 assert!(key.end_entity_cert().is_ok());
567 let sans = leaf_dns_sans(&leaf).unwrap();
568 assert!(sans.contains(&"api.example.com".to_string()));
569 let meta = parse_cert_meta(&leaf).unwrap();
570 let now = now_unix();
571 assert!(
572 meta.not_after > now && meta.not_after <= now + 3_700,
573 "{:?}",
574 meta
575 );
576 assert!(meta.not_before <= now);
577 }
578
579 #[test]
580 fn load_certified_key_round_trips_and_rejects_foreign_key() {
581 let a = rcgen::generate_simple_self_signed(s(&["a.example.com"])).unwrap();
582 let b = rcgen::generate_simple_self_signed(s(&["b.example.com"])).unwrap();
583 let (ck, leaf) = load_certified_key(&a.cert.pem(), &a.signing_key.serialize_pem()).unwrap();
584 assert_eq!(ck.end_entity_cert().unwrap().as_ref(), leaf.as_slice());
585 assert!(!leaf_spki(&leaf).unwrap().is_empty());
586 let err = load_certified_key(&a.cert.pem(), &b.signing_key.serialize_pem()).unwrap_err();
587 assert!(matches!(err, AcmeError::Certificate(_)), "{err}");
588 assert!(load_certified_key("not pem", &a.signing_key.serialize_pem()).is_err());
589 }
590
591 #[test]
592 fn parse_cert_meta_reads_validity_issuer_serial() {
593 let a = rcgen::generate_simple_self_signed(s(&["a.example.com"])).unwrap();
594 let meta = parse_cert_meta(a.cert.der()).unwrap();
595 assert!(meta.not_after > meta.not_before);
596 assert!(!meta.serial.is_empty());
597 assert!(meta.issuer.contains("rcgen"), "{}", meta.issuer);
598 }
599
600 #[tokio::test]
601 async fn start_seeds_placeholders_then_reuses_a_stored_cert() {
602 let dir = std::env::temp_dir().join(format!("fb_acme_start_{}", std::process::id()));
603 let _ = std::fs::remove_dir_all(&dir);
604 let system: crate::config::SystemConfig = serde_yaml::from_str(&format!(
610 "acme:\n terms_of_service_agreed: true\n directory_url: https://127.0.0.1:1/directory\n storage:\n type: filesystem\n dir: {}\ntls:\n acme:\n domains: [s.example.com]\n",
611 dir.display().to_string().replace('\\', "/")
612 ))
613 .unwrap();
614 let cfg = system.acme.as_ref().unwrap();
615 let tls = system.tls.as_ref().unwrap();
616 let resources = crate::plugins::resources::PluginResources::new(None);
617 let metrics = crate::metrics::GatewayMetrics::new();
618
619 let rt = start(cfg, tls, &resources, &metrics).await.unwrap();
620 assert_eq!(rt.placeholder_ids(), vec!["s.example.com".to_string()]);
621 assert_eq!(rt.storage_label, "filesystem");
622 assert!(metrics.render().contains(
623 "featherbit_acme_cert_state{cert_id=\"s.example.com\",state=\"placeholder\"} 1"
624 ));
625 let seeded = rt.certs.load().get("s.example.com").unwrap().clone();
628 assert!(seeded.meta.not_after > now_unix(), "{:?}", seeded.meta);
629 assert!(metrics.render().contains(
630 "featherbit_acme_cert_not_after_timestamp_seconds{cert_id=\"s.example.com\"} 0"
631 ));
632
633 let issued = rcgen::generate_simple_self_signed(vec!["s.example.com".to_string()]).unwrap();
635 let storage = build_storage(cfg, &resources).unwrap();
636 let (id, _) = CertId::from_domains(&["s.example.com".into()]).unwrap();
637 storage
638 .save_cert(
639 &id,
640 &StoredCert {
641 chain_pem: issued.cert.pem(),
642 key_pem: issued.signing_key.serialize_pem(),
643 issued_at: now_unix(),
644 },
645 )
646 .await
647 .unwrap();
648 let rt2 = start(cfg, tls, &resources, &crate::metrics::GatewayMetrics::new())
649 .await
650 .unwrap();
651 assert!(rt2.placeholder_ids().is_empty());
652 assert_eq!(
653 rt2.certs.load().get("s.example.com").unwrap().state,
654 CertState::Issued
655 );
656 let _ = std::fs::remove_dir_all(dir);
657 }
658
659 #[test]
660 fn stored_cert_debug_redacts_the_private_key() {
661 let cert = StoredCert {
662 chain_pem: "-----BEGIN CERTIFICATE-----".into(),
663 key_pem: "-----BEGIN PRIVATE KEY-----
664SECRET"
665 .into(),
666 issued_at: 7,
667 };
668 let rendered = format!("{cert:?}");
669 assert!(!rendered.contains("SECRET"), "{rendered}");
670 assert!(rendered.contains("<redacted>"), "{rendered}");
671 assert!(rendered.contains("BEGIN CERTIFICATE"), "{rendered}");
672 }
673
674 #[test]
675 fn publish_and_update_swap_map_entries() {
676 let certs = new_managed_certs();
677 let (id, domains) = CertId::from_domains(&s(&["a.example.com"])).unwrap();
678 let (key, leaf) = placeholder_cert(&domains).unwrap();
679 let leaf_copy = leaf.clone();
680 publish(
681 &certs,
682 &id,
683 ManagedCert {
684 key,
685 leaf_der: leaf,
686 state: CertState::Placeholder,
687 meta: parse_cert_meta(&leaf_copy).unwrap(),
688 domains,
689 },
690 );
691 assert_eq!(
692 certs.load().get(id.as_str()).unwrap().state,
693 CertState::Placeholder
694 );
695 update(&certs, &id, |c| c.state = CertState::Failed);
696 assert_eq!(
697 certs.load().get(id.as_str()).unwrap().state,
698 CertState::Failed
699 );
700 assert_eq!(CertState::Failed.as_str(), "failed");
701 }
702}