From 6af5fa834c242f0022829e7d0f9d57d2e28f3313 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 17:36:29 +0900 Subject: [PATCH 01/22] openssl-sys version --- stdlib/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/Cargo.toml b/stdlib/Cargo.toml index fd6d3e8a598..cc6f76a1a64 100644 --- a/stdlib/Cargo.toml +++ b/stdlib/Cargo.toml @@ -107,7 +107,7 @@ gethostname = "1.0.2" socket2 = { version = "0.6.0", features = ["all"] } dns-lookup = "3.0" openssl = { version = "0.10.72", optional = true } -openssl-sys = { version = "0.9.80", optional = true } +openssl-sys = { version = "0.9.110", optional = true } openssl-probe = { version = "0.1.5", optional = true } foreign-types-shared = { version = "0.1.1", optional = true } From b6a1272b2c11ed7d64b1afdf102a4d8a5bffa744 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 15:15:41 +0900 Subject: [PATCH 02/22] selected_alpn_protocol --- stdlib/src/ssl.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index c9a9e15f8e3..602c218d2c5 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -1056,6 +1056,31 @@ mod _ssl { .map(cipher_to_tuple) } + #[pymethod] + fn selected_alpn_protocol(&self) -> Option { + #[cfg(ossl102)] + { + let stream = self.stream.read(); + unsafe { + let mut out: *const libc::c_uchar = std::ptr::null(); + let mut outlen: libc::c_uint = 0; + + sys::SSL_get0_alpn_selected(stream.ssl().as_ptr(), &mut out, &mut outlen); + + if out.is_null() { + None + } else { + let slice = std::slice::from_raw_parts(out, outlen as usize); + Some(String::from_utf8_lossy(slice).into_owned()) + } + } + } + #[cfg(not(ossl102))] + { + None + } + } + #[cfg(osslconf = "OPENSSL_NO_COMP")] #[pymethod] fn compression(&self) -> Option<&'static str> { From 02d49fab73a333a671a3bc3050697cc919ca0927 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 17:18:12 +0900 Subject: [PATCH 03/22] get_ciphers --- stdlib/src/ssl.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 602c218d2c5..55227e001a5 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -39,7 +39,7 @@ mod _ssl { socket::{self, PySocket}, vm::{ Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyBaseExceptionRef, PyStrRef, PyType, PyTypeRef, PyWeak}, + builtins::{PyBaseExceptionRef, PyListRef, PyStrRef, PyType, PyTypeRef, PyWeak}, class_or_notimplemented, convert::{ToPyException, ToPyObject}, exceptions, @@ -569,6 +569,64 @@ mod _ssl { }) } + #[pymethod] + fn get_ciphers(&self, vm: &VirtualMachine) -> PyResult { + let ctx = self.ctx(); + let ssl = ssl::Ssl::new(&*ctx).map_err(|e| convert_openssl_error(vm, e))?; + + unsafe { + let ciphers_ptr = SSL_get_ciphers(ssl.as_ptr()); + if ciphers_ptr.is_null() { + return Ok(vm.ctx.new_list(vec![])); + } + + let num_ciphers = sys::OPENSSL_sk_num(ciphers_ptr as *const _); + let mut result = Vec::new(); + + for i in 0..num_ciphers { + let cipher_ptr = + sys::OPENSSL_sk_value(ciphers_ptr as *const _, i) as *const sys::SSL_CIPHER; + let cipher = ssl::SslCipherRef::from_ptr(cipher_ptr as *mut _); + + let (name, version, bits) = cipher_to_tuple(cipher); + let dict = vm.ctx.new_dict(); + dict.set_item("name", vm.ctx.new_str(name).into(), vm)?; + dict.set_item("protocol", vm.ctx.new_str(version).into(), vm)?; + dict.set_item("secret_bits", vm.ctx.new_int(bits).into(), vm)?; + result.push(dict.into()); + } + + Ok(vm.ctx.new_list(result)) + } + } + + #[pymethod] + fn set_ecdh_curve(&self, name: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { + use openssl::ec::{EcGroup, EcKey}; + + let curve_name = name.as_str(); + if curve_name.contains('\0') { + return Err(exceptions::cstring_error(vm)); + } + + // Find the NID for the curve name using OBJ_sn2nid + let name_cstr = name.to_cstring(vm)?; + let nid_raw = unsafe { sys::OBJ_sn2nid(name_cstr.as_ptr()) }; + if nid_raw == 0 { + return Err(vm.new_value_error(format!("unknown curve name: {}", curve_name))); + } + let nid = Nid::from_raw(nid_raw); + + // Create EC key from the curve + let group = EcGroup::from_curve_name(nid).map_err(|e| convert_openssl_error(vm, e))?; + let key = EcKey::from_group(&group).map_err(|e| convert_openssl_error(vm, e))?; + + // Set the temporary ECDH key + self.builder() + .set_tmp_ecdh(&key) + .map_err(|e| convert_openssl_error(vm, e)) + } + #[pygetset] fn options(&self) -> libc::c_ulong { self.ctx.read().options().bits() as _ From 1878e3dd2c421707f80d7ea9ac13a1e2b3f0445d Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 17:18:39 +0900 Subject: [PATCH 04/22] get_channel_binding --- stdlib/src/ssl.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 55227e001a5..551d3b22bf1 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -39,7 +39,7 @@ mod _ssl { socket::{self, PySocket}, vm::{ Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyBaseExceptionRef, PyListRef, PyStrRef, PyType, PyTypeRef, PyWeak}, + builtins::{PyBaseExceptionRef, PyBytesRef, PyListRef, PyStrRef, PyType, PyTypeRef, PyWeak}, class_or_notimplemented, convert::{ToPyException, ToPyObject}, exceptions, @@ -1139,6 +1139,49 @@ mod _ssl { } } + #[pymethod] + fn get_channel_binding( + &self, + cb_type: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult> { + const CB_MAXLEN: usize = 512; + + let cb_type_str = cb_type.as_ref().map_or("tls-unique", |s| s.as_str()); + + if cb_type_str != "tls-unique" { + return Err(vm.new_value_error(format!( + "Unsupported channel binding type '{}'", + cb_type_str + ))); + } + + let stream = self.stream.read(); + let ssl_ptr = stream.ssl().as_ptr(); + + unsafe { + let session_reused = sys::SSL_session_reused(ssl_ptr) != 0; + let is_client = matches!(self.socket_type, SslServerOrClient::Client); + + // Use XOR logic from CPython + let use_finished = session_reused ^ is_client; + + let mut buf = vec![0u8; CB_MAXLEN]; + let len = if use_finished { + sys::SSL_get_finished(ssl_ptr, buf.as_mut_ptr() as *mut _, CB_MAXLEN) + } else { + sys::SSL_get_peer_finished(ssl_ptr, buf.as_mut_ptr() as *mut _, CB_MAXLEN) + }; + + if len == 0 { + Ok(None) + } else { + buf.truncate(len); + Ok(Some(vm.ctx.new_bytes(buf))) + } + } + } + #[cfg(osslconf = "OPENSSL_NO_COMP")] #[pymethod] fn compression(&self) -> Option<&'static str> { From 0f68a426869869744450e213112b9f6340fd96ee Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 16:17:26 +0900 Subject: [PATCH 05/22] Add Tls1_1 and Tls1_2 --- stdlib/src/ssl.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 551d3b22bf1..65c7a806074 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -116,6 +116,10 @@ mod _ssl { #[pyattr] const PROTOCOL_TLSv1: u32 = SslVersion::Tls1 as u32; #[pyattr] + const PROTOCOL_TLSv1_1: u32 = SslVersion::Tls1_1 as u32; + #[pyattr] + const PROTOCOL_TLSv1_2: u32 = SslVersion::Tls1_2 as u32; + #[pyattr] const PROTO_MINIMUM_SUPPORTED: i32 = ProtoVersion::MinSupported as i32; #[pyattr] const PROTO_SSLv3: i32 = ProtoVersion::Ssl3 as i32; @@ -265,7 +269,8 @@ mod _ssl { Ssl3 = 1, Tls, Tls1, - // TODO: Tls1_1, Tls1_2 ? + Tls1_1, + Tls1_2, TlsClient = 0x10, TlsServer, } @@ -480,7 +485,9 @@ mod _ssl { let method = match proto { // SslVersion::Ssl3 => unsafe { ssl::SslMethod::from_ptr(sys::SSLv3_method()) }, SslVersion::Tls => ssl::SslMethod::tls(), - // TODO: Tls1_1, Tls1_2 ? + SslVersion::Tls1 => ssl::SslMethod::tls(), + SslVersion::Tls1_1 => ssl::SslMethod::tls(), + SslVersion::Tls1_2 => ssl::SslMethod::tls(), SslVersion::TlsClient => ssl::SslMethod::tls_client(), SslVersion::TlsServer => ssl::SslMethod::tls_server(), _ => return Err(vm.new_value_error("invalid protocol version")), From b64fdd78f3adcdffe63f2cb4648a115a96d91af1 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 16:26:11 +0900 Subject: [PATCH 06/22] consts --- stdlib/src/ssl.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 65c7a806074..0090ac2ea1c 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -26,7 +26,7 @@ cfg_if::cfg_if! { } #[allow(non_upper_case_globals)] -#[pymodule(with(ossl101, windows))] +#[pymodule(with(ossl101, ossl111, windows))] mod _ssl { use super::{bio, probe}; use crate::{ @@ -84,19 +84,23 @@ mod _ssl { SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE, - // #ifdef SSL_OP_SINGLE_ECDH_USE - // SSL_OP_SINGLE_ECDH_USE as OP_SINGLE_ECDH_USE - // #endif // X509_V_FLAG_CRL_CHECK as VERIFY_CRL_CHECK_LEAF, // sys::X509_V_FLAG_CRL_CHECK|sys::X509_V_FLAG_CRL_CHECK_ALL as VERIFY_CRL_CHECK_CHAIN // X509_V_FLAG_X509_STRICT as VERIFY_X509_STRICT, SSL_ERROR_ZERO_RETURN, SSL_OP_CIPHER_SERVER_PREFERENCE as OP_CIPHER_SERVER_PREFERENCE, + SSL_OP_LEGACY_SERVER_CONNECT as OP_LEGACY_SERVER_CONNECT, SSL_OP_NO_SSLv2 as OP_NO_SSLv2, SSL_OP_NO_SSLv3 as OP_NO_SSLv3, SSL_OP_NO_TICKET as OP_NO_TICKET, SSL_OP_NO_TLSv1 as OP_NO_TLSv1, SSL_OP_SINGLE_DH_USE as OP_SINGLE_DH_USE, + SSL_OP_SINGLE_ECDH_USE as OP_SINGLE_ECDH_USE, + X509_V_FLAG_ALLOW_PROXY_CERTS as VERIFY_ALLOW_PROXY_CERTS, + X509_V_FLAG_CRL_CHECK as VERIFY_CRL_CHECK_LEAF, + X509_V_FLAG_PARTIAL_CHAIN as VERIFY_X509_PARTIAL_CHAIN, + X509_V_FLAG_TRUSTED_FIRST as VERIFY_X509_TRUSTED_FIRST, + X509_V_FLAG_X509_STRICT as VERIFY_X509_STRICT, }; // taken from CPython, should probably be kept up to date with their version if it ever changes @@ -150,7 +154,7 @@ mod _ssl { #[pyattr] const HAS_SNI: bool = true; #[pyattr] - const HAS_ECDH: bool = false; + const HAS_ECDH: bool = true; #[pyattr] const HAS_NPN: bool = false; #[pyattr] From 666381ab15b984304ffc04c3a65de35fe9456f76 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 16:57:08 +0900 Subject: [PATCH 07/22] fix ssl truncate bug --- stdlib/src/ssl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 0090ac2ea1c..af64107710c 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -1404,7 +1404,7 @@ mod _ssl { let ret = match inner_buffer { Either::A(_buf) => vm.ctx.new_int(count).into(), Either::B(mut buf) => { - buf.truncate(n); + buf.truncate(count); buf.shrink_to_fit(); vm.ctx.new_bytes(buf).into() } From efddfce74a6150861d19c51e6c65cc8c8cd0d7e0 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 17:18:26 +0900 Subject: [PATCH 08/22] verify_flags --- stdlib/src/ssl.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index af64107710c..6024e1fcb5f 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -685,6 +685,42 @@ mod _ssl { Ok(()) } #[pygetset] + fn verify_flags(&self) -> libc::c_ulong { + unsafe { + let ctx_ptr = self.ctx().as_ptr(); + let param = sys::SSL_CTX_get0_param(ctx_ptr); + sys::X509_VERIFY_PARAM_get_flags(param) + } + } + #[pygetset(setter)] + fn set_verify_flags(&self, new_flags: libc::c_ulong, vm: &VirtualMachine) -> PyResult<()> { + unsafe { + let ctx_ptr = self.ctx().as_ptr(); + let param = sys::SSL_CTX_get0_param(ctx_ptr); + let flags = sys::X509_VERIFY_PARAM_get_flags(param); + let clear = flags & !new_flags; + let set = !flags & new_flags; + + if clear != 0 { + if sys::X509_VERIFY_PARAM_clear_flags(param, clear) == 0 { + return Err(vm.new_exception_msg( + ssl_error(vm), + "Failed to clear verify flags".to_owned(), + )); + } + } + if set != 0 { + if sys::X509_VERIFY_PARAM_set_flags(param, set) == 0 { + return Err(vm.new_exception_msg( + ssl_error(vm), + "Failed to set verify flags".to_owned(), + )); + } + } + Ok(()) + } + } + #[pygetset] fn check_hostname(&self) -> bool { self.check_hostname.load() } From a262e856bb3b41f145242180717226af25959379 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 15:41:53 +0900 Subject: [PATCH 09/22] shared ciphers --- stdlib/src/ssl.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 6024e1fcb5f..af2fa7e8bb5 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -1161,6 +1161,69 @@ mod _ssl { .map(cipher_to_tuple) } + #[pymethod] + fn shared_ciphers(&self, vm: &VirtualMachine) -> Option { + #[cfg(ossl110)] + { + let stream = self.stream.read(); + unsafe { + let server_ciphers = SSL_get_ciphers(stream.ssl().as_ptr()); + if server_ciphers.is_null() { + return None; + } + + let client_ciphers = SSL_get_client_ciphers(stream.ssl().as_ptr()); + if client_ciphers.is_null() { + return None; + } + + let mut result = Vec::new(); + let num_server = sys::OPENSSL_sk_num(server_ciphers as *const _); + let num_client = sys::OPENSSL_sk_num(client_ciphers as *const _); + + for i in 0..num_server { + let server_cipher_ptr = sys::OPENSSL_sk_value(server_ciphers as *const _, i) + as *const sys::SSL_CIPHER; + + // Check if client supports this cipher by comparing pointers + let mut found = false; + for j in 0..num_client { + let client_cipher_ptr = + sys::OPENSSL_sk_value(client_ciphers as *const _, j) + as *const sys::SSL_CIPHER; + + if server_cipher_ptr == client_cipher_ptr { + found = true; + break; + } + } + + if found { + let cipher = ssl::SslCipherRef::from_ptr(server_cipher_ptr as *mut _); + let (name, version, bits) = cipher_to_tuple(cipher); + let tuple = vm.new_tuple(( + vm.ctx.new_str(name), + vm.ctx.new_str(version), + vm.ctx.new_int(bits), + )); + result.push(tuple.into()); + } + } + + if result.is_empty() { + None + } else { + Some(vm.ctx.new_list(result)) + } + } + } + #[cfg(not(ossl110))] + { + let _ = vm; + None + } + } + #[pymethod] fn selected_alpn_protocol(&self) -> Option { #[cfg(ossl102)] @@ -1536,6 +1599,16 @@ mod _ssl { unsafe impl Send for PySslMemoryBio {} unsafe impl Sync for PySslMemoryBio {} + // OpenSSL functions not in openssl-sys + unsafe extern "C" { + fn SSL_get_ciphers(ssl: *const sys::SSL) -> *const sys::stack_st_SSL_CIPHER; + } + + #[cfg(ossl110)] + unsafe extern "C" { + fn SSL_get_client_ciphers(ssl: *const sys::SSL) -> *const sys::stack_st_SSL_CIPHER; + } + // OpenSSL BIO helper functions // These are typically macros in OpenSSL, implemented via BIO_ctrl const BIO_CTRL_PENDING: libc::c_int = 10; From 97f11dce721afcf81db59906fcb3a1f36b32803b Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 17:36:47 +0900 Subject: [PATCH 10/22] verify_client_post_handshake --- stdlib/src/ssl.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index af2fa7e8bb5..d064a9e8317 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -1292,6 +1292,29 @@ mod _ssl { } } + #[pymethod] + fn verify_client_post_handshake(&self, vm: &VirtualMachine) -> PyResult<()> { + #[cfg(ossl111)] + { + let stream = self.stream.read(); + let result = unsafe { SSL_verify_client_post_handshake(stream.ssl().as_ptr()) }; + if result == 0 { + Err(vm.new_exception_msg( + ssl_error(vm), + "Post-handshake authentication failed".to_owned(), + )) + } else { + Ok(()) + } + } + #[cfg(not(ossl111))] + { + Err(vm.new_not_implemented_error( + "Post-handshake auth is not supported by your OpenSSL version.".to_owned() + )) + } + } + #[cfg(osslconf = "OPENSSL_NO_COMP")] #[pymethod] fn compression(&self) -> Option<&'static str> { @@ -1609,6 +1632,11 @@ mod _ssl { fn SSL_get_client_ciphers(ssl: *const sys::SSL) -> *const sys::stack_st_SSL_CIPHER; } + #[cfg(ossl111)] + unsafe extern "C" { + fn SSL_verify_client_post_handshake(ssl: *const sys::SSL) -> libc::c_int; + } + // OpenSSL BIO helper functions // These are typically macros in OpenSSL, implemented via BIO_ctrl const BIO_CTRL_PENDING: libc::c_int = 10; From 5b2b64c40ef72a2d960d95b38cbb752ea032cb0f Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 18:56:54 +0900 Subject: [PATCH 11/22] shutdown --- stdlib/src/ssl.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index d064a9e8317..06b7d24303c 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -1310,11 +1310,43 @@ mod _ssl { #[cfg(not(ossl111))] { Err(vm.new_not_implemented_error( - "Post-handshake auth is not supported by your OpenSSL version.".to_owned() + "Post-handshake auth is not supported by your OpenSSL version.".to_owned(), )) } } + #[pymethod] + fn shutdown(&self, vm: &VirtualMachine) -> PyResult> { + let stream = self.stream.read(); + let ssl_ptr = stream.ssl().as_ptr(); + + // Perform SSL shutdown + let ret = unsafe { sys::SSL_shutdown(ssl_ptr) }; + + if ret < 0 { + // Error occurred + let err = unsafe { + let err_code = sys::SSL_get_error(ssl_ptr, ret); + err_code + }; + + if err == sys::SSL_ERROR_WANT_READ || err == sys::SSL_ERROR_WANT_WRITE { + // Non-blocking would block - this is okay for shutdown + // Return the underlying socket + } else { + return Err(vm.new_exception_msg( + ssl_error(vm), + format!("SSL shutdown failed: error code {}", err), + )); + } + } + + // Return the underlying socket + // Get the socket from the stream (SocketStream wraps PyRef) + let socket = stream.get_ref(); + Ok(socket.0.clone()) + } + #[cfg(osslconf = "OPENSSL_NO_COMP")] #[pymethod] fn compression(&self) -> Option<&'static str> { From 6abf7f13bdbcadaad749b27ea8cf0de98d207ce3 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Thu, 23 Oct 2025 17:29:37 +0900 Subject: [PATCH 12/22] get_verified_chain --- stdlib/src/ssl.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 06b7d24303c..7d1b31e4a4e 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -1146,6 +1146,37 @@ mod _ssl { Some(vm.ctx.new_list(certs).into()) } + #[pymethod] + fn get_verified_chain(&self, vm: &VirtualMachine) -> Option { + let stream = self.stream.read(); + unsafe { + let chain = sys::SSL_get0_verified_chain(stream.ssl().as_ptr()); + if chain.is_null() { + return None; + } + + let num_certs = sys::OPENSSL_sk_num(chain as *const _); + let mut certs = Vec::new(); + + for i in 0..num_certs { + let cert_ptr = sys::OPENSSL_sk_value(chain as *const _, i) as *mut sys::X509; + if cert_ptr.is_null() { + continue; + } + let cert = X509Ref::from_ptr(cert_ptr); + if let Ok(der) = cert.to_der() { + certs.push(vm.ctx.new_bytes(der).into()); + } + } + + if certs.is_empty() { + None + } else { + Some(vm.ctx.new_list(certs)) + } + } + } + #[pymethod] fn version(&self) -> Option<&'static str> { let v = self.stream.read().ssl().version_str(); From c3dad1d12875c9c4bb490033ac882681d6905a6c Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sat, 25 Oct 2025 22:28:21 +0900 Subject: [PATCH 13/22] fix lints --- stdlib/src/ssl.rs | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 7d1b31e4a4e..fe864544736 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -39,7 +39,9 @@ mod _ssl { socket::{self, PySocket}, vm::{ Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyBaseExceptionRef, PyBytesRef, PyListRef, PyStrRef, PyType, PyTypeRef, PyWeak}, + builtins::{ + PyBaseExceptionRef, PyBytesRef, PyListRef, PyStrRef, PyType, PyTypeRef, PyWeak, + }, class_or_notimplemented, convert::{ToPyException, ToPyObject}, exceptions, @@ -583,7 +585,7 @@ mod _ssl { #[pymethod] fn get_ciphers(&self, vm: &VirtualMachine) -> PyResult { let ctx = self.ctx(); - let ssl = ssl::Ssl::new(&*ctx).map_err(|e| convert_openssl_error(vm, e))?; + let ssl = ssl::Ssl::new(&ctx).map_err(|e| convert_openssl_error(vm, e))?; unsafe { let ciphers_ptr = SSL_get_ciphers(ssl.as_ptr()); @@ -701,21 +703,17 @@ mod _ssl { let clear = flags & !new_flags; let set = !flags & new_flags; - if clear != 0 { - if sys::X509_VERIFY_PARAM_clear_flags(param, clear) == 0 { - return Err(vm.new_exception_msg( - ssl_error(vm), - "Failed to clear verify flags".to_owned(), - )); - } + if clear != 0 && sys::X509_VERIFY_PARAM_clear_flags(param, clear) == 0 { + return Err(vm.new_exception_msg( + ssl_error(vm), + "Failed to clear verify flags".to_owned(), + )); } - if set != 0 { - if sys::X509_VERIFY_PARAM_set_flags(param, set) == 0 { - return Err(vm.new_exception_msg( - ssl_error(vm), - "Failed to set verify flags".to_owned(), - )); - } + if set != 0 && sys::X509_VERIFY_PARAM_set_flags(param, set) == 0 { + return Err(vm.new_exception_msg( + ssl_error(vm), + "Failed to set verify flags".to_owned(), + )); } Ok(()) } @@ -1356,10 +1354,7 @@ mod _ssl { if ret < 0 { // Error occurred - let err = unsafe { - let err_code = sys::SSL_get_error(ssl_ptr, ret); - err_code - }; + let err = unsafe { sys::SSL_get_error(ssl_ptr, ret) }; if err == sys::SSL_ERROR_WANT_READ || err == sys::SSL_ERROR_WANT_WRITE { // Non-blocking would block - this is okay for shutdown From 68f8655b125d814d204bffe471b137f697458211 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 24 Oct 2025 21:01:23 +0900 Subject: [PATCH 14/22] fix _wrap_socket --- stdlib/src/ssl.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index fe864544736..244a7bbbd16 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -884,6 +884,20 @@ mod _ssl { args: WrapSocketArgs, vm: &VirtualMachine, ) -> PyResult { + // validate socket type and context protocol + if !args.server_side && zelf.protocol == SslVersion::TlsServer { + return Err(vm.new_exception_msg( + ssl_error(vm), + "Cannot create a client socket with a PROTOCOL_TLS_SERVER context".to_owned(), + )); + } + if args.server_side && zelf.protocol == SslVersion::TlsClient { + return Err(vm.new_exception_msg( + ssl_error(vm), + "Cannot create a server socket with a PROTOCOL_TLS_CLIENT context".to_owned(), + )); + } + let mut ssl = ssl::Ssl::new(&zelf.ctx()).map_err(|e| convert_openssl_error(vm, e))?; let socket_type = if args.server_side { From 9ddd43cf9e446f81b916bf67f994cd2653e9a01b Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 24 Oct 2025 21:04:33 +0900 Subject: [PATCH 15/22] Fix convert_openssl_error --- stdlib/src/ssl.rs | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 244a7bbbd16..63c24fdb913 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -1914,23 +1914,39 @@ mod _ssl { "certificate verify failed" => "CERTIFICATE_VERIFY_FAILED", _ => default_errstr, }; - let msg = if let Some(lib) = e.library() { - // add `library` attribute - let attr_name = vm.ctx.as_ref().intern_str("library"); - cls.set_attr(attr_name, vm.ctx.new_str(lib).into()); + + // Build message + let lib_obj = e.library(); + let msg = if let Some(lib) = lib_obj { format!("[{lib}] {errstr} ({file}:{line})") } else { format!("{errstr} ({file}:{line})") }; - // add `reason` attribute - let attr_name = vm.ctx.as_ref().intern_str("reason"); - cls.set_attr(attr_name, vm.ctx.new_str(errstr).into()); + // Create exception instance let reason = sys::ERR_GET_REASON(e.code()); - vm.new_exception( + let exc = vm.new_exception( cls, vec![vm.ctx.new_int(reason).into(), vm.ctx.new_str(msg).into()], - ) + ); + + // Set attributes on instance, not class + let exc_obj: PyObjectRef = exc.into(); + + // Set reason attribute (always set, even if just the error string) + let reason_value = vm.ctx.new_str(errstr); + let _ = exc_obj.set_attr("reason", reason_value, vm); + + // Set library attribute (None if not available) + let library_value: PyObjectRef = if let Some(lib) = lib_obj { + vm.ctx.new_str(lib).into() + } else { + vm.ctx.none() + }; + let _ = exc_obj.set_attr("library", library_value, vm); + + // Convert back to PyBaseExceptionRef + exc_obj.downcast().unwrap() } None => vm.new_exception_empty(cls), } From fa5475223fdb82517b34b6b9d4e9d9d68bfca2d7 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Fri, 24 Oct 2025 21:05:04 +0900 Subject: [PATCH 16/22] clean up ssl --- stdlib/src/ssl.rs | 107 +++++++++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 39 deletions(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 63c24fdb913..d1749e6c033 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -68,7 +68,8 @@ mod _ssl { ffi::CStr, fmt, io::{Read, Write}, - path::Path, + path::{Path, PathBuf}, + sync::LazyLock, time::Instant, }; @@ -193,7 +194,8 @@ mod _ssl { #[pyattr(name = "_OPENSSL_API_VERSION")] fn _openssl_api_version(_vm: &VirtualMachine) -> OpensslVersionInfo { - let openssl_api_version = i64::from_str_radix(env!("OPENSSL_API_VERSION"), 16).unwrap(); + let openssl_api_version = i64::from_str_radix(env!("OPENSSL_API_VERSION"), 16) + .expect("OPENSSL_API_VERSION is malformed"); parse_version_info(openssl_api_version) } @@ -251,7 +253,8 @@ mod _ssl { /// SSL/TLS connection terminated abruptly. #[pyattr(name = "SSLEOFError", once)] fn ssl_eof_error(vm: &VirtualMachine) -> PyTypeRef { - PyType::new_simple_heap("ssl.SSLEOFError", &ssl_error(vm), &vm.ctx).unwrap() + vm.ctx + .new_exception_type("ssl", "SSLEOFError", Some(vec![ssl_error(vm)])) } type OpensslVersionInfo = (u8, u8, u8, u8, u8); @@ -352,14 +355,17 @@ mod _ssl { } type PyNid = (libc::c_int, String, String, Option); - fn obj2py(obj: &Asn1ObjectRef) -> PyNid { + fn obj2py(obj: &Asn1ObjectRef, vm: &VirtualMachine) -> PyResult { let nid = obj.nid(); - ( - nid.as_raw(), - nid.short_name().unwrap().to_owned(), - nid.long_name().unwrap().to_owned(), - obj2txt(obj, true), - ) + let short_name = nid + .short_name() + .map_err(|_| vm.new_value_error("NID has no short name".to_owned()))? + .to_owned(); + let long_name = nid + .long_name() + .map_err(|_| vm.new_value_error("NID has no long name".to_owned()))? + .to_owned(); + Ok((nid.as_raw(), short_name, long_name, obj2txt(obj, true))) } #[derive(FromArgs)] @@ -373,55 +379,81 @@ mod _ssl { fn txt2obj(args: Txt2ObjArgs, vm: &VirtualMachine) -> PyResult { _txt2obj(&args.txt.to_cstring(vm)?, !args.name) .as_deref() - .map(obj2py) .ok_or_else(|| vm.new_value_error(format!("unknown object '{}'", args.txt))) + .and_then(|obj| obj2py(obj, vm)) } #[pyfunction] fn nid2obj(nid: libc::c_int, vm: &VirtualMachine) -> PyResult { _nid2obj(Nid::from_raw(nid)) .as_deref() - .map(obj2py) .ok_or_else(|| vm.new_value_error(format!("unknown NID {nid}"))) + .and_then(|obj| obj2py(obj, vm)) } - fn get_cert_file_dir() -> (&'static Path, &'static Path) { - let probe = probe(); - // on windows, these should be utf8 strings - fn path_from_bytes(c: &CStr) -> &Path { + // Lazily compute and cache cert file/dir paths + static CERT_PATHS: LazyLock<(PathBuf, PathBuf)> = LazyLock::new(|| { + fn path_from_cstr(c: &CStr) -> PathBuf { #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; - std::ffi::OsStr::from_bytes(c.to_bytes()).as_ref() + std::ffi::OsStr::from_bytes(c.to_bytes()).into() } #[cfg(windows)] { - c.to_str().unwrap().as_ref() + // Use lossy conversion for potential non-UTF8 + PathBuf::from(c.to_string_lossy().as_ref()) } } - let cert_file = probe.cert_file.as_deref().unwrap_or_else(|| { - path_from_bytes(unsafe { CStr::from_ptr(sys::X509_get_default_cert_file()) }) - }); - let cert_dir = probe.cert_dir.as_deref().unwrap_or_else(|| { - path_from_bytes(unsafe { CStr::from_ptr(sys::X509_get_default_cert_dir()) }) - }); + + let probe = probe(); + let cert_file = probe + .cert_file + .as_ref() + .map(PathBuf::from) + .unwrap_or_else(|| { + path_from_cstr(unsafe { CStr::from_ptr(sys::X509_get_default_cert_file()) }) + }); + let cert_dir = probe + .cert_dir + .as_ref() + .map(PathBuf::from) + .unwrap_or_else(|| { + path_from_cstr(unsafe { CStr::from_ptr(sys::X509_get_default_cert_dir()) }) + }); (cert_file, cert_dir) + }); + + fn get_cert_file_dir() -> (&'static Path, &'static Path) { + let (cert_file, cert_dir) = &*CERT_PATHS; + (cert_file.as_path(), cert_dir.as_path()) } + // Lazily compute and cache cert environment variable names + static CERT_ENV_NAMES: LazyLock<(String, String)> = LazyLock::new(|| { + let cert_file_env = unsafe { CStr::from_ptr(sys::X509_get_default_cert_file_env()) } + .to_string_lossy() + .into_owned(); + let cert_dir_env = unsafe { CStr::from_ptr(sys::X509_get_default_cert_dir_env()) } + .to_string_lossy() + .into_owned(); + (cert_file_env, cert_dir_env) + }); + #[pyfunction] fn get_default_verify_paths( vm: &VirtualMachine, ) -> PyResult<(&'static str, PyObjectRef, &'static str, PyObjectRef)> { - let cert_file_env = unsafe { CStr::from_ptr(sys::X509_get_default_cert_file_env()) } - .to_str() - .unwrap(); - let cert_dir_env = unsafe { CStr::from_ptr(sys::X509_get_default_cert_dir_env()) } - .to_str() - .unwrap(); + let (cert_file_env, cert_dir_env) = &*CERT_ENV_NAMES; let (cert_file, cert_dir) = get_cert_file_dir(); let cert_file = OsPath::new_str(cert_file).filename(vm); let cert_dir = OsPath::new_str(cert_dir).filename(vm); - Ok((cert_file_env, cert_file, cert_dir_env, cert_dir)) + Ok(( + cert_file_env.as_str(), + cert_file, + cert_dir_env.as_str(), + cert_dir, + )) } #[pyfunction(name = "RAND_status")] @@ -1871,12 +1903,12 @@ mod _ssl { } #[pygetset] - fn id(&self, vm: &VirtualMachine) -> PyObjectRef { + fn id(&self, vm: &VirtualMachine) -> PyBytesRef { unsafe { let mut len: libc::c_uint = 0; let id_ptr = sys::SSL_SESSION_get_id(self.session, &mut len); let id_slice = std::slice::from_raw_parts(id_ptr, len as usize); - vm.ctx.new_bytes(id_slice.to_vec()).into() + vm.ctx.new_bytes(id_slice.to_vec()) } } @@ -2256,21 +2288,18 @@ mod windows { Cryptography::PKCS_7_ASN_ENCODING => vm.new_pyobj(ascii!("pkcs_7_asn")), other => vm.new_pyobj(other), }; - let usage: PyObjectRef = match c.valid_uses()? { + let usage: PyObjectRef = match c.valid_uses().map_err(|e| e.to_pyexception(vm))? { ValidUses::All => vm.ctx.new_bool(true).into(), ValidUses::Oids(oids) => PyFrozenSet::from_iter( vm, oids.into_iter().map(|oid| vm.ctx.new_str(oid).into()), - ) - .unwrap() + )? .into_ref(&vm.ctx) .into(), }; Ok(vm.new_tuple((cert, enc_type, usage)).into()) }); - let certs = certs - .collect::, _>>() - .map_err(|e: std::io::Error| e.to_pyexception(vm))?; + let certs: Vec = certs.collect::>>()?; Ok(certs) } } From 04f2b0a939010296f74ff526a54f879bdcf507a9 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 26 Oct 2025 00:37:54 +0900 Subject: [PATCH 17/22] X509_check_ca --- stdlib/src/ssl.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index d1749e6c033..d7b2dfe84b8 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -878,8 +878,16 @@ mod _ssl { let certs = ctx.cert_store().all_certificates(); #[cfg(not(ossl300))] let certs = ctx.cert_store().objects().iter().filter_map(|x| x.x509()); + + // Filter to only include CA certificates (Basic Constraints: CA=TRUE) let certs = certs .into_iter() + .filter(|cert| { + unsafe { + // X509_check_ca() returns 1 for CA certificates + X509_check_ca(cert.as_ptr()) == 1 + } + }) .map(|ref cert| cert_to_py(vm, cert, binary_form)) .collect::, _>>()?; Ok(certs) @@ -1727,6 +1735,12 @@ mod _ssl { unsafe impl Sync for PySslMemoryBio {} // OpenSSL functions not in openssl-sys + + unsafe extern "C" { + // X509_check_ca returns 1 for CA certificates, 0 otherwise + fn X509_check_ca(x: *const sys::X509) -> libc::c_int; + } + unsafe extern "C" { fn SSL_get_ciphers(ssl: *const sys::SSL) -> *const sys::stack_st_SSL_CIPHER; } From e568f493e396dcea07a63d9889a27659612820ca Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 26 Oct 2025 00:38:23 +0900 Subject: [PATCH 18/22] set default verify flag --- stdlib/src/ssl.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index d7b2dfe84b8..6d491e46a1e 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -568,6 +568,13 @@ mod _ssl { .set_session_id_context(b"Python") .map_err(|e| convert_openssl_error(vm, e))?; + // Set default verify flags: VERIFY_X509_TRUSTED_FIRST + unsafe { + let ctx_ptr = builder.as_ptr(); + let param = sys::SSL_CTX_get0_param(ctx_ptr); + sys::X509_VERIFY_PARAM_set_flags(param, sys::X509_V_FLAG_TRUSTED_FIRST); + } + PySslContext { ctx: PyRwLock::new(builder), check_hostname: AtomicCell::new(check_hostname), From 9fa10eb2da3ec9eda13d5c17bee70db40d33af3f Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 26 Oct 2025 00:39:35 +0900 Subject: [PATCH 19/22] Fix version --- stdlib/src/ssl.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 6d491e46a1e..9dfcf5decfd 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -2096,7 +2096,8 @@ mod _ssl { dict.set_item("subject", name_to_py(cert.subject_name())?, vm)?; dict.set_item("issuer", name_to_py(cert.issuer_name())?, vm)?; - dict.set_item("version", vm.new_pyobj(cert.version()), vm)?; + // X.509 version: OpenSSL uses 0-based (0=v1, 1=v2, 2=v3) but Python uses 1-based (1=v1, 2=v2, 3=v3) + dict.set_item("version", vm.new_pyobj(cert.version() + 1), vm)?; let serial_num = cert .serial_number() From b030e1ab76085e2abccf703ac215c466ac574687 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 26 Oct 2025 00:48:11 +0900 Subject: [PATCH 20/22] fix import --- stdlib/src/ssl.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 9dfcf5decfd..35ad796ed1b 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -39,9 +39,7 @@ mod _ssl { socket::{self, PySocket}, vm::{ Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{ - PyBaseExceptionRef, PyBytesRef, PyListRef, PyStrRef, PyType, PyTypeRef, PyWeak, - }, + builtins::{PyBaseExceptionRef, PyBytesRef, PyListRef, PyStrRef, PyTypeRef, PyWeak}, class_or_notimplemented, convert::{ToPyException, ToPyObject}, exceptions, From 422ea8c060dd39b0c45cd0484a7651d2e532ebdc Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 26 Oct 2025 00:45:00 +0900 Subject: [PATCH 21/22] consts --- stdlib/src/ssl.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stdlib/src/ssl.rs b/stdlib/src/ssl.rs index 35ad796ed1b..bf77d6b6907 100644 --- a/stdlib/src/ssl.rs +++ b/stdlib/src/ssl.rs @@ -90,6 +90,7 @@ mod _ssl { // X509_V_FLAG_X509_STRICT as VERIFY_X509_STRICT, SSL_ERROR_ZERO_RETURN, SSL_OP_CIPHER_SERVER_PREFERENCE as OP_CIPHER_SERVER_PREFERENCE, + SSL_OP_ENABLE_MIDDLEBOX_COMPAT as OP_ENABLE_MIDDLEBOX_COMPAT, SSL_OP_LEGACY_SERVER_CONNECT as OP_LEGACY_SERVER_CONNECT, SSL_OP_NO_SSLv2 as OP_NO_SSLv2, SSL_OP_NO_SSLv3 as OP_NO_SSLv3, @@ -552,6 +553,7 @@ mod _ssl { options |= SslOptions::CIPHER_SERVER_PREFERENCE; options |= SslOptions::SINGLE_DH_USE; options |= SslOptions::SINGLE_ECDH_USE; + options |= SslOptions::ENABLE_MIDDLEBOX_COMPAT; builder.set_options(options); let mode = ssl::SslMode::ACCEPT_MOVING_WRITE_BUFFER | ssl::SslMode::AUTO_RETRY; From 91df67c21b41337cd9d0b12d01a7186eae28eb54 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sun, 26 Oct 2025 09:30:04 +0900 Subject: [PATCH 22/22] fix httplib --- Lib/test/test_httplib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 5a59f372ad3..9b631fb397a 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1780,8 +1780,6 @@ def test_networked_bad_cert(self): h.request('GET', '/') self.assertEqual(exc_info.exception.reason, 'CERTIFICATE_VERIFY_FAILED') - # TODO: RUSTPYTHON - @unittest.expectedFailure @unittest.skipIf(sys.platform == 'darwin', 'Occasionally success on macOS') def test_local_unknown_cert(self): # The custom cert isn't known to the default trust bundle