Skip to main content

sozu_lib/
crypto.rs

1//! Crypto provider selection.
2//!
3//! - `crypto-ring` (default): pure Rust via [ring](https://github.com/briansmith/ring).
4//! - `crypto-aws-lc-rs`: post-quantum-capable [aws-lc-rs](https://github.com/aws/aws-lc-rs).
5//!   Requires `cmake`.
6//! - `crypto-openssl`: system OpenSSL via [rustls-openssl](https://github.com/rustls/rustls-openssl).
7//!   Requires `cmake` + OpenSSL headers.
8//! - `fips`: implies `crypto-aws-lc-rs` plus `rustls/fips` (FIPS 140-3 build).
9//!
10//! At least one provider feature must be enabled. When several are enabled
11//! together (e.g. `cargo build --all-features` in CI, or `--features fips`
12//! on top of the default `crypto-ring`), a deterministic precedence chain
13//! selects one: `fips > ring > aws-lc-rs > openssl`. `fips` always wins, so
14//! a binary built with `--features fips` runs aws-lc-rs in FIPS mode even
15//! when `crypto-ring` is also enabled. Downstream packaging (Dockerfile,
16//! RPM, PKGBUILD) selects exactly one provider explicitly.
17
18use std::sync::LazyLock;
19
20use rustls::crypto::CryptoProvider;
21
22static DEFAULT_PROVIDER: LazyLock<CryptoProvider> = LazyLock::new(default_provider);
23
24#[cfg(not(any(
25    feature = "crypto-ring",
26    feature = "crypto-aws-lc-rs",
27    feature = "crypto-openssl"
28)))]
29compile_error!(
30    "No crypto provider selected. Enable one of: `crypto-ring`, `crypto-aws-lc-rs`, or `crypto-openssl`."
31);
32
33// `fips` wins. It implies `crypto-aws-lc-rs` plus `rustls/fips`, so the
34// resulting binary uses aws-lc-rs in FIPS mode regardless of which other
35// provider features are enabled by `default` or `--all-features`. This
36// lets `cargo build --features fips` produce a genuine FIPS binary even
37// while the default `crypto-ring` is still active, instead of silently
38// linking ring and emitting a non-FIPS provider in a `+fips`-tagged
39// build. Below `fips`, the precedence chain falls back to `ring >
40// aws-lc-rs > openssl`, matching the binary default. Downstream
41// packaging surfaces (`Dockerfile`, `os-build/...`) still select exactly
42// one feature in production builds.
43
44#[cfg(feature = "fips")]
45pub use rustls::crypto::aws_lc_rs::{
46    cipher_suite::{
47        TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
48        TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
49        TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
50        TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384, TLS13_CHACHA20_POLY1305_SHA256,
51    },
52    default_provider,
53    sign::any_supported_type,
54};
55
56#[cfg(all(feature = "crypto-ring", not(feature = "fips")))]
57pub use rustls::crypto::ring::{
58    cipher_suite::{
59        TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
60        TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
61        TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
62        TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384, TLS13_CHACHA20_POLY1305_SHA256,
63    },
64    default_provider,
65    sign::any_supported_type,
66};
67
68#[cfg(all(
69    feature = "crypto-aws-lc-rs",
70    not(feature = "fips"),
71    not(feature = "crypto-ring")
72))]
73pub use rustls::crypto::aws_lc_rs::{
74    cipher_suite::{
75        TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
76        TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
77        TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
78        TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384, TLS13_CHACHA20_POLY1305_SHA256,
79    },
80    default_provider,
81    sign::any_supported_type,
82};
83
84#[cfg(all(
85    feature = "crypto-openssl",
86    not(feature = "fips"),
87    not(feature = "crypto-ring"),
88    not(feature = "crypto-aws-lc-rs")
89))]
90pub use rustls_openssl::{
91    cipher_suite::{
92        TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
93        TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
94        TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
95        TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384, TLS13_CHACHA20_POLY1305_SHA256,
96    },
97    default_provider,
98};
99
100/// Load a private key into a signing key.
101///
102/// For `ring` and `aws-lc-rs`, this delegates to `sign::any_supported_type`.
103/// For `rustls-openssl`, this delegates to `KeyProvider::load_private_key`.
104#[cfg(all(
105    feature = "crypto-openssl",
106    not(feature = "fips"),
107    not(feature = "crypto-ring"),
108    not(feature = "crypto-aws-lc-rs")
109))]
110pub fn any_supported_type(
111    der: &rustls::pki_types::PrivateKeyDer<'_>,
112) -> Result<std::sync::Arc<dyn rustls::sign::SigningKey>, rustls::Error> {
113    use rustls::crypto::KeyProvider;
114    rustls_openssl::KeyProvider.load_private_key(der.clone_key())
115}
116
117/// Look up a key exchange group by its string name.
118///
119/// Accepts the standard TLS named group identifiers used in sozu configuration:
120/// - `"x25519"` / `"X25519"` — Curve25519 ECDHE
121/// - `"secp256r1"` / `"P-256"` — NIST P-256 ECDHE
122/// - `"secp384r1"` / `"P-384"` — NIST P-384 ECDHE
123/// - `"X25519MLKEM768"` — Post-quantum hybrid (aws-lc-rs and openssl only)
124///
125/// Returns `None` if the group name is unknown or not supported by the compiled provider.
126pub fn kx_group_by_name(name: &str) -> Option<&'static dyn rustls::crypto::SupportedKxGroup> {
127    debug_assert_provider_precedence();
128    let provider = &*DEFAULT_PROVIDER;
129    // Invariant: the resolved provider always advertises at least one key
130    // exchange group, otherwise no TLS handshake could ever complete. This
131    // catches a mis-linked or empty provider build.
132    debug_assert!(
133        !provider.kx_groups.is_empty(),
134        "the active crypto provider must advertise at least one kx group"
135    );
136    let named_group = match name {
137        "x25519" | "X25519" => rustls::NamedGroup::X25519,
138        "secp256r1" | "P-256" => rustls::NamedGroup::secp256r1,
139        "secp384r1" | "P-384" => rustls::NamedGroup::secp384r1,
140        "X25519MLKEM768" => rustls::NamedGroup::X25519MLKEM768,
141        _ => return None,
142    };
143    let resolved = provider
144        .kx_groups
145        .iter()
146        .find(|g| g.name() == named_group)
147        .copied();
148    // Post-condition: a resolved group reports exactly the name we searched
149    // for — the provider lookup must not return a mismatched group.
150    debug_assert!(
151        resolved.is_none_or(|g| g.name() == named_group),
152        "resolved kx group must match the requested named group"
153    );
154    resolved
155}
156
157/// Look up a cipher suite by its string name, filtered through the active
158/// crypto provider's supported set.
159///
160/// Accepts the rustls cipher suite names used in sozu configuration.
161/// Returns `None` if the name is unknown OR if the suite is not present
162/// in `default_provider().cipher_suites` for the active provider build.
163///
164/// The filter step is what keeps a FIPS build (aws-lc-rs with the
165/// upstream `fips` feature) from silently advertising non-FIPS suites
166/// such as ChaCha20-Poly1305 when `DEFAULT_CIPHER_LIST` includes them:
167/// rustls 0.23 only reports `ServerConfig::fips() == true` if every
168/// configured cipher and KX group is FIPS-approved (see
169/// `rustls/src/crypto/mod.rs` and `aws_lc_rs/mod.rs` ChaCha gating
170/// under `feature = "fips"`).
171pub fn cipher_suite_by_name(name: &str) -> Option<rustls::SupportedCipherSuite> {
172    debug_assert_provider_precedence();
173    // Invariant: the resolved provider always advertises at least one cipher
174    // suite — an empty set means a broken provider link.
175    debug_assert!(
176        !DEFAULT_PROVIDER.cipher_suites.is_empty(),
177        "the active crypto provider must advertise at least one cipher suite"
178    );
179    let candidate = match name {
180        "TLS13_AES_256_GCM_SHA384" => Some(TLS13_AES_256_GCM_SHA384),
181        "TLS13_AES_128_GCM_SHA256" => Some(TLS13_AES_128_GCM_SHA256),
182        "TLS13_CHACHA20_POLY1305_SHA256" => Some(TLS13_CHACHA20_POLY1305_SHA256),
183        "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" => Some(TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384),
184        "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" => Some(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
185        "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" => {
186            Some(TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)
187        }
188        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" => Some(TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384),
189        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" => Some(TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
190        "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" => {
191            Some(TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256)
192        }
193        _ => None,
194    }?;
195
196    // Only return the suite if the active provider actually advertises
197    // it. Compare by `CipherSuite` enum (newtype around the IANA value)
198    // since `SupportedCipherSuite` does not implement `Eq`.
199    let wanted = candidate.suite();
200    let resolved = DEFAULT_PROVIDER
201        .cipher_suites
202        .iter()
203        .find(|s| s.suite() == wanted)
204        .copied();
205    // Post-condition: a resolved suite is the exact IANA suite we matched on
206    // by name — the provider filter must not substitute a different suite.
207    debug_assert!(
208        resolved.is_none_or(|s| s.suite() == wanted),
209        "resolved cipher suite must match the suite the name mapped to"
210    );
211    resolved
212}
213
214/// Compile-time precedence-chain assertion: encodes the `fips > ring >
215/// aws-lc-rs > openssl` resolution that the `#[cfg(...)]` gates on the
216/// `pub use` blocks above implement, expressed via the `cfg!()` macro so it
217/// is evaluated against the *enabled feature set* rather than restating the
218/// type system. Called from the runtime provider lookups so a feature-gate
219/// regression (e.g. dropping a `not(feature = "fips")` guard) trips a
220/// `debug_assert!` in tests instead of silently linking the wrong provider.
221///
222/// The `cfg!(...)` calls are const-folded, so in release this whole function
223/// collapses to nothing.
224#[inline]
225fn debug_assert_provider_precedence() {
226    // At least one provider feature is enabled — mirrors the `compile_error!`
227    // guard at the top of the module, but as a runtime-checkable invariant.
228    debug_assert!(
229        cfg!(feature = "crypto-ring")
230            || cfg!(feature = "crypto-aws-lc-rs")
231            || cfg!(feature = "crypto-openssl"),
232        "at least one crypto provider feature must be enabled"
233    );
234    // `fips` implies `crypto-aws-lc-rs` (the Cargo manifest wires this), so a
235    // FIPS build always has aws-lc-rs available as its backing provider.
236    debug_assert!(
237        !cfg!(feature = "fips") || cfg!(feature = "crypto-aws-lc-rs"),
238        "the `fips` feature must imply `crypto-aws-lc-rs`"
239    );
240    // The aws-lc-rs `pub use` is selected only when neither `fips` nor
241    // `crypto-ring` wins ahead of it — i.e. ring outranks aws-lc-rs, which
242    // outranks openssl. Assert the openssl arm only activates when every
243    // higher-precedence provider feature is absent.
244    debug_assert!(
245        !cfg!(all(
246            feature = "crypto-openssl",
247            not(feature = "fips"),
248            not(feature = "crypto-ring"),
249            not(feature = "crypto-aws-lc-rs")
250        )) || cfg!(feature = "crypto-openssl"),
251        "the openssl provider arm is only reachable with openssl enabled"
252    );
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use rustls::NamedGroup;
259
260    #[test]
261    fn default_provider_has_tls13_cipher_suites() {
262        let provider = default_provider();
263        let names: Vec<_> = provider.cipher_suites.iter().map(|cs| cs.suite()).collect();
264        assert!(
265            names.contains(&rustls::CipherSuite::TLS13_AES_256_GCM_SHA384),
266            "provider must support TLS13_AES_256_GCM_SHA384"
267        );
268        assert!(
269            names.contains(&rustls::CipherSuite::TLS13_AES_128_GCM_SHA256),
270            "provider must support TLS13_AES_128_GCM_SHA256"
271        );
272        // CHACHA20_POLY1305 is not FIPS-approved
273        #[cfg(not(feature = "fips"))]
274        assert!(
275            names.contains(&rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256),
276            "provider must support TLS13_CHACHA20_POLY1305_SHA256"
277        );
278    }
279
280    #[test]
281    fn default_provider_has_tls12_cipher_suites() {
282        let provider = default_provider();
283        let names: Vec<_> = provider.cipher_suites.iter().map(|cs| cs.suite()).collect();
284        assert!(
285            names.contains(&rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384),
286            "provider must support TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
287        );
288        assert!(
289            names.contains(&rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384),
290            "provider must support TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
291        );
292    }
293
294    #[test]
295    fn default_provider_has_classical_kx_groups() {
296        let provider = default_provider();
297        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
298        // X25519 is not FIPS-approved (only NIST curves are)
299        #[cfg(not(feature = "fips"))]
300        assert!(
301            groups.contains(&NamedGroup::X25519),
302            "provider must support X25519 key exchange"
303        );
304        assert!(
305            groups.contains(&NamedGroup::secp256r1),
306            "provider must support secp256r1 key exchange"
307        );
308        assert!(
309            groups.contains(&NamedGroup::secp384r1),
310            "provider must support secp384r1 key exchange"
311        );
312    }
313
314    #[cfg(all(feature = "crypto-aws-lc-rs", not(feature = "fips")))]
315    #[test]
316    fn aws_lc_rs_supports_post_quantum_kx() {
317        let provider = default_provider();
318        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
319        assert!(
320            groups.contains(&NamedGroup::X25519MLKEM768),
321            "aws-lc-rs provider must support X25519MLKEM768 post-quantum key exchange"
322        );
323    }
324
325    #[cfg(all(feature = "crypto-aws-lc-rs", not(feature = "fips")))]
326    #[test]
327    fn aws_lc_rs_has_more_kx_groups_than_classical() {
328        let provider = default_provider();
329        // aws-lc-rs should have more kx groups than just the 3 classical ones
330        // (X25519, secp256r1, secp384r1) because it also includes PQ groups
331        assert!(
332            provider.kx_groups.len() > 3,
333            "aws-lc-rs should have more than 3 kx groups (has {}), including post-quantum",
334            provider.kx_groups.len()
335        );
336    }
337
338    #[cfg(feature = "fips")]
339    #[test]
340    fn fips_provider_has_nist_kx_groups() {
341        let provider = default_provider();
342        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
343        assert!(
344            groups.contains(&NamedGroup::secp256r1),
345            "FIPS provider must support secp256r1"
346        );
347        assert!(
348            groups.contains(&NamedGroup::secp384r1),
349            "FIPS provider must support secp384r1"
350        );
351    }
352
353    #[cfg(feature = "fips")]
354    #[test]
355    fn fips_provider_has_aes_gcm_cipher_suites() {
356        let provider = default_provider();
357        let suites: Vec<_> = provider.cipher_suites.iter().map(|cs| cs.suite()).collect();
358        assert!(
359            suites.contains(&rustls::CipherSuite::TLS13_AES_256_GCM_SHA384),
360            "FIPS provider must support TLS13_AES_256_GCM_SHA384"
361        );
362        assert!(
363            suites.contains(&rustls::CipherSuite::TLS13_AES_128_GCM_SHA256),
364            "FIPS provider must support TLS13_AES_128_GCM_SHA256"
365        );
366    }
367
368    #[cfg(all(feature = "crypto-ring", not(feature = "fips")))]
369    #[test]
370    fn ring_has_no_mlkem() {
371        let provider = default_provider();
372        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
373        assert!(
374            !groups.contains(&NamedGroup::X25519MLKEM768),
375            "ring provider should not advertise X25519MLKEM768"
376        );
377    }
378
379    #[cfg(all(
380        feature = "crypto-openssl",
381        not(feature = "fips"),
382        not(feature = "crypto-ring"),
383        not(feature = "crypto-aws-lc-rs")
384    ))]
385    #[test]
386    fn openssl_pq_kx_depends_on_openssl_version() {
387        let provider = default_provider();
388        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
389        let has_pq = groups.contains(&NamedGroup::X25519MLKEM768);
390        // X25519MLKEM768 is only available with OpenSSL 3.5+.
391        // On older versions, the provider should still work with classical groups.
392        if has_pq {
393            println!("OpenSSL 3.5+ detected: X25519MLKEM768 is available");
394        } else {
395            println!("OpenSSL < 3.5: X25519MLKEM768 not available, classical groups only");
396        }
397        // In all cases, classical groups must be present
398        assert!(groups.contains(&NamedGroup::X25519));
399        assert!(groups.contains(&NamedGroup::secp256r1));
400    }
401
402    #[cfg(not(feature = "fips"))]
403    #[test]
404    fn kx_group_by_name_resolves_x25519() {
405        let group = kx_group_by_name("x25519").expect("x25519 should be supported");
406        assert_eq!(group.name(), NamedGroup::X25519);
407    }
408
409    #[cfg(not(feature = "fips"))]
410    #[test]
411    fn kx_group_by_name_resolves_x25519_uppercase() {
412        let group = kx_group_by_name("X25519").expect("X25519 should be supported");
413        assert_eq!(group.name(), NamedGroup::X25519);
414    }
415
416    #[cfg(feature = "fips")]
417    #[test]
418    fn kx_group_by_name_returns_none_for_x25519_in_fips() {
419        assert!(
420            kx_group_by_name("x25519").is_none(),
421            "X25519 is not FIPS-approved"
422        );
423        assert!(
424            kx_group_by_name("X25519").is_none(),
425            "X25519 is not FIPS-approved"
426        );
427    }
428
429    #[test]
430    fn kx_group_by_name_resolves_p256() {
431        let group = kx_group_by_name("P-256").expect("P-256 should be supported");
432        assert_eq!(group.name(), NamedGroup::secp256r1);
433    }
434
435    #[test]
436    fn kx_group_by_name_resolves_secp256r1() {
437        let group = kx_group_by_name("secp256r1").expect("secp256r1 should be supported");
438        assert_eq!(group.name(), NamedGroup::secp256r1);
439    }
440
441    #[test]
442    fn kx_group_by_name_resolves_p384() {
443        let group = kx_group_by_name("P-384").expect("P-384 should be supported");
444        assert_eq!(group.name(), NamedGroup::secp384r1);
445    }
446
447    #[test]
448    fn kx_group_by_name_returns_none_for_unknown() {
449        assert!(kx_group_by_name("P-521").is_none());
450        assert!(kx_group_by_name("unknown").is_none());
451        assert!(kx_group_by_name("").is_none());
452    }
453
454    #[cfg(all(feature = "crypto-aws-lc-rs", not(feature = "fips")))]
455    #[test]
456    fn kx_group_by_name_resolves_x25519mlkem768() {
457        let group = kx_group_by_name("X25519MLKEM768").expect("X25519MLKEM768 should be supported");
458        assert_eq!(group.name(), NamedGroup::X25519MLKEM768);
459    }
460
461    #[cfg(all(feature = "crypto-ring", not(feature = "fips")))]
462    #[test]
463    fn kx_group_by_name_returns_none_for_mlkem_on_ring() {
464        assert!(
465            kx_group_by_name("X25519MLKEM768").is_none(),
466            "ring does not support X25519MLKEM768"
467        );
468    }
469
470    #[test]
471    fn can_load_rsa_private_key() {
472        use rustls::pki_types::PrivateKeyDer;
473        use rustls::pki_types::pem::PemObject;
474
475        let key_pem = include_str!("../assets/key.pem");
476        let private_key =
477            PrivateKeyDer::from_pem_slice(key_pem.as_bytes()).expect("failed to parse PEM key");
478        any_supported_type(&private_key).expect("provider must be able to load RSA private key");
479    }
480
481    #[test]
482    fn can_build_server_config_with_tls13() {
483        use std::sync::Arc;
484
485        let provider = default_provider();
486        let config = rustls::ServerConfig::builder_with_provider(Arc::new(provider))
487            .with_protocol_versions(&[&rustls::version::TLS13])
488            .expect("failed to build TLS 1.3 config")
489            .with_no_client_auth()
490            .with_cert_resolver(Arc::new(crate::tls::MutexCertificateResolver::default()));
491        assert!(
492            !config.alpn_protocols.contains(&b"h2".to_vec()),
493            "default config should not have ALPN set"
494        );
495    }
496
497    #[test]
498    fn can_build_server_config_with_tls12_and_tls13() {
499        use std::sync::Arc;
500
501        let provider = default_provider();
502        rustls::ServerConfig::builder_with_provider(Arc::new(provider))
503            .with_protocol_versions(&[&rustls::version::TLS12, &rustls::version::TLS13])
504            .expect("failed to build TLS 1.2+1.3 config")
505            .with_no_client_auth()
506            .with_cert_resolver(Arc::new(crate::tls::MutexCertificateResolver::default()));
507    }
508
509    #[cfg(all(feature = "crypto-aws-lc-rs", not(feature = "fips")))]
510    #[test]
511    fn pq_kx_compatible_with_server_config() {
512        use std::sync::Arc;
513
514        let provider = default_provider();
515        // Verify the PQ kx group is present
516        let has_pq = provider
517            .kx_groups
518            .iter()
519            .any(|g| g.name() == NamedGroup::X25519MLKEM768);
520        assert!(has_pq, "X25519MLKEM768 must be in the provider");
521
522        // Build a TLS 1.3 config (PQ kx is only for TLS 1.3)
523        let config = rustls::ServerConfig::builder_with_provider(Arc::new(provider))
524            .with_protocol_versions(&[&rustls::version::TLS13])
525            .expect("TLS 1.3 config with PQ kx should build successfully")
526            .with_no_client_auth()
527            .with_cert_resolver(Arc::new(crate::tls::MutexCertificateResolver::default()));
528
529        // The config should be valid
530        assert!(
531            !config.alpn_protocols.contains(&b"h2".to_vec()),
532            "default config should not have ALPN set"
533        );
534    }
535
536    #[test]
537    fn default_cipher_list_names_resolve_to_valid_suites() {
538        use sozu_command::config::DEFAULT_CIPHER_LIST;
539
540        let all_suites = [
541            ("TLS13_AES_256_GCM_SHA384", TLS13_AES_256_GCM_SHA384.suite()),
542            ("TLS13_AES_128_GCM_SHA256", TLS13_AES_128_GCM_SHA256.suite()),
543            (
544                "TLS13_CHACHA20_POLY1305_SHA256",
545                TLS13_CHACHA20_POLY1305_SHA256.suite(),
546            ),
547            (
548                "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
549                TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.suite(),
550            ),
551            (
552                "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
553                TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.suite(),
554            ),
555            (
556                "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
557                TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256.suite(),
558            ),
559            (
560                "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
561                TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384.suite(),
562            ),
563            (
564                "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
565                TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256.suite(),
566            ),
567            (
568                "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
569                TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256.suite(),
570            ),
571        ];
572
573        // Verify every name in DEFAULT_CIPHER_LIST matches a known suite
574        for name in DEFAULT_CIPHER_LIST {
575            let found = all_suites.iter().any(|(n, _)| *n == name);
576            assert!(
577                found,
578                "DEFAULT_CIPHER_LIST entry {name:?} does not match any known cipher suite"
579            );
580        }
581
582        // Verify the count matches (no duplicates, no missing)
583        assert_eq!(
584            DEFAULT_CIPHER_LIST.len(),
585            all_suites.len(),
586            "DEFAULT_CIPHER_LIST length should match number of known suites"
587        );
588    }
589
590    #[test]
591    fn cipher_suite_by_name_resolves_tls13() {
592        assert!(cipher_suite_by_name("TLS13_AES_256_GCM_SHA384").is_some());
593        assert!(cipher_suite_by_name("TLS13_AES_128_GCM_SHA256").is_some());
594        #[cfg(not(feature = "fips"))]
595        assert!(cipher_suite_by_name("TLS13_CHACHA20_POLY1305_SHA256").is_some());
596    }
597
598    #[test]
599    fn cipher_suite_by_name_resolves_tls12() {
600        assert!(cipher_suite_by_name("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384").is_some());
601        assert!(cipher_suite_by_name("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384").is_some());
602    }
603
604    #[test]
605    fn cipher_suite_by_name_returns_none_for_unknown() {
606        assert!(cipher_suite_by_name("UNKNOWN_CIPHER").is_none());
607        assert!(cipher_suite_by_name("").is_none());
608        // OpenSSL-style names should NOT match (this was the old bug)
609        assert!(cipher_suite_by_name("TLS_AES_256_GCM_SHA384").is_none());
610    }
611}