Skip to main content

eggnet_tls/
lib.rs

1//! Neutral rustls server identity, trust, client-authentication, and reload
2//! configuration.
3//!
4//! This crate intentionally contains no EggServe, Eggress, HTTP runtime, proxy,
5//! tracing, CLI, or Python dependencies. Applications own transport adapters,
6//! filesystem watching, and operational logging.
7//!
8//! The crate provides bounded PEM parsing, validated SNI identity selection,
9//! explicit WebPKI client authentication, and atomic replacement of immutable
10//! snapshots for new handshakes. It does not provide certificate management,
11//! filesystem watching, or application-level TLS policy.
12use std::collections::HashMap;
13use std::fmt;
14use std::fs::File;
15use std::io::BufReader;
16use std::path::Path;
17use std::sync::{Arc, RwLock};
18
19use rustls::ServerConfig;
20use rustls_pki_types::pem::PemObject;
21use rustls_pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer};
22
23/// Maximum SNI identities per [`TlsServerConfig`] (exact + wildcard combined).
24pub const MAX_TLS_IDENTITIES: usize = 64;
25/// Maximum DNS length for an SNI identity (DNS limit, without `*.` prefix).
26pub const MAX_SNI_LEN: usize = 253;
27/// Maximum trust anchors for client authentication.
28pub const MAX_TRUST_ROOTS: usize = 256;
29/// Maximum CRLs for client authentication.
30pub const MAX_CRLS: usize = 16;
31/// Maximum ALPN protocols advertised by one server configuration.
32pub const MAX_ALPN_PROTOCOLS: usize = 16;
33/// Maximum bytes per ALPN protocol identifier (TLS wire limit).
34pub const MAX_ALPN_PROTOCOL_LEN: usize = 255;
35/// Maximum certificates per server identity chain.
36pub const MAX_IDENTITY_CHAIN: usize = 8;
37/// Maximum PEM bytes accepted for trust-root/CRL parsing (1 MiB).
38pub const MAX_TRUST_PEM_BYTES: usize = 1024 * 1024;
39
40#[derive(Debug)]
41pub enum TlsError {
42    CertFileNotFound(String),
43    KeyFileNotFound(String),
44    CertReadError(String),
45    KeyReadError(String),
46    NoCertificatesFound,
47    NoPrivateKeyFound,
48    MultiplePrivateKeysFound,
49    InvalidKey(String),
50    InvalidSniName(String),
51    DuplicateIdentity(String),
52    TooManyIdentities,
53    NoIdentities,
54    InvalidTrustRoots(String),
55    TooManyTrustRoots,
56    InvalidCrl(String),
57    TooManyCrls,
58    TrustRootsRequired,
59    InvalidAlpn(String),
60}
61
62impl fmt::Display for TlsError {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::CertFileNotFound(path) => write!(f, "certificate file not found: {path}"),
66            Self::KeyFileNotFound(path) => write!(f, "key file not found: {path}"),
67            Self::CertReadError(msg) => write!(f, "failed to read certificate: {msg}"),
68            Self::KeyReadError(msg) => write!(f, "failed to read key: {msg}"),
69            Self::NoCertificatesFound => {
70                write!(f, "no valid certificates found in certificate file")
71            }
72            Self::NoPrivateKeyFound => write!(f, "no valid private key found in key file"),
73            Self::MultiplePrivateKeysFound => {
74                write!(f, "multiple private keys found; exactly one is required")
75            }
76            Self::InvalidKey(msg) => write!(f, "invalid private key: {msg}"),
77            Self::InvalidSniName(msg) => write!(f, "invalid SNI identity: {msg}"),
78            Self::DuplicateIdentity(name) => write!(f, "duplicate TLS identity: {name}"),
79            Self::TooManyIdentities => {
80                write!(f, "too many TLS identities (max {MAX_TLS_IDENTITIES})")
81            }
82            Self::NoIdentities => write!(f, "no TLS identities configured"),
83            Self::InvalidTrustRoots(msg) => write!(f, "invalid trust roots: {msg}"),
84            Self::TooManyTrustRoots => {
85                write!(f, "too many trust roots (max {MAX_TRUST_ROOTS})")
86            }
87            Self::InvalidCrl(msg) => write!(f, "invalid CRL: {msg}"),
88            Self::TooManyCrls => write!(f, "too many CRLs (max {MAX_CRLS})"),
89            Self::TrustRootsRequired => {
90                write!(f, "client authentication requires at least one trust root")
91            }
92            Self::InvalidAlpn(msg) => write!(f, "invalid ALPN protocols: {msg}"),
93        }
94    }
95}
96
97impl std::error::Error for TlsError {}
98
99/// Client-certificate authentication policy (Plan 203 Track C).
100///
101/// Built-in verification uses rustls/WebPKI against explicit trust roots.
102/// No Python callback is invoked during verification. Revocation is explicit:
103/// when no CRL policy is configured, no revocation checking is implied.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum ClientAuthMode {
106    /// No client authentication offered (default, backwards compatible).
107    #[default]
108    Disabled,
109    /// Client authentication offered; unauthenticated clients allowed.
110    /// Authenticated clients expose verified metadata.
111    Optional,
112    /// Handshake fails when no acceptable client certificate is supplied.
113    Required,
114}
115
116/// Load a rustls server configuration from PEM certificate and key files.
117pub fn load_tls_config(cert_path: &Path, key_path: &Path) -> Result<Arc<ServerConfig>, TlsError> {
118    load_tls_config_with_http2(cert_path, key_path, cfg!(feature = "http2"))
119}
120
121/// Load a rustls server configuration and explicitly choose the advertised
122/// application protocols.
123///
124/// Rust CLI callers built with `http2` normally use the default loader, which
125/// advertises `h2` before `http/1.1`. Compatibility frontends can pass
126/// `false` to keep their HTTP/1.1-only contract even when linked with a core
127/// build that has H2 enabled.
128pub fn load_tls_config_with_http2(
129    cert_path: &Path,
130    key_path: &Path,
131    http2: bool,
132) -> Result<Arc<ServerConfig>, TlsError> {
133    load_tls_config_with_alpn(cert_path, key_path, http_alpn_protocols(http2))
134}
135
136/// Load a rustls server configuration with an explicit ALPN advertisement.
137///
138/// This is the neutral hook for non-HTTP transports (Plan 222): the caller
139/// supplies its own protocol identifiers (e.g. proxy-negotiated ALPN) instead
140/// of the HTTP `h2`/`http/1.1` convenience. An empty list advertises no ALPN.
141/// Entries are bounded ([`MAX_ALPN_PROTOCOLS`] × [`MAX_ALPN_PROTOCOL_LEN`]).
142pub fn load_tls_config_with_alpn(
143    cert_path: &Path,
144    key_path: &Path,
145    alpn: Vec<Vec<u8>>,
146) -> Result<Arc<ServerConfig>, TlsError> {
147    validate_alpn(&alpn)?;
148    let (certs, key) = load_identity(cert_path, key_path)?;
149    build_single_cert_config(certs, key, alpn)
150}
151
152/// Validate an explicit ALPN advertisement list (neutral hook, Plan 222).
153fn validate_alpn(alpn: &[Vec<u8>]) -> Result<(), TlsError> {
154    if alpn.len() > MAX_ALPN_PROTOCOLS {
155        return Err(TlsError::InvalidAlpn(format!(
156            "too many ALPN protocols (max {MAX_ALPN_PROTOCOLS})"
157        )));
158    }
159    for protocol in alpn {
160        if protocol.is_empty() || protocol.len() > MAX_ALPN_PROTOCOL_LEN {
161            return Err(TlsError::InvalidAlpn(format!(
162                "ALPN protocol length must be 1..={MAX_ALPN_PROTOCOL_LEN}"
163            )));
164        }
165    }
166    Ok(())
167}
168
169/// Parse one identity from PEM bytes (caller-supplied in-memory material).
170///
171/// Validates exactly one private key and at least one certificate; key/cert
172/// pairing is checked via the active crypto provider before return.
173pub fn parse_identity_pem(
174    cert_pem: &[u8],
175    key_pem: &[u8],
176) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), TlsError> {
177    use std::io::Cursor;
178    let certs = CertificateDer::pem_reader_iter(Cursor::new(cert_pem))
179        .collect::<Result<Vec<_>, _>>()
180        .map_err(|e| TlsError::CertReadError(e.to_string()))?;
181    if certs.is_empty() {
182        return Err(TlsError::NoCertificatesFound);
183    }
184    if certs.len() > MAX_IDENTITY_CHAIN {
185        return Err(TlsError::InvalidKey(format!(
186            "certificate chain too long (max {MAX_IDENTITY_CHAIN})"
187        )));
188    }
189    let mut private_key = None;
190    let mut key_count = 0;
191    for item in PrivateKeyDer::pem_reader_iter(Cursor::new(key_pem)) {
192        let key = item.map_err(|e| TlsError::KeyReadError(e.to_string()))?;
193        key_count += 1;
194        private_key = Some(key);
195    }
196    if key_count > 1 {
197        return Err(TlsError::MultiplePrivateKeysFound);
198    }
199    let key = private_key.ok_or(TlsError::NoPrivateKeyFound)?;
200    validate_key_pair(&certs, &key)?;
201    Ok((certs, key))
202}
203
204/// Parse trust anchors from PEM bytes (bounded, validated at load).
205pub fn parse_trust_roots_pem(pem: &[u8]) -> Result<Vec<CertificateDer<'static>>, TlsError> {
206    if pem.len() > MAX_TRUST_PEM_BYTES {
207        return Err(TlsError::InvalidTrustRoots("trust PEM too large".into()));
208    }
209    use std::io::Cursor;
210    let certs = CertificateDer::pem_reader_iter(Cursor::new(pem))
211        .collect::<Result<Vec<_>, _>>()
212        .map_err(|e| TlsError::InvalidTrustRoots(e.to_string()))?;
213    if certs.is_empty() {
214        return Err(TlsError::InvalidTrustRoots("no certificates found".into()));
215    }
216    if certs.len() > MAX_TRUST_ROOTS {
217        return Err(TlsError::TooManyTrustRoots);
218    }
219    // Validate each anchor parses as a trust anchor now (fail before ready).
220    let mut store = rustls::RootCertStore::empty();
221    for cert in &certs {
222        store
223            .add(cert.clone())
224            .map_err(|e| TlsError::InvalidTrustRoots(e.to_string()))?;
225    }
226    Ok(certs)
227}
228
229/// Parse CRLs from PEM bytes (bounded, validated at load).
230pub fn parse_crls_pem(pem: &[u8]) -> Result<Vec<CertificateRevocationListDer<'static>>, TlsError> {
231    if pem.len() > MAX_TRUST_PEM_BYTES {
232        return Err(TlsError::InvalidCrl("CRL PEM too large".into()));
233    }
234    use std::io::Cursor;
235    let crls = CertificateRevocationListDer::pem_reader_iter(Cursor::new(pem))
236        .collect::<Result<Vec<_>, _>>()
237        .map_err(|e| TlsError::InvalidCrl(e.to_string()))?;
238    if crls.len() > MAX_CRLS {
239        return Err(TlsError::TooManyCrls);
240    }
241    Ok(crls)
242}
243
244fn build_single_cert_config(
245    certs: Vec<CertificateDer<'static>>,
246    key: PrivateKeyDer<'static>,
247    alpn: Vec<Vec<u8>>,
248) -> Result<Arc<ServerConfig>, TlsError> {
249    if certs.len() > MAX_IDENTITY_CHAIN {
250        return Err(TlsError::InvalidKey(format!(
251            "certificate chain too long (max {MAX_IDENTITY_CHAIN})"
252        )));
253    }
254    let mut config = ServerConfig::builder()
255        .with_no_client_auth()
256        .with_single_cert(certs, key)
257        .map_err(|e| TlsError::InvalidKey(e.to_string()))?;
258    config.alpn_protocols = alpn;
259    // Conservative defaults: no 0-RTT, rustls default ticket policy (stateless
260    // disabled / NeverProducesTickets). Set explicitly so upgrades cannot
261    // silently enable early data.
262    config.max_early_data_size = 0;
263    Ok(Arc::new(config))
264}
265
266/// Load one certificate chain and exactly one private key from PEM files.
267pub fn load_identity(
268    cert_path: &Path,
269    key_path: &Path,
270) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), TlsError> {
271    let cert_file = File::open(cert_path)
272        .map_err(|_| TlsError::CertFileNotFound(cert_path.display().to_string()))?;
273    let cert_reader = BufReader::new(cert_file);
274    let certs = CertificateDer::pem_reader_iter(cert_reader)
275        .collect::<Result<Vec<_>, _>>()
276        .map_err(|e| TlsError::CertReadError(e.to_string()))?;
277    if certs.is_empty() {
278        return Err(TlsError::NoCertificatesFound);
279    }
280    if certs.len() > MAX_IDENTITY_CHAIN {
281        return Err(TlsError::InvalidKey(format!(
282            "certificate chain too long (max {MAX_IDENTITY_CHAIN})"
283        )));
284    }
285
286    let key_file = File::open(key_path)
287        .map_err(|_| TlsError::KeyFileNotFound(key_path.display().to_string()))?;
288    let key_reader = BufReader::new(key_file);
289    let mut private_key = None;
290    let mut key_count = 0;
291    for item in PrivateKeyDer::pem_reader_iter(key_reader) {
292        let key = item.map_err(|e| TlsError::KeyReadError(e.to_string()))?;
293        key_count += 1;
294        private_key = Some(key);
295    }
296    if key_count > 1 {
297        return Err(TlsError::MultiplePrivateKeysFound);
298    }
299    let key = private_key.ok_or(TlsError::NoPrivateKeyFound)?;
300    validate_key_pair(&certs, &key)?;
301    Ok((certs, key))
302}
303
304/// Validate key/cert pairing early via the ring provider (never defer an
305/// obvious mismatch to the first hostile connection).
306fn validate_key_pair(
307    certs: &[CertificateDer<'static>],
308    key: &PrivateKeyDer<'static>,
309) -> Result<(), TlsError> {
310    let provider = rustls::crypto::ring::default_provider();
311    let signing_key = provider
312        .key_provider
313        .load_private_key(key.clone_key())
314        .map_err(|e| TlsError::InvalidKey(e.to_string()))?;
315    let ck = rustls::sign::CertifiedKey::new(certs.to_vec(), signing_key);
316    ck.keys_match()
317        .map_err(|e| TlsError::InvalidKey(e.to_string()))?;
318    Ok(())
319}
320
321/// Normalize and validate an SNI identity name.
322///
323/// Returns `(is_wildcard, normalized)` where wildcard names keep the `*.`
324/// prefix lowercased (e.g. `*.example.com`). Exact names are lowercased DNS
325/// names. Validation is DNS-conventional: ASCII, length bounds, label rules;
326/// wildcard is single-level only (`*.example.com` matches one label, never
327/// bare `example.com` or multi-level).
328fn normalize_sni_name(name: &str) -> Result<(bool, String), TlsError> {
329    let trimmed = name.trim();
330    if trimmed.is_empty() {
331        return Err(TlsError::InvalidSniName("empty SNI name".into()));
332    }
333    if trimmed.len() > MAX_SNI_LEN + 2 {
334        return Err(TlsError::InvalidSniName(format!(
335            "SNI name too long (max {MAX_SNI_LEN})"
336        )));
337    }
338    let lower = trimmed.to_ascii_lowercase();
339    if let Some(rest) = lower.strip_prefix("*.") {
340        if rest.is_empty() || rest.contains('*') {
341            return Err(TlsError::InvalidSniName(
342                "wildcard must be exactly '*.suffix'".into(),
343            ));
344        }
345        validate_dns_labels(rest)?;
346        // Conventional wildcard: suffix should contain a dot (avoid `*.com`).
347        if !rest.contains('.') {
348            return Err(TlsError::InvalidSniName(
349                "wildcard suffix must contain a dot (e.g. *.example.com)".into(),
350            ));
351        }
352        if rest.len() > MAX_SNI_LEN {
353            return Err(TlsError::InvalidSniName(format!(
354                "wildcard suffix too long (max {MAX_SNI_LEN})"
355            )));
356        }
357        Ok((true, format!("*.{rest}")))
358    } else {
359        if lower.contains('*') {
360            return Err(TlsError::InvalidSniName(
361                "wildcard only allowed as leading '*.'".into(),
362            ));
363        }
364        validate_dns_labels(&lower)?;
365        // Extra strictness via rustls DNS parsing for exact names.
366        if rustls::pki_types::DnsName::try_from(lower.as_str()).is_err() {
367            return Err(TlsError::InvalidSniName(format!(
368                "invalid DNS name: {trimmed}"
369            )));
370        }
371        Ok((false, lower))
372    }
373}
374
375fn validate_dns_labels(name: &str) -> Result<(), TlsError> {
376    if name.is_empty() || name.len() > MAX_SNI_LEN {
377        return Err(TlsError::InvalidSniName(format!(
378            "DNS name length must be 1..={MAX_SNI_LEN}"
379        )));
380    }
381    if name.starts_with('.') || name.ends_with('.') || name.starts_with('-') {
382        return Err(TlsError::InvalidSniName(
383            "invalid DNS label placement".into(),
384        ));
385    }
386    for label in name.split('.') {
387        if label.is_empty() || label.len() > 63 {
388            return Err(TlsError::InvalidSniName("invalid DNS label length".into()));
389        }
390        if label.starts_with('-') || label.ends_with('-') {
391            return Err(TlsError::InvalidSniName(
392                "DNS label must not start/end with '-'".into(),
393            ));
394        }
395        if !label
396            .bytes()
397            .all(|b| b.is_ascii_alphanumeric() || b == b'-')
398        {
399            return Err(TlsError::InvalidSniName(
400                "DNS labels must be ASCII alphanumeric or '-'".into(),
401            ));
402        }
403    }
404    Ok(())
405}
406
407/// SNI resolver with exact priority, single-level wildcard fallback, and an
408/// optional default for no-SNI/no-match clients.
409///
410/// No blocking filesystem/network IO occurs in [`rustls::server::ResolvesServerCert::resolve`];
411/// it is pure in-memory map lookup. SNI input is bounded (253 chars) before
412/// use; key/cert bytes are never logged.
413#[derive(Debug)]
414struct SniResolver {
415    exact: HashMap<String, Arc<rustls::sign::CertifiedKey>>,
416    wildcards: Vec<(String, Arc<rustls::sign::CertifiedKey>)>,
417    default: Option<Arc<rustls::sign::CertifiedKey>>,
418}
419
420impl rustls::server::ResolvesServerCert for SniResolver {
421    fn resolve(
422        &self,
423        client_hello: rustls::server::ClientHello<'_>,
424    ) -> Option<Arc<rustls::sign::CertifiedKey>> {
425        if let Some(name) = client_hello.server_name() {
426            if name.len() > MAX_SNI_LEN {
427                return self.default.clone();
428            }
429            let lower = name.to_ascii_lowercase();
430            if let Some(k) = self.exact.get(&lower) {
431                return Some(k.clone());
432            }
433            // Single-level wildcard: `*.example.com` matches `foo.example.com`
434            // only (one label, non-empty, no extra dots in the label).
435            if let Some(dot) = lower.find('.') {
436                let (label, suffix) = lower.split_at(dot);
437                let suffix = &suffix[1..];
438                if !label.is_empty() && !label.contains('.') {
439                    for (w_suffix, key) in &self.wildcards {
440                        if suffix == w_suffix {
441                            return Some(key.clone());
442                        }
443                    }
444                }
445            }
446            self.default.clone()
447        } else {
448            self.default.clone()
449        }
450    }
451}
452
453/// Validated production TLS identity configuration (Plan 203 Tracks A–C).
454///
455/// Immutable once built; reload via [`TlsReloadHandle`] (all-or-nothing for
456/// new handshakes; established connections keep their session). Exposes only
457/// sanitized metadata (identity names, client-auth mode, ALPN); never private
458/// keys.
459#[derive(Debug, Clone)]
460pub struct TlsServerConfig {
461    config: Arc<ServerConfig>,
462    identities: Vec<String>,
463    has_default: bool,
464    client_auth: ClientAuthMode,
465    alpn: Vec<Vec<u8>>,
466}
467
468impl TlsServerConfig {
469    /// Builder for [`TlsServerConfig`].
470    pub fn builder() -> TlsServerConfigBuilder {
471        TlsServerConfigBuilder::new()
472    }
473
474    /// The validated rustls server configuration for new handshakes.
475    pub fn server_config(&self) -> &Arc<ServerConfig> {
476        &self.config
477    }
478
479    /// Clone the inner server configuration (for reload handles / acceptors).
480    pub fn into_server_config(&self) -> Arc<ServerConfig> {
481        self.config.clone()
482    }
483
484    /// Configured SNI identity names (exact and `*.` wildcards, lowercased).
485    pub fn identities(&self) -> &[String] {
486        &self.identities
487    }
488
489    /// Whether a default identity exists for no-SNI/no-match clients.
490    pub fn has_default_identity(&self) -> bool {
491        self.has_default
492    }
493
494    /// Configured client-authentication policy.
495    pub fn client_auth(&self) -> ClientAuthMode {
496        self.client_auth
497    }
498
499    /// Advertised ALPN protocols.
500    pub fn alpn_protocols(&self) -> &[Vec<u8>] {
501        &self.alpn
502    }
503}
504
505/// Builder for [`TlsServerConfig`].
506#[derive(Debug, Default)]
507pub struct TlsServerConfigBuilder {
508    identities: Vec<(String, Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
509    default: Option<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
510    client_auth: ClientAuthMode,
511    roots: Vec<CertificateDer<'static>>,
512    crls: Vec<CertificateRevocationListDer<'static>>,
513    http2: bool,
514    alpn: Option<Vec<Vec<u8>>>,
515}
516
517impl TlsServerConfigBuilder {
518    /// Create an empty builder (ALPN defaults to crate `http2` feature).
519    pub fn new() -> Self {
520        Self {
521            http2: cfg!(feature = "http2"),
522            ..Default::default()
523        }
524    }
525
526    /// Advertise H2 before HTTP/1.1 when `true` (default follows the crate
527    /// `http2` feature). Coherent with `RuntimeConfig::http2.enabled`.
528    /// Clears any explicit [`Self::alpn_protocols`] override (last call wins).
529    pub fn http2(mut self, enabled: bool) -> Self {
530        self.http2 = enabled;
531        self.alpn = None;
532        self
533    }
534
535    /// Advertise an explicit ALPN list instead of the HTTP convenience.
536    ///
537    /// Neutral hook for non-HTTP transports (Plan 222): proxy or custom
538    /// transports supply their own protocol identifiers. An empty list
539    /// advertises no ALPN. Entries are bounded ([`MAX_ALPN_PROTOCOLS`] ×
540    /// [`MAX_ALPN_PROTOCOL_LEN`]); out-of-range input fails here, never at
541    /// the first handshake. Overrides [`Self::http2`] (last call wins).
542    pub fn alpn_protocols(mut self, protocols: Vec<Vec<u8>>) -> Result<Self, TlsError> {
543        validate_alpn(&protocols)?;
544        self.alpn = Some(protocols);
545        Ok(self)
546    }
547
548    /// Add one SNI identity (exact DNS or `*.suffix` wildcard).
549    pub fn add_identity(
550        mut self,
551        name: &str,
552        certs: Vec<CertificateDer<'static>>,
553        key: PrivateKeyDer<'static>,
554    ) -> Result<Self, TlsError> {
555        if self.identities.len() + usize::from(self.default.is_some()) >= MAX_TLS_IDENTITIES {
556            return Err(TlsError::TooManyIdentities);
557        }
558        let (is_wildcard, normalized) = normalize_sni_name(name)?;
559        let _ = is_wildcard;
560        if self.identities.iter().any(|(n, _, _)| n == &normalized) {
561            return Err(TlsError::DuplicateIdentity(normalized));
562        }
563        if certs.is_empty() {
564            return Err(TlsError::NoCertificatesFound);
565        }
566        if certs.len() > MAX_IDENTITY_CHAIN {
567            return Err(TlsError::InvalidKey(format!(
568                "certificate chain too long (max {MAX_IDENTITY_CHAIN})"
569            )));
570        }
571        validate_key_pair(&certs, &key)?;
572        self.identities.push((normalized, certs, key));
573        Ok(self)
574    }
575
576    /// Set the default identity for no-SNI/no-match clients.
577    pub fn default_identity(
578        mut self,
579        certs: Vec<CertificateDer<'static>>,
580        key: PrivateKeyDer<'static>,
581    ) -> Result<Self, TlsError> {
582        if certs.is_empty() {
583            return Err(TlsError::NoCertificatesFound);
584        }
585        if certs.len() > MAX_IDENTITY_CHAIN {
586            return Err(TlsError::InvalidKey(format!(
587                "certificate chain too long (max {MAX_IDENTITY_CHAIN})"
588            )));
589        }
590        validate_key_pair(&certs, &key)?;
591        self.default = Some((certs, key));
592        Ok(self)
593    }
594
595    /// Convenience for a single identity (default-only, SNI-agnostic).
596    pub fn single_identity(
597        self,
598        certs: Vec<CertificateDer<'static>>,
599        key: PrivateKeyDer<'static>,
600    ) -> Result<Self, TlsError> {
601        self.default_identity(certs, key)
602    }
603
604    /// Disable client authentication (default).
605    pub fn client_auth_disabled(mut self) -> Self {
606        self.client_auth = ClientAuthMode::Disabled;
607        self.roots.clear();
608        self.crls.clear();
609        self
610    }
611
612    /// Require client certificates verified against `roots`.
613    pub fn client_auth_required(
614        mut self,
615        roots: Vec<CertificateDer<'static>>,
616    ) -> Result<Self, TlsError> {
617        if roots.is_empty() {
618            return Err(TlsError::TrustRootsRequired);
619        }
620        if roots.len() > MAX_TRUST_ROOTS {
621            return Err(TlsError::TooManyTrustRoots);
622        }
623        self.client_auth = ClientAuthMode::Required;
624        self.roots = roots;
625        Ok(self)
626    }
627
628    /// Offer client authentication; allow unauthenticated clients.
629    pub fn client_auth_optional(
630        mut self,
631        roots: Vec<CertificateDer<'static>>,
632    ) -> Result<Self, TlsError> {
633        if roots.is_empty() {
634            return Err(TlsError::TrustRootsRequired);
635        }
636        if roots.len() > MAX_TRUST_ROOTS {
637            return Err(TlsError::TooManyTrustRoots);
638        }
639        self.client_auth = ClientAuthMode::Optional;
640        self.roots = roots;
641        Ok(self)
642    }
643
644    /// Attach CRLs to the current client-auth policy (explicit revocation).
645    ///
646    /// Without CRLs no revocation checking is implied. Bounded and validated
647    /// at build time.
648    pub fn with_crls(
649        mut self,
650        crls: Vec<CertificateRevocationListDer<'static>>,
651    ) -> Result<Self, TlsError> {
652        if crls.len() > MAX_CRLS {
653            return Err(TlsError::TooManyCrls);
654        }
655        self.crls = crls;
656        Ok(self)
657    }
658
659    /// Build the validated [`TlsServerConfig`].
660    ///
661    /// Fails before readiness on invalid names, key/cert mismatch, empty
662    /// trust, or unparsable CRLs. Never defers obvious pairing errors to the
663    /// first hostile connection.
664    pub fn build(self) -> Result<TlsServerConfig, TlsError> {
665        if self.identities.is_empty() && self.default.is_none() {
666            return Err(TlsError::NoIdentities);
667        }
668        if self.client_auth != ClientAuthMode::Disabled && self.roots.is_empty() {
669            return Err(TlsError::TrustRootsRequired);
670        }
671        let provider = rustls::crypto::ring::default_provider();
672        let mut exact: HashMap<String, Arc<rustls::sign::CertifiedKey>> = HashMap::new();
673        let mut wildcards: Vec<(String, Arc<rustls::sign::CertifiedKey>)> = Vec::new();
674        let mut identity_names: Vec<String> = Vec::new();
675        for (name, certs, key) in self.identities {
676            let signing_key = provider
677                .key_provider
678                .load_private_key(key)
679                .map_err(|e| TlsError::InvalidKey(e.to_string()))?;
680            let ck = Arc::new(rustls::sign::CertifiedKey::new(certs, signing_key));
681            ck.keys_match()
682                .map_err(|e| TlsError::InvalidKey(e.to_string()))?;
683            if let Some(suffix) = name.strip_prefix("*.") {
684                wildcards.push((suffix.to_owned(), ck));
685            } else {
686                exact.insert(name.clone(), ck);
687            }
688            identity_names.push(name);
689        }
690        let default = match self.default {
691            Some((certs, key)) => {
692                let signing_key = provider
693                    .key_provider
694                    .load_private_key(key)
695                    .map_err(|e| TlsError::InvalidKey(e.to_string()))?;
696                let ck = Arc::new(rustls::sign::CertifiedKey::new(certs, signing_key));
697                ck.keys_match()
698                    .map_err(|e| TlsError::InvalidKey(e.to_string()))?;
699                Some(ck)
700            }
701            None => None,
702        };
703        let has_default = default.is_some();
704        let resolver = Arc::new(SniResolver {
705            exact,
706            wildcards,
707            default,
708        });
709        let verifier = build_client_verifier(self.client_auth, &self.roots, self.crls)?;
710        let mut config = ServerConfig::builder()
711            .with_client_cert_verifier(verifier)
712            .with_cert_resolver(resolver);
713        let alpn = self.alpn.unwrap_or_else(|| http_alpn_protocols(self.http2));
714        config.alpn_protocols = alpn.clone();
715        config.max_early_data_size = 0;
716        identity_names.sort();
717        Ok(TlsServerConfig {
718            config: Arc::new(config),
719            identities: identity_names,
720            has_default,
721            client_auth: self.client_auth,
722            alpn,
723        })
724    }
725}
726
727fn build_client_verifier(
728    mode: ClientAuthMode,
729    roots: &[CertificateDer<'static>],
730    crls: Vec<CertificateRevocationListDer<'static>>,
731) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>, TlsError> {
732    use rustls::server::WebPkiClientVerifier;
733    match mode {
734        ClientAuthMode::Disabled => Ok(WebPkiClientVerifier::no_client_auth()),
735        ClientAuthMode::Optional | ClientAuthMode::Required => {
736            if roots.is_empty() {
737                return Err(TlsError::TrustRootsRequired);
738            }
739            let mut store = rustls::RootCertStore::empty();
740            for root in roots {
741                store
742                    .add(root.clone())
743                    .map_err(|e| TlsError::InvalidTrustRoots(e.to_string()))?;
744            }
745            let builder = WebPkiClientVerifier::builder(Arc::new(store));
746            let builder = if crls.is_empty() {
747                builder
748            } else {
749                builder.with_crls(crls)
750            };
751            let builder = match mode {
752                ClientAuthMode::Optional => builder.allow_unauthenticated(),
753                _ => builder,
754            };
755            builder
756                .build()
757                .map_err(|e| TlsError::InvalidTrustRoots(e.to_string()))
758        }
759    }
760}
761
762/// Atomic reload handle for TLS identity/trust state (Plan 203 Track F).
763///
764/// Immutable validated [`ServerConfig`] snapshots are swapped atomically;
765/// new handshakes read the current snapshot while established connections
766/// keep their session. Reload is all-or-nothing: build the replacement
767/// [`TlsServerConfig`] first (which validates), then [`Self::replace`];
768/// a failed build never touches the live snapshot.
769///
770/// No filesystem watcher is provided; operators load PEM and call replace
771/// when they decide files changed.
772#[derive(Clone, Debug)]
773pub struct TlsReloadHandle {
774    inner: Arc<RwLock<Arc<ServerConfig>>>,
775}
776
777impl TlsReloadHandle {
778    /// Create a handle around an initial validated configuration.
779    pub fn new(initial: Arc<ServerConfig>) -> Self {
780        Self {
781            inner: Arc::new(RwLock::new(initial)),
782        }
783    }
784
785    /// Create a handle from a validated [`TlsServerConfig`].
786    pub fn from_tls_server_config(config: &TlsServerConfig) -> Self {
787        Self::new(config.into_server_config())
788    }
789
790    /// Current snapshot for the next handshake (cheap `Arc` clone).
791    pub fn current(&self) -> Arc<ServerConfig> {
792        self.inner
793            .read()
794            .map(|g| g.clone())
795            .unwrap_or_else(|poisoned| poisoned.into_inner().clone())
796    }
797
798    /// Atomically replace the snapshot for new handshakes.
799    ///
800    /// Existing connections are unaffected. Never fails; the caller must
801    /// validate via [`TlsServerConfigBuilder::build`] before calling so a
802    /// failed build cannot corrupt live state.
803    pub fn replace(&self, next: Arc<ServerConfig>) {
804        if let Ok(mut guard) = self.inner.write() {
805            *guard = next;
806        }
807    }
808
809    /// Replace from a validated [`TlsServerConfig`].
810    pub fn replace_tls_server_config(&self, next: &TlsServerConfig) {
811        self.replace(next.into_server_config());
812    }
813}
814
815/// HTTP ALPN advertisement for the [`TlsServerConfigBuilder::http2`]
816/// convenience: `h2` before `http/1.1` when enabled, else `http/1.1` only.
817///
818/// Non-HTTP transports must not reuse this helper; they supply their own
819/// identifiers via [`TlsServerConfigBuilder::alpn_protocols`] (Plan 222).
820pub fn http_alpn_protocols(http2: bool) -> Vec<Vec<u8>> {
821    if http2 {
822        vec![b"h2".to_vec(), b"http/1.1".to_vec()]
823    } else {
824        vec![b"http/1.1".to_vec()]
825    }
826}
827
828#[cfg(test)]
829mod tests {
830    use super::{http_alpn_protocols, normalize_sni_name};
831
832    #[test]
833    fn h2_alpn_is_preferred_only_when_enabled() {
834        assert_eq!(
835            http_alpn_protocols(true),
836            vec![b"h2".to_vec(), b"http/1.1".to_vec()]
837        );
838        assert_eq!(http_alpn_protocols(false), vec![b"http/1.1".to_vec()]);
839    }
840
841    #[test]
842    fn sni_names_validate() {
843        assert!(normalize_sni_name("example.com").is_ok());
844        assert!(normalize_sni_name("LOCALHOST").is_ok());
845        assert!(normalize_sni_name("*.example.com").is_ok());
846        assert!(normalize_sni_name("").is_err());
847        assert!(normalize_sni_name("*").is_err());
848        assert!(normalize_sni_name("*.com").is_err());
849        assert!(normalize_sni_name("*.example.com.evil.*").is_err());
850        assert!(normalize_sni_name("bad_host!").is_err());
851        let (wild, norm) = normalize_sni_name("*.Example.COM").unwrap();
852        assert!(wild);
853        assert_eq!(norm, "*.example.com");
854        let (wild, norm) = normalize_sni_name("Example.COM").unwrap();
855        assert!(!wild);
856        assert_eq!(norm, "example.com");
857    }
858}