Skip to main content

orca_proxy/acme/
provider.rs

1//! ACME certificate provisioning via `instant-acme`.
2//!
3//! Handles account creation/caching, HTTP-01 challenges, CSR generation,
4//! and certificate download — all in pure Rust, no certbot needed.
5
6use 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/// Pure-Rust ACME provider backed by `instant-acme`.
20#[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    /// Provision a TLS certificate for the given domain via ACME HTTP-01.
41    ///
42    /// The proxy must be serving HTTP on port 80 so Let's Encrypt can reach
43    /// `/.well-known/acme-challenge/{token}`.
44    ///
45    /// Returns `(cert_pem, key_pem)` as byte vectors.
46    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        // Process authorizations
55        self.handle_authorizations(&mut order).await?;
56
57        // Poll until order is ready for finalization
58        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        // Finalize: instant-acme generates the key + CSR internally
65        let key_pem = order.finalize().await?;
66        let cert_pem = order.poll_certificate(&RetryPolicy::default()).await?;
67
68        // Save to cache
69        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    /// Process all authorizations for an order, handling HTTP-01 challenges.
77    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            // Clean up challenge token after validation
103            self.challenges.write().await.remove(&token);
104        }
105        Ok(())
106    }
107
108    /// Load ACME account from cache or create a new one.
109    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        // Cache the account credentials
135        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    /// Save provisioned cert and key to the cache directory.
146    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    /// Path to the cached ACME account credentials.
157    fn account_cache_path(&self) -> PathBuf {
158        default_orca_dir().join("acme-account.json")
159    }
160
161    /// Ensure a valid cert exists for the domain — load from cache or provision.
162    ///
163    /// Returns a `TlsAcceptor` ready for use, or an error if provisioning fails.
164    pub async fn ensure_cert(&self, domain: &str) -> anyhow::Result<tokio_rustls::TlsAcceptor> {
165        // Check cache first
166        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        // Provision new cert
181        let (cert_pem, key_pem) = self.provision_cert(domain).await?;
182        self.build_acceptor_from_pem(&cert_pem, &key_pem)
183    }
184
185    /// Build a TlsAcceptor from PEM files on disk.
186    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    /// Build a TlsAcceptor from in-memory PEM bytes.
199    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}