postrust_proxy/tls/
sni.rs1use 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
32pub struct SniCertResolver {
34 keys: RwLock<HashMap<String, Arc<CertifiedKey>>>,
36 fallback: Option<Arc<CertifiedKey>>,
43}
44
45impl SniCertResolver {
46 pub fn new(fallback: Option<Arc<CertifiedKey>>) -> Self {
50 Self {
51 keys: RwLock::new(HashMap::new()),
52 fallback,
53 }
54 }
55
56 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 pub async fn refresh(&self, store: &CertificateStore) -> ProxyResult<usize> {
85 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 fn lookup(&self, name: &str) -> Option<Arc<CertifiedKey>> {
108 let keys = self.keys.read().unwrap_or_else(|e| e.into_inner());
109 let name = normalise(name);
113 if let Some(key) = keys.get(&name) {
114 return Some(key.clone());
115 }
116 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
128fn 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 None => self.fallback.clone(),
144 }
145 }
146}
147
148impl 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 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 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 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 assert!(resolver.lookup("a.b.example.com").is_none());
214 assert!(resolver.lookup("example.com").is_none());
216 }
217
218 #[test]
219 fn a_wildcard_does_not_swallow_a_public_suffix() {
220 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 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}