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(
198 &self,
199 domain: &str,
200 ) -> Result<Option<StoredCertificate>, StorageError> {
201 let domain_path = self.domain_path(domain);
202 let cert_path = domain_path.join("cert.pem");
203 let key_path = domain_path.join("key.pem");
204 let meta_path = domain_path.join("meta.json");
205
206 if !cert_path.exists() {
207 trace!(domain = %domain, "No stored certificate found");
208 return Ok(None);
209 }
210
211 let cert_pem = fs::read_to_string(&cert_path)?;
212 let key_pem = fs::read_to_string(&key_path)?;
213 let meta_content = fs::read_to_string(&meta_path)?;
214 let meta: CertificateMeta = serde_json::from_str(&meta_content)?;
215
216 debug!(
217 domain = %domain,
218 expires = %meta.expires,
219 "Loaded stored certificate"
220 );
221
222 Ok(Some(StoredCertificate {
223 cert_pem,
224 key_pem,
225 meta,
226 }))
227 }
228
229 pub fn save_certificate(
231 &self,
232 domain: &str,
233 cert_pem: &str,
234 key_pem: &str,
235 expires: DateTime<Utc>,
236 all_domains: &[String],
237 ) -> Result<(), StorageError> {
238 let domain_path = self.domain_path(domain);
239 fs::create_dir_all(&domain_path)?;
240
241 let cert_path = domain_path.join("cert.pem");
242 let key_path = domain_path.join("key.pem");
243 let meta_path = domain_path.join("meta.json");
244
245 fs::write(&cert_path, cert_pem)?;
247
248 fs::write(&key_path, key_pem)?;
250 #[cfg(unix)]
251 {
252 use std::os::unix::fs::PermissionsExt;
253 fs::set_permissions(&key_path, fs::Permissions::from_mode(0o600))?;
254 }
255
256 let meta = CertificateMeta {
258 expires,
259 issued: Utc::now(),
260 domains: all_domains.to_vec(),
261 issuer: Some("Let's Encrypt".to_string()),
262 };
263 let meta_content = serde_json::to_string_pretty(&meta)?;
264 fs::write(&meta_path, meta_content)?;
265
266 info!(
267 domain = %domain,
268 expires = %expires,
269 "Saved certificate to storage"
270 );
271
272 Ok(())
273 }
274
275 pub fn needs_renewal(
281 &self,
282 domain: &str,
283 renew_before_days: u32,
284 ) -> Result<bool, StorageError> {
285 let Some(cert) = self.load_certificate(domain)? else {
286 debug!(domain = %domain, "No certificate exists, needs issuance");
287 return Ok(true);
288 };
289
290 let renew_threshold = Utc::now() + chrono::Duration::days(i64::from(renew_before_days));
291 let needs_renewal = cert.meta.expires <= renew_threshold;
292
293 if needs_renewal {
294 debug!(
295 domain = %domain,
296 expires = %cert.meta.expires,
297 threshold = %renew_threshold,
298 "Certificate needs renewal"
299 );
300 } else {
301 trace!(
302 domain = %domain,
303 expires = %cert.meta.expires,
304 "Certificate is still valid"
305 );
306 }
307
308 Ok(needs_renewal)
309 }
310
311 pub fn certificate_paths(&self, domain: &str) -> Option<(PathBuf, PathBuf)> {
315 let domain_path = self.domain_path(domain);
316 let cert_path = domain_path.join("cert.pem");
317 let key_path = domain_path.join("key.pem");
318
319 if cert_path.exists() && key_path.exists() {
320 Some((cert_path, key_path))
321 } else {
322 None
323 }
324 }
325
326 pub fn list_domains(&self) -> Result<Vec<String>, StorageError> {
328 let domains_path = self.base_path.join("domains");
329
330 if !domains_path.exists() {
331 return Ok(Vec::new());
332 }
333
334 let mut domains = Vec::new();
335 for entry in fs::read_dir(&domains_path)? {
336 let entry = entry?;
337 if entry.file_type()?.is_dir() {
338 if let Some(name) = entry.file_name().to_str() {
339 domains.push(name.to_string());
340 }
341 }
342 }
343
344 Ok(domains)
345 }
346
347 pub fn delete_certificate(&self, domain: &str) -> Result<(), StorageError> {
349 let domain_path = self.domain_path(domain);
350
351 if domain_path.exists() {
352 fs::remove_dir_all(&domain_path)?;
353 info!(domain = %domain, "Deleted stored certificate");
354 } else {
355 warn!(domain = %domain, "Certificate to delete not found");
356 }
357
358 Ok(())
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use tempfile::TempDir;
366
367 fn setup_storage() -> (TempDir, CertificateStorage) {
368 let temp_dir = TempDir::new().unwrap();
369 let storage = CertificateStorage::new(temp_dir.path()).unwrap();
370 (temp_dir, storage)
371 }
372
373 #[test]
374 fn test_storage_creation() {
375 let (_temp_dir, storage) = setup_storage();
376 assert!(storage.base_path().exists());
377 assert!(storage.base_path().join("domains").exists());
378 }
379
380 #[test]
381 fn test_credentials_json_save_load() {
382 let (_temp_dir, storage) = setup_storage();
383
384 let test_json = r#"{"test": "credentials"}"#;
385 storage.save_credentials_json(test_json).unwrap();
386
387 let loaded = storage.load_credentials_json().unwrap();
388 assert!(loaded.is_some());
389 assert_eq!(loaded.unwrap(), test_json);
390 }
391
392 #[test]
393 fn test_certificate_save_load() {
394 let (_temp_dir, storage) = setup_storage();
395
396 let cert_pem = "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----";
397 let key_pem = "-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----";
398 let expires = Utc::now() + chrono::Duration::days(90);
399
400 storage
401 .save_certificate(
402 "example.com",
403 cert_pem,
404 key_pem,
405 expires,
406 &["example.com".to_string()],
407 )
408 .unwrap();
409
410 let loaded = storage.load_certificate("example.com").unwrap();
411 assert!(loaded.is_some());
412 let loaded = loaded.unwrap();
413 assert_eq!(loaded.cert_pem, cert_pem);
414 assert_eq!(loaded.key_pem, key_pem);
415 }
416
417 #[test]
418 fn test_needs_renewal_no_cert() {
419 let (_temp_dir, storage) = setup_storage();
420 assert!(storage.needs_renewal("nonexistent.com", 30).unwrap());
421 }
422
423 #[test]
424 fn test_needs_renewal_expiring_soon() {
425 let (_temp_dir, storage) = setup_storage();
426
427 let expires = Utc::now() + chrono::Duration::days(15);
429 storage
430 .save_certificate(
431 "expiring.com",
432 "cert",
433 "key",
434 expires,
435 &["expiring.com".to_string()],
436 )
437 .unwrap();
438
439 assert!(storage.needs_renewal("expiring.com", 30).unwrap());
441 }
442
443 #[test]
444 fn test_needs_renewal_still_valid() {
445 let (_temp_dir, storage) = setup_storage();
446
447 let expires = Utc::now() + chrono::Duration::days(60);
449 storage
450 .save_certificate(
451 "valid.com",
452 "cert",
453 "key",
454 expires,
455 &["valid.com".to_string()],
456 )
457 .unwrap();
458
459 assert!(!storage.needs_renewal("valid.com", 30).unwrap());
461 }
462
463 #[test]
464 fn test_list_domains() {
465 let (_temp_dir, storage) = setup_storage();
466
467 storage
468 .save_certificate(
469 "a.com",
470 "cert",
471 "key",
472 Utc::now() + chrono::Duration::days(90),
473 &["a.com".to_string()],
474 )
475 .unwrap();
476 storage
477 .save_certificate(
478 "b.com",
479 "cert",
480 "key",
481 Utc::now() + chrono::Duration::days(90),
482 &["b.com".to_string()],
483 )
484 .unwrap();
485
486 let domains = storage.list_domains().unwrap();
487 assert_eq!(domains.len(), 2);
488 assert!(domains.contains(&"a.com".to_string()));
489 assert!(domains.contains(&"b.com".to_string()));
490 }
491
492 #[test]
493 fn test_delete_certificate() {
494 let (_temp_dir, storage) = setup_storage();
495
496 storage
497 .save_certificate(
498 "delete.com",
499 "cert",
500 "key",
501 Utc::now() + chrono::Duration::days(90),
502 &["delete.com".to_string()],
503 )
504 .unwrap();
505
506 assert!(storage.load_certificate("delete.com").unwrap().is_some());
507
508 storage.delete_certificate("delete.com").unwrap();
509
510 assert!(storage.load_certificate("delete.com").unwrap().is_none());
511 }
512}