◐ Shell
reader mode source ↗
Skip to content
Merged
Show file tree
Changes from all commits
File filter
Conversations
Jump to
Diff view
Apply and reload
Show whitespace
Diff view
Apply and reload
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.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.
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"));
}

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
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;
}
}
}

// 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]
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);

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

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

// 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();

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

// 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));
}
return Err(e);
}
}
}
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")))]
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 {
}
}

// Certificate type for SSL module (pure Rust implementation)
#[pyattr]
#[pyclass(module = "_ssl", name = "Certificate")]
Expand Down
Loading
Toggle all file notes Toggle all file annotations