Skip to content

Commit e939d92

Browse files
committed
ssl: emit the full peer certificate dict
Use the same certificate dict helper for getpeercert() and Certificate.get_info() so OCSP, caIssuers, and CRL URLs are present. Clear CHANNEL_BINDING_TYPES on the rustls backend, and add a MemoryBIO leftover-plaintext snippet. Assisted-by: Claude
1 parent 1201e22 commit e939d92

6 files changed

Lines changed: 54 additions & 129 deletions

File tree

‎Lib/ssl.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@ class _TLSMessageType:
268268
socket_error = OSError # keep that public name in module namespace
269269

270270
CHANNEL_BINDING_TYPES = ['tls-unique']
271+
# TODO: RUSTPYTHON; rustls does not expose TLS Finished messages
272+
if "rustls" in OPENSSL_VERSION:
273+
CHANNEL_BINDING_TYPES = []
271274

272275
HAS_NEVER_CHECK_COMMON_NAME = hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT')
273276

‎Lib/test/test_asyncio/test_events.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1289,7 +1289,6 @@ def test_create_unix_server_ssl_verified(self):
12891289
server.close()
12901290
self.loop.run_until_complete(proto.done)
12911291

1292-
@unittest.expectedFailure # TODO: RUSTPYTHON; - SSL peer certificate format differs
12931292
@unittest.skipIf(ssl is None, 'No ssl module')
12941293
def test_create_server_ssl_verified(self):
12951294
proto = MyProto(loop=self.loop)

‎Lib/test/test_ssl.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4192,7 +4192,6 @@ def test_default_ecdh_curve(self):
41924192
s.connect((HOST, server.port))
41934193
self.assertIn("ECDH", s.cipher()[0])
41944194

4195-
@unittest.expectedFailure # TODO: RUSTPYTHON
41964195
@unittest.skipUnless("tls-unique" in ssl.CHANNEL_BINDING_TYPES,
41974196
"'tls-unique' channel binding not available")
41984197
def test_tls_unique_channel_binding(self):

‎crates/stdlib/src/ssl.rs‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3723,11 +3723,7 @@ mod _ssl {
37233723
return Ok(Some(vm.ctx.new_dict().into()));
37243724
}
37253725

3726-
// Parse DER certificate and convert to dict (outside lock)
3727-
let (_, cert) = x509_parser::parse_x509_certificate(&der_bytes)
3728-
.map_err(|e| vm.new_value_error(format!("Failed to parse certificate: {e}")))?;
3729-
3730-
cert::cert_to_dict(vm, &cert).map(Some)
3726+
cert::cert_der_to_dict_helper(vm, &der_bytes).map(Some)
37313727
}
37323728

37333729
#[pymethod]
@@ -4643,7 +4639,6 @@ mod _ssl {
46434639
}
46444640

