Skip to main content

featherbit/acme/
challenge.rs

1//! TLS-ALPN-01 (RFC 8737) challenge solving.
2//!
3//! The CA opens a TLS connection to the domain on 443 with ALPN `acme-tls/1`
4//! and expects a self-signed certificate whose SAN is the domain and which
5//! carries a critical `acmeIdentifier` extension holding the SHA-256 of the key
6//! authorization. `server::tls`' resolver asks a [`ChallengeSolver`] for that
7//! certificate; [`TlsAlpnSolver`] builds it from the key authorization the
8//! order state machine registered.
9//!
10//! The resolver call is synchronous, so the solver keeps an in-process cache.
11//! Registrations also go to [`CertStorage`] so a *peer* instance (behind a TCP
12//! load balancer) can adopt them via [`TlsAlpnSolver::refresh_from_storage`].
13
14use 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";
26/// A pending challenge that outlives this is stale; storage and cache both drop it.
27pub const CHALLENGE_TTL: Duration = Duration::from_secs(600);
28
29pub trait ChallengeSolver: Send + Sync + std::fmt::Debug {
30    /// The challenge certificate for `server_name` (SNI, any case), if a
31    /// validation is pending for it.
32    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
41/// Self-signed challenge certificate: SAN = `domain`, critical `acmeIdentifier`
42/// = SHA-256(`key_auth`), fresh key, valid for the challenge TTL.
43pub 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    // Not `load_certified_key`: it calls `CertifiedKey::keys_match()`, which
57    // parses the leaf through rustls-webpki's strict `EndEntityCert`, and that
58    // rejects any certificate carrying a critical extension it doesn't
59    // recognize — exactly the critical `acmeIdentifier` this certificate
60    // exists to carry. Cert and key were just minted together above, so the
61    // match is already guaranteed; build the `CertifiedKey` directly instead.
62    certified_key_from_pem(&cert.pem(), &key.serialize_pem())
63}
64
65/// Like [`super::load_certified_key`] but skips `CertifiedKey::keys_match()`
66/// (see the comment in [`build_challenge_cert`] for why that check can't run
67/// on a challenge certificate).
68fn 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    /// Registers a pending validation: persisted (for peers) and cached (for
141    /// this instance's resolver).
142    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    /// Adopts (or evicts) challenges another instance registered in storage.
155    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    /// The `acmeIdentifier` extension (RFC 8737 §3): OID 1.3.6.1.5.5.7.1.31,
206    /// critical, value = DER OCTET STRING of the SHA-256 key-auth digest.
207    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}