mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
b701e5cbe9
commit
979576e2e4
11 files changed
+781
-643
No files matched your search
Generated
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,6 @@ rand = { workspace = true }
|
||||
mt19937 = { workspace = true }
|
||||
|
||||
# Crypto:
|
||||
sha2 = { workspace = true }
|
||||
constant_time_eq = { workspace = true }
|
||||
|
||||
# compression
|
||||
|
||||
+82
-430
@@ -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
@@ -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(())
|
||||
})?;
|
||||
|
||||
|
||||
Reference in new issue
Block a user