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 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 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 fs::write(&self.cert_path, cert_pem).await?;
67 fs::write(&self.key_path, key_pem).await?;
68
69 #[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 let san_domains = Vec::new();
137 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 if !self.cert_path.exists() || !self.key_path.exists() {
151 return Ok(false);
152 }
153
154 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 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 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 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 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 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
287pub 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 store.save().await.unwrap();
321 assert!(store_path.exists());
322
323 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 let days_until_expiry = cert.days_until_expiry();
346 assert!((29..=30).contains(&days_until_expiry));
347 assert!(cert.needs_renewal(45)); assert!(!cert.needs_renewal(25)); 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}