Skip to main content

postrust_proxy/tls/
sni.rs

1//! Per-domain certificate selection at handshake time.
2//!
3//! The HTTPS listener used to be built with `with_single_cert`, which answers
4//! every handshake with the same certificate whatever name the client asked
5//! for. That is fine for one domain and wrong for a multi-tenant proxy: ACME
6//! issued a certificate per tenant domain, stored it, renewed it, and nothing
7//! ever served it. This is the piece that reads them back.
8//!
9//! # Why there is a second cache here
10//!
11//! [`rustls::server::ResolvesServerCert::resolve`] is synchronous and runs
12//! inside the handshake. [`CertificateStore`] is asynchronous -- it reads a
13//! database and the filesystem -- so it cannot be consulted from `resolve`
14//! without blocking a runtime thread on every connection.
15//!
16//! So certificates are parsed into handshake-ready [`CertifiedKey`]s ahead of
17//! time and held in a `std` lock that `resolve` can read directly. The cost is
18//! that a newly issued certificate is served once the next refresh has run, or
19//! immediately if whoever issued it calls [`SniCertResolver::refresh`].
20
21use std::collections::HashMap;
22use std::sync::{Arc, RwLock};
23
24use tokio_rustls::rustls::crypto::aws_lc_rs::sign::any_supported_type;
25use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer};
26use tokio_rustls::rustls::server::{ClientHello, ResolvesServerCert};
27use tokio_rustls::rustls::sign::CertifiedKey;
28
29use crate::error::{ProxyError, ProxyResult};
30use crate::tls::cert_store::CertificateStore;
31
32/// Resolves a certificate from the store by the SNI name of the handshake.
33pub struct SniCertResolver {
34    /// Parsed and ready to serve, by the domain they were issued for.
35    keys: RwLock<HashMap<String, Arc<CertifiedKey>>>,
36    /// Served when SNI names nothing we hold, and when the client sends no
37    /// SNI at all -- an IP-address client, or an old one.
38    ///
39    /// Without a fallback such a handshake is refused, which is the correct
40    /// answer but a confusing one if the operator also configured a static
41    /// certificate and expected it to work.
42    fallback: Option<Arc<CertifiedKey>>,
43}
44
45impl SniCertResolver {
46    /// Build a resolver holding nothing but the fallback.
47    ///
48    /// Call [`refresh`](Self::refresh) to populate it from the store.
49    pub fn new(fallback: Option<Arc<CertifiedKey>>) -> Self {
50        Self {
51            keys: RwLock::new(HashMap::new()),
52            fallback,
53        }
54    }
55
56    /// Build the fallback from the statically configured PEM pair.
57    pub fn certified_key(cert_pem: &[u8], key_pem: &[u8]) -> ProxyResult<Arc<CertifiedKey>> {
58        crate::tls::server::install_crypto_provider();
59
60        let certs: Vec<CertificateDer<'static>> =
61            rustls_pemfile::certs(&mut std::io::BufReader::new(cert_pem))
62                .filter_map(|r| r.ok())
63                .collect();
64        if certs.is_empty() {
65            return Err(ProxyError::Tls("no certificates found in PEM".into()));
66        }
67
68        let key: PrivateKeyDer<'static> =
69            rustls_pemfile::private_key(&mut std::io::BufReader::new(key_pem))
70                .map_err(|e| ProxyError::Tls(format!("could not parse private key: {e}")))?
71                .ok_or_else(|| ProxyError::Tls("no private key found in PEM".into()))?;
72
73        let signing_key = any_supported_type(&key)
74            .map_err(|e| ProxyError::Tls(format!("unsupported private key: {e}")))?;
75
76        Ok(Arc::new(CertifiedKey::new(certs, signing_key)))
77    }
78
79    /// Reload every certificate the store holds.
80    ///
81    /// Returns how many are now being served. A certificate that will not
82    /// parse is logged and skipped rather than failing the refresh: one bad
83    /// row must not take every other tenant's TLS down with it.
84    pub async fn refresh(&self, store: &CertificateStore) -> ProxyResult<usize> {
85        // `load_all`, not `get` per domain: `get` answers from the store's own
86        // memory cache, so a certificate another instance renewed would never
87        // be seen here.
88        let certificates = store.load_all().await?;
89        let mut fresh = HashMap::with_capacity(certificates.len());
90
91        for cert in certificates {
92            match Self::certified_key(&cert.cert_pem, &cert.key_pem) {
93                Ok(key) => {
94                    fresh.insert(cert.domain, key);
95                }
96                Err(e) => tracing::warn!("Certificate for {} will not load: {}", cert.domain, e),
97            }
98        }
99
100        let count = fresh.len();
101        *self.keys.write().unwrap_or_else(|e| e.into_inner()) = fresh;
102        tracing::info!("Serving {} certificate(s) by SNI", count);
103        Ok(count)
104    }
105
106    /// Look up a name, exactly and then as the wildcard that would cover it.
107    fn lookup(&self, name: &str) -> Option<Arc<CertifiedKey>> {
108        let keys = self.keys.read().unwrap_or_else(|e| e.into_inner());
109        // SNI is case-insensitive; a certificate may have been stored either
110        // way, so both sides are lowered before comparing.
111        let name = name.to_ascii_lowercase();
112        if let Some(key) = keys.get(&name) {
113            return Some(key.clone());
114        }
115        // `*.example.com` covers `a.example.com` and, per RFC 6125, exactly one
116        // label -- not `a.b.example.com`, and not the bare `example.com`.
117        let (_, parent) = name.split_once('.')?;
118        if parent.contains('.') {
119            if let Some(key) = keys.get(&format!("*.{parent}")) {
120                return Some(key.clone());
121            }
122        }
123        None
124    }
125}
126
127impl ResolvesServerCert for SniCertResolver {
128    fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
129        match client_hello.server_name() {
130            Some(name) => self.lookup(name).or_else(|| self.fallback.clone()),
131            // No SNI. There is no name to match, so the fallback is the only
132            // honest answer.
133            None => self.fallback.clone(),
134        }
135    }
136}
137
138/// Names the domains held, never the keys. A private key that reaches a log is
139/// a private key that has to be rotated.
140impl std::fmt::Debug for SniCertResolver {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        let keys = self.keys.read().unwrap_or_else(|e| e.into_inner());
143        f.debug_struct("SniCertResolver")
144            .field("domains", &keys.keys().collect::<Vec<_>>())
145            .field("has_fallback", &self.fallback.is_some())
146            .finish()
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    // The fixtures `tls::validate` already uses; see testdata/README.md.
155    // Which name a fixture covers does not matter here -- this resolver is
156    // keyed by the domain a certificate was *stored* under, and checking that
157    // the chain matches that name is `tls::validate`'s job, done at upload.
158    const CERT: &[u8] = include_bytes!("testdata/valid.pem");
159    const KEY: &[u8] = include_bytes!("testdata/valid.key.pem");
160
161    fn resolver_with(domains: &[&str]) -> SniCertResolver {
162        let resolver = SniCertResolver::new(None);
163        let key = SniCertResolver::certified_key(CERT, KEY).unwrap();
164        let keys = domains
165            .iter()
166            .map(|d| (d.to_string(), key.clone()))
167            .collect();
168        *resolver.keys.write().unwrap() = keys;
169        resolver
170    }
171
172    #[test]
173    fn an_exact_name_is_found() {
174        let resolver = resolver_with(&["a.example.com"]);
175        assert!(resolver.lookup("a.example.com").is_some());
176        assert!(resolver.lookup("b.example.com").is_none());
177    }
178
179    #[test]
180    fn sni_is_matched_case_insensitively() {
181        let resolver = resolver_with(&["a.example.com"]);
182        assert!(resolver.lookup("A.Example.COM").is_some());
183    }
184
185    #[test]
186    fn a_wildcard_covers_exactly_one_label() {
187        let resolver = resolver_with(&["*.example.com"]);
188        assert!(resolver.lookup("a.example.com").is_some());
189        // Two labels down is not covered by a single wildcard.
190        assert!(resolver.lookup("a.b.example.com").is_none());
191        // Nor is the bare parent domain.
192        assert!(resolver.lookup("example.com").is_none());
193    }
194
195    #[test]
196    fn a_wildcard_does_not_swallow_a_public_suffix() {
197        // `*.com` must not be reachable by asking for `example.com`, or one
198        // stored certificate would answer for every domain under it.
199        let resolver = resolver_with(&["*.com"]);
200        assert!(resolver.lookup("example.com").is_none());
201    }
202
203    #[test]
204    fn the_fallback_answers_when_nothing_matches() {
205        let fallback = SniCertResolver::certified_key(CERT, KEY).unwrap();
206        let resolver = SniCertResolver::new(Some(fallback));
207        // Nothing is loaded, so every name falls back.
208        assert!(resolver.lookup("anything.example.com").is_none());
209        assert!(resolver.fallback.is_some());
210    }
211
212    #[test]
213    fn a_pem_that_will_not_parse_is_an_error_not_a_panic() {
214        assert!(SniCertResolver::certified_key(b"nonsense", b"nonsense").is_err());
215    }
216}