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(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 = name.to_ascii_lowercase();
112 if let Some(key) = keys.get(&name) {
113 return Some(key.clone());
114 }
115 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 None => self.fallback.clone(),
134 }
135 }
136}
137
138impl 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 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 assert!(resolver.lookup("a.b.example.com").is_none());
191 assert!(resolver.lookup("example.com").is_none());
193 }
194
195 #[test]
196 fn a_wildcard_does_not_swallow_a_public_suffix() {
197 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 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}