Skip to content

Commit a8aa383

Browse files
authored
ssl: retain WebPKI-selected certificate chains (#8653)
Port the connection-owned chain builder from Pyre. Preserve verification inputs with TLS configurations and sessions, report the WebPKI-selected path, and publish only its selected capath anchor. Match CPython chain reporting for CERT_NONE and resumed sessions. Assisted-by: Codex:GPT-6
1 parent 5346842 commit a8aa383

6 files changed

Lines changed: 411 additions & 95 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ rustls-graviola = "0.4"
280280
rustls-native-certs = "0.8"
281281
rustls-pemfile = "2.2"
282282
rustls-platform-verifier = "0.7"
283+
rustls-webpki = { version = "0.103", default-features = false, features = ["std"] }
283284
rustyline = "18"
284285
serde = { package = "serde_core", version = "1.0.225", default-features = false, features = ["alloc"] }
285286
schannel = "0.1.29"

‎crates/stdlib/Cargo.toml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ ssl-openssl-vendor = ["ssl-openssl", "openssl/vendored"]
2424
tkinter = ["dep:tk-sys", "dep:tcl-sys", "dep:widestring"]
2525
flame-it = ["flame"]
2626

27-
__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"]
27+
__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"]
2828

2929
[dependencies]
3030
# rustpython crates
@@ -94,6 +94,7 @@ foreign-types-shared = { workspace = true, optional = true }
9494
rustls = { workspace = true, default-features = false, features = ["std", "tls12"], optional = true }
9595
rustls-pemfile = { workspace = true, optional = true }
9696
rustls-platform-verifier = { workspace = true, optional = true }
97+
rustls-webpki = { workspace = true, optional = true }
9798
x509-cert = { workspace = true, features = ["pem", "builder"], optional = true }
9899
x509-parser = { workspace = true, optional = true }
99100
der = { workspace = true, optional = true }
@@ -113,5 +114,8 @@ widestring = { workspace = true }
113114
insta = { workspace = true }
114115
rustpython-pylib = { workspace = true, features = [ "freeze-stdlib" ] }
115116

117+
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
118+
rustls = { workspace = true, features = ["aws_lc_rs"] }
119+
116120
[lints]
117121
workspace = true

‎crates/stdlib/src/ssl.rs‎

Lines changed: 67 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ mod oid;
1919
// Certificate operations module (parsing, validation, conversion)
2020
mod cert;
2121

22+
mod chain;
23+
2224
/// OpenSSL cipher string parsing for `set_ciphers`
2325
mod 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

Comments
 (0)