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(normalise(&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 normalised the same way -- here, and in
111        // `refresh` as the table is built.
112        let name = normalise(name);
113        if let Some(key) = keys.get(&name) {
114            return Some(key.clone());
115        }
116        // `*.example.com` covers `a.example.com` and, per RFC 6125, exactly one
117        // label -- not `a.b.example.com`, and not the bare `example.com`.
118        let (_, parent) = name.split_once('.')?;
119        if parent.contains('.') {
120            if let Some(key) = keys.get(&format!("*.{parent}")) {
121                return Some(key.clone());
122            }
123        }
124        None
125    }
126}
127
128/// The form names are compared in: lowercase, without a trailing dot.
129///
130/// Applied to both sides. The lookup used to lower the client's name but not
131/// the stored one, so a certificate stored under `Example.com` was never
132/// served to anyone.
133fn normalise(name: &str) -> String {
134    name.trim_end_matches('.').to_ascii_lowercase()
135}
136
137impl ResolvesServerCert for SniCertResolver {
138    fn resolve(&self, client_hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
139        match client_hello.server_name() {
140            Some(name) => self.lookup(name).or_else(|| self.fallback.clone()),
141            // No SNI. There is no name to match, so the fallback is the only
142            // honest answer.
143            None => self.fallback.clone(),
144        }
145    }
146}
147
148/// Names the domains held, never the keys. A private key that reaches a log is
149/// a private key that has to be rotated.
150impl std::fmt::Debug for SniCertResolver {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        let keys = self.keys.read().unwrap_or_else(|e| e.into_inner());
153        f.debug_struct("SniCertResolver")
154            .field("domains", &keys.keys().collect::<Vec<_>>())
155            .field("has_fallback", &self.fallback.is_some())
156            .finish()
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    // The fixtures `tls::validate` already uses; see testdata/README.md.
165    // Which name a fixture covers does not matter here -- this resolver is
166    // keyed by the domain a certificate was *stored* under, and checking that
167    // the chain matches that name is `tls::validate`'s job, done at upload.
168    const CERT: &[u8] = include_bytes!("testdata/valid.pem");
169    const KEY: &[u8] = include_bytes!("testdata/valid.key.pem");
170
171    fn resolver_with(domains: &[&str]) -> SniCertResolver {
172        let resolver = SniCertResolver::new(None);
173        let key = SniCertResolver::certified_key(CERT, KEY).unwrap();
174        // Through `normalise`, as `refresh` builds the table.
175        let keys = domains
176            .iter()
177            .map(|d| (normalise(d), key.clone()))
178            .collect();
179        *resolver.keys.write().unwrap() = keys;
180        resolver
181    }
182
183    #[test]
184    fn an_exact_name_is_found() {
185        let resolver = resolver_with(&["a.example.com"]);
186        assert!(resolver.lookup("a.example.com").is_some());
187        assert!(resolver.lookup("b.example.com").is_none());
188    }
189
190    #[test]
191    fn sni_is_matched_case_insensitively() {
192        let resolver = resolver_with(&["a.example.com"]);
193        assert!(resolver.lookup("A.Example.COM").is_some());
194    }
195
196    #[test]
197    fn a_certificate_stored_with_uppercase_or_a_trailing_dot_is_served() {
198        // Stored verbatim and looked up lowercased, `Example.COM` was never
199        // matched by anything.
200        let resolver = resolver_with(&["A.Example.COM", "b.example.com.", "*.Wild.Example.com"]);
201        assert!(resolver.lookup("a.example.com").is_some());
202        assert!(resolver.lookup("A.EXAMPLE.COM").is_some());
203        assert!(resolver.lookup("b.example.com").is_some());
204        assert!(resolver.lookup("b.example.com.").is_some());
205        assert!(resolver.lookup("x.wild.example.com").is_some());
206    }
207
208    #[test]
209    fn a_wildcard_covers_exactly_one_label() {
210        let resolver = resolver_with(&["*.example.com"]);
211        assert!(resolver.lookup("a.example.com").is_some());
212        // Two labels down is not covered by a single wildcard.
213        assert!(resolver.lookup("a.b.example.com").is_none());
214        // Nor is the bare parent domain.
215        assert!(resolver.lookup("example.com").is_none());
216    }
217
218    #[test]
219    fn a_wildcard_does_not_swallow_a_public_suffix() {
220        // `*.com` must not be reachable by asking for `example.com`, or one
221        // stored certificate would answer for every domain under it.
222        let resolver = resolver_with(&["*.com"]);
223        assert!(resolver.lookup("example.com").is_none());
224    }
225
226    #[test]
227    fn the_fallback_answers_when_nothing_matches() {
228        let fallback = SniCertResolver::certified_key(CERT, KEY).unwrap();
229        let resolver = SniCertResolver::new(Some(fallback));
230        // Nothing is loaded, so every name falls back.
231        assert!(resolver.lookup("anything.example.com").is_none());
232        assert!(resolver.fallback.is_some());
233    }
234
235    #[test]
236    fn a_pem_that_will_not_parse_is_an_error_not_a_panic() {
237        assert!(SniCertResolver::certified_key(b"nonsense", b"nonsense").is_err());
238    }
239}