Support custom rustls providers (#7657)

* Modularize rustls as work towards providers

`rustls`'s architecture is very clean and trait-driven. There are many
providers for `rustls` including the built-in `aws-lc-rs` and `ring` as
well as backends for `boringssl`, `graviola`, `openssl`, `mbedtls`, etc.

This commit removes the hard dependency on `aws-lc-rs` and adds support
for `ring`. It works towards #7059 as well.

* Clean up rustls features

* Remove ring as an explicit feature
* ssl-rustls is the default and implies aws-lc

* Support custom rustls crypto providers

The new feature, `ssl-rustls-no-provider`, enables custom rustls
providers. By default, `aws-lc-rs` is enabled which matches the old
behavior and keeps backward compatibility.

I wrote a new type that abstracts what we need from crypto providers.
CryptoExt encapsulates the ticketer as well as cipher suites and KX
groups. I wrote fallbacks to help select a reasonable default if a
provider is missing features (they all seem to support the same things
though).

I also wrote an example to show how to actually use custom providers.

* Fix duplicate VERIFY_X509 constants and unused imports in ssl module

Remove duplicate VERIFY_X509_STRICT/VERIFY_X509_PARTIAL_CHAIN definitions
from compat.rs (already defined in _ssl module with #[pyattr]).
Remove unused imports: ClientConnection, ServerConnection.

* no-provider as default

* Fix CI failures: openssl build, wasm target, cargo-shear

- Update openssl.rs to use renamed sock_wait/SockWaitKind and add vm parameter
- Add skip_ssl for wasm32-wasip2 target (aws-lc-sys cannot build for wasm)
- Remove unused workspace dependency aws-lc-rs
- Fix foreign-types-shared version to match openssl's dependency (0.1)
- Restore Cargo.lock from upstream/main

---------

Co-authored-by: Jeong, YunWon <jeong@youknowone.org>
This commit is contained in:
Joshua MegnauthandJeong, YunWon authored and GitHub committed 2026-05-28 11:07:09 +09:00
1 parent 5add3d6a69
commit f347ae4fbb
17 files changed
+488 -121

No files matched your search

+3 -2
View File
@@ -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
@@ -785,7 +786,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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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:
Generated
+168 -18
View File
@@ -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",
@@ -3591,7 +3747,6 @@ version = "0.5.0"
dependencies = [
"adler32",
"ascii",
"aws-lc-rs",
"base64",
"blake2",
"bzip2",
@@ -3606,7 +3761,7 @@ dependencies = [
"dyn-clone",
"flame",
"flate2",
"foreign-types-shared 0.3.1",
"foreign-types-shared",
"gethostname",
"hex",
"hmac 0.12.1",
@@ -4538,12 +4693,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"
@@ -4575,6 +4724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"atomic",
"getrandom 0.4.2",
"js-sys",
"wasm-bindgen",
]
+20 -4
View File
@@ -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"
+1 -1
View File
@@ -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.
+6 -6
View File
@@ -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 }
+4 -4
View File
@@ -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),
+19 -12
View File
@@ -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<SslNeeds>, 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(
+27 -20
View File
@@ -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<Self> {
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<PyBytesRef> {
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))
}
+5 -4
View File
@@ -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
+17 -45
View File
@@ -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<Arc<rustls::server::ServerSessionMemoryCache>>,
/// Shared ticketer for TLS 1.2 session tickets (stateless resumption)
pub ticketer: Option<Arc<dyn rustls::server::ProducesTickets>>,
pub ticketer: Option<Arc<dyn ProducesTickets>>,
}
/// 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<Vec<rustls::SupportedCipherSuite>>,
kx_groups: Option<Vec<&'static dyn rustls::crypto::SupportedKxGroup>>,
) -> Arc<rustls::crypto::CryptoProvider> {
use rustls::crypto::aws_lc_rs::{ALL_CIPHER_SUITES, ALL_KX_GROUPS};
let default_provider = rustls::crypto::aws_lc_rs::default_provider();
cipher_suites: Option<Vec<SupportedCipherSuite>>,
kx_groups: Option<Vec<&'static dyn SupportedKxGroup>>,
) -> Arc<CryptoProvider> {
let default_provider = CryptoExt::get_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()),
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<ServerConfig, String> {
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<Vec<u8>>, 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<ClientConfig, String> {
// 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<Clien
};
// Wrap with PartialChainVerifier if VERIFY_X509_PARTIAL_CHAIN is set
const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x80000;
let verifier = if options.verify_flags & VERIFY_X509_PARTIAL_CHAIN != 0 {
use crate::ssl::cert::PartialChainVerifier;
Arc::new(PartialChainVerifier::new(
@@ -2123,8 +2098,7 @@ pub(super) fn curve_name_to_kx_group(
curve: &str,
) -> Result<Vec<&'static dyn SupportedKxGroup>, 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)
+132
View File
@@ -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<CryptoExt> = 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<fn(&PrivateKeyDer<'_>) -> Result<Arc<dyn SigningKey>, Error>>,
pub ticketer: fn() -> Result<Arc<dyn ProducesTickets>, 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<Arc<dyn SigningKey>, 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()))
}
}
+65
View File
@@ -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);
}
+16
View File
@@ -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) {
+1 -1
View File
@@ -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`