featherbit/acme/
challenge.rs1use std::collections::HashMap;
15use std::sync::{Arc, RwLock};
16use std::time::{Duration, Instant};
17
18use rustls::pki_types::pem::PemObject;
19use rustls::pki_types::{CertificateDer, PrivateKeyDer};
20use rustls::sign::CertifiedKey;
21
22use super::storage::CertStorage;
23use super::AcmeError;
24
25pub const ACME_TLS_ALPN: &[u8] = b"acme-tls/1";
26pub const CHALLENGE_TTL: Duration = Duration::from_secs(600);
28
29pub trait ChallengeSolver: Send + Sync + std::fmt::Debug {
30 fn challenge_cert(&self, server_name: &str) -> Option<Arc<CertifiedKey>>;
33}
34
35pub fn key_authorization_digest(key_auth: &str) -> Vec<u8> {
36 ring::digest::digest(&ring::digest::SHA256, key_auth.as_bytes())
37 .as_ref()
38 .to_vec()
39}
40
41pub fn build_challenge_cert(domain: &str, key_auth: &str) -> Result<Arc<CertifiedKey>, AcmeError> {
44 let mut params = rcgen::CertificateParams::new(vec![domain.to_string()])
45 .map_err(|e| AcmeError::Crypto(e.to_string()))?;
46 let mut ext = rcgen::CustomExtension::new_acme_identifier(&key_authorization_digest(key_auth));
47 ext.set_criticality(true);
48 params.custom_extensions = vec![ext];
49 let now = time::OffsetDateTime::now_utc();
50 params.not_before = now - time::Duration::minutes(5);
51 params.not_after = now + time::Duration::seconds(CHALLENGE_TTL.as_secs() as i64 + 300);
52 let key = rcgen::KeyPair::generate().map_err(|e| AcmeError::Crypto(e.to_string()))?;
53 let cert = params
54 .self_signed(&key)
55 .map_err(|e| AcmeError::Crypto(e.to_string()))?;
56 certified_key_from_pem(&cert.pem(), &key.serialize_pem())
63}
64
65fn certified_key_from_pem(chain_pem: &str, key_pem: &str) -> Result<Arc<CertifiedKey>, AcmeError> {
69 let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(chain_pem.as_bytes())
70 .collect::<Result<Vec<_>, _>>()
71 .map_err(|e| AcmeError::Certificate(format!("chain PEM: {e}")))?;
72 if chain.is_empty() {
73 return Err(AcmeError::Certificate(
74 "chain PEM contains no certificates".into(),
75 ));
76 }
77 let key = PrivateKeyDer::from_pem_slice(key_pem.as_bytes())
78 .map_err(|e| AcmeError::Certificate(format!("key PEM: {e}")))?;
79 let signing_key = super::provider()
80 .key_provider
81 .load_private_key(key)
82 .map_err(|e| AcmeError::Certificate(format!("certificate/key: {e}")))?;
83 Ok(Arc::new(CertifiedKey::new(chain, signing_key)))
84}
85
86struct Cached {
87 key_auth: String,
88 cert: Arc<CertifiedKey>,
89 expires_at: Instant,
90}
91
92pub struct TlsAlpnSolver {
93 storage: Arc<dyn CertStorage>,
94 cache: RwLock<HashMap<String, Cached>>,
95 ttl: Duration,
96}
97
98impl std::fmt::Debug for TlsAlpnSolver {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 write!(
101 f,
102 "TlsAlpnSolver({} pending)",
103 self.cache.read().map(|c| c.len()).unwrap_or(0)
104 )
105 }
106}
107
108impl TlsAlpnSolver {
109 pub fn new(storage: Arc<dyn CertStorage>) -> Arc<Self> {
110 Self::with_ttl(storage, CHALLENGE_TTL)
111 }
112
113 pub fn with_ttl(storage: Arc<dyn CertStorage>, ttl: Duration) -> Arc<Self> {
114 Arc::new(Self {
115 storage,
116 cache: RwLock::new(HashMap::new()),
117 ttl,
118 })
119 }
120
121 fn cache_insert(&self, domain: &str, key_auth: &str) -> Result<(), AcmeError> {
122 let cert = build_challenge_cert(domain, key_auth)?;
123 let mut cache = self.cache.write().unwrap_or_else(|e| e.into_inner());
124 cache.insert(
125 domain.to_ascii_lowercase(),
126 Cached {
127 key_auth: key_auth.to_string(),
128 cert,
129 expires_at: Instant::now() + self.ttl,
130 },
131 );
132 Ok(())
133 }
134
135 fn cache_remove(&self, domain: &str) {
136 let mut cache = self.cache.write().unwrap_or_else(|e| e.into_inner());
137 cache.remove(&domain.to_ascii_lowercase());
138 }
139
140 pub async fn register(&self, domain: &str, key_auth: &str) -> Result<(), AcmeError> {
143 self.storage
144 .put_challenge(domain, key_auth, self.ttl)
145 .await?;
146 self.cache_insert(domain, key_auth)
147 }
148
149 pub async fn clear(&self, domain: &str) -> Result<(), AcmeError> {
150 self.cache_remove(domain);
151 self.storage.remove_challenge(domain).await
152 }
153
154 pub async fn refresh_from_storage(&self, domains: &[String]) -> Result<(), AcmeError> {
156 for domain in domains {
157 match self.storage.get_challenge(domain).await? {
158 Some(key_auth) => {
159 let already = {
160 let cache = self.cache.read().unwrap_or_else(|e| e.into_inner());
161 cache
162 .get(&domain.to_ascii_lowercase())
163 .is_some_and(|c| c.key_auth == key_auth)
164 };
165 if !already {
166 self.cache_insert(domain, &key_auth)?;
167 }
168 }
169 None => self.cache_remove(domain),
170 }
171 }
172 Ok(())
173 }
174
175 #[cfg(test)]
176 pub fn cached_domains(&self) -> Vec<String> {
177 let now = Instant::now();
178 let cache = self.cache.read().unwrap_or_else(|e| e.into_inner());
179 let mut v: Vec<String> = cache
180 .iter()
181 .filter(|(_, c)| c.expires_at > now)
182 .map(|(d, _)| d.clone())
183 .collect();
184 v.sort();
185 v
186 }
187}
188
189impl ChallengeSolver for TlsAlpnSolver {
190 fn challenge_cert(&self, server_name: &str) -> Option<Arc<CertifiedKey>> {
191 let name = server_name.to_ascii_lowercase();
192 let cache = self.cache.read().unwrap_or_else(|e| e.into_inner());
193 match cache.get(&name) {
194 Some(c) if c.expires_at > Instant::now() => Some(c.cert.clone()),
195 _ => None,
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::acme::storage::fs::FsCertStorage;
204
205 fn acme_identifier(leaf_der: &[u8]) -> Option<(bool, Vec<u8>)> {
208 use x509_parser::prelude::*;
209 let (_, cert) = X509Certificate::from_der(leaf_der).unwrap();
210 cert.extensions()
211 .iter()
212 .find(|e| e.oid.to_id_string() == "1.3.6.1.5.5.7.1.31")
213 .map(|e| (e.critical, e.value.to_vec()))
214 }
215
216 fn temp_storage(tag: &str) -> Arc<dyn CertStorage> {
217 let d = std::env::temp_dir().join(format!("fb_acme_ch_{}_{}", tag, std::process::id()));
218 let _ = std::fs::remove_dir_all(&d);
219 Arc::new(FsCertStorage::new(d))
220 }
221
222 #[test]
223 fn challenge_cert_carries_critical_acme_identifier_with_digest() {
224 let ck = build_challenge_cert("x.example.com", "token.thumbprint").unwrap();
225 let leaf = ck.end_entity_cert().unwrap();
226 let (critical, value) = acme_identifier(leaf.as_ref()).expect("extension present");
227 assert!(critical);
228 let digest = key_authorization_digest("token.thumbprint");
229 assert_eq!(value.len(), 34);
230 assert_eq!(&value[..2], &[0x04, 0x20]);
231 assert_eq!(&value[2..], digest.as_slice());
232 assert_eq!(
233 crate::acme::leaf_dns_sans(leaf.as_ref()).unwrap(),
234 vec!["x.example.com".to_string()]
235 );
236 }
237
238 #[tokio::test]
239 async fn register_serves_then_clear_stops() {
240 let solver = TlsAlpnSolver::new(temp_storage("reg"));
241 assert!(solver.challenge_cert("a.example.com").is_none());
242 solver.register("a.example.com", "ka").await.unwrap();
243 assert!(solver.challenge_cert("a.example.com").is_some());
244 assert!(
245 solver.challenge_cert("A.EXAMPLE.COM").is_some(),
246 "SNI is case-insensitive"
247 );
248 assert!(solver.challenge_cert("b.example.com").is_none());
249 assert_eq!(solver.cached_domains(), vec!["a.example.com".to_string()]);
250 solver.clear("a.example.com").await.unwrap();
251 assert!(solver.challenge_cert("a.example.com").is_none());
252 }
253
254 #[tokio::test]
255 async fn refresh_from_storage_picks_up_a_peer_registration() {
256 let storage = temp_storage("peer");
257 let peer = TlsAlpnSolver::new(storage.clone());
258 let me = TlsAlpnSolver::new(storage);
259 peer.register("p.example.com", "peer-ka").await.unwrap();
260 assert!(me.challenge_cert("p.example.com").is_none());
261 me.refresh_from_storage(&["p.example.com".to_string(), "none.example.com".to_string()])
262 .await
263 .unwrap();
264 let ck = me
265 .challenge_cert("p.example.com")
266 .expect("adopted from storage");
267 let (_, value) = acme_identifier(ck.end_entity_cert().unwrap().as_ref()).unwrap();
268 assert_eq!(&value[2..], key_authorization_digest("peer-ka").as_slice());
269 peer.clear("p.example.com").await.unwrap();
270 me.refresh_from_storage(&["p.example.com".to_string()])
271 .await
272 .unwrap();
273 assert!(
274 me.challenge_cert("p.example.com").is_none(),
275 "cleared upstream ⇒ evicted"
276 );
277 }
278
279 #[tokio::test]
280 async fn cache_entries_expire() {
281 let solver = TlsAlpnSolver::with_ttl(temp_storage("ttl"), Duration::from_millis(30));
282 solver.register("t.example.com", "ka").await.unwrap();
283 assert!(solver.challenge_cert("t.example.com").is_some());
284 tokio::time::sleep(Duration::from_millis(60)).await;
285 assert!(solver.challenge_cert("t.example.com").is_none());
286 }
287}