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?;
59 if status != OrderStatus::Ready {
60 anyhow::bail!("Order not ready after challenges: {status:?}");
61 }
62 info!(domain, "ACME order ready, finalizing");
63
64 let key_pem = order.finalize().await?;
66 let cert_pem = order.poll_certificate(&RetryPolicy::default()).await?;
67
68 self.save_cert(domain, cert_pem.as_bytes(), key_pem.as_bytes())
70 .await?;
71
72 info!(domain, "Certificate provisioned and cached");
73 Ok((cert_pem.into_bytes(), key_pem.into_bytes()))
74 }
75
76 async fn handle_authorizations(&self, order: &mut instant_acme::Order) -> anyhow::Result<()> {
78 let mut authorizations = order.authorizations();
79 while let Some(result) = authorizations.next().await {
80 let mut authz = result?;
81
82 if authz.status == AuthorizationStatus::Valid {
83 debug!("Authorization already valid");
84 continue;
85 }
86
87 let mut challenge = authz
88 .challenge(ChallengeType::Http01)
89 .ok_or_else(|| anyhow::anyhow!("No HTTP-01 challenge offered"))?;
90
91 let token = challenge.token.clone();
92 let key_auth = challenge.key_authorization().as_str().to_string();
93
94 debug!(token = %token, "Serving HTTP-01 challenge");
95 self.challenges
96 .write()
97 .await
98 .insert(token.clone(), key_auth);
99
100 challenge.set_ready().await?;
101
102 self.challenges.write().await.remove(&token);
104 }
105 Ok(())
106 }
107
108 async fn load_or_create_account(&self) -> anyhow::Result<Account> {
110 let account_path = self.account_cache_path();
111
112 if account_path.exists() {
113 debug!("Loading cached ACME account");
114 let json = tokio::fs::read_to_string(&account_path).await?;
115 let creds: AccountCredentials = serde_json::from_str(&json)?;
116 let account = Account::builder()?.from_credentials(creds).await?;
117 return Ok(account);
118 }
119
120 info!(email = %self.email, "Creating new ACME account");
121 let contact = format!("mailto:{}", self.email);
122 let (account, credentials) = Account::builder()?
123 .create(
124 &NewAccount {
125 contact: &[&contact],
126 terms_of_service_agreed: true,
127 only_return_existing: false,
128 },
129 LetsEncrypt::Production.url().to_owned(),
130 None,
131 )
132 .await?;
133
134 if let Some(parent) = account_path.parent() {
136 tokio::fs::create_dir_all(parent).await?;
137 }
138 let json = serde_json::to_string_pretty(&credentials)?;
139 tokio::fs::write(&account_path, json).await?;
140 info!("ACME account cached at {}", account_path.display());
141
142 Ok(account)
143 }
144
145 async fn save_cert(&self, domain: &str, cert_pem: &[u8], key_pem: &[u8]) -> anyhow::Result<()> {
147 tokio::fs::create_dir_all(&self.cache_dir).await?;
148 let cert_path = self.cache_dir.join(format!("{domain}.cert.pem"));
149 let key_path = self.cache_dir.join(format!("{domain}.key.pem"));
150 tokio::fs::write(&cert_path, cert_pem).await?;
151 tokio::fs::write(&key_path, key_pem).await?;
152 debug!(domain, "Saved cert to {}", cert_path.display());
153 Ok(())
154 }
155
156 fn account_cache_path(&self) -> PathBuf {
158 default_orca_dir().join("acme-account.json")
159 }
160
161 pub async fn ensure_cert(&self, domain: &str) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
165 let cert_path = self.cache_dir.join(format!("{domain}.cert.pem"));
167 let key_path = self.cache_dir.join(format!("{domain}.key.pem"));
168
169 if cert_path.exists()
170 && key_path.exists()
171 && let Ok(days) = super::certs::check_cert_expiry(&cert_path)
172 {
173 if days >= super::RENEWAL_THRESHOLD_DAYS {
174 debug!(domain, days_remaining = days, "Using cached cert");
175 return self.build_acceptor(&cert_path, &key_path);
176 }
177 info!(domain, days_remaining = days, "Cert expiring, renewing");
178 }
179
180 let (cert_pem, key_pem) = self.provision_cert(domain).await?;
182 self.build_acceptor_from_pem(&cert_pem, &key_pem)
183 }
184
185 fn build_acceptor(
187 &self,
188 cert_path: &std::path::Path,
189 key_path: &std::path::Path,
190 ) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
191 let (certs, key) = super::certs::load_pem_certs(cert_path, key_path)?;
192 let config = rustls::ServerConfig::builder()
193 .with_no_client_auth()
194 .with_single_cert(certs, key)?;
195 Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
196 }
197
198 fn build_acceptor_from_pem(
200 &self,
201 cert_pem: &[u8],
202 key_pem: &[u8],
203 ) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
204 let certs = rustls_pemfile::certs(&mut &cert_pem[..]).collect::<Result<Vec<_>, _>>()?;
205 let key = rustls_pemfile::private_key(&mut &key_pem[..])?
206 .ok_or_else(|| anyhow::anyhow!("no private key in PEM data"))?;
207 let config = rustls::ServerConfig::builder()
208 .with_no_client_auth()
209 .with_single_cert(certs, key)?;
210 Ok(tokio_rustls::TlsAcceptor::from(Arc::new(config)))
211 }
212}