From ffbfe7205fa10c1f56b2205583728bf614a95419 Mon Sep 17 00:00:00 2001 From: Yujong Lee Date: Fri, 18 Sep 2026 19:40:16 -0700 Subject: [PATCH] refactor(rust): parse TLS settings through FromStr, From and TryFrom KeyExchangeGroup and Tls12CipherSuite parse with FromStr and fail with Unsupported, so a setting rustls cannot honor is a typed error instead of a missing value. The cipher string conversions cannot fail and use From. The rustls ClientConfig is built with TryFrom<&HttpClientConfig>, and the built-in root store is constructed in one expression --- litellm-rust/crates/http/src/config.rs | 8 +- litellm-rust/crates/http/src/lib.rs | 2 +- litellm-rust/crates/http/src/tls.rs | 208 +++++++++++++------------ 3 files changed, 113 insertions(+), 105 deletions(-) diff --git a/litellm-rust/crates/http/src/config.rs b/litellm-rust/crates/http/src/config.rs index ebe557788ca..30216405fc8 100644 --- a/litellm-rust/crates/http/src/config.rs +++ b/litellm-rust/crates/http/src/config.rs @@ -7,7 +7,7 @@ use std::{ use crate::{ error::Error, settings::{HttpSettings, SslVerify, TcpKeepalive}, - tls::{self, KeyExchangeGroup, Tls12CipherSuite, Unsupported}, + tls::{CipherSelection, KeyExchangeGroup, Tls12CipherSuite, Unsupported}, }; #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -43,7 +43,7 @@ impl HttpClientConfig { let (key_exchange_group, unsupported_curve) = match settings .ssl_ecdh_curve .as_deref() - .map(KeyExchangeGroup::from_openssl_name) + .map(str::parse::) { None => (None, None), Some(Ok(group)) => (Some(group), None), @@ -52,7 +52,7 @@ impl HttpClientConfig { let ciphers = settings .ssl_security_level .as_deref() - .map(tls::parse_cipher_string); + .map(CipherSelection::from); let verify = match &settings.ssl_verify { Some(SslVerify::Disabled) => Verify::Disabled, Some(SslVerify::CaBundle(path)) => Verify::CaBundle(path.clone()), @@ -91,7 +91,7 @@ impl HttpClientConfig { pub fn client_builder(&self) -> Result { let base = reqwest::Client::builder() - .use_preconfigured_tls(tls::client_config(self)?) + .use_preconfigured_tls(rustls::ClientConfig::try_from(self)?) .connect_timeout(self.connect_timeout) .pool_idle_timeout(self.pool_idle_timeout); let with_keepalive = match self.tcp_keepalive { diff --git a/litellm-rust/crates/http/src/lib.rs b/litellm-rust/crates/http/src/lib.rs index 45f370d3a9c..e222d0e3f50 100644 --- a/litellm-rust/crates/http/src/lib.rs +++ b/litellm-rust/crates/http/src/lib.rs @@ -10,4 +10,4 @@ pub use error::Error; pub use pool::{ClientVariant, HttpClientPool}; pub use proxy::EnvironmentProxies; pub use settings::{HttpSettings, SslVerify, TcpKeepalive}; -pub use tls::{KeyExchangeGroup, Tls12CipherSuite, Unsupported, client_config}; +pub use tls::{KeyExchangeGroup, Tls12CipherSuite, Unsupported}; diff --git a/litellm-rust/crates/http/src/tls.rs b/litellm-rust/crates/http/src/tls.rs index 605200c0be6..49405b97366 100644 --- a/litellm-rust/crates/http/src/tls.rs +++ b/litellm-rust/crates/http/src/tls.rs @@ -1,4 +1,4 @@ -use std::{fmt, path::Path, sync::Arc}; +use std::{fmt, path::Path, str::FromStr, sync::Arc}; use rustls::{ CipherSuite, ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme, @@ -19,8 +19,10 @@ pub enum KeyExchangeGroup { Secp384r1, } -impl KeyExchangeGroup { - pub(crate) fn from_openssl_name(name: &str) -> Result { +impl FromStr for KeyExchangeGroup { + type Err = Unsupported; + + fn from_str(name: &str) -> Result { match name.trim().to_ascii_lowercase().as_str() { "x25519" => Ok(Self::X25519), "prime256v1" | "secp256r1" | "p-256" => Ok(Self::Secp256r1), @@ -28,7 +30,9 @@ impl KeyExchangeGroup { _ => Err(Unsupported::EcdhCurve(name.to_owned())), } } +} +impl KeyExchangeGroup { fn supported(self) -> &'static dyn SupportedKxGroup { match self { Self::X25519 => ring::kx_group::X25519, @@ -48,19 +52,23 @@ pub enum Tls12CipherSuite { EcdheRsaChacha20, } -impl Tls12CipherSuite { - fn from_openssl_name(name: &str) -> Option { +impl FromStr for Tls12CipherSuite { + type Err = Unsupported; + + fn from_str(name: &str) -> Result { match name { - "ECDHE-ECDSA-AES128-GCM-SHA256" => Some(Self::EcdheEcdsaAes128Gcm), - "ECDHE-ECDSA-AES256-GCM-SHA384" => Some(Self::EcdheEcdsaAes256Gcm), - "ECDHE-ECDSA-CHACHA20-POLY1305" => Some(Self::EcdheEcdsaChacha20), - "ECDHE-RSA-AES128-GCM-SHA256" => Some(Self::EcdheRsaAes128Gcm), - "ECDHE-RSA-AES256-GCM-SHA384" => Some(Self::EcdheRsaAes256Gcm), - "ECDHE-RSA-CHACHA20-POLY1305" => Some(Self::EcdheRsaChacha20), - _ => None, + "ECDHE-ECDSA-AES128-GCM-SHA256" => Ok(Self::EcdheEcdsaAes128Gcm), + "ECDHE-ECDSA-AES256-GCM-SHA384" => Ok(Self::EcdheEcdsaAes256Gcm), + "ECDHE-ECDSA-CHACHA20-POLY1305" => Ok(Self::EcdheEcdsaChacha20), + "ECDHE-RSA-AES128-GCM-SHA256" => Ok(Self::EcdheRsaAes128Gcm), + "ECDHE-RSA-AES256-GCM-SHA384" => Ok(Self::EcdheRsaAes256Gcm), + "ECDHE-RSA-CHACHA20-POLY1305" => Ok(Self::EcdheRsaChacha20), + _ => Err(Unsupported::CipherToken(name.to_owned())), } } +} +impl Tls12CipherSuite { fn suite(self) -> CipherSuite { match self { Self::EcdheEcdsaAes128Gcm => CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, @@ -101,46 +109,47 @@ enum CipherToken { Unsupported(Unsupported), } -fn cipher_token(token: &str) -> CipherToken { - if let Some(suite) = Tls12CipherSuite::from_openssl_name(token) { - return CipherToken::Suite(suite); - } - match token { - "DEFAULT" | "ALL" | "HIGH" => CipherToken::EverySuite, - "@STRENGTH" | "@SECLEVEL=2" => CipherToken::Ordering, - level if level.starts_with("@SECLEVEL=") => { - CipherToken::Unsupported(Unsupported::SecurityLevel(level.to_owned())) +impl From<&str> for CipherToken { + fn from(token: &str) -> Self { + match token { + "DEFAULT" | "ALL" | "HIGH" => Self::EverySuite, + "@STRENGTH" | "@SECLEVEL=2" => Self::Ordering, + level if level.starts_with("@SECLEVEL=") => { + Self::Unsupported(Unsupported::SecurityLevel(level.to_owned())) + } + name => name.parse().map_or_else(Self::Unsupported, Self::Suite), } - other => CipherToken::Unsupported(Unsupported::CipherToken(other.to_owned())), } } -pub(crate) fn parse_cipher_string(value: &str) -> CipherSelection { - let tokens: Vec = tokenize(value) - .iter() - .map(|token| cipher_token(token)) - .collect(); - let every_suite = tokens - .iter() - .any(|token| matches!(token, CipherToken::EverySuite)); - let mut suites: Vec = tokens - .iter() - .filter_map(|token| match token { - CipherToken::Suite(suite) => Some(*suite), - _ => None, - }) - .collect(); - suites.sort_unstable(); - suites.dedup(); - CipherSelection { - tls12_cipher_suites: (!every_suite && !suites.is_empty()).then_some(suites), - unsupported: tokens - .into_iter() +impl From<&str> for CipherSelection { + fn from(value: &str) -> Self { + let tokens: Vec = tokenize(value) + .iter() + .map(|token| CipherToken::from(token.as_str())) + .collect(); + let every_suite = tokens + .iter() + .any(|token| matches!(token, CipherToken::EverySuite)); + let mut suites: Vec = tokens + .iter() .filter_map(|token| match token { - CipherToken::Unsupported(unsupported) => Some(unsupported), + CipherToken::Suite(suite) => Some(*suite), _ => None, }) - .collect(), + .collect(); + suites.sort_unstable(); + suites.dedup(); + CipherSelection { + tls12_cipher_suites: (!every_suite && !suites.is_empty()).then_some(suites), + unsupported: tokens + .into_iter() + .filter_map(|token| match token { + CipherToken::Unsupported(unsupported) => Some(unsupported), + _ => None, + }) + .collect(), + } } } @@ -155,57 +164,56 @@ fn tokenize(value: &str) -> Vec { .collect() } -pub fn client_config(config: &HttpClientConfig) -> Result { - let base = ring::default_provider(); - let provider = Arc::new(CryptoProvider { - kx_groups: config - .key_exchange_group - .map_or_else(|| base.kx_groups.clone(), |group| vec![group.supported()]), - cipher_suites: base - .cipher_suites - .iter() - .copied() - .filter(|suite| { - suite.tls13().is_some() - || config - .tls12_cipher_suites - .as_ref() - .is_none_or(|allowed| allowed.iter().any(|a| a.suite() == suite.suite())) - }) - .collect(), - ..base - }); - let builder = ClientConfig::builder_with_provider(Arc::clone(&provider)) - .with_safe_default_protocol_versions() - .map_err(|error| Error::Client(error.to_string()))?; - let verified = match &config.verify { - Verify::Disabled => builder - .dangerous() - .with_custom_certificate_verifier(Arc::new(NoVerification(provider))), - Verify::BuiltInRoots => builder.with_root_certificates(built_in_roots()), - Verify::CaBundle(path) => builder.with_root_certificates(bundle_roots(path)?), - }; - let mut tls = match &config.client_certificate { - None => verified.with_no_client_auth(), - Some(path) => { - let (chain, key) = identity(path)?; - verified - .with_client_auth_cert(chain, key) - .map_err(|error| invalid_pem(path, error))? - } - }; - tls.alpn_protocols = if config.http2 { - vec![b"h2".to_vec(), b"http/1.1".to_vec()] - } else { - vec![b"http/1.1".to_vec()] - }; - Ok(tls) -} +impl TryFrom<&HttpClientConfig> for ClientConfig { + type Error = Error; -fn built_in_roots() -> RootCertStore { - let mut store = RootCertStore::empty(); - store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - store + fn try_from(config: &HttpClientConfig) -> Result { + let base = ring::default_provider(); + let provider = Arc::new(CryptoProvider { + kx_groups: config + .key_exchange_group + .map_or_else(|| base.kx_groups.clone(), |group| vec![group.supported()]), + cipher_suites: base + .cipher_suites + .iter() + .copied() + .filter(|suite| { + suite.tls13().is_some() + || config.tls12_cipher_suites.as_ref().is_none_or(|allowed| { + allowed.iter().any(|a| a.suite() == suite.suite()) + }) + }) + .collect(), + ..base + }); + let builder = ClientConfig::builder_with_provider(Arc::clone(&provider)) + .with_safe_default_protocol_versions() + .map_err(|error| Error::Client(error.to_string()))?; + let verified = match &config.verify { + Verify::Disabled => builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerification(provider))), + Verify::BuiltInRoots => builder.with_root_certificates(RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + }), + Verify::CaBundle(path) => builder.with_root_certificates(bundle_roots(path)?), + }; + let mut tls = match &config.client_certificate { + None => verified.with_no_client_auth(), + Some(path) => { + let (chain, key) = identity(path)?; + verified + .with_client_auth_cert(chain, key) + .map_err(|error| invalid_pem(path, error))? + } + }; + tls.alpn_protocols = if config.http2 { + vec![b"h2".to_vec(), b"http/1.1".to_vec()] + } else { + vec![b"http/1.1".to_vec()] + }; + Ok(tls) + } } fn bundle_roots(path: &Path) -> Result { @@ -327,7 +335,7 @@ mod tests { #[case] curve: &str, #[case] expected: NamedGroup, ) { - let tls = client_config(&config(HttpSettings { + let tls = ClientConfig::try_from(&config(HttpSettings { ssl_ecdh_curve: Some(curve.into()), ..HttpSettings::default() })) @@ -337,7 +345,7 @@ mod tests { #[test] fn default_settings_offer_every_group_and_suite_of_the_provider() { - let tls = client_config(&config(HttpSettings::default())).unwrap(); + let tls = ClientConfig::try_from(&config(HttpSettings::default())).unwrap(); let provider = ring::default_provider(); assert_eq!(offered_groups(&tls).len(), provider.kx_groups.len()); assert_eq!( @@ -348,7 +356,7 @@ mod tests { #[test] fn named_suites_are_the_only_tls12_suites_offered_and_tls13_stays() { - let tls = client_config(&config(HttpSettings { + let tls = ClientConfig::try_from(&config(HttpSettings { ssl_security_level: Some("ECDHE-RSA-AES256-GCM-SHA384".into()), ..HttpSettings::default() })) @@ -369,7 +377,7 @@ mod tests { #[case(true, &[b"h2".as_slice(), b"http/1.1".as_slice()])] #[case(false, &[b"http/1.1".as_slice()])] fn alpn_offers_h2_only_when_http2_is_on(#[case] http2: bool, #[case] expected: &[&[u8]]) { - let tls = client_config(&config(HttpSettings { + let tls = ClientConfig::try_from(&config(HttpSettings { http2, ..HttpSettings::default() })) @@ -388,7 +396,7 @@ mod tests { b"-----BEGIN CERTIFICATE-----\nAA==\n-----END CERTIFICATE-----\n", ) .unwrap(); - let result = client_config(&HttpClientConfig { + let result = ClientConfig::try_from(&HttpClientConfig { client_certificate: Some(path.clone()), ..config(HttpSettings::default()) })