Skip to main content

postrust_proxy/tls/
acme.rs

1//! ACME certificate management using rustls-acme.
2
3use crate::config::AcmeConfig;
4use crate::error::{ProxyError, ProxyResult};
5use crate::tls::CertificateStore;
6use rustls_acme::caches::DirCache;
7use rustls_acme::AcmeConfig as RustlsAcmeConfig;
8use std::path::Path;
9use std::sync::Arc;
10use tokio_rustls::rustls::ServerConfig;
11use tokio_util::sync::CancellationToken;
12use tracing::{error, info};
13
14/// ACME certificate manager.
15pub struct AcmeManager {
16    /// ACME configuration
17    config: AcmeConfig,
18    /// Certificate store for persistence
19    cert_store: Arc<CertificateStore>,
20    /// Cache directory for ACME state
21    cache_dir: std::path::PathBuf,
22}
23
24impl AcmeManager {
25    /// Create a new ACME manager.
26    pub fn new(
27        config: AcmeConfig,
28        cert_store: Arc<CertificateStore>,
29        cache_dir: impl AsRef<Path>,
30    ) -> Self {
31        Self {
32            config,
33            cert_store,
34            cache_dir: cache_dir.as_ref().to_path_buf(),
35        }
36    }
37
38    /// Create an ACME resolver for TLS.
39    ///
40    /// This returns an Arc<ServerConfig> that automatically handles ACME challenges
41    /// and certificate renewal.
42    pub fn create_resolver(&self, domains: Vec<String>) -> ProxyResult<Arc<ServerConfig>> {
43        if !self.config.enabled {
44            return Err(ProxyError::Tls("ACME is not enabled".into()));
45        }
46
47        let directory = if self.config.staging {
48            "https://acme-staging-v02.api.letsencrypt.org/directory"
49        } else {
50            "https://acme-v02.api.letsencrypt.org/directory"
51        };
52
53        let contacts: Vec<String> = self
54            .config
55            .email
56            .as_ref()
57            .map(|email| vec![format!("mailto:{}", email)])
58            .unwrap_or_default();
59
60        info!("Setting up ACME for domains: {:?}", domains);
61
62        // Create ACME state with directory cache
63        let cache_dir = self.cache_dir.clone();
64        let cache = DirCache::new(cache_dir);
65
66        let state = RustlsAcmeConfig::new(domains)
67            .contact(contacts)
68            .cache(cache)
69            .directory(directory)
70            .state();
71
72        // Get the resolver which handles ACME challenges
73        let resolver = state.resolver();
74
75        // Build server config with ACME resolver
76        let server_config = ServerConfig::builder()
77            .with_no_client_auth()
78            .with_cert_resolver(resolver);
79
80        Ok(Arc::new(server_config))
81    }
82
83    /// Build a ServerConfig from PEM-encoded certificate and key.
84    pub fn build_server_config_from_pem(
85        cert_pem: &[u8],
86        key_pem: &[u8],
87    ) -> ProxyResult<Arc<ServerConfig>> {
88        use rustls_pemfile::{certs, private_key};
89        use std::io::BufReader;
90
91        // Parse certificates
92        let certs: Vec<_> = certs(&mut BufReader::new(cert_pem))
93            .filter_map(|r| r.ok())
94            .collect();
95
96        if certs.is_empty() {
97            return Err(ProxyError::Tls("No certificates found in PEM".into()));
98        }
99
100        // Parse private key
101        let key = private_key(&mut BufReader::new(key_pem))
102            .map_err(|e| ProxyError::Tls(format!("Failed to parse private key: {}", e)))?
103            .ok_or_else(|| ProxyError::Tls("No private key found in PEM".into()))?;
104
105        // Build server config
106        let config = ServerConfig::builder()
107            .with_no_client_auth()
108            .with_single_cert(certs, key)
109            .map_err(|e| ProxyError::Tls(format!("Failed to build TLS config: {}", e)))?;
110
111        Ok(Arc::new(config))
112    }
113
114    /// Start background certificate renewal task.
115    pub async fn start_renewal_task(self: Arc<Self>, cancel_token: CancellationToken) {
116        let check_interval = std::time::Duration::from_secs(86400); // Daily
117
118        info!("ACME certificate renewal task started");
119
120        loop {
121            tokio::select! {
122                _ = cancel_token.cancelled() => {
123                    info!("ACME renewal task stopped");
124                    break;
125                }
126                _ = tokio::time::sleep(check_interval) => {
127                    if let Err(e) = self.check_renewals().await {
128                        error!("Certificate renewal check failed: {}", e);
129                    }
130                }
131            }
132        }
133    }
134
135    /// Check for certificates that need renewal.
136    async fn check_renewals(&self) -> ProxyResult<()> {
137        let domains = self.cert_store.list_domains().await?;
138        let now = chrono::Utc::now();
139        let renewal_threshold = chrono::Duration::days(30);
140
141        for domain in domains {
142            if let Some(cert) = self.cert_store.get(&domain).await {
143                if let Some(expires_at) = cert.expires_at {
144                    if expires_at < now + renewal_threshold {
145                        info!(
146                            "Certificate for {} needs renewal (expires {})",
147                            domain, expires_at
148                        );
149                        // Note: With rustls-acme, renewal happens automatically via the resolver
150                        // This is just for monitoring/alerting purposes
151                    }
152                }
153            }
154        }
155
156        Ok(())
157    }
158
159    /// Check if ACME is enabled.
160    pub fn is_enabled(&self) -> bool {
161        self.config.enabled
162    }
163
164    /// Get configured domains.
165    pub fn domains(&self) -> &[String] {
166        &self.config.domains
167    }
168}