◐ Shell
reader mode source ↗
Skip to content

PySSLCertificate#6219

Merged
youknowone merged 1 commit into
RustPython:mainfrom
youknowone:pycert
Oct 27, 2025
Merged

PySSLCertificate#6219
youknowone merged 1 commit into
RustPython:mainfrom
youknowone:pycert

Conversation

@youknowone

@youknowone youknowone commented Oct 27, 2025

Copy link
Copy Markdown
Member

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

coderabbitai Bot commented Oct 27, 2025

Copy link
Copy Markdown
Contributor

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hide comment

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hide comment

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)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hide comment

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hide comment

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.

Hide details View details @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