Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Dec 20, 2025

Summary by CodeRabbit

  • New Features

    • Added Pre-Shared Key (PSK) callback support for client and server TLS contexts
    • Certificate loading now accepts callable passwords
    • Certificates gain comparison, hashing, and improved string representation; more certificate metadata exposed (OCSP, CA Issuers, CRL DP)
  • Bug Fixes

    • More robust DER/PEM parsing and clearer error messages
    • ALPN protocol slicing corrected
    • Stricter CA validation semantics and safer callback lifecycle handling
  • Chores

    • CI: added an OpenSSL-specific build/test step

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 20, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds OpenSSL-specific CI build step and extensive OpenSSL/ssl changes: PSK client/server callback storage and APIs, callable password support for certificate loading, stricter DER/PEM handling, certificate trait/visibility updates, ALPN and select/handshake refinements, and related ex_data lifecycle logic.

Changes

Cohort / File(s) Summary
CI workflow updates
.github/workflows/ci.yaml
Adds a "Test openssl build" CI step that runs cargo build --no-default-features --features ssl-openssl on Linux runners, placed after the "check compilation without threading" step.
OpenSSL core and PSK/password handling
crates/stdlib/src/openssl.rs
Adds PSK client/server callback storage and public APIs (set_psk_client_callback, set_psk_server_callback) with ex_data indices and cleanup. Changes LoadCertChainArgs.password to Option<PyObjectRef> to accept strings/bytes or Python-callables, implements password-extraction/callback invocation with length checks, tightens DER/PEM parsing & EOF handling, fixes ALPN slicing, updates SelectRet variants (removes IsBlocking), adjusts set_options behavior, and refactors ex_data/message/SNI callback lifecycle.
Certificate object and metadata
crates/stdlib/src/openssl/cert.rs
Makes PySSLCertificate.cert pub(crate), adds Comparable, Hashable, Representable implementations, returns PEM as Python string, pads serialNumber to even-length hex, exposes Registered ID in subjectAltName, and includes OCSP responders, CA Issuers and CRL distribution points in certificate dict.
Spelling dictionary
.cspell.dict/cpython.txt
Adds new token distpoint to the cspell dictionary.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Py as Python code
participant VM as VirtualMachine
participant Rust as PySslContext (Rust)
participant OpenSSL as OpenSSL C API
Note over Py,VM: Certificate load with callable password / PSK callback registration
Py->>VM: call load_cert_chain / set_psk_* (passes PyObject)
VM->>Rust: invoke PySslContext.method (store callback or call password)
Rust->>VM: if password callable -> call back into Python to get password bytes
VM-->>Rust: return password bytes or raise error
Rust->>OpenSSL: pass password / set ex_data (SSL_CTX or SSL) and call OpenSSL load functions
OpenSSL-->>Rust: returns success or error (DER/PEM parse results, handshake events)
Rust->>VM: translate OpenSSL result into PyResult / raise exceptions

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas to focus review on:
    • ex_data index creation/cleanup and safe dropping of Python objects (msg/SNI/PSK)
    • Callable password invocation: buffer size limits, encoding handling, and error propagation
    • Changes to SelectRet and handshake paths that affect blocking semantics
    • Certificate trait implementations for correctness and hashing/equality consistency

Possibly related PRs

Suggested reviewers

  • coolreader18
  • ShaharNaveh

Poem

🐰 I nibble code and weave a key,
PSK hooks now live with glee.
Passwords answer when you call,
Certificates stand proud and tall.
CI checks OpenSSL on Linux' lea. 🎩🔐

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.13% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'More openssl impl' is vague and non-descriptive, using generic phrasing that doesn't convey specific information about the substantial changes made to OpenSSL functionality. Consider a more specific title that highlights the main feature added, such as 'Add PSK callbacks and enhanced certificate handling to OpenSSL' or 'Implement PSK client/server callbacks in SSL context'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 Recent 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 7bcb91e and 57c3f4e.

