@@ -19,6 +19,8 @@ mod oid;
1919// Certificate operations module (parsing, validation, conversion)
2020mod cert;
2121
22+ mod chain;
23+
2224/// OpenSSL cipher string parsing for `set_ciphers`
2325mod cipher;
2426
@@ -83,7 +85,7 @@ mod _ssl {
8385 use parking_lot:: { Mutex as ParkingMutex , RwLock as ParkingRwLock } ;
8486 use pem_rfc7468:: { LineEnding , encode_string} ;
8587 use rustls:: {
86- ClientConnection , Connection , HandshakeKind , RootCertStore , ServerConfig , ServerConnection ,
88+ ClientConnection , Connection , HandshakeKind , RootCertStore , ServerConnection ,
8789 client:: { ClientSessionMemoryCache , ClientSessionStore } ,
8890 crypto:: SupportedKxGroup ,
8991 pki_types:: { CertificateDer , CertificateRevocationListDer , PrivateKeyDer , ServerName } ,
@@ -95,6 +97,7 @@ mod _ssl {
9597
9698 // Import certificate operations module
9799 use super :: cert;
100+ use super :: chain:: { self , VerifiedChainBuilder } ;
98101
99102 // Import OID module
100103 use super :: cipher;
@@ -688,7 +691,7 @@ mod _ssl {
688691 verify_flags : PyRwLock < i32 > ,
689692 // Rustls configuration (built lazily)
690693 #[ pytraverse( skip) ]
691- server_config : PyRwLock < Option < Arc < ServerConfig > > > ,
694+ server_config : PyRwLock < Option < chain :: ServerConfig > > ,
692695 // Certificate store
693696 #[ pytraverse( skip) ]
694697 root_certs : PyRwLock < RootCertStore > ,
@@ -1897,6 +1900,8 @@ mod _ssl {
18971900 ) ,
18981901 session : PyRwLock :: new ( None ) ,
18991902 client_config : PyRwLock :: new ( None ) ,
1903+ chain_builder : PyRwLock :: new ( None ) ,
1904+ verified_chain : PyRwLock :: new ( None ) ,
19001905 client_session_store : PyRwLock :: new ( None ) ,
19011906 incoming_bio : None ,
19021907 outgoing_bio : None ,
@@ -1989,6 +1994,8 @@ mod _ssl {
19891994 ) ,
19901995 session : PyRwLock :: new ( None ) ,
19911996 client_config : PyRwLock :: new ( None ) ,
1997+ chain_builder : PyRwLock :: new ( None ) ,
1998+ verified_chain : PyRwLock :: new ( None ) ,
19921999 client_session_store : PyRwLock :: new ( None ) ,
19932000 incoming_bio : Some ( args. incoming ) ,
19942001 outgoing_bio : Some ( args. outgoing ) ,
@@ -2351,6 +2358,10 @@ mod _ssl {
23512358 // SSLSession can reuse the same verifier and client credentials.
23522359 #[ pytraverse( skip) ]
23532360 client_config : PyRwLock < Option < Arc < rustls:: ClientConfig > > > ,
2361+ #[ pytraverse( skip) ]
2362+ chain_builder : PyRwLock < Option < Arc < VerifiedChainBuilder > > > ,
2363+ #[ pytraverse( skip) ]
2364+ verified_chain : PyRwLock < Option < Vec < Vec < u8 > > > > ,
23542365 // Per-connection store containing the session selected by this connection.
23552366 #[ pytraverse( skip) ]
23562367 client_session_store : PyRwLock < Option < Arc < PythonClientSessionStore > > > ,
@@ -2525,6 +2536,11 @@ mod _ssl {
25252536 let session = PySSLSession {
25262537 context_identity,
25272538 client_config,
2539+ chain_builder : self
2540+ . chain_builder
2541+ . read ( )
2542+ . clone ( )
2543+ . expect ( "connection configured" ) ,
25282544 session_store,
25292545 server_name : rustls_server_name,
25302546 kind : session_kind,
@@ -2539,63 +2555,46 @@ mod _ssl {
25392555 * self . session . write ( ) = Some ( py_session) ;
25402556 }
25412557
2542- // Complete handshake and create session
2543- /// Track which CA certificate from capath was used to verify peer
2544- ///
2545- /// This simulates lazy loading behavior: capath certificates
2546- /// are only added to get_ca_certs() after they're actually used in a handshake.
2547- fn track_used_ca_from_capath ( & self ) -> Result < ( ) , String > {
2548- // Extract capath_certs, releasing context lock quickly
2549- let capath_certs = {
2550- let context = self . context . read ( ) ;
2551- let certs = context. capath_certificates ( ) ;
2552- if certs. is_empty ( ) {
2553- return Ok ( ( ) ) ;
2554- }
2555- certs
2556- } ;
2557-
2558- // Extract peer certificates, releasing connection lock quickly
2559- let top_cert_der = {
2560- let conn_guard = self . connection . lock ( ) ;
2561- let conn = conn_guard. as_ref ( ) . ok_or ( "No connection" ) ?;
2562- let peer_certs = conn. peer_certificates ( ) . ok_or ( "No peer certificates" ) ?;
2563- if peer_certs. is_empty ( ) {
2564- return Ok ( ( ) ) ;
2565- }
2566- peer_certs
2567- . iter ( )
2568- . map ( |c| c. as_ref ( ) . to_vec ( ) )
2569- . next_back ( )
2570- . expect ( "is_empty checked above" )
2558+ /// Retain the verified path and publish its selected capath anchor.
2559+ fn record_verified_chain ( & self , was_resumed : bool ) {
2560+ let Some ( builder) = self . chain_builder . read ( ) . clone ( ) else {
2561+ return ;
25712562 } ;
2572-
2573- // Get the top certificate in the chain (closest to root)
2574- // Note: Server usually doesn't send the root CA, so we check the last cert's issuer
2575- let ( _, top_cert) = x509_parser:: parse_x509_certificate ( & top_cert_der)
2576- . map_err ( |e| format ! ( "Failed to parse top cert: {e}" ) ) ?;
2577-
2578- let top_issuer = top_cert. issuer ( ) ;
2579-
2580- // Find matching CA in capath certs (skip unparseable certificates)
2581- let matching_ca = capath_certs. iter ( ) . find_map ( |ca_der| {
2582- let ( _, ca) = x509_parser:: parse_x509_certificate ( ca_der) . ok ( ) ?;
2583- // Check if this CA is self-signed (root CA) and matches the issuer
2584- ( ca. subject ( ) == ca. issuer ( ) && ca. subject ( ) == top_issuer) . then ( || ca_der. clone ( ) )
2563+ // Like SSL_get0_verified_chain, resumption has no newly verified
2564+ // path. The peer certificate remains available separately. If the
2565+ // peer declined resumption, build the full handshake's path below.
2566+ if was_resumed {
2567+ * self . verified_chain . write ( ) = None ;
2568+ return ;
2569+ }
2570+ let chain = self . connection . lock ( ) . as_ref ( ) . and_then ( |conn| {
2571+ builder. build (
2572+ conn. peer_certificates ( ) ?,
2573+ rustls:: pki_types:: UnixTime :: now ( ) ,
2574+ )
25852575 } ) ;
2586-
2587- // Update ca_certs_der if we found a match
2588- if let Some ( ca_der) = matching_ca {
2589- let context = self . context . read ( ) ;
2590- let mut ca_certs_der = context. ca_certs_der . write ( ) ;
2591- if !ca_certs_der. iter ( ) . any ( |c| c == & ca_der) {
2592- ca_certs_der. push ( ca_der) ;
2593- * context. x509_cert_count . write ( ) += 1 ;
2576+ * self . verified_chain . write ( ) = chain;
2577+ let Some ( anchor) = self
2578+ . verified_chain
2579+ . read ( )
2580+ . as_ref ( )
2581+ . and_then ( |chain| chain. last ( ) )
2582+ . cloned ( )
2583+ else {
2584+ return ;
2585+ } ;
2586+ let context = self . context . read ( ) ;
2587+ let mut root_certs = context. root_certs . write ( ) ;
2588+ let mut ca_certs_der = context. ca_certs_der . write ( ) ;
2589+ if !ca_certs_der. contains ( & anchor) && builder. root_der . contains ( & anchor) {
2590+ let is_ca = cert:: is_ca_certificate ( & anchor) ;
2591+ ca_certs_der. push ( anchor. clone ( ) ) ;
2592+ let _ = root_certs. add ( anchor. into ( ) ) ;
2593+ * context. x509_cert_count . write ( ) += 1 ;
2594+ if is_ca {
25942595 * context. ca_cert_count . write ( ) += 1 ;
25952596 }
25962597 }
2597-
2598- Ok ( ( ) )
25992598 }
26002599
26012600 fn complete_handshake ( & self , vm : & VirtualMachine ) {
@@ -2621,12 +2620,7 @@ mod _ssl {
26212620 }
26222621 }
26232622
2624- // Track CA certificate used during handshake (client-side only)
2625- // This simulates lazy loading behavior for capath certificates
2626- if !self . server_side {
2627- // Don't fail handshake if tracking fails
2628- let _ = self . track_used_ca_from_capath ( ) ;
2629- }
2623+ self . record_verified_chain ( was_resumed) ;
26302624
26312625 self . create_session_after_handshake ( was_resumed, vm) ;
26322626 }
@@ -3265,7 +3259,7 @@ mod _ssl {
32653259
32663260 // Check if client certificate verification is required
32673261 let verify_mode = * ctx. verify_mode . read ( ) ;
3268- let ( root_store, _ ) = ctx. verification_roots ( ) ;
3262+ let ( root_store, ca_certs_der ) = ctx. verification_roots ( ) ;
32693263 let pha_enabled = * ctx. post_handshake_auth . read ( ) ;
32703264
32713265 // Check if TLS 1.3 is being used
@@ -3357,6 +3351,7 @@ mod _ssl {
33573351 // Build server config using compat helper
33583352 let config_options = ServerConfigOptions {
33593353 protocol_settings,
3354+ ca_certs_der,
33603355 cert_chain : certs_clone,
33613356 private_key : key_clone,
33623357 root_store : if request_initial_cert {
@@ -3387,20 +3382,20 @@ mod _ssl {
33873382 } ;
33883383 drop ( ctx) ;
33893384
3390- let config_arc = if let Some ( cached) = cached_config_arc {
3385+ let ( config_arc, chain_builder ) = if let Some ( cached) = cached_config_arc {
33913386 cached
33923387 } else {
33933388 let config =
33943389 create_server_config ( config_options) . map_err ( |e| vm. new_value_error ( e) ) ?;
3395- let config_arc = Arc :: new ( config) ;
33963390
33973391 if cache_server_config {
33983392 let ctx = self . context . read ( ) ;
3399- * ctx. server_config . write ( ) = Some ( config_arc . clone ( ) ) ;
3393+ * ctx. server_config . write ( ) = Some ( config . clone ( ) ) ;
34003394 }
34013395
3402- config_arc
3396+ config
34033397 } ;
3398+ * self . chain_builder . write ( ) = Some ( chain_builder) ;
34043399
34053400 let conn = ServerConnection :: new ( config_arc) . map_err ( |e| {
34063401 vm. new_value_error ( format ! ( "Failed to create server connection: {e}" ) )
@@ -3505,7 +3500,7 @@ mod _ssl {
35053500
35063501 let explicit_session = self . session . read ( ) . clone ( ) ;
35073502 let session_store = Arc :: new ( PythonClientSessionStore :: new ( session_cache) ) ;
3508- let config = if let Some ( session) = explicit_session {
3503+ let ( config, chain_builder ) = if let Some ( session) = explicit_session {
35093504 let session = session
35103505 . downcast_ref :: < PySSLSession > ( )
35113506 . ok_or_else ( || vm. new_type_error ( "Value is not a SSLSession." ) ) ?;
@@ -3524,15 +3519,11 @@ mod _ssl {
35243519 let mut config = ( * session. client_config ) . clone ( ) ;
35253520 config. resumption =
35263521 rustls:: client:: Resumption :: store ( session_store. clone ( ) ) ;
3527- Arc :: new ( config)
3522+ ( Arc :: new ( config) , session . chain_builder . clone ( ) )
35283523 } else {
35293524 let config_options = ClientConfigOptions {
35303525 protocol_settings,
3531- root_store : if verify_mode != CERT_NONE {
3532- Some ( root_store_clone)
3533- } else {
3534- None
3535- } ,
3526+ root_store : Some ( root_store_clone) ,
35363527 ca_certs_der : ca_certs_der_clone,
35373528 cert_chain : if !cert_chain_clone. is_empty ( ) {
35383529 Some ( cert_chain_clone)
@@ -3546,12 +3537,10 @@ mod _ssl {
35463537 session_store : Some ( session_store. clone ( ) ) ,
35473538 crls : crls_clone,
35483539 } ;
3549- Arc :: new (
3550- create_client_config ( config_options)
3551- . map_err ( |e| vm. new_value_error ( e) ) ?,
3552- )
3540+ create_client_config ( config_options) . map_err ( |e| vm. new_value_error ( e) ) ?
35533541 } ;
35543542
3543+ * self . chain_builder . write ( ) = Some ( chain_builder) ;
35553544 * self . client_config . write ( ) = Some ( config. clone ( ) ) ;
35563545 * self . client_session_store . write ( ) = Some ( session_store) ;
35573546
@@ -4132,17 +4121,7 @@ mod _ssl {
41324121
41334122 #[ pymethod]
41344123 fn get_verified_chain ( & self , vm : & VirtualMachine ) -> Option < PyListRef > {
4135- // Get peer certificates (what peer sent during handshake)
4136- let conn_guard = self . connection . lock ( ) ;
4137- let conn = ( * conn_guard) . as_ref ( ) ?;
4138- let peer_certs = conn. peer_certificates ( ) ;
4139- let peer_certs_slice = peer_certs?;
4140-
4141- // Build the verified chain using cert module
4142- let ctx_guard = self . context . read ( ) ;
4143- let ca_certs_der = ctx_guard. ca_certs_der . read ( ) ;
4144-
4145- let chain_der = cert:: build_verified_chain ( peer_certs_slice, & ca_certs_der) ;
4124+ let chain_der = self . verified_chain . read ( ) . clone ( ) ?;
41464125
41474126 // Convert DER chain to Python list of Certificate objects
41484127 let cert_list: Vec < PyObjectRef > = chain_der
@@ -4776,6 +4755,7 @@ mod _ssl {
47764755 struct PySSLSession {
47774756 context_identity : Arc < ( ) > ,
47784757 client_config : Arc < rustls:: ClientConfig > ,
4758+ chain_builder : Arc < VerifiedChainBuilder > ,
47794759 session_store : Arc < PythonClientSessionStore > ,
47804760 server_name : Option < ServerName < ' static > > ,
47814761 kind : ClientSessionKind ,
0 commit comments