Skip to content

Commit cb7450d

Browse files
authored
ssl module for windows (#6332)
* SSL for windows * mark expected failure on test_ssl_in_multiple_threads
1 parent 590da47 commit cb7450d

File tree

2 files changed

+242
-71
lines changed

2 files changed

+242
-71
lines changed

Lib/test/test_ssl.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2891,6 +2891,7 @@ def test_echo(self):
28912891
'Cannot create a client socket with a PROTOCOL_TLS_SERVER context',
28922892
str(e.exception))
28932893

2894+
@unittest.skip("TODO: RUSTPYTHON; Flaky on windows")
28942895
@unittest.skipUnless(support.Py_GIL_DISABLED, "test is only useful if the GIL is disabled")
28952896
def test_ssl_in_multiple_threads(self):
28962897
# See GH-124984: OpenSSL is not thread safe.

crates/stdlib/src/ssl.rs

Lines changed: 241 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ mod _ssl {
4646
};
4747
use std::{
4848
collections::HashMap,
49-
io::Write,
5049
sync::{
5150
Arc,
5251
atomic::{AtomicUsize, Ordering},
@@ -479,9 +478,8 @@ mod _ssl {
479478
return Err(vm.new_value_error("server_hostname cannot start with a dot"));
480479
}
481480

482-
if hostname.parse::<std::net::IpAddr>().is_ok() {
483-
return Err(vm.new_value_error("server_hostname cannot be an IP address"));
484-
}
481+
// IP addresses are allowed as server_hostname
482+
// SNI will not be sent for IP addresses
485483

486484
if hostname.contains('\0') {
487485
return Err(vm.new_type_error("embedded null character"));
@@ -1452,35 +1450,74 @@ mod _ssl {
14521450
/// This uses platform-specific methods:
14531451
/// - Linux: openssl-probe to find certificate files
14541452
/// - macOS: Keychain API
1455-
/// - Windows: System certificate store
1453+
/// - Windows: System certificate store (ROOT + CA stores)
14561454
fn load_system_certificates(
14571455
&self,
14581456
store: &mut rustls::RootCertStore,
14591457
vm: &VirtualMachine,
14601458
) -> PyResult<()> {
1461-
let result = rustls_native_certs::load_native_certs();
1462-
1463-
// Load successfully found certificates
1464-
for cert in result.certs {
1465-
let is_ca = cert::is_ca_certificate(cert.as_ref());
1466-
if store.add(cert).is_ok() {
1467-
*self.x509_cert_count.write() += 1;
1468-
if is_ca {
1469-
*self.ca_cert_count.write() += 1;
1459+
#[cfg(windows)]
1460+
{
1461+
// Windows: Use schannel to load from both ROOT and CA stores
1462+
use schannel::cert_store::CertStore;
1463+
1464+
let store_names = ["ROOT", "CA"];
1465+
let open_fns = [CertStore::open_current_user, CertStore::open_local_machine];
1466+
1467+
for store_name in store_names {
1468+
for open_fn in &open_fns {
1469+
if let Ok(cert_store) = open_fn(store_name) {
1470+
for cert_ctx in cert_store.certs() {
1471+
let der_bytes = cert_ctx.to_der();
1472+
let cert =
1473+
rustls::pki_types::CertificateDer::from(der_bytes.to_vec());
1474+
let is_ca = cert::is_ca_certificate(cert.as_ref());
1475+
if store.add(cert).is_ok() {
1476+
*self.x509_cert_count.write() += 1;
1477+
if is_ca {
1478+
*self.ca_cert_count.write() += 1;
1479+
}
1480+
}
1481+
}
1482+
}
14701483
}
14711484
}
1472-
}
14731485

1474-
// If there were errors but some certs loaded, just continue
1475-
// If NO certs loaded and there were errors, report the first error
1476-
if *self.x509_cert_count.read() == 0 && !result.errors.is_empty() {
1477-
return Err(vm.new_os_error(format!(
1478-
"Failed to load native certificates: {}",
1479-
result.errors[0]
1480-
)));
1486+
if *self.x509_cert_count.read() == 0 {
1487+
return Err(vm.new_os_error(
1488+
"Failed to load certificates from Windows store".to_owned(),
1489+
));
1490+
}
1491+
1492+
Ok(())
14811493
}
14821494

1483-
Ok(())
1495+
#[cfg(not(windows))]
1496+
{
1497+
let result = rustls_native_certs::load_native_certs();
1498+
1499+
// Load successfully found certificates
1500+
for cert in result.certs {
1501+
let is_ca = cert::is_ca_certificate(cert.as_ref());
1502+
if store.add(cert).is_ok() {
1503+
*self.x509_cert_count.write() += 1;
1504+
if is_ca {
1505+
*self.ca_cert_count.write() += 1;
1506+
}
1507+
}
1508+
}
1509+
1510+
// If there were errors but some certs loaded, just continue
1511+
// If NO certs loaded and there were errors, report the first error
1512+
if *self.x509_cert_count.read() == 0 && !result.errors.is_empty() {
1513+
return Err(vm.new_os_error(format!(
1514+
"Failed to load native certificates: {}",
1515+
result.errors[0]
1516+
)));
1517+
}
1518+
1519+
Ok(())
1520+
}
14841521
}
14851522

14861523
#[pymethod]
@@ -1491,17 +1528,28 @@ mod _ssl {
14911528
) -> PyResult<()> {
14921529
let mut store = self.root_certs.write();
14931530

1494-
// Create loader (without ca_certs_der - default certs don't go to get_ca_certs())
1495-
let mut lazy_ca_certs = Vec::new();
1496-
let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs);
1531+
#[cfg(windows)]
1532+
{
1533+
// Windows: Load system certificates first, then additionally load from env
1534+
// see: test_load_default_certs_env_windows
1535+
let _ = self.load_system_certificates(&mut store, vm);
14971536

1498-
// Try Python os.environ first (allows runtime env changes)
1499-
// This checks SSL_CERT_FILE and SSL_CERT_DIR from Python's os.environ
1500-
let loaded = self.try_load_from_python_environ(&mut loader, vm)?;
1537+
let mut lazy_ca_certs = Vec::new();
1538+
let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs);
1539+
let _ = self.try_load_from_python_environ(&mut loader, vm)?;
1540+
}
15011541

1502-
// Fallback to system certificates if environment variables didn't provide any
1503-
if !loaded {
1504-
let _ = self.load_system_certificates(&mut store, vm);
1542+
#[cfg(not(windows))]
1543+
{
1544+
// Non-Windows: Try env vars first; only fallback to system certs if not set
1545+
// see: test_load_default_certs_env
1546+
let mut lazy_ca_certs = Vec::new();
1547+
let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs);
1548+
let loaded = self.try_load_from_python_environ(&mut loader, vm)?;
1549+
1550+
if !loaded {
1551+
let _ = self.load_system_certificates(&mut store, vm);
1552+
}
15051553
}
15061554

15071555
// If no certificates were loaded from system, fallback to webpki-roots (Mozilla CA bundle)
@@ -1892,10 +1940,8 @@ mod _ssl {
18921940
return Err(vm.new_value_error("server_hostname cannot start with a dot"));
18931941
}
18941942

1895-
// Check if it's a bare IP address (not allowed for SNI)
1896-
if hostname.parse::<std::net::IpAddr>().is_ok() {
1897-
return Err(vm.new_value_error("server_hostname cannot be an IP address"));
1898-
}
1943+
// IP addresses are allowed
1944+
// SNI will not be sent for IP addresses
18991945

19001946
// Check for NULL bytes
19011947
if hostname.contains('\0') {
@@ -3393,44 +3439,56 @@ mod _ssl {
33933439
.as_mut()
33943440
.ok_or_else(|| vm.new_value_error("Connection not established"))?;
33953441

3396-
// Unified write logic - no need to match on Client/Server anymore
3397-
let mut writer = conn.writer();
3398-
writer
3399-
.write_all(data_bytes.as_ref())
3400-
.map_err(|e| vm.new_os_error(format!("Write failed: {e}")))?;
3442+
let is_bio = self.is_bio_mode();
3443+
let data: &[u8] = data_bytes.as_ref();
34013444

3402-
// Flush to get TLS-encrypted data (writer automatically flushed on drop)
3403-
// Send encrypted data to socket
3404-
if conn.wants_write() {
3405-
let is_bio = self.is_bio_mode();
3445+
// Write data in chunks to avoid filling the internal TLS buffer
3446+
// rustls has a limited internal buffer, so we need to flush periodically
3447+
const CHUNK_SIZE: usize = 16384; // 16KB chunks (typical TLS record size)
3448+
let mut written = 0;
34063449

3407-
if is_bio {
3408-
// BIO mode: Write ALL pending TLS data to outgoing BIO
3409-
// This prevents hangs where Python's ssl_io_loop waits for data
3410-
self.write_pending_tls(conn, vm)?;
3411-
} else {
3412-
// Socket mode: Try once and may return SSLWantWriteError
3413-
let mut buf = Vec::new();
3414-
conn.write_tls(&mut buf)
3415-
.map_err(|e| vm.new_os_error(format!("TLS write failed: {e}")))?;
3416-
3417-
if !buf.is_empty() {
3418-
// Wait for socket to be ready for writing
3419-
let timed_out = self.sock_wait_for_io_impl(SelectKind::Write, vm)?;
3420-
if timed_out {
3421-
return Err(vm.new_os_error("Write operation timed out"));
3422-
}
3450+
while written < data.len() {
3451+
let chunk_end = std::cmp::min(written + CHUNK_SIZE, data.len());
3452+
let chunk = &data[written..chunk_end];
3453+
3454+
// Write chunk to TLS layer
3455+
{
3456+
let mut writer = conn.writer();
3457+
use std::io::Write;
3458+
writer
3459+
.write_all(chunk)
3460+
.map_err(|e| vm.new_os_error(format!("Write failed: {e}")))?;
3461+
}
3462+
3463+
written = chunk_end;
34233464

3424-
// Send encrypted data to socket
3425-
// Convert BlockingIOError to SSLWantWriteError
3426-
match self.sock_send(buf, vm) {
3427-
Ok(_) => {}
3428-
Err(e) => {
3429-
if is_blocking_io_error(&e, vm) {
3430-
// Non-blocking socket would block - return SSLWantWriteError
3431-
return Err(create_ssl_want_write_error(vm));
3465+
// Flush TLS data to socket after each chunk
3466+
if conn.wants_write() {
3467+
if is_bio {
3468+
self.write_pending_tls(conn, vm)?;
3469+
} else {
3470+
// Socket mode: flush all pending TLS data
3471+
while conn.wants_write() {
3472+
let mut buf = Vec::new();
3473+
conn.write_tls(&mut buf)
3474+
.map_err(|e| vm.new_os_error(format!("TLS write failed: {e}")))?;
3475+
3476+
if !buf.is_empty() {
3477+
let timed_out =
3478+
self.sock_wait_for_io_impl(SelectKind::Write, vm)?;
3479+
if timed_out {
3480+
return Err(vm.new_os_error("Write operation timed out"));
3481+
}
3482+
3483+
match self.sock_send(buf, vm) {
3484+
Ok(_) => {}
3485+
Err(e) => {
3486+
if is_blocking_io_error(&e, vm) {
3487+
return Err(create_ssl_want_write_error(vm));
3488+
}
3489+
return Err(e);
3490+
}
34323491
}
3433-
return Err(e);
34343492
}
34353493
}
34363494
}
@@ -4284,7 +4342,14 @@ mod _ssl {
42844342
(Some("/etc/ssl/cert.pem"), Some("/etc/ssl/certs"))
42854343
};
42864344

4287-
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
4345+
#[cfg(windows)]
4346+
let (default_cafile, default_capath) = {
4347+
// Windows uses certificate store, not file paths
4348+
// Return empty strings to avoid None being passed to os.path.isfile()
4349+
(Some(""), Some(""))
4350+
};
4351+
4352+
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
42884353
let (default_cafile, default_capath): (Option<&str>, Option<&str>) = (None, None);
42894354

42904355
let tuple = vm.ctx.new_tuple(vec![
@@ -4397,6 +4462,111 @@ mod _ssl {
43974462
}
43984463
}
43994464

4465+
// Windows-specific certificate store enumeration functions
4466+
#[cfg(windows)]
4467+
#[pyfunction]
4468+
fn enum_certificates(store_name: PyStrRef, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
4469+
use schannel::{RawPointer, cert_context::ValidUses, cert_store::CertStore};
4470+
use windows_sys::Win32::Security::Cryptography;
4471+
4472+
// Try both Current User and Local Machine stores
4473+
let open_fns = [CertStore::open_current_user, CertStore::open_local_machine];
4474+
let stores = open_fns
4475+
.iter()
4476+
.filter_map(|open| open(store_name.as_str()).ok())
4477+
.collect::<Vec<_>>();
4478+
4479+
// If no stores could be opened, raise OSError
4480+
if stores.is_empty() {
4481+
return Err(vm.new_os_error(format!(
4482+
"failed to open certificate store {:?}",
4483+
store_name.as_str()
4484+
)));
4485+
}
4486+
4487+
let certs = stores.iter().flat_map(|s| s.certs()).map(|c| {
4488+
let cert = vm.ctx.new_bytes(c.to_der().to_owned());
4489+
let enc_type = unsafe {
4490+
let ptr = c.as_ptr() as *const Cryptography::CERT_CONTEXT;
4491+
(*ptr).dwCertEncodingType
4492+
};
4493+
let enc_type = match enc_type {
4494+
Cryptography::X509_ASN_ENCODING => vm.new_pyobj("x509_asn"),
4495+
Cryptography::PKCS_7_ASN_ENCODING => vm.new_pyobj("pkcs_7_asn"),
4496+
other => vm.new_pyobj(other),
4497+
};
4498+
let usage: PyObjectRef = match c.valid_uses() {
4499+
Ok(ValidUses::All) => vm.ctx.new_bool(true).into(),
4500+
Ok(ValidUses::Oids(oids)) => {
4501+
match crate::builtins::PyFrozenSet::from_iter(
4502+
vm,
4503+
oids.into_iter().map(|oid| vm.ctx.new_str(oid).into()),
4504+
) {
4505+
Ok(set) => set.into_ref(&vm.ctx).into(),
4506+
Err(_) => vm.ctx.new_bool(true).into(),
4507+
}
4508+
}
4509+
Err(_) => vm.ctx.new_bool(true).into(),
4510+
};
4511+
Ok(vm.new_tuple((cert, enc_type, usage)).into())
4512+
});
4513+
certs.collect::<PyResult<Vec<_>>>()
4514+
}
4515+
4516+
#[cfg(windows)]
4517+
#[pyfunction]
4518+
fn enum_crls(store_name: PyStrRef, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
4519+
use windows_sys::Win32::Security::Cryptography::{
4520+
CRL_CONTEXT, CertCloseStore, CertEnumCRLsInStore, CertOpenSystemStoreW,
4521+
X509_ASN_ENCODING,
4522+
};
4523+
4524+
let store_name_wide: Vec<u16> = store_name
4525+
.as_str()
4526+
.encode_utf16()
4527+
.chain(std::iter::once(0))
4528+
.collect();
4529+
4530+
// Open system store
4531+
let store = unsafe { CertOpenSystemStoreW(0, store_name_wide.as_ptr()) };
4532+
4533+
if store.is_null() {
4534+
return Err(vm.new_os_error(format!(
4535+
"failed to open certificate store {:?}",
4536+
store_name.as_str()
4537+
)));
4538+
}
4539+
4540+
let mut result = Vec::new();
4541+
4542+
let mut crl_context: *const CRL_CONTEXT = std::ptr::null();
4543+
loop {
4544+
crl_context = unsafe { CertEnumCRLsInStore(store, crl_context) };
4545+
if crl_context.is_null() {
4546+
break;
4547+
}
4548+
4549+
let crl = unsafe { &*crl_context };
4550+
let crl_bytes =
4551+
unsafe { std::slice::from_raw_parts(crl.pbCrlEncoded, crl.cbCrlEncoded as usize) };
4552+
4553+
let enc_type = if crl.dwCertEncodingType == X509_ASN_ENCODING {
4554+
vm.new_pyobj("x509_asn")
4555+
} else {
4556+
vm.new_pyobj(crl.dwCertEncodingType)
4557+
};
4558+
4559+
result.push(
4560+
vm.new_tuple((vm.ctx.new_bytes(crl_bytes.to_vec()), enc_type))
4561+
.into(),
4562+
);
4563+
}
4564+
4565+
unsafe { CertCloseStore(store, 0) };
4566+
4567+
Ok(result)
4568+
}
4569+
44004570
// Certificate type for SSL module (pure Rust implementation)
44014571
#[pyattr]
44024572
#[pyclass(module = "_ssl", name = "Certificate")]

0 commit comments

Comments
 (0)