-
Notifications
You must be signed in to change notification settings - Fork 1.4k
PySSLCertificate #6219
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PySSLCertificate #6219
Conversation
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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 _sslThis 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 consistentlyENCODING_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 formattingManual 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
⛔ Files ignored due to path filters (1)
Lib/test/test_urllib2_localnet.pyis 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 (runcargo 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.rsstdlib/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.rsstdlib/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 conversionSwitching 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 safeLength probing, allocation, and set_len are in the correct order. UTF‑8 fallback is robust.
66-95: public_bytes(): default and error mapping look goodDefaulting to PEM matches CPython. Error conversion is consistent.
242-255: cert_to_certificate/cert_to_py: API surface looks goodReturning PySSLCertificate for owned X509 and bytes/dict for getpeercert(binary=…) matches expectations. Error conversion via convert_openssl_error is consistent.
There was a problem hiding this 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
20and294for OpenSSL error codes, and inconsistently retrieves the SSLEOFError class viavm.class("_ssl", "SSLEOFError")instead ofPySslEOFError::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_AUXas 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
📒 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 (runcargo 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.rsstdlib/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.rsstdlib/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)
There was a problem hiding this 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_pyclosure 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_certificateare 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) and294(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
📒 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 (runcargo 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.rsstdlib/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.rsstdlib/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
0c957e7 to
8153d55
Compare
There was a problem hiding this 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
⛔ Files ignored due to path filters (1)
Lib/test/test_urllib2_localnet.pyis 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 (runcargo 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.rsstdlib/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.rsstdlib/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_chaincorrectly:
- 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_chainnow correctly propagates conversion errors instead of silently dropping certificates. Thecontinueon line 1254 for null pointers is appropriate since it doesn't mask conversion failures.
1987-1990: LGTM: Appropriate visibility change.Making
convert_openssl_errorpub(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_pyfunction 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_certfunction is exposed as a Python function but lacks documentation. The leading underscore suggests internal use, but it's unclear if this is meant for:
- Internal testing only
- Public API for certificate debugging
- 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_bytesmethod correctly handles only the exposed formats:ENCODING_DERandENCODING_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.
| // 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 rustLength 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 2Length 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 || trueLength 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.rsLength 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.
| 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", | ||
| ), | ||
| }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| 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() | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
| } 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() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Summary by CodeRabbit
New Features
Bug Fixes