Skip to main content

blitz_net/
tls.rs

1//! Native trust anchors and nagoya socket transport.
2//!
3//! DNS always runs on a nagoya worker, including when the fetch future is
4//! polled directly by a caller or a local reactor.
5
6use 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
33/// The RSA fallback policy, for a second connection after an ECC-only refusal.
34///
35/// Every native root, RSA ones included; certificate signatures may be RSA
36/// PKCS#1 v1.5 and the server CertificateVerify RSA-PSS. CT still applies to
37/// chains ending in a public root.
38pub(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
65/// The native roots that are also built-in public roots, matched by subject
66/// and key. Like Chrome, CT is enforced only on chains ending in one of
67/// these; locally added anchors (enterprise or test CAs) skip it.
68fn 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
101/// HTTP/2 preferred; HTTP/1.1 for servers that do not offer it.
102pub(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    // Explicit even when an embedder installed another process default.
110    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        // ECC-only: a chain holding an RSA key anywhere, anchor included, is
115        // refused with unsupported_certificate before any signature check.
116        // A valid chain ending in a public root must then be publicly
117        // logged under the CT policy.
118        .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
128/// How the TLS connection to each host most recently went: `true` when it
129/// needed the RSA fallback, `false` when ECC-only succeeded.
130///
131/// A record for display and inspection only. It is never consulted before a
132/// handshake: every new connection is ECC-only first, so the server sees the
133/// refusal (alert 43) each time.
134static HOSTS: Mutex<Option<HashMap<String, HostStats>>> = Mutex::new(None);
135
136/// Connection counters for one host, since the process started.
137#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
138pub struct HostStats {
139    /// New TCP connections opened, TLS attempts and fallbacks included.
140    pub connections: u32,
141    /// ECC-only handshakes that ended in the RSA refusal or alert 40.
142    pub refusals: u32,
143    /// RSA fallback connections that completed.
144    pub fallbacks: u32,
145    /// Whether the latest completed TLS connection used the RSA fallback.
146    pub last_rsa_fallback: Option<bool>,
147    /// The protocol the latest connection to this host settled on: `h2`,
148    /// `h2 (coalesced)` onto another host's connection, or `http/1.1`.
149    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
175/// Connection counters for every host contacted so far, sorted by host.
176pub 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
189/// Whether the latest TLS connection to `host` used the RSA fallback.
190///
191/// `None` when no TLS connection to it has completed.
192pub 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    // ECC-only first, on every new connection. Never skipped from memory.
211    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            // Logged here as well as returned: a page that drops the error
249            // otherwise leaves the host looking like a stalled connection.
250            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
290/// One TLS connection attempt. The outer error is a transport failure; the
291/// inner one is the handshake failing, with the rustls error kept.
292async 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
320/// Why a handshake failed, in words a page can show.
321///
322/// The errno only says the session is over; the rustls error kept by the
323/// session says why.
324fn 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                // Read directly into the destination's spare capacity.
351                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                // TlsSession exposes an initialised plaintext slice rather
359                // than a read_buf operation. Avoid a second staging buffer.
360                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}