orca_proxy/acme/
provider.rs1use std::collections::HashMap;
7use std::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 super::default_orca_dir;
18
19#[derive(Clone)]
21pub struct AcmeProvider {
22 email: String,
23 cache_dir: PathBuf,
24 challenges: Arc<RwLock<HashMap<String, String>>>,
25}
26
27impl AcmeProvider {
28 pub fn new(
29 email: String,
30 cache_dir: PathBuf,
31 challenges: Arc<RwLock<HashMap<String, String>>>,
32 ) -> Self {
33 Self {
34 email,
35 cache_dir,
36 challenges,
37 }
38 }
39
40 pub async fn provision_cert(&self, domain: &str) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
47 info!(domain, "Starting ACME certificate provisioning");
48
49 let account = self.load_or_create_account().await?;
50 let identifiers = vec![Identifier::Dns(domain.to_string())];
51 let mut order = account.new_order(&NewOrder::new(&identifiers)).await?;
52 debug!(domain, "ACME order created");
53
54 self.handle_authorizations(&mut order).await?;
56
57 let status = order.poll_ready(&RetryPolicy::default()).await?;
60
61 self.challenges.write().await.clear();
63
64 if status != OrderStatus::Ready {
65 anyhow::bail!("Order not ready after challenges: {status:?}");
66 }
67 info!(domain, "ACME order ready, finalizing");
68
69 let key_pem = order.finalize().await?;
71 let cert_pem = order.poll_certificate(&RetryPolicy::default()).await?;
72
73 self.save_cert(domain, cert_pem.as_bytes(), key_pem.as_bytes())
75 .await?;
76
77 info!(domain, "Certificate provisioned and cached");
78 Ok((cert_pem.into_bytes(), key_pem.into_bytes()))
79 }
80
81 async fn handle_authorizations(&self, order: &mut instant_acme::Order) -> anyhow::Result<()> {
83 let mut authorizations = order.authorizations();
84 while let Some(result) = authorizations.next().await {
85 let mut authz = result?;
86
87 if authz.status == AuthorizationStatus::Valid {
88 debug!("Authorization already valid");
89 continue;
90 }
91
92 let mut challenge = authz
93 .challenge(ChallengeType::Http01)
94 .ok_or_else(|| anyhow::anyhow!("No HTTP-01 challenge offered"))?;
95
96 let token = challenge.token.clone();
97 let key_auth = challenge.key_authorization().as_str().to_string();
98
99 debug!(token = %token, "Serving HTTP-01 challenge");
100 self.challenges
101 .write()
102 .await
103 .insert(token.clone(), key_auth);
104
105 challenge.set_ready().await?;
106
107 }
112
113 Ok(())
114 }
115
116 async fn load_or_create_account(&self) -> anyhow::Result<Account> {
118 let account_path = self.account_cache_path();
119
120 if account_path.exists() {
121 debug!("Loading cached ACME account");
122 let json = tokio::fs::read_to_string(&account_path).await?;
123 let creds: AccountCredentials = serde_json::from_str(&json)?;
124 let account = Account::builder()?.from_credentials(creds).await?;
125 return Ok(account);
126 }
127
128 info!(email = %self.email, "Creating new ACME account");
129 let contact = format!("mailto:{}", self.email);
130 let (account, credentials) = Account::builder()?
131 .create(
132 &NewAccount {
133 contact: &[&contact],
134 terms_of_service_agreed: true,
135 only_return_existing: false,
136 },
137 LetsEncrypt::Production.url().to_owned(),
138 None,
139 )
140 .await?;
141
142 if let Some(parent) = account_path.parent() {
144 tokio::fs::create_dir_all(parent).await?;
145 }
146 let json = serde_json::to_string_pretty(&credentials)?;
147 tokio::fs::write(&account_path, json).await?;
148 info!("ACME account cached at {}", account_path.display());
149
150 Ok(account)
151 }
152
153 async fn save_cert(&self, domain: &str, cert_pem: &[u8], key_pem: &[u8]) -> anyhow::Result<()> {
155 tokio::fs::create_dir_all(&self.cache_dir).await?;
156 let cert_path = self.cache_dir.join(format!("{domain}.cert.pem"));
157 let key_path = self.cache_dir.join(format!("{domain}.key.pem"));
158 tokio::fs::write(&cert_path, cert_pem).await?;
159 tokio::fs::write(&key_path, key_pem).await?;
160 debug!(domain, "Saved cert to {}", cert_path.display());
161 Ok(())
162 }
163
164 fn account_cache_path(&self) -> PathBuf {
166 default_orca_dir().join("acme-account.json")
167 }
168
169 pub async fn ensure_cert(&self, domain: &str) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
173 let cert_path = self.cache_dir.join(format!("{domain}.cert.pem"));
175 let key_path = self.cache_dir.join(format!("{domain}.key.pem"));
176
177 if cert_path.exists()
178 && key_path.exists()
179 && let Ok(days) = super::certs::check_cert_expiry(&cert_path)
180 {
181 if days >= super::RENEWAL_THRESHOLD_DAYS {
182 debug!(domain, days_remaining = days, "Using cached cert");
183 return self.build_acceptor(&cert_path, &key_path);
184 }
185 info!(domain, days_remaining = days, "Cert expiring, renewing");
186 }
187
188 let (cert_pem, key_pem) = self.provision_cert(domain).await?;
190 self.build_acceptor_from_pem(&cert_pem, &key_pem)
191 }
192
193 fn build_acceptor(
195 &self,
196 cert_path: &std::path::Path,
197 key_path: &std::path::Path,
198 ) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
199 let (certs, key) = super::certs::load_pem_certs(cert_path, key_path)?;
200 let config = rustls::ServerConfig::builder()
201 .with_no_client_auth()
202 .with_single_cert(certs, key)?;
203 Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
204 }
205
206 fn build_acceptor_from_pem(
208 &self,
209 cert_pem: &[u8],
210 key_pem: &[u8],
211 ) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
212 let certs = rustls_pemfile::certs(&mut &cert_pem[..]).collect::<Result<Vec<_>, _>>()?;
213 let key = rustls_pemfile::private_key(&mut &key_pem[..])?
214 .ok_or_else(|| anyhow::anyhow!("no private key in PEM data"))?;
215 let config = rustls::ServerConfig::builder()
216 .with_no_client_auth()
217 .with_single_cert(certs, key)?;
218 Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
219 }
220}