Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Oct 27, 2025

Summary by CodeRabbit

  • New Features

    • Added a Certificate type with public_bytes() and get_info() for viewing and exporting certificates.
    • Support for DER, PEM and auxiliary PEM encodings when exporting.
    • SSL peer certificate APIs now return rich Certificate objects instead of raw DER bytes.
    • Richer certificate parsing: subject, issuer, validity, serial, and subjectAltName details exposed.
  • Bug Fixes

    • Improved SSL error handling with explicit EOF detection and a dedicated EOF SSL error mapping.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

Walkthrough

Adds a new ssl_cert submodule exposing a Certificate PyO3 type and helpers, integrates it into the main _ssl module, replaces DER-byte returns with Certificate objects for peer/chain APIs, introduces encoding constants, and extends OpenSSL error mapping to include EOF/SSLEOFError.

Changes

Cohort / File(s) Summary
New Certificate Module
stdlib/src/ssl/cert.rs
Adds ssl_cert submodule with PySSLCertificate PyO3 class, public_bytes() and get_info() methods, helpers cert_to_certificate(), cert_to_py(), _test_decode_cert(), obj2txt(), and internal cert_to_dict() including subjectAltName parsing and UTF‑8‑tolerant encoding.
SSL Module Integration
stdlib/src/ssl.rs
Adds mod cert, re-exports PySSLCertificate, cert_to_certificate, cert_to_py, obj2txt; introduces ENCODING_PEM, ENCODING_DER, ENCODING_PEM_AUX constants; changes convert_openssl_error visibility to pub(crate) and maps EOF → SSLEOFError (OpenSSL 3.0 aware); updates peer/chain APIs to return Certificate objects instead of DER bytes.

Sequence Diagram(s)

sequenceDiagram
    participant Py as Python
    participant SSL as _ssl (ssl.rs)
    participant Cert as ssl_cert (cert.rs)
    participant OpenSSL as OpenSSL

    Py->>SSL: getpeercert / get_verified_chain / get_unverified_chain
    SSL->>Cert: cert_to_certificate(vm, x509)
    Cert->>OpenSSL: inspect/encode X509
    OpenSSL-->>Cert: X509 data / bytes
    Cert->>Cert: cert_to_dict() or build PySSLCertificate
    Cert-->>SSL: PySSLCertificate object(s)
    SSL-->>Py: return Certificate object(s)

    alt request raw bytes via public_bytes
        Py->>Cert: PySSLCertificate.public_bytes(format)
        Cert->>OpenSSL: encode (DER/PEM)
        OpenSSL-->>Cert: encoded bytes
        Cert-->>Py: bytes
    end

    alt OpenSSL error occurs
        OpenSSL-->>SSL: ErrorStack / error
        SSL->>SSL: convert_openssl_error(vm, err) (maps EOF→SSLEOFError when applicable)
        SSL-->>Py: raise mapped exception
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • cert_to_dict() subject/issuer and subjectAltName parsing and edge-case formatting (IPv6, DirName, other types).
    • FFI/OpenSSL error conversion and EOF → SSLEOFError mapping across OpenSSL versions.
    • PyO3 exposure and visibility (pub(crate) vs exported names) and places in _ssl that now return PySSLCertificate.

Possibly related PRs

Suggested reviewers

  • coolreader18
  • ShaharNaveh

Poem

🐰 A rabbit hops through code with glee,
Certificates grown from X509 tree,
PEM or DER, a gentle byte,
Subjects and SANs now shining bright,
Hop — secure connections take flight! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "PySSLCertificate" directly references the central new type being introduced in this changeset. The PR's primary objective is to add a new certificate module with the PySSLCertificate struct, along with supporting helper functions and methods for certificate handling in SSL operations. The title is specific, clear, and not vague—it names the key component being introduced rather than using generic terms. While the title is minimal and doesn't explicitly describe an action (like "Add" or "Implement"), it effectively highlights the most important change from the developer's perspective by naming the newly exposed public type.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@youknowone youknowone marked this pull request as ready for review October 27, 2025 10:27
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
stdlib/src/ssl.rs (2)

75-82: Unused import of PySSLCertificate in _ssl

This re-import is not needed for exposure; ssl_cert already registers the class via #[pyclass]. Keeping it requires allow(unused_imports). Remove for cleanliness.

-    // Re-export PySSLCertificate to make it available in the _ssl module
-    // It will be automatically exposed to Python via #[pyclass]
-    #[allow(unused_imports)]
-    use super::cert::PySSLCertificate;
+    // PySSLCertificate is registered by cert::ssl_cert; no import needed here.

188-195: Expose and support ENCODING_PEM_AUX consistently

ENCODING_PEM_AUX exists here but is private and unused. To match CPython’s accepted encodings and to let Certificate.public_bytes() accept PEM_AUX, make it pub(crate) and handle it in cert.rs as an alias of PEM.

-    #[pyattr]
-    const ENCODING_PEM_AUX: i32 = sys::X509_FILETYPE_PEM + 0x100;
+    #[pyattr]
+    pub(crate) const ENCODING_PEM_AUX: i32 = sys::X509_FILETYPE_PEM + 0x100;

