1use std::fs;
18use std::path::{Path, PathBuf};
19
20use chrono::{DateTime, Utc};
21use serde::{Deserialize, Serialize};
22use tracing::{debug, info, trace, warn};
23
24use super::error::StorageError;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct CertificateMeta {
29 pub expires: DateTime<Utc>,
31 pub issued: DateTime<Utc>,
33 pub domains: Vec<String>,
35 #[serde(default)]
37 pub issuer: Option<String>,
38}
39
40#[derive(Debug, Clone)]
42pub struct StoredCertificate {
43 pub cert_pem: String,
45 pub key_pem: String,
47 pub meta: CertificateMeta,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct StoredAccountCredentials {
57 #[serde(default)]
59 pub contact_email: Option<String>,
60 pub created: DateTime<Utc>,
62}
63
64#[derive(Debug)]
69pub struct CertificateStorage {
70 base_path: PathBuf,
72}
73
74impl CertificateStorage {
75 pub fn new(base_path: &Path) -> Result<Self, StorageError> {
85 fs::create_dir_all(base_path)?;
87
88 let domains_path = base_path.join("domains");
90 fs::create_dir_all(&domains_path)?;
91
92 #[cfg(unix)]
94 {
95 use std::os::unix::fs::PermissionsExt;
96 let perms = fs::Permissions::from_mode(0o700);
97 fs::set_permissions(base_path, perms.clone())?;
98 fs::set_permissions(&domains_path, perms)?;
99 }
100
101 info!(
102 storage_path = %base_path.display(),
103 "Initialized ACME certificate storage"
104 );
105
106 Ok(Self {
107 base_path: base_path.to_path_buf(),
108 })
109 }
110
111 pub fn base_path(&self) -> &Path {
113 &self.base_path
114 }
115
116 pub fn load_account(&self) -> Result<Option<StoredAccountCredentials>, StorageError> {
122 let account_path = self.base_path.join("account.json");
123
124 if !account_path.exists() {
125 trace!("No stored ACME account found");
126 return Ok(None);
127 }
128
129 let content = fs::read_to_string(&account_path)?;
130 let creds: StoredAccountCredentials = serde_json::from_str(&content)?;
131
132 debug!(
133 contact = ?creds.contact_email,
134 created = %creds.created,
135 "Loaded ACME account credentials"
136 );
137 Ok(Some(creds))
138 }
139
140 pub fn save_account(&self, creds: &StoredAccountCredentials) -> Result<(), StorageError> {
142 let account_path = self.base_path.join("account.json");
143 let content = serde_json::to_string_pretty(creds)?;
144 fs::write(&account_path, content)?;
145
146 #[cfg(unix)]
148 {
149 use std::os::unix::fs::PermissionsExt;
150 fs::set_permissions(&account_path, fs::Permissions::from_mode(0o600))?;
151 }
152
153 info!(contact = ?creds.contact_email, "Saved ACME account credentials");
154 Ok(())
155 }
156
157 pub fn load_credentials_json(&self) -> Result<Option<String>, StorageError> {
159 let creds_path = self.base_path.join("credentials.json");
160
161 if !creds_path.exists() {
162 trace!("No stored ACME credentials found");
163 return Ok(None);
164 }
165
166 let content = fs::read_to_string(&creds_path)?;
167 debug!("Loaded ACME credentials JSON");
168 Ok(Some(content))
169 }
170
171 pub fn save_credentials_json(&self, json: &str) -> Result<(), StorageError> {
173 let creds_path = self.base_path.join("credentials.json");
174 fs::write(&creds_path, json)?;
175
176 #[cfg(unix)]
178 {
179 use std::os::unix::fs::PermissionsExt;
180 fs::set_permissions(&creds_path, fs::Permissions::from_mode(0o600))?;
181 }
182
183 info!("Saved ACME credentials JSON");
184 Ok(())
185 }
186
187 fn domain_path(&self, domain: &str) -> PathBuf {
193 self.base_path.join("domains").join(domain)
194 }
195
196 pub fn load_certificate(&self, domain: &str) -> Result<Option<StoredCertificate>, StorageError> {
198 let domain_path = self.domain_path(domain);
199 let cert_path = domain_path.join("cert.pem");
200 let key_path = domain_path.join("key.pem");
201 let meta_path = domain_path.join("meta.json");
202
203 if !cert_path.exists() {
204 trace!(domain = %domain, "No stored certificate found");
205 return Ok(None);
206 }
207
208 let cert_pem = fs::read_to_string(&cert_path)?;
209 let key_pem = fs::read_to_string(&key_path)?;
210 let meta_content = fs::read_to_string(&meta_path)?;
211 let meta: CertificateMeta = serde_json::from_str(&meta_content)?;
212
213 debug!(
214 domain = %domain,
215 expires = %meta.expires,
216 "Loaded stored certificate"
217 );
218
219 Ok(Some(StoredCertificate {
220 cert_pem,
221 key_pem,
222 meta,
223 }))
224 }
225
226 pub fn save_certificate(
228 &self,
229 domain: &str,
230 cert_pem: &str,
231 key_pem: &str,
232 expires: DateTime<Utc>,
233 all_domains: &[String],
234 ) -> Result<(), StorageError> {
235 let domain_path = self.domain_path(domain);
236 fs::create_dir_all(&domain_path)?;
237
238 let cert_path = domain_path.join("cert.pem");
239 let key_path = domain_path.join("key.pem");
240 let meta_path = domain_path.join("meta.json");
241
242 fs::write(&cert_path, cert_pem)?;
244
245 fs::write(&key_path, key_pem)?;
247 #[cfg(unix)]
248 {
249 use std::os::unix::fs::PermissionsExt;
250 fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600))?;
251 }
252
253 let meta = CertificateMeta {
255 expires,
256 issued: Utc::now(),
257 domains: all_domains.to_vec(),
258 issuer: Some("Let's Encrypt".to_string()),
259 };
260 let meta_content = serde_json::to_string_pretty(&meta)?;
261 fs::write(&meta_path, meta_content)?;
262
263 info!(
264 domain = %domain,
265 expires = %expires,
266 "Saved certificate to storage"
267 );
268
269 Ok(())
270 }
271
272 pub fn needs_renewal(&self, domain: &str, renew_before_days: u32) -> Result<bool, StorageError> {
278 let Some(cert) = self.load_certificate(domain)? else {
279 debug!(domain = %domain, "No certificate exists, needs issuance");
280 return Ok(true);
281 };
282
283 let renew_threshold = Utc::now() + chrono::Duration::days(i64::from(renew_before_days));
284 let needs_renewal = cert.meta.expires <= renew_threshold;
285
286 if needs_renewal {
287 debug!(
288 domain = %domain,
289 expires = %cert.meta.expires,
290 threshold = %renew_threshold,
291 "Certificate needs renewal"
292 );
293 } else {
294 trace!(
295 domain = %domain,
296 expires = %cert.meta.expires,
297 "Certificate is still valid"
298 );
299 }
300
301 Ok(needs_renewal)
302 }
303
304 pub fn certificate_paths(&self, domain: &str) -> Option<(PathBuf, PathBuf)> {
308 let domain_path = self.domain_path(domain);
309 let cert_path = domain_path.join("cert.pem");
310 let key_path = domain_path.join("key.pem");
311
312 if cert_path.exists() && key_path.exists() {
313 Some((cert_path, key_path))
314 } else {
315 None
316 }
317 }
318
319 pub fn list_domains(&self) -> Result<Vec<String>, StorageError> {
321 let domains_path = self.base_path.join("domains");
322
323 if !domains_path.exists() {
324 return Ok(Vec::new());
325 }
326
327 let mut domains = Vec::new();
328 for entry in fs::read_dir(&domains_path)? {
329 let entry = entry?;
330 if entry.file_type()?.is_dir() {
331 if let Some(name) = entry.file_name().to_str() {
332 domains.push(name.to_string());
333 }
334 }
335 }
336
337 Ok(domains)
338 }
339
340 pub fn delete_certificate(&self, domain: &str) -> Result<(), StorageError> {
342 let domain_path = self.domain_path(domain);
343
344 if domain_path.exists() {
345 fs::remove_dir_all(&domain_path)?;
346 info!(domain = %domain, "Deleted stored certificate");
347 } else {
348 warn!(domain = %domain, "Certificate to delete not found");
349 }
350
351 Ok(())
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use tempfile::TempDir;
359
360 fn setup_storage() -> (TempDir, CertificateStorage) {
361 let temp_dir = TempDir::new().unwrap();
362 let storage = CertificateStorage::new(temp_dir.path()).unwrap();
363 (temp_dir, storage)
364 }
365
366 #[test]
367 fn test_storage_creation() {
368 let (_temp_dir, storage) = setup_storage();
369 assert!(storage.base_path().exists());
370 assert!(storage.base_path().join("domains").exists());
371 }
372
373 #[test]
374 fn test_credentials_json_save_load() {
375 let (_temp_dir, storage) = setup_storage();
376
377 let test_json = r#"{"test": "credentials"}"#;
378 storage.save_credentials_json(test_json).unwrap();
379
380 let loaded = storage.load_credentials_json().unwrap();
381 assert!(loaded.is_some());
382 assert_eq!(loaded.unwrap(), test_json);
383 }
384
385 #[test]
386 fn test_certificate_save_load() {
387 let (_temp_dir, storage) = setup_storage();
388
389 let cert_pem = "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----";
390 let key_pem = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
391 let expires = Utc::now() + chrono::Duration::days(90);
392
393 storage
394 .save_certificate(
395 "example.com",
396 cert_pem,
397 key_pem,
398 expires,
399 &["example.com".to_string()],
400 )
401 .unwrap();
402
403 let loaded = storage.load_certificate("example.com").unwrap();
404 assert!(loaded.is_some());
405 let loaded = loaded.unwrap();
406 assert_eq!(loaded.cert_pem, cert_pem);
407 assert_eq!(loaded.key_pem, key_pem);
408 }
409
410 #[test]
411 fn test_needs_renewal_no_cert() {
412 let (_temp_dir, storage) = setup_storage();
413 assert!(storage.needs_renewal("nonexistent.com", 30).unwrap());
414 }
415
416 #[test]
417 fn test_needs_renewal_expiring_soon() {
418 let (_temp_dir, storage) = setup_storage();
419
420 let expires = Utc::now() + chrono::Duration::days(15);
422 storage
423 .save_certificate(
424 "expiring.com",
425 "cert",
426 "key",
427 expires,
428 &["expiring.com".to_string()],
429 )
430 .unwrap();
431
432 assert!(storage.needs_renewal("expiring.com", 30).unwrap());
434 }
435
436 #[test]
437 fn test_needs_renewal_still_valid() {
438 let (_temp_dir, storage) = setup_storage();
439
440 let expires = Utc::now() + chrono::Duration::days(60);
442 storage
443 .save_certificate(
444 "valid.com",
445 "cert",
446 "key",
447 expires,
448 &["valid.com".to_string()],
449 )
450 .unwrap();
451
452 assert!(!storage.needs_renewal("valid.com", 30).unwrap());
454 }
455
456 #[test]
457 fn test_list_domains() {
458 let (_temp_dir, storage) = setup_storage();
459
460 storage
461 .save_certificate(
462 "a.com",
463 "cert",
464 "key",
465 Utc::now() + chrono::Duration::days(90),
466 &["a.com".to_string()],
467 )
468 .unwrap();
469 storage
470 .save_certificate(
471 "b.com",
472 "cert",
473 "key",
474 Utc::now() + chrono::Duration::days(90),
475 &["b.com".to_string()],
476 )
477 .unwrap();
478
479 let domains = storage.list_domains().unwrap();
480 assert_eq!(domains.len(), 2);
481 assert!(domains.contains(&"a.com".to_string()));
482 assert!(domains.contains(&"b.com".to_string()));
483 }
484
485 #[test]
486 fn test_delete_certificate() {
487 let (_temp_dir, storage) = setup_storage();
488
489 storage
490 .save_certificate(
491 "delete.com",
492 "cert",
493 "key",
494 Utc::now() + chrono::Duration::days(90),
495 &["delete.com".to_string()],
496 )
497 .unwrap();
498
499 assert!(storage.load_certificate("delete.com").unwrap().is_some());
500
501 storage.delete_certificate("delete.com").unwrap();
502
503 assert!(storage.load_certificate("delete.com").unwrap().is_none());
504 }
505}