featherbit/acme/storage/
mod.rs1use 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 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 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 async fn try_acquire_lease(
39 &self,
40 id: &CertId,
41 owner: &str,
42 ttl: Duration,
43 ) -> Result<bool, AcmeError>;
44 async fn renew_lease(&self, id: &CertId, owner: &str, ttl: Duration)
46 -> Result<bool, AcmeError>;
47 async fn release_lease(&self, id: &CertId, owner: &str) -> Result<(), AcmeError>;
49}
50
51#[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(); 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}