diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 81961f80385..5d6cc6120cd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,7 +19,7 @@ concurrency: cancel-in-progress: true env: - CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls,host_env + CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env CARGO_ARGS_NO_SSL: --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,host_env # Crates excluded from workspace builds: # - rustpython_wasm: requires wasm target @@ -186,6 +186,7 @@ jobs: skip_ssl: true - os: ubuntu-latest target: wasm32-wasip2 + skip_ssl: true - os: ubuntu-latest target: x86_64-unknown-freebsd skip_ssl: true @@ -760,7 +761,7 @@ jobs: clang: true - name: build rustpython - run: cargo build --release --target wasm32-wasip1 --features freeze-stdlib,stdlib --verbose + run: cargo build --release --target wasm32-wasip1 --no-default-features --features freeze-stdlib,stdlib,stdio,importlib,host_env --verbose - name: run snippets run: wasmer run --dir "$(pwd)" target/wasm32-wasip1/release/rustpython.wasm -- "$(pwd)/extra_tests/snippets/stdlib_random.py" - name: run cpython unittest diff --git a/.github/workflows/cron-ci.yaml b/.github/workflows/cron-ci.yaml index b925db85fe9..ac1a1cadbfb 100644 --- a/.github/workflows/cron-ci.yaml +++ b/.github/workflows/cron-ci.yaml @@ -14,7 +14,7 @@ on: - .github/workflows/cron-ci.yaml env: - CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls,jit,host_env + CARGO_ARGS: --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls-aws-lc,jit,host_env FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' # TODO: Remove on 2026/06/02 jobs: @@ -41,7 +41,7 @@ jobs: - run: sudo apt-get update && sudo apt-get -y install lcov - name: Run cargo-llvm-cov with Rust tests. - run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --verbose --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls,jit,host_env + run: cargo llvm-cov --no-report --workspace --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher --verbose --no-default-features --features stdlib,importlib,stdio,encodings,ssl-rustls-aws-lc,jit,host_env - name: Run cargo-llvm-cov with Python snippets. run: python scripts/cargo-llvm-cov.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4494efcf8e7..be92c5e4704 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,7 +68,7 @@ jobs: libtool: true - name: Build RustPython - run: cargo build --release --target=${{ matrix.target }} --verbose --no-default-features --features stdlib,stdio,importlib,encodings,sqlite,host_env,ssl-rustls,threading,jit + run: cargo build --release --target=${{ matrix.target }} --verbose --no-default-features --features stdlib,stdio,importlib,encodings,sqlite,host_env,ssl-rustls-aws-lc,threading,jit - name: Rename Binary run: cp target/${{ matrix.target }}/release/rustpython target/rustpython-release-${{ runner.os }}-${{ matrix.target }} diff --git a/.github/workflows/update-caches.yml b/.github/workflows/update-caches.yml index 53e857246dc..392717e5a2c 100644 --- a/.github/workflows/update-caches.yml +++ b/.github/workflows/update-caches.yml @@ -19,7 +19,7 @@ env: CARGO_PROFILE_TEST_DEBUG: 0 CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_RELEASE_DEBUG: 0 - CARGO_ARGS: --workspace --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls,host_env,threading,jit --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher + CARGO_ARGS: --workspace --no-default-features --features stdlib,importlib,stdio,encodings,sqlite,ssl-rustls-aws-lc,host_env,threading,jit --exclude rustpython_wasm --exclude rustpython-compiler-source --exclude rustpython-venvlauncher jobs: build-caches: diff --git a/Cargo.lock b/Cargo.lock index 4f211e65a19..8296b2a465e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -256,7 +256,6 @@ checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-fips-sys", "aws-lc-sys", - "untrusted 0.7.1", "zeroize", ] @@ -694,6 +693,17 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-models" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "657f625ff361906f779745d08375ae3cc9fef87a35fba5f22874cf773010daf4" +dependencies = [ + "hax-lib", + "pastey", + "rand 0.9.4", +] + [[package]] name = "cpubits" version = "0.1.1" @@ -1318,7 +1328,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared 0.1.1", + "foreign-types-shared", ] [[package]] @@ -1327,12 +1337,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - [[package]] name = "fs_extra" version = "1.3.0" @@ -1472,6 +1476,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "graviola" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4387e0458389da24c6fe732531e65595c7c4a32b027f98f4789e512e28224465" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", +] + [[package]] name = "half" version = "2.7.1" @@ -1507,6 +1521,43 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hax-lib" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543f93241d32b3f00569201bfce9d7a93c92c6421b23c77864ac929dc947b9fc" +dependencies = [ + "hax-lib-macros", + "num-bigint", + "num-traits", +] + +[[package]] +name = "hax-lib-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8755751e760b11021765bb04cb4a6c4e24742688d9f3aa14c2079638f537b0f" +dependencies = [ + "hax-lib-macros-types", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "hax-lib-macros-types" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f177c9ae8ea456e2f71ff3c1ea47bf4464f772a05133fcbba56cd5ba169035a2" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "heck" version = "0.5.0" @@ -1986,6 +2037,70 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libcrux-intrinsics" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b5db005ff8001e026b73a6842ee81bbef8ec5ff0e1915a67ae65fd2a9fafa5" +dependencies = [ + "core-models", + "hax-lib", +] + +[[package]] +name = "libcrux-ml-kem" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14ab3e477de9df6ee1273a114018ff62c4996ca9220070c4e5cb1743f94a67d" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-secrets", + "libcrux-sha3", + "libcrux-traits", +] + +[[package]] +name = "libcrux-platform" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4" +dependencies = [ + "libc", +] + +[[package]] +name = "libcrux-secrets" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b" +dependencies = [ + "hax-lib", +] + +[[package]] +name = "libcrux-sha3" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ae0b7d0e1cc4793a609fd0ff2ca3b3a3fabae523770c619a3d4bc86417b0d7" +dependencies = [ + "hax-lib", + "libcrux-intrinsics", + "libcrux-platform", + "libcrux-traits", +] + +[[package]] +name = "libcrux-traits" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e4fa89f3f5e34b47f928b22b1b78395a0d4ec23b1f583db635f128159d65f" +dependencies = [ + "libcrux-secrets", + "rand 0.9.4", +] + [[package]] name = "libffi" version = "5.1.0" @@ -2532,6 +2647,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -2781,6 +2902,28 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "proc-macro-utils" version = "0.10.0" @@ -3171,7 +3314,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.17", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -3226,6 +3369,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-graviola" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323c712e50c59ceb2ba9ad4d79dcfd3e0046a082d61efa87fcdf8f59af04473c" +dependencies = [ + "graviola", + "libcrux-ml-kem", + "rustls", +] + [[package]] name = "rustls-native-certs" version = "0.8.3" @@ -3292,7 +3446,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -3308,6 +3462,8 @@ dependencies = [ "libc", "log", "pyo3", + "rustls", + "rustls-graviola", "rustpython-capi", "rustpython-compiler", "rustpython-pylib", @@ -3590,7 +3746,6 @@ version = "0.5.0" dependencies = [ "adler32", "ascii", - "aws-lc-rs", "base64", "blake2", "bzip2", @@ -3605,7 +3760,7 @@ dependencies = [ "dyn-clone", "flame", "flate2", - "foreign-types-shared 0.3.1", + "foreign-types-shared", "gethostname", "hex", "hmac 0.12.1", @@ -4537,12 +4692,6 @@ dependencies = [ "rand 0.8.6", ] -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -4574,6 +4723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "atomic", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] diff --git a/Cargo.toml b/Cargo.toml index 98b6cb1e1db..916804bd951 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true [features] capi = ["dep:rustpython-capi", "threading"] -default = ["threading", "stdlib", "stdio", "importlib", "ssl-rustls", "host_env"] +default = ["threading", "stdlib", "stdio", "importlib", "ssl-rustls-aws-lc", "host_env"] host_env = ["rustpython-vm/host_env", "rustpython-stdlib?/host_env"] importlib = ["rustpython-vm/importlib"] encodings = ["rustpython-vm/encodings"] @@ -22,8 +22,10 @@ freeze-stdlib = ["stdlib", "rustpython-vm/freeze-stdlib", "rustpython-pylib?/fre jit = ["rustpython-vm/jit"] threading = ["rustpython-vm/threading", "rustpython-stdlib/threading"] sqlite = ["rustpython-stdlib/sqlite"] -ssl = [] +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-fips = ["ssl-rustls-aws-lc", "rustls/fips"] ssl-openssl = ["ssl", "rustpython-stdlib/ssl-openssl"] ssl-vendor = ["ssl-openssl", "rustpython-stdlib/ssl-vendor"] tkinter = ["rustpython-stdlib/tkinter"] @@ -46,6 +48,9 @@ dirs = "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 } @@ -70,6 +75,17 @@ harness = false name = "rustpython" path = "src/main.rs" +[[example]] +name = "custom_tls_providers" +path = "examples/custom_tls_providers.rs" +required-features = [ + "rustls-graviola", + "rustls/ring", + "rustpython-pylib/freeze-stdlib", + "rustpython-stdlib/ssl-rustls", + "rustpython-vm/freeze-stdlib", +] + [profile.dev.package."*"] opt-level = 3 @@ -178,7 +194,6 @@ phf = { version = "0.13.1", default-features = false, features = ["macros"]} adler32 = "1.2.0" approx = "0.5.1" ascii = "1.1" -aws-lc-rs = "1.16.3" base64 = "0.22" blake2 = "0.10.4" bitflags = "2.11.0" @@ -202,7 +217,7 @@ exitcode = "1.1.2" flame = "0.2.2" flamer = "0.5" flate2 = { version = "1.1.9", default-features = false } -foreign-types-shared = "0.3.1" +foreign-types-shared = "0.1" gethostname = "1.0.2" getrandom = { version = "0.3", features = ["std"] } glob = "0.3" @@ -263,6 +278,7 @@ rapidhash = "4.4.1" 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-platform-verifier = "0.7" diff --git a/README.md b/README.md index 6949c6e66e2..3a27f77a570 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ rustpython ### SSL provider -For HTTPS requests, `ssl-rustls` feature is enabled by default. You can replace it with `ssl-openssl` feature if your environment requires OpenSSL. +For HTTPS requests, `ssl-rustls-aws-lc` is enabled by default for the RustPython binary. Embedders can use `rustpython-stdlib`'s provider-agnostic `ssl-rustls` feature and install their own rustls crypto provider, or replace rustls with `ssl-openssl` if their environment requires OpenSSL. Note that to use OpenSSL on Windows, you may need to install OpenSSL, or you can enable the `ssl-vendor` feature instead, which compiles OpenSSL for you but requires a C compiler, perl, and `make`. OpenSSL version 3 is expected and tested in CI. Older versions may not work. diff --git a/crates/stdlib/Cargo.toml b/crates/stdlib/Cargo.toml index 88afc243274..714127ae31e 100644 --- a/crates/stdlib/Cargo.toml +++ b/crates/stdlib/Cargo.toml @@ -16,15 +16,16 @@ host_env = ["rustpython-vm/host_env"] compiler = ["rustpython-vm/compiler"] threading = ["rustpython-common/threading", "rustpython-vm/threading"] sqlite = ["dep:libsqlite3-sys"] -# SSL backends - default to rustls -ssl = [] -ssl-rustls = ["ssl", "rustls", "rustls-native-certs", "rustls-pemfile", "rustls-platform-verifier", "x509-cert", "x509-parser", "der", "pem-rfc7468", "webpki-roots", "aws-lc-rs", "oid-registry", "pkcs8"] -ssl-rustls-fips = ["ssl-rustls", "aws-lc-rs/fips"] +# SSL backends +ssl = ["host_env"] +ssl-rustls = ["__ssl-rustls", "rustls/custom-provider"] ssl-openssl = ["ssl", "openssl", "openssl-sys", "foreign-types-shared", "openssl-probe"] ssl-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"] + [dependencies] # rustpython crates rustpython-derive = { workspace = true } @@ -113,7 +114,7 @@ openssl-probe = { workspace = true, optional = true } foreign-types-shared = { workspace = true, optional = true } # Rustls dependencies (optional, for ssl-rustls feature) -rustls = { workspace = true, default-features = false, features = ["std", "tls12", "aws_lc_rs"], optional = true } +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-platform-verifier = { workspace = true, optional = true } @@ -122,7 +123,6 @@ 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 } -aws-lc-rs = { workspace = true, optional = true } oid-registry = { workspace = true, features = ["x509", "pkcs1", "nist_algs"], optional = true } pkcs8 = { workspace = true, features = ["encryption", "pkcs5", "pem"], optional = true } diff --git a/crates/stdlib/src/lib.rs b/crates/stdlib/src/lib.rs index 4670b07f06c..b1ae53af853 100644 --- a/crates/stdlib/src/lib.rs +++ b/crates/stdlib/src/lib.rs @@ -128,11 +128,11 @@ mod openssl; #[cfg(all( feature = "host_env", not(target_arch = "wasm32"), - feature = "ssl-rustls" + feature = "__ssl-rustls" ))] -mod ssl; +pub mod ssl; -#[cfg(all(feature = "ssl-openssl", feature = "ssl-rustls", not(clippy)))] +#[cfg(all(feature = "ssl-openssl", feature = "__ssl-rustls", not(clippy)))] compile_error!(r#"features "ssl-openssl" and "ssl-rustls" are mutually exclusive"#); #[cfg(all( @@ -246,7 +246,7 @@ pub fn stdlib_module_defs(ctx: &Context) -> Vec<&'static builtins::PyModuleDef> #[cfg(all( feature = "host_env", not(target_arch = "wasm32"), - feature = "ssl-rustls" + feature = "__ssl-rustls" ))] ssl::module_def(ctx), statistics::module_def(ctx), diff --git a/crates/stdlib/src/openssl.rs b/crates/stdlib/src/openssl.rs index f559726c224..a7f9cb2a49d 100644 --- a/crates/stdlib/src/openssl.rs +++ b/crates/stdlib/src/openssl.rs @@ -2292,12 +2292,17 @@ mod _ssl { self.0.get_timeout().map(|d| Instant::now() + d) } - fn select(&self, needs: SslNeeds, deadline: &SocketDeadline) -> SelectRet { + fn select( + &self, + needs: SslNeeds, + deadline: &SocketDeadline, + vm: &VirtualMachine, + ) -> SelectRet { let sock = match self.0.sock_opt() { Some(s) => s, None => return SelectRet::Closed, }; - // For blocking sockets without timeout, call sock_select with None timeout + // For blocking sockets without timeout, call sock_wait with None timeout // to actually block waiting for data instead of busy-looping let timeout = match &deadline { Ok(deadline) => match deadline.checked_duration_since(Instant::now()) { @@ -2307,13 +2312,14 @@ mod _ssl { Err(true) => None, // Blocking: no timeout, wait indefinitely Err(false) => return SelectRet::Nonblocking, }; - let res = socket::sock_select( + let res = socket::sock_wait( &sock, match needs { - SslNeeds::Read => socket::SelectKind::Read, - SslNeeds::Write => socket::SelectKind::Write, + SslNeeds::Read => socket::SockWaitKind::Read, + SslNeeds::Write => socket::SockWaitKind::Write, }, timeout, + vm, ); match res { Ok(true) => SelectRet::TimedOut, @@ -2325,13 +2331,14 @@ mod _ssl { &self, err: &ssl::Error, deadline: &SocketDeadline, + vm: &VirtualMachine, ) -> (Option, SelectRet) { let needs = match err.code() { ssl::ErrorCode::WANT_READ => Some(SslNeeds::Read), ssl::ErrorCode::WANT_WRITE => Some(SslNeeds::Write), _ => None, }; - let state = needs.map_or(SelectRet::Ok, |needs| self.select(needs, deadline)); + let state = needs.map_or(SelectRet::Ok, |needs| self.select(needs, deadline, vm)); (needs, state) } } @@ -2850,7 +2857,7 @@ mod _ssl { break; } // Wait briefly for peer's close_notify before retrying - match socket_stream.select(SslNeeds::Read, &deadline) { + match socket_stream.select(SslNeeds::Read, &deadline, vm) { SelectRet::TimedOut => { return Err(socket::timeout_error_msg( vm, @@ -2888,7 +2895,7 @@ mod _ssl { }; // Wait on the socket - match socket_stream.select(needs, &deadline) { + match socket_stream.select(needs, &deadline, vm) { SelectRet::TimedOut => { let msg = if err == sys::SSL_ERROR_WANT_READ { "The read operation timed out" @@ -2984,7 +2991,7 @@ mod _ssl { let (needs, state) = stream .get_ref() .expect("handshake called in bio mode; should only be called in socket mode") - .socket_needs(&err, &timeout); + .socket_needs(&err, &timeout, vm); match state { SelectRet::TimedOut => { // Clean up SNI ex_data before returning error @@ -3038,7 +3045,7 @@ mod _ssl { .get_ref() .expect("write called in bio mode; should only be called in socket mode"); let timeout = socket_ref.timeout_deadline(); - let state = socket_ref.select(SslNeeds::Write, &timeout); + let state = socket_ref.select(SslNeeds::Write, &timeout, vm); match state { SelectRet::TimedOut => { return Err(socket::timeout_error_msg( @@ -3058,7 +3065,7 @@ mod _ssl { let (needs, state) = stream .get_ref() .expect("write called in bio mode; should only be called in socket mode") - .socket_needs(&err, &timeout); + .socket_needs(&err, &timeout, vm); match state { SelectRet::TimedOut => { return Err(socket::timeout_error_msg( @@ -3229,7 +3236,7 @@ mod _ssl { let (needs, state) = stream .get_ref() .expect("read called in bio mode; should only be called in socket mode") - .socket_needs(&err, &timeout); + .socket_needs(&err, &timeout, vm); match state { SelectRet::TimedOut => { return Err(socket::timeout_error_msg( diff --git a/crates/stdlib/src/ssl.rs b/crates/stdlib/src/ssl.rs index 8548220380f..d36481a0062 100644 --- a/crates/stdlib/src/ssl.rs +++ b/crates/stdlib/src/ssl.rs @@ -25,6 +25,9 @@ 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)] @@ -64,7 +67,6 @@ mod _ssl { sync::atomic::{AtomicUsize, Ordering}, time::Duration, }; - use rustls::crypto::aws_lc_rs::ALL_CIPHER_SUITES; use std::{ collections::{HashMap, hash_map::DefaultHasher}, io::BufRead, @@ -98,6 +100,8 @@ mod _ssl { 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) @@ -635,7 +639,7 @@ mod _ssl { return Err("No cipher can be selected".to_string()); } - let all_suites = ALL_CIPHER_SUITES; + let all_suites = CryptoExt::get_ext().all_ciphers_or_default(); let mut selected = Vec::new(); for part in cipher_str.split(':') { @@ -1058,6 +1062,8 @@ mod _ssl { #[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)?; @@ -1200,15 +1206,14 @@ mod _ssl { } // Additional validation: Create CertifiedKey to ensure rustls accepts it - let signing_key = - rustls::crypto::aws_lc_rs::sign::any_supported_type(&key).map_err(|_| { - vm.new_os_subtype_error( - PySSLError::class(&vm.ctx).to_owned(), - None, - "[SSL: KEY_VALUES_MISMATCH] key values mismatch", - ) - .upcast() - })?; + 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() { @@ -1523,7 +1528,8 @@ mod _ssl { // Dynamically generate cipher list from rustls ALL_CIPHER_SUITES // This automatically includes all cipher suites supported by the current rustls version - let cipher_list = ALL_CIPHER_SUITES + let cipher_list = CryptoExt::get_ext() + .all_ciphers_or_default() .iter() .map(|suite| { // Extract cipher information using unified helper @@ -2217,6 +2223,8 @@ mod _ssl { (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 @@ -2309,7 +2317,7 @@ mod _ssl { rustls_server_session_store: rustls::server::ServerSessionMemoryCache::new( SSL_SESSION_CACHE_SIZE, ), - server_ticketer: rustls::crypto::aws_lc_rs::Ticketer::new() + 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), @@ -4864,35 +4872,34 @@ mod _ssl { #[pyfunction] fn RAND_status() -> i32 { - 1 // Always have good randomness with aws-lc-rs + 1 // The configured rustls provider supplies cryptographic randomness. } #[pyfunction] fn RAND_add(_string: PyObjectRef, _entropy: f64) { - // No-op: aws-lc-rs handles its own entropy + // 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 { - use aws_lc_rs::rand::{SecureRandom, SystemRandom}; - // 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 rng = SystemRandom::new(); let mut buf = vec![0u8; n_usize]; - rng.fill(&mut buf) + 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)> { - // In rustls/aws-lc-rs, all random bytes are cryptographically strong + // Rustls providers expose cryptographically strong random bytes. let bytes = RAND_bytes(n, vm)?; Ok((bytes, true)) } diff --git a/crates/stdlib/src/ssl/cert.rs b/crates/stdlib/src/ssl/cert.rs index b15ee04011c..e304781b644 100644 --- a/crates/stdlib/src/ssl/cert.rs +++ b/crates/stdlib/src/ssl/cert.rs @@ -22,7 +22,10 @@ use rustpython_vm::{PyObjectRef, PyResult, VirtualMachine}; use std::collections::HashSet; use x509_parser::prelude::*; -use super::_ssl::{VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT}; +use super::{ + _ssl::{VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT}, + providers::CryptoExt, +}; // Certificate Verification Constants @@ -1268,9 +1271,7 @@ pub(super) fn validate_cert_key_match( // For rustls, the actual validation happens when creating CertifiedKey // We can attempt to create a signing key to verify the key is valid - use rustls::crypto::aws_lc_rs::sign::any_supported_type; - - match any_supported_type(private_key) { + 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 diff --git a/crates/stdlib/src/ssl/compat.rs b/crates/stdlib/src/ssl/compat.rs index 2d6bb369e9a..7ed65ce8f4c 100644 --- a/crates/stdlib/src/ssl/compat.rs +++ b/crates/stdlib/src/ssl/compat.rs @@ -20,22 +20,24 @@ use crate::vm::VirtualMachine; use alloc::sync::Arc; use parking_lot::RwLock as ParkingRwLock; use rustls::Connection; -use rustls::RootCertStore; use rustls::client::ClientConfig; -use rustls::crypto::SupportedKxGroup; +use rustls::crypto::{CryptoProvider, SupportedKxGroup}; use rustls::pki_types::{CertificateDer, CertificateRevocationListDer, PrivateKeyDer}; -use rustls::server::ResolvesServerCert; -use rustls::server::ServerConfig; +use rustls::server::{ProducesTickets, ResolvesServerCert, ServerConfig, WebPkiClientVerifier}; use rustls::sign::CertifiedKey; +use rustls::{RootCertStore, SupportedCipherSuite}; use rustpython_vm::builtins::{PyBaseException, PyBaseExceptionRef}; use rustpython_vm::convert::IntoPyException; use rustpython_vm::function::ArgBytesLike; use rustpython_vm::{AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject}; use std::io::Read; -use std::sync::Once; + +use super::providers::CryptoExt; // Import PySSLSocket from parent module -use super::_ssl::{PySSLSocket, SSL3_RT_MAX_PACKET_SIZE, VERIFY_X509_STRICT}; +use super::_ssl::{ + PySSLSocket, SSL3_RT_MAX_PACKET_SIZE, VERIFY_X509_PARTIAL_CHAIN, VERIFY_X509_STRICT, +}; // Import error types and helper functions from error module use super::error::{ @@ -43,23 +45,6 @@ use super::error::{ create_ssl_want_read_error, create_ssl_want_write_error, create_ssl_zero_return_error, }; -// CryptoProvider Initialization: - -/// Ensure the default CryptoProvider is installed (thread-safe, runs once) -/// -/// This is necessary because rustls 0.23+ requires a process-level CryptoProvider -/// to be installed before using default_provider(). We use Once to ensure this -/// happens exactly once, even if called from multiple threads. -static INIT_PROVIDER: Once = Once::new(); - -fn ensure_default_provider() { - INIT_PROVIDER.call_once(|| { - let _ = rustls::crypto::CryptoProvider::install_default( - rustls::crypto::aws_lc_rs::default_provider(), - ); - }); -} - // OpenSSL Constants: // OpenSSL error library codes (include/openssl/err.h) @@ -449,7 +434,7 @@ pub(super) struct ServerConfigOptions { /// Session storage for server-side session resumption pub session_storage: Option>, /// Shared ticketer for TLS 1.2 session tickets (stateless resumption) - pub ticketer: Option>, + pub ticketer: Option>, } /// Options for creating a client TLS configuration @@ -482,15 +467,14 @@ pub(super) struct ClientConfigOptions { /// 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 { - use rustls::crypto::aws_lc_rs::{ALL_CIPHER_SUITES, ALL_KX_GROUPS}; - let default_provider = rustls::crypto::aws_lc_rs::default_provider(); - - Arc::new(rustls::crypto::CryptoProvider { - cipher_suites: cipher_suites.unwrap_or_else(|| ALL_CIPHER_SUITES.to_vec()), - kx_groups: kx_groups.unwrap_or_else(|| ALL_KX_GROUPS.to_vec()), + 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, @@ -502,11 +486,6 @@ fn create_custom_crypto_provider( /// This abstracts the complex rustls ServerConfig building logic, /// matching SSL_CTX initialization for server sockets. pub(super) fn create_server_config(options: ServerConfigOptions) -> Result { - use rustls::server::WebPkiClientVerifier; - - // Ensure default CryptoProvider is installed - ensure_default_provider(); - // Create custom crypto provider using helper function let custom_provider = create_custom_crypto_provider( options.protocol_settings.cipher_suites.clone(), @@ -684,9 +663,6 @@ fn apply_alpn_with_fallback(config_alpn: &mut Vec>, alpn_protocols: &[Ve /// This abstracts the complex rustls ClientConfig building logic, /// matching SSL_CTX initialization for client sockets. pub(super) fn create_client_config(options: ClientConfigOptions) -> Result { - // Ensure default CryptoProvider is installed - ensure_default_provider(); - // Create custom crypto provider using helper function let custom_provider = create_custom_crypto_provider( options.protocol_settings.cipher_suites.clone(), @@ -754,7 +730,6 @@ pub(super) fn create_client_config(options: ClientConfigOptions) -> Result Result, String> { // Get the default crypto provider's key exchange groups - let provider = rustls::crypto::aws_lc_rs::default_provider(); - let all_groups = &provider.kx_groups; + let all_groups = CryptoExt::get_ext().all_kx_or_default(); match curve { // P-256 (also known as secp256r1 or prime256v1) @@ -2149,14 +2123,12 @@ pub(super) fn curve_name_to_kx_group( .map(|g| vec![*g]) .ok_or_else(|| "X25519 not supported by crypto provider".to_owned()), // P-521 (also known as secp521r1 or prime521v1) - // Now supported with aws-lc-rs crypto provider "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 - // Now supported with aws-lc-rs crypto provider "X448" | "x448" => all_groups .iter() .find(|g| g.name() == rustls::NamedGroup::X448) diff --git a/crates/stdlib/src/ssl/providers.rs b/crates/stdlib/src/ssl/providers.rs new file mode 100644 index 00000000000..478d02ff933 --- /dev/null +++ b/crates/stdlib/src/ssl/providers.rs @@ -0,0 +1,132 @@ +//! Utilities for user-settable cryptography providers. +//! +//! This has two main moving parts: [`CryptoProvider`] and [`CryptoExt`]. [`CryptoProvider`] +//! is always implemented by the cryptography crate because it's a trait from Rustls. RustPython +//! needs some extra data such as all of the cipher suites supported by an implementation. +//! The [`CryptoExt`] table stores that extra data if it exists and provides convenience methods +//! as a fallback. +//! +//! Both the [`CryptoProvider`] and [`CryptoExt`] are process-level structs that need to be +//! set before any TLS operations. [`CryptoExt::set_provider`] is thread-safe and runs once. +//! It sets both once per process. + +use alloc::sync::Arc; +use std::sync::OnceLock; + +use rustls::{ + Error, SignatureScheme, SupportedCipherSuite, + crypto::{CryptoProvider, SupportedKxGroup}, + pki_types::PrivateKeyDer, + server::ProducesTickets, + sign::SigningKey, +}; + +static CRYPTO_EXT: OnceLock = OnceLock::new(); + +#[derive(Clone, Copy)] +pub struct CryptoExt { + pub all_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>>, + pub ticketer: fn() -> Result, Error>, +} + +impl CryptoExt { + #[inline] + #[must_use] + pub fn get_ext() -> &'static Self { + CRYPTO_EXT + .get() + .expect("A CryptoProvider must be set before TLS") + } + + #[inline] + #[must_use] + pub fn get_provider() -> &'static CryptoProvider { + CryptoProvider::get_default().expect("A CryptoProvider must be set before TLS") + } + + /// Returns all [`SupportedCipherSuite`] or the provider's defaults. + /// + /// # Panics + /// 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(|| { + CryptoProvider::get_default() + .expect("A CryptoProvider has been set if CryptoExt is set") + .cipher_suites + .as_slice() + }) + } + + /// Returns all [`SupportedKxGroup`] or the provider's defaults. + /// + /// # Panics + /// Panics if a [`CryptoProvider`] hasn't been set. + #[must_use] + pub fn all_kx_or_default(&self) -> &'static [&'static dyn SupportedKxGroup] { + self.all_kx_groups.unwrap_or_else(|| { + CryptoProvider::get_default() + .expect("A CryptoProvider has been set if CryptoExt is set") + .kx_groups + .as_slice() + }) + } + + /// Return the first supported [`SigningKey`] for a [`PrivateKeyDer`]. + /// + /// Ideally, this function should be provided by the backend implementation or + /// the user. This fallback filters out insecure algorithms then picks the first available key + /// if it exists. + /// + /// # Panics + /// Panics if a [`CryptoProvider`] hasn't been set. + pub fn any_supported_key(&self, der: &PrivateKeyDer<'_>) -> Result, Error> { + self.any_supported_key.map_or_else( + || { + let provider = CryptoProvider::get_default() + .expect("A CryptoProvider has been set if CryptoExt is set"); + let key = provider.key_provider.load_private_key(der.clone_key())?; + + for scheme in provider + .signature_verification_algorithms + .mapping + .iter() + .filter_map(|(scheme, _)| { + (!matches!( + scheme, + SignatureScheme::RSA_PKCS1_SHA1 + | SignatureScheme::ECDSA_SHA1_Legacy + | SignatureScheme::Unknown(_), + )) + .then_some(*scheme) + }) + { + if key.choose_scheme(&[scheme]).is_some() { + return Ok(key); + } + } + + Err(Error::General( + "failed to parse private key as RSA, ECDSA, or EdDSA".into(), + )) + }, + |f| f(der), + ) + } + + /// Set a process-level [`CryptoProvider`] and [`CryptoExt`]. + /// + /// A provider must be set before any cryptographic operations. All crypto ops panic if a provider + /// is unset. + pub fn set_provider(provider: CryptoProvider, extension: Self) -> Result<(), Error> { + provider + .install_default() + .map_err(|_| Error::General("A default CryptoProvider is already set".into()))?; + CRYPTO_EXT + .set(extension) + .map_err(|_| Error::General("A CryptoExt is already set".into())) + } +} diff --git a/examples/custom_tls_providers.rs b/examples/custom_tls_providers.rs new file mode 100644 index 00000000000..1f382fc1b84 --- /dev/null +++ b/examples/custom_tls_providers.rs @@ -0,0 +1,65 @@ +//! Example project to demonstrate how to set a custom rustls provider for RustPython. + +// spell-checker: ignore graviola + +use std::env; + +use rustls::crypto::ring; +use rustpython_pylib::FROZEN_STDLIB; +use rustpython_stdlib::{ssl::providers::CryptoExt, stdlib_module_defs}; +use rustpython_vm::Interpreter; + +const SCRIPT: &str = r#" +import urllib.request + +with urllib.request.urlopen("https://python.org") as response: + assert response.status == 200 +"#; + +fn main() { + let provider = env::args() + .skip(1) + .find_map(|arg| match &*arg { + "--ring" => Some("ring"), + "--graviola" => Some("graviola"), + _ => None, + }) + .unwrap_or("ring"); + + match provider { + "ring" => { + let ext = CryptoExt { + all_cipher_suites: Some(ring::ALL_CIPHER_SUITES), + all_kx_groups: Some(ring::ALL_KX_GROUPS), + any_supported_key: Some(ring::sign::any_supported_type), + ticketer: ring::Ticketer::new, + }; + CryptoExt::set_provider(ring::default_provider(), ext).unwrap(); + println!("Using ring for cryptography"); + } + "graviola" => { + let ext = CryptoExt { + all_cipher_suites: Some(rustls_graviola::suites::ALL_CIPHER_SUITES), + all_kx_groups: Some(rustls_graviola::kx::ALL_KX_GROUPS), + any_supported_key: None, + ticketer: rustls_graviola::Ticketer::new, + }; + CryptoExt::set_provider(rustls_graviola::default_provider(), ext).unwrap(); + println!("Using Graviola for cryptography"); + } + unsupported => panic!("Unsupported provider: {unsupported}"), + } + + let builder = Interpreter::builder(Default::default()); + let defs = stdlib_module_defs(&builder.ctx); + let result = builder + .add_native_modules(&defs) + .add_frozen_modules(FROZEN_STDLIB) + .build() + .run(|vm| { + let scope = vm.new_scope_with_builtins(); + vm.run_block_expr(scope, SCRIPT).map(|_| ()) + }); + + assert_eq!(0, result); +} diff --git a/src/interpreter.rs b/src/interpreter.rs index 9060f3a7bec..230192d1e21 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -14,6 +14,8 @@ impl InterpreterBuilderExt for InterpreterBuilder { fn init_stdlib(self) -> Self { let defs = rustpython_stdlib::stdlib_module_defs(&self.ctx); let builder = self.add_native_modules(&defs); + #[cfg(all(feature = "ssl-rustls-aws-lc", not(target_arch = "wasm32")))] + let builder = builder.init_hook(install_default_tls_provider); cfg_select! { feature = "freeze-stdlib" => { @@ -26,6 +28,20 @@ impl InterpreterBuilderExt for InterpreterBuilder { } } +#[cfg(all(feature = "ssl-rustls-aws-lc", not(target_arch = "wasm32")))] +fn install_default_tls_provider(_vm: &mut crate::VirtualMachine) { + use rustls::crypto::aws_lc_rs; + use rustpython_stdlib::ssl::providers::CryptoExt; + + let ext = CryptoExt { + all_cipher_suites: Some(aws_lc_rs::ALL_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, + }; + let _ = CryptoExt::set_provider(aws_lc_rs::default_provider(), ext); +} + /// Set stdlib_dir for frozen standard library #[cfg(all(feature = "stdlib", feature = "freeze-stdlib"))] fn set_frozen_stdlib_dir(vm: &mut crate::VirtualMachine) { diff --git a/src/lib.rs b/src/lib.rs index a8384244cfa..14deb2972d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ pub use shell::run_shell; not(any(feature = "ssl-rustls", feature = "ssl-openssl")) ))] compile_error!( - "Feature \"ssl\" is now enabled by either \"ssl-rustls\" or \"ssl-openssl\" to be enabled. Do not manually pass \"ssl\" feature. To enable ssl-openssl, use --no-default-features to disable ssl-rustls" + "Feature \"ssl\" is now enabled by either \"ssl-rustls\" or \"ssl-openssl\". Do not manually pass \"ssl\" feature. To enable ssl-openssl, use --no-default-features to disable ssl-rustls*" ); /// The main cli of the `rustpython` interpreter. This function will return `std::process::ExitCode`