orca_proxy/acme/
provider.rs1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use instant_acme::{
11 Account, AccountCredentials, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt,
12 NewAccount, NewOrder, OrderStatus, RetryPolicy,
13};
14use tokio::sync::RwLock;
15use tracing::{debug, info};
16
17use orca_core::fsutil;
18
19use super::default_account_path;
20
21#[derive(Clone)]
23pub struct AcmeProvider {
24 email: String,
25 cache_dir: PathBuf,
26 challenges: Arc<RwLock<HashMap<String, String>>>,
27}
28
29impl AcmeProvider {
30 pub fn new(
31 email: String,
32 cache_dir: PathBuf,
33 challenges: Arc<RwLock<HashMap<String, String>>>,
34 ) -> Self {
35 Self {
36 email,
37 cache_dir,
38 challenges,
39 }
40 }
41
42 pub async fn provision_cert(&self, domain: &str) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
49 info!(domain, "Starting ACME certificate provisioning");
50
51 let account = self.load_or_create_account().await?;
52 let identifiers = vec![Identifier::Dns(domain.to_string())];
53 let mut order = account.new_order(&NewOrder::new(&identifiers)).await?;
54 debug!(domain, "ACME order created");
55
56 self.handle_authorizations(&mut order).await?;
58
59 let status = order.poll_ready(&RetryPolicy::default()).await?;
62
63 self.challenges.write().await.clear();
65
66 if status != OrderStatus::Ready {
67 anyhow::bail!("Order not ready after challenges: {status:?}");
68 }
69 info!(domain, "ACME order ready, finalizing");
70
71 let key_pem = order.finalize().await?;
73 let cert_pem = order.poll_certificate(&RetryPolicy::default()).await?;
74
75 self.save_cert(domain, cert_pem.as_bytes(), key_pem.as_bytes())
77 .await?;
78
79 info!(domain, "Certificate provisioned and cached");
80 Ok((cert_pem.into_bytes(), key_pem.into_bytes()))
81 }
82
83 async fn handle_authorizations(&self, order: &mut instant_acme::Order) -> anyhow::Result<()> {
85 let mut authorizations = order.authorizations();
86 while let Some(result) = authorizations.next().await {
87 let mut authz = result?;
88
89 if authz.status == AuthorizationStatus::Valid {
90 debug!("Authorization already valid");
91 continue;
92 }
93
94 let mut challenge = authz
95 .challenge(ChallengeType::Http01)
96 .ok_or_else(|| anyhow::anyhow!("No HTTP-01 challenge offered"))?;
97
98 let token = challenge.token.clone();
99 let key_auth = challenge.key_authorization().as_str().to_string();
100
101 debug!(token = %token, "Serving HTTP-01 challenge");
102 self.challenges
103 .write()
104 .await
105 .insert(token.clone(), key_auth);
106
107 challenge.set_ready().await?;
108
109 }
114
115 Ok(())
116 }
117
118 async fn load_or_create_account(&self) -> anyhow::Result<Account> {
120 let account_path = self.account_cache_path();
121
122 if account_path.exists() {
123 debug!("Loading cached ACME account");
124 let json = tokio::fs::read_to_string(&account_path).await?;
125 let creds: AccountCredentials = serde_json::from_str(&json)?;
126 let account = Account::builder()?.from_credentials(creds).await?;
127 return Ok(account);
128 }
129
130 info!(email = %self.email, "Creating new ACME account");
131 let contact = format!("mailto:{}", self.email);
132 let directory = std::env::var("ORCA_ACME_DIRECTORY")
139 .unwrap_or_else(|_| LetsEncrypt::Production.url().to_owned());
140 info!(directory = %directory, "Using ACME directory");
141 let (account, credentials) = Account::builder()?
142 .create(
143 &NewAccount {
144 contact: &[&contact],
145 terms_of_service_agreed: true,
146 only_return_existing: false,
147 },
148 directory,
149 None,
150 )
151 .await?;
152
153 let json = serde_json::to_string_pretty(&credentials)?;
156 let path = account_path.clone();
157 tokio::task::spawn_blocking(move || fsutil::write_private(&path, json.as_bytes()))
158 .await??;
159 info!("ACME account cached at {}", account_path.display());
160
161 Ok(account)
162 }
163
164 async fn save_cert(&self, domain: &str, cert_pem: &[u8], key_pem: &[u8]) -> anyhow::Result<()> {
169 let dir = self.cache_dir.clone();
170 let cert_path = self.cache_dir.join(format!("{domain}.cert.pem"));
171 let key_path = self.cache_dir.join(format!("{domain}.key.pem"));
172 let (cert, key) = (cert_pem.to_vec(), key_pem.to_vec());
173 let saved_cert = cert_path.clone();
174 tokio::task::spawn_blocking(move || -> std::io::Result<()> {
175 fsutil::create_private_dir(&dir)?;
176 fsutil::write_private(&key_path, &key)?;
177 fsutil::write_private(&cert_path, &cert)
178 })
179 .await??;
180 debug!(domain, "Saved cert to {}", saved_cert.display());
181 Ok(())
182 }
183
184 fn account_cache_path(&self) -> PathBuf {
186 default_account_path()
187 }
188
189 pub async fn ensure_cert(&self, domain: &str) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
193 let cert_path = self.cache_dir.join(format!("{domain}.cert.pem"));
195 let key_path = self.cache_dir.join(format!("{domain}.key.pem"));
196
197 if cert_path.exists()
198 && key_path.exists()
199 && let Ok(days) = super::certs::check_cert_expiry(&cert_path)
200 {
201 if days >= super::RENEWAL_THRESHOLD_DAYS {
202 debug!(domain, days_remaining = days, "Using cached cert");
203 return self.build_acceptor(&cert_path, &key_path);
204 }
205 info!(domain, days_remaining = days, "Cert expiring, renewing");
206 }
207
208 let (cert_pem, key_pem) = self.provision_cert(domain).await?;
210 self.build_acceptor_from_pem(&cert_pem, &key_pem)
211 }
212
213 fn build_acceptor(
215 &self,
216 cert_path: &std::path::Path,
217 key_path: &std::path::Path,
218 ) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
219 let (certs, key) = super::certs::load_pem_certs(cert_path, key_path)?;
220 let config = rustls::ServerConfig::builder()
221 .with_no_client_auth()
222 .with_single_cert(certs, key)?;
223 Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
224 }
225
226 fn build_acceptor_from_pem(
228 &self,
229 cert_pem: &[u8],
230 key_pem: &[u8],
231 ) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
232 let certs = rustls_pemfile::certs(&mut &cert_pem[..]).collect::<Result<Vec<_>, _>>()?;
233 let key = rustls_pemfile::private_key(&mut &key_pem[..])?
234 .ok_or_else(|| anyhow::anyhow!("no private key in PEM data"))?;
235 let config = rustls::ServerConfig::builder()
236 .with_no_client_auth()
237 .with_single_cert(certs, key)?;
238 Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
239 }
240}
241
242pub(crate) fn secure_existing_key_material(
248 cache_dir: &Path,
249 account: &Path,
250) -> std::io::Result<usize> {
251 let mut tightened = 0;
252 if account.exists() && fsutil::restrict(account, 0o600)? {
253 tightened += 1;
254 }
255 if !cache_dir.is_dir() {
256 return Ok(tightened);
257 }
258 if fsutil::restrict(cache_dir, 0o700)? {
259 tightened += 1;
260 }
261 for entry in std::fs::read_dir(cache_dir)? {
262 let path = entry?.path();
263 let is_key = path
264 .file_name()
265 .and_then(|n| n.to_str())
266 .is_some_and(|n| n.ends_with(".key.pem"));
267 if is_key && path.is_file() && fsutil::restrict(&path, 0o600)? {
268 tightened += 1;
269 }
270 }
271 Ok(tightened)
272}
273
274#[cfg(test)]
275#[path = "provider_tests.rs"]
276mod tests;