diff --git a/Cargo.lock b/Cargo.lock index 8ab53ac5bd0..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", @@ -3881,7 +3766,8 @@ dependencies = [ "rustpython-sre_engine", "rustyline", "scopeguard", - "serde_core", + "serde", + "serde_bytes", "static_assertions", "strum", "strum_macros", @@ -3913,8 +3799,8 @@ dependencies = [ "rustpython-pylib", "rustpython-stdlib", "rustpython-vm", + "serde", "serde-wasm-bindgen", - "serde_core", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -4058,6 +3944,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" @@ -4100,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" @@ -4118,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]] @@ -4129,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]] @@ -4139,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", ] @@ -4150,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", ] @@ -4169,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" @@ -4234,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" @@ -4251,7 +4117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der 0.8.0", + "der", ] [[package]] @@ -4506,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" @@ -4912,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" @@ -5314,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" @@ -5450,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 81ecbf86172..d47f67b64c9 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"] @@ -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", @@ -197,7 +196,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 +271,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,11 +284,12 @@ 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 = { 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" @@ -326,9 +324,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/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_sendfile.py b/Lib/test/test_asyncio/test_sendfile.py index e266d57742a..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): @@ -577,10 +573,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 +580,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() 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_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 diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 7cfbe0c97dc..e8e5d9352e9 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.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: @@ -1481,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: @@ -1492,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() @@ -2092,6 +2095,7 @@ def test_ciphers(self): cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") s.connect(self.server_addr) + @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) @@ -2863,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. @@ -3071,6 +3074,7 @@ def test_ecc_cert(self): @unittest.skipUnless(IS_OPENSSL_3_0_0, "test requires RFC 5280 check added in OpenSSL 3.0+") + @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() @@ -3996,7 +4000,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 +4262,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 +4275,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 +4294,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 +4359,7 @@ def test_sendfile(self): s.sendfile(file) self.assertEqual(s.recv(1024), TEST_DATA) - @unittest.expectedFailure # TODO: RUSTPYTHON + @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 @@ -4417,7 +4417,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.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() @@ -5036,7 +5036,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.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 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/rustls-data/obj_mac.num b/crates/stdlib/rustls-data/obj_mac.num new file mode 100644 index 00000000000..e72170b47f7 --- /dev/null +++ b/crates/stdlib/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/rustls-data/objects.txt b/crates/stdlib/rustls-data/objects.txt new file mode 100644 index 00000000000..946cdf5ec68 --- /dev/null +++ b/crates/stdlib/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 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..17081c43462 --- /dev/null +++ b/crates/stdlib/src/rustls.rs @@ -0,0 +1,5308 @@ +// 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 rustpython_host_env::errno; + +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, 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")] + 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 args = LoadVerifyLocationsArgs { + cafile, + 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 = 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_NONE, + 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::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::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)) + .map_err(|e| e.into_py_err(vm))?; + } + + 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 hello = accepted.client_hello(); + + // 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(|| { + 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; + } + } + }; + + // 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)? { + Ok(conn) => conn, + Err((err, alert)) => { + *state = State::new_alert_from_rustls_error(err, alert, vm)?; + continue; + } + }; + *state = State::HasConnection { + state: ConnectionState::Handshaking, + conn, + }; + } + + 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..])) + .map_err(|e| e.into_py_err(vm))?; + *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_with_sending_alert_on_error(conn, conn_state, true, vm)?; + *conn_state = ConnectionState::Connected(CloseNotifyState::None); + break Ok(()); + } + + State::HasConnection { + state: + ConnectionState::Connected(_) + | ConnectionState::ShuttingDown + | 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()); + } + }; + } + } + + #[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)); + } + + State::HasConnection { + state: ConnectionState::SendingAlertAfterError(err), + conn, + } => { + self.send_alert_after_error(conn, vm)?; + return Err(err.clone()); + } + }; + + // 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_with_sending_alert_on_error(conn, conn_state, 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, conn_state) = match &mut *state { + State::ServerWaitingForClientHello(_) + | State::ServerSendingAlert { .. } + | State::HasConnection { + state: ConnectionState::Handshaking, + .. + } => { + unreachable!("BUG: write() is in wrong state") + } + + State::HasConnection { + state: + conn_state @ ConnectionState::Connected( + CloseNotifyState::None | CloseNotifyState::Received, + ), + conn, + } => (conn, conn_state), + + State::HasConnection { + state: + ConnectionState::Connected(CloseNotifyState::Sent) + | ConnectionState::ShuttingDown + | ConnectionState::ShutDown, + .. + } => { + 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_with_sending_alert_on_error(conn, conn_state, 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_with_sending_alert_on_error(conn, conn_state, 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) + .map_err(|e| e.into_py_err(vm))?; + *conn_state = ConnectionState::ShutDown; + break; + } + + State::HasConnection { + state: ConnectionState::ShutDown, + .. + } => { + 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, + ) -> 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() { + let mut io = self.io.write(); + let _ = io.with_io(vm, |io| conn.complete_io(io))?; + } + 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 + .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 = 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()) + } + + #[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]; + 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 { + // 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, + }, +} + +#[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), +} + +#[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::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 + | ConnectionState::SendingAlertAfterError(_), + 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 + | ConnectionState::SendingAlertAfterError(_), + 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) -> SslResult + 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 => { + 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. + let err = err + .downcast::() + .expect("BUG: Not a rustls Error"); + Err(SslError::Rustls(err)) + } + + _ => Err(SslError::Io(err)), + }, + } + } + + 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 = 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..d79c7feacac 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,29 @@ 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), + /// rustls error + Rustls(rustls::Error), /// 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, } impl SslError { @@ -215,54 +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), - } - } - } - } - // OpenSSL 3.0 changed transport EOF from SSL_ERROR_SYSCALL with - // zero return value to SSL_ERROR_SSL with SSL_R_UNEXPECTED_EOF_WHILE_READING. - // In rustls, these cases correspond to unexpected connection closure: - rustls::Error::InvalidMessage(_) => { - // UnexpectedMessage, CorruptMessage, etc. → SSLEOFError - // Matches CPython's "EOF occurred in violation of protocol" - Self::Eof - } - rustls::Error::PeerIncompatible(peer_err) => { - // Check for specific incompatibility types - use rustls::PeerIncompatible; - match peer_err { - PeerIncompatible::NoCipherSuitesInCommon => { - // Maps to OpenSSL SSL_R_NO_SHARED_CIPHER (lib=20, reason=193) - Self::NoCipherSuites - } - _ => { - // Other protocol incompatibilities → SSLEOFError - Self::Eof - } - } - } - _ => Self::Ssl(format!("{err}")), - } - } - /// Create SSLError with library and reason from string values /// /// This is the base helper for creating SSLError with _library and _reason @@ -335,12 +268,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,32 +307,73 @@ 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::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) => { + 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") - } Self::Io(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => { create_ssl_eof_error(vm).upcast() } @@ -386,1754 +385,9 @@ 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) - } } } } 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/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..546f7b268aa --- /dev/null +++ b/crates/vm/src/convert/rust_py_serde.rs @@ -0,0 +1,778 @@ +use core::{error::Error, fmt}; + +use serde::ser::{ + Serialize, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, + SerializeTupleStruct, SerializeTupleVariant, Serializer, +}; + +use crate::{ + PyObjectRef, VirtualMachine, + builtins::{PyBaseExceptionRef, PyDictRef}, +}; + +// TODO: Add a shortcut implementation for `py_serde::PyObjectSerializer`. +/// 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, +} + +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 { + name.serialize(self) + } + + 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 crate::{Interpreter, convert::RustPySerDeConf, py_serde::PyObjectSerializer}; + + 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, + val_unit_struct: TestUnitStruct, + } + + #[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 }, + } + + #[derive(Serialize)] + struct TestUnitStruct; + + #[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, + }, + }, + val_unit_struct: TestUnitStruct, + }; + + 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) == 25\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\ + 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\ + 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)); + }); + } + + #[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)); + }); + } +} 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 7775c34e053..26f6c9e52af 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::{ @@ -60,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 @@ -589,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, @@ -639,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)?); @@ -2241,6 +2278,36 @@ impl VirtualMachine { let s = unsafe { OsString::from_encoded_bytes_unchecked(bytes) }; 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 + F: FnOnce(&RustPySerDe<'a>) -> Result, + { + 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 + 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 { @@ -2268,12 +2335,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" @@ -2297,8 +2364,6 @@ mod tests { #[test] fn frozen_origname_matches() { - use rustpython_vm as vm; - vm::Interpreter::builder(Default::default()) .build() .enter(|vm| { @@ -2319,4 +2384,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, + ) + } } 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/extra_tests/snippets/stdlib_ssl.py b/extra_tests/snippets/stdlib_ssl.py new file mode 100644 index 00000000000..61201eb9684 --- /dev/null +++ b/extra_tests/snippets/stdlib_ssl.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 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,