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    let provider = &*DEFAULT_PROVIDER;
128    let named_group = match name {
129        "x25519" | "X25519" => rustls::NamedGroup::X25519,
130        "secp256r1" | "P-256" => rustls::NamedGroup::secp256r1,
131        "secp384r1" | "P-384" => rustls::NamedGroup::secp384r1,
132        "X25519MLKEM768" => rustls::NamedGroup::X25519MLKEM768,
133        _ => return None,
134    };
135    provider
136        .kx_groups
137        .iter()
138        .find(|g| g.name() == named_group)
139        .copied()
140}
141
142/// Look up a cipher suite by its string name, filtered through the active
143/// crypto provider's supported set.
144///
145/// Accepts the rustls cipher suite names used in sozu configuration.
146/// Returns `None` if the name is unknown OR if the suite is not present
147/// in `default_provider().cipher_suites` for the active provider build.
148///
149/// The filter step is what keeps a FIPS build (aws-lc-rs with the
150/// upstream `fips` feature) from silently advertising non-FIPS suites
151/// such as ChaCha20-Poly1305 when `DEFAULT_CIPHER_LIST` includes them:
152/// rustls 0.23 only reports `ServerConfig::fips() == true` if every
153/// configured cipher and KX group is FIPS-approved (see
154/// `rustls/src/crypto/mod.rs` and `aws_lc_rs/mod.rs` ChaCha gating
155/// under `feature = "fips"`).
156pub fn cipher_suite_by_name(name: &str) -> Option<rustls::SupportedCipherSuite> {
157    let candidate = match name {
158        "TLS13_AES_256_GCM_SHA384" => Some(TLS13_AES_256_GCM_SHA384),
159        "TLS13_AES_128_GCM_SHA256" => Some(TLS13_AES_128_GCM_SHA256),
160        "TLS13_CHACHA20_POLY1305_SHA256" => Some(TLS13_CHACHA20_POLY1305_SHA256),
161        "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" => Some(TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384),
162        "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" => Some(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
163        "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" => {
164            Some(TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256)
165        }
166        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" => Some(TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384),
167        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" => Some(TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
168        "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" => {
169            Some(TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256)
170        }
171        _ => None,
172    }?;
173
174    // Only return the suite if the active provider actually advertises
175    // it. Compare by `CipherSuite` enum (newtype around the IANA value)
176    // since `SupportedCipherSuite` does not implement `Eq`.
177    let wanted = candidate.suite();
178    DEFAULT_PROVIDER
179        .cipher_suites
180        .iter()
181        .find(|s| s.suite() == wanted)
182        .copied()
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use rustls::NamedGroup;
189
190    #[test]
191    fn default_provider_has_tls13_cipher_suites() {
192        let provider = default_provider();
193        let names: Vec<_> = provider.cipher_suites.iter().map(|cs| cs.suite()).collect();
194        assert!(
195            names.contains(&rustls::CipherSuite::TLS13_AES_256_GCM_SHA384),
196            "provider must support TLS13_AES_256_GCM_SHA384"
197        );
198        assert!(
199            names.contains(&rustls::CipherSuite::TLS13_AES_128_GCM_SHA256),
200            "provider must support TLS13_AES_128_GCM_SHA256"
201        );
202        // CHACHA20_POLY1305 is not FIPS-approved
203        #[cfg(not(feature = "fips"))]
204        assert!(
205            names.contains(&rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256),
206            "provider must support TLS13_CHACHA20_POLY1305_SHA256"
207        );
208    }
209
210    #[test]
211    fn default_provider_has_tls12_cipher_suites() {
212        let provider = default_provider();
213        let names: Vec<_> = provider.cipher_suites.iter().map(|cs| cs.suite()).collect();
214        assert!(
215            names.contains(&rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384),
216            "provider must support TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
217        );
218        assert!(
219            names.contains(&rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384),
220            "provider must support TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
221        );
222    }
223
224    #[test]
225    fn default_provider_has_classical_kx_groups() {
226        let provider = default_provider();
227        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
228        // X25519 is not FIPS-approved (only NIST curves are)
229        #[cfg(not(feature = "fips"))]
230        assert!(
231            groups.contains(&NamedGroup::X25519),
232            "provider must support X25519 key exchange"
233        );
234        assert!(
235            groups.contains(&NamedGroup::secp256r1),
236            "provider must support secp256r1 key exchange"
237        );
238        assert!(
239            groups.contains(&NamedGroup::secp384r1),
240            "provider must support secp384r1 key exchange"
241        );
242    }
243
244    #[cfg(all(feature = "crypto-aws-lc-rs", not(feature = "fips")))]
245    #[test]
246    fn aws_lc_rs_supports_post_quantum_kx() {
247        let provider = default_provider();
248        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
249        assert!(
250            groups.contains(&NamedGroup::X25519MLKEM768),
251            "aws-lc-rs provider must support X25519MLKEM768 post-quantum key exchange"
252        );
253    }
254
255    #[cfg(all(feature = "crypto-aws-lc-rs", not(feature = "fips")))]
256    #[test]
257    fn aws_lc_rs_has_more_kx_groups_than_classical() {
258        let provider = default_provider();
259        // aws-lc-rs should have more kx groups than just the 3 classical ones
260        // (X25519, secp256r1, secp384r1) because it also includes PQ groups
261        assert!(
262            provider.kx_groups.len() > 3,
263            "aws-lc-rs should have more than 3 kx groups (has {}), including post-quantum",
264            provider.kx_groups.len()
265        );
266    }
267
268    #[cfg(feature = "fips")]
269    #[test]
270    fn fips_provider_has_nist_kx_groups() {
271        let provider = default_provider();
272        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
273        assert!(
274            groups.contains(&NamedGroup::secp256r1),
275            "FIPS provider must support secp256r1"
276        );
277        assert!(
278            groups.contains(&NamedGroup::secp384r1),
279            "FIPS provider must support secp384r1"
280        );
281    }
282
283    #[cfg(feature = "fips")]
284    #[test]
285    fn fips_provider_has_aes_gcm_cipher_suites() {
286        let provider = default_provider();
287        let suites: Vec<_> = provider.cipher_suites.iter().map(|cs| cs.suite()).collect();
288        assert!(
289            suites.contains(&rustls::CipherSuite::TLS13_AES_256_GCM_SHA384),
290            "FIPS provider must support TLS13_AES_256_GCM_SHA384"
291        );
292        assert!(
293            suites.contains(&rustls::CipherSuite::TLS13_AES_128_GCM_SHA256),
294            "FIPS provider must support TLS13_AES_128_GCM_SHA256"
295        );
296    }
297
298    #[cfg(all(feature = "crypto-ring", not(feature = "fips")))]
299    #[test]
300    fn ring_has_no_mlkem() {
301        let provider = default_provider();
302        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
303        assert!(
304            !groups.contains(&NamedGroup::X25519MLKEM768),
305            "ring provider should not advertise X25519MLKEM768"
306        );
307    }
308
309    #[cfg(all(
310        feature = "crypto-openssl",
311        not(feature = "fips"),
312        not(feature = "crypto-ring"),
313        not(feature = "crypto-aws-lc-rs")
314    ))]
315    #[test]
316    fn openssl_pq_kx_depends_on_openssl_version() {
317        let provider = default_provider();
318        let groups: Vec<_> = provider.kx_groups.iter().map(|g| g.name()).collect();
319        let has_pq = groups.contains(&NamedGroup::X25519MLKEM768);
320        // X25519MLKEM768 is only available with OpenSSL 3.5+.
321        // On older versions, the provider should still work with classical groups.
322        if has_pq {
323            println!("OpenSSL 3.5+ detected: X25519MLKEM768 is available");
324        } else {
325            println!("OpenSSL < 3.5: X25519MLKEM768 not available, classical groups only");
326        }
327        // In all cases, classical groups must be present
328        assert!(groups.contains(&NamedGroup::X25519));
329        assert!(groups.contains(&NamedGroup::secp256r1));
330    }
331
332    #[cfg(not(feature = "fips"))]
333    #[test]
334    fn kx_group_by_name_resolves_x25519() {
335        let group = kx_group_by_name("x25519").expect("x25519 should be supported");
336        assert_eq!(group.name(), NamedGroup::X25519);
337    }
338
339    #[cfg(not(feature = "fips"))]
340    #[test]
341    fn kx_group_by_name_resolves_x25519_uppercase() {
342        let group = kx_group_by_name("X25519").expect("X25519 should be supported");
343        assert_eq!(group.name(), NamedGroup::X25519);
344    }
345
346    #[cfg(feature = "fips")]
347    #[test]
348    fn kx_group_by_name_returns_none_for_x25519_in_fips() {
349        assert!(
350            kx_group_by_name("x25519").is_none(),
351            "X25519 is not FIPS-approved"
352        );
353        assert!(
354            kx_group_by_name("X25519").is_none(),
355            "X25519 is not FIPS-approved"
356        );
357    }
358
359    #[test]
360    fn kx_group_by_name_resolves_p256() {
361        let group = kx_group_by_name("P-256").expect("P-256 should be supported");
362        assert_eq!(group.name(), NamedGroup::secp256r1);
363    }
364
365    #[test]
366    fn kx_group_by_name_resolves_secp256r1() {
367        let group = kx_group_by_name("secp256r1").expect("secp256r1 should be supported");
368        assert_eq!(group.name(), NamedGroup::secp256r1);
369    }
370
371    #[test]
372    fn kx_group_by_name_resolves_p384() {
373        let group = kx_group_by_name("P-384").expect("P-384 should be supported");
374        assert_eq!(group.name(), NamedGroup::secp384r1);
375    }
376
377    #[test]
378    fn kx_group_by_name_returns_none_for_unknown() {
379        assert!(kx_group_by_name("P-521").is_none());
380        assert!(kx_group_by_name("unknown").is_none());
381        assert!(kx_group_by_name("").is_none());
382    }
383
384    #[cfg(all(feature = "crypto-aws-lc-rs", not(feature = "fips")))]
385    #[test]
386    fn kx_group_by_name_resolves_x25519mlkem768() {
387        let group = kx_group_by_name("X25519MLKEM768").expect("X25519MLKEM768 should be supported");
388        assert_eq!(group.name(), NamedGroup::X25519MLKEM768);
389    }
390
391    #[cfg(all(feature = "crypto-ring", not(feature = "fips")))]
392    #[test]
393    fn kx_group_by_name_returns_none_for_mlkem_on_ring() {
394        assert!(
395            kx_group_by_name("X25519MLKEM768").is_none(),
396            "ring does not support X25519MLKEM768"
397        );
398    }
399
400    #[test]
401    fn can_load_rsa_private_key() {
402        use rustls::pki_types::PrivateKeyDer;
403        use rustls::pki_types::pem::PemObject;
404
405        let key_pem = include_str!("../assets/key.pem");
406        let private_key =
407            PrivateKeyDer::from_pem_slice(key_pem.as_bytes()).expect("failed to parse PEM key");
408        any_supported_type(&private_key).expect("provider must be able to load RSA private key");
409    }
410
411    #[test]
412    fn can_build_server_config_with_tls13() {
413        use std::sync::Arc;
414
415        let provider = default_provider();
416        let config = rustls::ServerConfig::builder_with_provider(Arc::new(provider))
417            .with_protocol_versions(&[&rustls::version::TLS13])
418            .expect("failed to build TLS 1.3 config")
419            .with_no_client_auth()
420            .with_cert_resolver(Arc::new(crate::tls::MutexCertificateResolver::default()));
421        assert!(
422            !config.alpn_protocols.contains(&b"h2".to_vec()),
423            "default config should not have ALPN set"
424        );
425    }
426
427    #[test]
428    fn can_build_server_config_with_tls12_and_tls13() {
429        use std::sync::Arc;
430
431        let provider = default_provider();
432        rustls::ServerConfig::builder_with_provider(Arc::new(provider))
433            .with_protocol_versions(&[&rustls::version::TLS12, &rustls::version::TLS13])
434            .expect("failed to build TLS 1.2+1.3 config")
435            .with_no_client_auth()
436            .with_cert_resolver(Arc::new(crate::tls::MutexCertificateResolver::default()));
437    }
438
439    #[cfg(all(feature = "crypto-aws-lc-rs", not(feature = "fips")))]
440    #[test]
441    fn pq_kx_compatible_with_server_config() {
442        use std::sync::Arc;
443
444        let provider = default_provider();
445        // Verify the PQ kx group is present
446        let has_pq = provider
447            .kx_groups
448            .iter()
449            .any(|g| g.name() == NamedGroup::X25519MLKEM768);
450        assert!(has_pq, "X25519MLKEM768 must be in the provider");
451
452        // Build a TLS 1.3 config (PQ kx is only for TLS 1.3)
453        let config = rustls::ServerConfig::builder_with_provider(Arc::new(provider))
454            .with_protocol_versions(&[&rustls::version::TLS13])
455            .expect("TLS 1.3 config with PQ kx should build successfully")
456            .with_no_client_auth()
457            .with_cert_resolver(Arc::new(crate::tls::MutexCertificateResolver::default()));
458
459        // The config should be valid
460        assert!(
461            !config.alpn_protocols.contains(&b"h2".to_vec()),
462            "default config should not have ALPN set"
463        );
464    }
465
466    #[test]
467    fn default_cipher_list_names_resolve_to_valid_suites() {
468        use sozu_command::config::DEFAULT_CIPHER_LIST;
469
470        let all_suites = [
471            ("TLS13_AES_256_GCM_SHA384", TLS13_AES_256_GCM_SHA384.suite()),
472            ("TLS13_AES_128_GCM_SHA256", TLS13_AES_128_GCM_SHA256.suite()),
473            (
474                "TLS13_CHACHA20_POLY1305_SHA256",
475                TLS13_CHACHA20_POLY1305_SHA256.suite(),
476            ),
477            (
478                "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
479                TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.suite(),
480            ),
481            (
482                "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
483                TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.suite(),
484            ),
485            (
486                "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
487                TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256.suite(),
488            ),
489            (
490                "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
491                TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384.suite(),
492            ),
493            (
494                "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
495                TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256.suite(),
496            ),
497            (
498                "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
499                TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256.suite(),
500            ),
501        ];
502
503        // Verify every name in DEFAULT_CIPHER_LIST matches a known suite
504        for name in DEFAULT_CIPHER_LIST {
505            let found = all_suites.iter().any(|(n, _)| *n == name);
506            assert!(
507                found,
508                "DEFAULT_CIPHER_LIST entry {name:?} does not match any known cipher suite"
509            );
510        }
511
512        // Verify the count matches (no duplicates, no missing)
513        assert_eq!(
514            DEFAULT_CIPHER_LIST.len(),
515            all_suites.len(),
516            "DEFAULT_CIPHER_LIST length should match number of known suites"
517        );
518    }
519
520    #[test]
521    fn cipher_suite_by_name_resolves_tls13() {
522        assert!(cipher_suite_by_name("TLS13_AES_256_GCM_SHA384").is_some());
523        assert!(cipher_suite_by_name("TLS13_AES_128_GCM_SHA256").is_some());
524        #[cfg(not(feature = "fips"))]
525        assert!(cipher_suite_by_name("TLS13_CHACHA20_POLY1305_SHA256").is_some());
526    }
527
528    #[test]
529    fn cipher_suite_by_name_resolves_tls12() {
530        assert!(cipher_suite_by_name("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384").is_some());
531        assert!(cipher_suite_by_name("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384").is_some());
532    }
533
534    #[test]
535    fn cipher_suite_by_name_returns_none_for_unknown() {
536        assert!(cipher_suite_by_name("UNKNOWN_CIPHER").is_none());
537        assert!(cipher_suite_by_name("").is_none());
538        // OpenSSL-style names should NOT match (this was the old bug)
539        assert!(cipher_suite_by_name("TLS_AES_256_GCM_SHA384").is_none());
540    }
541}