Skip to content

Commit a79cee3

Browse files
committed
PySSLCertificate
1 parent 517b55b commit a79cee3

File tree

3 files changed

+334
-151
lines changed

3 files changed

+334
-151
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: 100 additions & 147 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,18 @@ 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+
196+
// OpenSSL error codes for unexpected EOF detection
197+
const ERR_LIB_SSL: i32 = 20;
198+
const SSL_R_UNEXPECTED_EOF_WHILE_READING: i32 = 294;
199+
181200
// the openssl version from the API headers
182201

183202
#[pyattr(name = "OPENSSL_VERSION")]
@@ -349,32 +368,6 @@ mod _ssl {
349368
fn _nid2obj(nid: Nid) -> Option<Asn1Object> {
350369
unsafe { ptr2obj(sys::OBJ_nid2obj(nid.as_raw())) }
351370
}
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-
}
378371

379372
type PyNid = (libc::c_int, String, String, Option<String>);
380373
fn obj2py(obj: &Asn1ObjectRef, vm: &VirtualMachine) -> PyResult<PyNid> {
@@ -387,7 +380,12 @@ mod _ssl {
387380
.long_name()
388381
.map_err(|_| vm.new_value_error("NID has no long name".to_owned()))?
389382
.to_owned();
390-
Ok((nid.as_raw(), short_name, long_name, obj2txt(obj, true)))
383+
Ok((
384+
nid.as_raw(),
385+
short_name,
386+
long_name,
387+
cert::obj2txt(obj, true),
388+
))
391389
}
392390

393391
#[derive(FromArgs)]
@@ -1219,46 +1217,54 @@ mod _ssl {
12191217
}
12201218

12211219
#[pymethod]
1222-
fn get_unverified_chain(&self, vm: &VirtualMachine) -> Option<PyObjectRef> {
1220+
fn get_unverified_chain(&self, vm: &VirtualMachine) -> PyResult<Option<PyListRef>> {
12231221
let stream = self.stream.read();
1224-
let chain = stream.ssl().peer_cert_chain()?;
1222+
let Some(chain) = stream.ssl().peer_cert_chain() else {
1223+
return Ok(None);
1224+
};
12251225

1226+
// Return Certificate objects
12261227
let certs: Vec<PyObjectRef> = chain
12271228
.iter()
1228-
.filter_map(|cert| cert.to_der().ok().map(|der| vm.ctx.new_bytes(der).into()))
1229-
.collect();
1230-
1231-
Some(vm.ctx.new_list(certs).into())
1229+
.map(|cert| unsafe {
1230+
sys::X509_up_ref(cert.as_ptr());
1231+
let owned = X509::from_ptr(cert.as_ptr());
1232+
cert_to_certificate(vm, owned)
1233+
})
1234+
.collect::<PyResult<_>>()?;
1235+
Ok(Some(vm.ctx.new_list(certs)))
12321236
}
12331237

12341238
#[pymethod]
1235-
fn get_verified_chain(&self, vm: &VirtualMachine) -> Option<PyListRef> {
1239+
fn get_verified_chain(&self, vm: &VirtualMachine) -> PyResult<Option<PyListRef>> {
12361240
let stream = self.stream.read();
12371241
unsafe {
12381242
let chain = sys::SSL_get0_verified_chain(stream.ssl().as_ptr());
12391243
if chain.is_null() {
1240-
return None;
1244+
return Ok(None);
12411245
}
12421246

12431247
let num_certs = sys::OPENSSL_sk_num(chain as *const _);
1244-
let mut certs = Vec::new();
12451248

1249+
let mut certs = Vec::with_capacity(num_certs as usize);
1250+
// Return Certificate objects
12461251
for i in 0..num_certs {
12471252
let cert_ptr = sys::OPENSSL_sk_value(chain as *const _, i) as *mut sys::X509;
12481253
if cert_ptr.is_null() {
12491254
continue;
12501255
}
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-
}
1256+
// Clone the X509 certificate to create an owned copy
1257+
sys::X509_up_ref(cert_ptr);
1258+
let owned_cert = X509::from_ptr(cert_ptr);
1259+
let cert_obj = cert_to_certificate(vm, owned_cert)?;
1260+
certs.push(cert_obj);
12551261
}
12561262

1257-
if certs.is_empty() {
1263+
Ok(if certs.is_empty() {
12581264
None
12591265
} else {
12601266
Some(vm.ctx.new_list(certs))
1261-
}
1267+
})
12621268
}
12631269
}
12641270

@@ -1978,7 +1984,10 @@ mod _ssl {
19781984
}
19791985

19801986
#[track_caller]
1981-
fn convert_openssl_error(vm: &VirtualMachine, err: ErrorStack) -> PyBaseExceptionRef {
1987+
pub(crate) fn convert_openssl_error(
1988+
vm: &VirtualMachine,
1989+
err: ErrorStack,
1990+
) -> PyBaseExceptionRef {
19821991
let cls = PySslError::class(&vm.ctx).to_owned();
19831992
match err.errors().last() {
19841993
Some(e) => {
@@ -2047,18 +2056,49 @@ mod _ssl {
20472056
),
20482057
ssl::ErrorCode::SYSCALL => match e.io_error() {
20492058
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-
),
2059+
// When no I/O error and OpenSSL error queue is empty,
2060+
// this is an EOF in violation of protocol -> SSLEOFError
2061+
// Need to set args[0] = SSL_ERROR_EOF for suppress_ragged_eofs check
2062+
None => {
2063+
return vm.new_exception(
2064+
PySslEOFError::class(&vm.ctx).to_owned(),
2065+
vec![
2066+
vm.ctx.new_int(SSL_ERROR_EOF).into(),
2067+
vm.ctx
2068+
.new_str("EOF occurred in violation of protocol")
2069+
.into(),
2070+
],
2071+
);
2072+
}
20542073
},
2055-
ssl::ErrorCode::SSL => match e.ssl_error() {
2056-
Some(e) => return convert_openssl_error(vm, e.clone()),
2057-
None => (
2074+
ssl::ErrorCode::SSL => {
2075+
// Check for OpenSSL 3.0 SSL_R_UNEXPECTED_EOF_WHILE_READING
2076+
if let Some(ssl_err) = e.ssl_error() {
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+
if lib == ERR_LIB_SSL && reason == SSL_R_UNEXPECTED_EOF_WHILE_READING {
2084+
return vm.new_exception(
2085+
vm.class("_ssl", "SSLEOFError"),
2086+
vec![
2087+
vm.ctx.new_int(SSL_ERROR_EOF).into(),
2088+
vm.ctx
2089+
.new_str("EOF occurred in violation of protocol")
2090+
.into(),
2091+
],
2092+
);
2093+
}
2094+
}
2095+
return convert_openssl_error(vm, ssl_err.clone());
2096+
}
2097+
(
20582098
PySslError::class(&vm.ctx).to_owned(),
20592099
"A failure in the SSL library occurred",
2060-
),
2061-
},
2100+
)
2101+
}
20622102
_ => (
20632103
PySslError::class(&vm.ctx).to_owned(),
20642104
"A failure in the SSL library occurred",
@@ -2106,93 +2146,6 @@ mod _ssl {
21062146
(cipher.name(), cipher.version(), cipher.bits().secret)
21072147
}
21082148

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-
21962149
impl Read for SocketStream {
21972150
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
21982151
let mut socket: &PySocket = &self.0;

0 commit comments

Comments
 (0)