📒 Files selected for processing (4)
  • .cspell.dict/cpython.txt (1 hunks)
  • .github/workflows/ci.yaml (1 hunks)
  • crates/stdlib/src/openssl.rs (24 hunks)
  • crates/stdlib/src/openssl/cert.rs (8 hunks)

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 December 20, 2025 14:32
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

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

108-134: Equality comparison logic uses unusual pattern.

The comparison at line 133 uses eq.cmp(&true) which is an unusual way to express boolean equality. While it works, it's less readable than a direct approach.

🔎 Suggested simplification
-            let eq = self_der == other_der;
-            Ok(op.eval_ord(eq.cmp(&true)).into())
+            let result = self_der == other_der;
+            Ok(PyComparisonValue::Implemented(op.eval_eq(result)))

Alternatively, if eval_eq is not available, consider:

-            Ok(op.eval_ord(eq.cmp(&true)).into())
+            let matches = match op {
+                PyComparisonOp::Eq => result,
+                PyComparisonOp::Ne => !result,
+                _ => unreachable!(),
+            };
+            Ok(PyComparisonValue::Implemented(matches))
📜 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 6660170 and 7bcb91e.

📒 Files selected for processing (3)
  • .github/workflows/ci.yaml (1 hunks)
  • crates/stdlib/src/openssl.rs (24 hunks)
  • crates/stdlib/src/openssl/cert.rs (8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

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

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/stdlib/src/openssl/cert.rs
  • crates/stdlib/src/openssl.rs
🧠 Learnings (1)
📚 Learning: 2025-11-29T12:17:28.606Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T12:17:28.606Z
Learning: Run `cargo test --workspace --exclude rustpython_wasm` to execute Rust unit tests

Applied to files:

  • .github/workflows/ci.yaml
🧬 Code graph analysis (1)
crates/stdlib/src/openssl/cert.rs (1)
crates/stdlib/src/openssl.rs (4)
  • convert_openssl_error (3664-3767)
  • new (537-541)
  • new (4093-4103)
  • value (3025-3026)
⏰ 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 rust tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run tests under miri
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Check Rust code with clippy
🔇 Additional comments (18)
.github/workflows/ci.yaml (1)

150-154: LGTM - OpenSSL build verification step.

This CI step ensures the ssl-openssl feature compiles correctly on Linux. The restriction to Linux is appropriate given cross-platform OpenSSL complexities.

crates/stdlib/src/openssl/cert.rs (6)

137-143: LGTM - Hashable implementation.

Using X509_subject_name_hash is a reasonable approach for certificate hashing, consistent with how OpenSSL identifies certificates.


145-166: LGTM - Representable implementation.

The representation logic correctly handles edge cases like missing short names and non-UTF8 data, providing a readable certificate description.


88-97: LGTM - PEM encoding returns string.

Returning PEM as a string aligns with CPython's ssl module behavior where PEM-encoded certificates are text.


201-209: LGTM - Serial number even-length padding.

Ensuring the serial number hex string has even length (byte-aligned) matches CPython's certificate representation.


287-339: LGTM - Certificate extension fields.

The additions for OCSP responders, CA Issuers, and CRL distribution points correctly populate the certificate dictionary, matching CPython's ssl module output.


260-276: Union field access for GEN_RID is implemented correctly.

The code properly accesses the GENERAL_NAME union field d for GEN_RID type. After verifying the type with (*ptr).type_ == sys::GEN_RID, it correctly casts (*ptr).d to *const sys::ASN1_OBJECT. The implementation includes proper null checking and uses the standard OpenSSL obj2txt() conversion function. The union access pattern is safe and follows C FFI conventions for discriminated union handling.

crates/stdlib/src/openssl.rs (11)

605-636: LGTM - Message callback ex_data management.

The dedicated ex_data index for msg_callback and the free function properly manage the Python object reference lifecycle, preventing memory leaks and use-after-free issues.


1126-1146: LGTM - Options update with clear/set delta.

The logic correctly computes which options to clear and set, ensuring the final options match the requested value rather than just ORing new options.


1367-1373: LGTM - ALPN protocol slice bounds fix.

Using pos..pos + proto.len() correctly returns only the selected protocol bytes, preventing potential buffer overread.


1485-1500: LGTM - Empty cadata error handling.

The differentiated error messages for PEM vs DER format help users understand why their certificate data wasn't loaded.


1960-1977: LGTM - Password bytes extraction helper.

The helper correctly handles all expected password types (str, bytes, bytearray) with clear error messaging.


2117-2134: LGTM - ex_data setup with proper reference counting.

The pattern of cloning, calling into_raw(), and relying on msg_callback_data_free to decrement the refcount correctly manages the Python object lifecycle.


2555-2578: LGTM - Server-side peer cert chain fix.

Correctly handles the OpenSSL behavior where SSL_get_peer_cert_chain excludes the peer certificate for server-side sockets, matching CPython's behavior.


2844-2880: LGTM - DER certificate loading with improved EOF detection.

The enhanced logic correctly distinguishes between:

  1. Clean EOF (all data consumed as valid certificates)
  2. Parse error (garbage data after valid certificates)
  3. Empty/invalid data

This matches CPython's certificate loading behavior.


1599-1601: LGTM - X509_check_ca return value fix.

X509_check_ca returns non-zero for any CA certificate type (not just 1), so the != 0 check is correct.


886-889: LGTM - PSK callback field additions.

The new fields for PSK callbacks and identity hint are properly initialized with PyMutex<Option<_>> for thread-safe optional storage.


1872-1921: Password callback implementation is safe.

The callback closure captures vm and pw_obj by reference within a synchronous operation. OpenSSL's PEM_read_bio_PrivateKey invokes the callback immediately during the read operation to request the password, and the closure is not stored for later use. The references remain valid throughout the function's execution, and error propagation via RefCell is correctly implemented.

Comment on lines +1384 to +1454
#[pymethod]
fn set_psk_client_callback(
&self,
callback: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult<()> {
// Cannot add PSK client callback to a server context
if self.protocol == SslVersion::TlsServer {
return Err(vm
.new_os_subtype_error(
PySSLError::class(&vm.ctx).to_owned(),
None,
"Cannot add PSK client callback to a PROTOCOL_TLS_SERVER context"
.to_owned(),
)
.upcast());
}

if vm.is_none(&callback) {
*self.psk_client_callback.lock() = None;
unsafe {
sys::SSL_CTX_set_psk_client_callback(self.builder().as_ptr(), None);
}
} else {
if !callback.is_callable() {
return Err(vm.new_type_error("callback must be callable".to_owned()));
}
*self.psk_client_callback.lock() = Some(callback);
// Note: The actual callback will be invoked via SSL app_data mechanism
// when do_handshake is called
}
Ok(())
}

#[pymethod]
fn set_psk_server_callback(
&self,
callback: PyObjectRef,
identity_hint: OptionalArg<PyStrRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
// Cannot add PSK server callback to a client context
if self.protocol == SslVersion::TlsClient {
return Err(vm
.new_os_subtype_error(
PySSLError::class(&vm.ctx).to_owned(),
None,
"Cannot add PSK server callback to a PROTOCOL_TLS_CLIENT context"
.to_owned(),
)
.upcast());
}

if vm.is_none(&callback) {
*self.psk_server_callback.lock() = None;
*self.psk_identity_hint.lock() = None;
unsafe {
sys::SSL_CTX_set_psk_server_callback(self.builder().as_ptr(), None);
}
} else {
if !callback.is_callable() {
return Err(vm.new_type_error("callback must be callable".to_owned()));
}
*self.psk_server_callback.lock() = Some(callback);
if let OptionalArg::Present(hint) = identity_hint {
*self.psk_identity_hint.lock() = Some(hint.as_str().to_owned());
}
// Note: The actual callback will be invoked via SSL app_data mechanism
}
Ok(())
}
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

🏁 Script executed:

#!/bin/bash
# Search for where psk_client_callback and psk_server_callback are actually used
rg -n "psk_client_callback|psk_server_callback" --type rust -B 3 -A 3 crates/stdlib/src/openssl.rs

Repository: RustPython/RustPython

Length of output: 2720


🏁 Script executed:

#!/bin/bash
# Search for do_handshake implementation and any usage of stored callbacks
rg -n "do_handshake|psk_client_callback\|psk_server_callback" --type rust crates/stdlib/src/openssl.rs | head -50

Repository: RustPython/RustPython

Length of output: 820


🏁 Script executed:

#!/bin/bash
# Get the do_handshake implementation around line 2866
sed -n '2860,2920p' crates/stdlib/src/openssl.rs

Repository: RustPython/RustPython

Length of output: 2828


🏁 Script executed:

#!/bin/bash
# Search for where psk_client_callback and psk_server_callback are USED (not just stored)
# Look for any invocations or retrievals of these stored callbacks
rg -n "psk_client_callback\|psk_server_callback" --type rust crates/stdlib/src/openssl.rs | grep -v "fn set_psk" | grep -v "\.lock()"

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Search more broadly for callback usage and wrapper functions
rg -n "psk_client_callback|psk_server_callback|raw_client_psk|raw_server_psk" --type rust crates/stdlib/src/openssl.rs

Repository: RustPython/RustPython

Length of output: 852


🏁 Script executed:

#!/bin/bash
# Check if there are any wrapper functions defined for PSK callbacks
rg -n "extern.*psk|fn.*psk.*callback|unsafe.*psk" --type rust crates/stdlib/src/openssl.rs | head -20

Repository: RustPython/RustPython

Length of output: 146


🏁 Script executed:

#!/bin/bash
# Final check: see if callbacks are ever retrieved from the lock anywhere
rg -n "\.psk_client_callback\|\.psk_server_callback" --type rust crates/stdlib/src/openssl.rs

Repository: RustPython/RustPython

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Confirm: search for any actual invocation/retrieval of these callbacks
rg -n "lock()" crates/stdlib/src/openssl.rs | grep -i psk

Repository: RustPython/RustPython

Length of output: 482


PSK callbacks stored but not registered with OpenSSL—no callback wrapper function is invoked.

The methods store Python callbacks in psk_client_callback/psk_server_callback, but fail to register C-compatible wrapper functions with OpenSSL. When a non-None callback is set, the code should call sys::SSL_CTX_set_psk_client_callback(self.builder().as_ptr(), Some(wrapper_fn)) to register an actual C callback. Currently this only happens when clearing (setting to None). The stored callbacks are never retrieved or invoked—they're unreachable from both OpenSSL and the handshake code. Either implement the wrapper function and OpenSSL registration, or clarify if this is an intentional partial implementation pending follow-up work.

🤖 Prompt for AI Agents
In crates/stdlib/src/openssl.rs around lines 1384-1454, the PSK client/server
setters store Python callbacks but do not register C-compatible wrapper
functions with OpenSSL, so OpenSSL cannot call into the stored Python callbacks;
fix by implementing extern "C" wrapper functions for client and server PSK
callbacks that retrieve the Rust context (via SSL_CTX app_data or ex_data),
acquire the GIL, call the stored PyObject callback and translate return values
into the OpenSSL-required types (handle errors by returning appropriate failure
codes), and when setting a non-None callback call
sys::SSL_CTX_set_psk_client_callback(self.builder().as_ptr(),
Some(psk_client_wrapper)) or sys::SSL_CTX_set_psk_server_callback(...,
Some(psk_server_wrapper)); also ensure you set the context pointer in OpenSSL
(app_data/ex_data) so the wrapper can find self, and when clearing (None)
unregister the callback by passing None and clear the stored app_data/ex_data;
include proper locking, lifetime and error handling around the Python call.

@youknowone youknowone merged commit 898fe85 into RustPython:main Dec 20, 2025
12 of 13 checks passed
@youknowone youknowone deleted the openssl branch December 20, 2025 15:38
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