Follow-up diff for cert.rs is included in that file’s comment.

stdlib/src/ssl/cert.rs (2)

24-24: Import ENCODING_PEM_AUX and accept it in public_bytes()

You added ENCODING_PEM_AUX in _ssl; mirror CPython by treating it like PEM. Also make the constant pub(crate) in ssl.rs.

-    use crate::ssl::_ssl::{ENCODING_DER, ENCODING_PEM, convert_openssl_error};
+    use crate::ssl::_ssl::{ENCODING_DER, ENCODING_PEM, ENCODING_PEM_AUX, convert_openssl_error};

And in public_bytes:

-            match format {
+            match format {
                 x if x == ENCODING_DER => {
                     // DER encoding
                     let der = self
                         .cert
                         .to_der()
                         .map_err(|e| convert_openssl_error(vm, e))?;
                     Ok(vm.ctx.new_bytes(der).into())
                 }
-                x if x == ENCODING_PEM => {
+                x if x == ENCODING_PEM || x == ENCODING_PEM_AUX => {
                     // PEM encoding
                     let pem = self
                         .cert
                         .to_pem()
                         .map_err(|e| convert_openssl_error(vm, e))?;
                     Ok(vm.ctx.new_bytes(pem).into())
                 }
                 _ => Err(vm.new_value_error("Unsupported format".to_owned())),
             }

157-187: Prefer std::net for SAN IP formatting

Manual IPv6 formatting is non‑canonical (uppercase, no compression). Use Ipv4Addr/Ipv6Addr for correct presentation.

-                    } else if let Some(ip) = gen_name.ipaddress() {
-                        // Parse IP address properly (IPv4 or IPv6)
-                        let ip_str = if ip.len() == 4 {
-                            // IPv4
-                            format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
-                        } else if ip.len() == 16 {
-                            // IPv6 - format like: "X:X:X:X:X:X:X:X" (not compressed)
-                            format!(
-                                "{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}",
-                                (ip[0] as u16) << 8 | ip[1] as u16,
-                                (ip[2] as u16) << 8 | ip[3] as u16,
-                                (ip[4] as u16) << 8 | ip[5] as u16,
-                                (ip[6] as u16) << 8 | ip[7] as u16,
-                                (ip[8] as u16) << 8 | ip[9] as u16,
-                                (ip[10] as u16) << 8 | ip[11] as u16,
-                                (ip[12] as u16) << 8 | ip[13] as u16,
-                                (ip[14] as u16) << 8 | ip[15] as u16
-                            )
-                        } else {
-                            // Fallback for unexpected length
-                            String::from_utf8_lossy(ip).into_owned()
-                        };
-                        vm.new_tuple((ascii!("IP Address"), ip_str)).into()
+                    } else if let Some(ip) = gen_name.ipaddress() {
+                        use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+                        let ip_str = match ip.len() {
+                            4 => IpAddr::V4(Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3])).to_string(),
+                            16 => {
+                                let mut seg = [0u16; 8];
+                                for i in 0..8 { seg[i] = u16::from_be_bytes([ip[2*i], ip[2*i+1]]); }
+                                IpAddr::V6(Ipv6Addr::from(seg)).to_string()
+                            }
+                            _ => String::from_utf8_lossy(ip).into_owned(),
+                        };
+                        vm.new_tuple((ascii!("IP Address"), ip_str)).into()
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 517b55b and 36475a3.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_urllib2_localnet.py is excluded by !Lib/**
📒 Files selected for processing (2)
  • stdlib/src/ssl.rs (11 hunks)
  • stdlib/src/ssl/cert.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Format Rust code with the default rustfmt style (run cargo fmt)
Run clippy and fix any warnings or lints introduced by your changes
Follow Rust best practices for error handling and memory management

Files:

  • stdlib/src/ssl.rs
  • stdlib/src/ssl/cert.rs
{vm,stdlib}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use RustPython macros (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/ssl.rs
  • stdlib/src/ssl/cert.rs
🧬 Code graph analysis (2)
stdlib/src/ssl.rs (3)
derive-impl/src/util.rs (1)
  • with (305-317)
stdlib/src/ssl/cert.rs (3)
  • cert_to_certificate (243-245)
  • cert_to_py (248-255)
  • obj2txt (26-51)
vm/src/vm/mod.rs (1)
  • class (568-577)
stdlib/src/ssl/cert.rs (1)
stdlib/src/ssl.rs (1)
  • convert_openssl_error (1986-2040)
🔇 Additional comments (4)
stdlib/src/ssl.rs (1)

379-384: Good: centralize OID text conversion

Switching to cert::obj2txt avoids duplication and keeps ASN.1 OID formatting consistent across the module.

stdlib/src/ssl/cert.rs (3)

26-51: OBJ_obj2txt wrapper looks good and safe

Length probing, allocation, and set_len are in the correct order. UTF‑8 fallback is robust.


66-95: public_bytes(): default and error mapping look good

Defaulting to PEM matches CPython. Error conversion is consistent.


242-255: cert_to_certificate/cert_to_py: API surface looks good

Returning PySSLCertificate for owned X509 and bytes/dict for getpeercert(binary=…) matches expectations. Error conversion via convert_openssl_error is consistent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
stdlib/src/ssl.rs (2)

1220-1231: Propagate certificate conversion errors instead of silently dropping certificates.

The code still swallows errors with .ok(), which can mask VM failures and yield incomplete chains. This matches the unresolved past review comment.

Apply this diff to propagate errors:

-        fn get_unverified_chain(&self, vm: &VirtualMachine) -> Option<PyObjectRef> {
+        fn get_unverified_chain(&self, vm: &VirtualMachine) -> PyResult<Option<PyObjectRef>> {
             let stream = self.stream.read();
             let chain = stream.ssl().peer_cert_chain()?;
 
-            // Return Certificate objects
-            let certs: Vec<PyObjectRef> = chain
-                .iter()
-                .filter_map(|cert| {
-                    // Clone the X509 certificate to create an owned copy
-                    unsafe {
-                        sys::X509_up_ref(cert.as_ptr());
-                        let owned_cert = X509::from_ptr(cert.as_ptr());
-                        cert_to_certificate(vm, owned_cert).ok()
-                    }
-                })
-                .collect();
-
-            Some(vm.ctx.new_list(certs).into())
+            let certs: Vec<PyObjectRef> = chain
+                .iter()
+                .map(|cert| unsafe {
+                    sys::X509_up_ref(cert.as_ptr());
+                    let owned_cert = X509::from_ptr(cert.as_ptr());
+                    cert_to_certificate(vm, owned_cert)
+                })
+                .collect::<PyResult<_>>()?;
+            Ok(Some(vm.ctx.new_list(certs).into()))
         }

1248-1259: Propagate certificate conversion errors in verified chain construction.

The code still swallows errors with if let Ok(), which can silently drop certificates. This matches the unresolved past review comment.

Apply this diff to propagate errors:

-        fn get_verified_chain(&self, vm: &VirtualMachine) -> Option<PyListRef> {
+        fn get_verified_chain(&self, vm: &VirtualMachine) -> PyResult<Option<PyListRef>> {
             let stream = self.stream.read();
             unsafe {
                 let chain = sys::SSL_get0_verified_chain(stream.ssl().as_ptr());
                 if chain.is_null() {
-                    return None;
+                    return Ok(None);
                 }
 
                 let num_certs = sys::OPENSSL_sk_num(chain as *const _);
-                let mut certs = Vec::new();
+                let mut certs: Vec<PyObjectRef> = Vec::with_capacity(num_certs as usize);
 
-                // Return Certificate objects
                 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;
                     }
-                    // Clone the X509 certificate to create an owned copy
                     sys::X509_up_ref(cert_ptr);
                     let owned_cert = X509::from_ptr(cert_ptr);
-                    if let Ok(cert_obj) = cert_to_certificate(vm, owned_cert) {
-                        certs.push(cert_obj);
-                    }
+                    let cert_obj = cert_to_certificate(vm, owned_cert)?;
+                    certs.push(cert_obj);
                 }
 
                 if certs.is_empty() {
-                    None
+                    Ok(None)
                 } else {
-                    Some(vm.ctx.new_list(certs))
+                    Ok(Some(vm.ctx.new_list(certs)))
                 }
             }
         }
🧹 Nitpick comments (3)
stdlib/src/ssl.rs (1)

2073-2102: Replace magic numbers with named constants and use consistent exception class access.

The code uses magic numbers 20 and 294 for OpenSSL error codes, and inconsistently retrieves the SSLEOFError class via vm.class("_ssl", "SSLEOFError") instead of PySslEOFError::class(&vm.ctx).

Define constants for the error codes and use the consistent exception class pattern:

+    // OpenSSL 3.0 error codes
+    const ERR_LIB_SSL: u32 = 20;
+    const SSL_R_UNEXPECTED_EOF_WHILE_READING: u32 = 294;
+
     #[track_caller]
     fn convert_ssl_error(
         vm: &VirtualMachine,
         e: impl std::borrow::Borrow<ssl::Error>,
     ) -> PyBaseExceptionRef {
         let e = e.borrow();
         let (cls, msg) = match e.code() {
             // ... existing code ...
             ssl::ErrorCode::SSL => {
-                // Check for OpenSSL 3.0 SSL_R_UNEXPECTED_EOF_WHILE_READING
                 if let Some(ssl_err) = e.ssl_error() {
-                    // SSL_R_UNEXPECTED_EOF_WHILE_READING = 294 (0x126)
-                    // In OpenSSL 3.0+, unexpected EOF is reported as SSL_ERROR_SSL
-                    // with this specific reason code instead of SSL_ERROR_SYSCALL
                     unsafe {
                         let err_code = sys::ERR_peek_last_error();
                         let reason = sys::ERR_GET_REASON(err_code);
                         let lib = sys::ERR_GET_LIB(err_code);
-                        // ERR_LIB_SSL = 20, SSL_R_UNEXPECTED_EOF_WHILE_READING = 294
-                        if lib == 20 && reason == 294 {
+                        if lib == ERR_LIB_SSL && reason == SSL_R_UNEXPECTED_EOF_WHILE_READING {
                             return vm.new_exception(
-                                vm.class("_ssl", "SSLEOFError"),
+                                PySslEOFError::class(&vm.ctx).to_owned(),
                                 vec![
                                     vm.ctx.new_int(SSL_ERROR_EOF).into(),
                                     vm.ctx
                                         .new_str("EOF occurred in violation of protocol")
                                         .into(),
                                 ],
                             );
                         }
                     }
                     return convert_openssl_error(vm, ssl_err.clone());
                 }
stdlib/src/ssl/cert.rs (2)

68-95: Consider handling ENCODING_PEM_AUX or documenting why it's unsupported.

The parent module exposes ENCODING_PEM_AUX as a constant, but this method doesn't handle it. Either implement PEM_AUX encoding support or document the limitation.

If PEM_AUX support is intended:

                 x if x == ENCODING_PEM => {
                     // PEM encoding
                     let pem = self
                         .cert
                         .to_pem()
                         .map_err(|e| convert_openssl_error(vm, e))?;
                     Ok(vm.ctx.new_bytes(pem).into())
                 }
+                x if x == super::_ssl::ENCODING_PEM_AUX => {
+                    // PEM encoding with auxiliary info
+                    // Note: X509::to_pem() doesn't support AUX format
+                    // Would need to use d2i_X509_AUX_bio or similar
+                    Err(vm.new_not_implemented_error(
+                        "ENCODING_PEM_AUX is not yet supported".to_owned()
+                    ))
+                }
                 _ => Err(vm.new_value_error("Unsupported format".to_owned())),

167-186: Consider using std::net::Ipv6Addr for simpler IPv6 formatting.

The manual bit-shifting works but is verbose. Using the standard library could improve readability.

                         let ip_str = if ip.len() == 4 {
                             // IPv4
                             format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
                         } else if ip.len() == 16 {
-                            // IPv6 - format like: "X:X:X:X:X:X:X:X" (not compressed)
-                            format!(
-                                "{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}",
-                                (ip[0] as u16) << 8 | ip[1] as u16,
-                                (ip[2] as u16) << 8 | ip[3] as u16,
-                                (ip[4] as u16) << 8 | ip[5] as u16,
-                                (ip[6] as u16) << 8 | ip[7] as u16,
-                                (ip[8] as u16) << 8 | ip[9] as u16,
-                                (ip[10] as u16) << 8 | ip[11] as u16,
-                                (ip[12] as u16) << 8 | ip[13] as u16,
-                                (ip[14] as u16) << 8 | ip[15] as u16
-                            )
+                            // IPv6
+                            let octets: [u8; 16] = ip.try_into().expect("length checked");
+                            std::net::Ipv6Addr::from(octets).to_string()
                         } else {
                             // Fallback for unexpected length
                             String::from_utf8_lossy(ip).into_owned()
                         };

Note: If CPython compatibility requires uppercase hex without compression, keep the current implementation.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36475a3 and 36fcec0.

📒 Files selected for processing (2)
  • stdlib/src/ssl.rs (11 hunks)
  • stdlib/src/ssl/cert.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Format Rust code with the default rustfmt style (run cargo fmt)
Run clippy and fix any warnings or lints introduced by your changes
Follow Rust best practices for error handling and memory management

Files:

  • stdlib/src/ssl.rs
  • stdlib/src/ssl/cert.rs
{vm,stdlib}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use RustPython macros (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/ssl.rs
  • stdlib/src/ssl/cert.rs
🧬 Code graph analysis (2)
stdlib/src/ssl.rs (3)
derive-impl/src/util.rs (1)
  • with (305-317)
stdlib/src/ssl/cert.rs (3)
  • cert_to_certificate (243-245)
  • cert_to_py (248-255)
  • obj2txt (26-51)
vm/src/vm/mod.rs (1)
  • class (568-577)
stdlib/src/ssl/cert.rs (1)
stdlib/src/ssl.rs (1)
  • convert_openssl_error (1986-2040)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Check Rust code with rustfmt and clippy
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Run rust tests (windows-latest)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
stdlib/src/ssl/cert.rs (1)

107-128: Extract duplicate name_to_py closure to reduce code duplication.

The name_to_py closure is defined identically at lines 107-128 and 195-218, violating DRY principles.

Also applies to: 195-218

stdlib/src/ssl.rs (1)

1220-1234: Propagate errors instead of swallowing them with .ok().

Errors from cert_to_certificate are silently dropped with .ok(), which can mask allocation or VM failures and yield truncated chains. This was flagged in a previous review but not yet addressed.

Change the function signature to return PyResult<Option<PyObjectRef>> and propagate errors:

-        fn get_unverified_chain(&self, vm: &VirtualMachine) -> Option<PyObjectRef> {
+        fn get_unverified_chain(&self, vm: &VirtualMachine) -> PyResult<Option<PyObjectRef>> {
             let stream = self.stream.read();
             let chain = stream.ssl().peer_cert_chain()?;
 
-            // Return Certificate objects
-            let certs: Vec<PyObjectRef> = chain
-                .iter()
-                .filter_map(|cert| {
-                    // Clone the X509 certificate to create an owned copy
-                    unsafe {
-                        sys::X509_up_ref(cert.as_ptr());
-                        let owned_cert = X509::from_ptr(cert.as_ptr());
-                        cert_to_certificate(vm, owned_cert).ok()
-                    }
-                })
-                .collect();
-
-            Some(vm.ctx.new_list(certs).into())
+            let certs: Vec<PyObjectRef> = chain
+                .iter()
+                .map(|cert| unsafe {
+                    sys::X509_up_ref(cert.as_ptr());
+                    let owned = X509::from_ptr(cert.as_ptr());
+                    cert_to_certificate(vm, owned)
+                })
+                .collect::<PyResult<_>>()?;
+            Ok(Some(vm.ctx.new_list(certs).into()))
         }
🧹 Nitpick comments (1)
stdlib/src/ssl.rs (1)

2072-2101: Consider defining constants for OpenSSL error codes.

The magic numbers 20 (ERR_LIB_SSL) and 294 (SSL_R_UNEXPECTED_EOF_WHILE_READING) are hardcoded but well-documented in comments. For better maintainability, consider defining them as constants.

Example:

const ERR_LIB_SSL: u32 = 20;
const SSL_R_UNEXPECTED_EOF_WHILE_READING: u32 = 294;

// Then use in the condition:
if lib == ERR_LIB_SSL && reason == SSL_R_UNEXPECTED_EOF_WHILE_READING {
    // ...
}
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36fcec0 and 4085720.

📒 Files selected for processing (2)
  • stdlib/src/ssl.rs (10 hunks)
  • stdlib/src/ssl/cert.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Format Rust code with the default rustfmt style (run cargo fmt)
Run clippy and fix any warnings or lints introduced by your changes
Follow Rust best practices for error handling and memory management

Files:

  • stdlib/src/ssl/cert.rs
  • stdlib/src/ssl.rs
{vm,stdlib}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use RustPython macros (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/ssl/cert.rs
  • stdlib/src/ssl.rs
🧬 Code graph analysis (2)
stdlib/src/ssl/cert.rs (1)
stdlib/src/ssl.rs (1)
  • convert_openssl_error (1985-2039)
stdlib/src/ssl.rs (3)
derive-impl/src/util.rs (1)
  • with (305-317)
stdlib/src/ssl/cert.rs (3)
  • cert_to_certificate (243-245)
  • cert_to_py (248-255)
  • obj2txt (26-51)
vm/src/vm/mod.rs (1)
  • class (568-577)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Check Rust code with rustfmt and clippy

@youknowone youknowone force-pushed the pycert branch 2 times, most recently from 0c957e7 to 8153d55 Compare October 27, 2025 12:13
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
stdlib/src/ssl/cert.rs (1)

26-51: Consider defensive error handling over assertions.

The function uses assert! for OpenSSL error conditions (lines 31, 43). While OpenSSL should not return errors for these calls in normal operation, replacing assertions with proper error returns would be more defensive and prevent potential panics.

Consider returning Result<String, ErrorStack> and propagating errors:

pub(crate) fn obj2txt(obj: &Asn1ObjectRef, no_name: bool) -> Result<String, ErrorStack> {
    let no_name = i32::from(no_name);
    let ptr = obj.as_ptr();
    let b = unsafe {
        let buflen = sys::OBJ_obj2txt(std::ptr::null_mut(), 0, ptr, no_name);
        if buflen <= 0 {
            return Err(ErrorStack::get());
        }
        // ... rest of implementation
    };
    // ...
}
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4085720 and a79cee3.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_urllib2_localnet.py is excluded by !Lib/**
📒 Files selected for processing (2)
  • stdlib/src/ssl.rs (10 hunks)
  • stdlib/src/ssl/cert.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Format Rust code with the default rustfmt style (run cargo fmt)
Run clippy and fix any warnings or lints introduced by your changes
Follow Rust best practices for error handling and memory management

Files:

  • stdlib/src/ssl/cert.rs
  • stdlib/src/ssl.rs
{vm,stdlib}/**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use RustPython macros (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/ssl/cert.rs
  • stdlib/src/ssl.rs
🧬 Code graph analysis (2)
stdlib/src/ssl/cert.rs (1)
stdlib/src/ssl.rs (1)
  • convert_openssl_error (1987-2041)
stdlib/src/ssl.rs (2)
stdlib/src/ssl/cert.rs (3)
  • cert_to_certificate (213-215)
  • cert_to_py (218-225)
  • obj2txt (26-51)
vm/src/vm/mod.rs (1)
  • class (568-577)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Check Rust code with rustfmt and clippy
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run rust tests (ubuntu-latest)
🔇 Additional comments (10)
stdlib/src/ssl.rs (6)

3-4: LGTM: Clean module declaration.

The mod cert; declaration properly introduces the new certificate handling submodule.


31-31: LGTM: Proper module integration.

The cert::ssl_cert submodule is correctly wired into the _ssl module, and the imports/re-exports properly expose PySSLCertificate to Python. The past submodule annotation issue has been addressed.

Also applies to: 75-81


383-388: LGTM: Clean delegation to cert module.

The refactoring properly delegates OID-to-text conversion to cert::obj2txt, avoiding code duplication.


1220-1236: LGTM: Proper error propagation and Certificate object construction.

The updated get_unverified_chain correctly:

  • Propagates errors instead of silently dropping failed conversions (past issue resolved)
  • Returns Certificate objects via cert_to_certificate
  • Safely manages X509 reference counting with X509_up_ref

1239-1268: LGTM: Proper error propagation in verified chain construction.

The get_verified_chain now correctly propagates conversion errors instead of silently dropping certificates. The continue on line 1254 for null pointers is appropriate since it doesn't mask conversion failures.


1987-1990: LGTM: Appropriate visibility change.

Making convert_openssl_error pub(crate) allows the cert module to properly convert OpenSSL errors while maintaining encapsulation.

stdlib/src/ssl/cert.rs (4)

1-6: LGTM: Proper submodule structure.

The module exports and #[pymodule(sub)] annotation correctly define this as a submodule of _ssl, addressing the past review concern.


103-120: LGTM: Clean extraction addresses past duplication.

The name_to_py function properly eliminates the duplicate code identified in previous reviews, with appropriate UTF-8 handling.


227-233: Clarify purpose of _test_decode_cert.

The _test_decode_cert function is exposed as a Python function but lacks documentation. The leading underscore suggests internal use, but it's unclear if this is meant for:

  1. Internal testing only
  2. Public API for certificate debugging
  3. Temporary scaffolding to be removed

Consider either:

  • Adding a docstring explaining its purpose and usage
  • Removing it if it was only for development
  • Moving it behind a test-only feature flag if it's for testing
/// Test helper to decode a certificate file and return its dictionary representation.
/// For internal testing only - not part of the stable API.
#[pyfunction]
pub(crate) fn _test_decode_cert(path: FsPath, vm: &VirtualMachine) -> PyResult {

68-95: Review comment is incorrect — ENCODING_PEM_AUX is not exposed to Python.

ENCODING_PEM_AUX is defined as a private const (not pub and without #[pyattr]), so it is not accessible to Python users. The public_bytes method correctly handles only the exposed formats: ENCODING_DER and ENCODING_PEM. Additionally, CPython's ssl.Certificate.public_bytes does not support PEM_AUX, so there is no requirement to implement this format.

Likely an incorrect or invalid review comment.

Comment on lines +188 to +198
// Encoding constants for Certificate.public_bytes()
#[pyattr]
pub(crate) const ENCODING_PEM: i32 = sys::X509_FILETYPE_PEM;
#[pyattr]
pub(crate) const ENCODING_DER: i32 = sys::X509_FILETYPE_ASN1;
#[pyattr]
const ENCODING_PEM_AUX: i32 = sys::X509_FILETYPE_PEM + 0x100;

// OpenSSL error codes for unexpected EOF detection
const ERR_LIB_SSL: i32 = 20;
const SSL_R_UNEXPECTED_EOF_WHILE_READING: i32 = 294;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Clarify ENCODING_PEM_AUX usage and magic number.

The constant ENCODING_PEM_AUX uses a magic number (0x100) added to X509_FILETYPE_PEM, but it's unclear if this constant is actually used anywhere in the codebase. Additionally, the hardcoded OpenSSL error codes (ERR_LIB_SSL = 20, SSL_R_UNEXPECTED_EOF_WHILE_READING = 294) could become outdated across OpenSSL versions.

Run the following to check if ENCODING_PEM_AUX is used:

Consider adding a comment explaining the 0x100 offset for ENCODING_PEM_AUX, or removing it if unused. For the error codes, consider referencing the OpenSSL version these codes correspond to in a comment.


🏁 Script executed:

#!/bin/bash
# Search for ENCODING_PEM_AUX usage
rg -n 'ENCODING_PEM_AUX' --type rust

Length of output: 87



---

🏁 Script executed:

```shell
#!/bin/bash
# Check for broader usage and context of these constants
# Look for ERR_LIB_SSL and SSL_R_UNEXPECTED_EOF_WHILE_READING usage
rg -n 'ERR_LIB_SSL|SSL_R_UNEXPECTED_EOF_WHILE_READING' --type rust -C 3

Length of output: 1583


🏁 Script executed:

#!/bin/bash
# Search for X509_FILETYPE patterns to understand the constants better
rg -n 'X509_FILETYPE|ENCODING_PEM|ENCODING_DER' --type rust -C 2

Length of output: 1707


🏁 Script executed:

#!/bin/bash
# Check if there are tests or examples that use these constants
fd -e rs -e toml -e md | xargs grep -l 'ENCODING_PEM_AUX\|ssl.*encoding' 2>/dev/null || true

Length of output: 17



---

🏁 Script executed:

```shell
#!/bin/bash
# Check OpenSSL documentation or sys module for X509_FILETYPE_PEM_AUX reference
rg -n 'X509_FILETYPE_PEM_AUX|0x100' --type rust stdlib/src/ssl.rs

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Look for any comments or documentation about the 0x100 offset
sed -n '188,200p' stdlib/src/ssl.rs

Length of output: 491


Add documentation for ENCODING_PEM_AUX and the 0x100 offset magic number.

ENCODING_PEM_AUX is exposed to Python via #[pyattr] but unused in Rust code; clarify whether this is intentional public API or dead code. Additionally, add a comment explaining the 0x100 offset, which appears to follow OpenSSL's own X509_FILETYPE_PEM_AUX pattern but lacks documentation.

The hardcoded error codes ERR_LIB_SSL and SSL_R_UNEXPECTED_EOF_WHILE_READING are actively used in the OpenSSL 3.0+ error detection logic at line 2083 and do not present a versioning concern in this context.

🤖 Prompt for AI Agents
In stdlib/src/ssl.rs around lines 188 to 198, ENCODING_PEM_AUX is exposed to
Python but unused in Rust and the 0x100 magic offset lacks explanation; update
the source by adding a short doc comment above ENCODING_PEM_AUX stating that
this attribute is intentionally exported for compatibility with OpenSSL's
X509_FILETYPE_PEM_AUX (kept as part of the public Python API), and explain that
the 0x100 value is the OpenSSL-defined offset used to distinguish PEM_AUX from
PEM; also add a brief comment near ERR_LIB_SSL and
SSL_R_UNEXPECTED_EOF_WHILE_READING noting these are OpenSSL numeric error codes
relied on for EOF detection so they are intentionally hardcoded and should be
kept if used elsewhere.

Comment on lines +2074 to +2101
ssl::ErrorCode::SSL => {
// Check for OpenSSL 3.0 SSL_R_UNEXPECTED_EOF_WHILE_READING
if let Some(ssl_err) = e.ssl_error() {
// In OpenSSL 3.0+, unexpected EOF is reported as SSL_ERROR_SSL
// with this specific reason code instead of SSL_ERROR_SYSCALL
unsafe {
let err_code = sys::ERR_peek_last_error();
let reason = sys::ERR_GET_REASON(err_code);
let lib = sys::ERR_GET_LIB(err_code);
if lib == ERR_LIB_SSL && reason == SSL_R_UNEXPECTED_EOF_WHILE_READING {
return vm.new_exception(
vm.class("_ssl", "SSLEOFError"),
vec![
vm.ctx.new_int(SSL_ERROR_EOF).into(),
vm.ctx
.new_str("EOF occurred in violation of protocol")
.into(),
],
);
}
}
return convert_openssl_error(vm, ssl_err.clone());
}
(
PySslError::class(&vm.ctx).to_owned(),
"A failure in the SSL library occurred",
),
},
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use PySslEOFError::class for consistency.

The SSL_ERROR_SSL handler uses vm.class("_ssl", "SSLEOFError") at line 2085, which performs a runtime module lookup. This is inconsistent with the SSL_ERROR_SYSCALL handler (line 2064) which uses PySslEOFError::class(&vm.ctx).to_owned(). Runtime lookups are more fragile and could fail if the module isn't fully initialized.

Apply this diff for consistency:

                         if lib == ERR_LIB_SSL && reason == SSL_R_UNEXPECTED_EOF_WHILE_READING {
                             return vm.new_exception(
-                                vm.class("_ssl", "SSLEOFError"),
+                                PySslEOFError::class(&vm.ctx).to_owned(),
                                 vec![
                                     vm.ctx.new_int(SSL_ERROR_EOF).into(),
                                     vm.ctx
🤖 Prompt for AI Agents
In stdlib/src/ssl.rs around lines 2074 to 2101, replace the runtime module
lookup vm.class("_ssl", "SSLEOFError") with the compile-time reference
PySslEOFError::class(&vm.ctx).to_owned() so the EOF error construction uses the
same PySslEOFError::class(&vm.ctx).to_owned() pattern as the SSL_ERROR_SYSCALL
branch; update the returned vm.new_exception call to pass that class reference
instead of the vm.class(...) lookup.

Comment on lines +163 to +182
let ip_str = if ip.len() == 4 {
// IPv4
format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
} else if ip.len() == 16 {
// IPv6 - format like: "X:X:X:X:X:X:X:X" (not compressed)
format!(
"{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}",
(ip[0] as u16) << 8 | ip[1] as u16,
(ip[2] as u16) << 8 | ip[3] as u16,
(ip[4] as u16) << 8 | ip[5] as u16,
(ip[6] as u16) << 8 | ip[7] as u16,
(ip[8] as u16) << 8 | ip[9] as u16,
(ip[10] as u16) << 8 | ip[11] as u16,
(ip[12] as u16) << 8 | ip[13] as u16,
(ip[14] as u16) << 8 | ip[15] as u16
)
} else {
// Fallback for unexpected length
String::from_utf8_lossy(ip).into_owned()
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify IPv6 format matches CPython behavior.

The IPv6 formatting (lines 167-178) explicitly uses non-compressed format, which differs from the canonical IPv6 representation that uses :: for zero compression. CPython might use compressed format for consistency with standard IPv6 notation.

If CPython uses compression, consider using Rust's standard library:

else if ip.len() == 16 {
    // IPv6 - use standard parsing for canonical format
    let ip_addr = std::net::Ipv6Addr::from([
        ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7],
        ip[8], ip[9], ip[10], ip[11], ip[12], ip[13], ip[14], ip[15]
    ]);
    ip_addr.to_string()
}

🌐 Web query:

Does CPython's ssl.Certificate represent IPv6 addresses in subjectAltName with zero compression (::)?

💡 Result:

Short answer: Yes. CPython decodes iPAddress SANs from their 16‑byte/4‑byte octet form and emits the usual textual IP form (IPv6 using the system/OpenSSL inet_ntop/ipaddress canonical compression, e.g. :: for zero runs). See the ssl docs noting IPv6 address strings and the RFC describing iPAddress storage in SAN as octet strings. [1][2]

Sources

  • Python ssl docs (getpeercert / subjectAltName; note about IPv6 address strings). [1]
  • RFC (iPAddress in subjectAltName stored as octet string; IPv6 is 16 octets). [2]

Fix IPv6 format to use canonical compression matching CPython behavior.

IPv6 addresses in subjectAltName should use compressed format (e.g., :: for zero runs), as CPython does via OpenSSL's inet_ntop. The current code at lines 167-178 formats all 8 segments without compression, diverging from the canonical representation.

Replace with:

else if ip.len() == 16 {
    // IPv6 - use standard library for canonical compressed format
    let ip_addr = std::net::Ipv6Addr::from([
        ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7],
        ip[8], ip[9], ip[10], ip[11], ip[12], ip[13], ip[14], ip[15]
    ]);
    ip_addr.to_string()
}
🤖 Prompt for AI Agents
In stdlib/src/ssl/cert.rs around lines 163 to 182, the IPv6 branch currently
formats all eight 16-bit segments without using canonical compression, producing
non-standard representations; replace that branch to construct a
std::net::Ipv6Addr from the 16-byte slice and call to_string() so the standard
library produces the canonical (compressed) IPv6 textual form (e.g., "::" for
runs of zeros), keeping the IPv4 and fallback branches unchanged.

Comment on lines +186 to +203
} else {
// Handle DirName, Registered ID, and othername
// Check if this is a directory name
if let Some(dirname) = gen_name.directory_name()
&& let Ok(py_name) = name_to_py(vm, dirname)
{
return vm.new_tuple((ascii!("DirName"), py_name)).into();
}

// TODO: Handle Registered ID (GEN_RID)
// CPython implementation uses i2t_ASN1_OBJECT to convert OID
// This requires accessing GENERAL_NAME union which is complex in Rust
// For now, we return <unsupported> for unhandled types

// For othername and other unsupported types
vm.new_tuple((ascii!("othername"), ascii!("<unsupported>")))
.into()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify unsupported SAN type representation matches CPython.

The fallback for unsupported subjectAltName types returns ("othername", "<unsupported>"). The TODO comment (lines 195-198) indicates incomplete handling of Registered ID types. Verify that CPython uses the same representation for unsupported types.

If the behavior differs significantly, consider documenting the limitation or tracking it as a known issue.


🌐 Web query:

How does CPython's ssl module represent unsupported subjectAltName types like othername and Registered ID?

💡 Result:

Short answer: CPython's ssl module does not expose otherName or registeredID SANs in the high‑level getpeercert() dictionary — it only returns the common textual SAN types (DNS, IP Address, RFC822/email, URI, directoryName, etc.). Other/unsupported GeneralName choices are not converted into readable entries and are effectively omitted from the subjectAltName tuple returned by getpeercert(); match_hostname also only looks at DNS and IP Address entries. [1][2]

Sources:

  • ssl.getpeercert() / ssl module docs (examples of subjectAltName output). [1]
  • ssl.match_hostname implementation (only checks 'DNS' and 'IP Address' from subjectAltName). [2]

Remove or refactor unsupported SAN type handling to match CPython behavior.

CPython's ssl module does not expose otherName or registeredID SANs in the high-level getpeercert() dictionary — unsupported GeneralName types are omitted entirely from the subjectAltName tuple. The current code returns ("othername", "<unsupported>") for unsupported types (line 202), which diverges from CPython. Either omit unsupported types from the output entirely or confirm that exposing them with an <unsupported> marker is intentional and document this as a known deviation from CPython.

🤖 Prompt for AI Agents
In stdlib/src/ssl/cert.rs around lines 186 to 203, currently unsupported
GeneralName types fall through to returning vm.new_tuple(("othername",
"<unsupported>")) which diverges from CPython; change the logic to omit
unsupported SAN types instead of returning an "<unsupported>" tuple — i.e., when
encountering otherName, registeredID, or any unhandled GeneralName variant, do
not produce a tuple for it and continue processing the rest of names (or simply
skip/continue), ensuring subjectAltName contains only the supported entries; if
you intentionally want to expose unsupported types, add a clear comment and
documented flag, otherwise remove the "<unsupported>" return path so behavior
matches CPython.

@youknowone youknowone merged commit 9a2792a into RustPython:main Oct 27, 2025
12 checks passed
@youknowone youknowone deleted the pycert branch October 27, 2025 13:32
@coderabbitai coderabbitai bot mentioned this pull request Oct 29, 2025
This was referenced Dec 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant