1use super::{Error, pool::Origin};
7use bytes::BytesMut;
8use nago_rustls::{
9 TlsSession,
10 rustls::{ClientConfig, ClientConnection, RootCertStore},
11 rustls_pki_types::ServerName,
12};
13use nagoya::{net::TcpStream, reactor::Handle};
14use ps_rustcrypto::{
15 fallback,
16 policy::{self, ct::CtVerifier},
17};
18use std::{
19 collections::HashMap,
20 sync::{Arc, Mutex, OnceLock},
21};
22
23const READ_CHUNK: usize = 16 * 1024;
24
25pub(super) fn default_config() -> Result<Arc<ClientConfig>, Error> {
26 static CONFIG: OnceLock<Result<Arc<ClientConfig>, String>> = OnceLock::new();
27 match CONFIG.get_or_init(build_config) {
28 Ok(config) => Ok(config.clone()),
29 Err(error) => Err(Error::Tls(error.clone())),
30 }
31}
32
33pub(super) fn rsa_fallback_config() -> Result<Arc<ClientConfig>, Error> {
39 static CONFIG: OnceLock<Result<Arc<ClientConfig>, String>> = OnceLock::new();
40 match CONFIG.get_or_init(build_rsa_fallback_config) {
41 Ok(config) => Ok(config.clone()),
42 Err(error) => Err(Error::Tls(error.clone())),
43 }
44}
45
46fn native_roots() -> Result<RootCertStore, String> {
47 let native = rustls_native_certs::load_native_certs();
48 let mut roots = RootCertStore::empty();
49 let (accepted, rejected) = roots.add_parsable_certificates(native.certs);
50 if accepted == 0 {
51 return Err(format!(
52 "no usable native TLS trust anchors: {} loading errors, {rejected} rejected certificates",
53 native.errors.len(),
54 ));
55 }
56 if !native.errors.is_empty() || rejected != 0 {
57 eprintln!(
58 "TLS trust store: {} loading errors, {rejected} rejected certificates",
59 native.errors.len(),
60 );
61 }
62 Ok(roots)
63}
64
65fn public_roots(native: &RootCertStore) -> RootCertStore {
69 let mut public = RootCertStore::empty();
70 public.roots = native
71 .roots
72 .iter()
73 .filter(|root| {
74 webpki_roots::TLS_SERVER_ROOTS.iter().any(|built_in| {
75 root.subject.as_ref() == built_in.subject.as_ref()
76 && root.subject_public_key_info.as_ref()
77 == built_in.subject_public_key_info.as_ref()
78 })
79 })
80 .cloned()
81 .collect();
82 public
83}
84
85fn build_rsa_fallback_config() -> Result<Arc<ClientConfig>, String> {
86 let roots = native_roots()?;
87 let public = public_roots(&roots);
88 let verifier = fallback::rsa_fallback_ct_verifier(roots, public)
89 .map_err(|error| format!("TLS trust store: {error}"))?;
90 let provider = Arc::new(ps_rustcrypto::provider_with_rsa_pss());
91 let mut config = ClientConfig::builder_with_provider(provider)
92 .with_safe_default_protocol_versions()
93 .map_err(|error| error.to_string())?
94 .dangerous()
95 .with_custom_certificate_verifier(verifier)
96 .with_no_client_auth();
97 config.alpn_protocols = alpn();
98 Ok(Arc::new(config))
99}
100
101pub(super) fn alpn() -> Vec<Vec<u8>> {
103 vec![b"h2".to_vec(), b"http/1.1".to_vec()]
104}
105
106fn build_config() -> Result<Arc<ClientConfig>, String> {
107 let roots = native_roots()?;
108 let public = public_roots(&roots);
109 let provider = Arc::new(ps_rustcrypto::provider());
111 let mut config = ClientConfig::builder_with_provider(provider)
112 .with_safe_default_protocol_versions()
113 .map_err(|error| error.to_string())?
114 .dangerous()
119 .with_custom_certificate_verifier(
120 CtVerifier::ecc_only(roots, public)
121 .map_err(|error| format!("TLS trust store: {error}"))?,
122 )
123 .with_no_client_auth();
124 config.alpn_protocols = alpn();
125 Ok(Arc::new(config))
126}
127
128static HOSTS: Mutex<Option<HashMap<String, HostStats>>> = Mutex::new(None);
135
136#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
138pub struct HostStats {
139 pub connections: u32,
141 pub refusals: u32,
143 pub fallbacks: u32,
145 pub last_rsa_fallback: Option<bool>,
147 pub last_alpn: Option<&'static str>,
150}
151
152fn update(host: &str, change: impl FnOnce(&mut HostStats)) -> HostStats {
153 let Ok(mut map) = HOSTS.lock() else {
154 return HostStats::default();
155 };
156 let stats = map
157 .get_or_insert_with(HashMap::new)
158 .entry(host.to_ascii_lowercase())
159 .or_default();
160 change(stats);
161 *stats
162}
163
164pub(super) fn record_alpn(host: &str, alpn: &'static str) {
165 update(host, |stats| stats.last_alpn = Some(alpn));
166}
167
168fn record(host: &str, used_fallback: bool) {
169 update(host, |stats| {
170 stats.last_rsa_fallback = Some(used_fallback);
171 stats.fallbacks += u32::from(used_fallback);
172 });
173}
174
175pub fn connection_stats() -> Vec<(String, HostStats)> {
177 let mut all: Vec<_> = HOSTS
178 .lock()
179 .ok()
180 .and_then(|map| {
181 map.as_ref()
182 .map(|map| map.iter().map(|(h, s)| (h.clone(), *s)).collect())
183 })
184 .unwrap_or_default();
185 all.sort_by(|a, b| a.0.cmp(&b.0));
186 all
187}
188
189pub fn rsa_fallback_state(host: &str) -> Option<bool> {
193 HOSTS
194 .lock()
195 .ok()?
196 .as_ref()?
197 .get(&host.to_ascii_lowercase())
198 .and_then(|stats| stats.last_rsa_fallback)
199}
200
201pub(super) async fn open(
202 origin: &Origin,
203 handle: &Handle,
204 config: Arc<ClientConfig>,
205 rsa_fallback: Option<Arc<ClientConfig>>,
206) -> Result<Transport, Error> {
207 if !origin.tls {
208 return Ok(Transport::Plain(connect(origin, handle).await?));
209 }
210 let error = match handshake(origin, handle, config).await? {
212 Ok(tls) => {
213 record(&origin.host, false);
214 return Ok(Transport::Tls(tls));
215 }
216 Err(error) => error,
217 };
218 let Some(rsa_fallback) = rsa_fallback.filter(|_| {
219 error
220 .rustls
221 .as_ref()
222 .is_some_and(fallback::wants_rsa_fallback)
223 }) else {
224 return Err(Error::Tls(error.message));
225 };
226 let host = &origin.host;
227 let reason = if error.rustls.as_ref().is_some_and(policy::is_rsa_refusal) {
228 "refused the RSA certificate chain, alert 43 unsupported_certificate sent"
229 } else if matches!(
230 error.rustls,
231 Some(nago_rustls::rustls::Error::AlertReceived(_))
232 ) {
233 "server rejected the ECC-only hello with alert 40 handshake_failure"
234 } else {
235 "server answered the ECC-only hello with a non-handshake record"
236 };
237 update(host, |stats| stats.refusals += 1);
238 info(format_args!("TLS {host}: ECC-only {reason}"));
239 info(format_args!(
240 "TLS {host}: RSA fallback, new connection allowing RSA-PSS CertificateVerify and RSA certificate signatures"
241 ));
242 match handshake(origin, handle, rsa_fallback).await? {
243 Ok(tls) => {
244 record(host, true);
245 Ok(Transport::Tls(tls))
246 }
247 Err(error) => {
248 info(format_args!(
251 "TLS {host}: RSA fallback failed: {}",
252 error.message
253 ));
254 Err(Error::Tls(format!("RSA fallback: {}", error.message)))
255 }
256 }
257}
258
259fn info(message: std::fmt::Arguments<'_>) {
260 #[cfg(feature = "tracing")]
261 tracing::info!("{message}");
262 #[cfg(not(feature = "tracing"))]
263 eprintln!("info: {message}");
264}
265
266struct Failed {
267 message: String,
268 rustls: Option<nago_rustls::rustls::Error>,
269}
270
271async fn connect(origin: &Origin, handle: &Handle) -> Result<TcpStream, Error> {
272 let host = origin.host.clone();
273 let port = origin.port;
274 let addresses = nagoya::spawn(async move {
275 nagoya::net::resolve(&host, port).map_err(|error| Error::Dns(error.to_string()))
276 })
277 .await
278 .ok_or_else(|| Error::Dns("resolver task was cancelled".to_owned()))??;
279
280 let stats = update(&origin.host, |stats| stats.connections += 1);
281 info(format_args!(
282 "net {}: new connection #{} (tls {})",
283 origin.host, stats.connections, origin.tls
284 ));
285 nagoya::net::connect_any(&addresses, handle)
286 .await
287 .map_err(|error| Error::Io(error.to_string()))
288}
289
290async fn handshake(
293 origin: &Origin,
294 handle: &Handle,
295 config: Arc<ClientConfig>,
296) -> Result<Result<Box<TlsSession<TcpStream>>, Failed>, Error> {
297 let stream = connect(origin, handle).await?;
298 let name = ServerName::try_from(origin.host.to_string())
299 .map_err(|_| Error::InvalidRequest("invalid TLS server name"))?;
300 let session =
301 ClientConnection::new(config, name).map_err(|error| Error::Tls(error.to_string()))?;
302 let mut tls = TlsSession::client(stream, session);
303 if let Err(errno) = tls.handshake().await {
304 return Ok(Err(Failed {
305 message: handshake_failure(tls.tls_error(), errno),
306 rustls: tls.tls_error().cloned(),
307 }));
308 }
309 if tls
310 .alpn_protocol()
311 .is_some_and(|protocol| protocol != b"http/1.1" && protocol != b"h2")
312 {
313 return Err(Error::Tls(
314 "peer negotiated an unsupported ALPN protocol".to_owned(),
315 ));
316 }
317 Ok(Ok(Box::new(tls)))
318}
319
320fn handshake_failure(
325 error: Option<&nago_rustls::rustls::Error>,
326 errno: impl std::fmt::Display,
327) -> String {
328 match error {
329 Some(error) if policy::is_rsa_refusal(error) => {
330 "site requires RSA, blocked by policy (alert 43 unsupported_certificate)".to_owned()
331 }
332 Some(error) if policy::is_key_type_refusal(error) => {
333 "site certificate key is not ECDSA or Ed25519, blocked by policy (alert 43 unsupported_certificate)".to_owned()
334 }
335 Some(error) if policy::ct::is_not_logged(error) => "certificate not publicly logged".to_owned(),
336 Some(error) => format!("handshake: {error}"),
337 None => format!("handshake: {errno}"),
338 }
339}
340
341pub(super) enum Transport {
342 Plain(TcpStream),
343 Tls(Box<TlsSession<TcpStream>>),
344}
345
346impl Transport {
347 pub async fn read_into(&mut self, buffer: &mut BytesMut) -> Result<usize, Error> {
348 match self {
349 Self::Plain(stream) => {
350 buffer.reserve(READ_CHUNK);
352 stream
353 .read_buf(buffer)
354 .await
355 .map_err(|error| Error::Io(error.to_string()))
356 }
357 Self::Tls(stream) => {
358 let start = buffer.len();
361 buffer.resize(start + READ_CHUNK, 0);
362 match stream.read(&mut buffer[start..]).await {
363 Ok(read) => {
364 buffer.truncate(start + read);
365 Ok(read)
366 }
367 Err(error) => {
368 buffer.truncate(start);
369 Err(Error::Io(error.to_string()))
370 }
371 }
372 }
373 }
374 }
375
376 pub async fn write_all(&mut self, bytes: &[u8]) -> Result<(), Error> {
377 match self {
378 Self::Plain(stream) => stream
379 .write_all(bytes)
380 .await
381 .map_err(|error| Error::Write(error.to_string())),
382 Self::Tls(stream) => stream
383 .write_all(bytes)
384 .await
385 .map_err(|error| Error::Write(error.to_string())),
386 }
387 }
388}