Skip to main content

sova_acme/
storage.rs

1//! On-disk ACME account + certificate storage.
2
3use serde::{Deserialize, Serialize};
4use sova_core::{Error, Result};
5use std::path::PathBuf;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default)]
9pub struct CertMeta {
10    pub domains: Vec<String>,
11    pub not_after_unix: u64,
12    pub staging: bool,
13}
14
15#[derive(Clone, Debug)]
16pub struct AcmeStorage {
17    root: PathBuf,
18}
19
20impl AcmeStorage {
21    pub fn new(root: impl Into<PathBuf>) -> Self {
22        Self { root: root.into() }
23    }
24
25    pub fn cert_path(&self) -> PathBuf {
26        self.root.join("cert.pem")
27    }
28
29    pub fn key_path(&self) -> PathBuf {
30        self.root.join("key.pem")
31    }
32
33    pub fn account_path(&self) -> PathBuf {
34        self.root.join("account.json")
35    }
36
37    pub fn meta_path(&self) -> PathBuf {
38        self.root.join("meta.json")
39    }
40
41    pub fn ensure_dir(&self) -> Result<()> {
42        std::fs::create_dir_all(&self.root)
43            .map_err(|e| Error::Internal(format!("acme dir {}: {e}", self.root.display())))
44    }
45
46    pub fn has_cert(&self) -> bool {
47        self.cert_path().is_file() && self.key_path().is_file()
48    }
49
50    pub fn load_meta(&self) -> Option<CertMeta> {
51        let raw = std::fs::read_to_string(self.meta_path()).ok()?;
52        serde_json::from_str(&raw).ok()
53    }
54
55    pub fn save_meta(&self, meta: &CertMeta) -> Result<()> {
56        self.ensure_dir()?;
57        let raw = serde_json::to_string_pretty(meta)
58            .map_err(|e| Error::Internal(format!("acme meta serialize: {e}")))?;
59        std::fs::write(self.meta_path(), raw)
60            .map_err(|e| Error::Internal(format!("acme meta write: {e}")))
61    }
62
63    pub fn load_account_json(&self) -> Option<String> {
64        std::fs::read_to_string(self.account_path()).ok()
65    }
66
67    pub fn save_account_json(&self, json: &str) -> Result<()> {
68        self.ensure_dir()?;
69        std::fs::write(self.account_path(), json)
70            .map_err(|e| Error::Internal(format!("acme account write: {e}")))
71    }
72
73    pub fn write_pem(&self, cert_pem: &str, key_pem: &str) -> Result<()> {
74        self.ensure_dir()?;
75        std::fs::write(self.cert_path(), cert_pem)
76            .map_err(|e| Error::Internal(format!("acme cert write: {e}")))?;
77        std::fs::write(self.key_path(), key_pem)
78            .map_err(|e| Error::Internal(format!("acme key write: {e}")))
79    }
80}
81
82pub fn not_after_from_pem(cert_pem: &str) -> Option<u64> {
83    let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes()).ok()?;
84    let (_, cert) = x509_parser::parse_x509_certificate(pem.contents.as_ref()).ok()?;
85    let ts = cert.validity().not_after.timestamp();
86    if ts < 0 {
87        None
88    } else {
89        Some(ts as u64)
90    }
91}
92
93pub fn now_unix() -> u64 {
94    SystemTime::now()
95        .duration_since(UNIX_EPOCH)
96        .map(|d| d.as_secs())
97        .unwrap_or(0)
98}
99
100pub fn needs_renew(meta: &CertMeta, renew_days: u64) -> bool {
101    let now = now_unix();
102    let threshold = renew_days.saturating_mul(24 * 3600);
103    meta.not_after_unix.saturating_sub(now) <= threshold
104}