46454641
impl PySSLCertificate {
4646-
// Parse the certificate lazily
46474642
fn parse(&self) -> Result<x509_parser::certificate::X509Certificate<'_>, String> {
46484643
match x509_parser::parse_x509_certificate(&self.der_bytes) {
46494644
Ok((_, cert)) => Ok(cert),
@@ -4682,8 +4677,7 @@ mod _ssl {
46824677

46834678
#[pymethod]
46844679
fn get_info(&self, vm: &VirtualMachine) -> PyResult {
4685-
let cert = self.parse().map_err(|e| vm.new_value_error(e))?;
4686-
cert::cert_to_dict(vm, &cert)
4680+
cert::cert_der_to_dict_helper(vm, &self.der_bytes)
46874681
}
46884682
}
46894683

‎crates/stdlib/src/ssl/cert.rs‎

Lines changed: 3 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -228,45 +228,6 @@ fn normalize_wildcard_hostname(hostname: &str) -> &str {
228228
hostname.strip_prefix("*.").unwrap_or(hostname)
229229
}
230230

231-
/// Process Subject Alternative Name (SAN) general names into Python tuples
232-
///
233-
/// Converts X.509 GeneralName entries into Python tuple format.
234-
/// Returns a vector of PyObjectRef tuples in the format: (type, value)
235-
fn process_san_general_names(
236-
vm: &VirtualMachine,
237-
general_names: &[GeneralName<'_>],
238-
) -> Vec<PyObjectRef> {
239-
general_names
240-
.iter()
241-
.filter_map(|name| match name {
242-
GeneralName::DNSName(dns) => Some(vm.new_tuple(("DNS", *dns)).into()),
243-
GeneralName::IPAddress(ip) => {
244-
let ip_str = format_ip_address(ip);
245-
Some(vm.new_tuple(("IP Address", ip_str)).into())
246-
}
247-
GeneralName::RFC822Name(email) => Some(vm.new_tuple(("email", *email)).into()),
248-
GeneralName::URI(uri) => Some(vm.new_tuple(("URI", *uri)).into()),
249-
GeneralName::DirectoryName(dn) => {
250-
let dn_str = format!("{dn}");
251-
Some(vm.new_tuple(("DirName", dn_str)).into())
252-
}
253-
GeneralName::RegisteredID(oid) => {
254-
let oid_str = oid.to_string();
255-
Some(vm.new_tuple(("Registered ID", oid_str)).into())
256-
}
257-
GeneralName::OtherName(oid, value) => {
258-
let oid_str = oid.to_string();
259-
let value_str = format!("{value:?}");
260-
Some(
261-
vm.new_tuple(("othername", format!("{oid_str}:{value_str}")))
262-
.into(),
263-
)
264-
}
265-
_ => None,
266-
})
267-
.collect()
268-
}
269-
270231
// Certificate Validation and Parsing
271232

272233
/// Check if a certificate is a CA certificate by examining the Basic Constraints extension
@@ -294,87 +255,10 @@ pub(super) fn is_ca_certificate(cert_der: &[u8]) -> bool {
294255
cert.version().0 == 0 && cert.subject() == cert.issuer()
295256
}
296257

297-
/// Convert an X509Name to Python nested tuple format for SSL certificate dicts
298-
///
299-
/// Format: ((('CN', 'example.com'),), (('O', 'Example Org'),), ...)
300-
fn name_to_py(vm: &VirtualMachine, name: &x509_parser::x509::X509Name<'_>) -> PyObjectRef {
301-
let list = name
302-
.iter()
303-
.flat_map(|rdn| {
304-
// Each RDN can have multiple attributes
305-
rdn.iter()
306-
.map(|attr| {
307-
let oid_str = attr.attr_type().to_id_string();
308-
let value_str = attr.attr_value().as_str().unwrap_or("").to_string();
309-
let key = oid_to_attribute_name(&oid_str);
310-
311-
vm.new_tuple((vm.new_tuple((vm.ctx.new_str(key), vm.ctx.new_str(value_str))),))
312-
.into()
313-
})
314-
.collect::<Vec<_>>()
315-
})
316-
.collect::<Vec<PyObjectRef>>();
317-
318-
vm.ctx.new_tuple(list).into()
319-
}
320-
321-
/// Convert DER-encoded certificate to Python dict (for getpeercert with binary_form=False)
322-
///
323-
/// Returns a dict with fields: subject, issuer, version, serialNumber,
324-
/// notBefore, notAfter, subjectAltName (if present)
325-
pub(super) fn cert_to_dict(
326-
vm: &VirtualMachine,
327-
cert: &x509_parser::certificate::X509Certificate<'_>,
328-
) -> PyResult {
329-
let dict = vm.ctx.new_dict();
330-
331-
// Subject and Issuer
332-
dict.set_item("subject", name_to_py(vm, cert.subject()), vm)?;
333-
dict.set_item("issuer", name_to_py(vm, cert.issuer()), vm)?;
334-
335-
// Version (X.509 v3 = version 2 in the cert, but Python uses 3)
336-
dict.set_item(
337-
"version",
338-
vm.ctx.new_int(cert.version().0 as i32 + 1).into(),
339-
vm,
340-
)?;
341-
342-
// Serial number - hex format with even length
343-
let serial = format_serial_number(&cert.serial);
344-
dict.set_item("serialNumber", vm.ctx.new_str(serial).into(), vm)?;
345-
346-
// Validity dates - format with GMT using jiff
347-
dict.set_item(
348-
"notBefore",
349-
vm.ctx
350-
.new_str(format_asn1_time(&cert.validity().not_before))
351-
.into(),
352-
vm,
353-
)?;
354-
dict.set_item(
355-
"notAfter",
356-
vm.ctx
357-
.new_str(format_asn1_time(&cert.validity().not_after))
358-
.into(),
359-
vm,
360-
)?;
361-
362-
// Subject Alternative Names (if present)
363-
if let Ok(Some(san_ext)) = cert.subject_alternative_name() {
364-
let san_list = process_san_general_names(vm, &san_ext.value.general_names);
365-
366-
if !san_list.is_empty() {
367-
dict.set_item("subjectAltName", vm.ctx.new_tuple(san_list).into(), vm)?;
368-
}
369-
}
370-
371-
Ok(dict.into())
372-
}
373-
374-
/// Convert DER-encoded certificate to Python dict (for get_ca_certs)
258+
/// Convert DER-encoded certificate to Python dict.
375259
///
376-
/// Similar to cert_to_dict but includes additional fields like crlDistributionPoints
377-
/// and uses CPython's specific ordering: issuer, notAfter, notBefore, serialNumber, subject, version
260+
/// Includes OCSP, caIssuers, and crlDistributionPoints when present, and uses
261+
/// the field order: issuer, notAfter, notBefore, serialNumber, subject, version
378262
pub(super) fn cert_der_to_dict_helper(
379263
vm: &VirtualMachine,
380264
cert_der: &[u8],

‎extra_tests/snippets/stdlib_ssl.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pathlib
2+
import ssl
3+
4+
CERT = (
5+
pathlib.Path(__file__).resolve().parent.parent.parent
6+
/ "Lib"
7+
/ "test"
8+
/ "certdata"
9+
/ "keycert.pem"
10+
)
11+
12+
TAIL = b"tail"
13+
14+
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
15+
server_context.load_cert_chain(str(CERT))
16+
client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
17+
client_context.check_hostname = False
18+
client_context.verify_mode = ssl.CERT_NONE
19+
server_context.maximum_version = client_context.maximum_version = ssl.TLSVersion.TLSv1_2
20+
21+
client_in, client_out = ssl.MemoryBIO(), ssl.MemoryBIO()
22+
server_in, server_out = ssl.MemoryBIO(), ssl.MemoryBIO()
23+
client = client_context.wrap_bio(client_in, client_out)
24+
server = server_context.wrap_bio(server_in, server_out, server_side=True)
25+
26+
for _ in range(5):
27+
try:
28+
client.do_handshake()
29+
except ssl.SSLWantReadError:
30+
pass
31+
server_in.write(client_out.read())
32+
try:
33+
server.do_handshake()
34+
except ssl.SSLWantReadError:
35+
pass
36+
client_in.write(server_out.read())
37+
client.do_handshake()
38+
server.do_handshake()
39+
40+
try:
41+
server.unwrap()
42+
except ssl.SSLWantReadError:
43+
pass
44+
client_in.write(server_out.read() + TAIL)
45+
client.unwrap()
46+
assert client_in.read() == TAIL

0 commit comments

Comments
 (0)