ssl: move TlsConnection and session store to host_env (#8678)

TlsConnection, RecordCursor, rustls error mapping, hostname/ALPN/version
helpers, and the capturing client session store now live in host_env.
stdlib _ssl keeps socket/MemoryBIO I/O and Python objects.

Remove unused stdlib sha2 and leftover workspace TLS crates.

Assisted-by: Grok:grok-4.6
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-09 04:04:16 +09:00
1 parent b701e5cbe9
commit 979576e2e4
11 files changed
+781 -643

No files matched your search

Generated
+1 -1
View File
@@ -3365,6 +3365,7 @@ dependencies = [
"rustpython-wtf8",
"rustyline",
"schannel",
"sha2",
"socket2",
"system-configuration",
"termios",
@@ -3542,7 +3543,6 @@ dependencies = [
"rustpython-unicode",
"rustpython-vm",
"scopeguard",
"sha2",
"tcl-sys",
"tk-sys",
"uuid",
+2 -1
View File
@@ -10,7 +10,7 @@ license.workspace = true
[features]
native-certs = ["dep:rustls-native-certs"]
ssl = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-webpki", "dep:x509-parser", "dep:der", "dep:pkcs8"]
ssl = ["dep:rustls", "dep:rustls-pemfile", "dep:rustls-webpki", "dep:x509-parser", "dep:der", "dep:pkcs8", "dep:sha2"]
[dependencies]
rustpython-wtf8 = { workspace = true }
@@ -28,6 +28,7 @@ rustls-webpki = { workspace = true, optional = true }
x509-parser = { workspace = true, optional = true }
der = { workspace = true, optional = true }
pkcs8 = { workspace = true, features = ["encryption", "pkcs5", "pem"], optional = true }
sha2 = { workspace = true, optional = true }
[target.'cfg(unix)'.dependencies]
nix = { workspace = true }
+254
View File
@@ -0,0 +1,254 @@
//! Sans-I/O rustls connection plus one-record transport helpers.
use std::io::{Read, Write};
use rustls::{
Connection, HandshakeKind, IoState, ProtocolVersion, SupportedCipherSuite,
pki_types::CertificateDer,
};
use super::error::TlsError;
pub const TLS_RECORD_HEADER_SIZE: usize = 5;
const SSL3_RT_MAX_MD_SIZE: usize = 64;
const SSL3_RT_MAX_PLAIN_LENGTH: usize = 16384;
const SSL3_RT_MAX_COMPRESSED_OVERHEAD: usize = 1024;
const SSL3_RT_MAX_ENCRYPTED_OVERHEAD: usize = 256 + SSL3_RT_MAX_MD_SIZE;
const SSL3_RT_MAX_COMPRESSED_LENGTH: usize =
SSL3_RT_MAX_PLAIN_LENGTH + SSL3_RT_MAX_COMPRESSED_OVERHEAD;
const SSL3_RT_MAX_ENCRYPTED_LENGTH: usize =
SSL3_RT_MAX_ENCRYPTED_OVERHEAD + SSL3_RT_MAX_COMPRESSED_LENGTH;
pub const SSL3_RT_MAX_PACKET_SIZE: usize = SSL3_RT_MAX_ENCRYPTED_LENGTH + TLS_RECORD_HEADER_SIZE;
/// One TLS record's header and remaining body, so a transport read never
/// lifts more than the record currently being parsed.
#[derive(Debug, Default)]
pub struct RecordCursor {
header: [u8; TLS_RECORD_HEADER_SIZE],
header_read: usize,
body_left: usize,
}
impl RecordCursor {
/// Bytes still owed before the next record boundary.
#[must_use]
pub fn want(&self) -> usize {
if self.body_left > 0 {
self.body_left
} else {
TLS_RECORD_HEADER_SIZE - self.header_read
}
}
/// True while the 5-byte header is still incomplete, or after a
/// zero-length body (the next read starts a new header).
#[must_use]
pub fn in_header(&self) -> bool {
self.body_left == 0
}
/// Account for `data`, which must be no longer than [`want`].
pub fn consume(&mut self, data: &[u8]) {
if self.body_left > 0 {
self.body_left -= data.len();
return;
}
self.header[self.header_read..self.header_read + data.len()].copy_from_slice(data);
self.header_read += data.len();
if self.header_read == TLS_RECORD_HEADER_SIZE {
self.body_left = u16::from_be_bytes([self.header[3], self.header[4]]) as usize;
self.header_read = 0;
}
}
}
/// rustls connection plus write-retry state. Unsent TLS stays on the
/// interpreter object so the rustls lock is not held across socket I/O.
#[derive(Debug)]
pub struct TlsConnection {
inner: Connection,
write_buffered_len: usize,
}
impl TlsConnection {
#[must_use]
pub fn new(inner: Connection) -> Self {
Self {
inner,
write_buffered_len: 0,
}
}
#[must_use]
pub fn inner(&self) -> &Connection {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut Connection {
&mut self.inner
}
#[must_use]
pub fn wants_read(&self) -> bool {
self.inner.wants_read()
}
#[must_use]
pub fn wants_write(&self) -> bool {
self.inner.wants_write()
}
#[must_use]
pub fn is_handshaking(&self) -> bool {
self.inner.is_handshaking()
}
#[must_use]
pub fn protocol_version(&self) -> Option<ProtocolVersion> {
self.inner.protocol_version()
}
#[must_use]
pub fn negotiated_cipher_suite(&self) -> Option<SupportedCipherSuite> {
self.inner.negotiated_cipher_suite()
}
#[must_use]
pub fn alpn_protocol(&self) -> Option<&[u8]> {
self.inner.alpn_protocol()
}
#[must_use]
pub fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
self.inner.peer_certificates()
}
#[must_use]
pub fn handshake_kind(&self) -> Option<HandshakeKind> {
self.inner.handshake_kind()
}
#[must_use]
pub fn write_buffered_len(&self) -> usize {
self.write_buffered_len
}
pub fn set_write_buffered_len(&mut self, n: usize) {
self.write_buffered_len = n;
}
#[must_use]
pub fn pending_plaintext(&mut self) -> usize {
use std::io::BufRead;
self.inner.reader().fill_buf().map_or(0, |buf| buf.len())
}
pub fn send_close_notify(&mut self) {
self.inner.send_close_notify();
}
pub fn process_packets(&mut self) -> Result<IoState, TlsError> {
self.inner
.process_new_packets()
.map_err(TlsError::from_rustls)
}
pub fn peer_has_closed(&mut self) -> Result<bool, TlsError> {
Ok(self.process_packets()?.peer_has_closed())
}
/// Drain every record rustls has queued for the peer.
pub fn drain_tls(&mut self) -> Result<Vec<u8>, TlsError> {
let mut buf = Vec::new();
let n = self.inner.write_tls(&mut buf).map_err(TlsError::Io)?;
if n > 0 { Ok(buf) } else { Ok(Vec::new()) }
}
/// Feed ciphertext. An empty slice means the transport returned EOF.
pub fn feed_tls(&mut self, data: &[u8]) -> Result<(), TlsError> {
if data.is_empty() {
return if self.peer_has_closed()? {
Err(TlsError::ZeroReturn)
} else {
Err(TlsError::Eof)
};
}
let mut offset = 0;
while offset < data.len() {
let remaining = &data[offset..];
let mut cursor = std::io::Cursor::new(remaining);
match self.inner.read_tls(&mut cursor) {
Ok(0) => {
self.process_packets()?;
let mut retry = std::io::Cursor::new(remaining);
match self.inner.read_tls(&mut retry) {
Ok(0) => break,
Ok(n) => {
offset += n;
if offset < data.len() {
self.process_packets()?;
}
}
Err(error) => return Err(TlsError::Io(error)),
}
}
Ok(read) => {
offset += read;
if offset < data.len() {
self.process_packets()?;
}
}
Err(error) => return Err(TlsError::Io(error)),
}
}
Ok(())
}
/// Read application data. `None` means rustls has no plaintext yet.
pub fn read_plaintext(&mut self, buf: &mut [u8]) -> Result<Option<usize>, TlsError> {
match self.inner.reader().read(buf) {
Ok(n) => Ok(Some(n)),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
Err(error) => Err(TlsError::Io(error)),
}
}
/// Write application data. `is_bio` turns a full buffer into `WantWrite`.
pub fn write_plaintext(&mut self, data: &[u8], is_bio: bool) -> Result<usize, TlsError> {
match self.inner.writer().write(data) {
Ok(0) if !data.is_empty() => {
if is_bio {
Err(TlsError::WantWrite)
} else {
Err(TlsError::Syscall("Write failed: buffer full".to_string()))
}
}
Ok(n) => Ok(n),
Err(error) => {
if is_bio {
Err(TlsError::WantWrite)
} else {
Err(TlsError::Syscall(format!("Write failed: {error}")))
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn record_cursor_splits_header_and_body() {
let mut cursor = RecordCursor::default();
assert_eq!(cursor.want(), 5);
cursor.consume(&[22, 3, 3]);
assert_eq!(cursor.want(), 2);
cursor.consume(&[0, 4]);
assert_eq!(cursor.want(), 4);
cursor.consume(&[1, 2, 3, 4]);
assert_eq!(cursor.want(), 5);
}
}
+5
View File
@@ -19,3 +19,8 @@ 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;
pub const PROTO_TLSV1_2: i32 = 0x0303;
pub const PROTO_TLSV1_3: i32 = 0x0304;
pub const OP_NO_TLSV1_2: i32 = 0x0800_0000;
pub const OP_NO_TLSV1_3: i32 = 0x2000_0000;
+68
View File
@@ -0,0 +1,68 @@
//! rustls errors mapped to the `_ssl` error space, without Python exceptions.
use rustls::CertificateError;
const ERR_LIB_SSL: i32 = 20;
/// TLS engine error before the interpreter builds an exception.
#[derive(Debug)]
pub enum TlsError {
WantRead,
WantWrite,
Syscall(String),
Ssl(String),
ZeroReturn,
Eof,
PreauthData,
CertVerification(CertificateError),
Io(std::io::Error),
Timeout(String),
AlertReceived { lib: i32, reason: i32 },
NoCipherSuites,
}
impl TlsError {
fn alert_to_openssl_reason(alert: rustls::AlertDescription) -> i32 {
1000 + i32::from(u8::from(alert))
}
/// Map a rustls error into the `_ssl` error space.
#[must_use]
pub fn from_rustls(err: rustls::Error) -> Self {
match err {
rustls::Error::InvalidCertificate(cert_err) => Self::CertVerification(cert_err),
rustls::Error::AlertReceived(alert_desc) => match alert_desc {
rustls::AlertDescription::CloseNotify => Self::ZeroReturn,
_ => Self::AlertReceived {
lib: ERR_LIB_SSL,
reason: Self::alert_to_openssl_reason(alert_desc),
},
},
rustls::Error::InvalidMessage(_) => Self::Eof,
rustls::Error::PeerIncompatible(peer_err) => {
use rustls::PeerIncompatible;
match peer_err {
PeerIncompatible::NoCipherSuitesInCommon => Self::NoCipherSuites,
_ => Self::Eof,
}
}
other => Self::Ssl(format!("{other}")),
}
}
#[must_use]
pub fn is_eof(&self) -> bool {
matches!(self, Self::Eof)
}
#[must_use]
pub fn is_zero_return(&self) -> bool {
matches!(self, Self::ZeroReturn)
}
}
impl From<std::io::Error> for TlsError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
+14
View File
@@ -10,16 +10,30 @@ pub mod cert;
pub mod chain;
pub mod cipher;
pub mod config;
pub mod connection;
pub mod constants;
pub mod error;
pub mod handshake;
pub mod keylog;
pub mod msg;
pub mod oid;
pub mod protocol;
pub mod providers;
pub mod session;
pub mod verify;
pub mod x509;
pub use bio::{MemoryBio, MemoryBioError};
pub use cert::{DecodedCertificate, decode_certificate, is_ca_certificate};
pub use connection::{
RecordCursor, SSL3_RT_MAX_PACKET_SIZE, TLS_RECORD_HEADER_SIZE, TlsConnection,
};
pub use constants::*;
pub use error::TlsError;
pub use protocol::{
AlpnError, HostnameError, parse_length_prefixed_alpn, rustls_versions, validate_hostname,
};
pub use providers::CryptoExt;
pub use session::{
CapturingClientSessionStore, ClientSessionKind, SESSION_CACHE_SIZE, SessionCache, SessionData,
};
+150
View File
@@ -0,0 +1,150 @@
//! Protocol version, ALPN, and hostname helpers.
use rustls::version::{TLS12, TLS13};
use super::constants::{OP_NO_TLSV1_2, OP_NO_TLSV1_3, PROTO_TLSV1_2, PROTO_TLSV1_3};
/// Why `server_hostname` was rejected.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostnameError {
Empty,
LeadingDot,
EmbeddedNul,
TooLong,
}
impl HostnameError {
#[must_use]
pub fn message(&self) -> &'static str {
match self {
Self::Empty => "server_hostname cannot be an empty string",
Self::LeadingDot => "server_hostname cannot start with a dot",
Self::EmbeddedNul => "embedded null character",
Self::TooLong => "server_hostname is too long (maximum 253 characters)",
}
}
}
/// Validate `server_hostname` the way `_SSLSocket` does.
pub fn validate_hostname(hostname: &str) -> Result<(), HostnameError> {
if hostname.is_empty() {
return Err(HostnameError::Empty);
}
if hostname.starts_with('.') {
return Err(HostnameError::LeadingDot);
}
if hostname.as_bytes().contains(&0) {
return Err(HostnameError::EmbeddedNul);
}
if hostname.len() > 253 {
return Err(HostnameError::TooLong);
}
Ok(())
}
/// Convert PROTO/OP bits into the rustls version slice.
#[must_use]
pub fn rustls_versions(
minimum: i32,
maximum: i32,
options: i32,
) -> &'static [&'static rustls::SupportedProtocolVersion] {
static TLS12_ONLY: &[&rustls::SupportedProtocolVersion] = &[&TLS12];
static TLS13_ONLY: &[&rustls::SupportedProtocolVersion] = &[&TLS13];
let min = if minimum == -2 {
PROTO_TLSV1_2
} else {
minimum
};
let max = if maximum == -1 {
PROTO_TLSV1_3
} else {
maximum
};
let tls12_disabled = options & OP_NO_TLSV1_2 != 0;
let tls13_disabled = options & OP_NO_TLSV1_3 != 0;
let want_tls12 =
(min == 0 || min <= PROTO_TLSV1_2) && (max == 0 || max >= PROTO_TLSV1_2) && !tls12_disabled;
let want_tls13 =
(min == 0 || min <= PROTO_TLSV1_3) && (max == 0 || max >= PROTO_TLSV1_3) && !tls13_disabled;
match (want_tls12, want_tls13) {
(true, true) | (false, false) => rustls::DEFAULT_VERSIONS,
(true, false) => TLS12_ONLY,
(false, true) => TLS13_ONLY,
}
}
/// Why a length-prefixed ALPN list was rejected.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlpnError(pub String);
/// Parse `[len][proto]...` the way `ssl.py` hands it to `_set_alpn_protocols`.
pub fn parse_length_prefixed_alpn(bytes: &[u8]) -> Result<Vec<Vec<u8>>, AlpnError> {
let mut alpn_list = Vec::new();
let mut offset = 0;
while offset < bytes.len() {
if offset + 1 > bytes.len() {
return Err(AlpnError(format!(
"Invalid ALPN protocol data: unexpected end at offset {offset}"
)));
}
let proto_len = bytes[offset] as usize;
offset += 1;
if proto_len == 0 {
return Err(AlpnError(format!(
"Invalid ALPN protocol data: protocol length cannot be 0 at offset {}",
offset - 1
)));
}
if offset + proto_len > bytes.len() {
return Err(AlpnError(format!(
"Invalid ALPN protocol data: expected {} bytes at offset {}, but only {} bytes remain",
proto_len,
offset,
bytes.len() - offset
)));
}
alpn_list.push(bytes[offset..offset + proto_len].to_vec());
offset += proto_len;
}
Ok(alpn_list)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hostname_rejects_empty_dot_nul_and_overlong() {
assert_eq!(validate_hostname(""), Err(HostnameError::Empty));
assert_eq!(
validate_hostname(".example"),
Err(HostnameError::LeadingDot)
);
assert_eq!(
validate_hostname("ex\0ample"),
Err(HostnameError::EmbeddedNul)
);
assert_eq!(
validate_hostname(&"a".repeat(254)),
Err(HostnameError::TooLong)
);
assert!(validate_hostname("localhost").is_ok());
}
#[test]
fn alpn_reads_length_prefixed_protocols() {
assert_eq!(
parse_length_prefixed_alpn(&[
2, b'h', b'2', 8, b'h', b't', b't', b'p', b'/', b'1', b'.', b'1'
])
.unwrap(),
[b"h2".to_vec(), b"http/1.1".to_vec()]
);
assert!(parse_length_prefixed_alpn(&[0]).is_err());
assert!(parse_length_prefixed_alpn(&[3, b'h', b'2']).is_err());
}
}
+151
View File
@@ -0,0 +1,151 @@
//! rustls client session store plus synthetic session metadata.
use alloc::sync::Arc;
use core::sync::atomic::{AtomicUsize, Ordering};
use std::{collections::HashMap, time::SystemTime};
use parking_lot::{Mutex, RwLock};
use rustls::{
NamedGroup,
client::{
ClientSessionMemoryCache, ClientSessionStore, Tls12ClientSessionValue,
Tls13ClientSessionValue,
},
pki_types::ServerName,
};
use sha2::{Digest, Sha256};
pub const SESSION_CACHE_SIZE: usize = 256;
static NEXT_SESSION_NONCE: AtomicUsize = AtomicUsize::new(1);
/// Synthetic session metadata. rustls does not expose ticket bytes.
#[derive(Debug, Clone)]
pub struct SessionData {
pub server_name: String,
pub session_id: Vec<u8>,
pub creation_time: SystemTime,
pub lifetime: u64,
}
impl SessionData {
#[must_use]
pub fn new(server_name: &str, lifetime: u64) -> Self {
let creation_time = SystemTime::now();
let nonce = NEXT_SESSION_NONCE.fetch_add(1, Ordering::Relaxed);
let mut hasher = Sha256::new();
hasher.update(server_name.as_bytes());
hasher.update(
creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_le_bytes(),
);
hasher.update(nonce.to_le_bytes());
Self {
server_name: server_name.to_owned(),
session_id: hasher.finalize()[..16].to_vec(),
creation_time,
lifetime,
}
}
}
pub type SessionCache = Arc<RwLock<HashMap<Vec<u8>, Arc<Mutex<SessionData>>>>>;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClientSessionKind {
Tls12,
Tls13,
}
/// rustls session store that also records metadata for `SSLSession`.
#[derive(Debug)]
pub struct CapturingClientSessionStore {
inner: Arc<ClientSessionMemoryCache>,
session_cache: SessionCache,
}
impl CapturingClientSessionStore {
#[must_use]
pub fn new(session_cache: SessionCache) -> Self {
Self {
inner: Arc::new(ClientSessionMemoryCache::new(SESSION_CACHE_SIZE)),
session_cache,
}
}
pub fn transfer_session(
&self,
target: &Self,
server_name: &ServerName<'static>,
kind: ClientSessionKind,
) {
if let Some(group) = self.kx_hint(server_name) {
target.set_kx_hint(server_name.clone(), group);
}
match kind {
ClientSessionKind::Tls12 => {
if let Some(session) = self.tls12_session(server_name) {
target.set_tls12_session(server_name.clone(), session);
}
}
ClientSessionKind::Tls13 => {
if let Some(ticket) = self.take_tls13_ticket(server_name) {
target.insert_tls13_ticket(server_name.clone(), ticket);
}
}
}
}
fn record_metadata(&self, server_name: &ServerName<'static>) {
let server_name_str = server_name.to_str();
let session_data = SessionData::new(server_name_str.as_ref(), 7200);
self.session_cache.write().insert(
server_name_str.as_bytes().to_vec(),
Arc::new(Mutex::new(session_data)),
);
}
}
impl ClientSessionStore for CapturingClientSessionStore {
fn set_kx_hint(&self, server_name: ServerName<'static>, group: NamedGroup) {
self.inner.set_kx_hint(server_name, group);
}
fn kx_hint(&self, server_name: &ServerName<'_>) -> Option<NamedGroup> {
self.inner.kx_hint(server_name)
}
fn set_tls12_session(&self, server_name: ServerName<'static>, value: Tls12ClientSessionValue) {
self.inner.set_tls12_session(server_name.clone(), value);
self.record_metadata(&server_name);
}
fn tls12_session(&self, server_name: &ServerName<'_>) -> Option<Tls12ClientSessionValue> {
self.inner.tls12_session(server_name)
}
fn remove_tls12_session(&self, server_name: &ServerName<'static>) {
self.inner.remove_tls12_session(server_name);
self.session_cache
.write()
.remove(server_name.to_str().as_bytes());
}
fn insert_tls13_ticket(
&self,
server_name: ServerName<'static>,
value: Tls13ClientSessionValue,
) {
self.inner.insert_tls13_ticket(server_name.clone(), value);
self.record_metadata(&server_name);
}
fn take_tls13_ticket(
&self,
server_name: &ServerName<'static>,
) -> Option<Tls13ClientSessionValue> {
self.inner.take_tls13_ticket(server_name)
}
}
-1
View File
@@ -65,7 +65,6 @@ rand = { workspace = true }
mt19937 = { workspace = true }
# Crypto:
sha2 = { workspace = true }
constant_time_eq = { workspace = true }
# compression
+82 -430
View File
@@ -5,9 +5,8 @@
//! This module provides SSL/TLS support without requiring C dependencies.
//! It implements the Python ssl module API using:
//! - rustls: TLS protocol implementation
//! - x509-parser/x509-cert: Certificate parsing
//! - x509-parser: Certificate parsing
//! - ring: Cryptographic primitives
//! - rustls-platform-verifier: Platform-native certificate verification
//!
//! DO NOT add openssl dependency here.
//!
@@ -44,8 +43,8 @@ mod _ssl {
AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
VirtualMachine,
builtins::{
PyBaseExceptionRef, PyByteArray, PyBytesRef, PyListRef, PyStrRef, PyType,
PyTypeRef, PyUtf8StrRef, PyWeak,
PyBaseExceptionRef, PyBytesRef, PyListRef, PyStrRef, PyType, PyTypeRef,
PyUtf8StrRef, PyWeak,
},
convert::IntoPyException,
function::{
@@ -69,27 +68,26 @@ mod _ssl {
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use memchr::memchr;
use rustpython_vm::exceptions;
use std::{
collections::{HashMap, hash_map::DefaultHasher},
io::BufRead,
time::SystemTime,
};
// Rustls imports
use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock};
use parking_lot::RwLock as ParkingRwLock;
use pem_rfc7468::{LineEnding, encode_string};
use rustls::{
ClientConnection, Connection, HandshakeKind, RootCertStore,
client::{ClientSessionMemoryCache, ClientSessionStore},
crypto::SupportedKxGroup,
pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer, ServerName},
server::{Accepted, ResolvesServerCert},
sign::CertifiedKey,
version::{TLS12, TLS13},
};
use sha2::{Digest, Sha256};
use rustpython_host_env::ssl::{
CapturingClientSessionStore, ClientSessionKind, RecordCursor, SessionCache, SessionData,
TlsConnection,
};
/// Caps unsent TLS so BIO writes stay inside asyncio's 64 KiB
/// socket-transport high-water mark when SO_SNDBUF is small.
@@ -149,8 +147,6 @@ mod _ssl {
#[pyattr]
const PROTOCOL_TLSv1_3: i32 = rustpython_host_env::ssl::PROTOCOL_TLSV1_3;
static NEXT_SSL_SESSION_NONCE: AtomicUsize = AtomicUsize::new(1);
// Protocol version constants for TLSVersion enum
#[pyattr]
const PROTO_SSLv3: i32 = 0x0300;
@@ -159,9 +155,9 @@ mod _ssl {
#[pyattr]
const PROTO_TLSv1_1: i32 = 0x0302;
#[pyattr]
const PROTO_TLSv1_2: i32 = 0x0303;
const PROTO_TLSv1_2: i32 = rustpython_host_env::ssl::PROTO_TLSV1_2;
#[pyattr]
const PROTO_TLSv1_3: i32 = 0x0304;
const PROTO_TLSv1_3: i32 = rustpython_host_env::ssl::PROTO_TLSV1_3;
// Minimum and maximum supported protocol versions for rustls
// Use special values -2 and -1 to avoid enum name conflicts
@@ -177,30 +173,9 @@ mod _ssl {
// Buffer sizes and limits (OpenSSL/CPython compatibility)
const PEM_BUFSIZE: usize = 1024;
// OpenSSL: ssl/ssl_local.h
const SSL3_RT_HEADER_LENGTH: usize = 5;
// This is the maximum MAC (digest) size used by the SSL library. Currently
// maximum of 20 is used by SHA1, but we reserve for future extension for
// 512-bit hashes.
const SSL3_RT_MAX_MD_SIZE: usize = 64;
// Maximum plaintext length: defined by SSL/TLS standards
const SSL3_RT_MAX_PLAIN_LENGTH: usize = 16384;
// Maximum compression overhead: defined by SSL/TLS standards
const SSL3_RT_MAX_COMPRESSED_OVERHEAD: usize = 1024;
// The standards give a maximum encryption overhead of 1024 bytes. In
// practice the value is lower than this. The overhead is the maximum number
// of padding bytes (256) plus the mac size.
const SSL3_RT_MAX_ENCRYPTED_OVERHEAD: usize = 256 + SSL3_RT_MAX_MD_SIZE;
const SSL3_RT_MAX_COMPRESSED_LENGTH: usize =
SSL3_RT_MAX_PLAIN_LENGTH + SSL3_RT_MAX_COMPRESSED_OVERHEAD;
const SSL3_RT_MAX_ENCRYPTED_LENGTH: usize =
SSL3_RT_MAX_ENCRYPTED_OVERHEAD + SSL3_RT_MAX_COMPRESSED_LENGTH;
pub(crate) const SSL3_RT_MAX_PACKET_SIZE: usize =
SSL3_RT_MAX_ENCRYPTED_LENGTH + SSL3_RT_HEADER_LENGTH;
// SSL session cache size (common practice, similar to OpenSSL defaults)
const SSL_SESSION_CACHE_SIZE: usize = 256;
rustpython_host_env::ssl::SSL3_RT_MAX_PACKET_SIZE;
const SSL_SESSION_CACHE_SIZE: usize = rustpython_host_env::ssl::SESSION_CACHE_SIZE;
// Certificate verification modes
#[pyattr]
@@ -242,9 +217,9 @@ mod _ssl {
#[pyattr]
const OP_NO_TLSv1_1: i32 = 0x10000000;
#[pyattr]
const OP_NO_TLSv1_2: i32 = 0x08000000;
const OP_NO_TLSv1_2: i32 = rustpython_host_env::ssl::OP_NO_TLSV1_2;
#[pyattr]
const OP_NO_TLSv1_3: i32 = 0x20000000;
const OP_NO_TLSv1_3: i32 = rustpython_host_env::ssl::OP_NO_TLSV1_3;
#[pyattr]
const OP_NO_COMPRESSION: i32 = 0x00020000;
#[pyattr]
@@ -389,263 +364,23 @@ mod _ssl {
#[pyattr]
const ENCODING_PEM_AUX: i32 = 0x101; // PEM + 0x100
/// Validate server hostname for TLS SNI
///
/// Checks that the hostname:
/// - Is not empty
/// - Does not start with a dot
/// - Is not an IP address (SNI requires DNS names)
/// - Does not contain null bytes
/// - Does not exceed 253 characters (DNS limit)
///
/// Returns Ok(()) if validation passes, or an appropriate error.
/// Validate server hostname for TLS SNI.
fn validate_hostname(hostname: &str, vm: &VirtualMachine) -> PyResult<()> {
if hostname.is_empty() {
return Err(vm.new_value_error("server_hostname cannot be an empty string"));
}
if hostname.starts_with('.') {
return Err(vm.new_value_error("server_hostname cannot start with a dot"));
}
// IP addresses are allowed as server_hostname
// SNI will not be sent for IP addresses
if memchr(b'\0', hostname.as_bytes()).is_some() {
cold_path();
return Err(exceptions::nul_char_type_error(vm));
}
if hostname.len() > 253 {
return Err(vm.new_value_error("server_hostname is too long (maximum 253 characters)"));
}
Ok(())
}
// Session data structure for tracking TLS sessions
#[derive(Debug, Clone)]
struct SessionData {
_server_name: String,
session_id: Vec<u8>,
creation_time: SystemTime,
lifetime: u64,
}
impl SessionData {
// NOTE: This is NOT the actual TLS session ID, just a unique identifier.
fn new(server_name: &str, lifetime: u64) -> Self {
let creation_time = SystemTime::now();
let nonce = NEXT_SSL_SESSION_NONCE.fetch_add(1, Ordering::Relaxed);
let mut hasher = Sha256::new();
hasher.update(server_name.as_bytes());
hasher.update(
creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_le_bytes(),
);
hasher.update(nonce.to_le_bytes());
Self {
_server_name: server_name.to_owned(),
session_id: hasher.finalize()[..16].to_vec(),
creation_time,
lifetime,
use rustpython_host_env::ssl::HostnameError;
match rustpython_host_env::ssl::validate_hostname(hostname) {
Ok(()) => Ok(()),
Err(HostnameError::EmbeddedNul) => {
cold_path();
Err(exceptions::nul_char_type_error(vm))
}
Err(error) => Err(vm.new_value_error(error.message().to_owned())),
}
}
// Type alias to simplify complex session cache type
type SessionCache = Arc<ParkingRwLock<HashMap<Vec<u8>, Arc<ParkingMutex<SessionData>>>>>;
// SESSION EMULATION IMPLEMENTATION
//
// IMPORTANT: This is an EMULATION of CPython's SSL session management.
// Rustls 0.23 does NOT expose session data (ticket bytes, session IDs, etc.)
// through public APIs. All session value fields are private.
//
// LIMITATIONS:
// - Session IDs are generated from metadata (server name + timestamp hash)
// NOT actual TLS session IDs
// - Ticket data is not stored (Rustls keeps it internally)
// - Session resumption works (via Rustls's automatic mechanism)
// but we can't access the actual session state
//
// This implementation provides:
// ✓ session.id - synthetic ID based on metadata
// ✓ session.time - creation timestamp
// ✓ session.timeout - default lifetime value
// ✓ session.has_ticket - always True when session exists
// ✓ session_reused - tracked via handshake_kind()
// ✗ Actual TLS session ID/ticket data - NOT ACCESSIBLE
// Custom ClientSessionStore that tracks session metadata for Python access
// NOTE: This wraps ClientSessionMemoryCache and records metadata when sessions are stored
#[derive(Debug)]
struct PythonClientSessionStore {
inner: Arc<ClientSessionMemoryCache>,
session_cache: SessionCache,
}
impl PythonClientSessionStore {
fn new(session_cache: SessionCache) -> Self {
Self {
inner: Arc::new(ClientSessionMemoryCache::new(SSL_SESSION_CACHE_SIZE)),
session_cache,
}
}
fn transfer_session(
&self,
target: &Self,
server_name: &ServerName<'static>,
kind: ClientSessionKind,
) {
if let Some(group) = self.kx_hint(server_name) {
target.set_kx_hint(server_name.clone(), group);
}
match kind {
ClientSessionKind::Tls12 => {
if let Some(session) = self.tls12_session(server_name) {
target.set_tls12_session(server_name.clone(), session);
}
}
ClientSessionKind::Tls13 => {
if let Some(ticket) = self.take_tls13_ticket(server_name) {
target.insert_tls13_ticket(server_name.clone(), ticket);
}
}
}
}
}
impl ClientSessionStore for PythonClientSessionStore {
fn set_kx_hint(&self, server_name: ServerName<'static>, group: rustls::NamedGroup) {
self.inner.set_kx_hint(server_name, group);
}
fn kx_hint(&self, server_name: &ServerName<'_>) -> Option<rustls::NamedGroup> {
self.inner.kx_hint(server_name)
}
fn set_tls12_session(
&self,
server_name: ServerName<'static>,
value: rustls::client::Tls12ClientSessionValue,
) {
// Store in inner cache for actual resumption (Rustls handles this)
self.inner.set_tls12_session(server_name.clone(), value);
// Record metadata in Python-accessible cache
// NOTE: We can't access value.session_id or value.ticket (private fields)
// So we generate a synthetic ID from metadata
let server_name_str = server_name.to_str();
let session_data = SessionData::new(server_name_str.as_ref(), 7200);
let key = server_name_str.as_bytes().to_vec();
self.session_cache
.write()
.insert(key, Arc::new(ParkingMutex::new(session_data)));
}
fn tls12_session(
&self,
server_name: &ServerName<'_>,
) -> Option<rustls::client::Tls12ClientSessionValue> {
self.inner.tls12_session(server_name)
}
fn remove_tls12_session(&self, server_name: &ServerName<'static>) {
self.inner.remove_tls12_session(server_name);
// Also remove from Python cache
let key = server_name.to_str().as_bytes().to_vec();
self.session_cache.write().remove(&key);
}
fn insert_tls13_ticket(
&self,
server_name: ServerName<'static>,
value: rustls::client::Tls13ClientSessionValue,
) {
// Store in inner cache for actual resumption (Rustls handles this)
self.inner.insert_tls13_ticket(server_name.clone(), value);
// Record metadata in Python-accessible cache
// NOTE: We can't access value.ticket or value.lifetime_secs (private fields)
// So we use default values
let server_name_str = server_name.to_str();
let session_data = SessionData::new(server_name_str.as_ref(), 7200);
let key = server_name_str.as_bytes().to_vec();
self.session_cache
.write()
.insert(key, Arc::new(ParkingMutex::new(session_data)));
}
fn take_tls13_ticket(
&self,
server_name: &ServerName<'static>,
) -> Option<rustls::client::Tls13ClientSessionValue> {
self.inner.take_tls13_ticket(server_name)
}
}
/// Parse length-prefixed ALPN protocol list
///
/// Format: [len1, proto1..., len2, proto2..., ...]
///
/// This is the wire format used by Python's ssl.py when calling _set_alpn_protocols().
/// Each protocol is prefixed with a single byte indicating its length.
///
/// # Arguments
/// * `bytes` - The length-prefixed protocol data
/// * `vm` - VirtualMachine for error creation
///
/// # Returns
/// * `Ok(Vec<Vec<u8>>)` - List of protocol names as byte vectors
/// * `Err(PyBaseExceptionRef)` - ValueError with detailed error message
/// Parse `[len][proto]...` the way `ssl.py` hands it to `_set_alpn_protocols`.
fn parse_length_prefixed_alpn(bytes: &[u8], vm: &VirtualMachine) -> PyResult<Vec<Vec<u8>>> {
let mut alpn_list = Vec::new();
let mut offset = 0;
while offset < bytes.len() {
// Check if we can read the length byte
if offset + 1 > bytes.len() {
return Err(vm.new_value_error(format!(
"Invalid ALPN protocol data: unexpected end at offset {offset}",
)));
}
let proto_len = bytes[offset] as usize;
offset += 1;
// Validate protocol length
if proto_len == 0 {
return Err(vm.new_value_error(format!(
"Invalid ALPN protocol data: protocol length cannot be 0 at offset {}",
offset - 1
)));
}
// Check if we have enough bytes for the protocol data
if offset + proto_len > bytes.len() {
return Err(vm.new_value_error(format!(
"Invalid ALPN protocol data: expected {} bytes at offset {}, but only {} bytes remain",
proto_len, offset, bytes.len() - offset
)));
}
// Extract protocol bytes
let proto = bytes[offset..offset + proto_len].to_vec();
alpn_list.push(proto);
offset += proto_len;
}
Ok(alpn_list)
rustpython_host_env::ssl::parse_length_prefixed_alpn(bytes)
.map_err(|error| vm.new_value_error(error.0))
}
// SSLContext - manages TLS configuration
@@ -1897,10 +1632,7 @@ mod _ssl {
send: socket_class.get_attr("send", vm)?,
recv: socket_class.get_attr("recv", vm)?,
},
tls_record_header_buf: vm
.ctx
.new_bytearray(Vec::with_capacity(TLS_RECORD_HEADER_SIZE))
.into(),
record_cursor: PyMutex::new(RecordCursor::default()),
key_log: Arc::new(super::keylog::ConnectionKeyLog::new(zelf.key_log.clone())),
context: PyRwLock::new(zelf),
server_side: args.server_side,
@@ -1920,7 +1652,6 @@ mod _ssl {
verified_chain: PyRwLock::new(None),
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)),
@@ -1987,10 +1718,7 @@ mod _ssl {
outgoing: args.outgoing,
},
tls_record_header_buf: vm
.ctx
.new_bytearray(Vec::with_capacity(TLS_RECORD_HEADER_SIZE))
.into(),
record_cursor: PyMutex::new(RecordCursor::default()),
key_log: Arc::new(super::keylog::ConnectionKeyLog::new(zelf.key_log.clone())),
context: PyRwLock::new(zelf),
server_side,
@@ -2010,7 +1738,6 @@ mod _ssl {
verified_chain: PyRwLock::new(None),
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)),
@@ -2394,9 +2121,8 @@ mod _ssl {
#[derive(Debug, PyPayload)]
pub(crate) struct PySSLSocket {
io: SocketOrBio,
// Header of currently read TLS record.
#[pytraverse(skip)]
tls_record_header_buf: PyObjectRef,
record_cursor: PyMutex<RecordCursor>,
// SSL context
context: PyRwLock<PyRef<PySSLContext>>,
#[pytraverse(skip)]
@@ -2409,7 +2135,7 @@ mod _ssl {
server_hostname: PyRwLock<Option<String>>,
// TLS connection state
#[pytraverse(skip)]
connection: PyMutex<Option<Connection>>,
connection: PyMutex<Option<TlsConnection>>,
// Includes the saved exception while a rejected handshake sends its alert.
state: PyMutex<TlsState>,
// Session was reused (for session resumption tracking)
@@ -2429,17 +2155,13 @@ mod _ssl {
verified_chain: PyRwLock<Option<Vec<Vec<u8>>>>,
// Per-connection store containing the session selected by this connection.
#[pytraverse(skip)]
client_session_store: PyRwLock<Option<Arc<PythonClientSessionStore>>>,
client_session_store: PyRwLock<Option<Arc<CapturingClientSessionStore>>>,
// Pending TLS output buffer for non-blocking sockets
// Stores unsent TLS bytes when sock_send() would block
// This prevents data loss when write_tls() drains rustls' internal buffer
// but the socket cannot accept all the data immediately
#[pytraverse(skip)]
pub(crate) pending_tls_output: PyMutex<Vec<u8>>,
// Tracks bytes already buffered in rustls for the current write operation
// 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>>,
@@ -2451,9 +2173,6 @@ mod _ssl {
deferred_cert_error: Arc<ParkingRwLock<Option<String>>>,
}
/// TLS record header size (content_type + version + length).
const TLS_RECORD_HEADER_SIZE: usize = 5;
#[pyclass(with(Constructor, Representable), flags(BASETYPE))]
impl PySSLSocket {
// Check if this is BIO mode
@@ -2472,10 +2191,23 @@ mod _ssl {
/// rustls connection lock. Hold only around rustls operations, never
/// across socket or MemoryBIO I/O.
pub(crate) fn connection(&self) -> &PyMutex<Option<Connection>> {
pub(crate) fn connection(&self) -> &PyMutex<Option<TlsConnection>> {
&self.connection
}
pub(crate) fn write_buffered_len(&self) -> usize {
self.connection
.lock()
.as_ref()
.map_or(0, TlsConnection::write_buffered_len)
}
pub(crate) fn set_write_buffered_len(&self, n: usize) {
if let Some(conn) = self.connection.lock().as_mut() {
conn.set_write_buffered_len(n);
}
}
pub(crate) fn observe_tls(&self, write: bool, bytes: &[u8], vm: &VirtualMachine) {
if bytes.is_empty() {
return;
@@ -2605,7 +2337,7 @@ mod _ssl {
.as_ref()
.and_then(|session| session.downcast_ref::<PySSLSession>())
.map_or(cached_session_data, |session| SessionData {
_server_name: server_name.clone().unwrap_or_default(),
server_name: server_name.clone().unwrap_or_default(),
session_id: session.session_id.clone(),
creation_time: session.creation_time,
lifetime: session.lifetime,
@@ -2970,31 +2702,22 @@ mod _ssl {
.map_err(|_| vm.new_os_error("Expected bytes from recv"))
};
let tls_record_header_buf = self
.tls_record_header_buf
.clone()
.downcast::<PyByteArray>()
.expect("BUG: tls_record_header_buf is not PyByteArray");
let buf_len = tls_record_header_buf.borrow_buf().len();
let (mut collected, remaining) = if buf_len < TLS_RECORD_HEADER_SIZE {
let bytes = obj_to_bytes(self.sock_recv(TLS_RECORD_HEADER_SIZE - buf_len, vm)?)?;
let mut header = tls_record_header_buf.borrow_buf_mut();
header.extend_from_slice(bytes.as_bytes());
if header.len() < TLS_RECORD_HEADER_SIZE {
let (mut collected, remaining) = if self.record_cursor.lock().in_header() {
let want = self.record_cursor.lock().want();
let bytes = obj_to_bytes(self.sock_recv(want, vm)?)?;
self.record_cursor.lock().consume(bytes.as_bytes());
if self.record_cursor.lock().in_header() {
return Ok(bytes);
}
let remaining = u16::from_be_bytes([header[3], header[4]]);
let remaining = self.record_cursor.lock().want();
if remaining == 0 {
header.clear();
return Ok(bytes);
}
(Some(bytes.as_bytes().to_vec()), remaining)
} else {
let header = tls_record_header_buf.borrow_buf();
(None, u16::from_be_bytes([header[3], header[4]]))
(None, self.record_cursor.lock().want())
};
let bytes = match self.sock_recv(remaining as usize, vm) {
let bytes = match self.sock_recv(remaining, vm) {
Ok(bytes) => obj_to_bytes(bytes)?,
Err(error) => {
// Header bytes already consumed must reach the TLS parser,
@@ -3008,13 +2731,7 @@ mod _ssl {
return Err(error);
}
};
let mut header = tls_record_header_buf.borrow_buf_mut();
let remaining = remaining - bytes.len() as u16;
if remaining == 0 {
header.clear();
} else {
header[3..5].copy_from_slice(&remaining.to_be_bytes());
}
self.record_cursor.lock().consume(bytes.as_bytes());
if let Some(collected) = collected.as_mut() {
collected.extend_from_slice(bytes.as_bytes());
}
@@ -3066,59 +2783,13 @@ mod _ssl {
Ok(())
}
// Helper function to convert Python PROTO_* constants to rustls versions
fn get_rustls_versions(
minimum: i32,
maximum: i32,
options: i32,
) -> &'static [&'static rustls::SupportedProtocolVersion] {
// Rustls only supports TLS 1.2 and 1.3
// PROTO_TLSv1_2 = 0x0303, PROTO_TLSv1_3 = 0x0304
// PROTO_MINIMUM_SUPPORTED = -2, PROTO_MAXIMUM_SUPPORTED = -1
// If minimum and maximum are 0, use default (both TLS 1.2 and 1.3)
// Static arrays for single-version configurations
static TLS12_ONLY: &[&rustls::SupportedProtocolVersion] = &[&TLS12];
static TLS13_ONLY: &[&rustls::SupportedProtocolVersion] = &[&TLS13];
// Normalize special values: -2 (MINIMUM_SUPPORTED) → TLS 1.2, -1 (MAXIMUM_SUPPORTED) → TLS 1.3
let min = if minimum == -2 {
PROTO_TLSv1_2
} else {
minimum
};
let max = if maximum == -1 {
PROTO_TLSv1_3
} else {
maximum
};
// Check if versions are disabled by options
let tls12_disabled = (options & OP_NO_TLSv1_2) != 0;
let tls13_disabled = (options & OP_NO_TLSv1_3) != 0;
let want_tls12 = (min == 0 || min <= PROTO_TLSv1_2)
&& (max == 0 || max >= PROTO_TLSv1_2)
&& !tls12_disabled;
let want_tls13 = (min == 0 || min <= PROTO_TLSv1_3)
&& (max == 0 || max >= PROTO_TLSv1_3)
&& !tls13_disabled;
match (want_tls12, want_tls13) {
(true, true) => rustls::DEFAULT_VERSIONS, // Both TLS 1.2 and 1.3
(true, false) => TLS12_ONLY, // Only TLS 1.2
(false, true) => TLS13_ONLY, // Only TLS 1.3
(false, false) => rustls::DEFAULT_VERSIONS, // Fallback to default
}
}
/// Helper: Prepare TLS versions from context settings
fn prepare_tls_versions(&self) -> &'static [&'static rustls::SupportedProtocolVersion] {
let ctx = self.context.read();
let min_ver = *ctx.minimum_version.read();
let max_ver = *ctx.maximum_version.read();
let options = *ctx.options.read();
Self::get_rustls_versions(min_ver, max_ver, options)
rustpython_host_env::ssl::rustls_versions(min_ver, max_ver, options)
}
/// Helper: Prepare KX groups (ECDH curve) from context settings
@@ -3171,7 +2842,7 @@ mod _ssl {
/// Continue the accepted ClientHello using the context selected by SNI.
fn initialize_server_connection(
&self,
conn_guard: &mut Option<Connection>,
conn_guard: &mut Option<TlsConnection>,
accepted: Accepted,
vm: &VirtualMachine,
) -> PyResult<()> {
@@ -3300,7 +2971,7 @@ mod _ssl {
match accepted.into_connection(config_arc) {
Ok(mut conn) => {
conn.set_buffer_limit(TLS_IO_BUFFER_LIMIT);
*conn_guard = Some(Connection::Server(conn));
*conn_guard = Some(TlsConnection::new(Connection::Server(conn)));
*self.state.lock() = TlsState::Handshaking;
}
Err((error, mut alert)) => {
@@ -3396,7 +3067,7 @@ mod _ssl {
};
let explicit_session = self.session.read().clone();
let session_store = Arc::new(PythonClientSessionStore::new(session_cache));
let session_store = Arc::new(CapturingClientSessionStore::new(session_cache));
let (mut config, chain_builder) = if let Some(session) = explicit_session {
let session = session
.downcast_ref::<PySSLSession>()
@@ -3447,7 +3118,7 @@ mod _ssl {
})?;
conn.set_buffer_limit(TLS_IO_BUFFER_LIMIT);
*conn_guard = Some(Connection::Client(conn));
*conn_guard = Some(TlsConnection::new(Connection::Client(conn)));
}
}
drop(conn_guard);
@@ -3457,10 +3128,12 @@ mod _ssl {
if let Err(error @ (SslError::Rustls(_) | SslError::PreauthData)) = handshake_result {
// rustls queued the fatal alert while processing the failing
// record. Retain it and the original error across output retries.
let mut bytes = Vec::new();
if let Some(conn) = self.connection.lock().as_mut() {
let _ = conn.write_tls(&mut bytes);
}
let bytes = self
.connection
.lock()
.as_mut()
.and_then(|conn| conn.drain_tls().ok())
.unwrap_or_default();
self.reject_connection(error.into_py_err(vm), bytes, vm);
return self.accept_client_hello(vm).map(|_| ());
}
@@ -3577,9 +3250,7 @@ mod _ssl {
Some(conn) => conn,
None => return Err(create_ssl_eof_error(vm).upcast()),
};
let mut reader = conn.reader();
reader.fill_buf().map_or(0, |buf| buf.len())
conn.pending_plaintext()
};
if pending > 0 {
let mut buf = vec![0u8; pending.min(len)];
@@ -3600,9 +3271,7 @@ mod _ssl {
Some(conn) => conn,
None => return Err(create_ssl_zero_return_error(vm).upcast()),
};
let mut reader = conn.reader();
reader.fill_buf().map_or(0, |buf| buf.len())
conn.pending_plaintext()
};
if pending > 0 {
let mut buf = vec![0u8; pending.min(len)];
@@ -3657,19 +3326,7 @@ mod _ssl {
Some(c) => c,
None => return 0, // No connection established yet
};
// Use rustls Reader's fill_buf() to check buffered plaintext
// fill_buf() returns a reference to buffered data without consuming it
// This matches OpenSSL's SSL_pending() behavior
let mut reader = conn.reader();
match reader.fill_buf() {
Ok(buf) => buf.len(),
Err(_) => {
// WouldBlock or other errors mean no data available
// Return 0 like OpenSSL does when buffer is empty
0
}
}
conn.pending_plaintext()
}
#[pymethod]
@@ -4026,15 +3683,14 @@ mod _ssl {
}
let result = (|| loop {
let mut bytes = Vec::new();
{
let bytes = {
let mut conn_guard = self.connection.lock();
let conn = conn_guard
.as_mut()
.ok_or_else(|| vm.new_value_error("Connection not established"))?;
conn.write_tls(&mut bytes)
.map_err(|e| e.into_pyexception(vm))?;
}
conn.drain_tls()
.map_err(|e| SslError::from(e).into_py_err(vm))?
};
super::compat::send_all_bytes(self, bytes, vm, deadline)
.map_err(|e| e.into_py_err(vm))?;
@@ -4043,8 +3699,8 @@ mod _ssl {
let conn = conn_guard
.as_mut()
.ok_or_else(|| vm.new_value_error("Connection not established"))?;
conn.process_new_packets()
.map_err(|e| SslError::from_rustls(e).into_py_err(vm))?
conn.process_packets()
.map_err(|e| SslError::from(e).into_py_err(vm))?
};
if io_state.plaintext_bytes_to_read() != 0 {
return Err(SslError::create_ssl_error_with_reason(
@@ -4093,10 +3749,12 @@ mod _ssl {
{
// A protocol failure cannot resume as a normal shutdown. Keep
// any fatal alert rustls queued and preserve the original error.
let mut bytes = Vec::new();
if let Some(conn) = self.connection.lock().as_mut() {
let _ = conn.write_tls(&mut bytes);
}
let bytes = self
.connection
.lock()
.as_mut()
.and_then(|conn| conn.drain_tls().ok())
.unwrap_or_default();
self.reject_connection(error.clone(), bytes, vm);
return self
.accept_client_hello(vm)
@@ -4156,7 +3814,7 @@ mod _ssl {
// Check connection exists and protocol version
let conn_guard = self.connection.lock();
if let Some(conn) = conn_guard.as_ref() {
let version = match conn {
let version = match conn.inner() {
Connection::Client(_) => {
return Err(vm.new_value_error(
"Post-handshake authentication requires server socket",
@@ -4360,12 +4018,6 @@ mod _ssl {
// SSLSession - represents a cached SSL session
// NOTE: This is an EMULATION - actual session data is managed by Rustls internally
#[derive(Debug, Clone, Copy)]
enum ClientSessionKind {
Tls12,
Tls13,
}
#[pyattr]
#[pyclass(name = "SSLSession", module = "ssl")]
#[derive(Debug, PyPayload)]
@@ -4373,7 +4025,7 @@ mod _ssl {
context_identity: Arc<()>,
client_config: Arc<rustls::ClientConfig>,
chain_builder: Arc<VerifiedChainBuilder>,
session_store: Arc<PythonClientSessionStore>,
session_store: Arc<CapturingClientSessionStore>,
server_name: Option<ServerName<'static>>,
kind: ClientSessionKind,
// Session ID - synthetic ID generated from metadata (NOT actual TLS session ID)
+54 -210
View File
@@ -17,13 +17,11 @@ mod ssl_data;
use crate::socket::{SockWaitKind, timeout_error_msg};
use crate::vm::VirtualMachine;
use rustls::Connection;
use rustpython_host_env::ssl::{TlsConnection, TlsError};
use rustpython_vm::builtins::{PyBaseException, PyBaseExceptionRef};
use rustpython_vm::convert::IntoPyException;
use rustpython_vm::function::ArgBytesLike;
use rustpython_vm::{AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject};
use std::io::Read;
// Import PySSLSocket from parent module
use super::_ssl::{PySSLSocket, SSL3_RT_MAX_PACKET_SIZE};
@@ -130,13 +128,6 @@ pub(super) enum SslError {
}
impl SslError {
/// Convert TLS alert code to OpenSSL error reason code
/// OpenSSL uses reason = 1000 + alert_code for TLS alerts
fn alert_to_openssl_reason(alert: rustls::AlertDescription) -> i32 {
// AlertDescription can be converted to u8 via as u8 cast
1000 + (u8::from(alert) as i32)
}
/// Convert rustls error to SslError
pub(super) fn from_rustls(err: rustls::Error) -> Self {
Self::Rustls(err)
@@ -144,67 +135,22 @@ impl SslError {
pub(super) fn is_eof(&self) -> bool {
match self {
Self::Rustls(error) => matches!(Self::map_rustls(error.clone()), Self::Eof),
Self::Rustls(error) => TlsError::from_rustls(error.clone()).is_eof(),
Self::Eof => true,
_ => false,
}
}
pub(super) fn is_zero_return(&self) -> bool {
matches!(
self,
Self::ZeroReturn
| Self::Rustls(rustls::Error::AlertReceived(
rustls::AlertDescription::CloseNotify
))
)
match self {
Self::Rustls(error) => TlsError::from_rustls(error.clone()).is_zero_return(),
Self::ZeroReturn => true,
_ => false,
}
}
fn map_rustls(err: rustls::Error) -> Self {
match err {
rustls::Error::InvalidCertificate(cert_err) => Self::CertVerification(cert_err),
rustls::Error::AlertReceived(alert_desc) => {
// Map TLS alerts to OpenSSL-compatible error codes
// lib = 20 (ERR_LIB_SSL), reason = 1000 + alert_code
match alert_desc {
rustls::AlertDescription::CloseNotify => {
// Special case: close_notify is handled as ZeroReturn
Self::ZeroReturn
}
_ => {
// All other alerts: convert to OpenSSL error code
// This includes InternalError (80 -> reason 1080)
Self::AlertReceived {
lib: ERR_LIB_SSL,
reason: Self::alert_to_openssl_reason(alert_desc),
}
}
}
}
// OpenSSL 3.0 changed transport EOF from SSL_ERROR_SYSCALL with
// zero return value to SSL_ERROR_SSL with SSL_R_UNEXPECTED_EOF_WHILE_READING.
// In rustls, these cases correspond to unexpected connection closure:
rustls::Error::InvalidMessage(_) => {
// UnexpectedMessage, CorruptMessage, etc. → SSLEOFError
// Matches CPython's "EOF occurred in violation of protocol"
Self::Eof
}
rustls::Error::PeerIncompatible(peer_err) => {
// Check for specific incompatibility types
use rustls::PeerIncompatible;
match peer_err {
PeerIncompatible::NoCipherSuitesInCommon => {
// Maps to OpenSSL SSL_R_NO_SHARED_CIPHER (lib=20, reason=193)
Self::NoCipherSuites
}
_ => {
// Other protocol incompatibilities → SSLEOFError
Self::Eof
}
}
}
_ => Self::Ssl(format!("{err}")),
}
TlsError::from_rustls(err).into()
}
/// Create SSLError with library and reason from string values
@@ -344,6 +290,25 @@ impl SslError {
}
}
impl From<TlsError> for SslError {
fn from(err: TlsError) -> Self {
match err {
TlsError::WantRead => Self::WantRead,
TlsError::WantWrite => Self::WantWrite,
TlsError::Syscall(msg) => Self::Syscall(msg),
TlsError::Ssl(msg) => Self::Ssl(msg),
TlsError::ZeroReturn => Self::ZeroReturn,
TlsError::Eof => Self::Eof,
TlsError::PreauthData => Self::PreauthData,
TlsError::CertVerification(cert_err) => Self::CertVerification(cert_err),
TlsError::Io(err) => Self::Io(err),
TlsError::Timeout(msg) => Self::Timeout(msg),
TlsError::AlertReceived { lib, reason } => Self::AlertReceived { lib, reason },
TlsError::NoCipherSuites => Self::NoCipherSuites,
}
}
}
pub(super) type SslResult<T> = Result<T, SslError>;
pub(super) use rustpython_host_env::ssl::config::{
ClientConfigOptions, MultiCertResolver, ProtocolSettings, ServerConfigOptions,
@@ -363,7 +328,7 @@ pub(super) fn is_blocking_io_error(err: &Py<PyBaseException>, vm: &VirtualMachin
fn with_conn_mut<R>(
socket: &PySSLSocket,
vm: &VirtualMachine,
f: impl FnOnce(&mut Connection) -> SslResult<R>,
f: impl FnOnce(&mut TlsConnection) -> SslResult<R>,
) -> SslResult<R> {
let mut guard = socket.connection().lock();
let conn = guard
@@ -480,14 +445,14 @@ fn recv_at_most_one_tls_record_for_data(
Ok(data) => Ok(data),
Err(SslError::Eof) => {
with_conn_mut(socket, vm, |conn| {
conn.process_new_packets().map_err(SslError::from_rustls)?;
conn.process_packets().map_err(SslError::from)?;
Ok(())
})?;
Ok(vm.ctx.new_bytes(vec![]).into())
}
Err(SslError::Py(e)) => {
with_conn_mut(socket, vm, |conn| {
conn.process_new_packets().map_err(SslError::from_rustls)?;
conn.process_packets().map_err(SslError::from)?;
Ok(())
})?;
if is_connection_closed_error(&e, vm) {
@@ -518,26 +483,8 @@ fn handshake_read_data(socket: &PySSLSocket, vm: &VirtualMachine) -> SslResult<(
///
/// Returns Ok(Some(n)) if n bytes were read, Ok(None) if would block,
/// or Err on real errors.
fn try_read_plaintext(conn: &mut Connection, buf: &mut [u8]) -> SslResult<Option<usize>> {
let mut reader = conn.reader();
match reader.read(buf) {
Ok(0) => {
// EOF from TLS connection
Ok(Some(0))
}
Ok(n) => {
// Successfully read n bytes
Ok(Some(n))
}
Err(e) if e.kind() != std::io::ErrorKind::WouldBlock => {
// Real error
Err(SslError::Io(e))
}
Err(_) => {
// WouldBlock - no plaintext available
Ok(None)
}
}
fn try_read_plaintext(conn: &mut TlsConnection, buf: &mut [u8]) -> SslResult<Option<usize>> {
conn.read_plaintext(buf).map_err(SslError::from)
}
/// Equivalent to OpenSSL's SSL_do_handshake()
@@ -565,7 +512,7 @@ pub(super) fn ssl_do_handshake(socket: &PySSLSocket, vm: &VirtualMachine) -> Ssl
}
handshake_read_data(socket, vm)?;
with_conn_mut(socket, vm, |conn| {
if let Err(error) = conn.process_new_packets() {
if let Err(error) = conn.inner_mut().process_new_packets() {
return Err(if matches!(error, rustls::Error::InvalidMessage(_)) {
SslError::PreauthData
} else {
@@ -714,9 +661,7 @@ pub(super) fn ssl_read(
// This is critical for asyncore-based applications that rely on recv() returning
// 0 or raising SSL_ERROR_ZERO_RETURN to detect connection close.
let peer_closed = with_conn_mut(socket, vm, |conn| {
conn.process_new_packets()
.map(|io_state| io_state.peer_has_closed())
.map_err(SslError::from_rustls)
conn.peer_has_closed().map_err(SslError::from)
})?;
if peer_closed {
return Err(SslError::ZeroReturn);
@@ -739,9 +684,7 @@ pub(super) fn ssl_read(
// If close_notify was already received, return ZeroReturn (clean closure)
// Otherwise, return Eof (unexpected EOF)
let peer_closed = with_conn_mut(socket, vm, |conn| {
conn.process_new_packets()
.map(|io_state| io_state.peer_has_closed())
.map_err(SslError::from_rustls)
conn.peer_has_closed().map_err(SslError::from)
})?;
if peer_closed {
return Err(SslError::ZeroReturn);
@@ -752,7 +695,7 @@ pub(super) fn ssl_read(
// Feed data to rustls and process
with_conn_mut(socket, vm, |conn| {
ssl_read_tls_records(conn, data, false, vm)?;
conn.process_new_packets().map_err(SslError::from_rustls)?;
conn.process_packets().map_err(SslError::from)?;
Ok(())
})?;
@@ -822,7 +765,7 @@ pub(super) fn ssl_write(
// Check if we already have data buffered from a previous retry
// (prevents duplicate writes when retrying after WantWrite/WantRead)
let already_buffered = *socket.write_buffered_len.lock();
let already_buffered = socket.write_buffered_len();
// Only write plaintext if not already buffered
// Track how much we wrote for partial write handling
@@ -831,38 +774,14 @@ pub(super) fn ssl_write(
if already_buffered == 0 {
// Write plaintext to rustls (= SSL_write_ex internal buffer write)
bytes_written_to_rustls = with_conn_mut(socket, vm, |conn| {
let mut writer = conn.writer();
use std::io::Write;
// Use write() instead of write_all() to support partial writes.
// In BIO mode (asyncio), when the internal buffer is full,
// we want to write as much as possible and return that count,
// rather than failing completely.
match writer.write(data) {
Ok(0) if !data.is_empty() => {
// Buffer is full and nothing could be written.
// In BIO mode, return WantWrite so the caller can
// drain the outgoing BIO and retry.
if is_bio {
return Err(SslError::WantWrite);
}
Err(SslError::Syscall("Write failed: buffer full".to_string()))
}
Ok(n) => Ok(n),
Err(e) => {
if is_bio {
// In BIO mode, treat write errors as WantWrite
return Err(SslError::WantWrite);
}
Err(SslError::Syscall(format!("Write failed: {e}")))
}
}
conn.write_plaintext(data, is_bio).map_err(SslError::from)
})?;
// Mark data as buffered (only the portion we actually wrote)
*socket.write_buffered_len.lock() = bytes_written_to_rustls;
socket.set_write_buffered_len(bytes_written_to_rustls);
} else if already_buffered != data.len() {
// Caller is retrying with different data - this is a protocol error
// Clear the buffer state and return an SSL error (bad write retry)
*socket.write_buffered_len.lock() = 0;
socket.set_write_buffered_len(0);
return Err(SslError::Ssl("bad write retry".to_string()));
}
// else: already_buffered == data.len(), this is a valid retry
@@ -902,7 +821,7 @@ pub(super) fn ssl_write(
// If we had a partial write to rustls, return partial success
// instead of error to match OpenSSL partial-write semantics
if bytes_written_to_rustls > 0 && bytes_written_to_rustls < data.len() {
*socket.write_buffered_len.lock() = 0;
socket.set_write_buffered_len(0);
return Ok(bytes_written_to_rustls);
}
// Keep write_buffered_len set so we don't re-buffer on retry
@@ -913,7 +832,7 @@ pub(super) fn ssl_write(
if is_bio {
// If we had a partial write to rustls, return partial success
if bytes_written_to_rustls > 0 && bytes_written_to_rustls < data.len() {
*socket.write_buffered_len.lock() = 0;
socket.set_write_buffered_len(0);
return Ok(bytes_written_to_rustls);
}
// Keep write_buffered_len set so we don't re-buffer on retry
@@ -923,7 +842,7 @@ pub(super) fn ssl_write(
let recv_result = recv_at_most_one_tls_record_for_data(socket, vm)?;
with_conn_mut(socket, vm, |conn| {
ssl_read_tls_records(conn, recv_result, false, vm)?;
conn.process_new_packets().map_err(SslError::from_rustls)?;
conn.process_packets().map_err(SslError::from)?;
Ok(())
})?;
// Continue loop
@@ -931,7 +850,7 @@ pub(super) fn ssl_write(
Err(e @ SslError::Timeout(_)) => {
// If we had a partial write to rustls, return partial success
if bytes_written_to_rustls > 0 && bytes_written_to_rustls < data.len() {
*socket.write_buffered_len.lock() = 0;
socket.set_write_buffered_len(0);
return Ok(bytes_written_to_rustls);
}
// Preserve buffered state so retry doesn't duplicate data
@@ -940,7 +859,7 @@ pub(super) fn ssl_write(
}
Err(e) => {
// Clear buffer state on error
*socket.write_buffered_len.lock() = 0;
socket.set_write_buffered_len(0);
return Err(e);
}
}
@@ -965,7 +884,7 @@ pub(super) fn ssl_write(
};
// Write completed successfully - clear buffer state
*socket.write_buffered_len.lock() = 0;
socket.set_write_buffered_len(0);
Ok(actual_written)
}
@@ -973,18 +892,13 @@ pub(super) fn ssl_write(
// Helper functions (private-ish, used by public SSL functions)
/// Write TLS records from rustls to socket
fn ssl_write_tls_records(conn: &mut Connection) -> SslResult<Vec<u8>> {
let mut buf = Vec::new();
let n = conn
.write_tls(&mut buf as &mut dyn std::io::Write)
.map_err(SslError::Io)?;
if n > 0 { Ok(buf) } else { Ok(Vec::new()) }
fn ssl_write_tls_records(conn: &mut TlsConnection) -> SslResult<Vec<u8>> {
conn.drain_tls().map_err(SslError::from)
}
/// Read TLS records from socket to rustls
pub(super) fn ssl_read_tls_records(
conn: &mut Connection,
conn: &mut TlsConnection,
data: PyObjectRef,
is_bio: bool,
vm: &VirtualMachine,
@@ -995,81 +909,11 @@ pub(super) fn ssl_read_tls_records(
let bytes_data = bytes.borrow_buf();
if bytes_data.is_empty() {
// different error for BIO vs socket mode
if is_bio {
// In BIO mode, no data means WANT_READ
return Err(SslError::WantRead);
}
// In socket mode, empty recv() means TCP EOF (FIN received)
// Need to distinguish:
// 1. Clean shutdown: received TLS close_notify → return ZeroReturn (0 bytes)
// 2. Unexpected EOF: no close_notify → return Eof (SSLEOFError)
//
// SSL_ERROR_ZERO_RETURN vs SSL_ERROR_EOF logic
// CPython checks SSL_get_shutdown() & SSL_RECEIVED_SHUTDOWN
//
// Process any buffered TLS records (may contain close_notify)
match conn.process_new_packets() {
Ok(io_state) => {
if io_state.peer_has_closed() {
// Received close_notify - normal SSL closure (SSL_ERROR_ZERO_RETURN)
return Err(SslError::ZeroReturn);
}
// No close_notify - ragged EOF (SSL_ERROR_EOF → SSLEOFError)
// CPython raises SSLEOFError here, which SSLSocket.read() handles
// based on suppress_ragged_eofs setting
return Err(SslError::Eof);
}
Err(e) => return Err(SslError::from_rustls(e)),
}
if bytes_data.is_empty() && is_bio {
return Err(SslError::WantRead);
}
// Feed all received data to read_tls - loop to consume all data
// read_tls may not consume all data in one call, and buffer may become full
let mut offset = 0;
while offset < bytes_data.len() {
let remaining = &bytes_data[offset..];
let mut cursor = std::io::Cursor::new(remaining);
match conn.read_tls(&mut cursor) {
Ok(read_bytes) => {
if read_bytes == 0 {
// Buffer is full - process existing packets to make room
conn.process_new_packets().map_err(SslError::from_rustls)?;
// Try again - if we still can't consume, break
let mut retry_cursor = std::io::Cursor::new(remaining);
match conn.read_tls(&mut retry_cursor) {
Ok(0) => {
// Still can't consume - break to avoid infinite loop
break;
}
Ok(n) => {
offset += n;
if offset < bytes_data.len() {
conn.process_new_packets().map_err(SslError::from_rustls)?;
}
}
Err(e) => {
return Err(SslError::Io(e));
}
}
} else {
offset += read_bytes;
if offset < bytes_data.len() {
conn.process_new_packets().map_err(SslError::from_rustls)?;
}
}
}
Err(e) => {
// Real error - propagate it
return Err(SslError::Io(e));
}
}
}
Ok(())
conn.feed_tls(bytes_data.as_ref()).map_err(SslError::from)
}
/// Check if an exception is a connection closed error
@@ -1142,7 +986,7 @@ fn ssl_ensure_data_available(socket: &PySSLSocket, vm: &VirtualMachine) -> SslRe
return Err(SslError::WantRead);
}
with_conn_mut(socket, vm, |conn| {
conn.process_new_packets().map_err(SslError::from_rustls)?;
conn.process_packets().map_err(SslError::from)?;
Ok(())
})?;
if is_connection_closed_error(&e, vm) {
@@ -1188,7 +1032,7 @@ fn ssl_ensure_data_available(socket: &PySSLSocket, vm: &VirtualMachine) -> SslRe
// Feed data to rustls and process packets
with_conn_mut(socket, vm, |conn| {
ssl_read_tls_records(conn, data, is_bio, vm)?;
conn.process_new_packets().map_err(SslError::from_rustls)?;
conn.process_packets().map_err(SslError::from)?;
Ok(())
})?;