diff --git a/Cargo.lock b/Cargo.lock index a5c8c2829d3..0b048798234 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3626,6 +3626,7 @@ dependencies = [ "rustls", "rustls-pemfile", "rustls-platform-verifier", + "rustls-webpki", "rustpython-common", "rustpython-derive", "rustpython-host_env", diff --git a/Cargo.toml b/Cargo.toml index 3ff3029d813..3fd59e08802 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -280,6 +280,7 @@ rustls-graviola = "0.4" rustls-native-certs = "0.8" rustls-pemfile = "2.2" rustls-platform-verifier = "0.7" +rustls-webpki = { version = "0.103", default-features = false, features = ["std"] } rustyline = "18" serde = { package = "serde_core", version = "1.0.225", default-features = false, features = ["alloc"] } schannel = "0.1.29" diff --git a/crates/stdlib/Cargo.toml b/crates/stdlib/Cargo.toml index 0fead982275..0a424895f28 100644 --- a/crates/stdlib/Cargo.toml +++ b/crates/stdlib/Cargo.toml @@ -24,7 +24,7 @@ ssl-openssl-vendor = ["ssl-openssl", "openssl/vendored"] tkinter = ["dep:tk-sys", "dep:tcl-sys", "dep:widestring"] flame-it = ["flame"] -__ssl-rustls = ["ssl", "rustpython-host_env/native-certs", "rustls", "rustls-pemfile", "rustls-platform-verifier", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "oid-registry", "pkcs8"] +__ssl-rustls = ["ssl", "rustpython-host_env/native-certs", "rustls", "rustls-pemfile", "rustls-platform-verifier", "rustls-webpki", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "oid-registry", "pkcs8"] [dependencies] # rustpython crates @@ -94,6 +94,7 @@ foreign-types-shared = { workspace = true, optional = true } rustls = { workspace = true, default-features = false, features = ["std", "tls12"], optional = true } rustls-pemfile = { workspace = true, optional = true } rustls-platform-verifier = { workspace = true, optional = true } +rustls-webpki = { workspace = true, optional = true } x509-cert = { workspace = true, features = ["pem", "builder"], optional = true } x509-parser = { workspace = true, optional = true } der = { workspace = true, optional = true } @@ -113,5 +114,8 @@ widestring = { workspace = true } insta = { workspace = true } rustpython-pylib = { workspace = true, features = [ "freeze-stdlib" ] } +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +rustls = { workspace = true, features = ["aws_lc_rs"] } + [lints] workspace = true diff --git a/crates/stdlib/src/ssl.rs b/crates/stdlib/src/ssl.rs index 8a567c84f7b..2e29d9f6fbe 100644 --- a/crates/stdlib/src/ssl.rs +++ b/crates/stdlib/src/ssl.rs @@ -19,6 +19,8 @@ mod oid; // Certificate operations module (parsing, validation, conversion) mod cert; +mod chain; + /// OpenSSL cipher string parsing for `set_ciphers` mod cipher; @@ -83,7 +85,7 @@ mod _ssl { use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock}; use pem_rfc7468::{LineEnding, encode_string}; use rustls::{ - ClientConnection, Connection, HandshakeKind, RootCertStore, ServerConfig, ServerConnection, + ClientConnection, Connection, HandshakeKind, RootCertStore, ServerConnection, client::{ClientSessionMemoryCache, ClientSessionStore}, crypto::SupportedKxGroup, pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer, ServerName}, @@ -95,6 +97,7 @@ mod _ssl { // Import certificate operations module use super::cert; + use super::chain::{self, VerifiedChainBuilder}; // Import OID module use super::cipher; @@ -688,7 +691,7 @@ mod _ssl { verify_flags: PyRwLock, // Rustls configuration (built lazily) #[pytraverse(skip)] - server_config: PyRwLock>>, + server_config: PyRwLock>, // Certificate store #[pytraverse(skip)] root_certs: PyRwLock, @@ -1897,6 +1900,8 @@ mod _ssl { ), session: PyRwLock::new(None), client_config: PyRwLock::new(None), + chain_builder: PyRwLock::new(None), + verified_chain: PyRwLock::new(None), client_session_store: PyRwLock::new(None), incoming_bio: None, outgoing_bio: None, @@ -1989,6 +1994,8 @@ mod _ssl { ), session: PyRwLock::new(None), client_config: PyRwLock::new(None), + chain_builder: PyRwLock::new(None), + verified_chain: PyRwLock::new(None), client_session_store: PyRwLock::new(None), incoming_bio: Some(args.incoming), outgoing_bio: Some(args.outgoing), @@ -2351,6 +2358,10 @@ mod _ssl { // SSLSession can reuse the same verifier and client credentials. #[pytraverse(skip)] client_config: PyRwLock>>, + #[pytraverse(skip)] + chain_builder: PyRwLock>>, + #[pytraverse(skip)] + verified_chain: PyRwLock>>>, // Per-connection store containing the session selected by this connection. #[pytraverse(skip)] client_session_store: PyRwLock>>, @@ -2525,6 +2536,11 @@ mod _ssl { let session = PySSLSession { context_identity, client_config, + chain_builder: self + .chain_builder + .read() + .clone() + .expect("connection configured"), session_store, server_name: rustls_server_name, kind: session_kind, @@ -2539,63 +2555,46 @@ mod _ssl { *self.session.write() = Some(py_session); } - // Complete handshake and create session - /// Track which CA certificate from capath was used to verify peer - /// - /// This simulates lazy loading behavior: capath certificates - /// are only added to get_ca_certs() after they're actually used in a handshake. - fn track_used_ca_from_capath(&self) -> Result<(), String> { - // Extract capath_certs, releasing context lock quickly - let capath_certs = { - let context = self.context.read(); - let certs = context.capath_certificates(); - if certs.is_empty() { - return Ok(()); - } - certs - }; - - // Extract peer certificates, releasing connection lock quickly - let top_cert_der = { - let conn_guard = self.connection.lock(); - let conn = conn_guard.as_ref().ok_or("No connection")?; - let peer_certs = conn.peer_certificates().ok_or("No peer certificates")?; - if peer_certs.is_empty() { - return Ok(()); - } - peer_certs - .iter() - .map(|c| c.as_ref().to_vec()) - .next_back() - .expect("is_empty checked above") + /// Retain the verified path and publish its selected capath anchor. + fn record_verified_chain(&self, was_resumed: bool) { + let Some(builder) = self.chain_builder.read().clone() else { + return; }; - - // Get the top certificate in the chain (closest to root) - // Note: Server usually doesn't send the root CA, so we check the last cert's issuer - let (_, top_cert) = x509_parser::parse_x509_certificate(&top_cert_der) - .map_err(|e| format!("Failed to parse top cert: {e}"))?; - - let top_issuer = top_cert.issuer(); - - // Find matching CA in capath certs (skip unparseable certificates) - let matching_ca = capath_certs.iter().find_map(|ca_der| { - let (_, ca) = x509_parser::parse_x509_certificate(ca_der).ok()?; - // Check if this CA is self-signed (root CA) and matches the issuer - (ca.subject() == ca.issuer() && ca.subject() == top_issuer).then(|| ca_der.clone()) + // Like SSL_get0_verified_chain, resumption has no newly verified + // path. The peer certificate remains available separately. If the + // peer declined resumption, build the full handshake's path below. + if was_resumed { + *self.verified_chain.write() = None; + return; + } + let chain = self.connection.lock().as_ref().and_then(|conn| { + builder.build( + conn.peer_certificates()?, + rustls::pki_types::UnixTime::now(), + ) }); - - // Update ca_certs_der if we found a match - if let Some(ca_der) = matching_ca { - let context = self.context.read(); - let mut ca_certs_der = context.ca_certs_der.write(); - if !ca_certs_der.iter().any(|c| c == &ca_der) { - ca_certs_der.push(ca_der); - *context.x509_cert_count.write() += 1; + *self.verified_chain.write() = chain; + let Some(anchor) = self + .verified_chain + .read() + .as_ref() + .and_then(|chain| chain.last()) + .cloned() + else { + return; + }; + let context = self.context.read(); + let mut root_certs = context.root_certs.write(); + let mut ca_certs_der = context.ca_certs_der.write(); + if !ca_certs_der.contains(&anchor) && builder.root_der.contains(&anchor) { + let is_ca = cert::is_ca_certificate(&anchor); + ca_certs_der.push(anchor.clone()); + let _ = root_certs.add(anchor.into()); + *context.x509_cert_count.write() += 1; + if is_ca { *context.ca_cert_count.write() += 1; } } - - Ok(()) } fn complete_handshake(&self, vm: &VirtualMachine) { @@ -2621,12 +2620,7 @@ mod _ssl { } } - // Track CA certificate used during handshake (client-side only) - // This simulates lazy loading behavior for capath certificates - if !self.server_side { - // Don't fail handshake if tracking fails - let _ = self.track_used_ca_from_capath(); - } + self.record_verified_chain(was_resumed); self.create_session_after_handshake(was_resumed, vm); } @@ -3265,7 +3259,7 @@ mod _ssl { // Check if client certificate verification is required let verify_mode = *ctx.verify_mode.read(); - let (root_store, _) = ctx.verification_roots(); + let (root_store, ca_certs_der) = ctx.verification_roots(); let pha_enabled = *ctx.post_handshake_auth.read(); // Check if TLS 1.3 is being used @@ -3357,6 +3351,7 @@ mod _ssl { // Build server config using compat helper let config_options = ServerConfigOptions { protocol_settings, + ca_certs_der, cert_chain: certs_clone, private_key: key_clone, root_store: if request_initial_cert { @@ -3387,20 +3382,20 @@ mod _ssl { }; drop(ctx); - let config_arc = if let Some(cached) = cached_config_arc { + let (config_arc, chain_builder) = if let Some(cached) = cached_config_arc { cached } else { let config = create_server_config(config_options).map_err(|e| vm.new_value_error(e))?; - let config_arc = Arc::new(config); if cache_server_config { let ctx = self.context.read(); - *ctx.server_config.write() = Some(config_arc.clone()); + *ctx.server_config.write() = Some(config.clone()); } - config_arc + config }; + *self.chain_builder.write() = Some(chain_builder); let conn = ServerConnection::new(config_arc).map_err(|e| { vm.new_value_error(format!("Failed to create server connection: {e}")) @@ -3505,7 +3500,7 @@ mod _ssl { let explicit_session = self.session.read().clone(); let session_store = Arc::new(PythonClientSessionStore::new(session_cache)); - let config = if let Some(session) = explicit_session { + let (config, chain_builder) = if let Some(session) = explicit_session { let session = session .downcast_ref::() .ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?; @@ -3524,15 +3519,11 @@ mod _ssl { let mut config = (*session.client_config).clone(); config.resumption = rustls::client::Resumption::store(session_store.clone()); - Arc::new(config) + (Arc::new(config), session.chain_builder.clone()) } else { let config_options = ClientConfigOptions { protocol_settings, - root_store: if verify_mode != CERT_NONE { - Some(root_store_clone) - } else { - None - }, + root_store: Some(root_store_clone), ca_certs_der: ca_certs_der_clone, cert_chain: if !cert_chain_clone.is_empty() { Some(cert_chain_clone) @@ -3546,12 +3537,10 @@ mod _ssl { session_store: Some(session_store.clone()), crls: crls_clone, }; - Arc::new( - create_client_config(config_options) - .map_err(|e| vm.new_value_error(e))?, - ) + create_client_config(config_options).map_err(|e| vm.new_value_error(e))? }; + *self.chain_builder.write() = Some(chain_builder); *self.client_config.write() = Some(config.clone()); *self.client_session_store.write() = Some(session_store); @@ -4132,17 +4121,7 @@ mod _ssl { #[pymethod] fn get_verified_chain(&self, vm: &VirtualMachine) -> Option { - // Get peer certificates (what peer sent during handshake) - let conn_guard = self.connection.lock(); - let conn = (*conn_guard).as_ref()?; - let peer_certs = conn.peer_certificates(); - let peer_certs_slice = peer_certs?; - - // Build the verified chain using cert module - let ctx_guard = self.context.read(); - let ca_certs_der = ctx_guard.ca_certs_der.read(); - - let chain_der = cert::build_verified_chain(peer_certs_slice, &ca_certs_der); + let chain_der = self.verified_chain.read().clone()?; // Convert DER chain to Python list of Certificate objects let cert_list: Vec = chain_der @@ -4776,6 +4755,7 @@ mod _ssl { struct PySSLSession { context_identity: Arc<()>, client_config: Arc, + chain_builder: Arc, session_store: Arc, server_name: Option>, kind: ClientSessionKind, diff --git a/crates/stdlib/src/ssl/chain.rs b/crates/stdlib/src/ssl/chain.rs new file mode 100644 index 00000000000..a99c7b816f5 --- /dev/null +++ b/crates/stdlib/src/ssl/chain.rs @@ -0,0 +1,284 @@ +// spell-checker: ignore pycacert + +//! Connection-owned inputs for reconstructing WebPKI's verified path. + +use alloc::sync::Arc; +use rustls::{ + RootCertStore, + crypto::WebPkiSupportedAlgorithms, + pki_types::{CertificateDer, CertificateRevocationListDer, UnixTime}, +}; + +use super::_ssl::VERIFY_X509_PARTIAL_CHAIN; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum Purpose { + Unverified, + ServerAuth, + ClientAuth, +} + +/// Like Pyre's `VerifiedChainBuilder`, retain the configuration used by the +/// connection. Reading a mutable SSLContext after the handshake can select a +/// different root, and issuer names alone do not establish a verified path. +#[derive(Debug)] +pub(super) struct VerifiedChainBuilder { + pub purpose: Purpose, + pub roots: RootCertStore, + pub root_der: Vec>, + pub crls: Vec>, + pub only_end_entity_revocation: bool, + pub supported: WebPkiSupportedAlgorithms, + pub allow_trusted_leaf: bool, + pub verify_flags: i32, +} + +pub(super) type ServerConfig = (Arc, Arc); +pub(super) type ClientConfig = (Arc, Arc); + +impl VerifiedChainBuilder { + pub(super) fn build( + &self, + peer_chain: &[CertificateDer<'_>], + now: UnixTime, + ) -> Option>> { + self.build_path(peer_chain, now).or_else(|| { + // OpenSSL still attempts to build a chain under CERT_NONE; a + // verification failure does not prevent returning the peer chain. + (self.purpose == Purpose::Unverified && !peer_chain.is_empty()).then(|| { + peer_chain + .iter() + .map(|cert| cert.as_ref().to_vec()) + .collect() + }) + }) + } + + fn build_path(&self, peer_chain: &[CertificateDer<'_>], now: UnixTime) -> Option>> { + let (end_entity, intermediates) = peer_chain.split_first()?; + + let certificate = webpki::EndEntityCert::try_from(end_entity).ok()?; + let parsed_crls = self + .crls + .iter() + .map(|crl| { + webpki::BorrowedCertRevocationList::from_der(crl.as_ref()) + .map(webpki::CertRevocationList::from) + }) + .collect::, _>>() + .ok()?; + let crl_refs: Vec<_> = parsed_crls.iter().collect(); + let revocation = if crl_refs.is_empty() { + None + } else { + let builder = webpki::RevocationOptionsBuilder::new(&crl_refs) + .ok()? + .with_expiration_policy(webpki::ExpirationPolicy::Ignore); + let builder = if self.only_end_entity_revocation { + builder.with_depth(webpki::RevocationCheckDepth::EndEntity) + } else { + builder + }; + Some(builder.build()) + }; + let usage = match self.purpose { + Purpose::ServerAuth | Purpose::Unverified => webpki::KeyUsage::server_auth(), + Purpose::ClientAuth => webpki::KeyUsage::client_auth(), + }; + let path = certificate.verify_for_usage( + self.supported.all, + &self.roots.roots, + intermediates, + now, + usage, + revocation, + None, + ); + let Ok(path) = path else { + // PartialChainVerifier also accepts an explicitly trusted leaf. + // This is only used after the connection's verifier has succeeded. + if self.allow_trusted_leaf + && self + .root_der + .iter() + .any(|cert| cert.as_slice() == end_entity.as_ref()) + && x509_parser::parse_x509_certificate(end_entity.as_ref()).is_ok_and( + |(_, cert)| { + cert.subject() == cert.issuer() + || self.verify_flags & VERIFY_X509_PARTIAL_CHAIN != 0 + }, + ) + { + return Some(vec![end_entity.as_ref().to_vec()]); + } + return None; + }; + + // CertLoader retains DER even when RootCertStore rejects an entry, so + // the two vectors need not have matching indices. Match the complete + // selected trust anchor, including its public key and name constraints. + let anchor_der = self.root_der.iter().find(|der| { + webpki::anchor_from_trusted_cert(&CertificateDer::from(der.as_slice())) + .is_ok_and(|anchor| &anchor == path.anchor()) + }); + let mut chain = vec![end_entity.as_ref().to_vec()]; + chain.extend( + path.intermediate_certificates() + .map(|cert| cert.der().as_ref().to_vec()), + ); + // The Mozilla fallback supplies only TrustAnchors, not full DER. + // Preserve the verified leaf/intermediates when no root DER exists. + if let Some(anchor_der) = anchor_der + && chain.last() != Some(anchor_der) + { + chain.push(anchor_der.clone()); + } + Some(chain) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::time::Duration; + + fn certificate(mut pem: &[u8]) -> CertificateDer<'static> { + rustls_pemfile::certs(&mut pem).next().unwrap().unwrap() + } + + fn leaf() -> CertificateDer<'static> { + certificate(include_bytes!("../../../../Lib/test/certdata/keycert3.pem")) + } + + fn root() -> CertificateDer<'static> { + certificate(include_bytes!("../../../../Lib/test/certdata/pycacert.pem")) + } + + fn builder(root_der: Vec>) -> VerifiedChainBuilder { + let mut roots = RootCertStore::empty(); + for der in &root_der { + let _ = roots.add(CertificateDer::from(der.clone())); + } + VerifiedChainBuilder { + purpose: Purpose::ServerAuth, + roots, + root_der, + crls: Vec::new(), + only_end_entity_revocation: false, + supported: rustls::crypto::aws_lc_rs::default_provider() + .signature_verification_algorithms, + allow_trusted_leaf: false, + verify_flags: 0, + } + } + + fn verification_time() -> UnixTime { + // The vendored test certificates are valid at this fixed instant. + UnixTime::since_unix_epoch(Duration::from_secs(1_700_000_000)) + } + + #[test] + fn selected_anchor_matches_key_not_just_issuer_name() { + let root = root(); + let mut wrong_root = root.as_ref().to_vec(); + let (_, parsed) = x509_parser::parse_x509_certificate(&root).unwrap(); + let key = parsed.public_key().raw; + let key_offset = root + .windows(key.len()) + .position(|bytes| bytes == key) + .unwrap(); + // Change a modulus byte while preserving the certificate's subject and + // DER structure. Trust anchors are inputs; their self-signature is not + // verified, but this key cannot verify the leaf's signature. + wrong_root[key_offset + key.len() / 2] ^= 1; + let builder = builder(vec![wrong_root, root.as_ref().to_vec()]); + assert_eq!(builder.roots.len(), 2); + assert_eq!( + builder.build(&[leaf()], verification_time()).unwrap(), + vec![leaf().as_ref().to_vec(), root.as_ref().to_vec()], + ); + } + + #[test] + fn unused_peer_certificates_are_not_part_of_the_verified_path() { + let root = root(); + let unrelated = certificate(include_bytes!("../../../../Lib/test/certdata/keycert.pem")); + let builder = builder(vec![root.as_ref().to_vec()]); + assert_eq!( + builder + .build(&[leaf(), unrelated], verification_time()) + .unwrap(), + vec![leaf().as_ref().to_vec(), root.as_ref().to_vec()], + ); + } + + #[test] + fn rejected_root_der_does_not_shift_the_selected_anchor() { + let root = root(); + let builder = builder(vec![vec![0], root.as_ref().to_vec()]); + assert_eq!(builder.roots.len(), 1); + assert_eq!( + builder + .build(&[leaf()], verification_time()) + .unwrap() + .last(), + Some(&root.as_ref().to_vec()) + ); + } + + #[test] + fn unverified_connections_without_an_issuer_return_the_presented_chain() { + let mut builder = builder(Vec::new()); + builder.purpose = Purpose::Unverified; + assert_eq!( + builder.build(&[leaf()], verification_time()), + Some(vec![leaf().as_ref().to_vec()]) + ); + } + + #[test] + fn unverified_connections_can_build_from_configured_roots() { + let root = root(); + let mut builder = builder(vec![root.as_ref().to_vec()]); + builder.purpose = Purpose::Unverified; + assert_eq!( + builder + .build(&[leaf()], verification_time()) + .unwrap() + .last(), + Some(&root.as_ref().to_vec()) + ); + } + + #[test] + fn anchor_without_der_preserves_the_verified_leaf() { + let mut builder = builder(vec![root().as_ref().to_vec()]); + builder.root_der.clear(); + assert_eq!( + builder.build(&[leaf()], verification_time()), + Some(vec![leaf().as_ref().to_vec()]) + ); + } + + #[test] + fn client_auth_uses_the_client_certificate_purpose() { + let mut builder = builder(vec![root().as_ref().to_vec()]); + builder.purpose = Purpose::ClientAuth; + assert_eq!( + builder.build(&[leaf()], verification_time()).unwrap().len(), + 2 + ); + } + + #[test] + fn explicitly_trusted_leaf_has_a_one_certificate_path() { + let leaf = leaf(); + let mut builder = builder(vec![leaf.as_ref().to_vec()]); + builder.allow_trusted_leaf = true; + builder.verify_flags = VERIFY_X509_PARTIAL_CHAIN; + assert_eq!( + builder.build(core::slice::from_ref(&leaf), verification_time()), + Some(vec![leaf.as_ref().to_vec()]) + ); + } +} diff --git a/crates/stdlib/src/ssl/compat.rs b/crates/stdlib/src/ssl/compat.rs index 81f0c8b7091..8371ff1e4e9 100644 --- a/crates/stdlib/src/ssl/compat.rs +++ b/crates/stdlib/src/ssl/compat.rs @@ -32,6 +32,7 @@ use rustpython_vm::function::ArgBytesLike; use rustpython_vm::{AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject}; use std::io::Read; +use super::chain::{self, Purpose, VerifiedChainBuilder}; use super::providers::CryptoExt; // Import PySSLSocket from parent module @@ -423,6 +424,7 @@ pub(super) struct ServerConfigOptions { pub private_key: PrivateKeyDer<'static>, /// Root certificates for client verification (if required) pub root_store: Option, + pub ca_certs_der: Vec>, /// Whether to request client certificate pub request_client_cert: bool, /// Whether to use deferred client certificate validation (TLS 1.3) @@ -485,21 +487,44 @@ fn create_custom_crypto_provider( /// /// This abstracts the complex rustls ServerConfig building logic, /// matching SSL_CTX initialization for server sockets. -pub(super) fn create_server_config(options: ServerConfigOptions) -> Result { +pub(super) fn create_server_config( + options: ServerConfigOptions, +) -> Result { // Create custom crypto provider using helper function let custom_provider = create_custom_crypto_provider( options.protocol_settings.cipher_suites.clone(), options.protocol_settings.kx_groups.clone(), ); + let chain_builder = Arc::new(VerifiedChainBuilder { + purpose: if options.request_client_cert { + Purpose::ClientAuth + } else { + Purpose::Unverified + }, + roots: options + .root_store + .clone() + .unwrap_or_else(RootCertStore::empty), + root_der: options.ca_certs_der, + crls: Vec::new(), + only_end_entity_revocation: false, + supported: custom_provider.signature_verification_algorithms, + allow_trusted_leaf: false, + verify_flags: 0, + }); + // Step 1: Build the appropriate client cert verifier based on settings let client_cert_verifier: Option> = if let Some(root_store) = options.root_store { if options.request_client_cert { // Client certificate verification required - let base_verifier = WebPkiClientVerifier::builder(Arc::new(root_store)) - .build() - .map_err(|e| format!("Failed to create client verifier: {e}"))?; + let base_verifier = WebPkiClientVerifier::builder_with_provider( + Arc::new(root_store), + custom_provider.clone(), + ) + .build() + .map_err(|e| format!("Failed to create client verifier: {e}"))?; if options.use_deferred_validation { // TLS 1.3: Use deferred validation @@ -563,7 +588,7 @@ pub(super) fn create_server_config(options: ServerConfigOptions) -> Result>, alpn_protocols: &[Ve /// /// This abstracts the complex rustls ClientConfig building logic, /// matching SSL_CTX initialization for client sockets. -pub(super) fn create_client_config(options: ClientConfigOptions) -> Result { +pub(super) fn create_client_config( + options: ClientConfigOptions, +) -> Result { // Create custom crypto provider using helper function let custom_provider = create_custom_crypto_provider( options.protocol_settings.cipher_suites.clone(), options.protocol_settings.kx_groups.clone(), ); + let chain_builder = Arc::new(VerifiedChainBuilder { + purpose: if options.verify_server_cert { + Purpose::ServerAuth + } else { + Purpose::Unverified + }, + roots: options + .root_store + .clone() + .unwrap_or_else(RootCertStore::empty), + root_der: options.ca_certs_der.clone(), + crls: options.crls.clone(), + only_end_entity_revocation: options.verify_flags & X509_V_FLAG_CRL_CHECK != 0, + supported: custom_provider.signature_verification_algorithms, + allow_trusted_leaf: options.check_hostname + || options.verify_flags & VERIFY_X509_PARTIAL_CHAIN != 0, + verify_flags: options.verify_flags, + }); + // Step 1: Build the appropriate verifier based on verification settings let verifier: Arc = if options .verify_server_cert @@ -792,7 +838,7 @@ pub(super) fn create_client_config(options: ClientConfigOptions) -> Result