ssl: share rustls engine in host_env and fire msg callbacks (#8665)

* ssl: fire msg callbacks and compute tls-unique

Observe TLS records to invoke _msg_callback, and derive TLS 1.2
tls-unique from the KeyLog master secret plus rustls PRF.
Restore CHANNEL_BINDING_TYPES and drop the remaining test_ssl
RUSTPYTHON skips.

Assisted-by: Claude

* lzma: build the xz-core engine on wasm32

Expose `_lzma` on wasm32 and Android by compiling the xz-core
engine on those targets instead of gating it out.

Assisted-by: Claude

* ssl: move the rustls engine into rustpython-common

Extract the VM-independent rustls helpers (MemoryBio, certificate
decode, cipher strings, OID table, chain builder, providers, and
TLS record observation) so stdlib _ssl wraps a shared engine.

Assisted-by: Claude

* ssl: keep the rustls engine in host_env

TLS helpers share the host trust store and default verify paths, so
they belong next to native_certs rather than in rustpython-common.

Assisted-by: Claude

* stdlib: drop unused jiff after moving cert decode

Assisted-by: Claude
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-08 00:25:16 +09:00
1 parent ae7bc5b619
commit 3a8a196996
22 files changed
+892 -392

No files matched your search

Generated
+4 -1
View File
@@ -3476,7 +3476,10 @@ dependencies = [
"parking_lot",
"paste",
"rustix",
"rustls",
"rustls-native-certs",
"rustls-pemfile",
"rustls-webpki",
"rustpython-wtf8",
"rustyline",
"schannel",
@@ -3486,6 +3489,7 @@ dependencies = [
"which",
"widestring",
"windows-sys 0.61.2",
"x509-parser",
]
[[package]]
@@ -3626,7 +3630,6 @@ dependencies = [
"indexmap",
"insta",
"itertools 0.15.0",
"jiff",
"libc",
"libsqlite3-sys",
"malachite-bigint",
Vendored
-3
View File
@@ -268,9 +268,6 @@ import warnings
socket_error = OSError # keep that public name in module namespace
CHANNEL_BINDING_TYPES = ['tls-unique']
# TODO: RUSTPYTHON; rustls does not expose TLS Finished messages
if "rustls" in OPENSSL_VERSION:
CHANNEL_BINDING_TYPES = []
HAS_NEVER_CHECK_COMMON_NAME = hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT')
-2
View File
@@ -3059,7 +3059,6 @@ class ThreadedTests(unittest.TestCase):
'Cannot create a client socket with a PROTOCOL_TLS_SERVER context',
str(e.exception))
@unittest.skip("TODO: RUSTPYTHON; flaky")
@unittest.skipUnless(support.Py_GIL_DISABLED, "test is only useful if the GIL is disabled")
def test_ssl_in_multiple_threads(self):
# See GH-124984: OpenSSL is not thread safe.
@@ -5221,7 +5220,6 @@ class TestSSLDebug(unittest.TestCase):
with self.assertRaises(TypeError):
client_context._msg_callback = object()
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ('read', <TLSVersion.TLSv1_2: 771>, <_TLSContentType.HANDSHAKE: 22>, <_TLSMessageType.SERVER_KEY_EXCHANGE: 12>) not found in []
def test_msg_callback_tls12(self):
client_context, server_context, hostname = testing_context()
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
+8
View File
@@ -10,6 +10,7 @@ license.workspace = true
[features]
native-certs = ["dep:rustls-native-certs"]
ssl = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-webpki", "dep:x509-parser"]
[dependencies]
rustpython-wtf8 = { workspace = true }
@@ -21,6 +22,10 @@ num-traits = { workspace = true }
parking_lot = { workspace = true }
paste = { workspace = true }
widestring = { workspace = true }
rustls = { workspace = true, default-features = false, features = ["std", "tls12"], optional = true }
rustls-pemfile = { workspace = true, optional = true }
rustls-webpki = { workspace = true, optional = true }
x509-parser = { workspace = true, optional = true }
[target.'cfg(unix)'.dependencies]
nix = { workspace = true }
@@ -89,6 +94,9 @@ windows-sys = { workspace = true, features = [
"Win32_UI_WindowsAndMessaging",
] }
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
rustls = { workspace = true, features = ["aws_lc_rs"] }
[build-dependencies]
cc = "1"
+2
View File
@@ -33,6 +33,8 @@ pub mod locale;
#[cfg(all(feature = "native-certs", not(target_arch = "wasm32")))]
pub mod native_certs;
pub mod readline;
#[cfg(feature = "ssl")]
pub mod ssl;
#[cfg(windows)]
pub mod windows;
+112
View File
@@ -0,0 +1,112 @@
//! In-memory encrypted transport used by `SSLObject`.
//!
//! The unread suffix is kept as `(Vec, start)` so a read does not shift the
//! whole buffer. Compaction happens only after a substantial prefix has been
//! consumed.
/// Write rejected because [`MemoryBio::write_eof`] has already been called.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemoryBioError;
impl core::fmt::Display for MemoryBioError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("cannot write() after write_eof()")
}
}
impl core::error::Error for MemoryBioError {}
/// In-memory BIO used by `MemoryBIO` and `SSLObject`.
#[derive(Debug, Default)]
pub struct MemoryBio {
buffer: Vec<u8>,
start: usize,
eof_written: bool,
}
impl MemoryBio {
/// Create an empty BIO.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Number of unread bytes.
#[must_use]
pub fn pending(&self) -> usize {
self.buffer.len() - self.start
}
/// True once [`write_eof`] has been called and every unread byte has been
/// consumed.
///
/// [`write_eof`]: MemoryBio::write_eof
#[must_use]
pub fn eof(&self) -> bool {
self.eof_written && self.pending() == 0
}
/// True after [`write_eof`], even if unread bytes remain.
///
/// [`write_eof`]: MemoryBio::write_eof
#[must_use]
pub fn eof_written(&self) -> bool {
self.eof_written
}
/// Take up to `size` unread bytes.
pub fn read(&mut self, size: usize) -> Vec<u8> {
let count = size.min(self.pending());
let end = self.start + count;
let out = self.buffer[self.start..end].to_vec();
self.start = end;
self.compact();
out
}
/// Append `data`. Fails after [`write_eof`].
///
/// [`write_eof`]: MemoryBio::write_eof
pub fn write(&mut self, data: &[u8]) -> Result<usize, MemoryBioError> {
if self.eof_written {
return Err(MemoryBioError);
}
self.compact();
self.buffer.extend_from_slice(data);
Ok(data.len())
}
/// Mark that no further writes will arrive.
pub fn write_eof(&mut self) {
self.eof_written = true;
}
fn compact(&mut self) {
if self.start == self.buffer.len() {
self.buffer.clear();
self.start = 0;
} else if self.start >= 4096 && self.start * 2 >= self.buffer.len() {
self.buffer.copy_within(self.start.., 0);
self.buffer.truncate(self.buffer.len() - self.start);
self.start = 0;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_write_and_eof() {
let mut bio = MemoryBio::new();
assert_eq!(bio.write(b"abcd").unwrap(), 4);
assert_eq!(bio.read(2), b"ab");
assert_eq!(bio.pending(), 2);
bio.write_eof();
assert!(!bio.eof());
assert_eq!(bio.read(8), b"cd");
assert!(bio.eof());
assert_eq!(bio.write(b"x"), Err(MemoryBioError));
}
}
+252
View File
@@ -0,0 +1,252 @@
//! X.509 decode helpers used by `_ssl.Certificate` and `get_ca_certs()`.
use x509_parser::prelude::*;
/// One RDN, as a list of `(attribute name, value)` pairs.
pub type DistinguishedName = Vec<Vec<(String, String)>>;
/// One subjectAltName entry after the names `_ssl` publishes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubjectAlternativeName {
pub kind: &'static str,
pub value: String,
pub directory_name: DistinguishedName,
}
/// Owned projection of the X.509 fields exposed by `_test_decode_cert()`
/// and by `SSLContext.get_ca_certs()`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedCertificate {
pub issuer: DistinguishedName,
pub subject: DistinguishedName,
pub not_after: String,
pub not_before: String,
pub serial_number: String,
pub version: i32,
pub ocsp: Vec<String>,
pub ca_issuers: Vec<String>,
pub crl_distribution_points: Vec<String>,
pub subject_alt_names: Vec<SubjectAlternativeName>,
}
/// Map a distinguished-name OID to the attribute name `_ssl` reports.
#[must_use]
pub fn oid_to_attribute_name(oid_str: &str) -> &str {
match oid_str {
"2.5.4.3" => "commonName",
"2.5.4.6" => "countryName",
"2.5.4.7" => "localityName",
"2.5.4.8" => "stateOrProvinceName",
"2.5.4.10" => "organizationName",
"2.5.4.11" => "organizationalUnitName",
"1.2.840.113549.1.9.1" => "emailAddress",
_ => oid_str,
}
}
/// Format raw IP address bytes the way `getpeercert()` prints them.
#[must_use]
pub fn format_ip_address(ip: &[u8]) -> String {
if ip.len() == 4 {
format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
} else if ip.len() == 16 {
let segments = [
u16::from_be_bytes([ip[0], ip[1]]),
u16::from_be_bytes([ip[2], ip[3]]),
u16::from_be_bytes([ip[4], ip[5]]),
u16::from_be_bytes([ip[6], ip[7]]),
u16::from_be_bytes([ip[8], ip[9]]),
u16::from_be_bytes([ip[10], ip[11]]),
u16::from_be_bytes([ip[12], ip[13]]),
u16::from_be_bytes([ip[14], ip[15]]),
];
format!(
"{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}",
segments[0],
segments[1],
segments[2],
segments[3],
segments[4],
segments[5],
segments[6],
segments[7]
)
} else {
format!("{ip:?}")
}
}
fn decode_name(name: &x509_parser::x509::X509Name<'_>) -> DistinguishedName {
name.iter()
.map(|rdn| {
rdn.iter()
.map(|attribute| {
let oid = attribute.attr_type().to_id_string();
let value = attribute.attr_value().as_str().map_or_else(
|_| match core::str::from_utf8(attribute.attr_value().data) {
Ok(s) => s.to_string(),
Err(_) => {
String::from_utf8_lossy(attribute.attr_value().data).into_owned()
}
},
str::to_string,
);
(oid_to_attribute_name(&oid).to_string(), value)
})
.collect()
})
.collect()
}
fn format_certificate_time(value: &x509_parser::time::ASN1Time) -> String {
let date = value.to_datetime();
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
format!(
"{} {:>2} {:02}:{:02}:{:02} {:04} GMT",
MONTHS[u8::from(date.month()) as usize - 1],
date.day(),
date.hour(),
date.minute(),
date.second(),
date.year(),
)
}
/// Decode a DER certificate into the fields `_ssl` publishes.
pub fn decode_certificate(der: &[u8]) -> Result<DecodedCertificate, String> {
use x509_parser::extensions::{DistributionPointName, GeneralName, ParsedExtension};
use x509_parser::oid_registry::{
OID_PKIX_AUTHORITY_INFO_ACCESS, OID_X509_EXT_CRL_DISTRIBUTION_POINTS,
};
let (_, cert) = x509_parser::parse_x509_certificate(der)
.map_err(|error| format!("Failed to parse certificate: {error}"))?;
let mut serial = cert.serial.to_str_radix(16).to_uppercase();
if serial.len() % 2 == 1 {
serial.insert(0, '0');
}
let mut decoded = DecodedCertificate {
issuer: decode_name(cert.issuer()),
subject: decode_name(cert.subject()),
not_after: format_certificate_time(&cert.validity().not_after),
not_before: format_certificate_time(&cert.validity().not_before),
serial_number: serial,
version: cert.version().0 as i32 + 1,
ocsp: Vec::new(),
ca_issuers: Vec::new(),
crl_distribution_points: Vec::new(),
subject_alt_names: Vec::new(),
};
if let Ok(extensions) = cert.tbs_certificate.extensions_map() {
if let Some(extension) = extensions.get(&OID_PKIX_AUTHORITY_INFO_ACCESS)
&& let ParsedExtension::AuthorityInfoAccess(access) = extension.parsed_extension()
{
for description in &access.accessdescs {
if let GeneralName::URI(uri) = &description.access_location {
match description.access_method.to_id_string().as_str() {
"1.3.6.1.5.5.7.48.1" => decoded.ocsp.push((*uri).to_string()),
"1.3.6.1.5.5.7.48.2" => decoded.ca_issuers.push((*uri).to_string()),
_ => {}
}
}
}
}
if let Some(extension) = extensions.get(&OID_X509_EXT_CRL_DISTRIBUTION_POINTS)
&& let ParsedExtension::CRLDistributionPoints(points) = extension.parsed_extension()
{
for point in &points.points {
if let Some(DistributionPointName::FullName(names)) = &point.distribution_point {
for name in names {
if let GeneralName::URI(uri) = name {
decoded.crl_distribution_points.push((*uri).to_string());
}
}
}
}
}
}
if let Ok(Some(extension)) = cert.subject_alternative_name() {
for name in &extension.value.general_names {
let entry = match name {
GeneralName::DNSName(value) => SubjectAlternativeName {
kind: "DNS",
value: (*value).to_string(),
directory_name: Vec::new(),
},
GeneralName::IPAddress(value) => SubjectAlternativeName {
kind: "IP Address",
value: format_ip_address(value),
directory_name: Vec::new(),
},
GeneralName::RFC822Name(value) => SubjectAlternativeName {
kind: "email",
value: (*value).to_string(),
directory_name: Vec::new(),
},
GeneralName::URI(value) => SubjectAlternativeName {
kind: "URI",
value: (*value).to_string(),
directory_name: Vec::new(),
},
GeneralName::OtherName(_, _) => SubjectAlternativeName {
kind: "othername",
value: "<unsupported>".to_string(),
directory_name: Vec::new(),
},
GeneralName::DirectoryName(value) => SubjectAlternativeName {
kind: "DirName",
value: String::new(),
directory_name: decode_name(value),
},
GeneralName::RegisteredID(value) => SubjectAlternativeName {
kind: "Registered ID",
value: value.to_id_string(),
directory_name: Vec::new(),
},
_ => continue,
};
decoded.subject_alt_names.push(entry);
}
}
Ok(decoded)
}
/// True when Basic Constraints mark the certificate as a CA, or when a
/// self-issued X.509v1 certificate has no extensions.
#[must_use]
pub fn is_ca_certificate(cert_der: &[u8]) -> bool {
let Ok((_, cert)) = X509Certificate::from_der(cert_der) else {
return false;
};
if let Ok(Some(ext)) = cert.basic_constraints() {
return ext.value.ca;
}
cert.version().0 == 0 && cert.subject() == cert.issuer()
}
/// Parse PEM or a single DER certificate into DER bodies.
pub fn read_certificates(data: &[u8]) -> Result<Vec<Vec<u8>>, String> {
if data
.windows(b"-----BEGIN ".len())
.any(|w| w == b"-----BEGIN ")
{
let mut input = data;
rustls_pemfile::certs(&mut input)
.map(|cert| cert.map(|der| der.as_ref().to_vec()))
.collect::<Result<Vec<_>, _>>()
.map_err(|error| format!("Failed to parse PEM certificate: {error}"))
} else {
x509_parser::parse_x509_certificate(data)
.map_err(|error| format!("Failed to parse certificate: {error}"))?;
Ok(vec![data.to_vec()])
}
}
@@ -9,10 +9,10 @@ use rustls::{
pki_types::{CertificateDer, CertificateRevocationListDer, UnixTime},
};
use super::_ssl::VERIFY_X509_PARTIAL_CHAIN;
use super::constants::VERIFY_X509_PARTIAL_CHAIN;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Purpose {
pub enum Purpose {
Unverified,
ServerAuth,
ClientAuth,
@@ -22,7 +22,7 @@ pub(super) enum Purpose {
/// connection. Reading a mutable SSLContext after the handshake can select a
/// different root, and issuer names alone do not establish a verified path.
#[derive(Debug)]
pub(super) struct VerifiedChainBuilder {
pub struct VerifiedChainBuilder {
pub purpose: Purpose,
pub roots: RootCertStore,
pub root_der: Vec<Vec<u8>>,
@@ -33,15 +33,12 @@ pub(super) struct VerifiedChainBuilder {
pub verify_flags: i32,
}
pub(super) type ServerConfig = (Arc<rustls::ServerConfig>, Arc<VerifiedChainBuilder>);
pub(super) type ClientConfig = (Arc<rustls::ClientConfig>, Arc<VerifiedChainBuilder>);
pub type ServerConfig = (Arc<rustls::ServerConfig>, Arc<VerifiedChainBuilder>);
pub type ClientConfig = (Arc<rustls::ClientConfig>, Arc<VerifiedChainBuilder>);
impl VerifiedChainBuilder {
pub(super) fn build(
&self,
peer_chain: &[CertificateDer<'_>],
now: UnixTime,
) -> Option<Vec<Vec<u8>>> {
#[must_use]
pub fn build(&self, peer_chain: &[CertificateDer<'_>], now: UnixTime) -> Option<Vec<Vec<u8>>> {
self.build_path(peer_chain, now).or_else(|| {
// OpenSSL still attempts to build a chain under CERT_NONE; a
// verification failure does not prevent returning the peer chain.
@@ -15,14 +15,14 @@
use core::str::FromStr;
use rustls::{CipherSuite, SupportedCipherSuite, crypto::SupportedKxGroup};
use rustpython_common::lock::LazyLock;
use std::sync::LazyLock;
use super::providers::CryptoExt;
// See `man SSL_CTX_set_security_level` for details.
const SECURITY_LEVEL_TO_MIN_BITS: &[u16] = &[0, 80, 112, 128, 192, 256];
pub(super) struct CipherList<'a> {
pub struct CipherList<'a> {
ops: Vec<CipherFilterOp<'a>>,
}
@@ -148,7 +148,7 @@ impl SuiteBType {
}
impl<'a> CipherList<'a> {
pub(super) fn parse_to_rustls(
pub fn parse_to_rustls(
s: &'a str,
) -> Result<WithOptionSuiteB<Vec<SupportedCipherSuite>>, &'static str> {
Self::parse(s)?.to_rustls()
@@ -421,7 +421,7 @@ impl<'a> CipherFilterSubOpList<'a> {
}
}
pub(super) fn kx_group_by_name(
pub fn kx_group_by_name(
name: rustls::NamedGroup,
error_name: &'static str,
) -> Result<&'static dyn SupportedKxGroup, &'static str> {
@@ -433,7 +433,8 @@ pub(super) fn kx_group_by_name(
.ok_or(error_name)
}
pub(super) fn kx_group_by_openssl_name(name: &str) -> Option<&'static dyn SupportedKxGroup> {
#[must_use]
pub fn kx_group_by_openssl_name(name: &str) -> Option<&'static dyn SupportedKxGroup> {
CryptoExt::get_ext()
.all_kx_or_default()
.iter()
@@ -531,7 +532,7 @@ impl<'a> CipherFilterSubOp<'a> {
/// What `SSLContext.get_ciphers()` reports about one suite, laid out the way
/// `SSL_CIPHER_description` lays it out, padding included.
pub(super) struct CipherDescription {
pub struct CipherDescription {
pub id: u32,
pub name: &'static str,
pub protocol: &'static str,
@@ -539,7 +540,7 @@ pub(super) struct CipherDescription {
pub description: String,
}
pub(super) fn describe(suite: &SupportedCipherSuite) -> CipherDescription {
pub fn describe(suite: &SupportedCipherSuite) -> CipherDescription {
let entry = CIPHER_MAPPINGS.entry(suite.suite().into());
let tls13 = suite.tls13().is_some();
let protocol = if tls13 { "TLSv1.3" } else { "TLSv1.2" };
@@ -802,11 +803,12 @@ impl CipherMappings {
}
}
pub(super) fn default_cipher_string() -> String {
pub fn default_cipher_string() -> String {
CIPHER_MAPPINGS.default_cipher_string()
}
pub(super) fn restore_default_tls13(
#[must_use]
pub fn restore_default_tls13(
mut selected: Vec<SupportedCipherSuite>,
defaults: &[SupportedCipherSuite],
) -> Vec<SupportedCipherSuite> {
+21
View File
@@ -0,0 +1,21 @@
//! Protocol, verification, and option values published by `_ssl`.
pub const PROTOCOL_TLS: i32 = 2;
pub const PROTOCOL_TLS_CLIENT: i32 = 16;
pub const PROTOCOL_TLS_SERVER: i32 = 17;
pub const PROTOCOL_TLSV1: i32 = 3;
pub const PROTOCOL_TLSV1_1: i32 = 4;
pub const PROTOCOL_TLSV1_2: i32 = 5;
pub const PROTOCOL_TLSV1_3: i32 = 6;
pub const CERT_NONE: i32 = 0;
pub const CERT_OPTIONAL: i32 = 1;
pub const CERT_REQUIRED: i32 = 2;
pub const VERIFY_DEFAULT: i32 = 0;
pub const VERIFY_CRL_CHECK_LEAF: i32 = 4;
pub const VERIFY_CRL_CHECK_CHAIN: i32 = 12;
pub const VERIFY_X509_STRICT: i32 = 32;
pub const VERIFY_ALLOW_PROXY_CERTS: i32 = 64;
pub const VERIFY_X509_TRUSTED_FIRST: i32 = 32768;
pub const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x80000;
+20
View File
@@ -0,0 +1,20 @@
//! Host rustls `_ssl` engine.
//!
//! TLS helpers live here because they share the host trust store, default
//! verify paths, and certificate files with the rest of `host_env`. The
//! engine reports plain Rust results so interpreter layers can provide their
//! own object and exception adapters.
pub mod bio;
pub mod cert;
pub mod chain;
pub mod cipher;
pub mod constants;
pub mod msg;
pub mod oid;
pub mod providers;
pub use bio::{MemoryBio, MemoryBioError};
pub use cert::{DecodedCertificate, decode_certificate, is_ca_certificate};
pub use constants::*;
pub use providers::CryptoExt;
+204
View File
@@ -0,0 +1,204 @@
//! TLS record observation for `_msg_callback` and tls-unique.
use rustls::SupportedCipherSuite;
pub const SSL3_RT_CHANGE_CIPHER_SPEC: i32 = 20;
pub const SSL3_RT_ALERT: i32 = 21;
pub const SSL3_RT_HANDSHAKE: i32 = 22;
pub const SSL3_RT_APPLICATION_DATA: u8 = 23;
pub const SSL3_RT_HEADER: i32 = 256;
pub const SSL3_MT_CHANGE_CIPHER_SPEC: i32 = 0x0101;
const TLS_RECORD_HEADER_SIZE: usize = 5;
/// Per-connection assembler for TLS records and handshake messages.
#[derive(Debug, Default)]
pub struct MsgState {
incoming_record: Vec<u8>,
outgoing_record: Vec<u8>,
incoming_hs: Vec<u8>,
outgoing_hs: Vec<u8>,
transcript: Vec<u8>,
saw_ccs: bool,
}
/// One `_msg_callback` event reconstructed from a TLS record.
#[derive(Debug)]
pub struct MsgEvent {
pub version: i32,
pub content_type: i32,
pub msg_type: i32,
pub data: Vec<u8>,
}
impl MsgState {
/// Handshake bytes observed before ChangeCipherSpec.
#[must_use]
pub fn transcript(&self) -> &[u8] {
&self.transcript
}
/// Consume ciphertext and return the records and handshake messages it
/// completed.
pub fn observe(&mut self, write: bool, bytes: &[u8]) -> Vec<MsgEvent> {
let (record_buf, hs_buf) = if write {
(&mut self.outgoing_record, &mut self.outgoing_hs)
} else {
(&mut self.incoming_record, &mut self.incoming_hs)
};
record_buf.extend_from_slice(bytes);
let mut events = Vec::new();
for (content_type, version, payload) in take_records(record_buf) {
let mut header = Vec::with_capacity(TLS_RECORD_HEADER_SIZE);
header.push(content_type);
header.extend_from_slice(&version.to_be_bytes());
header.extend_from_slice(&(payload.len() as u16).to_be_bytes());
events.push(MsgEvent {
version: version as i32,
content_type: SSL3_RT_HEADER,
msg_type: content_type as i32,
data: header,
});
match content_type {
x if x == SSL3_RT_CHANGE_CIPHER_SPEC as u8 => {
events.push(MsgEvent {
version: version as i32,
content_type: SSL3_RT_CHANGE_CIPHER_SPEC,
msg_type: SSL3_MT_CHANGE_CIPHER_SPEC,
data: payload,
});
hs_buf.clear();
self.saw_ccs = true;
}
x if x == SSL3_RT_ALERT as u8 => {
let msg_type = payload.get(1).copied().map_or(-1, |b| b as i32);
events.push(MsgEvent {
version: version as i32,
content_type: SSL3_RT_ALERT,
msg_type,
data: payload,
});
}
x if x == SSL3_RT_HANDSHAKE as u8 => {
if self.saw_ccs {
continue;
}
hs_buf.extend_from_slice(&payload);
for message in take_handshake_messages(hs_buf) {
self.transcript.extend_from_slice(&message);
let msg_type = message.first().copied().map_or(-1, |b| b as i32);
events.push(MsgEvent {
version: version as i32,
content_type: SSL3_RT_HANDSHAKE,
msg_type,
data: message,
});
}
}
SSL3_RT_APPLICATION_DATA => {}
_ => events.push(MsgEvent {
version: version as i32,
content_type: content_type as i32,
msg_type: -1,
data: payload,
}),
}
}
events
}
}
fn take_records(buf: &mut Vec<u8>) -> Vec<(u8, u16, Vec<u8>)> {
let mut records = Vec::new();
loop {
if buf.len() < TLS_RECORD_HEADER_SIZE {
break;
}
let len = u16::from_be_bytes([buf[3], buf[4]]) as usize;
if buf.len() < TLS_RECORD_HEADER_SIZE + len {
break;
}
let content_type = buf[0];
let version = u16::from_be_bytes([buf[1], buf[2]]);
let payload = buf[TLS_RECORD_HEADER_SIZE..TLS_RECORD_HEADER_SIZE + len].to_vec();
buf.drain(..TLS_RECORD_HEADER_SIZE + len);
records.push((content_type, version, payload));
}
records
}
fn take_handshake_messages(buf: &mut Vec<u8>) -> Vec<Vec<u8>> {
let mut messages = Vec::new();
loop {
if buf.len() < 4 {
break;
}
let len = u32::from_be_bytes([0, buf[1], buf[2], buf[3]]) as usize;
if buf.len() < 4 + len {
break;
}
messages.push(buf[..4 + len].to_vec());
buf.drain(..4 + len);
}
messages
}
/// TLS 1.2 `tls-unique` binding from the master secret and handshake transcript.
#[must_use]
pub fn tls12_unique(
suite: SupportedCipherSuite,
master_secret: &[u8],
transcript: &[u8],
server_side: bool,
session_reused: bool,
) -> Option<Vec<u8>> {
let SupportedCipherSuite::Tls12(suite) = suite else {
return None;
};
if master_secret.is_empty() || transcript.is_empty() {
return None;
}
let hash = suite.common.hash_provider.hash(transcript);
let is_client = !server_side;
let ours = session_reused ^ is_client;
let label: &[u8] = match (ours, server_side) {
(true, false) | (false, true) => b"client finished",
(true, true) | (false, false) => b"server finished",
};
let mut out = vec![0u8; 12];
suite
.prf_provider
.for_secret(&mut out, master_secret, label, hash.as_ref());
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splits_complete_records_and_handshake_messages() {
let mut state = MsgState::default();
let mut record = vec![22, 3, 3, 0, 8];
record.extend_from_slice(&[12, 0, 0, 4, 1, 2, 3, 4]);
let events = state.observe(false, &record);
assert_eq!(events.len(), 2);
assert_eq!(events[0].content_type, SSL3_RT_HEADER);
assert_eq!(events[1].content_type, SSL3_RT_HANDSHAKE);
assert_eq!(events[1].msg_type, 12);
assert_eq!(state.transcript(), [12, 0, 0, 4, 1, 2, 3, 4]);
}
#[test]
fn ccs_stops_transcript() {
let mut state = MsgState::default();
let mut hs = vec![22, 3, 3, 0, 4];
hs.extend_from_slice(&[1, 0, 0, 0]);
state.observe(true, &hs);
state.observe(true, &[20, 3, 3, 0, 1, 1]);
let before = state.transcript().to_vec();
let mut finished = vec![22, 3, 3, 0, 4];
finished.extend_from_slice(&[20, 0, 0, 0]);
state.observe(true, &finished);
assert_eq!(state.transcript(), before);
}
}
@@ -17,7 +17,7 @@ use std::collections::HashMap;
/// OID entry with openssl-compatible metadata
#[derive(Debug, Clone)]
pub(super) struct OidEntry {
pub struct OidEntry {
/// NID (OpenSSL Numerical Identifier) - must match CPython/OpenSSL values
pub nid: i32,
/// Short name (e.g., "CN", "serverAuth")
@@ -32,13 +32,14 @@ pub(super) struct OidEntry {
impl OidEntry {
/// Get OID as string (e.g., "2.5.4.3"), if this object has one
pub(super) fn oid_string(&self) -> Option<&str> {
#[must_use]
pub fn oid_string(&self) -> Option<&str> {
self.oid.as_deref()
}
}
/// OID table with multiple indices for fast lookup
pub(super) struct OidTable {
pub struct OidTable {
/// All entries
entries: Vec<OidEntry>,
/// NID -> index mapping
@@ -79,17 +80,20 @@ impl OidTable {
}
}
pub(super) fn find_by_nid(&self, nid: i32) -> Option<&OidEntry> {
#[must_use]
pub fn find_by_nid(&self, nid: i32) -> Option<&OidEntry> {
self.nid_to_idx.get(&nid).map(|&idx| &self.entries[idx])
}
pub(super) fn find_by_oid_string(&self, oid_str: &str) -> Option<&OidEntry> {
#[must_use]
pub fn find_by_oid_string(&self, oid_str: &str) -> Option<&OidEntry> {
self.oid_str_to_idx
.get(canonical_oid(oid_str)?.as_str())
.map(|&idx| &self.entries[idx])
}
pub(super) fn find_by_name(&self, name: &str) -> Option<&OidEntry> {
#[must_use]
pub fn find_by_name(&self, name: &str) -> Option<&OidEntry> {
// OpenSSL object names are case-sensitive. Try the short name first,
// as OBJ_txt2obj does when a short and long name collide.
self.short_name_to_idx
@@ -125,16 +129,15 @@ fn canonical_oid(oid: &str) -> Option<String> {
}
/// Global OID table
static OID_TABLE: rustpython_common::lock::LazyLock<OidTable> =
rustpython_common::lock::LazyLock::new(OidTable::build);
static OID_TABLE: std::sync::LazyLock<OidTable> = std::sync::LazyLock::new(OidTable::build);
/// OpenSSL's NID assignments: one `identifier<whitespace>nid` line per object.
static OBJ_MAC_NUM: &str = include_str!("../../rustls-data/obj_mac.num");
static OBJ_MAC_NUM: &str = include_str!("data/obj_mac.num");
/// OpenSSL's object database: the numeric OID, short name and description of
/// each object, with `!Alias` / `!Cname` / `!module` / `!global` directives
/// between them.
static OBJECTS_TXT: &str = include_str!("../../rustls-data/objects.txt");
static OBJECTS_TXT: &str = include_str!("data/objects.txt");
/// The name `objects.pl` derives for an object or alias: qualified by the
/// enclosing `!module`, with `-` spelled `_`.
@@ -269,17 +272,20 @@ fn build_oid_entries() -> Vec<OidEntry> {
// Public API Functions
/// Find OID entry by NID
pub(super) fn find_by_nid(nid: i32) -> Option<&'static OidEntry> {
#[must_use]
pub fn find_by_nid(nid: i32) -> Option<&'static OidEntry> {
OID_TABLE.find_by_nid(nid)
}
/// Find OID entry by OID string (e.g., "2.5.4.3")
pub(super) fn find_by_oid_string(oid_str: &str) -> Option<&'static OidEntry> {
#[must_use]
pub fn find_by_oid_string(oid_str: &str) -> Option<&'static OidEntry> {
OID_TABLE.find_by_oid_string(oid_str)
}
/// Find OID entry by name (short or long name)
pub(super) fn find_by_name(name: &str) -> Option<&'static OidEntry> {
#[must_use]
pub fn find_by_name(name: &str) -> Option<&'static OidEntry> {
OID_TABLE.find_by_name(name)
}
File renamed without changes.
+1 -2
View File
@@ -24,7 +24,7 @@ ssl-openssl-vendor = ["ssl-openssl", "openssl/vendored"]
tkinter = ["dep:tk-sys", "dep:tcl-sys", "dep:widestring"]
flame-it = ["flame"]
__ssl-rustls = ["ssl", "rustpython-host_env/native-certs", "rustls", "rustls-pemfile", "rustls-platform-verifier", "rustls-webpki", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "oid-registry", "pkcs8"]
__ssl-rustls = ["ssl", "rustpython-host_env/ssl", "rustpython-host_env/native-certs", "rustls", "rustls-pemfile", "rustls-platform-verifier", "rustls-webpki", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "oid-registry", "pkcs8"]
[dependencies]
# rustpython crates
@@ -72,7 +72,6 @@ constant_time_eq = { workspace = true }
adler32 = { workspace = true }
# tkinter
jiff = { workspace = true }
tk-sys = { workspace = true, optional = true }
tcl-sys = { workspace = true, optional = true }
widestring = { workspace = true, optional = true }
+141 -84
View File
@@ -13,17 +13,9 @@
//!
//! Warning: This library contains AI-generated code and comments. Do not trust any code or comment without verification. Please have a qualified expert review the code and remove this notice after review.
// OID (Object Identifier) management module
mod oid;
// Certificate operations module (parsing, validation, conversion)
mod cert;
mod chain;
/// OpenSSL cipher string parsing for `set_ciphers`
mod cipher;
// OpenSSL compatibility layer (abstracts rustls operations)
mod compat;
@@ -32,9 +24,9 @@ mod error;
mod handshake;
mod keylog;
mod msg;
// Utilities for setting a Rustls cryptography provider.
pub mod providers;
pub use rustpython_host_env::ssl::{chain, cipher, oid, providers};
pub(crate) use _ssl::module_def;
@@ -105,11 +97,11 @@ mod _ssl {
// Import certificate operations module
use super::cert;
use super::chain::{self, VerifiedChainBuilder};
// Import OID module
use super::cipher;
use super::oid;
use rustpython_host_env::ssl::{
chain::{self, VerifiedChainBuilder},
cipher, oid,
providers::CryptoExt,
};
// Import compat module (OpenSSL compatibility layer)
use super::compat::{
@@ -119,7 +111,6 @@ mod _ssl {
};
use super::handshake::TlsState;
use super::providers::CryptoExt;
// Type aliases for better readability
// Additional type alias for certificate/key pairs (SessionCache defined below)
@@ -139,24 +130,24 @@ mod _ssl {
// SSL/TLS Protocol versions
#[pyattr]
const PROTOCOL_TLS: i32 = 2; // Auto-negotiate best version
const PROTOCOL_TLS: i32 = rustpython_host_env::ssl::PROTOCOL_TLS; // Auto-negotiate best version
#[pyattr]
const PROTOCOL_SSLv23: i32 = PROTOCOL_TLS; // Alias for PROTOCOL_TLS
#[pyattr]
const PROTOCOL_TLS_CLIENT: i32 = 16;
const PROTOCOL_TLS_CLIENT: i32 = rustpython_host_env::ssl::PROTOCOL_TLS_CLIENT;
#[pyattr]
const PROTOCOL_TLS_SERVER: i32 = 17;
const PROTOCOL_TLS_SERVER: i32 = rustpython_host_env::ssl::PROTOCOL_TLS_SERVER;
// Note: rustls doesn't support TLS 1.0/1.1 for security reasons
// These are defined for API compatibility but will raise errors if used
#[pyattr]
const PROTOCOL_TLSv1: i32 = 3;
const PROTOCOL_TLSv1: i32 = rustpython_host_env::ssl::PROTOCOL_TLSV1;
#[pyattr]
const PROTOCOL_TLSv1_1: i32 = 4;
const PROTOCOL_TLSv1_1: i32 = rustpython_host_env::ssl::PROTOCOL_TLSV1_1;
#[pyattr]
const PROTOCOL_TLSv1_2: i32 = 5;
const PROTOCOL_TLSv1_2: i32 = rustpython_host_env::ssl::PROTOCOL_TLSV1_2;
#[pyattr]
const PROTOCOL_TLSv1_3: i32 = 6;
const PROTOCOL_TLSv1_3: i32 = rustpython_host_env::ssl::PROTOCOL_TLSV1_3;
static NEXT_SSL_SESSION_NONCE: AtomicUsize = AtomicUsize::new(1);
@@ -213,32 +204,33 @@ mod _ssl {
// Certificate verification modes
#[pyattr]
const CERT_NONE: i32 = 0;
const CERT_NONE: i32 = rustpython_host_env::ssl::CERT_NONE;
#[pyattr]
const CERT_OPTIONAL: i32 = 1;
const CERT_OPTIONAL: i32 = rustpython_host_env::ssl::CERT_OPTIONAL;
#[pyattr]
const CERT_REQUIRED: i32 = 2;
const CERT_REQUIRED: i32 = rustpython_host_env::ssl::CERT_REQUIRED;
// SSL Verification Flags / Certificate requirements
#[pyattr]
const VERIFY_DEFAULT: i32 = 0;
const VERIFY_DEFAULT: i32 = rustpython_host_env::ssl::VERIFY_DEFAULT;
#[pyattr]
const VERIFY_CRL_CHECK_LEAF: i32 = 4;
const VERIFY_CRL_CHECK_LEAF: i32 = rustpython_host_env::ssl::VERIFY_CRL_CHECK_LEAF;
#[pyattr]
const VERIFY_CRL_CHECK_CHAIN: i32 = 12;
const VERIFY_CRL_CHECK_CHAIN: i32 = rustpython_host_env::ssl::VERIFY_CRL_CHECK_CHAIN;
/// VERIFY_X509_STRICT flag for RFC 5280 strict compliance
/// When set, performs additional validation including AKI extension checks
#[pyattr]
pub(crate) const VERIFY_X509_STRICT: i32 = 32;
pub(crate) const VERIFY_X509_STRICT: i32 = rustpython_host_env::ssl::VERIFY_X509_STRICT;
#[pyattr]
const VERIFY_ALLOW_PROXY_CERTS: i32 = 64;
const VERIFY_ALLOW_PROXY_CERTS: i32 = rustpython_host_env::ssl::VERIFY_ALLOW_PROXY_CERTS;
#[pyattr]
const VERIFY_X509_TRUSTED_FIRST: i32 = 32768;
const VERIFY_X509_TRUSTED_FIRST: i32 = rustpython_host_env::ssl::VERIFY_X509_TRUSTED_FIRST;
/// VERIFY_X509_PARTIAL_CHAIN flag for partial chain validation
/// When set, accept certificates if any certificate in the chain is in the trust store
/// (not just root CAs). This matches OpenSSL's X509_V_FLAG_PARTIAL_CHAIN behavior.
#[pyattr]
pub(crate) const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x80000;
pub(crate) const VERIFY_X509_PARTIAL_CHAIN: i32 =
rustpython_host_env::ssl::VERIFY_X509_PARTIAL_CHAIN;
// Options (OpenSSL-compatible flags, mostly no-op in rustls)
#[pyattr]
@@ -1929,6 +1921,8 @@ mod _ssl {
client_session_store: PyRwLock::new(None),
pending_tls_output: PyMutex::new(Vec::new()),
write_buffered_len: PyMutex::new(0),
msg_state: PyMutex::new(super::msg::MsgState::default()),
pending_msg_exc: PyMutex::new(None),
deferred_cert_error: Arc::new(ParkingRwLock::new(None)),
};
@@ -2017,6 +2011,8 @@ mod _ssl {
client_session_store: PyRwLock::new(None),
pending_tls_output: PyMutex::new(Vec::new()),
write_buffered_len: PyMutex::new(0),
msg_state: PyMutex::new(super::msg::MsgState::default()),
pending_msg_exc: PyMutex::new(None),
deferred_cert_error: Arc::new(ParkingRwLock::new(None)),
};
@@ -2444,6 +2440,9 @@ mod _ssl {
// Prevents duplicate writes when retrying after WantWrite/WantRead
#[pytraverse(skip)]
pub(crate) write_buffered_len: PyMutex<usize>,
#[pytraverse(skip)]
msg_state: PyMutex<super::msg::MsgState>,
pending_msg_exc: PyMutex<Option<PyBaseExceptionRef>>,
// Deferred client certificate verification error (for TLS 1.3)
// Stores error message if client cert verification failed during handshake
// Error is raised on first I/O operation after handshake
@@ -2477,6 +2476,49 @@ mod _ssl {
&self.connection
}
pub(crate) fn observe_tls(&self, write: bool, bytes: &[u8], vm: &VirtualMachine) {
if bytes.is_empty() {
return;
}
let events = self.msg_state.lock().observe(write, bytes);
if events.is_empty() {
return;
}
let callback = self.context.read().msg_callback.read().clone();
let Some(callback) = callback else {
return;
};
let conn = self
.owner
.read()
.as_ref()
.and_then(|owner| owner.upgrade())
.unwrap_or_else(|| vm.ctx.none());
for event in events {
let result = callback.call(
(
conn.clone(),
vm.ctx.new_str(if write { "write" } else { "read" }),
vm.ctx.new_int(event.version),
vm.ctx.new_int(event.content_type),
vm.ctx.new_int(event.msg_type),
vm.ctx.new_bytes(event.data),
),
vm,
);
if let Err(exc) = result {
let mut pending = self.pending_msg_exc.lock();
if pending.is_none() {
*pending = Some(exc);
}
}
}
}
pub(crate) fn take_msg_exc(&self) -> Option<PyBaseExceptionRef> {
self.pending_msg_exc.lock().take()
}
// Check for deferred certificate verification errors (TLS 1.3)
// If an error exists, raise it and clear it from storage
fn check_deferred_cert_error(&self, vm: &VirtualMachine) -> PyResult<()> {
@@ -2748,7 +2790,13 @@ mod _ssl {
)
}
fn reject_connection(&self, error: PyBaseExceptionRef, bytes: Vec<u8>) {
fn reject_connection(
&self,
error: PyBaseExceptionRef,
bytes: Vec<u8>,
vm: &VirtualMachine,
) {
self.observe_tls(true, &bytes, vm);
self.pending_tls_output.lock().extend_from_slice(&bytes);
*self.state.lock() = TlsState::SendingAlert { error };
}
@@ -2771,6 +2819,7 @@ mod _ssl {
self.reject_connection(
error,
super::handshake::sni_alert(description),
vm,
);
continue;
}
@@ -2785,7 +2834,7 @@ mod _ssl {
} else {
SslError::from_rustls(error)
};
self.reject_connection(error.into_py_err(vm), bytes);
self.reject_connection(error.into_py_err(vm), bytes, vm);
continue;
}
Ok(None) => {}
@@ -2816,6 +2865,7 @@ mod _ssl {
},
);
}
self.observe_tls(false, bytes.as_bytes(), vm);
super::handshake::feed_acceptor(&mut acceptor, bytes.as_bytes())
.map_err(|e| e.into_pyexception(vm))?;
Ok(())
@@ -3258,7 +3308,7 @@ mod _ssl {
alert
.write_all(&mut bytes)
.map_err(|e| e.into_pyexception(vm))?;
self.reject_connection(SslError::from_rustls(error).into_py_err(vm), bytes);
self.reject_connection(SslError::from_rustls(error).into_py_err(vm), bytes, vm);
self.accept_client_hello(vm)?;
}
}
@@ -3289,7 +3339,7 @@ mod _ssl {
if matches!(*self.state.lock(), TlsState::SendingAlert { .. }) {
return Err(error);
}
self.reject_connection(error, super::handshake::sni_alert(40));
self.reject_connection(error, super::handshake::sni_alert(40), vm);
return self.accept_client_hello(vm).map(|_| ());
}
} else {
@@ -3411,10 +3461,13 @@ mod _ssl {
if let Some(conn) = self.connection.lock().as_mut() {
let _ = conn.write_tls(&mut bytes);
}
self.reject_connection(error.into_py_err(vm), bytes);
self.reject_connection(error.into_py_err(vm), bytes, vm);
return self.accept_client_hello(vm).map(|_| ());
}
handshake_result.map_err(|e| e.into_py_err(vm))?;
if let Some(exc) = self.take_msg_exc() {
return Err(exc);
}
self.complete_handshake(vm);
Ok(())
}
@@ -3510,6 +3563,9 @@ mod _ssl {
// Check for deferred certificate verification errors (TLS 1.3)
// Must be checked AFTER ssl_read, as the error is set during I/O
self.check_deferred_cert_error(vm)?;
if let Some(exc) = self.take_msg_exc() {
return Err(exc);
}
buf.truncate(n);
return_data(buf, &buffer, vm)
}
@@ -3650,6 +3706,9 @@ mod _ssl {
match result {
Ok(n) => {
self.check_deferred_cert_error(vm)?;
if let Some(exc) = self.take_msg_exc() {
return Err(exc);
}
Ok(n)
}
Err(crate::ssl::compat::SslError::WantRead) => {
@@ -4038,7 +4097,7 @@ mod _ssl {
if let Some(conn) = self.connection.lock().as_mut() {
let _ = conn.write_tls(&mut bytes);
}
self.reject_connection(error.clone(), bytes);
self.reject_connection(error.clone(), bytes, vm);
return self
.accept_client_hello(vm)
.map(|_| self.io.socket_object(vm));
@@ -4150,25 +4209,35 @@ mod _ssl {
vm: &VirtualMachine,
) -> PyResult<Option<PyBytesRef>> {
let cb_type_str = cb_type.as_ref().map_or("tls-unique", |s| s.as_str());
// rustls doesn't support channel binding (tls-unique, tls-server-end-point, etc.)
// This is because:
// 1. tls-unique requires access to TLS Finished messages, which rustls doesn't expose
// 2. tls-server-end-point requires the server certificate, which we don't track here
// 3. TLS 1.3 deprecated tls-unique anyway
//
// For compatibility, we'll return None (no channel binding available)
// rather than raising an error
if cb_type_str != "tls-unique" {
return Err(vm.new_value_error(format!(
"Unsupported channel binding type '{cb_type_str}'",
)));
return Err(super::msg::unknown_binding_type_error(cb_type_str, vm));
}
// Return None to indicate channel binding is not available
// This matches the behavior when the handshake hasn't completed yet
Ok(None)
if !self.handshake_completed() {
return Ok(None);
}
let master_secret = self.key_log.master_secret();
let transcript = self.msg_state.lock().transcript().to_vec();
let session_reused = *self.session_was_reused.lock();
let suite = {
let conn_guard = self.connection.lock();
conn_guard
.as_ref()
.and_then(|conn| conn.negotiated_cipher_suite())
};
let Some(master_secret) = master_secret else {
return Ok(None);
};
let Some(suite) = suite else {
return Ok(None);
};
Ok(super::msg::tls12_unique(
suite,
&master_secret,
&transcript,
self.server_side,
session_reused,
)
.map(|bytes| vm.ctx.new_bytes(bytes)))
}
}
@@ -4212,35 +4281,24 @@ mod _ssl {
#[pyclass(name = "MemoryBIO", module = "ssl")]
#[derive(Debug, PyPayload)]
struct PyMemoryBIO {
// Internal buffer
buffer: PyMutex<Vec<u8>>,
// EOF flag
eof: PyRwLock<bool>,
inner: PyMutex<rustpython_host_env::ssl::MemoryBio>,
}
#[pyclass(with(Constructor), flags(BASETYPE))]
impl PyMemoryBIO {
#[pymethod]
fn read(&self, len: OptionalArg<i32>, vm: &VirtualMachine) -> PyResult<PyBytesRef> {
let mut buffer = self.buffer.lock();
if buffer.is_empty() && *self.eof.read() {
// Return empty bytes at EOF
return Ok(vm.ctx.new_bytes(vec![]));
}
let mut bio = self.inner.lock();
let read_len = match len {
OptionalArg::Present(n) if n >= 0 => n as usize,
OptionalArg::Present(n) => {
return Err(vm.new_value_error(format!("negative read length: {n}")));
}
OptionalArg::Missing => buffer.len(), // Read all available
OptionalArg::Missing => bio.pending(),
};
let actual_len = read_len.min(buffer.len());
let data = buffer.drain(..actual_len).collect::<Vec<u8>>();
Ok(vm.ctx.new_bytes(data))
Ok(vm.ctx.new_bytes(bio.read(read_len)))
}
#[pymethod]
@@ -4257,29 +4315,29 @@ mod _ssl {
// Convert to bytes-like object
let bytes_like = ArgBytesLike::try_from_object(vm, buf)?;
let data = bytes_like.borrow_buf();
let len = data.len();
let mut buffer = self.buffer.lock();
buffer.extend_from_slice(&data);
Ok(len)
self.inner.lock().write(&data).map_err(|err| {
vm.new_os_subtype_error(
PySSLError::class(&vm.ctx).to_owned(),
None,
err.to_string(),
)
.upcast()
})
}
#[pymethod]
fn write_eof(&self, _vm: &VirtualMachine) {
*self.eof.write() = true;
self.inner.lock().write_eof();
}
#[pygetset]
fn pending(&self) -> i32 {
self.buffer.lock().len() as i32
self.inner.lock().pending() as i32
}
#[pygetset]
fn eof(&self) -> bool {
// EOF is true only when buffer is empty AND write_eof has been called
let pending = self.buffer.lock().len();
pending == 0 && *self.eof.read()
self.inner.lock().eof()
}
}
@@ -4295,8 +4353,7 @@ mod _ssl {
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
Ok(Self {
buffer: PyMutex::new(Vec::new()),
eof: PyRwLock::new(false),
inner: PyMutex::new(rustpython_host_env::ssl::MemoryBio::new()),
})
}
}
+50 -260
View File
@@ -10,7 +10,6 @@
//! - Loading certificates from files, directories, and bytes
use alloc::sync::Arc;
use jiff::{Timestamp, Zoned, tz::TimeZone};
use parking_lot::RwLock as ParkingRwLock;
use rustls::{
DigitallySignedStruct, RootCertStore, SignatureScheme,
@@ -22,8 +21,9 @@ use rustpython_vm::{PyObjectRef, PyResult, VirtualMachine};
use std::collections::HashSet;
use x509_parser::prelude::*;
use super::{
_ssl::{VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT},
use rustpython_host_env::ssl::{
cert as ssl_cert,
constants::{VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT},
providers::CryptoExt,
};
@@ -138,87 +138,7 @@ mod cert_error {
}
}
// Helper Functions for Certificate Parsing
/// Map X.509 OID to human-readable attribute name
///
/// Converts common X.509 Distinguished Name OIDs to their standard names.
/// Returns the OID string itself if not recognized.
fn oid_to_attribute_name(oid_str: &str) -> &str {
match oid_str {
"2.5.4.3" => "commonName",
"2.5.4.6" => "countryName",
"2.5.4.7" => "localityName",
"2.5.4.8" => "stateOrProvinceName",
"2.5.4.10" => "organizationName",
"2.5.4.11" => "organizationalUnitName",
"1.2.840.113549.1.9.1" => "emailAddress",
_ => oid_str,
}
}
/// Format IP address (IPv4 or IPv6) to string
///
/// Formats raw IP address bytes according to standard notation:
/// - IPv4: dotted decimal (e.g., "192.0.2.1")
/// - IPv6: colon-separated hex (e.g., "2001:DB8:0:0:0:0:0:1")
fn format_ip_address(ip: &[u8]) -> String {
if ip.len() == 4 {
// IPv4
format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
} else if ip.len() == 16 {
// IPv6 - format in full form without compression (uppercase)
// CPython returns IPv6 in full form: 2001:DB8:0:0:0:0:0:1 (not 2001:db8::1)
let segments = [
u16::from_be_bytes([ip[0], ip[1]]),
u16::from_be_bytes([ip[2], ip[3]]),
u16::from_be_bytes([ip[4], ip[5]]),
u16::from_be_bytes([ip[6], ip[7]]),
u16::from_be_bytes([ip[8], ip[9]]),
u16::from_be_bytes([ip[10], ip[11]]),
u16::from_be_bytes([ip[12], ip[13]]),
u16::from_be_bytes([ip[14], ip[15]]),
];
format!(
"{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}",
segments[0],
segments[1],
segments[2],
segments[3],
segments[4],
segments[5],
segments[6],
segments[7]
)
} else {
// Unknown format - return as debug string
format!("{ip:?}")
}
}
/// Format ASN.1 time to string
///
/// Formats certificate validity dates in the format:
/// "Mon DD HH:MM:SS YYYY GMT"
fn format_asn1_time(time: &x509_parser::time::ASN1Time) -> String {
let timestamp =
Timestamp::from_second(time.timestamp()).expect("ASN1Time must be valid timestamp");
Zoned::new(timestamp, TimeZone::UTC)
.strftime("%b %e %H:%M:%S %Y GMT")
.to_string()
}
/// Format certificate serial number to hexadecimal string with even padding
///
/// Converts a BigUint serial number to uppercase hex string, ensuring
/// even length by prepending '0' if necessary.
fn format_serial_number(serial: &num_bigint::BigUint) -> String {
let mut serial_str = serial.to_str_radix(16).to_uppercase();
if serial_str.len() % 2 == 1 {
serial_str.insert(0, '0');
}
serial_str
}
pub(super) use rustpython_host_env::ssl::cert::is_ca_certificate;
/// Normalize wildcard hostname by stripping "*." prefix
///
@@ -228,33 +148,6 @@ fn normalize_wildcard_hostname(hostname: &str) -> &str {
hostname.strip_prefix("*.").unwrap_or(hostname)
}
// Certificate Validation and Parsing
/// Check if a certificate is a CA certificate by examining the Basic Constraints extension
///
/// Returns `true` if the certificate has Basic Constraints with CA=true,
/// `false` otherwise (including parse errors or missing extension).
/// This matches OpenSSL's X509_check_ca() behavior.
pub(super) fn is_ca_certificate(cert_der: &[u8]) -> bool {
// Parse the certificate
let Ok((_, cert)) = X509Certificate::from_der(cert_der) else {
return false;
};
// Check Basic Constraints extension
// If extension exists and CA=true, it's a CA certificate
// Otherwise (no extension or CA=false), it's NOT a CA certificate
if let Ok(Some(ext)) = cert.basic_constraints() {
return ext.value.ca;
}
// X509_check_ca() also retains OpenSSL's legacy trust-anchor rule: a
// self-issued X.509v1 certificate has no extensions at all, but is still
// classified as a CA. CPython's test CA at capath/4e1295a3.0 exercises
// precisely this case.
cert.version().0 == 0 && cert.subject() == cert.issuer()
}
/// Convert DER-encoded certificate to Python dict.
///
/// Includes OCSP, caIssuers, and crlDistributionPoints when present, and uses
@@ -263,173 +156,70 @@ pub(super) fn cert_der_to_dict_helper(
vm: &VirtualMachine,
cert_der: &[u8],
) -> PyResult<PyObjectRef> {
// Parse the certificate using x509-parser
let (_, cert) = x509_parser::parse_x509_certificate(cert_der)
.map_err(|e| vm.new_value_error(format!("Failed to parse certificate: {e}")))?;
let decoded = ssl_cert::decode_certificate(cert_der).map_err(|e| vm.new_value_error(e))?;
// Helper to convert X509Name to nested tuple format
let name_to_tuple = |name: &x509_parser::x509::X509Name<'_>| -> PyResult {
let name_to_tuple = |name: &ssl_cert::DistinguishedName| -> PyObjectRef {
let mut entries = Vec::new();
for rdn in name.iter() {
for attr in rdn.iter() {
let oid_str = attr.attr_type().to_id_string();
// Get value as bytes and convert to string
let value_str = if let Ok(s) = attr.attr_value().as_str() {
s.to_string()
} else {
let value_bytes = attr.attr_value().data;
match core::str::from_utf8(value_bytes) {
Ok(s) => s.to_string(),
Err(_) => String::from_utf8_lossy(value_bytes).into_owned(),
}
};
let key = oid_to_attribute_name(&oid_str);
for rdn in name {
for (key, value) in rdn {
let entry =
vm.new_tuple((vm.ctx.new_str(key.to_string()), vm.ctx.new_str(value_str)));
vm.new_tuple((vm.ctx.new_str(key.as_str()), vm.ctx.new_str(value.as_str())));
entries.push(vm.new_tuple((entry,)).into());
}
}
Ok(vm.ctx.new_tuple(entries).into())
vm.ctx.new_tuple(entries).into()
};
let dict = vm.ctx.new_dict();
// CPython ordering: issuer, notAfter, notBefore, serialNumber, subject, version
dict.set_item("issuer", name_to_tuple(cert.issuer())?, vm)?;
// Validity - format with GMT using jiff
dict.set_item("issuer", name_to_tuple(&decoded.issuer), vm)?;
dict.set_item("notAfter", vm.ctx.new_str(decoded.not_after).into(), vm)?;
dict.set_item("notBefore", vm.ctx.new_str(decoded.not_before).into(), vm)?;
dict.set_item(
"notAfter",
vm.ctx
.new_str(format_asn1_time(&cert.validity().not_after))
.into(),
vm,
)?;
dict.set_item(
"notBefore",
vm.ctx
.new_str(format_asn1_time(&cert.validity().not_before))
.into(),
"serialNumber",
vm.ctx.new_str(decoded.serial_number).into(),
vm,
)?;
dict.set_item("subject", name_to_tuple(&decoded.subject), vm)?;
dict.set_item("version", vm.ctx.new_int(decoded.version).into(), vm)?;
// Serial number - hex format with even length
let serial = format_serial_number(&cert.serial);
dict.set_item("serialNumber", vm.ctx.new_str(serial).into(), vm)?;
dict.set_item("subject", name_to_tuple(cert.subject())?, vm)?;
// Version
dict.set_item(
"version",
vm.ctx.new_int(cert.version().0 as i32 + 1).into(),
vm,
)?;
// Authority Information Access (OCSP and caIssuers) - use x509-parser's extensions_map
let mut ocsp_urls = Vec::new();
let mut ca_issuer_urls = Vec::new();
let mut crl_urls = Vec::new();
if let Ok(ext_map) = cert.tbs_certificate.extensions_map() {
use x509_parser::extensions::{GeneralName, ParsedExtension};
use x509_parser::oid_registry::{
OID_PKIX_AUTHORITY_INFO_ACCESS, OID_X509_EXT_CRL_DISTRIBUTION_POINTS,
};
// Authority Information Access
if let Some(ext) = ext_map.get(&OID_PKIX_AUTHORITY_INFO_ACCESS)
&& let ParsedExtension::AuthorityInfoAccess(aia) = &ext.parsed_extension()
{
for desc in &aia.accessdescs {
if let GeneralName::URI(uri) = &desc.access_location {
let method_str = desc.access_method.to_id_string();
if method_str == "1.3.6.1.5.5.7.48.1" {
// OCSP
ocsp_urls.push(vm.ctx.new_str(uri.to_string()).into());
} else if method_str == "1.3.6.1.5.5.7.48.2" {
// caIssuers
ca_issuer_urls.push(vm.ctx.new_str(uri.to_string()).into());
}
}
}
}
// CRL Distribution Points
if let Some(ext) = ext_map.get(&OID_X509_EXT_CRL_DISTRIBUTION_POINTS)
&& let ParsedExtension::CRLDistributionPoints(cdp) = &ext.parsed_extension()
{
for dp in &cdp.points {
if let Some(dist_point) = &dp.distribution_point {
use x509_parser::extensions::DistributionPointName;
if let DistributionPointName::FullName(names) = dist_point {
for name in names {
if let GeneralName::URI(uri) = name {
crl_urls.push(vm.ctx.new_str(uri.to_string()).into());
}
}
}
}
}
}
if !decoded.ocsp.is_empty() {
let urls = decoded
.ocsp
.into_iter()
.map(|url| vm.ctx.new_str(url).into())
.collect();
dict.set_item("OCSP", vm.ctx.new_tuple(urls).into(), vm)?;
}
if !decoded.ca_issuers.is_empty() {
let urls = decoded
.ca_issuers
.into_iter()
.map(|url| vm.ctx.new_str(url).into())
.collect();
dict.set_item("caIssuers", vm.ctx.new_tuple(urls).into(), vm)?;
}
if !decoded.crl_distribution_points.is_empty() {
let urls = decoded
.crl_distribution_points
.into_iter()
.map(|url| vm.ctx.new_str(url).into())
.collect();
dict.set_item("crlDistributionPoints", vm.ctx.new_tuple(urls).into(), vm)?;
}
if !ocsp_urls.is_empty() {
dict.set_item("OCSP", vm.ctx.new_tuple(ocsp_urls).into(), vm)?;
}
if !ca_issuer_urls.is_empty() {
dict.set_item("caIssuers", vm.ctx.new_tuple(ca_issuer_urls).into(), vm)?;
}
if !crl_urls.is_empty() {
dict.set_item(
"crlDistributionPoints",
vm.ctx.new_tuple(crl_urls).into(),
vm,
)?;
}
// Subject Alternative Names
if let Ok(Some(san_ext)) = cert.subject_alternative_name() {
if !decoded.subject_alt_names.is_empty() {
let mut san_entries = Vec::new();
for name in &san_ext.value.general_names {
use x509_parser::extensions::GeneralName;
match name {
GeneralName::DNSName(dns) => {
san_entries.push(vm.new_tuple(("DNS", *dns)).into());
}
GeneralName::IPAddress(ip) => {
let ip_str = format_ip_address(ip);
san_entries.push(vm.new_tuple(("IP Address", ip_str)).into());
}
GeneralName::RFC822Name(email) => {
san_entries.push(vm.new_tuple(("email", *email)).into());
}
GeneralName::URI(uri) => {
san_entries.push(vm.new_tuple(("URI", *uri)).into());
}
GeneralName::OtherName(_oid, _data) => {
// OtherName is not fully supported, mark as unsupported
san_entries.push(vm.new_tuple(("othername", "<unsupported>")).into());
}
GeneralName::DirectoryName(name) => {
// Convert X509Name to nested tuple format
let dir_tuple = name_to_tuple(name)?;
san_entries.push(vm.new_tuple(("DirName", dir_tuple)).into());
}
GeneralName::RegisteredID(oid) => {
// Convert OID to string representation
let oid_str = oid.to_id_string();
san_entries.push(vm.new_tuple(("Registered ID", oid_str)).into());
}
_ => {}
for name in decoded.subject_alt_names {
if name.kind == "DirName" {
san_entries.push(
vm.new_tuple(("DirName", name_to_tuple(&name.directory_name)))
.into(),
);
} else {
san_entries.push(vm.new_tuple((name.kind, name.value)).into());
}
}
if !san_entries.is_empty() {
dict.set_item("subjectAltName", vm.ctx.new_tuple(san_entries).into(), vm)?;
}
dict.set_item("subjectAltName", vm.ctx.new_tuple(san_entries).into(), vm)?;
}
Ok(dict.into())
+13 -3
View File
@@ -32,8 +32,10 @@ use rustpython_vm::function::ArgBytesLike;
use rustpython_vm::{AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject};
use std::io::Read;
use super::chain::{self, Purpose, VerifiedChainBuilder};
use super::providers::CryptoExt;
use rustpython_host_env::ssl::{
chain::{self, Purpose, VerifiedChainBuilder},
providers::CryptoExt,
};
// Import PySSLSocket from parent module
use super::_ssl::{
@@ -897,6 +899,7 @@ pub(super) fn send_all_bytes(
deadline: Option<std::time::Instant>,
) -> SslResult<()> {
// Retain newly drained records before a fallible flush of earlier output.
socket.observe_tls(true, &buf, vm);
socket.pending_tls_output.lock().extend_from_slice(&buf);
socket
.flush_pending_tls_output(vm, deadline)
@@ -969,6 +972,7 @@ pub(super) fn recv_at_most_one_tls_record(
SslError::Py(e)
}
})?;
socket.observe_tls(false, bytes.as_bytes(), vm);
if bytes.is_empty() {
Err(if socket.is_bio_mode() && !socket.transport_eof() {
SslError::WantRead
@@ -1689,6 +1693,12 @@ fn ssl_ensure_data_available(socket: &PySSLSocket, vm: &VirtualMachine) -> SslRe
return Err(SslError::Eof);
}
if is_bio {
let bytes = ArgBytesLike::try_from_object(vm, data.clone())
.map_err(|_| SslError::Syscall("Expected bytes-like object".to_string()))?;
socket.observe_tls(false, bytes.borrow_buf().as_ref(), vm);
}
// Feed data to rustls and process packets
with_conn_mut(socket, vm, |conn| {
ssl_read_tls_records(conn, data, is_bio, vm)?;
@@ -1754,7 +1764,7 @@ impl ResolvesServerCert for MultiCertResolver {
pub(super) fn curve_name_to_kx_group(
curve: &str,
) -> Result<Vec<&'static dyn SupportedKxGroup>, String> {
super::cipher::kx_group_by_openssl_name(curve)
rustpython_host_env::ssl::cipher::kx_group_by_openssl_name(curve)
.map(|group| vec![group])
.ok_or_else(|| format!("unknown curve name '{curve}'"))
}
+18 -5
View File
@@ -11,25 +11,38 @@ pub(super) struct KeyLog(pub AppendLog);
/// Each connection follows its current Python context. This routing state must
/// not be shared through the context's config cache or an explicit SSLSession.
#[derive(Debug)]
pub(super) struct ConnectionKeyLog(parking_lot::RwLock<Arc<KeyLog>>);
pub(super) struct ConnectionKeyLog {
sink: parking_lot::RwLock<Arc<KeyLog>>,
master_secret: parking_lot::Mutex<Option<Vec<u8>>>,
}
impl ConnectionKeyLog {
pub(super) fn new(sink: Arc<KeyLog>) -> Self {
Self(parking_lot::RwLock::new(sink))
Self {
sink: parking_lot::RwLock::new(sink),
master_secret: parking_lot::Mutex::new(None),
}
}
pub(super) fn set_sink(&self, sink: Arc<KeyLog>) {
*self.0.write() = sink;
*self.sink.write() = sink;
}
pub(super) fn master_secret(&self) -> Option<Vec<u8>> {
self.master_secret.lock().clone()
}
}
impl rustls::KeyLog for ConnectionKeyLog {
fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) {
self.0.read().log(label, client_random, secret);
if label == "CLIENT_RANDOM" {
*self.master_secret.lock() = Some(secret.to_vec());
}
self.sink.read().log(label, client_random, secret);
}
fn will_log(&self, label: &str) -> bool {
self.0.read().will_log(label)
label == "CLIENT_RANDOM" || self.sink.read().will_log(label)
}
}
+9
View File
@@ -0,0 +1,9 @@
// TLS record observation adapter. The engine lives in rustpython-common.
use rustpython_vm::{VirtualMachine, builtins::PyBaseExceptionRef};
pub(super) use rustpython_host_env::ssl::msg::{MsgState, tls12_unique};
pub(super) fn unknown_binding_type_error(cb_type: &str, vm: &VirtualMachine) -> PyBaseExceptionRef {
vm.new_value_error(format!("'{cb_type}' channel binding type not implemented"))
}