Skip to content

Commit 36475a3

Browse files
committed
PySSLCertificate
1 parent 517b55b commit 36475a3

File tree

3 files changed

+354
-140
lines changed

3 files changed

+354
-140
lines changed

Lib/test/test_urllib2_localnet.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -568,17 +568,13 @@ def test_200_with_parameters(self):
568568
self.assertEqual(data, expected_response)
569569
self.assertEqual(handler.requests, ["/bizarre", b"get=with_feeling"])
570570

571-
# TODO: RUSTPYTHON
572-
@unittest.expectedFailure
573571
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
574572
def test_https(self):
575573
handler = self.start_https_server()
576574
context = ssl.create_default_context(cafile=CERT_localhost)
577575
data = self.urlopen("https://localhost:%s/bizarre" % handler.port, context=context)
578576
self.assertEqual(data, b"we care a bit")
579577

580-
# TODO: RUSTPYTHON
581-
@unittest.expectedFailure
582578
@unittest.skipIf(os.name == "nt", "TODO: RUSTPYTHON, ValueError: illegal environment variable name")
583579
def test_https_with_cafile(self):
584580
handler = self.start_https_server(certfile=CERT_localhost)

stdlib/src/ssl.rs

Lines changed: 90 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// spell-checker:disable
22

3+
mod cert;
4+
35
use crate::vm::{PyRef, VirtualMachine, builtins::PyModule};
46
use openssl_probe::ProbeResult;
57

@@ -26,15 +28,12 @@ cfg_if::cfg_if! {
2628
}
2729

