Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -2891,6 +2891,7 @@ def test_echo(self):
'Cannot create a client socket with a PROTOCOL_TLS_SERVER context',
str(e.exception))

@unittest.skip("TODO: RUSTPYTHON; Flaky on windows")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no idea why this is failing

@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.
Expand Down
312 changes: 241 additions & 71 deletions crates/stdlib/src/ssl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ mod _ssl {
};
use std::{
collections::HashMap,
io::Write,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
Expand Down Expand Up @@ -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"));
Expand Down Expand Up @@ -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();

// 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;
#[cfg(windows)]
{
// Windows: Use schannel to load from both ROOT and CA stores
use schannel::cert_store::CertStore;

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 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]
)));
if *self.x509_cert_count.read() == 0 {
return Err(vm.new_os_error(
"Failed to load certificates from Windows store".to_owned(),
));
}

Ok(())
}

Ok(())
#[cfg(not(windows))]
{
let result = rustls_native_certs::load_native_certs();

// 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(())
}
Comment on lines +1459 to +1520
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider improving error handling to match non-Windows behavior.

The Windows implementation returns an error if x509_cert_count is 0, regardless of whether store opening succeeded. The non-Windows code (lines 1512-1517) only returns an error if both x509_cert_count == 0 AND there were errors during loading.

The non-Windows approach is more resilient—it succeeds if any certificates loaded, even if some failed. Consider updating the Windows implementation to allow partial success and provide more specific error messages (e.g., "stores exist but are empty" vs "failed to open stores").

+                let had_errors = store_names.iter().all(|&name| {
+                    open_fns.iter().all(|open_fn| open_fn(name).is_err())
+                });
+
                 if *self.x509_cert_count.read() == 0 {
+                    if !had_errors {
+                        // Stores opened but were empty - not an error
+                        return Ok(());
+                    }
                     return Err(vm.new_os_error(
                         "Failed to load certificates from Windows store".to_owned(),
                     ));
                 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In crates/stdlib/src/ssl.rs around lines 1459-1520, the Windows branch currently
returns an error whenever x509_cert_count == 0; change it to mirror the
non-Windows behavior by collecting errors while attempting to open stores and
iterate certs, and only return an error if no certs were loaded and there were
errors. Specifically: add a Vec<String> to accumulate failures from
open_fn(store_name) and any store iteration failures, continue loading any certs
that succeed and increment counts as before, and after the loops if
x509_cert_count == 0 then if errors is non-empty return a vm.os_error with the
first error, else return a vm.os_error stating the stores were opened but
contain no certificates; otherwise return Ok(()).

}

#[pymethod]
Expand All @@ -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);
#[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);

// 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)?;
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)?;
}

// Fallback to system certificates if environment variables didn't provide any
if !loaded {
let _ = self.load_system_certificates(&mut store, 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)
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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}")))?;

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"));
}
while written < data.len() {
let chunk_end = std::cmp::min(written + CHUNK_SIZE, data.len());
let chunk = &data[written..chunk_end];

// 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}")))?;
}

written = chunk_end;

// 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));
// 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);
}
}
}
Expand Down Expand Up @@ -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)))]
Comment on lines +4345 to +4352
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

CPython ssl.get_default_verify_paths() Windows openssl_cafile openssl_capath return values

💡 Result:

Short answer:

  • ssl.get_default_verify_paths() returns a DefaultVerifyPaths(namedtuple) with fields cafile, capath, openssl_cafile_env, openssl_cafile, openssl_capath_env, openssl_capath. The cafile/capath fields are the resolved paths or None if the file/dir does not exist. [1]

  • On Windows, the OpenSSL hard‑coded defaults that Python exposes are typically:

    • openssl_cafile_env = "SSL_CERT_FILE"
    • openssl_cafile = "C:\Program Files\Common Files\SSL\cert.pem"
    • openssl_capath_env = "SSL_CERT_DIR"
    • openssl_capath = "C:\Program Files\Common Files\SSL\certs"
      The runtime cafile and capath (the cafile/capath fields) will be those resolved paths if they exist, otherwise None. [1][2]

Sources:

  • Python ssl.get_default_verify_paths() docs. [1]
  • Explanation / defaults for OpenSSL on Windows (example used by PyOxidizer and others). [2]

Return None instead of empty strings for Windows certificate paths to match CPython behavior.

CPython's ssl.get_default_verify_paths() returns None for the cafile and capath fields when the hardcoded default paths do not exist on Windows (rather than returning empty strings). The current implementation returning Some("") on Windows (lines 4349-4350) deviates from this behavior. Update to return None or the actual path if it exists, consistent with how macOS and Linux are handled.

🤖 Prompt for AI Agents
In crates/stdlib/src/ssl.rs around lines 4346 to 4353, the Windows branch
currently returns Some("") for (default_cafile, default_capath) which differs
from CPython; change the Windows branch to return (None, None) when no valid
cafile/capath exists (or the actual path wrapped in Some(...) if you detect a
real file/directory), matching the macOS/Linux logic and CPython behavior so
absent/default paths are represented as None rather than empty strings.

let (default_cafile, default_capath): (Option<&str>, Option<&str>) = (None, None);

let tuple = vm.ctx.new_tuple(vec![
Expand Down Expand Up @@ -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")]
Expand Down
Loading