Skip to main content

featherbit/acme/storage/
mod.rs

1//! ACME state storage: account credentials, per-certificate key+chain, pending
2//! TLS-ALPN-01 key authorizations, and the renewal lease that keeps N gateway
3//! instances from ordering the same certificate at once.
4//!
5//! Backends: [`fs::FsCertStorage`] (single node, or a shared volume for certs —
6//! not for challenges) and, behind the `redis-store` feature,
7//! `redis::RedisCertStorage` (cluster-ready; keys sealed at rest).
8
9use std::time::Duration;
10
11use async_trait::async_trait;
12
13use super::{AcmeError, CertId, StoredCert};
14
15pub mod fs;
16#[cfg(feature = "redis-store")]
17pub mod redis;
18
19#[async_trait]
20pub trait CertStorage: Send + Sync {
21    /// Operator-facing name shown in the Admin API (`filesystem`, `store:<name>`).
22    fn label(&self) -> String;
23    async fn load_account(&self) -> Result<Option<Vec<u8>>, AcmeError>;
24    async fn save_account(&self, creds: &[u8]) -> Result<(), AcmeError>;
25    async fn load_cert(&self, id: &CertId) -> Result<Option<StoredCert>, AcmeError>;
26    async fn save_cert(&self, id: &CertId, cert: &StoredCert) -> Result<(), AcmeError>;
27    /// Registers `key_auth` for `domain`; expires after `ttl`.
28    async fn put_challenge(
29        &self,
30        domain: &str,
31        key_auth: &str,
32        ttl: Duration,
33    ) -> Result<(), AcmeError>;
34    async fn get_challenge(&self, domain: &str) -> Result<Option<String>, AcmeError>;
35    async fn remove_challenge(&self, domain: &str) -> Result<(), AcmeError>;
36    /// `true` when this `owner` now holds the lease for `id` (fresh or already
37    /// its own); `false` when another live owner holds it.
38    async fn try_acquire_lease(
39        &self,
40        id: &CertId,
41        owner: &str,
42        ttl: Duration,
43    ) -> Result<bool, AcmeError>;
44    /// Extends the lease iff `owner` holds it.
45    async fn renew_lease(&self, id: &CertId, owner: &str, ttl: Duration)
46        -> Result<bool, AcmeError>;
47    /// Releases iff `owner` holds it (a no-op otherwise).
48    async fn release_lease(&self, id: &CertId, owner: &str) -> Result<(), AcmeError>;
49}
50
51/// Backend-agnostic behavior every `CertStorage` must satisfy. Each backend's
52/// tests call `run_all`. TTL checks sleep ≥1 s because backends may keep
53/// second-resolution expiries.
54#[cfg(test)]
55pub(crate) mod contract {
56    use super::*;
57    use std::sync::Arc;
58
59    pub async fn run_all(storage: Arc<dyn CertStorage>) {
60        account_round_trip(&*storage).await;
61        cert_round_trip(&*storage).await;
62        challenge_ttl(&*storage).await;
63        lease_semantics(&*storage).await;
64    }
65
66    async fn account_round_trip(s: &dyn CertStorage) {
67        assert_eq!(s.load_account().await.unwrap(), None);
68        s.save_account(b"{\"id\":\"acct\"}").await.unwrap();
69        assert_eq!(
70            s.load_account().await.unwrap().unwrap(),
71            b"{\"id\":\"acct\"}"
72        );
73        s.save_account(b"v2").await.unwrap();
74        assert_eq!(s.load_account().await.unwrap().unwrap(), b"v2");
75    }
76
77    async fn cert_round_trip(s: &dyn CertStorage) {
78        let (id, _) =
79            CertId::from_domains(&["a.example.com".into(), "b.example.com".into()]).unwrap();
80        assert!(s.load_cert(&id).await.unwrap().is_none());
81        let cert = StoredCert {
82            chain_pem: "CHAIN".into(),
83            key_pem: "KEY".into(),
84            issued_at: 1_700_000_000,
85        };
86        s.save_cert(&id, &cert).await.unwrap();
87        assert_eq!(s.load_cert(&id).await.unwrap().unwrap(), cert);
88        let (other, _) = CertId::from_domains(&["c.example.com".into()]).unwrap();
89        assert!(s.load_cert(&other).await.unwrap().is_none());
90    }
91
92    async fn challenge_ttl(s: &dyn CertStorage) {
93        assert!(s.get_challenge("x.example.com").await.unwrap().is_none());
94        s.put_challenge("x.example.com", "tok.thumb", Duration::from_secs(60))
95            .await
96            .unwrap();
97        assert_eq!(
98            s.get_challenge("x.example.com").await.unwrap().unwrap(),
99            "tok.thumb"
100        );
101        s.remove_challenge("x.example.com").await.unwrap();
102        assert!(s.get_challenge("x.example.com").await.unwrap().is_none());
103        s.put_challenge("y.example.com", "old", Duration::from_millis(50))
104            .await
105            .unwrap();
106        tokio::time::sleep(Duration::from_millis(1_100)).await;
107        assert!(
108            s.get_challenge("y.example.com").await.unwrap().is_none(),
109            "expired reads as absent"
110        );
111    }
112
113    async fn lease_semantics(s: &dyn CertStorage) {
114        let (id, _) = CertId::from_domains(&["lease.example.com".into()]).unwrap();
115        let ttl = Duration::from_secs(30);
116        assert!(s.try_acquire_lease(&id, "me", ttl).await.unwrap());
117        assert!(!s.try_acquire_lease(&id, "peer", ttl).await.unwrap());
118        assert!(
119            s.try_acquire_lease(&id, "me", ttl).await.unwrap(),
120            "re-entrant for the owner"
121        );
122        assert!(s.renew_lease(&id, "me", ttl).await.unwrap());
123        assert!(!s.renew_lease(&id, "peer", ttl).await.unwrap());
124        s.release_lease(&id, "peer").await.unwrap(); // not the owner: no-op
125        assert!(!s.try_acquire_lease(&id, "peer", ttl).await.unwrap());
126        s.release_lease(&id, "me").await.unwrap();
127        assert!(s
128            .try_acquire_lease(&id, "peer", Duration::from_millis(50))
129            .await
130            .unwrap());
131        tokio::time::sleep(Duration::from_millis(1_100)).await;
132        assert!(
133            s.try_acquire_lease(&id, "me", ttl).await.unwrap(),
134            "expired lease is free"
135        );
136        s.release_lease(&id, "me").await.unwrap();
137    }
138}