bws_web_server/ssl/
certificate.rs

1use chrono::{DateTime, Utc};
2use rustls_pemfile::{certs, private_key};
3use serde::{Deserialize, Serialize};
4use std::io::BufReader;
5use std::path::PathBuf;
6use tokio::fs;
7use x509_parser::prelude::*;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Certificate {
11    pub domain: String,
12    pub cert_path: PathBuf,
13    pub key_path: PathBuf,
14    pub issued_at: DateTime<Utc>,
15    pub expires_at: DateTime<Utc>,
16    pub issuer: String,
17    pub san_domains: Vec<String>,
18    pub auto_renew: bool,
19    pub last_renewal_check: Option<DateTime<Utc>>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CertificateStore {
24    pub certificates: Vec<Certificate>,
25    pub storage_path: PathBuf,
26}
27
28impl Certificate {
29    pub async fn from_files(
30        domain: String,
31        cert_path: PathBuf,
32        key_path: PathBuf,
33        auto_renew: bool,
34    ) -> Result<Self, Box<dyn std::error::Error>> {
35        // Read certificate file
36        let cert_data = fs::read(&cert_path).await?;
37        let cert_info = Self::parse_certificate(&cert_data)?;
38
39        Ok(Certificate {
40            domain,
41            cert_path,
42            key_path,
43            issued_at: cert_info.issued_at,
44            expires_at: cert_info.expires_at,
45            issuer: cert_info.issuer,
46            san_domains: cert_info.san_domains,
47            auto_renew,
48            last_renewal_check: None,
49        })
50    }
51
52    pub async fn save_certificate(
53        &self,
54        cert_pem: &str,
55        key_pem: &str,
56    ) -> Result<(), Box<dyn std::error::Error>> {
57        // Ensure parent directories exist
58        if let Some(parent) = self.cert_path.parent() {
59            fs::create_dir_all(parent).await?;
60        }
61        if let Some(parent) = self.key_path.parent() {
62            fs::create_dir_all(parent).await?;
63        }
64
65        // Write certificate and key files
66        fs::write(&self.cert_path, cert_pem).await?;
67        fs::write(&self.key_path, key_pem).await?;
68
69        // Set appropriate permissions (600 for key file)
70        #[cfg(unix)]
71        {
72            use std::os::unix::fs::PermissionsExt;
73            let key_perms = std::fs::Permissions::from_mode(0o600);
74            std::fs::set_permissions(&self.key_path, key_perms)?;
75
76            let cert_perms = std::fs::Permissions::from_mode(0o644);
77            std::fs::set_permissions(&self.cert_path, cert_perms)?;
78        }
79
80        log::info!(
81            "Certificate saved for {} at {} and {}",
82            self.domain,
83            self.cert_path.display(),
84            self.key_path.display()
85        );
86
87        Ok(())
88    }
89
90    pub fn days_until_expiry(&self) -> i64 {
91        let now = Utc::now();
92        (self.expires_at - now).num_days()
93    }
94
95    pub fn needs_renewal(&self, days_before_expiry: i64) -> bool {
96        self.auto_renew && self.days_until_expiry() <= days_before_expiry
97    }
98
99    pub fn is_expired(&self) -> bool {
100        Utc::now() > self.expires_at
101    }
102
103    pub fn covers_domain(&self, domain: &str) -> bool {
104        self.domain == domain || self.san_domains.contains(&domain.to_string())
105    }
106
107    fn parse_certificate(cert_data: &[u8]) -> Result<CertificateInfo, Box<dyn std::error::Error>> {
108        let mut reader = BufReader::new(cert_data);
109        let certs = certs(&mut reader)
110            .collect::<Result<Vec<_>, _>>()
111            .map_err(|e| format!("Failed to parse certificate: {}", e))?;
112
113        if certs.is_empty() {
114            return Err("No certificates found in file".into());
115        }
116
117        let cert = &certs[0];
118        let (_, parsed_cert) = X509Certificate::from_der(cert.as_ref())
119            .map_err(|e| format!("Failed to parse X509 certificate: {}", e))?;
120
121        let issued_at = DateTime::from_timestamp(parsed_cert.validity().not_before.timestamp(), 0)
122            .unwrap_or_else(Utc::now);
123
124        let expires_at = DateTime::from_timestamp(parsed_cert.validity().not_after.timestamp(), 0)
125            .unwrap_or_else(|| Utc::now() + chrono::Duration::days(90));
126
127        let issuer = parsed_cert
128            .issuer()
129            .iter_common_name()
130            .next()
131            .and_then(|cn| cn.as_str().ok())
132            .unwrap_or("Unknown")
133            .to_string();
134
135        // Extract SAN domains - simplified for now
136        let san_domains = Vec::new();
137        // TODO: Implement proper SAN parsing
138        log::debug!("Certificate SAN parsing not implemented - using subject CN only");
139
140        Ok(CertificateInfo {
141            issued_at,
142            expires_at,
143            issuer,
144            san_domains,
145        })
146    }
147
148    pub async fn validate_certificate_files(&self) -> Result<bool, Box<dyn std::error::Error>> {
149        // Check if files exist
150        if !self.cert_path.exists() || !self.key_path.exists() {
151            return Ok(false);
152        }
153
154        // Try to load certificate
155        let cert_data = fs::read(&self.cert_path).await?;
156        let mut cert_reader = BufReader::new(cert_data.as_slice());
157        let certs_result = certs(&mut cert_reader).collect::<Result<Vec<_>, _>>();
158        if certs_result.is_err() {
159            return Ok(false);
160        }
161
162        // Try to load private key
163        let key_data = fs::read(&self.key_path).await?;
164        let mut key_reader = BufReader::new(key_data.as_slice());
165        let key_result = private_key(&mut key_reader);
166        if key_result.is_err() {
167            return Ok(false);
168        }
169
170        Ok(true)
171    }
172
173    pub async fn get_rustls_config(
174        &self,
175    ) -> Result<rustls::ServerConfig, Box<dyn std::error::Error>> {
176        // Load certificate chain
177        let cert_data = fs::read(&self.cert_path).await?;
178        let mut cert_reader = BufReader::new(cert_data.as_slice());
179        let cert_chain = certs(&mut cert_reader)
180            .collect::<Result<Vec<_>, _>>()
181            .map_err(|e| format!("Failed to load certificate: {}", e))?;
182
183        // Load private key
184        let key_data = fs::read(&self.key_path).await?;
185        let mut key_reader = BufReader::new(key_data.as_slice());
186        let private_key = private_key(&mut key_reader)
187            .map_err(|e| format!("Failed to load private key: {}", e))?
188            .ok_or("No private key found")?;
189
190        // Create rustls config
191        let config = rustls::ServerConfig::builder()
192            .with_no_client_auth()
193            .with_single_cert(cert_chain, private_key)
194            .map_err(|e| format!("Invalid certificate/key: {}", e))?;
195
196        Ok(config)
197    }
198}
199
200impl CertificateStore {
201    pub fn new(storage_path: PathBuf) -> Self {
202        Self {
203            certificates: Vec::new(),
204            storage_path,
205        }
206    }
207
208    pub async fn load(&mut self) -> Result<(), Box<dyn std::error::Error>> {
209        if !self.storage_path.exists() {
210            log::info!("Certificate store file not found, starting with empty store");
211            return Ok(());
212        }
213
214        let data = fs::read_to_string(&self.storage_path).await?;
215        let store: CertificateStore = toml::from_str(&data)?;
216        self.certificates = store.certificates;
217
218        log::info!("Loaded {} certificates from store", self.certificates.len());
219        Ok(())
220    }
221
222    pub async fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
223        let data = toml::to_string_pretty(self)?;
224        if let Some(parent) = self.storage_path.parent() {
225            fs::create_dir_all(parent).await?;
226        }
227        fs::write(&self.storage_path, data).await?;
228        log::info!(
229            "Saved certificate store with {} certificates",
230            self.certificates.len()
231        );
232        Ok(())
233    }
234
235    pub fn add_certificate(&mut self, certificate: Certificate) {
236        // Remove any existing certificate for the same domain
237        self.certificates
238            .retain(|cert| cert.domain != certificate.domain);
239        self.certificates.push(certificate);
240    }
241
242    pub fn get_certificate(&self, domain: &str) -> Option<&Certificate> {
243        self.certificates
244            .iter()
245            .find(|cert| cert.covers_domain(domain))
246    }
247
248    pub fn get_certificates_needing_renewal(&self, days_before_expiry: i64) -> Vec<&Certificate> {
249        self.certificates
250            .iter()
251            .filter(|cert| cert.needs_renewal(days_before_expiry))
252            .collect()
253    }
254
255    pub fn get_expired_certificates(&self) -> Vec<&Certificate> {
256        self.certificates
257            .iter()
258            .filter(|cert| cert.is_expired())
259            .collect()
260    }
261
262    pub fn update_renewal_check(&mut self, domain: &str) {
263        if let Some(cert) = self.certificates.iter_mut().find(|c| c.domain == domain) {
264            cert.last_renewal_check = Some(Utc::now());
265        }
266    }
267
268    pub fn remove_certificate(&mut self, domain: &str) -> bool {
269        let original_len = self.certificates.len();
270        self.certificates.retain(|cert| cert.domain != domain);
271        self.certificates.len() != original_len
272    }
273
274    pub fn list_certificates(&self) -> &[Certificate] {
275        &self.certificates
276    }
277}
278
279#[derive(Debug)]
280struct CertificateInfo {
281    issued_at: DateTime<Utc>,
282    expires_at: DateTime<Utc>,
283    issuer: String,
284    san_domains: Vec<String>,
285}
286
287// Helper functions for certificate management
288pub fn get_certificate_path(domain: &str, cert_dir: &str) -> PathBuf {
289    PathBuf::from(cert_dir).join(format!("{}.crt", domain))
290}
291
292pub fn get_key_path(domain: &str, cert_dir: &str) -> PathBuf {
293    PathBuf::from(cert_dir).join(format!("{}.key", domain))
294}
295
296pub async fn ensure_certificate_directory(
297    cert_dir: &str,
298) -> Result<(), Box<dyn std::error::Error>> {
299    let path = PathBuf::from(cert_dir);
300    if !path.exists() {
301        fs::create_dir_all(&path).await?;
302        log::info!("Created certificate directory: {}", path.display());
303    }
304    Ok(())
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use tempfile::tempdir;
311
312    #[tokio::test]
313    async fn test_certificate_store() {
314        let temp_dir = tempdir().unwrap();
315        let store_path = temp_dir.path().join("certificates.toml");
316
317        let store = CertificateStore::new(store_path.clone());
318
319        // Test saving empty store
320        store.save().await.unwrap();
321        assert!(store_path.exists());
322
323        // Test loading empty store
324        let mut store2 = CertificateStore::new(store_path);
325        store2.load().await.unwrap();
326        assert_eq!(store2.certificates.len(), 0);
327    }
328
329    #[test]
330    fn test_certificate_expiry() {
331        let now = Utc::now();
332        let cert = Certificate {
333            domain: "example.com".to_string(),
334            cert_path: PathBuf::from("test.crt"),
335            key_path: PathBuf::from("test.key"),
336            issued_at: now - chrono::Duration::days(60),
337            expires_at: now + chrono::Duration::days(30),
338            issuer: "Test CA".to_string(),
339            san_domains: vec!["www.example.com".to_string()],
340            auto_renew: true,
341            last_renewal_check: None,
342        };
343
344        // Allow for small timing differences (29-30 days)
345        let days_until_expiry = cert.days_until_expiry();
346        assert!((29..=30).contains(&days_until_expiry));
347        assert!(cert.needs_renewal(45)); // Should renew if 45 days or less
348        assert!(!cert.needs_renewal(25)); // Should not renew if more than 30 days left
349        assert!(!cert.is_expired());
350        assert!(cert.covers_domain("example.com"));
351        assert!(cert.covers_domain("www.example.com"));
352        assert!(!cert.covers_domain("other.com"));
353    }
354}