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}