ssl module for windows (#6332)

* SSL for windows

* mark expected failure on test_ssl_in_multiple_threads
This commit is contained in:
Jeong, YunWon
2025-12-08 00:08:53 +09:00
committed by GitHub
parent 590da47499
commit cb7450df31
2 changed files with 241 additions and 70 deletions

View File

@@ -2891,6 +2891,7 @@ class ThreadedTests(unittest.TestCase):
'Cannot create a client socket with a PROTOCOL_TLS_SERVER context',
str(e.exception))
@unittest.skip("TODO: RUSTPYTHON; Flaky on windows")
@unittest.skipUnless(support.Py_GIL_DISABLED, "test is only useful if the GIL is disabled")
def test_ssl_in_multiple_threads(self):
# See GH-124984: OpenSSL is not thread safe.

View File

@@ -46,7 +46,6 @@ mod _ssl {
};
use std::{
collections::HashMap,
io::Write,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
@@ -479,9 +478,8 @@ mod _ssl {
return Err(vm.new_value_error("server_hostname cannot start with a dot"));
}
if hostname.parse::<std::net::IpAddr>().is_ok() {
return Err(vm.new_value_error("server_hostname cannot be an IP address"));
}
// IP addresses are allowed as server_hostname
// SNI will not be sent for IP addresses
if hostname.contains('\0') {
return Err(vm.new_type_error("embedded null character"));
@@ -1452,35 +1450,74 @@ mod _ssl {
/// This uses platform-specific methods:
/// - Linux: openssl-probe to find certificate files
/// - macOS: Keychain API
/// - Windows: System certificate store
/// - Windows: System certificate store (ROOT + CA stores)
fn load_system_certificates(
&self,
store: &mut rustls::RootCertStore,
vm: &VirtualMachine,
) -> PyResult<()> {
let result = rustls_native_certs::load_native_certs();
#[cfg(windows)]
{
// Windows: Use schannel to load from both ROOT and CA stores
use schannel::cert_store::CertStore;
// Load successfully found certificates
for cert in result.certs {
let is_ca = cert::is_ca_certificate(cert.as_ref());
if store.add(cert).is_ok() {
*self.x509_cert_count.write() += 1;
if is_ca {
*self.ca_cert_count.write() += 1;
let store_names = ["ROOT", "CA"];
let open_fns = [CertStore::open_current_user, CertStore::open_local_machine];
for store_name in store_names {
for open_fn in &open_fns {
if let Ok(cert_store) = open_fn(store_name) {
for cert_ctx in cert_store.certs() {
let der_bytes = cert_ctx.to_der();
let cert =
rustls::pki_types::CertificateDer::from(der_bytes.to_vec());
let is_ca = cert::is_ca_certificate(cert.as_ref());
if store.add(cert).is_ok() {
*self.x509_cert_count.write() += 1;
if is_ca {
*self.ca_cert_count.write() += 1;
}
}
}
}
}
}
if *self.x509_cert_count.read() == 0 {
return Err(vm.new_os_error(
"Failed to load certificates from Windows store".to_owned(),
));
}
Ok(())
}
// If there were errors but some certs loaded, just continue
// If NO certs loaded and there were errors, report the first error
if *self.x509_cert_count.read() == 0 && !result.errors.is_empty() {
return Err(vm.new_os_error(format!(
"Failed to load native certificates: {}",
result.errors[0]
)));
}
#[cfg(not(windows))]
{
let result = rustls_native_certs::load_native_certs();
Ok(())
// Load successfully found certificates
for cert in result.certs {
let is_ca = cert::is_ca_certificate(cert.as_ref());
if store.add(cert).is_ok() {
*self.x509_cert_count.write() += 1;
if is_ca {
*self.ca_cert_count.write() += 1;
}
}
}
// If there were errors but some certs loaded, just continue
// If NO certs loaded and there were errors, report the first error
if *self.x509_cert_count.read() == 0 && !result.errors.is_empty() {
return Err(vm.new_os_error(format!(
"Failed to load native certificates: {}",
result.errors[0]
)));
}
Ok(())
}
}
#[pymethod]
@@ -1491,17 +1528,28 @@ mod _ssl {
) -> PyResult<()> {
let mut store = self.root_certs.write();
// Create loader (without ca_certs_der - default certs don't go to get_ca_certs())
let mut lazy_ca_certs = Vec::new();
let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs);
// Try Python os.environ first (allows runtime env changes)
// This checks SSL_CERT_FILE and SSL_CERT_DIR from Python's os.environ
let loaded = self.try_load_from_python_environ(&mut loader, vm)?;
// Fallback to system certificates if environment variables didn't provide any
if !loaded {
#[cfg(windows)]
{
// Windows: Load system certificates first, then additionally load from env
// see: test_load_default_certs_env_windows
let _ = self.load_system_certificates(&mut store, vm);
let mut lazy_ca_certs = Vec::new();
let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs);
let _ = self.try_load_from_python_environ(&mut loader, vm)?;
}
#[cfg(not(windows))]
{
// Non-Windows: Try env vars first; only fallback to system certs if not set
// see: test_load_default_certs_env
let mut lazy_ca_certs = Vec::new();
let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs);
let loaded = self.try_load_from_python_environ(&mut loader, vm)?;
if !loaded {
let _ = self.load_system_certificates(&mut store, vm);
}
}
// If no certificates were loaded from system, fallback to webpki-roots (Mozilla CA bundle)
@@ -1892,10 +1940,8 @@ mod _ssl {
return Err(vm.new_value_error("server_hostname cannot start with a dot"));
}
// Check if it's a bare IP address (not allowed for SNI)
if hostname.parse::<std::net::IpAddr>().is_ok() {
return Err(vm.new_value_error("server_hostname cannot be an IP address"));
}
// IP addresses are allowed
// SNI will not be sent for IP addresses
// Check for NULL bytes
if hostname.contains('\0') {
@@ -3393,44 +3439,56 @@ mod _ssl {
.as_mut()
.ok_or_else(|| vm.new_value_error("Connection not established"))?;
// Unified write logic - no need to match on Client/Server anymore
let mut writer = conn.writer();
writer
.write_all(data_bytes.as_ref())
.map_err(|e| vm.new_os_error(format!("Write failed: {e}")))?;
let is_bio = self.is_bio_mode();
let data: &[u8] = data_bytes.as_ref();
// Flush to get TLS-encrypted data (writer automatically flushed on drop)
// Send encrypted data to socket
if conn.wants_write() {
let is_bio = self.is_bio_mode();
// Write data in chunks to avoid filling the internal TLS buffer
// rustls has a limited internal buffer, so we need to flush periodically
const CHUNK_SIZE: usize = 16384; // 16KB chunks (typical TLS record size)
let mut written = 0;
if is_bio {
// BIO mode: Write ALL pending TLS data to outgoing BIO
// This prevents hangs where Python's ssl_io_loop waits for data
self.write_pending_tls(conn, vm)?;
} else {
// Socket mode: Try once and may return SSLWantWriteError
let mut buf = Vec::new();
conn.write_tls(&mut buf)
.map_err(|e| vm.new_os_error(format!("TLS write failed: {e}")))?;
while written < data.len() {
let chunk_end = std::cmp::min(written + CHUNK_SIZE, data.len());
let chunk = &data[written..chunk_end];
if !buf.is_empty() {
// Wait for socket to be ready for writing
let timed_out = self.sock_wait_for_io_impl(SelectKind::Write, vm)?;
if timed_out {
return Err(vm.new_os_error("Write operation timed out"));
}
// Write chunk to TLS layer
{
let mut writer = conn.writer();
use std::io::Write;
writer
.write_all(chunk)
.map_err(|e| vm.new_os_error(format!("Write failed: {e}")))?;
}
// Send encrypted data to socket
// Convert BlockingIOError to SSLWantWriteError
match self.sock_send(buf, vm) {
Ok(_) => {}
Err(e) => {
if is_blocking_io_error(&e, vm) {
// Non-blocking socket would block - return SSLWantWriteError
return Err(create_ssl_want_write_error(vm));
written = chunk_end;
// Flush TLS data to socket after each chunk
if conn.wants_write() {
if is_bio {
self.write_pending_tls(conn, vm)?;
} else {
// Socket mode: flush all pending TLS data
while conn.wants_write() {
let mut buf = Vec::new();
conn.write_tls(&mut buf)
.map_err(|e| vm.new_os_error(format!("TLS write failed: {e}")))?;
if !buf.is_empty() {
let timed_out =
self.sock_wait_for_io_impl(SelectKind::Write, vm)?;
if timed_out {
return Err(vm.new_os_error("Write operation timed out"));
}
match self.sock_send(buf, vm) {
Ok(_) => {}
Err(e) => {
if is_blocking_io_error(&e, vm) {
return Err(create_ssl_want_write_error(vm));
}
return Err(e);
}
}
return Err(e);
}
}
}
@@ -4284,7 +4342,14 @@ mod _ssl {
(Some("/etc/ssl/cert.pem"), Some("/etc/ssl/certs"))
};
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
#[cfg(windows)]
let (default_cafile, default_capath) = {
// Windows uses certificate store, not file paths
// Return empty strings to avoid None being passed to os.path.isfile()
(Some(""), Some(""))
};
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
let (default_cafile, default_capath): (Option<&str>, Option<&str>) = (None, None);
let tuple = vm.ctx.new_tuple(vec![
@@ -4397,6 +4462,111 @@ mod _ssl {
}
}
// Windows-specific certificate store enumeration functions
#[cfg(windows)]
#[pyfunction]
fn enum_certificates(store_name: PyStrRef, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
use schannel::{RawPointer, cert_context::ValidUses, cert_store::CertStore};
use windows_sys::Win32::Security::Cryptography;
// Try both Current User and Local Machine stores
let open_fns = [CertStore::open_current_user, CertStore::open_local_machine];
let stores = open_fns
.iter()
.filter_map(|open| open(store_name.as_str()).ok())
.collect::<Vec<_>>();
// If no stores could be opened, raise OSError
if stores.is_empty() {
return Err(vm.new_os_error(format!(
"failed to open certificate store {:?}",
store_name.as_str()
)));
}
let certs = stores.iter().flat_map(|s| s.certs()).map(|c| {
let cert = vm.ctx.new_bytes(c.to_der().to_owned());
let enc_type = unsafe {
let ptr = c.as_ptr() as *const Cryptography::CERT_CONTEXT;
(*ptr).dwCertEncodingType
};
let enc_type = match enc_type {
Cryptography::X509_ASN_ENCODING => vm.new_pyobj("x509_asn"),
Cryptography::PKCS_7_ASN_ENCODING => vm.new_pyobj("pkcs_7_asn"),
other => vm.new_pyobj(other),
};
let usage: PyObjectRef = match c.valid_uses() {
Ok(ValidUses::All) => vm.ctx.new_bool(true).into(),
Ok(ValidUses::Oids(oids)) => {
match crate::builtins::PyFrozenSet::from_iter(
vm,
oids.into_iter().map(|oid| vm.ctx.new_str(oid).into()),
) {
Ok(set) => set.into_ref(&vm.ctx).into(),
Err(_) => vm.ctx.new_bool(true).into(),
}
}
Err(_) => vm.ctx.new_bool(true).into(),
};
Ok(vm.new_tuple((cert, enc_type, usage)).into())
});
certs.collect::<PyResult<Vec<_>>>()
}
#[cfg(windows)]
#[pyfunction]
fn enum_crls(store_name: PyStrRef, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
use windows_sys::Win32::Security::Cryptography::{
CRL_CONTEXT, CertCloseStore, CertEnumCRLsInStore, CertOpenSystemStoreW,
X509_ASN_ENCODING,
};
let store_name_wide: Vec<u16> = store_name
.as_str()
.encode_utf16()
.chain(std::iter::once(0))
.collect();
// Open system store
let store = unsafe { CertOpenSystemStoreW(0, store_name_wide.as_ptr()) };
if store.is_null() {
return Err(vm.new_os_error(format!(
"failed to open certificate store {:?}",
store_name.as_str()
)));
}
let mut result = Vec::new();
let mut crl_context: *const CRL_CONTEXT = std::ptr::null();
loop {
crl_context = unsafe { CertEnumCRLsInStore(store, crl_context) };
if crl_context.is_null() {
break;
}
let crl = unsafe { &*crl_context };
let crl_bytes =
unsafe { std::slice::from_raw_parts(crl.pbCrlEncoded, crl.cbCrlEncoded as usize) };
let enc_type = if crl.dwCertEncodingType == X509_ASN_ENCODING {
vm.new_pyobj("x509_asn")
} else {
vm.new_pyobj(crl.dwCertEncodingType)
};
result.push(
vm.new_tuple((vm.ctx.new_bytes(crl_bytes.to_vec()), enc_type))
.into(),
);
}
unsafe { CertCloseStore(store, 0) };
Ok(result)
}
// Certificate type for SSL module (pure Rust implementation)
#[pyattr]
#[pyclass(module = "_ssl", name = "Certificate")]