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