2830
#[allow(non_upper_case_globals)]
29-
#[pymodule(with(ossl101, ossl111, windows))]
31+
#[pymodule(with(cert::ssl_cert, ossl101, ossl111, windows))]
3032
mod _ssl {
3133
use super::{bio, probe};
3234
use crate::{
33-
common::{
34-
ascii,
35-
lock::{
36-
PyMappedRwLockReadGuard, PyMutex, PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard,
37-
},
35+
common::lock::{
36+
PyMappedRwLockReadGuard, PyMutex, PyRwLock, PyRwLockReadGuard, PyRwLockWriteGuard,
3837
},
3938
socket::{self, PySocket},
4039
vm::{
@@ -43,7 +42,7 @@ mod _ssl {
4342
PyBaseExceptionRef, PyBytesRef, PyListRef, PyOSError, PyStrRef, PyTypeRef, PyWeak,
4443
},
4544
class_or_notimplemented,
46-
convert::{ToPyException, ToPyObject},
45+
convert::ToPyException,
4746
exceptions,
4847
function::{
4948
ArgBytesLike, ArgCallable, ArgMemoryBuffer, ArgStrOrBytesLike, Either, FsPath,
@@ -60,7 +59,7 @@ mod _ssl {
6059
error::ErrorStack,
6160
nid::Nid,
6261
ssl::{self, SslContextBuilder, SslOptions, SslVerifyMode},
63-
x509::{self, X509, X509Ref},
62+
x509::X509,
6463
};
6564
use openssl_sys as sys;
6665
use rustpython_vm::ospath::OsPath;
@@ -73,6 +72,14 @@ mod _ssl {
7372
time::Instant,
7473
};
7574

75+
// Import certificate types from parent module
76+
use super::cert::{self, cert_to_certificate, cert_to_py};
77+
78+
// Re-export PySSLCertificate to make it available in the _ssl module
79+
// It will be automatically exposed to Python via #[pyclass]
80+
#[allow(unused_imports)]
81+
use super::cert::PySSLCertificate;
82+
7683
// Constants
7784
#[pyattr]
7885
use sys::{
@@ -178,6 +185,14 @@ mod _ssl {
178185
#[pyattr]
179186
const HAS_PSK: bool = true;
180187

188+
// Encoding constants for Certificate.public_bytes()
189+
#[pyattr]
190+
pub(crate) const ENCODING_PEM: i32 = sys::X509_FILETYPE_PEM;
191+
#[pyattr]
192+
pub(crate) const ENCODING_DER: i32 = sys::X509_FILETYPE_ASN1;
193+
#[pyattr]
194+
const ENCODING_PEM_AUX: i32 = sys::X509_FILETYPE_PEM + 0x100;
195+
181196
// the openssl version from the API headers
182197

183198
#[pyattr(name = "OPENSSL_VERSION")]
@@ -349,32 +364,6 @@ mod _ssl {
349364
fn _nid2obj(nid: Nid) -> Option<Asn1Object> {
350365
unsafe { ptr2obj(sys::OBJ_nid2obj(nid.as_raw())) }
351366
}
352-
fn obj2txt(obj: &Asn1ObjectRef, no_name: bool) -> Option<String> {
353-
let no_name = i32::from(no_name);
354-
let ptr = obj.as_ptr();
355-
let b = unsafe {
356-
let buflen = sys::OBJ_obj2txt(std::ptr::null_mut(), 0, ptr, no_name);
357-
assert!(buflen >= 0);
358-
if buflen == 0 {
359-
return None;
360-
}
361-
let buflen = buflen as usize;
362-
let mut buf = Vec::<u8>::with_capacity(buflen + 1);
363-
let ret = sys::OBJ_obj2txt(
364-
buf.as_mut_ptr() as *mut libc::c_char,
365-
buf.capacity() as _,
366-
ptr,
367-
no_name,
368-
);
369-
assert!(ret >= 0);
370-
// SAFETY: OBJ_obj2txt initialized the buffer successfully
371-
buf.set_len(buflen);
372-
buf
373-
};
374-
let s = String::from_utf8(b)
375-
.unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned());
376-
Some(s)
377-
}
378367

379368
type PyNid = (libc::c_int, String, String, Option<String>);
380369
fn obj2py(obj: &Asn1ObjectRef, vm: &VirtualMachine) -> PyResult<PyNid> {
@@ -387,7 +376,12 @@ mod _ssl {
387376
.long_name()
388377
.map_err(|_| vm.new_value_error("NID has no long name".to_owned()))?
389378
.to_owned();
390-
Ok((nid.as_raw(), short_name, long_name, obj2txt(obj, true)))
379+
Ok((
380+
nid.as_raw(),
381+
short_name,
382+
long_name,
383+
cert::obj2txt(obj, true),
384+
))
391385
}
392386

393387
#[derive(FromArgs)]
@@ -1223,9 +1217,17 @@ mod _ssl {
12231217
let stream = self.stream.read();
12241218
let chain = stream.ssl().peer_cert_chain()?;
12251219

1220+
// Return Certificate objects
12261221
let certs: Vec<PyObjectRef> = chain
12271222
.iter()
1228-
.filter_map(|cert| cert.to_der().ok().map(|der| vm.ctx.new_bytes(der).into()))
1223+
.filter_map(|cert| {
1224+
// Clone the X509 certificate to create an owned copy
1225+
unsafe {
1226+
sys::X509_up_ref(cert.as_ptr());
1227+
let owned_cert = X509::from_ptr(cert.as_ptr());
1228+
cert_to_certificate(vm, owned_cert).ok()
1229+
}
1230+
})
12291231
.collect();
12301232

12311233
Some(vm.ctx.new_list(certs).into())
@@ -1243,14 +1245,17 @@ mod _ssl {
12431245
let num_certs = sys::OPENSSL_sk_num(chain as *const _);
12441246
let mut certs = Vec::new();
12451247

1248+
// Return Certificate objects
12461249
for i in 0..num_certs {
12471250
let cert_ptr = sys::OPENSSL_sk_value(chain as *const _, i) as *mut sys::X509;
12481251
if cert_ptr.is_null() {
12491252
continue;
12501253
}
1251-
let cert = X509Ref::from_ptr(cert_ptr);
1252-
if let Ok(der) = cert.to_der() {
1253-
certs.push(vm.ctx.new_bytes(der).into());
1254+
// Clone the X509 certificate to create an owned copy
1255+
sys::X509_up_ref(cert_ptr);
1256+
let owned_cert = X509::from_ptr(cert_ptr);
1257+
if let Ok(cert_obj) = cert_to_certificate(vm, owned_cert) {
1258+
certs.push(cert_obj);
12541259
}
12551260
}
12561261

@@ -1978,7 +1983,10 @@ mod _ssl {
19781983
}
19791984

19801985
#[track_caller]
1981-
fn convert_openssl_error(vm: &VirtualMachine, err: ErrorStack) -> PyBaseExceptionRef {
1986+
pub(crate) fn convert_openssl_error(
1987+
vm: &VirtualMachine,
1988+
err: ErrorStack,
1989+
) -> PyBaseExceptionRef {
19821990
let cls = PySslError::class(&vm.ctx).to_owned();
19831991
match err.errors().last() {
19841992
Some(e) => {
@@ -2047,18 +2055,51 @@ mod _ssl {
20472055
),
20482056
ssl::ErrorCode::SYSCALL => match e.io_error() {
20492057
Some(io_err) => return io_err.to_pyexception(vm),
2050-
None => (
2051-
PySslSyscallError::class(&vm.ctx).to_owned(),
2052-
"EOF occurred in violation of protocol",
2053-
),
2058+
// When no I/O error and OpenSSL error queue is empty,
2059+
// this is an EOF in violation of protocol -> SSLEOFError
2060+
// Need to set args[0] = SSL_ERROR_EOF for suppress_ragged_eofs check
2061+
None => {
2062+
return vm.new_exception(
2063+
PySslSyscallError::class(&vm.ctx).to_owned(),
2064+
vec![
2065+
vm.ctx.new_int(SSL_ERROR_EOF).into(),
2066+
vm.ctx
2067+
.new_str("EOF occurred in violation of protocol")
2068+
.into(),
2069+
],
2070+
);
2071+
}
20542072
},
2055-
ssl::ErrorCode::SSL => match e.ssl_error() {
2056-
Some(e) => return convert_openssl_error(vm, e.clone()),
2057-
None => (
2073+
ssl::ErrorCode::SSL => {
2074+
// Check for OpenSSL 3.0 SSL_R_UNEXPECTED_EOF_WHILE_READING
2075+
if let Some(ssl_err) = e.ssl_error() {
2076+
// SSL_R_UNEXPECTED_EOF_WHILE_READING = 294 (0x126)
2077+
// In OpenSSL 3.0+, unexpected EOF is reported as SSL_ERROR_SSL
2078+
// with this specific reason code instead of SSL_ERROR_SYSCALL
2079+
unsafe {
2080+
let err_code = sys::ERR_peek_last_error();
2081+
let reason = sys::ERR_GET_REASON(err_code);
2082+
let lib = sys::ERR_GET_LIB(err_code);
2083+
// ERR_LIB_SSL = 20, SSL_R_UNEXPECTED_EOF_WHILE_READING = 294
2084+
if lib == 20 && reason == 294 {
2085+
return vm.new_exception(
2086+
vm.class("_ssl", "SSLEOFError"),
2087+
vec![
2088+
vm.ctx.new_int(SSL_ERROR_EOF).into(),
2089+
vm.ctx
2090+
.new_str("EOF occurred in violation of protocol")
2091+
.into(),
2092+
],
2093+
);
2094+
}
2095+
}
2096+
return convert_openssl_error(vm, ssl_err.clone());
2097+
}
2098+
(
20582099
PySslError::class(&vm.ctx).to_owned(),
20592100
"A failure in the SSL library occurred",
2060-
),
2061-
},
2101+
)
2102+
}
20622103
_ => (
20632104
PySslError::class(&vm.ctx).to_owned(),
20642105
"A failure in the SSL library occurred",
@@ -2106,93 +2147,6 @@ mod _ssl {
21062147
(cipher.name(), cipher.version(), cipher.bits().secret)
21072148
}
21082149

2109-
fn cert_to_py(vm: &VirtualMachine, cert: &X509Ref, binary: bool) -> PyResult {
2110-
let r = if binary {
2111-
let b = cert.to_der().map_err(|e| convert_openssl_error(vm, e))?;
2112-
vm.ctx.new_bytes(b).into()
2113-
} else {
2114-
let dict = vm.ctx.new_dict();
2115-
2116-
let name_to_py = |name: &x509::X509NameRef| -> PyResult {
2117-
let list = name
2118-
.entries()
2119-
.map(|entry| {
2120-
let txt = obj2txt(entry.object(), false).to_pyobject(vm);
2121-
let data = vm.ctx.new_str(entry.data().as_utf8()?.to_owned());
2122-
Ok(vm.new_tuple(((txt, data),)).into())
2123-
})
2124-
.collect::<Result<_, _>>()
2125-
.map_err(|e| convert_openssl_error(vm, e))?;
2126-
Ok(vm.ctx.new_tuple(list).into())
2127-
};
2128-
2129-
dict.set_item("subject", name_to_py(cert.subject_name())?, vm)?;
2130-
dict.set_item("issuer", name_to_py(cert.issuer_name())?, vm)?;
2131-
// X.509 version: OpenSSL uses 0-based (0=v1, 1=v2, 2=v3) but Python uses 1-based (1=v1, 2=v2, 3=v3)
2132-
dict.set_item("version", vm.new_pyobj(cert.version() + 1), vm)?;
2133-
2134-
let serial_num = cert
2135-
.serial_number()
2136-
.to_bn()
2137-
.and_then(|bn| bn.to_hex_str())
2138-
.map_err(|e| convert_openssl_error(vm, e))?;
2139-
dict.set_item(
2140-
"serialNumber",
2141-
vm.ctx.new_str(serial_num.to_owned()).into(),
2142-
vm,
2143-
)?;
2144-
2145-
dict.set_item(
2146-
"notBefore",
2147-
vm.ctx.new_str(cert.not_before().to_string()).into(),
2148-
vm,
2149-
)?;
2150-
dict.set_item(
2151-
"notAfter",
2152-
vm.ctx.new_str(cert.not_after().to_string()).into(),
2153-
vm,
2154-
)?;
2155-
2156-
#[allow(clippy::manual_map)]
2157-
if let Some(names) = cert.subject_alt_names() {
2158-
let san = names
2159-
.iter()
2160-
.filter_map(|gen_name| {
2161-
if let Some(email) = gen_name.email() {
2162-
Some(vm.new_tuple((ascii!("email"), email)).into())
2163-
} else if let Some(dnsname) = gen_name.dnsname() {
2164-
Some(vm.new_tuple((ascii!("DNS"), dnsname)).into())
2165-
} else if let Some(ip) = gen_name.ipaddress() {
2166-
Some(
2167-
vm.new_tuple((
2168-
ascii!("IP Address"),
2169-
String::from_utf8_lossy(ip).into_owned(),
2170-
))
2171-
.into(),
2172-
)
2173-
} else {
2174-
// TODO: convert every type of general name:
2175-
// https://github.com/python/cpython/blob/3.6/Modules/_ssl.c#L1092-L1231
2176-
None
2177-
}
2178-
})
2179-
.collect();
2180-
dict.set_item("subjectAltName", vm.ctx.new_tuple(san).into(), vm)?;
2181-
};
2182-
2183-
dict.into()
2184-
};
2185-
Ok(r)
2186-
}
2187-
2188-
#[pyfunction]
2189-
fn _test_decode_cert(path: FsPath, vm: &VirtualMachine) -> PyResult {
2190-
let path = path.to_path_buf(vm)?;
2191-
let pem = std::fs::read(path).map_err(|e| e.to_pyexception(vm))?;
2192-
let x509 = X509::from_pem(&pem).map_err(|e| convert_openssl_error(vm, e))?;
2193-
cert_to_py(vm, &x509, false)
2194-
}
2195-
21962150
impl Read for SocketStream {
21972151
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
21982152
let mut socket: &PySocket = &self.0;

0 commit comments

Comments
 (0)