dwara_core/security/
tls.rs

1//! TLS machinery for listeners (DW-007, feature analysis 4.10 / 4.13).
2//!
3//! Responsibilities:
4//!
5//! - process-global crypto provider installation (aws-lc-rs; must happen
6//!   before the first rustls object is created),
7//! - building a hot-reloadable `rustls::ServerConfig` from a
8//!   [`ListenerTls`] terminate block, with SNI-based certificate selection
9//!   (the single `cert_file`/`key_file` pair is the fallback certificate,
10//!   `certificates` entries are matched by SNI),
11//! - outbound TRUST for https dials (#121): the default webpki public
12//!   root set, PEM-bundle root stores for the `trusted_ca_file` config
13//!   field (private CAs), and the shared HTTP/1.1 client-config shape
14//!   used by upstream connectors, active health probes, and the JWKS
15//!   fetcher,
16//! - a minimal TLS ClientHello SNI parser used by passthrough routing,
17//!   and the passthrough byte-splice itself.
18//!
19//! Hot reload model: [`TlsTermination`] keeps the current
20//! `Arc<rustls::ServerConfig>` behind an `ArcSwap`. Each accepted
21//! connection clones the current `Arc` into a fresh `TlsAcceptor`, so a
22//! swap only affects handshakes that start after it: in-flight TLS
23//! sessions keep their negotiated configuration and existing connections
24//! are never dropped by a reload.
25//!
26//! Documented v1 limitation: TLS 1.3 and 1.2 are both enabled with
27//! rustls's default (modern) cipher-suite policy; no cipher/version
28//! overrides are configurable yet.
29
30use std::collections::BTreeMap;
31use std::path::PathBuf;
32use std::sync::Arc;
33
34use arc_swap::ArcSwap;
35use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
36use rustls::server::{ClientHello, ResolvesServerCert};
37use rustls::sign::CertifiedKey;
38use rustls::ServerConfig;
39use tokio::io::AsyncWriteExt;
40use tokio::net::TcpStream;
41use zeroize::Zeroizing;
42
43use crate::config::{Gateway, ListenerTls, SniRoute};
44
45/// Error loading or building TLS material.
46#[derive(Debug)]
47pub enum TlsError {
48    Io(std::io::Error),
49    /// A PEM file parsed to zero usable items.
50    EmptyPem {
51        path: PathBuf,
52        what: &'static str,
53    },
54    Rustls(rustls::Error),
55    /// A certificate from a trusted-CA PEM file was rejected by the
56    /// root store (not usable as a trust anchor).
57    RootUnusable(String),
58    /// The admin mTLS client-CA verifier could not be built (root
59    /// loading or verifier construction failed).
60    ClientAuth(String),
61    /// No certificate material at all in a terminate block.
62    NoCertificates,
63    /// The private key does not match the leaf certificate (public keys
64    /// differ). Detected at build AND reload time so a torn cert/key pair
65    /// on disk can never be swapped into the live configuration.
66    KeyMismatch {
67        cert_file: PathBuf,
68        key_file: PathBuf,
69    },
70}
71
72impl std::fmt::Display for TlsError {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            TlsError::Io(e) => write!(f, "tls file error: {e}"),
76            TlsError::EmptyPem { path, what } => {
77                write!(f, "no {what} found in PEM file {}", path.display())
78            }
79            TlsError::Rustls(e) => write!(f, "tls error: {e}"),
80            TlsError::RootUnusable(m) => {
81                write!(f, "certificate not usable as a trust anchor: {m}")
82            }
83            TlsError::ClientAuth(m) => write!(f, "admin client-auth setup failed: {m}"),
84            TlsError::NoCertificates => {
85                write!(f, "tls terminate block has no certificate material")
86            }
87            TlsError::KeyMismatch {
88                cert_file,
89                key_file,
90            } => write!(
91                f,
92                "private key {} does not match leaf certificate {}",
93                key_file.display(),
94                cert_file.display()
95            ),
96        }
97    }
98}
99
100impl std::error::Error for TlsError {}
101
102impl From<std::io::Error> for TlsError {
103    fn from(e: std::io::Error) -> Self {
104        TlsError::Io(e)
105    }
106}
107
108impl From<rustls::Error> for TlsError {
109    fn from(e: rustls::Error) -> Self {
110        TlsError::Rustls(e)
111    }
112}
113
114/// Install the aws-lc-rs crypto provider as the process-global rustls
115/// default. Idempotent: installing twice (e.g. binary + tests in one
116/// process) returns Ok the first time and an ignorable error afterwards.
117///
118/// When the `fips` cargo feature is ON (DW-111), the binary calls
119/// [`crate::security::fips::install_fips_provider`] instead, which
120/// installs the same aws-lc-rs provider (the FIPS-validated code path)
121/// and then runs the self-test to confirm. This function remains the
122/// default install path for non-FIPS builds.
123pub fn install_aws_lc_rs_provider() {
124    // install_default returns Err(previous provider) when one is already
125    // installed; that is the idempotent success case for us.
126    let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
127}
128
129/// DW-111: build a FIPS-restricted `CryptoProvider` from the aws-lc-rs
130/// default provider, filtering the cipher suite list to the FIPS-approved
131/// allowlist in [`crate::security::fips::FIPS_ALLOWED_CIPHERS`]. The
132/// returned provider is identical to the aws-lc-rs default except its
133/// `cipher_suites` vector contains only the FIPS-approved suites. A suite
134/// in the allowlist that the provider does not support is silently
135/// skipped (the provider's support is the ground truth; the allowlist is
136/// the restriction).
137fn fips_provider() -> rustls::crypto::CryptoProvider {
138    use crate::security::fips::FIPS_ALLOWED_CIPHERS;
139
140    let provider = rustls::crypto::aws_lc_rs::default_provider();
141    let suites: Vec<rustls::SupportedCipherSuite> = provider
142        .cipher_suites
143        .iter()
144        .filter(|suite| {
145            let name = format!("{:?}", suite.suite()).to_ascii_lowercase();
146            FIPS_ALLOWED_CIPHERS.contains(&name.as_str())
147        })
148        .copied()
149        .collect();
150    rustls::crypto::CryptoProvider {
151        cipher_suites: suites,
152        ..provider
153    }
154}
155
156/// The Mozilla (webpki) public root set as a rustls `RootCertStore`: the
157/// DEFAULT trust for every outbound https dial — upstream connectors,
158/// active health probes, and the JWKS fetcher (#121 made the per-entity
159/// override configurable; this remains what an entity without a
160/// `trusted_ca_file` verifies against). Lives here, next to the other
161/// root-store machinery, so all outbound callers share one definition
162/// instead of each rebuilding it inline.
163pub fn webpki_root_store() -> rustls::RootCertStore {
164    let mut roots = rustls::RootCertStore::empty();
165    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
166    roots
167}
168
169/// REL-08 (#219): the OS-native root CA store as a rustls
170/// `RootCertStore`. Loaded via `rustls-native-certs`, which reads the
171/// platform trust store (macOS keychain, Linux CA bundle, Windows root
172/// store). Used by upstreams that opt into `use_system_roots` instead of
173/// the bundled webpki set. Failures to load native certs fall back to
174/// the webpki set rather than failing every TLS handshake, because a
175/// missing OS trust store is a deployment mistake, not a security
176/// decision — and the operator's intent ("trust the OS") is best
177/// approximated by "trust what we have" rather than "trust nothing".
178pub fn system_root_store() -> rustls::RootCertStore {
179    let res = rustls_native_certs::load_native_certs();
180    let mut roots = rustls::RootCertStore::empty();
181    for c in res.certs {
182        let _ = roots.add(c);
183    }
184    if roots.is_empty() {
185        // No native certs (or every cert failed to parse): fall back to
186        // webpki rather than fail every handshake (an empty store is
187        // never a valid configuration). Any load errors are logged at
188        // debug level — the operator opted into the OS store and a
189        // silent fallback is preferable to a loud failure that blocks
190        // all traffic to this upstream.
191        for e in res.errors {
192            tracing::debug!(code = "native_cert_load_error", "{e}");
193        }
194        return webpki_root_store();
195    }
196    roots
197}
198
199/// Build a rustls `RootCertStore` from a PEM file of CA certificates
200/// (#121). The file may carry SEVERAL certificates — a typical CA bundle
201/// lists an anchor plus intermediates — and every certificate in it
202/// becomes a trust anchor. Fails with [`TlsError::Io`] when the file
203/// cannot be read or parsed, and with [`TlsError::EmptyPem`] when it
204/// parses to zero certificates (an empty trust set is always a
205/// configuration mistake, never a valid "trust nothing" — that would
206/// silently fail every TLS handshake).
207pub fn root_store_from_pem_file(path: &str) -> Result<rustls::RootCertStore, TlsError> {
208    let ppath = PathBuf::from(path);
209    let certs = CertificateDer::pem_file_iter(&ppath)
210        .map_err(|e| TlsError::Io(std::io::Error::other(e.to_string())))?
211        .collect::<Result<Vec<_>, _>>()
212        .map_err(|e| TlsError::Io(std::io::Error::other(e.to_string())))?;
213    if certs.is_empty() {
214        return Err(TlsError::EmptyPem {
215            path: ppath,
216            what: "CA certificates",
217        });
218    }
219    let mut roots = rustls::RootCertStore::empty();
220    for c in certs {
221        roots
222            .add(c)
223            .map_err(|e| TlsError::RootUnusable(e.to_string()))?;
224    }
225    Ok(roots)
226}
227
228/// HTTP/1.1-ALPN rustls client config over the given trust roots: the
229/// shared shape for every outbound https dial that speaks HTTP/1.1 —
230/// `https`-protocol upstream connectors, active health probes, and the
231/// JWKS fetcher (#121). Keeping one constructor here means a config
232/// generation applies IDENTICAL trust and handshake policy across the
233/// pooled client, the probes, and the fetcher; `http2` upstreams build
234/// their own config (same roots, `h2` ALPN).
235pub fn https_h1_client_config(roots: rustls::RootCertStore) -> rustls::ClientConfig {
236    let mut cfg = rustls::ClientConfig::builder()
237        .with_root_certificates(roots)
238        .with_no_client_auth();
239    cfg.alpn_protocols = vec![b"http/1.1".to_vec()];
240    cfg
241}
242
243/// SEC-03: HTTP/1.1-ALPN rustls client config WITH client auth (mTLS
244/// to the upstream). Loads the client cert chain and private key from
245/// PEM files and installs them via `with_client_auth_cert`. Used when
246/// an upstream configures `mtls.client_cert_file` + `mtls.client_key_file`.
247/// Fails with [`TlsError::Io`] when the files cannot be read or parsed.
248pub fn https_h1_client_config_with_auth(
249    roots: rustls::RootCertStore,
250    client_cert_file: &str,
251    client_key_file: &str,
252) -> Result<rustls::ClientConfig, TlsError> {
253    let certs = load_cert_chain(client_cert_file)?;
254    let key = load_private_key_der(client_key_file)?;
255    let mut cfg = rustls::ClientConfig::builder()
256        .with_root_certificates(roots)
257        .with_client_auth_cert(certs, key)
258        .map_err(TlsError::Rustls)?;
259    cfg.alpn_protocols = vec![b"http/1.1".to_vec()];
260    Ok(cfg)
261}
262
263/// SEC-03: HTTP/2-ALPN rustls client config WITH client auth (mTLS to
264/// the upstream). Same as [`https_h1_client_config_with_auth`] but with
265/// `h2` ALPN and a client locked to HTTP/2.
266pub fn https_h2_client_config_with_auth(
267    roots: rustls::RootCertStore,
268    client_cert_file: &str,
269    client_key_file: &str,
270) -> Result<rustls::ClientConfig, TlsError> {
271    let certs = load_cert_chain(client_cert_file)?;
272    let key = load_private_key_der(client_key_file)?;
273    let mut cfg = rustls::ClientConfig::builder()
274        .with_root_certificates(roots)
275        .with_client_auth_cert(certs, key)
276        .map_err(TlsError::Rustls)?;
277    cfg.alpn_protocols = vec![b"h2".to_vec()];
278    Ok(cfg)
279}
280
281/// SEC-03: Load a PEM private key as `PrivateKeyDer` (the form
282/// `with_client_auth_cert` expects). Mirrors the OAuth2 mTLS key loader.
283fn load_private_key_der(path: &str) -> Result<PrivateKeyDer<'static>, TlsError> {
284    let ppath = PathBuf::from(path);
285    let pem = Zeroizing::new(std::fs::read(&ppath)?);
286    let key = PrivateKeyDer::pem_slice_iter(&pem)
287        .next()
288        .ok_or_else(|| TlsError::EmptyPem {
289            path: ppath,
290            what: "private key",
291        })?
292        .map_err(|e| TlsError::Io(std::io::Error::other(e.to_string())))?;
293    Ok(key)
294}
295
296/// DW-105: HTTP/1.1-ALPN rustls client config with post-quantum hybrid
297/// key exchange opt-in. When `pq` is `true`, the config is built with a
298/// provider that has the X25519+ML-KEM hybrid kx group prepended. The
299/// classical X25519 group remains as a fallback for non-PQ servers.
300/// When `pq` is `false`, this is identical to
301/// [`https_h1_client_config`].
302pub fn https_h1_client_config_pq(roots: rustls::RootCertStore, pq: bool) -> rustls::ClientConfig {
303    if pq {
304        let provider = Arc::new(crate::security::pq::pq_provider());
305        let mut cfg = rustls::ClientConfig::builder_with_provider(provider)
306            .with_safe_default_protocol_versions()
307            .map_err(|e| std::io::Error::other(e.to_string()))
308            .expect("safe default protocol versions must be available")
309            .with_root_certificates(roots)
310            .with_no_client_auth();
311        cfg.alpn_protocols = vec![b"http/1.1".to_vec()];
312        cfg
313    } else {
314        https_h1_client_config(roots)
315    }
316}
317
318/// HTTP/3-ALPN rustls client config over the given trust roots (DW-108):
319/// the shared shape for every outbound H3/QUIC dial — the H3 upstream
320/// connector and the QUIC active health probe. QUIC mandates TLS 1.3,
321/// so this always negotiates `h3` over the same root store the pooled
322/// https connector would use (#121: webpki by default, the upstream's
323/// `trusted_ca_file` bundle when configured). Keeping the constructor
324/// here means one trust/handshake policy across the pooled H3 client and
325/// its probes, mirroring [`https_h1_client_config`].
326pub fn https_h3_client_config(roots: rustls::RootCertStore) -> rustls::ClientConfig {
327    let mut cfg = rustls::ClientConfig::builder()
328        .with_root_certificates(roots)
329        .with_no_client_auth();
330    cfg.alpn_protocols = vec![b"h3".to_vec()];
331    // DW-108: no 0-RTT upstream dialing (replay risk footgun). rustls
332    // does not expose early-data knobs on ClientConfig directly; the
333    // default is no early data, which is the safe choice.
334    cfg
335}
336
337fn load_cert_chain(path: &str) -> Result<Vec<CertificateDer<'static>>, TlsError> {
338    let path = PathBuf::from(path);
339    let certs = CertificateDer::pem_file_iter(&path)
340        .map_err(|e| TlsError::Io(std::io::Error::other(e.to_string())))?
341        .collect::<Result<Vec<_>, _>>()
342        .map_err(|e| TlsError::Io(std::io::Error::other(e.to_string())))?;
343    if certs.is_empty() {
344        return Err(TlsError::EmptyPem {
345            path,
346            what: "certificates",
347        });
348    }
349    Ok(certs)
350}
351
352fn load_signing_key(path: &str) -> Result<Arc<dyn rustls::sign::SigningKey>, TlsError> {
353    let ppath = PathBuf::from(path);
354    // #120 key-material hygiene: the PEM file body is plaintext secret
355    // material. Reading it into a Zeroizing buffer wipes the raw bytes on
356    // drop (the old pem_file_iter path left the decoded text in heap
357    // memory after the iterator finished).
358    let pem = Zeroizing::new(std::fs::read(&ppath)?);
359    // PrivateKeyDer implements Zeroize (rustls-pki-types), so the parsed
360    // DER secret is wiped when this wrapper drops — including the
361    // error paths below. The clone_key() copy handed to the provider is
362    // the one residue: aws-lc copies what it needs into its own
363    // structures, but that DER buffer itself is freed unwiped
364    // (PrivateKeyDer implements Zeroize with no Drop). The provider's
365    // own key material is wiped by aws-lc on free.
366    let key = Zeroizing::new(
367        PrivateKeyDer::pem_slice_iter(&pem)
368            .next()
369            .ok_or(TlsError::EmptyPem {
370                path: ppath,
371                what: "private keys",
372            })?
373            .map_err(|e| TlsError::Io(std::io::Error::other(e.to_string())))?,
374    );
375    // Loading a signing key requires an installed provider; the binary
376    // installs aws-lc-rs at startup, tests call the installer too.
377    rustls::crypto::aws_lc_rs::default_provider()
378        .key_provider
379        .load_private_key(key.clone_key())
380        .map_err(TlsError::Rustls)
381}
382
383/// Read one DER TLV: returns (tag, content, remainder after the element).
384/// Used by [`spki_of_leaf`] to walk the certificate structure without a
385/// full X.509 parser dependency.
386fn der_elem(buf: &[u8]) -> Option<(u8, &[u8], &[u8])> {
387    let tag = *buf.first()?;
388    let mut i = 1usize;
389    let first = *buf.get(i)?;
390    let len = if first < 0x80 {
391        i += 1;
392        first as usize
393    } else {
394        let n = (first & 0x7f) as usize;
395        if n == 0 || n > 8 {
396            return None;
397        }
398        i += 1;
399        let mut l = 0usize;
400        for &b in buf.get(i..i + n)? {
401            l = (l << 8) | b as usize;
402        }
403        i += n;
404        l
405    };
406    // An 8-byte length field can encode a value near usize::MAX, which
407    // would overflow the `i + len` addition: bound the element end
408    // BEFORE slicing (checked add + length filter, then the slice is
409    // always in range).
410    let end = len.checked_add(i).filter(|end| *end <= buf.len())?;
411    Some((tag, &buf[i..end], &buf[end..]))
412}
413
414/// Walk the TBS of a leaf certificate to the SUBJECT RDNSequence content
415/// (the bytes INSIDE the subject SEQUENCE header — unlike
416/// [`spki_of_leaf`], which keeps its header because that is the SPKI
417/// encoding the key comparison needs). None on any structural
418/// shortcoming.
419fn subject_of_leaf<'a>(cert: &'a CertificateDer<'a>) -> Option<&'a [u8]> {
420    let (tag, cert_content, _) = der_elem(cert.as_ref())?;
421    if tag != 0x30 {
422        return None;
423    }
424    let (tag, tbs, _) = der_elem(cert_content)?;
425    if tag != 0x30 {
426        return None;
427    }
428    let mut rest = tbs;
429    // Optional [0] EXPLICIT version comes before the serial INTEGER.
430    if let Some((tag, _, tail)) = der_elem(rest) {
431        if tag & 0xc0 == 0x80 {
432            rest = tail;
433        }
434    }
435    // serialNumber, signature, issuer, validity — then subject (the 5th
436    // element after the optional version).
437    for _ in 0..4 {
438        let (_, _, tail) = der_elem(rest)?;
439        rest = tail;
440    }
441    let (tag, subject_content, _) = der_elem(rest)?;
442    if tag != 0x30 {
443        return None;
444    }
445    Some(subject_content)
446}
447
448/// Walk the TBS of a leaf certificate to the ISSUER RDNSequence content
449/// (DW-035): the bytes INSIDE the issuer SEQUENCE header. The issuer is
450/// the 4th element of TBSCertificate (after the optional version):
451/// serialNumber, signature, issuer. None on any structural shortcoming.
452/// Same DER-walk substrate as [`subject_of_leaf`].
453fn issuer_of_leaf<'a>(cert: &'a CertificateDer<'a>) -> Option<&'a [u8]> {
454    let (tag, cert_content, _) = der_elem(cert.as_ref())?;
455    if tag != 0x30 {
456        return None;
457    }
458    let (tag, tbs, _) = der_elem(cert_content)?;
459    if tag != 0x30 {
460        return None;
461    }
462    let mut rest = tbs;
463    // Optional [0] EXPLICIT version comes before the serial INTEGER.
464    if let Some((tag, _, tail)) = der_elem(rest) {
465        if tag & 0xc0 == 0x80 {
466            rest = tail;
467        }
468    }
469    // serialNumber, signature, then issuer (the 3rd element after the
470    // optional version).
471    for _ in 0..2 {
472        let (_, _, tail) = der_elem(rest)?;
473        rest = tail;
474    }
475    let (tag, issuer_content, _) = der_elem(rest)?;
476    if tag != 0x30 {
477        return None;
478    }
479    Some(issuer_content)
480}
481
482/// Extract the issuer CommonName of a leaf certificate (DW-035): the
483/// value of the FIRST CN attribute (OID 2.5.4.3) in the issuer RDN
484/// sequence. Same strict UTF-8 decode as [`subject_cn_of_leaf`]; `None`
485/// when the issuer carries no decodable CN. THE CERTIFICATE IS ALREADY
486/// VERIFIED at the TLS layer when this runs; extraction only reads the
487/// value for the `X-Client-Cert-Issuer-CN` forwarding header.
488pub fn issuer_cn_of_leaf(cert: &CertificateDer<'_>) -> Option<String> {
489    let issuer = issuer_of_leaf(cert)?;
490    cn_of_rdn_sequence(issuer)
491}
492
493/// Walk the TBS of a leaf certificate to the VALIDITY SEQUENCE content
494/// (DW-035): the bytes INSIDE the validity SEQUENCE. The validity is
495/// the 5th element of TBSCertificate (after the optional version):
496/// serialNumber, signature, issuer, validity. None on any structural
497/// shortcoming.
498fn validity_of_leaf<'a>(cert: &'a CertificateDer<'a>) -> Option<&'a [u8]> {
499    let (tag, cert_content, _) = der_elem(cert.as_ref())?;
500    if tag != 0x30 {
501        return None;
502    }
503    let (tag, tbs, _) = der_elem(cert_content)?;
504    if tag != 0x30 {
505        return None;
506    }
507    let mut rest = tbs;
508    // Optional [0] EXPLICIT version comes before the serial INTEGER.
509    if let Some((tag, _, tail)) = der_elem(rest) {
510        if tag & 0xc0 == 0x80 {
511            rest = tail;
512        }
513    }
514    // serialNumber, signature, issuer, validity (the 4th element after
515    // the optional version).
516    for _ in 0..3 {
517        let (_, _, tail) = der_elem(rest)?;
518        rest = tail;
519    }
520    let (tag, validity_content, _) = der_elem(rest)?;
521    if tag != 0x30 {
522        return None;
523    }
524    Some(validity_content)
525}
526
527/// Extract the `notAfter` timestamp of a leaf certificate (DW-035) as
528/// Unix epoch seconds. The validity SEQUENCE holds two time values:
529/// `notBefore` then `notAfter`, each a UTCTime (tag 0x17, YYMMDDHHMMSSZ)
530/// or GeneralizedTime (tag 0x18, YYYYMMDDHHMMSSZ). Returns `None` on
531/// any structural shortcoming or an unparseable time. THE CERTIFICATE
532/// IS ALREADY VERIFIED at the TLS layer when this runs; extraction only
533/// reads the value for the `X-Client-Cert-Not-After` forwarding header.
534pub fn not_after_unix_secs(cert: &CertificateDer<'_>) -> Option<i64> {
535    let validity = validity_of_leaf(cert)?;
536    // Skip notBefore (the first element) to reach notAfter.
537    let (_, _, after_not_before) = der_elem(validity)?;
538    let (tag, time_bytes, _) = der_elem(after_not_before)?;
539    match tag {
540        0x17 => parse_utc_time(time_bytes),
541        0x18 => parse_generalized_time(time_bytes),
542        _ => None,
543    }
544}
545
546/// Parse a UTCTime value (tag 0x17, `YYMMDDHHMMSSZ`) into Unix epoch
547/// seconds. RFC 5280 section 4.1.2.5.1: the year is interpreted as
548/// 1950-2049 for YY 00-49 and 1900-1999 for YY 50-99. The trailing `Z`
549/// (UTC) is required; no offsets are supported (certificates carry UTC
550/// times only).
551fn parse_utc_time(bytes: &[u8]) -> Option<i64> {
552    // Minimum: YYMMDDHHMMSSZ (13 bytes). Seconds are required in
553    // X.509 (RFC 5280); fractional seconds are not allowed in UTCTime.
554    if bytes.len() < 13 || *bytes.last()? != b'Z' {
555        return None;
556    }
557    let body = &bytes[..bytes.len() - 1];
558    let yy = two_digit_decimal(body, 0)?;
559    let year = if yy < 50 { 2000 + yy } else { 1900 + yy };
560    let month = two_digit_decimal(body, 2)? as u32;
561    let day = two_digit_decimal(body, 4)? as u32;
562    let hour = two_digit_decimal(body, 6)? as u32;
563    let minute = two_digit_decimal(body, 8)? as u32;
564    let second = two_digit_decimal(body, 10)? as u32;
565    epoch_secs(year, month, day, hour, minute, second)
566}
567
568/// Parse a GeneralizedTime value (tag 0x18, `YYYYMMDDHHMMSSZ`) into
569/// Unix epoch seconds. RFC 5280 section 4.1.2.5.2: GeneralizedTime is
570/// used for dates after 2049; the trailing `Z` (UTC) is required.
571fn parse_generalized_time(bytes: &[u8]) -> Option<i64> {
572    if bytes.len() < 15 || *bytes.last()? != b'Z' {
573        return None;
574    }
575    let body = &bytes[..bytes.len() - 1];
576    let year = four_digit_decimal(body, 0)?;
577    let month = two_digit_decimal(body, 4)? as u32;
578    let day = two_digit_decimal(body, 6)? as u32;
579    let hour = two_digit_decimal(body, 8)? as u32;
580    let minute = two_digit_decimal(body, 10)? as u32;
581    let second = two_digit_decimal(body, 12)? as u32;
582    epoch_secs(year, month, day, hour, minute, second)
583}
584
585/// Read two ASCII decimal digits at `offset` as a u16.
586fn two_digit_decimal(buf: &[u8], offset: usize) -> Option<u16> {
587    let bytes = buf.get(offset..offset + 2)?;
588    let hi = (bytes[0] as char).to_digit(10)?;
589    let lo = (bytes[1] as char).to_digit(10)?;
590    Some((hi * 10 + lo) as u16)
591}
592
593/// Read four ASCII decimal digits at `offset` as a u16 (year).
594fn four_digit_decimal(buf: &[u8], offset: usize) -> Option<u16> {
595    let bytes = buf.get(offset..offset + 4)?;
596    let mut v = 0u16;
597    for &b in bytes {
598        v = v * 10 + (b as char).to_digit(10)? as u16;
599    }
600    Some(v)
601}
602
603/// Convert a calendar date + time to Unix epoch seconds using a
604/// civil-from-days algorithm (Howard Hinnant, no chrono dependency).
605/// Returns None for an invalid date (month outside 1..=12, day outside
606/// the month's range, or a year outside the u16-representable range).
607fn epoch_secs(year: u16, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> Option<i64> {
608    if !(1..=12).contains(&month) || day == 0 || hour >= 24 || minute >= 60 || second >= 60 {
609        return None;
610    }
611    let y = year as i64;
612    let m = month as i64;
613    let d = day as i64;
614    // Days from the civil epoch (1970-01-01) to (y, m, d). The algorithm
615    // is from Howard Hinnant's "date" library (the days_from_civil
616    // function), public domain.
617    let y_adj = if m <= 2 { y - 1 } else { y };
618    let era = if y_adj >= 0 { y_adj } else { y_adj - 399 } / 400;
619    let yoe = (y_adj - era * 400) as u64; // [0, 399]
620    let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d - 1; // [0, 365]
621    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy as u64; // [0, 146096]
622    let days = era * 146097 + doe as i64 - 719468;
623    let secs = days * 86400 + (hour as i64) * 3600 + (minute as i64) * 60 + second as i64;
624    // Validate the day is in range for the month (the algorithm above
625    // does not reject e.g. Feb 30; a round-trip check would, but the
626    // certificate was already verified by rustls so a structurally
627    // invalid date here is a malformed cert, not an attack surface).
628    // Accept the computed value: rustls's own validity check already
629    // rejected an expired/not-yet-valid cert at the TLS layer when
630    // verification ran.
631    Some(secs)
632}
633
634/// Extract the FIRST CN attribute (OID 2.5.4.3) from an RDN sequence's
635/// raw DER content (the bytes INSIDE the SEQUENCE header). Shared by
636/// [`subject_cn_of_leaf`] and [`issuer_cn_of_leaf`]; strict UTF-8
637/// decode (reject rather than fold, the same rationale).
638fn cn_of_rdn_sequence(rdn_sequence: &[u8]) -> Option<String> {
639    let mut rdns = rdn_sequence;
640    while let Some((set_tag, set_content, tail)) = der_elem(rdns) {
641        if set_tag != 0x31 {
642            return None;
643        }
644        let mut attrs = set_content;
645        while let Some((seq_tag, attr, attr_tail)) = der_elem(attrs) {
646            if seq_tag == 0x30 {
647                if let Some((oid_tag, oid, after_oid)) = der_elem(attr) {
648                    if oid_tag == 0x06 && oid == [0x55, 0x04, 0x03] {
649                        if let Some((val_tag, val, _)) = der_elem(after_oid) {
650                            if matches!(val_tag, 0x0c | 0x13 | 0x16) {
651                                return Some(std::str::from_utf8(val).ok()?.to_string());
652                            }
653                        }
654                    }
655                }
656            }
657            attrs = attr_tail;
658        }
659        rdns = tail;
660    }
661    None
662}
663
664/// Extract the subject CommonName of a leaf certificate (#124): the
665/// value of the FIRST CN attribute (OID 2.5.4.3) in the subject RDN
666/// sequence, decoded from UTF8String / PrintableString / IA5String (the
667/// string types CNs carry in practice). The decode is STRICT UTF-8:
668/// invalid CN bytes yield `None` rather than being lossy-folded to
669/// U+FFFD, which could collide two distinct malformed names into one
670/// selector. `None` also when the subject carries no CN at all — either
671/// way such a certificate can only match a by-fingerprint mTLS
672/// credential (the caller falls back to the fingerprint selector).
673/// Hand-rolled DER walk (no X.509 parser dependency), same substrate as
674/// the private SPKI extractor in this module. THE CERTIFICATE IS
675/// ALREADY VERIFIED at the TLS layer when this runs; extraction only
676/// reads the match value.
677pub fn subject_cn_of_leaf(cert: &CertificateDer<'_>) -> Option<String> {
678    let subject = subject_of_leaf(cert)?;
679    cn_of_rdn_sequence(subject)
680}
681
682/// Format the SHA-256 fingerprint of a certificate DER as lowercase
683/// colon-separated hex (DW-035): the format the
684/// `mtls_consumer_mapping.consumers[].fingerprint` config field and the
685/// `X-Client-Cert-Fingerprint` forwarding header use. The raw hex
686/// (no colons) is the credential SELECTOR the authn registry indexes;
687/// this colon-separated form is the OPERATOR-facing display format
688/// (config and headers), so the two never collide.
689pub fn fingerprint_colon_hex(cert: &CertificateDer<'_>) -> String {
690    use sha2::Digest;
691    let digest = sha2::Sha256::digest(cert.as_ref());
692    let hex = crate::config::credentials::sha256_hex(cert.as_ref());
693    // Group the 64-char hex into 32 byte pairs joined by ':'.
694    let bytes = hex.as_bytes();
695    let mut out = String::with_capacity(64 + 31);
696    for (i, chunk) in bytes.chunks(2).enumerate() {
697        if i > 0 {
698            out.push(':');
699        }
700        out.push_str(std::str::from_utf8(chunk).unwrap_or(""));
701    }
702    let _ = digest; // sha256_hex already computed; keep the import used.
703    out
704}
705
706/// Extract the DER-encoded SubjectPublicKeyInfo of a leaf certificate by
707/// walking Certificate -> TBSCertificate -> (version?) serial, signature,
708/// issuer, validity, subject, SPKI. Returns the SPKI DER including its own
709/// SEQUENCE header (the same encoding `SigningKey::public_key` yields) or
710/// None on any structural shortcoming (treated as a mismatch by the
711/// caller, which rejects).
712pub fn spki_of_leaf(cert: &CertificateDer<'_>) -> Option<Vec<u8>> {
713    // Certificate SEQUENCE -> TBSCertificate SEQUENCE.
714    let (tag, cert_content, _) = der_elem(cert.as_ref())?;
715    if tag != 0x30 {
716        return None;
717    }
718    let (tag, tbs, _) = der_elem(cert_content)?;
719    if tag != 0x30 {
720        return None;
721    }
722    let mut rest = tbs;
723    // Optional [0] EXPLICIT version comes before the serial INTEGER.
724    if let Some((tag, _, tail)) = der_elem(rest) {
725        if tag & 0xc0 == 0x80 {
726            rest = tail;
727        }
728    }
729    // serialNumber, signature, issuer, validity, subject, then SPKI.
730    for _ in 0..5 {
731        let (_, _, tail) = der_elem(rest)?;
732        rest = tail;
733    }
734    let (tag, _, after_spki) = der_elem(rest)?;
735    if tag != 0x30 {
736        return None;
737    }
738    Some(rest[..rest.len() - after_spki.len()].to_vec())
739}
740
741/// Load one cert/key pair as a `CertifiedKey`, rejecting pairs whose
742/// private key does not match the leaf certificate (public-key/SPKI
743/// comparison via aws-lc-rs, available through the rustls signing key).
744fn load_certified_key(cert_file: &str, key_file: &str) -> Result<Arc<CertifiedKey>, TlsError> {
745    let certs = load_cert_chain(cert_file)?;
746    let key = load_signing_key(key_file)?;
747    let spki_ok = key
748        .public_key()
749        .map(|k| k.as_ref().to_vec())
750        .zip(spki_of_leaf(&certs[0]))
751        .is_some_and(|(key_spki, cert_spki)| key_spki == cert_spki);
752    if !spki_ok {
753        return Err(TlsError::KeyMismatch {
754            cert_file: PathBuf::from(cert_file),
755            key_file: PathBuf::from(key_file),
756        });
757    }
758    Ok(Arc::new(CertifiedKey::new(certs, key)))
759}
760
761/// SNI-aware certificate resolver: exact server-name match against the
762/// `certificates` entries; falls back to the single pair (or the first
763/// entry when no pair is configured). Names are compared lowercase per
764/// the SNI specification.
765pub struct SniCertResolver {
766    // Debug is required by ResolvesServerCert; derived on a snapshot of
767    // the key names only, via a manual impl below.
768    by_name: BTreeMap<String, Arc<CertifiedKey>>,
769    fallback: Arc<CertifiedKey>,
770}
771
772impl SniCertResolver {
773    /// Build from a terminate-mode [`ListenerTls`]. The fallback order is:
774    /// the single `cert_file`/`key_file` pair if present, else the first
775    /// `certificates` entry.
776    pub fn build(tls: &ListenerTls) -> Result<Self, TlsError> {
777        let mut by_name = BTreeMap::new();
778        let mut fallback: Option<Arc<CertifiedKey>> = None;
779        let mut first: Option<Arc<CertifiedKey>> = None;
780
781        if let (Some(cert_file), Some(key_file)) = (&tls.cert_file, &tls.key_file) {
782            fallback = Some(load_certified_key(cert_file, key_file)?);
783        }
784        for c in &tls.certificates {
785            let key = load_certified_key(&c.cert_file, &c.key_file)?;
786            for n in &c.server_names {
787                by_name.insert(n.to_ascii_lowercase(), Arc::clone(&key));
788            }
789            if first.is_none() {
790                first = Some(key);
791            }
792        }
793        let fallback = fallback.or(first).ok_or(TlsError::NoCertificates)?;
794        Ok(SniCertResolver { by_name, fallback })
795    }
796}
797
798impl std::fmt::Debug for SniCertResolver {
799    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
800        f.debug_struct("SniCertResolver")
801            .field("names", &self.by_name.keys().collect::<Vec<_>>())
802            .finish_non_exhaustive()
803    }
804}
805
806impl ResolvesServerCert for SniCertResolver {
807    fn resolve(&self, client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
808        client_hello
809            .server_name()
810            .and_then(|sni| self.by_name.get(&sni.to_ascii_lowercase()))
811            .map(Arc::clone)
812            .or_else(|| Some(Arc::clone(&self.fallback)))
813    }
814}
815
816/// A hot-reloadable TLS termination configuration for one listener.
817///
818/// The current `ServerConfig` lives behind an `ArcSwap`; every accepted
819/// connection snapshots the current `Arc` when its handshake starts.
820/// [`TlsTermination::reload`] swaps in a rebuilt config; handshakes that
821/// began earlier finish with the configuration they negotiated.
822pub struct TlsTermination {
823    config: ArcSwap<ServerConfig>,
824    /// The cert/key paths this config was built from, so a watcher can
825    /// observe exactly the files that matter.
826    pub watched_paths: Vec<PathBuf>,
827}
828
829impl TlsTermination {
830    /// Build from a terminate-mode [`ListenerTls`] block.
831    pub fn build(tls: &ListenerTls) -> Result<Self, TlsError> {
832        let mut watched = Vec::new();
833        if let (Some(c), Some(k)) = (&tls.cert_file, &tls.key_file) {
834            watched.push(PathBuf::from(c));
835            watched.push(PathBuf::from(k));
836        }
837        for c in &tls.certificates {
838            watched.push(PathBuf::from(&c.cert_file));
839            watched.push(PathBuf::from(&c.key_file));
840        }
841        Ok(TlsTermination {
842            config: ArcSwap::from_pointee(Self::server_config(tls)?),
843            watched_paths: watched,
844        })
845    }
846
847    fn server_config(tls: &ListenerTls) -> Result<ServerConfig, TlsError> {
848        let resolver = SniCertResolver::build(tls)?;
849        // DW-105: when the listener opts in to post-quantum hybrid key
850        // exchange (`pq: true`), build the server config with a provider
851        // that has the X25519+ML-KEM hybrid kx group prepended. The
852        // classical X25519 group remains as a fallback for non-PQ
853        // clients. Validation rejects `pq: true` + FIPS mode (ML-KEM is
854        // not FIPS-validated).
855        let provider = if tls.pq {
856            Arc::new(crate::security::pq::pq_provider())
857        } else {
858            Arc::new(fips_provider())
859        };
860        // #124 client-certificate authn: with a `client_ca_file` the
861        // listener REQUESTS a client certificate and verifies any
862        // presented one against the bundle (the admin mTLS verifier
863        // pattern), but — unlike the admin listener — does NOT require
864        // one: `allow_unauthenticated` keeps the listener open to API-key
865        // / Basic / JWT / anonymous traffic, and authn matches the
866        // VERIFIED certificate against consumers' `mtls` credentials. An
867        // UNVERIFIED certificate fails the handshake here and never
868        // reaches the authenticator (mTLS authn only ever sees
869        // certificates rustls has already chained to the configured CA).
870        //
871        // DW-111: when the `fips` cargo feature is ON, the server config
872        // is built with the FIPS-approved cipher suites only (the
873        // process-default provider's suite list filtered to the
874        // allowlist in `security::fips`). Non-FIPS builds use the
875        // default builder (rustls's modern cipher-suite policy).
876        {
877            let mut config = match &tls.client_ca_file {
878                Some(client_ca) => {
879                    let roots = root_store_from_pem_file(client_ca)?;
880                    let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
881                        Arc::new(roots),
882                        Arc::clone(&provider),
883                    )
884                    .allow_unauthenticated()
885                    .build()
886                    .map_err(|e| TlsError::ClientAuth(format!("building client verifier: {e}")))?;
887                    ServerConfig::builder_with_provider(Arc::clone(&provider))
888                        .with_safe_default_protocol_versions()
889                        .map_err(TlsError::Rustls)?
890                        .with_client_cert_verifier(verifier)
891                        .with_cert_resolver(Arc::new(resolver))
892                }
893                None => ServerConfig::builder_with_provider(Arc::clone(&provider))
894                    .with_safe_default_protocol_versions()
895                    .map_err(TlsError::Rustls)?
896                    .with_no_client_auth()
897                    .with_cert_resolver(Arc::new(resolver)),
898            };
899            config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
900            Ok(config)
901        }
902    }
903
904    /// The current server config; clone the `Arc` per accepted connection.
905    pub fn config(&self) -> Arc<ServerConfig> {
906        self.config.load_full()
907    }
908
909    /// Rebuild from (possibly changed files on disk) and swap in. On any
910    /// error — including a cert/key pair whose key does not match the
911    /// leaf certificate (torn reload) — the current config is untouched
912    /// and the previous certificates keep serving.
913    pub fn reload(&self, tls: &ListenerTls) -> Result<(), TlsError> {
914        let built = Self::server_config(tls)?;
915        self.config.store(Arc::new(built));
916        Ok(())
917    }
918}
919
920/// Cert resolver that always answers with the one configured key: the
921/// admin listener serves a single identity (no SNI selection).
922struct SingleCertResolver(Arc<CertifiedKey>);
923
924impl std::fmt::Debug for SingleCertResolver {
925    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
926        f.debug_struct("SingleCertResolver").finish_non_exhaustive()
927    }
928}
929
930impl ResolvesServerCert for SingleCertResolver {
931    fn resolve(&self, _client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
932        Some(Arc::clone(&self.0))
933    }
934}
935
936/// Build the mTLS-ONLY `rustls::ServerConfig` for the admin listener
937/// (DW-022, decision 6). Client certificates are REQUIRED and must chain
938/// to the CA in `client_ca_file` (`WebPkiClientVerifier` over a root
939/// store loaded from that PEM); the server presents the single
940/// cert/key pair. The key is checked against the leaf exactly like
941/// dataplane termination, so a torn pair cannot boot. ALPN advertises
942/// HTTP/1.1 only — the admin API is a small request/response surface.
943pub fn admin_mtls_server_config(
944    tls: &crate::config::AdminTlsConfig,
945) -> Result<ServerConfig, TlsError> {
946    let certified = load_certified_key(&tls.cert_file, &tls.key_file)?;
947    // Same PEM-bundle loading as the outbound trusted_ca_file path (#121):
948    // one root-store-from-file helper, shared by admin mTLS and outbound
949    // connectors, so bundle semantics cannot drift between them.
950    let roots = root_store_from_pem_file(&tls.client_ca_file)?;
951    // DW-111: when the `fips` cargo feature is ON, the admin mTLS config
952    // is also built with the FIPS-approved cipher suites only (the same
953    // restriction as dataplane termination). Non-FIPS builds use the
954    // default builder.
955    {
956        let provider = Arc::new(fips_provider());
957        let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
958            Arc::new(roots),
959            Arc::clone(&provider),
960        )
961        .build()
962        .map_err(|e| TlsError::ClientAuth(format!("building admin client verifier: {e}")))?;
963        let mut config = ServerConfig::builder_with_provider(Arc::clone(&provider))
964            .with_safe_default_protocol_versions()
965            .map_err(TlsError::Rustls)?
966            .with_client_cert_verifier(verifier)
967            .with_cert_resolver(Arc::new(SingleCertResolver(certified)));
968        config.alpn_protocols = vec![b"http/1.1".to_vec()];
969        Ok(config)
970    }
971}
972
973/// DW-025 fuzz fix: every u16be call site must tolerate slices shorter
974/// than two bytes (`slice::get(i..)` succeeds for i <= len even when
975/// fewer than 2 bytes remain), so the helper returns Option and
976/// truncation anywhere is "no SNI" instead of an index panic.
977fn u16be(b: &[u8]) -> Option<u16> {
978    Some(u16::from_be_bytes([*b.first()?, *b.get(1)?]))
979}
980
981/// Handshake-message length out of a ClientHello handshake header
982/// (the type byte is checked by the caller): 3-byte big-endian.
983fn hs_len_of(header: &[u8]) -> usize {
984    ((header[1] as usize) << 16) | ((header[2] as usize) << 8) | header[3] as usize
985}
986
987/// Reassemble the first handshake message body (the ClientHello body)
988/// out of a connection prefix that may carry the message FRAGMENTED
989/// across several TLS records (#120: TLS records cap their payload at
990/// 16384 bytes, so larger ClientHellos arrive as multiple records and
991/// must be reassembled before parsing).
992///
993/// Single-record hellos borrow from `buf`; fragmented hellos copy the
994/// record-payload tails into one buffer. Returns `None` while the
995/// message is incomplete, over the reassembly budget, or structurally
996/// unusable.
997fn client_hello_body(buf: &[u8]) -> Option<std::borrow::Cow<'_, [u8]>> {
998    // Record header: type(1) version(2) length(2)
999    if buf.len() < 5 || buf[0] != 0x16 {
1000        return None;
1001    }
1002    let rec_len = u16be(buf.get(3..)?)? as usize;
1003    let first = buf.get(5..5 + rec_len)?;
1004    // Handshake header: type(1) length(3); expect ClientHello (0x01).
1005    if first.len() < 4 || first[0] != 0x01 {
1006        return None;
1007    }
1008    let hs_len = hs_len_of(first);
1009    // Refuse hellos whose handshake message alone exceeds the
1010    // reassembly budget: a bounded close, never an unbounded buffer.
1011    if 4 + hs_len > MAX_HELLO_BYTES {
1012        return None;
1013    }
1014    if first.len() >= 4 + hs_len {
1015        return Some(std::borrow::Cow::Borrowed(&first[4..4 + hs_len]));
1016    }
1017    // Fragmented: the first record holds a prefix of the message;
1018    // append the tails carried by subsequent complete handshake records
1019    // until the message is whole.
1020    let mut body = Vec::with_capacity(hs_len);
1021    body.extend_from_slice(&first[4..]);
1022    let mut pos = 5 + rec_len;
1023    while body.len() < hs_len {
1024        let hdr = buf.get(pos..pos + 5)?;
1025        if hdr[0] != 0x16 {
1026            // Handshake bytes travel in handshake records only; a record
1027            // of another type cannot complete the message.
1028            return None;
1029        }
1030        let rl = u16be(hdr.get(3..)?)? as usize;
1031        let payload = buf.get(pos + 5..pos + 5 + rl)?;
1032        let take = (hs_len - body.len()).min(payload.len());
1033        body.extend_from_slice(&payload[..take]);
1034        pos += 5 + rl;
1035    }
1036    Some(std::borrow::Cow::Owned(body))
1037}
1038
1039/// Parse the SNI server name out of a TLS ClientHello, reassembling
1040/// handshake fragments across records when the hello does not fit in
1041/// one (#120).
1042///
1043/// Minimal record parser (no new dependency): walks the TLS record and
1044/// handshake headers, skips session id / cipher suites / compression
1045/// methods, then scans the extension list for `server_name` (ext 0) and
1046/// returns the first `host_name` entry. Returns `None` on any structural
1047/// shortcoming; callers treat that as "no SNI".
1048pub fn sni_from_client_hello(buf: &[u8]) -> Option<String> {
1049    let body = client_hello_body(buf)?;
1050    let body = body.as_ref();
1051    let mut i = 0usize;
1052    i += 2; // client version
1053    i += 32; // random
1054             // session id
1055    let sid_len = *body.get(i)? as usize;
1056    i += 1 + sid_len;
1057    // cipher suites
1058    let cs_len = u16be(body.get(i..)?)? as usize;
1059    i += 2 + cs_len;
1060    // compression methods
1061    let cm_len = *body.get(i)? as usize;
1062    i += 1 + cm_len;
1063    // extensions
1064    let ext_total = u16be(body.get(i..)?)? as usize;
1065    i += 2;
1066    let ext_end = i + ext_total;
1067    while i + 4 <= ext_end && i + 4 <= body.len() {
1068        let ext_type = u16be(body.get(i..)?)?;
1069        let ext_len = u16be(body.get(i + 2..)?)? as usize;
1070        let ext = body.get(i + 4..i + 4 + ext_len)?;
1071        if ext_type == 0x0000 {
1072            // server_name list: total length (2), then entries:
1073            // type (1, must be 0 host_name), length (2), bytes.
1074            if ext.len() < 2 {
1075                return None;
1076            }
1077            let list_len = u16be(ext)? as usize;
1078            let list = ext.get(2..2 + list_len)?;
1079            if list.is_empty() {
1080                return None;
1081            }
1082            let name_type = list[0];
1083            if name_type != 0x00 || list.len() < 4 {
1084                return None;
1085            }
1086            let name_len = u16be(list.get(1..)?)? as usize;
1087            let name = list.get(3..3 + name_len)?;
1088            return std::str::from_utf8(name).ok().map(|s| s.to_string());
1089        }
1090        i += 4 + ext_len;
1091    }
1092    None
1093}
1094
1095/// Passthrough routing outcome.
1096#[derive(Debug, PartialEq, Eq)]
1097pub enum PassthroughAction {
1098    /// Splice to this upstream endpoint.
1099    Forward { host: String, port: u16 },
1100    /// Close the connection (non-TLS, no SNI, or no matching route).
1101    Close,
1102}
1103
1104/// Endpoint resolver for passthrough routing: maps an upstream NAME to a
1105/// load-balanced `(address, port)`. The dataplane caller builds it from
1106/// the CURRENT registry (see dwara-bin's listener), so passthrough picks
1107/// follow config reloads; `Send + Sync` because the passthrough path runs
1108/// on spawned tasks. `None` means "no dataplane" — the first configured
1109/// endpoint is used.
1110pub type EndpointPicker<'a> = &'a (dyn Fn(&str) -> Option<(String, u16)> + Send + Sync);
1111
1112/// Resolve a passthrough SNI route against the gateway config.
1113///
1114/// Routing rule (v1, documented): the SNI server name is matched exactly
1115/// against the `sni_routes` entries of the listener. A hit forwards to an
1116/// endpoint of the referenced upstream. Endpoint selection is delegated to
1117/// `pick`: the caller supplies a resolver that maps an upstream NAME to a
1118/// load-balanced `(address, port)` (DW-011; the dataplane caller builds it
1119/// from the CURRENT registry, so passthrough picks follow config reloads
1120/// too — see dwara-bin's listener). The pick carries no hash key — a byte
1121/// splice has no per-request client-IP semantics to weight beyond
1122/// stickiness, so `ip_hash` degrades to its smooth-RR fallback. Without a
1123/// resolver (callers without a dataplane) the FIRST configured endpoint is
1124/// used. No match, no SNI, or a non-TLS client closes the connection.
1125pub fn resolve_passthrough(
1126    sni: Option<&str>,
1127    routes: &[SniRoute],
1128    gateway: &Gateway,
1129    pick: Option<EndpointPicker<'_>>,
1130) -> PassthroughAction {
1131    let Some(name) = sni else {
1132        return PassthroughAction::Close;
1133    };
1134    for r in routes {
1135        if r.server_names.iter().any(|n| n.eq_ignore_ascii_case(name)) {
1136            let upstream = gateway.upstreams.iter().find(|u| u.name == r.upstream);
1137            let endpoint = upstream.and_then(|u| {
1138                // Prefer the caller's (balancer-backed) pick; fall back to
1139                // the first configured endpoint. Single-load pick: index
1140                // and address:port come from the same state snapshot, so a
1141                // concurrent reload cannot pair an index from one set with
1142                // an address from another.
1143                pick.and_then(|pick| pick(&u.name))
1144                    .or_else(|| u.endpoints.first().map(|e| (e.address.clone(), e.port)))
1145            });
1146            if let Some((host, port)) = endpoint {
1147                return PassthroughAction::Forward { host, port };
1148            }
1149            return PassthroughAction::Close;
1150        }
1151    }
1152    PassthroughAction::Close
1153}
1154
1155/// Total reassembly budget for one passthrough ClientHello (#120): a
1156/// hello fragmented across records is accumulated up to this size, and
1157/// anything whose handshake message alone is larger is refused (the
1158/// connection is closed). Real ClientHellos are a few KB; 64 KiB covers
1159/// four maximal TLS records.
1160const MAX_HELLO_BYTES: usize = 64 * 1024;
1161/// Maximum bytes peeked while looking for a complete ClientHello: the
1162/// reassembly budget plus per-record header overhead and slack for
1163/// coalesced trailing records. The decision fires as soon as the hello
1164/// is complete, so a FULL window without one means the hello exceeds
1165/// the budget and the connection is refused instead of spinning until
1166/// the peek timeout.
1167const PEEK_LIMIT: usize = MAX_HELLO_BYTES + 5 + 1024;
1168/// How long to wait for a complete ClientHello before giving up.
1169const PEEK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
1170
1171/// What the peek loop should do after examining the buffered prefix of
1172/// a passthrough connection.
1173enum HelloPeek {
1174    /// A complete ClientHello carrying SNI.
1175    Sni(String),
1176    /// Nothing that can still arrive would change the answer (non-TLS
1177    /// bytes, a structurally complete hello without SNI, a handshake
1178    /// message that is not a ClientHello, or a hello over the
1179    /// reassembly budget): decide now.
1180    Settled,
1181    /// The ClientHello is fragmented across records (or bytes are still
1182    /// arriving) and the message is not complete yet: keep waiting,
1183    /// bounded by [`PEEK_TIMEOUT`] and [`PEEK_LIMIT`].
1184    Pending,
1185}
1186
1187/// Examine a peeked connection prefix: is the ClientHello complete (and
1188/// what SNI does it carry), definitively undecidable-as-no-SNI, or still
1189/// missing bytes that may yet arrive? A hello split across records keeps
1190/// the answer [`HelloPeek::Pending`] until its last fragment lands (#120).
1191fn examine_peeked(seen: &[u8]) -> HelloPeek {
1192    // Early reject (documented): a TLS record always starts with the
1193    // handshake content type 0x16, so anything else is a non-TLS client
1194    // (e.g. plain HTTP) — decide immediately instead of waiting.
1195    if seen.first() != Some(&0x16) {
1196        return HelloPeek::Settled;
1197    }
1198    // Walk complete handshake records, counting the bytes of the first
1199    // handshake message they carry.
1200    let mut pos = 0usize;
1201    // Total bytes the message needs (4-byte header + body), once read.
1202    let mut needed: Option<usize> = None;
1203    // Message bytes present so far (header + body fragments).
1204    let mut have = 0usize;
1205    loop {
1206        let Some(hdr) = seen.get(pos..pos + 5) else {
1207            // A further record header is absent or truncated: if the
1208            // message is still short, more records may complete it.
1209            return HelloPeek::Pending;
1210        };
1211        if hdr[0] != 0x16 {
1212            // Handshake bytes travel in handshake records only; a record
1213            // of another type while the message is short cannot complete
1214            // it, so there is nothing left to wait for.
1215            return HelloPeek::Settled;
1216        }
1217        let Some(rec_len) = u16be(hdr.get(3..).unwrap_or(&[])) else {
1218            // Unreachable for a 5-byte header slice; kept as a guarded
1219            // fallback per the DW-025 no-index-panic style.
1220            return HelloPeek::Pending;
1221        };
1222        let rec_len = rec_len as usize;
1223        let Some(payload) = seen.get(pos + 5..pos + 5 + rec_len) else {
1224            // Record payload still arriving.
1225            return HelloPeek::Pending;
1226        };
1227        if needed.is_none() {
1228            if payload.len() < 4 {
1229                // Boundary decision: a hello fragmented INSIDE its 4-byte
1230                // handshake header (no real TLS stack does this) is not
1231                // reassembled — the first record must carry the whole
1232                // header so the message length is readable.
1233                return HelloPeek::Settled;
1234            }
1235            if payload[0] != 0x01 {
1236                return HelloPeek::Settled;
1237            }
1238            let hs_len = hs_len_of(payload);
1239            if 4 + hs_len > MAX_HELLO_BYTES {
1240                return HelloPeek::Settled;
1241            }
1242            needed = Some(4 + hs_len);
1243        }
1244        // Every payload byte of these records belongs to the message
1245        // stream (the 4-byte handshake header included): the message
1246        // occupies a contiguous byte range starting at the first
1247        // record's payload.
1248        have += payload.len();
1249        if have >= needed.unwrap_or(0) {
1250            // The whole message is buffered: parse it for SNI.
1251            return match sni_from_client_hello(seen) {
1252                Some(name) => HelloPeek::Sni(name),
1253                None => HelloPeek::Settled,
1254            };
1255        }
1256        pos += 5 + rec_len;
1257    }
1258}
1259
1260/// Peek the TLS ClientHello SNI off a TCP stream WITHOUT splicing.
1261///
1262/// This is the peek-only half of [`handle_passthrough`], extracted so
1263/// the DW-103 L4 TCP dispatcher can reuse the EXACT SNI extraction
1264/// (the same bounded reassembly, the same 64 KiB budget, the same 10s
1265/// peek timeout) and then own its own splice (with an idle timeout and
1266/// L4 metrics). Returns `Ok(None)` when the client sends no SNI, a
1267/// non-TLS hello, or the peek times out. Peeking (never reading) keeps
1268/// the bytes available for the upstream once splicing starts.
1269pub async fn peek_client_hello_sni(stream: &mut TcpStream) -> std::io::Result<Option<String>> {
1270    let mut scratch = vec![0u8; PEEK_LIMIT];
1271    let started = std::time::Instant::now();
1272    loop {
1273        let n = stream.peek(&mut scratch).await?;
1274        if n == 0 {
1275            return Ok(None);
1276        }
1277        let seen = &scratch[..n];
1278        match examine_peeked(seen) {
1279            HelloPeek::Sni(name) => return Ok(Some(name)),
1280            HelloPeek::Settled => return Ok(None),
1281            HelloPeek::Pending => {
1282                if n >= PEEK_LIMIT || started.elapsed() >= PEEK_TIMEOUT {
1283                    return Ok(None);
1284                }
1285                tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1286            }
1287        }
1288    }
1289}
1290
1291/// Peek the ClientHello off a passthrough connection, decide the route,
1292/// and either splice both directions to the upstream or close.
1293///
1294/// `pick` is the endpoint resolver handed to [`resolve_passthrough`]
1295/// (upstream name -> load-balanced `(address, port)`; None falls back to
1296/// the first configured endpoint). Peeking (never reading) keeps the
1297/// bytes available for the upstream once splicing starts: the entire
1298/// hello — including fragments that had to be reassembled for the SNI
1299/// decision (#120) — is still in the socket buffer and is replayed to
1300/// the upstream by the splice. A ClientHello fragmented across records
1301/// is waited for (bounded) rather than closed as no-SNI.
1302pub async fn handle_passthrough(
1303    stream: &mut TcpStream,
1304    tls: &ListenerTls,
1305    gateway: &Gateway,
1306    pick: Option<EndpointPicker<'_>>,
1307) -> std::io::Result<PassthroughAction> {
1308    let mut scratch = vec![0u8; PEEK_LIMIT];
1309    let started = std::time::Instant::now();
1310    let sni = loop {
1311        let n = stream.peek(&mut scratch).await?;
1312        if n == 0 {
1313            return Err(std::io::Error::new(
1314                std::io::ErrorKind::UnexpectedEof,
1315                "client closed before ClientHello",
1316            ));
1317        }
1318        let seen = &scratch[..n];
1319        match examine_peeked(seen) {
1320            HelloPeek::Sni(name) => break Some(name),
1321            HelloPeek::Settled => break None,
1322            HelloPeek::Pending => {
1323                // A full window without a decision means the handshake
1324                // message exceeds the reassembly budget: refuse now
1325                // rather than spinning until the peek timeout.
1326                if n >= PEEK_LIMIT || started.elapsed() >= PEEK_TIMEOUT {
1327                    break None;
1328                }
1329                tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1330            }
1331        }
1332    };
1333
1334    match resolve_passthrough(sni.as_deref(), &tls.sni_routes, gateway, pick) {
1335        PassthroughAction::Forward { host, port } => {
1336            let mut upstream = TcpStream::connect((host.as_str(), port)).await?;
1337            let _ = stream.set_nodelay(true);
1338            let _ = upstream.set_nodelay(true);
1339            tokio::io::copy_bidirectional(stream, &mut upstream).await?;
1340            let _ = stream.shutdown().await;
1341            let _ = upstream.shutdown().await;
1342            Ok(PassthroughAction::Forward { host, port })
1343        }
1344        PassthroughAction::Close => {
1345            let _ = stream.shutdown().await;
1346            Ok(PassthroughAction::Close)
1347        }
1348    }
1349}
1350
1351// ---------------------------------------------------------------------------
1352// DW-107: SPIFFE/SPIRE mTLS integration point (service mesh mode).
1353//
1354// The functions below are the documented seam between the mesh domain
1355// (which produces SVID cert/key material and the trust bundle from the
1356// SPIRE Workload API) and the TLS machinery here (which would consume
1357// that material to build the mTLS server/client config). They are
1358// SCAFFOLDED behind the `mesh` cargo feature: the shapes compile so
1359// the integration contract is fixed, but the actual rustls config
1360// construction is a documented no-op today (the `spiffe` crate would
1361// be added when production-ready). The seam is kept here, in
1362// `security::tls`, rather than in `mesh`, so the dependency direction
1363// stays downward (`mesh` is a peer of `security`, both above `config`;
1364// `mesh` does not import `security`).
1365// ---------------------------------------------------------------------------
1366
1367/// The mTLS role the SPIFFE-aware TLS config plays for a connection.
1368/// Inbound sidecar connections act as the mTLS server (terminate the
1369/// peer's SVID); outbound sidecar connections act as the mTLS client
1370/// (present the local workload's SVID).
1371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1372pub enum SpiffeMtlsRole {
1373    /// The sidecar terminates the peer's mTLS (inbound): the SVID
1374    /// cert/key is the server certificate, the trust bundle verifies
1375    /// the peer (client) SVID.
1376    Server,
1377    /// The sidecar presents the local workload's SVID (outbound): the
1378    /// SVID cert/key is the client certificate, the trust bundle
1379    /// verifies the remote sidecar's (server) SVID.
1380    Client,
1381}
1382
1383/// A SPIFFE-aware mTLS rustls config: either a client config (for
1384/// outbound upstream connections) or a server config (for inbound
1385/// listener connections). Built from an X.509 SVID and a trust
1386/// bundle by [`build_spiffe_mtls_config`].
1387pub enum SpiffeMtlsConfig {
1388    /// A client config (outbound): presents the SVID as the client
1389    /// cert, verifies peer (server) SVIDs against the trust bundle.
1390    Client(rustls::ClientConfig),
1391    /// A server config (inbound): presents the SVID as the server
1392    /// cert, verifies peer (client) SVIDs against the trust bundle.
1393    Server(rustls::ServerConfig),
1394}
1395
1396/// Build a SPIFFE-aware mTLS rustls config from an X.509 SVID and a
1397/// trust bundle. This is the integration point between the mesh domain
1398/// (which fetches SVIDs from the SPIRE Workload API) and the TLS
1399/// machinery here.
1400///
1401/// The SVID's cert chain and private key are loaded as the presented
1402/// certificate; the trust bundle is loaded as the peer-verification
1403/// root store. The resulting config uses rustls's default certificate
1404/// verifier (which validates the chain against the root store).
1405///
1406/// # Role
1407///
1408/// - [`SpiffeMtlsRole::Server`]: builds a `ServerConfig` that presents
1409///   the SVID as the server cert and verifies peer (client) SVIDs
1410///   against the trust bundle (with client auth required).
1411/// - [`SpiffeMtlsRole::Client`]: builds a `ClientConfig` that presents
1412///   the SVID as the client cert and verifies peer (server) SVIDs
1413///   against the trust bundle.
1414pub fn build_spiffe_mtls_config(
1415    role: SpiffeMtlsRole,
1416    svid: &crate::mesh::SpiffeSvid,
1417    trust_bundle: &crate::mesh::SpiffeTrustBundle,
1418) -> Result<SpiffeMtlsConfig, TlsError> {
1419    // Load the trust bundle into a root store.
1420    let mut roots = rustls::RootCertStore::empty();
1421    for cert_der in &trust_bundle.x509_certs {
1422        roots
1423            .add(CertificateDer::from(cert_der.clone()))
1424            .map_err(|e| TlsError::RootUnusable(e.to_string()))?;
1425    }
1426
1427    // Load the SVID cert chain and private key.
1428    let cert_chain: Vec<CertificateDer<'static>> = svid
1429        .x509_cert
1430        .iter()
1431        .map(|der| CertificateDer::from(der.clone()))
1432        .collect();
1433    if cert_chain.is_empty() {
1434        return Err(TlsError::NoCertificates);
1435    }
1436    let private_key = PrivateKeyDer::try_from(svid.private_key.clone())
1437        .map_err(|e| TlsError::RootUnusable(format!("invalid svid private key: {e}")))?;
1438
1439    tracing::info!(
1440        code = "spiffe_mtls_config_built",
1441        role = ?role,
1442        cert_chain_len = cert_chain.len(),
1443        trust_bundle_len = trust_bundle.x509_certs.len(),
1444        spiffe_id = %svid.spiffe_id,
1445        "built SPIFFE-aware mTLS rustls config from SVID and trust bundle"
1446    );
1447
1448    match role {
1449        SpiffeMtlsRole::Server => {
1450            // Server config: present the SVID as the server cert,
1451            // verify peer (client) SVIDs against the trust bundle.
1452            let verifier = rustls::server::WebPkiClientVerifier::builder(Arc::new(roots))
1453                .build()
1454                .map_err(|e| TlsError::RootUnusable(format!("client verifier: {e}")))?;
1455            let cfg = rustls::ServerConfig::builder()
1456                .with_client_cert_verifier(verifier)
1457                .with_single_cert(cert_chain, private_key)
1458                .map_err(TlsError::Rustls)?;
1459            Ok(SpiffeMtlsConfig::Server(cfg))
1460        }
1461        SpiffeMtlsRole::Client => {
1462            // Client config: present the SVID as the client cert,
1463            // verify peer (server) SVIDs against the trust bundle.
1464            let cfg = rustls::ClientConfig::builder()
1465                .with_root_certificates(roots)
1466                .with_client_auth_cert(cert_chain, private_key)
1467                .map_err(TlsError::Rustls)?;
1468            Ok(SpiffeMtlsConfig::Client(cfg))
1469        }
1470    }
1471}
1472
1473/// Extract the SPIFFE ID (the URI SAN) from a verified peer
1474/// certificate. This is the auth identity the mesh uses for policy
1475/// decisions: the sidecar terminates mTLS, verifies the peer SVID
1476/// against the trust bundle, then extracts the SPIFFE ID from the
1477/// peer cert's URI SAN and hands it to the auth layer as the
1478/// principal/consumer.
1479///
1480/// The extraction is a DER walk to the SubjectAltName extension,
1481/// then the URI entry tagged with the `spiffe://` scheme. This is
1482/// the same substrate as `spki_of_leaf` (no X.509 parser dependency).
1483///
1484/// Returns `None` when the cert has no URI SAN or the URI is not a
1485/// SPIFFE ID.
1486pub fn extract_spiffe_id_from_peer_cert(
1487    cert: &rustls::pki_types::CertificateDer<'_>,
1488) -> Option<crate::mesh::SpiffeIdentity> {
1489    // The DER walk: find the SubjectAltName extension (OID
1490    // 2.5.29.17), then iterate the GeneralNames looking for a URI
1491    // entry (tag 0x86 = context-specific [6]) that starts with
1492    // "spiffe://".
1493    let der = cert.as_ref();
1494    let san_ext = find_extension(der, &[0x55, 0x1D, 0x11])?; // OID 2.5.29.17
1495    let san_value = unwrap_octet_string(san_ext)?;
1496    let uri = find_uri_san(san_value)?;
1497    crate::mesh::SpiffeIdentity::parse(uri)
1498}
1499
1500/// Find an X.509 extension by its OID bytes. Returns the extension's
1501/// value (the OCTET STRING contents). This is a minimal DER walk; it
1502/// does not validate the full cert structure (the cert is already
1503/// verified by rustls before this function is called).
1504fn find_extension<'a>(der: &'a [u8], oid: &[u8]) -> Option<&'a [u8]> {
1505    // A minimal DER walker: find the tbsCertificate SEQUENCE, then
1506    // iterate its fields looking for the extensions [3] EXPLICIT
1507    // tagged element, then find the extension with the matching OID.
1508    // This is intentionally minimal (the cert is already verified).
1509    let mut offset = 0;
1510    // Outer SEQUENCE (Certificate)
1511    let (cert_seq, _after_outer) = read_sequence(der, offset)?;
1512    offset = 0;
1513    // tbsCertificate SEQUENCE (first field of Certificate)
1514    let (tbs, _) = read_sequence(cert_seq, offset)?;
1515    // Walk the tbsCertificate fields: version [0], serial, sigAlgo,
1516    // issuer, validity, subject, extensions [3].
1517    let mut p = 0;
1518    // Skip version (optional [0] EXPLICIT)
1519    if p < tbs.len() && tbs[p] == 0xA0 {
1520        let (_, after) = read_tagged(tbs, p)?;
1521        p = after;
1522    }
1523    // Skip serial (INTEGER)
1524    let (_, after) = read_tagged(tbs, p)?;
1525    p = after;
1526    // Skip sigAlgo (SEQUENCE)
1527    let (_, after) = read_tagged(tbs, p)?;
1528    p = after;
1529    // Skip issuer (SEQUENCE)
1530    let (_, after) = read_tagged(tbs, p)?;
1531    p = after;
1532    // Skip validity (SEQUENCE)
1533    let (_, after) = read_tagged(tbs, p)?;
1534    p = after;
1535    // Skip subject (SEQUENCE)
1536    let (_, after) = read_tagged(tbs, p)?;
1537    p = after;
1538    // extensions [3] EXPLICIT
1539    if p >= tbs.len() || tbs[p] != 0xA3 {
1540        return None;
1541    }
1542    let (ext_seq, _) = read_tagged(tbs, p)?;
1543    // ext_seq is a SEQUENCE of Extension SEQUENCEs
1544    let mut ep = 0;
1545    while ep < ext_seq.len() {
1546        let (ext, after) = read_sequence(ext_seq, ep)?;
1547        ep = after;
1548        // ext is: OID, BOOLEAN (optional critical), OCTET STRING value
1549        let (oid_val, after_oid) = read_tagged(ext, 0)?;
1550        if oid_val.len() != oid.len() || oid_val != oid {
1551            continue;
1552        }
1553        // Skip the OID tag byte (the read_tagged returns the value
1554        // without the tag, but the OID value includes the tag byte
1555        // in the raw DER; here oid_val is the OID content).
1556        let mut vp = after_oid;
1557        // Skip optional BOOLEAN (critical)
1558        if vp < ext.len() && ext[vp] == 0x01 {
1559            let (_, after) = read_tagged(ext, vp)?;
1560            vp = after;
1561        }
1562        // OCTET STRING value
1563        let (octet, _) = read_tagged(ext, vp)?;
1564        return Some(octet);
1565    }
1566    None
1567}
1568
1569/// Read a DER SEQUENCE at the given offset, returning the sequence
1570/// contents and the offset after the sequence.
1571fn read_sequence(der: &[u8], offset: usize) -> Option<(&[u8], usize)> {
1572    if offset >= der.len() || der[offset] != 0x30 {
1573        return None;
1574    }
1575    read_tlv(der, offset)
1576}
1577
1578/// Read a DER TLV (tag-length-value) at the given offset, returning
1579/// the value and the offset after the TLV.
1580fn read_tagged(der: &[u8], offset: usize) -> Option<(&[u8], usize)> {
1581    if offset >= der.len() {
1582        return None;
1583    }
1584    read_tlv(der, offset)
1585}
1586
1587/// Read a DER TLV at the given offset.
1588fn read_tlv(der: &[u8], offset: usize) -> Option<(&[u8], usize)> {
1589    if offset + 1 >= der.len() {
1590        return None;
1591    }
1592    let len_byte = der[offset + 1];
1593    let (len, header_len) = if len_byte < 0x80 {
1594        (len_byte as usize, 2)
1595    } else {
1596        let num_len_bytes = (len_byte & 0x7F) as usize;
1597        if num_len_bytes == 0 || num_len_bytes > 4 || offset + 2 + num_len_bytes > der.len() {
1598            return None;
1599        }
1600        let mut len = 0usize;
1601        for i in 0..num_len_bytes {
1602            len = (len << 8) | der[offset + 2 + i] as usize;
1603        }
1604        (len, 2 + num_len_bytes)
1605    };
1606    let start = offset + header_len;
1607    let end = start + len;
1608    if end > der.len() {
1609        return None;
1610    }
1611    Some((&der[start..end], end))
1612}
1613
1614/// Unwrap an OCTET STRING, returning its contents.
1615fn unwrap_octet_string(der: &[u8]) -> Option<&[u8]> {
1616    if der.is_empty() || der[0] != 0x04 {
1617        return None;
1618    }
1619    read_tlv(der, 0).map(|(v, _)| v)
1620}
1621
1622/// Find a URI SAN (tag 0x86 = context-specific [6]) in a
1623/// SubjectAltName extension value. Returns the URI as a string slice.
1624fn find_uri_san(san: &[u8]) -> Option<&str> {
1625    let mut offset = 0;
1626    while offset < san.len() {
1627        let (val, after) = read_tlv(san, offset)?;
1628        // URI GeneralName is context-specific [6] = tag 0x86.
1629        if san[offset] == 0x86 {
1630            return std::str::from_utf8(val).ok();
1631        }
1632        offset = after;
1633    }
1634    None
1635}