From ef042f0730445741141f3273e1a1c8a4a28c88cd Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sun, 24 May 2026 15:45:58 +0200 Subject: [PATCH 01/25] Add a test for ssl with ssl.MemoryBIO It checks that BIO mode does not read more data than required for TLS session. --- .../stdlib_ssl_bio_unencrypted_trailer.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 extra_tests/snippets/stdlib_ssl_bio_unencrypted_trailer.py diff --git a/extra_tests/snippets/stdlib_ssl_bio_unencrypted_trailer.py b/extra_tests/snippets/stdlib_ssl_bio_unencrypted_trailer.py new file mode 100644 index 00000000000..61201eb9684 --- /dev/null +++ b/extra_tests/snippets/stdlib_ssl_bio_unencrypted_trailer.py @@ -0,0 +1,46 @@ +import pathlib +import ssl + +CERT = ( + pathlib.Path(__file__).resolve().parent.parent.parent + / "Lib" + / "test" + / "certdata" + / "keycert.pem" +) + +TAIL = b"tail" + +server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +server_context.load_cert_chain(CERT) +client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +client_context.check_hostname = False +client_context.verify_mode = ssl.CERT_NONE +server_context.maximum_version = client_context.maximum_version = ssl.TLSVersion.TLSv1_2 + +client_in, client_out = ssl.MemoryBIO(), ssl.MemoryBIO() +server_in, server_out = ssl.MemoryBIO(), ssl.MemoryBIO() +client = client_context.wrap_bio(client_in, client_out) +server = server_context.wrap_bio(server_in, server_out, server_side=True) + +for _ in range(5): + try: + client.do_handshake() + except ssl.SSLWantReadError: + pass + server_in.write(client_out.read()) + try: + server.do_handshake() + except ssl.SSLWantReadError: + pass + client_in.write(server_out.read()) +client.do_handshake() +server.do_handshake() + +try: + server.unwrap() +except ssl.SSLWantReadError: + pass +client_in.write(server_out.read() + TAIL) +client.unwrap() +assert client_in.read() == TAIL From aab1759ca8d5e9160dbf4a2d01746f8358ab8cc6 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Tue, 26 May 2026 19:05:46 +0200 Subject: [PATCH 02/25] Add Serde serializer that converts structs into basic Python objects --- Cargo.lock | 15 +- Cargo.toml | 3 +- crates/vm/Cargo.toml | 3 + crates/vm/src/convert/mod.rs | 9 + crates/vm/src/convert/rust_py_serde.rs | 737 +++++++++++++++++++++++++ crates/vm/src/vm/mod.rs | 22 + 6 files changed, 786 insertions(+), 3 deletions(-) create mode 100644 crates/vm/src/convert/rust_py_serde.rs diff --git a/Cargo.lock b/Cargo.lock index 8ab53ac5bd0..b52cff30d31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3881,7 +3881,8 @@ dependencies = [ "rustpython-sre_engine", "rustyline", "scopeguard", - "serde_core", + "serde", + "serde_bytes", "static_assertions", "strum", "strum_macros", @@ -3913,8 +3914,8 @@ dependencies = [ "rustpython-pylib", "rustpython-stdlib", "rustpython-vm", + "serde", "serde-wasm-bindgen", - "serde_core", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -4058,6 +4059,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index 81ecbf86172..2a4215d4ad8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -291,7 +291,8 @@ rustls-native-certs = "0.8" rustls-pemfile = "2.2" rustls-platform-verifier = "0.7" rustyline = "18" -serde = { package = "serde_core", version = "1.0.225", default-features = false, features = ["alloc"] } +serde = { version = "1.0.225", default-features = false, features = ["alloc", "derive"] } +serde_bytes = { version = "0.11.19", default-features = false, features = ["std"] } schannel = "0.1.29" scopeguard = "1" serde-wasm-bindgen = "0.6.5" diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index 83e41fa1f5f..52f3b5c86da 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -96,6 +96,9 @@ widestring = { workspace = true } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] wasm-bindgen = { workspace = true, optional = true } +[dev-dependencies] +serde_bytes = { workspace = true } + [build-dependencies] chrono = { workspace = true } glob = { workspace = true } diff --git a/crates/vm/src/convert/mod.rs b/crates/vm/src/convert/mod.rs index 7aed8b90321..912848e85b6 100644 --- a/crates/vm/src/convert/mod.rs +++ b/crates/vm/src/convert/mod.rs @@ -7,3 +7,12 @@ pub use into_object::IntoObject; pub use to_pyobject::{IntoPyException, ToPyException, ToPyObject, ToPyResult}; pub use transmute_from::TransmuteFromObject; pub use try_from::{TryFromBorrowedObject, TryFromObject}; + +#[cfg(feature = "serde")] +mod rust_py_serde; + +#[cfg(feature = "serde")] +pub use rust_py_serde::{ + RustPySerDe, RustPySerDeConf, RustPySerDeError, RustPySerDeSeqKind, RustToPyMapSerializer, + RustToPySeqSerializer, RustToPyStructVariantSerializer, RustToPyTupleVariantSerializer, +}; diff --git a/crates/vm/src/convert/rust_py_serde.rs b/crates/vm/src/convert/rust_py_serde.rs new file mode 100644 index 00000000000..ee9808c8d33 --- /dev/null +++ b/crates/vm/src/convert/rust_py_serde.rs @@ -0,0 +1,737 @@ +use core::{error::Error, fmt}; + +use serde::ser::{ + Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, + SerializeTupleStruct, SerializeTupleVariant, Serializer, +}; + +use crate::{ + PyObjectRef, VirtualMachine, + builtins::{PyBaseExceptionRef, PyDictRef}, +}; + +/// Panics on unit values and unit structures. +pub struct RustPySerDe<'a> { + vm: &'a VirtualMachine, + conf: RustPySerDeConf, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct RustPySerDeConf { + pub lists: RustPySerDeSeqKind, + pub tuples: RustPySerDeSeqKind, + pub tuple_structs: RustPySerDeSeqKind, + pub tuple_variants: RustPySerDeSeqKind, +} + +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +pub enum RustPySerDeSeqKind { + AsTuple, + AsList, +} + +impl<'a> Serializer for &'a RustPySerDe<'a> { + type Ok = PyObjectRef; + type Error = RustPySerDeError; + + type SerializeSeq = RustToPySeqSerializer<'a>; + type SerializeTuple = RustToPySeqSerializer<'a>; + type SerializeTupleStruct = RustToPySeqSerializer<'a>; + type SerializeTupleVariant = RustToPyTupleVariantSerializer<'a>; + type SerializeMap = RustToPyMapSerializer<'a>; + type SerializeStruct = RustToPyMapSerializer<'a>; + type SerializeStructVariant = RustToPyStructVariantSerializer<'a>; + + fn serialize_bool(self, v: bool) -> Result { + Ok(self.vm.ctx.new_bool(v).into()) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_i64(self, v: i64) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_i128(self, v: i128) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_u8(self, v: u8) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_u64(self, v: u64) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_u128(self, v: u128) -> Result { + Ok(self.vm.ctx.new_int(v).into()) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(self.vm.ctx.new_float(v.into()).into()) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(self.vm.ctx.new_float(v).into()) + } + + fn serialize_char(self, v: char) -> Result { + Ok(self.vm.ctx.new_str(v).into()) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(self.vm.ctx.new_str(v).into()) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(self.vm.ctx.new_bytes(v.to_vec()).into()) + } + + fn serialize_none(self) -> Result { + Ok(self.vm.ctx.none()) + } + + fn serialize_some(self, value: &T) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + unimplemented!("BUG: Unit value cannot be serialized into a Python object") + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + unimplemented!("BUG: Unit struct value cannot be serialized into a Python object") + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + Ok(self.vm.ctx.new_str(variant).into()) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + let dict = self.vm.ctx.new_dict(); + dict.set_item(variant, value.serialize(self)?, self.vm)?; + Ok(dict.into()) + } + + fn serialize_seq(self, len: Option) -> Result { + let vec = if let Some(capacity) = len { + Vec::with_capacity(capacity) + } else { + Vec::new() + }; + Ok(RustToPySeqSerializer { ser: self, vec }) + } + + fn serialize_tuple(self, len: usize) -> Result { + Ok(RustToPySeqSerializer { + ser: self, + vec: Vec::with_capacity(len), + }) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + len: usize, + ) -> Result { + Ok(RustToPySeqSerializer { + ser: self, + vec: Vec::with_capacity(len), + }) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(RustToPyTupleVariantSerializer { + ser: self, + vec: Vec::with_capacity(len), + variant, + }) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(RustToPyMapSerializer { + ser: self, + dict: self.vm.ctx.new_dict(), + key: None, + }) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(RustToPyMapSerializer { + ser: self, + dict: self.vm.ctx.new_dict(), + key: None, + }) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize, + ) -> Result { + Ok(RustToPyStructVariantSerializer { + ser: self, + dict: self.vm.ctx.new_dict(), + variant, + }) + } +} + +impl<'a> RustPySerDe<'a> { + pub(crate) fn new(vm: &'a VirtualMachine, conf: RustPySerDeConf) -> Self { + Self { vm, conf } + } +} + +impl Default for RustPySerDeConf { + fn default() -> Self { + Self { + lists: RustPySerDeSeqKind::AsList, + tuples: RustPySerDeSeqKind::AsTuple, + tuple_structs: RustPySerDeSeqKind::AsTuple, + tuple_variants: RustPySerDeSeqKind::AsTuple, + } + } +} + +impl RustPySerDeConf { + #[must_use] + pub fn lists_as_tuples(mut self) -> Self { + self.lists = RustPySerDeSeqKind::AsTuple; + self + } + + #[must_use] + pub fn tuples_as_lists(mut self) -> Self { + self.tuples = RustPySerDeSeqKind::AsList; + self + } + + #[must_use] + pub fn tuple_variants_as_lists(mut self) -> Self { + self.tuple_variants = RustPySerDeSeqKind::AsList; + self + } + + #[must_use] + pub fn tuple_structs_as_lists(mut self) -> Self { + self.tuple_structs = RustPySerDeSeqKind::AsList; + self + } +} + +pub struct RustToPySeqSerializer<'a> { + ser: &'a RustPySerDe<'a>, + vec: Vec, +} + +impl SerializeSeq for RustToPySeqSerializer<'_> { + type Ok = PyObjectRef; + type Error = RustPySerDeError; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.vec.push(value.serialize(self.ser)?); + Ok(()) + } + + fn end(self) -> Result { + match self.ser.conf.lists { + RustPySerDeSeqKind::AsList => Ok(self.ser.vm.ctx.new_list(self.vec).into()), + RustPySerDeSeqKind::AsTuple => Ok(self.ser.vm.ctx.new_tuple(self.vec).into()), + } + } +} + +impl SerializeTuple for RustToPySeqSerializer<'_> { + type Ok = PyObjectRef; + type Error = RustPySerDeError; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.vec.push(value.serialize(self.ser)?); + Ok(()) + } + + fn end(self) -> Result { + match self.ser.conf.tuples { + RustPySerDeSeqKind::AsList => Ok(self.ser.vm.ctx.new_list(self.vec).into()), + RustPySerDeSeqKind::AsTuple => Ok(self.ser.vm.ctx.new_tuple(self.vec).into()), + } + } +} + +impl SerializeTupleStruct for RustToPySeqSerializer<'_> { + type Ok = PyObjectRef; + type Error = RustPySerDeError; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.vec.push(value.serialize(self.ser)?); + Ok(()) + } + + fn end(self) -> Result { + match self.ser.conf.tuple_structs { + RustPySerDeSeqKind::AsList => Ok(self.ser.vm.ctx.new_list(self.vec).into()), + RustPySerDeSeqKind::AsTuple => Ok(self.ser.vm.ctx.new_tuple(self.vec).into()), + } + } +} + +pub struct RustToPyTupleVariantSerializer<'a> { + ser: &'a RustPySerDe<'a>, + vec: Vec, + variant: &'a str, +} + +impl SerializeTupleVariant for RustToPyTupleVariantSerializer<'_> { + type Ok = PyObjectRef; + type Error = RustPySerDeError; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.vec.push(value.serialize(self.ser)?); + Ok(()) + } + + fn end(self) -> Result { + let obj = match self.ser.conf.tuple_variants { + RustPySerDeSeqKind::AsList => self.ser.vm.ctx.new_list(self.vec).into(), + RustPySerDeSeqKind::AsTuple => self.ser.vm.ctx.new_tuple(self.vec).into(), + }; + let dict = self.ser.vm.ctx.new_dict(); + dict.set_item(self.variant, obj, self.ser.vm)?; + Ok(dict.into()) + } +} + +pub struct RustToPyMapSerializer<'a> { + ser: &'a RustPySerDe<'a>, + dict: PyDictRef, + key: Option, +} + +impl SerializeMap for RustToPyMapSerializer<'_> { + type Ok = PyObjectRef; + type Error = RustPySerDeError; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + assert!(self.key.is_none(), "BUG: Double key serialization"); + self.key = Some(key.serialize(self.ser)?); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + let key = self.key.take().expect("BUG: Value without a key"); + self.dict + .set_item(&*key, value.serialize(self.ser)?, self.ser.vm)?; + Ok(()) + } + + fn serialize_entry(&mut self, key: &K, value: &V) -> Result<(), Self::Error> + where + K: ?Sized + Serialize, + V: ?Sized + Serialize, + { + self.dict.set_item( + &*key.serialize(self.ser)?, + value.serialize(self.ser)?, + self.ser.vm, + )?; + Ok(()) + } + + fn end(self) -> Result { + Ok(self.dict.into()) + } +} + +impl SerializeStruct for RustToPyMapSerializer<'_> { + type Ok = PyObjectRef; + type Error = RustPySerDeError; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.dict + .set_item(key, value.serialize(self.ser)?, self.ser.vm)?; + Ok(()) + } + + fn end(self) -> Result { + Ok(self.dict.into()) + } +} + +pub struct RustToPyStructVariantSerializer<'a> { + ser: &'a RustPySerDe<'a>, + dict: PyDictRef, + variant: &'a str, +} + +impl SerializeStructVariant for RustToPyStructVariantSerializer<'_> { + type Ok = PyObjectRef; + type Error = RustPySerDeError; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.dict + .set_item(key, value.serialize(self.ser)?, self.ser.vm)?; + Ok(()) + } + + fn end(self) -> Result { + let dict = self.ser.vm.ctx.new_dict(); + dict.set_item(self.variant, self.dict.into(), self.ser.vm)?; + Ok(dict.into()) + } +} + +pub enum RustPySerDeError { + Py(PyBaseExceptionRef), + SerDe(String), +} + +impl Error for RustPySerDeError {} + +impl fmt::Debug for RustPySerDeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for RustPySerDeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Py(_) => f.write_str("RustPySerDeError::Py(...)"), + Self::SerDe(_) => f.write_str("RustPySerDeError::SerDe(...)"), + } + } +} + +impl serde::ser::Error for RustPySerDeError { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + Self::SerDe(format!("Rust <-> Python serde: {msg}")) + } +} + +impl From for RustPySerDeError { + fn from(value: PyBaseExceptionRef) -> Self { + Self::Py(value) + } +} + +#[cfg(test)] +mod tests { + use alloc::collections::BTreeMap; + + use serde::Serialize; + + use rustpython_vm::Interpreter; + + use crate::convert::RustPySerDeConf; + + fn interpreter() -> Interpreter { + Interpreter::without_stdlib(Default::default()) + } + + #[derive(Serialize)] + struct TestStruct { + val_bool: bool, + val_u8: u8, + val_i8: i8, + val_u16: u16, + val_i16: i16, + val_u32: u32, + val_i32: i32, + val_u64: u64, + val_i64: i64, + val_u128: u128, + val_i128: i128, + val_usize: usize, + val_isize: isize, + val_f32: f32, + val_f64: f64, + val_char: char, + val_str: &'static str, + #[serde(with = "serde_bytes")] + val_bytes: &'static [u8], + val_none: Option, + val_some: Option<&'static str>, + val_list: Vec, + val_tuple: (&'static str, i32), + val_map: BTreeMap<&'static str, i32>, + val_struct: TestSubStruct, + } + + #[derive(Serialize)] + struct TestSubStruct { + a: TestSubEnum, + b: TestSubEnum, + c: TestSubEnum, + d: TestSubEnum, + } + + #[derive(Serialize)] + enum TestSubEnum { + Foo, + Bar(bool), + Baz(u32, &'static str), + Qux { aaa: String, bbb: i32 }, + } + + #[test] + fn serialize() { + let val = TestStruct { + val_bool: true, + val_u8: u8::MAX, + val_i8: i8::MIN, + val_u16: u16::MAX, + val_i16: i16::MIN, + val_u32: u32::MAX, + val_i32: i32::MIN, + val_u64: u64::MAX, + val_i64: i64::MIN, + val_u128: u128::MAX, + val_i128: i128::MIN, + val_usize: usize::MAX, + val_isize: isize::MIN, + val_f32: 234.25, + val_f64: 34342.3125, + val_char: 'x', + val_str: "hello", + val_bytes: b"byte string", + val_none: None, + val_some: Some("some"), + val_list: vec![1, 2, 3], + val_tuple: ("tuple", 4), + val_map: BTreeMap::from([("one", 1), ("two", 2)]), + val_struct: TestSubStruct { + a: TestSubEnum::Foo, + b: TestSubEnum::Bar(false), + c: TestSubEnum::Baz(357652, "test test one two three"), + d: TestSubEnum::Qux { + aaa: "hello world!".to_string(), + bbb: -3, + }, + }, + }; + + interpreter().enter(|vm| { + let val = vm.unwrap_pyresult(vm.with_serde(|serde| val.serialize(serde))); + + let scope = vm.new_scope_with_builtins(); + vm.unwrap_pyresult(scope.globals.set_item("val", val, vm)); + + let script = "\ + from sys import maxsize\n\ + \n\ + assert len(val) == 24\n\ + assert val['val_bool']\n\ + assert val['val_u8'] == 255\n\ + assert val['val_i8'] == -128\n\ + assert val['val_u16'] == 65535\n\ + assert val['val_i16'] == -32768\n\ + assert val['val_u32'] == 4294967295\n\ + assert val['val_i32'] == -2147483648\n\ + assert val['val_u64'] == 18446744073709551615\n\ + assert val['val_i64'] == -9223372036854775808\n\ + assert val['val_u128'] == 340282366920938463463374607431768211455\n\ + assert val['val_i128'] == -170141183460469231731687303715884105728\n\ + assert val['val_usize'] == maxsize * 2 + 1\n\ + assert val['val_isize'] == -maxsize - 1\n\ + assert val['val_f32'] == 234.25\n\ + assert val['val_f64'] == 34342.3125\n\ + assert val['val_char'] == 'x'\n\ + assert val['val_str'] == 'hello'\n\ + assert isinstance(val['val_str'], str)\n\ + assert val['val_bytes'] == b'byte string'\n\ + assert isinstance(val['val_bytes'], bytes)\n\ + assert val['val_none'] is None\n\ + assert val['val_some'] == 'some'\n\ + assert val['val_list'] == [1, 2, 3]\n\ + assert isinstance(val['val_list'], list)\n\ + assert val['val_tuple'] == ('tuple', 4)\n\ + assert isinstance(val['val_tuple'], tuple)\n\ + assert val['val_map'] == {'one': 1, 'two': 2}\n\ + assert isinstance(val['val_map'], dict)\n\ + \n\ + val = val['val_struct']\n\ + assert len(val) == 4\n\ + assert isinstance(val, dict)\n\ + \n\ + assert val['a'] == 'Foo'\n\ + assert val['b'] == {'Bar': False}\n\ + assert val['c'] == {'Baz': (357652, 'test test one two three')}\n\ + assert val['d'] == {'Qux': {'aaa': 'hello world!', 'bbb': -3}}\n\ + "; + let _ = vm.unwrap_pyresult(vm.run_block_expr(scope, script)); + }); + } + + #[test] + fn serialize_lists_as_tuples() { + interpreter().enter(|vm| { + let val = vm.unwrap_pyresult( + vm.with_serde_conf(RustPySerDeConf::default().lists_as_tuples(), |serde| { + vec![1, 2, 3].serialize(serde) + }), + ); + + let scope = vm.new_scope_with_builtins(); + vm.unwrap_pyresult(scope.globals.set_item("val", val, vm)); + + let script = "\ + assert val == (1, 2, 3)\n\ + assert isinstance(val, tuple)\n\ + "; + let _ = vm.unwrap_pyresult(vm.run_block_expr(scope, script)); + }); + } + + #[test] + fn serialize_tuples_as_lists() { + interpreter().enter(|vm| { + let val = vm.unwrap_pyresult( + vm.with_serde_conf(RustPySerDeConf::default().tuples_as_lists(), |serde| { + (1, 2, 3).serialize(serde) + }), + ); + + let scope = vm.new_scope_with_builtins(); + vm.unwrap_pyresult(scope.globals.set_item("val", val, vm)); + + let script = "\ + assert val == [1, 2, 3]\n\ + assert isinstance(val, list)\n\ + "; + let _ = vm.unwrap_pyresult(vm.run_block_expr(scope, script)); + }); + } + + #[derive(Serialize)] + struct TestTupleStruct(u8, u8, u8); + + #[test] + fn serialize_tuple_structs_as_lists() { + interpreter().enter(|vm| { + let val = vm.unwrap_pyresult(vm.with_serde_conf( + RustPySerDeConf::default().tuple_structs_as_lists(), + |serde| TestTupleStruct(3, 2, 1).serialize(serde), + )); + + let scope = vm.new_scope_with_builtins(); + vm.unwrap_pyresult(scope.globals.set_item("val", val, vm)); + + let script = "\ + assert val == [3, 2, 1]\n\ + assert isinstance(val, list)\n\ + "; + let _ = vm.unwrap_pyresult(vm.run_block_expr(scope, script)); + }); + } + + #[derive(Serialize)] + enum TupleVariant { + Variant(u8, u8, u8), + } + + #[test] + fn serialize_tuple_variants_as_lists() { + interpreter().enter(|vm| { + let val = vm.unwrap_pyresult(vm.with_serde_conf( + RustPySerDeConf::default().tuple_variants_as_lists(), + |serde| TupleVariant::Variant(11, 22, 33).serialize(serde), + )); + + let scope = vm.new_scope_with_builtins(); + vm.unwrap_pyresult(scope.globals.set_item("val", val, vm)); + + let script = "\ + assert val == {'Variant': [11, 22, 33]}\n\ + assert isinstance(val['Variant'], list)\n\ + "; + let _ = vm.unwrap_pyresult(vm.run_block_expr(scope, script)); + }); + } +} diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 7775c34e053..512643e9bb5 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -16,6 +16,8 @@ mod vm_new; mod vm_object; mod vm_ops; +#[cfg(feature = "serde")] +use crate::convert::{RustPySerDe, RustPySerDeConf, RustPySerDeError}; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, builtins::{ @@ -2241,6 +2243,26 @@ impl VirtualMachine { let s = unsafe { OsString::from_encoded_bytes_unchecked(bytes) }; Ok(Cow::Owned(s)) } + + #[cfg(feature = "serde")] + pub fn with_serde<'a, T, F>(&'a self, f: F) -> PyResult + where + F: FnOnce(&RustPySerDe<'a>) -> Result, + { + self.with_serde_conf(RustPySerDeConf::default(), f) + } + + #[cfg(feature = "serde")] + pub fn with_serde_conf<'a, T, F>(&'a self, conf: RustPySerDeConf, f: F) -> PyResult + where + F: FnOnce(&RustPySerDe<'a>) -> Result, + { + let serde = RustPySerDe::new(self, conf); + f(&serde).map_err(|e| match e { + RustPySerDeError::Py(err) => err, + RustPySerDeError::SerDe(err) => self.new_value_error(err), + }) + } } impl AsRef for VirtualMachine { From ea88e05de08dfff47236e29bd69c7ba5e79ef419 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sat, 30 May 2026 17:44:07 +0200 Subject: [PATCH 03/25] Support module monkey-patching on VM level This can be used to alter Python-native modules imported from cpython in runtime. --- crates/vm/src/import.rs | 2 + crates/vm/src/vm/interpreter.rs | 1 + crates/vm/src/vm/mod.rs | 66 +++++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/crates/vm/src/import.rs b/crates/vm/src/import.rs index 798bc258b7f..548ffc61aaf 100644 --- a/crates/vm/src/import.rs +++ b/crates/vm/src/import.rs @@ -442,6 +442,8 @@ pub(crate) fn import_module_level( } }; + vm.run_module_loaded_hooks(&abs_name, module.clone())?; + // Handle fromlist let has_from = match fromlist.as_ref().filter(|fl| !vm.is_none(fl)) { Some(fl) => fl.clone().try_to_bool(vm)?, diff --git a/crates/vm/src/vm/interpreter.rs b/crates/vm/src/vm/interpreter.rs index ecdfea01a29..f5001bb1fb5 100644 --- a/crates/vm/src/vm/interpreter.rs +++ b/crates/vm/src/vm/interpreter.rs @@ -106,6 +106,7 @@ where let global_state = PyRc::new(PyGlobalState { config, module_defs: all_module_defs, + module_loaded_hooks: PyMutex::default(), frozen, stacksize: AtomicCell::new(0), thread_count: AtomicCell::new(0), diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 512643e9bb5..0ddfec59308 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -62,6 +62,8 @@ pub use setting::{CheckHashPycsMode, Paths, PyConfig, Settings}; pub const MAX_MEMORY_SIZE: usize = isize::MAX as usize; +pub type ModuleLoadedHook = fn(&VirtualMachine, PyObjectRef) -> PyResult<()>; + // Objects are live when they are on stack, or referenced by a name (for now) /// Top level container of a python virtual machine. In theory you could @@ -591,6 +593,7 @@ pub(crate) struct CallableCache { pub struct PyGlobalState { pub config: PyConfig, pub module_defs: BTreeMap<&'static str, &'static builtins::PyModuleDef>, + pub module_loaded_hooks: PyMutex>>, pub frozen: HashMap<&'static str, FrozenModule, rapidhash::quality::RandomState>, pub stacksize: AtomicCell, pub thread_count: AtomicCell, @@ -641,6 +644,38 @@ pub fn process_hash_secret_seed() -> u32 { } impl VirtualMachine { + pub fn register_module_loaded_hook( + &self, + module_name: impl Into, + hook: ModuleLoadedHook, + ) { + self.state + .module_loaded_hooks + .lock() + .entry(module_name.into()) + .or_default() + .push(hook); + } + + pub(crate) fn run_module_loaded_hooks( + &self, + module_name: &str, + module: PyObjectRef, + ) -> PyResult<()> { + let hooks = self + .state + .module_loaded_hooks + .lock() + .get(module_name) + .cloned(); + if let Some(hooks) = hooks { + for hook in hooks { + hook(self, module.clone())?; + } + } + Ok(()) + } + fn init_callable_cache(&mut self) -> PyResult<()> { self.callable_cache.len = Some(self.builtins.get_attr("len", self)?); self.callable_cache.isinstance = Some(self.builtins.get_attr("isinstance", self)?); @@ -2290,12 +2325,12 @@ pub fn resolve_frozen_alias(name: &str) -> &str { #[cfg(test)] mod tests { + use rustpython_vm as vm; + use super::*; #[test] fn nested_frozen() { - use rustpython_vm as vm; - vm::Interpreter::builder(Default::default()) .add_frozen_modules(rustpython_vm::py_freeze!( dir = "../../../../extra_tests/snippets" @@ -2319,8 +2354,6 @@ mod tests { #[test] fn frozen_origname_matches() { - use rustpython_vm as vm; - vm::Interpreter::builder(Default::default()) .build() .enter(|vm| { @@ -2341,4 +2374,29 @@ mod tests { ); }); } + + #[test] + fn module_loaded_hook_can_patch_imported_module() { + vm::Interpreter::builder(Default::default()) + .build() + .enter(|vm| { + vm.register_module_loaded_hook("sys", mark_module_loaded); + let module = vm.import("sys", 0).unwrap(); + assert!( + module + .get_attr("__rustpython_module_loaded_hook_ran__", vm) + .unwrap() + .try_to_bool(vm) + .unwrap() + ); + }); + } + + fn mark_module_loaded(vm: &VirtualMachine, module: PyObjectRef) -> PyResult<()> { + module.set_attr( + "__rustpython_module_loaded_hook_ran__", + vm.ctx.new_bool(true), + vm, + ) + } } From 4b663e0090a7feee4214b081826e1681f4065d17 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Wed, 27 May 2026 19:16:07 +0200 Subject: [PATCH 04/25] Add OpenSSL data files used to build OID and NID tables Currently parsing is done in run time but this really can be moved to compile time. Imported from OpenSSL 4.0.0: * https://github.com/openssl/openssl/raw/11b7b6ea3b65a584e1d31408ed1bdb139465cffd/crypto/objects/objects.txt * https://github.com/openssl/openssl/raw/11b7b6ea3b65a584e1d31408ed1bdb139465cffd/crypto/objects/obj_mac.num --- crates/stdlib/src/rustls-data/obj_mac.num | 1501 +++++++++++++++ crates/stdlib/src/rustls-data/objects.txt | 2069 +++++++++++++++++++++ 2 files changed, 3570 insertions(+) create mode 100644 crates/stdlib/src/rustls-data/obj_mac.num create mode 100644 crates/stdlib/src/rustls-data/objects.txt diff --git a/crates/stdlib/src/rustls-data/obj_mac.num b/crates/stdlib/src/rustls-data/obj_mac.num new file mode 100644 index 00000000000..e72170b47f7 --- /dev/null +++ b/crates/stdlib/src/rustls-data/obj_mac.num @@ -0,0 +1,1501 @@ +undef 0 +rsadsi 1 +pkcs 2 +md2 3 +md5 4 +rc4 5 +rsaEncryption 6 +md2WithRSAEncryption 7 +md5WithRSAEncryption 8 +pbeWithMD2AndDES_CBC 9 +pbeWithMD5AndDES_CBC 10 +X500 11 +X509 12 +commonName 13 +countryName 14 +localityName 15 +stateOrProvinceName 16 +organizationName 17 +organizationalUnitName 18 +rsa 19 +pkcs7 20 +pkcs7_data 21 +pkcs7_signed 22 +pkcs7_enveloped 23 +pkcs7_signedAndEnveloped 24 +pkcs7_digest 25 +pkcs7_encrypted 26 +pkcs3 27 +dhKeyAgreement 28 +des_ecb 29 +des_cfb64 30 +des_cbc 31 +des_ede_ecb 32 +des_ede3_ecb 33 +idea_cbc 34 +idea_cfb64 35 +idea_ecb 36 +rc2_cbc 37 +rc2_ecb 38 +rc2_cfb64 39 +rc2_ofb64 40 +sha 41 +shaWithRSAEncryption 42 +des_ede_cbc 43 +des_ede3_cbc 44 +des_ofb64 45 +idea_ofb64 46 +pkcs9 47 +pkcs9_emailAddress 48 +pkcs9_unstructuredName 49 +pkcs9_contentType 50 +pkcs9_messageDigest 51 +pkcs9_signingTime 52 +pkcs9_countersignature 53 +pkcs9_challengePassword 54 +pkcs9_unstructuredAddress 55 +pkcs9_extCertAttributes 56 +netscape 57 +netscape_cert_extension 58 +netscape_data_type 59 +des_ede_cfb64 60 +des_ede3_cfb64 61 +des_ede_ofb64 62 +des_ede3_ofb64 63 +sha1 64 +sha1WithRSAEncryption 65 +dsaWithSHA 66 +dsa_2 67 +pbeWithSHA1AndRC2_CBC 68 +id_pbkdf2 69 +dsaWithSHA1_2 70 +netscape_cert_type 71 +netscape_base_url 72 +netscape_revocation_url 73 +netscape_ca_revocation_url 74 +netscape_renewal_url 75 +netscape_ca_policy_url 76 +netscape_ssl_server_name 77 +netscape_comment 78 +netscape_cert_sequence 79 +desx_cbc 80 +id_ce 81 +subject_key_identifier 82 +key_usage 83 +private_key_usage_period 84 +subject_alt_name 85 +issuer_alt_name 86 +basic_constraints 87 +crl_number 88 +certificate_policies 89 +authority_key_identifier 90 +bf_cbc 91 +bf_ecb 92 +bf_cfb64 93 +bf_ofb64 94 +mdc2 95 +mdc2WithRSA 96 +rc4_40 97 +rc2_40_cbc 98 +givenName 99 +surname 100 +initials 101 +uniqueIdentifier 102 +crl_distribution_points 103 +md5WithRSA 104 +serialNumber 105 +title 106 +description 107 +cast5_cbc 108 +cast5_ecb 109 +cast5_cfb64 110 +cast5_ofb64 111 +pbeWithMD5AndCast5_CBC 112 +dsaWithSHA1 113 +md5_sha1 114 +sha1WithRSA 115 +dsa 116 +ripemd160 117 +ripemd160WithRSA 119 +rc5_cbc 120 +rc5_ecb 121 +rc5_cfb64 122 +rc5_ofb64 123 +rle_compression 124 +zlib_compression 125 +ext_key_usage 126 +id_pkix 127 +id_kp 128 +server_auth 129 +client_auth 130 +code_sign 131 +email_protect 132 +time_stamp 133 +ms_code_ind 134 +ms_code_com 135 +ms_ctl_sign 136 +ms_sgc 137 +ms_efs 138 +ns_sgc 139 +delta_crl 140 +crl_reason 141 +invalidity_date 142 +sxnet 143 +pbe_WithSHA1And128BitRC4 144 +pbe_WithSHA1And40BitRC4 145 +pbe_WithSHA1And3_Key_TripleDES_CBC 146 +pbe_WithSHA1And2_Key_TripleDES_CBC 147 +pbe_WithSHA1And128BitRC2_CBC 148 +pbe_WithSHA1And40BitRC2_CBC 149 +keyBag 150 +pkcs8ShroudedKeyBag 151 +certBag 152 +crlBag 153 +secretBag 154 +safeContentsBag 155 +friendlyName 156 +localKeyID 157 +x509Certificate 158 +sdsiCertificate 159 +x509Crl 160 +pbes2 161 +pbmac1 162 +hmacWithSHA1 163 +id_qt_cps 164 +id_qt_unotice 165 +rc2_64_cbc 166 +SMIMECapabilities 167 +pbeWithMD2AndRC2_CBC 168 +pbeWithMD5AndRC2_CBC 169 +pbeWithSHA1AndDES_CBC 170 +ms_ext_req 171 +ext_req 172 +name 173 +dnQualifier 174 +id_pe 175 +id_ad 176 +info_access 177 +ad_OCSP 178 +ad_ca_issuers 179 +OCSP_sign 180 +iso 181 +member_body 182 +ISO_US 183 +X9_57 184 +X9cm 185 +pkcs1 186 +pkcs5 187 +SMIME 188 +id_smime_mod 189 +id_smime_ct 190 +id_smime_aa 191 +id_smime_alg 192 +id_smime_cd 193 +id_smime_spq 194 +id_smime_cti 195 +id_smime_mod_cms 196 +id_smime_mod_ess 197 +id_smime_mod_oid 198 +id_smime_mod_msg_v3 199 +id_smime_mod_ets_eSignature_88 200 +id_smime_mod_ets_eSignature_97 201 +id_smime_mod_ets_eSigPolicy_88 202 +id_smime_mod_ets_eSigPolicy_97 203 +id_smime_ct_receipt 204 +id_smime_ct_authData 205 +id_smime_ct_publishCert 206 +id_smime_ct_TSTInfo 207 +id_smime_ct_TDTInfo 208 +id_smime_ct_contentInfo 209 +id_smime_ct_DVCSRequestData 210 +id_smime_ct_DVCSResponseData 211 +id_smime_aa_receiptRequest 212 +id_smime_aa_securityLabel 213 +id_smime_aa_mlExpandHistory 214 +id_smime_aa_contentHint 215 +id_smime_aa_msgSigDigest 216 +id_smime_aa_encapContentType 217 +id_smime_aa_contentIdentifier 218 +id_smime_aa_macValue 219 +id_smime_aa_equivalentLabels 220 +id_smime_aa_contentReference 221 +id_smime_aa_encrypKeyPref 222 +id_smime_aa_signingCertificate 223 +id_smime_aa_smimeEncryptCerts 224 +id_smime_aa_timeStampToken 225 +id_smime_aa_ets_sigPolicyId 226 +id_smime_aa_ets_commitmentType 227 +id_smime_aa_ets_signerLocation 228 +id_smime_aa_ets_signerAttr 229 +id_smime_aa_ets_otherSigCert 230 +id_smime_aa_ets_contentTimestamp 231 +id_smime_aa_ets_CertificateRefs 232 +id_smime_aa_ets_RevocationRefs 233 +id_smime_aa_ets_certValues 234 +id_smime_aa_ets_revocationValues 235 +id_smime_aa_ets_escTimeStamp 236 +id_smime_aa_ets_certCRLTimestamp 237 +id_smime_aa_ets_archiveTimeStamp 238 +id_smime_aa_signatureType 239 +id_smime_aa_dvcs_dvc 240 +id_smime_alg_ESDHwith3DES 241 +id_smime_alg_ESDHwithRC2 242 +id_smime_alg_3DESwrap 243 +id_smime_alg_RC2wrap 244 +id_smime_alg_ESDH 245 +id_smime_alg_CMS3DESwrap 246 +id_smime_alg_CMSRC2wrap 247 +id_smime_cd_ldap 248 +id_smime_spq_ets_sqt_uri 249 +id_smime_spq_ets_sqt_unotice 250 +id_smime_cti_ets_proofOfOrigin 251 +id_smime_cti_ets_proofOfReceipt 252 +id_smime_cti_ets_proofOfDelivery 253 +id_smime_cti_ets_proofOfSender 254 +id_smime_cti_ets_proofOfApproval 255 +id_smime_cti_ets_proofOfCreation 256 +md4 257 +id_pkix_mod 258 +id_qt 259 +id_it 260 +id_pkip 261 +id_alg 262 +id_cmc 263 +id_on 264 +id_pda 265 +id_aca 266 +id_qcs 267 +id_cct 268 +id_pkix1_explicit_88 269 +id_pkix1_implicit_88 270 +id_pkix1_explicit_93 271 +id_pkix1_implicit_93 272 +id_mod_crmf 273 +id_mod_cmc 274 +id_mod_kea_profile_88 275 +id_mod_kea_profile_93 276 +id_mod_cmp 277 +id_mod_qualified_cert_88 278 +id_mod_qualified_cert_93 279 +id_mod_attribute_cert 280 +id_mod_timestamp_protocol 281 +id_mod_ocsp 282 +id_mod_dvcs 283 +id_mod_cmp2000 284 +biometricInfo 285 +qcStatements 286 +ac_auditIdentity 287 +ac_targeting 288 +aaControls 289 +sbgp_ipAddrBlock 290 +sbgp_autonomousSysNum 291 +sbgp_routerIdentifier 292 +textNotice 293 +ipsecEndSystem 294 +ipsecTunnel 295 +ipsecUser 296 +dvcs 297 +id_it_caProtEncCert 298 +id_it_signKeyPairTypes 299 +id_it_encKeyPairTypes 300 +id_it_preferredSymmAlg 301 +id_it_caKeyUpdateInfo 302 +id_it_currentCRL 303 +id_it_unsupportedOIDs 304 +id_it_subscriptionRequest 305 +id_it_subscriptionResponse 306 +id_it_keyPairParamReq 307 +id_it_keyPairParamRep 308 +id_it_revPassphrase 309 +id_it_implicitConfirm 310 +id_it_confirmWaitTime 311 +id_it_origPKIMessage 312 +id_regCtrl 313 +id_regInfo 314 +id_regCtrl_regToken 315 +id_regCtrl_authenticator 316 +id_regCtrl_pkiPublicationInfo 317 +id_regCtrl_pkiArchiveOptions 318 +id_regCtrl_oldCertID 319 +id_regCtrl_protocolEncrKey 320 +id_regInfo_utf8Pairs 321 +id_regInfo_certReq 322 +id_alg_des40 323 +id_alg_noSignature 324 +id_alg_dh_sig_hmac_sha1 325 +id_alg_dh_pop 326 +id_cmc_statusInfo 327 +id_cmc_identification 328 +id_cmc_identityProof 329 +id_cmc_dataReturn 330 +id_cmc_transactionId 331 +id_cmc_senderNonce 332 +id_cmc_recipientNonce 333 +id_cmc_addExtensions 334 +id_cmc_encryptedPOP 335 +id_cmc_decryptedPOP 336 +id_cmc_lraPOPWitness 337 +id_cmc_getCert 338 +id_cmc_getCRL 339 +id_cmc_revokeRequest 340 +id_cmc_regInfo 341 +id_cmc_responseInfo 342 +id_cmc_queryPending 343 +id_cmc_popLinkRandom 344 +id_cmc_popLinkWitness 345 +id_cmc_confirmCertAcceptance 346 +id_on_personalData 347 +id_pda_dateOfBirth 348 +id_pda_placeOfBirth 349 +id_pda_pseudonym 350 +id_pda_gender 351 +id_pda_countryOfCitizenship 352 +id_pda_countryOfResidence 353 +id_aca_authenticationInfo 354 +id_aca_accessIdentity 355 +id_aca_chargingIdentity 356 +id_aca_group 357 +id_aca_role 358 +id_qcs_pkixQCSyntax_v1 359 +id_cct_crs 360 +id_cct_PKIData 361 +id_cct_PKIResponse 362 +ad_timeStamping 363 +ad_dvcs 364 +id_pkix_OCSP_basic 365 +id_pkix_OCSP_Nonce 366 +id_pkix_OCSP_CrlID 367 +id_pkix_OCSP_acceptableResponses 368 +id_pkix_OCSP_noCheck 369 +id_pkix_OCSP_archiveCutoff 370 +id_pkix_OCSP_serviceLocator 371 +id_pkix_OCSP_extendedStatus 372 +id_pkix_OCSP_valid 373 +id_pkix_OCSP_path 374 +id_pkix_OCSP_trustRoot 375 +algorithm 376 +rsaSignature 377 +X500algorithms 378 +org 379 +dod 380 +iana 381 +Directory 382 +Management 383 +Experimental 384 +Private 385 +Security 386 +SNMPv2 387 +Mail 388 +Enterprises 389 +dcObject 390 +domainComponent 391 +Domain 392 +joint_iso_ccitt 393 +selected_attribute_types 394 +clearance 395 +md4WithRSAEncryption 396 +ac_proxying 397 +sinfo_access 398 +id_aca_encAttrs 399 +role 400 +policy_constraints 401 +target_information 402 +no_rev_avail 403 +ccitt 404 +ansi_X9_62 405 +X9_62_prime_field 406 +X9_62_characteristic_two_field 407 +X9_62_id_ecPublicKey 408 +X9_62_prime192v1 409 +X9_62_prime192v2 410 +X9_62_prime192v3 411 +X9_62_prime239v1 412 +X9_62_prime239v2 413 +X9_62_prime239v3 414 +X9_62_prime256v1 415 +ecdsa_with_SHA1 416 +ms_csp_name 417 +aes_128_ecb 418 +aes_128_cbc 419 +aes_128_ofb128 420 +aes_128_cfb128 421 +aes_192_ecb 422 +aes_192_cbc 423 +aes_192_ofb128 424 +aes_192_cfb128 425 +aes_256_ecb 426 +aes_256_cbc 427 +aes_256_ofb128 428 +aes_256_cfb128 429 +hold_instruction_code 430 +hold_instruction_none 431 +hold_instruction_call_issuer 432 +hold_instruction_reject 433 +data 434 +pss 435 +ucl 436 +pilot 437 +pilotAttributeType 438 +pilotAttributeSyntax 439 +pilotObjectClass 440 +pilotGroups 441 +iA5StringSyntax 442 +caseIgnoreIA5StringSyntax 443 +pilotObject 444 +pilotPerson 445 +account 446 +document 447 +room 448 +documentSeries 449 +rFC822localPart 450 +dNSDomain 451 +domainRelatedObject 452 +friendlyCountry 453 +simpleSecurityObject 454 +pilotOrganization 455 +pilotDSA 456 +qualityLabelledData 457 +userId 458 +textEncodedORAddress 459 +rfc822Mailbox 460 +info 461 +favouriteDrink 462 +roomNumber 463 +photo 464 +userClass 465 +host 466 +manager 467 +documentIdentifier 468 +documentTitle 469 +documentVersion 470 +documentAuthor 471 +documentLocation 472 +homeTelephoneNumber 473 +secretary 474 +otherMailbox 475 +lastModifiedTime 476 +lastModifiedBy 477 +aRecord 478 +pilotAttributeType27 479 +mXRecord 480 +nSRecord 481 +sOARecord 482 +cNAMERecord 483 +associatedDomain 484 +associatedName 485 +homePostalAddress 486 +personalTitle 487 +mobileTelephoneNumber 488 +pagerTelephoneNumber 489 +friendlyCountryName 490 +organizationalStatus 491 +janetMailbox 492 +mailPreferenceOption 493 +buildingName 494 +dSAQuality 495 +singleLevelQuality 496 +subtreeMinimumQuality 497 +subtreeMaximumQuality 498 +personalSignature 499 +dITRedirect 500 +audio 501 +documentPublisher 502 +x500UniqueIdentifier 503 +mime_mhs 504 +mime_mhs_headings 505 +mime_mhs_bodies 506 +id_hex_partial_message 507 +id_hex_multipart_message 508 +generationQualifier 509 +pseudonym 510 +InternationalRA 511 +id_set 512 +set_ctype 513 +set_msgExt 514 +set_attr 515 +set_policy 516 +set_certExt 517 +set_brand 518 +setct_PANData 519 +setct_PANToken 520 +setct_PANOnly 521 +setct_OIData 522 +setct_PI 523 +setct_PIData 524 +setct_PIDataUnsigned 525 +setct_HODInput 526 +setct_AuthResBaggage 527 +setct_AuthRevReqBaggage 528 +setct_AuthRevResBaggage 529 +setct_CapTokenSeq 530 +setct_PInitResData 531 +setct_PI_TBS 532 +setct_PResData 533 +setct_AuthReqTBS 534 +setct_AuthResTBS 535 +setct_AuthResTBSX 536 +setct_AuthTokenTBS 537 +setct_CapTokenData 538 +setct_CapTokenTBS 539 +setct_AcqCardCodeMsg 540 +setct_AuthRevReqTBS 541 +setct_AuthRevResData 542 +setct_AuthRevResTBS 543 +setct_CapReqTBS 544 +setct_CapReqTBSX 545 +setct_CapResData 546 +setct_CapRevReqTBS 547 +setct_CapRevReqTBSX 548 +setct_CapRevResData 549 +setct_CredReqTBS 550 +setct_CredReqTBSX 551 +setct_CredResData 552 +setct_CredRevReqTBS 553 +setct_CredRevReqTBSX 554 +setct_CredRevResData 555 +setct_PCertReqData 556 +setct_PCertResTBS 557 +setct_BatchAdminReqData 558 +setct_BatchAdminResData 559 +setct_CardCInitResTBS 560 +setct_MeAqCInitResTBS 561 +setct_RegFormResTBS 562 +setct_CertReqData 563 +setct_CertReqTBS 564 +setct_CertResData 565 +setct_CertInqReqTBS 566 +setct_ErrorTBS 567 +setct_PIDualSignedTBE 568 +setct_PIUnsignedTBE 569 +setct_AuthReqTBE 570 +setct_AuthResTBE 571 +setct_AuthResTBEX 572 +setct_AuthTokenTBE 573 +setct_CapTokenTBE 574 +setct_CapTokenTBEX 575 +setct_AcqCardCodeMsgTBE 576 +setct_AuthRevReqTBE 577 +setct_AuthRevResTBE 578 +setct_AuthRevResTBEB 579 +setct_CapReqTBE 580 +setct_CapReqTBEX 581 +setct_CapResTBE 582 +setct_CapRevReqTBE 583 +setct_CapRevReqTBEX 584 +setct_CapRevResTBE 585 +setct_CredReqTBE 586 +setct_CredReqTBEX 587 +setct_CredResTBE 588 +setct_CredRevReqTBE 589 +setct_CredRevReqTBEX 590 +setct_CredRevResTBE 591 +setct_BatchAdminReqTBE 592 +setct_BatchAdminResTBE 593 +setct_RegFormReqTBE 594 +setct_CertReqTBE 595 +setct_CertReqTBEX 596 +setct_CertResTBE 597 +setct_CRLNotificationTBS 598 +setct_CRLNotificationResTBS 599 +setct_BCIDistributionTBS 600 +setext_genCrypt 601 +setext_miAuth 602 +setext_pinSecure 603 +setext_pinAny 604 +setext_track2 605 +setext_cv 606 +set_policy_root 607 +setCext_hashedRoot 608 +setCext_certType 609 +setCext_merchData 610 +setCext_cCertRequired 611 +setCext_tunneling 612 +setCext_setExt 613 +setCext_setQualf 614 +setCext_PGWYcapabilities 615 +setCext_TokenIdentifier 616 +setCext_Track2Data 617 +setCext_TokenType 618 +setCext_IssuerCapabilities 619 +setAttr_Cert 620 +setAttr_PGWYcap 621 +setAttr_TokenType 622 +setAttr_IssCap 623 +set_rootKeyThumb 624 +set_addPolicy 625 +setAttr_Token_EMV 626 +setAttr_Token_B0Prime 627 +setAttr_IssCap_CVM 628 +setAttr_IssCap_T2 629 +setAttr_IssCap_Sig 630 +setAttr_GenCryptgrm 631 +setAttr_T2Enc 632 +setAttr_T2cleartxt 633 +setAttr_TokICCsig 634 +setAttr_SecDevSig 635 +set_brand_IATA_ATA 636 +set_brand_Diners 637 +set_brand_AmericanExpress 638 +set_brand_JCB 639 +set_brand_Visa 640 +set_brand_MasterCard 641 +set_brand_Novus 642 +des_cdmf 643 +rsaOAEPEncryptionSET 644 +itu_t 645 +joint_iso_itu_t 646 +international_organizations 647 +ms_smartcard_login 648 +ms_upn 649 +aes_128_cfb1 650 +aes_192_cfb1 651 +aes_256_cfb1 652 +aes_128_cfb8 653 +aes_192_cfb8 654 +aes_256_cfb8 655 +des_cfb1 656 +des_cfb8 657 +des_ede3_cfb1 658 +des_ede3_cfb8 659 +streetAddress 660 +postalCode 661 +id_ppl 662 +proxyCertInfo 663 +id_ppl_anyLanguage 664 +id_ppl_inheritAll 665 +name_constraints 666 +Independent 667 +sha256WithRSAEncryption 668 +sha384WithRSAEncryption 669 +sha512WithRSAEncryption 670 +sha224WithRSAEncryption 671 +sha256 672 +sha384 673 +sha512 674 +sha224 675 +identified_organization 676 +certicom_arc 677 +wap 678 +wap_wsg 679 +X9_62_id_characteristic_two_basis 680 +X9_62_onBasis 681 +X9_62_tpBasis 682 +X9_62_ppBasis 683 +X9_62_c2pnb163v1 684 +X9_62_c2pnb163v2 685 +X9_62_c2pnb163v3 686 +X9_62_c2pnb176v1 687 +X9_62_c2tnb191v1 688 +X9_62_c2tnb191v2 689 +X9_62_c2tnb191v3 690 +X9_62_c2onb191v4 691 +X9_62_c2onb191v5 692 +X9_62_c2pnb208w1 693 +X9_62_c2tnb239v1 694 +X9_62_c2tnb239v2 695 +X9_62_c2tnb239v3 696 +X9_62_c2onb239v4 697 +X9_62_c2onb239v5 698 +X9_62_c2pnb272w1 699 +X9_62_c2pnb304w1 700 +X9_62_c2tnb359v1 701 +X9_62_c2pnb368w1 702 +X9_62_c2tnb431r1 703 +secp112r1 704 +secp112r2 705 +secp128r1 706 +secp128r2 707 +secp160k1 708 +secp160r1 709 +secp160r2 710 +secp192k1 711 +secp224k1 712 +secp224r1 713 +secp256k1 714 +secp384r1 715 +secp521r1 716 +sect113r1 717 +sect113r2 718 +sect131r1 719 +sect131r2 720 +sect163k1 721 +sect163r1 722 +sect163r2 723 +sect193r1 724 +sect193r2 725 +sect233k1 726 +sect233r1 727 +sect239k1 728 +sect283k1 729 +sect283r1 730 +sect409k1 731 +sect409r1 732 +sect571k1 733 +sect571r1 734 +wap_wsg_idm_ecid_wtls1 735 +wap_wsg_idm_ecid_wtls3 736 +wap_wsg_idm_ecid_wtls4 737 +wap_wsg_idm_ecid_wtls5 738 +wap_wsg_idm_ecid_wtls6 739 +wap_wsg_idm_ecid_wtls7 740 +wap_wsg_idm_ecid_wtls8 741 +wap_wsg_idm_ecid_wtls9 742 +wap_wsg_idm_ecid_wtls10 743 +wap_wsg_idm_ecid_wtls11 744 +wap_wsg_idm_ecid_wtls12 745 +any_policy 746 +policy_mappings 747 +inhibit_any_policy 748 +ipsec3 749 +ipsec4 750 +camellia_128_cbc 751 +camellia_192_cbc 752 +camellia_256_cbc 753 +camellia_128_ecb 754 +camellia_192_ecb 755 +camellia_256_ecb 756 +camellia_128_cfb128 757 +camellia_192_cfb128 758 +camellia_256_cfb128 759 +camellia_128_cfb1 760 +camellia_192_cfb1 761 +camellia_256_cfb1 762 +camellia_128_cfb8 763 +camellia_192_cfb8 764 +camellia_256_cfb8 765 +camellia_128_ofb128 766 +camellia_192_ofb128 767 +camellia_256_ofb128 768 +subject_directory_attributes 769 +issuing_distribution_point 770 +certificate_issuer 771 +korea 772 +kisa 773 +kftc 774 +npki_alg 775 +seed_ecb 776 +seed_cbc 777 +seed_ofb128 778 +seed_cfb128 779 +hmac_md5 780 +hmac_sha1 781 +id_PasswordBasedMAC 782 +id_DHBasedMac 783 +id_it_suppLangTags 784 +caRepository 785 +id_smime_ct_compressedData 786 +id_ct_asciiTextWithCRLF 787 +id_aes128_wrap 788 +id_aes192_wrap 789 +id_aes256_wrap 790 +ecdsa_with_Recommended 791 +ecdsa_with_Specified 792 +ecdsa_with_SHA224 793 +ecdsa_with_SHA256 794 +ecdsa_with_SHA384 795 +ecdsa_with_SHA512 796 +hmacWithMD5 797 +hmacWithSHA224 798 +hmacWithSHA256 799 +hmacWithSHA384 800 +hmacWithSHA512 801 +dsa_with_SHA224 802 +dsa_with_SHA256 803 +whirlpool 804 +cryptopro 805 +cryptocom 806 +id_GostR3411_94_with_GostR3410_2001 807 +id_GostR3411_94_with_GostR3410_94 808 +id_GostR3411_94 809 +id_HMACGostR3411_94 810 +id_GostR3410_2001 811 +id_GostR3410_94 812 +id_Gost28147_89 813 +gost89_cnt 814 +id_Gost28147_89_MAC 815 +id_GostR3411_94_prf 816 +id_GostR3410_2001DH 817 +id_GostR3410_94DH 818 +id_Gost28147_89_CryptoPro_KeyMeshing 819 +id_Gost28147_89_None_KeyMeshing 820 +id_GostR3411_94_TestParamSet 821 +id_GostR3411_94_CryptoProParamSet 822 +id_Gost28147_89_TestParamSet 823 +id_Gost28147_89_CryptoPro_A_ParamSet 824 +id_Gost28147_89_CryptoPro_B_ParamSet 825 +id_Gost28147_89_CryptoPro_C_ParamSet 826 +id_Gost28147_89_CryptoPro_D_ParamSet 827 +id_Gost28147_89_CryptoPro_Oscar_1_1_ParamSet 828 +id_Gost28147_89_CryptoPro_Oscar_1_0_ParamSet 829 +id_Gost28147_89_CryptoPro_RIC_1_ParamSet 830 +id_GostR3410_94_TestParamSet 831 +id_GostR3410_94_CryptoPro_A_ParamSet 832 +id_GostR3410_94_CryptoPro_B_ParamSet 833 +id_GostR3410_94_CryptoPro_C_ParamSet 834 +id_GostR3410_94_CryptoPro_D_ParamSet 835 +id_GostR3410_94_CryptoPro_XchA_ParamSet 836 +id_GostR3410_94_CryptoPro_XchB_ParamSet 837 +id_GostR3410_94_CryptoPro_XchC_ParamSet 838 +id_GostR3410_2001_TestParamSet 839 +id_GostR3410_2001_CryptoPro_A_ParamSet 840 +id_GostR3410_2001_CryptoPro_B_ParamSet 841 +id_GostR3410_2001_CryptoPro_C_ParamSet 842 +id_GostR3410_2001_CryptoPro_XchA_ParamSet 843 +id_GostR3410_2001_CryptoPro_XchB_ParamSet 844 +id_GostR3410_94_a 845 +id_GostR3410_94_aBis 846 +id_GostR3410_94_b 847 +id_GostR3410_94_bBis 848 +id_Gost28147_89_cc 849 +id_GostR3410_94_cc 850 +id_GostR3410_2001_cc 851 +id_GostR3411_94_with_GostR3410_94_cc 852 +id_GostR3411_94_with_GostR3410_2001_cc 853 +id_GostR3410_2001_ParamSet_cc 854 +hmac 855 +LocalKeySet 856 +freshest_crl 857 +id_on_permanentIdentifier 858 +searchGuide 859 +businessCategory 860 +postalAddress 861 +postOfficeBox 862 +physicalDeliveryOfficeName 863 +telephoneNumber 864 +telexNumber 865 +teletexTerminalIdentifier 866 +facsimileTelephoneNumber 867 +x121Address 868 +internationaliSDNNumber 869 +registeredAddress 870 +destinationIndicator 871 +preferredDeliveryMethod 872 +presentationAddress 873 +supportedApplicationContext 874 +member 875 +owner 876 +roleOccupant 877 +seeAlso 878 +userPassword 879 +userCertificate 880 +cACertificate 881 +authorityRevocationList 882 +certificateRevocationList 883 +crossCertificatePair 884 +enhancedSearchGuide 885 +protocolInformation 886 +distinguishedName 887 +uniqueMember 888 +houseIdentifier 889 +supportedAlgorithms 890 +deltaRevocationList 891 +dmdName 892 +id_alg_PWRI_KEK 893 +cmac 894 +aes_128_gcm 895 +aes_128_ccm 896 +id_aes128_wrap_pad 897 +aes_192_gcm 898 +aes_192_ccm 899 +id_aes192_wrap_pad 900 +aes_256_gcm 901 +aes_256_ccm 902 +id_aes256_wrap_pad 903 +aes_128_ctr 904 +aes_192_ctr 905 +aes_256_ctr 906 +id_camellia128_wrap 907 +id_camellia192_wrap 908 +id_camellia256_wrap 909 +anyExtendedKeyUsage 910 +mgf1 911 +rsassaPss 912 +aes_128_xts 913 +aes_256_xts 914 +rc4_hmac_md5 915 +aes_128_cbc_hmac_sha1 916 +aes_192_cbc_hmac_sha1 917 +aes_256_cbc_hmac_sha1 918 +rsaesOaep 919 +dhpublicnumber 920 +brainpoolP160r1 921 +brainpoolP160t1 922 +brainpoolP192r1 923 +brainpoolP192t1 924 +brainpoolP224r1 925 +brainpoolP224t1 926 +brainpoolP256r1 927 +brainpoolP256t1 928 +brainpoolP320r1 929 +brainpoolP320t1 930 +brainpoolP384r1 931 +brainpoolP384t1 932 +brainpoolP512r1 933 +brainpoolP512t1 934 +pSpecified 935 +dhSinglePass_stdDH_sha1kdf_scheme 936 +dhSinglePass_stdDH_sha224kdf_scheme 937 +dhSinglePass_stdDH_sha256kdf_scheme 938 +dhSinglePass_stdDH_sha384kdf_scheme 939 +dhSinglePass_stdDH_sha512kdf_scheme 940 +dhSinglePass_cofactorDH_sha1kdf_scheme 941 +dhSinglePass_cofactorDH_sha224kdf_scheme 942 +dhSinglePass_cofactorDH_sha256kdf_scheme 943 +dhSinglePass_cofactorDH_sha384kdf_scheme 944 +dhSinglePass_cofactorDH_sha512kdf_scheme 945 +dh_std_kdf 946 +dh_cofactor_kdf 947 +aes_128_cbc_hmac_sha256 948 +aes_192_cbc_hmac_sha256 949 +aes_256_cbc_hmac_sha256 950 +ct_precert_scts 951 +ct_precert_poison 952 +ct_precert_signer 953 +ct_cert_scts 954 +jurisdictionLocalityName 955 +jurisdictionStateOrProvinceName 956 +jurisdictionCountryName 957 +aes_128_ocb 958 +aes_192_ocb 959 +aes_256_ocb 960 +camellia_128_gcm 961 +camellia_128_ccm 962 +camellia_128_ctr 963 +camellia_128_cmac 964 +camellia_192_gcm 965 +camellia_192_ccm 966 +camellia_192_ctr 967 +camellia_192_cmac 968 +camellia_256_gcm 969 +camellia_256_ccm 970 +camellia_256_ctr 971 +camellia_256_cmac 972 +id_scrypt 973 +id_tc26 974 +gost89_cnt_12 975 +gost_mac_12 976 +id_tc26_algorithms 977 +id_tc26_sign 978 +id_GostR3410_2012_256 979 +id_GostR3410_2012_512 980 +id_tc26_digest 981 +id_GostR3411_2012_256 982 +id_GostR3411_2012_512 983 +id_tc26_signwithdigest 984 +id_tc26_signwithdigest_gost3410_2012_256 985 +id_tc26_signwithdigest_gost3410_2012_512 986 +id_tc26_mac 987 +id_tc26_hmac_gost_3411_2012_256 988 +id_tc26_hmac_gost_3411_2012_512 989 +id_tc26_cipher 990 +id_tc26_agreement 991 +id_tc26_agreement_gost_3410_2012_256 992 +id_tc26_agreement_gost_3410_2012_512 993 +id_tc26_constants 994 +id_tc26_sign_constants 995 +id_tc26_gost_3410_2012_512_constants 996 +id_tc26_gost_3410_2012_512_paramSetTest 997 +id_tc26_gost_3410_2012_512_paramSetA 998 +id_tc26_gost_3410_2012_512_paramSetB 999 +id_tc26_digest_constants 1000 +id_tc26_cipher_constants 1001 +id_tc26_gost_28147_constants 1002 +id_tc26_gost_28147_param_Z 1003 +INN 1004 +OGRN 1005 +SNILS 1006 +subjectSignTool 1007 +issuerSignTool 1008 +gost89_cbc 1009 +gost89_ecb 1010 +gost89_ctr 1011 +kuznyechik_ecb 1012 +kuznyechik_ctr 1013 +kuznyechik_ofb 1014 +kuznyechik_cbc 1015 +kuznyechik_cfb 1016 +kuznyechik_mac 1017 +chacha20_poly1305 1018 +chacha20 1019 +tlsfeature 1020 +tls1_prf 1021 +ipsec_IKE 1022 +capwapAC 1023 +capwapWTP 1024 +sshClient 1025 +sshServer 1026 +sendRouter 1027 +sendProxiedRouter 1028 +sendOwner 1029 +sendProxiedOwner 1030 +id_pkinit 1031 +pkInitClientAuth 1032 +pkInitKDC 1033 +X25519 1034 +X448 1035 +hkdf 1036 +kx_rsa 1037 +kx_ecdhe 1038 +kx_dhe 1039 +kx_ecdhe_psk 1040 +kx_dhe_psk 1041 +kx_rsa_psk 1042 +kx_psk 1043 +kx_srp 1044 +kx_gost 1045 +auth_rsa 1046 +auth_ecdsa 1047 +auth_psk 1048 +auth_dss 1049 +auth_gost01 1050 +auth_gost12 1051 +auth_srp 1052 +auth_null 1053 +fips_none 1054 +fips_140_2 1055 +blake2b512 1056 +blake2s256 1057 +id_smime_ct_contentCollection 1058 +id_smime_ct_authEnvelopedData 1059 +id_ct_xml 1060 +poly1305 1061 +siphash 1062 +kx_any 1063 +auth_any 1064 +aria_128_ecb 1065 +aria_128_cbc 1066 +aria_128_cfb128 1067 +aria_128_ofb128 1068 +aria_128_ctr 1069 +aria_192_ecb 1070 +aria_192_cbc 1071 +aria_192_cfb128 1072 +aria_192_ofb128 1073 +aria_192_ctr 1074 +aria_256_ecb 1075 +aria_256_cbc 1076 +aria_256_cfb128 1077 +aria_256_ofb128 1078 +aria_256_ctr 1079 +aria_128_cfb1 1080 +aria_192_cfb1 1081 +aria_256_cfb1 1082 +aria_128_cfb8 1083 +aria_192_cfb8 1084 +aria_256_cfb8 1085 +id_smime_aa_signingCertificateV2 1086 +ED25519 1087 +ED448 1088 +organizationIdentifier 1089 +countryCode3c 1090 +countryCode3n 1091 +dnsName 1092 +x509ExtAdmission 1093 +sha512_224 1094 +sha512_256 1095 +sha3_224 1096 +sha3_256 1097 +sha3_384 1098 +sha3_512 1099 +shake128 1100 +shake256 1101 +hmac_sha3_224 1102 +hmac_sha3_256 1103 +hmac_sha3_384 1104 +hmac_sha3_512 1105 +dsa_with_SHA384 1106 +dsa_with_SHA512 1107 +dsa_with_SHA3_224 1108 +dsa_with_SHA3_256 1109 +dsa_with_SHA3_384 1110 +dsa_with_SHA3_512 1111 +ecdsa_with_SHA3_224 1112 +ecdsa_with_SHA3_256 1113 +ecdsa_with_SHA3_384 1114 +ecdsa_with_SHA3_512 1115 +RSA_SHA3_224 1116 +RSA_SHA3_256 1117 +RSA_SHA3_384 1118 +RSA_SHA3_512 1119 +aria_128_ccm 1120 +aria_192_ccm 1121 +aria_256_ccm 1122 +aria_128_gcm 1123 +aria_192_gcm 1124 +aria_256_gcm 1125 +ffdhe2048 1126 +ffdhe3072 1127 +ffdhe4096 1128 +ffdhe6144 1129 +ffdhe8192 1130 +cmcCA 1131 +cmcRA 1132 +sm4_ecb 1133 +sm4_cbc 1134 +sm4_ofb128 1135 +sm4_cfb1 1136 +sm4_cfb128 1137 +sm4_cfb8 1138 +sm4_ctr 1139 +ISO_CN 1140 +oscca 1141 +sm_scheme 1142 +sm3 1143 +sm3WithRSAEncryption 1144 +sha512_224WithRSAEncryption 1145 +sha512_256WithRSAEncryption 1146 +id_tc26_gost_3410_2012_256_constants 1147 +id_tc26_gost_3410_2012_256_paramSetA 1148 +id_tc26_gost_3410_2012_512_paramSetC 1149 +ISO_UA 1150 +ua_pki 1151 +dstu28147 1152 +dstu28147_ofb 1153 +dstu28147_cfb 1154 +dstu28147_wrap 1155 +hmacWithDstu34311 1156 +dstu34311 1157 +dstu4145le 1158 +dstu4145be 1159 +uacurve0 1160 +uacurve1 1161 +uacurve2 1162 +uacurve3 1163 +uacurve4 1164 +uacurve5 1165 +uacurve6 1166 +uacurve7 1167 +uacurve8 1168 +uacurve9 1169 +ieee 1170 +ieee_siswg 1171 +sm2 1172 +id_tc26_cipher_gostr3412_2015_magma 1173 +magma_ctr_acpkm 1174 +magma_ctr_acpkm_omac 1175 +id_tc26_cipher_gostr3412_2015_kuznyechik 1176 +kuznyechik_ctr_acpkm 1177 +kuznyechik_ctr_acpkm_omac 1178 +id_tc26_wrap 1179 +id_tc26_wrap_gostr3412_2015_magma 1180 +magma_kexp15 1181 +id_tc26_wrap_gostr3412_2015_kuznyechik 1182 +kuznyechik_kexp15 1183 +id_tc26_gost_3410_2012_256_paramSetB 1184 +id_tc26_gost_3410_2012_256_paramSetC 1185 +id_tc26_gost_3410_2012_256_paramSetD 1186 +magma_ecb 1187 +magma_ctr 1188 +magma_ofb 1189 +magma_cbc 1190 +magma_cfb 1191 +magma_mac 1192 +hmacWithSHA512_224 1193 +hmacWithSHA512_256 1194 +gmac 1195 +kmac128 1196 +kmac256 1197 +aes_128_siv 1198 +aes_192_siv 1199 +aes_256_siv 1200 +blake2bmac 1201 +blake2smac 1202 +sshkdf 1203 +SM2_with_SM3 1204 +sskdf 1205 +x963kdf 1206 +x942kdf 1207 +id_on_SmtpUTF8Mailbox 1208 +XmppAddr 1209 +SRVName 1210 +NAIRealm 1211 +modp_1536 1212 +modp_2048 1213 +modp_3072 1214 +modp_4096 1215 +modp_6144 1216 +modp_8192 1217 +kx_gost18 1218 +cmcArchive 1219 +id_kp_bgpsec_router 1220 +id_kp_BrandIndicatorforMessageIdentification 1221 +cmKGA 1222 +id_it_caCerts 1223 +id_it_rootCaKeyUpdate 1224 +id_it_certReqTemplate 1225 +OGRNIP 1226 +classSignTool 1227 +classSignToolKC1 1228 +classSignToolKC2 1229 +classSignToolKC3 1230 +classSignToolKB1 1231 +classSignToolKB2 1232 +classSignToolKA1 1233 +id_ct_routeOriginAuthz 1234 +id_ct_rpkiManifest 1235 +id_ct_rpkiGhostbusters 1236 +id_ct_resourceTaggedAttest 1237 +id_cp 1238 +sbgp_ipAddrBlockv2 1239 +sbgp_autonomousSysNumv2 1240 +ipAddr_asNumber 1241 +ipAddr_asNumberv2 1242 +rpkiManifest 1243 +signedObject 1244 +rpkiNotify 1245 +id_ct_geofeedCSVwithCRLF 1246 +id_ct_signedChecklist 1247 +sm4_gcm 1248 +sm4_ccm 1249 +id_ct_ASPA 1250 +id_mod_cmp2000_02 1251 +id_mod_cmp2021_88 1252 +id_mod_cmp2021_02 1253 +id_it_rootCaCert 1254 +id_it_certProfile 1255 +id_it_crlStatusList 1256 +id_it_crls 1257 +id_regCtrl_altCertTemplate 1258 +id_regCtrl_algId 1259 +id_regCtrl_rsaKeyLen 1260 +id_aa_ets_attrCertificateRefs 1261 +id_aa_ets_attrRevocationRefs 1262 +id_aa_CMSAlgorithmProtection 1263 +itu_t_identified_organization 1264 +etsi 1265 +electronic_signature_standard 1266 +ess_attributes 1267 +id_aa_ets_mimeType 1268 +id_aa_ets_longTermValidation 1269 +id_aa_ets_SignaturePolicyDocument 1270 +id_aa_ets_archiveTimestampV3 1271 +id_aa_ATSHashIndex 1272 +cades 1273 +cades_attributes 1274 +id_aa_ets_signerAttrV2 1275 +id_aa_ets_sigPolicyStore 1276 +id_aa_ATSHashIndex_v2 1277 +id_aa_ATSHashIndex_v3 1278 +signedAssertion 1279 +id_aa_ets_archiveTimestampV2 1280 +hmacWithSM3 1281 +oracle 1282 +oracle_jdk_trustedkeyusage 1283 +id_ct_signedTAL 1284 +brainpoolP256r1tls13 1285 +brainpoolP384r1tls13 1286 +brainpoolP512r1tls13 1287 +brotli 1288 +zstd 1289 +sm4_xts 1290 +ms_ntds_obj_sid 1291 +ms_ntds_sec_ext 1292 +ms_cert_templ 1293 +ms_app_policies 1294 +authority_attribute_identifier 1295 +role_spec_cert_identifier 1296 +basic_att_constraints 1297 +delegated_name_constraints 1298 +time_specification 1299 +attribute_descriptor 1300 +user_notice 1301 +soa_identifier 1302 +acceptable_cert_policies 1303 +acceptable_privilege_policies 1304 +indirect_issuer 1305 +no_assertion 1306 +id_aa_issuing_distribution_point 1307 +issued_on_behalf_of 1308 +single_use 1309 +group_ac 1310 +allowed_attribute_assignments 1311 +attribute_mappings 1312 +holder_name_constraints 1313 +authorization_validation 1314 +prot_restrict 1315 +subject_alt_public_key_info 1316 +alt_signature_algorithm 1317 +alt_signature_value 1318 +associated_information 1319 +id_ct_rpkiSignedPrefixList 1320 +id_on_hardwareModuleName 1321 +id_kp_wisun_fan_device 1322 +ac_auditEntity 1323 +tcg 1324 +tcg_tcpaSpecVersion 1325 +tcg_attribute 1326 +tcg_protocol 1327 +tcg_algorithm 1328 +tcg_platformClass 1329 +tcg_ce 1330 +tcg_kp 1331 +tcg_ca 1332 +tcg_address 1333 +tcg_registry 1334 +tcg_traits 1335 +tcg_common 1336 +tcg_at_platformManufacturerStr 1337 +tcg_at_platformManufacturerId 1338 +tcg_at_platformConfigUri 1339 +tcg_at_platformModel 1340 +tcg_at_platformVersion 1341 +tcg_at_platformSerial 1342 +tcg_at_platformConfiguration 1343 +tcg_at_platformIdentifier 1344 +tcg_at_tpmManufacturer 1345 +tcg_at_tpmModel 1346 +tcg_at_tpmVersion 1347 +tcg_at_securityQualities 1348 +tcg_at_tpmProtectionProfile 1349 +tcg_at_tpmSecurityTarget 1350 +tcg_at_tbbProtectionProfile 1351 +tcg_at_tbbSecurityTarget 1352 +tcg_at_tpmIdLabel 1353 +tcg_at_tpmSpecification 1354 +tcg_at_tcgPlatformSpecification 1355 +tcg_at_tpmSecurityAssertions 1356 +tcg_at_tbbSecurityAssertions 1357 +tcg_at_tcgCredentialSpecification 1358 +tcg_at_tcgCredentialType 1359 +tcg_at_previousPlatformCertificates 1360 +tcg_at_tbbSecurityAssertions_v3 1361 +tcg_at_cryptographicAnchors 1362 +tcg_at_platformConfiguration_v1 1363 +tcg_at_platformConfiguration_v2 1364 +tcg_at_platformConfiguration_v3 1365 +tcg_at_platformConfigUri_v3 1366 +tcg_algorithm_null 1367 +tcg_kp_EKCertificate 1368 +tcg_kp_PlatformAttributeCertificate 1369 +tcg_kp_AIKCertificate 1370 +tcg_kp_PlatformKeyCertificate 1371 +tcg_kp_DeltaPlatformAttributeCertificate 1372 +tcg_kp_DeltaPlatformKeyCertificate 1373 +tcg_kp_AdditionalPlatformAttributeCertificate 1374 +tcg_kp_AdditionalPlatformKeyCertificate 1375 +tcg_ce_relevantCredentials 1376 +tcg_ce_relevantManifests 1377 +tcg_ce_virtualPlatformAttestationService 1378 +tcg_ce_migrationControllerAttestationService 1379 +tcg_ce_migrationControllerRegistrationService 1380 +tcg_ce_virtualPlatformBackupService 1381 +tcg_prt_tpmIdProtocol 1382 +tcg_address_ethernetmac 1383 +tcg_address_wlanmac 1384 +tcg_address_bluetoothmac 1385 +tcg_registry_componentClass 1386 +tcg_registry_componentClass_tcg 1387 +tcg_registry_componentClass_ietf 1388 +tcg_registry_componentClass_dmtf 1389 +tcg_registry_componentClass_pcie 1390 +tcg_registry_componentClass_disk 1391 +tcg_cap_verifiedPlatformCertificate 1392 +tcg_tr_ID 1393 +tcg_tr_category 1394 +tcg_tr_registry 1395 +tcg_tr_ID_Boolean 1396 +tcg_tr_ID_CertificateIdentifier 1397 +tcg_tr_ID_CommonCriteria 1398 +tcg_tr_ID_componentClass 1399 +tcg_tr_ID_componentIdentifierV11 1400 +tcg_tr_ID_FIPSLevel 1401 +tcg_tr_ID_ISO9000Level 1402 +tcg_tr_ID_networkMAC 1403 +tcg_tr_ID_OID 1404 +tcg_tr_ID_PEN 1405 +tcg_tr_ID_platformFirmwareCapabilities 1406 +tcg_tr_ID_platformFirmwareSignatureVerification 1407 +tcg_tr_ID_platformFirmwareUpdateCompliance 1408 +tcg_tr_ID_platformHardwareCapabilities 1409 +tcg_tr_ID_RTM 1410 +tcg_tr_ID_status 1411 +tcg_tr_ID_URI 1412 +tcg_tr_ID_UTF8String 1413 +tcg_tr_ID_IA5String 1414 +tcg_tr_ID_PEMCertString 1415 +tcg_tr_ID_PublicKey 1416 +tcg_tr_cat_platformManufacturer 1417 +tcg_tr_cat_platformModel 1418 +tcg_tr_cat_platformVersion 1419 +tcg_tr_cat_platformSerial 1420 +tcg_tr_cat_platformManufacturerIdentifier 1421 +tcg_tr_cat_platformOwnership 1422 +tcg_tr_cat_componentClass 1423 +tcg_tr_cat_componentManufacturer 1424 +tcg_tr_cat_componentModel 1425 +tcg_tr_cat_componentSerial 1426 +tcg_tr_cat_componentStatus 1427 +tcg_tr_cat_componentLocation 1428 +tcg_tr_cat_componentRevision 1429 +tcg_tr_cat_componentFieldReplaceable 1430 +tcg_tr_cat_EKCertificate 1431 +tcg_tr_cat_IAKCertificate 1432 +tcg_tr_cat_IDevIDCertificate 1433 +tcg_tr_cat_DICECertificate 1434 +tcg_tr_cat_SPDMCertificate 1435 +tcg_tr_cat_PEMCertificate 1436 +tcg_tr_cat_PlatformCertificate 1437 +tcg_tr_cat_DeltaPlatformCertificate 1438 +tcg_tr_cat_RebasePlatformCertificate 1439 +tcg_tr_cat_genericCertificate 1440 +tcg_tr_cat_CommonCriteria 1441 +tcg_tr_cat_componentIdentifierV11 1442 +tcg_tr_cat_FIPSLevel 1443 +tcg_tr_cat_ISO9000 1444 +tcg_tr_cat_networkMAC 1445 +tcg_tr_cat_attestationProtocol 1446 +tcg_tr_cat_PEN 1447 +tcg_tr_cat_platformFirmwareCapabilities 1448 +tcg_tr_cat_platformHardwareCapabilities 1449 +tcg_tr_cat_platformFirmwareSignatureVerification 1450 +tcg_tr_cat_platformFirmwareUpdateCompliance 1451 +tcg_tr_cat_RTM 1452 +tcg_tr_cat_PublicKey 1453 +ML_KEM_512 1454 +ML_KEM_768 1455 +ML_KEM_1024 1456 +ML_DSA_44 1457 +ML_DSA_65 1458 +ML_DSA_87 1459 +SLH_DSA_SHA2_128s 1460 +SLH_DSA_SHA2_128f 1461 +SLH_DSA_SHA2_192s 1462 +SLH_DSA_SHA2_192f 1463 +SLH_DSA_SHA2_256s 1464 +SLH_DSA_SHA2_256f 1465 +SLH_DSA_SHAKE_128s 1466 +SLH_DSA_SHAKE_128f 1467 +SLH_DSA_SHAKE_192s 1468 +SLH_DSA_SHAKE_192f 1469 +SLH_DSA_SHAKE_256s 1470 +SLH_DSA_SHAKE_256f 1471 +HASH_ML_DSA_44_WITH_SHA512 1472 +HASH_ML_DSA_65_WITH_SHA512 1473 +HASH_ML_DSA_87_WITH_SHA512 1474 +SLH_DSA_SHA2_128s_WITH_SHA256 1475 +SLH_DSA_SHA2_128f_WITH_SHA256 1476 +SLH_DSA_SHA2_192s_WITH_SHA512 1477 +SLH_DSA_SHA2_192f_WITH_SHA512 1478 +SLH_DSA_SHA2_256s_WITH_SHA512 1479 +SLH_DSA_SHA2_256f_WITH_SHA512 1480 +SLH_DSA_SHAKE_128s_WITH_SHAKE128 1481 +SLH_DSA_SHAKE_128f_WITH_SHAKE128 1482 +SLH_DSA_SHAKE_192s_WITH_SHAKE256 1483 +SLH_DSA_SHAKE_192f_WITH_SHAKE256 1484 +SLH_DSA_SHAKE_256s_WITH_SHAKE256 1485 +SLH_DSA_SHAKE_256f_WITH_SHAKE256 1486 +aes_128_cbc_hmac_sha1_etm 1487 +aes_192_cbc_hmac_sha1_etm 1488 +aes_256_cbc_hmac_sha1_etm 1489 +aes_128_cbc_hmac_sha256_etm 1490 +aes_192_cbc_hmac_sha256_etm 1491 +aes_256_cbc_hmac_sha256_etm 1492 +aes_128_cbc_hmac_sha512_etm 1493 +aes_192_cbc_hmac_sha512_etm 1494 +aes_256_cbc_hmac_sha512_etm 1495 +HKDF_SHA256 1496 +HKDF_SHA384 1497 +HKDF_SHA512 1498 +id_smime_ori 1499 +id_smime_ori_kem 1500 +id_alg_hss_lms_hashsig 1501 diff --git a/crates/stdlib/src/rustls-data/objects.txt b/crates/stdlib/src/rustls-data/objects.txt new file mode 100644 index 00000000000..946cdf5ec68 --- /dev/null +++ b/crates/stdlib/src/rustls-data/objects.txt @@ -0,0 +1,2069 @@ +# CCITT was renamed to ITU-T quite some time ago +0 : ITU-T : itu-t +!Alias ccitt itu-t + +1 : ISO : iso + +2 : JOINT-ISO-ITU-T : joint-iso-itu-t +!Alias joint-iso-ccitt joint-iso-itu-t + +iso 2 : member-body : ISO Member Body + +iso 3 : identified-organization + +# GMAC OID +iso 0 9797 3 4 : GMAC : gmac + +# HMAC OIDs +identified-organization 6 1 5 5 8 1 1 : HMAC-MD5 : hmac-md5 +identified-organization 6 1 5 5 8 1 2 : HMAC-SHA1 : hmac-sha1 + +# "1.3.36.8.3.3" +identified-organization 36 8 3 3 : x509ExtAdmission : Professional Information or basis for Admission + +identified-organization 132 : certicom-arc + +identified-organization 111 : ieee +ieee 2 1619 : ieee-siswg : IEEE Security in Storage Working Group + +joint-iso-itu-t 23 : international-organizations : International Organizations + +international-organizations 43 : wap +wap 1 : wap-wsg + +joint-iso-itu-t 5 1 5 : selected-attribute-types : Selected Attribute Types + +selected-attribute-types 55 : clearance + +member-body 840 : ISO-US : ISO US Member Body +ISO-US 10040 : X9-57 : X9.57 +X9-57 4 : X9cm : X9.57 CM ? + +member-body 156 : ISO-CN : ISO CN Member Body +ISO-CN 10197 : oscca +oscca 1 : sm-scheme + +!Cname dsa +X9cm 1 : DSA : dsaEncryption +X9cm 3 : DSA-SHA1 : dsaWithSHA1 + + +ISO-US 10045 : ansi-X9-62 : ANSI X9.62 +!module X9-62 +!Alias id-fieldType ansi-X9-62 1 +X9-62_id-fieldType 1 : prime-field +X9-62_id-fieldType 2 : characteristic-two-field +X9-62_characteristic-two-field 3 : id-characteristic-two-basis +X9-62_id-characteristic-two-basis 1 : onBasis +X9-62_id-characteristic-two-basis 2 : tpBasis +X9-62_id-characteristic-two-basis 3 : ppBasis +!Alias id-publicKeyType ansi-X9-62 2 +X9-62_id-publicKeyType 1 : id-ecPublicKey +!Alias ellipticCurve ansi-X9-62 3 +!Alias c-TwoCurve X9-62_ellipticCurve 0 +X9-62_c-TwoCurve 1 : c2pnb163v1 +X9-62_c-TwoCurve 2 : c2pnb163v2 +X9-62_c-TwoCurve 3 : c2pnb163v3 +X9-62_c-TwoCurve 4 : c2pnb176v1 +X9-62_c-TwoCurve 5 : c2tnb191v1 +X9-62_c-TwoCurve 6 : c2tnb191v2 +X9-62_c-TwoCurve 7 : c2tnb191v3 +X9-62_c-TwoCurve 8 : c2onb191v4 +X9-62_c-TwoCurve 9 : c2onb191v5 +X9-62_c-TwoCurve 10 : c2pnb208w1 +X9-62_c-TwoCurve 11 : c2tnb239v1 +X9-62_c-TwoCurve 12 : c2tnb239v2 +X9-62_c-TwoCurve 13 : c2tnb239v3 +X9-62_c-TwoCurve 14 : c2onb239v4 +X9-62_c-TwoCurve 15 : c2onb239v5 +X9-62_c-TwoCurve 16 : c2pnb272w1 +X9-62_c-TwoCurve 17 : c2pnb304w1 +X9-62_c-TwoCurve 18 : c2tnb359v1 +X9-62_c-TwoCurve 19 : c2pnb368w1 +X9-62_c-TwoCurve 20 : c2tnb431r1 +!Alias primeCurve X9-62_ellipticCurve 1 +X9-62_primeCurve 1 : prime192v1 +X9-62_primeCurve 2 : prime192v2 +X9-62_primeCurve 3 : prime192v3 +X9-62_primeCurve 4 : prime239v1 +X9-62_primeCurve 5 : prime239v2 +X9-62_primeCurve 6 : prime239v3 +X9-62_primeCurve 7 : prime256v1 +!Alias id-ecSigType ansi-X9-62 4 +!global +X9-62_id-ecSigType 1 : ecdsa-with-SHA1 +X9-62_id-ecSigType 2 : ecdsa-with-Recommended +X9-62_id-ecSigType 3 : ecdsa-with-Specified +ecdsa-with-Specified 1 : ecdsa-with-SHA224 +ecdsa-with-Specified 2 : ecdsa-with-SHA256 +ecdsa-with-Specified 3 : ecdsa-with-SHA384 +ecdsa-with-Specified 4 : ecdsa-with-SHA512 + +# SECG curve OIDs from "SEC 2: Recommended Elliptic Curve Domain Parameters" +# (http://www.secg.org/) +!Alias secg_ellipticCurve certicom-arc 0 +# SECG prime curves OIDs +secg-ellipticCurve 6 : secp112r1 +secg-ellipticCurve 7 : secp112r2 +secg-ellipticCurve 28 : secp128r1 +secg-ellipticCurve 29 : secp128r2 +secg-ellipticCurve 9 : secp160k1 +secg-ellipticCurve 8 : secp160r1 +secg-ellipticCurve 30 : secp160r2 +secg-ellipticCurve 31 : secp192k1 +# NOTE: the curve secp192r1 is the same as prime192v1 defined above +# and is therefore omitted +secg-ellipticCurve 32 : secp224k1 +secg-ellipticCurve 33 : secp224r1 +secg-ellipticCurve 10 : secp256k1 +# NOTE: the curve secp256r1 is the same as prime256v1 defined above +# and is therefore omitted +secg-ellipticCurve 34 : secp384r1 +secg-ellipticCurve 35 : secp521r1 +# SECG characteristic two curves OIDs +secg-ellipticCurve 4 : sect113r1 +secg-ellipticCurve 5 : sect113r2 +secg-ellipticCurve 22 : sect131r1 +secg-ellipticCurve 23 : sect131r2 +secg-ellipticCurve 1 : sect163k1 +secg-ellipticCurve 2 : sect163r1 +secg-ellipticCurve 15 : sect163r2 +secg-ellipticCurve 24 : sect193r1 +secg-ellipticCurve 25 : sect193r2 +secg-ellipticCurve 26 : sect233k1 +secg-ellipticCurve 27 : sect233r1 +secg-ellipticCurve 3 : sect239k1 +secg-ellipticCurve 16 : sect283k1 +secg-ellipticCurve 17 : sect283r1 +secg-ellipticCurve 36 : sect409k1 +secg-ellipticCurve 37 : sect409r1 +secg-ellipticCurve 38 : sect571k1 +secg-ellipticCurve 39 : sect571r1 + +# WAP/TLS curve OIDs (http://www.wapforum.org/) +!Alias wap-wsg-idm-ecid wap-wsg 4 +wap-wsg-idm-ecid 1 : wap-wsg-idm-ecid-wtls1 +wap-wsg-idm-ecid 3 : wap-wsg-idm-ecid-wtls3 +wap-wsg-idm-ecid 4 : wap-wsg-idm-ecid-wtls4 +wap-wsg-idm-ecid 5 : wap-wsg-idm-ecid-wtls5 +wap-wsg-idm-ecid 6 : wap-wsg-idm-ecid-wtls6 +wap-wsg-idm-ecid 7 : wap-wsg-idm-ecid-wtls7 +wap-wsg-idm-ecid 8 : wap-wsg-idm-ecid-wtls8 +wap-wsg-idm-ecid 9 : wap-wsg-idm-ecid-wtls9 +wap-wsg-idm-ecid 10 : wap-wsg-idm-ecid-wtls10 +wap-wsg-idm-ecid 11 : wap-wsg-idm-ecid-wtls11 +wap-wsg-idm-ecid 12 : wap-wsg-idm-ecid-wtls12 + + +ISO-US 113533 7 66 10 : CAST5-CBC : cast5-cbc + : CAST5-ECB : cast5-ecb +!Cname cast5-cfb64 + : CAST5-CFB : cast5-cfb +!Cname cast5-ofb64 + : CAST5-OFB : cast5-ofb +!Cname pbeWithMD5AndCast5-CBC +ISO-US 113533 7 66 12 : : pbeWithMD5AndCast5CBC + +# Macs for CMP and CRMF +ISO-US 113533 7 66 13 : id-PasswordBasedMAC : password based MAC +ISO-US 113533 7 66 30 : id-DHBasedMac : Diffie-Hellman based MAC + +ISO-US 113549 : rsadsi : RSA Data Security, Inc. + +rsadsi 1 : pkcs : RSA Data Security, Inc. PKCS + +pkcs 1 : pkcs1 +pkcs1 1 : : rsaEncryption +pkcs1 2 : RSA-MD2 : md2WithRSAEncryption +pkcs1 3 : RSA-MD4 : md4WithRSAEncryption +pkcs1 4 : RSA-MD5 : md5WithRSAEncryption +pkcs1 5 : RSA-SHA1 : sha1WithRSAEncryption +# According to PKCS #1 version 2.1 +pkcs1 7 : RSAES-OAEP : rsaesOaep +pkcs1 8 : MGF1 : mgf1 +pkcs1 9 : PSPECIFIED : pSpecified +pkcs1 10 : RSASSA-PSS : rsassaPss + +pkcs1 11 : RSA-SHA256 : sha256WithRSAEncryption +pkcs1 12 : RSA-SHA384 : sha384WithRSAEncryption +pkcs1 13 : RSA-SHA512 : sha512WithRSAEncryption +pkcs1 14 : RSA-SHA224 : sha224WithRSAEncryption +pkcs1 15 : RSA-SHA512/224 : sha512-224WithRSAEncryption +pkcs1 16 : RSA-SHA512/256 : sha512-256WithRSAEncryption + +pkcs 3 : pkcs3 +pkcs3 1 : : dhKeyAgreement + +pkcs 5 : pkcs5 +pkcs5 1 : PBE-MD2-DES : pbeWithMD2AndDES-CBC +pkcs5 3 : PBE-MD5-DES : pbeWithMD5AndDES-CBC +pkcs5 4 : PBE-MD2-RC2-64 : pbeWithMD2AndRC2-CBC +pkcs5 6 : PBE-MD5-RC2-64 : pbeWithMD5AndRC2-CBC +pkcs5 10 : PBE-SHA1-DES : pbeWithSHA1AndDES-CBC +pkcs5 11 : PBE-SHA1-RC2-64 : pbeWithSHA1AndRC2-CBC +!Cname id_pbkdf2 +pkcs5 12 : : PBKDF2 +!Cname pbes2 +pkcs5 13 : : PBES2 +!Cname pbmac1 +pkcs5 14 : : PBMAC1 + +pkcs 7 : pkcs7 +pkcs7 1 : : pkcs7-data +!Cname pkcs7-signed +pkcs7 2 : : pkcs7-signedData +!Cname pkcs7-enveloped +pkcs7 3 : : pkcs7-envelopedData +!Cname pkcs7-signedAndEnveloped +pkcs7 4 : : pkcs7-signedAndEnvelopedData +!Cname pkcs7-digest +pkcs7 5 : : pkcs7-digestData +!Cname pkcs7-encrypted +pkcs7 6 : : pkcs7-encryptedData + +pkcs 9 : pkcs9 +!module pkcs9 +pkcs9 1 : : emailAddress +pkcs9 2 : : unstructuredName +pkcs9 3 : : contentType +pkcs9 4 : : messageDigest +pkcs9 5 : : signingTime +pkcs9 6 : : countersignature +pkcs9 7 : : challengePassword +pkcs9 8 : : unstructuredAddress +!Cname extCertAttributes +pkcs9 9 : : extendedCertificateAttributes +!global + +!Cname ext-req +pkcs9 14 : extReq : Extension Request + +!Cname SMIMECapabilities +pkcs9 15 : SMIME-CAPS : S/MIME Capabilities + +# S/MIME +!Cname SMIME +pkcs9 16 : SMIME : S/MIME +SMIME 0 : id-smime-mod +SMIME 1 : id-smime-ct +SMIME 2 : id-smime-aa +SMIME 3 : id-smime-alg +SMIME 4 : id-smime-cd +SMIME 5 : id-smime-spq +SMIME 6 : id-smime-cti +SMIME 13 : id-smime-ori + +# S/MIME Modules +id-smime-mod 1 : id-smime-mod-cms +id-smime-mod 2 : id-smime-mod-ess +id-smime-mod 3 : id-smime-mod-oid +id-smime-mod 4 : id-smime-mod-msg-v3 +id-smime-mod 5 : id-smime-mod-ets-eSignature-88 +id-smime-mod 6 : id-smime-mod-ets-eSignature-97 +id-smime-mod 7 : id-smime-mod-ets-eSigPolicy-88 +id-smime-mod 8 : id-smime-mod-ets-eSigPolicy-97 + +# S/MIME Content Types +id-smime-ct 1 : id-smime-ct-receipt +id-smime-ct 2 : id-smime-ct-authData +id-smime-ct 3 : id-smime-ct-publishCert +id-smime-ct 4 : id-smime-ct-TSTInfo +id-smime-ct 5 : id-smime-ct-TDTInfo +id-smime-ct 6 : id-smime-ct-contentInfo +id-smime-ct 7 : id-smime-ct-DVCSRequestData +id-smime-ct 8 : id-smime-ct-DVCSResponseData +id-smime-ct 9 : id-smime-ct-compressedData +id-smime-ct 19 : id-smime-ct-contentCollection +id-smime-ct 23 : id-smime-ct-authEnvelopedData +id-smime-ct 24 : id-ct-routeOriginAuthz +id-smime-ct 26 : id-ct-rpkiManifest +id-smime-ct 27 : id-ct-asciiTextWithCRLF +id-smime-ct 28 : id-ct-xml +id-smime-ct 35 : id-ct-rpkiGhostbusters +id-smime-ct 36 : id-ct-resourceTaggedAttest +id-smime-ct 47 : id-ct-geofeedCSVwithCRLF +id-smime-ct 48 : id-ct-signedChecklist +id-smime-ct 49 : id-ct-ASPA +id-smime-ct 50 : id-ct-signedTAL +id-smime-ct 51 : id-ct-rpkiSignedPrefixList + +# S/MIME Attributes +id-smime-aa 1 : id-smime-aa-receiptRequest +id-smime-aa 2 : id-smime-aa-securityLabel +id-smime-aa 3 : id-smime-aa-mlExpandHistory +id-smime-aa 4 : id-smime-aa-contentHint +id-smime-aa 5 : id-smime-aa-msgSigDigest +# obsolete +id-smime-aa 6 : id-smime-aa-encapContentType +id-smime-aa 7 : id-smime-aa-contentIdentifier +# obsolete +id-smime-aa 8 : id-smime-aa-macValue +id-smime-aa 9 : id-smime-aa-equivalentLabels +id-smime-aa 10 : id-smime-aa-contentReference +id-smime-aa 11 : id-smime-aa-encrypKeyPref +id-smime-aa 12 : id-smime-aa-signingCertificate +id-smime-aa 13 : id-smime-aa-smimeEncryptCerts +id-smime-aa 14 : id-smime-aa-timeStampToken +id-smime-aa 15 : id-smime-aa-ets-sigPolicyId +id-smime-aa 16 : id-smime-aa-ets-commitmentType +id-smime-aa 17 : id-smime-aa-ets-signerLocation +id-smime-aa 18 : id-smime-aa-ets-signerAttr +id-smime-aa 19 : id-smime-aa-ets-otherSigCert +id-smime-aa 20 : id-smime-aa-ets-contentTimestamp +id-smime-aa 21 : id-smime-aa-ets-CertificateRefs +id-smime-aa 22 : id-smime-aa-ets-RevocationRefs +id-smime-aa 23 : id-smime-aa-ets-certValues +id-smime-aa 24 : id-smime-aa-ets-revocationValues +id-smime-aa 25 : id-smime-aa-ets-escTimeStamp +id-smime-aa 26 : id-smime-aa-ets-certCRLTimestamp +id-smime-aa 27 : id-smime-aa-ets-archiveTimeStamp +id-smime-aa 28 : id-smime-aa-signatureType +id-smime-aa 29 : id-smime-aa-dvcs-dvc +id-smime-aa 44 : id-aa-ets-attrCertificateRefs +id-smime-aa 45 : id-aa-ets-attrRevocationRefs +id-smime-aa 47 : id-smime-aa-signingCertificateV2 +id-smime-aa 48 : id-aa-ets-archiveTimestampV2 + +# S/MIME Algorithm Identifiers +# obsolete +id-smime-alg 1 : id-smime-alg-ESDHwith3DES +# obsolete +id-smime-alg 2 : id-smime-alg-ESDHwithRC2 +# obsolete +id-smime-alg 3 : id-smime-alg-3DESwrap +# obsolete +id-smime-alg 4 : id-smime-alg-RC2wrap +id-smime-alg 5 : id-smime-alg-ESDH +id-smime-alg 6 : id-smime-alg-CMS3DESwrap +id-smime-alg 7 : id-smime-alg-CMSRC2wrap +id-smime-alg 9 : id-alg-PWRI-KEK +id-smime-alg 17 : id-alg-hss-lms-hashsig +id-smime-alg 28 : id-alg-hkdf-with-sha256 : HKDF-SHA256 +id-smime-alg 29 : id-alg-hkdf-with-sha384 : HKDF-SHA384 +id-smime-alg 30 : id-alg-hkdf-with-sha512 : HKDF-SHA512 + +# S/MIME Certificate Distribution +id-smime-cd 1 : id-smime-cd-ldap + +# S/MIME Signature Policy Qualifier +id-smime-spq 1 : id-smime-spq-ets-sqt-uri +id-smime-spq 2 : id-smime-spq-ets-sqt-unotice + +# S/MIME Commitment Type Identifier +id-smime-cti 1 : id-smime-cti-ets-proofOfOrigin +id-smime-cti 2 : id-smime-cti-ets-proofOfReceipt +id-smime-cti 3 : id-smime-cti-ets-proofOfDelivery +id-smime-cti 4 : id-smime-cti-ets-proofOfSender +id-smime-cti 5 : id-smime-cti-ets-proofOfApproval +id-smime-cti 6 : id-smime-cti-ets-proofOfCreation + +# S/MIME OtherRecipientInfo Type Identifier +id-smime-ori 3 : id-smime-ori-kem + +pkcs9 20 : : friendlyName +pkcs9 21 : : localKeyID +!Alias ms-corp 1 3 6 1 4 1 311 +!Cname ms-csp-name +ms-corp 17 1 : CSPName : Microsoft CSP Name +ms-corp 17 2 : LocalKeySet : Microsoft Local Key set +!Alias certTypes pkcs9 22 +certTypes 1 : : x509Certificate +certTypes 2 : : sdsiCertificate +!Alias crlTypes pkcs9 23 +crlTypes 1 : : x509Crl + +pkcs9 52 : id-aa-CMSAlgorithmProtection + +!Alias pkcs12 pkcs 12 +!Alias pkcs12-pbeids pkcs12 1 + +!Cname pbe-WithSHA1And128BitRC4 +pkcs12-pbeids 1 : PBE-SHA1-RC4-128 : pbeWithSHA1And128BitRC4 +!Cname pbe-WithSHA1And40BitRC4 +pkcs12-pbeids 2 : PBE-SHA1-RC4-40 : pbeWithSHA1And40BitRC4 +!Cname pbe-WithSHA1And3_Key_TripleDES-CBC +pkcs12-pbeids 3 : PBE-SHA1-3DES : pbeWithSHA1And3-KeyTripleDES-CBC +!Cname pbe-WithSHA1And2_Key_TripleDES-CBC +pkcs12-pbeids 4 : PBE-SHA1-2DES : pbeWithSHA1And2-KeyTripleDES-CBC +!Cname pbe-WithSHA1And128BitRC2-CBC +pkcs12-pbeids 5 : PBE-SHA1-RC2-128 : pbeWithSHA1And128BitRC2-CBC +!Cname pbe-WithSHA1And40BitRC2-CBC +pkcs12-pbeids 6 : PBE-SHA1-RC2-40 : pbeWithSHA1And40BitRC2-CBC + +!Alias pkcs12-Version1 pkcs12 10 +!Alias pkcs12-BagIds pkcs12-Version1 1 +pkcs12-BagIds 1 : : keyBag +pkcs12-BagIds 2 : : pkcs8ShroudedKeyBag +pkcs12-BagIds 3 : : certBag +pkcs12-BagIds 4 : : crlBag +pkcs12-BagIds 5 : : secretBag +pkcs12-BagIds 6 : : safeContentsBag + +rsadsi 2 2 : MD2 : md2 +rsadsi 2 4 : MD4 : md4 +rsadsi 2 5 : MD5 : md5 + : MD5-SHA1 : md5-sha1 +rsadsi 2 6 : : hmacWithMD5 +rsadsi 2 7 : : hmacWithSHA1 + +sm-scheme 301 : SM2 : sm2 + +sm-scheme 401 : SM3 : sm3 +sm-scheme 504 : RSA-SM3 : sm3WithRSAEncryption + +sm-scheme 501 : SM2-SM3 : SM2-with-SM3 + +# From GM/T 0091-2020 +sm3 3 1 : : hmacWithSM3 + +# From RFC4231 +rsadsi 2 8 : : hmacWithSHA224 +rsadsi 2 9 : : hmacWithSHA256 +rsadsi 2 10 : : hmacWithSHA384 +rsadsi 2 11 : : hmacWithSHA512 + +# From RFC8018 +rsadsi 2 12 : : hmacWithSHA512-224 +rsadsi 2 13 : : hmacWithSHA512-256 + +rsadsi 3 2 : RC2-CBC : rc2-cbc + : RC2-ECB : rc2-ecb +!Cname rc2-cfb64 + : RC2-CFB : rc2-cfb +!Cname rc2-ofb64 + : RC2-OFB : rc2-ofb + : RC2-40-CBC : rc2-40-cbc + : RC2-64-CBC : rc2-64-cbc +rsadsi 3 4 : RC4 : rc4 + : RC4-40 : rc4-40 +rsadsi 3 7 : DES-EDE3-CBC : des-ede3-cbc +rsadsi 3 8 : RC5-CBC : rc5-cbc + : RC5-ECB : rc5-ecb +!Cname rc5-cfb64 + : RC5-CFB : rc5-cfb +!Cname rc5-ofb64 + : RC5-OFB : rc5-ofb + +!Cname ms-ext-req +ms-corp 2 1 14 : msExtReq : Microsoft Extension Request +!Cname ms-code-ind +ms-corp 2 1 21 : msCodeInd : Microsoft Individual Code Signing +!Cname ms-code-com +ms-corp 2 1 22 : msCodeCom : Microsoft Commercial Code Signing +!Cname ms-ctl-sign +ms-corp 10 3 1 : msCTLSign : Microsoft Trust List Signing +!Cname ms-sgc +ms-corp 10 3 3 : msSGC : Microsoft Server Gated Crypto +!Cname ms-efs +ms-corp 10 3 4 : msEFS : Microsoft Encrypted File System +!Cname ms-smartcard-login +ms-corp 20 2 2 : msSmartcardLogin : Microsoft Smartcard Login +!Cname ms-upn +ms-corp 20 2 3 : msUPN : Microsoft User Principal Name + +ms-corp 25 2 : ms-ntds-sec-ext : Microsoft NTDS CA Extension +ms-corp 25 2 1 : ms-ntds-obj-sid : Microsoft NTDS AD objectSid +ms-corp 21 7 : ms-cert-templ : Microsoft certificate template +ms-corp 21 10 : ms-app-policies : Microsoft Application Policies Extension + +1 3 6 1 4 1 188 7 1 1 2 : IDEA-CBC : idea-cbc + : IDEA-ECB : idea-ecb +!Cname idea-cfb64 + : IDEA-CFB : idea-cfb +!Cname idea-ofb64 + : IDEA-OFB : idea-ofb + +1 3 6 1 4 1 3029 1 2 : BF-CBC : bf-cbc + : BF-ECB : bf-ecb +!Cname bf-cfb64 + : BF-CFB : bf-cfb +!Cname bf-ofb64 + : BF-OFB : bf-ofb + +!Cname id-pkix +1 3 6 1 5 5 7 : PKIX + +# PKIX Arcs +id-pkix 0 : id-pkix-mod +id-pkix 1 : id-pe +id-pkix 2 : id-qt +id-pkix 3 : id-kp +id-pkix 4 : id-it +id-pkix 5 : id-pkip +id-pkix 6 : id-alg +id-pkix 7 : id-cmc +id-pkix 8 : id-on +id-pkix 9 : id-pda +id-pkix 10 : id-aca +id-pkix 11 : id-qcs +id-pkix 14 : id-cp +id-pkix 12 : id-cct +id-pkix 21 : id-ppl +id-pkix 48 : id-ad + +# PKIX Modules +id-pkix-mod 1 : id-pkix1-explicit-88 +id-pkix-mod 2 : id-pkix1-implicit-88 +id-pkix-mod 3 : id-pkix1-explicit-93 +id-pkix-mod 4 : id-pkix1-implicit-93 +id-pkix-mod 5 : id-mod-crmf +id-pkix-mod 6 : id-mod-cmc +id-pkix-mod 7 : id-mod-kea-profile-88 +id-pkix-mod 8 : id-mod-kea-profile-93 +id-pkix-mod 9 : id-mod-cmp +id-pkix-mod 10 : id-mod-qualified-cert-88 +id-pkix-mod 11 : id-mod-qualified-cert-93 +id-pkix-mod 12 : id-mod-attribute-cert +id-pkix-mod 13 : id-mod-timestamp-protocol +id-pkix-mod 14 : id-mod-ocsp +id-pkix-mod 15 : id-mod-dvcs +id-pkix-mod 16 : id-mod-cmp2000 +id-pkix-mod 50 : id-mod-cmp2000-02 +id-pkix-mod 99 : id-mod-cmp2021-88 +id-pkix-mod 100 : id-mod-cmp2021-02 + +# PKIX Private Extensions +!Cname info-access +id-pe 1 : authorityInfoAccess : Authority Information Access +id-pe 2 : biometricInfo : Biometric Info +id-pe 3 : qcStatements +id-pe 4 : ac-auditIdentity : X509v3 Audit Identity +!Alias ac-auditEntity ac-auditIdentity +id-pe 5 : ac-targeting +id-pe 6 : aaControls +id-pe 7 : sbgp-ipAddrBlock +id-pe 8 : sbgp-autonomousSysNum +id-pe 9 : sbgp-routerIdentifier +id-pe 10 : ac-proxying +!Cname sinfo-access +id-pe 11 : subjectInfoAccess : Subject Information Access +id-pe 14 : proxyCertInfo : Proxy Certificate Information +id-pe 24 : tlsfeature : TLS Feature +id-pe 28 : sbgp-ipAddrBlockv2 +id-pe 29 : sbgp-autonomousSysNumv2 + +# PKIX policyQualifiers for Internet policy qualifiers +id-qt 1 : id-qt-cps : Policy Qualifier CPS +id-qt 2 : id-qt-unotice : Policy Qualifier User Notice +id-qt 3 : textNotice + +# https://www.iana.org/assignments/smi-numbers/smi-numbers.xhtml#smi-numbers-1.3.6.1.5.5.7.3 +# PKIX key purpose identifiers +!Cname server-auth +id-kp 1 : serverAuth : TLS Web Server Authentication +!Cname client-auth +id-kp 2 : clientAuth : TLS Web Client Authentication +!Cname code-sign +id-kp 3 : codeSigning : Code Signing +!Cname email-protect +id-kp 4 : emailProtection : E-mail Protection +id-kp 5 : ipsecEndSystem : IPSec End System +id-kp 6 : ipsecTunnel : IPSec Tunnel +id-kp 7 : ipsecUser : IPSec User +!Cname time-stamp +id-kp 8 : timeStamping : Time Stamping +# From OCSP spec RFC2560 +!Cname OCSP-sign +id-kp 9 : OCSPSigning : OCSP Signing +id-kp 10 : DVCS : dvcs +!Cname ipsec-IKE +id-kp 17 : ipsecIKE : ipsec Internet Key Exchange +id-kp 18 : capwapAC : Ctrl/provision WAP Access +id-kp 19 : capwapWTP : Ctrl/Provision WAP Termination +!Cname sshClient +id-kp 21 : secureShellClient : SSH Client +!Cname sshServer +id-kp 22 : secureShellServer : SSH Server +id-kp 23 : sendRouter : Send Router +id-kp 24 : sendProxiedRouter : Send Proxied Router +id-kp 25 : sendOwner : Send Owner +id-kp 26 : sendProxiedOwner : Send Proxied Owner +id-kp 27 : cmcCA : CMC Certificate Authority +id-kp 28 : cmcRA : CMC Registration Authority +id-kp 29 : cmcArchive : CMC Archive Server +id-kp 30 : id-kp-bgpsec-router : BGPsec Router +id-kp 31 : id-kp-BrandIndicatorforMessageIdentification : Brand Indicator for Message Identification +id-kp 32 : cmKGA : Certificate Management Key Generation Authority + +# https://www.iana.org/assignments/smi-numbers/smi-numbers.xhtml#smi-numbers-1.3.6.1.5.5.7.4 +# CMP information types +id-it 1 : id-it-caProtEncCert +id-it 2 : id-it-signKeyPairTypes +id-it 3 : id-it-encKeyPairTypes +id-it 4 : id-it-preferredSymmAlg +id-it 5 : id-it-caKeyUpdateInfo +id-it 6 : id-it-currentCRL +id-it 7 : id-it-unsupportedOIDs +# [Reserved and Obsolete]: +id-it 8 : id-it-subscriptionRequest +# [Reserved and Obsolete]: +id-it 9 : id-it-subscriptionResponse +id-it 10 : id-it-keyPairParamReq +id-it 11 : id-it-keyPairParamRep +id-it 12 : id-it-revPassphrase +id-it 13 : id-it-implicitConfirm +id-it 14 : id-it-confirmWaitTime +id-it 15 : id-it-origPKIMessage +id-it 16 : id-it-suppLangTags +id-it 17 : id-it-caCerts +id-it 18 : id-it-rootCaKeyUpdate +id-it 19 : id-it-certReqTemplate +id-it 20 : id-it-rootCaCert +id-it 21 : id-it-certProfile +id-it 22 : id-it-crlStatusList +id-it 23 : id-it-crls + +# CRMF registration +id-pkip 1 : id-regCtrl +id-pkip 2 : id-regInfo + +# CRMF registration controls +id-regCtrl 1 : id-regCtrl-regToken +id-regCtrl 2 : id-regCtrl-authenticator +id-regCtrl 3 : id-regCtrl-pkiPublicationInfo +id-regCtrl 4 : id-regCtrl-pkiArchiveOptions +id-regCtrl 5 : id-regCtrl-oldCertID +id-regCtrl 6 : id-regCtrl-protocolEncrKey +id-regCtrl 7 : id-regCtrl-altCertTemplate +# id-regCtrl 8 : id-regCtrl-wtlsTemplate [Reserved and Obsolete] +# id-regCtrl 9 : id-regCtrl-regTokenUTF8 [Reserved and Obsolete] +# id-regCtrl 10 : id-regCtrl-authenticatorUTF8 [Reserved and Obsolete] +id-regCtrl 11 : id-regCtrl-algId +id-regCtrl 12 : id-regCtrl-rsaKeyLen + +# CRMF registration information +id-regInfo 1 : id-regInfo-utf8Pairs +id-regInfo 2 : id-regInfo-certReq + +# algorithms +id-alg 1 : id-alg-des40 +id-alg 2 : id-alg-noSignature +id-alg 3 : id-alg-dh-sig-hmac-sha1 +id-alg 4 : id-alg-dh-pop + +# CMC controls +id-cmc 1 : id-cmc-statusInfo +id-cmc 2 : id-cmc-identification +id-cmc 3 : id-cmc-identityProof +id-cmc 4 : id-cmc-dataReturn +id-cmc 5 : id-cmc-transactionId +id-cmc 6 : id-cmc-senderNonce +id-cmc 7 : id-cmc-recipientNonce +id-cmc 8 : id-cmc-addExtensions +id-cmc 9 : id-cmc-encryptedPOP +id-cmc 10 : id-cmc-decryptedPOP +id-cmc 11 : id-cmc-lraPOPWitness +id-cmc 15 : id-cmc-getCert +id-cmc 16 : id-cmc-getCRL +id-cmc 17 : id-cmc-revokeRequest +id-cmc 18 : id-cmc-regInfo +id-cmc 19 : id-cmc-responseInfo +id-cmc 21 : id-cmc-queryPending +id-cmc 22 : id-cmc-popLinkRandom +id-cmc 23 : id-cmc-popLinkWitness +id-cmc 24 : id-cmc-confirmCertAcceptance + +# other names +id-on 1 : id-on-personalData +id-on 3 : id-on-permanentIdentifier : Permanent Identifier +id-on 4 : id-on-hardwareModuleName : Hardware Module Name +id-on 5 : id-on-xmppAddr : XmppAddr +id-on 7 : id-on-dnsSRV : SRVName +id-on 8 : id-on-NAIRealm : NAIRealm +id-on 9 : id-on-SmtpUTF8Mailbox : Smtp UTF8 Mailbox + +# personal data attributes +id-pda 1 : id-pda-dateOfBirth +id-pda 2 : id-pda-placeOfBirth +id-pda 3 : id-pda-gender +id-pda 4 : id-pda-countryOfCitizenship +id-pda 5 : id-pda-countryOfResidence + +# attribute certificate attributes +id-aca 1 : id-aca-authenticationInfo +id-aca 2 : id-aca-accessIdentity +id-aca 3 : id-aca-chargingIdentity +id-aca 4 : id-aca-group +# attention : the following seems to be obsolete, replace by 'role' +id-aca 5 : id-aca-role +id-aca 6 : id-aca-encAttrs + +# qualified certificate statements +id-qcs 1 : id-qcs-pkixQCSyntax-v1 + +# PKIX Certificate Policies +id-cp 2 : ipAddr-asNumber +id-cp 3 : ipAddr-asNumberv2 + +# CMC content types +id-cct 1 : id-cct-crs +id-cct 2 : id-cct-PKIData +id-cct 3 : id-cct-PKIResponse + +# Predefined Proxy Certificate policy languages +id-ppl 0 : id-ppl-anyLanguage : Any language +id-ppl 1 : id-ppl-inheritAll : Inherit all +id-ppl 2 : id-ppl-independent : Independent + +# access descriptors for authority info access extension +!Cname ad-OCSP +id-ad 1 : OCSP : OCSP +!Cname ad-ca-issuers +id-ad 2 : caIssuers : CA Issuers +!Cname ad-timeStamping +id-ad 3 : ad_timestamping : AD Time Stamping +!Cname ad-dvcs +id-ad 4 : AD_DVCS : ad dvcs +id-ad 5 : caRepository : CA Repository +id-ad 10 : rpkiManifest : RPKI Manifest +id-ad 11 : signedObject : Signed Object +id-ad 13 : rpkiNotify : RPKI Notify + +!Alias id-pkix-OCSP ad-OCSP +!module id-pkix-OCSP +!Cname basic +id-pkix-OCSP 1 : basicOCSPResponse : Basic OCSP Response +id-pkix-OCSP 2 : Nonce : OCSP Nonce +id-pkix-OCSP 3 : CrlID : OCSP CRL ID +id-pkix-OCSP 4 : acceptableResponses : Acceptable OCSP Responses +id-pkix-OCSP 5 : noCheck : OCSP No Check +id-pkix-OCSP 6 : archiveCutoff : OCSP Archive Cutoff +id-pkix-OCSP 7 : serviceLocator : OCSP Service Locator +id-pkix-OCSP 8 : extendedStatus : Extended OCSP Status +id-pkix-OCSP 9 : valid +id-pkix-OCSP 10 : path +id-pkix-OCSP 11 : trustRoot : Trust Root +!global + +1 3 14 3 2 : algorithm : algorithm +algorithm 3 : RSA-NP-MD5 : md5WithRSA +algorithm 6 : DES-ECB : des-ecb +algorithm 7 : DES-CBC : des-cbc +!Cname des-ofb64 +algorithm 8 : DES-OFB : des-ofb +!Cname des-cfb64 +algorithm 9 : DES-CFB : des-cfb +algorithm 11 : rsaSignature +!Cname dsa-2 +algorithm 12 : DSA-old : dsaEncryption-old +algorithm 13 : DSA-SHA : dsaWithSHA +algorithm 15 : RSA-SHA : shaWithRSAEncryption +!Cname des-ede-ecb +algorithm 17 : DES-EDE : des-ede +!Cname des-ede3-ecb + : DES-EDE3 : des-ede3 + : DES-EDE-CBC : des-ede-cbc +!Cname des-ede-cfb64 + : DES-EDE-CFB : des-ede-cfb +!Cname des-ede3-cfb64 + : DES-EDE3-CFB : des-ede3-cfb +!Cname des-ede-ofb64 + : DES-EDE-OFB : des-ede-ofb +!Cname des-ede3-ofb64 + : DES-EDE3-OFB : des-ede3-ofb + : DESX-CBC : desx-cbc +algorithm 18 : SHA : sha +algorithm 26 : SHA1 : sha1 +!Cname dsaWithSHA1-2 +algorithm 27 : DSA-SHA1-old : dsaWithSHA1-old +algorithm 29 : RSA-SHA1-2 : sha1WithRSA + +1 3 36 3 2 1 : RIPEMD160 : ripemd160 +1 3 36 3 3 1 2 : RSA-RIPEMD160 : ripemd160WithRSA + +1 3 6 1 4 1 1722 12 2 1 : BLAKE2BMAC : blake2bmac +1 3 6 1 4 1 1722 12 2 2 : BLAKE2SMAC : blake2smac +blake2bmac 16 : BLAKE2b512 : blake2b512 +blake2smac 8 : BLAKE2s256 : blake2s256 + +!Cname sxnet +1 3 101 1 4 1 : SXNetID : Strong Extranet ID + +2 5 : X500 : directory services (X.500) + +X500 4 : X509 +X509 3 : CN : commonName +X509 4 : SN : surname +X509 5 : : serialNumber +X509 6 : C : countryName +X509 7 : L : localityName +X509 8 : ST : stateOrProvinceName +X509 9 : street : streetAddress +X509 10 : O : organizationName +X509 11 : OU : organizationalUnitName +X509 12 : title : title +X509 13 : : description +X509 14 : : searchGuide +X509 15 : : businessCategory +X509 16 : : postalAddress +X509 17 : : postalCode +X509 18 : : postOfficeBox +X509 19 : : physicalDeliveryOfficeName +X509 20 : : telephoneNumber +X509 21 : : telexNumber +X509 22 : : teletexTerminalIdentifier +X509 23 : : facsimileTelephoneNumber +X509 24 : : x121Address +X509 25 : : internationaliSDNNumber +X509 26 : : registeredAddress +X509 27 : : destinationIndicator +X509 28 : : preferredDeliveryMethod +X509 29 : : presentationAddress +X509 30 : : supportedApplicationContext +X509 31 : member : +X509 32 : owner : +X509 33 : : roleOccupant +X509 34 : seeAlso : +X509 35 : : userPassword +X509 36 : : userCertificate +X509 37 : : cACertificate +X509 38 : : authorityRevocationList +X509 39 : : certificateRevocationList +X509 40 : : crossCertificatePair +X509 41 : name : name +X509 42 : GN : givenName +X509 43 : initials : initials +X509 44 : : generationQualifier +X509 45 : : x500UniqueIdentifier +X509 46 : dnQualifier : dnQualifier +X509 47 : : enhancedSearchGuide +X509 48 : : protocolInformation +X509 49 : : distinguishedName +X509 50 : : uniqueMember +X509 51 : : houseIdentifier +X509 52 : : supportedAlgorithms +X509 53 : : deltaRevocationList +X509 54 : dmdName : +X509 65 : : pseudonym +X509 72 : role : role +X509 97 : : organizationIdentifier +X509 98 : c3 : countryCode3c +X509 99 : n3 : countryCode3n +X509 100 : : dnsName + + +X500 8 : X500algorithms : directory services - algorithms +X500algorithms 1 1 : RSA : rsa +X500algorithms 3 100 : RSA-MDC2 : mdc2WithRSA +X500algorithms 3 101 : MDC2 : mdc2 + +X500 29 : id-ce +!Cname subject-directory-attributes +id-ce 9 : subjectDirectoryAttributes : X509v3 Subject Directory Attributes +!Cname subject-key-identifier +id-ce 14 : subjectKeyIdentifier : X509v3 Subject Key Identifier +!Cname key-usage +id-ce 15 : keyUsage : X509v3 Key Usage +!Cname private-key-usage-period +id-ce 16 : privateKeyUsagePeriod : X509v3 Private Key Usage Period +!Cname subject-alt-name +id-ce 17 : subjectAltName : X509v3 Subject Alternative Name +!Cname issuer-alt-name +id-ce 18 : issuerAltName : X509v3 Issuer Alternative Name +!Cname basic-constraints +id-ce 19 : basicConstraints : X509v3 Basic Constraints +!Cname crl-number +id-ce 20 : crlNumber : X509v3 CRL Number +!Cname crl-reason +id-ce 21 : CRLReason : X509v3 CRL Reason Code +!Cname invalidity-date +id-ce 24 : invalidityDate : Invalidity Date +!Cname delta-crl +id-ce 27 : deltaCRL : X509v3 Delta CRL Indicator +!Cname issuing-distribution-point +id-ce 28 : issuingDistributionPoint : X509v3 Issuing Distribution Point +!Cname certificate-issuer +id-ce 29 : certificateIssuer : X509v3 Certificate Issuer +!Cname name-constraints +id-ce 30 : nameConstraints : X509v3 Name Constraints +!Cname crl-distribution-points +id-ce 31 : crlDistributionPoints : X509v3 CRL Distribution Points +!Cname certificate-policies +id-ce 32 : certificatePolicies : X509v3 Certificate Policies +!Cname any-policy +certificate-policies 0 : anyPolicy : X509v3 Any Policy +!Cname policy-mappings +id-ce 33 : policyMappings : X509v3 Policy Mappings +!Cname authority-key-identifier +id-ce 35 : authorityKeyIdentifier : X509v3 Authority Key Identifier +!Cname policy-constraints +id-ce 36 : policyConstraints : X509v3 Policy Constraints +!Cname ext-key-usage +id-ce 37 : extendedKeyUsage : X509v3 Extended Key Usage +!Cname authority-attribute-identifier +id-ce 38 : authorityAttributeIdentifier : X509v3 Authority Attribute Identifier +!Cname role-spec-cert-identifier +id-ce 39 : roleSpecCertIdentifier : X509v3 Role Specification Certificate Identifier +!Cname basic-att-constraints +id-ce 41 : basicAttConstraints : X509v3 Basic Attribute Certificate Constraints +!Cname delegated-name-constraints +id-ce 42 : delegatedNameConstraints : X509v3 Delegated Name Constraints +!Cname time-specification +id-ce 43 : timeSpecification : X509v3 Time Specification +!Cname freshest-crl +id-ce 46 : freshestCRL : X509v3 Freshest CRL +!Cname attribute-descriptor +id-ce 48 : attributeDescriptor : X509v3 Attribute Descriptor +!Cname user-notice +id-ce 49 : userNotice : X509v3 User Notice +!Cname soa-identifier +id-ce 50 : sOAIdentifier : X509v3 Source of Authority Identifier +!Cname acceptable-cert-policies +id-ce 52 : acceptableCertPolicies : X509v3 Acceptable Certification Policies +!Cname inhibit-any-policy +id-ce 54 : inhibitAnyPolicy : X509v3 Inhibit Any Policy +!Cname target-information +id-ce 55 : targetInformation : X509v3 AC Targeting +!Cname no-rev-avail +id-ce 56 : noRevAvail : X509v3 No Revocation Available +!Cname acceptable-privilege-policies +id-ce 57 : acceptablePrivPolicies : X509v3 Acceptable Privilege Policies +!Cname indirect-issuer +id-ce 61 : indirectIssuer : X509v3 Indirect Issuer +!Cname no-assertion +id-ce 62 : noAssertion : X509v3 No Assertion +!Cname id-aa-issuing-distribution-point +id-ce 63 : aAissuingDistributionPoint : X509v3 Attribute Authority Issuing Distribution Point +!Cname issued-on-behalf-of +id-ce 64 : issuedOnBehalfOf : X509v3 Issued On Behalf Of +!Cname single-use +id-ce 65 : singleUse : X509v3 Single Use +!Cname group-ac +id-ce 66 : groupAC : X509v3 Group Attribute Certificate +!Cname allowed-attribute-assignments +id-ce 67 : allowedAttributeAssignments : X509v3 Allowed Attribute Assignments +!Cname attribute-mappings +id-ce 68 : attributeMappings : X509v3 Attribute Mappings +!Cname holder-name-constraints +id-ce 69 : holderNameConstraints : X509v3 Holder Name Constraints +!Cname authorization-validation +id-ce 70 : authorizationValidation : X509v3 Authorization Validation +!Cname prot-restrict +id-ce 71 : protRestrict : X509v3 Protocol Restriction +!Cname subject-alt-public-key-info +id-ce 72 : subjectAltPublicKeyInfo : X509v3 Subject Alternative Public Key Info +!Cname alt-signature-algorithm +id-ce 73 : altSignatureAlgorithm : X509v3 Alternative Signature Algorithm +!Cname alt-signature-value +id-ce 74 : altSignatureValue : X509v3 Alternative Signature Value +!Cname associated-information +id-ce 75 : associatedInformation : X509v3 Associated Information + +# From RFC5280 +ext-key-usage 0 : anyExtendedKeyUsage : Any Extended Key Usage + + +!Cname netscape +2 16 840 1 113730 : Netscape : Netscape Communications Corp. +!Cname netscape-cert-extension +netscape 1 : nsCertExt : Netscape Certificate Extension +!Cname netscape-data-type +netscape 2 : nsDataType : Netscape Data Type +!Cname netscape-cert-type +netscape-cert-extension 1 : nsCertType : Netscape Cert Type +!Cname netscape-base-url +netscape-cert-extension 2 : nsBaseUrl : Netscape Base Url +!Cname netscape-revocation-url +netscape-cert-extension 3 : nsRevocationUrl : Netscape Revocation Url +!Cname netscape-ca-revocation-url +netscape-cert-extension 4 : nsCaRevocationUrl : Netscape CA Revocation Url +!Cname netscape-renewal-url +netscape-cert-extension 7 : nsRenewalUrl : Netscape Renewal Url +!Cname netscape-ca-policy-url +netscape-cert-extension 8 : nsCaPolicyUrl : Netscape CA Policy Url +!Cname netscape-ssl-server-name +netscape-cert-extension 12 : nsSslServerName : Netscape SSL Server Name +!Cname netscape-comment +netscape-cert-extension 13 : nsComment : Netscape Comment +!Cname netscape-cert-sequence +netscape-data-type 5 : nsCertSequence : Netscape Certificate Sequence +!Cname ns-sgc +netscape 4 1 : nsSGC : Netscape Server Gated Crypto + +# iso(1) +iso 3 : ORG : org +org 6 : DOD : dod +dod 1 : IANA : iana +!Alias internet iana + +internet 1 : directory : Directory +internet 2 : mgmt : Management +internet 3 : experimental : Experimental +internet 4 : private : Private +internet 5 : security : Security +internet 6 : snmpv2 : SNMPv2 +# Documents refer to "internet 7" as "mail". This however leads to ambiguities +# with RFC2798, Section 9.1.3, where "mail" is defined as the short name for +# rfc822Mailbox. The short name is therefore here left out for a reason. +# Subclasses of "mail", e.g. "MIME MHS" don't constitute a problem, as +# references are realized via long name "Mail" (with capital M). +internet 7 : : Mail + +Private 1 : enterprises : Enterprises + +# RFC 2247 +Enterprises 1466 344 : dcobject : dcObject + +# Wi-SUN Assigned Value Registry +Enterprises 45605 1 : id-kp-wisun-fan-device : Wi-SUN Alliance Field Area Network (FAN) + +# RFC 1495 +Mail 1 : mime-mhs : MIME MHS +mime-mhs 1 : mime-mhs-headings : mime-mhs-headings +mime-mhs 2 : mime-mhs-bodies : mime-mhs-bodies +mime-mhs-headings 1 : id-hex-partial-message : id-hex-partial-message +mime-mhs-headings 2 : id-hex-multipart-message : id-hex-multipart-message + +# RFC 3274 +!Cname zlib-compression +id-smime-alg 8 : ZLIB : zlib compression + +# AES aka Rijndael + +!Alias csor 2 16 840 1 101 3 +!Alias nistAlgorithms csor 4 +!Alias aes nistAlgorithms 1 + +aes 1 : AES-128-ECB : aes-128-ecb +aes 2 : AES-128-CBC : aes-128-cbc +!Cname aes-128-ofb128 +aes 3 : AES-128-OFB : aes-128-ofb +!Cname aes-128-cfb128 +aes 4 : AES-128-CFB : aes-128-cfb +aes 5 : id-aes128-wrap +aes 6 : id-aes128-GCM : aes-128-gcm +aes 7 : id-aes128-CCM : aes-128-ccm +aes 8 : id-aes128-wrap-pad + +aes 21 : AES-192-ECB : aes-192-ecb +aes 22 : AES-192-CBC : aes-192-cbc +!Cname aes-192-ofb128 +aes 23 : AES-192-OFB : aes-192-ofb +!Cname aes-192-cfb128 +aes 24 : AES-192-CFB : aes-192-cfb +aes 25 : id-aes192-wrap +aes 26 : id-aes192-GCM : aes-192-gcm +aes 27 : id-aes192-CCM : aes-192-ccm +aes 28 : id-aes192-wrap-pad + +aes 41 : AES-256-ECB : aes-256-ecb +aes 42 : AES-256-CBC : aes-256-cbc +!Cname aes-256-ofb128 +aes 43 : AES-256-OFB : aes-256-ofb +!Cname aes-256-cfb128 +aes 44 : AES-256-CFB : aes-256-cfb +aes 45 : id-aes256-wrap +aes 46 : id-aes256-GCM : aes-256-gcm +aes 47 : id-aes256-CCM : aes-256-ccm +aes 48 : id-aes256-wrap-pad + +ieee-siswg 0 1 1 : AES-128-XTS : aes-128-xts +ieee-siswg 0 1 2 : AES-256-XTS : aes-256-xts + +# There are no OIDs for these modes... + + : AES-128-CFB1 : aes-128-cfb1 + : AES-192-CFB1 : aes-192-cfb1 + : AES-256-CFB1 : aes-256-cfb1 + : AES-128-CFB8 : aes-128-cfb8 + : AES-192-CFB8 : aes-192-cfb8 + : AES-256-CFB8 : aes-256-cfb8 + : AES-128-CTR : aes-128-ctr + : AES-192-CTR : aes-192-ctr + : AES-256-CTR : aes-256-ctr + : AES-128-OCB : aes-128-ocb + : AES-192-OCB : aes-192-ocb + : AES-256-OCB : aes-256-ocb + : DES-CFB1 : des-cfb1 + : DES-CFB8 : des-cfb8 + : DES-EDE3-CFB1 : des-ede3-cfb1 + : DES-EDE3-CFB8 : des-ede3-cfb8 + +# OIDs for SHA224, SHA256, SHA385 and SHA512, according to x9.84 and +# http://csrc.nist.gov/groups/ST/crypto_apps_infra/csor/algorithms.html +# "Middle" names are specified to be id-sha256, id-sha384, etc., but +# we adhere to unprefixed capitals for backward compatibility... +!Alias nist_hashalgs nistAlgorithms 2 +nist_hashalgs 1 : SHA256 : sha256 +nist_hashalgs 2 : SHA384 : sha384 +nist_hashalgs 3 : SHA512 : sha512 +nist_hashalgs 4 : SHA224 : sha224 +nist_hashalgs 5 : SHA512-224 : sha512-224 +nist_hashalgs 6 : SHA512-256 : sha512-256 +nist_hashalgs 7 : SHA3-224 : sha3-224 +nist_hashalgs 8 : SHA3-256 : sha3-256 +nist_hashalgs 9 : SHA3-384 : sha3-384 +nist_hashalgs 10 : SHA3-512 : sha3-512 +nist_hashalgs 11 : SHAKE128 : shake128 +nist_hashalgs 12 : SHAKE256 : shake256 +nist_hashalgs 13 : id-hmacWithSHA3-224 : hmac-sha3-224 +nist_hashalgs 14 : id-hmacWithSHA3-256 : hmac-sha3-256 +nist_hashalgs 15 : id-hmacWithSHA3-384 : hmac-sha3-384 +nist_hashalgs 16 : id-hmacWithSHA3-512 : hmac-sha3-512 +# Below two are incomplete OIDs, to be uncommented when we figure out +# how to handle them... +# nist_hashalgs 17 : id-shake128-len : shake128-len +# nist_hashalgs 18 : id-shake256-len : shake256-len +nist_hashalgs 19 : KMAC128 : kmac128 +nist_hashalgs 20 : KMAC256 : kmac256 +# nist_hashalgs 21 : KMAC128-XOF : kmac128-xof +# nist_hashalgs 22 : KMAC256-XOF : kmac256-xof + +# OIDs for dsa-with-sha224 and dsa-with-sha256 +!Alias dsa_with_sha2 nistAlgorithms 3 +dsa_with_sha2 1 : dsa_with_SHA224 +dsa_with_sha2 2 : dsa_with_SHA256 +# Above two belong below, but kept as they are for backward compatibility +!Alias sigAlgs nistAlgorithms 3 +sigAlgs 3 : id-dsa-with-sha384 : dsa_with_SHA384 +sigAlgs 4 : id-dsa-with-sha512 : dsa_with_SHA512 +sigAlgs 5 : id-dsa-with-sha3-224 : dsa_with_SHA3-224 +sigAlgs 6 : id-dsa-with-sha3-256 : dsa_with_SHA3-256 +sigAlgs 7 : id-dsa-with-sha3-384 : dsa_with_SHA3-384 +sigAlgs 8 : id-dsa-with-sha3-512 : dsa_with_SHA3-512 +sigAlgs 9 : id-ecdsa-with-sha3-224 : ecdsa_with_SHA3-224 +sigAlgs 10 : id-ecdsa-with-sha3-256 : ecdsa_with_SHA3-256 +sigAlgs 11 : id-ecdsa-with-sha3-384 : ecdsa_with_SHA3-384 +sigAlgs 12 : id-ecdsa-with-sha3-512 : ecdsa_with_SHA3-512 +sigAlgs 13 : id-rsassa-pkcs1-v1_5-with-sha3-224 : RSA-SHA3-224 +sigAlgs 14 : id-rsassa-pkcs1-v1_5-with-sha3-256 : RSA-SHA3-256 +sigAlgs 15 : id-rsassa-pkcs1-v1_5-with-sha3-384 : RSA-SHA3-384 +sigAlgs 16 : id-rsassa-pkcs1-v1_5-with-sha3-512 : RSA-SHA3-512 +sigAlgs 17 : id-ml-dsa-44 : ML-DSA-44 +sigAlgs 18 : id-ml-dsa-65 : ML-DSA-65 +sigAlgs 19 : id-ml-dsa-87 : ML-DSA-87 +sigAlgs 20 : id-slh-dsa-sha2-128s : SLH-DSA-SHA2-128s +sigAlgs 21 : id-slh-dsa-sha2-128f : SLH-DSA-SHA2-128f +sigAlgs 22 : id-slh-dsa-sha2-192s : SLH-DSA-SHA2-192s +sigAlgs 23 : id-slh-dsa-sha2-192f : SLH-DSA-SHA2-192f +sigAlgs 24 : id-slh-dsa-sha2-256s : SLH-DSA-SHA2-256s +sigAlgs 25 : id-slh-dsa-sha2-256f : SLH-DSA-SHA2-256f +sigAlgs 26 : id-slh-dsa-shake-128s : SLH-DSA-SHAKE-128s +sigAlgs 27 : id-slh-dsa-shake-128f : SLH-DSA-SHAKE-128f +sigAlgs 28 : id-slh-dsa-shake-192s : SLH-DSA-SHAKE-192s +sigAlgs 29 : id-slh-dsa-shake-192f : SLH-DSA-SHAKE-192f +sigAlgs 30 : id-slh-dsa-shake-256s : SLH-DSA-SHAKE-256s +sigAlgs 31 : id-slh-dsa-shake-256f : SLH-DSA-SHAKE-256f +sigAlgs 32 : id-hash-ml-dsa-44-with-sha512 : HASH-ML-DSA-44-WITH-SHA512 +sigAlgs 33 : id-hash-ml-dsa-65-with-sha512 : HASH-ML-DSA-65-WITH-SHA512 +sigAlgs 34 : id-hash-ml-dsa-87-with-sha512 : HASH-ML-DSA-87-WITH-SHA512 +sigAlgs 35 : id-hash-slh-dsa-sha2-128s-with-sha256 : SLH-DSA-SHA2-128s-WITH-SHA256 +sigAlgs 36 : id-hash-slh-dsa-sha2-128f-with-sha256 : SLH-DSA-SHA2-128f-WITH-SHA256 +sigAlgs 37 : id-hash-slh-dsa-sha2-192s-with-sha512 : SLH-DSA-SHA2-192s-WITH-SHA512 +sigAlgs 38 : id-hash-slh-dsa-sha2-192f-with-sha512 : SLH-DSA-SHA2-192f-WITH-SHA512 +sigAlgs 39 : id-hash-slh-dsa-sha2-256s-with-sha512 : SLH-DSA-SHA2-256s-WITH-SHA512 +sigAlgs 40 : id-hash-slh-dsa-sha2-256f-with-sha512 : SLH-DSA-SHA2-256f-WITH-SHA512 +sigAlgs 41 : id-hash-slh-dsa-shake-128s-with-shake128 : SLH-DSA-SHAKE-128s-WITH-SHAKE128 +sigAlgs 42 : id-hash-slh-dsa-shake-128f-with-shake128 : SLH-DSA-SHAKE-128f-WITH-SHAKE128 +sigAlgs 43 : id-hash-slh-dsa-shake-192s-with-shake256 : SLH-DSA-SHAKE-192s-WITH-SHAKE256 +sigAlgs 44 : id-hash-slh-dsa-shake-192f-with-shake256 : SLH-DSA-SHAKE-192f-WITH-SHAKE256 +sigAlgs 45 : id-hash-slh-dsa-shake-256s-with-shake256 : SLH-DSA-SHAKE-256s-WITH-SHAKE256 +sigAlgs 46 : id-hash-slh-dsa-shake-256f-with-shake256 : SLH-DSA-SHAKE-256f-WITH-SHAKE256 + +# Hold instruction CRL entry extension +!Cname hold-instruction-code +id-ce 23 : holdInstructionCode : Hold Instruction Code +!Alias holdInstruction X9-57 2 +!Cname hold-instruction-none +holdInstruction 1 : holdInstructionNone : Hold Instruction None +!Cname hold-instruction-call-issuer +holdInstruction 2 : holdInstructionCallIssuer : Hold Instruction Call Issuer +!Cname hold-instruction-reject +holdInstruction 3 : holdInstructionReject : Hold Instruction Reject + +# OID's from ITU-T. Most of this is defined in RFC 1274. A couple of +# them are also mentioned in RFC 2247 +# OIDs specific to Electronic Signature Standard/CAdES are as specified in +# ETSI EN 319 122-1 V1.2.1 (2021-10): +# Electronic Signatures and Infrastructures (ESI); CAdES digital signatures; +# Part 1: Building blocks and CAdES baseline signatures +itu-t 4 : itu-t-identified-organization +itu-t-identified-organization 0: etsi +etsi 1733 : electronic-signature-standard +electronic-signature-standard 2: ess-attributes +ess-attributes 1 : id-aa-ets-mimeType +ess-attributes 2 : id-aa-ets-longTermValidation +ess-attributes 3 : id-aa-ets-SignaturePolicyDocument +ess-attributes 4 : id-aa-ets-archiveTimestampV3 +ess-attributes 5 : id-aa-ATSHashIndex +etsi 19122 : cades +cades 1 : cades-attributes +cades-attributes 1 : id-aa-ets-signerAttrV2 +cades-attributes 3 : id-aa-ets-sigPolicyStore +cades-attributes 4 : id-aa-ATSHashIndex-v2 +cades-attributes 5 : id-aa-ATSHashIndex-v3 +cades-attributes 6 : signedAssertion + +itu-t 9 : data +data 2342 : pss +pss 19200300 : ucl +ucl 100 : pilot +pilot 1 : : pilotAttributeType +pilot 3 : : pilotAttributeSyntax +pilot 4 : : pilotObjectClass +pilot 10 : : pilotGroups +pilotAttributeSyntax 4 : : iA5StringSyntax +pilotAttributeSyntax 5 : : caseIgnoreIA5StringSyntax +pilotObjectClass 3 : : pilotObject +pilotObjectClass 4 : : pilotPerson +pilotObjectClass 5 : account +pilotObjectClass 6 : document +pilotObjectClass 7 : room +pilotObjectClass 9 : : documentSeries +pilotObjectClass 13 : domain : Domain +pilotObjectClass 14 : : rFC822localPart +pilotObjectClass 15 : : dNSDomain +pilotObjectClass 17 : : domainRelatedObject +pilotObjectClass 18 : : friendlyCountry +pilotObjectClass 19 : : simpleSecurityObject +pilotObjectClass 20 : : pilotOrganization +pilotObjectClass 21 : : pilotDSA +pilotObjectClass 22 : : qualityLabelledData +pilotAttributeType 1 : UID : userId +pilotAttributeType 2 : : textEncodedORAddress +pilotAttributeType 3 : mail : rfc822Mailbox +pilotAttributeType 4 : info +pilotAttributeType 5 : : favouriteDrink +pilotAttributeType 6 : : roomNumber +pilotAttributeType 7 : photo +pilotAttributeType 8 : : userClass +pilotAttributeType 9 : host +pilotAttributeType 10 : manager +pilotAttributeType 11 : : documentIdentifier +pilotAttributeType 12 : : documentTitle +pilotAttributeType 13 : : documentVersion +pilotAttributeType 14 : : documentAuthor +pilotAttributeType 15 : : documentLocation +pilotAttributeType 20 : : homeTelephoneNumber +pilotAttributeType 21 : secretary +pilotAttributeType 22 : : otherMailbox +pilotAttributeType 23 : : lastModifiedTime +pilotAttributeType 24 : : lastModifiedBy +pilotAttributeType 25 : DC : domainComponent +pilotAttributeType 26 : : aRecord +pilotAttributeType 27 : : pilotAttributeType27 +pilotAttributeType 28 : : mXRecord +pilotAttributeType 29 : : nSRecord +pilotAttributeType 30 : : sOARecord +pilotAttributeType 31 : : cNAMERecord +pilotAttributeType 37 : : associatedDomain +pilotAttributeType 38 : : associatedName +pilotAttributeType 39 : : homePostalAddress +pilotAttributeType 40 : : personalTitle +pilotAttributeType 41 : : mobileTelephoneNumber +pilotAttributeType 42 : : pagerTelephoneNumber +pilotAttributeType 43 : : friendlyCountryName +pilotAttributeType 44 : uid : uniqueIdentifier +pilotAttributeType 45 : : organizationalStatus +pilotAttributeType 46 : : janetMailbox +pilotAttributeType 47 : : mailPreferenceOption +pilotAttributeType 48 : : buildingName +pilotAttributeType 49 : : dSAQuality +pilotAttributeType 50 : : singleLevelQuality +pilotAttributeType 51 : : subtreeMinimumQuality +pilotAttributeType 52 : : subtreeMaximumQuality +pilotAttributeType 53 : : personalSignature +pilotAttributeType 54 : : dITRedirect +pilotAttributeType 55 : audio +pilotAttributeType 56 : : documentPublisher + +international-organizations 42 : id-set : Secure Electronic Transactions + +id-set 0 : set-ctype : content types +id-set 1 : set-msgExt : message extensions +id-set 3 : set-attr +id-set 5 : set-policy +id-set 7 : set-certExt : certificate extensions +id-set 8 : set-brand + +set-ctype 0 : setct-PANData +set-ctype 1 : setct-PANToken +set-ctype 2 : setct-PANOnly +set-ctype 3 : setct-OIData +set-ctype 4 : setct-PI +set-ctype 5 : setct-PIData +set-ctype 6 : setct-PIDataUnsigned +set-ctype 7 : setct-HODInput +set-ctype 8 : setct-AuthResBaggage +set-ctype 9 : setct-AuthRevReqBaggage +set-ctype 10 : setct-AuthRevResBaggage +set-ctype 11 : setct-CapTokenSeq +set-ctype 12 : setct-PInitResData +set-ctype 13 : setct-PI-TBS +set-ctype 14 : setct-PResData +set-ctype 16 : setct-AuthReqTBS +set-ctype 17 : setct-AuthResTBS +set-ctype 18 : setct-AuthResTBSX +set-ctype 19 : setct-AuthTokenTBS +set-ctype 20 : setct-CapTokenData +set-ctype 21 : setct-CapTokenTBS +set-ctype 22 : setct-AcqCardCodeMsg +set-ctype 23 : setct-AuthRevReqTBS +set-ctype 24 : setct-AuthRevResData +set-ctype 25 : setct-AuthRevResTBS +set-ctype 26 : setct-CapReqTBS +set-ctype 27 : setct-CapReqTBSX +set-ctype 28 : setct-CapResData +set-ctype 29 : setct-CapRevReqTBS +set-ctype 30 : setct-CapRevReqTBSX +set-ctype 31 : setct-CapRevResData +set-ctype 32 : setct-CredReqTBS +set-ctype 33 : setct-CredReqTBSX +set-ctype 34 : setct-CredResData +set-ctype 35 : setct-CredRevReqTBS +set-ctype 36 : setct-CredRevReqTBSX +set-ctype 37 : setct-CredRevResData +set-ctype 38 : setct-PCertReqData +set-ctype 39 : setct-PCertResTBS +set-ctype 40 : setct-BatchAdminReqData +set-ctype 41 : setct-BatchAdminResData +set-ctype 42 : setct-CardCInitResTBS +set-ctype 43 : setct-MeAqCInitResTBS +set-ctype 44 : setct-RegFormResTBS +set-ctype 45 : setct-CertReqData +set-ctype 46 : setct-CertReqTBS +set-ctype 47 : setct-CertResData +set-ctype 48 : setct-CertInqReqTBS +set-ctype 49 : setct-ErrorTBS +set-ctype 50 : setct-PIDualSignedTBE +set-ctype 51 : setct-PIUnsignedTBE +set-ctype 52 : setct-AuthReqTBE +set-ctype 53 : setct-AuthResTBE +set-ctype 54 : setct-AuthResTBEX +set-ctype 55 : setct-AuthTokenTBE +set-ctype 56 : setct-CapTokenTBE +set-ctype 57 : setct-CapTokenTBEX +set-ctype 58 : setct-AcqCardCodeMsgTBE +set-ctype 59 : setct-AuthRevReqTBE +set-ctype 60 : setct-AuthRevResTBE +set-ctype 61 : setct-AuthRevResTBEB +set-ctype 62 : setct-CapReqTBE +set-ctype 63 : setct-CapReqTBEX +set-ctype 64 : setct-CapResTBE +set-ctype 65 : setct-CapRevReqTBE +set-ctype 66 : setct-CapRevReqTBEX +set-ctype 67 : setct-CapRevResTBE +set-ctype 68 : setct-CredReqTBE +set-ctype 69 : setct-CredReqTBEX +set-ctype 70 : setct-CredResTBE +set-ctype 71 : setct-CredRevReqTBE +set-ctype 72 : setct-CredRevReqTBEX +set-ctype 73 : setct-CredRevResTBE +set-ctype 74 : setct-BatchAdminReqTBE +set-ctype 75 : setct-BatchAdminResTBE +set-ctype 76 : setct-RegFormReqTBE +set-ctype 77 : setct-CertReqTBE +set-ctype 78 : setct-CertReqTBEX +set-ctype 79 : setct-CertResTBE +set-ctype 80 : setct-CRLNotificationTBS +set-ctype 81 : setct-CRLNotificationResTBS +set-ctype 82 : setct-BCIDistributionTBS + +set-msgExt 1 : setext-genCrypt : generic cryptogram +set-msgExt 3 : setext-miAuth : merchant initiated auth +set-msgExt 4 : setext-pinSecure +set-msgExt 5 : setext-pinAny +set-msgExt 7 : setext-track2 +set-msgExt 8 : setext-cv : additional verification + +set-policy 0 : set-policy-root + +set-certExt 0 : setCext-hashedRoot +set-certExt 1 : setCext-certType +set-certExt 2 : setCext-merchData +set-certExt 3 : setCext-cCertRequired +set-certExt 4 : setCext-tunneling +set-certExt 5 : setCext-setExt +set-certExt 6 : setCext-setQualf +set-certExt 7 : setCext-PGWYcapabilities +set-certExt 8 : setCext-TokenIdentifier +set-certExt 9 : setCext-Track2Data +set-certExt 10 : setCext-TokenType +set-certExt 11 : setCext-IssuerCapabilities + +set-attr 0 : setAttr-Cert +set-attr 1 : setAttr-PGWYcap : payment gateway capabilities +set-attr 2 : setAttr-TokenType +set-attr 3 : setAttr-IssCap : issuer capabilities + +setAttr-Cert 0 : set-rootKeyThumb +setAttr-Cert 1 : set-addPolicy + +setAttr-TokenType 1 : setAttr-Token-EMV +setAttr-TokenType 2 : setAttr-Token-B0Prime + +setAttr-IssCap 3 : setAttr-IssCap-CVM +setAttr-IssCap 4 : setAttr-IssCap-T2 +setAttr-IssCap 5 : setAttr-IssCap-Sig + +setAttr-IssCap-CVM 1 : setAttr-GenCryptgrm : generate cryptogram +setAttr-IssCap-T2 1 : setAttr-T2Enc : encrypted track 2 +setAttr-IssCap-T2 2 : setAttr-T2cleartxt : cleartext track 2 + +setAttr-IssCap-Sig 1 : setAttr-TokICCsig : ICC or token signature +setAttr-IssCap-Sig 2 : setAttr-SecDevSig : secure device signature + +set-brand 1 : set-brand-IATA-ATA +set-brand 30 : set-brand-Diners +set-brand 34 : set-brand-AmericanExpress +set-brand 35 : set-brand-JCB +set-brand 4 : set-brand-Visa +set-brand 5 : set-brand-MasterCard +set-brand 6011 : set-brand-Novus + +rsadsi 3 10 : DES-CDMF : des-cdmf +rsadsi 1 1 6 : rsaOAEPEncryptionSET + + : Oakley-EC2N-3 : ipsec3 + : Oakley-EC2N-4 : ipsec4 + +iso 0 10118 3 0 55 : whirlpool + +# GOST OIDs + +member-body 643 2 2 : cryptopro +member-body 643 2 9 : cryptocom +member-body 643 7 1 : id-tc26 + +cryptopro 3 : id-GostR3411-94-with-GostR3410-2001 : GOST R 34.11-94 with GOST R 34.10-2001 +cryptopro 4 : id-GostR3411-94-with-GostR3410-94 : GOST R 34.11-94 with GOST R 34.10-94 +!Cname id-GostR3411-94 +cryptopro 9 : md_gost94 : GOST R 34.11-94 +cryptopro 10 : id-HMACGostR3411-94 : HMAC GOST 34.11-94 +!Cname id-GostR3410-2001 +cryptopro 19 : gost2001 : GOST R 34.10-2001 +!Cname id-GostR3410-94 +cryptopro 20 : gost94 : GOST R 34.10-94 +!Cname id-Gost28147-89 +cryptopro 21 : gost89 : GOST 28147-89 + : gost89-cnt + : gost89-cnt-12 + : gost89-cbc + : gost89-ecb + : gost89-ctr +!Cname id-Gost28147-89-MAC +cryptopro 22 : gost-mac : GOST 28147-89 MAC + : gost-mac-12 +!Cname id-GostR3411-94-prf +cryptopro 23 : prf-gostr3411-94 : GOST R 34.11-94 PRF +cryptopro 98 : id-GostR3410-2001DH : GOST R 34.10-2001 DH +cryptopro 99 : id-GostR3410-94DH : GOST R 34.10-94 DH + +cryptopro 14 1 : id-Gost28147-89-CryptoPro-KeyMeshing +cryptopro 14 0 : id-Gost28147-89-None-KeyMeshing + +# GOST parameter set OIDs + +cryptopro 30 0 : id-GostR3411-94-TestParamSet +cryptopro 30 1 : id-GostR3411-94-CryptoProParamSet + +cryptopro 31 0 : id-Gost28147-89-TestParamSet +cryptopro 31 1 : id-Gost28147-89-CryptoPro-A-ParamSet +cryptopro 31 2 : id-Gost28147-89-CryptoPro-B-ParamSet +cryptopro 31 3 : id-Gost28147-89-CryptoPro-C-ParamSet +cryptopro 31 4 : id-Gost28147-89-CryptoPro-D-ParamSet +cryptopro 31 5 : id-Gost28147-89-CryptoPro-Oscar-1-1-ParamSet +cryptopro 31 6 : id-Gost28147-89-CryptoPro-Oscar-1-0-ParamSet +cryptopro 31 7 : id-Gost28147-89-CryptoPro-RIC-1-ParamSet + +cryptopro 32 0 : id-GostR3410-94-TestParamSet +cryptopro 32 2 : id-GostR3410-94-CryptoPro-A-ParamSet +cryptopro 32 3 : id-GostR3410-94-CryptoPro-B-ParamSet +cryptopro 32 4 : id-GostR3410-94-CryptoPro-C-ParamSet +cryptopro 32 5 : id-GostR3410-94-CryptoPro-D-ParamSet + +cryptopro 33 1 : id-GostR3410-94-CryptoPro-XchA-ParamSet +cryptopro 33 2 : id-GostR3410-94-CryptoPro-XchB-ParamSet +cryptopro 33 3 : id-GostR3410-94-CryptoPro-XchC-ParamSet + +cryptopro 35 0 : id-GostR3410-2001-TestParamSet +cryptopro 35 1 : id-GostR3410-2001-CryptoPro-A-ParamSet +cryptopro 35 2 : id-GostR3410-2001-CryptoPro-B-ParamSet +cryptopro 35 3 : id-GostR3410-2001-CryptoPro-C-ParamSet + +cryptopro 36 0 : id-GostR3410-2001-CryptoPro-XchA-ParamSet +cryptopro 36 1 : id-GostR3410-2001-CryptoPro-XchB-ParamSet + +id-GostR3410-94 1 : id-GostR3410-94-a +id-GostR3410-94 2 : id-GostR3410-94-aBis +id-GostR3410-94 3 : id-GostR3410-94-b +id-GostR3410-94 4 : id-GostR3410-94-bBis + +# Cryptocom LTD GOST OIDs + +cryptocom 1 6 1 : id-Gost28147-89-cc : GOST 28147-89 Cryptocom ParamSet +!Cname id-GostR3410-94-cc +cryptocom 1 5 3 : gost94cc : GOST 34.10-94 Cryptocom +!Cname id-GostR3410-2001-cc +cryptocom 1 5 4 : gost2001cc : GOST 34.10-2001 Cryptocom + +cryptocom 1 3 3 : id-GostR3411-94-with-GostR3410-94-cc : GOST R 34.11-94 with GOST R 34.10-94 Cryptocom +cryptocom 1 3 4 : id-GostR3411-94-with-GostR3410-2001-cc : GOST R 34.11-94 with GOST R 34.10-2001 Cryptocom + +cryptocom 1 8 1 : id-GostR3410-2001-ParamSet-cc : GOST R 3410-2001 Parameter Set Cryptocom + +# TC26 GOST OIDs + +id-tc26 1 : id-tc26-algorithms +id-tc26-algorithms 1 : id-tc26-sign +!Cname id-GostR3410-2012-256 +id-tc26-sign 1 : gost2012_256: GOST R 34.10-2012 with 256 bit modulus +!Cname id-GostR3410-2012-512 +id-tc26-sign 2 : gost2012_512: GOST R 34.10-2012 with 512 bit modulus + +id-tc26-algorithms 2 : id-tc26-digest +!Cname id-GostR3411-2012-256 +id-tc26-digest 2 : md_gost12_256: GOST R 34.11-2012 with 256 bit hash +!Cname id-GostR3411-2012-512 +id-tc26-digest 3 : md_gost12_512: GOST R 34.11-2012 with 512 bit hash + +id-tc26-algorithms 3 : id-tc26-signwithdigest +id-tc26-signwithdigest 2: id-tc26-signwithdigest-gost3410-2012-256: GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit) +id-tc26-signwithdigest 3: id-tc26-signwithdigest-gost3410-2012-512: GOST R 34.10-2012 with GOST R 34.11-2012 (512 bit) + +id-tc26-algorithms 4 : id-tc26-mac +id-tc26-mac 1 : id-tc26-hmac-gost-3411-2012-256 : HMAC GOST 34.11-2012 256 bit +id-tc26-mac 2 : id-tc26-hmac-gost-3411-2012-512 : HMAC GOST 34.11-2012 512 bit + +id-tc26-algorithms 5 : id-tc26-cipher +id-tc26-cipher 1 : id-tc26-cipher-gostr3412-2015-magma +id-tc26-cipher-gostr3412-2015-magma 1 : magma-ctr-acpkm +id-tc26-cipher-gostr3412-2015-magma 2 : magma-ctr-acpkm-omac +id-tc26-cipher 2 : id-tc26-cipher-gostr3412-2015-kuznyechik +id-tc26-cipher-gostr3412-2015-kuznyechik 1 : kuznyechik-ctr-acpkm +id-tc26-cipher-gostr3412-2015-kuznyechik 2 : kuznyechik-ctr-acpkm-omac + +id-tc26-algorithms 6 : id-tc26-agreement +id-tc26-agreement 1 : id-tc26-agreement-gost-3410-2012-256 +id-tc26-agreement 2 : id-tc26-agreement-gost-3410-2012-512 + +id-tc26-algorithms 7 : id-tc26-wrap +id-tc26-wrap 1 : id-tc26-wrap-gostr3412-2015-magma +id-tc26-wrap-gostr3412-2015-magma 1 : magma-kexp15 +id-tc26-wrap 2 : id-tc26-wrap-gostr3412-2015-kuznyechik +id-tc26-wrap-gostr3412-2015-kuznyechik 1 : kuznyechik-kexp15 + +id-tc26 2 : id-tc26-constants + +id-tc26-constants 1 : id-tc26-sign-constants +id-tc26-sign-constants 1: id-tc26-gost-3410-2012-256-constants +id-tc26-gost-3410-2012-256-constants 1 : id-tc26-gost-3410-2012-256-paramSetA: GOST R 34.10-2012 (256 bit) ParamSet A +id-tc26-gost-3410-2012-256-constants 2 : id-tc26-gost-3410-2012-256-paramSetB: GOST R 34.10-2012 (256 bit) ParamSet B +id-tc26-gost-3410-2012-256-constants 3 : id-tc26-gost-3410-2012-256-paramSetC: GOST R 34.10-2012 (256 bit) ParamSet C +id-tc26-gost-3410-2012-256-constants 4 : id-tc26-gost-3410-2012-256-paramSetD: GOST R 34.10-2012 (256 bit) ParamSet D +id-tc26-sign-constants 2: id-tc26-gost-3410-2012-512-constants +id-tc26-gost-3410-2012-512-constants 0 : id-tc26-gost-3410-2012-512-paramSetTest: GOST R 34.10-2012 (512 bit) testing parameter set +id-tc26-gost-3410-2012-512-constants 1 : id-tc26-gost-3410-2012-512-paramSetA: GOST R 34.10-2012 (512 bit) ParamSet A +id-tc26-gost-3410-2012-512-constants 2 : id-tc26-gost-3410-2012-512-paramSetB: GOST R 34.10-2012 (512 bit) ParamSet B +id-tc26-gost-3410-2012-512-constants 3 : id-tc26-gost-3410-2012-512-paramSetC: GOST R 34.10-2012 (512 bit) ParamSet C + +id-tc26-constants 2 : id-tc26-digest-constants +id-tc26-constants 5 : id-tc26-cipher-constants +id-tc26-cipher-constants 1 : id-tc26-gost-28147-constants +id-tc26-gost-28147-constants 1 : id-tc26-gost-28147-param-Z : GOST 28147-89 TC26 parameter set + +member-body 643 3 131 1 1 : INN : INN +member-body 643 100 1 : OGRN : OGRN +member-body 643 100 3 : SNILS : SNILS +member-body 643 100 5 : OGRNIP : OGRNIP +member-body 643 100 111 : subjectSignTool : Signing Tool of Subject +member-body 643 100 112 : issuerSignTool : Signing Tool of Issuer +member-body 643 100 113 : classSignTool : Class of Signing Tool +member-body 643 100 113 1 : classSignToolKC1 : Class of Signing Tool KC1 +member-body 643 100 113 2 : classSignToolKC2 : Class of Signing Tool KC2 +member-body 643 100 113 3 : classSignToolKC3 : Class of Signing Tool KC3 +member-body 643 100 113 4 : classSignToolKB1 : Class of Signing Tool KB1 +member-body 643 100 113 5 : classSignToolKB2 : Class of Signing Tool KB2 +member-body 643 100 113 6 : classSignToolKA1 : Class of Signing Tool KA1 + +#GOST R34.13-2015 Grasshopper "Kuznechik" + : kuznyechik-ecb + : kuznyechik-ctr + : kuznyechik-ofb + : kuznyechik-cbc + : kuznyechik-cfb + : kuznyechik-mac + +#GOST R34.13-2015 Magma + : magma-ecb + : magma-ctr + : magma-ofb + : magma-cbc + : magma-cfb + : magma-mac + +# Definitions for Camellia cipher - CBC MODE + +1 2 392 200011 61 1 1 1 2 : CAMELLIA-128-CBC : camellia-128-cbc +1 2 392 200011 61 1 1 1 3 : CAMELLIA-192-CBC : camellia-192-cbc +1 2 392 200011 61 1 1 1 4 : CAMELLIA-256-CBC : camellia-256-cbc +1 2 392 200011 61 1 1 3 2 : id-camellia128-wrap +1 2 392 200011 61 1 1 3 3 : id-camellia192-wrap +1 2 392 200011 61 1 1 3 4 : id-camellia256-wrap + +# Definitions for Camellia cipher - ECB, CFB, OFB MODE + +!Alias ntt-ds 0 3 4401 5 +!Alias camellia ntt-ds 3 1 9 + +camellia 1 : CAMELLIA-128-ECB : camellia-128-ecb +!Cname camellia-128-ofb128 +camellia 3 : CAMELLIA-128-OFB : camellia-128-ofb +!Cname camellia-128-cfb128 +camellia 4 : CAMELLIA-128-CFB : camellia-128-cfb +camellia 6 : CAMELLIA-128-GCM : camellia-128-gcm +camellia 7 : CAMELLIA-128-CCM : camellia-128-ccm +camellia 9 : CAMELLIA-128-CTR : camellia-128-ctr +camellia 10 : CAMELLIA-128-CMAC : camellia-128-cmac + +camellia 21 : CAMELLIA-192-ECB : camellia-192-ecb +!Cname camellia-192-ofb128 +camellia 23 : CAMELLIA-192-OFB : camellia-192-ofb +!Cname camellia-192-cfb128 +camellia 24 : CAMELLIA-192-CFB : camellia-192-cfb +camellia 26 : CAMELLIA-192-GCM : camellia-192-gcm +camellia 27 : CAMELLIA-192-CCM : camellia-192-ccm +camellia 29 : CAMELLIA-192-CTR : camellia-192-ctr +camellia 30 : CAMELLIA-192-CMAC : camellia-192-cmac + +camellia 41 : CAMELLIA-256-ECB : camellia-256-ecb +!Cname camellia-256-ofb128 +camellia 43 : CAMELLIA-256-OFB : camellia-256-ofb +!Cname camellia-256-cfb128 +camellia 44 : CAMELLIA-256-CFB : camellia-256-cfb +camellia 46 : CAMELLIA-256-GCM : camellia-256-gcm +camellia 47 : CAMELLIA-256-CCM : camellia-256-ccm +camellia 49 : CAMELLIA-256-CTR : camellia-256-ctr +camellia 50 : CAMELLIA-256-CMAC : camellia-256-cmac + +# There are no OIDs for these modes... + + : CAMELLIA-128-CFB1 : camellia-128-cfb1 + : CAMELLIA-192-CFB1 : camellia-192-cfb1 + : CAMELLIA-256-CFB1 : camellia-256-cfb1 + : CAMELLIA-128-CFB8 : camellia-128-cfb8 + : CAMELLIA-192-CFB8 : camellia-192-cfb8 + : CAMELLIA-256-CFB8 : camellia-256-cfb8 + +# Definitions for ARIA cipher + +!Alias aria 1 2 410 200046 1 1 +aria 1 : ARIA-128-ECB : aria-128-ecb +aria 2 : ARIA-128-CBC : aria-128-cbc +!Cname aria-128-cfb128 +aria 3 : ARIA-128-CFB : aria-128-cfb +!Cname aria-128-ofb128 +aria 4 : ARIA-128-OFB : aria-128-ofb +aria 5 : ARIA-128-CTR : aria-128-ctr + +aria 6 : ARIA-192-ECB : aria-192-ecb +aria 7 : ARIA-192-CBC : aria-192-cbc +!Cname aria-192-cfb128 +aria 8 : ARIA-192-CFB : aria-192-cfb +!Cname aria-192-ofb128 +aria 9 : ARIA-192-OFB : aria-192-ofb +aria 10 : ARIA-192-CTR : aria-192-ctr + +aria 11 : ARIA-256-ECB : aria-256-ecb +aria 12 : ARIA-256-CBC : aria-256-cbc +!Cname aria-256-cfb128 +aria 13 : ARIA-256-CFB : aria-256-cfb +!Cname aria-256-ofb128 +aria 14 : ARIA-256-OFB : aria-256-ofb +aria 15 : ARIA-256-CTR : aria-256-ctr + +# There are no OIDs for these ARIA modes... + : ARIA-128-CFB1 : aria-128-cfb1 + : ARIA-192-CFB1 : aria-192-cfb1 + : ARIA-256-CFB1 : aria-256-cfb1 + : ARIA-128-CFB8 : aria-128-cfb8 + : ARIA-192-CFB8 : aria-192-cfb8 + : ARIA-256-CFB8 : aria-256-cfb8 + +aria 37 : ARIA-128-CCM : aria-128-ccm +aria 38 : ARIA-192-CCM : aria-192-ccm +aria 39 : ARIA-256-CCM : aria-256-ccm +aria 34 : ARIA-128-GCM : aria-128-gcm +aria 35 : ARIA-192-GCM : aria-192-gcm +aria 36 : ARIA-256-GCM : aria-256-gcm + +# Definitions for SEED cipher - ECB, CBC, OFB mode + +member-body 410 200004 : KISA : kisa +kisa 1 3 : SEED-ECB : seed-ecb +kisa 1 4 : SEED-CBC : seed-cbc +!Cname seed-cfb128 +kisa 1 5 : SEED-CFB : seed-cfb +!Cname seed-ofb128 +kisa 1 6 : SEED-OFB : seed-ofb + + +# Definitions for SM4 cipher + +sm-scheme 104 1 : SM4-ECB : sm4-ecb +sm-scheme 104 2 : SM4-CBC : sm4-cbc +!Cname sm4-ofb128 +sm-scheme 104 3 : SM4-OFB : sm4-ofb +!Cname sm4-cfb128 +sm-scheme 104 4 : SM4-CFB : sm4-cfb +sm-scheme 104 5 : SM4-CFB1 : sm4-cfb1 +sm-scheme 104 6 : SM4-CFB8 : sm4-cfb8 +sm-scheme 104 7 : SM4-CTR : sm4-ctr +sm-scheme 104 8 : SM4-GCM : sm4-gcm +sm-scheme 104 9 : SM4-CCM : sm4-ccm +sm-scheme 104 10 : SM4-XTS : sm4-xts + +# There is no OID that just denotes "HMAC" oddly enough... + + : HMAC : hmac +# Nor CMAC either + : CMAC : cmac + +# Synthetic composite ciphersuites + : RC4-HMAC-MD5 : rc4-hmac-md5 + : AES-128-CBC-HMAC-SHA1 : aes-128-cbc-hmac-sha1 + : AES-192-CBC-HMAC-SHA1 : aes-192-cbc-hmac-sha1 + : AES-256-CBC-HMAC-SHA1 : aes-256-cbc-hmac-sha1 + : AES-128-CBC-HMAC-SHA256 : aes-128-cbc-hmac-sha256 + : AES-192-CBC-HMAC-SHA256 : aes-192-cbc-hmac-sha256 + : AES-256-CBC-HMAC-SHA256 : aes-256-cbc-hmac-sha256 + : ChaCha20-Poly1305 : chacha20-poly1305 + : ChaCha20 : chacha20 + : AES-128-CBC-HMAC-SHA1-ETM : aes-128-cbc-hmac-sha1-etm + : AES-192-CBC-HMAC-SHA1-ETM : aes-192-cbc-hmac-sha1-etm + : AES-256-CBC-HMAC-SHA1-ETM : aes-256-cbc-hmac-sha1-etm + : AES-128-CBC-HMAC-SHA256-ETM : aes-128-cbc-hmac-sha256-etm + : AES-192-CBC-HMAC-SHA256-ETM : aes-192-cbc-hmac-sha256-etm + : AES-256-CBC-HMAC-SHA256-ETM : aes-256-cbc-hmac-sha256-etm + : AES-128-CBC-HMAC-SHA512-ETM : aes-128-cbc-hmac-sha512-etm + : AES-192-CBC-HMAC-SHA512-ETM : aes-192-cbc-hmac-sha512-etm + : AES-256-CBC-HMAC-SHA512-ETM : aes-256-cbc-hmac-sha512-etm + +ISO-US 10046 2 1 : dhpublicnumber : X9.42 DH + +# RFC 5639 curve OIDs (see http://www.ietf.org/rfc/rfc5639.txt) +# versionOne OBJECT IDENTIFIER ::= { +# iso(1) identified-organization(3) teletrust(36) algorithm(3) +# signature-algorithm(3) ecSign(2) ecStdCurvesAndGeneration(8) +# ellipticCurve(1) 1 } +1 3 36 3 3 2 8 1 1 1 : brainpoolP160r1 +1 3 36 3 3 2 8 1 1 2 : brainpoolP160t1 +1 3 36 3 3 2 8 1 1 3 : brainpoolP192r1 +1 3 36 3 3 2 8 1 1 4 : brainpoolP192t1 +1 3 36 3 3 2 8 1 1 5 : brainpoolP224r1 +1 3 36 3 3 2 8 1 1 6 : brainpoolP224t1 +1 3 36 3 3 2 8 1 1 7 : brainpoolP256r1 +# Alternate NID to represent the TLSv1.3 brainpoolP256r1 group + : brainpoolP256r1tls13 +1 3 36 3 3 2 8 1 1 8 : brainpoolP256t1 +1 3 36 3 3 2 8 1 1 9 : brainpoolP320r1 +1 3 36 3 3 2 8 1 1 10 : brainpoolP320t1 +1 3 36 3 3 2 8 1 1 11 : brainpoolP384r1 +# Alternate NID to represent the TLSv1.3 brainpoolP384r1 group + : brainpoolP384r1tls13 +1 3 36 3 3 2 8 1 1 12 : brainpoolP384t1 +1 3 36 3 3 2 8 1 1 13 : brainpoolP512r1 +# Alternate NID to represent the TLSv1.3 brainpoolP512r1 group + : brainpoolP512r1tls13 +1 3 36 3 3 2 8 1 1 14 : brainpoolP512t1 + +# ECDH schemes from RFC5753 +!Alias x9-63-scheme 1 3 133 16 840 63 0 +!Alias secg-scheme certicom-arc 1 + +x9-63-scheme 2 : dhSinglePass-stdDH-sha1kdf-scheme +secg-scheme 11 0 : dhSinglePass-stdDH-sha224kdf-scheme +secg-scheme 11 1 : dhSinglePass-stdDH-sha256kdf-scheme +secg-scheme 11 2 : dhSinglePass-stdDH-sha384kdf-scheme +secg-scheme 11 3 : dhSinglePass-stdDH-sha512kdf-scheme + +x9-63-scheme 3 : dhSinglePass-cofactorDH-sha1kdf-scheme +secg-scheme 14 0 : dhSinglePass-cofactorDH-sha224kdf-scheme +secg-scheme 14 1 : dhSinglePass-cofactorDH-sha256kdf-scheme +secg-scheme 14 2 : dhSinglePass-cofactorDH-sha384kdf-scheme +secg-scheme 14 3 : dhSinglePass-cofactorDH-sha512kdf-scheme +# NIDs for use with lookup tables. + : dh-std-kdf + : dh-cofactor-kdf + +# RFC 6962 Extension OIDs (see http://www.ietf.org/rfc/rfc6962.txt) +1 3 6 1 4 1 11129 2 4 2 : ct_precert_scts : CT Precertificate SCTs +1 3 6 1 4 1 11129 2 4 3 : ct_precert_poison : CT Precertificate Poison +1 3 6 1 4 1 11129 2 4 4 : ct_precert_signer : CT Precertificate Signer +1 3 6 1 4 1 11129 2 4 5 : ct_cert_scts : CT Certificate SCTs + +# CABForum EV SSL Certificate Guidelines +# (see https://cabforum.org/extended-validation/) +# OIDs for Subject Jurisdiction of Incorporation or Registration +ms-corp 60 2 1 1 : jurisdictionL : jurisdictionLocalityName +ms-corp 60 2 1 2 : jurisdictionST : jurisdictionStateOrProvinceName +ms-corp 60 2 1 3 : jurisdictionC : jurisdictionCountryName + +# SCRYPT algorithm +!Cname id-scrypt +1 3 6 1 4 1 11591 4 11 : id-scrypt : scrypt + +# NID for TLS1 PRF + : TLS1-PRF : tls1-prf + +# NID for HKDF + : HKDF : hkdf + +# NID for SSHKDF + : SSHKDF : sshkdf + +# NID for SSKDF + : SSKDF : sskdf +# NID for X942KDF + : X942KDF : x942kdf + +# NID for X963-2001 KDF + : X963KDF : x963kdf + +# RFC 4556 +1 3 6 1 5 2 3 : id-pkinit +id-pkinit 4 : pkInitClientAuth : PKINIT Client Auth +id-pkinit 5 : pkInitKDC : Signing KDC Response + +# From RFC8410 +1 3 101 110 : X25519 +1 3 101 111 : X448 +1 3 101 112 : ED25519 +1 3 101 113 : ED448 + + +# NIDs for cipher key exchange + : KxRSA : kx-rsa + : KxECDHE : kx-ecdhe + : KxDHE : kx-dhe + : KxECDHE-PSK : kx-ecdhe-psk + : KxDHE-PSK : kx-dhe-psk + : KxRSA_PSK : kx-rsa-psk + : KxPSK : kx-psk + : KxSRP : kx-srp + : KxGOST : kx-gost + : KxGOST18 : kx-gost18 + : KxANY : kx-any + +# NIDs for cipher authentication + : AuthRSA : auth-rsa + : AuthECDSA : auth-ecdsa + : AuthPSK : auth-psk + : AuthDSS : auth-dss + : AuthGOST01 : auth-gost01 + : AuthGOST12 : auth-gost12 + : AuthSRP : auth-srp + : AuthNULL : auth-null + : AuthANY : auth-any +# NID for Poly1305 + : Poly1305 : poly1305 +# NID for SipHash + : SipHash : siphash +# NIDs for RFC7919 DH parameters + : ffdhe2048 + : ffdhe3072 + : ffdhe4096 + : ffdhe6144 + : ffdhe8192 +# NIDs for RFC3526 DH parameters + : modp_1536 + : modp_2048 + : modp_3072 + : modp_4096 + : modp_6144 + : modp_8192 + +# OIDs for DSTU-4145/DSTU-7564 (http://zakon2.rada.gov.ua/laws/show/z0423-17) + +# DSTU OIDs +member-body 804 : ISO-UA +ISO-UA 2 1 1 1 : ua-pki +ua-pki 1 1 1 : dstu28147 : DSTU Gost 28147-2009 +dstu28147 2 : dstu28147-ofb : DSTU Gost 28147-2009 OFB mode +dstu28147 3 : dstu28147-cfb : DSTU Gost 28147-2009 CFB mode +dstu28147 5 : dstu28147-wrap : DSTU Gost 28147-2009 key wrap + +ua-pki 1 1 2 : hmacWithDstu34311 : HMAC DSTU Gost 34311-95 +ua-pki 1 2 1 : dstu34311 : DSTU Gost 34311-95 + +ua-pki 1 3 1 1 : dstu4145le : DSTU 4145-2002 little endian +dstu4145le 1 1 : dstu4145be : DSTU 4145-2002 big endian + +# 1.2.804. 2.1.1.1 1.3.1.1 .2.6 +# UA ua-pki 4145 le +# DSTU named curves +dstu4145le 2 0 : uacurve0 : DSTU curve 0 +dstu4145le 2 1 : uacurve1 : DSTU curve 1 +dstu4145le 2 2 : uacurve2 : DSTU curve 2 +dstu4145le 2 3 : uacurve3 : DSTU curve 3 +dstu4145le 2 4 : uacurve4 : DSTU curve 4 +dstu4145le 2 5 : uacurve5 : DSTU curve 5 +dstu4145le 2 6 : uacurve6 : DSTU curve 6 +dstu4145le 2 7 : uacurve7 : DSTU curve 7 +dstu4145le 2 8 : uacurve8 : DSTU curve 8 +dstu4145le 2 9 : uacurve9 : DSTU curve 9 +# NID for AES-SIV + : AES-128-SIV : aes-128-siv + : AES-192-SIV : aes-192-siv + : AES-256-SIV : aes-256-siv + + +!Cname oracle +joint-iso-itu-t 16 840 1 113894 : oracle-organization : Oracle organization +# Jdk trustedKeyUsage attribute +oracle 746875 1 1 : oracle-jdk-trustedkeyusage : Trusted key usage (Oracle) + +# NID for compression + : brotli : Brotli compression + : zstd : Zstandard compression + +2 23 133 : tcg : Trusted Computing Group + +tcg 1 : tcg-tcpaSpecVersion +tcg 2 : tcg-attribute : Trusted Computing Group Attributes +tcg 3 : tcg-protocol : Trusted Computing Group Protocols +tcg 4 : tcg-algorithm : Trusted Computing Group Algorithms +tcg 5 : tcg-platformClass : Trusted Computing Group Platform Classes +tcg 6 : tcg-ce : Trusted Computing Group Certificate Extensions +tcg 8 : tcg-kp : Trusted Computing Group Key Purposes +tcg 11 : tcg-ca : Trusted Computing Group Certificate Policies +tcg 17 : tcg-address : Trusted Computing Group Address Formats +tcg 18 : tcg-registry : Trusted Computing Group Registry +tcg 19 : tcg-traits : Trusted Computing Group Traits + +tcg-platformClass 1 : tcg-common : Trusted Computing Group Common +tcg-common 1 : tcg-at-platformManufacturerStr : TCG Platform Manufacturer String +tcg-common 2 : tcg-at-platformManufacturerId : TCG Platform Manufacturer ID +tcg-common 3 : tcg-at-platformConfigUri : TCG Platform Configuration URI +tcg-common 4 : tcg-at-platformModel : TCG Platform Model +tcg-common 5 : tcg-at-platformVersion : TCG Platform Version +tcg-common 6 : tcg-at-platformSerial : TCG Platform Serial Number +tcg-common 7 : tcg-at-platformConfiguration : TCG Platform Configuration +tcg-common 8 : tcg-at-platformIdentifier : TCG Platform Identifier + +tcg-attribute 1 : tcg-at-tpmManufacturer : TPM Manufacturer +tcg-attribute 2 : tcg-at-tpmModel : TPM Model +tcg-attribute 3 : tcg-at-tpmVersion : TPM Version +tcg-attribute 10 : tcg-at-securityQualities : Security Qualities +tcg-attribute 11 : tcg-at-tpmProtectionProfile : TPM Protection Profile +tcg-attribute 12 : tcg-at-tpmSecurityTarget : TPM Security Target +tcg-attribute 13 : tcg-at-tbbProtectionProfile : TBB Protection Profile +tcg-attribute 14 : tcg-at-tbbSecurityTarget : TBB Security Target +tcg-attribute 15 : tcg-at-tpmIdLabel : TPM ID Label +tcg-attribute 16 : tcg-at-tpmSpecification : TPM Specification +tcg-attribute 17 : tcg-at-tcgPlatformSpecification : TPM Platform Specification +tcg-attribute 18 : tcg-at-tpmSecurityAssertions : TPM Security Assertions +tcg-attribute 19 : tcg-at-tbbSecurityAssertions : TBB Security Assertions +tcg-attribute 23 : tcg-at-tcgCredentialSpecification : TCG Credential Specification +tcg-attribute 25 : tcg-at-tcgCredentialType : TCG Credential Type +tcg-attribute 26 : tcg-at-previousPlatformCertificates : TCG Previous Platform Certificates +tcg-attribute 27 : tcg-at-tbbSecurityAssertions-v3 : TCG TBB Security Assertions V3 +tcg-attribute 28 : tcg-at-cryptographicAnchors : TCG Cryptographic Anchors + +tcg-at-platformConfiguration 1 : tcg-at-platformConfiguration-v1 : Platform Configuration Version 1 +tcg-at-platformConfiguration 2 : tcg-at-platformConfiguration-v2 : Platform Configuration Version 2 +tcg-at-platformConfiguration 3 : tcg-at-platformConfiguration-v3 : Platform Configuration Version 3 +tcg-at-platformConfiguration 4 : tcg-at-platformConfigUri-v3 : Platform Configuration URI Version 3 + +tcg-algorithm 1 : tcg-algorithm-null : TCG NULL Algorithm + +tcg-kp 1 : tcg-kp-EKCertificate : Endorsement Key Certificate +tcg-kp 2 : tcg-kp-PlatformAttributeCertificate : Platform Attribute Certificate +tcg-kp 3 : tcg-kp-AIKCertificate : Attestation Identity Key Certificate +tcg-kp 4 : tcg-kp-PlatformKeyCertificate : Platform Key Certificate +tcg-kp 5 : tcg-kp-DeltaPlatformAttributeCertificate : Delta Platform Attribute Certificate +tcg-kp 6 : tcg-kp-DeltaPlatformKeyCertificate : Delta Platform Key Certificate +tcg-kp 7 : tcg-kp-AdditionalPlatformAttributeCertificate : Additional Platform Attribute Certificate +tcg-kp 8 : tcg-kp-AdditionalPlatformKeyCertificate : Additional Platform Key Certificate + +tcg-ce 2 : tcg-ce-relevantCredentials : Relevant Credentials +tcg-ce 3 : tcg-ce-relevantManifests : Relevant Manifests +tcg-ce 4 : tcg-ce-virtualPlatformAttestationService : Virtual Platform Attestation Service +tcg-ce 5 : tcg-ce-migrationControllerAttestationService : Migration Controller Attestation Service +tcg-ce 6 : tcg-ce-migrationControllerRegistrationService : Migration Controller Registration Service +tcg-ce 7 : tcg-ce-virtualPlatformBackupService : Virtual Platform Backup Service + +tcg-protocol 1 : tcg-prt-tpmIdProtocol : TCG TPM Protocol + +tcg-address 1 : tcg-address-ethernetmac : Ethernet MAC Address +tcg-address 2 : tcg-address-wlanmac : WLAN MAC Address +tcg-address 3 : tcg-address-bluetoothmac : Bluetooth MAC Address + +tcg-registry 3 : tcg-registry-componentClass : TCG Component Class + +tcg-registry-componentClass 1 : tcg-registry-componentClass-tcg : Trusted Computed Group Registry +tcg-registry-componentClass 2 : tcg-registry-componentClass-ietf : Internet Engineering Task Force Registry +tcg-registry-componentClass 3 : tcg-registry-componentClass-dmtf : Distributed Management Task Force Registry +tcg-registry-componentClass 4 : tcg-registry-componentClass-pcie : PCIE Component Class +tcg-registry-componentClass 5 : tcg-registry-componentClass-disk : Disk Component Class + +tcg-ca 4 : tcg-cap-verifiedPlatformCertificate : TCG Verified Platform Certificate CA Policy + +tcg-traits 1 : tcg-tr-ID : TCG Trait Identifiers +tcg-traits 2 : tcg-tr-category : TCG Trait Categories +tcg-traits 3 : tcg-tr-registry : TCG Trait Registries + +tcg-tr-ID 1 : tcg-tr-ID-Boolean : Boolean Trait +tcg-tr-ID 2 : tcg-tr-ID-CertificateIdentifier : Certificate Identifier Trait +tcg-tr-ID 3 : tcg-tr-ID-CommonCriteria : Common Criteria Trait +tcg-tr-ID 4 : tcg-tr-ID-componentClass : Component Class Trait +tcg-tr-ID 5 : tcg-tr-ID-componentIdentifierV11 : Component Identifier V1.1 Trait +tcg-tr-ID 6 : tcg-tr-ID-FIPSLevel : FIPS Level Trait +tcg-tr-ID 7 : tcg-tr-ID-ISO9000Level : ISO 9000 Level Trait +tcg-tr-ID 8 : tcg-tr-ID-networkMAC : Network MAC Trait +tcg-tr-ID 9 : tcg-tr-ID-OID : Object Identifier Trait +tcg-tr-ID 10 : tcg-tr-ID-PEN : Private Enterprise Number Trait +tcg-tr-ID 11 : tcg-tr-ID-platformFirmwareCapabilities : Platform Firmware Capabilities Trait +tcg-tr-ID 12 : tcg-tr-ID-platformFirmwareSignatureVerification : Platform Firmware Signature Verification Trait +tcg-tr-ID 13 : tcg-tr-ID-platformFirmwareUpdateCompliance : Platform Firmware Update Compliance Trait +tcg-tr-ID 14 : tcg-tr-ID-platformHardwareCapabilities : Platform Hardware Capabilities Trait +tcg-tr-ID 15 : tcg-tr-ID-RTM : Root of Trust for Measurement Trait +tcg-tr-ID 16 : tcg-tr-ID-status : Attribute Status Trait +tcg-tr-ID 17 : tcg-tr-ID-URI : Uniform Resource Identifier Trait +tcg-tr-ID 18 : tcg-tr-ID-UTF8String : UTF8String Trait +tcg-tr-ID 19 : tcg-tr-ID-IA5String : IA5String Trait +tcg-tr-ID 20 : tcg-tr-ID-PEMCertString : PEM-Encoded Certificate String Trait +tcg-tr-ID 21 : tcg-tr-ID-PublicKey : Public Key Trait + +tcg-tr-category 1 : tcg-tr-cat-platformManufacturer : Platform Manufacturer Trait Category +tcg-tr-category 2 : tcg-tr-cat-platformModel : Platform Model Trait Category +tcg-tr-category 3 : tcg-tr-cat-platformVersion : Platform Version Trait Category +tcg-tr-category 4 : tcg-tr-cat-platformSerial : Platform Serial Trait Category +tcg-tr-category 5 : tcg-tr-cat-platformManufacturerIdentifier : Platform Manufacturer Identifier Trait Category +tcg-tr-category 6 : tcg-tr-cat-platformOwnership : Platform Ownership Trait Category +tcg-tr-category 7 : tcg-tr-cat-componentClass : Component Class Trait Category +tcg-tr-category 8 : tcg-tr-cat-componentManufacturer : Component Manufacturer Trait Category +tcg-tr-category 9 : tcg-tr-cat-componentModel : Component Model Trait Category +tcg-tr-category 10 : tcg-tr-cat-componentSerial : Component Serial Trait Category +tcg-tr-category 11 : tcg-tr-cat-componentStatus : Component Status Trait Category +tcg-tr-category 12 : tcg-tr-cat-componentLocation : Component Location Trait Category +tcg-tr-category 13 : tcg-tr-cat-componentRevision : Component Revision Trait Category +tcg-tr-category 14 : tcg-tr-cat-componentFieldReplaceable : Component Field Replaceable Trait Category +tcg-tr-category 15 : tcg-tr-cat-EKCertificate : EK Certificate Trait Category +tcg-tr-category 16 : tcg-tr-cat-IAKCertificate : IAK Certificate Trait Category +tcg-tr-category 17 : tcg-tr-cat-IDevIDCertificate : IDevID Certificate Trait Category +tcg-tr-category 18 : tcg-tr-cat-DICECertificate : DICE Certificate Trait Category +tcg-tr-category 19 : tcg-tr-cat-SPDMCertificate : SPDM Certificate Trait Category +tcg-tr-category 20 : tcg-tr-cat-PEMCertificate : PEM Certificate Trait Category +tcg-tr-category 21 : tcg-tr-cat-PlatformCertificate : Platform Certificate Trait Category +tcg-tr-category 22 : tcg-tr-cat-DeltaPlatformCertificate : Delta Platform Certificate Trait Category +tcg-tr-category 23 : tcg-tr-cat-RebasePlatformCertificate : Rebase Platform Certificate Trait Category +tcg-tr-category 24 : tcg-tr-cat-genericCertificate : Generic Certificate Trait Category +tcg-tr-category 25 : tcg-tr-cat-CommonCriteria : Common Criteria Trait Category +tcg-tr-category 26 : tcg-tr-cat-componentIdentifierV11 : Component Identifier V1.1 Trait Category +tcg-tr-category 27 : tcg-tr-cat-FIPSLevel : FIPS Level Trait Category +tcg-tr-category 28 : tcg-tr-cat-ISO9000 : ISO 9000 Trait Category +tcg-tr-category 29 : tcg-tr-cat-networkMAC : Network MAC Trait Category +tcg-tr-category 30 : tcg-tr-cat-attestationProtocol : Attestation Protocol Trait Category +tcg-tr-category 31 : tcg-tr-cat-PEN : Private Enterprise Number Trait Category +tcg-tr-category 32 : tcg-tr-cat-platformFirmwareCapabilities : Platform Firmware Capabilities Trait Category +tcg-tr-category 33 : tcg-tr-cat-platformHardwareCapabilities : Platform Hardware Capabilities Trait Category +tcg-tr-category 34 : tcg-tr-cat-platformFirmwareSignatureVerification : Platform Firmware Signature Verification Trait Category +tcg-tr-category 35 : tcg-tr-cat-platformFirmwareUpdateCompliance : Platform Firmware Update Compliance Trait Category +tcg-tr-category 36 : tcg-tr-cat-RTM : Root of Trust of Measurement Trait Category +tcg-tr-category 37 : tcg-tr-cat-PublicKey : Public Key Trait Category + +!Alias nistKems nistAlgorithms 4 +nistKems 1 : id-alg-ml-kem-512 : ML-KEM-512 +nistKems 2 : id-alg-ml-kem-768 : ML-KEM-768 +nistKems 3 : id-alg-ml-kem-1024 : ML-KEM-1024 From d2b1496bcc89275ba0662d4775beb2df991eef88 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sat, 30 May 2026 14:02:32 +0200 Subject: [PATCH 05/25] Enable/disable ssl-related tests from cpython test suite Enabled tests: * test_create_server_ssl_verified * test_create_connection_memory_leak * test_handshake_timeout_handler_leak * test_tls_unique_channel_binding - now correctly skipped because we monkey-patch ssl.CHANNEL_BINDING_TYPES * test_sni_callback_alert * test_sni_callback_raising * test_sni_callback_wrong_return_type Tests skipped with rustls implementation specifically (should work with OpenSSL): * test_load_dh_params - rustls does not support Diffie-Hellman key exchange (it uses elliptic curve Diffie-Hellman) * test_get_ca_certs_capath - test expects that certificates are loaded lazily which is hard to implement with rustls * test_verify_strict - rustls does not perform some cert checks that OpenSSL does in strict mode Previously disbled tests changed to rustls-specific skip: * test_session - current rustls does not expose session info * test_session_handling * test_msg_callback_tls12 - _msg_callback cannot be implemented with rustls --- Lib/test/test_asyncio/test_events.py | 1 - Lib/test/test_asyncio/test_ssl.py | 2 -- Lib/test/test_ssl.py | 13 ++++++------- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index b60c7452f3f..82c997f0abd 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -1288,7 +1288,6 @@ def test_create_unix_server_ssl_verified(self): server.close() self.loop.run_until_complete(proto.done) - @unittest.expectedFailure # TODO: RUSTPYTHON; - SSL peer certificate format differs @unittest.skipIf(ssl is None, 'No ssl module') def test_create_server_ssl_verified(self): proto = MyProto(loop=self.loop) diff --git a/Lib/test/test_asyncio/test_ssl.py b/Lib/test/test_asyncio/test_ssl.py index 932b1dace4f..ca15fc3bdd4 100644 --- a/Lib/test/test_asyncio/test_ssl.py +++ b/Lib/test/test_asyncio/test_ssl.py @@ -738,7 +738,6 @@ async def client(addr): asyncio.wait_for(client(srv.addr), timeout=support.SHORT_TIMEOUT)) - @unittest.expectedFailure # TODO: RUSTPYTHON; - gc.collect() doesn't release SSLContext properly def test_create_connection_memory_leak(self): HELLO_MSG = b'1' * self.PAYLOAD_SIZE @@ -1617,7 +1616,6 @@ async def test(): else: self.fail('Unexpected ResourceWarning: {}'.format(cm.warning)) - @unittest.expectedFailure # TODO: RUSTPYTHON; - gc.collect() doesn't release SSLContext properly def test_handshake_timeout_handler_leak(self): s = socket.socket(socket.AF_INET) s.bind(('127.0.0.1', 0)) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 7cfbe0c97dc..ce494aa9183 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1346,6 +1346,7 @@ def test_load_verify_cadata(self): with self.assertRaises(ssl.SSLError): ctx.load_verify_locations(cadata=cacert_der + b"A") + @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls does not support custom DH parameters") def test_load_dh_params(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) try: @@ -2092,6 +2093,7 @@ def test_ciphers(self): cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") s.connect(self.server_addr) + @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; capath certificates are loaded eagerly instead of on request") def test_get_ca_certs_capath(self): # capath certs are loaded on request ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -3071,6 +3073,7 @@ def test_ecc_cert(self): @unittest.skipUnless(IS_OPENSSL_3_0_0, "test requires RFC 5280 check added in OpenSSL 3.0+") + @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls cert verification does not match OpenSSL's VERIFY_X509_STRICT") def test_verify_strict(self): # verification fails by default, since the server cert is non-conforming client_context = ssl.create_default_context() @@ -3996,7 +3999,6 @@ def test_default_ecdh_curve(self): s.connect((HOST, server.port)) self.assertIn("ECDH", s.cipher()[0]) - @unittest.expectedFailure # TODO: RUSTPYTHON @unittest.skipUnless("tls-unique" in ssl.CHANNEL_BINDING_TYPES, "'tls-unique' channel binding not available") def test_tls_unique_channel_binding(self): @@ -4259,7 +4261,6 @@ def servername_cb(ssl_sock, server_name, initial_context): self.check_common_name(stats, SIGNED_CERTFILE_HOSTNAME) self.assertEqual(calls, []) - @unittest.expectedFailure # TODO: RUSTPYTHON; + TLSV1_ALERT_ACCESS_DENIED def test_sni_callback_alert(self): # Returning a TLS alert is reflected to the connecting client server_context, other_context, client_context = self.sni_contexts() @@ -4273,7 +4274,6 @@ def cb_returning_alert(ssl_sock, server_name, initial_context): sni_name='supermessage') self.assertEqual(cm.exception.reason, 'TLSV1_ALERT_ACCESS_DENIED') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_sni_callback_raising(self): # Raising fails the connection with a TLS handshake failure alert. server_context, other_context, client_context = self.sni_contexts() @@ -4293,7 +4293,6 @@ def cb_raising(ssl_sock, server_name, initial_context): self.assertRegex(cm.exception.reason, regex) self.assertEqual(catch.unraisable.exc_type, ZeroDivisionError) - @unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'SSLEOFError' object has no attribute 'reason' def test_sni_callback_wrong_return_type(self): # Returning the wrong return type terminates the TLS connection # with an internal error alert. @@ -4359,7 +4358,7 @@ def test_sendfile(self): s.sendfile(file) self.assertEqual(s.recv(1024), TEST_DATA) - @unittest.expectedFailure # TODO: RUSTPYTHON + @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "AttributeError: 'NoneType' object has no attribute 'id'") def test_session(self): client_context, server_context, hostname = testing_context() # TODO: sessions aren't compatible with TLSv1.3 yet @@ -4417,7 +4416,7 @@ def test_session(self): self.assertEqual(sess_stat['accept'], 4) self.assertEqual(sess_stat['hits'], 2) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: False != True + @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; AssertionError: None is not true") def test_session_handling(self): client_context, server_context, hostname = testing_context() client_context2, _, _ = testing_context() @@ -5036,7 +5035,7 @@ def msg_cb(conn, direction, version, content_type, msg_type, data): with self.assertRaises(TypeError): client_context._msg_callback = object() - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ('read', , <_TLSContentType.HANDSHAKE: 22>, <_TLSMessageType.SERVER_KEY_EXCHANGE: 12>) not found in [] + @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; AssertionError: ('read', , <_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 From 6c311f82d5118ec2ccc1e79ec5ecbfeaddaa942e Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sun, 31 May 2026 12:11:02 +0200 Subject: [PATCH 06/25] Revert "Skip flaky tests (#7961)" This reverts commit e1d9a1123eff81fac38de2c824ac121f41c72585. --- Lib/test/test_asyncio/test_sendfile.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Lib/test/test_asyncio/test_sendfile.py b/Lib/test/test_asyncio/test_sendfile.py index e266d57742a..8cd49a2ce9d 100644 --- a/Lib/test/test_asyncio/test_sendfile.py +++ b/Lib/test/test_asyncio/test_sendfile.py @@ -577,10 +577,6 @@ class PollEventLoopTests(SendfileTestsBase, def create_event_loop(self): return asyncio.SelectorEventLoop(selectors.PollSelector()) - @unittest.skipIf(sys.platform != "win32", "TODO: RUSTPYTHON; Flaky on CI") - def test_sendfile_ssl_pre_and_post_data(self): - return super().test_sendfile_ssl_pre_and_post_data() - # Should always exist. class SelectEventLoopTests(SendfileTestsBase, test_utils.TestCase): @@ -588,10 +584,6 @@ class SelectEventLoopTests(SendfileTestsBase, def create_event_loop(self): return asyncio.SelectorEventLoop(selectors.SelectSelector()) - @unittest.skipIf(sys.platform != "win32", "TODO: RUSTPYTHON; Flaky on CI") - def test_sendfile_ssl_pre_and_post_data(self): - return super().test_sendfile_ssl_pre_and_post_data() - if __name__ == '__main__': unittest.main() From 3f5cb40e89a9c8d93ef051818af5022e23168adb Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sun, 31 May 2026 12:10:42 +0200 Subject: [PATCH 07/25] Revert "Skip flaky test (#7967)" This reverts commit 52305c0c7268aabcdca681aff8f6d95f8e0b1e2e. --- Lib/test/test_asyncio/test_sendfile.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Lib/test/test_asyncio/test_sendfile.py b/Lib/test/test_asyncio/test_sendfile.py index 8cd49a2ce9d..dcd963b3355 100644 --- a/Lib/test/test_asyncio/test_sendfile.py +++ b/Lib/test/test_asyncio/test_sendfile.py @@ -566,10 +566,6 @@ class EPollEventLoopTests(SendfileTestsBase, def create_event_loop(self): return asyncio.SelectorEventLoop(selectors.EpollSelector()) - @unittest.skipIf(sys.platform != "win32", "TODO: RUSTPYTHON; Flaky on CI") - def test_sendfile_ssl_pre_and_post_data(self): - return super().test_sendfile_ssl_pre_and_post_data() - if hasattr(selectors, 'PollSelector'): class PollEventLoopTests(SendfileTestsBase, test_utils.TestCase): From b5a1ea354004e739591c75277330cc86a85f3764 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Sun, 24 May 2026 23:58:42 +0200 Subject: [PATCH 08/25] Almost a complete rewrite of rustls integration Main changes: * Now it contains understandable connection state machine instead of a handful of entangled variables. * OID and NID mappings are generated from OpenSSL data files. Some mappings are still hardcoded but those are much smaller than before and are actually testable. * OpenSSL-compatible cipher list string (`man openssl-ciphers`) is parsed correctly (I hope so). * Difference between socket IO and buffered IO handling is minimal. Please check the module-level doc comment in crates/stdlib/src/rustls.rs for additional information. What is missing: * ssl timeout support. Normal socket timeouts still work just fine but Python's ssl module implementation handles timeouts for whole SSL/TLS operations, like handshake. This does not break anything and just makes timeouts imprecise so I believe that proper timeouts can be implemented later. * Socket IO still uses _socket.socket.send/recv instead of methods of the socket object itself. Otherwise everything hangs and this must be investigated. * See other TODO items in crates/stdlib/src/rustls.rs --- Cargo.lock | 269 +- Cargo.toml | 10 +- crates/stdlib/Cargo.toml | 18 +- crates/stdlib/src/lib.rs | 1 + crates/stdlib/src/rustls.rs | 5178 ++++++++++++++++++++++++++++ crates/stdlib/src/socket.rs | 5 +- crates/stdlib/src/ssl.rs | 5135 --------------------------- crates/stdlib/src/ssl/cert.rs | 1780 ---------- crates/stdlib/src/ssl/compat.rs | 1911 +--------- crates/stdlib/src/ssl/error.rs | 15 - crates/stdlib/src/ssl/oid.rs | 465 --- crates/stdlib/src/ssl/providers.rs | 13 +- examples/custom_tls_providers.rs | 2 + src/interpreter.rs | 1 + 14 files changed, 5334 insertions(+), 9469 deletions(-) create mode 100644 crates/stdlib/src/rustls.rs delete mode 100644 crates/stdlib/src/ssl.rs delete mode 100644 crates/stdlib/src/ssl/cert.rs delete mode 100644 crates/stdlib/src/ssl/oid.rs diff --git a/Cargo.lock b/Cargo.lock index b52cff30d31..bbcf795305c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,13 +16,13 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "aes" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ "cipher", "cpubits", - "cpufeatures 0.3.0", + "cpufeatures", ] [[package]] @@ -353,16 +353,7 @@ version = "0.11.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690" dependencies = [ - "digest 0.11.3", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", + "digest", ] [[package]] @@ -488,7 +479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", "rand_core 0.10.1", ] @@ -538,8 +529,8 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer 0.12.0", - "crypto-common 0.2.2", + "block-buffer", + "crypto-common", "inout", ] @@ -599,9 +590,9 @@ dependencies = [ [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "collection_literals" @@ -660,12 +651,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-oid" version = "0.10.2" @@ -721,15 +706,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -993,16 +969,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -1036,27 +1002,13 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "der_derive", - "flagset", - "pem-rfc7468 0.7.0", - "zeroize", -] - [[package]] name = "der" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" dependencies = [ - "const-oid 0.10.2", - "pem-rfc7468 1.0.0", + "const-oid", "zeroize", ] @@ -1074,17 +1026,6 @@ dependencies = [ "rusticata-macros", ] -[[package]] -name = "der_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "deranged" version = "0.5.8" @@ -1105,25 +1046,15 @@ dependencies = [ "syn", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", - "const-oid 0.10.2", - "crypto-common 0.2.2", + "block-buffer", + "const-oid", + "crypto-common", "ctutils", ] @@ -1264,12 +1195,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "flagset" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" - [[package]] name = "flame" version = "0.2.2" @@ -1379,16 +1304,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "get-size-derive2" version = "0.7.4" @@ -1604,7 +1519,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.3", + "digest", ] [[package]] @@ -1981,7 +1896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", ] [[package]] @@ -2316,7 +2231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.11.3", + "digest", ] [[package]] @@ -2656,28 +2571,10 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest 0.11.3", + "digest", "hmac", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "pem-rfc7468" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" -dependencies = [ - "base64ct", -] - [[package]] name = "phf" version = "0.11.3" @@ -2773,12 +2670,12 @@ checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" dependencies = [ "aes", "cbc", - "der 0.8.0", + "der", "pbkdf2", "rand_core 0.10.1", "scrypt", "sha2", - "spki 0.8.0", + "spki", ] [[package]] @@ -2787,10 +2684,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der 0.8.0", + "der", "pkcs5", "rand_core 0.10.1", - "spki 0.8.0", + "spki", ] [[package]] @@ -3390,15 +3287,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -3754,8 +3642,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "csv-core", - "der 0.8.0", - "digest 0.11.3", + "digest", "dns-lookup", "dyn-clone", "flame", @@ -3787,16 +3674,15 @@ dependencies = [ "parking_lot", "paste", "pbkdf2", - "pem-rfc7468 1.0.0", "phf 0.13.1", "pkcs8", "pymath", "rand 0.10.1", "rapidhash", "rustls", - "rustls-native-certs", - "rustls-pemfile", + "rustls-pki-types", "rustls-platform-verifier", + "rustls-webpki", "rustpython-common", "rustpython-derive", "rustpython-host_env", @@ -3806,7 +3692,8 @@ dependencies = [ "rustpython-ruff_source_file", "rustpython-ruff_text_size", "rustpython-vm", - "sha1 0.11.0", + "serde", + "sha1", "sha2", "sha3", "shake", @@ -3818,9 +3705,7 @@ dependencies = [ "unic-ucd-age", "unicode_names2 2.0.0", "uuid", - "webpki-roots", "widestring", - "x509-cert", "x509-parser", "xml", "xz", @@ -4111,17 +3996,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.11.0" @@ -4129,8 +4003,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -4140,8 +4014,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -4150,7 +4024,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ - "digest 0.11.3", + "digest", "keccak", "sponge-cursor", ] @@ -4161,7 +4035,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" dependencies = [ - "digest 0.11.3", + "digest", "keccak", "sponge-cursor", ] @@ -4180,15 +4054,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "simd-adler32" version = "0.3.9" @@ -4245,16 +4110,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - [[package]] name = "spki" version = "0.8.0" @@ -4262,7 +4117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.0", + "der", ] [[package]] @@ -4517,27 +4372,6 @@ dependencies = [ "shared-build", ] -[[package]] -name = "tls_codec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "toml" version = "1.1.2+spec-1.1.0" @@ -4923,15 +4757,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webpki-roots" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "8.0.2" @@ -5325,20 +5150,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" -[[package]] -name = "x509-cert" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" -dependencies = [ - "const-oid 0.9.6", - "der 0.7.10", - "sha1 0.10.6", - "signature", - "spki 0.7.3", - "tls_codec", -] - [[package]] name = "x509-parser" version = "0.18.1" @@ -5461,20 +5272,6 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 2a4215d4ad8..5f7fec75476 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ threading = ["rustpython-vm/threading", "rustpython-stdlib/threading"] sqlite = ["rustpython-stdlib/sqlite"] ssl = ["host_env"] ssl-rustls = ["ssl", "rustpython-stdlib/ssl-rustls"] -ssl-rustls-aws-lc = ["ssl-rustls", "dep:rustls", "rustls/aws_lc_rs"] +ssl-rustls-aws-lc = ["ssl-rustls", "dep:rustls", "rustls/aws_lc_rs", "rustpython-stdlib/ssl-rustls-aws-lc"] ssl-rustls-aws-lc-fips = ["ssl-rustls-aws-lc", "rustls/fips"] ssl-openssl = ["ssl", "rustpython-stdlib/ssl-openssl"] ssl-openssl-vendor = ["ssl-openssl", "rustpython-stdlib/ssl-openssl-vendor"] @@ -197,7 +197,6 @@ ruff_source_file = { package = "rustpython-ruff_source_file", version = "0.15.8" # ruff_text_size = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" } # ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", rev = "c2a8815842f9dc5d24ec19385eae0f1a7188b0d9" } -der = { version = "0.8", features = ["alloc", "oid", "pem", "zeroize"] } phf = { version = "0.13.1", default-features = false, features = ["macros"]} adler32 = "1.2.0" approx = "0.5.1" @@ -273,7 +272,6 @@ optional = "0.5" parking_lot = "0.12.3" paste = "1.0.15" pbkdf2 = "0.13" -pem-rfc7468 = "1.0" pkcs8 = "0.11" proc-macro2 = "1.0.105" psm = "0.1" @@ -287,9 +285,9 @@ result-like = "0.5.0" rustix = { version = "1.1", features = ["event", "param", "system"] } rustls = { version = "0.23.39", default-features = false } rustls-graviola = "0.3" -rustls-native-certs = "0.8" -rustls-pemfile = "2.2" +rustls-pki-types = { version = "1.14.1", default-features = false } rustls-platform-verifier = "0.7" +webpki = { package = "rustls-webpki", version = "0.103.13", default-features = false } rustyline = "18" serde = { version = "1.0.225", default-features = false, features = ["alloc", "derive"] } serde_bytes = { version = "0.11.19", default-features = false, features = ["std"] } @@ -327,9 +325,7 @@ windows-sys = "0.61.2" wasm-bindgen = "0.2.106" wasm-bindgen-futures = "0.4" web-sys = "0.3" -webpki-roots = "1.0" which = "8" -x509-cert = "0.2.5" x509-parser = "0.18" xml = "1.3" writeable = "0.6" diff --git a/crates/stdlib/Cargo.toml b/crates/stdlib/Cargo.toml index 61ecf413a7d..0299373ff3c 100644 --- a/crates/stdlib/Cargo.toml +++ b/crates/stdlib/Cargo.toml @@ -19,12 +19,14 @@ sqlite = ["dep:libsqlite3-sys"] # SSL backends ssl = ["host_env"] ssl-rustls = ["__ssl-rustls", "rustls/custom-provider"] +ssl-rustls-aws-lc = ["ssl-rustls", "rustls/aws_lc_rs"] +ssl-rustls-fips = ["ssl-rustls-aws-lc", "rustls/fips"] ssl-openssl = ["ssl", "openssl", "openssl-sys", "foreign-types-shared", "openssl-probe"] ssl-openssl-vendor = ["ssl-openssl", "openssl/vendored"] tkinter = ["dep:tk-sys", "dep:tcl-sys", "dep:widestring"] flame-it = ["flame"] -__ssl-rustls = ["ssl", "rustls", "rustls-native-certs", "rustls-pemfile", "rustls-platform-verifier", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "oid-registry", "pkcs8"] +__ssl-rustls = ["ssl", "rustls", "rustls-pki-types", "rustls-platform-verifier", "webpki", "x509-parser", "oid-registry", "pkcs8", "serde", "rustpython-vm/serde"] [dependencies] # rustpython crates @@ -116,16 +118,13 @@ foreign-types-shared = { workspace = true, optional = true } # Rustls dependencies (optional, for ssl-rustls feature) rustls = { workspace = true, default-features = false, features = ["std", "tls12"], optional = true } -rustls-native-certs = { workspace = true, optional = true } -rustls-pemfile = { workspace = true, optional = true } +rustls-pki-types = { workspace = true, optional = true } rustls-platform-verifier = { workspace = true, optional = true } -x509-cert = { workspace = true, features = ["pem", "builder"], optional = true } +webpki = { workspace = true, optional = true } x509-parser = { workspace = true, optional = true } -der = { workspace = true, optional = true } -pem-rfc7468 = { workspace = true, features = ["alloc"], optional = true } -webpki-roots = { workspace = true, optional = true } -oid-registry = { workspace = true, features = ["x509", "pkcs1", "nist_algs"], optional = true } -pkcs8 = { workspace = true, features = ["encryption", "pkcs5", "pem"], optional = true } +pkcs8 = { workspace = true, features = ["encryption", "pkcs5"], optional = true } +oid-registry = { workspace = true, optional = true } +serde = { workspace = true, optional = true } [target.'cfg(not(any(target_os = "android", target_arch = "wasm32")))'.dependencies] libsqlite3-sys = { workspace = true, features = ["bundled"], optional = true } @@ -141,6 +140,7 @@ system-configuration = { workspace = true } [dev-dependencies] insta = { workspace = true } +rustls = { workspace = true, default-features = false, features = ["aws_lc_rs", "std", "tls12"] } rustpython-pylib = { workspace = true, features = [ "freeze-stdlib" ] } diff --git a/crates/stdlib/src/lib.rs b/crates/stdlib/src/lib.rs index 11930787480..7d8adc4a310 100644 --- a/crates/stdlib/src/lib.rs +++ b/crates/stdlib/src/lib.rs @@ -131,6 +131,7 @@ mod openssl; not(target_arch = "wasm32"), feature = "__ssl-rustls" ))] +#[path = "rustls.rs"] pub mod ssl; #[cfg(all(feature = "ssl-openssl", feature = "__ssl-rustls", not(clippy)))] diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs new file mode 100644 index 00000000000..9b764925c8b --- /dev/null +++ b/crates/stdlib/src/rustls.rs @@ -0,0 +1,5178 @@ +// spell-checker: ignore ders SUITEB COMPLEMENTOFDEFAULT COMPLEMENTOFALL AESGCM MLKEM nids + +//! SSL/TLS implementation using rustls +//! +//! Warning: This module implements security primitives and it was not audited properly. +//! +//! Warning: This module still contains LLM-generated code. +//! +//! cpython's original ssl module was designed around OpenSSL and thus tightly coupled to +//! OpenSSL API and internals. 100% compatible re-implementation using any other SSL/TLS library +//! is near to impossible. +//! +//! This module uses `rustls` to provide a "best effort" compatibility with original `ssl` +//! implementation. In particular: +//! * Security-related functionality that is not supported by `rustls` is not implemented +//! and raises errors. +//! * Most of the SSLContext.options are not supported, set to zero and thus ignored. +//! All unsupported options are either irrelevant to security or meant to lower it. +//! * `rustls` is designed to be safe to use by default. However, it does not perform +//! all the certificate checks that OpenSSL does when VERIFY_X509_STRICT is enabled. +//! Unfortunately, a some client code may set VERIFY_X509_STRICT by default so we have to silently +//! ignore it. +//! * To support verifying certificates with both "default" certificate stores +//! (`SSLContext.load_default_certs()`) and provided root certificates +//! (`SSLContext.load_verify_locations()`) this implementation uses combined certificate +//! verifier consisting of `rustls_platform_verifier::Verifier` and `WebPkiServerVerifier`. +//! Combined certificate verifier reports certificates as valid when at least one of the underlying +//! verifiers reports it as valid and all others report "unknown issuer". +//! CRL verification control is unreliable with `SSLContext.load_default_certs()` because +//! `rustls_platform_verifier::Verifier` does not have settings for this and CRL support +//! varies by platform. +//! * Exposing TLS sessions to client code is not supported, dummy value returned. See comments inside +//! `PySSLSocket::set_session()`. Session resumption works out of the box. +//! * Channel binding are not supported and raises error. See comments inside `PySSLSocket::get_channel_binding()`. +//! * Post-handshake authentication is not supported, `SSLSocket.verify_client_post_handshake()` raises an error. +//! * SSLContext.hostname_checks_common_name is ignored because `rustls` always uses alt names to check server name. + +use alloc::{rc::Rc, sync::Arc}; +use core::{ + net::Ipv6Addr, + str::FromStr, + sync::atomic::{AtomicUsize, Ordering}, +}; +use std::{ + collections::{HashMap, HashSet}, + path::Path, +}; + +use base64::{Engine, prelude::BASE64_STANDARD}; +use chrono::{DateTime, Utc}; +use pkcs8::{EncryptedPrivateKeyInfoRef, PrivateKeyInfoRef, der::Decode}; +use rustls::{ + CipherSuite, Connection, DigitallySignedStruct, DistinguishedName, ProtocolVersion, + RootCertStore, SignatureScheme, SupportedCipherSuite, + client::WebPkiServerVerifier, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::{CryptoProvider, SupportedKxGroup}, + server::{AcceptedAlert, Acceptor}, +}; +use rustls_pki_types::{ + CertificateDer, CertificateRevocationListDer, DnsName, IpAddr, Ipv4Addr, ServerName, UnixTime, +}; +use serde::{Serialize, Serializer}; +use sha2::{Digest, Sha256}; +use x509_parser::{ + extensions::{DistributionPointName, GeneralName, ParsedExtension}, + oid_registry::{ + OID_PKIX_ACCESS_DESCRIPTOR_CA_ISSUERS, OID_PKIX_ACCESS_DESCRIPTOR_OCSP, + OID_PKIX_AUTHORITY_INFO_ACCESS, OID_X509_EXT_CRL_DISTRIBUTION_POINTS, Oid, OidEntry, + OidRegistry, + }, + parse_x509_certificate, parse_x509_crl, + pem::Pem, + time::ASN1Time, + x509::X509Name, +}; + +use crate::{ + common::lock::LazyLock, + vm::{ + AsObject as _, PyObjectRef, PyResult, TryFromObject, VirtualMachine, + builtins::{PyBaseExceptionRef, PyTupleRef, PyUtf8StrRef}, + convert::{IntoObject, RustPySerDeConf}, + function::{ArgBytesLike, OptionalArg}, + }, +}; + +#[path = "ssl/compat.rs"] +mod compat; +// SSL exception types (shared with openssl backend) +#[path = "ssl/error.rs"] +mod error; +#[path = "ssl/providers.rs"] +pub mod providers; + +// TODO: SslError should not convert errors to strings to check the type. +use compat::{SslError, SslResult}; +use providers::CryptoExt; + +pub(crate) use _ssl::module_def; + +#[allow(non_snake_case)] +#[allow(non_upper_case_globals)] +#[pymodule(with(error::ssl_error))] +mod _ssl { + use alloc::sync::Arc; + use core::{ + hash::{Hash as _, Hasher as _}, + slice, + sync::atomic::{AtomicBool, AtomicI32, AtomicUsize, Ordering}, + }; + use std::{ + hash::DefaultHasher, + io::{BufRead, Read, Write}, + time::{SystemTime, UNIX_EPOCH}, + }; + + use itertools::Itertools as _; + use rustls::{ + ALL_VERSIONS, ClientConfig, ClientConnection, Connection, HandshakeKind, ProtocolVersion, + ServerConfig, SupportedCipherSuite, SupportedProtocolVersion, + client::Resumption, + crypto::{CryptoProvider, SupportedKxGroup}, + server::{ + Accepted, AcceptedAlert, NoClientAuth, WebPkiClientVerifier, danger::ClientCertVerifier, + }, + sign::CertifiedKey, + }; + use rustls_pki_types::{CertificateDer, IpAddr, Ipv4Addr, PrivateKeyDer, ServerName}; + use serde::Serialize as _; + use x509_parser::{oid_registry::Oid, parse_x509_certificate}; + + use crate::{ + common::{ + hash::PyHash, + lock::{PyMutex, PyRwLock}, + }, + vm::{ + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, + VirtualMachine, + builtins::{ + PyBytesRef, PyListRef, PyModule, PyStrRef, PyTupleRef, PyType, PyTypeRef, + PyUtf8StrRef, + }, + convert::{IntoObject, IntoPyException}, + function::{ + ArgBytesLike, ArgMemoryBuffer, Either, FsPath, FuncArgs, OptionalArg, + PyComparisonValue, + }, + object::PyWeak, + stdlib::_warnings, + types::{Comparable, Constructor, Hashable, PyComparisonOp, Representable}, + }, + }; + + use super::{ + CIPHER_MAPPINGS, CertInfo, CertStore, CipherDescriptionDict, CipherList, CloseNotifyState, + ConnectionState, CrlCheck, CustomServerCertVerifier, DerKind, Io, OID_MAPPINGS, Password, + SECURITY_LEVEL_TO_MIN_BITS, State, Stats, WithOptionSuiteB, cipher_to_tuple, + cipher_to_version, compat::SslError, der_to_pem_cert, ensure_single_der_bytes, + load_der_bytes_from_der, load_der_bytes_from_pem, load_der_bytes_from_pem_or_der_bytes, + load_der_bytes_from_pem_or_der_file, providers::CryptoExt, + }; + + #[expect(clippy::unnecessary_wraps, reason = "pymodule hook expects PyResult")] + pub(crate) fn module_exec(vm: &VirtualMachine, module: &Py) -> PyResult<()> { + __module_exec(vm, module); + vm.register_module_loaded_hook("ssl", patch_ssl_module); + Ok(()) + } + + fn patch_ssl_module(vm: &VirtualMachine, module: PyObjectRef) -> PyResult<()> { + const CHANNEL_BINDING_TYPES: &str = "CHANNEL_BINDING_TYPES"; + // Check that it already exists and is a list. + let _ = PyListRef::try_from_object(vm, module.get_attr(CHANNEL_BINDING_TYPES, vm)?)?; + // rustls does not support OpenSSL's channel bindings. + // See SSLSocket::get_channel_binding() for details. + module.set_attr(CHANNEL_BINDING_TYPES, vm.ctx.new_list(vec![]), vm) + } + + // Constants matching Python ssl module + + // SSL/TLS Protocol versions + #[pyattr] + const PROTOCOL_TLS: i32 = 2; // Auto-negotiate best version + #[pyattr] + const PROTOCOL_SSLv23: i32 = PROTOCOL_TLS; + #[pyattr] + const PROTOCOL_TLS_CLIENT: i32 = 16; + #[pyattr] + const PROTOCOL_TLS_SERVER: i32 = 17; + + // 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; + #[pyattr] + const PROTOCOL_TLSv1_1: i32 = 4; + #[pyattr] + const PROTOCOL_TLSv1_2: i32 = 5; + #[pyattr] + const PROTOCOL_TLSv1_3: i32 = 6; + + // Protocol version constants for TLSVersion enum + #[pyattr] + const PROTO_SSLv3: i32 = 0x0300; + #[pyattr] + const PROTO_TLSv1: i32 = 0x0301; + #[pyattr] + const PROTO_TLSv1_1: i32 = 0x0302; + #[pyattr] + const PROTO_TLSv1_2: i32 = 0x0303; + #[pyattr] + const PROTO_TLSv1_3: i32 = 0x0304; + + // Minimum and maximum supported protocol versions for rustls + #[pyattr] + const PROTO_MINIMUM_SUPPORTED: i32 = -2; // special value + #[pyattr] + const PROTO_MAXIMUM_SUPPORTED: i32 = -1; // special value + + // Certificate verification modes + #[pyattr] + const CERT_NONE: i32 = 0; + #[pyattr] + const CERT_OPTIONAL: i32 = 1; + #[pyattr] + const CERT_REQUIRED: i32 = 2; + + // SSL Verification Flags / Certificate requirements + #[pyattr] + const VERIFY_DEFAULT: i32 = 0x00000000; + #[pyattr] + const VERIFY_CRL_CHECK_LEAF: i32 = super::VERIFY_CRL_CHECK_LEAF; + #[pyattr] + const VERIFY_CRL_CHECK_CHAIN: i32 = super::VERIFY_CRL_CHECK_CHAIN; + // rustls strictly verifies certificates by default but does not do some checks that + // OpenSSL does in this mode (Authority Key Identifier verification, for example). + // We have to ignore this because a lot of clients set this by default. + #[pyattr] + const VERIFY_X509_STRICT: i32 = 0x00000000; + #[pyattr] + const VERIFY_ALLOW_PROXY_CERTS: i32 = 0x00000000; // not supported by rustls + #[pyattr] + const VERIFY_X509_TRUSTED_FIRST: i32 = 0x00000000; // this is the default behaviour and is not configurable in rustls + #[pyattr] + const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x00000000; // not supported by rustls + + // Options (OpenSSL-compatible flags, mostly no-op in rustls) + #[pyattr] + const OP_NO_SSLv2: i32 = 0x00000000; // rustls does not support SSLv2.0 + #[pyattr] + const OP_NO_SSLv3: i32 = 0x00000000; // rustls does not support SSLv3.0 + #[pyattr] + const OP_NO_TLSv1: i32 = 0x00000000; // rustls does not support TLSv1.0 + #[pyattr] + const OP_NO_TLSv1_1: i32 = 0x00000000; // rustls does not support TLSv1.1 + #[pyattr] + const OP_NO_TLSv1_2: i32 = 0x08000000; + #[pyattr] + const OP_NO_TLSv1_3: i32 = 0x20000000; + #[pyattr] + const OP_NO_COMPRESSION: i32 = 0x00000000; // rustls does not support compression + #[pyattr] + const OP_CIPHER_SERVER_PREFERENCE: i32 = 0x00400000; + #[pyattr] + const OP_SINGLE_DH_USE: i32 = 0x00000000; // rustls does not support Diffie-Hellman key exchange + #[pyattr] + const OP_SINGLE_ECDH_USE: i32 = 0x00000000; // rustls does not reuse ECDHE keys by default + #[pyattr] + const OP_NO_TICKET: i32 = 0x00004000; + #[pyattr] + const OP_LEGACY_SERVER_CONNECT: i32 = 0x00000000; // rustls does not support this + #[pyattr] + const OP_NO_RENEGOTIATION: i32 = 0x00000000; // rustls does not support renegotiation + // TODO: Should be easy to support. But it lowers security and we might just ignore it. + #[pyattr] + const OP_IGNORE_UNEXPECTED_EOF: i32 = 0x00000000; + #[pyattr] + const OP_ENABLE_MIDDLEBOX_COMPAT: i32 = 0x00000000; // rustls does not support this + // Reflect what rustls supports + #[pyattr] + // | OP_NO_SSLv3 | OP_ENABLE_MIDDLEBOX_COMPAT + const OP_ALL: i32 = OP_CIPHER_SERVER_PREFERENCE; + + // Alert types (matching _TLSAlertType enum) + #[pyattr] + const ALERT_DESCRIPTION_CLOSE_NOTIFY: i32 = 0; + #[pyattr] + const ALERT_DESCRIPTION_UNEXPECTED_MESSAGE: i32 = 10; + #[pyattr] + const ALERT_DESCRIPTION_BAD_RECORD_MAC: i32 = 20; + #[pyattr] + const ALERT_DESCRIPTION_DECRYPTION_FAILED: i32 = 21; + #[pyattr] + const ALERT_DESCRIPTION_RECORD_OVERFLOW: i32 = 22; + #[pyattr] + const ALERT_DESCRIPTION_DECOMPRESSION_FAILURE: i32 = 30; + #[pyattr] + const ALERT_DESCRIPTION_HANDSHAKE_FAILURE: i32 = 40; + #[pyattr] + const ALERT_DESCRIPTION_NO_CERTIFICATE: i32 = 41; + #[pyattr] + const ALERT_DESCRIPTION_BAD_CERTIFICATE: i32 = 42; + #[pyattr] + const ALERT_DESCRIPTION_UNSUPPORTED_CERTIFICATE: i32 = 43; + #[pyattr] + const ALERT_DESCRIPTION_CERTIFICATE_REVOKED: i32 = 44; + #[pyattr] + const ALERT_DESCRIPTION_CERTIFICATE_EXPIRED: i32 = 45; + #[pyattr] + const ALERT_DESCRIPTION_CERTIFICATE_UNKNOWN: i32 = 46; + #[pyattr] + const ALERT_DESCRIPTION_ILLEGAL_PARAMETER: i32 = 47; + #[pyattr] + const ALERT_DESCRIPTION_UNKNOWN_CA: i32 = 48; + #[pyattr] + const ALERT_DESCRIPTION_ACCESS_DENIED: i32 = 49; + #[pyattr] + const ALERT_DESCRIPTION_DECODE_ERROR: i32 = 50; + #[pyattr] + const ALERT_DESCRIPTION_DECRYPT_ERROR: i32 = 51; + #[pyattr] + const ALERT_DESCRIPTION_EXPORT_RESTRICTION: i32 = 60; + #[pyattr] + const ALERT_DESCRIPTION_PROTOCOL_VERSION: i32 = 70; + #[pyattr] + const ALERT_DESCRIPTION_INSUFFICIENT_SECURITY: i32 = 71; + #[pyattr] + const ALERT_DESCRIPTION_INTERNAL_ERROR: i32 = 80; + #[pyattr] + const ALERT_DESCRIPTION_INAPPROPRIATE_FALLBACK: i32 = 86; + #[pyattr] + const ALERT_DESCRIPTION_USER_CANCELLED: i32 = 90; + #[pyattr] + const ALERT_DESCRIPTION_NO_RENEGOTIATION: i32 = 100; + #[pyattr] + const ALERT_DESCRIPTION_MISSING_EXTENSION: i32 = 109; + #[pyattr] + const ALERT_DESCRIPTION_UNSUPPORTED_EXTENSION: i32 = 110; + #[pyattr] + const ALERT_DESCRIPTION_CERTIFICATE_UNOBTAINABLE: i32 = 111; + #[pyattr] + const ALERT_DESCRIPTION_UNRECOGNIZED_NAME: i32 = 112; + #[pyattr] + const ALERT_DESCRIPTION_BAD_CERTIFICATE_STATUS_RESPONSE: i32 = 113; + #[pyattr] + const ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE: i32 = 114; + #[pyattr] + const ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY: i32 = 115; + #[pyattr] + const ALERT_DESCRIPTION_CERTIFICATE_REQUIRED: i32 = 116; + #[pyattr] + const ALERT_DESCRIPTION_NO_APPLICATION_PROTOCOL: i32 = 120; + + // Version info - reporting as OpenSSL 3.3.0 for compatibility + #[pyattr] + const OPENSSL_VERSION_NUMBER: i32 = 0x30300000; // OpenSSL 3.3.0 + // TODO: Add version of rustls, used cryptography provider and enabled features here. + #[pyattr] + const OPENSSL_VERSION: &str = "OpenSSL 3.3.0 (rustls)"; + #[pyattr] + const OPENSSL_VERSION_INFO: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); // 3.3.0 release + #[pyattr] + const _OPENSSL_API_VERSION: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); // 3.3.0 release + + #[pyattr(once)] + fn _DEFAULT_CIPHERS(_vm: &VirtualMachine) -> String { + CIPHER_MAPPINGS + .default + .iter() + .map(|id| CIPHER_MAPPINGS.id_to_openssl[id]) + .join(":") + } + + // Has features + #[pyattr] + const HAS_SNI: bool = true; + #[pyattr] + const HAS_TLS_UNIQUE: bool = false; // Not supported in rustls + #[pyattr] + const HAS_ECDH: bool = true; + #[pyattr] + const HAS_NPN: bool = false; // Deprecated, not supported in rustls use ALPN + #[pyattr] + const HAS_ALPN: bool = true; + #[pyattr] + const HAS_PSK: bool = false; // PSK not supported in rustls + #[pyattr] + const HAS_SSLv2: bool = false; // Not supported in rustls for security + #[pyattr] + const HAS_SSLv3: bool = false; // Not supported in rustls for security + #[pyattr] + const HAS_TLSv1: bool = false; // Not supported in rustls for security + #[pyattr] + const HAS_TLSv1_1: bool = false; // Not supported in rustls for security + #[pyattr] + const HAS_TLSv1_2: bool = true; + #[pyattr] + const HAS_TLSv1_3: bool = true; + #[pyattr] + const HAS_PHA: bool = false; // Post-Handshake Auth not supported in rustls + + // Encoding constants (matching OpenSSL) + #[pyattr] + const ENCODING_PEM: i32 = 1; + #[pyattr] + const ENCODING_DER: i32 = 2; + + #[pyattr] + const HOSTFLAG_NEVER_CHECK_SUBJECT: i32 = 0x00000001; // rustls always uses alt names to check server name + + // Matches recent versions of OpenSSL; + const SSL_SESSION_CACHE_MAX_SIZE_DEFAULT: usize = 1024 * 10; + + // _SSLContext - manages TLS configuration + #[pyattr] + #[pyclass(module = "_ssl", name = "_SSLContext")] + #[derive(Debug, PyPayload)] + struct PySSLContext { + protocol: i32, + ciphers: PyRwLock>>, + options: AtomicI32, + ecdh_curve: PyRwLock>>, + verify_mode: AtomicI32, + check_hostname: AtomicBool, + verify_flags: AtomicI32, + num_tickets: AtomicUsize, + minimum_version: AtomicI32, + maximum_version: AtomicI32, + use_system_certificates: AtomicBool, + alpn_protocols: PyRwLock>>, + sni_callback: PyRwLock, + msg_callback: PyRwLock, + cert_chain: PyRwLock>, PrivateKeyDer<'static>)>>, + stats: Arc, + cert_store: PyRwLock, + post_handshake_auth: AtomicBool, + session_cache: Resumption, + host_flags: AtomicI32, + } + + impl Representable for PySSLContext { + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(format!("", zelf.protocol)) + } + } + + impl Constructor for PySSLContext { + type Args = (i32,); + + fn py_new( + _cls: &Py, + (protocol,): Self::Args, + vm: &VirtualMachine, + ) -> PyResult { + // Validate protocol + if !matches!( + protocol, + PROTOCOL_TLS_CLIENT + | PROTOCOL_TLS_SERVER + | PROTOCOL_TLS + | PROTOCOL_TLSv1_2 + | PROTOCOL_TLSv1_3 + ) { + return Err( + vm.new_value_error(format!("protocol {protocol} is not supported by rustls")) + ); + } + + let client_protocol = protocol == PROTOCOL_TLS_CLIENT; + let (minimum_version, maximum_version) = match protocol { + PROTOCOL_TLSv1_2 => (PROTO_TLSv1_2, PROTO_TLSv1_2), + PROTOCOL_TLSv1_3 => (PROTO_TLSv1_3, PROTO_TLSv1_3), + _ => (PROTO_TLSv1_2, PROTO_TLSv1_3), + }; + let stats = Arc::new(Stats::default()); + + Ok(Self { + protocol, + ciphers: PyRwLock::new(( + CryptoExt::get_ext().default_ciphers_or_provider().to_vec(), + None, + )), + options: AtomicI32::new(OP_ALL), + ecdh_curve: PyRwLock::new(None), + + verify_mode: AtomicI32::new(if client_protocol { + CERT_REQUIRED + } else { + CERT_NONE + }), + + check_hostname: AtomicBool::new(client_protocol), + verify_flags: AtomicI32::new(VERIFY_DEFAULT), + num_tickets: AtomicUsize::new(2), + minimum_version: AtomicI32::new(minimum_version), + maximum_version: AtomicI32::new(maximum_version), + use_system_certificates: AtomicBool::new(false), + alpn_protocols: PyRwLock::new(vec![]), + sni_callback: PyRwLock::new(vm.ctx.none()), + msg_callback: PyRwLock::new(vm.ctx.none()), + cert_chain: PyRwLock::new(Vec::new()), + stats: stats.clone(), + cert_store: PyRwLock::new(CertStore::empty(stats)), + post_handshake_auth: AtomicBool::new(false), + session_cache: Resumption::in_memory_sessions(SSL_SESSION_CACHE_MAX_SIZE_DEFAULT), + host_flags: AtomicI32::new(0), + }) + } + } + + #[pyclass(with(Constructor, Representable), flags(BASETYPE))] + impl PySSLContext { + #[pygetset] + fn protocol(&self) -> i32 { + self.protocol + } + + #[pymethod] + fn set_ciphers(&self, ciphers: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<()> { + let mut ciphers = CipherList::parse_to_rustls(ciphers.as_str()).map_err(|_| { + SslError::Ssl("No cipher can be selected".to_string()).into_py_err(vm) + })?; + + // TLS 1.3 cipher suites cannot be disabled with set_ciphers(). + for cipher in CryptoExt::get_ext() + .default_ciphers_or_provider() + .iter() + .rev() + { + if cipher.tls13().is_some() && !ciphers.0.contains(cipher) { + // We assume that TLS 1.3 is the most secure thing possible so it should be preferred. + ciphers.0.insert(0, *cipher); + } + } + + *self.ciphers.write() = ciphers; + Ok(()) + } + + #[pymethod] + fn get_ciphers(&self, vm: &VirtualMachine) -> PyResult> { + self.ciphers + .read() + .0 + .iter() + .map(CipherDescriptionDict::new) + .map(|c| vm.with_serde(|s| c.serialize(s))) + .collect::>() + } + + #[pygetset(setter)] + fn set_options(&self, value: i32, vm: &VirtualMachine) -> PyResult<()> { + if value < 0 { + return Err(vm.new_value_error("options must be non-negative")); + } + + const DEPRECATED_OPS: i32 = OP_NO_SSLv2 + | OP_NO_SSLv3 + | OP_NO_TLSv1 + | OP_NO_TLSv1_1 + | OP_NO_TLSv1_2 + | OP_NO_TLSv1_3; + if (value & DEPRECATED_OPS) != 0 { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + "ssl.OP_NO_* options are deprecated".to_string(), + 2, + vm, + )?; + } + + self.options.store(value, Ordering::Relaxed); + Ok(()) + } + + #[pygetset] + fn options(&self) -> i32 { + self.options.load(Ordering::Relaxed) + } + + #[pymethod] + fn set_ecdh_curve(&self, name: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let curve_name = if let Ok(s) = PyUtf8StrRef::try_from_object(vm, name.clone()) { + s.as_str().to_owned() + } else if let Ok(b) = ArgBytesLike::try_from_object(vm, name) { + String::from_utf8(b.borrow_buf().to_vec()) + .map_err(|_| vm.new_value_error("Invalid curve name encoding"))? + } else { + return Err(vm.new_type_error("ECDH curve name must be str or bytes")); + }; + + if let Some(ecdh_curve) = CIPHER_MAPPINGS.name_to_kx_group.get(&curve_name) { + *self.ecdh_curve.write() = Some(vec![*ecdh_curve]); + Ok(()) + } else { + Err(vm.new_value_error(format!("unknown curve name '{curve_name}'"))) + } + } + + #[pygetset(setter)] + fn set_verify_mode(&self, mode: i32, vm: &VirtualMachine) -> PyResult<()> { + if ![CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED].contains(&mode) { + return Err(vm.new_value_error("invalid verify mode")); + } + // Cannot set CERT_NONE when check_hostname is enabled + if mode == CERT_NONE && self.check_hostname.load(Ordering::Relaxed) { + return Err(vm.new_value_error( + "Cannot set verify_mode to CERT_NONE when check_hostname is enabled", + )); + } + self.verify_mode.store(mode, Ordering::Relaxed); + Ok(()) + } + + #[pygetset] + fn verify_mode(&self) -> i32 { + self.verify_mode.load(Ordering::Relaxed) + } + + #[pygetset(setter)] + fn set_check_hostname(&self, value: bool) { + // When check_hostname is enabled, ensure verify_mode is at least CERT_REQUIRED + if value { + let _ = self.verify_mode.compare_exchange( + CERT_NONE, + CERT_REQUIRED, + Ordering::Relaxed, + Ordering::Relaxed, + ); + } + self.check_hostname.store(value, Ordering::Relaxed); + } + + #[pygetset] + fn check_hostname(&self) -> bool { + self.check_hostname.load(Ordering::Relaxed) + } + + #[pygetset(setter)] + fn set_num_tickets(&self, value: isize, vm: &VirtualMachine) -> PyResult<()> { + let value = value + .try_into() + .map_err(|_| vm.new_value_error(format!("num_tickets is out of range: {value}")))?; + + if self.protocol != PROTOCOL_TLS_SERVER { + return Err( + vm.new_value_error("num_tickets can only be set on server-side contexts") + ); + } + self.num_tickets.store(value, Ordering::Relaxed); + Ok(()) + } + + #[pygetset] + fn num_tickets(&self) -> usize { + self.num_tickets.load(Ordering::Relaxed) + } + + #[pygetset] + fn minimum_version(&self) -> i32 { + self.minimum_version.load(Ordering::Relaxed) + } + + #[pygetset(setter)] + fn set_minimum_version(&self, mut value: i32, vm: &VirtualMachine) -> PyResult<()> { + value = Self::sanitize_version(value, vm)?; + if value > self.maximum_version.load(Ordering::Relaxed) { + Err(vm.new_value_error( + "new SSLContext.minimum_version is greater than SSLContext.maximum_version", + )) + } else { + self.minimum_version.store(value, Ordering::Relaxed); + Ok(()) + } + } + + #[pygetset] + fn maximum_version(&self) -> i32 { + self.maximum_version.load(Ordering::Relaxed) + } + + #[pygetset(setter)] + fn set_maximum_version(&self, mut value: i32, vm: &VirtualMachine) -> PyResult<()> { + value = Self::sanitize_version(value, vm)?; + if value < self.minimum_version.load(Ordering::Relaxed) { + Err(vm.new_value_error( + "new SSLContext.maximum_version is less than SSLContext.minimum_version", + )) + } else { + self.maximum_version.store(value, Ordering::Relaxed); + Ok(()) + } + } + + fn sanitize_version(mut value: i32, vm: &VirtualMachine) -> PyResult { + if ![ + PROTO_MINIMUM_SUPPORTED, + PROTO_MAXIMUM_SUPPORTED, + PROTO_SSLv3, + PROTO_TLSv1, + PROTO_TLSv1_1, + PROTO_TLSv1_2, + PROTO_TLSv1_3, + ] + .contains(&value) + { + return Err(vm.new_value_error(format!("invalid protocol version: {value}"))); + } + + if value == PROTO_MINIMUM_SUPPORTED { + value = PROTO_TLSv1_2; + } else if value == PROTO_MAXIMUM_SUPPORTED { + value = PROTO_TLSv1_3; + } + + if ![PROTO_TLSv1_2, PROTO_TLSv1_3].contains(&value) { + return Err(vm.new_value_error( + "rustls only supports ssl.TLSVersion.TLSv1_2 and ssl.TLSVersion.TLSv1_3", + )); + } + + Ok(value) + } + + #[pymethod] + fn set_default_verify_paths(&self, vm: &VirtualMachine) -> PyResult<()> { + // Check for environment variable overrides. + // Needs to be done from inside Python in a case if environment is only modified there. + let os_module = vm.import("os", 0)?; + let environ = os_module.get_attr("environ", vm)?; + + let cafile = self.get_env(&environ, CERT_FILE_ENV, vm)?; + let capath = self.get_env(&environ, CERT_DIR_ENV, vm)?; + + if cafile.is_some() || capath.is_some() { + // Load certificates and certificate revocation lists from specified paths. + let has_cafile = cafile.is_some(); + let args = LoadVerifyLocationsArgs { + cafile, + capath: if has_cafile { None } else { capath }, + cadata: OptionalArg::Missing, + }; + self.load_verify_locations(args, vm)?; + } else { + // Enable system verifier only if we do not have env vars set. + self.use_system_certificates.store(true, Ordering::Relaxed); + } + + Ok(()) + } + + fn get_env( + &self, + environ: &PyObjectRef, + name: &str, + vm: &VirtualMachine, + ) -> PyResult> { + let res = environ.get_item(name, vm); + match res { + Ok(obj) => FsPath::try_from_object(vm, obj).map(Some), + Err(err) if err.fast_isinstance(vm.ctx.exceptions.key_error) => Ok(None), + Err(err) => Err(err), + } + } + + #[pymethod] + fn _set_alpn_protocols(&self, protos: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { + use std::io::Read; + + let bytes = protos.borrow_buf(); + let mut bytes: &[u8] = &bytes; + + let mut alpn_protocols = Vec::new(); + while !bytes.is_empty() { + let mut len = 0; + bytes + .read_exact(slice::from_mut(&mut len)) + .expect("BUG: Impossible"); + + if len == 0 { + return Err(vm.new_value_error( + "Invalid ALPN protocol data: protocol length cannot be 0", + )); + } + + let mut protocol = vec![0; len.into()]; + bytes.read_exact(&mut protocol).map_err(|_| { + vm.new_value_error( + "Invalid ALPN protocol data: not enough bytes to read protocol", + ) + })?; + + alpn_protocols.push(protocol); + } + + *self.alpn_protocols.write() = alpn_protocols; + Ok(()) + } + + #[pymethod] + fn cert_store_stats(&self, vm: &VirtualMachine) -> PyResult { + vm.with_serde(|s| self.stats.cert_store.serialize(s)) + } + + #[pymethod] + fn session_stats(&self, vm: &VirtualMachine) -> PyResult { + vm.with_serde(|s| self.stats.session.serialize(s)) + } + + #[pygetset(setter)] + fn set_sni_callback(&self, callback: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + if !vm.is_none(&callback) && !callback.is_callable() { + return Err(vm.new_type_error("sni_callback must be callable or None")); + } + *self.sni_callback.write() = callback; + Ok(()) + } + + #[pygetset] + fn sni_callback(&self) -> PyObjectRef { + self.sni_callback.read().clone() + } + + #[pygetset] + fn security_level(&self) -> usize { + let min_bits = self + .ciphers + .read() + .0 + .iter() + .map(|c| u16::from(c.suite())) + .map(|i| CIPHER_MAPPINGS.id_to_bits[&i]) + .min() + .expect("BUG: Impossible"); + for (level, required_bits) in SECURITY_LEVEL_TO_MIN_BITS.iter().enumerate().rev() { + if min_bits >= *required_bits { + return level; + } + } + unreachable!("BUG: Impossible") + } + + #[pymethod] + fn load_cert_chain(&self, args: LoadCertChainArgs, vm: &VirtualMachine) -> PyResult<()> { + let mut password = Password::new(args.password, vm)?; + + let mut priv_key = if let Some(keyfile) = args.keyfile { + let keyfile_path = keyfile.to_path_buf(vm)?; + let keyfile_str = keyfile.to_string_lossy(); + let ders = load_der_bytes_from_pem_or_der_file( + keyfile_path, + &[DerKind::Key], + &mut password, + vm, + ) + .map_err(|e| e.into_py_err(vm))?; + let der = + ensure_single_der_bytes(&keyfile_str, ders).map_err(|e| e.into_py_err(vm))?; + Some(der.bytes) + } else { + None + }; + + let kinds = if priv_key.is_some() { + &[DerKind::Cert][..] + } else { + &[DerKind::Cert, DerKind::Key][..] + }; + + let certfile_path = args.certfile.to_path_buf(vm)?; + let ders = load_der_bytes_from_pem_or_der_file(certfile_path, kinds, &mut password, vm) + .map_err(|e| e.into_py_err(vm))?; + let mut certs = Vec::with_capacity(ders.len()); + for der in ders { + if der.kind == DerKind::Cert { + certs.push(der.bytes); + } else { + // Private key + if priv_key.is_some() { + return Err(vm.new_value_error("more than one private key found")); + } + priv_key = Some(der.bytes); + } + } + + let priv_key = + priv_key.ok_or_else(|| SslError::Ssl("PEM lib".to_string()).into_py_err(vm))?; + + // Check that certificate matches the private key (if any). + let first = certs + .first() + .ok_or_else(|| SslError::Ssl("PEM lib".to_string()).into_py_err(vm))?; + let (_, first) = parse_x509_certificate(first).map_err(|e| { + vm.new_value_error(format!("failed to parse first certificate from chain: {e}")) + })?; + + // Try to get public key. + let private_key_der: PrivateKeyDer<'_> = priv_key + .as_slice() + .try_into() + .map_err(|e| vm.new_value_error(format!("failed to parse private key: {e}")))?; + let sign_key = CryptoExt::get_ext() + .any_supported_key(&private_key_der) + .map_err(|e| vm.new_value_error(format!("failed to parse private key: {e}")))?; + let pub_key = sign_key + .public_key() + .ok_or_else(|| vm.new_value_error("can not get public key"))?; + + if first.tbs_certificate.public_key().raw != pub_key.as_ref() { + return Err(SslError::Ssl("KEY_VALUES_MISMATCH".to_string()).into_py_err(vm)); + } + + // Check remaining certificates. + for cert in &certs[1..] { + let _ = parse_x509_certificate(cert).map_err(|e| { + vm.new_value_error(format!("failed to parse certificate from chain: {e}")) + })?; + } + + self.cert_chain.write().push(( + certs.into_iter().map(Into::into).collect(), + priv_key.try_into().map_err(|e| { + vm.new_value_error(format!("failed to prepare private key: {e}")) + })?, + )); + Ok(()) + } + + #[pymethod] + fn load_verify_locations( + &self, + args: LoadVerifyLocationsArgs, + vm: &VirtualMachine, + ) -> PyResult<()> { + let cafile = args.cafile; + let capath = args.capath; + let cadata = args.cadata.flatten(); + + if cafile.is_none() && capath.is_none() && cadata.is_none() { + return Err(vm.new_type_error("cafile, capath and cadata cannot be all omitted")); + } + + // Load from cafile + if let Some(cafile) = cafile { + let ders = load_der_bytes_from_pem_or_der_file( + cafile.to_path_buf(vm)?, + &[DerKind::Cert, DerKind::Crl], + &mut Password::None, + vm, + ) + .map_err(|e| e.into_py_err(vm))?; + self.cert_store.write().add_ders(&ders); + } + + // Load from capath + if let Some(capath) = capath { + let capath = capath.to_path_buf(vm)?; + let paths = vm + .allow_threads(|| rustpython_host_env::fs::read_dir(capath)) + .map_err(|e| e.into_pyexception(vm))?; + for path in paths { + let path = path.map_err(|e| e.into_pyexception(vm))?; + if !path.path().is_file() { + continue; + } + + let ders = load_der_bytes_from_pem_or_der_file( + path.path(), + &[DerKind::Cert, DerKind::Crl], + &mut Password::None, + vm, + ) + .map_err(|e| e.into_py_err(vm))?; + self.cert_store.write().add_ders(&ders); + } + } + + // Load from cadata + if let Some(cadata) = cadata { + let (bytes, is_pem_text) = match cadata { + Either::A(d) => (d.as_bytes().to_vec(), true), + Either::B(d) => (d.borrow_buf().to_vec(), false), + }; + + let ders = if is_pem_text { + let (ders, _) = load_der_bytes_from_pem( + "", + &bytes, + &[DerKind::Cert, DerKind::Crl], + &mut Password::None, + vm, + ) + .map_err(|e| e.into_py_err(vm))?; + + if ders.is_empty() && !bytes.is_empty() { + return Err(SslError::CadataNoStartLine.into_py_err(vm)); + } + + ders + } else { + load_der_bytes_from_der( + "", + &bytes, + &[DerKind::Cert, DerKind::Crl], + &mut Password::None, + vm, + ) + .map_err(|_| SslError::CadataNotEnoughData.into_py_err(vm))? + }; + + self.cert_store.write().add_ders(&ders); + } + + Ok(()) + } + + #[pymethod] + fn get_ca_certs(&self, args: GetCertArgs, vm: &VirtualMachine) -> PyResult { + let binary_form = if let OptionalArg::Present(binary_form) = args.binary_form { + binary_form + } else { + false + }; + + let cert_store = self.cert_store.read(); + let mut list = Vec::::with_capacity(cert_store.all_certs().len()); + if binary_form { + for cert in cert_store.all_certs() { + list.push(vm.ctx.new_bytes(cert.clone()).into()); + } + } else { + for cert in cert_store.all_certs() { + list.push(CertInfo::parse_to_py(cert, vm)?); + } + } + Ok(vm.ctx.new_list(list)) + } + + #[pymethod] + fn _wrap_socket( + zelf: PyRef, + args: WrapSocketArgs, + vm: &VirtualMachine, + ) -> PyResult> { + let io = Io::from_socket(args.sock, vm)?; + Self::create_socket( + zelf, + io, + args.server_side, + args.server_hostname, + args.owner, + args.session, + vm, + ) + } + + #[pymethod] + fn _wrap_bio( + zelf: PyRef, + args: WrapBioArgs, + vm: &VirtualMachine, + ) -> PyResult> { + let io = Io::from_bio(args.incoming, args.outgoing); + Self::create_socket( + zelf, + io, + args.server_side, + args.server_hostname, + args.owner, + args.session, + vm, + ) + } + + fn create_socket( + zelf: PyRef, + io: Io, + server_side: OptionalArg, + server_hostname: OptionalArg>, + owner: PyObjectRef, + session: OptionalArg>, + vm: &VirtualMachine, + ) -> PyResult> { + let server_side = server_side.unwrap_or(false); + let server_hostname = server_hostname + .into_option() + .flatten() + .map(|h| h.to_string()); + let owner = owner.downgrade(None, vm)?; + + if server_side && zelf.protocol == PROTOCOL_TLS_CLIENT { + return Err(SslError::Ssl( + "Cannot create a server socket with a PROTOCOL_TLS_CLIENT context".to_string(), + ) + .into_py_err(vm)); + } + if !server_side && zelf.protocol == PROTOCOL_TLS_SERVER { + return Err(SslError::Ssl( + "Cannot create a client socket with a PROTOCOL_TLS_SERVER context".to_string(), + ) + .into_py_err(vm)); + } + + let state = if server_side { + State::new_handshaking_server() + } else { + if server_hostname.as_ref().is_some_and(|h| h.contains('\0')) { + return Err(vm.new_type_error("server_hostname cannot contain null bytes")); + } + let server_hostname = server_hostname + .as_ref() + .map(|h| ServerName::try_from(h.as_str())) + .transpose() + .map_err(|e| vm.new_value_error(format!("Invalid server name: {e}")))? + .map(|h| h.to_owned()); + State::new_handshaking_client(Self::create_client_connection( + &zelf, + server_hostname, + vm, + )?) + }; + + let socket = PySSLSocket { + context: PyRwLock::new(zelf), + owner, + io: PyRwLock::new(io), + server_side, + server_hostname: PyRwLock::new(server_hostname), + state: PyRwLock::new(state), + shared_ciphers: PyRwLock::new(None), + }; + + // TODO: Implement session support. + if let Some(session) = session.into_option().flatten() { + socket.set_session(session, vm)?; + } + + socket + .into_ref_with_type(vm, vm.class("_ssl", "_SSLSocket")) + .map_err(|_| vm.new_type_error("Failed to create SSLSocket")) + } + + fn create_client_connection( + &self, + server_name: Option>, + vm: &VirtualMachine, + ) -> PyResult { + let crypto = self.create_crypto_provider(); + + // Certificate verifier. + let use_system_certificates = self.use_system_certificates.load(Ordering::Relaxed); + let crl_check = CrlCheck::from_verify_flags(self.verify_flags()); + if use_system_certificates && !matches!(crl_check, CrlCheck::None) { + _warnings::warn( + vm.ctx.exceptions.runtime_warning, + "rustls default platform verifier does not support disabling ssl.VERIFY_CRL_CHECK_*".to_owned(), + 2, + vm, + )?; + }; + + let verifier = CustomServerCertVerifier::new( + self.verify_mode() == CERT_REQUIRED, + use_system_certificates, + &self.cert_store.read(), + crypto.clone(), + self.check_hostname(), + crl_check, + ) + .map_err(|e| e.into_py_err(vm))?; + + // Client configuration. + let config = ClientConfig::builder_with_provider(crypto) + .with_protocol_versions(&self.get_supported_versions()) + .map_err(|e| vm.new_value_error(format!("failed to create rustls client: {e}")))? + .dangerous() + .with_custom_certificate_verifier(Arc::new(verifier)); + let mut config = if let Some((cert_chain, priv_key)) = self.cert_chain.read().last() { + config + .with_client_auth_cert(cert_chain.clone(), priv_key.clone_key()) + .map_err(|e| { + vm.new_value_error(format!("failed to set client certificate chain: {e}")) + })? + } else { + config.with_no_client_auth() + }; + + config.alpn_protocols = self.alpn_protocols.read().clone(); + config.resumption = self.session_cache.clone(); + + // Server name. + let server_name = if let Some(server_name) = server_name { + server_name + } else { + config.enable_sni = false; + // rustls always needs a ServerName, so provide it an invalid IPv4 address. + ServerName::IpAddress(IpAddr::V4(Ipv4Addr::from([0, 0, 0, 0]))) + }; + + Ok(ClientConnection::new(Arc::new(config), server_name) + .map_err(|e| SslError::from_rustls(e).into_py_err(vm))? + .into()) + } + + // PySSLSocket::do_handshake() calls this after receiving ClientHello with Listener. + fn create_server_connection( + &self, + accepted: Accepted, + vm: &VirtualMachine, + ) -> PyResult> { + let crypto = self.create_crypto_provider(); + + // Find certificate chain with a key matching algorithms requested by client. + // TODO: Search by a requested host name too. + let cert_chain = self.cert_chain.read(); + if cert_chain.is_empty() { + return Err(SslError::from_rustls(rustls::Error::PeerIncompatible( + rustls::PeerIncompatible::NoCipherSuitesInCommon, + )) + .into_py_err(vm)); + } + let (cert_chain, priv_key) = { + let client_hello = accepted.client_hello(); + let signature_schemes = client_hello.signature_schemes(); + cert_chain + .iter() + .find(|(cert_chain, priv_key)| { + CertifiedKey::from_der(cert_chain.clone(), priv_key.clone_key(), &crypto) + .is_ok_and(|certified_key| { + certified_key.key.choose_scheme(signature_schemes).is_some() + }) + }) + .unwrap_or_else(|| cert_chain.last().expect("BUG: Impossible")) + }; + + // Server configuration. + let mut config = ServerConfig::builder_with_provider(crypto.clone()) + .with_protocol_versions(&self.get_supported_versions()) + .map_err(|e| vm.new_value_error(format!("failed to create rustls server: {e}")))? + .with_client_cert_verifier(self.create_client_cert_verifier(crypto, vm)?) + .with_single_cert(cert_chain.clone(), priv_key.clone_key()) + .map_err(|e| { + vm.new_value_error(format!("failed to set server certificate chain: {e}")) + })?; + + // ALPN protocols. + let alpn_protocols = self.alpn_protocols.read(); + if accepted + .client_hello() + .alpn() + .is_some_and(|mut client_protocols| { + client_protocols.any(|client_protocol| { + alpn_protocols + .iter() + .any(|server_protocol| server_protocol.as_slice() == client_protocol) + }) + }) + { + // Configure acceptable ALPN protocols only if client's is supported one. + // This matches cpython's ssl behaviour that allows connections when server + // does not know protocol requested by client. + config.alpn_protocols = alpn_protocols.clone(); + } + + config.ignore_client_order = + self.is_one_of_options_enabled(OP_CIPHER_SERVER_PREFERENCE); + + if self.is_one_of_options_enabled(OP_NO_TICKET) || (self.num_tickets() == 0) { + config.send_tls13_tickets = 0; + } else { + config.ticketer = (CryptoExt::get_ext().ticketer)().map_err(|e| { + vm.new_value_error(format!("failed to create TLS ticketer: {e}")) + })?; + config.send_tls13_tickets = self.num_tickets(); + } + + Ok(match accepted.into_connection(Arc::new(config)) { + Ok(conn) => Ok(conn.into()), + Err((err, alert)) => Err((err, alert)), + }) + } + + fn create_crypto_provider(&self) -> Arc { + let mut provider = CryptoExt::get_provider().clone(); + + let suite_b = { + let ciphers = self.ciphers.read(); + provider.cipher_suites = ciphers.0.clone(); + if let Some(kx_groups) = ciphers.1.as_ref() { + provider.kx_groups = kx_groups.clone(); + true + } else { + false + } + }; + + { + let ecdh_curve = self.ecdh_curve.read(); + if !suite_b && let Some(ecdh_curve) = ecdh_curve.as_ref() { + provider.kx_groups = ecdh_curve.clone(); + } + } + + Arc::new(provider) + } + + fn get_supported_versions(&self) -> Vec<&'static SupportedProtocolVersion> { + let mut versions = + Vec::<&'static SupportedProtocolVersion>::with_capacity(ALL_VERSIONS.len()); + for version in ALL_VERSIONS { + let proto = u16::from(version.version).into(); + let add = match version.version { + ProtocolVersion::TLSv1_2 => { + self.proto_within_range(proto) + && !self.is_one_of_options_enabled(OP_NO_TLSv1_2) + } + + ProtocolVersion::TLSv1_3 => { + self.proto_within_range(proto) + && !self.is_one_of_options_enabled(OP_NO_TLSv1_3) + } + + _ => self.proto_within_range(proto), + }; + if add { + versions.push(version); + } + } + versions + } + + fn proto_within_range(&self, proto: i32) -> bool { + (self.minimum_version()..=self.maximum_version()).contains(&proto) + } + + fn create_client_cert_verifier( + &self, + crypto: Arc, + vm: &VirtualMachine, + ) -> PyResult> { + let verify_mode = self.verify_mode(); + if verify_mode == CERT_NONE { + Ok(Arc::new(NoClientAuth)) + } else { + let cert_store = self.cert_store.read(); + let builder = WebPkiClientVerifier::builder_with_provider( + Arc::new(cert_store.certs.clone()), + crypto, + ) + .with_crls(cert_store.crls.clone()); + if verify_mode == CERT_OPTIONAL { + builder.allow_unauthenticated().build() + } else { + builder.build() + } + .map_err(|e| { + SslError::Ssl(format!("failed to create client certificate verifier: {e}")) + .into_py_err(vm) + }) + } + } + + fn is_one_of_options_enabled(&self, op: i32) -> bool { + (self.options() & op) != 0 + } + + #[pygetset(setter)] + fn set_verify_flags(&self, value: i32) { + self.verify_flags.store(value, Ordering::Relaxed); + } + + #[pygetset] + fn verify_flags(&self) -> i32 { + self.verify_flags.load(Ordering::Relaxed) + } + + // Completely unsupported by rustls. + + #[pymethod] + fn load_dh_params(&self, _filepath: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + Err(SslError::PemLib( + "NO_START_LINE: ssl.SSLContext.load_dh_params is not supported by rustls" + .to_string(), + ) + .into_py_err(vm)) + } + + #[pygetset] + fn post_handshake_auth(&self) -> bool { + self.post_handshake_auth.load(Ordering::Relaxed) + } + + #[pygetset(setter)] + fn set_post_handshake_auth(&self, value: bool, vm: &VirtualMachine) -> PyResult<()> { + // Some libraries, like urllib.request, always set this to True for whatever reason. + self.post_handshake_auth.store(value, Ordering::Relaxed); + if value { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + "ssl.SSLContext.post_handshake_auth is not supported by rustls".to_string(), + 2, + vm, + )?; + } + Ok(()) + } + + #[pygetset(setter)] + fn set__msg_callback(&self, callback: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let is_none = vm.is_none(&callback); + if !is_none && !callback.is_callable() { + return Err(vm.new_type_error("msg_callback must be callable or None")); + } + + if !is_none { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + "rustls does not support SSLContext._msg_callback".to_string(), + 2, + vm, + )?; + } + + *self.msg_callback.write() = callback; + Ok(()) + } + + #[pygetset] + fn _msg_callback(&self) -> PyObjectRef { + self.msg_callback.read().clone() + } + + #[pygetset(setter)] + fn set__host_flags(&self, value: i32) { + self.host_flags.store(value, Ordering::Relaxed); + } + + #[pygetset] + fn _host_flags(&self) -> i32 { + self.host_flags.load(Ordering::Relaxed) + } + } + + #[derive(FromArgs)] + struct LoadCertChainArgs { + certfile: FsPath, + + #[pyarg(any, optional)] + keyfile: Option, + + #[pyarg(any, optional)] + password: OptionalArg, + } + + #[derive(FromArgs)] + struct LoadVerifyLocationsArgs { + #[pyarg(any, default)] + cafile: Option, + + #[pyarg(any, default)] + capath: Option, + + #[pyarg(any, optional, error_msg = "cadata should be a str or bytes")] + cadata: OptionalArg>>, + } + + #[derive(FromArgs)] + struct GetCertArgs { + #[pyarg(any, optional)] + binary_form: OptionalArg, + } + + #[derive(FromArgs)] + struct WrapSocketArgs { + sock: PyObjectRef, + #[pyarg(positional, optional)] + server_side: OptionalArg, + #[pyarg(positional, optional)] + server_hostname: OptionalArg>, + #[pyarg(named)] + owner: PyObjectRef, + #[pyarg(named, optional)] + session: OptionalArg>, + } + + #[derive(FromArgs)] + struct WrapBioArgs { + incoming: PyObjectRef, + outgoing: PyObjectRef, + #[pyarg(named, optional)] + server_side: OptionalArg, + #[pyarg(named, optional)] + server_hostname: OptionalArg>, + #[pyarg(named)] + owner: PyObjectRef, + #[pyarg(named, optional)] + session: OptionalArg>, + } + + // SSLSocket - represents a TLS-wrapped socket + #[pyattr] + #[pyclass(module = "_ssl", name = "_SSLSocket")] + #[derive(Debug, PyPayload)] + struct PySSLSocket { + context: PyRwLock>, + owner: PyRef, + io: PyRwLock, + server_side: bool, + server_hostname: PyRwLock>, + state: PyRwLock, + shared_ciphers: PyRwLock>>, + } + + impl Representable for PySSLSocket { + #[inline] + fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok("".to_owned()) + } + } + + impl Constructor for PySSLSocket { + type Args = (); + + fn slot_new(_cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error( + "Cannot directly instantiate SSLSocket, use SSLContext.wrap_socket()", + )) + } + + fn py_new(_cls: &Py, _args: Self::Args, vm: &VirtualMachine) -> PyResult { + Err(vm.new_not_implemented_error( + "Cannot directly instantiate SSLSocket, use SSLContext.wrap_socket()", + )) + } + } + + #[pyclass(with(Constructor, Representable), flags(BASETYPE))] + impl PySSLSocket { + #[pygetset(setter)] + fn set_context(&self, value: PyRef, _vm: &VirtualMachine) { + *self.context.write() = value; + } + + #[pygetset] + fn context(&self) -> PyRef { + self.context.read().clone() + } + + #[pygetset] + fn server_side(&self) -> bool { + self.server_side + } + + #[pygetset(setter)] + fn set_server_hostname(&self, value: Option) { + *self.server_hostname.write() = value.map(|s| s.to_string()); + } + + #[pygetset] + fn server_hostname(&self) -> Option { + self.server_hostname.read().clone() + } + + #[pymethod] + fn do_handshake(&self, vm: &VirtualMachine) -> PyResult<()> { + 'outer: loop { + let mut state = self.state.write(); + + match &mut *state { + State::ServerWaitingForClientHello(acceptor) => { + let accepted = loop { + { + let mut io = self.io.write(); + let _ = io.with_io(vm, |io| acceptor.read_tls(io))?; + } + + match acceptor.accept() { + Ok(Some(accepted)) => break accepted, + Ok(None) => {} + + Err((err, alert)) => { + *state = State::new_alert_from_rustls_error(err, alert, vm)?; + continue 'outer; + } + } + }; + + let context = self.context(); + let hello = accepted.client_hello(); + + // Remember shared cipher suites. + { + let our_ciphers = context.ciphers.read(); + *self.shared_ciphers.write() = Some( + hello + .cipher_suites() + .iter() + .filter_map(|c| { + our_ciphers + .0 + .iter() + .find(|oc| u16::from(*c) == u16::from(oc.suite())) + }) + .copied() + .collect(), + ); + } + + // Call SNI callback (if any). + let sni_callback = context.sni_callback(); + if !vm.is_none(&sni_callback) { + let owner = self.owner.upgrade().ok_or_else(|| { + vm.new_value_error( + "ssl.SSLSocket was dropped before _ssl._SSLSocket", + ) + })?; + let res = sni_callback.call((owner, hello.server_name(), context), vm); + + match res { + Ok(res) if vm.is_none(&res) => {} + + Ok(res) => match res.try_to_value::(vm) { + Ok(alert_code) + if (0..=u8::MAX as i32).contains(&alert_code) => + { + let error = SslError::Ssl( + "TLS connection rejected by SNI callback".to_string(), + ) + .into_py_err(vm); + + *state = State::new_alert_from_sni_callback_error( + error, + alert_code as u8, + ); + continue 'outer; + } + + _ => { + let type_error = vm.new_type_error(format!( + "servername callback must return None or an integer, not '{}'", + res.class().name() + )); + vm.run_unraisable(type_error, None, res); + let error = SslError::Ssl( + "SNI callback returned invalid value".to_string(), + ) + .into_py_err(vm); + + *state = State::new_alert_from_sni_callback_error( + error, + ALERT_DESCRIPTION_INTERNAL_ERROR as u8, + ); + continue 'outer; + } + }, + + Err(exc) => { + vm.run_unraisable(exc, None, vm.ctx.none()); + let error = SslError::Ssl( + "SNI callback raised an exception".to_string(), + ) + .into_py_err(vm); + *state = State::new_alert_from_sni_callback_error( + error, + ALERT_DESCRIPTION_HANDSHAKE_FAILURE as u8, + ); + continue 'outer; + } + } + }; + + // Create rustls connection. + let conn = + match self.context.read().create_server_connection(accepted, vm)? { + Ok(conn) => conn, + Err((err, alert)) => { + *state = State::new_alert_from_rustls_error(err, alert, vm)?; + continue; + } + }; + *state = State::HasConnection { + state: ConnectionState::Handshaking, + conn, + }; + let State::HasConnection { state, conn, .. } = &mut *state else { + unreachable!("BUG: Impossible") + }; + + self.complete_io(conn, true, vm)?; + *state = ConnectionState::Connected(CloseNotifyState::None); + break Ok(()); + } + + State::ServerSendingAlert { + error, + alert_buf, + alert_buf_pos, + } => { + let mut io = self.io.write(); + let sent = io.with_io(vm, |io| io.write(&alert_buf[*alert_buf_pos..]))?; + *alert_buf_pos += sent; + if *alert_buf_pos == alert_buf.len() { + break Err(error.clone()); + } + } + + State::HasConnection { + state: conn_state @ ConnectionState::Handshaking, + conn, + } => { + self.complete_io(conn, true, vm)?; + *conn_state = ConnectionState::Connected(CloseNotifyState::None); + break Ok(()); + } + + State::HasConnection { + state: + ConnectionState::Connected(_) + | ConnectionState::ShuttingDown + | ConnectionState::ShutDown, + .. + } => break Ok(()), // handshake already done + }; + } + } + + #[pymethod] + fn read( + &self, + len: isize, + buffer: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + // Ensure handshake done. + self.do_handshake(vm)?; + + // Prepare buffer. + let mut owned_buffer = buffer + .map(Either::A) + .unwrap_or_else(|| Either::B(Vec::new())); + + let read = { + let buffer_mut = match &mut owned_buffer { + Either::A(buffer) if len < 0 => &mut buffer.borrow_buf_mut()[..], + + Either::A(buffer) => { + let len = buffer.len().min(len as usize); + &mut buffer.borrow_buf_mut()[..len] + } + + Either::B(_) if len < 0 => return Err(vm.new_value_error("negative read len")), + + Either::B(buffer) => { + let len = len as usize; + buffer.resize(len, 0); + &mut buffer[..] + } + }; + + let mut state = self.state.write(); + self.read_inner(&mut state, buffer_mut, vm)? + }; + + if let Some(read) = read { + match owned_buffer { + Either::A(_) => Ok(vm.ctx.new_int(read).into()), + + Either::B(mut bytes) => { + bytes.truncate(read); + Ok(vm.ctx.new_bytes(bytes).into()) + } + } + } else { + // Close Notify already received. + match owned_buffer { + Either::A(_) => Ok(vm.ctx.new_int(0).into()), + Either::B(_) => Ok(vm.ctx.new_bytes(Vec::new()).into()), + } + } + } + + fn read_inner( + &self, + state: &mut State, + buffer: &mut [u8], + vm: &VirtualMachine, + ) -> PyResult> { + let (conn, conn_state) = match &mut *state { + State::ServerWaitingForClientHello(_) + | State::ServerSendingAlert { .. } + | State::HasConnection { + state: ConnectionState::Handshaking, + .. + } => { + unreachable!("BUG: read() is in wrong state") + } + + State::HasConnection { + state: + conn_state @ ConnectionState::Connected( + CloseNotifyState::None | CloseNotifyState::Sent, + ), + conn, + } => (conn, conn_state), + + State::HasConnection { + state: ConnectionState::Connected(CloseNotifyState::Received), + .. + } => { + return Ok(None); + } + + State::HasConnection { + state: ConnectionState::ShuttingDown | ConnectionState::ShutDown, + .. + } => { + return Err(SslError::ZeroReturn.into_py_err(vm)); + } + }; + + // Do the read. + loop { + match conn.reader().read(buffer) { + Ok(read) => { + if (read == 0) && !buffer.is_empty() { + // Close Notify received. + match conn_state { + ConnectionState::Connected(CloseNotifyState::None) => { + *conn_state = + ConnectionState::Connected(CloseNotifyState::Received) + } + + ConnectionState::Connected(CloseNotifyState::Sent) => { + // Sent + Received => shutdown almost complete, need to ensure that IO is done. + *conn_state = ConnectionState::ShuttingDown + } + + _ => { + unreachable!( + "BUG: Other ConnectionState variants handled earlier in read()" + ) + } + } + } + break Ok(Some(read)); + } + + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { + // There is no plaintext data in internal buffers, need to do IO. + self.complete_io(conn, true, vm)?; + } + + Err(err) => return Err(SslError::Io(err).into_py_err(vm)), + } + } + } + + #[pymethod] + fn write(&self, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + // Ensure handshake done. + self.do_handshake(vm)?; + + let mut state = self.state.write(); + let conn = match &mut *state { + State::ServerWaitingForClientHello(_) + | State::ServerSendingAlert { .. } + | State::HasConnection { + state: ConnectionState::Handshaking, + .. + } => { + unreachable!("BUG: write() is in wrong state") + } + + State::HasConnection { + state: + ConnectionState::Connected(CloseNotifyState::None | CloseNotifyState::Received), + conn, + } => conn, + + State::HasConnection { + state: + ConnectionState::Connected(CloseNotifyState::Sent) + | ConnectionState::ShuttingDown + | ConnectionState::ShutDown, + .. + } => { + return Err(SslError::ZeroReturn.into_py_err(vm)); + } + }; + + // Send previously queued data, if any. + self.complete_io(conn, false, vm)?; + + let data = data.borrow_buf(); + let written = conn + .writer() + .write(&data) + .map_err(|e| SslError::Io(e).into_py_err(vm))?; + + self.complete_io(conn, false, vm)?; + + Ok(written) + } + + #[pymethod] + fn shutdown(&self, vm: &VirtualMachine) -> PyResult { + loop { + let mut state = self.state.write(); + + match &mut *state { + State::ServerWaitingForClientHello(_) + | State::ServerSendingAlert { .. } + | State::HasConnection { + state: ConnectionState::Handshaking, + .. + } => { + return Err(SslError::Ssl( + "cannot perform TLS shutdown before handshake completed".to_string(), + ) + .into_py_err(vm)); + } + + State::HasConnection { + state: + ConnectionState::Connected(close_notify_state @ CloseNotifyState::None), + conn, + } => { + conn.send_close_notify(); + *close_notify_state = CloseNotifyState::Sent; + } + + State::HasConnection { + state: conn_state @ ConnectionState::Connected(CloseNotifyState::Received), + conn, + } => { + conn.send_close_notify(); + *conn_state = ConnectionState::ShuttingDown; + } + + State::HasConnection { + state: ConnectionState::Connected(CloseNotifyState::Sent), + .. + } => { + let mut byte = 0; + if self.read_inner(&mut state, slice::from_mut(&mut byte), vm)? == Some(1) { + return Err(SslError::Ssl(format!( + "Expected TLS Close Notify but received plaintext byte {byte}" + )) + .into_py_err(vm)); + } + } + + State::HasConnection { + state: conn_state @ ConnectionState::ShuttingDown, + conn, + } => { + self.complete_io(conn, true, vm)?; + *conn_state = ConnectionState::ShutDown; + break; + } + + State::HasConnection { + state: ConnectionState::ShutDown, + .. + } => { + break; + } + }; + } + + Ok(self.io.read().to_socket(vm)) + } + + // When handshaking, complete_io() returns only after handshake is complete. + // TODO: This might call certificate verifier which might be blocking and require network access on its own. + // option 1: Extract process_new_packets() from complete_io() to wrap it in allow_threads(). + // option 2: Introduce VirtualMachine::disallow_threads() and use it inside the IO wrapper instead. + fn complete_io( + &self, + conn: &mut Connection, + read_and_write: bool, + vm: &VirtualMachine, + ) -> PyResult<()> { + // complete_io() when writing if !conn.wants_write() may read data past the Close Notify. + // TODO: Remove this check when proper rustls unbuffered API is used. + if read_and_write || conn.wants_write() { + let mut io = self.io.write(); + let _ = io.with_io(vm, |io| conn.complete_io(io))?; + } + Ok(()) + } + + #[pymethod] + fn pending(&self, vm: &VirtualMachine) -> PyResult { + self.state + .write() + .get_connection_mut() + .map(|conn| self.pending_inner(conn, vm).map(|l| l.unwrap_or(0))) + .transpose() + .map(|l| l.unwrap_or(0)) + } + + fn pending_inner( + &self, + conn: &mut Connection, + vm: &VirtualMachine, + ) -> PyResult> { + match conn.reader().fill_buf().map(|b| b.len()) { + Ok(len) => Ok(Some(len)), + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => Ok(None), + Err(err) => Err(SslError::Io(err).into_py_err(vm)), + } + } + + #[pymethod] + fn getpeercert( + &self, + args: GetPeerCertArgs, + vm: &VirtualMachine, + ) -> PyResult> { + let state = self.state.read(); + let Some(conn) = state.get_connection() else { + return Err(vm.new_value_error("handshake not done yet")); + }; + + let binary_form = args.binary_form.unwrap_or(false); + + let Some(certs) = conn.peer_certificates() else { + return Ok(None); + }; + let Some(cert) = certs.first() else { + return Ok(None); + }; + + if binary_form { + Ok(Some(vm.ctx.new_bytes(cert.as_ref().to_vec()).into_object())) + } else if self.context.read().verify_mode() == CERT_NONE { + Ok(Some(vm.ctx.new_dict().into())) + } else { + CertInfo::parse_to_py(cert, vm).map(|cert| Some(cert.into_object())) + } + } + + #[pymethod] + fn cipher(&self, vm: &VirtualMachine) -> Option { + self.state + .read() + .get_connection() + .and_then(|c| c.negotiated_cipher_suite()) + .map(|c| cipher_to_tuple(&c, vm)) + } + + #[pymethod] + fn version(&self) -> Option<&'static str> { + self.state + .read() + .get_connection() + .and_then(|c| c.negotiated_cipher_suite()) + .map(|c| cipher_to_version(&c)) + } + + #[pymethod] + fn selected_alpn_protocol(&self, vm: &VirtualMachine) -> PyResult> { + self.state + .read() + .get_connection() + .and_then(|conn| conn.alpn_protocol()) + .map(|a| { + String::from_utf8(a.to_vec()) + .map_err(|_| vm.new_value_error("ALPN protocol is not valid UTF-8")) + }) + .transpose() + } + + #[pymethod] + fn session_reused(&self) -> bool { + self.state + .read() + .get_connection() + .is_some_and(|c| matches!(c.handshake_kind(), Some(HandshakeKind::Resumed))) + } + + #[pymethod] + fn get_verified_chain(&self, vm: &VirtualMachine) -> Option { + // rustls does not expose a separate verified chain. + self.get_unverified_chain(vm) + } + + #[pymethod] + fn get_unverified_chain(&self, vm: &VirtualMachine) -> Option { + let state = self.state.read(); + let certs = state.get_connection().and_then(|c| c.peer_certificates())?; + let certs = certs + .iter() + .map(|cert| { + PySSLCertificate { + bytes: cert.as_ref().to_vec(), + } + .into_ref(&vm.ctx) + .into_object() + }) + .collect(); + Some(vm.ctx.new_list(certs).into_object()) + } + + #[pymethod] + fn shared_ciphers(&self, vm: &VirtualMachine) -> Option { + let shared_ciphers = self.shared_ciphers.read(); + shared_ciphers.as_ref().map(|c| { + vm.ctx + .new_list(c.iter().map(|c| cipher_to_tuple(c, vm).into()).collect()) + }) + } + + // Needed for tests. + #[pygetset] + fn owner(&self) -> Option { + self.owner.upgrade() + } + + // Completely unsupported by rustls. + + #[pygetset(setter)] + fn set_session(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + // SSLSocket.session exists to persist sessions within a single process. + // rustls supports process-local sessions but does not expose any session info. + // This will change in the next version: https://github.com/rustls/rustls/pull/2907 + // Also see: + // * https://github.com/rustls/rustls/issues/466#issuecomment-1478728279 + // * https://github.com/rustls/rustls/issues/2287 + // TODO: Implement proper SSLSocket.session when new rustls releases. + + if value.try_downcast_ref::(vm).is_err() { + Err(vm.new_value_error("session is not SSLSession")) + } else { + Ok(()) + } + } + + #[pygetset] + fn session(&self) -> PySSLSession { + // Return some dummy session object. + PySSLSession { + creation_time: SystemTime::now(), + } + } + + #[pymethod] + fn selected_npn_protocol(&self) -> Option<()> { + // rustls doesn't support NPN, only ALPN + None + } + + #[pymethod] + fn compression(&self) -> Option<()> { + // rustls doesn't support compression + None + } + + #[pymethod] + fn verify_client_post_handshake(&self, vm: &VirtualMachine) -> PyResult<()> { + Err(vm.new_not_implemented_error( + "ssl.SSLSocket.verify_client_post_handshake() is not supported by rustls", + )) + } + + #[pymethod] + fn get_channel_binding( + &self, + cb_type: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult> { + let cb_type = cb_type + .as_ref() + .map(|cb_type| cb_type.as_str()) + .unwrap_or("tls-unique"); + + // rustls does not support `tls-unique` channel binding: + // * https://github.com/rustls/rustls/issues/995 + // * https://github.com/rustls/rustls/issues/1089 + // Some other channel binding types might be implementable with current rustls. + Err(vm.new_value_error(format!( + "{cb_type} channel binding type not supported by rustls" + ))) + } + } + + #[derive(FromArgs)] + struct GetPeerCertArgs { + #[pyarg(any, optional)] + binary_form: OptionalArg, + } + + #[pyattr] + #[pyclass(module = "_ssl", name = "MemoryBIO")] + #[derive(Debug, PyPayload)] + struct PyMemoryBIO { + // Internal buffer + buffer: PyMutex>, + // EOF flag + eof: AtomicBool, + } + + impl Representable for PyMemoryBIO { + #[inline] + fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok("".to_owned()) + } + } + + impl Constructor for PyMemoryBIO { + type Args = (); + + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { + Ok(Self { + buffer: PyMutex::new(Vec::new()), + eof: AtomicBool::new(false), + }) + } + } + + #[pyclass(with(Constructor), flags(BASETYPE))] + impl PyMemoryBIO { + #[pymethod] + fn read(&self, len: OptionalArg, vm: &VirtualMachine) -> PyResult { + let len = len + .map(|l| l.try_into()) + .transpose() + .map_err(|_| vm.new_value_error(format!("length is out of range: {len:?}")))?; + + let mut buffer = self.buffer.lock(); + + if buffer.is_empty() && self.eof.load(Ordering::Relaxed) { + // Return empty bytes at EOF + return Ok(vm.ctx.new_bytes(vec![])); + } + + let len = len.unwrap_or(buffer.len()); + let len = len.min(buffer.len()); + let data = buffer.drain(..len).collect::>(); + + Ok(vm.ctx.new_bytes(data)) + } + + #[pymethod] + fn write(&self, buf: PyObjectRef, vm: &VirtualMachine) -> PyResult { + // Check if buf is contiguous if it is a memoryview + if let Ok(mem_view) = buf.get_attr("c_contiguous", vm) { + // It's a memoryview, check if contiguous + let is_contiguous: bool = mem_view.try_to_bool(vm)?; + if !is_contiguous { + return Err(vm.new_exception_msg( + vm.ctx.exceptions.buffer_error.to_owned(), + "non-contiguous buffer is not supported".into(), + )); + } + } + // 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) + } + + #[pymethod] + fn write_eof(&self, _vm: &VirtualMachine) { + self.eof.store(true, Ordering::Relaxed); + } + + #[pygetset] + fn pending(&self) -> usize { + self.buffer.lock().len() + } + + #[pygetset] + fn eof(&self) -> bool { + // EOF is true only when buffer is empty AND write_eof has been called + self.buffer.lock().is_empty() && self.eof.load(Ordering::Relaxed) + } + } + + #[pyattr] + #[pyclass(module = "_ssl", name = "SSLSession")] + #[derive(Debug, PyPayload)] + struct PySSLSession { + creation_time: SystemTime, + } + + impl Representable for PySSLSession { + #[inline] + fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok("".to_owned()) + } + } + + #[pyclass(flags(BASETYPE))] + impl PySSLSession { + #[pygetset] + fn time(&self) -> u64 { + self.creation_time + .duration_since(UNIX_EPOCH) + .expect("BUG: What year this is?!") + .as_secs() + } + + #[pygetset] + fn timeout(&self) -> u64 { + 60 * 60 * 24 + } + + #[pygetset] + fn ticket_lifetime_hint(&self) -> u64 { + 60 * 60 * 24 + } + + #[pygetset] + fn id(&self, vm: &VirtualMachine) -> PyBytesRef { + vm.ctx.new_bytes(vec![0, 1, 2, 3]) + } + + #[pygetset] + fn has_ticket(&self) -> bool { + false + } + } + + // Windows-specific certificate store enumeration functions + #[cfg(windows)] + #[pyfunction] + fn enum_certificates( + store_name: PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult> { + let store_name_str = store_name.as_str(); + let certs = + vm.allow_threads(|| rustpython_host_env::cert_store::enum_certificates(store_name_str)); + if !certs.had_open_store { + return Err(vm.new_os_error(format!( + "failed to open certificate store {store_name_str:?}" + ))); + } + + let certs = certs.entries.into_iter().map(|c| { + let cert = vm.ctx.new_bytes(c.der); + let enc_type = match c.encoding { + rustpython_host_env::cert_store::EncodingType::X509Asn => vm.new_pyobj("x509_asn"), + rustpython_host_env::cert_store::EncodingType::Pkcs7Asn => { + vm.new_pyobj("pkcs_7_asn") + } + rustpython_host_env::cert_store::EncodingType::Other(other) => vm.new_pyobj(other), + }; + let usage: PyObjectRef = match c.valid_uses { + Ok(rustpython_host_env::cert_store::CertificateUses::All) => { + vm.ctx.new_bool(true).into() + } + Ok(rustpython_host_env::cert_store::CertificateUses::Oids(oids)) => { + match crate::builtins::PyFrozenSet::from_iter( + vm, + oids.into_iter().map(|oid| vm.ctx.new_str(oid).into()), + ) { + Ok(set) => set.into_ref(&vm.ctx).into(), + Err(_) => vm.ctx.new_bool(true).into(), + } + } + Err(_) => vm.ctx.new_bool(true).into(), + }; + Ok(vm.new_tuple((cert, enc_type, usage)).into()) + }); + certs.collect::>>() + } + + #[cfg(windows)] + #[pyfunction] + fn enum_crls(store_name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult> { + let store_name_str = store_name.as_str(); + let crls = vm + .allow_threads(|| rustpython_host_env::cert_store::enum_crls(store_name_str)) + .map_err(|_| { + vm.new_os_error(format!( + "failed to open certificate store {store_name_str:?}" + )) + })?; + + Ok(crls + .into_iter() + .map(|crl| { + let enc_type = match crl.encoding { + rustpython_host_env::cert_store::EncodingType::X509Asn => { + vm.new_pyobj("x509_asn") + } + rustpython_host_env::cert_store::EncodingType::Pkcs7Asn => { + vm.new_pyobj("pkcs_7_asn") + } + rustpython_host_env::cert_store::EncodingType::Other(other) => { + vm.new_pyobj(other) + } + }; + vm.new_tuple((vm.ctx.new_bytes(crl.der), enc_type)).into() + }) + .collect()) + } + + #[derive(FromArgs)] + struct Txt2ObjArgs { + txt: PyUtf8StrRef, + + #[pyarg(named, optional)] + name: OptionalArg, + } + + #[pyfunction] + fn txt2obj(args: Txt2ObjArgs, vm: &VirtualMachine) -> PyResult { + let txt = args.txt.as_str(); + let name = args.name.unwrap_or(false); + + // Lookup by oid first. + let mut entry = txt + .split('.') + .map(|s| s.parse()) + .collect::, _>>() + .ok() + .and_then(|o| Oid::from(&o).ok()) + .and_then(|o| OID_MAPPINGS.oid_to_entry.get(&o).map(|e| (o, e))); + + if name && entry.is_none() { + entry = OID_MAPPINGS + .name_to_oid + .get(txt) + .and_then(|o| OID_MAPPINGS.oid_to_entry.get(o).map(|e| (o.clone(), e))) + } + + let (oid, entry) = + entry.ok_or_else(|| vm.new_value_error(format!("unknown object '{txt}'")))?; + let oid_sn = (oid, entry.sn()); + + // Return tuple: (nid, shortname, longname, oid) + Ok(vm + .new_tuple(( + OID_MAPPINGS + .oid_sn_to_nid + .get(&oid_sn) + .ok_or_else(|| { + vm.new_value_error(format!("object '{txt}' does not have a known NID")) + }) + .map(|n| vm.ctx.new_int(*n))?, + vm.ctx.new_str(entry.sn()), + vm.ctx.new_str(entry.description()), + vm.ctx.new_str(oid_sn.0.to_string()), + )) + .into()) + } + + #[pyfunction] + fn nid2obj(nid: i32, vm: &VirtualMachine) -> PyResult { + let nid = nid + .try_into() + .map_err(|_| vm.new_value_error(format!("unknown NID {nid}")))?; + let oid = OID_MAPPINGS + .nid_to_oid + .get(&nid) + .ok_or_else(|| vm.new_value_error(format!("unknown NID {nid}")))?; + let entry = OID_MAPPINGS.oid_to_entry.get(oid).expect("BUG: Impossible"); + + // Return tuple: (nid, shortname, longname, oid) + Ok(vm + .new_tuple(( + vm.ctx.new_int(nid), + vm.ctx.new_str(entry.sn()), + vm.ctx.new_str(entry.description()), + vm.ctx.new_str(oid.to_string()), + )) + .into()) + } + + #[pyfunction] + fn get_default_verify_paths(vm: &VirtualMachine) -> PyTupleRef { + const DEV_NULL: &str = cfg_select! { + windows => "nul", + _ => "/dev/null", + }; + + // Lib/ssl.py expects: (openssl_cafile_env, openssl_cafile, openssl_capath_env, openssl_capath) + vm.ctx.new_tuple(vec![ + vm.ctx.new_str(CERT_FILE_ENV).into(), + vm.ctx.new_str(DEV_NULL).into(), + vm.ctx.new_str(CERT_DIR_ENV).into(), + vm.ctx.new_str(DEV_NULL).into(), + ]) + } + + // See `man openssl-env`. + const CERT_FILE_ENV: &str = "SSL_CERT_FILE"; + const CERT_DIR_ENV: &str = "SSL_CERT_DIR"; + + #[pyfunction] + fn RAND_status() -> bool { + // Pretend that used RNG always has enough entropy + // RAND_bytes() will just block if system does not have enough entropy. + true + } + + #[pyfunction] + fn RAND_add(_string: PyObjectRef, _entropy: f64) { + // There is no way to easily support this. + // RAND_bytes() will just block if system does not have enough entropy. + } + + #[pyfunction] + fn RAND_bytes(len: isize, vm: &VirtualMachine) -> PyResult { + let len = len + .try_into() + .map_err(|_| vm.new_value_error(format!("length is out of range: {len}")))?; + + let rng = CryptoExt::get_provider().secure_random; + let mut buf = vec![0u8; len]; + vm.allow_threads(|| rng.fill(&mut buf)) + .map_err(|_| vm.new_os_error("Failed to generate random bytes"))?; + Ok(PyBytesRef::from(vm.ctx.new_bytes(buf))) + } + + // Used in test_ssl.py. + #[pyfunction] + fn _test_decode_cert(path: FsPath, vm: &VirtualMachine) -> PyResult { + let ders = load_der_bytes_from_pem_or_der_file( + path.to_path_buf(vm)?, + &[DerKind::Cert], + &mut Password::None, + vm, + ) + .map_err(|e| e.into_py_err(vm))?; + let der = ensure_single_der_bytes(&path.to_string_lossy(), ders) + .map_err(|e| e.into_py_err(vm))?; + CertInfo::parse_to_py(&der.bytes, vm) + } + + #[pyfunction] + fn DER_cert_to_PEM_cert(der_cert: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + let bytes = der_cert.borrow_buf(); + let pem = der_to_pem_cert(&bytes) + .ok_or_else(|| vm.new_memory_error("certificate is too big for PEM encoding"))?; + Ok(vm.ctx.new_str(pem)) + } + + #[pyfunction] + fn PEM_cert_to_DER_cert(pem_cert: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { + let ders = load_der_bytes_from_pem_or_der_bytes( + "", + pem_cert.as_bytes().to_vec(), + &[DerKind::Cert], + &mut Password::None, + vm, + ) + .map_err(|e| e.into_py_err(vm))?; + let der = ensure_single_der_bytes("", ders).map_err(|e| e.into_py_err(vm))?; + Ok(vm.ctx.new_bytes(der.bytes)) + } + + #[pyattr] + #[pyclass(module = "_ssl", name = "Certificate")] + #[derive(Debug, PyPayload)] + struct PySSLCertificate { + bytes: Vec, + } + + // Implement Comparable trait for PySSLCertificate + impl Comparable for PySSLCertificate { + fn cmp( + zelf: &Py, + other: &PyObject, + op: PyComparisonOp, + _vm: &VirtualMachine, + ) -> PyResult { + op.eq_only(|| { + if let Some(other_cert) = other.downcast_ref::() { + Ok((zelf.bytes == other_cert.bytes).into()) + } else { + Ok(PyComparisonValue::NotImplemented) + } + }) + } + } + + // Implement Hashable trait for PySSLCertificate + impl Hashable for PySSLCertificate { + fn hash(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + let mut hasher = DefaultHasher::new(); + zelf.bytes.hash(&mut hasher); + Ok(hasher.finish() as PyHash) + } + } + + // Implement Representable trait for PySSLCertificate + impl Representable for PySSLCertificate { + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(parse_x509_certificate(&zelf.bytes).map_or_else( + |_| "".to_string(), + |(_, c)| format!("", c.subject()), + )) + } + } + + #[pyclass(with(Comparable, Hashable, Representable))] + impl PySSLCertificate { + #[pymethod] + fn public_bytes(&self, format: OptionalArg, vm: &VirtualMachine) -> PyResult { + let format = format.unwrap_or(ENCODING_PEM); + + match format { + ENCODING_DER => Ok(vm.ctx.new_bytes(self.bytes.clone()).into()), + + ENCODING_PEM => { + let pem = der_to_pem_cert(&self.bytes).ok_or_else(|| { + vm.new_memory_error("certificate is too big for PEM encoding") + })?; + Ok(vm.ctx.new_str(pem).into()) + } + + _ => Err(vm.new_value_error("Unsupported format")), + } + } + + #[pymethod] + fn get_info(&self, vm: &VirtualMachine) -> PyResult { + CertInfo::parse_to_py(&self.bytes, vm) + } + } +} + +// +// Connection state. +// + +enum State { + ServerWaitingForClientHello(Acceptor), + + ServerSendingAlert { + error: PyBaseExceptionRef, + alert_buf: [u8; TLS_RECORD_HEADER_LEN + TLS_ALERT_RECORD_LEN], + alert_buf_pos: usize, + }, + + HasConnection { + state: ConnectionState, + conn: Connection, + }, +} + +#[derive(Debug)] +enum ConnectionState { + Handshaking, + Connected(CloseNotifyState), + ShuttingDown, + ShutDown, +} + +#[derive(Debug)] +enum CloseNotifyState { + None, + Received, + Sent, +} + +impl core::fmt::Debug for State { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::ServerWaitingForClientHello(_) => f + .debug_tuple("ServerWaitingForClientHello") + .field(&"Acceptor") + .finish(), + + Self::ServerSendingAlert { error, .. } => f + .debug_struct("ServerSendingAlert") + .field("error", error) + .finish(), + + Self::HasConnection { state, conn } => f + .debug_struct("Handshaking") + .field("state", state) + .field("conn", conn) + .finish(), + } + } +} + +impl State { + fn new_handshaking_server() -> Self { + Self::ServerWaitingForClientHello(Acceptor::default()) + } + + fn new_handshaking_client(conn: Connection) -> Self { + Self::HasConnection { + state: ConnectionState::Handshaking, + conn, + } + } + + fn new_alert_from_rustls_error( + error: rustls::Error, + mut alert: AcceptedAlert, + vm: &VirtualMachine, + ) -> PyResult { + let mut alert_buf = [0u8; TLS_RECORD_HEADER_LEN + TLS_ALERT_RECORD_LEN]; + let mut alert_buf_mut: &mut [u8] = &mut alert_buf; + + if alert.write_all(&mut alert_buf_mut).is_err() || !alert_buf_mut.is_empty() { + Err(SslError::Ssl("TLS alert is too long or too short".to_string()).into_py_err(vm)) + } else { + Ok(Self::ServerSendingAlert { + error: SslError::from_rustls(error).into_py_err(vm), + alert_buf, + alert_buf_pos: 0, + }) + } + } + + fn new_alert_from_sni_callback_error(error: PyBaseExceptionRef, alert_code: u8) -> Self { + Self::ServerSendingAlert { + error, + + #[rustfmt::skip] + alert_buf: [ + 0x15, // type == alert + 0x03, 0x03, // version == TLS 1.2 (TODO: Is it fine that we hardcode TLS 1.2 here?) + 0x00, 0x02, // length == 2 bytes + 0x02, // alert level == fatal + alert_code, // code returned by SNI callback + ], + + alert_buf_pos: 0, + } + } + + fn get_connection(&self) -> Option<&Connection> { + match self { + Self::ServerWaitingForClientHello(_) + | Self::ServerSendingAlert { .. } + | Self::HasConnection { + state: ConnectionState::Handshaking, + .. + } => None, + + Self::HasConnection { + state: + ConnectionState::Connected(_) + | ConnectionState::ShuttingDown + | ConnectionState::ShutDown, + conn, + } => Some(conn), + } + } + + fn get_connection_mut(&mut self) -> Option<&mut Connection> { + match self { + Self::ServerWaitingForClientHello(_) + | Self::ServerSendingAlert { .. } + | Self::HasConnection { + state: ConnectionState::Handshaking, + .. + } => None, + + Self::HasConnection { + state: + ConnectionState::Connected(_) + | ConnectionState::ShuttingDown + | ConnectionState::ShutDown, + conn, + } => Some(conn), + } + } +} + +// +// IO wrapper. +// + +#[derive(Debug)] +struct Io { + // TODO: Support timeouts. + socket_or_bio: SocketOrBio, + hdr: [u8; TLS_RECORD_HEADER_LEN], + hdr_len: usize, +} + +const TLS_RECORD_HEADER_LEN: usize = 5; +const TLS_ALERT_RECORD_LEN: usize = 2; + +#[derive(Debug)] +enum SocketOrBio { + Socket { + socket: PyObjectRef, + + // TODO: Investigate why normal `sock.send()`/`sock.recv()` lead to a hang. + sock_send_method: PyObjectRef, + sock_recv_method: PyObjectRef, + }, + + Bio { + incoming: PyObjectRef, + outgoing: PyObjectRef, + }, +} + +impl Io { + fn from_socket(socket: PyObjectRef, vm: &VirtualMachine) -> PyResult { + // TODO: Call send() and recv() directly. Currently this deadlocks for some reason. + let socket_mod = vm.import("socket", 0)?; + let socket_class = socket_mod.get_attr("socket", vm)?; + Ok(Self { + socket_or_bio: SocketOrBio::Socket { + socket, + sock_send_method: socket_class.get_attr("send", vm)?, + sock_recv_method: socket_class.get_attr("recv", vm)?, + }, + hdr: [0; TLS_RECORD_HEADER_LEN], + hdr_len: 0, + }) + } + + fn from_bio(incoming: PyObjectRef, outgoing: PyObjectRef) -> Self { + Self { + socket_or_bio: SocketOrBio::Bio { incoming, outgoing }, + hdr: [0; TLS_RECORD_HEADER_LEN], + hdr_len: 0, + } + } + + fn with_io(&mut self, vm: &VirtualMachine, f: F) -> PyResult + where + F: FnOnce(&mut WithIo<'_>) -> std::io::Result, + { + let mut io = WithIo { + io: self, + vm, + error: None, + }; + match f(&mut io) { + Ok(value) => Ok(value), + + Err(err) => match err.kind() { + std::io::ErrorKind::Other => { + Err(io.error.take().expect("BUG: Io.error is not set")) + } + + std::io::ErrorKind::InvalidData => { + // ConnectionCommon::complete_io() wraps TLS processing errors in InvalidData. + let err = err + .downcast::() + .expect("BUG: Not a rustls Error"); + Err(SslError::from_rustls(err).into_py_err(vm)) + } + + _ => Err(SslError::Io(err).into_py_err(vm)), + }, + } + } + + fn to_socket(&self, vm: &VirtualMachine) -> PyObjectRef { + match &self.socket_or_bio { + SocketOrBio::Socket { socket, .. } => socket.clone(), + SocketOrBio::Bio { .. } => vm.ctx.none(), + } + } +} + +struct WithIo<'a> { + io: &'a mut Io, + vm: &'a VirtualMachine, + error: Option, +} + +impl std::io::Read for WithIo<'_> { + // Read no more than a single TLS entry. + // TODO: Wait for better unbuffered API in rustls. + // See https://github.com/rustls/rustls/pull/2905 + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if buf.is_empty() { + return Ok(0); + } + + if self.io.hdr_len < TLS_RECORD_HEADER_LEN { + // We do not have a full TLS record header, start receiving one. + let len = buf.len().min(TLS_RECORD_HEADER_LEN - self.io.hdr_len); + let buf = &mut buf[..len]; + let read = self.read_inner(buf)?; + self.io.hdr[self.io.hdr_len..self.io.hdr_len + len].copy_from_slice(buf); + self.io.hdr_len += read; + + if self.io.hdr_len == TLS_RECORD_HEADER_LEN { + // Parse the body length. + let record_body_len = u16::from_be_bytes([self.io.hdr[3], self.io.hdr[4]]); + + // Zero-length TLS record. + if record_body_len == 0 { + self.io.hdr_len = 0; + } + } + + Ok(read) + } else { + // Parse the body length. + let mut record_body_len = u16::from_be_bytes([self.io.hdr[3], self.io.hdr[4]]); + // Validity of length value will be checked by rustls. + let buf_len = buf.len(); + let buf = &mut buf[..buf_len.min(record_body_len.into())]; + + let read = self.read_inner(buf)?; + + record_body_len -= read as u16; + if record_body_len == 0 { + // Start reading next record. + self.io.hdr_len = 0; + } else { + // Update remaining length in the header. + self.io.hdr.as_mut_slice()[3..5].copy_from_slice(&record_body_len.to_be_bytes()); + } + + Ok(read) + } + } +} + +impl std::io::Write for WithIo<'_> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if buf.is_empty() { + return Ok(0); + } + + let res = match &self.io.socket_or_bio { + SocketOrBio::Socket { + socket, + + sock_send_method, + .. + } => sock_send_method.call( + (socket.clone(), self.vm.ctx.new_bytes(buf.to_vec())), + self.vm, + ), + + SocketOrBio::Bio { outgoing, .. } => outgoing + .get_attr("write", self.vm) + .and_then(|w| w.call((self.vm.ctx.new_bytes(buf.to_vec()),), self.vm)), + } + .and_then(|b| usize::try_from_object(self.vm, b)); + + match res { + Ok(len) => Ok(len), + + Err(err) => { + assert!(self.error.is_none(), "BUG: Duplicate error"); + + if err.fast_isinstance(self.vm.ctx.exceptions.blocking_io_error) { + self.error = Some(SslError::WantWrite.into_py_err(self.vm)); + Err(std::io::Error::other("SSLWantWriteError")) + } else { + self.error = Some(err); + Err(std::io::Error::other("Python IO error when writing")) + } + } + } + } + + fn flush(&mut self) -> std::io::Result<()> { + // Neither socket nor buffer IO need this. + Ok(()) + } +} + +impl WithIo<'_> { + fn read_inner(&mut self, buf: &mut [u8]) -> std::io::Result { + match self.read_inner_py(buf.len()) { + Ok(Some(bytes)) => { + if bytes.is_empty() { + // Zero read means EOF. + self.error = Some(SslError::Eof.into_py_err(self.vm)); + Err(std::io::Error::other("SSLEOFError")) + } else { + let bytes = bytes.borrow_buf(); + buf[..bytes.len()].copy_from_slice(&bytes); + Ok(bytes.len()) + } + } + + Ok(None) => { + assert!(self.error.is_none(), "BUG: Duplicate error"); + self.error = Some(SslError::WantRead.into_py_err(self.vm)); + Err(std::io::Error::other("SSLWantReadError")) + } + + Err(err) => { + assert!(self.error.is_none(), "BUG: Duplicate error"); + self.error = Some(err); + Err(std::io::Error::other("Python IO error when reading")) + } + } + } + + fn read_inner_py(&mut self, len: usize) -> PyResult> { + let res = match &self.io.socket_or_bio { + SocketOrBio::Socket { + socket, + + sock_recv_method, + .. + } => sock_recv_method.call((socket.clone(), self.vm.ctx.new_int(len)), self.vm), + + SocketOrBio::Bio { incoming, .. } => incoming + .get_attr("read", self.vm) + .and_then(|r| r.call((self.vm.ctx.new_int(len),), self.vm)), + } + .and_then(|b| ArgBytesLike::try_from_object(self.vm, b)); + + if let SocketOrBio::Bio { incoming, .. } = &self.io.socket_or_bio { + let bytes = res?; + if bytes.is_empty() { + let eof = incoming.get_attr("eof", self.vm)?; + let eof = bool::try_from_object(self.vm, eof)?; + if eof { Ok(Some(bytes)) } else { Ok(None) } + } else { + Ok(Some(bytes)) + } + } else { + match res { + Ok(bytes) => Ok(Some(bytes)), + + Err(err) if err.fast_isinstance(self.vm.ctx.exceptions.blocking_io_error) => { + Ok(None) + } + + Err(err) => Err(err), + } + } + } +} + +// +// Cipher info. +// + +#[derive(Serialize)] +struct CipherDescriptionDict { + id: u16, + name: &'static str, + protocol: &'static str, + description: &'static str, + strength_bits: u16, + alg_bits: u16, +} + +impl CipherDescriptionDict { + fn new(cipher: &SupportedCipherSuite) -> Self { + let id = cipher.suite().into(); + let bits = CIPHER_MAPPINGS.id_to_bits[&id]; + Self { + id, + name: CIPHER_MAPPINGS.id_to_openssl[&id], + + protocol: match cipher.version().version { + ProtocolVersion::TLSv1_2 => "TLSv1.2", + ProtocolVersion::TLSv1_3 => "TLSv1.3", + + // This is tested by that_all_rustls_tls_versions_are_known(). + // This may happen after rustls update, just add more ciphers above is this case. + version => unreachable!("BUG: Unknown TLS version {version:?}"), + }, + + description: CIPHER_MAPPINGS.id_to_openssl[&id], + strength_bits: bits, + alg_bits: bits, + } + } +} + +fn cipher_to_tuple(cipher: &SupportedCipherSuite, vm: &VirtualMachine) -> PyTupleRef { + let id = cipher.suite().into(); + + vm.ctx.new_tuple(vec![ + vm.ctx + .new_str(CIPHER_MAPPINGS.id_to_openssl[&id]) + .into_object(), + vm.ctx.new_str(cipher_to_version(cipher)).into_object(), + vm.ctx + .new_int(CIPHER_MAPPINGS.id_to_bits[&id]) + .into_object(), + ]) +} + +fn cipher_to_version(cipher: &SupportedCipherSuite) -> &'static str { + match cipher.version().version { + ProtocolVersion::TLSv1_2 => "TLSv1.2", + ProtocolVersion::TLSv1_3 => "TLSv1.3", + _ => "unknown", + } +} + +// +// PEM, DER, certificate and private key utilities. +// + +fn ensure_single_der_bytes(path_str: &str, mut ders: Vec) -> SslResult { + let mut ders = ders.drain(..); + let der = ders.next().expect("BUG: Impossible"); + if ders.next().is_some() { + return Err(SslError::Ssl(format!( + "more than one certificate in {path_str}" + ))); + } + Ok(der) +} + +fn load_der_bytes_from_pem_or_der_file( + path: impl AsRef, + kinds: &[DerKind], + password: &mut Password, + vm: &VirtualMachine, +) -> SslResult> { + load_der_bytes_from_pem_or_der_file_inner(path.as_ref(), kinds, password, vm) +} + +fn load_der_bytes_from_pem_or_der_file_inner( + path: &Path, + kinds: &[DerKind], + password: &mut Password, + vm: &VirtualMachine, +) -> SslResult> { + let bytes = vm + .allow_threads(|| rustpython_host_env::fs::read(path)) + .map_err(SslError::Io)?; + load_der_bytes_from_pem_or_der_bytes(&format!("{path:?}"), bytes, kinds, password, vm) +} + +// This function does not verify that returned DER data is correct. +// rustls or x509_parser will check for correctness later. +fn load_der_bytes_from_pem_or_der_bytes( + path_str: &str, + bytes: Vec, + kinds: &[DerKind], + password: &mut Password, + vm: &VirtualMachine, +) -> SslResult> { + assert!(!kinds.is_empty(), "BUG Empty PEM/DER kinds"); + + let (mut ders, first_pem_entry_not_read) = + load_der_bytes_from_pem(path_str, &bytes, kinds, password, vm)?; + + if first_pem_entry_not_read { + // PEM reading failed right away so this must be DER (possibly more than one + // DER-encoded object in the same file). + ders = load_der_bytes_from_der(path_str, &bytes, kinds, password, vm)?; + } + + if ders.is_empty() { + Err(SslError::PemLib("no PEM certificates found".to_string())) + } else { + Ok(ders) + } +} + +fn load_der_bytes_from_pem( + path_str: &str, + bytes: &[u8], + kinds: &[DerKind], + password: &mut Password, + vm: &VirtualMachine, +) -> SslResult<(Vec, bool)> { + let mut ders = Vec::new(); + let mut first_pem_entry_not_read = true; + for pem in Pem::iter_from_buffer(bytes) { + if first_pem_entry_not_read { + if pem.is_err() { + break; + } + first_pem_entry_not_read = false; + } + let pem = pem.map_err(|e| SslError::PemLib(e.to_string()))?; + + let (kind, bytes) = match pem.label.as_str() { + "CERTIFICATE" | "TRUSTED CERTIFICATE" if kinds.contains(&DerKind::Cert) => { + (DerKind::Cert, pem.contents) + } + + "CERTIFICATE REVOCATION LIST" | "X509 CRL" if kinds.contains(&DerKind::Crl) => { + (DerKind::Crl, pem.contents) + } + + "PRIVATE KEY" | "EC PRIVATE KEY" | "RSA PRIVATE KEY" + if kinds.contains(&DerKind::Key) => + { + (DerKind::Key, pem.contents) + } + + "ENCRYPTED PRIVATE KEY" if kinds.contains(&DerKind::Key) => ( + DerKind::Key, + decrypt_private_key(path_str, &pem.contents, password, vm)?.1, + ), + + _ => continue, + }; + ders.push(DerBytes { kind, bytes }); + } + + Ok((ders, first_pem_entry_not_read)) +} + +fn load_der_bytes_from_der( + path_str: &str, + mut bytes: &[u8], + kinds: &[DerKind], + password: &mut Password, + vm: &VirtualMachine, +) -> SslResult> { + let mut ders = Vec::new(); + while !bytes.is_empty() { + let mut last_error = None; + for kind in kinds { + match kind { + DerKind::Key => match decrypt_private_key(path_str, bytes, password, vm) { + Ok((rem, parsed_bytes)) => { + bytes = rem; + ders.push(DerBytes { + kind: DerKind::Key, + bytes: parsed_bytes, + }); + last_error = None; + break; + } + + Err(err) => last_error = Some(err), + }, + + DerKind::Crl => match parse_x509_crl(bytes) { + Ok((rem, crl)) => { + bytes = rem; + ders.push(DerBytes { + kind: DerKind::Crl, + bytes: crl.as_raw().to_vec(), + }); + last_error = None; + break; + } + + Err(err) => { + last_error = Some(SslError::FailedToReadDer(format!( + "certificate revocation list from {path_str}: {err}" + ))) + } + }, + + DerKind::Cert => match parse_x509_certificate(bytes) { + Ok((rem, cert)) => { + bytes = rem; + ders.push(DerBytes { + kind: DerKind::Cert, + bytes: cert.as_raw().to_vec(), + }); + last_error = None; + break; + } + + Err(err) => { + last_error = Some(SslError::FailedToReadDer(format!( + "certificate from {path_str}: {err}" + ))) + } + }, + } + } + + if let Some(err) = last_error { + return Err(err); + } + } + Ok(ders) +} + +struct DerBytes { + kind: DerKind, + bytes: Vec, +} + +#[derive(Eq, PartialEq, Clone, Copy)] +enum DerKind { + Cert, + Crl, + Key, +} + +fn decrypt_private_key<'a>( + path_str: &str, + bytes: &'a [u8], + password: &mut Password, + vm: &VirtualMachine, +) -> SslResult<(&'a [u8], Vec)> { + // Try to parse as encrypted private key and keep any trailing data. + let mut aligned_bytes = bytes; + let rem_plus_encrypted = loop { + match EncryptedPrivateKeyInfoRef::from_der(aligned_bytes) { + Ok(encrypted) => break Some((&bytes[aligned_bytes.len()..], encrypted)), + + Err(err) => { + if let pkcs8::der::ErrorKind::TrailingData { decoded, .. } = err.kind() { + aligned_bytes = &aligned_bytes[..decoded.try_into().unwrap()] + } else { + break None; + } + } + } + }; + + if let Some((rem, encrypted)) = rem_plus_encrypted { + // Try to decrypt + let password = password.password(vm).map_err(SslError::Py)?; + let decrypted = encrypted.decrypt(password).map_err(|e| { + SslError::Ssl(format!( + "failed to decrypt private key from {path_str}: {e}" + )) + })?; + Ok((rem, decrypted.as_bytes().to_vec())) + } else { + // Parse as plain text private key and keep any trailing data. + let mut aligned_bytes = bytes; + let rem = loop { + match PrivateKeyInfoRef::from_der(aligned_bytes) { + Ok(_) => break &bytes[aligned_bytes.len()..], + + Err(err) => { + if let pkcs8::der::ErrorKind::TrailingData { decoded, .. } = err.kind() { + aligned_bytes = &aligned_bytes[..decoded.try_into().unwrap()] + } else { + return Err(SslError::Ssl(format!( + "invalid private key in {path_str}: {err}" + ))); + } + } + } + }; + Ok((rem, bytes[..bytes.len() - rem.len()].to_vec())) + } +} + +enum Password { + None, + Callable(PyObjectRef), + Bytes(Vec), +} + +impl Password { + const MAX_PASSWORD_LEN: usize = 1024; + + fn new(password: OptionalArg, vm: &VirtualMachine) -> PyResult { + let password = match password { + OptionalArg::Missing => return Ok(Self::None), + OptionalArg::Present(password) => password, + }; + + if vm.is_none(&password) { + Ok(Self::None) + } else if password.is_callable() { + Ok(Self::Callable(password)) + } else if let Ok(password) = ArgBytesLike::try_from_object(vm, password.clone()) { + Ok(Self::Bytes(Self::validate( + password.borrow_buf().to_vec(), + vm, + )?)) + } else if let Ok(password) = PyUtf8StrRef::try_from_object(vm, password) { + Ok(Self::Bytes(Self::validate( + password.as_str().as_bytes().to_vec(), + vm, + )?)) + } else { + Err(vm.new_type_error("password should be a string or callable")) + } + } + + fn password(&mut self, vm: &VirtualMachine) -> PyResult<&[u8]> { + match self { + // TODO: Prompt user for password. + Self::None => Err(vm.new_value_error("no password provided")), + Self::Bytes(bytes) => Ok(bytes.as_slice()), + + Self::Callable(callable) => { + let password = callable.call((), vm)?; + if let Ok(password) = ArgBytesLike::try_from_object(vm, password.clone()) { + *self = Self::Bytes(Self::validate(password.borrow_buf().to_vec(), vm)?); + // TODO: Rewrite without recursion? + self.password(vm) + } else if let Ok(password) = PyUtf8StrRef::try_from_object(vm, password) { + *self = Self::Bytes(Self::validate(password.as_str().as_bytes().to_vec(), vm)?); + // TODO: Rewrite without recursion? + self.password(vm) + } else { + Err(vm.new_type_error("password callback must return a string")) + } + } + } + } + + fn validate(bytes: Vec, vm: &VirtualMachine) -> PyResult> { + if bytes.len() > Self::MAX_PASSWORD_LEN { + Err(vm.new_value_error(format!( + "password cannot be longer than {} bytes", + Self::MAX_PASSWORD_LEN + ))) + } else { + Ok(bytes) + } + } +} + +fn der_to_pem_cert(der: &[u8]) -> Option { + // TODO: Encode line by line to consume less memory. + const MAX_LINE_LEN: usize = 64; + + let len = base64::encoded_len(der.len(), true)?; + let mut enc_buf = String::with_capacity(len); + BASE64_STANDARD.encode_string(der, &mut enc_buf); + + let mut buf = String::with_capacity(len + (len / MAX_LINE_LEN) + 100); + buf.push_str("-----BEGIN CERTIFICATE-----\n"); + for line in enc_buf + .as_bytes() + .chunks(MAX_LINE_LEN) + .map(|b| str::from_utf8(b).expect("BUG: Impossible")) + { + buf.push_str(line); + buf.push('\n'); + } + buf.push_str("-----END CERTIFICATE-----\n"); + Some(buf) +} + +#[allow(non_snake_case)] +#[derive(Serialize)] +struct CertInfo { + #[serde(skip_serializing_if = "Vec::is_empty")] + OCSP: Vec, + + #[serde(skip_serializing_if = "Vec::is_empty")] + caIssuers: Vec, + + #[serde(skip_serializing_if = "Vec::is_empty")] + crlDistributionPoints: Vec, + + #[serde(skip_serializing_if = "Vec::is_empty")] + issuer: Vec<((String, String),)>, + + notAfter: String, + notBefore: String, + serialNumber: String, + + #[serde(skip_serializing_if = "Vec::is_empty")] + subject: Vec<((String, String),)>, + + #[serde(skip_serializing_if = "Vec::is_empty")] + subjectAltName: Vec, + + version: u32, +} + +#[derive(Serialize)] +#[serde(untagged)] +enum CertInfoPairOrNested { + Pair(&'static str, String), + Nested(&'static str, Vec<((String, String),)>), +} + +impl CertInfo { + fn parse_to_py(bytes: &[u8], vm: &VirtualMachine) -> PyResult { + let cert = + Self::parse(bytes).map_err(|_| vm.new_value_error("failed to parse certificate"))?; + vm.with_serde_conf(RustPySerDeConf::default().lists_as_tuples(), |serde| { + cert.serialize(serde) + }) + } + + fn parse(bytes: &[u8]) -> Result { + let (_, cert) = parse_x509_certificate(bytes).map_err(|_| "failed to parse certificate")?; + + let tbs_exts = cert + .tbs_certificate + .extensions_map() + .map_err(|_| "duplicate TBSCertificate extension")?; + + // CA issuers and OCSP URLs + let mut ocsp_urls = Vec::new(); + let mut issuer_urls = Vec::new(); + if let Some(ext) = tbs_exts.get(&OID_PKIX_AUTHORITY_INFO_ACCESS) { + let ext = if let ParsedExtension::AuthorityInfoAccess(ext) = &ext.parsed_extension() { + ext + } else { + return Err("wrong data in authorityInfoAccess extension"); + }; + for desc in &ext.accessdescs { + let uri = if let GeneralName::URI(uri) = &desc.access_location { + uri + } else { + // We are interested in URIs only + continue; + }; + if desc.access_method == OID_PKIX_ACCESS_DESCRIPTOR_OCSP { + ocsp_urls.push(uri.to_string()); + } else if desc.access_method == OID_PKIX_ACCESS_DESCRIPTOR_CA_ISSUERS { + issuer_urls.push(uri.to_string()); + } + // Ignore other access methods. + } + } + + // CRL distribution points + let mut crl_urls = Vec::new(); + if let Some(ext) = tbs_exts.get(&OID_X509_EXT_CRL_DISTRIBUTION_POINTS) { + let ext = if let ParsedExtension::CRLDistributionPoints(ext) = &ext.parsed_extension() { + ext + } else { + return Err("wrong data in cRLDistributionPoints extension"); + }; + for point in ext + .points + .iter() + .filter_map(|p| p.distribution_point.as_ref()) + { + let names = if let DistributionPointName::FullName(names) = point { + names + } else { + continue; + }; + for name in names { + if let GeneralName::URI(uri) = name { + crl_urls.push(uri.to_string()); + } + } + } + } + + // Serial number + let mut serial_number = cert.serial.to_str_radix(16).to_uppercase(); + if serial_number.len() % 2 == 1 { + serial_number.insert(0, '0'); + } + + // Alternative URLs + let alt_names = if let Some(alt_names) = cert + .subject_alternative_name() + .map_err(|_| "Subject Alternative Name extension is invalid")? + { + alt_names + .value + .general_names + .iter() + .map(|alt_name| { + match alt_name { + GeneralName::DNSName(dns) => { + Ok(CertInfoPairOrNested::Pair("DNS", dns.to_string())) + } + + GeneralName::IPAddress(ip) => { + let ip_str = match ip.len() { + 4 => format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]), + + 16 => format!( + "{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}:{:X}", + (u16::from(ip[0]) << 8) | u16::from(ip[1]), + (u16::from(ip[2]) << 8) | u16::from(ip[3]), + (u16::from(ip[4]) << 8) | u16::from(ip[5]), + (u16::from(ip[6]) << 8) | u16::from(ip[7]), + (u16::from(ip[8]) << 8) | u16::from(ip[9]), + (u16::from(ip[10]) << 8) | u16::from(ip[11]), + (u16::from(ip[12]) << 8) | u16::from(ip[13]), + (u16::from(ip[14]) << 8) | u16::from(ip[15]), + ), + + _ => return Err("invalid length of IPv4/IPv6 address"), + }; + Ok(CertInfoPairOrNested::Pair("IP Address", ip_str)) + } + + GeneralName::RFC822Name(email) => { + Ok(CertInfoPairOrNested::Pair("email", email.to_string())) + } + + GeneralName::URI(uri) => { + Ok(CertInfoPairOrNested::Pair("URI", uri.to_string())) + } + + GeneralName::OtherName(_oid, _data) => Ok(CertInfoPairOrNested::Pair( + "othername", + //format!("{}={}", oid.to_string(), hex::encode(data)), + // Python tests actually expect ``... + "".to_string(), + )), + + GeneralName::DirectoryName(name) => Ok(CertInfoPairOrNested::Nested( + "DirName", + Self::name_to_vec(name)?, + )), + + GeneralName::RegisteredID(oid) => { + // Convert OID to string representation + let oid_str = oid.to_id_string(); + Ok(CertInfoPairOrNested::Pair("Registered ID", oid_str)) + } + + _ => Err("Unknown type of Subject Alternative Name"), + } + }) + .collect::>()? + } else { + vec![] + }; + + Ok(Self { + OCSP: ocsp_urls, + caIssuers: issuer_urls, + crlDistributionPoints: crl_urls, + issuer: Self::name_to_vec(&cert.issuer)?, + notAfter: Self::datetime_to_string(&cert.validity.not_after)?, + notBefore: Self::datetime_to_string(&cert.validity.not_before)?, + serialNumber: serial_number, + subject: Self::name_to_vec(&cert.subject)?, + subjectAltName: alt_names, + version: cert.version.0 + 1, + }) + } + + fn name_to_vec(name: &X509Name<'_>) -> Result, &'static str> { + let mut entries = Vec::with_capacity(8); + for rdn in name.iter() { + for attr in rdn.iter() { + let attr_name = OID_MAPPINGS + .oid_to_entry + .get(attr.attr_type()) + .ok_or("unknown attribute in X509Name")? + .description(); + let attr_value = attr + .attr_value() + .as_str() + .or_else(|_| str::from_utf8(attr.attr_value().data)) + .map_err(|_| "attribute value of X509Name is not a valid UTF-8")?; + + entries.push(((attr_name.to_string(), attr_value.to_string()),)); + } + } + Ok(entries) + } + + fn datetime_to_string(date_time: &ASN1Time) -> Result { + Ok(DateTime::::from_timestamp(date_time.timestamp(), 0) + .ok_or("ASN1Time is not valid")? + .format("%b %e %H:%M:%S %Y GMT") + .to_string()) + } +} + +// +// Custom certificate verifiers. +// + +const VERIFY_CRL_CHECK_LEAF: i32 = 0x00000004; +const VERIFY_CRL_CHECK_CHAIN: i32 = 0x0000000c; + +#[derive(Debug)] +struct CustomServerCertVerifier { + verify_server_certificates: bool, + verifiers: Vec>, + check_hostname: bool, + root_hint_subjects: Vec, + crl_check_enabled_and_no_platform_verifier_and_no_crl_loaded: bool, +} + +#[derive(Debug)] +enum CrlCheck { + None, + Leaf, + Chain, +} + +impl ServerCertVerifier for CustomServerCertVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &ServerName<'_>, + ocsp_response: &[u8], + now: UnixTime, + ) -> Result { + if !self.verify_server_certificates { + // Server cert verification disabled. + return Ok(ServerCertVerified::assertion()); + } + + if self.crl_check_enabled_and_no_platform_verifier_and_no_crl_loaded { + // cpython's ssl rejects all certificates if CRL check is requested + // but no CRL loaded. + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::UnknownRevocationStatus, + )); + } + + let server_name = if !self.check_hostname + && let Some(server_name) = Self::first_server_name(end_entity) + { + // Substitute real server name with a name extracted from a server-provided + // certificate to circumvent rustls's server name check if SSLContext.check_hostname + // is False. + server_name + } else { + server_name.clone() + }; + + let mut last_ok = None; + for verifier in &self.verifiers { + let res = verifier.verify_server_cert( + end_entity, + intermediates, + &server_name, + ocsp_response, + now, + ); + + // Certificate is valid if at least one of verifiers report it as valid and other + // verifiers report "unknown issuer" because they do not have a matching root certificate. + match res { + Err(rustls::Error::InvalidCertificate(rustls::CertificateError::UnknownIssuer)) => { + } + + Ok(verified) => last_ok = Some(verified), + + Err(err) => return Err(err), // any other error from any verifier means that certificate is invalid + } + } + + if let Some(verified) = last_ok.take() { + Ok(verified) + } else { + // No verifiers but verification required. + Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::UnknownIssuer, + )) + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + if !self.verify_server_certificates { + // Server cert verification disabled. + return Ok(HandshakeSignatureValid::assertion()); + } + + self.verifiers + .first() + .ok_or(rustls::Error::InvalidCertificate( + rustls::CertificateError::BadSignature, + ))? + .verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + if !self.verify_server_certificates { + // Server cert verification disabled. + return Ok(HandshakeSignatureValid::assertion()); + } + + self.verifiers + .first() + .ok_or(rustls::Error::InvalidCertificate( + rustls::CertificateError::BadSignature, + ))? + .verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + let mut schemes = Vec::new(); + + if self.verifiers.is_empty() { + // Provide some default list when we are either not really verifying anything or reject everything. + schemes.extend_from_slice(&[ + SignatureScheme::RSA_PKCS1_SHA1, + SignatureScheme::ECDSA_SHA1_Legacy, + SignatureScheme::RSA_PKCS1_SHA256, + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::ECDSA_NISTP521_SHA512, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::ED25519, + SignatureScheme::ED448, + SignatureScheme::ML_DSA_44, + SignatureScheme::ML_DSA_65, + SignatureScheme::ML_DSA_87, + ]); + } else { + // Intersection of sets. + for (i, verifier) in self.verifiers.iter().enumerate() { + if i == 0 { + schemes.extend_from_slice(&verifier.supported_verify_schemes()) + } else { + let other_schemes = verifier.supported_verify_schemes(); + schemes.retain(|s| other_schemes.contains(s)); + } + } + } + + schemes + } + + fn requires_raw_public_keys(&self) -> bool { + self.verifiers.iter().any(|v| v.requires_raw_public_keys()) + } + + fn root_hint_subjects(&self) -> Option<&[DistinguishedName]> { + if self.root_hint_subjects.is_empty() { + None + } else { + Some(&self.root_hint_subjects) + } + } +} + +impl CustomServerCertVerifier { + fn new( + verify_server_certificates: bool, + use_system_certificates: bool, + cert_store: &CertStore, + crypto: Arc, + check_hostname: bool, + crl_check: CrlCheck, + ) -> SslResult { + if !verify_server_certificates { + // Server cert verification disabled. + return Ok(Self { + verify_server_certificates: false, + verifiers: vec![], + check_hostname: false, + root_hint_subjects: vec![], + crl_check_enabled_and_no_platform_verifier_and_no_crl_loaded: false, + }); + } + + let mut verifiers = Vec::>::with_capacity(2); + let mut root_hint_subjects = Vec::new(); + + // WebPkiServerVerifier + if cert_store.certs.is_empty() { + if !matches!(crl_check, CrlCheck::None) && !cert_store.crls.is_empty() { + return Err(SslError::Ssl( + "rustls is unable to check certificate revocation with WebPkiServerVerifier but \ + verify certificates using default platform verifier".to_string(), + )); + } + } else { + let mut builder = WebPkiServerVerifier::builder_with_provider( + Arc::new(cert_store.certs.clone()), + crypto.clone(), + ); + if !matches!(crl_check, CrlCheck::None) { + builder = builder.with_crls(cert_store.crls.clone()); + if matches!(crl_check, CrlCheck::Leaf) { + builder = builder.only_check_end_entity_revocation(); + } + } + let webpki = builder.build().map_err(|e| { + SslError::Ssl(format!("failed to create WebPkiServerVerifier: {e}")) + })?; + + root_hint_subjects.extend_from_slice(webpki.root_hint_subjects().unwrap_or(&[])); + verifiers.push(webpki); + }; + + // Platform verifier. + if use_system_certificates { + let platform_verifier = + rustls_platform_verifier::Verifier::new(crypto).map_err(|e| { + SslError::Ssl(format!( + "failed to create rustls_platform_verifier::Verifier: {e}" + )) + })?; + + root_hint_subjects + .extend_from_slice(platform_verifier.root_hint_subjects().unwrap_or(&[])); + verifiers.push(Arc::new(platform_verifier)); + }; + + Ok(Self { + verify_server_certificates, + verifiers, + check_hostname, + root_hint_subjects, + + crl_check_enabled_and_no_platform_verifier_and_no_crl_loaded: !matches!( + crl_check, + CrlCheck::None + ) + && !use_system_certificates + && cert_store.crls.is_empty(), + }) + } + + fn first_server_name<'a>(end_entity: &'a CertificateDer<'a>) -> Option> { + let (_, cert) = parse_x509_certificate(end_entity.as_ref()).ok()?; + let san = cert.subject_alternative_name().ok().flatten()?; + san.value.general_names.iter().find_map(|name| match name { + GeneralName::DNSName(dns) => DnsName::try_from_str(dns).ok().map(ServerName::DnsName), + + GeneralName::IPAddress(ip) => match ip.len() { + 4 => Some(ServerName::IpAddress(IpAddr::V4(Ipv4Addr::from([ + ip[0], ip[1], ip[2], ip[3], + ])))), + + 16 => Some(ServerName::IpAddress(IpAddr::V6( + Ipv6Addr::from([ + ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7], ip[8], ip[9], + ip[10], ip[11], ip[12], ip[13], ip[14], ip[15], + ]) + .into(), + ))), + + _ => None, + }, + + _ => None, + }) + } +} + +impl CrlCheck { + fn from_verify_flags(flags: i32) -> Self { + if (flags & VERIFY_CRL_CHECK_CHAIN) != 0 { + Self::Chain + } else if (flags & VERIFY_CRL_CHECK_LEAF) != 0 { + Self::Leaf + } else { + Self::None + } + } +} + +#[derive(Debug)] +struct CertStore { + certs: RootCertStore, + raw_ca_certs: Vec>, + crls: Vec>, + known: HashSet>, + stats: Arc, +} + +impl CertStore { + fn empty(stats: Arc) -> Self { + Self { + certs: RootCertStore::empty(), + raw_ca_certs: vec![], + crls: vec![], + known: HashSet::new(), + stats, + } + } + + fn add_ders(&mut self, ders: &[DerBytes]) { + for der in ders { + match der.kind { + DerKind::Cert => self.add_cert(&der.bytes), + DerKind::Crl => self.add_crl(&der.bytes), + DerKind::Key => {} // ignore private keys + } + } + } + + fn add_cert(&mut self, cert: &[u8]) { + let hash = Self::hash_bytes(cert); + if self.known.contains(&hash) { + // Do not add duplicates. + return; + } + let _ = self.known.insert(hash); + + let Ok((_, parsed)) = parse_x509_certificate(cert) else { + // Silently skip invalid certificates, like OpenSSL does. + return; + }; + + if parsed.is_ca() || (parsed.subject() == parsed.issuer()) { + // Add self-signed non-CA (no Basic Constraints) certs too. + let cert_der = CertificateDer::from_slice(cert); + if self.certs.add(cert_der).is_ok() { + let _ = self.stats.cert_store.x509.fetch_add(1, Ordering::Relaxed); + + if parsed.is_ca() || parsed.version().0 == 0 { + // Treat self-signed non-CA certs as CA only if version is 0. + // This matches cpython/OpenSSL behaviour. + self.raw_ca_certs.push(cert.to_vec()); + let _ = self + .stats + .cert_store + .x509_ca + .fetch_add(1, Ordering::Relaxed); + } + } + } + } + + fn add_crl(&mut self, crl: &[u8]) { + let hash = Self::hash_bytes(crl); + if self.known.contains(&hash) { + // Do not add duplicates. + return; + } + let _ = self.known.insert(hash); + + if parse_x509_crl(crl).is_ok() { + let crl = CertificateRevocationListDer::from(crl.to_vec()); + self.crls.push(crl); + let _ = self.stats.cert_store.crl.fetch_add(1, Ordering::Relaxed); + } + } + + fn hash_bytes(cert: &[u8]) -> Vec { + Sha256::digest(cert).to_vec() + } + + fn all_certs(&self) -> &[Vec] { + &self.raw_ca_certs + } +} + +// +// Stats +// + +#[derive(Default, Debug)] +struct Stats { + cert_store: CertStoreStats, + session: SessionStats, +} + +#[derive(Serialize, Default, Debug)] +struct CertStoreStats { + #[serde(serialize_with = "serialize_atomic_usize")] + crl: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + x509: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + x509_ca: AtomicUsize, +} + +#[derive(Serialize, Default, Debug)] +struct SessionStats { + #[serde(serialize_with = "serialize_atomic_usize")] + number: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + connect: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + connect_good: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + connect_renegotiate: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + accept: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + accept_good: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + accept_renegotiate: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + hits: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + misses: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + timeouts: AtomicUsize, + + #[serde(serialize_with = "serialize_atomic_usize")] + cache_full: AtomicUsize, +} + +fn serialize_atomic_usize(atomic: &AtomicUsize, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_u64(atomic.load(Ordering::Relaxed) as u64) +} + +// +// OpenSSL cipher string, see `man openssl-ciphers` for details. +// + +struct CipherList<'a> { + ops: Vec>, +} + +enum CipherFilterOp<'a> { + /// The cipher string @STRENGTH can be used at any point to sort the current cipher list in order of encryption + /// algorithm key length. + Strength, + + /// The cipher string @SECLEVEL=n can be used at any point to set the security level to n. + SecLevel(usize), + + /// Just add matching ciphers to the end of the current list. + Append(CipherFilterSubOpList<'a>), + + /// If ! is used then the ciphers are permanently deleted from the list. The ciphers deleted can never reappear + /// in the list even if they are explicitly stated. + DelAndBlock(CipherFilterSubOpList<'a>), + + /// If - is used then the ciphers are deleted from the list, but some or all of the ciphers can be added again + /// by later options. + Del(CipherFilterSubOpList<'a>), + + /// If + is used then the ciphers are moved to the end of the list. This option doesn't add any new ciphers it + /// just moves matching existing ones. + MoveToEnd(CipherFilterSubOpList<'a>), +} + +struct CipherFilterSubOpList<'a> { + sub_ops: Vec>, +} + +enum CipherFilterSubOp<'a> { + /// Default cipher list. Valid only as a first operation. + Default, + + /// The ciphers included in ALL, but not enabled by default. + ComplementOfDefault, + + /// All cipher suites except the eNULL ciphers. + All, + + /// The cipher suites not enabled by ALL, currently eNULL. + ComplementOfAll, + + /// The list of enabled cipher suites will be loaded from the system crypto policy configuration file. + ProfileSystem, + + /// "High" encryption cipher suites. + High, + + /// "Medium" encryption cipher suites. + Medium, + + /// "Low" encryption cipher suites. + Low, + + /// Lists cipher suites which are only supported in at least TLS v1.0. + TlsV10, + + /// Lists cipher suites which are only supported in at least TLS v1.2. + TlsV12, + + /// Lists cipher suites which are only supported in at least SSL v3. + SslV3, + + /// Enables suite B mode of operation. + SuiteB(SuiteBType), + + /// All cipher suites using encryption algorithm in Cipher Block Chaining (CBC) mode. + Cbc, + + /// AES in Galois Counter Mode (GCM): these cipher suites are only supported in TLS v1.2. + AesGcm, + + /// Match by message authentication algorithm. + Auth(&'a str), + + /// Match by key exchange algorithm. + KeyEx(&'a str), + + /// Match by part of an OpenSSL name that usually contains key exchange algorithm and symmetric cipher + /// and may contain other identifiers. + Part(&'a str), + + /// Match by full OpenSSL or IANA cipher name. + Full(&'a str), +} + +enum SuiteBType { + Use128Permit192, + Use128Only, + Use192Only, +} + +impl<'a> CipherList<'a> { + fn parse_to_rustls( + s: &'a str, + ) -> Result>, &'static str> { + Self::parse(s)?.to_rustls() + } + + fn parse(s: &'a str) -> Result { + let ops: Vec<_> = s + .split(|c: char| c == ':' || c == ',' || c.is_ascii_whitespace()) + .filter(|s| !s.is_empty()) + .enumerate() + .map(|(i, s)| { + let suite_b = match s { + "SUITEB128" => Some(CipherFilterSubOp::SuiteB(SuiteBType::Use128Permit192)), + "SUITEB128ONLY" => Some(CipherFilterSubOp::SuiteB(SuiteBType::Use128Only)), + "SUITEB192" => Some(CipherFilterSubOp::SuiteB(SuiteBType::Use192Only)), + _ => None, + }; + + match (i, s, suite_b) { + (0, "DEFAULT", _) => Ok(CipherFilterOp::Append( + CipherFilterSubOpList::from_sub_op(CipherFilterSubOp::Default), + )), + + (0, _, Some(suite_b)) => Ok(CipherFilterOp::Append( + CipherFilterSubOpList::from_sub_op(suite_b), + )), + + (_, _, _) => CipherFilterOp::parse(s), + } + }) + .collect::>()?; + if ops.is_empty() { + Err("list of ciphers is empty") + } else { + Ok(Self { ops }) + } + } + + fn to_rustls(&self) -> Result>, &'static str> { + let mut min_bits = SECURITY_LEVEL_TO_MIN_BITS[0]; + let mut block_list = Vec::new(); + let mut ids = Vec::new(); + + let sanitize = |ids: &mut Vec, min_bits, block_list: &[u16]| { + ids.retain(|id| CIPHER_MAPPINGS.id_to_bits[id] >= min_bits); + ids.retain(|id| !block_list.contains(id)); + }; + let extend = |ids: &mut Vec, source: &[u16]| { + // Extend and deduplicate. + for id in source { + if !ids.contains(id) { + ids.push(*id); + } + } + }; + let ids_to_suits = |ids: &[u16]| { + ids.iter() + .map(|id| *CIPHER_MAPPINGS.id_to_cipher[id]) + .collect() + }; + + for op in &self.ops { + match op { + CipherFilterOp::Strength => { + ids.sort_by_key(|id| -i32::from(CIPHER_MAPPINGS.id_to_bits[id])) + } + + CipherFilterOp::SecLevel(level) => { + min_bits = *SECURITY_LEVEL_TO_MIN_BITS + .get(*level) + .ok_or("@SECLEVEL value too big")?; + sanitize(&mut ids, min_bits, &block_list); + } + + CipherFilterOp::Append(sub_op_list) => { + let (mut new_ids, suite_b) = sub_op_list.to_rustls_ids()?; + if suite_b.is_some() { + // SUITEB* cipherstrings should appear first in the cipher list and anything + // after them is ignored. + return Ok((ids_to_suits(&new_ids), suite_b)); + } + sanitize(&mut new_ids, min_bits, &block_list); + extend(&mut ids, &new_ids); + } + + CipherFilterOp::DelAndBlock(sub_op_list) => { + extend(&mut block_list, &sub_op_list.to_rustls_ids()?.0); + sanitize(&mut ids, min_bits, &block_list); + } + + CipherFilterOp::Del(sub_op_list) => { + let (del_ids, _) = sub_op_list.to_rustls_ids()?; + ids.retain(|id| !del_ids.contains(id)); + } + + CipherFilterOp::MoveToEnd(sub_op_list) => { + let (move_ids, _) = sub_op_list.to_rustls_ids()?; + ids.sort_by_key(|id| move_ids.contains(id)) + } + } + } + + Ok((ids_to_suits(&ids), None)) + } +} + +impl<'a> CipherFilterOp<'a> { + fn parse(mut s: &'a str) -> Result { + if s == "@STRENGTH" { + return Ok(Self::Strength); + } + const SECLEVEL: &str = "@SECLEVEL="; + if s.starts_with(SECLEVEL) { + return Ok(Self::SecLevel( + usize::from_str(s.get(SECLEVEL.len()..).unwrap_or("")) + .map_err(|_| "invalid @SECLEVEL value")?, + )); + } + + let prefix = s.get(..1).unwrap_or(""); + if ["!", "-", "+"].contains(&prefix) { + s = s.get(1..).unwrap_or(""); + } + Ok(match prefix { + "!" => Self::DelAndBlock(CipherFilterSubOpList::parse(s)?), + "-" => Self::Del(CipherFilterSubOpList::parse(s)?), + "+" => Self::MoveToEnd(CipherFilterSubOpList::parse(s)?), + _ => Self::Append(CipherFilterSubOpList::parse(s)?), + }) + } +} + +impl<'a> CipherFilterSubOpList<'a> { + fn parse(s: &'a str) -> Result { + let sub_ops: Vec<_> = s + .split('+') + .filter(|s| !s.is_empty()) + .map(CipherFilterSubOp::parse) + .collect::>()?; + if sub_ops.is_empty() { + Err("list of cipher filtering operations is empty") + } else { + Ok(Self { sub_ops }) + } + } + + fn from_sub_op(sub_op: CipherFilterSubOp<'a>) -> Self { + Self { + sub_ops: vec![sub_op], + } + } + + fn to_rustls_ids(&self) -> Result>, &'static str> { + let mut ids = Vec::new(); + for sub_op in &self.sub_ops { + match sub_op { + CipherFilterSubOp::Default => { + Self::extend_or_intersect(&mut ids, &CIPHER_MAPPINGS.default) + } + + CipherFilterSubOp::ComplementOfDefault => { + Self::extend_or_intersect(&mut ids, &CIPHER_MAPPINGS.complement_of_default) + } + + CipherFilterSubOp::All => Self::extend_or_intersect(&mut ids, &CIPHER_MAPPINGS.all), + + CipherFilterSubOp::ComplementOfAll => { + Self::extend_or_intersect(&mut ids, &CIPHER_MAPPINGS.complement_of_all) + } + + CipherFilterSubOp::ProfileSystem => { + return Err( + "reading cipher suites from system crypto policy file is not supported with rustls", + ); + } + + // Here we trust that all default rustls cipher suites can be considered "high". + CipherFilterSubOp::High => { + Self::extend_or_intersect(&mut ids, &CIPHER_MAPPINGS.default) + } + CipherFilterSubOp::Medium => { + Self::extend_or_intersect(&mut ids, &CIPHER_MAPPINGS.default) + } + CipherFilterSubOp::Low => { + Self::extend_or_intersect(&mut ids, &CIPHER_MAPPINGS.default) + } + + CipherFilterSubOp::TlsV10 | CipherFilterSubOp::SslV3 => {} // rustls does not support older ciphers + + CipherFilterSubOp::TlsV12 => { + Self::extend_or_intersect(&mut ids, &CIPHER_MAPPINGS.tls_1_2) + } + + // RFC 6460 + CipherFilterSubOp::SuiteB(SuiteBType::Use128Permit192) => { + return Ok(( + vec![ + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.into(), + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.into(), + ], + Some(vec![ + kx_group_by_name(rustls::NamedGroup::secp256r1, "secp256r1")?, + kx_group_by_name(rustls::NamedGroup::secp384r1, "secp384r1")?, + ]), + )); + } + CipherFilterSubOp::SuiteB(SuiteBType::Use128Only) => { + return Ok(( + vec![CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.into()], + Some(vec![kx_group_by_name( + rustls::NamedGroup::secp256r1, + "secp256r1", + )?]), + )); + } + CipherFilterSubOp::SuiteB(SuiteBType::Use192Only) => { + return Ok(( + vec![CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.into()], + Some(vec![kx_group_by_name( + rustls::NamedGroup::secp384r1, + "secp384r1", + )?]), + )); + } + + CipherFilterSubOp::Cbc => { + let mut rhs = Vec::with_capacity(CIPHER_MAPPINGS.iana_to_id.len()); + + // OpenSSL names might contain either -CBC- or -CBC3-, IANA seems to only contain _CBC_. + for (iana, rustls_id) in &CIPHER_MAPPINGS.iana_to_id { + if iana.split('_').any(|s| s == "CBC") { + rhs.push(*rustls_id) + } + } + + Self::extend_or_intersect(&mut ids, &rhs) + } + + CipherFilterSubOp::AesGcm => { + let mut rhs = Vec::with_capacity(CIPHER_MAPPINGS.iana_to_id.len()); + + for (openssl, rustls_id) in &CIPHER_MAPPINGS.openssl_to_id { + if openssl.split(['-', '_']).any(|s| s.starts_with("AES")) + && openssl.split(['-', '_']).any(|s| s == "GCM") + { + rhs.push(*rustls_id) + } + } + + Self::extend_or_intersect(&mut ids, &rhs) + } + + CipherFilterSubOp::Auth(auth) => { + let rhs: Vec<_> = CIPHER_MAPPINGS + .id_to_cipher + .iter() + .filter_map(|(k, v)| { + match v { + SupportedCipherSuite::Tls12(c) => { + let mut maybe_id = None; + for scheme in c.sign { + if scheme + .as_str() + .is_some_and(|s| s.split('_').any(|s| s == *auth)) + { + maybe_id = Some(*k); + } + } + maybe_id + } + + // usable_for_signature_algorithm() always returns true for TLS 1.3. + SupportedCipherSuite::Tls13(_) => Some(*k), + } + }) + .collect(); + Self::extend_or_intersect(&mut ids, &rhs) + } + + CipherFilterSubOp::KeyEx(key_ex) => { + let rhs: Vec<_> = CIPHER_MAPPINGS + .id_to_key_ex + .iter() + .filter_map(|(k, v)| if v == key_ex { Some(*k) } else { None }) + .collect(); + Self::extend_or_intersect(&mut ids, &rhs) + } + + CipherFilterSubOp::Part(part) => { + let mut rhs = Vec::with_capacity(CIPHER_MAPPINGS.iana_to_id.len()); + + for (openssl, rustls_id) in &CIPHER_MAPPINGS.openssl_to_id { + if openssl.split(['-', '_']).any(|s| &s == part) { + rhs.push(*rustls_id) + } + } + + Self::extend_or_intersect(&mut ids, &rhs) + } + + CipherFilterSubOp::Full(full) => { + if let Some(id) = CIPHER_MAPPINGS + .openssl_to_id + .get(full) + .or_else(|| CIPHER_MAPPINGS.iana_to_id.get(full)) + { + Self::extend_or_intersect(&mut ids, &[*id]) + } + } + } + } + Ok((ids, None)) + } + + fn extend_or_intersect(lhs: &mut Vec, rhs: &[u16]) { + if lhs.is_empty() { + lhs.extend_from_slice(rhs) + } else { + lhs.retain(|id| rhs.contains(id)) + } + } +} + +fn kx_group_by_name( + name: rustls::NamedGroup, + error_name: &'static str, +) -> Result<&'static dyn SupportedKxGroup, &'static str> { + CryptoExt::get_ext() + .all_kx_or_default() + .iter() + .find(|g| g.name() == name) + .copied() + .ok_or(error_name) +} + +type WithOptionSuiteB = (T, Option>); + +impl<'a> CipherFilterSubOp<'a> { + fn parse(mut s: &'a str) -> Result { + Ok(match s { + "DEFAULT" => return Err("DEFAULT specified at wrong position in the cipher string"), + "SUITEB128" => { + return Err("SUITEB128 specified at wrong position in the cipher string"); + } + "SUITEB128ONLY" => { + return Err("SUITEB128ONLY specified at wrong position in the cipher string"); + } + "SUITEB192" => { + return Err("SUITEB192 specified at wrong position in the cipher string"); + } + + "COMPLEMENTOFDEFAULT" => Self::ComplementOfDefault, + "ALL" => Self::All, + "COMPLEMENTOFALL" => Self::ComplementOfAll, + "PROFILE=SYSTEM" => Self::ProfileSystem, + "HIGH" => Self::High, + "MEDIUM" => Self::Medium, + "LOW" => Self::Low, + "TLSv1.0" => Self::TlsV10, + "TLSv1.2" => Self::TlsV12, + "SSLv3" => Self::SslV3, + "CBC" => Self::Cbc, + "AESGCM" => Self::AesGcm, + + // RSA is an alias for kRSA. + "RSA" => Self::KeyEx("RSA"), + + _ => { + let prefix = s.get(..1).unwrap_or(""); + if ["a", "k", "e"].contains(&prefix) { + s = s.get(1..).unwrap_or(""); + } + + if s.is_empty() { + return Err("item of cipher string is empty"); + } + if !s + .chars() + .all(|c| char::is_ascii_alphanumeric(&c) || matches!(c, '-' | '_')) + { + return Err("item of cipher string contains invalid characters"); + } + + match prefix { + "a" => Self::Auth(s), + "k" => Self::KeyEx(s), + "e" => Self::Part(s), + + _ => { + if s.contains(['_', '-']) { + Self::Full(s) + } else { + Self::Part(s) + } + } + } + } + }) + } +} + +static CIPHER_MAPPINGS: LazyLock = LazyLock::new(CipherMappings::new); + +struct CipherMappings { + complement_of_default: Vec, + complement_of_all: Vec, + default: Vec, + all: Vec, + tls_1_2: Vec, + // TODO: Consolidate id_to_* into single HashMap. + id_to_openssl: HashMap, + id_to_key_ex: HashMap, + id_to_bits: HashMap, + id_to_cipher: HashMap, + openssl_to_id: HashMap<&'static str, u16>, + iana_to_id: HashMap<&'static str, u16>, + + name_to_kx_group: HashMap, +} + +impl CipherMappings { + fn new() -> Self { + let all_cipher_suites = CryptoExt::get_ext().all_ciphers_or_default(); + let default_cipher_suites = CryptoExt::get_ext().default_ciphers_or_provider(); + + let mut all = Vec::with_capacity(all_cipher_suites.len()); + let mut tls_1_2 = Vec::with_capacity(all_cipher_suites.len()); + let mut id_to_openssl = HashMap::with_capacity(all_cipher_suites.len()); + let mut id_to_key_ex = HashMap::with_capacity(all_cipher_suites.len()); + let mut id_to_bits = HashMap::with_capacity(all_cipher_suites.len()); + let mut id_to_cipher = HashMap::with_capacity(all_cipher_suites.len()); + let mut openssl_to_id = HashMap::with_capacity(all_cipher_suites.len()); + let mut iana_to_id = HashMap::with_capacity(all_cipher_suites.len()); + + for cipher in all_cipher_suites { + // See https://www.ssl.org/cipher-suite-mapping + let (openssl, iana, key_ex, bits, min_tls_ver) = match cipher.suite() { + CipherSuite::TLS13_AES_256_GCM_SHA384 => ( + "TLS_AES_256_GCM_SHA384", + "TLS_AES_256_GCM_SHA384", + "ECDH", + 256, + 13, + ), + + CipherSuite::TLS13_AES_128_GCM_SHA256 => ( + "TLS_AES_128_GCM_SHA256", + "TLS_AES_128_GCM_SHA256", + "ECDH", + 128, + 13, + ), + + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 => ( + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_CHACHA20_POLY1305_SHA256", + "ECDH", + 256, + 13, + ), + + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 => ( + "ECDHE-ECDSA-AES256-GCM-SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "ECDH", + 256, + 12, + ), + + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 => ( + "ECDHE-ECDSA-AES128-GCM-SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "ECDH", + 128, + 12, + ), + + CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 => ( + "ECDHE-ECDSA-CHACHA20-POLY1305", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "ECDH", + 256, + 12, + ), + + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 => ( + "ECDHE-RSA-AES256-GCM-SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "ECDH", + 256, + 12, + ), + + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 => ( + "ECDHE-RSA-AES128-GCM-SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "ECDH", + 128, + 12, + ), + + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 => ( + "ECDHE-RSA-CHACHA20-POLY1305", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "ECDH", + 256, + 12, + ), + + // This is tested by that_all_rustls_ciphers_are_known(). + // This may happen after rustls update, just add more ciphers above is this case. + _ => unreachable!("BUG: Unknown cipher suite {cipher:?}"), + }; + + let id = cipher.suite().into(); + + if bits > 0 { + all.push(id); + } + if min_tls_ver >= 12 { + tls_1_2.push(id); + } + let _ = id_to_openssl.insert(id, openssl); + let _ = id_to_key_ex.insert(id, key_ex); + let _ = id_to_bits.insert(id, bits); + let _ = id_to_cipher.insert(id, cipher); + let _ = openssl_to_id.insert(openssl, id); + let _ = iana_to_id.insert(iana, id); + } + + let default: Vec<_> = default_cipher_suites + .iter() + .map(|c| u16::from(c.suite())) + .collect(); + + Self { + complement_of_default: all_cipher_suites + .iter() + .filter(|c| !default.contains(&c.suite().into())) + .map(|c| u16::from(c.suite())) + .collect(), + complement_of_all: all_cipher_suites + .iter() + .filter(|c| !all.contains(&c.suite().into())) + .map(|c| u16::from(c.suite())) + .collect(), + + default, + all, + tls_1_2, + id_to_openssl, + id_to_key_ex, + id_to_bits, + id_to_cipher, + openssl_to_id, + iana_to_id, + + name_to_kx_group: CryptoExt::get_ext() + .all_kx_or_default() + .iter() + .map(|g| (kx_group_openssl_name(*g).to_owned(), *g)) + .collect(), + } + } +} + +fn kx_group_openssl_name(group: &dyn SupportedKxGroup) -> &'static str { + match group.name() { + rustls::NamedGroup::secp256r1 => "prime256v1", + rustls::NamedGroup::secp384r1 => "secp384r1", + rustls::NamedGroup::X25519 => "X25519", + rustls::NamedGroup::MLKEM768 => "MLKEM768", + rustls::NamedGroup::MLKEM1024 => "MLKEM1024", + rustls::NamedGroup::secp256r1MLKEM768 => "SecP256r1MLKEM768", + rustls::NamedGroup::X25519MLKEM768 => "X25519MLKEM768", + + // This is tested by that_all_rustls_kx_groups_have_openssl_names() + name => unreachable!("BUG: Unknown key exchange group {name:?}"), + } +} + +// See `man SSL_CTX_set_security_level` for details. +const SECURITY_LEVEL_TO_MIN_BITS: &[u16] = &[0, 80, 112, 128, 192, 256]; + +// +// Oid registry for txt2obj() and nid2obj() +// + +static OID_MAPPINGS: LazyLock = LazyLock::new(OidMappings::new); + +struct OidMappings { + name_to_oid: HashMap<&'static str, Oid<'static>>, + oid_to_entry: OidRegistry<'static>, + oid_sn_to_nid: HashMap<(Oid<'static>, &'static str), u16>, + nid_to_oid: HashMap>, +} + +impl OidMappings { + fn new() -> Self { + let mut name_to_oid = HashMap::new(); + let mut oid_to_entry = OidRegistry::default(); + let mut oid_sn_to_nid = HashMap::new(); + let mut nid_to_oid = HashMap::new(); + + // See https://github.com/openssl/openssl/blob/11b7b6ea3b65a584e1d31408ed1bdb139465cffd/crypto/objects/README.md + // See https://github.com/openssl/openssl/blob/11b7b6ea3b65a584e1d31408ed1bdb139465cffd/crypto/objects/objects.pl + // TODO: Do this in compile time. + let obj_mac_num = include_str!("rustls-data/obj_mac.num"); + let objects_txt = include_str!("rustls-data/objects.txt"); + + let nids: HashMap<_, _> = obj_mac_num + .split('\n') + .map(str::trim) + .filter(|l| !l.is_empty()) + .map(str::split_whitespace) + .map(|mut line| { + let name = line.next().expect("BUG: Impossible"); + let nid = line + .next() + .expect("BUG: No NID") + .parse() + .expect("BUG: Invalid NID"); + (name, nid) + }) + .collect(); + + let mut aliases: HashMap, Rc>> = HashMap::new(); + let mut cname = None; + let mut module: Option<&'static str> = None; + for line in objects_txt + .split('\n') + .map(str::trim) + .filter(|l| !l.is_empty()) + .filter(|l| !l.starts_with('#')) + { + let prepend_module = |s: &str| { + if let Some(module) = module { + let mut str_buf = String::with_capacity(module.len() + 1 + s.len()); + str_buf.push_str(module); + str_buf.push('_'); + str_buf.push_str(s); + Rc::new(str_buf.replace('-', "_")) + } else { + Rc::new(s.replace('-', "_")) + } + }; + + if line.starts_with('!') { + let mut splitted = line.split_whitespace(); + match splitted.next().expect("BUG: Impossible") { + "!Alias" => { + let alias = splitted.next().expect("BUG: No alias after !Alias"); + + let mut oid = Vec::with_capacity(16); + for oid_part in splitted { + // Resolve aliases to make sure that they are not recursive. + if let Some(oid_part) = aliases.get(&oid_part.replace('-', "_")) { + oid.extend_from_slice(oid_part); + } else { + oid.push( + u64::from_str(oid_part) + .expect("BUG: OID part in alias can not be parsed as u64"), + ); + } + } + assert!(!oid.is_empty(), "BUG: Empty OID for alias {alias}"); + let res = aliases.insert(prepend_module(alias), Rc::new(oid)); + assert!(res.is_none(), "BUG: Duplicate alias {alias}"); + } + + "!Cname" => { + assert!(cname.is_none(), "BUG: Double !Cname"); + cname = Some(splitted.next().expect("BUG: No name after !Cname")); + assert!( + splitted.next().is_none(), + "BUG: Extra elements after !Cname" + ); + } + + "!module" => { + assert!(module.is_none(), "BUG: Double !module"); + module = Some(splitted.next().expect("BUG: No name after !module")); + assert!( + splitted.next().is_none(), + "BUG: Extra elements after !module" + ); + } + + "!global" => { + assert!(module.is_some(), "BUG: !global without !module"); + module = None; + assert!( + splitted.next().is_none(), + "BUG: Extra elements after !global" + ); + } + + cmd => panic!("BUG: Unknown objects.txt command: {cmd}"), + } + continue; + } + + // OID string + let mut line = line.split(':').map(|s| s.trim()); + let oid_str = line.next().expect("BUG: No OID"); + let mut oid = Vec::with_capacity(16); + for oid_part in oid_str.split_whitespace() { + if let Some(oid_part) = aliases.get(&oid_part.replace('-', "_")) { + oid.extend_from_slice(oid_part); + } else { + oid.push( + u64::from_str(oid_part).expect("BUG: OID part can not be parsed as u64"), + ); + } + } + let oid = Rc::new(oid); + + // Short name and description + let sn = line.next().expect("BUG: No SN"); + let desc = line.next(); + if desc.is_some() { + assert!( + line.next().is_none(), + "BUG: Extra elements after OID, SN and description" + ); + } + let desc = desc.unwrap_or(""); + + // Add into aliases. + let mut added_now = Vec::with_capacity(3); + if let Some(cname) = cname.take() { + let owned_cname = prepend_module(cname); + added_now.push(owned_cname.clone()); + let res = aliases.insert(owned_cname.clone(), oid.clone()); + assert!(res.is_none(), "BUG: Duplicate cname {owned_cname}"); + }; + if !desc.is_empty() { + let owned_desc = prepend_module(desc); + if !added_now.contains(&owned_desc) { + added_now.push(owned_desc.clone()); + let res = aliases.insert(owned_desc.clone(), oid.clone()); + assert!(res.is_none(), "BUG: Duplicate description {owned_desc}"); + } + }; + if !sn.is_empty() { + let owned_sn = prepend_module(sn); + if !added_now.contains(&owned_sn) { + added_now.push(owned_sn.clone()); + let res = aliases.insert(owned_sn.clone(), oid.clone()); + assert!(res.is_none(), "BUG: Duplicate SN {owned_sn}"); + } + } + + if matches!(oid.as_slice(), [] | [1 | 2]) { + // Can not be added into registry. + continue; + } + let owned_oid = Oid::from(&oid).expect("BUG: Invalid OID array"); + if !sn.is_empty() { + let res = name_to_oid.insert(sn, owned_oid.clone()); + assert!( + res.is_none(), + "BUG: Duplicate SN -> OID mapping: {sn} -> {owned_oid}" + ); + } + if !desc.is_empty() && desc != sn { + let res = name_to_oid.insert(desc, owned_oid.clone()); + assert!( + res.is_none(), + "BUG: Duplicate Description -> OID mapping: {sn} -> {owned_oid}" + ); + } + // Allow some duplicated OIDs. + if oid_to_entry + .insert(owned_oid.clone(), OidEntry::new(sn, desc)) + .is_some() + && !matches!(oid.as_slice(), [1, 3]) + { + panic!("BUG: Duplicate OID: {oid:?}"); + } + + for added in &added_now { + if let Some(nid) = nids.get(added.as_str()) { + let res = oid_sn_to_nid.insert((owned_oid.clone(), sn), *nid); + assert!( + res.is_none(), + "BUG: Duplicate (OID, SN) -> NID mapping: ({owned_oid}, {sn}) -> {nid}" + ); + break; + } + } + for added in &added_now { + if let Some(nid) = nids.get(added.as_str()) { + let res = nid_to_oid.insert(*nid, owned_oid.clone()); + assert!( + res.is_none(), + "BUG: Duplicate NID -> OID mapping: {nid} -> {owned_oid}" + ); + } + } + } + assert!(cname.is_none(), "BUG: Unused !Cname"); + assert!(module.is_none(), "BUG: !module not closed"); + + Self { + name_to_oid, + oid_to_entry, + oid_sn_to_nid, + nid_to_oid, + } + } +} + +// TODO: Test with different providers. +#[cfg(test)] +mod tests { + use core::hint::black_box; + + use std::sync::Once; + + use rustls::crypto::aws_lc_rs; + + use super::*; + + #[test] + fn that_all_rustls_tls_versions_are_known() { + install_test_crypto_provider(); + for cipher in CryptoExt::get_ext().all_ciphers_or_default() { + let _ = black_box(CipherDescriptionDict::new(cipher)); + } + } + + #[test] + fn that_all_rustls_ciphers_are_known() { + install_test_crypto_provider(); + let _ = black_box(&CIPHER_MAPPINGS.id_to_openssl); + } + + #[test] + fn that_all_rustls_kx_groups_have_openssl_names() { + install_test_crypto_provider(); + let _ = black_box(&CIPHER_MAPPINGS.name_to_kx_group); + } + + #[test] + fn cipher_list_default_and_names() { + install_test_crypto_provider(); + + let default = CryptoExt::get_ext() + .default_ciphers_or_provider() + .iter() + .map(|suite| suite.suite()) + .collect::>(); + let (suites, suite_b) = CipherList::parse_to_rustls("DEFAULT").unwrap(); + + assert!(suite_b.is_none()); + assert_eq!( + suites.iter().map(|suite| suite.suite()).collect::>(), + default + ); + assert_eq!( + cipher_names("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, ECDHE-ECDSA-AES128-GCM-SHA256"), + [ + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + ] + ); + assert_eq!( + cipher_names("AES128+aECDSA"), + ["ECDHE-ECDSA-AES128-GCM-SHA256"] + ); + } + + #[test] + fn cipher_list_deletes_and_moves() { + install_test_crypto_provider(); + + assert_eq!( + cipher_names( + "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:+ECDHE-ECDSA-AES128-GCM-SHA256" + ), + [ + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES128-GCM-SHA256", + ] + ); + assert_eq!( + cipher_names( + "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:-ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:!ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256" + ), + ["ECDHE-ECDSA-AES128-GCM-SHA256"] + ); + } + + #[test] + fn cipher_list_strength_and_security_level() { + install_test_crypto_provider(); + + assert_eq!( + cipher_names("ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:@STRENGTH"), + ["ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-RSA-AES128-GCM-SHA256"] + ); + assert_eq!( + cipher_names("ECDHE-RSA-AES128-GCM-SHA256:@SECLEVEL=4:ECDHE-RSA-AES256-GCM-SHA384"), + ["ECDHE-RSA-AES256-GCM-SHA384"] + ); + } + + #[test] + fn cipher_list_suite_b() { + install_test_crypto_provider(); + + let (suites, suite_b) = CipherList::parse_to_rustls("SUITEB128:ALL").unwrap(); + + assert_eq!( + suites.iter().map(|suite| suite.suite()).collect::>(), + [ + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + ] + ); + assert!(suite_b.is_some()); + assert!(CipherList::parse_to_rustls("ALL:SUITEB128").is_err()); + } + + #[test] + fn cipher_list_errors() { + install_test_crypto_provider(); + + assert!(CipherList::parse_to_rustls("ALL:DEFAULT").is_err()); + assert!(CipherList::parse_to_rustls("ALL:@SECLEVEL=6").is_err()); + assert!(CipherList::parse_to_rustls("PROFILE=SYSTEM").is_err()); + assert!(CipherList::parse_to_rustls(";").is_err()); + assert!(CipherList::parse_to_rustls("").is_err()); + } + + fn cipher_names(s: &str) -> Vec<&'static str> { + install_test_crypto_provider(); + + let (suites, suite_b) = CipherList::parse_to_rustls(s).unwrap(); + assert!(suite_b.is_none()); + suites + .iter() + .map(|suite| { + let id: u16 = suite.suite().into(); + CIPHER_MAPPINGS.id_to_openssl[&id] + }) + .collect() + } + + fn install_test_crypto_provider() { + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + let ext = CryptoExt { + all_cipher_suites: Some(aws_lc_rs::ALL_CIPHER_SUITES), + default_cipher_suites: Some(aws_lc_rs::DEFAULT_CIPHER_SUITES), + all_kx_groups: Some(aws_lc_rs::ALL_KX_GROUPS), + any_supported_key: Some(aws_lc_rs::sign::any_supported_type), + ticketer: aws_lc_rs::Ticketer::new, + }; + CryptoExt::set_provider(aws_lc_rs::default_provider(), ext).unwrap(); + }) + } + + #[test] + fn oid_mappings() { + let _ = black_box(&OID_MAPPINGS.name_to_oid); + let _ = black_box(&OID_MAPPINGS.oid_to_entry); + let _ = black_box(&OID_MAPPINGS.oid_sn_to_nid); + let _ = black_box(&OID_MAPPINGS.name_to_oid); + } +} diff --git a/crates/stdlib/src/socket.rs b/crates/stdlib/src/socket.rs index 366de2ecc21..4706e91dc6c 100644 --- a/crates/stdlib/src/socket.rs +++ b/crates/stdlib/src/socket.rs @@ -3,7 +3,9 @@ pub(crate) use _socket::module_def; #[cfg(feature = "ssl")] -pub(super) use _socket::{PySocket, SockWaitKind, sock_wait, timeout_error_msg}; +pub(super) use _socket::timeout_error_msg; +#[cfg(feature = "ssl-openssl")] +pub(super) use _socket::{PySocket, SockWaitKind, sock_wait}; #[pymodule] mod _socket { @@ -2452,6 +2454,7 @@ mod _socket { } /// returns Ok(true) on timeout + #[cfg(feature = "ssl-openssl")] pub(crate) fn sock_wait( sock: &Socket, wait_kind: SockWaitKind, diff --git a/crates/stdlib/src/ssl.rs b/crates/stdlib/src/ssl.rs deleted file mode 100644 index d36481a0062..00000000000 --- a/crates/stdlib/src/ssl.rs +++ /dev/null @@ -1,5135 +0,0 @@ -// spell-checker: ignore ssleof aesccm aesgcm capath getblocking setblocking ENDTLS TLSEXT - -//! Pure Rust SSL/TLS implementation using rustls -//! -//! 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 -//! - ring: Cryptographic primitives -//! - rustls-platform-verifier: Platform-native certificate verification -//! -//! DO NOT add openssl dependency here. -//! -//! 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; - -// OpenSSL compatibility layer (abstracts rustls operations) -mod compat; - -// SSL exception types (shared with openssl backend) -mod error; - -// Utilities for setting a Rustls cryptography provider. -pub mod providers; - -pub(crate) use _ssl::module_def; - -#[allow(non_snake_case)] -#[allow(non_upper_case_globals)] -#[pymodule(with(error::ssl_error))] -mod _ssl { - use crate::{ - common::{ - hash::PyHash, - lock::{PyMutex, PyRwLock}, - }, - socket::{PySocket, SockWaitKind, sock_wait, timeout_error_msg}, - vm::{ - AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, - VirtualMachine, - builtins::{ - PyBaseExceptionRef, PyByteArray, PyBytesRef, PyListRef, PyStrRef, PyType, - PyTypeRef, PyUtf8StrRef, - }, - convert::IntoPyException, - function::{ - ArgBytesLike, ArgMemoryBuffer, Either, FuncArgs, OptionalArg, PyComparisonValue, - }, - stdlib::_warnings, - types::{Comparable, Constructor, Hashable, PyComparisonOp, Representable}, - }, - }; - - // Import error types used in this module (others are exposed via pymodule(with(...))) - use super::error::{ - PySSLError, create_ssl_eof_error, create_ssl_want_read_error, create_ssl_want_write_error, - create_ssl_zero_return_error, - }; - use alloc::sync::Arc; - use core::{ - hash::{Hash, Hasher}, - sync::atomic::{AtomicUsize, Ordering}, - time::Duration, - }; - use std::{ - collections::{HashMap, hash_map::DefaultHasher}, - io::BufRead, - time::SystemTime, - }; - - // Rustls imports - use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock}; - use pem_rfc7468::{LineEnding, encode_string}; - use rustls::{ - ClientConnection, Connection, HandshakeKind, RootCertStore, ServerConfig, ServerConnection, - client::{ClientSessionMemoryCache, ClientSessionStore}, - crypto::SupportedKxGroup, - pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer, ServerName}, - server::{ClientHello, ResolvesServerCert}, - sign::CertifiedKey, - version::{TLS12, TLS13}, - }; - use sha2::{Digest, Sha256}; - - // Import certificate operations module - use super::cert; - - // Import OID module - use super::oid; - - // Import compat module (OpenSSL compatibility layer) - use super::compat::{ - ClientConfigOptions, MultiCertResolver, ProtocolSettings, ServerConfigOptions, SslError, - create_client_config, create_server_config, curve_name_to_kx_group, extract_cipher_info, - get_cipher_encryption_desc, is_blocking_io_error, normalize_cipher_name, ssl_do_handshake, - }; - - use super::providers::CryptoExt; - - // Type aliases for better readability - // Additional type alias for certificate/key pairs (SessionCache and SniCertName defined below) - - /// Certificate and private key pair used in SSL contexts - type CertKeyPair = (Arc, PrivateKeyDer<'static>); - - // Constants matching Python ssl module - - // SSL/TLS Protocol versions - #[pyattr] - const PROTOCOL_TLS: i32 = 2; // Auto-negotiate best version - #[pyattr] - const PROTOCOL_SSLv23: i32 = PROTOCOL_TLS; // Alias for PROTOCOL_TLS - #[pyattr] - const PROTOCOL_TLS_CLIENT: i32 = 16; - #[pyattr] - const PROTOCOL_TLS_SERVER: i32 = 17; - - // 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; - #[pyattr] - const PROTOCOL_TLSv1_1: i32 = 4; - #[pyattr] - const PROTOCOL_TLSv1_2: i32 = 5; - #[pyattr] - const PROTOCOL_TLSv1_3: i32 = 6; - - // Protocol version constants for TLSVersion enum - #[pyattr] - const PROTO_SSLv3: i32 = 0x0300; - #[pyattr] - const PROTO_TLSv1: i32 = 0x0301; - #[pyattr] - const PROTO_TLSv1_1: i32 = 0x0302; - #[pyattr] - const PROTO_TLSv1_2: i32 = 0x0303; - #[pyattr] - const PROTO_TLSv1_3: i32 = 0x0304; - - // Minimum and maximum supported protocol versions for rustls - // Use special values -2 and -1 to avoid enum name conflicts - #[pyattr] - const PROTO_MINIMUM_SUPPORTED: i32 = -2; // special value - #[pyattr] - const PROTO_MAXIMUM_SUPPORTED: i32 = -1; // special value - - // Internal constants for rustls actual supported versions - // rustls only supports TLS 1.2 and TLS 1.3 - const MINIMUM_VERSION: i32 = PROTO_TLSv1_2; // 0x0303 - const MAXIMUM_VERSION: i32 = PROTO_TLSv1_3; // 0x0304 - - // 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; - - // Certificate verification modes - #[pyattr] - const CERT_NONE: i32 = 0; - #[pyattr] - const CERT_OPTIONAL: i32 = 1; - #[pyattr] - const CERT_REQUIRED: i32 = 2; - - // SSL Verification Flags / Certificate requirements - #[pyattr] - const VERIFY_DEFAULT: i32 = 0; - #[pyattr] - const VERIFY_CRL_CHECK_LEAF: i32 = 4; - #[pyattr] - const VERIFY_CRL_CHECK_CHAIN: i32 = 12; - /// 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; - #[pyattr] - const VERIFY_ALLOW_PROXY_CERTS: i32 = 64; - #[pyattr] - const VERIFY_X509_TRUSTED_FIRST: i32 = 32768; - /// 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; - - // Options (OpenSSL-compatible flags, mostly no-op in rustls) - #[pyattr] - const OP_NO_SSLv2: i32 = 0x00000000; // Not supported anyway - #[pyattr] - const OP_NO_SSLv3: i32 = 0x02000000; - #[pyattr] - const OP_NO_TLSv1: i32 = 0x04000000; - #[pyattr] - const OP_NO_TLSv1_1: i32 = 0x10000000; - #[pyattr] - const OP_NO_TLSv1_2: i32 = 0x08000000; - #[pyattr] - const OP_NO_TLSv1_3: i32 = 0x20000000; - #[pyattr] - const OP_NO_COMPRESSION: i32 = 0x00020000; - #[pyattr] - const OP_CIPHER_SERVER_PREFERENCE: i32 = 0x00400000; - #[pyattr] - const OP_SINGLE_DH_USE: i32 = 0x00000000; // No-op in rustls - #[pyattr] - const OP_SINGLE_ECDH_USE: i32 = 0x00000000; // No-op in rustls - #[pyattr] - const OP_NO_TICKET: i32 = 0x00004000; - #[pyattr] - const OP_LEGACY_SERVER_CONNECT: i32 = 0x00000004; - #[pyattr] - const OP_NO_RENEGOTIATION: i32 = 0x40000000; - #[pyattr] - const OP_IGNORE_UNEXPECTED_EOF: i32 = 0x00000080; - #[pyattr] - const OP_ENABLE_MIDDLEBOX_COMPAT: i32 = 0x00100000; - #[pyattr] - const OP_ALL: i32 = 0x00000BFB; // Combined "safe" options (reduced for i32, excluding OP_LEGACY_SERVER_CONNECT for OpenSSL 3.0.0+ compatibility) - - // Alert types (matching _TLSAlertType enum) - #[pyattr] - const ALERT_DESCRIPTION_CLOSE_NOTIFY: i32 = 0; - #[pyattr] - const ALERT_DESCRIPTION_UNEXPECTED_MESSAGE: i32 = 10; - #[pyattr] - const ALERT_DESCRIPTION_BAD_RECORD_MAC: i32 = 20; - #[pyattr] - const ALERT_DESCRIPTION_DECRYPTION_FAILED: i32 = 21; - #[pyattr] - const ALERT_DESCRIPTION_RECORD_OVERFLOW: i32 = 22; - #[pyattr] - const ALERT_DESCRIPTION_DECOMPRESSION_FAILURE: i32 = 30; - #[pyattr] - const ALERT_DESCRIPTION_HANDSHAKE_FAILURE: i32 = 40; - #[pyattr] - const ALERT_DESCRIPTION_NO_CERTIFICATE: i32 = 41; - #[pyattr] - const ALERT_DESCRIPTION_BAD_CERTIFICATE: i32 = 42; - #[pyattr] - const ALERT_DESCRIPTION_UNSUPPORTED_CERTIFICATE: i32 = 43; - #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_REVOKED: i32 = 44; - #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_EXPIRED: i32 = 45; - #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_UNKNOWN: i32 = 46; - #[pyattr] - const ALERT_DESCRIPTION_ILLEGAL_PARAMETER: i32 = 47; - #[pyattr] - const ALERT_DESCRIPTION_UNKNOWN_CA: i32 = 48; - #[pyattr] - const ALERT_DESCRIPTION_ACCESS_DENIED: i32 = 49; - #[pyattr] - const ALERT_DESCRIPTION_DECODE_ERROR: i32 = 50; - #[pyattr] - const ALERT_DESCRIPTION_DECRYPT_ERROR: i32 = 51; - #[pyattr] - const ALERT_DESCRIPTION_EXPORT_RESTRICTION: i32 = 60; - #[pyattr] - const ALERT_DESCRIPTION_PROTOCOL_VERSION: i32 = 70; - #[pyattr] - const ALERT_DESCRIPTION_INSUFFICIENT_SECURITY: i32 = 71; - #[pyattr] - const ALERT_DESCRIPTION_INTERNAL_ERROR: i32 = 80; - #[pyattr] - const ALERT_DESCRIPTION_INAPPROPRIATE_FALLBACK: i32 = 86; - #[pyattr] - const ALERT_DESCRIPTION_USER_CANCELLED: i32 = 90; - #[pyattr] - const ALERT_DESCRIPTION_NO_RENEGOTIATION: i32 = 100; - #[pyattr] - const ALERT_DESCRIPTION_MISSING_EXTENSION: i32 = 109; - #[pyattr] - const ALERT_DESCRIPTION_UNSUPPORTED_EXTENSION: i32 = 110; - #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_UNOBTAINABLE: i32 = 111; - #[pyattr] - const ALERT_DESCRIPTION_UNRECOGNIZED_NAME: i32 = 112; - #[pyattr] - const ALERT_DESCRIPTION_BAD_CERTIFICATE_STATUS_RESPONSE: i32 = 113; - #[pyattr] - const ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE: i32 = 114; - #[pyattr] - const ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY: i32 = 115; - #[pyattr] - const ALERT_DESCRIPTION_CERTIFICATE_REQUIRED: i32 = 116; - #[pyattr] - const ALERT_DESCRIPTION_NO_APPLICATION_PROTOCOL: i32 = 120; - - // Version info - reporting as OpenSSL 3.3.0 for compatibility - #[pyattr] - const OPENSSL_VERSION_NUMBER: i32 = 0x30300000; // OpenSSL 3.3.0 (808452096) - #[pyattr] - const OPENSSL_VERSION: &str = "OpenSSL 3.3.0 (rustls/0.23)"; - #[pyattr] - const OPENSSL_VERSION_INFO: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); // 3.3.0 release - #[pyattr] - const _OPENSSL_API_VERSION: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); // 3.3.0 release - - // Default cipher list for rustls - using modern secure ciphers - #[pyattr] - const _DEFAULT_CIPHERS: &str = - "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256"; - - // Has features - #[pyattr] - const HAS_SNI: bool = true; - #[pyattr] - const HAS_TLS_UNIQUE: bool = false; // Not supported - #[pyattr] - const HAS_ECDH: bool = true; - #[pyattr] - const HAS_NPN: bool = false; // Deprecated, use ALPN - #[pyattr] - const HAS_ALPN: bool = true; - #[pyattr] - const HAS_PSK: bool = false; // PSK not supported in rustls - #[pyattr] - const HAS_SSLv2: bool = false; - #[pyattr] - const HAS_SSLv3: bool = false; - #[pyattr] - const HAS_TLSv1: bool = false; // Not supported for security - #[pyattr] - const HAS_TLSv1_1: bool = false; // Not supported for security - #[pyattr] - const HAS_TLSv1_2: bool = true; // rustls supports TLS 1.2 - #[pyattr] - const HAS_TLSv1_3: bool = true; - #[pyattr] - const HAS_PHA: bool = false; // Post-Handshake Auth not supported in rustls - - // Encoding constants (matching OpenSSL) - #[pyattr] - const ENCODING_PEM: i32 = 1; - #[pyattr] - const ENCODING_DER: i32 = 2; - #[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. - 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 hostname.contains('\0') { - return Err(vm.new_type_error("embedded null character")); - } - - if hostname.len() > 253 { - return Err(vm.new_value_error("server_hostname is too long (maximum 253 characters)")); - } - - Ok(()) - } - - // SNI certificate resolver that uses shared mutable state - // The Python SNI callback updates this state, and resolve() reads from it - #[derive(Debug)] - struct SniCertResolver { - // SNI state: (certificate, server_name) - sni_state: Arc>, - } - - impl ResolvesServerCert for SniCertResolver { - fn resolve(&self, client_hello: ClientHello<'_>) -> Option> { - let mut state = self.sni_state.lock(); - - // Extract and store SNI from client hello for later use - if let Some(sni) = client_hello.server_name() { - state.1 = Some(sni.to_string()); - } else { - state.1 = None; - } - - // Return the current certificate (may have been updated by Python callback) - Some(state.0.clone()) - } - } - - // Session data structure for tracking TLS sessions - #[derive(Debug, Clone)] - struct SessionData { - _server_name: String, - session_id: Vec, - creation_time: SystemTime, - lifetime: u64, - } - - // Type alias to simplify complex session cache type - type SessionCache = Arc, Arc>>>>; - - // Type alias for SNI state - type SniCertName = (Arc, Option); - - // 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 - - // Generate a synthetic session ID from server name and timestamp - // NOTE: This is NOT the actual TLS session ID, just a unique identifier - fn generate_session_id_from_metadata(server_name: &str, time: &SystemTime) -> Vec { - let mut hasher = Sha256::new(); - hasher.update(server_name.as_bytes()); - hasher.update( - time.duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs() - .to_le_bytes(), - ); - hasher.finalize()[..16].to_vec() - } - - // 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, - session_cache: SessionCache, - } - - 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 { - 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 creation_time = SystemTime::now(); - let server_name_str = server_name.to_str(); - let session_data = SessionData { - _server_name: server_name_str.as_ref().to_string(), - session_id: generate_session_id_from_metadata( - server_name_str.as_ref(), - &creation_time, - ), - creation_time, - lifetime: 7200, // TLS 1.2 default session lifetime - }; - - 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 { - 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 creation_time = SystemTime::now(); - let server_name_str = server_name.to_str(); - let session_data = SessionData { - _server_name: server_name_str.to_string(), - session_id: generate_session_id_from_metadata( - server_name_str.as_ref(), - &creation_time, - ), - creation_time, - lifetime: 7200, // Default TLS 1.3 ticket lifetime (Rustls uses this) - }; - - 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 { - 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>)` - List of protocol names as byte vectors - /// * `Err(PyBaseExceptionRef)` - ValueError with detailed error message - fn parse_length_prefixed_alpn(bytes: &[u8], vm: &VirtualMachine) -> PyResult>> { - 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) - } - - /// Parse OpenSSL cipher string to rustls SupportedCipherSuite list - /// - /// Supports patterns like: - /// - "AES128" → filters for AES_128 - /// - "AES256" → filters for AES_256 - /// - "AES128:AES256" → both - /// - "ECDHE+AESGCM" → ECDHE AND AESGCM (both conditions must match) - /// - "ALL" or "DEFAULT" → all available - /// - "!MD5" → exclusion (ignored, rustls doesn't support weak ciphers anyway) - fn parse_cipher_string(cipher_str: &str) -> Result, String> { - if cipher_str.is_empty() { - return Err("No cipher can be selected".to_string()); - } - - let all_suites = CryptoExt::get_ext().all_ciphers_or_default(); - let mut selected = Vec::new(); - - for part in cipher_str.split(':') { - let part = part.trim(); - - // Skip exclusions (rustls doesn't support these) - if part.starts_with('!') { - continue; - } - - // Skip priority markers starting with + - if part.starts_with('+') { - continue; - } - - // Match pattern - match part { - "ALL" | "DEFAULT" | "HIGH" => { - // Add all available cipher suites - selected.extend_from_slice(all_suites); - } - _ => { - // Check if this is a compound pattern with + (AND condition) - // e.g., "ECDHE+AESGCM" means ECDHE AND AESGCM - let patterns: Vec<&str> = part.split('+').collect(); - - let mut found_any = false; - for suite in all_suites { - let name = format!("{:?}", suite.suite()); - - // Check if all patterns match (AND condition) - let matches = patterns.iter().all(|&pattern| { - // Handle common OpenSSL pattern variations - if pattern.contains("AES128") { - name.contains("AES_128") - } else if pattern.contains("AES256") { - name.contains("AES_256") - } else if pattern == "AESGCM" { - // AESGCM: AES with GCM mode - name.contains("AES") && name.contains("GCM") - } else if pattern == "AESCCM" { - // AESCCM: AES with CCM mode - name.contains("AES") && name.contains("CCM") - } else if pattern == "CHACHA20" { - name.contains("CHACHA20") - } else if pattern == "ECDHE" { - name.contains("ECDHE") - } else if pattern == "DHE" { - // DHE but not ECDHE - name.contains("DHE") && !name.contains("ECDHE") - } else if pattern == "ECDH" { - // ECDH but not ECDHE - name.contains("ECDH") && !name.contains("ECDHE") - } else if pattern == "DH" { - // DH but not DHE or ECDH - name.contains("DH") - && !name.contains("DHE") - && !name.contains("ECDH") - } else if pattern == "RSA" { - name.contains("RSA") - } else if pattern == "AES" { - name.contains("AES") - } else if pattern == "ECDSA" { - name.contains("ECDSA") - } else { - // Direct substring match for other patterns - name.contains(pattern) - } - }); - - if matches { - selected.push(*suite); - found_any = true; - } - } - - if !found_any { - // No matching cipher suite found - warn but continue - } - } - } - } - - // Remove duplicates - selected.dedup_by_key(|s| s.suite()); - - if selected.is_empty() { - Err("No cipher can be selected".to_string()) - } else { - Ok(selected) - } - } - - // SSLContext - manages TLS configuration - #[pyattr] - #[pyclass(name = "_SSLContext", module = "ssl", traverse)] - #[derive(Debug, PyPayload)] - struct PySSLContext { - #[pytraverse(skip)] - protocol: i32, - #[pytraverse(skip)] - check_hostname: PyRwLock, - #[pytraverse(skip)] - verify_mode: PyRwLock, - #[pytraverse(skip)] - verify_flags: PyRwLock, - // Rustls configuration (built lazily) - #[pytraverse(skip)] - server_config: PyRwLock>>, - // Certificate store - #[pytraverse(skip)] - root_certs: PyRwLock, - // Store full CA certificates for get_ca_certs() - // RootCertStore only keeps TrustAnchors, not full certificates - #[pytraverse(skip)] - ca_certs_der: PyRwLock>>, - // Store CA certificates from capath for lazy loading simulation - // (CPython only returns these in get_ca_certs() after they're used in handshake) - #[pytraverse(skip)] - capath_certs_der: PyRwLock>>, - // Certificate Revocation Lists for CRL checking - #[pytraverse(skip)] - crls: PyRwLock>>, - // Server certificate/key pairs (supports multiple for RSA+ECC dual mode) - // OpenSSL allows multiple cert/key pairs to be loaded, and selects the appropriate - // one based on client capabilities during handshake - // Stored as (CertifiedKey, PrivateKeyDer) to support both server and client usage - #[pytraverse(skip)] - cert_keys: PyRwLock>, - // Options - #[pytraverse(skip)] - options: PyRwLock, - // ALPN protocols - #[pytraverse(skip)] - alpn_protocols: PyRwLock>>, - // TLS 1.3 features - #[pytraverse(skip)] - post_handshake_auth: PyRwLock, - #[pytraverse(skip)] - num_tickets: PyRwLock, - // Protocol version limits - #[pytraverse(skip)] - minimum_version: PyRwLock, - #[pytraverse(skip)] - maximum_version: PyRwLock, - // SNI callback for server-side (contains PyObjectRef - needs GC tracking) - sni_callback: PyRwLock>, - // Message callback for debugging (contains PyObjectRef - needs GC tracking) - msg_callback: PyRwLock>, - // ECDH curve name for key exchange - #[pytraverse(skip)] - ecdh_curve: PyRwLock>, - // Certificate statistics for cert_store_stats() - #[pytraverse(skip)] - ca_cert_count: PyRwLock, // Number of CA certificates - #[pytraverse(skip)] - x509_cert_count: PyRwLock, // Total number of certificates - // Session management - #[pytraverse(skip)] - client_session_cache: SessionCache, - // Rustls session store for actual TLS session resumption - #[pytraverse(skip)] - rustls_session_store: Arc, - // Rustls server session store for server-side session resumption - #[pytraverse(skip)] - rustls_server_session_store: Arc, - // Shared ticketer for TLS 1.2 session tickets - #[pytraverse(skip)] - server_ticketer: Arc, - // Server-side session statistics - #[pytraverse(skip)] - accept_count: AtomicUsize, // Total number of accepts - #[pytraverse(skip)] - session_hits: AtomicUsize, // Number of session reuses - // Cipher suite selection - /// Selected cipher suites (None = use all rustls defaults) - #[pytraverse(skip)] - selected_ciphers: PyRwLock>>, - } - - #[derive(FromArgs)] - struct WrapSocketArgs { - sock: PyObjectRef, - server_side: bool, - #[pyarg(positional, optional)] - server_hostname: OptionalArg>, - #[pyarg(named, optional)] - owner: OptionalArg, - #[pyarg(named, optional)] - session: OptionalArg, - } - - #[derive(FromArgs)] - struct WrapBioArgs { - incoming: PyRef, - outgoing: PyRef, - #[pyarg(named, optional)] - server_side: OptionalArg, - #[pyarg(named, optional)] - server_hostname: OptionalArg>, - #[pyarg(named, optional)] - owner: OptionalArg, - #[pyarg(named, optional)] - session: OptionalArg, - } - - #[derive(FromArgs)] - struct LoadVerifyLocationsArgs { - #[pyarg(any, optional, error_msg = "path should be a str or bytes")] - cafile: OptionalArg>>, - #[pyarg(any, optional, error_msg = "path should be a str or bytes")] - capath: OptionalArg>>, - #[pyarg(any, optional, error_msg = "cadata should be a str or bytes")] - cadata: OptionalArg>>, - } - - #[derive(FromArgs)] - struct LoadCertChainArgs { - #[pyarg(any, error_msg = "path should be a str or bytes")] - certfile: Either, - #[pyarg(any, optional, error_msg = "path should be a str or bytes")] - keyfile: OptionalArg>>, - #[pyarg(any, optional)] - password: OptionalArg, - } - - #[derive(FromArgs)] - struct GetCertArgs { - #[pyarg(any, optional)] - binary_form: OptionalArg, - } - - #[pyclass(with(Constructor, Representable), flags(BASETYPE))] - impl PySSLContext { - // Helper method to convert DER certificate bytes to Python dict - fn cert_der_to_dict(&self, vm: &VirtualMachine, cert_der: &[u8]) -> PyResult { - cert::cert_der_to_dict_helper(vm, cert_der) - } - - #[pygetset] - fn check_hostname(&self) -> bool { - *self.check_hostname.read() - } - - #[pygetset(setter)] - fn set_check_hostname(&self, value: bool) { - *self.check_hostname.write() = value; - // When check_hostname is enabled, ensure verify_mode is at least CERT_REQUIRED - if value { - let current_verify_mode = *self.verify_mode.read(); - if current_verify_mode == CERT_NONE { - *self.verify_mode.write() = CERT_REQUIRED; - } - } - } - - #[pygetset] - fn verify_mode(&self) -> i32 { - *self.verify_mode.read() - } - - #[pygetset(setter)] - fn set_verify_mode(&self, mode: i32, vm: &VirtualMachine) -> PyResult<()> { - if !(CERT_NONE..=CERT_REQUIRED).contains(&mode) { - return Err(vm.new_value_error("invalid verify mode")); - } - // Cannot set CERT_NONE when check_hostname is enabled - if mode == CERT_NONE && *self.check_hostname.read() { - return Err(vm.new_value_error( - "Cannot set verify_mode to CERT_NONE when check_hostname is enabled", - )); - } - *self.verify_mode.write() = mode; - Ok(()) - } - - #[pygetset] - fn protocol(&self) -> i32 { - self.protocol - } - - #[pygetset] - fn verify_flags(&self) -> i32 { - *self.verify_flags.read() - } - - #[pygetset(setter)] - fn set_verify_flags(&self, value: i32) { - *self.verify_flags.write() = value; - } - - #[pygetset] - fn post_handshake_auth(&self) -> bool { - *self.post_handshake_auth.read() - } - - #[pygetset(setter)] - fn set_post_handshake_auth(&self, value: bool) { - *self.post_handshake_auth.write() = value; - } - - #[pygetset] - fn num_tickets(&self) -> i32 { - *self.num_tickets.read() - } - - #[pygetset(setter)] - fn set_num_tickets(&self, value: i32, vm: &VirtualMachine) -> PyResult<()> { - if value < 0 { - return Err(vm.new_value_error("num_tickets must be a non-negative integer")); - } - if self.protocol != PROTOCOL_TLS_SERVER { - return Err( - vm.new_value_error("num_tickets can only be set on server-side contexts") - ); - } - *self.num_tickets.write() = value; - Ok(()) - } - - #[pygetset] - fn options(&self) -> i32 { - *self.options.read() - } - - #[pygetset(setter)] - fn set_options(&self, value: i32, vm: &VirtualMachine) -> PyResult<()> { - // Validate that the value is non-negative - if value < 0 { - return Err(vm.new_value_error("options must be non-negative")); - } - - // Deprecated SSL/TLS protocol version options - let opt_no = OP_NO_SSLv2 - | OP_NO_SSLv3 - | OP_NO_TLSv1 - | OP_NO_TLSv1_1 - | OP_NO_TLSv1_2 - | OP_NO_TLSv1_3; - - // Get current options and calculate newly set bits - let old_opts = *self.options.read(); - let set = !old_opts & value; // Bits being newly set - - // Warn if any deprecated options are being newly set - if (set & opt_no) != 0 { - _warnings::warn( - vm.ctx.exceptions.deprecation_warning, - "ssl.OP_NO_SSL*/ssl.OP_NO_TLS* options are deprecated".to_owned(), - 2, // stack_level = 2 - vm, - )?; - } - - *self.options.write() = value; - Ok(()) - } - - #[pygetset] - fn minimum_version(&self) -> i32 { - let v = *self.minimum_version.read(); - // return MINIMUM_SUPPORTED if value is 0 - if v == 0 { PROTO_MINIMUM_SUPPORTED } else { v } - } - - #[pygetset(setter)] - fn set_minimum_version(&self, value: i32, vm: &VirtualMachine) -> PyResult<()> { - // Validate that the value is a valid TLS version constant - // Valid values: 0 (default), -2 (MINIMUM_SUPPORTED), -1 (MAXIMUM_SUPPORTED), - // or 0x0300-0x0304 (SSLv3-TLSv1.3) - if value != 0 - && value != -2 - && value != -1 - && !(PROTO_SSLv3..=PROTO_TLSv1_3).contains(&value) - { - return Err(vm.new_value_error(format!("invalid protocol version: {value}"))); - } - // Convert special values to rustls actual supported versions - // MINIMUM_SUPPORTED (-2) -> 0 (auto-negotiate) - // MAXIMUM_SUPPORTED (-1) -> MAXIMUM_VERSION (TLSv1.3) - let normalized_value = match value { - PROTO_MINIMUM_SUPPORTED => 0, // Auto-negotiate - PROTO_MAXIMUM_SUPPORTED => MAXIMUM_VERSION, // TLSv1.3 - _ => value, - }; - *self.minimum_version.write() = normalized_value; - Ok(()) - } - - #[pygetset] - fn maximum_version(&self) -> i32 { - let v = *self.maximum_version.read(); - // return MAXIMUM_SUPPORTED if value is 0 - if v == 0 { PROTO_MAXIMUM_SUPPORTED } else { v } - } - - #[pygetset(setter)] - fn set_maximum_version(&self, value: i32, vm: &VirtualMachine) -> PyResult<()> { - // Validate that the value is a valid TLS version constant - // Valid values: 0 (default), -2 (MINIMUM_SUPPORTED), -1 (MAXIMUM_SUPPORTED), - // or 0x0300-0x0304 (SSLv3-TLSv1.3) - if value != 0 - && value != -2 - && value != -1 - && !(PROTO_SSLv3..=PROTO_TLSv1_3).contains(&value) - { - return Err(vm.new_value_error(format!("invalid protocol version: {value}"))); - } - // Convert special values to rustls actual supported versions - // MAXIMUM_SUPPORTED (-1) -> 0 (auto-negotiate) - // MINIMUM_SUPPORTED (-2) -> MINIMUM_VERSION (TLSv1.2) - let normalized_value = match value { - PROTO_MAXIMUM_SUPPORTED => 0, // Auto-negotiate - PROTO_MINIMUM_SUPPORTED => MINIMUM_VERSION, // TLSv1.2 - _ => value, - }; - *self.maximum_version.write() = normalized_value; - Ok(()) - } - - #[pymethod] - fn load_cert_chain(&self, args: LoadCertChainArgs, vm: &VirtualMachine) -> PyResult<()> { - let crypto_ext = CryptoExt::get_ext(); - - // Parse certfile argument (str or bytes) to path - let cert_path = Self::parse_path_arg(&args.certfile, vm)?; - - // Parse keyfile argument (default to certfile if not provided) - let key_path = match args.keyfile { - OptionalArg::Present(Some(ref k)) => Self::parse_path_arg(k, vm)?, - _ => cert_path.clone(), - }; - - // Parse password argument (str, bytes-like, or callable) - // Callable passwords are NOT invoked immediately (lazy evaluation) - let (password_str, password_callable) = - Self::parse_password_argument(&args.password, vm)?; - - // Validate immediate password length (limit: PEM_BUFSIZE = 1024 bytes) - if let Some(ref pwd) = password_str - && pwd.len() > PEM_BUFSIZE - { - return Err(vm.new_value_error(format!( - "password cannot be longer than {PEM_BUFSIZE} bytes", - ))); - } - - // First attempt: Load with immediate password (or None if callable) - let mut result = - cert::load_cert_chain_from_file(&cert_path, &key_path, password_str.as_deref()); - - // If failed and callable exists, invoke it and retry - // This implements lazy evaluation: callable only invoked if password is actually needed - if result.is_err() - && let Some(callable) = password_callable - { - // Invoke callable - exceptions propagate naturally - let pwd_result = callable.call((), vm)?; - - // Convert callable result to string - let password_from_callable = if let Ok(pwd_str) = - PyUtf8StrRef::try_from_object(vm, pwd_result.clone()) - { - pwd_str.as_str().to_owned() - } else if let Ok(pwd_bytes_like) = ArgBytesLike::try_from_object(vm, pwd_result) { - String::from_utf8(pwd_bytes_like.borrow_buf().to_vec()).map_err(|_| { - vm.new_type_error("password callback returned invalid UTF-8 bytes") - })? - } else { - return Err( - vm.new_type_error("password callback must return a string or bytes") - ); - }; - - // Validate callable password length - if password_from_callable.len() > PEM_BUFSIZE { - return Err(vm.new_value_error(format!( - "password cannot be longer than {PEM_BUFSIZE} bytes", - ))); - } - - // Retry with callable password - result = cert::load_cert_chain_from_file( - &cert_path, - &key_path, - Some(&password_from_callable), - ); - } - - // Process result - let (certs, key) = result.map_err(|e| { - // Try to downcast to io::Error to preserve errno information - if let Ok(io_err) = e.downcast::() { - match io_err.kind() { - // File access errors (NotFound, PermissionDenied) - preserve errno - std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => { - io_err.into_pyexception(vm) - } - // Other io::Error types - std::io::ErrorKind::Other => { - let msg = io_err.to_string(); - if msg.contains("Failed to decrypt") || msg.contains("wrong password") { - // Wrong password error - vm.new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - msg, - ) - .upcast() - } else { - // [SSL] PEM lib - super::compat::SslError::create_ssl_error_with_reason( - vm, - Some("SSL"), - "", - "PEM lib", - ) - } - } - // PEM parsing errors - [SSL] PEM lib - _ => super::compat::SslError::create_ssl_error_with_reason( - vm, - Some("SSL"), - "", - "PEM lib", - ), - } - } else { - // Unknown error type - [SSL] PEM lib - super::compat::SslError::create_ssl_error_with_reason( - vm, - Some("SSL"), - "", - "PEM lib", - ) - } - })?; - - // Validate certificate and key match - cert::validate_cert_key_match(&certs, &key).map_err(|e| { - let msg = if e.contains("key values mismatch") { - "[SSL: KEY_VALUES_MISMATCH] key values mismatch".to_owned() - } else { - e - }; - vm.new_os_subtype_error(PySSLError::class(&vm.ctx).to_owned(), Some(0), msg) - .upcast() - })?; - - // Auto-build certificate chain: if only leaf cert is in file, try to add CA certs - // This matches OpenSSL behavior where it automatically includes intermediate/CA certs - let mut full_chain = certs.clone(); - if full_chain.len() == 1 { - // Only have leaf cert, try to build chain from CA certs - let ca_certs_der = self.ca_certs_der.read(); - if !ca_certs_der.is_empty() { - // Use build_verified_chain to construct full chain - let chain_result = cert::build_verified_chain(&full_chain, &ca_certs_der); - if chain_result.len() > 1 { - // Successfully built a longer chain - full_chain = chain_result.into_iter().map(CertificateDer::from).collect(); - } - } - } - - // Additional validation: Create CertifiedKey to ensure rustls accepts it - let signing_key = crypto_ext.any_supported_key(&key).map_err(|_| { - vm.new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "[SSL: KEY_VALUES_MISMATCH] key values mismatch", - ) - .upcast() - })?; - - let certified_key = CertifiedKey::new(full_chain.clone(), signing_key); - if certified_key.keys_match().is_err() { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "[SSL: KEY_VALUES_MISMATCH] key values mismatch", - ) - .upcast()); - } - - // Add cert/key pair to collection (OpenSSL allows multiple cert/key pairs) - // Store both CertifiedKey (for server) and PrivateKeyDer (for client mTLS) - let cert_der = &full_chain[0]; - let mut cert_keys = self.cert_keys.write(); - - // Remove any existing cert/key pair with the same certificate - // (This allows updating cert/key pair without duplicating) - cert_keys.retain(|(existing, _)| &existing.cert[0] != cert_der); - - // Add new cert/key pair as tuple - cert_keys.push((Arc::new(certified_key), key)); - - Ok(()) - } - - #[pymethod] - fn load_verify_locations( - &self, - args: LoadVerifyLocationsArgs, - vm: &VirtualMachine, - ) -> PyResult<()> { - // Check that at least one argument is provided - let has_cafile = matches!(&args.cafile, OptionalArg::Present(Some(_))); - let has_capath = matches!(&args.capath, OptionalArg::Present(Some(_))); - let has_cadata = matches!(&args.cadata, OptionalArg::Present(Some(_))); - - if !has_cafile && !has_capath && !has_cadata { - return Err(vm.new_type_error("cafile, capath and cadata cannot be all omitted")); - } - - // Parse arguments BEFORE acquiring locks to reduce lock scope - let cafile_path = if let OptionalArg::Present(Some(ref cafile_obj)) = args.cafile { - Some(Self::parse_path_arg(cafile_obj, vm)?) - } else { - None - }; - - let capath_dir = if let OptionalArg::Present(Some(ref capath_obj)) = args.capath { - Some(Self::parse_path_arg(capath_obj, vm)?) - } else { - None - }; - - let cadata_parsed = if let OptionalArg::Present(Some(ref cadata_obj)) = args.cadata { - let is_string = matches!(cadata_obj, Either::A(_)); - let data_vec = self.parse_cadata_arg(cadata_obj, vm)?; - Some((data_vec, is_string)) - } else { - None - }; - - // Check for CRL before acquiring main locks - let (crl_opt, cafile_is_crl) = if let Some(ref path) = cafile_path { - let crl = self.load_crl_from_file(path, vm)?; - let is_crl = crl.is_some(); - (crl, is_crl) - } else { - (None, false) - }; - - // If it's a CRL, just add it (separate lock, no conflict with root_store) - if let Some(crl) = crl_opt { - self.crls.write().push(crl); - } - - // Now acquire write locks for certificate loading - let mut root_store = self.root_certs.write(); - let mut ca_certs_der = self.ca_certs_der.write(); - - // Load from file (if not CRL) - if let Some(ref path) = cafile_path - && !cafile_is_crl - { - // Not a CRL, load as certificate - let stats = - self.load_certs_from_file_helper(&mut root_store, &mut ca_certs_der, path, vm)?; - self.update_cert_stats(stats); - } - - // Load from directory (don't add to ca_certs_der) - if let Some(ref dir_path) = capath_dir { - let stats = self.load_certs_from_dir_helper(&mut root_store, dir_path, vm)?; - self.update_cert_stats(stats); - } - - // Load from bytes or str - if let Some((ref data_vec, is_string)) = cadata_parsed { - let stats = self.load_certs_from_bytes_helper( - &mut root_store, - &mut ca_certs_der, - data_vec, - is_string, // PEM only for strings - vm, - )?; - self.update_cert_stats(stats); - } - - Ok(()) - } - - /// Helper: Get path from Python's os.environ - fn get_env_path( - environ: &PyObject, - var_name: &str, - vm: &VirtualMachine, - ) -> PyResult { - let path_obj = environ.get_item(var_name, vm)?; - path_obj.try_into_value(vm) - } - - /// Helper: Try to load certificates from Python's os.environ variables - /// - /// Returns true if certificates were successfully loaded. - /// - /// We use Python's os.environ instead of Rust's std::env - /// because Python code can modify os.environ at runtime (e.g., - /// `os.environ['SSL_CERT_FILE'] = '/path'`), but rustls-native-certs uses - /// std::env which only sees the process environment at startup. - fn try_load_from_python_environ( - &self, - loader: &mut cert::CertLoader<'_>, - vm: &VirtualMachine, - ) -> PyResult { - use std::path::Path; - - let os_module = vm.import("os", 0)?; - let environ = os_module.get_attr("environ", vm)?; - - // Try SSL_CERT_FILE first - if let Ok(cert_file) = Self::get_env_path(&environ, "SSL_CERT_FILE", vm) - && Path::new(&cert_file).exists() - && let Ok(stats) = loader.load_from_file(&cert_file) - { - self.update_cert_stats(stats); - return Ok(true); - } - - // Try SSL_CERT_DIR (only if SSL_CERT_FILE didn't work) - if let Ok(cert_dir) = Self::get_env_path(&environ, "SSL_CERT_DIR", vm) - && Path::new(&cert_dir).is_dir() - && let Ok(stats) = loader.load_from_dir(&cert_dir) - { - self.update_cert_stats(stats); - return Ok(true); - } - - Ok(false) - } - - /// Helper: Load system certificates using rustls-native-certs - /// - /// This uses platform-specific methods: - /// - Linux: openssl-probe to find certificate files - /// - macOS: Keychain API - /// - Windows: System certificate store (ROOT + CA stores) - fn load_system_certificates( - &self, - store: &mut rustls::RootCertStore, - vm: &VirtualMachine, - ) -> PyResult<()> { - #[cfg(windows)] - { - let store_names = ["ROOT", "CA"]; - - for store_name in store_names { - let certs = rustpython_host_env::cert_store::enum_certificates(store_name); - for cert_ctx in certs.entries { - let cert = rustls::pki_types::CertificateDer::from(cert_ctx.der.to_vec()); - let is_ca = cert::is_ca_certificate(cert.as_ref()); - if store.add(cert).is_ok() { - *self.x509_cert_count.write() += 1; - if is_ca { - *self.ca_cert_count.write() += 1; - } - } - } - } - - if *self.x509_cert_count.read() == 0 { - return Err(vm.new_os_error("Failed to load certificates from Windows store")); - } - - Ok(()) - } - - #[cfg(not(windows))] - { - let result = rustls_native_certs::load_native_certs(); - - // Load successfully found certificates - for cert in result.certs { - let is_ca = cert::is_ca_certificate(cert.as_ref()); - if store.add(cert).is_ok() { - *self.x509_cert_count.write() += 1; - if is_ca { - *self.ca_cert_count.write() += 1; - } - } - } - - // If there were errors but some certs loaded, just continue - // If NO certs loaded and there were errors, report the first error - if *self.x509_cert_count.read() == 0 && !result.errors.is_empty() { - return Err(vm.new_os_error(format!( - "Failed to load native certificates: {}", - result.errors[0] - ))); - } - - Ok(()) - } - } - - #[pymethod] - fn load_default_certs( - &self, - _purpose: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult<()> { - let mut store = self.root_certs.write(); - - #[cfg(windows)] - { - // Windows: Load system certificates first, then additionally load from env - // see: test_load_default_certs_env_windows - let _ = self.load_system_certificates(&mut store, vm); - - let mut lazy_ca_certs = Vec::new(); - let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs); - let _ = self.try_load_from_python_environ(&mut loader, vm)?; - } - - #[cfg(not(windows))] - { - // Non-Windows: Try env vars first; only fallback to system certs if not set - // see: test_load_default_certs_env - let mut lazy_ca_certs = Vec::new(); - let mut loader = cert::CertLoader::new(&mut store, &mut lazy_ca_certs); - let loaded = self.try_load_from_python_environ(&mut loader, vm)?; - - if !loaded { - let _ = self.load_system_certificates(&mut store, vm); - } - } - - // If no certificates were loaded from system, fallback to webpki-roots (Mozilla CA bundle) - // This ensures we always have some trusted root certificates even if system cert loading fails - if *self.x509_cert_count.read() == 0 { - use webpki_roots; - - // webpki_roots provides TLS_SERVER_ROOTS as &[TrustAnchor] - // We can use extend() to add them to the RootCertStore - let webpki_count = webpki_roots::TLS_SERVER_ROOTS.len(); - store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - - *self.x509_cert_count.write() += webpki_count; - *self.ca_cert_count.write() += webpki_count; - } - - Ok(()) - } - - #[pymethod] - fn set_alpn_protocols(&self, protocols: PyListRef, vm: &VirtualMachine) -> PyResult<()> { - let mut alpn_list = Vec::new(); - for item in protocols.borrow_vec().iter() { - let bytes = ArgBytesLike::try_from_object(vm, item.clone())?; - alpn_list.push(bytes.borrow_buf().to_vec()); - } - *self.alpn_protocols.write() = alpn_list; - Ok(()) - } - - #[pymethod] - fn _set_alpn_protocols(&self, protos: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { - let bytes = protos.borrow_buf(); - let alpn_list = parse_length_prefixed_alpn(&bytes, vm)?; - *self.alpn_protocols.write() = alpn_list; - Ok(()) - } - - #[pymethod] - fn set_ciphers(&self, ciphers: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<()> { - let cipher_str = ciphers.as_str(); - - // Parse cipher string and store selected ciphers - let selected_ciphers = parse_cipher_string(cipher_str).map_err(|e| { - vm.new_os_subtype_error(PySSLError::class(&vm.ctx).to_owned(), None, e) - .upcast() - })?; - - // Store in context - *self.selected_ciphers.write() = Some(selected_ciphers); - - Ok(()) - } - - #[pymethod] - fn get_ciphers(&self, vm: &VirtualMachine) -> PyListRef { - // Dynamically generate cipher list from rustls ALL_CIPHER_SUITES - // This automatically includes all cipher suites supported by the current rustls version - - let cipher_list = CryptoExt::get_ext() - .all_ciphers_or_default() - .iter() - .map(|suite| { - // Extract cipher information using unified helper - let cipher_info = extract_cipher_info(suite); - - // Convert to OpenSSL-style name - // e.g., "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" -> "ECDHE-RSA-AES128-GCM-SHA256" - let openssl_name = normalize_cipher_name(&cipher_info.name); - - // Determine key exchange and auth methods - let (kx, auth) = if cipher_info.protocol == "TLSv1.3" { - // TLS 1.3 doesn't distinguish - all use modern algos - ("any", "any") - } else if cipher_info.name.contains("ECDHE") { - // TLS 1.2 with ECDHE - let auth = if cipher_info.name.contains("ECDSA") { - "ECDSA" - } else if cipher_info.name.contains("RSA") { - "RSA" - } else { - "any" - }; - ("ECDH", auth) - } else { - ("any", "any") - }; - - // Build description string - // Format: "{name} {protocol} Kx={kx} Au={auth} Enc={enc} Mac={mac}" - let enc = get_cipher_encryption_desc(&openssl_name); - - let description = format!( - "{} {} Kx={} Au={} Enc={} Mac=AEAD", - openssl_name, cipher_info.protocol, kx, auth, enc - ); - - // Create cipher dict - let dict = vm.ctx.new_dict(); - dict.set_item("name", vm.ctx.new_str(openssl_name).into(), vm) - .unwrap(); - dict.set_item("protocol", vm.ctx.new_str(cipher_info.protocol).into(), vm) - .unwrap(); - dict.set_item("id", vm.ctx.new_int(0).into(), vm).unwrap(); // Placeholder ID - dict.set_item("strength_bits", vm.ctx.new_int(cipher_info.bits).into(), vm) - .unwrap(); - dict.set_item("alg_bits", vm.ctx.new_int(cipher_info.bits).into(), vm) - .unwrap(); - dict.set_item("description", vm.ctx.new_str(description).into(), vm) - .unwrap(); - dict.into() - }) - .collect::>(); - - PyListRef::from(vm.ctx.new_list(cipher_list)) - } - - #[pymethod] - fn set_default_verify_paths(&self, vm: &VirtualMachine) -> PyResult<()> { - // Just call load_default_certs - self.load_default_certs(OptionalArg::Missing, vm) - } - - #[pymethod] - fn cert_store_stats(&self, vm: &VirtualMachine) -> PyResult { - // Use the certificate counters that are updated in load_verify_locations - let x509_count = *self.x509_cert_count.read() as i32; - let ca_count = *self.ca_cert_count.read() as i32; - - let dict = vm.ctx.new_dict(); - dict.set_item("x509", vm.ctx.new_int(x509_count).into(), vm)?; - dict.set_item("crl", vm.ctx.new_int(0).into(), vm)?; // CRL not supported - dict.set_item("x509_ca", vm.ctx.new_int(ca_count).into(), vm)?; - Ok(dict.into()) - } - - #[pymethod] - fn session_stats(&self, vm: &VirtualMachine) -> PyResult { - // Return session statistics - // NOTE: This is a partial implementation - rustls doesn't expose all OpenSSL stats - let dict = vm.ctx.new_dict(); - - // Number of sessions currently in the cache - let session_count = self.client_session_cache.read().len() as i32; - dict.set_item("number", vm.ctx.new_int(session_count).into(), vm)?; - - // Client-side statistics (not tracked separately in this implementation) - dict.set_item("connect", vm.ctx.new_int(0).into(), vm)?; - dict.set_item("connect_good", vm.ctx.new_int(0).into(), vm)?; - dict.set_item("connect_renegotiate", vm.ctx.new_int(0).into(), vm)?; // rustls doesn't support renegotiation - - // Server-side statistics - let accept_count = self.accept_count.load(Ordering::SeqCst) as i32; - dict.set_item("accept", vm.ctx.new_int(accept_count).into(), vm)?; - dict.set_item("accept_good", vm.ctx.new_int(accept_count).into(), vm)?; // Assume all accepts are good - dict.set_item("accept_renegotiate", vm.ctx.new_int(0).into(), vm)?; // rustls doesn't support renegotiation - - // Session reuse statistics - let hits = self.session_hits.load(Ordering::SeqCst) as i32; - dict.set_item("hits", vm.ctx.new_int(hits).into(), vm)?; - - // Misses, timeouts, and cache_full are not tracked in this implementation - dict.set_item("misses", vm.ctx.new_int(0).into(), vm)?; - dict.set_item("timeouts", vm.ctx.new_int(0).into(), vm)?; - dict.set_item("cache_full", vm.ctx.new_int(0).into(), vm)?; - - Ok(dict.into()) - } - - #[pygetset] - fn sni_callback(&self) -> Option { - self.sni_callback.read().clone() - } - - #[pygetset(setter)] - fn set_sni_callback( - &self, - callback: Option, - vm: &VirtualMachine, - ) -> PyResult<()> { - // Validate callback is callable or None - if let Some(ref cb) = callback - && !cb.is(vm.ctx.types.none_type) - && !cb.is_callable() - { - return Err(vm.new_type_error("sni_callback must be callable or None")); - } - *self.sni_callback.write() = callback; - Ok(()) - } - - #[pymethod] - fn set_servername_callback( - &self, - callback: Option, - vm: &VirtualMachine, - ) -> PyResult<()> { - // Alias for set_sni_callback - self.set_sni_callback(callback, vm) - } - - #[pygetset] - fn security_level(&self) -> i32 { - // rustls uses a fixed security level - // Return 2 which is a reasonable default (equivalent to OpenSSL 1.1.0+ level 2) - 2 - } - - #[pygetset] - fn _msg_callback(&self) -> Option { - self.msg_callback.read().clone() - } - - #[pygetset(setter)] - fn set__msg_callback( - &self, - callback: Option, - vm: &VirtualMachine, - ) -> PyResult<()> { - // Validate callback is callable or None - if let Some(ref cb) = callback - && !cb.is(vm.ctx.types.none_type) - && !cb.is_callable() - { - return Err(vm.new_type_error("msg_callback must be callable or None")); - } - *self.msg_callback.write() = callback; - Ok(()) - } - - #[pymethod] - fn get_ca_certs(&self, args: GetCertArgs, vm: &VirtualMachine) -> PyResult { - let binary_form = args.binary_form.unwrap_or(false); - let ca_certs_der = self.ca_certs_der.read(); - - let mut certs = Vec::new(); - for cert_der in ca_certs_der.iter() { - // Parse certificate to check if it's a CA and get info - match x509_parser::parse_x509_certificate(cert_der) { - Ok((_, cert)) => { - // Check if this is a CA certificate (BasicConstraints: CA=TRUE) - let is_ca = if let Ok(Some(bc_ext)) = cert.basic_constraints() { - bc_ext.value.ca - } else { - false - }; - - // Only include CA certificates - if !is_ca { - continue; - } - - if binary_form { - // Return DER-encoded certificate as bytes - certs.push(vm.ctx.new_bytes(cert_der.clone()).into()); - } else { - // Return certificate as dict (use helper from _test_decode_cert) - let dict = self.cert_der_to_dict(vm, cert_der)?; - certs.push(dict); - } - } - Err(_) => { - // Skip invalid certificates - continue; - } - } - } - - Ok(PyListRef::from(vm.ctx.new_list(certs))) - } - - #[pymethod] - fn load_dh_params(&self, filepath: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - // Validate filepath is not None - if vm.is_none(&filepath) { - return Err(vm.new_type_error("DH params filepath cannot be None")); - } - - // Validate filepath is str or bytes - let path_str = if let Ok(s) = PyUtf8StrRef::try_from_object(vm, filepath.clone()) { - s.as_str().to_owned() - } else if let Ok(b) = ArgBytesLike::try_from_object(vm, filepath) { - String::from_utf8(b.borrow_buf().to_vec()) - .map_err(|_| vm.new_value_error("Invalid path encoding"))? - } else { - return Err(vm.new_type_error("DH params filepath must be str or bytes")); - }; - - // Check if file exists - if !std::path::Path::new(&path_str).exists() { - // Create FileNotFoundError with errno=ENOENT (2) - let exc = vm.new_os_subtype_error( - vm.ctx.exceptions.file_not_found_error.to_owned(), - Some(2), // errno = ENOENT (2) - "No such file or directory", - ); - // Set filename attribute - let _ = exc - .as_object() - .set_attr("filename", vm.ctx.new_str(path_str), vm); - return Err(exc.upcast()); - } - - // Validate that the file contains DH parameters - // Read the file and check for DH PARAMETERS header - let contents = rustpython_host_env::fs::read_to_string(&path_str) - .map_err(|e| vm.new_os_error(e.to_string()))?; - - if !contents.contains("BEGIN DH PARAMETERS") - && !contents.contains("BEGIN X9.42 DH PARAMETERS") - { - // File exists but doesn't contain DH parameters - raise SSLError - // [PEM: NO_START_LINE] no start line - return Err(super::compat::SslError::create_ssl_error_with_reason( - vm, - Some("PEM"), - "NO_START_LINE", - "[PEM: NO_START_LINE] no start line", - )); - } - - // rustls doesn't use DH parameters (it uses ECDHE for key exchange) - // This is a no-op for compatibility with OpenSSL-based code - Ok(()) - } - - #[pymethod] - fn set_ecdh_curve(&self, name: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - // Validate name is not None - if vm.is_none(&name) { - return Err(vm.new_type_error("ECDH curve name cannot be None")); - } - - // Validate name is str or bytes - let curve_name = if let Ok(s) = PyUtf8StrRef::try_from_object(vm, name.clone()) { - s.as_str().to_owned() - } else if let Ok(b) = ArgBytesLike::try_from_object(vm, name) { - String::from_utf8(b.borrow_buf().to_vec()) - .map_err(|_| vm.new_value_error("Invalid curve name encoding"))? - } else { - return Err(vm.new_type_error("ECDH curve name must be str or bytes")); - }; - - // Validate curve name (common curves for compatibility) - // rustls supports: X25519, secp256r1 (prime256v1), secp384r1 - let valid_curves = [ - "prime256v1", - "secp256r1", - "prime384v1", - "secp384r1", - "prime521v1", - "secp521r1", - "X25519", - "x25519", - "x448", // For future compatibility - ]; - - if !valid_curves.contains(&curve_name.as_str()) { - return Err(vm.new_value_error(format!("unknown curve name '{curve_name}'"))); - } - - // Store the curve name to be used during handshake - // This will limit the key exchange groups offered/accepted - *self.ecdh_curve.write() = Some(curve_name); - Ok(()) - } - - #[pymethod] - fn _wrap_socket( - zelf: PyRef, - args: WrapSocketArgs, - vm: &VirtualMachine, - ) -> PyResult> { - let socket_mod = vm.import("socket", 0)?; - let socket_class = socket_mod.get_attr("socket", vm)?; - - // Convert server_hostname to Option - // Handle both missing argument and None value - let hostname = match args.server_hostname.into_option().flatten() { - Some(hostname_str) => { - let hostname = hostname_str.as_str(); - - // Validate hostname - if hostname.is_empty() { - return Err(vm.new_value_error("server_hostname cannot be an empty string")); - } - - // Check if it starts with a dot - if hostname.starts_with('.') { - return Err(vm.new_value_error("server_hostname cannot start with a dot")); - } - - // IP addresses are allowed - // SNI will not be sent for IP addresses - - // Check for NULL bytes - if hostname.contains('\0') { - return Err(vm.new_type_error("embedded null character")); - } - - Some(hostname.to_string()) - } - None => None, - }; - - // Validate socket type and context protocol - if args.server_side && zelf.protocol == PROTOCOL_TLS_CLIENT { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "Cannot create a server socket with a PROTOCOL_TLS_CLIENT context", - ) - .upcast()); - } - if !args.server_side && zelf.protocol == PROTOCOL_TLS_SERVER { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "Cannot create a client socket with a PROTOCOL_TLS_SERVER context", - ) - .upcast()); - } - - // Create _SSLSocket instance - let ssl_socket = PySSLSocket { - sock: args.sock.clone(), - sock_send_method: socket_class.get_attr("send", vm)?, - sock_recv_method: socket_class.get_attr("recv", vm)?, - tls_record_header_buf: vm - .ctx - .new_bytearray(Vec::with_capacity(TLS_RECORD_HEADER_SIZE)) - .into(), - context: PyRwLock::new(zelf), - server_side: args.server_side, - server_hostname: PyRwLock::new(hostname), - connection: PyMutex::new(None), - handshake_done: PyMutex::new(false), - session_was_reused: PyMutex::new(false), - owner: PyRwLock::new(args.owner.into_option()), - // Filter out Python None objects - only store actual SSLSession objects - session: PyRwLock::new(args.session.into_option().filter(|s| !vm.is_none(s))), - incoming_bio: None, - outgoing_bio: None, - sni_state: PyRwLock::new(None), - pending_context: PyRwLock::new(None), - client_hello_buffer: PyMutex::new(None), - sni_callback_processed: PyMutex::new(false), - shutdown_state: PyMutex::new(ShutdownState::NotStarted), - pending_tls_output: PyMutex::new(Vec::new()), - write_buffered_len: PyMutex::new(0), - deferred_cert_error: Arc::new(ParkingRwLock::new(None)), - }; - - // Create PyRef with correct type - let ssl_socket_ref = ssl_socket - .into_ref_with_type(vm, vm.class("_ssl", "_SSLSocket")) - .map_err(|_| vm.new_type_error("Failed to create SSLSocket"))?; - - Ok(ssl_socket_ref) - } - - #[pymethod] - fn _wrap_bio( - zelf: PyRef, - args: WrapBioArgs, - vm: &VirtualMachine, - ) -> PyResult> { - // Convert server_hostname to Option - // Handle both missing argument and None value - let hostname = match args.server_hostname.into_option().flatten() { - Some(hostname_str) => { - let hostname = hostname_str.as_str(); - validate_hostname(hostname, vm)?; - Some(hostname.to_string()) - } - None => None, - }; - - // Extract server_side value - let server_side = args.server_side.unwrap_or(false); - - // Validate socket type and context protocol - if server_side && zelf.protocol == PROTOCOL_TLS_CLIENT { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "Cannot create a server socket with a PROTOCOL_TLS_CLIENT context", - ) - .upcast()); - } - if !server_side && zelf.protocol == PROTOCOL_TLS_SERVER { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "Cannot create a client socket with a PROTOCOL_TLS_SERVER context", - ) - .upcast()); - } - - // Create _SSLSocket instance with BIO mode - let ssl_socket = PySSLSocket { - // No socket in BIO mode - sock: vm.ctx.none(), - sock_send_method: vm.ctx.none(), - sock_recv_method: vm.ctx.none(), - - tls_record_header_buf: vm.ctx.none(), - context: PyRwLock::new(zelf), - server_side, - server_hostname: PyRwLock::new(hostname), - connection: PyMutex::new(None), - handshake_done: PyMutex::new(false), - session_was_reused: PyMutex::new(false), - owner: PyRwLock::new(args.owner.into_option()), - // Filter out Python None objects - only store actual SSLSession objects - session: PyRwLock::new(args.session.into_option().filter(|s| !vm.is_none(s))), - incoming_bio: Some(args.incoming), - outgoing_bio: Some(args.outgoing), - sni_state: PyRwLock::new(None), - pending_context: PyRwLock::new(None), - client_hello_buffer: PyMutex::new(None), - sni_callback_processed: PyMutex::new(false), - shutdown_state: PyMutex::new(ShutdownState::NotStarted), - pending_tls_output: PyMutex::new(Vec::new()), - write_buffered_len: PyMutex::new(0), - deferred_cert_error: Arc::new(ParkingRwLock::new(None)), - }; - - let ssl_socket_ref = ssl_socket - .into_ref_with_type(vm, vm.class("_ssl", "_SSLSocket")) - .map_err(|_| vm.new_type_error("Failed to create SSLSocket"))?; - - Ok(ssl_socket_ref) - } - - // Helper functions (private): - - /// Parse path argument (str or bytes) to string - fn parse_path_arg( - arg: &Either, - vm: &VirtualMachine, - ) -> PyResult { - match arg { - Either::A(s) => Ok(s.clone().try_into_utf8(vm)?.as_str().to_owned()), - Either::B(b) => String::from_utf8(b.borrow_buf().to_vec()) - .map_err(|_| vm.new_value_error("path contains invalid UTF-8")), - } - } - - /// Parse password argument (str, bytes-like, or callable) - /// - /// Returns (immediate_password, callable) where: - /// - immediate_password: Some(string) if password is str/bytes, None if callable - /// - callable: Some(PyObjectRef) if password is callable, None otherwise - fn parse_password_argument( - password: &OptionalArg, - vm: &VirtualMachine, - ) -> PyResult<(Option, Option)> { - match password { - OptionalArg::Present(p) => { - if vm.is_none(p) { - return Ok((None, None)); - } - - // Try string - if let Ok(pwd_str) = PyUtf8StrRef::try_from_object(vm, p.clone()) { - Ok((Some(pwd_str.as_str().to_owned()), None)) - } - // Try bytes-like - else if let Ok(pwd_bytes_like) = ArgBytesLike::try_from_object(vm, p.clone()) - { - let pwd = String::from_utf8(pwd_bytes_like.borrow_buf().to_vec()) - .map_err(|_| vm.new_type_error("password bytes must be valid UTF-8"))?; - Ok((Some(pwd), None)) - } - // Try callable - else if p.is_callable() { - Ok((None, Some(p.clone()))) - } else { - Err(vm.new_type_error("password should be a string or callable")) - } - } - _ => Ok((None, None)), - } - } - - /// Helper: Load certificates from file into existing store - fn load_certs_from_file_helper( - &self, - root_store: &mut RootCertStore, - ca_certs_der: &mut Vec>, - path: &str, - vm: &VirtualMachine, - ) -> PyResult { - let mut loader = cert::CertLoader::new(root_store, ca_certs_der); - loader.load_from_file(path).map_err(|e| { - // Preserve errno for file access errors (NotFound, PermissionDenied) - match e.kind() { - std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => { - e.into_pyexception(vm) - } - // PEM parsing errors - _ => super::compat::SslError::create_ssl_error_with_reason( - vm, - Some("X509"), - "", - "PEM lib", - ), - } - }) - } - - /// Helper: Load certificates from directory into existing store - fn load_certs_from_dir_helper( - &self, - root_store: &mut RootCertStore, - path: &str, - vm: &VirtualMachine, - ) -> PyResult { - // Load certs and store them in capath_certs_der for lazy loading simulation - // (CPython only returns these in get_ca_certs() after they're used in handshake) - let mut capath_certs = Vec::new(); - let mut loader = cert::CertLoader::new(root_store, &mut capath_certs); - let stats = loader - .load_from_dir(path) - .map_err(|e| e.into_pyexception(vm))?; - - // Store loaded certs for potential tracking after handshake - *self.capath_certs_der.write() = capath_certs; - - Ok(stats) - } - - /// Helper: Load certificates from bytes into existing store - fn load_certs_from_bytes_helper( - &self, - root_store: &mut RootCertStore, - ca_certs_der: &mut Vec>, - data: &[u8], - pem_only: bool, - vm: &VirtualMachine, - ) -> PyResult { - let mut loader = cert::CertLoader::new(root_store, ca_certs_der); - // treat_all_as_ca=true: CPython counts all certificates loaded via cadata as CA certs - // regardless of their Basic Constraints extension - // pem_only=true for string input - loader - .load_from_bytes_ex(data, true, pem_only) - .map_err(|e| { - // Preserve specific error messages from cert.rs - let err_msg = e.to_string(); - if err_msg.contains("no start line") { - vm.new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "no start line: cadata does not contain a certificate", - ) - .upcast() - } else if err_msg.contains("not enough data") { - vm.new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "not enough data: cadata does not contain a certificate", - ) - .upcast() - } else { - vm.new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - err_msg, - ) - .upcast() - } - }) - } - - /// Helper: Try to parse data as CRL (PEM or DER format) - fn try_parse_crl( - &self, - data: &[u8], - ) -> Result, String> { - // Try PEM format first - let mut cursor = std::io::Cursor::new(data); - let mut crl_iter = rustls_pemfile::crls(&mut cursor); - if let Some(Ok(crl)) = crl_iter.next() { - return Ok(crl); - } - - // Try DER format - // Basic validation: CRL should start with SEQUENCE tag (0x30) - if !data.is_empty() && data[0] == 0x30 { - return Ok(CertificateRevocationListDer::from(data.to_vec())); - } - - Err("Not a valid CRL file".to_string()) - } - - /// Helper: Load CRL from file - fn load_crl_from_file( - &self, - path: &str, - vm: &VirtualMachine, - ) -> PyResult>> { - let data = rustpython_host_env::fs::read(path).map_err(|e| match e.kind() { - std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied => { - e.into_pyexception(vm) - } - _ => vm.new_os_error(e.to_string()), - })?; - - match self.try_parse_crl(&data) { - Ok(crl) => Ok(Some(crl)), - Err(_) => Ok(None), // Not a CRL file, might be a cert file - } - } - - /// Helper: Parse cadata argument (str or bytes) - fn parse_cadata_arg( - &self, - arg: &Either, - vm: &VirtualMachine, - ) -> PyResult> { - match arg { - Either::A(s) => Ok(s.clone().try_into_utf8(vm)?.as_str().as_bytes().to_vec()), - Either::B(b) => Ok(b.borrow_buf().to_vec()), - } - } - - /// Helper: Update certificate statistics - fn update_cert_stats(&self, stats: cert::CertStats) { - *self.x509_cert_count.write() += stats.total_certs; - *self.ca_cert_count.write() += stats.ca_certs; - } - } - - impl Representable for PySSLContext { - #[inline] - fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - Ok(format!("", zelf.protocol)) - } - } - - impl Constructor for PySSLContext { - type Args = (i32,); - - fn py_new( - _cls: &Py, - (protocol,): Self::Args, - vm: &VirtualMachine, - ) -> PyResult { - let crypto_ext = CryptoExt::get_ext(); - - // Validate protocol - match protocol { - PROTOCOL_TLS | PROTOCOL_TLS_CLIENT | PROTOCOL_TLS_SERVER | PROTOCOL_TLSv1_2 - | PROTOCOL_TLSv1_3 => { - // Valid protocols - } - PROTOCOL_TLSv1 | PROTOCOL_TLSv1_1 => { - return Err(vm.new_value_error( - "TLS 1.0 and 1.1 are not supported by rustls for security reasons", - )); - } - _ => { - return Err(vm.new_value_error(format!("invalid protocol version: {protocol}"))); - } - } - - // Set default options - // OP_ALL | OP_NO_SSLv2 | OP_NO_SSLv3 | OP_NO_COMPRESSION | - // OP_CIPHER_SERVER_PREFERENCE | OP_SINGLE_DH_USE | OP_SINGLE_ECDH_USE | - // OP_ENABLE_MIDDLEBOX_COMPAT - let default_options = OP_ALL - | OP_NO_SSLv2 - | OP_NO_SSLv3 - | OP_NO_COMPRESSION - | OP_CIPHER_SERVER_PREFERENCE - | OP_SINGLE_DH_USE - | OP_SINGLE_ECDH_USE - | OP_ENABLE_MIDDLEBOX_COMPAT; - - // Set default verify_mode based on protocol - // PROTOCOL_TLS_CLIENT defaults to CERT_REQUIRED - // PROTOCOL_TLS_SERVER defaults to CERT_NONE - let default_verify_mode = if protocol == PROTOCOL_TLS_CLIENT { - CERT_REQUIRED - } else { - CERT_NONE - }; - - // Set default verify_flags based on protocol - // Both PROTOCOL_TLS_CLIENT and PROTOCOL_TLS_SERVER only set VERIFY_X509_TRUSTED_FIRST - // Note: VERIFY_X509_PARTIAL_CHAIN and VERIFY_X509_STRICT are NOT set here - // - they're only added by create_default_context() in Python's ssl.py - let default_verify_flags = VERIFY_DEFAULT | VERIFY_X509_TRUSTED_FIRST; - - // Set minimum and maximum protocol versions based on protocol constant - // specific protocol versions fix both min and max - let (min_version, max_version) = match protocol { - PROTOCOL_TLSv1_2 => (PROTO_TLSv1_2, PROTO_TLSv1_2), // Only TLS 1.2 - PROTOCOL_TLSv1_3 => (PROTO_TLSv1_3, PROTO_TLSv1_3), // Only TLS 1.3 - _ => (PROTO_MINIMUM_SUPPORTED, PROTO_MAXIMUM_SUPPORTED), // Auto-negotiate - }; - - // IMPORTANT: Create shared session cache BEFORE PySSLContext - // Both client_session_cache and PythonClientSessionStore.session_cache - // MUST point to the same HashMap to ensure Python-level and Rustls-level - // sessions are synchronized - let shared_session_cache = Arc::new(ParkingRwLock::new(HashMap::new())); - let rustls_client_store = Arc::new(PythonClientSessionStore { - inner: Arc::new(rustls::client::ClientSessionMemoryCache::new( - SSL_SESSION_CACHE_SIZE, - )), - session_cache: shared_session_cache.clone(), - }); - - Ok(Self { - protocol, - check_hostname: PyRwLock::new(protocol == PROTOCOL_TLS_CLIENT), - verify_mode: PyRwLock::new(default_verify_mode), - verify_flags: PyRwLock::new(default_verify_flags), - server_config: PyRwLock::new(None), - root_certs: PyRwLock::new(RootCertStore::empty()), - ca_certs_der: PyRwLock::new(Vec::new()), - capath_certs_der: PyRwLock::new(Vec::new()), - crls: PyRwLock::new(Vec::new()), - cert_keys: PyRwLock::new(Vec::new()), - options: PyRwLock::new(default_options), - alpn_protocols: PyRwLock::new(Vec::new()), - post_handshake_auth: PyRwLock::new(false), - num_tickets: PyRwLock::new(2), // TLS 1.3 default - minimum_version: PyRwLock::new(min_version), - maximum_version: PyRwLock::new(max_version), - sni_callback: PyRwLock::new(None), - msg_callback: PyRwLock::new(None), - ecdh_curve: PyRwLock::new(None), - ca_cert_count: PyRwLock::new(0), - x509_cert_count: PyRwLock::new(0), - // Use the shared cache created above - client_session_cache: shared_session_cache, - rustls_session_store: rustls_client_store, - rustls_server_session_store: rustls::server::ServerSessionMemoryCache::new( - SSL_SESSION_CACHE_SIZE, - ), - server_ticketer: (crypto_ext.ticketer)() - .expect("Failed to create shared ticketer for TLS 1.2 session resumption"), - accept_count: AtomicUsize::new(0), - session_hits: AtomicUsize::new(0), - selected_ciphers: PyRwLock::new(None), - }) - } - } - - // SSLSocket - represents a TLS-wrapped socket - #[pyattr] - #[pyclass(name = "_SSLSocket", module = "ssl", traverse)] - #[derive(Debug, PyPayload)] - pub(crate) struct PySSLSocket { - // Underlying socket - sock: PyObjectRef, - // Cached socket.socket.send - #[pytraverse(skip)] - sock_send_method: PyObjectRef, - // Cached socket.socket.recv - #[pytraverse(skip)] - sock_recv_method: PyObjectRef, - // Header of currently read TLS record. - #[pytraverse(skip)] - tls_record_header_buf: PyObjectRef, - // SSL context - context: PyRwLock>, - // Server-side or client-side - #[pytraverse(skip)] - server_side: bool, - // Server hostname for SNI - #[pytraverse(skip)] - server_hostname: PyRwLock>, - // TLS connection state - #[pytraverse(skip)] - connection: PyMutex>, - // Handshake completed flag - #[pytraverse(skip)] - handshake_done: PyMutex, - // Session was reused (for session resumption tracking) - #[pytraverse(skip)] - session_was_reused: PyMutex, - // Owner (SSLSocket instance that owns this _SSLSocket) - owner: PyRwLock>, - // Session for resumption - session: PyRwLock>, - // MemoryBIO mode (optional) - incoming_bio: Option>, - outgoing_bio: Option>, - // SNI certificate resolver state (for server-side only) - #[pytraverse(skip)] - sni_state: PyRwLock>>>, - // Pending context change (for SNI callback deferred handling) - pending_context: PyRwLock>>, - // Buffer to store ClientHello for connection recreation - #[pytraverse(skip)] - client_hello_buffer: PyMutex>>, - // Whether the Python SNI callback has already been run for this handshake - #[pytraverse(skip)] - sni_callback_processed: PyMutex, - // Shutdown state for tracking close-notify exchange - #[pytraverse(skip)] - shutdown_state: PyMutex, - // 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>, - // 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, - // 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 - // Using Arc to share with the certificate verifier - #[pytraverse(skip)] - deferred_cert_error: Arc>>, - } - - // Shutdown state for tracking close-notify exchange - #[derive(Debug, Clone, Copy, PartialEq)] - enum ShutdownState { - NotStarted, // unwrap() not called yet - SentCloseNotify, // close-notify sent, waiting for peer's response - Completed, // unwrap() completed successfully - } - - /// 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 - pub(crate) fn is_bio_mode(&self) -> bool { - self.incoming_bio.is_some() && self.outgoing_bio.is_some() - } - - // Get incoming BIO reference (for EOF checking) - pub(crate) fn incoming_bio(&self) -> Option { - self.incoming_bio.as_ref().map(|bio| bio.clone().into()) - } - - // 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<()> { - let error_opt = self.deferred_cert_error.read().clone(); - if let Some(error_msg) = error_opt { - // Clear the error so it's only raised once - *self.deferred_cert_error.write() = None; - // Raise OSError with the stored error message - return Err(vm.new_os_error(error_msg)); - } - Ok(()) - } - - // Get socket timeout as Duration - pub(crate) fn get_socket_timeout(&self, vm: &VirtualMachine) -> PyResult> { - if self.is_bio_mode() { - return Ok(None); - } - - // Get timeout from socket - let timeout_obj = self.sock.get_attr("gettimeout", vm)?.call((), vm)?; - - // timeout can be None (blocking), 0.0 (non-blocking), or positive float - if vm.is_none(&timeout_obj) { - // None means blocking forever - Ok(None) - } else { - let timeout_float: f64 = timeout_obj.try_into_value(vm)?; - if timeout_float <= 0.0 { - // 0 means non-blocking - Ok(Some(Duration::from_secs(0))) - } else { - // Positive timeout - Ok(Some(Duration::from_secs_f64(timeout_float))) - } - } - } - - // Create and store a session object after successful handshake - fn create_session_after_handshake(&self, vm: &VirtualMachine) { - // Only create session for client-side connections - if self.server_side { - return; - } - - // Check if session already exists - let session_opt = self.session.read().clone(); - if let Some(ref s) = session_opt { - if vm.is_none(s) { - } else { - return; - } - } - - // Get server hostname - let server_name = self.server_hostname.read().clone(); - - // Try to get session data from context's session cache - // IMPORTANT: Acquire and release locks quickly to avoid deadlock - let context = self.context.read(); - let session_cache_arc = context.client_session_cache.clone(); - drop(context); // Release context lock ASAP - - let (session_id, creation_time, lifetime) = if let Some(ref name) = server_name { - let key = name.as_bytes().to_vec(); - - // Clone the data we need while holding the lock, then immediately release - let session_data_opt = { - let cache_guard = session_cache_arc.read(); - cache_guard.get(&key).cloned() // Clone Arc> - }; // Lock released here - - if let Some(session_data_arc) = session_data_opt { - let data = session_data_arc.lock(); - let result = (data.session_id.clone(), data.creation_time, data.lifetime); - drop(data); // Explicit unlock - result - } else { - // Create new session ID if not in cache - let time = std::time::SystemTime::now(); - (generate_session_id_from_metadata(name, &time), time, 7200) - } - } else { - // No server name, use defaults - let time = std::time::SystemTime::now(); - (vec![0; 16], time, 7200) - }; - - // Create a new SSLSession object with real metadata - let session = PySSLSession { - // Use dummy session data to indicate we have a ticket - // TLS 1.2+ always uses session tickets/resumption - session_data: vec![1], // Non-empty to indicate has_ticket=True - session_id, - creation_time, - lifetime, - }; - - let py_session = session.into_pyobject(vm); - - *self.session.write() = Some(py_session); - } - - // Complete handshake and create session - /// Track which CA certificate from capath was used to verify peer - /// - /// This simulates lazy loading behavior: capath certificates - /// are only added to get_ca_certs() after they're actually used in a handshake. - fn track_used_ca_from_capath(&self) -> Result<(), String> { - // Extract capath_certs, releasing context lock quickly - let capath_certs = { - let context = self.context.read(); - let certs = context.capath_certs_der.read(); - if certs.is_empty() { - return Ok(()); - } - certs.clone() - }; - - // Extract peer certificates, releasing connection lock quickly - let top_cert_der = { - let conn_guard = self.connection.lock(); - let conn = conn_guard.as_ref().ok_or("No connection")?; - let peer_certs = conn.peer_certificates().ok_or("No peer certificates")?; - if peer_certs.is_empty() { - return Ok(()); - } - peer_certs - .iter() - .map(|c| c.as_ref().to_vec()) - .next_back() - .expect("is_empty checked above") - }; - - // Get the top certificate in the chain (closest to root) - // Note: Server usually doesn't send the root CA, so we check the last cert's issuer - let (_, top_cert) = x509_parser::parse_x509_certificate(&top_cert_der) - .map_err(|e| format!("Failed to parse top cert: {e}"))?; - - let top_issuer = top_cert.issuer(); - - // Find matching CA in capath certs (skip unparseable certificates) - let matching_ca = capath_certs.iter().find_map(|ca_der| { - let (_, ca) = x509_parser::parse_x509_certificate(ca_der).ok()?; - // Check if this CA is self-signed (root CA) and matches the issuer - (ca.subject() == ca.issuer() && ca.subject() == top_issuer).then(|| ca_der.clone()) - }); - - // Update ca_certs_der if we found a match - if let Some(ca_der) = matching_ca { - let context = self.context.read(); - let mut ca_certs_der = context.ca_certs_der.write(); - if !ca_certs_der.iter().any(|c| c == &ca_der) { - ca_certs_der.push(ca_der); - } - } - - Ok(()) - } - - fn complete_handshake(&self, vm: &VirtualMachine) { - *self.handshake_done.lock() = true; - - // Check if session was resumed - get value and release lock immediately - let was_resumed = self - .connection - .lock() - .as_ref() - .is_some_and(|conn| conn.handshake_kind() == Some(HandshakeKind::Resumed)); - - *self.session_was_reused.lock() = was_resumed; - - // Update context session statistics if server-side - if self.server_side { - let context = self.context.read(); - // Increment accept count for every successful server handshake - context.accept_count.fetch_add(1, Ordering::SeqCst); - // Increment hits count if session was resumed - if was_resumed { - context.session_hits.fetch_add(1, Ordering::SeqCst); - } - } - - // Track CA certificate used during handshake (client-side only) - // This simulates lazy loading behavior for capath certificates - if !self.server_side { - // Don't fail handshake if tracking fails - let _ = self.track_used_ca_from_capath(); - } - - self.create_session_after_handshake(vm); - } - - // Internal implementation with timeout control - pub(crate) fn sock_wait_for_io_impl( - &self, - wait_kind: SockWaitKind, - vm: &VirtualMachine, - ) -> PyResult { - if self.is_bio_mode() { - // BIO mode doesn't use select - return Ok(false); - } - - // Get timeout - let timeout = self.get_socket_timeout(vm)?; - - // Check for non-blocking mode (timeout = 0) - if let Some(t) = timeout - && t.is_zero() - { - // Non-blocking mode - don't use select - return Ok(false); - } - - // Use select with the effective timeout - let py_socket: PyRef = self.sock.clone().try_into_value(vm)?; - let socket = py_socket - .sock() - .map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?; - - sock_wait(&socket, wait_kind, timeout, vm) - } - - // Internal implementation with explicit timeout override - pub(crate) fn sock_wait_for_io_with_timeout( - &self, - wait_kind: SockWaitKind, - timeout: Option, - vm: &VirtualMachine, - ) -> PyResult { - if self.is_bio_mode() { - // BIO mode doesn't use select - return Ok(false); - } - - if let Some(t) = timeout - && t.is_zero() - { - // Non-blocking mode - don't use select - return Ok(false); - } - - let py_socket: PyRef = self.sock.clone().try_into_value(vm)?; - let socket = py_socket - .sock() - .map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?; - - sock_wait(&socket, wait_kind, timeout, vm).map_err(|e| e.into_pyexception(vm)) - } - - // SNI (Server Name Indication) Helper Methods: - // These methods support the server-side handshake SNI callback mechanism - - /// Check if this is the first read during handshake (for SNI callback) - /// Returns true until the SNI callback has been processed. - pub(crate) fn is_first_sni_read(&self) -> bool { - !*self.sni_callback_processed.lock() - } - - /// Check if SNI callback is configured - pub(crate) fn has_sni_callback(&self) -> bool { - // Nested read locks are safe - self.context.read().sni_callback.read().is_some() - } - - /// Save ClientHello data for potential connection recreation. - pub(crate) fn save_client_hello_from_bytes(&self, bytes_data: &[u8]) { - let mut buffer = self.client_hello_buffer.lock(); - match buffer.as_mut() { - Some(existing) => existing.extend_from_slice(bytes_data), - None => *buffer = Some(bytes_data.to_vec()), - } - } - - /// Get the extracted SNI name from resolver - pub(crate) fn get_extracted_sni_name(&self) -> Option { - // Clone the Arc option to avoid nested lock (sni_state.read -> arc.lock) - let sni_state_opt = self.sni_state.read().clone(); - sni_state_opt.as_ref().and_then(|arc| arc.lock().1.clone()) - } - - /// Invoke the Python SNI callback - pub(crate) fn invoke_sni_callback( - &self, - sni_name: Option<&str>, - vm: &VirtualMachine, - ) -> PyResult<()> { - let callback = self - .context - .read() - .sni_callback - .read() - .clone() - .ok_or_else(|| vm.new_value_error("SNI callback not set"))?; - - let ssl_sock = self.owner.read().clone().unwrap_or_else(|| vm.ctx.none()); - let server_name_py: PyObjectRef = match sni_name { - Some(name) => vm.ctx.new_str(name.to_string()).into(), - None => vm.ctx.none(), - }; - let initial_context: PyObjectRef = self.context.read().clone().into(); - - // catches exceptions from the callback and reports them as unraisable - let result = match callback.call((ssl_sock, server_name_py, initial_context), vm) { - Ok(result) => result, - Err(exc) => { - vm.run_unraisable( - exc, - Some("in ssl servername callback".to_owned()), - callback.clone(), - ); - // Return SSL error like SSL_TLSEXT_ERR_ALERT_FATAL - let ssl_exc: PyBaseExceptionRef = vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "SNI callback raised exception", - ) - .upcast(); - let _ = ssl_exc.as_object().set_attr( - "reason", - vm.ctx.new_str("TLSV1_ALERT_INTERNAL_ERROR"), - vm, - ); - return Err(ssl_exc); - } - }; - - // Check return value type (must be None or integer) - if !vm.is_none(&result) { - // Try to convert to integer - if result.try_to_value::(vm).is_err() { - // Type conversion failed - raise TypeError as unraisable - let type_error = vm.new_type_error(format!( - "servername callback must return None or an integer, not '{}'", - result.class().name() - )); - vm.run_unraisable(type_error, None, result); - - // Return SSL error with reason set to TLSV1_ALERT_INTERNAL_ERROR - // - // RUSTLS API LIMITATION: - // We cannot send a TLS InternalError alert to the client here because: - // 1. Rustls does not provide a public API like send_fatal_alert() - // 2. This method is called AFTER dropping the connection lock (to prevent deadlock) - // 3. By the time we detect the error, the connection is no longer available - // - // CPython/OpenSSL behavior: - // - SNI callback runs inside SSL_do_handshake with connection active - // - Sets *al = SSL_AD_INTERNAL_ERROR - // - OpenSSL automatically sends alert before returning - // - // RustPython/Rustls behavior: - // - SNI callback runs after dropping connection lock (deadlock prevention) - // - Exception has _reason='TLSV1_ALERT_INTERNAL_ERROR' for error reporting - // - TCP connection closes without sending TLS alert to client - // - // If rustls adds send_fatal_alert() API in the future, we should: - // - Re-acquire connection lock after callback - // - Call: connection.send_fatal_alert(AlertDescription::InternalError) - // - Then close connection - let exc: PyBaseExceptionRef = vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "SNI callback returned invalid type", - ) - .upcast(); - let _ = exc.as_object().set_attr( - "reason", - vm.ctx.new_str("TLSV1_ALERT_INTERNAL_ERROR"), - vm, - ); - return Err(exc); - } - } - - Ok(()) - } - - // Helper to call socket methods, bypassing any SSL wrapper - pub(crate) fn sock_recv(&self, size: usize, vm: &VirtualMachine) -> PyResult { - // In BIO mode, read from incoming BIO (flags not supported) - if let Some(ref bio) = self.incoming_bio { - let bio_obj: PyObjectRef = bio.clone().into(); - let read_method = bio_obj.get_attr("read", vm)?; - return read_method.call((vm.ctx.new_int(size),), vm); - } - - self.sock_recv_method - .call((self.sock.clone(), vm.ctx.new_int(size)), vm) - } - - // Helper to receive data for at most one TLS record. - // May return incomplete data but never returns more when completes a - // previously incomplete TLS record. - pub(crate) fn sock_recv_at_most_one_tls_record( - &self, - vm: &VirtualMachine, - ) -> PyResult { - let obj_to_bytes = |bytes_obj| { - PyBytesRef::try_from_object(vm, bytes_obj) - .map_err(|_| vm.new_os_error("Expected bytes from recv".to_string())) - }; - - let tls_record_header_buf = self - .tls_record_header_buf - .clone() - .downcast::() - .expect("BUG: tls_record_header_buf is not PyByteArray"); - - let buf_len = tls_record_header_buf.borrow_buf().len(); - - let (mut with_header, mut remaining_record_body_len) = - if buf_len < TLS_RECORD_HEADER_SIZE { - // We do not have a full TLS record header, start receiving one. - let bytes_obj = self.sock_recv(TLS_RECORD_HEADER_SIZE - buf_len, vm)?; - let bytes = obj_to_bytes(bytes_obj)?; - - let mut buf = tls_record_header_buf.borrow_buf_mut(); - buf.extend_from_slice(bytes.as_bytes()); - - if buf.len() < TLS_RECORD_HEADER_SIZE { - return Ok(bytes); - } - - // Parse the remaining length. - let record_body_len = u16::from_be_bytes([buf[3], buf[4]]); - // Validity of length value will be checked by rustls. - - // Zero-length TLS record. - if record_body_len == 0 { - buf.clear(); - return Ok(bytes); - } - - let mut bytes_vec = bytes.as_bytes().to_vec(); - bytes_vec.reserve(record_body_len as usize); - (Some(bytes_vec), record_body_len) - } else { - let buf = tls_record_header_buf.borrow_buf(); - let remaining_record_body_len = u16::from_be_bytes([buf[3], buf[4]]); - (None, remaining_record_body_len) - }; - - // We have full record header and are in a process of receiving a record. - let bytes_obj = self.sock_recv(remaining_record_body_len as usize, vm)?; - let bytes = obj_to_bytes(bytes_obj)?; - - if let Some(with_header) = with_header.as_mut() { - with_header.extend_from_slice(bytes.as_bytes()); - } - - let mut buf = tls_record_header_buf.borrow_buf_mut(); - remaining_record_body_len -= bytes.len() as u16; - if remaining_record_body_len == 0 { - // Record received completely, need to start a new one beginning with its header. - buf.clear(); - } else { - // Update remaining length in the header. - buf.as_mut_slice()[3..5].copy_from_slice(&remaining_record_body_len.to_be_bytes()); - } - - if let Some(with_header) = with_header { - Ok(vm.ctx.new_bytes(with_header)) - } else { - Ok(bytes) - } - } - - /// Socket send - just sends data, caller must handle pending flush - /// Use flush_pending_tls_output before this if ordering is important - pub(crate) fn sock_send(&self, data: &[u8], vm: &VirtualMachine) -> PyResult { - // In BIO mode, write to outgoing BIO - if let Some(ref bio) = self.outgoing_bio { - let bio_obj: PyObjectRef = bio.clone().into(); - let write_method = bio_obj.get_attr("write", vm)?; - return write_method.call((vm.ctx.new_bytes(data.to_vec()),), vm); - } - - self.sock_send_method - .call((self.sock.clone(), vm.ctx.new_bytes(data.to_vec())), vm) - } - - /// Flush any pending TLS output data to the socket - /// Optional deadline parameter allows respecting a read deadline during flush - pub(crate) fn flush_pending_tls_output( - &self, - vm: &VirtualMachine, - deadline: Option, - ) -> PyResult<()> { - let mut pending = self.pending_tls_output.lock(); - if pending.is_empty() { - return Ok(()); - } - - let socket_timeout = self.get_socket_timeout(vm)?; - let is_non_blocking = socket_timeout.is_some_and(|t| t.is_zero()); - - let mut sent_total = 0; - - while sent_total < pending.len() { - // Calculate timeout: use deadline if provided, otherwise use socket timeout - let timeout_to_use = if let Some(dl) = deadline { - let now = std::time::Instant::now(); - if now >= dl { - // Deadline already passed - *pending = pending[sent_total..].to_vec(); - return Err( - timeout_error_msg(vm, "The operation timed out".to_string()).upcast() - ); - } - Some(dl - now) - } else { - socket_timeout - }; - - // Use sock_wait directly with calculated timeout - let py_socket: PyRef = self.sock.clone().try_into_value(vm)?; - let socket = py_socket - .sock() - .map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?; - let timed_out = sock_wait(&socket, SockWaitKind::Write, timeout_to_use, vm)?; - - if timed_out { - // Keep unsent data in pending buffer - *pending = pending[sent_total..].to_vec(); - if is_non_blocking { - return Err(create_ssl_want_write_error(vm).upcast()); - } - return Err( - timeout_error_msg(vm, "The write operation timed out".to_string()).upcast(), - ); - } - - match self.sock_send(&pending[sent_total..], vm) { - Ok(result) => { - let sent: usize = result.try_to_value::(vm)?.try_into().unwrap_or(0); - if sent == 0 { - if is_non_blocking { - // Keep unsent data in pending buffer - *pending = pending[sent_total..].to_vec(); - return Err(create_ssl_want_write_error(vm).upcast()); - } - // Socket said ready but sent 0 bytes - retry - continue; - } - sent_total += sent; - } - Err(e) => { - if is_blocking_io_error(&e, vm) { - if is_non_blocking { - // Keep unsent data in pending buffer - *pending = pending[sent_total..].to_vec(); - return Err(create_ssl_want_write_error(vm).upcast()); - } - continue; - } - // Keep unsent data in pending buffer for other errors too - *pending = pending[sent_total..].to_vec(); - return Err(e); - } - } - } - - // All data sent successfully - pending.clear(); - Ok(()) - } - - /// Send TLS output data to socket, saving unsent bytes to pending buffer - /// This prevents data loss when rustls' write_tls() drains its internal buffer - /// but the socket cannot accept all the data immediately - fn send_tls_output(&self, buf: Vec, vm: &VirtualMachine) -> PyResult<()> { - if buf.is_empty() { - return Ok(()); - } - - let timeout = self.get_socket_timeout(vm)?; - let is_non_blocking = timeout.is_some_and(|t| t.is_zero()); - - let mut sent_total = 0; - while sent_total < buf.len() { - let timed_out = self.sock_wait_for_io_impl(SockWaitKind::Write, vm)?; - if timed_out { - // Save unsent data to pending buffer - self.pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err( - timeout_error_msg(vm, "The write operation timed out".to_string()).upcast(), - ); - } - - match self.sock_send(&buf[sent_total..], vm) { - Ok(result) => { - let sent: usize = result.try_to_value::(vm)?.try_into().unwrap_or(0); - if sent == 0 { - if is_non_blocking { - // Save unsent data to pending buffer - self.pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(create_ssl_want_write_error(vm).upcast()); - } - continue; - } - sent_total += sent; - } - Err(e) => { - if is_blocking_io_error(&e, vm) { - if is_non_blocking { - // Save unsent data to pending buffer - self.pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(create_ssl_want_write_error(vm).upcast()); - } - continue; - } - // Save unsent data for other errors too - self.pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(e); - } - } - } - - Ok(()) - } - - /// Flush all pending TLS output data, respecting socket timeout - /// Used during handshake completion and shutdown() to ensure all data is sent - pub(crate) fn blocking_flush_all_pending(&self, vm: &VirtualMachine) -> PyResult<()> { - // Get socket timeout to respect during flush - let timeout = self.get_socket_timeout(vm)?; - if timeout.is_some_and(|t| t.is_zero()) { - return self.flush_pending_tls_output(vm, None); - } - - loop { - let pending_data = { - let pending = self.pending_tls_output.lock(); - if pending.is_empty() { - return Ok(()); - } - pending.clone() - }; - - // Wait for socket to be writable, respecting socket timeout - let py_socket: PyRef = self.sock.clone().try_into_value(vm)?; - let socket = py_socket - .sock() - .map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?; - let timed_out = sock_wait(&socket, SockWaitKind::Write, timeout, vm)?; - - if timed_out { - return Err( - timeout_error_msg(vm, "The write operation timed out".to_string()).upcast(), - ); - } - - // Try to send pending data (use raw to avoid recursion) - match self.sock_send(&pending_data, vm) { - Ok(result) => { - let sent: usize = result.try_to_value::(vm)?.try_into().unwrap_or(0); - if sent > 0 { - let mut pending = self.pending_tls_output.lock(); - pending.drain(..sent); - } - // If sent == 0, loop will retry with sock_wait - } - Err(e) => { - if is_blocking_io_error(&e, vm) { - continue; - } - return Err(e); - } - } - } - } - - // 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) - } - - /// Helper: Prepare KX groups (ECDH curve) from context settings - fn prepare_kx_groups( - &self, - vm: &VirtualMachine, - ) -> PyResult>> { - let ctx = self.context.read(); - let ecdh_curve = ctx.ecdh_curve.read().clone(); - drop(ctx); - - if let Some(ref curve_name) = ecdh_curve { - match curve_name_to_kx_group(curve_name) { - Ok(groups) => Ok(Some(groups)), - Err(e) => Err(vm.new_value_error(format!("Failed to set ECDH curve: {e}"))), - } - } else { - Ok(None) - } - } - - /// Helper: Prepare all common protocol settings (versions, KX groups, ciphers, ALPN) - fn prepare_protocol_settings(&self, vm: &VirtualMachine) -> PyResult { - let ctx = self.context.read(); - let versions = self.prepare_tls_versions(); - let kx_groups = self.prepare_kx_groups(vm)?; - let cipher_suites = ctx.selected_ciphers.read().clone(); - let alpn_protocols = ctx.alpn_protocols.read().clone(); - - Ok(ProtocolSettings { - versions, - kx_groups, - cipher_suites, - alpn_protocols, - }) - } - - /// Initialize server-side TLS connection with configuration - /// - /// This method handles all server-side setup including: - /// - Certificate and key validation - /// - Client authentication configuration - /// - SNI (Server Name Indication) setup - /// - ALPN protocol negotiation - /// - Session resumption configuration - /// - /// Returns the configured ServerConnection. - fn initialize_server_connection( - &self, - conn_guard: &mut Option, - vm: &VirtualMachine, - ) -> PyResult<()> { - let ctx = self.context.read(); - let cert_keys = ctx.cert_keys.read(); - - if cert_keys.is_empty() { - return Err(vm.new_value_error( - "Server-side connection requires certificate and key (use load_cert_chain)", - )); - } - - // Clone cert_keys for use in config - // PrivateKeyDer doesn't implement Clone, use clone_key() - let cert_keys_clone: Vec = cert_keys - .iter() - .map(|(ck, pk)| (ck.clone(), pk.clone_key())) - .collect(); - drop(cert_keys); - - // Prepare common protocol settings (TLS versions, ECDH curve, cipher suites, ALPN) - let protocol_settings = self.prepare_protocol_settings(vm)?; - let min_ver = *ctx.minimum_version.read(); - - // Check if client certificate verification is required - let verify_mode = *ctx.verify_mode.read(); - let root_store = ctx.root_certs.read(); - let pha_enabled = *ctx.post_handshake_auth.read(); - - // Check if TLS 1.3 is being used - let is_tls13 = min_ver >= PROTO_TLSv1_3; - - // For TLS 1.3: always use deferred validation for client certificates - // For TLS 1.2: use immediate validation during handshake - let use_deferred_validation = is_tls13 - && !pha_enabled - && (verify_mode == CERT_REQUIRED || verify_mode == CERT_OPTIONAL); - - // For TLS 1.3 + PHA: if PHA is enabled, don't request cert in initial handshake - // The certificate will be requested later via verify_client_post_handshake() - let request_initial_cert = if pha_enabled { - // PHA enabled: don't request cert initially (will use PHA later) - false - } else if verify_mode == CERT_REQUIRED || verify_mode == CERT_OPTIONAL { - // PHA not enabled or TLS 1.2: request cert in initial handshake - true - } else { - // CERT_NONE - false - }; - - // Check if SNI callback is set - let sni_callback = ctx.sni_callback.read().clone(); - let use_sni_resolver = sni_callback.is_some(); - - // Create SNI state if needed (to be stored in PySSLSocket later) - // For SNI, use the first cert_key pair as the initial certificate - let sni_state: Option>> = if use_sni_resolver { - // Use first cert_key as initial certificate for SNI - // Extract CertifiedKey from tuple - let (first_cert_key, _) = &cert_keys_clone[0]; - let first_cert_key = first_cert_key.clone(); - - // Check if we already have existing SNI state (from previous connection) - let existing_sni_state = self.sni_state.read().clone(); - - if let Some(sni_state_arc) = existing_sni_state { - // Reuse existing Arc and update its contents - // This is crucial: rustls SniCertResolver holds references to this Arc - let mut state = sni_state_arc.lock(); - state.0 = first_cert_key; - state.1 = None; // Reset SNI name for new connection - drop(state); - - // Return the existing Arc (not a new one!) - Some(sni_state_arc) - } else { - // First connection: create new SNI state - Some(Arc::new(ParkingMutex::new((first_cert_key, None)))) - } - } else { - None - }; - - // Determine which cert resolver to use - // Priority: SNI > Multi-cert/Single-cert via MultiCertResolver - let cert_resolver: Option> = if use_sni_resolver { - // SNI takes precedence - use first cert_key for initial setup - sni_state.as_ref().map(|sni_state_arc| { - Arc::new(SniCertResolver { - sni_state: sni_state_arc.clone(), - }) as Arc - }) - } else { - // Use MultiCertResolver for all cases (single or multiple certs) - // Extract CertifiedKey from tuples for MultiCertResolver - let cert_keys_only: Vec> = - cert_keys_clone.iter().map(|(ck, _)| ck.clone()).collect(); - Some(Arc::new(MultiCertResolver::new(cert_keys_only))) - }; - - // Extract cert_chain and private_key from first cert_key - // - // Note: Since we always use cert_resolver now, these values won't actually be used - // by create_server_config. But we still need to provide them for the API signature. - let (first_cert_key, _) = &cert_keys_clone[0]; - let certs_clone = first_cert_key.cert.clone(); - - // Provide a dummy key since cert_resolver will handle cert selection - let key_clone = PrivateKeyDer::Pkcs8(Vec::new().into()); - - // Get shared server session storage and ticketer from context - let server_session_storage = ctx.rustls_server_session_store.clone(); - let server_ticketer = ctx.server_ticketer.clone(); - - // Build server config using compat helper - let config_options = ServerConfigOptions { - protocol_settings, - cert_chain: certs_clone, - private_key: key_clone, - root_store: if request_initial_cert { - Some(root_store.clone()) - } else { - None - }, - request_client_cert: request_initial_cert, - use_deferred_validation, - cert_resolver, - deferred_cert_error: if use_deferred_validation { - Some(self.deferred_cert_error.clone()) - } else { - None - }, - session_storage: Some(server_session_storage), - ticketer: Some(server_ticketer), - }; - - drop(root_store); - - // Check if we have a cached ServerConfig - let cached_config_arc = ctx.server_config.read().clone(); - drop(ctx); - - let config_arc = if let Some(cached) = cached_config_arc { - // Don't use cache when SNI is enabled, because each connection needs - // a fresh SniCertResolver with the correct Arc references - if use_sni_resolver { - let config = - create_server_config(config_options).map_err(|e| vm.new_value_error(e))?; - Arc::new(config) - } else { - cached - } - } else { - let config = - create_server_config(config_options).map_err(|e| vm.new_value_error(e))?; - let config_arc = Arc::new(config); - - // Cache the ServerConfig for future connections - let ctx = self.context.read(); - *ctx.server_config.write() = Some(config_arc.clone()); - drop(ctx); - - config_arc - }; - - let conn = ServerConnection::new(config_arc).map_err(|e| { - vm.new_value_error(format!("Failed to create server connection: {e}")) - })?; - - *conn_guard = Some(Connection::Server(conn)); - - // If ClientHello buffer exists (from SNI callback), re-inject it - if let Some(ref hello_data) = *self.client_hello_buffer.lock() - && let Some(Connection::Server(ref mut server)) = *conn_guard - { - let mut cursor = std::io::Cursor::new(hello_data.as_slice()); - let _ = server.read_tls(&mut cursor); - - // Process the re-injected ClientHello - let _ = server.process_new_packets(); - - // DON'T clear buffer - keep it to prevent callback from being invoked again - // The buffer being non-empty signals that SNI callback was already processed - } - - // Store SNI state if we're using SNI resolver - if let Some(sni_state_arc) = sni_state { - *self.sni_state.write() = Some(sni_state_arc); - } - - Ok(()) - } - - #[pymethod] - fn do_handshake(&self, vm: &VirtualMachine) -> PyResult<()> { - // Check if handshake already done - if *self.handshake_done.lock() { - return Ok(()); - } - - let mut conn_guard = self.connection.lock(); - - // Initialize connection if not already done - if conn_guard.is_none() { - // Check for pending context change (from SNI callback) - if let Some(new_ctx) = self.pending_context.write().take() { - *self.context.write() = new_ctx; - } - - if self.server_side { - // Server-side connection - delegate to helper method - self.initialize_server_connection(&mut conn_guard, vm)?; - } else { - // Client-side connection - let ctx = self.context.read(); - - // Prepare common protocol settings (TLS versions, ECDH curve, cipher suites, ALPN) - let protocol_settings = self.prepare_protocol_settings(vm)?; - - // Clone values we need before building config - let verify_mode = *ctx.verify_mode.read(); - let root_store_clone = ctx.root_certs.read().clone(); - let ca_certs_der_clone = ctx.ca_certs_der.read().clone(); - - // For client mTLS: extract cert_chain and private_key from first cert_key (if any) - // Now we store both CertifiedKey and PrivateKeyDer as tuple - let cert_keys_guard = ctx.cert_keys.read(); - let (cert_chain_clone, private_key_opt) = if !cert_keys_guard.is_empty() { - let (first_cert_key, private_key) = &cert_keys_guard[0]; - let certs = first_cert_key.cert.clone(); - (certs, Some(private_key.clone_key())) - } else { - (Vec::new(), None) - }; - drop(cert_keys_guard); - - let check_hostname = *ctx.check_hostname.read(); - let verify_flags = *ctx.verify_flags.read(); - - // Get session store before dropping ctx - let session_store = ctx.rustls_session_store.clone(); - - // Get CRLs for revocation checking - let crls_clone = ctx.crls.read().clone(); - - // Drop ctx early to avoid borrow conflicts - drop(ctx); - - // Build client config using compat helper - let config_options = ClientConfigOptions { - protocol_settings, - root_store: if verify_mode != CERT_NONE { - Some(root_store_clone) - } else { - None - }, - ca_certs_der: ca_certs_der_clone, - cert_chain: if !cert_chain_clone.is_empty() { - Some(cert_chain_clone) - } else { - None - }, - private_key: private_key_opt, - verify_server_cert: verify_mode != CERT_NONE, - check_hostname, - verify_flags, - session_store: Some(session_store), - crls: crls_clone, - }; - - let config = - create_client_config(config_options).map_err(|e| vm.new_value_error(e))?; - - // Parse server name for SNI - // Convert to ServerName - use rustls::pki_types::ServerName; - let hostname_opt = self.server_hostname.read().clone(); - - let server_name = if let Some(ref hostname) = hostname_opt { - // Use the provided hostname for SNI - ServerName::try_from(hostname.clone()).map_err(|e| { - vm.new_value_error(format!("Invalid server hostname: {e:?}")) - })? - } else { - // When server_hostname=None, use an IP address to suppress SNI - // no hostname = no SNI extension - ServerName::IpAddress( - core::net::IpAddr::V4(core::net::Ipv4Addr::new(127, 0, 0, 1)).into(), - ) - }; - - let conn = ClientConnection::new(Arc::new(config), server_name.clone()) - .map_err(|e| { - vm.new_value_error(format!("Failed to create client connection: {e}")) - })?; - - *conn_guard = Some(Connection::Client(conn)); - } - } - - // Perform the actual handshake by exchanging data with the socket/BIO - - let conn = conn_guard.as_mut().expect("unreachable"); - let is_client = matches!(conn, Connection::Client(_)); - let handshake_result = ssl_do_handshake(conn, self, vm); - drop(conn_guard); - - if is_client { - // CLIENT is simple - no SNI callback handling needed - handshake_result.map_err(|e| e.into_py_err(vm))?; - self.complete_handshake(vm); - Ok(()) - } else { - // Use OpenSSL-compatible handshake for server - // Handle SNI callback restart - match handshake_result { - Ok(()) => { - // Handshake completed successfully - self.complete_handshake(vm); - Ok(()) - } - Err(SslError::SniCallbackRestart) => { - // SNI detected - need to call callback and recreate connection - - // Get the SNI name that was extracted (may be None if client didn't send SNI) - let sni_name = self.get_extracted_sni_name(); - - // Now safe to call Python callback (no locks held) - self.invoke_sni_callback(sni_name.as_deref(), vm)?; - *self.sni_callback_processed.lock() = true; - - // Clear connection to trigger recreation - *self.connection.lock() = None; - - // Recursively call do_handshake to recreate with new context - self.do_handshake(vm) - } - Err(e) => { - // Other errors - convert to Python exception - Err(e.into_py_err(vm)) - } - } - } - } - - #[pymethod] - fn read( - &self, - len: OptionalArg, - buffer: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult { - // Convert len to usize, defaulting to 1024 if not provided - // -1 means read all available data (treat as large buffer size) - let len_val = len.unwrap_or(PEM_BUFSIZE as isize); - let mut len = if len_val == -1 { - // -1 is only valid when a buffer is provided - match &buffer { - OptionalArg::Present(buf_arg) => buf_arg.len(), - OptionalArg::Missing => { - return Err(vm.new_value_error("negative read length")); - } - } - } else if len_val < 0 { - return Err(vm.new_value_error("negative read length")); - } else { - len_val as usize - }; - - // if buffer is provided, limit len to buffer size - if let OptionalArg::Present(buf_arg) = &buffer { - let buf_len = buf_arg.len(); - if len_val <= 0 || len > buf_len { - len = buf_len; - } - } - - // return empty bytes immediately for len=0 - if len == 0 { - return match buffer { - OptionalArg::Present(_) => Ok(vm.ctx.new_int(0).into()), - OptionalArg::Missing => Ok(vm.ctx.new_bytes(vec![]).into()), - }; - } - - // Ensure handshake is done - if not, complete it first - // This matches OpenSSL behavior where SSL_read() auto-completes handshake - if !*self.handshake_done.lock() { - self.do_handshake(vm)?; - } - - // Check if connection has been shut down - // Only block after shutdown is COMPLETED, not during shutdown process - let shutdown_state = *self.shutdown_state.lock(); - if shutdown_state == ShutdownState::Completed { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "cannot read after shutdown", - ) - .upcast()); - } - - // Helper function to handle return value based on buffer presence - let return_data = |data: Vec, - buffer_arg: &OptionalArg, - vm: &VirtualMachine| - -> PyResult { - match buffer_arg { - OptionalArg::Present(buf_arg) => { - // Write into buffer and return number of bytes written - let n = data.len(); - if n > 0 { - let mut buf = buf_arg.borrow_buf_mut(); - let buf_slice = &mut *buf; - let copy_len = n.min(buf_slice.len()); - buf_slice[..copy_len].copy_from_slice(&data[..copy_len]); - } - Ok(vm.ctx.new_int(n).into()) - } - OptionalArg::Missing => { - // Return bytes object - Ok(vm.ctx.new_bytes(data).into()) - } - } - }; - - // Use compat layer for unified read logic with proper EOF handling - // This matches SSL_read_ex() approach - let mut buf = vec![0u8; len]; - let read_result = { - let mut conn_guard = self.connection.lock(); - let conn = conn_guard - .as_mut() - .ok_or_else(|| vm.new_value_error("Connection not established"))?; - crate::ssl::compat::ssl_read(conn, &mut buf, self, vm) - }; - match read_result { - Ok(n) => { - // 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)?; - buf.truncate(n); - return_data(buf, &buffer, vm) - } - Err(crate::ssl::compat::SslError::Eof) => { - // If plaintext is still buffered, return it before EOF. - let pending = { - let mut conn_guard = self.connection.lock(); - let conn = match conn_guard.as_mut() { - 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()) - }; - if pending > 0 { - let mut buf = vec![0u8; pending.min(len)]; - let read_retry = { - let mut conn_guard = self.connection.lock(); - let conn = conn_guard - .as_mut() - .ok_or_else(|| vm.new_value_error("Connection not established"))?; - crate::ssl::compat::ssl_read(conn, &mut buf, self, vm) - }; - if let Ok(n) = read_retry { - buf.truncate(n); - return return_data(buf, &buffer, vm); - } - } - // EOF occurred in violation of protocol (unexpected closure) - Err(create_ssl_eof_error(vm).upcast()) - } - Err(crate::ssl::compat::SslError::ZeroReturn) => { - // If plaintext is still buffered, return it before clean EOF. - let pending = { - let mut conn_guard = self.connection.lock(); - let conn = match conn_guard.as_mut() { - 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()) - }; - if pending > 0 { - let mut buf = vec![0u8; pending.min(len)]; - let read_retry = { - let mut conn_guard = self.connection.lock(); - let conn = conn_guard - .as_mut() - .ok_or_else(|| vm.new_value_error("Connection not established"))?; - crate::ssl::compat::ssl_read(conn, &mut buf, self, vm) - }; - if let Ok(n) = read_retry { - buf.truncate(n); - return return_data(buf, &buffer, vm); - } - } - // Clean closure via close_notify from peer. - // If we already sent close_notify (unwrap was called), - // raise SSLZeroReturnError (bidirectional shutdown). - // Otherwise return empty bytes, which callers (asyncore, - // asyncio sslproto) interpret as EOF. - let our_shutdown_state = *self.shutdown_state.lock(); - if our_shutdown_state == ShutdownState::SentCloseNotify - || our_shutdown_state == ShutdownState::Completed - { - Err(create_ssl_zero_return_error(vm).upcast()) - } else { - return_data(vec![], &buffer, vm) - } - } - Err(crate::ssl::compat::SslError::WantRead) => { - // Non-blocking mode: would block - Err(create_ssl_want_read_error(vm).upcast()) - } - Err(crate::ssl::compat::SslError::WantWrite) => { - // Non-blocking mode: would block on write - Err(create_ssl_want_write_error(vm).upcast()) - } - Err(crate::ssl::compat::SslError::Timeout(msg)) => { - Err(timeout_error_msg(vm, msg).upcast()) - } - Err(crate::ssl::compat::SslError::Py(e)) => { - // Python exception - pass through - Err(e) - } - Err(e) => { - // Other SSL errors - Err(e.into_py_err(vm)) - } - } - } - - #[pymethod] - fn pending(&self) -> usize { - // Returns the number of already decrypted bytes available for read - // This is critical for asyncore's readable() method which checks socket.pending() > 0 - let mut conn_guard = self.connection.lock(); - let conn = match conn_guard.as_mut() { - 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 - } - } - } - - #[pymethod] - fn write(&self, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult { - let data_bytes = data.borrow_buf(); - let data_len = data_bytes.len(); - - if data_len == 0 { - return Ok(0); - } - - // Ensure handshake is done (SSL_write auto-completes handshake) - if !*self.handshake_done.lock() { - self.do_handshake(vm)?; - } - - // Check shutdown state - // Only block after shutdown is COMPLETED, not during shutdown process - if *self.shutdown_state.lock() == ShutdownState::Completed { - return Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "cannot write after shutdown", - ) - .upcast()); - } - - // Call ssl_write (matches CPython's SSL_write_ex loop) - let result = { - let mut conn_guard = self.connection.lock(); - let conn = conn_guard - .as_mut() - .ok_or_else(|| vm.new_value_error("Connection not established"))?; - - crate::ssl::compat::ssl_write(conn, data_bytes.as_ref(), self, vm) - }; - - match result { - Ok(n) => { - self.check_deferred_cert_error(vm)?; - Ok(n) - } - Err(crate::ssl::compat::SslError::WantRead) => { - Err(create_ssl_want_read_error(vm).upcast()) - } - Err(crate::ssl::compat::SslError::WantWrite) => { - Err(create_ssl_want_write_error(vm).upcast()) - } - Err(crate::ssl::compat::SslError::Timeout(msg)) => { - Err(timeout_error_msg(vm, msg).upcast()) - } - Err(e) => Err(e.into_py_err(vm)), - } - } - - #[pymethod] - fn getpeercert( - &self, - args: GetCertArgs, - vm: &VirtualMachine, - ) -> PyResult> { - let binary = args.binary_form.unwrap_or(false); - - // Check if handshake is complete - if !*self.handshake_done.lock() { - return Err(vm.new_value_error("handshake not done yet")); - } - - // Extract DER bytes from connection, releasing lock quickly - let der_bytes = { - let conn_guard = self.connection.lock(); - let conn = conn_guard - .as_ref() - .ok_or_else(|| vm.new_value_error("No TLS connection established"))?; - - let Some(peer_certificates) = conn.peer_certificates() else { - return Ok(None); - }; - let cert = peer_certificates - .first() - .ok_or_else(|| vm.new_value_error("No peer certificate available"))?; - cert.as_ref().to_vec() - }; - - if binary { - // Return DER-encoded certificate as bytes - return Ok(Some(vm.ctx.new_bytes(der_bytes).into())); - } - - // Dictionary mode: check verify_mode - let verify_mode = *self.context.read().verify_mode.read(); - - if verify_mode == CERT_NONE { - // Return empty dict when CERT_NONE - return Ok(Some(vm.ctx.new_dict().into())); - } - - // Parse DER certificate and convert to dict (outside lock) - let (_, cert) = x509_parser::parse_x509_certificate(&der_bytes) - .map_err(|e| vm.new_value_error(format!("Failed to parse certificate: {e}")))?; - - cert::cert_to_dict(vm, &cert).map(Some) - } - - #[pymethod] - fn cipher(&self) -> Option<(String, String, i32)> { - // Extract cipher suite, releasing lock quickly - let suite = { - let conn_guard = self.connection.lock(); - conn_guard.as_ref()?.negotiated_cipher_suite()? - }; - - // Extract cipher information outside the lock - let cipher_info = extract_cipher_info(&suite); - - // Note: returns a 3-tuple (name, protocol_version, bits) - // The 'description' field is part of get_ciphers() output, not cipher() - Some(( - cipher_info.name, - cipher_info.protocol.to_string(), - cipher_info.bits, - )) - } - - #[pymethod] - fn version(&self) -> Option { - // Extract cipher suite, releasing lock quickly - let suite = { - let conn_guard = self.connection.lock(); - conn_guard.as_ref()?.negotiated_cipher_suite()? - }; - - // Convert to string outside the lock - let version_str = match suite.version().version { - rustls::ProtocolVersion::TLSv1_2 => "TLSv1.2", - rustls::ProtocolVersion::TLSv1_3 => "TLSv1.3", - _ => "Unknown", - }; - - Some(version_str.to_string()) - } - - #[pymethod] - fn selected_alpn_protocol(&self) -> Option { - let conn_guard = self.connection.lock(); - let conn = conn_guard.as_ref()?; - - let alpn_bytes = conn.alpn_protocol()?; - - // Null byte protocol (vec![0u8]) means no actual ALPN match (fallback protocol) - if alpn_bytes.is_empty() || alpn_bytes == [0u8] { - return None; - } - - // Convert bytes to string - String::from_utf8(alpn_bytes.to_vec()).ok() - } - - #[pymethod] - fn selected_npn_protocol(&self) -> Option { - // NPN (Next Protocol Negotiation) is the predecessor to ALPN - // It was deprecated in favor of ALPN (RFC 7301) - // Rustls doesn't support NPN, only ALPN - // Return None to indicate NPN is not supported - None - } - - #[pygetset] - fn owner(&self) -> Option { - self.owner.read().clone() - } - - #[pygetset(setter)] - fn set_owner(&self, owner: PyObjectRef, _vm: &VirtualMachine) { - *self.owner.write() = Some(owner); - } - - #[pygetset] - fn server_side(&self) -> bool { - self.server_side - } - - #[pygetset] - fn context(&self) -> PyRef { - self.context.read().clone() - } - - #[pygetset(setter)] - fn set_context(&self, value: PyRef, _vm: &VirtualMachine) { - // Update context reference immediately - // SSL_set_SSL_CTX allows context changes at any time, - // even after handshake completion - *self.context.write() = value; - - // Clear pending context as we've applied the change - *self.pending_context.write() = None; - } - - #[pygetset] - fn server_hostname(&self) -> Option { - self.server_hostname.read().clone() - } - - #[pygetset(setter)] - fn set_server_hostname( - &self, - value: Option, - vm: &VirtualMachine, - ) -> PyResult<()> { - // Check if handshake is already done - if *self.handshake_done.lock() { - return Err( - vm.new_value_error("Cannot set server_hostname on socket after handshake") - ); - } - - // Validate hostname - let hostname_string = value - .map(|s| { - validate_hostname(s.as_str(), vm)?; - Ok::(s.as_str().to_owned()) - }) - .transpose()?; - - *self.server_hostname.write() = hostname_string; - Ok(()) - } - - #[pygetset] - fn session(&self, vm: &VirtualMachine) -> PyObjectRef { - // Return the stored session object if any - let sess = self.session.read().clone(); - sess.unwrap_or_else(|| vm.ctx.none()) - } - - #[pygetset(setter)] - fn set_session(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - // Validate that value is an SSLSession - if !value.is(vm.ctx.types.none_type) { - // Try to downcast to SSLSession to validate - let _ = value - .downcast_ref::() - .ok_or_else(|| vm.new_type_error("Value is not a SSLSession."))?; - } - - // Check if this is a client socket - if self.server_side { - return Err(vm.new_value_error("Cannot set session for server-side SSLSocket")); - } - - // Check if handshake is already done - if *self.handshake_done.lock() { - return Err(vm.new_value_error("Cannot set session after handshake.")); - } - - // Store the session for potential use during handshake - *self.session.write() = if value.is(vm.ctx.types.none_type) { - None - } else { - Some(value) - }; - - Ok(()) - } - - #[pygetset] - fn session_reused(&self) -> bool { - // Return the tracked session reuse status - *self.session_was_reused.lock() - } - - #[pymethod] - fn compression(&self) -> Option<&'static str> { - // rustls doesn't support compression - None - } - - #[pymethod] - fn get_unverified_chain(&self, vm: &VirtualMachine) -> PyResult> { - // Get peer certificates from the connection - let conn_guard = self.connection.lock(); - let conn = conn_guard - .as_ref() - .ok_or_else(|| vm.new_value_error("Handshake not completed"))?; - - let certs = conn.peer_certificates(); - - let Some(certs) = certs else { - return Ok(None); - }; - - // Convert to list of Certificate objects - let cert_list: Vec = certs - .iter() - .map(|cert_der| { - let cert_bytes = cert_der.as_ref().to_vec(); - PySSLCertificate { - der_bytes: cert_bytes, - } - .into_ref(&vm.ctx) - .into() - }) - .collect(); - - Ok(Some(vm.ctx.new_list(cert_list))) - } - - #[pymethod] - fn get_verified_chain(&self, vm: &VirtualMachine) -> Option { - // Get peer certificates (what peer sent during handshake) - let conn_guard = self.connection.lock(); - let conn = (*conn_guard).as_ref()?; - let peer_certs = conn.peer_certificates(); - let peer_certs_slice = peer_certs?; - - // Build the verified chain using cert module - let ctx_guard = self.context.read(); - let ca_certs_der = ctx_guard.ca_certs_der.read(); - - let chain_der = cert::build_verified_chain(peer_certs_slice, &ca_certs_der); - - // Convert DER chain to Python list of Certificate objects - let cert_list: Vec = chain_der - .into_iter() - .map(|der_bytes| PySSLCertificate { der_bytes }.into_ref(&vm.ctx).into()) - .collect(); - - Some(vm.ctx.new_list(cert_list)) - } - - #[pymethod] - fn shutdown(&self, vm: &VirtualMachine) -> PyResult { - // Check current shutdown state - let current_state = *self.shutdown_state.lock(); - - // If already completed, return immediately - if current_state == ShutdownState::Completed { - if self.is_bio_mode() { - return Ok(vm.ctx.none()); - } - return Ok(self.sock.clone()); - } - - // Get connection - let mut conn_guard = self.connection.lock(); - let conn = conn_guard - .as_mut() - .ok_or_else(|| vm.new_value_error("Connection not established"))?; - - let is_bio = self.is_bio_mode(); - - // Step 1: Send our close_notify if not already sent - if current_state == ShutdownState::NotStarted { - // First, flush ALL pending TLS data BEFORE sending close_notify - // This is CRITICAL - close_notify must come AFTER all application data - // Otherwise data loss occurs when peer receives close_notify first - - // Step 1a: Flush any pending TLS records from rustls internal buffer - // This ensures all application data is converted to TLS records - while conn.wants_write() { - let mut buf = Vec::new(); - conn.write_tls(&mut buf) - .map_err(|e| vm.new_os_error(format!("TLS write failed: {e}")))?; - if !buf.is_empty() { - self.send_tls_output(buf, vm)?; - } - } - - // Step 1b: Flush pending_tls_output buffer to socket - if !is_bio { - // Socket mode: blocking flush to ensure data order - // Must complete before sending close_notify - self.blocking_flush_all_pending(vm)?; - } else { - // BIO mode: non-blocking flush (caller handles pending data) - let _ = self.flush_pending_tls_output(vm, None); - } - - conn.send_close_notify(); - - // Write close_notify to outgoing buffer/BIO - self.write_pending_tls(conn, vm)?; - // Ensure close_notify and any pending TLS data are flushed - if !is_bio { - self.flush_pending_tls_output(vm, None)?; - } - - // Update state - *self.shutdown_state.lock() = ShutdownState::SentCloseNotify; - } - - // Step 2: Try to read and process peer's close_notify - - // First check if we already have peer's close_notify - // This can happen if it was received during a previous read() call - let mut peer_closed = self.check_peer_closed(conn, vm)?; - - // If peer hasn't closed yet, try to read from socket - if !peer_closed { - // Check socket timeout mode - let timeout_mode = if !is_bio { - // Get socket timeout - match self.sock.get_attr("gettimeout", vm) { - Ok(method) => match method.call((), vm) { - Ok(timeout) => { - if vm.is_none(&timeout) { - // timeout=None means blocking - Some(None) - } else if let Ok(t) = timeout.try_float(vm).map(|f| f.to_f64()) { - if t == 0.0 { - // timeout=0 means non-blocking - Some(Some(0.0)) - } else { - // timeout>0 means timeout mode - Some(Some(t)) - } - } else { - None - } - } - Err(_) => None, - }, - Err(_) => None, - } - } else { - None // BIO mode - }; - - if is_bio { - // In BIO mode: non-blocking read attempt - if self.try_read_close_notify(conn, vm)? { - peer_closed = true; - } - } else if let Some(timeout) = timeout_mode { - match timeout { - Some(0.0) => { - // Non-blocking: return immediately after sending close_notify. - // Don't wait for peer's close_notify to avoid blocking. - drop(conn_guard); - // Best-effort flush; WouldBlock is expected in non-blocking mode. - // Other errors indicate close_notify may not have been sent, - // but we still complete shutdown to avoid inconsistent state. - let _ = self.flush_pending_tls_output(vm, None); - *self.shutdown_state.lock() = ShutdownState::Completed; - *self.connection.lock() = None; - return Ok(self.sock.clone()); - } - _ => { - // Blocking or timeout mode: wait for peer's close_notify. - // This is proper TLS shutdown - we should receive peer's - // close_notify before closing the connection. - drop(conn_guard); - - // Flush our close_notify first - if timeout.is_none() { - self.blocking_flush_all_pending(vm)?; - } else { - self.flush_pending_tls_output(vm, None)?; - } - - // Calculate deadline for timeout mode - let deadline = timeout.map(|t| { - std::time::Instant::now() + core::time::Duration::from_secs_f64(t) - }); - - // Wait for peer's close_notify - loop { - // Re-acquire connection lock for each iteration - let mut conn_guard = self.connection.lock(); - let conn = match conn_guard.as_mut() { - Some(c) => c, - None => break, // Connection already closed - }; - - // Check if peer already sent close_notify - if self.check_peer_closed(conn, vm)? { - break; - } - - drop(conn_guard); - - // Check timeout - let remaining_timeout = if let Some(dl) = deadline { - let now = std::time::Instant::now(); - if now >= dl { - // Timeout reached - raise TimeoutError - return Err(timeout_error_msg( - vm, - "The read operation timed out".to_string(), - ) - .upcast()); - } - Some(dl - now) - } else { - None // Blocking mode: no timeout - }; - - // Wait for socket to be readable - let timed_out = self.sock_wait_for_io_with_timeout( - SockWaitKind::Read, - remaining_timeout, - vm, - )?; - - if timed_out { - // Timeout waiting for peer's close_notify - return Err(timeout_error_msg( - vm, - "The read operation timed out".to_string(), - ) - .upcast()); - } - - // Try to read data from socket - let mut conn_guard = self.connection.lock(); - let conn = match conn_guard.as_mut() { - Some(c) => c, - None => break, - }; - - // Read and process any incoming TLS data - match self.try_read_close_notify(conn, vm) { - Ok(closed) => { - if closed { - break; - } - // Check again after processing - if self.check_peer_closed(conn, vm)? { - break; - } - } - Err(_) => { - // Socket error - peer likely closed connection - break; - } - } - } - - // Shutdown complete - *self.shutdown_state.lock() = ShutdownState::Completed; - *self.connection.lock() = None; - return Ok(self.sock.clone()); - } - } - } - - // Step 3: Check again if peer has sent close_notify (non-blocking/BIO mode only) - if !peer_closed { - peer_closed = self.check_peer_closed(conn, vm)?; - } - } - - drop(conn_guard); // Release lock before returning - - if !peer_closed { - // Still waiting for peer's close-notify - // Raise SSLWantReadError to signal app needs to transfer data - // This is correct for non-blocking sockets and BIO mode - return Err(create_ssl_want_read_error(vm).upcast()); - } - // Both close-notify exchanged, shutdown complete - *self.shutdown_state.lock() = ShutdownState::Completed; - - if is_bio { - return Ok(vm.ctx.none()); - } - Ok(self.sock.clone()) - } - - // Helper: Write all pending TLS data (including close_notify) to outgoing buffer/BIO - fn write_pending_tls(&self, conn: &mut Connection, vm: &VirtualMachine) -> PyResult<()> { - // First, flush any previously pending TLS output - // Must succeed before sending new data to maintain order - self.flush_pending_tls_output(vm, None)?; - - loop { - if !conn.wants_write() { - break; - } - - let mut buf = vec![0u8; SSL3_RT_MAX_PACKET_SIZE]; - let written = conn - .write_tls(&mut buf.as_mut_slice()) - .map_err(|e| vm.new_os_error(format!("TLS write failed: {e}")))?; - - if written == 0 { - break; - } - - // Send TLS data, saving unsent bytes to pending buffer if needed - self.send_tls_output(buf[..written].to_vec(), vm)?; - } - - Ok(()) - } - - // Helper: Try to read incoming data from socket/BIO - // Returns true if peer closed connection (with or without close_notify) - fn try_read_close_notify( - &self, - conn: &mut Connection, - vm: &VirtualMachine, - ) -> PyResult { - // In socket mode, peek first to avoid consuming post-TLS cleartext - // data. During STARTTLS, after close_notify exchange, the socket - // transitions to cleartext. Without peeking, sock_recv may consume - // cleartext data meant for the application after unwrap(). - if self.incoming_bio.is_none() { - return Ok(self.try_read_close_notify_socket(conn, vm)); - } - - // BIO mode: read from incoming BIO - match self.sock_recv(SSL3_RT_MAX_PACKET_SIZE, vm) { - Ok(bytes_obj) => { - let bytes = ArgBytesLike::try_from_object(vm, bytes_obj)?; - let data = bytes.borrow_buf(); - - if data.is_empty() { - if let Some(ref bio) = self.incoming_bio { - // BIO mode: check if EOF was signaled via write_eof() - let bio_obj: PyObjectRef = bio.clone().into(); - let eof_attr = bio_obj.get_attr("eof", vm)?; - let is_eof = eof_attr.try_to_bool(vm)?; - if !is_eof { - return Ok(false); - } - } - return Ok(true); - } - - let data_slice: &[u8] = data.as_ref(); - let mut cursor = std::io::Cursor::new(data_slice); - let _ = conn.read_tls(&mut cursor); - let _ = conn.process_new_packets(); - Ok(false) - } - Err(e) => { - if is_blocking_io_error(&e, vm) { - return Ok(false); - } - Ok(true) - } - } - } - - /// Socket-mode close_notify reader that respects TLS record boundaries. - /// Uses MSG_PEEK to inspect data before consuming, preventing accidental - /// consumption of post-TLS cleartext data during STARTTLS transitions. - /// - /// Equivalent to OpenSSL's `SSL_set_read_ahead(ssl, 0)` — rustls has no - /// such knob, so we enforce record-level reads manually via peek. - fn try_read_close_notify_socket(&self, conn: &mut Connection, vm: &VirtualMachine) -> bool { - // Consume at most one TLS record from the socket - match self.sock_recv_at_most_one_tls_record(vm) { - Ok(data) => { - if data.is_empty() { - return true; - } - - let data_slice: &[u8] = data.as_ref(); - let mut cursor = std::io::Cursor::new(data_slice); - let _ = conn.read_tls(&mut cursor); - let _ = conn.process_new_packets(); - false - } - Err(e) => { - if is_blocking_io_error(&e, vm) { - return false; - } - true - } - } - } - - // Helper: Check if peer has sent close_notify - fn check_peer_closed(&self, conn: &mut Connection, vm: &VirtualMachine) -> PyResult { - // Process any remaining packets and check peer_has_closed - let io_state = conn - .process_new_packets() - .map_err(|e| vm.new_os_error(format!("Failed to process packets: {e}")))?; - - Ok(io_state.peer_has_closed()) - } - - #[pymethod] - fn shared_ciphers(&self, vm: &VirtualMachine) -> Option { - // Return None for client-side sockets - if !self.server_side { - return None; - } - - // Check if handshake completed - if !*self.handshake_done.lock() { - return None; - } - - // Get negotiated cipher suite from rustls - let conn_guard = self.connection.lock(); - let conn = conn_guard.as_ref()?; - - let suite = conn.negotiated_cipher_suite()?; - - // Extract cipher information using unified helper - let cipher_info = extract_cipher_info(&suite); - - // Return as list with single tuple (name, version, bits) - let tuple = vm.ctx.new_tuple(vec![ - vm.ctx.new_str(cipher_info.name).into(), - vm.ctx.new_str(cipher_info.protocol).into(), - vm.ctx.new_int(cipher_info.bits).into(), - ]); - Some(vm.ctx.new_list(vec![tuple.into()])) - } - - #[pymethod] - fn verify_client_post_handshake(&self, vm: &VirtualMachine) -> PyResult<()> { - // TLS 1.3 post-handshake authentication - // This is only valid for server-side TLS 1.3 connections - - // Check if this is a server-side socket - if !self.server_side { - return Err(vm.new_value_error( - "Cannot perform post-handshake authentication on client-side socket", - )); - } - - // Check if handshake has been completed - if !*self.handshake_done.lock() { - return Err(vm.new_value_error( - "Handshake must be completed before post-handshake authentication", - )); - } - - // Check connection exists and protocol version - let conn_guard = self.connection.lock(); - if let Some(conn) = conn_guard.as_ref() { - let version = match conn { - Connection::Client(_) => { - return Err(vm.new_value_error( - "Post-handshake authentication requires server socket", - )); - } - Connection::Server(server) => server.protocol_version(), - }; - - // Post-handshake auth is only available in TLS 1.3 - if version != Some(rustls::ProtocolVersion::TLSv1_3) { - // Get SSLError class from ssl module (not _ssl) - // ssl.py imports _ssl.SSLError as ssl.SSLError - let ssl_mod = vm.import("ssl", 0)?; - let ssl_error_class = ssl_mod.get_attr("SSLError", vm)?; - - // Create SSLError instance with message containing WRONG_SSL_VERSION - let msg = "[SSL: WRONG_SSL_VERSION] wrong ssl version"; - let args = vm.ctx.new_tuple(vec![vm.ctx.new_str(msg).into()]); - let exc = ssl_error_class.call((args,), vm)?; - - return Err(exc - .downcast() - .map_err(|_| vm.new_type_error("Failed to create SSLError"))?); - } - } else { - return Err(vm.new_value_error("No SSL connection established")); - } - - // rustls doesn't provide an API for post-handshake authentication. - // The rustls TLS library does not support requesting client certificates - // after the initial handshake is completed. - // Raise SSLError instead of NotImplementedError for compatibility - Err(vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "Post-handshake authentication is not supported by the rustls backend. \ - The rustls TLS library does not provide an API to request client certificates \ - after the initial handshake. Consider requesting the client certificate \ - during the initial handshake by setting the appropriate verify_mode before \ - calling do_handshake().", - ) - .upcast()) - } - - #[pymethod] - fn get_channel_binding( - &self, - cb_type: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult> { - 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 None to indicate channel binding is not available - // This matches the behavior when the handshake hasn't completed yet - Ok(None) - } - } - - impl Representable for PySSLSocket { - #[inline] - fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { - Ok("".to_owned()) - } - } - - impl Constructor for PySSLSocket { - type Args = (); - - fn slot_new(_cls: PyTypeRef, _args: FuncArgs, vm: &VirtualMachine) -> PyResult { - Err(vm.new_type_error( - "Cannot directly instantiate SSLSocket, use SSLContext.wrap_socket()", - )) - } - - fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { - unimplemented!("use slot_new") - } - } - - // Clean up SSL socket resources on drop - impl Drop for PySSLSocket { - fn drop(&mut self) { - // Only clear connection state. - // Do NOT clear pending_tls_output - it may contain data that hasn't - // been flushed to the socket yet. SSLSocket._real_close() in Python - // doesn't call shutdown(), so when the socket is closed, pending TLS - // data would be lost if we clear it here. - // All fields (Vec, primitives) are automatically freed when the - // struct is dropped, so explicit clearing is unnecessary. - let _ = self.connection.lock().take(); - } - } - - // MemoryBIO - provides in-memory buffer for SSL/TLS I/O - #[pyattr] - #[pyclass(name = "MemoryBIO", module = "ssl")] - #[derive(Debug, PyPayload)] - struct PyMemoryBIO { - // Internal buffer - buffer: PyMutex>, - // EOF flag - eof: PyRwLock, - } - - #[pyclass(with(Constructor), flags(BASETYPE))] - impl PyMemoryBIO { - #[pymethod] - fn read(&self, len: OptionalArg, vm: &VirtualMachine) -> PyResult { - 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 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 - }; - - let actual_len = read_len.min(buffer.len()); - let data = buffer.drain(..actual_len).collect::>(); - - Ok(vm.ctx.new_bytes(data)) - } - - #[pymethod] - fn write(&self, buf: PyObjectRef, vm: &VirtualMachine) -> PyResult { - // Check if it's a memoryview and if it's contiguous - if let Ok(mem_view) = buf.get_attr("c_contiguous", vm) { - // It's a memoryview, check if contiguous - let is_contiguous: bool = mem_view.try_to_bool(vm)?; - if !is_contiguous { - return Err(vm.new_exception_msg( - vm.ctx.exceptions.buffer_error.to_owned(), - "non-contiguous buffer is not supported".into(), - )); - } - } - - // 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) - } - - #[pymethod] - fn write_eof(&self, _vm: &VirtualMachine) { - *self.eof.write() = true; - } - - #[pygetset] - fn pending(&self) -> i32 { - self.buffer.lock().len() 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() - } - } - - impl Representable for PyMemoryBIO { - #[inline] - fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { - Ok("".to_owned()) - } - } - - impl Constructor for PyMemoryBIO { - type Args = (); - - fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { - Ok(Self { - buffer: PyMutex::new(Vec::new()), - eof: PyRwLock::new(false), - }) - } - } - - // SSLSession - represents a cached SSL session - // NOTE: This is an EMULATION - actual session data is managed by Rustls internally - #[pyattr] - #[pyclass(name = "SSLSession", module = "ssl")] - #[derive(Debug, PyPayload)] - struct PySSLSession { - // Session data - serialized rustls session (EMULATED - kept empty) - session_data: Vec, - // Session ID - synthetic ID generated from metadata (NOT actual TLS session ID) - #[allow(dead_code)] - session_id: Vec, - // Session metadata - creation_time: std::time::SystemTime, - // Lifetime in seconds (default 7200 = 2 hours) - lifetime: u64, - } - - #[pyclass(flags(BASETYPE))] - impl PySSLSession { - #[pygetset] - fn time(&self) -> i64 { - // Return session creation time as Unix timestamp - self.creation_time - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64 - } - - #[pygetset] - fn timeout(&self) -> i64 { - // Return session timeout/lifetime in seconds - self.lifetime as i64 - } - - #[pygetset] - fn ticket_lifetime_hint(&self) -> i64 { - // Return ticket lifetime hint (same as timeout for rustls) - self.lifetime as i64 - } - - #[pygetset] - fn id(&self, vm: &VirtualMachine) -> PyBytesRef { - // Return session ID (hash of session data for uniqueness) - - let mut hasher = DefaultHasher::new(); - self.session_data.hash(&mut hasher); - let hash = hasher.finish(); - - // Convert hash to bytes - vm.ctx.new_bytes(hash.to_be_bytes().to_vec()) - } - - #[pygetset] - fn has_ticket(&self) -> bool { - // For rustls, if we have session data, we have a ticket - !self.session_data.is_empty() - } - } - - impl Representable for PySSLSession { - #[inline] - fn repr_str(_zelf: &Py, _vm: &VirtualMachine) -> PyResult { - Ok("".to_owned()) - } - } - - // Helper functions - - // OID module already imported at top of _ssl module - - #[derive(FromArgs)] - struct Txt2ObjArgs { - txt: PyUtf8StrRef, - #[pyarg(named, optional)] - name: OptionalArg, - } - - #[pyfunction] - fn txt2obj(args: Txt2ObjArgs, vm: &VirtualMachine) -> PyResult { - let txt = args.txt.as_str(); - let name = args.name.unwrap_or(false); - - // If name=False (default), only accept OID strings - // If name=True, accept both names and OID strings - let entry = if txt.chars().next().is_some_and(|c| c.is_ascii_digit()) { - // Looks like an OID string (starts with digit) - oid::find_by_oid_string(txt) - } else if name { - // name=True: allow shortname/longname lookup - oid::find_by_name(txt) - } else { - // name=False: only OID strings allowed, not names - None - }; - - let entry = entry.ok_or_else(|| vm.new_value_error(format!("unknown object '{txt}'")))?; - - // Return tuple: (nid, shortname, longname, oid) - Ok(vm - .new_tuple(( - vm.ctx.new_int(entry.nid), - vm.ctx.new_str(entry.short_name), - vm.ctx.new_str(entry.long_name), - vm.ctx.new_str(entry.oid_string()), - )) - .into()) - } - - #[pyfunction] - fn nid2obj(nid: i32, vm: &VirtualMachine) -> PyResult { - let entry = oid::find_by_nid(nid) - .ok_or_else(|| vm.new_value_error(format!("unknown NID {nid}")))?; - - // Return tuple: (nid, shortname, longname, oid) - Ok(vm - .new_tuple(( - vm.ctx.new_int(entry.nid), - vm.ctx.new_str(entry.short_name), - vm.ctx.new_str(entry.long_name), - vm.ctx.new_str(entry.oid_string()), - )) - .into()) - } - - #[pyfunction] - fn get_default_verify_paths(vm: &VirtualMachine) -> PyObjectRef { - // Return default certificate paths as a tuple - // Lib/ssl.py expects: (openssl_cafile_env, openssl_cafile, openssl_capath_env, openssl_capath) - // parts[0] = environment variable name for cafile - // parts[1] = default cafile path - // parts[2] = environment variable name for capath - // parts[3] = default capath path - - // Common default paths for different platforms - // These match the first candidates that rustls-native-certs/openssl-probe checks - let (default_cafile, default_capath): (Option<&str>, Option<&str>) = cfg_select! { - // macOS primarily uses Keychain API, but provides fallback paths - // for compatibility and when Keychain access fails - target_os = "macos" => (Some("/etc/ssl/cert.pem"), Some("/etc/ssl/certs")), - // Linux: matches openssl-probe's first candidate (/etc/ssl/cert.pem) - // openssl-probe checks multiple locations at runtime, but we return - // OpenSSL's compile-time default - target_os = "linux" => (Some("/etc/ssl/cert.pem"), Some("/etc/ssl/certs")), - // Windows uses certificate store, not file paths - // Return empty strings to avoid None being passed to os.path.isfile() - windows => (Some(""), Some("")), - _ => (None, None), - }; - - let tuple = vm.ctx.new_tuple(vec![ - vm.ctx.new_str("SSL_CERT_FILE").into(), // openssl_cafile_env - default_cafile.map_or_else(|| vm.ctx.none(), |s| vm.ctx.new_str(s).into()), // openssl_cafile - vm.ctx.new_str("SSL_CERT_DIR").into(), // openssl_capath_env - default_capath.map_or_else(|| vm.ctx.none(), |s| vm.ctx.new_str(s).into()), // openssl_capath - ]); - - tuple.into() - } - - #[pyfunction] - fn RAND_status() -> i32 { - 1 // The configured rustls provider supplies cryptographic randomness. - } - - #[pyfunction] - fn RAND_add(_string: PyObjectRef, _entropy: f64) { - // No-op: the configured rustls provider handles its own entropy. - // Accept any type (str, bytes, bytearray) - } - - #[pyfunction] - fn RAND_bytes(n: i64, vm: &VirtualMachine) -> PyResult { - // Validate n is not negative - if n < 0 { - return Err(vm.new_value_error("num must be positive")); - } - - let n_usize = n as usize; - let mut buf = vec![0u8; n_usize]; - CryptoExt::get_provider() - .secure_random - .fill(&mut buf) - .map_err(|_| vm.new_os_error("Failed to generate random bytes"))?; - Ok(PyBytesRef::from(vm.ctx.new_bytes(buf))) - } - - #[pyfunction] - fn RAND_pseudo_bytes(n: i64, vm: &VirtualMachine) -> PyResult<(PyBytesRef, bool)> { - // Rustls providers expose cryptographically strong random bytes. - let bytes = RAND_bytes(n, vm)?; - Ok((bytes, true)) - } - - /// Test helper to decode a certificate from a file path - /// - /// This is a simplified wrapper around cert_der_to_dict_helper that handles - /// file reading and PEM/DER auto-detection. Used by test suite. - #[pyfunction] - fn _test_decode_cert(path: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - // Read certificate file - let path_str = path.as_str(); - let cert_data = rustpython_host_env::fs::read(path_str).map_err(|e| { - vm.new_os_error(format!("Failed to read certificate file {path_str}: {e}")) - })?; - - // Auto-detect PEM vs DER format - let cert_der = if cert_data - .windows(27) - .any(|w| w == b"-----BEGIN CERTIFICATE-----") - { - // Parse PEM format - let mut cursor = std::io::Cursor::new(&cert_data); - rustls_pemfile::certs(&mut cursor) - .find_map(|r| r.ok()) - .ok_or_else(|| vm.new_value_error("No valid certificate found in PEM file"))? - .to_vec() - } else { - // Assume DER format - cert_data - }; - - // Reuse the comprehensive helper function - cert::cert_der_to_dict_helper(vm, &cert_der) - } - - #[pyfunction] - fn DER_cert_to_PEM_cert(der_cert: ArgBytesLike, vm: &VirtualMachine) -> PyResult { - let der_bytes = der_cert.borrow_buf(); - let bytes_slice: &[u8] = der_bytes.as_ref(); - - // Use pem-rfc7468 for RFC 7468 compliant PEM encoding - let pem_str = encode_string("CERTIFICATE", LineEnding::LF, bytes_slice) - .map_err(|e| vm.new_value_error(format!("PEM encoding failed: {e}")))?; - - Ok(vm.ctx.new_str(pem_str)) - } - - #[pyfunction] - fn PEM_cert_to_DER_cert(pem_cert: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - // Parse PEM format - let mut cursor = std::io::Cursor::new(pem_cert.as_bytes()); - let mut certs = rustls_pemfile::certs(&mut cursor); - - if let Some(Ok(cert)) = certs.next() { - Ok(vm.ctx.new_bytes(cert.to_vec())) - } else { - Err(vm.new_value_error("Failed to parse PEM certificate")) - } - } - - // Windows-specific certificate store enumeration functions - #[cfg(windows)] - #[pyfunction] - fn enum_certificates( - store_name: PyUtf8StrRef, - vm: &VirtualMachine, - ) -> PyResult> { - let store_name_str = store_name.as_str(); - let certs = rustpython_host_env::cert_store::enum_certificates(store_name_str); - if !certs.had_open_store { - return Err(vm.new_os_error(format!( - "failed to open certificate store {store_name_str:?}" - ))); - } - - let certs = certs.entries.into_iter().map(|c| { - let cert = vm.ctx.new_bytes(c.der); - let enc_type = match c.encoding { - rustpython_host_env::cert_store::EncodingType::X509Asn => vm.new_pyobj("x509_asn"), - rustpython_host_env::cert_store::EncodingType::Pkcs7Asn => { - vm.new_pyobj("pkcs_7_asn") - } - rustpython_host_env::cert_store::EncodingType::Other(other) => vm.new_pyobj(other), - }; - let usage: PyObjectRef = match c.valid_uses { - Ok(rustpython_host_env::cert_store::CertificateUses::All) => { - vm.ctx.new_bool(true).into() - } - Ok(rustpython_host_env::cert_store::CertificateUses::Oids(oids)) => { - match crate::builtins::PyFrozenSet::from_iter( - vm, - oids.into_iter().map(|oid| vm.ctx.new_str(oid).into()), - ) { - Ok(set) => set.into_ref(&vm.ctx).into(), - Err(_) => vm.ctx.new_bool(true).into(), - } - } - Err(_) => vm.ctx.new_bool(true).into(), - }; - Ok(vm.new_tuple((cert, enc_type, usage)).into()) - }); - certs.collect::>>() - } - - #[cfg(windows)] - #[pyfunction] - fn enum_crls(store_name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult> { - let store_name_str = store_name.as_str(); - let crls = rustpython_host_env::cert_store::enum_crls(store_name_str).map_err(|_| { - vm.new_os_error(format!( - "failed to open certificate store {store_name_str:?}" - )) - })?; - - Ok(crls - .into_iter() - .map(|crl| { - let enc_type = match crl.encoding { - rustpython_host_env::cert_store::EncodingType::X509Asn => { - vm.new_pyobj("x509_asn") - } - rustpython_host_env::cert_store::EncodingType::Pkcs7Asn => { - vm.new_pyobj("pkcs_7_asn") - } - rustpython_host_env::cert_store::EncodingType::Other(other) => { - vm.new_pyobj(other) - } - }; - vm.new_tuple((vm.ctx.new_bytes(crl.der), enc_type)).into() - }) - .collect()) - } - - // Certificate type for SSL module (pure Rust implementation) - #[pyattr] - #[pyclass(module = "_ssl", name = "Certificate")] - #[derive(Debug, PyPayload)] - pub(super) struct PySSLCertificate { - // Store the raw DER bytes - der_bytes: Vec, - } - - impl PySSLCertificate { - // Parse the certificate lazily - fn parse(&self) -> Result, String> { - match x509_parser::parse_x509_certificate(&self.der_bytes) { - Ok((_, cert)) => Ok(cert), - Err(e) => Err(format!("Failed to parse certificate: {e}")), - } - } - } - - #[pyclass(with(Comparable, Hashable, Representable))] - impl PySSLCertificate { - #[pymethod] - fn public_bytes( - &self, - format: OptionalArg, - vm: &VirtualMachine, - ) -> PyResult { - let format = format.unwrap_or(ENCODING_PEM); - - match format { - x if x == ENCODING_DER => { - // Return DER bytes directly - Ok(vm.ctx.new_bytes(self.der_bytes.clone()).into()) - } - x if x == ENCODING_PEM => { - // Convert DER to PEM using RFC 7468 compliant encoding - let pem_str = encode_string("CERTIFICATE", LineEnding::LF, &self.der_bytes) - .map_err(|e| vm.new_value_error(format!("PEM encoding failed: {e}")))?; - Ok(vm.ctx.new_str(pem_str).into()) - } - _ => Err(vm.new_value_error("Unsupported format")), - } - } - - #[pymethod] - fn get_info(&self, vm: &VirtualMachine) -> PyResult { - let cert = self.parse().map_err(|e| vm.new_value_error(e))?; - cert::cert_to_dict(vm, &cert) - } - } - - // Implement Comparable trait for PySSLCertificate - impl Comparable for PySSLCertificate { - fn cmp( - zelf: &Py, - other: &PyObject, - op: PyComparisonOp, - _vm: &VirtualMachine, - ) -> PyResult { - op.eq_only(|| { - if let Some(other_cert) = other.downcast_ref::() { - Ok((zelf.der_bytes == other_cert.der_bytes).into()) - } else { - Ok(PyComparisonValue::NotImplemented) - } - }) - } - } - - // Implement Hashable trait for PySSLCertificate - impl Hashable for PySSLCertificate { - fn hash(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - let mut hasher = DefaultHasher::new(); - zelf.der_bytes.hash(&mut hasher); - Ok(hasher.finish() as PyHash) - } - } - - // Implement Representable trait for PySSLCertificate - impl Representable for PySSLCertificate { - #[inline] - fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { - // Try to parse and show subject - match zelf.parse() { - Ok(cert) => { - let subject = cert.subject(); - // Get CN if available - let cn = subject - .iter_common_name() - .next() - .and_then(|attr| attr.as_str().ok()) - .unwrap_or("Unknown"); - Ok(format!("")) - } - Err(_) => Ok("".to_owned()), - } - } - } -} diff --git a/crates/stdlib/src/ssl/cert.rs b/crates/stdlib/src/ssl/cert.rs deleted file mode 100644 index e304781b644..00000000000 --- a/crates/stdlib/src/ssl/cert.rs +++ /dev/null @@ -1,1780 +0,0 @@ -// cspell: ignore accessdescs - -//! Certificate parsing, validation, and conversion utilities for SSL/TLS -//! -//! This module provides reusable functions for working with X.509 certificates: -//! - Parsing PEM/DER encoded certificates -//! - Validating certificate properties (CA status, etc.) -//! - Converting certificates to Python dict format -//! - Building and verifying certificate chains -//! - Loading certificates from files, directories, and bytes - -use alloc::sync::Arc; -use chrono::{DateTime, Utc}; -use parking_lot::RwLock as ParkingRwLock; -use rustls::{ - DigitallySignedStruct, RootCertStore, SignatureScheme, - client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, - pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}, - server::danger::{ClientCertVerified, ClientCertVerifier}, -}; -use rustpython_vm::{PyObjectRef, PyResult, VirtualMachine}; -use std::collections::HashSet; -use x509_parser::prelude::*; - -use super::{ - _ssl::{VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT}, - providers::CryptoExt, -}; - -// Certificate Verification Constants - -/// All supported signature schemes for certificate verification -/// -/// This list includes all modern signature algorithms supported by rustls. -/// Used by verifiers that accept any signature scheme (NoVerifier, EmptyRootStoreVerifier). -const ALL_SIGNATURE_SCHEMES: &[SignatureScheme] = &[ - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PKCS1_SHA512, - SignatureScheme::ECDSA_NISTP256_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::ECDSA_NISTP521_SHA512, - SignatureScheme::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512, - SignatureScheme::ED25519, -]; - -// Error Handling Utilities - -/// Certificate loading error types with specific error messages -/// -/// This module provides consistent error creation functions for certificate -/// operations, reducing code duplication and ensuring uniform error messages -/// across the codebase. -mod cert_error { - use alloc::sync::Arc; - use core::fmt::{Debug, Display}; - use std::io; - - /// Create InvalidData error with formatted message - pub(super) fn invalid_data(msg: impl Into) -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, msg.into()) - } - - /// PEM parsing error variants - pub(super) mod pem { - use super::*; - - pub(crate) fn no_start_line(context: &str) -> io::Error { - invalid_data(format!("no start line: {context}")) - } - - pub(crate) fn parse_failed(e: impl Display) -> io::Error { - invalid_data(format!("Failed to parse PEM certificate: {e}")) - } - - pub(crate) fn parse_failed_debug(e: impl Debug) -> io::Error { - invalid_data(format!("Failed to parse PEM certificate: {e:?}")) - } - - pub(crate) fn invalid_cert() -> io::Error { - invalid_data("No certificates found in certificate file") - } - } - - /// DER parsing error variants - pub(super) mod der { - use super::*; - - pub(crate) fn not_enough_data(context: &str) -> io::Error { - invalid_data(format!("not enough data: {context}")) - } - - pub(crate) fn parse_failed(e: impl Display) -> io::Error { - invalid_data(format!("Failed to parse DER certificate: {e}")) - } - } - - /// Private key error variants - pub(super) mod key { - use super::*; - - pub(crate) fn not_found(context: &str) -> io::Error { - invalid_data(format!("No private key found in {context}")) - } - - pub(crate) fn parse_failed(e: impl Display) -> io::Error { - invalid_data(format!("Failed to parse private key: {e}")) - } - - pub(crate) fn parse_encrypted_failed(e: impl Display) -> io::Error { - invalid_data(format!("Failed to parse encrypted private key: {e}")) - } - - pub(crate) fn decrypt_failed(e: impl Display) -> io::Error { - io::Error::other(format!( - "Failed to decrypt private key (wrong password?): {e}", - )) - } - } - - /// Convert error message to rustls::Error with InvalidCertificate wrapper - pub(super) fn to_rustls_invalid_cert(msg: impl Into) -> rustls::Error { - rustls::Error::InvalidCertificate(rustls::CertificateError::Other(rustls::OtherError( - Arc::new(invalid_data(msg)), - ))) - } - - /// Convert error message to rustls::Error with InvalidCertificate wrapper and custom ErrorKind - pub(super) fn to_rustls_cert_error( - kind: io::ErrorKind, - msg: impl Into, - ) -> rustls::Error { - rustls::Error::InvalidCertificate(rustls::CertificateError::Other(rustls::OtherError( - Arc::new(io::Error::new(kind, msg.into())), - ))) - } -} - -// 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 = time.timestamp(); - DateTime::::from_timestamp(timestamp, 0) - .expect("ASN1Time must be valid timestamp") - .format("%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 -} - -/// Normalize wildcard hostname by stripping "*." prefix -/// -/// Returns the normalized hostname without the wildcard prefix. -/// Used for wildcard certificate matching. -fn normalize_wildcard_hostname(hostname: &str) -> &str { - hostname.strip_prefix("*.").unwrap_or(hostname) -} - -/// Process Subject Alternative Name (SAN) general names into Python tuples -/// -/// Converts X.509 GeneralName entries into Python tuple format. -/// Returns a vector of PyObjectRef tuples in the format: (type, value) -fn process_san_general_names( - vm: &VirtualMachine, - general_names: &[GeneralName<'_>], -) -> Vec { - general_names - .iter() - .filter_map(|name| match name { - GeneralName::DNSName(dns) => Some(vm.new_tuple(("DNS", *dns)).into()), - GeneralName::IPAddress(ip) => { - let ip_str = format_ip_address(ip); - Some(vm.new_tuple(("IP Address", ip_str)).into()) - } - GeneralName::RFC822Name(email) => Some(vm.new_tuple(("email", *email)).into()), - GeneralName::URI(uri) => Some(vm.new_tuple(("URI", *uri)).into()), - GeneralName::DirectoryName(dn) => { - let dn_str = format!("{dn}"); - Some(vm.new_tuple(("DirName", dn_str)).into()) - } - GeneralName::RegisteredID(oid) => { - let oid_str = oid.to_string(); - Some(vm.new_tuple(("Registered ID", oid_str)).into()) - } - GeneralName::OtherName(oid, value) => { - let oid_str = oid.to_string(); - let value_str = format!("{value:?}"); - Some( - vm.new_tuple(("othername", format!("{oid_str}:{value_str}"))) - .into(), - ) - } - _ => None, - }) - .collect() -} - -// 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; - } - - // No Basic Constraints extension -> NOT a CA certificate - // (matches OpenSSL X509_check_ca() behavior) - false -} - -/// Convert an X509Name to Python nested tuple format for SSL certificate dicts -/// -/// Format: ((('CN', 'example.com'),), (('O', 'Example Org'),), ...) -fn name_to_py(vm: &VirtualMachine, name: &x509_parser::x509::X509Name<'_>) -> PyObjectRef { - let list = name - .iter() - .flat_map(|rdn| { - // Each RDN can have multiple attributes - rdn.iter() - .map(|attr| { - let oid_str = attr.attr_type().to_id_string(); - let value_str = attr.attr_value().as_str().unwrap_or("").to_string(); - let key = oid_to_attribute_name(&oid_str); - - vm.new_tuple((vm.new_tuple((vm.ctx.new_str(key), vm.ctx.new_str(value_str))),)) - .into() - }) - .collect::>() - }) - .collect::>(); - - vm.ctx.new_tuple(list).into() -} - -/// Convert DER-encoded certificate to Python dict (for getpeercert with binary_form=False) -/// -/// Returns a dict with fields: subject, issuer, version, serialNumber, -/// notBefore, notAfter, subjectAltName (if present) -pub(super) fn cert_to_dict( - vm: &VirtualMachine, - cert: &x509_parser::certificate::X509Certificate<'_>, -) -> PyResult { - let dict = vm.ctx.new_dict(); - - // Subject and Issuer - dict.set_item("subject", name_to_py(vm, cert.subject()), vm)?; - dict.set_item("issuer", name_to_py(vm, cert.issuer()), vm)?; - - // Version (X.509 v3 = version 2 in the cert, but Python uses 3) - dict.set_item( - "version", - vm.ctx.new_int(cert.version().0 as i32 + 1).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)?; - - // Validity dates - format with GMT using chrono - dict.set_item( - "notBefore", - vm.ctx - .new_str(format_asn1_time(&cert.validity().not_before)) - .into(), - vm, - )?; - dict.set_item( - "notAfter", - vm.ctx - .new_str(format_asn1_time(&cert.validity().not_after)) - .into(), - vm, - )?; - - // Subject Alternative Names (if present) - if let Ok(Some(san_ext)) = cert.subject_alternative_name() { - let san_list = process_san_general_names(vm, &san_ext.value.general_names); - - if !san_list.is_empty() { - dict.set_item("subjectAltName", vm.ctx.new_tuple(san_list).into(), vm)?; - } - } - - Ok(dict.into()) -} - -/// Convert DER-encoded certificate to Python dict (for get_ca_certs) -/// -/// Similar to cert_to_dict but includes additional fields like crlDistributionPoints -/// and uses CPython's specific ordering: issuer, notAfter, notBefore, serialNumber, subject, version -pub(super) fn cert_der_to_dict_helper( - vm: &VirtualMachine, - cert_der: &[u8], -) -> PyResult { - // 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}")))?; - - // Helper to convert X509Name to nested tuple format - let name_to_tuple = |name: &x509_parser::x509::X509Name<'_>| -> PyResult { - 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); - - let entry = - vm.new_tuple((vm.ctx.new_str(key.to_string()), vm.ctx.new_str(value_str))); - entries.push(vm.new_tuple((entry,)).into()); - } - } - Ok(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 chrono - 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(), - 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 !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() { - 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", "")).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()); - } - _ => {} - } - } - if !san_entries.is_empty() { - dict.set_item("subjectAltName", vm.ctx.new_tuple(san_entries).into(), vm)?; - } - } - - Ok(dict.into()) -} - -/// Build a verified certificate chain by adding CA certificates from the trust store -/// -/// Takes peer certificates (from TLS handshake) and extends the chain by finding -/// issuer certificates from the trust store until reaching a root certificate. -/// -/// Returns the complete chain as DER-encoded bytes. -pub(super) fn build_verified_chain( - peer_certs: &[CertificateDer<'static>], - ca_certs_der: &[Vec], -) -> Vec> { - let mut chain_der: Vec> = Vec::new(); - - // Start with peer certificates (what was sent during handshake) - for cert in peer_certs { - chain_der.push(cert.as_ref().to_vec()); - } - - // Keep adding issuers until we reach a root or can't find the issuer - while let Some(der) = chain_der.last() { - let last_cert_der = der; - - // Parse the last certificate in the chain - let (_, last_cert) = match X509Certificate::from_der(last_cert_der) { - Ok(parsed) => parsed, - Err(_) => break, - }; - - // Check if it's self-signed (root certificate) - if last_cert.subject() == last_cert.issuer() { - // This is a root certificate, we're done - break; - } - - // Try to find the issuer in the trust store - let issuer_name = last_cert.issuer(); - let mut found_issuer = false; - - for ca_der in ca_certs_der { - let (_, ca_cert) = match X509Certificate::from_der(ca_der) { - Ok(parsed) => parsed, - Err(_) => continue, - }; - - // Check if this CA's subject matches the certificate's issuer - if ca_cert.subject() == issuer_name { - // Check if we already have this certificate in the chain - if !chain_der.iter().any(|existing| existing == ca_der) { - chain_der.push(ca_der.clone()); - found_issuer = true; - break; - } - } - } - - if !found_issuer { - // Can't find issuer, stop here - break; - } - } - - chain_der -} - -/// Statistics from certificate loading operations -#[derive(Debug, Clone, Default)] -pub(super) struct CertStats { - pub total_certs: usize, - pub ca_certs: usize, -} - -/// Certificate loader that handles PEM/DER parsing and validation -/// -/// This structure encapsulates the common pattern of loading certificates -/// from various sources (files, directories, bytes) and adding them to -/// a RootCertStore while tracking statistics. -/// -/// Duplicate certificates are detected and only counted once. -pub(super) struct CertLoader<'a> { - store: &'a mut RootCertStore, - ca_certs_der: &'a mut Vec>, - seen_certs: HashSet>, -} - -impl<'a> CertLoader<'a> { - /// Create a new CertLoader with references to the store and DER cache - pub(super) fn new(store: &'a mut RootCertStore, ca_certs_der: &'a mut Vec>) -> Self { - // Initialize seen_certs with existing certificates - let seen_certs = ca_certs_der.iter().cloned().collect(); - Self { - store, - ca_certs_der, - seen_certs, - } - } - - /// Load certificates from a file (supports both PEM and DER formats) - /// - /// Returns statistics about loaded certificates - pub(super) fn load_from_file(&mut self, path: &str) -> Result { - let contents = rustpython_host_env::fs::read(path)?; - self.load_from_bytes(&contents) - } - - /// Load certificates from a directory - /// - /// Reads all files in the directory and attempts to parse them as certificates. - /// Invalid files are silently skipped (matches OpenSSL capath behavior). - pub(super) fn load_from_dir(&mut self, dir_path: &str) -> Result { - let entries = rustpython_host_env::fs::read_dir(dir_path)?; - let mut stats = CertStats::default(); - - for entry in entries { - let entry = entry?; - let path = entry.path(); - - // Skip directories and process all files - // OpenSSL capath uses hash-based naming like "4e1295a3.0" - if path.is_file() - && let Ok(contents) = rustpython_host_env::fs::read(&path) - { - // Ignore errors for individual files (some may not be certs) - if let Ok(file_stats) = self.load_from_bytes(&contents) { - stats.total_certs += file_stats.total_certs; - stats.ca_certs += file_stats.ca_certs; - } - } - } - - Ok(stats) - } - - /// Helper: Add a certificate to the store with duplicate checking - /// - /// Returns true if the certificate was added (not a duplicate), false if it was a duplicate. - fn add_cert_to_store( - &mut self, - cert_bytes: Vec, - cert_der: CertificateDer<'static>, - treat_all_as_ca: bool, - stats: &mut CertStats, - ) -> bool { - // Check for duplicates using HashSet - if !self.seen_certs.insert(cert_bytes.clone()) { - return false; // Duplicate certificate - skip - } - - // Determine if this is a CA certificate - let is_ca = if treat_all_as_ca { - true - } else { - is_ca_certificate(&cert_bytes) - }; - - // Store full DER for get_ca_certs() - self.ca_certs_der.push(cert_bytes); - - // Add to trust store (rustls may handle duplicates internally) - let _ = self.store.add(cert_der); - - // Update statistics - stats.total_certs += 1; - if is_ca { - stats.ca_certs += 1; - } - - true - } - - /// Load certificates from byte slice (auto-detects PEM vs DER format) - /// - /// Tries to parse as PEM first, falls back to DER if that fails. - /// Duplicate certificates are detected and only counted once. - /// - /// If `treat_all_as_ca` is true, all certificates are counted as CA certificates - /// regardless of their Basic Constraints (this matches - /// load_verify_locations with cadata parameter). - /// - /// If `pem_only` is true, only PEM parsing is attempted (for string input) - pub(super) fn load_from_bytes_ex( - &mut self, - data: &[u8], - treat_all_as_ca: bool, - pem_only: bool, - ) -> Result { - let mut stats = CertStats::default(); - - // Try to parse as PEM first - let mut cursor = std::io::Cursor::new(data); - let certs_iter = rustls_pemfile::certs(&mut cursor); - - let mut found_any = false; - let mut first_pem_error = None; // Store first PEM parsing error - for cert_result in certs_iter { - match cert_result { - Ok(cert) => { - found_any = true; - let cert_bytes = cert.to_vec(); - - // Validate that this is actually a valid X.509 certificate - // rustls_pemfile only does base64 decoding, not X.509 validation - if let Err(e) = X509Certificate::from_der(&cert_bytes) { - // Invalid X.509 certificate - return Err(cert_error::pem::parse_failed_debug(e)); - } - - // Add certificate using helper method (handles duplicates) - self.add_cert_to_store(cert_bytes, cert, treat_all_as_ca, &mut stats); - // Helper returns false for duplicates (skip counting) - } - Err(e) if !found_any => { - // PEM parsing failed on first certificate - if pem_only { - // For string input (PEM only), return "no start line" error - return Err(cert_error::pem::no_start_line( - "cadata does not contain a certificate", - )); - } - // Store the error and break to try DER format below - first_pem_error = Some(e); - break; - } - Err(e) => { - // PEM parsing failed after some certs were loaded - return Err(cert_error::pem::parse_failed(e)); - } - } - } - - // If PEM parsing found nothing, try DER format (unless pem_only) - // DER can have multiple certificates concatenated, so parse them sequentially - if !found_any && stats.total_certs == 0 { - // If we had a PEM parsing error, return it instead of trying DER fallback - // This ensures that malformed PEM files (like badcert.pem) raise an error - if let Some(e) = first_pem_error { - return Err(cert_error::pem::parse_failed(e)); - } - - // For PEM-only mode (string input), don't fallback to DER - if pem_only { - return Err(cert_error::pem::no_start_line( - "cadata does not contain a certificate", - )); - } - let mut remaining = data; - let mut loaded_count = 0; - - while !remaining.is_empty() { - match X509Certificate::from_der(remaining) { - Ok((rest, _parsed_cert)) => { - // Extract the DER bytes for this certificate - // Length = total remaining - bytes left after parsing - let cert_len = remaining.len() - rest.len(); - let cert_bytes = &remaining[..cert_len]; - let cert_der = CertificateDer::from(cert_bytes.to_vec()); - - // Add certificate using helper method (handles duplicates) - self.add_cert_to_store( - cert_bytes.to_vec(), - cert_der, - treat_all_as_ca, - &mut stats, - ); - - loaded_count += 1; - remaining = rest; // Move to next certificate - } - Err(e) => { - if loaded_count == 0 { - // Failed to parse first certificate - invalid data - return Err(cert_error::der::not_enough_data( - "cadata does not contain a certificate", - )); - } - // Loaded some certificates but failed on subsequent data (garbage) - return Err(cert_error::der::parse_failed(e)); - } - } - } - - // If we somehow got here with no certificates loaded - if loaded_count == 0 { - return Err(cert_error::der::not_enough_data( - "cadata does not contain a certificate", - )); - } - } - - Ok(stats) - } - - /// Load certificates from byte slice (auto-detects PEM vs DER format) - /// - /// This is a convenience wrapper that calls load_from_bytes_ex with treat_all_as_ca=false - /// and pem_only=false. - pub(super) fn load_from_bytes(&mut self, data: &[u8]) -> Result { - self.load_from_bytes_ex(data, false, false) - } -} - -// NoVerifier: disables certificate verification (for CERT_NONE mode) -#[derive(Debug)] -pub(super) struct NoVerifier; - -impl ServerCertVerifier for NoVerifier { - fn verify_server_cert( - &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp_response: &[u8], - _now: UnixTime, - ) -> Result { - // Accept all certificates without verification - Ok(ServerCertVerified::assertion()) - } - - fn verify_tls12_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - // Accept all signatures without verification - Ok(HandshakeSignatureValid::assertion()) - } - - fn verify_tls13_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - // Accept all signatures without verification - Ok(HandshakeSignatureValid::assertion()) - } - - fn supported_verify_schemes(&self) -> Vec { - ALL_SIGNATURE_SCHEMES.to_vec() - } -} - -// HostnameIgnoringVerifier: verifies certificate chain but ignores hostname -// This is used when check_hostname=False but verify_mode != CERT_NONE -// -// Unlike the previous implementation that used an inner WebPkiServerVerifier, -// this version uses webpki directly to verify only the certificate chain, -// completely bypassing hostname verification. -#[derive(Debug)] -pub(super) struct HostnameIgnoringVerifier { - inner: Arc, -} - -impl HostnameIgnoringVerifier { - /// Create a new HostnameIgnoringVerifier with a pre-built verifier - /// This is useful when you need to configure the verifier with CRLs or other options - pub(super) fn new_with_verifier(inner: Arc) -> Self { - Self { inner } - } -} - -impl ServerCertVerifier for HostnameIgnoringVerifier { - fn verify_server_cert( - &self, - end_entity: &CertificateDer<'_>, - intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, // Intentionally ignored - ocsp_response: &[u8], - now: UnixTime, - ) -> Result { - // Extract a hostname from the certificate to pass to inner verifier - // The inner verifier will validate certificate chain, trust anchors, etc. - // but may fail on hostname mismatch - we'll catch and ignore that error - let dummy_hostname = extract_first_dns_name(end_entity) - .unwrap_or_else(|| ServerName::try_from("localhost").expect("localhost is valid")); - - // Call inner verifier for full certificate validation - match self.inner.verify_server_cert( - end_entity, - intermediates, - &dummy_hostname, - ocsp_response, - now, - ) { - Ok(verified) => Ok(verified), - Err(e) => { - // Check if the error is a hostname mismatch - // If so, ignore it (that's the whole point of HostnameIgnoringVerifier) - match e { - rustls::Error::InvalidCertificate( - rustls::CertificateError::NotValidForName - | rustls::CertificateError::NotValidForNameContext { .. }, - ) => { - // Hostname mismatch - this is expected and acceptable - // The certificate chain, trust anchor, and expiry are valid - Ok(ServerCertVerified::assertion()) - } - _ => { - // Other errors (expired cert, untrusted CA, etc.) should propagate - Err(e) - } - } - } - } - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls12_signature(message, cert, dss) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls13_signature(message, cert, dss) - } - - fn supported_verify_schemes(&self) -> Vec { - self.inner.supported_verify_schemes() - } -} - -// Helper function to extract the first DNS name from a certificate -fn extract_first_dns_name(cert_der: &CertificateDer<'_>) -> Option> { - let (_, cert) = X509Certificate::from_der(cert_der.as_ref()).ok()?; - - // Try Subject Alternative Names first - if let Ok(Some(san_ext)) = cert.subject_alternative_name() { - for name in &san_ext.value.general_names { - if let x509_parser::extensions::GeneralName::DNSName(dns) = name { - // Remove wildcard prefix if present (e.g., "*.example.com" → "example.com") - // This allows us to use the domain for certificate chain verification - // when check_hostname=False - let dns_str = dns.to_string(); - let normalized_dns = normalize_wildcard_hostname(&dns_str); - - match ServerName::try_from(normalized_dns.to_string()) { - Ok(server_name) => { - return Some(server_name); - } - Err(_e) => { - // Continue to next - } - } - } - } - } - - // Fallback to Common Name - for rdn in cert.subject().iter() { - for attr in rdn.iter() { - if attr.attr_type() == &x509_parser::oid_registry::OID_X509_COMMON_NAME - && let Ok(cn) = attr.attr_value().as_str() - { - // Remove wildcard prefix if present - let normalized_cn = normalize_wildcard_hostname(cn); - - match ServerName::try_from(normalized_cn.to_string()) { - Ok(server_name) => { - return Some(server_name); - } - Err(_e) => {} - } - } - } - } - - None -} - -// Custom client certificate verifier for TLS 1.3 deferred validation -// This verifier always succeeds during handshake but stores verification errors -// for later retrieval during I/O operations -#[derive(Debug)] -pub(super) struct DeferredClientCertVerifier { - // The actual verifier that performs validation - inner: Arc, - // Shared storage for deferred error message - deferred_error: Arc>>, -} - -impl DeferredClientCertVerifier { - pub(super) fn new( - inner: Arc, - deferred_error: Arc>>, - ) -> Self { - Self { - inner, - deferred_error, - } - } -} - -impl ClientCertVerifier for DeferredClientCertVerifier { - fn offer_client_auth(&self) -> bool { - self.inner.offer_client_auth() - } - - fn client_auth_mandatory(&self) -> bool { - // Delegate to inner verifier to respect CERT_REQUIRED mode - // This ensures client certificates are mandatory when verify_mode=CERT_REQUIRED - self.inner.client_auth_mandatory() - } - - fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] { - self.inner.root_hint_subjects() - } - - fn verify_client_cert( - &self, - end_entity: &CertificateDer<'_>, - intermediates: &[CertificateDer<'_>], - now: UnixTime, - ) -> Result { - // Perform the actual verification - let result = self - .inner - .verify_client_cert(end_entity, intermediates, now); - - // If verification failed, store the error for the server's Python code - // AND return the error so rustls sends the appropriate TLS alert - if let Err(ref e) = result { - let error_msg = format!("certificate verify failed: {e}"); - *self.deferred_error.write() = Some(error_msg); - // Return the error to rustls so it sends the alert to the client - return result; - } - - result - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls12_signature(message, cert, dss) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls13_signature(message, cert, dss) - } - - fn supported_verify_schemes(&self) -> Vec { - self.inner.supported_verify_schemes() - } -} - -// Public Utility Functions - -/// Load certificate chain and private key from files -/// -/// This function loads a certificate chain from `cert_path` and a private key -/// from `key_path`. If `password` is provided, it will be used to decrypt -/// an encrypted private key. -/// -/// Returns (certificate_chain, private_key) on success. -/// -/// # Arguments -/// * `cert_path` - Path to certificate file (PEM or DER format) -/// * `key_path` - Path to private key file (PEM or DER format, optionally encrypted) -/// * `password` - Optional password for encrypted private key -/// -/// # Errors -/// Returns error if: -/// - Files cannot be read -/// - Certificate or key cannot be parsed -/// - Password is incorrect for encrypted key -pub(super) fn load_cert_chain_from_file( - cert_path: &str, - key_path: &str, - password: Option<&str>, -) -> Result<(Vec>, PrivateKeyDer<'static>), Box> { - // Load certificate file - preserve io::Error for errno - let cert_contents = rustpython_host_env::fs::read(cert_path)?; - - // Parse certificates (PEM format) - let mut cert_cursor = std::io::Cursor::new(&cert_contents); - let certs: Vec> = rustls_pemfile::certs(&mut cert_cursor) - .collect::, _>>() - .map_err(cert_error::pem::parse_failed)?; - - if certs.is_empty() { - return Err(Box::new(cert_error::pem::invalid_cert())); - } - - // Load private key file - preserve io::Error for errno - let key_contents = rustpython_host_env::fs::read(key_path)?; - - // Parse private key (supports PKCS8, RSA, EC formats) - let private_key = if let Some(pwd) = password { - // Try to parse as encrypted PKCS#8 - use der::SecretDocument; - use pkcs8::EncryptedPrivateKeyInfoRef; - use rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer}; - - let pem_str = String::from_utf8_lossy(&key_contents); - - // Extract just the ENCRYPTED PRIVATE KEY block if present - // (file may contain multiple PEM blocks like key + certificate) - let encrypted_key_pem = if let Some(start) = - pem_str.find("-----BEGIN ENCRYPTED PRIVATE KEY-----") - { - if let Some(end_marker) = pem_str[start..].find("-----END ENCRYPTED PRIVATE KEY-----") { - let end = start + end_marker + "-----END ENCRYPTED PRIVATE KEY-----".len(); - Some(&pem_str[start..end]) - } else { - None - } - } else { - None - }; - - // Try to decode and decrypt PEM-encoded encrypted private key using pkcs8's PEM support - let decrypted_key_result = if let Some(key_pem) = encrypted_key_pem { - match SecretDocument::from_pem(key_pem) { - Ok((label, doc)) => { - if label == "ENCRYPTED PRIVATE KEY" { - // Parse encrypted key info from DER - match EncryptedPrivateKeyInfoRef::try_from(doc.as_bytes()) { - Ok(encrypted_key) => { - // Decrypt with password - match encrypted_key.decrypt(pwd.as_bytes()) { - Ok(decrypted) => { - // Convert decrypted SecretDocument to PrivateKeyDer - let key_vec: Vec = decrypted.as_bytes().to_vec(); - let pkcs8_key: PrivatePkcs8KeyDer<'static> = key_vec.into(); - Some(PrivateKeyDer::Pkcs8(pkcs8_key)) - } - Err(e) => { - return Err(Box::new(cert_error::key::decrypt_failed(e))); - } - } - } - Err(e) => { - return Err(Box::new(cert_error::key::parse_encrypted_failed(e))); - } - } - } else { - None - } - } - Err(_) => None, - } - } else { - None - }; - - match decrypted_key_result { - Some(key) => key, - None => { - // Not encrypted PKCS#8, try as unencrypted key - // (password might have been provided for an unencrypted key) - let mut key_cursor = std::io::Cursor::new(&key_contents); - match rustls_pemfile::private_key(&mut key_cursor) { - Ok(Some(key)) => key, - Ok(None) => { - return Err(Box::new(cert_error::key::not_found("key file"))); - } - Err(e) => { - return Err(Box::new(cert_error::key::parse_failed(e))); - } - } - } - } - } else { - // No password provided - try to parse unencrypted key - let mut key_cursor = std::io::Cursor::new(&key_contents); - match rustls_pemfile::private_key(&mut key_cursor) { - Ok(Some(key)) => key, - Ok(None) => { - return Err(Box::new(cert_error::key::not_found("key file"))); - } - Err(e) => { - return Err(Box::new(cert_error::key::parse_failed(e))); - } - } - }; - - Ok((certs, private_key)) -} - -/// Validate that a certificate and private key match -/// -/// This function checks that the public key in the certificate matches -/// the provided private key. This is a basic sanity check to prevent -/// configuration errors. -/// -/// # Arguments -/// * `certs` - Certificate chain (first certificate is the leaf) -/// * `private_key` - Private key to validate against -/// -/// # Errors -/// Returns error if: -/// - Certificate chain is empty -/// - Public key extraction fails -/// - Keys don't match -/// -/// Note: This is a simplified validation. Full validation would require -/// signing and verifying a test message, which is complex with rustls. -pub(super) fn validate_cert_key_match( - certs: &[CertificateDer<'_>], - private_key: &PrivateKeyDer<'_>, -) -> Result<(), String> { - if certs.is_empty() { - return Err("Certificate chain is empty".to_string()); - } - - // For rustls, the actual validation happens when creating CertifiedKey - // We can attempt to create a signing key to verify the key is valid - match CryptoExt::get_ext().any_supported_key(private_key) { - Ok(_signing_key) => { - // If we can create a signing key, the private key is valid - // Rustls will validate the cert-key match when building config - Ok(()) - } - Err(_) => Err("PEM lib".to_string()), - } -} - -/// StrictCertVerifier: wraps a ServerCertVerifier and adds RFC 5280 strict validation -/// -/// When VERIFY_X509_STRICT flag is set, performs additional validation: -/// - Checks for Authority Key Identifier (AKI) extension (required by RFC 5280 Section 4.2.1.1) -/// - Validates other RFC 5280 compliance requirements -/// -/// This matches X509_V_FLAG_X509_STRICT behavior in OpenSSL. -#[derive(Debug)] -pub(super) struct StrictCertVerifier { - inner: Arc, - verify_flags: i32, -} - -impl StrictCertVerifier { - /// Create a new StrictCertVerifier - /// - /// # Arguments - /// * `inner` - The underlying verifier to wrap - /// * `verify_flags` - SSL verification flags (e.g., VERIFY_X509_STRICT) - pub(super) fn new(inner: Arc, verify_flags: i32) -> Self { - Self { - inner, - verify_flags, - } - } - - /// Check if a certificate has the Authority Key Identifier extension - /// - /// RFC 5280 Section 4.2.1.1 states that conforming CAs MUST include this - /// extension in all certificates except self-signed certificates. - fn check_aki_present(cert_der: &[u8]) -> Result<(), String> { - let (_, cert) = X509Certificate::from_der(cert_der) - .map_err(|e| format!("Failed to parse certificate: {e}"))?; - - // Check for Authority Key Identifier extension (OID 2.5.29.35) - let has_aki = cert - .tbs_certificate - .extensions() - .iter() - .any(|ext| ext.oid == oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER); - - if !has_aki { - return Err( - "certificate verification failed: certificate missing required Authority Key Identifier extension" - .to_string(), - ); - } - - Ok(()) - } -} - -impl ServerCertVerifier for StrictCertVerifier { - fn verify_server_cert( - &self, - end_entity: &CertificateDer<'_>, - intermediates: &[CertificateDer<'_>], - server_name: &ServerName<'_>, - ocsp_response: &[u8], - now: UnixTime, - ) -> Result { - // First, perform the standard verification - let result = self.inner.verify_server_cert( - end_entity, - intermediates, - server_name, - ocsp_response, - now, - )?; - - // If VERIFY_X509_STRICT flag is set, perform additional validation - if self.verify_flags & VERIFY_X509_STRICT != 0 { - // Check end entity certificate for AKI - // RFC 5280 Section 4.2.1.1: self-signed certificates are exempt from AKI requirement - if !is_self_signed(end_entity) { - Self::check_aki_present(end_entity.as_ref()) - .map_err(cert_error::to_rustls_invalid_cert)?; - } - - // Check intermediate certificates for AKI - for intermediate in intermediates { - Self::check_aki_present(intermediate.as_ref()) - .map_err(cert_error::to_rustls_invalid_cert)?; - } - } - - Ok(result) - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls12_signature(message, cert, dss) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls13_signature(message, cert, dss) - } - - fn supported_verify_schemes(&self) -> Vec { - self.inner.supported_verify_schemes() - } -} - -/// EmptyRootStoreVerifier: used when verify_mode != CERT_NONE but no CA certs are loaded -/// -/// This verifier always fails certificate verification with UnknownIssuer error, -/// when no root certificates are available. -/// This allows the SSL context to be created successfully, but handshake will fail -/// with a proper SSLCertVerificationError (verify_code=20, UNABLE_TO_GET_ISSUER_CERT_LOCALLY). -#[derive(Debug)] -pub(super) struct EmptyRootStoreVerifier; - -impl ServerCertVerifier for EmptyRootStoreVerifier { - fn verify_server_cert( - &self, - _end_entity: &CertificateDer<'_>, - _intermediates: &[CertificateDer<'_>], - _server_name: &ServerName<'_>, - _ocsp_response: &[u8], - _now: UnixTime, - ) -> Result { - // Always fail with UnknownIssuer - when no CA certs loaded - // This will be mapped to X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY (20) - Err(rustls::Error::InvalidCertificate( - rustls::CertificateError::UnknownIssuer, - )) - } - - fn verify_tls12_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - // Accept signatures during handshake - the cert verification will fail anyway - Ok(HandshakeSignatureValid::assertion()) - } - - fn verify_tls13_signature( - &self, - _message: &[u8], - _cert: &CertificateDer<'_>, - _dss: &DigitallySignedStruct, - ) -> Result { - // Accept signatures during handshake - the cert verification will fail anyway - Ok(HandshakeSignatureValid::assertion()) - } - - fn supported_verify_schemes(&self) -> Vec { - ALL_SIGNATURE_SCHEMES.to_vec() - } -} - -/// CRLCheckVerifier: Wraps a verifier to enforce CRL checking when flags are set -/// -/// This verifier ensures that when CRL checking flags are set (VERIFY_CRL_CHECK_LEAF = 4) -/// but no CRLs have been loaded, the verification fails with UnknownRevocationStatus. -/// This matches X509_V_FLAG_CRL_CHECK without loaded CRLs -/// causes "unable to get CRL" error. -#[derive(Debug)] -pub(super) struct CRLCheckVerifier { - inner: Arc, - has_crls: bool, - crl_check_enabled: bool, -} - -impl CRLCheckVerifier { - pub(super) fn new( - inner: Arc, - has_crls: bool, - crl_check_enabled: bool, - ) -> Self { - Self { - inner, - has_crls, - crl_check_enabled, - } - } -} - -impl ServerCertVerifier for CRLCheckVerifier { - fn verify_server_cert( - &self, - end_entity: &CertificateDer<'_>, - intermediates: &[CertificateDer<'_>], - server_name: &ServerName<'_>, - ocsp_response: &[u8], - now: UnixTime, - ) -> Result { - // If CRL checking is enabled but no CRLs are loaded, fail with UnknownRevocationStatus - // X509_V_ERR_UNABLE_TO_GET_CRL (3) - if self.crl_check_enabled && !self.has_crls { - return Err(rustls::Error::InvalidCertificate( - rustls::CertificateError::UnknownRevocationStatus, - )); - } - - // Otherwise, delegate to inner verifier - self.inner - .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now) - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls12_signature(message, cert, dss) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls13_signature(message, cert, dss) - } - - fn supported_verify_schemes(&self) -> Vec { - self.inner.supported_verify_schemes() - } -} - -/// Partial Chain Verifier - Handles VERIFY_X509_PARTIAL_CHAIN flag -/// -/// OpenSSL's X509_V_FLAG_PARTIAL_CHAIN allows verification to succeed if any certificate -/// in the presented chain is found in the trust store, not just the root CA. This is useful -/// for trusting intermediate certificates or self-signed certificates directly. -/// -/// rustls's WebPkiServerVerifier doesn't support this behavior by default, so we wrap it -/// to add partial chain support when the flag is set. -/// -/// Behavior: -/// 1. Try standard verification first (full chain to trusted root) -/// 2. If that fails and VERIFY_X509_PARTIAL_CHAIN is set: -/// - Check if the end-entity certificate is in the trust store -/// - If yes, accept the certificate as trusted -/// -/// This matches accepting self-signed certificates that -/// are explicitly loaded via load_verify_locations(). -#[derive(Debug)] -pub(super) struct PartialChainVerifier { - inner: Arc, - ca_certs_der: Vec>, - verify_flags: i32, -} - -impl PartialChainVerifier { - pub(super) fn new( - inner: Arc, - ca_certs_der: Vec>, - verify_flags: i32, - ) -> Self { - Self { - inner, - ca_certs_der, - verify_flags, - } - } -} - -impl ServerCertVerifier for PartialChainVerifier { - fn verify_server_cert( - &self, - end_entity: &CertificateDer<'_>, - intermediates: &[CertificateDer<'_>], - server_name: &ServerName<'_>, - ocsp_response: &[u8], - now: UnixTime, - ) -> Result { - // Try standard verification first - match self.inner.verify_server_cert( - end_entity, - intermediates, - server_name, - ocsp_response, - now, - ) { - Ok(result) => Ok(result), - Err(e) => { - // If verification failed, check if the end-entity certificate is in the trust store - // OpenSSL behavior: - // 1. Self-signed certs in trust store: ALWAYS trusted (flag not required) - // 2. Non-self-signed end-entity certs in trust store: require VERIFY_X509_PARTIAL_CHAIN - // 3. Intermediate certs in trust store: require VERIFY_X509_PARTIAL_CHAIN - let end_entity_der = end_entity.as_ref(); - if self - .ca_certs_der - .iter() - .any(|cert_der| cert_der.as_slice() == end_entity_der) - { - // End-entity certificate is in the trust store - // Check if this is a self-signed certificate - let is_self_signed_cert = is_self_signed(end_entity); - - // Self-signed: always trust (OpenSSL behavior) - // Non-self-signed: require VERIFY_X509_PARTIAL_CHAIN flag - if is_self_signed_cert || (self.verify_flags & VERIFY_X509_PARTIAL_CHAIN != 0) { - // Certificate is trusted, but still perform hostname verification - verify_hostname(end_entity, server_name)?; - return Ok(ServerCertVerified::assertion()); - } - } - // No match found or non-self-signed without flag - return original error - Err(e) - } - } - } - - fn verify_tls12_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls12_signature(message, cert, dss) - } - - fn verify_tls13_signature( - &self, - message: &[u8], - cert: &CertificateDer<'_>, - dss: &DigitallySignedStruct, - ) -> Result { - self.inner.verify_tls13_signature(message, cert, dss) - } - - fn supported_verify_schemes(&self) -> Vec { - self.inner.supported_verify_schemes() - } -} - -// Hostname Verification: - -/// Check if a certificate is self-signed by comparing issuer and subject. -/// Returns true if the certificate is self-signed (issuer == subject). -fn is_self_signed(cert_der: &CertificateDer<'_>) -> bool { - use x509_parser::prelude::*; - - // Parse the certificate - let Ok((_, cert)) = X509Certificate::from_der(cert_der.as_ref()) else { - // If we can't parse it, assume it's not self-signed (conservative approach) - return false; - }; - - // Compare issuer and subject - // A certificate is self-signed if issuer == subject - cert.issuer() == cert.subject() -} - -/// Verify that a certificate is valid for the given hostname/IP address. -/// This function checks Subject Alternative Names (SAN) and Common Name (CN). -fn verify_hostname( - cert_der: &CertificateDer<'_>, - server_name: &ServerName<'_>, -) -> Result<(), rustls::Error> { - use x509_parser::extensions::GeneralName; - use x509_parser::prelude::*; - - // Parse the certificate - let (_, cert) = X509Certificate::from_der(cert_der.as_ref()).map_err(|e| { - cert_error::to_rustls_invalid_cert(format!( - "Failed to parse certificate for hostname verification: {e}" - )) - })?; - - match server_name { - ServerName::DnsName(dns) => { - let expected_name = dns.as_ref(); - - // 1. Check Subject Alternative Names (SAN) - preferred method - if let Ok(Some(san_ext)) = cert.subject_alternative_name() { - for name in &san_ext.value.general_names { - if let GeneralName::DNSName(dns_name) = name - && hostname_matches(expected_name, dns_name) - { - return Ok(()); - } - } - } - - // 2. Fallback to Common Name (CN) - deprecated but still checked for compatibility - for rdn in cert.subject().iter() { - for attr in rdn.iter() { - if attr.attr_type() == &x509_parser::oid_registry::OID_X509_COMMON_NAME - && let Ok(cn) = attr.attr_value().as_str() - && hostname_matches(expected_name, cn) - { - return Ok(()); - } - } - } - - // No match found - return error - Err(cert_error::to_rustls_invalid_cert(format!( - "Hostname mismatch: certificate is not valid for '{expected_name}'", - ))) - } - ServerName::IpAddress(ip) => verify_ip_address(&cert, ip), - _ => { - // Unknown server name type - Err(cert_error::to_rustls_cert_error( - std::io::ErrorKind::InvalidInput, - "Unsupported server name type for hostname verification", - )) - } - } -} - -/// Match a hostname against a pattern, supporting wildcard certificates (*.example.com). -/// Implements RFC 6125 wildcard matching rules: -/// - Wildcard must be in the leftmost label -/// - Wildcard must be the only character in that label -/// - Wildcard must match at least one character -fn hostname_matches(expected: &str, pattern: &str) -> bool { - // Wildcard matching for *.example.com - if let Some(pattern_base) = pattern.strip_prefix("*.") { - // Find the first dot in expected hostname - if let Some(dot_pos) = expected.find('.') { - let expected_base = &expected[dot_pos + 1..]; - - // The base domains must match (case insensitive) - // and the leftmost label must not be empty - return dot_pos > 0 && expected_base.eq_ignore_ascii_case(pattern_base); - } - - // No dot in expected, can't match wildcard - return false; - } - - // Exact match (case insensitive per RFC 4343) - expected.eq_ignore_ascii_case(pattern) -} - -/// Verify that a certificate is valid for the given IP address. -/// Checks Subject Alternative Names for IP Address entries. -fn verify_ip_address( - cert: &X509Certificate<'_>, - expected_ip: &rustls::pki_types::IpAddr, -) -> Result<(), rustls::Error> { - use core::net::IpAddr; - use x509_parser::extensions::GeneralName; - - // Convert rustls IpAddr to std::net::IpAddr for comparison - let expected_std_ip: IpAddr = match expected_ip { - rustls::pki_types::IpAddr::V4(octets) => IpAddr::V4(core::net::Ipv4Addr::from(*octets)), - rustls::pki_types::IpAddr::V6(octets) => IpAddr::V6(core::net::Ipv6Addr::from(*octets)), - }; - - // Check Subject Alternative Names for IP addresses - if let Ok(Some(san_ext)) = cert.subject_alternative_name() { - for name in &san_ext.value.general_names { - if let GeneralName::IPAddress(cert_ip_bytes) = name { - // Parse the IP address from the certificate - let cert_ip = match cert_ip_bytes.len() { - 4 => { - // IPv4 - if let Ok(octets) = <[u8; 4]>::try_from(*cert_ip_bytes) { - IpAddr::V4(core::net::Ipv4Addr::from(octets)) - } else { - continue; - } - } - 16 => { - // IPv6 - if let Ok(octets) = <[u8; 16]>::try_from(*cert_ip_bytes) { - IpAddr::V6(core::net::Ipv6Addr::from(octets)) - } else { - continue; - } - } - _ => continue, // Invalid IP address length - }; - - if cert_ip == expected_std_ip { - return Ok(()); - } - } - } - } - - // No matching IP address found - Err(cert_error::to_rustls_invalid_cert(format!( - "IP address mismatch: certificate is not valid for '{expected_std_ip}'", - ))) -} diff --git a/crates/stdlib/src/ssl/compat.rs b/crates/stdlib/src/ssl/compat.rs index 7ed65ce8f4c..3f79c9aca2a 100644 --- a/crates/stdlib/src/ssl/compat.rs +++ b/crates/stdlib/src/ssl/compat.rs @@ -15,34 +15,16 @@ #[path = "../openssl/ssl_data_31.rs"] mod ssl_data; -use crate::socket::{SockWaitKind, timeout_error_msg}; +use crate::socket::timeout_error_msg; use crate::vm::VirtualMachine; -use alloc::sync::Arc; -use parking_lot::RwLock as ParkingRwLock; -use rustls::Connection; -use rustls::client::ClientConfig; -use rustls::crypto::{CryptoProvider, SupportedKxGroup}; -use rustls::pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer}; -use rustls::server::{ProducesTickets, ResolvesServerCert, ServerConfig, WebPkiClientVerifier}; -use rustls::sign::CertifiedKey; -use rustls::{RootCertStore, SupportedCipherSuite}; -use rustpython_vm::builtins::{PyBaseException, PyBaseExceptionRef}; +use rustpython_vm::builtins::PyBaseExceptionRef; use rustpython_vm::convert::IntoPyException; -use rustpython_vm::function::ArgBytesLike; -use rustpython_vm::{AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject}; -use std::io::Read; - -use super::providers::CryptoExt; - -// Import PySSLSocket from parent module -use super::_ssl::{ - PySSLSocket, SSL3_RT_MAX_PACKET_SIZE, VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT, -}; +use rustpython_vm::{AsObject, PyPayload, PyResult}; // Import error types and helper functions from error module use super::error::{ - PySSLCertVerificationError, PySSLError, create_ssl_eof_error, create_ssl_syscall_error, - create_ssl_want_read_error, create_ssl_want_write_error, create_ssl_zero_return_error, + PySSLCertVerificationError, PySSLError, create_ssl_eof_error, create_ssl_want_read_error, + create_ssl_want_write_error, create_ssl_zero_return_error, }; // OpenSSL Constants: @@ -52,12 +34,12 @@ use super::error::{ const ERR_LIB_SSL: i32 = 20; // OpenSSL SSL error reason codes (include/openssl/sslerr.h) -// #define SSL_R_NO_SHARED_CIPHER 193 +const SSL_R_NO_SUITABLE_KEY_SHARE: i32 = 101; +const SSL_R_NO_SUITABLE_SIGNATURE_ALGORITHM: i32 = 118; const SSL_R_NO_SHARED_CIPHER: i32 = 193; - -// OpenSSL X509 verification flags (include/openssl/x509_vfy.h) -// #define X509_V_FLAG_CRL_CHECK 4 -const X509_V_FLAG_CRL_CHECK: i32 = 4; +const SSL_R_NO_APPLICATION_PROTOCOL: i32 = 235; +const SSL_R_UNSUPPORTED_PROTOCOL: i32 = 258; +const SSL_R_NO_SUITABLE_GROUPS: i32 = 295; // X509 Certificate Verification Error Codes (OpenSSL Compatible): // @@ -181,30 +163,33 @@ pub(super) enum SslError { WantRead, /// SSL_ERROR_WANT_WRITE WantWrite, - /// SSL_ERROR_SYSCALL - Syscall(String), /// SSL_ERROR_SSL Ssl(String), + /// PEM parser error + PemLib(String), + /// DER parser error + FailedToReadDer(String), + /// Text cadata did not contain a certificate PEM block + CadataNoStartLine, + /// Binary cadata did not contain a DER certificate + CadataNotEnoughData, /// SSL_ERROR_ZERO_RETURN (clean closure with close_notify) ZeroReturn, /// Unexpected EOF without close_notify (protocol violation) Eof, - /// Non-TLS data received before handshake completed - PreauthData, /// Certificate verification error CertVerification(rustls::CertificateError), /// I/O error Io(std::io::Error), /// Timeout error (socket.timeout) + #[expect(dead_code, reason = "TODO: Implement timeouts")] Timeout(String), - /// SNI callback triggered - need to restart handshake - SniCallbackRestart, /// Python exception (pass through directly) Py(PyBaseExceptionRef), /// TLS alert received with OpenSSL-compatible error code AlertReceived { lib: i32, reason: i32 }, - /// NO_SHARED_CIPHER error (OpenSSL SSL_R_NO_SHARED_CIPHER) - NoCipherSuites, + /// OpenSSL-compatible SSL reason code + OpenSslReason(i32), } impl SslError { @@ -237,27 +222,33 @@ impl SslError { } } } - // 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 + let reason = match peer_err { + PeerIncompatible::NoCipherSuitesInCommon => SSL_R_NO_SHARED_CIPHER, + PeerIncompatible::NoKxGroupsInCommon + | PeerIncompatible::NoEcPointFormatsInCommon + | PeerIncompatible::EcPointsExtensionRequired + | PeerIncompatible::NamedGroupsExtensionRequired + | PeerIncompatible::UncompressedEcPointsRequired => SSL_R_NO_SUITABLE_GROUPS, + PeerIncompatible::KeyShareExtensionRequired => SSL_R_NO_SUITABLE_KEY_SHARE, + PeerIncompatible::NoCertificateRequestSignatureSchemesInCommon + | PeerIncompatible::NoSignatureSchemesInCommon + | PeerIncompatible::SignatureAlgorithmsExtensionRequired => { + SSL_R_NO_SUITABLE_SIGNATURE_ALGORITHM } - } + PeerIncompatible::ServerDoesNotSupportTls12Or13 + | PeerIncompatible::ServerTlsVersionIsDisabledByOurConfig + | PeerIncompatible::SupportedVersionsExtensionRequired + | PeerIncompatible::Tls12NotOffered + | PeerIncompatible::Tls12NotOfferedOrEnabled + | PeerIncompatible::Tls13RequiredForQuic => SSL_R_UNSUPPORTED_PROTOCOL, + _ => return Self::Ssl(format!("peer is incompatible: {peer_err:?}")), + }; + Self::OpenSslReason(reason) + } + rustls::Error::NoApplicationProtocol => { + Self::OpenSslReason(SSL_R_NO_APPLICATION_PROTOCOL) } _ => Self::Ssl(format!("{err}")), } @@ -335,12 +326,37 @@ impl SslError { .unwrap_or("UNKNOWN"); // Delegate to create_ssl_error_with_reason for actual exception creation - Self::create_ssl_error_with_reason( - vm, - Some(lib_str), - reason_str, - format!("[SSL] {reason_str}"), + let message = format!( + "[SSL: {reason_str}] {}", + reason_str.to_ascii_lowercase().replace('_', " ") + ); + Self::create_ssl_error_with_reason(vm, Some(lib_str), reason_str, message) + } + + fn create_plain_ssl_error(vm: &VirtualMachine, msg: impl Into) -> PyBaseExceptionRef { + vm.new_os_subtype_error( + PySSLError::class(&vm.ctx).to_owned(), + None, + format!("SSL error: {}", msg.into()), ) + .upcast() + } + + fn create_pem_ssl_error( + vm: &VirtualMachine, + msg: impl Into, + ) -> PyResult { + let msg = msg.into(); + let exc = vm.new_os_subtype_error( + PySSLError::class(&vm.ctx).to_owned(), + None, + format!("SSL error: {msg}"), + ); + exc.as_object() + .set_attr("library", vm.ctx.new_str("PEM").as_object().to_owned(), vm)?; + exc.as_object() + .set_attr("reason", vm.ctx.new_str(msg).as_object().to_owned(), vm)?; + Ok(exc.upcast()) } /// Convert to Python exception @@ -349,28 +365,22 @@ impl SslError { Self::WantRead => create_ssl_want_read_error(vm).upcast(), Self::WantWrite => create_ssl_want_write_error(vm).upcast(), Self::Timeout(msg) => timeout_error_msg(vm, msg).upcast(), - Self::Syscall(msg) => { - // SSLSyscallError with errno=SSL_ERROR_SYSCALL (5) - create_ssl_syscall_error(vm, msg).upcast() - } - Self::Ssl(msg) => vm - .new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - format!("SSL error: {msg}"), - ) - .upcast(), + Self::Ssl(msg) => Self::create_plain_ssl_error(vm, msg), + Self::PemLib(msg) => Self::create_pem_ssl_error(vm, format!("PEM lib: {msg}")) + .expect("unlikely to happen"), + Self::FailedToReadDer(msg) => { + Self::create_plain_ssl_error(vm, format!("Failed to read DER: {msg}")) + } + Self::CadataNoStartLine => Self::create_plain_ssl_error( + vm, + "no start line: cadata does not contain a certificate", + ), + Self::CadataNotEnoughData => Self::create_plain_ssl_error( + vm, + "not enough data: cadata does not contain a certificate", + ), Self::ZeroReturn => create_ssl_zero_return_error(vm).upcast(), Self::Eof => create_ssl_eof_error(vm).upcast(), - Self::PreauthData => { - // Non-TLS data received before handshake - Self::create_ssl_error_with_reason( - vm, - None, - "before TLS handshake with data", - "before TLS handshake with data", - ) - } Self::CertVerification(cert_err) => { // Use the proper cert verification error creator create_ssl_cert_verification_error(vm, &cert_err).expect("unlikely to happen") @@ -386,1754 +396,15 @@ impl SslError { ) .upcast(), Self::Io(err) => err.into_pyexception(vm), - Self::SniCallbackRestart => { - // This should be handled at PySSLSocket level - unreachable!("SniCallbackRestart should not reach Python layer") - } Self::Py(exc) => exc, Self::AlertReceived { lib, reason } => { Self::create_ssl_error_from_codes(vm, lib, reason) } - Self::NoCipherSuites => { - // OpenSSL error: lib=20 (ERR_LIB_SSL), reason=193 (SSL_R_NO_SHARED_CIPHER) - Self::create_ssl_error_from_codes(vm, ERR_LIB_SSL, SSL_R_NO_SHARED_CIPHER) + Self::OpenSslReason(reason) => { + Self::create_ssl_error_from_codes(vm, ERR_LIB_SSL, reason) } } } } pub(super) type SslResult = Result; -/// Common protocol settings shared between client and server connections -#[derive(Debug)] -pub(super) struct ProtocolSettings { - pub versions: &'static [&'static rustls::SupportedProtocolVersion], - pub kx_groups: Option>, - pub cipher_suites: Option>, - pub alpn_protocols: Vec>, -} - -/// Options for creating a server TLS configuration -#[derive(Debug)] -pub(super) struct ServerConfigOptions { - /// Common protocol settings (versions, ALPN, KX groups, cipher suites) - pub protocol_settings: ProtocolSettings, - /// Server certificate chain - pub cert_chain: Vec>, - /// Server private key - pub private_key: PrivateKeyDer<'static>, - /// Root certificates for client verification (if required) - pub root_store: Option, - /// Whether to request client certificate - pub request_client_cert: bool, - /// Whether to use deferred client certificate validation (TLS 1.3) - pub use_deferred_validation: bool, - /// Custom certificate resolver (for SNI support) - pub cert_resolver: Option>, - /// Deferred certificate error storage (for TLS 1.3) - pub deferred_cert_error: Option>>>, - /// Session storage for server-side session resumption - pub session_storage: Option>, - /// Shared ticketer for TLS 1.2 session tickets (stateless resumption) - pub ticketer: Option>, -} - -/// Options for creating a client TLS configuration -#[derive(Debug)] -pub(super) struct ClientConfigOptions { - /// Common protocol settings (versions, ALPN, KX groups, cipher suites) - pub protocol_settings: ProtocolSettings, - /// Root certificates for server verification - pub root_store: Option, - /// DER-encoded CA certificates (for partial chain verification) - pub ca_certs_der: Vec>, - /// Client certificate chain (for mTLS) - pub cert_chain: Option>>, - /// Client private key (for mTLS) - pub private_key: Option>, - /// Whether to verify server certificates (CERT_NONE disables verification) - pub verify_server_cert: bool, - /// Whether to check hostname against certificate (check_hostname) - pub check_hostname: bool, - /// SSL verification flags (e.g., VERIFY_X509_STRICT) - pub verify_flags: i32, - /// Session store for client-side session resumption - pub session_store: Option>, - /// Certificate Revocation Lists for CRL checking - pub crls: Vec>, -} - -/// Create custom CryptoProvider with specified cipher suites and key exchange groups -/// -/// This helper function consolidates the duplicated CryptoProvider creation logic -/// for both server and client configurations. -fn create_custom_crypto_provider( - cipher_suites: Option>, - kx_groups: Option>, -) -> Arc { - let default_provider = CryptoExt::get_provider(); - - Arc::new(CryptoProvider { - cipher_suites: cipher_suites.unwrap_or_else(|| default_provider.cipher_suites.clone()), - kx_groups: kx_groups.unwrap_or_else(|| default_provider.kx_groups.clone()), - signature_verification_algorithms: default_provider.signature_verification_algorithms, - secure_random: default_provider.secure_random, - key_provider: default_provider.key_provider, - }) -} - -/// Create a server TLS configuration -/// -/// This abstracts the complex rustls ServerConfig building logic, -/// matching SSL_CTX initialization for server sockets. -pub(super) fn create_server_config(options: ServerConfigOptions) -> Result { - // Create custom crypto provider using helper function - let custom_provider = create_custom_crypto_provider( - options.protocol_settings.cipher_suites.clone(), - options.protocol_settings.kx_groups.clone(), - ); - - // Step 1: Build the appropriate client cert verifier based on settings - let client_cert_verifier: Option> = - if let Some(root_store) = options.root_store { - if options.request_client_cert { - // Client certificate verification required - let base_verifier = WebPkiClientVerifier::builder(Arc::new(root_store)) - .build() - .map_err(|e| format!("Failed to create client verifier: {e}"))?; - - if options.use_deferred_validation { - // TLS 1.3: Use deferred validation - if let Some(deferred_error) = options.deferred_cert_error { - use crate::ssl::cert::DeferredClientCertVerifier; - let deferred_verifier = - DeferredClientCertVerifier::new(base_verifier, deferred_error); - Some(Arc::new(deferred_verifier)) - } else { - // No deferred error storage provided, use immediate validation - Some(base_verifier) - } - } else { - // TLS 1.2 or non-deferred: Use immediate validation - Some(base_verifier) - } - } else { - // No client authentication - None - } - } else { - // No root store - no client authentication - None - }; - - // Step 2: Create ServerConfig builder once with the selected verifier - let builder = ServerConfig::builder_with_provider(custom_provider) - .with_protocol_versions(options.protocol_settings.versions) - .map_err(|e| format!("Failed to create server config builder: {e}"))?; - - let builder = if let Some(verifier) = client_cert_verifier { - builder.with_client_cert_verifier(verifier) - } else { - builder.with_no_client_auth() - }; - - // Add certificate - let mut config = if let Some(resolver) = options.cert_resolver { - // Use custom cert resolver (e.g., for SNI) - builder.with_cert_resolver(resolver) - } else { - // Use single certificate - builder - .with_single_cert(options.cert_chain, options.private_key) - .map_err(|e| format!("Failed to set server certificate: {e}"))? - }; - - // Set ALPN protocols with fallback - apply_alpn_with_fallback( - &mut config.alpn_protocols, - &options.protocol_settings.alpn_protocols, - ); - - // Set session storage for server-side session resumption (TLS 1.3) - if let Some(session_storage) = options.session_storage { - config.session_storage = session_storage; - } - - // Set ticketer for TLS 1.2 session tickets (stateless resumption) - if let Some(ticketer) = options.ticketer { - config.ticketer = ticketer.clone(); - } - - Ok(config) -} - -/// Build WebPki verifier with CRL support -/// -/// This helper function consolidates the duplicated CRL setup logic for both -/// check_hostname=True and check_hostname=False cases. -fn build_webpki_verifier_with_crls( - root_store: Arc, - crls: Vec>, - verify_flags: i32, -) -> Result, String> { - use rustls::client::WebPkiServerVerifier; - - let mut verifier_builder = WebPkiServerVerifier::builder(root_store); - - // Check if CRL verification is requested - let crl_check_requested = verify_flags & X509_V_FLAG_CRL_CHECK != 0; - let has_crls = !crls.is_empty(); - - // Add CRLs if provided OR if CRL checking is explicitly requested - // (even with empty CRLs, rustls will fail verification if CRL checking is enabled) - if has_crls || crl_check_requested { - verifier_builder = verifier_builder.with_crls(crls); - - // Check if we should only verify end-entity (leaf) certificates - if verify_flags & X509_V_FLAG_CRL_CHECK != 0 { - verifier_builder = verifier_builder.only_check_end_entity_revocation(); - } - } - - let webpki_verifier = verifier_builder - .build() - .map_err(|e| format!("Failed to build WebPkiServerVerifier: {e}"))?; - - Ok(webpki_verifier as Arc) -} - -/// Apply verifier wrappers (CRLCheckVerifier and StrictCertVerifier) -/// -/// This helper function consolidates the duplicated verifier wrapping logic. -fn apply_verifier_wrappers( - verifier: Arc, - verify_flags: i32, - has_crls: bool, - ca_certs_der: Vec>, -) -> Arc { - let crl_check_requested = verify_flags & X509_V_FLAG_CRL_CHECK != 0; - - // Wrap with CRLCheckVerifier to enforce CRL checking when flags are set - let verifier = if crl_check_requested { - use crate::ssl::cert::CRLCheckVerifier; - Arc::new(CRLCheckVerifier::new( - verifier, - has_crls, - crl_check_requested, - )) - } else { - verifier - }; - - // Always use PartialChainVerifier when trust store is not empty - // This allows self-signed certificates in trust store to be trusted - // (OpenSSL behavior: self-signed certs are always trusted, non-self-signed require flag) - let verifier = if !ca_certs_der.is_empty() { - use crate::ssl::cert::PartialChainVerifier; - Arc::new(PartialChainVerifier::new( - verifier, - ca_certs_der, - verify_flags, - )) - } else { - verifier - }; - - // Wrap with StrictCertVerifier if VERIFY_X509_STRICT flag is set - if verify_flags & VERIFY_X509_STRICT != 0 { - Arc::new(super::cert::StrictCertVerifier::new(verifier, verify_flags)) - } else { - verifier - } -} - -/// Apply ALPN protocols -/// -/// OpenSSL 1.1.0f+ allows ALPN negotiation to fail without aborting handshake. -/// rustls follows RFC 7301 strictly and rejects connections with no matching protocol. -/// To emulate OpenSSL behavior, we add a special fallback protocol (null byte). -fn apply_alpn_with_fallback(config_alpn: &mut Vec>, alpn_protocols: &[Vec]) { - if !alpn_protocols.is_empty() { - *config_alpn = alpn_protocols.to_vec(); - config_alpn.push(vec![0u8]); // Add null byte as fallback marker - } -} - -/// Create a client TLS configuration -/// -/// This abstracts the complex rustls ClientConfig building logic, -/// matching SSL_CTX initialization for client sockets. -pub(super) fn create_client_config(options: ClientConfigOptions) -> Result { - // Create custom crypto provider using helper function - let custom_provider = create_custom_crypto_provider( - options.protocol_settings.cipher_suites.clone(), - options.protocol_settings.kx_groups.clone(), - ); - - // Step 1: Build the appropriate verifier based on verification settings - let verifier: Arc = if options - .verify_server_cert - { - // Verify server certificates - let root_store = options - .root_store - .ok_or("Root store required for server verification")?; - - let root_store_arc = Arc::new(root_store); - - // Check if root_store is empty (no CA certs loaded) - // CPython allows this and fails during handshake with SSLCertVerificationError - if root_store_arc.is_empty() { - // Use EmptyRootStoreVerifier - always fails with UnknownIssuer during handshake - use crate::ssl::cert::EmptyRootStoreVerifier; - Arc::new(EmptyRootStoreVerifier) - } else { - // Calculate has_crls once for both hostname verification paths - let has_crls = !options.crls.is_empty(); - - if options.check_hostname { - // Default behavior: verify both certificate chain and hostname - let base_verifier = build_webpki_verifier_with_crls( - root_store_arc, - options.crls, - options.verify_flags, - )?; - - // Apply CRL and Strict verifier wrappers using helper function - apply_verifier_wrappers( - base_verifier, - options.verify_flags, - has_crls, - options.ca_certs_der.clone(), - ) - } else { - // check_hostname=False: verify certificate chain but ignore hostname - use crate::ssl::cert::HostnameIgnoringVerifier; - - // Build verifier with CRL support using helper function - let webpki_verifier = build_webpki_verifier_with_crls( - root_store_arc, - options.crls, - options.verify_flags, - )?; - - // Apply CRL verifier wrapper if needed (without Strict wrapper yet) - let crl_check_requested = options.verify_flags & X509_V_FLAG_CRL_CHECK != 0; - let verifier = if crl_check_requested { - use crate::ssl::cert::CRLCheckVerifier; - Arc::new(CRLCheckVerifier::new( - webpki_verifier, - has_crls, - crl_check_requested, - )) as Arc - } else { - webpki_verifier - }; - - // Wrap with PartialChainVerifier if VERIFY_X509_PARTIAL_CHAIN is set - let verifier = if options.verify_flags & VERIFY_X509_PARTIAL_CHAIN != 0 { - use crate::ssl::cert::PartialChainVerifier; - Arc::new(PartialChainVerifier::new( - verifier, - options.ca_certs_der.clone(), - options.verify_flags, - )) as Arc - } else { - verifier - }; - - // Wrap with HostnameIgnoringVerifier to bypass hostname checking - let hostname_ignoring_verifier: Arc< - dyn rustls::client::danger::ServerCertVerifier, - > = Arc::new(HostnameIgnoringVerifier::new_with_verifier(verifier)); - - // Apply Strict verifier wrapper once at the end if needed - if options.verify_flags & VERIFY_X509_STRICT != 0 { - Arc::new(crate::ssl::cert::StrictCertVerifier::new( - hostname_ignoring_verifier, - options.verify_flags, - )) - } else { - hostname_ignoring_verifier - } - } - } - } else { - // CERT_NONE: disable all verification - use crate::ssl::cert::NoVerifier; - Arc::new(NoVerifier) - }; - - // Step 2: Create ClientConfig builder once with the selected verifier - let builder = ClientConfig::builder_with_provider(custom_provider) - .with_protocol_versions(options.protocol_settings.versions) - .map_err(|e| format!("Failed to create client config builder: {e}"))? - .dangerous() - .with_custom_certificate_verifier(verifier); - - // Add client certificate if provided (mTLS) - let mut config = - if let (Some(cert_chain), Some(private_key)) = (options.cert_chain, options.private_key) { - builder - .with_client_auth_cert(cert_chain, private_key) - .map_err(|e| format!("Failed to set client certificate: {e}"))? - } else { - builder.with_no_client_auth() - }; - - // Set ALPN protocols - apply_alpn_with_fallback( - &mut config.alpn_protocols, - &options.protocol_settings.alpn_protocols, - ); - - // Set session resumption - if let Some(session_store) = options.session_store { - use rustls::client::Resumption; - config.resumption = Resumption::store(session_store); - } - - Ok(config) -} - -/// Helper function - check if error is BlockingIOError -pub(super) fn is_blocking_io_error(err: &Py, vm: &VirtualMachine) -> bool { - err.fast_isinstance(vm.ctx.exceptions.blocking_io_error) -} - -// Socket I/O Helper Functions - -/// Send all bytes to socket, handling partial sends with blocking wait -/// -/// Loops until all bytes are sent. For blocking sockets, this will wait -/// until all data is sent. For non-blocking sockets, returns WantWrite -/// if no progress can be made. -/// Optional deadline parameter allows respecting a read deadline during flush. -fn send_all_bytes( - socket: &PySSLSocket, - buf: Vec, - vm: &VirtualMachine, - deadline: Option, -) -> SslResult<()> { - // First, flush any previously pending TLS data with deadline - socket - .flush_pending_tls_output(vm, deadline) - .map_err(SslError::Py)?; - - if buf.is_empty() { - return Ok(()); - } - - let mut sent_total = 0; - while sent_total < buf.len() { - // Check deadline before each send attempt - if let Some(dl) = deadline - && std::time::Instant::now() >= dl - { - socket - .pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(SslError::Timeout("The operation timed out".to_string())); - } - - // Wait for socket to be writable before sending - let timed_out = if let Some(dl) = deadline { - let now = std::time::Instant::now(); - if now >= dl { - socket - .pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(SslError::Timeout( - "The write operation timed out".to_string(), - )); - } - socket - .sock_wait_for_io_with_timeout(SockWaitKind::Write, Some(dl - now), vm) - .map_err(SslError::Py)? - } else { - socket - .sock_wait_for_io_impl(SockWaitKind::Write, vm) - .map_err(SslError::Py)? - }; - if timed_out { - socket - .pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(SslError::Timeout( - "The write operation timed out".to_string(), - )); - } - - match socket.sock_send(&buf[sent_total..], vm) { - Ok(result) => { - let sent: usize = result - .try_to_value::(vm) - .map_err(SslError::Py)? - .try_into() - .map_err(|_| SslError::Syscall("Invalid send return value".to_string()))?; - if sent == 0 { - // No progress - save unsent bytes to pending buffer - socket - .pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(SslError::WantWrite); - } - sent_total += sent; - } - Err(e) => { - if is_blocking_io_error(&e, vm) { - // Save unsent bytes to pending buffer - socket - .pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(SslError::WantWrite); - } - // For other errors, also save unsent bytes - socket - .pending_tls_output - .lock() - .extend_from_slice(&buf[sent_total..]); - return Err(SslError::Py(e)); - } - } - } - Ok(()) -} - -// Handshake Helper Functions - -/// Write TLS handshake data to socket/BIO -/// -/// Drains all pending TLS data from rustls and sends it to the peer. -/// Returns whether any progress was made. -fn handshake_write_loop( - conn: &mut Connection, - socket: &PySSLSocket, - force_initial_write: bool, - vm: &VirtualMachine, -) -> SslResult { - let mut made_progress = false; - - // Flush any previously pending TLS data before generating new output - // Must succeed before sending new data to maintain order - socket - .flush_pending_tls_output(vm, None) - .map_err(SslError::Py)?; - - while conn.wants_write() || force_initial_write { - if force_initial_write && !conn.wants_write() { - // No data to write on first iteration - break to avoid infinite loop - break; - } - - let mut buf = Vec::new(); - let written = conn - .write_tls(&mut buf as &mut dyn std::io::Write) - .map_err(SslError::Io)?; - - if written > 0 && !buf.is_empty() { - // Send all bytes to socket, handling partial sends - send_all_bytes(socket, buf, vm, None)?; - made_progress = true; - } else if written == 0 { - // No data written but wants_write is true - should not happen normally - // Break to avoid infinite loop - break; - } - - // Check if there's more to write - if !conn.wants_write() { - break; - } - } - - Ok(made_progress) -} - -/// Read at most one TLS record from the TCP socket. -/// -/// May return incomplete data but never returns more when completes a -/// previously incomplete TLS record. -/// -/// OpenSSL reads one TLS record at a time (no read-ahead by default). -/// Rustls, however, consumes all available TCP data when fed via read_tls(). -/// If a close_notify or other control record arrives alongside application -/// data, the eager read drains the TCP buffer, leaving the control record in -/// rustls's internal buffer where select() cannot see it. This causes -/// asyncore-based servers (which rely on select() for readability) to miss -/// the data and the peer times out. -/// -/// Fix: peek at the TCP buffer to find the first complete TLS record boundary -/// and recv() only that many bytes. Any remaining data stays in the kernel -/// buffer and remains visible to select(). -fn recv_at_most_one_tls_record( - socket: &PySSLSocket, - vm: &VirtualMachine, -) -> SslResult { - let bytes = socket.sock_recv_at_most_one_tls_record(vm).map_err(|e| { - if is_blocking_io_error(&e, vm) { - SslError::WantRead - } else { - SslError::Py(e) - } - })?; - if bytes.is_empty() { - Err(SslError::Eof) - } else { - Ok(bytes.into()) - } -} - -/// Read up to a single TLS record for post-handshake I/O while preserving the -/// SSL-vs-socket error precedence from the old sock_recv() path. -fn recv_at_most_one_tls_record_for_data( - conn: &mut Connection, - socket: &PySSLSocket, - vm: &VirtualMachine, -) -> SslResult { - match recv_at_most_one_tls_record(socket, vm) { - Ok(data) => Ok(data), - Err(SslError::Eof) => { - if let Err(rustls_err) = conn.process_new_packets() { - return Err(SslError::from_rustls(rustls_err)); - } - Ok(vm.ctx.new_bytes(vec![]).into()) - } - Err(SslError::Py(e)) => { - if let Err(rustls_err) = conn.process_new_packets() { - return Err(SslError::from_rustls(rustls_err)); - } - if is_connection_closed_error(&e, vm) { - return Err(SslError::Eof); - } - Err(SslError::Py(e)) - } - Err(e) => Err(e), - } -} - -fn handshake_read_data( - conn: &mut Connection, - socket: &PySSLSocket, - is_bio: bool, - is_server: bool, - vm: &VirtualMachine, -) -> SslResult<(bool, bool)> { - if !conn.wants_read() { - return Ok((false, false)); - } - - // SERVER-SPECIFIC: Check if this is before the SNI callback. - // sock_recv() may return only part of a TLS record, so keep capturing - // ClientHello fragments until process_new_packets() has produced a response. - let is_first_sni_read = is_server && socket.has_sni_callback() && socket.is_first_sni_read(); - - // Wait for data in socket mode - if !is_bio { - let timed_out = socket - .sock_wait_for_io_impl(SockWaitKind::Read, vm) - .map_err(SslError::Py)?; - - if timed_out { - // This should rarely happen now - only if socket itself has a timeout - // and we're waiting for required handshake data - return Err(SslError::Timeout("timed out".to_string())); - } - } - - let data_obj = if !is_bio { - // In socket mode, read one TLS record at a time to avoid consuming - // application data that may arrive alongside the final handshake - // record. This matches OpenSSL's default (no read-ahead) behaviour - // and keeps remaining data in the kernel buffer where select() can - // detect it. - recv_at_most_one_tls_record(socket, vm)? - } else { - match socket.sock_recv(SSL3_RT_MAX_PACKET_SIZE, vm) { - Ok(d) => d, - Err(e) => { - if is_blocking_io_error(&e, vm) { - return Err(SslError::WantRead); - } - if !conn.wants_write() && e.fast_isinstance(vm.ctx.exceptions.timeout_error) { - return Ok((false, false)); - } - return Err(SslError::Py(e)); - } - } - }; - - // SERVER-SPECIFIC: Save ClientHello fragments for potential connection recreation. - if is_first_sni_read { - // Extract bytes from PyObjectRef - use rustpython_vm::builtins::PyBytes; - if let Some(bytes_obj) = data_obj.downcast_ref::() { - socket.save_client_hello_from_bytes(bytes_obj.as_bytes()); - } - } - - // Feed data to rustls - ssl_read_tls_records(conn, data_obj, is_bio, vm)?; - - Ok((true, is_first_sni_read)) -} - -/// Handle handshake completion for server-side TLS 1.3 -/// -/// Tries to send NewSessionTicket in non-blocking mode to avoid deadlocks. -/// Returns true if handshake is complete and we should exit. -fn handle_handshake_complete( - conn: &mut Connection, - socket: &PySSLSocket, - _is_server: bool, - vm: &VirtualMachine, -) -> SslResult { - if conn.is_handshaking() { - return Ok(false); // Not complete yet - } - - // Handshake is complete! - // - // Different behavior for BIO mode vs socket mode: - // - // BIO mode (CPython-compatible): - // - Python code calls outgoing.read() to get pending data - // - We just return here and let Python handle the data - // - // Socket mode (rustls-specific): - // - OpenSSL automatically writes to socket in SSL_do_handshake() - // - We must explicitly call write_tls() to send pending data - // - Without this, client hangs waiting for server's NewSessionTicket - - if socket.is_bio_mode() { - // BIO mode: Write pending data to outgoing BIO (one-time drain) - // Python's ssl_io_loop will read from outgoing BIO - if conn.wants_write() { - // Call write_tls ONCE to drain pending data - // Do NOT loop on wants_write() - avoid infinite loop/deadlock - let tls_data = ssl_write_tls_records(conn)?; - if !tls_data.is_empty() { - send_all_bytes(socket, tls_data, vm, None)?; - } - - // IMPORTANT: Don't check wants_write() again! - // Python's ssl_io_loop will call do_handshake() again if needed - } - } else if conn.wants_write() { - // Send all pending data (e.g., TLS 1.3 NewSessionTicket) to socket - // Must drain ALL rustls buffer - don't break on WantWrite - while conn.wants_write() { - let tls_data = ssl_write_tls_records(conn)?; - if tls_data.is_empty() { - break; - } - match send_all_bytes(socket, tls_data, vm, None) { - Ok(()) => {} - Err(SslError::WantWrite) => { - // Socket buffer full, data saved to pending_tls_output - // Flush pending and continue draining rustls buffer - socket - .blocking_flush_all_pending(vm) - .map_err(SslError::Py)?; - } - Err(e) => return Err(e), - } - } - } - - // CRITICAL: Ensure all pending TLS data is sent before returning - // TLS 1.3 Finished must reach server before handshake is considered complete - // Without this, server may not process application data - if !socket.is_bio_mode() { - // Flush pending_tls_output to ensure all TLS data reaches the server - socket - .blocking_flush_all_pending(vm) - .map_err(SslError::Py)?; - } - - Ok(true) -} - -/// Try to read plaintext data from TLS connection buffer -/// -/// 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> { - 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) - } - } -} - -/// Equivalent to OpenSSL's SSL_do_handshake() -/// -/// Performs TLS handshake by exchanging data with the peer until completion. -/// This abstracts away the low-level rustls read_tls/write_tls loop. -/// -/// = SSL_do_handshake() -pub(super) fn ssl_do_handshake( - conn: &mut Connection, - socket: &PySSLSocket, - vm: &VirtualMachine, -) -> SslResult<()> { - // Check if handshake is already done - if !conn.is_handshaking() { - return Ok(()); - } - - let is_bio = socket.is_bio_mode(); - let is_server = matches!(conn, Connection::Server(_)); - let mut first_iteration = true; // Track if this is the first loop iteration - loop { - let mut made_progress = false; - - // IMPORTANT: In BIO mode, force initial write even if wants_write() is false - // rustls requires write_tls() to be called to generate ClientHello/ServerHello - let force_initial_write = is_bio && first_iteration; - - // Write TLS handshake data to socket/BIO - let write_progress = handshake_write_loop(conn, socket, force_initial_write, vm)?; - made_progress |= write_progress; - - // Read TLS handshake data from socket/BIO - let (read_progress, is_first_sni_read) = - handshake_read_data(conn, socket, is_bio, is_server, vm)?; - made_progress |= read_progress; - - // Process TLS packets (state machine) - if let Err(e) = conn.process_new_packets() { - // Send close_notify on error - if !is_bio { - conn.send_close_notify(); - // Flush any pending TLS data before sending close_notify - let _ = socket.flush_pending_tls_output(vm, None); - // Actually send the close_notify alert using send_all_bytes - // for proper partial send handling and retry logic - if let Ok(alert_data) = ssl_write_tls_records(conn) - && !alert_data.is_empty() - { - let _ = send_all_bytes(socket, alert_data, vm, None); - } - } - - // InvalidMessage during handshake means non-TLS data was received - // before the handshake completed (e.g., HTTP request to TLS server) - if matches!(e, rustls::Error::InvalidMessage(_)) { - return Err(SslError::PreauthData); - } - - // Certificate verification errors are already handled by from_rustls - - return Err(SslError::from_rustls(e)); - } - - // SERVER-SPECIFIC: Check SNI callback after processing packets. - // A partial TLS record can be read without producing any handshake - // response. Wait until rustls has processed a complete ClientHello. - if is_server && is_first_sni_read && socket.has_sni_callback() && conn.wants_write() { - // IMPORTANT: Do NOT call the callback here! - // The connection lock is still held, which would cause deadlock. - // Return SniCallbackRestart to signal do_handshake to: - // 1. Drop conn_guard - // 2. Call the callback (with Some(name) or None) - // 3. Restart handshake - return Err(SslError::SniCallbackRestart); - } - - // Check if handshake is complete and handle post-handshake processing - // CRITICAL: We do NOT check wants_read() - this matches CPython/OpenSSL behavior! - if handle_handshake_complete(conn, socket, is_server, vm)? { - return Ok(()); - } - - // In BIO mode, stop after one iteration - if is_bio { - // Before returning WANT error, write any pending TLS data to BIO - // This is critical: if wants_write is true after process_new_packets, - // we need to write that data to the outgoing BIO before returning - if conn.wants_write() { - // Write all pending TLS data to outgoing BIO - loop { - let mut buf = vec![0u8; SSL3_RT_MAX_PACKET_SIZE]; - let n = match conn.write_tls(&mut buf.as_mut_slice()) { - Ok(n) => n, - Err(_) => break, - }; - if n == 0 { - break; - } - // Send to outgoing BIO - send_all_bytes(socket, buf[..n].to_vec(), vm, None)?; - // Check if there's more to write - if !conn.wants_write() { - break; - } - } - // After writing, check if we still want more - // If all data was written, wants_write may now be false - if conn.wants_write() { - // Still need more - return WANT_WRITE - return Err(SslError::WantWrite); - } - // Otherwise fall through to check wants_read - } - - // Check if we need to read - if conn.wants_read() { - return Err(SslError::WantRead); - } - break; - } - - // Mark that we've completed the first iteration - first_iteration = false; - - // Improved loop termination logic: - // Continue looping if: - // 1. Rustls wants more I/O (wants_read or wants_write), OR - // 2. We made progress in this iteration - // - // This is more robust than just checking made_progress, because: - // - Rustls may need multiple iterations to process TLS state machine - // - Network delays may cause temporary "no progress" situations - // - wants_read/wants_write accurately reflect Rustls internal state - let should_continue = conn.wants_read() || conn.wants_write() || made_progress; - - if !should_continue { - break; - } - } - - // If we exit the loop without completing handshake, return appropriate error - if conn.is_handshaking() { - // For non-blocking sockets, return WantRead/WantWrite to signal caller - // should retry when socket is ready. This matches OpenSSL behavior. - if conn.wants_write() { - return Err(SslError::WantWrite); - } - if conn.wants_read() { - return Err(SslError::WantRead); - } - // Neither wants_read nor wants_write - this is a real error - Err(SslError::Syscall( - "SSL handshake failed: incomplete handshake".to_string(), - )) - } else { - // Handshake completed successfully (shouldn't reach here normally) - Ok(()) - } -} - -/// Equivalent to OpenSSL's SSL_read() -/// -/// Reads application data from TLS connection. -/// Automatically handles TLS record I/O as needed. -/// -/// = SSL_read_ex() -pub(super) fn ssl_read( - conn: &mut Connection, - buf: &mut [u8], - socket: &PySSLSocket, - vm: &VirtualMachine, -) -> SslResult { - let is_bio = socket.is_bio_mode(); - - // Get socket timeout and calculate deadline (= _PyDeadline_Init) - let deadline = if !is_bio { - match socket.get_socket_timeout(vm).map_err(SslError::Py)? { - Some(timeout) if !timeout.is_zero() => Some(std::time::Instant::now() + timeout), - _ => None, // None = blocking (no deadline), Some(0) = non-blocking (handled below) - } - } else { - None // BIO mode has no deadline - }; - - // CRITICAL: Flush any pending TLS output before reading - // This ensures data from previous write() calls is sent before we wait for response. - // Without this, write() may leave data in pending_tls_output (if socket buffer was full), - // and read() would timeout waiting for a response that the server never received. - if !is_bio { - socket - .flush_pending_tls_output(vm, deadline) - .map_err(SslError::Py)?; - } - - // Loop to handle TLS records and post-handshake messages - // Matches SSL_read behavior which loops until data is available - // - CPython uses OpenSSL's SSL_read which loops on SSL_ERROR_WANT_READ/WANT_WRITE - // - We use rustls which requires manual read_tls/process_new_packets loop - // - No iteration limit: relies on deadline and blocking I/O - // - Blocking sockets: sock_select() and recv() wait at kernel level (no CPU busy-wait) - // - Non-blocking sockets: immediate return on first WantRead - // - Deadline prevents timeout issues - - loop { - // Check deadline - if let Some(deadline) = deadline - && std::time::Instant::now() >= deadline - { - // Timeout expired - return Err(SslError::Timeout( - "The read operation timed out".to_string(), - )); - } - // Check if we need to read more TLS records BEFORE trying plaintext read - // This ensures we don't miss data that's already been processed - let needs_more_tls = conn.wants_read(); - - // Try to read plaintext from rustls buffer - if let Some(n) = try_read_plaintext(conn, buf)? { - if n == 0 { - // EOF from TLS - close_notify received - // Return ZeroReturn so Python raises SSLZeroReturnError - return Err(SslError::ZeroReturn); - } - return Ok(n); - } - - // No plaintext available and rustls doesn't want to read more TLS records - if !needs_more_tls { - // Check if connection needs to write data first (e.g., TLS key update, renegotiation) - // This mirrors the handshake logic which checks both wants_read() and wants_write() - if conn.wants_write() && !is_bio { - // Check deadline BEFORE attempting flush - if let Some(deadline) = deadline - && std::time::Instant::now() >= deadline - { - return Err(SslError::Timeout( - "The read operation timed out".to_string(), - )); - } - - // Flush pending TLS data before continuing - // CRITICAL: Pass deadline so flush respects read timeout - let tls_data = ssl_write_tls_records(conn)?; - if !tls_data.is_empty() { - // Use best-effort send - don't fail READ just because WRITE couldn't complete - match send_all_bytes(socket, tls_data, vm, deadline) { - Ok(()) => {} - Err(SslError::WantWrite) => { - // Socket buffer full - acceptable during READ operation - // Pending data will be sent on next write/read call - } - Err(SslError::Timeout(_)) => { - // Timeout during flush is acceptable during READ - // Pending data stays buffered for next operation - } - Err(e) => return Err(e), - } - } - - // Check deadline AFTER flush attempt - if let Some(deadline) = deadline - && std::time::Instant::now() >= deadline - { - return Err(SslError::Timeout( - "The read operation timed out".to_string(), - )); - } - - // After flushing, rustls may want to read again - continue loop - continue; - } - - // BIO mode: check for EOF - if is_bio && let Some(bio_obj) = socket.incoming_bio() { - let is_eof = bio_obj - .get_attr("eof", vm) - .and_then(|v| v.try_into_value::(vm)) - .unwrap_or(false); - if is_eof { - return Err(SslError::Eof); - } - } - - // For non-blocking sockets, return WantRead so caller can poll and retry. - // For blocking sockets (or sockets with timeout), wait for more data. - if !is_bio { - let timeout = socket.get_socket_timeout(vm).map_err(SslError::Py)?; - if let Some(t) = timeout - && t.is_zero() - { - // Non-blocking socket: check if peer has closed before returning WantRead - // If close_notify was received, we should return ZeroReturn (EOF), not WantRead - // This is critical for asyncore-based applications that rely on recv() returning - // 0 or raising SSL_ERROR_ZERO_RETURN to detect connection close. - let io_state = conn.process_new_packets().map_err(SslError::from_rustls)?; - if io_state.peer_has_closed() { - return Err(SslError::ZeroReturn); - } - // Non-blocking socket: return immediately - return Err(SslError::WantRead); - } - // Blocking socket or socket with timeout: try to read more data from socket. - // Even though rustls says it doesn't want to read, more TLS records may arrive. - // Use single-record reading to avoid consuming close_notify alongside data. - let data = recv_at_most_one_tls_record_for_data(conn, socket, vm)?; - - let bytes_read = data - .clone() - .try_into_value::(vm) - .map_or(0, |b| b.as_bytes().len()); - - if bytes_read == 0 { - // No more data available - check if this is clean shutdown or unexpected EOF - // If close_notify was already received, return ZeroReturn (clean closure) - // Otherwise, return Eof (unexpected EOF) - let io_state = conn.process_new_packets().map_err(SslError::from_rustls)?; - if io_state.peer_has_closed() { - return Err(SslError::ZeroReturn); - } - return Err(SslError::Eof); - } - - // Feed data to rustls and process - ssl_read_tls_records(conn, data, false, vm)?; - conn.process_new_packets().map_err(SslError::from_rustls)?; - - // Continue loop to try reading plaintext - continue; - } - - return Err(SslError::WantRead); - } - - // Read and process TLS records - match ssl_ensure_data_available(conn, socket, vm) { - Ok(_bytes_read) => { - // Successfully read and processed TLS data - // Continue loop to try reading plaintext - } - Err(e) => { - // Other errors - check for buffered plaintext before propagating - match try_read_plaintext(conn, buf)? { - Some(n) if n > 0 => { - // Have buffered plaintext - return it successfully - return Ok(n); - } - _ => { - // No buffered data - propagate the error - return Err(e); - } - } - } - } - } -} - -/// Equivalent to OpenSSL's SSL_write() -/// -/// Writes application data to TLS connection. -/// Automatically handles TLS record I/O as needed. -/// -/// = SSL_write_ex() -pub(super) fn ssl_write( - conn: &mut Connection, - data: &[u8], - socket: &PySSLSocket, - vm: &VirtualMachine, -) -> SslResult { - if data.is_empty() { - return Ok(0); - } - - let is_bio = socket.is_bio_mode(); - - // Get socket timeout and calculate deadline (= _PyDeadline_Init) - let deadline = if !is_bio { - match socket.get_socket_timeout(vm).map_err(SslError::Py)? { - Some(timeout) if !timeout.is_zero() => Some(std::time::Instant::now() + timeout), - _ => None, - } - } else { - None - }; - - // Flush any pending TLS output before writing new data - if !is_bio { - socket - .flush_pending_tls_output(vm, deadline) - .map_err(SslError::Py)?; - } - - // 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(); - - // Only write plaintext if not already buffered - // Track how much we wrote for partial write handling - let mut bytes_written_to_rustls = 0usize; - - if already_buffered == 0 { - // Write plaintext to rustls (= SSL_write_ex internal buffer write) - bytes_written_to_rustls = { - 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); - } - return Err(SslError::Syscall("Write failed: buffer full".to_string())); - } - Ok(n) => n, - Err(e) => { - if is_bio { - // In BIO mode, treat write errors as WantWrite - return Err(SslError::WantWrite); - } - return Err(SslError::Syscall(format!("Write failed: {e}"))); - } - } - }; - // Mark data as buffered (only the portion we actually wrote) - *socket.write_buffered_len.lock() = 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; - return Err(SslError::Ssl("bad write retry".to_string())); - } - // else: already_buffered == data.len(), this is a valid retry - - // Loop to send TLS records, handling WANT_READ/WANT_WRITE - // Matches CPython's do-while loop on SSL_ERROR_WANT_READ/WANT_WRITE - loop { - // Check deadline - if let Some(dl) = deadline - && std::time::Instant::now() >= dl - { - return Err(SslError::Timeout( - "The write operation timed out".to_string(), - )); - } - - // Check if rustls has TLS data to send - if !conn.wants_write() { - // All TLS data sent successfully - break; - } - - // Get TLS records from rustls - let tls_data = ssl_write_tls_records(conn)?; - if tls_data.is_empty() { - break; - } - - // Send TLS data to socket - match send_all_bytes(socket, tls_data, vm, deadline) { - Ok(()) => { - // Successfully sent, continue loop to check for more data - } - Err(SslError::WantWrite) => { - // Non-blocking socket would block - return WANT_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; - return Ok(bytes_written_to_rustls); - } - // Keep write_buffered_len set so we don't re-buffer on retry - return Err(SslError::WantWrite); - } - Err(SslError::WantRead) => { - // Need to read before write can complete (e.g., renegotiation) - 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; - return Ok(bytes_written_to_rustls); - } - // Keep write_buffered_len set so we don't re-buffer on retry - return Err(SslError::WantRead); - } - // For socket mode, try to read TLS data - let recv_result = recv_at_most_one_tls_record_for_data(conn, socket, vm)?; - ssl_read_tls_records(conn, recv_result, false, vm)?; - conn.process_new_packets().map_err(SslError::from_rustls)?; - // Continue loop - } - 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; - return Ok(bytes_written_to_rustls); - } - // Preserve buffered state so retry doesn't duplicate data - // (send_all_bytes saved unsent TLS bytes to pending_tls_output) - return Err(e); - } - Err(e) => { - // Clear buffer state on error - *socket.write_buffered_len.lock() = 0; - return Err(e); - } - } - } - - // Final flush to ensure all data is sent - if !is_bio { - socket - .flush_pending_tls_output(vm, deadline) - .map_err(SslError::Py)?; - } - - // Determine how many bytes we actually wrote - let actual_written = if bytes_written_to_rustls > 0 { - // Fresh write: return what we wrote to rustls - bytes_written_to_rustls - } else if already_buffered > 0 { - // Retry of previous write: return the full buffered amount - already_buffered - } else { - data.len() - }; - - // Write completed successfully - clear buffer state - *socket.write_buffered_len.lock() = 0; - - Ok(actual_written) -} - -// 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> { - 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()) } -} - -/// Read TLS records from socket to rustls -fn ssl_read_tls_records( - conn: &mut Connection, - data: PyObjectRef, - is_bio: bool, - vm: &VirtualMachine, -) -> SslResult<()> { - // Convert PyObject to bytes-like (supports bytes, bytearray, etc.) - let bytes = ArgBytesLike::try_from_object(vm, data) - .map_err(|_| SslError::Syscall("Expected bytes-like object".to_string()))?; - - 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)), - } - } - - // 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(()) -} - -/// Check if an exception is a connection closed error -/// In SSL context, these errors indicate unexpected connection termination without proper TLS shutdown -fn is_connection_closed_error(exc: &Py, vm: &VirtualMachine) -> bool { - use rustpython_vm::stdlib::errno::errors; - - // Check for ConnectionAbortedError, ConnectionResetError (Python exception types) - if exc.fast_isinstance(vm.ctx.exceptions.connection_aborted_error) - || exc.fast_isinstance(vm.ctx.exceptions.connection_reset_error) - { - return true; - } - - // Also check OSError with specific errno values (ECONNABORTED, ECONNRESET) - if exc.fast_isinstance(vm.ctx.exceptions.os_error) - && let Ok(errno) = exc.as_object().get_attr("errno", vm) - && let Ok(errno_int) = errno.try_int(vm) - && let Ok(errno_val) = errno_int.try_to_primitive::(vm) - { - return errno_val == errors::ECONNABORTED || errno_val == errors::ECONNRESET; - } - false -} - -/// Ensure TLS data is available for reading -/// Returns the number of bytes read from the socket -fn ssl_ensure_data_available( - conn: &mut Connection, - socket: &PySSLSocket, - vm: &VirtualMachine, -) -> SslResult { - // Unlike OpenSSL's SSL_read, rustls requires explicit I/O - if conn.wants_read() { - let is_bio = socket.is_bio_mode(); - - // For non-BIO mode (regular sockets), check if socket is ready first - // PERFORMANCE OPTIMIZATION: Only use select for sockets with timeout - // - Blocking sockets (timeout=None): Skip select, recv() will block naturally - // - Timeout sockets: Use select to enforce timeout - // - Non-blocking sockets: Skip select, recv() will return EAGAIN immediately - if !is_bio { - let timeout = socket.get_socket_timeout(vm).map_err(SslError::Py)?; - - // Only use select if socket has a positive timeout - if let Some(t) = timeout - && !t.is_zero() - { - // Socket has timeout - use select to enforce it - let timed_out = socket - .sock_wait_for_io_impl(SockWaitKind::Read, vm) - .map_err(SslError::Py)?; - if timed_out { - // Socket not ready within timeout - raise socket.timeout - return Err(SslError::Timeout( - "The read operation timed out".to_string(), - )); - } - } - // else: non-blocking socket (timeout=0) or blocking socket (timeout=None) - skip select - } - - // Read one TLS record at a time for non-BIO sockets (matching - // OpenSSL's default no-read-ahead behaviour). This prevents - // consuming a close_notify that arrives alongside application data, - // keeping it in the kernel buffer where select() can detect it. - let data = if !is_bio { - recv_at_most_one_tls_record_for_data(conn, socket, vm)? - } else { - match socket.sock_recv(SSL3_RT_MAX_PACKET_SIZE, vm) { - Ok(data) => data, - Err(e) => { - if is_blocking_io_error(&e, vm) { - return Err(SslError::WantRead); - } - if let Err(rustls_err) = conn.process_new_packets() { - return Err(SslError::from_rustls(rustls_err)); - } - if is_connection_closed_error(&e, vm) { - return Err(SslError::Eof); - } - return Err(SslError::Py(e)); - } - } - }; - - // Get the size of received data - let bytes_read = data - .clone() - .try_into_value::(vm) - .map_or(0, |b| b.as_bytes().len()); - - // Check if BIO has EOF set (incoming BIO closed) - let is_eof = if is_bio { - // Check incoming BIO's eof property - if let Some(bio_obj) = socket.incoming_bio() { - bio_obj - .get_attr("eof", vm) - .and_then(|v| v.try_into_value::(vm)) - .unwrap_or(false) - } else { - false - } - } else { - false - }; - - // If BIO EOF is set and no data available, treat as connection EOF - if is_eof && bytes_read == 0 { - return Err(SslError::Eof); - } - - // Feed data to rustls and process packets - ssl_read_tls_records(conn, data, is_bio, vm)?; - - // Process any packets we successfully read - conn.process_new_packets().map_err(SslError::from_rustls)?; - - Ok(bytes_read) - } else { - // No data to read - Ok(0) - } -} - -// Multi-Certificate Resolver for RSA/ECC Support - -/// Multi-certificate resolver that selects appropriate certificate based on client capabilities -/// -/// This resolver implements OpenSSL's behavior of supporting multiple certificate/key pairs -/// (e.g., one RSA and one ECC) and selecting the appropriate one based on the client's -/// supported signature algorithms during the TLS handshake. -/// -/// OpenSSL's SSL_CTX_use_certificate_chain_file can be called multiple -/// times to add different certificate types, and OpenSSL automatically selects the best one. -#[derive(Debug)] -pub(super) struct MultiCertResolver { - cert_keys: Vec>, -} - -impl MultiCertResolver { - /// Create a new multi-certificate resolver - pub(super) fn new(cert_keys: Vec>) -> Self { - Self { cert_keys } - } -} - -impl ResolvesServerCert for MultiCertResolver { - fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option> { - // Get the signature schemes supported by the client - let client_schemes = client_hello.signature_schemes(); - - // Try to find a certificate that matches the client's signature schemes - for cert_key in &self.cert_keys { - // Check if this certificate's signing key is compatible with any of the - // client's supported signature schemes - if let Some(_scheme) = cert_key.key.choose_scheme(client_schemes) { - return Some(cert_key.clone()); - } - } - - // If no perfect match, return the first certificate as fallback - // (This matches OpenSSL's behavior of using the first loaded cert if negotiation fails) - self.cert_keys.first().cloned() - } -} - -// Helper Functions for OpenSSL Compatibility: - -/// Normalize cipher suite name for OpenSSL compatibility -/// -/// Converts rustls cipher names to OpenSSL format: -/// - TLS_AES_256_GCM_SHA384 → AES256-GCM-SHA384 -/// - Replaces "AES-256" with "AES256" and "AES-128" with "AES128" -pub(super) fn normalize_cipher_name(rustls_name: &str) -> String { - rustls_name - .strip_prefix("TLS_") - .unwrap_or(rustls_name) - .replace("_WITH_", "_") - .replace('_', "-") - .replace("AES-256", "AES256") - .replace("AES-128", "AES128") -} - -/// Get cipher key size in bits from cipher suite name -/// -/// Returns: -/// - 256 for AES-256 and ChaCha20 -/// - 128 for AES-128 -/// - 0 for unknown ciphers -pub(super) fn get_cipher_key_bits(cipher_name: &str) -> i32 { - if cipher_name.contains("256") || cipher_name.contains("CHACHA20") { - 256 - } else if cipher_name.contains("128") { - 128 - } else { - 0 - } -} - -/// Get encryption algorithm description from cipher name -/// -/// Returns human-readable encryption description for OpenSSL compatibility -pub(super) fn get_cipher_encryption_desc(cipher_name: &str) -> &'static str { - if cipher_name.contains("AES256") { - "AESGCM(256)" - } else if cipher_name.contains("AES128") { - "AESGCM(128)" - } else if cipher_name.contains("CHACHA20") { - "CHACHA20-POLY1305(256)" - } else { - "Unknown" - } -} - -/// Normalize rustls cipher suite name to IANA standard format -/// -/// Converts rustls Debug format names to IANA standard: -/// - "TLS13_AES_256_GCM_SHA384" -> "TLS_AES_256_GCM_SHA384" -/// - Other names remain unchanged -pub(super) fn normalize_rustls_cipher_name(rustls_name: &str) -> String { - if rustls_name.starts_with("TLS13_") { - rustls_name.replace("TLS13_", "TLS_") - } else { - rustls_name.to_string() - } -} - -/// Convert rustls protocol version to string representation -/// -/// Returns the TLS version string -/// - TLSv1.2, TLSv1.3, or "Unknown" -pub(super) fn get_protocol_version_str(version: &rustls::SupportedProtocolVersion) -> &'static str { - match version.version { - rustls::ProtocolVersion::TLSv1_2 => "TLSv1.2", - rustls::ProtocolVersion::TLSv1_3 => "TLSv1.3", - _ => "Unknown", - } -} - -/// Cipher suite information -/// -/// Contains all relevant cipher information extracted from a rustls CipherSuite -pub(super) struct CipherInfo { - /// IANA standard cipher name (e.g., "TLS_AES_256_GCM_SHA384") - pub name: String, - /// TLS protocol version (e.g., "TLSv1.2", "TLSv1.3") - pub protocol: &'static str, - /// Key size in bits (e.g., 128, 256) - pub bits: i32, -} - -/// Extract cipher information from a rustls CipherSuite -/// -/// This consolidates the common cipher extraction logic used across -/// get_ciphers(), cipher(), and shared_ciphers() methods. -pub(super) fn extract_cipher_info(suite: &rustls::SupportedCipherSuite) -> CipherInfo { - let rustls_name = format!("{:?}", suite.suite()); - let name = normalize_rustls_cipher_name(&rustls_name); - let protocol = get_protocol_version_str(suite.version()); - let bits = get_cipher_key_bits(&name); - - CipherInfo { - name, - protocol, - bits, - } -} - -/// Convert curve name to rustls key exchange group -/// -/// Maps OpenSSL curve names (e.g., "prime256v1", "secp384r1") to rustls KxGroups. -/// Returns an error if the curve is not supported by rustls. -pub(super) fn curve_name_to_kx_group( - curve: &str, -) -> Result, String> { - // Get the default crypto provider's key exchange groups - let all_groups = CryptoExt::get_ext().all_kx_or_default(); - - match curve { - // P-256 (also known as secp256r1 or prime256v1) - "prime256v1" | "secp256r1" => { - // Find SECP256R1 in the provider's groups - all_groups - .iter() - .find(|g| g.name() == rustls::NamedGroup::secp256r1) - .map(|g| vec![*g]) - .ok_or_else(|| "secp256r1 not supported by crypto provider".to_owned()) - } - // P-384 (also known as secp384r1 or prime384v1) - "secp384r1" | "prime384v1" => all_groups - .iter() - .find(|g| g.name() == rustls::NamedGroup::secp384r1) - .map(|g| vec![*g]) - .ok_or_else(|| "secp384r1 not supported by crypto provider".to_owned()), - // X25519 - "X25519" | "x25519" => all_groups - .iter() - .find(|g| g.name() == rustls::NamedGroup::X25519) - .map(|g| vec![*g]) - .ok_or_else(|| "X25519 not supported by crypto provider".to_owned()), - // P-521 (also known as secp521r1 or prime521v1) - "prime521v1" | "secp521r1" => all_groups - .iter() - .find(|g| g.name() == rustls::NamedGroup::secp521r1) - .map(|g| vec![*g]) - .ok_or_else(|| "secp521r1 not supported by crypto provider".to_owned()), - // X448 - "X448" | "x448" => all_groups - .iter() - .find(|g| g.name() == rustls::NamedGroup::X448) - .map(|g| vec![*g]) - .ok_or_else(|| "X448 not supported by crypto provider".to_owned()), - _ => Err(format!("unknown curve name '{curve}'")), - } -} diff --git a/crates/stdlib/src/ssl/error.rs b/crates/stdlib/src/ssl/error.rs index 4e5def82bd5..e5c1da29fa2 100644 --- a/crates/stdlib/src/ssl/error.rs +++ b/crates/stdlib/src/ssl/error.rs @@ -136,19 +136,4 @@ pub(crate) mod ssl_error { "TLS/SSL connection has been closed (EOF)", ) } - - #[cfg_attr( - all(feature = "ssl-openssl", not(feature = "ssl-rustls")), - expect(dead_code) - )] - pub(crate) fn create_ssl_syscall_error( - vm: &VirtualMachine, - msg: impl Into, - ) -> PyRef { - vm.new_os_subtype_error( - PySSLSyscallError::class(&vm.ctx).to_owned(), - Some(SSL_ERROR_SYSCALL), - msg.into(), - ) - } } diff --git a/crates/stdlib/src/ssl/oid.rs b/crates/stdlib/src/ssl/oid.rs deleted file mode 100644 index ca059ff5000..00000000000 --- a/crates/stdlib/src/ssl/oid.rs +++ /dev/null @@ -1,465 +0,0 @@ -// spell-checker: disable - -//! OID (Object Identifier) management for SSL/TLS -//! -//! This module provides OID lookup functionality compatible with CPython's ssl module. -//! It uses oid-registry crate for well-known OIDs while maintaining NID (Numerical Identifier) -//! mappings for CPython compatibility. - -use oid_registry::asn1_rs::Oid; -use std::collections::HashMap; - -/// OID entry with openssl-compatible metadata -#[derive(Debug, Clone)] -pub(super) struct OidEntry { - /// NID (OpenSSL Numerical Identifier) - must match CPython/OpenSSL values - pub nid: i32, - /// Short name (e.g., "CN", "serverAuth") - pub short_name: &'static str, - /// Long name/description (e.g., "commonName", "TLS Web Server Authentication") - pub long_name: &'static str, - /// OID reference (static or dynamic) - pub oid: OidRef, -} - -/// OID reference - either from oid-registry or runtime-created -#[derive(Debug, Clone)] -pub(super) enum OidRef { - /// Static OID from oid-registry crate (stored as value) - Static(Oid<'static>), - /// OID string (for OIDs not in oid-registry) - parsed on demand - String(&'static str), -} - -impl OidEntry { - /// Create entry from oid-registry static constant - pub(super) fn from_static( - nid: i32, - short_name: &'static str, - long_name: &'static str, - oid: &Oid<'static>, - ) -> Self { - Self { - nid, - short_name, - long_name, - oid: OidRef::Static(oid.clone()), - } - } - - /// Create entry from OID string (for OIDs not in oid-registry) - pub(super) const fn from_string( - nid: i32, - short_name: &'static str, - long_name: &'static str, - oid_str: &'static str, - ) -> Self { - Self { - nid, - short_name, - long_name, - oid: OidRef::String(oid_str), - } - } - - /// Get OID as string (e.g., "2.5.4.3") - pub(super) fn oid_string(&self) -> String { - match &self.oid { - OidRef::Static(oid) => oid.to_id_string(), - OidRef::String(s) => s.to_string(), - } - } -} - -/// OID table with multiple indices for fast lookup -pub(super) struct OidTable { - /// All entries - entries: Vec, - /// NID -> index mapping - nid_to_idx: HashMap, - /// Short name -> index mapping - short_name_to_idx: HashMap<&'static str, usize>, - /// Long name -> index mapping (case-insensitive) - long_name_to_idx: HashMap, - /// OID string -> index mapping - oid_str_to_idx: HashMap, -} - -impl OidTable { - fn build() -> Self { - let entries = build_oid_entries(); - let mut nid_to_idx = HashMap::with_capacity(entries.len()); - let mut short_name_to_idx = HashMap::with_capacity(entries.len()); - let mut long_name_to_idx = HashMap::with_capacity(entries.len()); - let mut oid_str_to_idx = HashMap::with_capacity(entries.len()); - - for (idx, entry) in entries.iter().enumerate() { - nid_to_idx.insert(entry.nid, idx); - short_name_to_idx.insert(entry.short_name, idx); - long_name_to_idx.insert(entry.long_name.to_lowercase(), idx); - oid_str_to_idx.insert(entry.oid_string(), idx); - } - - Self { - entries, - nid_to_idx, - short_name_to_idx, - long_name_to_idx, - oid_str_to_idx, - } - } - - pub(super) 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> { - self.oid_str_to_idx - .get(oid_str) - .map(|&idx| &self.entries[idx]) - } - - pub(super) fn find_by_name(&self, name: &str) -> Option<&OidEntry> { - // Try short name first (exact match) - self.short_name_to_idx - .get(name) - .or_else(|| { - // Try long name (case-insensitive) - self.long_name_to_idx.get(&name.to_lowercase()) - }) - .map(|&idx| &self.entries[idx]) - } -} - -/// Global OID table -static OID_TABLE: rustpython_common::lock::LazyLock = - rustpython_common::lock::LazyLock::new(OidTable::build); - -/// Macro to define OID entry using oid-registry constant -macro_rules! oid_static { - ($nid:expr, $short:expr, $long:expr, $oid_const:path) => { - OidEntry::from_static($nid, $short, $long, &$oid_const) - }; -} - -/// Macro to define OID entry from string -macro_rules! oid_string { - ($nid:expr, $short:expr, $long:expr, $oid_str:expr) => { - OidEntry::from_string($nid, $short, $long, $oid_str) - }; -} - -/// Build the complete OID table -fn build_oid_entries() -> Vec { - vec![ - // Priority 1: X.509 DN Attributes (OpenSSL NID values) - // These NIDs MUST match OpenSSL for CPython compatibility - oid_static!(13, "CN", "commonName", oid_registry::OID_X509_COMMON_NAME), - oid_static!(14, "C", "countryName", oid_registry::OID_X509_COUNTRY_NAME), - oid_static!( - 15, - "L", - "localityName", - oid_registry::OID_X509_LOCALITY_NAME - ), - oid_static!( - 16, - "ST", - "stateOrProvinceName", - oid_registry::OID_X509_STATE_OR_PROVINCE_NAME - ), - oid_static!( - 17, - "O", - "organizationName", - oid_registry::OID_X509_ORGANIZATION_NAME - ), - oid_static!( - 18, - "OU", - "organizationalUnitName", - oid_registry::OID_X509_ORGANIZATIONAL_UNIT - ), - oid_static!(41, "name", "name", oid_registry::OID_X509_NAME), - oid_static!(42, "GN", "givenName", oid_registry::OID_X509_GIVEN_NAME), - oid_static!(43, "initials", "initials", oid_registry::OID_X509_INITIALS), - oid_static!( - 4, - "serialNumber", - "serialNumber", - oid_registry::OID_X509_SERIALNUMBER - ), - oid_static!(100, "surname", "surname", oid_registry::OID_X509_SURNAME), - // emailAddress is special - it's in PKCS#9, not X.509 - oid_static!( - 48, - "emailAddress", - "emailAddress", - oid_registry::OID_PKCS9_EMAIL_ADDRESS - ), - // Priority 2: X.509 Extensions (Critical ones) - oid_static!( - 82, - "subjectKeyIdentifier", - "X509v3 Subject Key Identifier", - oid_registry::OID_X509_EXT_SUBJECT_KEY_IDENTIFIER - ), - oid_static!( - 83, - "keyUsage", - "X509v3 Key Usage", - oid_registry::OID_X509_EXT_KEY_USAGE - ), - oid_static!( - 85, - "subjectAltName", - "X509v3 Subject Alternative Name", - oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME - ), - oid_static!( - 86, - "issuerAltName", - "X509v3 Issuer Alternative Name", - oid_registry::OID_X509_EXT_ISSUER_ALT_NAME - ), - oid_static!( - 87, - "basicConstraints", - "X509v3 Basic Constraints", - oid_registry::OID_X509_EXT_BASIC_CONSTRAINTS - ), - oid_static!( - 88, - "crlNumber", - "X509v3 CRL Number", - oid_registry::OID_X509_EXT_CRL_NUMBER - ), - oid_static!( - 90, - "authorityKeyIdentifier", - "X509v3 Authority Key Identifier", - oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER - ), - oid_static!( - 126, - "extendedKeyUsage", - "X509v3 Extended Key Usage", - oid_registry::OID_X509_EXT_EXTENDED_KEY_USAGE - ), - oid_static!( - 103, - "crlDistributionPoints", - "X509v3 CRL Distribution Points", - oid_registry::OID_X509_EXT_CRL_DISTRIBUTION_POINTS - ), - oid_static!( - 89, - "certificatePolicies", - "X509v3 Certificate Policies", - oid_registry::OID_X509_EXT_CERTIFICATE_POLICIES - ), - oid_static!( - 177, - "authorityInfoAccess", - "Authority Information Access", - oid_registry::OID_PKIX_AUTHORITY_INFO_ACCESS - ), - oid_static!( - 105, - "nameConstraints", - "X509v3 Name Constraints", - oid_registry::OID_X509_EXT_NAME_CONSTRAINTS - ), - // Priority 3: Extended Key Usage OIDs (not in oid-registry) - // These are defined in RFC 5280 but not in oid-registry, so we use strings - oid_string!( - 129, - "serverAuth", - "TLS Web Server Authentication", - "1.3.6.1.5.5.7.3.1" - ), - oid_string!( - 130, - "clientAuth", - "TLS Web Client Authentication", - "1.3.6.1.5.5.7.3.2" - ), - oid_string!(131, "codeSigning", "Code Signing", "1.3.6.1.5.5.7.3.3"), - oid_string!( - 132, - "emailProtection", - "E-mail Protection", - "1.3.6.1.5.5.7.3.4" - ), - oid_string!(133, "timeStamping", "Time Stamping", "1.3.6.1.5.5.7.3.8"), - oid_string!(180, "OCSPSigning", "OCSP Signing", "1.3.6.1.5.5.7.3.9"), - // Priority 4: Signature Algorithms - oid_static!( - 6, - "rsaEncryption", - "rsaEncryption", - oid_registry::OID_PKCS1_RSAENCRYPTION - ), - oid_static!( - 65, - "sha1WithRSAEncryption", - "sha1WithRSAEncryption", - oid_registry::OID_PKCS1_SHA1WITHRSA - ), - oid_static!( - 668, - "sha256WithRSAEncryption", - "sha256WithRSAEncryption", - oid_registry::OID_PKCS1_SHA256WITHRSA - ), - oid_static!( - 669, - "sha384WithRSAEncryption", - "sha384WithRSAEncryption", - oid_registry::OID_PKCS1_SHA384WITHRSA - ), - oid_static!( - 670, - "sha512WithRSAEncryption", - "sha512WithRSAEncryption", - oid_registry::OID_PKCS1_SHA512WITHRSA - ), - oid_static!( - 408, - "id-ecPublicKey", - "id-ecPublicKey", - oid_registry::OID_KEY_TYPE_EC_PUBLIC_KEY - ), - oid_static!( - 794, - "ecdsa-with-SHA256", - "ecdsa-with-SHA256", - oid_registry::OID_SIG_ECDSA_WITH_SHA256 - ), - oid_static!( - 795, - "ecdsa-with-SHA384", - "ecdsa-with-SHA384", - oid_registry::OID_SIG_ECDSA_WITH_SHA384 - ), - oid_static!( - 796, - "ecdsa-with-SHA512", - "ecdsa-with-SHA512", - oid_registry::OID_SIG_ECDSA_WITH_SHA512 - ), - // Priority 5: Hash Algorithms - oid_string!(64, "sha1", "sha1", "1.3.14.3.2.26"), - oid_static!(672, "sha256", "sha256", oid_registry::OID_NIST_HASH_SHA256), - oid_static!(673, "sha384", "sha384", oid_registry::OID_NIST_HASH_SHA384), - oid_static!(674, "sha512", "sha512", oid_registry::OID_NIST_HASH_SHA512), - oid_string!(675, "sha224", "sha224", "2.16.840.1.101.3.4.2.4"), - // Priority 6: Elliptic Curve OIDs - oid_static!(714, "secp256r1", "secp256r1", oid_registry::OID_EC_P256), - oid_string!(715, "secp384r1", "secp384r1", "1.3.132.0.34"), - oid_string!(716, "secp521r1", "secp521r1", "1.3.132.0.35"), - oid_string!(1172, "X25519", "X25519", "1.3.101.110"), - oid_string!(1173, "Ed25519", "Ed25519", "1.3.101.112"), - // Additional useful OIDs - oid_string!( - 183, - "subjectInfoAccess", - "Subject Information Access", - "1.3.6.1.5.5.7.1.11" - ), - oid_string!(920, "OCSP", "OCSP", "1.3.6.1.5.5.7.48.1"), - oid_string!(921, "caIssuers", "CA Issuers", "1.3.6.1.5.5.7.48.2"), - ] -} - -// Public API Functions - -/// Find OID entry by NID -pub(super) 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> { - 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> { - OID_TABLE.find_by_name(name) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn find_by_nid_ok() { - let entry = find_by_nid(13).unwrap(); - assert_eq!(entry.short_name, "CN"); - assert_eq!(entry.long_name, "commonName"); - assert_eq!(entry.oid_string(), "2.5.4.3"); - } - - #[test] - fn find_by_oid_string_ok() { - let entry = find_by_oid_string("2.5.4.3").unwrap(); - assert_eq!(entry.nid, 13); - assert_eq!(entry.short_name, "CN"); - } - - #[test] - fn find_by_name_short() { - let entry = find_by_name("CN").unwrap(); - assert_eq!(entry.nid, 13); - assert_eq!(entry.oid_string(), "2.5.4.3"); - } - - #[test] - fn find_by_name_long() { - let entry = find_by_name("commonName").unwrap(); - assert_eq!(entry.nid, 13); - assert_eq!(entry.short_name, "CN"); - } - - #[test] - fn find_by_name_case_insensitive() { - let entry = find_by_name("COMMONNAME").unwrap(); - assert_eq!(entry.nid, 13); - } - - #[test] - fn subject_alt_name() { - let entry = find_by_nid(85).unwrap(); - assert_eq!(entry.short_name, "subjectAltName"); - assert_eq!(entry.oid_string(), "2.5.29.17"); - } - - #[test] - fn server_auth_eku() { - let entry = find_by_nid(129).unwrap(); - assert_eq!(entry.short_name, "serverAuth"); - assert_eq!(entry.oid_string(), "1.3.6.1.5.5.7.3.1"); - } - - #[test] - fn no_duplicate_nids() { - let table = &*OID_TABLE; - assert_eq!( - table.entries.len(), - table.nid_to_idx.len(), - "Duplicate NIDs detected!" - ); - } - - #[test] - fn oid_count() { - let table = &*OID_TABLE; - // We should have 50+ OIDs defined - assert!( - table.entries.len() >= 50, - "Expected at least 50 OIDs, got {}", - table.entries.len() - ); - } -} diff --git a/crates/stdlib/src/ssl/providers.rs b/crates/stdlib/src/ssl/providers.rs index 478d02ff933..7d615f4ff3c 100644 --- a/crates/stdlib/src/ssl/providers.rs +++ b/crates/stdlib/src/ssl/providers.rs @@ -26,6 +26,7 @@ static CRYPTO_EXT: OnceLock = OnceLock::new(); #[derive(Clone, Copy)] pub struct CryptoExt { pub all_cipher_suites: Option<&'static [SupportedCipherSuite]>, + pub default_cipher_suites: Option<&'static [SupportedCipherSuite]>, pub all_kx_groups: Option<&'static [&'static dyn SupportedKxGroup]>, #[allow(clippy::type_complexity)] pub any_supported_key: Option) -> Result, Error>>, @@ -53,7 +54,17 @@ impl CryptoExt { /// Panics if a [`CryptoProvider`] hasn't been set. #[must_use] pub fn all_ciphers_or_default(&self) -> &'static [SupportedCipherSuite] { - self.all_cipher_suites.unwrap_or_else(|| { + self.all_cipher_suites + .unwrap_or_else(|| self.default_ciphers_or_provider()) + } + + /// Returns default [`SupportedCipherSuite`]s or the provider's configured ciphers. + /// + /// # Panics + /// Panics if a [`CryptoProvider`] hasn't been set. + #[must_use] + pub fn default_ciphers_or_provider(&self) -> &'static [SupportedCipherSuite] { + self.default_cipher_suites.unwrap_or_else(|| { CryptoProvider::get_default() .expect("A CryptoProvider has been set if CryptoExt is set") .cipher_suites diff --git a/examples/custom_tls_providers.rs b/examples/custom_tls_providers.rs index 1f382fc1b84..355f75342cc 100644 --- a/examples/custom_tls_providers.rs +++ b/examples/custom_tls_providers.rs @@ -30,6 +30,7 @@ fn main() { "ring" => { let ext = CryptoExt { all_cipher_suites: Some(ring::ALL_CIPHER_SUITES), + default_cipher_suites: Some(ring::DEFAULT_CIPHER_SUITES), all_kx_groups: Some(ring::ALL_KX_GROUPS), any_supported_key: Some(ring::sign::any_supported_type), ticketer: ring::Ticketer::new, @@ -40,6 +41,7 @@ fn main() { "graviola" => { let ext = CryptoExt { all_cipher_suites: Some(rustls_graviola::suites::ALL_CIPHER_SUITES), + default_cipher_suites: None, all_kx_groups: Some(rustls_graviola::kx::ALL_KX_GROUPS), any_supported_key: None, ticketer: rustls_graviola::Ticketer::new, diff --git a/src/interpreter.rs b/src/interpreter.rs index 230192d1e21..530e2a21e7c 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -35,6 +35,7 @@ fn install_default_tls_provider(_vm: &mut crate::VirtualMachine) { let ext = CryptoExt { all_cipher_suites: Some(aws_lc_rs::ALL_CIPHER_SUITES), + default_cipher_suites: Some(aws_lc_rs::DEFAULT_CIPHER_SUITES), all_kx_groups: Some(aws_lc_rs::ALL_KX_GROUPS), any_supported_key: Some(aws_lc_rs::sign::any_supported_type), ticketer: aws_lc_rs::Ticketer::new, From d0b27675b81957c5c99dd222644d42968c30abfb Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Mon, 1 Jun 2026 23:11:51 +0200 Subject: [PATCH 09/25] Skip test.test_httplib.HTTPSTest.test_networked_good_cert if rustls is used rustls does not support server host name verification by CN. Only Subject Alternative Names are supported. --- Lib/test/test_httplib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index f7ab3e576c0..b22eaa56fa1 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -2098,6 +2098,7 @@ def test_networked_trusted_by_default_cert(self): h.close() self.assertIn('text/html', content_type) + @unittest.skipIf("rustls" in __import__('ssl').OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls does not support server host name verification by CN") def test_networked_good_cert(self): # We feed the server's cert as a validating cert import ssl From 814b5cd70afb74bb9a8be31ccd3ace53bd67468b Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Tue, 2 Jun 2026 18:47:45 +0200 Subject: [PATCH 10/25] On client, both CERT_OPTIONAL and CERT_REQUIRED enable cert verification --- crates/stdlib/src/rustls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index 9b764925c8b..0ec9495a6fe 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -1162,7 +1162,7 @@ mod _ssl { }; let verifier = CustomServerCertVerifier::new( - self.verify_mode() == CERT_REQUIRED, + self.verify_mode() != CERT_NONE, use_system_certificates, &self.cert_store.read(), crypto.clone(), From 150128f95347ed07bcc03c384a8ace8943ba2dc2 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Wed, 3 Jun 2026 13:47:06 +0200 Subject: [PATCH 11/25] Rename stdlib_ssl_bio_unencrypted_trailer.py -> stdlib_ssl.py --- .../{stdlib_ssl_bio_unencrypted_trailer.py => stdlib_ssl.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename extra_tests/snippets/{stdlib_ssl_bio_unencrypted_trailer.py => stdlib_ssl.py} (100%) diff --git a/extra_tests/snippets/stdlib_ssl_bio_unencrypted_trailer.py b/extra_tests/snippets/stdlib_ssl.py similarity index 100% rename from extra_tests/snippets/stdlib_ssl_bio_unencrypted_trailer.py rename to extra_tests/snippets/stdlib_ssl.py From 196140b7596cbe19a90548ae902cadf53786c4b6 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Wed, 3 Jun 2026 13:53:58 +0200 Subject: [PATCH 12/25] Move crates/stdlib/src/rustls-data -> crates/stdlib/rustls-data --- crates/stdlib/{src => }/rustls-data/obj_mac.num | 0 crates/stdlib/{src => }/rustls-data/objects.txt | 0 crates/stdlib/src/rustls.rs | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename crates/stdlib/{src => }/rustls-data/obj_mac.num (100%) rename crates/stdlib/{src => }/rustls-data/objects.txt (100%) diff --git a/crates/stdlib/src/rustls-data/obj_mac.num b/crates/stdlib/rustls-data/obj_mac.num similarity index 100% rename from crates/stdlib/src/rustls-data/obj_mac.num rename to crates/stdlib/rustls-data/obj_mac.num diff --git a/crates/stdlib/src/rustls-data/objects.txt b/crates/stdlib/rustls-data/objects.txt similarity index 100% rename from crates/stdlib/src/rustls-data/objects.txt rename to crates/stdlib/rustls-data/objects.txt diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index 0ec9495a6fe..b0a17b0e6e8 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -4811,8 +4811,8 @@ impl OidMappings { // See https://github.com/openssl/openssl/blob/11b7b6ea3b65a584e1d31408ed1bdb139465cffd/crypto/objects/README.md // See https://github.com/openssl/openssl/blob/11b7b6ea3b65a584e1d31408ed1bdb139465cffd/crypto/objects/objects.pl // TODO: Do this in compile time. - let obj_mac_num = include_str!("rustls-data/obj_mac.num"); - let objects_txt = include_str!("rustls-data/objects.txt"); + let obj_mac_num = include_str!("../rustls-data/obj_mac.num"); + let objects_txt = include_str!("../rustls-data/objects.txt"); let nids: HashMap<_, _> = obj_mac_num .split('\n') From 0332046246ab9e8fcccb60254accc8b345edd837 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Wed, 3 Jun 2026 14:00:05 +0200 Subject: [PATCH 13/25] Use expectedFailureIf() instead of skipIf() for rustls-specific skips --- Lib/test/test_ssl.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index ce494aa9183..c788ff5f742 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1346,7 +1346,7 @@ def test_load_verify_cadata(self): with self.assertRaises(ssl.SSLError): ctx.load_verify_locations(cadata=cacert_der + b"A") - @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls does not support custom DH parameters") + @unittest.expectedFailureIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls does not support custom DH parameters") def test_load_dh_params(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) try: @@ -2093,7 +2093,7 @@ def test_ciphers(self): cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") s.connect(self.server_addr) - @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; capath certificates are loaded eagerly instead of on request") + @unittest.expectedFailureIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; capath certificates are loaded eagerly instead of on request") def test_get_ca_certs_capath(self): # capath certs are loaded on request ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -3073,7 +3073,7 @@ def test_ecc_cert(self): @unittest.skipUnless(IS_OPENSSL_3_0_0, "test requires RFC 5280 check added in OpenSSL 3.0+") - @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls cert verification does not match OpenSSL's VERIFY_X509_STRICT") + @unittest.expectedFailureIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls cert verification does not match OpenSSL's VERIFY_X509_STRICT") def test_verify_strict(self): # verification fails by default, since the server cert is non-conforming client_context = ssl.create_default_context() @@ -4358,7 +4358,7 @@ def test_sendfile(self): s.sendfile(file) self.assertEqual(s.recv(1024), TEST_DATA) - @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "AttributeError: 'NoneType' object has no attribute 'id'") + @unittest.expectedFailureIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; AttributeError: 'NoneType' object has no attribute 'id'") def test_session(self): client_context, server_context, hostname = testing_context() # TODO: sessions aren't compatible with TLSv1.3 yet @@ -4416,7 +4416,7 @@ def test_session(self): self.assertEqual(sess_stat['accept'], 4) self.assertEqual(sess_stat['hits'], 2) - @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; AssertionError: None is not true") + @unittest.expectedFailureIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; AssertionError: None is not true") def test_session_handling(self): client_context, server_context, hostname = testing_context() client_context2, _, _ = testing_context() @@ -5035,7 +5035,7 @@ def msg_cb(conn, direction, version, content_type, msg_type, data): with self.assertRaises(TypeError): client_context._msg_callback = object() - @unittest.skipIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; AssertionError: ('read', , <_TLSContentType.HANDSHAKE: 22>, <_TLSMessageType.SERVER_KEY_EXCHANGE: 12>) not found in []") + @unittest.expectedFailureIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; AssertionError: ('read', , <_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 From fdd514b964a715e73807c07ca1bd835e4e755065 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Wed, 3 Jun 2026 14:22:50 +0200 Subject: [PATCH 14/25] Fix `cargo shear` warn (graviola dep for examples/custom_tls_providers.rs) --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5f7fec75476..d47f67b64c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ env_logger = "0.11" flamescope = { version = "0.1.2", optional = true } rustls = { workspace = true, optional = true } -rustls-graviola = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] libc = { workspace = true } @@ -62,6 +61,7 @@ criterion = { workspace = true } pyo3 = { workspace = true, features = ["auto-initialize"] } rustpython-stdlib = { workspace = true } ruff_python_parser = { workspace = true } +rustls-graviola = { workspace = true } [[bench]] name = "execution" @@ -79,7 +79,6 @@ path = "src/main.rs" name = "custom_tls_providers" path = "examples/custom_tls_providers.rs" required-features = [ - "rustls-graviola", "rustls/ring", "rustpython-pylib/freeze-stdlib", "rustpython-stdlib/ssl-rustls", From 6b954efef2943feb7655be21d6ab3192bae5c5b2 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Wed, 3 Jun 2026 19:53:02 +0200 Subject: [PATCH 15/25] Call SNI callback earlier and re-read the context after it --- crates/stdlib/src/rustls.rs | 41 ++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index b0a17b0e6e8..cb545405371 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -1587,28 +1587,10 @@ mod _ssl { } }; - let context = self.context(); let hello = accepted.client_hello(); - // Remember shared cipher suites. - { - let our_ciphers = context.ciphers.read(); - *self.shared_ciphers.write() = Some( - hello - .cipher_suites() - .iter() - .filter_map(|c| { - our_ciphers - .0 - .iter() - .find(|oc| u16::from(*c) == u16::from(oc.suite())) - }) - .copied() - .collect(), - ); - } - // Call SNI callback (if any). + let context = self.context(); let sni_callback = context.sni_callback(); if !vm.is_none(&sni_callback) { let owner = self.owner.upgrade().ok_or_else(|| { @@ -1671,6 +1653,27 @@ mod _ssl { } }; + // SNI callback may change the context, so get it again. + let context = self.context(); + + // Remember shared cipher suites. + { + let our_ciphers = context.ciphers.read(); + *self.shared_ciphers.write() = Some( + hello + .cipher_suites() + .iter() + .filter_map(|c| { + our_ciphers + .0 + .iter() + .find(|oc| u16::from(*c) == u16::from(oc.suite())) + }) + .copied() + .collect(), + ); + } + // Create rustls connection. let conn = match self.context.read().create_server_connection(accepted, vm)? { From 99d20f8c1e1bb7f71333fdf6ebc3512af265ec44 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Wed, 3 Jun 2026 19:58:07 +0200 Subject: [PATCH 16/25] Remove allow_threads() from rustls integration I misunderstood it and thought that it has something to do with locking. --- crates/stdlib/src/rustls.rs | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index cb545405371..c04ba85237b 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -957,8 +957,7 @@ mod _ssl { // Load from capath if let Some(capath) = capath { let capath = capath.to_path_buf(vm)?; - let paths = vm - .allow_threads(|| rustpython_host_env::fs::read_dir(capath)) + let paths = rustpython_host_env::fs::read_dir(capath) .map_err(|e| e.into_pyexception(vm))?; for path in paths { let path = path.map_err(|e| e.into_pyexception(vm))?; @@ -1977,9 +1976,6 @@ mod _ssl { } // When handshaking, complete_io() returns only after handshake is complete. - // TODO: This might call certificate verifier which might be blocking and require network access on its own. - // option 1: Extract process_new_packets() from complete_io() to wrap it in allow_threads(). - // option 2: Introduce VirtualMachine::disallow_threads() and use it inside the IO wrapper instead. fn complete_io( &self, conn: &mut Connection, @@ -2341,8 +2337,7 @@ mod _ssl { vm: &VirtualMachine, ) -> PyResult> { let store_name_str = store_name.as_str(); - let certs = - vm.allow_threads(|| rustpython_host_env::cert_store::enum_certificates(store_name_str)); + let certs = rustpython_host_env::cert_store::enum_certificates(store_name_str); if !certs.had_open_store { return Err(vm.new_os_error(format!( "failed to open certificate store {store_name_str:?}" @@ -2382,13 +2377,11 @@ mod _ssl { #[pyfunction] fn enum_crls(store_name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult> { let store_name_str = store_name.as_str(); - let crls = vm - .allow_threads(|| rustpython_host_env::cert_store::enum_crls(store_name_str)) - .map_err(|_| { - vm.new_os_error(format!( - "failed to open certificate store {store_name_str:?}" - )) - })?; + let crls = rustpython_host_env::cert_store::enum_crls(store_name_str).map_err(|_| { + vm.new_os_error(format!( + "failed to open certificate store {store_name_str:?}" + )) + })?; Ok(crls .into_iter() @@ -2522,7 +2515,7 @@ mod _ssl { let rng = CryptoExt::get_provider().secure_random; let mut buf = vec![0u8; len]; - vm.allow_threads(|| rng.fill(&mut buf)) + rng.fill(&mut buf) .map_err(|_| vm.new_os_error("Failed to generate random bytes"))?; Ok(PyBytesRef::from(vm.ctx.new_bytes(buf))) } @@ -3131,9 +3124,7 @@ fn load_der_bytes_from_pem_or_der_file_inner( password: &mut Password, vm: &VirtualMachine, ) -> SslResult> { - let bytes = vm - .allow_threads(|| rustpython_host_env::fs::read(path)) - .map_err(SslError::Io)?; + let bytes = rustpython_host_env::fs::read(path).map_err(SslError::Io)?; load_der_bytes_from_pem_or_der_bytes(&format!("{path:?}"), bytes, kinds, password, vm) } From adce18be77a6a28d5e775e272ac7c1e5b3514b20 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Wed, 3 Jun 2026 20:08:55 +0200 Subject: [PATCH 17/25] Use certs from both SSL_CERT_FILE and SSL_CERT_DIR env vars if present --- Lib/test/test_ssl.py | 2 ++ crates/stdlib/src/rustls.rs | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index c788ff5f742..e05822dcf57 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1482,6 +1482,7 @@ def test_load_default_certs(self): self.assertRaises(TypeError, ctx.load_default_certs, 'SERVER_AUTH') @unittest.skipIf(sys.platform == "win32", "not-Windows specific") + @unittest.expectedFailureIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls does not support certificate lazy loading") def test_load_default_certs_env(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) with os_helper.EnvironmentVarGuard() as env: @@ -1493,6 +1494,7 @@ def test_load_default_certs_env(self): @unittest.skipUnless(sys.platform == "win32", "Windows specific") @unittest.skipIf(support.Py_DEBUG, "Debug build does not share environment between CRTs") + @unittest.expectedFailureIf("rustls" in ssl.OPENSSL_VERSION, "TODO: RUSTPYTHON; rustls does not support certificate lazy loading") def test_load_default_certs_env_windows(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.load_default_certs() diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index c04ba85237b..b26ca91723b 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -736,10 +736,9 @@ mod _ssl { if cafile.is_some() || capath.is_some() { // Load certificates and certificate revocation lists from specified paths. - let has_cafile = cafile.is_some(); let args = LoadVerifyLocationsArgs { cafile, - capath: if has_cafile { None } else { capath }, + capath, cadata: OptionalArg::Missing, }; self.load_verify_locations(args, vm)?; From 6036eaa525258f6a99404e5bf9b88dc4334724c2 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Thu, 4 Jun 2026 00:20:13 +0200 Subject: [PATCH 18/25] Keep rustls error in SslError until converted into Python error --- crates/stdlib/src/rustls.rs | 8 +-- crates/stdlib/src/ssl/compat.rs | 123 ++++++++++++++------------------ 2 files changed, 57 insertions(+), 74 deletions(-) diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index b26ca91723b..ee414f2ba74 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -1198,7 +1198,7 @@ mod _ssl { }; Ok(ClientConnection::new(Arc::new(config), server_name) - .map_err(|e| SslError::from_rustls(e).into_py_err(vm))? + .map_err(|e| SslError::Rustls(e).into_py_err(vm))? .into()) } @@ -1214,7 +1214,7 @@ mod _ssl { // TODO: Search by a requested host name too. let cert_chain = self.cert_chain.read(); if cert_chain.is_empty() { - return Err(SslError::from_rustls(rustls::Error::PeerIncompatible( + return Err(SslError::Rustls(rustls::Error::PeerIncompatible( rustls::PeerIncompatible::NoCipherSuitesInCommon, )) .into_py_err(vm)); @@ -2707,7 +2707,7 @@ impl State { Err(SslError::Ssl("TLS alert is too long or too short".to_string()).into_py_err(vm)) } else { Ok(Self::ServerSendingAlert { - error: SslError::from_rustls(error).into_py_err(vm), + error: SslError::Rustls(error).into_py_err(vm), alert_buf, alert_buf_pos: 0, }) @@ -2847,7 +2847,7 @@ impl Io { let err = err .downcast::() .expect("BUG: Not a rustls Error"); - Err(SslError::from_rustls(err).into_py_err(vm)) + Err(SslError::Rustls(err).into_py_err(vm)) } _ => Err(SslError::Io(err).into_py_err(vm)), diff --git a/crates/stdlib/src/ssl/compat.rs b/crates/stdlib/src/ssl/compat.rs index 3f79c9aca2a..d79c7feacac 100644 --- a/crates/stdlib/src/ssl/compat.rs +++ b/crates/stdlib/src/ssl/compat.rs @@ -177,8 +177,8 @@ pub(super) enum SslError { ZeroReturn, /// Unexpected EOF without close_notify (protocol violation) Eof, - /// Certificate verification error - CertVerification(rustls::CertificateError), + /// rustls error + Rustls(rustls::Error), /// I/O error Io(std::io::Error), /// Timeout error (socket.timeout) @@ -186,10 +186,6 @@ pub(super) enum SslError { Timeout(String), /// Python exception (pass through directly) Py(PyBaseExceptionRef), - /// TLS alert received with OpenSSL-compatible error code - AlertReceived { lib: i32, reason: i32 }, - /// OpenSSL-compatible SSL reason code - OpenSslReason(i32), } impl SslError { @@ -200,60 +196,6 @@ impl SslError { 1000 + (u8::from(alert) as i32) } - /// Convert rustls error to SslError - pub(super) fn from_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), - } - } - } - } - rustls::Error::PeerIncompatible(peer_err) => { - use rustls::PeerIncompatible; - let reason = match peer_err { - PeerIncompatible::NoCipherSuitesInCommon => SSL_R_NO_SHARED_CIPHER, - PeerIncompatible::NoKxGroupsInCommon - | PeerIncompatible::NoEcPointFormatsInCommon - | PeerIncompatible::EcPointsExtensionRequired - | PeerIncompatible::NamedGroupsExtensionRequired - | PeerIncompatible::UncompressedEcPointsRequired => SSL_R_NO_SUITABLE_GROUPS, - PeerIncompatible::KeyShareExtensionRequired => SSL_R_NO_SUITABLE_KEY_SHARE, - PeerIncompatible::NoCertificateRequestSignatureSchemesInCommon - | PeerIncompatible::NoSignatureSchemesInCommon - | PeerIncompatible::SignatureAlgorithmsExtensionRequired => { - SSL_R_NO_SUITABLE_SIGNATURE_ALGORITHM - } - PeerIncompatible::ServerDoesNotSupportTls12Or13 - | PeerIncompatible::ServerTlsVersionIsDisabledByOurConfig - | PeerIncompatible::SupportedVersionsExtensionRequired - | PeerIncompatible::Tls12NotOffered - | PeerIncompatible::Tls12NotOfferedOrEnabled - | PeerIncompatible::Tls13RequiredForQuic => SSL_R_UNSUPPORTED_PROTOCOL, - _ => return Self::Ssl(format!("peer is incompatible: {peer_err:?}")), - }; - Self::OpenSslReason(reason) - } - rustls::Error::NoApplicationProtocol => { - Self::OpenSslReason(SSL_R_NO_APPLICATION_PROTOCOL) - } - _ => Self::Ssl(format!("{err}")), - } - } - /// Create SSLError with library and reason from string values /// /// This is the base helper for creating SSLError with _library and _reason @@ -366,6 +308,57 @@ impl SslError { Self::WantWrite => create_ssl_want_write_error(vm).upcast(), Self::Timeout(msg) => timeout_error_msg(vm, msg).upcast(), Self::Ssl(msg) => Self::create_plain_ssl_error(vm, msg), + Self::Rustls(err) => match err { + rustls::Error::InvalidCertificate(cert_err) => { + create_ssl_cert_verification_error(vm, &cert_err).expect("unlikely to happen") + } + rustls::Error::AlertReceived(rustls::AlertDescription::CloseNotify) => { + create_ssl_zero_return_error(vm).upcast() + } + rustls::Error::AlertReceived(alert_desc) => Self::create_ssl_error_from_codes( + vm, + ERR_LIB_SSL, + Self::alert_to_openssl_reason(alert_desc), + ), + rustls::Error::PeerIncompatible(peer_err) => { + use rustls::PeerIncompatible; + let reason = match peer_err { + PeerIncompatible::NoCipherSuitesInCommon => SSL_R_NO_SHARED_CIPHER, + PeerIncompatible::NoKxGroupsInCommon + | PeerIncompatible::NoEcPointFormatsInCommon + | PeerIncompatible::EcPointsExtensionRequired + | PeerIncompatible::NamedGroupsExtensionRequired + | PeerIncompatible::UncompressedEcPointsRequired => { + SSL_R_NO_SUITABLE_GROUPS + } + PeerIncompatible::KeyShareExtensionRequired => SSL_R_NO_SUITABLE_KEY_SHARE, + PeerIncompatible::NoCertificateRequestSignatureSchemesInCommon + | PeerIncompatible::NoSignatureSchemesInCommon + | PeerIncompatible::SignatureAlgorithmsExtensionRequired => { + SSL_R_NO_SUITABLE_SIGNATURE_ALGORITHM + } + PeerIncompatible::ServerDoesNotSupportTls12Or13 + | PeerIncompatible::ServerTlsVersionIsDisabledByOurConfig + | PeerIncompatible::SupportedVersionsExtensionRequired + | PeerIncompatible::Tls12NotOffered + | PeerIncompatible::Tls12NotOfferedOrEnabled + | PeerIncompatible::Tls13RequiredForQuic => SSL_R_UNSUPPORTED_PROTOCOL, + _ => { + return Self::create_plain_ssl_error( + vm, + format!("peer is incompatible: {peer_err:?}"), + ); + } + }; + Self::create_ssl_error_from_codes(vm, ERR_LIB_SSL, reason) + } + rustls::Error::NoApplicationProtocol => Self::create_ssl_error_from_codes( + vm, + ERR_LIB_SSL, + SSL_R_NO_APPLICATION_PROTOCOL, + ), + _ => Self::create_plain_ssl_error(vm, err.to_string()), + }, Self::PemLib(msg) => Self::create_pem_ssl_error(vm, format!("PEM lib: {msg}")) .expect("unlikely to happen"), Self::FailedToReadDer(msg) => { @@ -381,10 +374,6 @@ impl SslError { ), Self::ZeroReturn => create_ssl_zero_return_error(vm).upcast(), Self::Eof => create_ssl_eof_error(vm).upcast(), - Self::CertVerification(cert_err) => { - // Use the proper cert verification error creator - create_ssl_cert_verification_error(vm, &cert_err).expect("unlikely to happen") - } Self::Io(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => { create_ssl_eof_error(vm).upcast() } @@ -397,12 +386,6 @@ impl SslError { .upcast(), Self::Io(err) => err.into_pyexception(vm), Self::Py(exc) => exc, - Self::AlertReceived { lib, reason } => { - Self::create_ssl_error_from_codes(vm, lib, reason) - } - Self::OpenSslReason(reason) => { - Self::create_ssl_error_from_codes(vm, ERR_LIB_SSL, reason) - } } } } From 4e79b93e034a6fc323aea38178fe581f34779534 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Thu, 4 Jun 2026 01:35:55 +0200 Subject: [PATCH 19/25] Ensure that TLS alerts are sent back on TLS errors --- crates/stdlib/src/rustls.rs | 131 ++++++++++++++++++++++++++++-------- 1 file changed, 102 insertions(+), 29 deletions(-) diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index ee414f2ba74..bca5db74599 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -157,9 +157,11 @@ mod _ssl { CIPHER_MAPPINGS, CertInfo, CertStore, CipherDescriptionDict, CipherList, CloseNotifyState, ConnectionState, CrlCheck, CustomServerCertVerifier, DerKind, Io, OID_MAPPINGS, Password, SECURITY_LEVEL_TO_MIN_BITS, State, Stats, WithOptionSuiteB, cipher_to_tuple, - cipher_to_version, compat::SslError, der_to_pem_cert, ensure_single_der_bytes, - load_der_bytes_from_der, load_der_bytes_from_pem, load_der_bytes_from_pem_or_der_bytes, - load_der_bytes_from_pem_or_der_file, providers::CryptoExt, + cipher_to_version, + compat::{SslError, SslResult}, + der_to_pem_cert, ensure_single_der_bytes, load_der_bytes_from_der, load_der_bytes_from_pem, + load_der_bytes_from_pem_or_der_bytes, load_der_bytes_from_pem_or_der_file, + providers::CryptoExt, }; #[expect(clippy::unnecessary_wraps, reason = "pymodule hook expects PyResult")] @@ -1571,7 +1573,9 @@ mod _ssl { let accepted = loop { { let mut io = self.io.write(); - let _ = io.with_io(vm, |io| acceptor.read_tls(io))?; + let _ = io + .with_io(vm, |io| acceptor.read_tls(io)) + .map_err(|e| e.into_py_err(vm))?; } match acceptor.accept() { @@ -1685,13 +1689,6 @@ mod _ssl { state: ConnectionState::Handshaking, conn, }; - let State::HasConnection { state, conn, .. } = &mut *state else { - unreachable!("BUG: Impossible") - }; - - self.complete_io(conn, true, vm)?; - *state = ConnectionState::Connected(CloseNotifyState::None); - break Ok(()); } State::ServerSendingAlert { @@ -1700,7 +1697,9 @@ mod _ssl { alert_buf_pos, } => { let mut io = self.io.write(); - let sent = io.with_io(vm, |io| io.write(&alert_buf[*alert_buf_pos..]))?; + let sent = io + .with_io(vm, |io| io.write(&alert_buf[*alert_buf_pos..])) + .map_err(|e| e.into_py_err(vm))?; *alert_buf_pos += sent; if *alert_buf_pos == alert_buf.len() { break Err(error.clone()); @@ -1711,7 +1710,7 @@ mod _ssl { state: conn_state @ ConnectionState::Handshaking, conn, } => { - self.complete_io(conn, true, vm)?; + self.complete_io_with_sending_alert_on_error(conn, conn_state, true, vm)?; *conn_state = ConnectionState::Connected(CloseNotifyState::None); break Ok(()); } @@ -1723,6 +1722,14 @@ mod _ssl { | ConnectionState::ShutDown, .. } => break Ok(()), // handshake already done + + State::HasConnection { + state: ConnectionState::SendingAlertAfterError(err), + conn, + } => { + self.send_alert_after_error(conn, vm)?; + return Err(err.clone()); + } }; } } @@ -1819,6 +1826,14 @@ mod _ssl { } => { return Err(SslError::ZeroReturn.into_py_err(vm)); } + + State::HasConnection { + state: ConnectionState::SendingAlertAfterError(err), + conn, + } => { + self.send_alert_after_error(conn, vm)?; + return Err(err.clone()); + } }; // Do the read. @@ -1850,7 +1865,7 @@ mod _ssl { Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { // There is no plaintext data in internal buffers, need to do IO. - self.complete_io(conn, true, vm)?; + self.complete_io_with_sending_alert_on_error(conn, conn_state, true, vm)?; } Err(err) => return Err(SslError::Io(err).into_py_err(vm)), @@ -1864,7 +1879,7 @@ mod _ssl { self.do_handshake(vm)?; let mut state = self.state.write(); - let conn = match &mut *state { + let (conn, conn_state) = match &mut *state { State::ServerWaitingForClientHello(_) | State::ServerSendingAlert { .. } | State::HasConnection { @@ -1876,9 +1891,11 @@ mod _ssl { State::HasConnection { state: - ConnectionState::Connected(CloseNotifyState::None | CloseNotifyState::Received), + conn_state @ ConnectionState::Connected( + CloseNotifyState::None | CloseNotifyState::Received, + ), conn, - } => conn, + } => (conn, conn_state), State::HasConnection { state: @@ -1889,10 +1906,18 @@ mod _ssl { } => { return Err(SslError::ZeroReturn.into_py_err(vm)); } + + State::HasConnection { + state: ConnectionState::SendingAlertAfterError(err), + conn, + } => { + self.send_alert_after_error(conn, vm)?; + return Err(err.clone()); + } }; // Send previously queued data, if any. - self.complete_io(conn, false, vm)?; + self.complete_io_with_sending_alert_on_error(conn, conn_state, false, vm)?; let data = data.borrow_buf(); let written = conn @@ -1900,7 +1925,7 @@ mod _ssl { .write(&data) .map_err(|e| SslError::Io(e).into_py_err(vm))?; - self.complete_io(conn, false, vm)?; + self.complete_io_with_sending_alert_on_error(conn, conn_state, false, vm)?; Ok(written) } @@ -1957,7 +1982,8 @@ mod _ssl { state: conn_state @ ConnectionState::ShuttingDown, conn, } => { - self.complete_io(conn, true, vm)?; + self.complete_io(conn, true, vm) + .map_err(|e| e.into_py_err(vm))?; *conn_state = ConnectionState::ShutDown; break; } @@ -1968,19 +1994,49 @@ mod _ssl { } => { break; } + + State::HasConnection { + state: ConnectionState::SendingAlertAfterError(err), + conn, + } => { + self.send_alert_after_error(conn, vm)?; + return Err(err.clone()); + } }; } Ok(self.io.read().to_socket(vm)) } + fn complete_io_with_sending_alert_on_error( + &self, + conn: &mut Connection, + conn_state: &mut ConnectionState, + read_and_write: bool, + vm: &VirtualMachine, + ) -> PyResult<()> { + match self.complete_io(conn, read_and_write, vm) { + Ok(()) => Ok(()), + + Err(err @ SslError::Rustls(_)) => { + // TLS implementation may want to send an alert after a TLS-level error. + let err = err.into_py_err(vm); + *conn_state = ConnectionState::SendingAlertAfterError(err.clone()); + self.send_alert_after_error(conn, vm)?; + Err(err) + } + + Err(err) => Err(err.into_py_err(vm)), + } + } + // When handshaking, complete_io() returns only after handshake is complete. fn complete_io( &self, conn: &mut Connection, read_and_write: bool, vm: &VirtualMachine, - ) -> PyResult<()> { + ) -> SslResult<()> { // complete_io() when writing if !conn.wants_write() may read data past the Close Notify. // TODO: Remove this check when proper rustls unbuffered API is used. if read_and_write || conn.wants_write() { @@ -1990,6 +2046,20 @@ mod _ssl { Ok(()) } + fn send_alert_after_error( + &self, + conn: &mut Connection, + vm: &VirtualMachine, + ) -> PyResult<()> { + while conn.wants_write() { + let mut io = self.io.write(); + let _ = io + .with_io(vm, |io| conn.write_tls(io)) + .map_err(|e| e.into_py_err(vm))?; + } + Ok(()) + } + #[pymethod] fn pending(&self, vm: &VirtualMachine) -> PyResult { self.state @@ -2652,6 +2722,7 @@ enum ConnectionState { Connected(CloseNotifyState), ShuttingDown, ShutDown, + SendingAlertAfterError(PyBaseExceptionRef), } #[derive(Debug)] @@ -2744,7 +2815,8 @@ impl State { state: ConnectionState::Connected(_) | ConnectionState::ShuttingDown - | ConnectionState::ShutDown, + | ConnectionState::ShutDown + | ConnectionState::SendingAlertAfterError(_), conn, } => Some(conn), } @@ -2763,7 +2835,8 @@ impl State { state: ConnectionState::Connected(_) | ConnectionState::ShuttingDown - | ConnectionState::ShutDown, + | ConnectionState::ShutDown + | ConnectionState::SendingAlertAfterError(_), conn, } => Some(conn), } @@ -2825,7 +2898,7 @@ impl Io { } } - fn with_io(&mut self, vm: &VirtualMachine, f: F) -> PyResult + fn with_io(&mut self, vm: &VirtualMachine, f: F) -> SslResult where F: FnOnce(&mut WithIo<'_>) -> std::io::Result, { @@ -2838,19 +2911,19 @@ impl Io { Ok(value) => Ok(value), Err(err) => match err.kind() { - std::io::ErrorKind::Other => { - Err(io.error.take().expect("BUG: Io.error is not set")) - } + std::io::ErrorKind::Other => Err(SslError::Py( + io.error.take().expect("BUG: Io.error is not set"), + )), std::io::ErrorKind::InvalidData => { // ConnectionCommon::complete_io() wraps TLS processing errors in InvalidData. let err = err .downcast::() .expect("BUG: Not a rustls Error"); - Err(SslError::Rustls(err).into_py_err(vm)) + Err(SslError::Rustls(err)) } - _ => Err(SslError::Io(err).into_py_err(vm)), + _ => Err(SslError::Io(err)), }, } } From 7117204688fc89db8b9d97fd2434eaa3c653b294 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Thu, 4 Jun 2026 02:08:54 +0200 Subject: [PATCH 20/25] Document TLS connection state machine --- crates/stdlib/src/rustls.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index bca5db74599..4e4d5898a92 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -2702,14 +2702,24 @@ mod _ssl { // enum State { + // Initial state for a server connection. + // Possible state transitions: + // * ServerWaitingForClientHello(_) -> ServerSendingAlert + // ^ rustls failed to accept the Client Hello or SNI callback failed or returned an alert code + // * ServerWaitingForClientHello(_) -> HasConnection { state: ConnectionState::Handshaking } + // ^ rustls accepted the Client Hello ServerWaitingForClientHello(Acceptor), + // Either rustls failed to accept the Client Hello or SNI callback failed/returned an alert code. + // This state is final. ServerSendingAlert { error: PyBaseExceptionRef, alert_buf: [u8; TLS_RECORD_HEADER_LEN + TLS_ALERT_RECORD_LEN], alert_buf_pos: usize, }, + // We have a rustls connection. Client connection starts here with { state: ConnectionState::Handshaking }. + // This state is final. HasConnection { state: ConnectionState, conn: Connection, @@ -2718,10 +2728,37 @@ enum State { #[derive(Debug)] enum ConnectionState { + // Initial state. + // Possible state transitions: + // * Handshaking -> Connected(CloseNotifyState::None) + // ^ After a successful TLS handshake + // * Handshaking -> SendingAlertAfterError(_) + // ^ TLS handshake failed for some reason, we might need to send some alert to the other side Handshaking, + + // This is the primary state that allows reading and writing plaintext data. + // Possible state transitions: + // * Connected(CloseNotifyState::None) -> Connected(CloseNotifyState::Received) + // ^ Close Notify received from the other side + // * Connected(CloseNotifyState::Sent) -> ShuttingDown + // ^ Close Notify was previously buffered for sending by PySSLSocket::shutdown() plus now we received + // it from the other side which means that TLS connection is near a complete shut down + // * Connected(_) -> SendingAlertAfterError(_) + // ^ TLS-level error, we might need to send some alert to the other side Connected(CloseNotifyState), + + // Outgoing Close Notify was buffered by PySSLSocket::shutdown() and now we are sending it. + // Possible state transitions: + // * ShuttingDown -> ShutDown + // ^ No more IO to do, connection is shut down completely ShuttingDown, + + // TLS connection is shut down completely. + // This state is final. ShutDown, + + // TLS-level error happened, we might need to send some alert to the other side. + // This state is final. SendingAlertAfterError(PyBaseExceptionRef), } From 5e02b751c9100aea93ced121e967869e1db39ffa Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Thu, 4 Jun 2026 02:47:29 +0200 Subject: [PATCH 21/25] Test Rust -> Python serializer with py_serde::PyObjectSerializer --- crates/vm/src/convert/rust_py_serde.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/vm/src/convert/rust_py_serde.rs b/crates/vm/src/convert/rust_py_serde.rs index ee9808c8d33..531836b851e 100644 --- a/crates/vm/src/convert/rust_py_serde.rs +++ b/crates/vm/src/convert/rust_py_serde.rs @@ -10,6 +10,7 @@ use crate::{ builtins::{PyBaseExceptionRef, PyDictRef}, }; +// TODO: Add a shortcut implementation for `py_serde::PyObjectSerializer`. /// Panics on unit values and unit structures. pub struct RustPySerDe<'a> { vm: &'a VirtualMachine, @@ -507,9 +508,7 @@ mod tests { use serde::Serialize; - use rustpython_vm::Interpreter; - - use crate::convert::RustPySerDeConf; + use crate::{Interpreter, convert::RustPySerDeConf, py_serde::PyObjectSerializer}; fn interpreter() -> Interpreter { Interpreter::without_stdlib(Default::default()) @@ -734,4 +733,23 @@ mod tests { let _ = vm.unwrap_pyresult(vm.run_block_expr(scope, script)); }); } + + #[test] + fn serialize_py_object() { + interpreter().enter(|vm| { + let obj = vm.ctx.new_str("test"); + let val = vm.unwrap_pyresult( + vm.with_serde(|serde| PyObjectSerializer::new(vm, &obj.into()).serialize(serde)), + ); + + let scope = vm.new_scope_with_builtins(); + vm.unwrap_pyresult(scope.globals.set_item("val", val, vm)); + + let script = "\ + assert val == 'test'\n\ + assert isinstance(val, str)\n\ + "; + let _ = vm.unwrap_pyresult(vm.run_block_expr(scope, script)); + }); + } } From 9feac877151cb11b3037ad3c9906ca0af7ad9a34 Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Thu, 4 Jun 2026 02:56:00 +0200 Subject: [PATCH 22/25] Add documentation for Rust -> Python serializer --- crates/vm/src/convert/rust_py_serde.rs | 18 +++++++++++++++++- crates/vm/src/vm/mod.rs | 10 ++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/vm/src/convert/rust_py_serde.rs b/crates/vm/src/convert/rust_py_serde.rs index 531836b851e..084bcbcdb77 100644 --- a/crates/vm/src/convert/rust_py_serde.rs +++ b/crates/vm/src/convert/rust_py_serde.rs @@ -11,23 +11,39 @@ use crate::{ }; // TODO: Add a shortcut implementation for `py_serde::PyObjectSerializer`. -/// Panics on unit values and unit structures. +/// Rust -> Python serializer. +/// +/// # Panics +/// +/// Panics on unit (`()`) values. pub struct RustPySerDe<'a> { vm: &'a VirtualMachine, conf: RustPySerDeConf, } +/// Configuration of Rust -> Python serializer. #[derive(Eq, PartialEq, Debug, Clone)] pub struct RustPySerDeConf { + /// How to serialize lists. pub lists: RustPySerDeSeqKind, + + /// How to serialize tuples. pub tuples: RustPySerDeSeqKind, + + /// How to serialize tuple structures. pub tuple_structs: RustPySerDeSeqKind, + + /// How to serialize tuple variants of enums. pub tuple_variants: RustPySerDeSeqKind, } +/// How to serialize sequences into Python types. #[derive(Eq, PartialEq, Debug, Clone, Copy)] pub enum RustPySerDeSeqKind { + /// Serialize sequences as Python tuples. AsTuple, + + /// Serialize sequences as Python lists. AsList, } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 0ddfec59308..26f6c9e52af 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -2279,6 +2279,11 @@ impl VirtualMachine { Ok(Cow::Owned(s)) } + /// Serialize Rust structures into Python with default configuration. + /// + /// # Panics + /// + /// Panics on unit (`()`) values. #[cfg(feature = "serde")] pub fn with_serde<'a, T, F>(&'a self, f: F) -> PyResult where @@ -2287,6 +2292,11 @@ impl VirtualMachine { self.with_serde_conf(RustPySerDeConf::default(), f) } + /// Serialize Rust structures into Python with provided configuration. + /// + /// # Panics + /// + /// Panics on unit (`()`) values. #[cfg(feature = "serde")] pub fn with_serde_conf<'a, T, F>(&'a self, conf: RustPySerDeConf, f: F) -> PyResult where From c56a8d425a1279fef5799f6c7ea519bb931af90e Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Thu, 4 Jun 2026 02:59:57 +0200 Subject: [PATCH 23/25] Serialize unit struct into Python string with just a struct name --- crates/vm/src/convert/rust_py_serde.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/vm/src/convert/rust_py_serde.rs b/crates/vm/src/convert/rust_py_serde.rs index 084bcbcdb77..546f7b268aa 100644 --- a/crates/vm/src/convert/rust_py_serde.rs +++ b/crates/vm/src/convert/rust_py_serde.rs @@ -138,8 +138,8 @@ impl<'a> Serializer for &'a RustPySerDe<'a> { unimplemented!("BUG: Unit value cannot be serialized into a Python object") } - fn serialize_unit_struct(self, _name: &'static str) -> Result { - unimplemented!("BUG: Unit struct value cannot be serialized into a Python object") + fn serialize_unit_struct(self, name: &'static str) -> Result { + name.serialize(self) } fn serialize_unit_variant( @@ -557,6 +557,7 @@ mod tests { val_tuple: (&'static str, i32), val_map: BTreeMap<&'static str, i32>, val_struct: TestSubStruct, + val_unit_struct: TestUnitStruct, } #[derive(Serialize)] @@ -575,6 +576,9 @@ mod tests { Qux { aaa: String, bbb: i32 }, } + #[derive(Serialize)] + struct TestUnitStruct; + #[test] fn serialize() { let val = TestStruct { @@ -610,6 +614,7 @@ mod tests { bbb: -3, }, }, + val_unit_struct: TestUnitStruct, }; interpreter().enter(|vm| { @@ -621,7 +626,7 @@ mod tests { let script = "\ from sys import maxsize\n\ \n\ - assert len(val) == 24\n\ + assert len(val) == 25\n\ assert val['val_bool']\n\ assert val['val_u8'] == 255\n\ assert val['val_i8'] == -128\n\ @@ -650,6 +655,8 @@ mod tests { assert isinstance(val['val_tuple'], tuple)\n\ assert val['val_map'] == {'one': 1, 'two': 2}\n\ assert isinstance(val['val_map'], dict)\n\ + assert val['val_unit_struct'] == 'TestUnitStruct'\n\ + assert isinstance(val['val_unit_struct'], str)\n\ \n\ val = val['val_struct']\n\ assert len(val) == 4\n\ From 21a68f2d2328fe39320df0a27624a16ac01aba7f Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Thu, 4 Jun 2026 03:19:32 +0200 Subject: [PATCH 24/25] Enable ThreadedTests.test_ssl_in_multiple_threads --- Lib/test/test_ssl.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index e05822dcf57..e8e5d9352e9 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -2867,7 +2867,6 @@ def test_echo(self): '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. From 8a43b8ef059f4b3b47b6fd1ef58dcc648e46250c Mon Sep 17 00:00:00 2001 From: Ivan Mironov Date: Tue, 2 Jun 2026 00:19:43 +0200 Subject: [PATCH 25/25] Workaround for test.test_ssl.ThreadedTests.test_wrong_cert_tls* Failure: 2026-06-01T20:33:22.6137502Z ====================================================================== 2026-06-01T20:33:22.6138687Z FAIL: test_wrong_cert_tls13 (test.test_ssl.ThreadedTests.test_wrong_cert_tls13) 2026-06-01T20:33:22.6139079Z ---------------------------------------------------------------------- 2026-06-01T20:33:22.6139922Z ConnectionAbortedError: [WinError 10053] An established connection was aborted by the software in your host machine. (os error 10053) 2026-06-01T20:33:22.6140076Z 2026-06-01T20:33:22.6140508Z During handling of the above exception, another exception occurred: 2026-06-01T20:33:22.6140757Z 2026-06-01T20:33:22.6141051Z Traceback (most recent call last): 2026-06-01T20:33:22.6141607Z File "D:\a\RustPython\RustPython\Lib\test\test_ssl.py", line 238, in wrapper 2026-06-01T20:33:22.6141895Z return func(*args, **kw) 2026-06-01T20:33:22.6142621Z File "D:\a\RustPython\RustPython\Lib\test\test_ssl.py", line 3267, in test_wrong_cert_tls13 2026-06-01T20:33:22.6142922Z with self.assertRaisesRegex( 2026-06-01T20:33:22.6143512Z ~~~~~~~~~~~~~~~~~~~~~~^ 2026-06-01T20:33:22.6143779Z OSError, 2026-06-01T20:33:22.6144033Z ^^^^^^^^ 2026-06-01T20:33:22.6144283Z ...<2 lines>... 2026-06-01T20:33:22.6144550Z 'Broken pipe' 2026-06-01T20:33:22.6144813Z ^^^^^^^^^^^^^ 2026-06-01T20:33:22.6145053Z ): 2026-06-01T20:33:22.6145328Z ^ 2026-06-01T20:33:22.6146900Z AssertionError: "alert unknown ca|EOF occurred|TLSV1_ALERT_UNKNOWN_CA|closed by the remote host|Connection reset by peer|Broken pipe" does not match "[WinError 10053] An established connection was aborted by the software in your host machine. (os error 10053)" There are two problems here: 1) WSAECONNABORTED is reported by some socket operation but test case checks only for stringified WSAECONNRESET. Both test_wrong_cert_tls* tests check for various kinds of connection-related errors so I suppose that those tests are problematic even in cpython (there are comments about this too). 2) RustPython produces wrong strings for OSError. cpython has a separate mapping for this: https://github.com/python/cpython/blob/5607950ef232dad16d75c0cf53101d9649d89115/Modules/errnomodule.c --- crates/stdlib/src/rustls.rs | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/stdlib/src/rustls.rs b/crates/stdlib/src/rustls.rs index 4e4d5898a92..17081c43462 100644 --- a/crates/stdlib/src/rustls.rs +++ b/crates/stdlib/src/rustls.rs @@ -75,6 +75,8 @@ use x509_parser::{ x509::X509Name, }; +use rustpython_host_env::errno; + use crate::{ common::lock::LazyLock, vm::{ @@ -2948,9 +2950,34 @@ impl Io { Ok(value) => Ok(value), Err(err) => match err.kind() { - std::io::ErrorKind::Other => Err(SslError::Py( - io.error.take().expect("BUG: Io.error is not set"), - )), + std::io::ErrorKind::Other => { + let err = io.error.take().expect("BUG: Io.error is not set"); + + // This is an ugly hack for test.test_ssl.ThreadedTests.test_wrong_cert_tls* + const ERRNO_INTO_ECONNRESET: &[i32] = &[ + errno::errors::ECONNRESET, + errno::errors::EPIPE, + errno::errors::ECONNABORTED, + ]; + if err.fast_isinstance(vm.ctx.exceptions.os_error) + && err + .as_object() + .get_attr("errno", vm) + .and_then(|e| e.try_into_value::(vm)) + .is_ok_and(|e| ERRNO_INTO_ECONNRESET.contains(&e)) + { + Err(SslError::Py( + vm.new_os_subtype_error( + vm.ctx.exceptions.connection_reset_error.to_owned(), + Some(errno::errors::ECONNRESET), + "Connection reset by peer", + ) + .upcast(), + )) + } else { + Err(SslError::Py(err)) + } + } std::io::ErrorKind::InvalidData => { // ConnectionCommon::complete_io() wraps TLS processing errors in InvalidData.