1mod backoff;
7pub(crate) mod certs;
8mod provider;
9pub mod renewal;
10mod resolver;
11
12pub use backoff::AcmeCooldown;
13pub use provider::AcmeProvider;
14pub use resolver::DynCertResolver;
15
16use std::collections::{HashMap, HashSet};
17use std::path::PathBuf;
18use std::sync::Arc;
19
20use tokio::sync::RwLock;
21use tokio_rustls::TlsAcceptor;
22use tracing::{info, warn};
23
24const RENEWAL_THRESHOLD_DAYS: i64 = 30;
26
27#[derive(Clone)]
29pub struct AcmeManager {
30 pub acme_email: String,
31 pub cache_dir: PathBuf,
32 challenges: Arc<RwLock<HashMap<String, String>>>,
33 domains: Arc<RwLock<HashSet<String>>>,
34 provision_lock: Arc<tokio::sync::Semaphore>,
36 backoff: Arc<std::sync::Mutex<backoff::Backoff>>,
38}
39
40impl AcmeManager {
41 pub fn new(email: impl Into<String>, cache_dir: impl Into<PathBuf>) -> Self {
42 Self {
43 acme_email: email.into(),
44 cache_dir: cache_dir.into(),
45 challenges: Arc::new(RwLock::new(HashMap::new())),
46 domains: Arc::new(RwLock::new(HashSet::new())),
47 provision_lock: Arc::new(tokio::sync::Semaphore::new(1)),
48 backoff: Arc::default(),
49 }
50 }
51
52 pub fn secure_key_material(&self) {
57 match provider::secure_existing_key_material(&self.cache_dir, &default_account_path()) {
58 Ok(0) => {}
59 Ok(n) => warn!(
60 tightened = n,
61 cache = %self.cache_dir.display(),
62 "restricted TLS key material that earlier orca versions left \
63 readable by other local users; consider rotating those keys"
64 ),
65 Err(e) => warn!(
66 cache = %self.cache_dir.display(),
67 "could not restrict TLS key material: {e}"
68 ),
69 }
70 }
71
72 pub fn with_default_cache(email: impl Into<String>) -> Self {
74 let cache_dir = default_orca_dir().join("certs");
75 Self::new(email, cache_dir)
76 }
77
78 pub async fn add_domain(&self, domain: impl Into<String>) {
80 let domain = domain.into();
81 info!(domain = %domain, "Registered domain for ACME");
82 self.domains.write().await.insert(domain);
83 }
84
85 pub async fn set_challenge(&self, token: String, authorization: String) {
87 self.challenges.write().await.insert(token, authorization);
88 }
89
90 pub async fn get_challenge_response(&self, token: &str) -> Option<String> {
92 self.challenges.read().await.get(token).cloned()
93 }
94
95 pub async fn clear_challenge(&self, token: &str) {
97 self.challenges.write().await.remove(token);
98 }
99
100 pub fn load_cached_certs(
102 &self,
103 domain: &str,
104 ) -> Option<(
105 Vec<rustls::pki_types::CertificateDer<'static>>,
106 rustls::pki_types::PrivateKeyDer<'static>,
107 )> {
108 let cert_path = self.cert_path(domain);
109 let key_path = self.key_path(domain);
110 if !cert_path.exists() || !key_path.exists() {
111 return None;
112 }
113 match certs::load_pem_certs(&cert_path, &key_path) {
114 Ok(pair) => Some(pair),
115 Err(e) => {
116 warn!(domain, error = %e, "Failed to load cached certs");
117 None
118 }
119 }
120 }
121
122 pub fn needs_renewal(&self, domain: &str) -> bool {
124 let cert_path = self.cert_path(domain);
125 if !cert_path.exists() {
126 return true;
127 }
128 match certs::check_cert_expiry(&cert_path) {
129 Ok(days) if days >= RENEWAL_THRESHOLD_DAYS => false,
130 Ok(days) => {
131 info!(domain, days_remaining = days, "Certificate expiring soon");
132 true
133 }
134 Err(e) => {
135 warn!(domain, error = %e, "Cannot check cert expiry");
136 true
137 }
138 }
139 }
140
141 pub fn tls_acceptor_for(&self, domain: &str) -> anyhow::Result<Option<TlsAcceptor>> {
143 let Some((certs, key)) = self.load_cached_certs(domain) else {
144 return Ok(None);
145 };
146 if self.needs_renewal(domain) {
147 warn!(domain, "Cert expiring soon — will auto-renew");
148 }
149 let config = crate::tls::with_h2_alpn(
150 rustls::ServerConfig::builder()
151 .with_no_client_auth()
152 .with_single_cert(certs, key)?,
153 );
154 Ok(Some(TlsAcceptor::from(Arc::new(config))))
155 }
156
157 pub async fn ensure_cert_for_resolver(
162 &self,
163 domain: &str,
164 resolver: &DynCertResolver,
165 ) -> anyhow::Result<()> {
166 if resolver.has_cert(domain) && !self.needs_renewal(domain) {
167 return Ok(());
168 }
169
170 let _permit = self
173 .provision_lock
174 .acquire()
175 .await
176 .map_err(|e| anyhow::anyhow!("ACME provision lock closed: {e}"))?;
177
178 if resolver.has_cert(domain) && !self.needs_renewal(domain) {
180 return Ok(());
181 }
182
183 let provider = self.provider();
184 let cert_path = self.cert_path(domain);
185 let key_path = self.key_path(domain);
186
187 let (cert_pem, key_pem) =
189 if cert_path.exists() && key_path.exists() && !self.needs_renewal(domain) {
190 info!(domain, "Loading cached cert for hot provisioning");
191 (std::fs::read(&cert_path)?, std::fs::read(&key_path)?)
192 } else {
193 self.backoff
196 .lock()
197 .unwrap_or_else(|e| e.into_inner())
198 .check(domain)?;
199 info!(domain, "Hot-provisioning TLS certificate");
200 match provider.provision_cert(domain).await {
201 Ok(pair) => {
202 self.backoff
203 .lock()
204 .unwrap_or_else(|e| e.into_inner())
205 .succeeded(domain);
206 pair
207 }
208 Err(e) => {
209 self.backoff
210 .lock()
211 .unwrap_or_else(|e| e.into_inner())
212 .failed(domain, &format!("{e:#}"));
213 return Err(e);
214 }
215 }
216 };
217
218 let certified_key = Self::build_certified_key(&cert_pem, &key_pem)?;
219 resolver.add_cert(domain, Arc::new(certified_key));
220 info!(domain, "Certificate ready (hot-provisioned)");
221 Ok(())
222 }
223
224 fn build_certified_key(
226 cert_pem: &[u8],
227 key_pem: &[u8],
228 ) -> anyhow::Result<rustls::sign::CertifiedKey> {
229 let certs: Vec<_> =
230 rustls_pemfile::certs(&mut &cert_pem[..]).collect::<Result<Vec<_>, _>>()?;
231 let key = rustls_pemfile::private_key(&mut &key_pem[..])?
232 .ok_or_else(|| anyhow::anyhow!("no private key in PEM data"))?;
233 let signing_key = rustls::crypto::aws_lc_rs::sign::any_supported_type(&key)?;
234 Ok(rustls::sign::CertifiedKey::new(certs, signing_key))
235 }
236
237 pub fn cert_path(&self, domain: &str) -> PathBuf {
238 self.cache_dir.join(format!("{domain}.cert.pem"))
239 }
240
241 pub fn key_path(&self, domain: &str) -> PathBuf {
242 self.cache_dir.join(format!("{domain}.key.pem"))
243 }
244
245 pub async fn domains(&self) -> Vec<String> {
246 self.domains.read().await.iter().cloned().collect()
247 }
248
249 pub fn provider(&self) -> AcmeProvider {
251 AcmeProvider::new(
252 self.acme_email.clone(),
253 self.cache_dir.clone(),
254 self.challenges.clone(),
255 )
256 }
257}
258
259pub(crate) fn default_orca_dir() -> PathBuf {
260 dirs::home_dir()
261 .unwrap_or_else(|| PathBuf::from("."))
262 .join(".orca")
263}
264
265pub(crate) fn default_account_path() -> PathBuf {
267 default_orca_dir().join("acme-account.json")
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[tokio::test]
275 async fn test_challenge_lifecycle() {
276 let mgr = AcmeManager::new("test@example.com", "/tmp/orca-test-certs");
277 assert!(mgr.get_challenge_response("tok1").await.is_none());
278 mgr.set_challenge("tok1".into(), "auth1".into()).await;
279 assert_eq!(mgr.get_challenge_response("tok1").await.unwrap(), "auth1");
280 mgr.clear_challenge("tok1").await;
281 assert!(mgr.get_challenge_response("tok1").await.is_none());
282 }
283
284 #[tokio::test]
285 async fn test_domain_registration() {
286 let mgr = AcmeManager::new("test@example.com", "/tmp/orca-test-certs");
287 mgr.add_domain("example.com").await;
288 assert!(mgr.domains().await.contains(&"example.com".to_string()));
289 }
290
291 #[test]
292 fn test_cert_paths() {
293 let mgr = AcmeManager::new("test@example.com", "/tmp/certs");
294 assert_eq!(
295 mgr.cert_path("example.com"),
296 PathBuf::from("/tmp/certs/example.com.cert.pem")
297 );
298 assert_eq!(
299 mgr.key_path("example.com"),
300 PathBuf::from("/tmp/certs/example.com.key.pem")
301 );
302 }
303
304 #[test]
305 fn test_missing_certs_needs_renewal() {
306 let mgr = AcmeManager::new("test@example.com", "/tmp/nonexistent-certs");
307 assert!(mgr.needs_renewal("example.com"));
308 }
309}
310
311#[cfg(test)]
312#[path = "backoff_wiring_tests.rs"]
313mod backoff_wiring_tests;