diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 9cb9818..28ec6ec 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -3,24 +3,10 @@ # # Ignored deliberately; mirror of deny.toml, which carries the full rationale. # -# RUSTSEC-2023-0071: "Marvin Attack" timing sidechannel in `rsa` 0.9.x, via -# jsonwebtoken's rust_crypto backend and the RustCrypto rustls provider. -# No fixed `rsa` release exists; RSA only verifies signatures with public keys -# (JWTs, TLS server certificates), and Marvin targets private-key timing. +# RUSTSEC-2023-0071: "Marvin Attack" timing sidechannel in `rsa`, via the +# RustCrypto JWT and TLS providers of sid-crypto. No fixed `rsa` release exists; +# RSA only verifies signatures with public keys (JWTs, TLS server certificates), +# both providers refuse RSA private keys, and Marvin targets private-key timing. # See https://github.com/structured-world/structured-proxy/issues/48 -# -# RUSTSEC-2026-0049 / -0098 / -0099 / -0104: `rustls-webpki` 0.102 via -# rustls-rustcrypto 0.0.2-alpha, which only reads its `alg_id` constants; -# certificates are verified by rustls' own, patched rustls-webpki 0.103. -# RUSTSEC-2024-0436: `paste` (unmaintained build-time proc-macro) via the same -# crate. Remove all five with the next rustls-rustcrypto release. -# See https://github.com/structured-world/structured-proxy/issues/98 [advisories] -ignore = [ - "RUSTSEC-2023-0071", - "RUSTSEC-2026-0049", - "RUSTSEC-2026-0098", - "RUSTSEC-2026-0099", - "RUSTSEC-2026-0104", - "RUSTSEC-2024-0436", -] +ignore = ["RUSTSEC-2023-0071"] diff --git a/Cargo.toml b/Cargo.toml index 6cce53a..7e07627 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,12 +145,10 @@ jsonwebtoken = { version = "11", default-features = false, features = [ # rustls provider the embedder installs is used instead. reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider", "json"] } rustls = { version = "0.23", default-features = false, features = ["std", "tls12"] } -# The pure-Rust provider of the `rust_crypto` backend. `0.0.2-alpha` still -# names its `rustls-webpki` 0.102 dependency, which carries four CRL / -# name-constraint advisories; the provider only reads algorithm identifier -# constants from it, while certificates are verified by rustls' own (patched) -# webpki. See deny.toml. -rustls-rustcrypto = { version = "0.0.2-alpha", optional = true } +# The pure-Rust TLS and JWT providers of the `rust_crypto` backend, over current +# RustCrypto primitives; rustls keeps TLS and path validation, jsonwebtoken keeps +# parsing and claims. RSA is public-key only. +sid-crypto = { version = "0.1", default-features = false, features = ["std", "tls12"], optional = true } webpki-roots = "1" globset = "0.4" base64 = "0.23" @@ -183,9 +181,9 @@ builtin_jwt = ["dep:jsonwebtoken"] # own rustls provider. With neither, the crate links no crypto backend at all. # # `rust_crypto` (default): pure-Rust RustCrypto backend. Pulls in `rsa`, which -# carries RUSTSEC-2023-0071 (Marvin Attack). Not exploitable on our verify-only -# path (public key, no private-key ops); see deny.toml / issue #48. -rust_crypto = ["builtin_jwt", "jsonwebtoken/rust_crypto", "dep:rustls-rustcrypto"] +# carries RUSTSEC-2023-0071 (Marvin Attack). Not exploitable: the JWT provider +# refuses RSA private keys and TLS only verifies with RSA; see deny.toml / #48. +rust_crypto = ["builtin_jwt", "sid-crypto/jwt-rustcrypto", "sid-crypto/tls-rustcrypto"] # `aws_lc_rs`: constant-time / FIPS-capable backend via aws-lc (C FFI), # advisory-free. Opt-in for consumers that allow FFI; enable with # `default-features = false, features = ["aws_lc_rs"]`. @@ -218,7 +216,10 @@ http-body = "1" # (src/tls/tests.rs), and the provider it runs on in every build. Default # features would pull in aws-lc. tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] } -rustls-rustcrypto = "0.0.2-alpha" +sid-crypto = { version = "0.1", default-features = false, features = ["std", "tls12", "tls-rustcrypto"] } +# The test server's own side of an RSA handshake: the pure-Rust provider only +# verifies RSA, which is what the client under test does. +rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"] } # benches/jwt_verify.rs. Plots and rayon are left out: numbers are enough. criterion = { version = "0.8", default-features = false, features = ["async_tokio", "cargo_bench_support"] } # The embedding-hooks integration test (tests/hooks.rs) writes hook impls using diff --git a/README.md b/README.md index 8ce19a8..c22136f 100644 --- a/README.md +++ b/README.md @@ -1339,7 +1339,7 @@ from `auth.jwt` (an Ed25519 PEM file or a JWKS endpoint), verified with | Feature | Backend | Notes | |---------|---------|-------| -| `rust_crypto` (default) | RustCrypto | Pure Rust. Uses `rsa`, which carries [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071); the Marvin attack targets private-key timing, and this path only verifies with public keys (see `deny.toml`). | +| `rust_crypto` (default) | RustCrypto, through `sid-crypto` | Pure Rust. Uses `rsa`, which carries [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071); the Marvin attack targets private-key timing, and this provider refuses RSA private keys and only verifies with public keys (see `deny.toml`). | | `aws_lc_rs` | aws-lc | Constant-time / FIPS-capable, advisory-free, links aws-lc through C FFI. | Both can be linked at once, as with any pair of Cargo features. `jsonwebtoken` @@ -1453,7 +1453,7 @@ needed. The rustls crypto provider is, in order: `rustls::crypto::CryptoProvider::install_default`, if any: an explicit choice wins; 2. aws-lc, with the `aws_lc_rs` feature; -3. the pure-Rust RustCrypto provider (`rustls-rustcrypto`), with `rust_crypto`. +3. the pure-Rust RustCrypto provider of `sid-crypto`, with `rust_crypto`. The default build is pure Rust; aws-lc (C) comes with `aws_lc_rs`. A `default-features = false` build brings no provider, so a crate that only @@ -1464,10 +1464,9 @@ before building the proxy (the outbound client needs one even for an The RustCrypto provider verifies RSA server signatures with `rsa`, under the same RUSTSEC-2023-0071 note as the `rust_crypto` JWT backend: only public-key -verification runs. Its current release still names `rustls-webpki` 0.102, whose -CRL and name-constraint advisories are listed in `deny.toml` with why they do not -apply: the provider reads only algorithm identifiers from it, and rustls -verifies certificates with its own patched `rustls-webpki`. +verification runs. A TLS listener on this provider therefore presents a P-256, +P-384 or Ed25519 key; an RSA key is refused at startup. Use `aws_lc_rs` or install +another provider to serve an RSA certificate. ## How it works diff --git a/deny.toml b/deny.toml index 2a1dc60..ff3d92b 100644 --- a/deny.toml +++ b/deny.toml @@ -5,47 +5,22 @@ # fall back to cargo-deny defaults. [advisories] -# RUSTSEC-2023-0071: "Marvin Attack" timing sidechannel in `rsa` 0.9.x. -# Chains: rsa -> jsonwebtoken (rust_crypto backend) -> structured-proxy, and -# rsa -> rustls-rustcrypto (outbound TLS provider) -> structured-proxy. +# RUSTSEC-2023-0071: "Marvin Attack" timing sidechannel in `rsa`. +# Chain: rsa -> sid-crypto (RustCrypto JWT and TLS providers) -> structured-proxy. # # Ignored deliberately. See https://github.com/structured-world/structured-proxy/issues/48 # 1. No fix exists: the advisory has patched:[] / unaffected:[] in the RustSec -# DB, and it names both the latest stable `rsa` (0.9.10) and the latest -# pre-release (0.10.0-rc.18) as affected. There is nothing to upgrade to, -# release candidates included. -# 2. We keep pure-Rust crypto over C: jsonwebtoken's `rust_crypto` backend over -# `aws_lc_rs`, and the RustCrypto rustls provider over `ring` / aws-lc, -# required by no-C consumers (e.g. CoordiNode ADR-013). +# DB. There is nothing to upgrade to, release candidates included. +# 2. We keep pure-Rust crypto over C, required by no-C consumers (e.g. +# CoordiNode ADR-013). # 3. RSA here only ever verifies signatures with public keys: JWTs, and the -# certificates and handshake signatures of the TLS servers the proxy calls -# (it presents no client certificate). Marvin targets private-key timing -# (decrypt/sign), which never runs on a verify path. +# certificates and handshake signatures of the TLS servers the proxy calls. +# Both providers refuse RSA private keys, and Marvin targets private-key +# timing (decrypt/sign), which cannot run on this path. # Revisit when RustCrypto ships a constant-time `rsa` stable release. # -# Both chains come with the `rust_crypto` feature: a `default-features = false` -# build links neither, and CI checks that none of the crates ignored here is in -# it. -# -# RUSTSEC-2026-0049 / -0098 / -0099 / -0104: `rustls-webpki` 0.102. -# Chain: rustls-webpki 0.102.8 -> rustls-rustcrypto 0.0.2-alpha -> structured-proxy. -# The advisories are in CRL handling and name-constraint checks. The provider -# only reads algorithm identifier constants from that crate (`webpki::alg_id`, -# in its src/verify/*.rs); certificates are verified by rustls' own -# `rustls-webpki` 0.103.15, in which all four are patched, so the affected code -# never runs. Remove these entries with the first `rustls-rustcrypto` release -# after 0.0.2-alpha: upstream master has dropped the dependency -# (RustCrypto/rustls-rustcrypto#107). Tracking: structured-world/structured-proxy#98 -# -# RUSTSEC-2024-0436: `paste` is unmaintained (no vulnerability). -# Chain: paste -> rustls-rustcrypto 0.0.2-alpha -> structured-proxy. -# A proc-macro that only runs at build time. Upstream is replacing it -# (RustCrypto/rustls-rustcrypto#101); remove with the same release as above. +# The chain comes with the `rust_crypto` feature: a `default-features = false` +# build does not link it, and CI checks that the crate ignored here is not in it. ignore = [ - { id = "RUSTSEC-2023-0071", reason = "No fixed `rsa` release exists: the advisory is patched:[] and names both the latest stable (0.9.10) and the latest pre-release (0.10.0-rc.18) as affected. We deliberately keep pure-Rust crypto (jsonwebtoken's `rust_crypto` backend, the RustCrypto rustls provider) over C. RSA only verifies signatures with public keys (JWTs, TLS server certificates and handshake signatures); Marvin targets private-key timing, not exploitable on a verify path. Revisit when RustCrypto ships a constant-time `rsa` stable. Tracking: structured-world/structured-proxy#48" }, - { id = "RUSTSEC-2026-0049", reason = "rustls-webpki 0.102 via rustls-rustcrypto 0.0.2-alpha, which only reads its `alg_id` constants; certificates (and CRLs) are verified by rustls' own rustls-webpki 0.103.15, which is patched. Remove with the next rustls-rustcrypto release. Tracking: structured-world/structured-proxy#98" }, - { id = "RUSTSEC-2026-0098", reason = "rustls-webpki 0.102 via rustls-rustcrypto 0.0.2-alpha, which only reads its `alg_id` constants; name constraints are checked by rustls' own rustls-webpki 0.103.15, which is patched. Remove with the next rustls-rustcrypto release. Tracking: structured-world/structured-proxy#98" }, - { id = "RUSTSEC-2026-0099", reason = "rustls-webpki 0.102 via rustls-rustcrypto 0.0.2-alpha, which only reads its `alg_id` constants; name constraints are checked by rustls' own rustls-webpki 0.103.15, which is patched. Remove with the next rustls-rustcrypto release. Tracking: structured-world/structured-proxy#98" }, - { id = "RUSTSEC-2026-0104", reason = "rustls-webpki 0.102 via rustls-rustcrypto 0.0.2-alpha, which only reads its `alg_id` constants; no CRL is parsed through it (the proxy configures no CRLs, and rustls uses its own rustls-webpki 0.103.15, which is patched). Remove with the next rustls-rustcrypto release. Tracking: structured-world/structured-proxy#98" }, - { id = "RUSTSEC-2024-0436", reason = "`paste` is unmaintained, not vulnerable: a build-time proc-macro of rustls-rustcrypto 0.0.2-alpha, which upstream is replacing (RustCrypto/rustls-rustcrypto#101). Remove with the next rustls-rustcrypto release. Tracking: structured-world/structured-proxy#98" }, + { id = "RUSTSEC-2023-0071", reason = "No fixed `rsa` release exists: the advisory is patched:[]. We deliberately keep pure-Rust crypto over C. The RustCrypto providers of sid-crypto refuse RSA private keys and RSA only verifies signatures with public keys; Marvin targets private-key timing, not exploitable on a verify path. Revisit when RustCrypto ships a constant-time `rsa` stable. Tracking: structured-world/structured-proxy#48" }, ] diff --git a/src/auth/crypto.rs b/src/auth/crypto.rs index be3728a..016b782 100644 --- a/src/auth/crypto.rs +++ b/src/auth/crypto.rs @@ -26,7 +26,7 @@ pub(crate) fn preferred_provider() -> &'static jsonwebtoken::crypto::CryptoProvi /// compiled with. #[cfg(all(feature = "rust_crypto", not(feature = "aws_lc_rs")))] pub(crate) fn preferred_provider() -> &'static jsonwebtoken::crypto::CryptoProvider { - &jsonwebtoken::crypto::rust_crypto::DEFAULT_PROVIDER + sid_crypto::jwt::rustcrypto::provider() } /// Select the `jsonwebtoken` crypto provider for this process. diff --git a/src/auth/crypto/tests.rs b/src/auth/crypto/tests.rs index 56c4f01..43640b2 100644 --- a/src/auth/crypto/tests.rs +++ b/src/auth/crypto/tests.rs @@ -13,10 +13,7 @@ fn tie_break_selects_aws_lc_rs() { "the tie-break must select the aws_lc_rs provider" ); assert!( - !std::ptr::eq( - selected, - &jsonwebtoken::crypto::rust_crypto::DEFAULT_PROVIDER - ), + !std::ptr::eq(selected, sid_crypto::jwt::rustcrypto::provider()), "the tie-break must not fall back to RustCrypto" ); } @@ -40,7 +37,7 @@ fn aws_lc_rs_only_build_installs_aws_lc_rs() { fn rust_crypto_only_build_installs_rust_crypto() { assert!(std::ptr::eq( super::preferred_provider(), - &jsonwebtoken::crypto::rust_crypto::DEFAULT_PROVIDER + sid_crypto::jwt::rustcrypto::provider() )); } diff --git a/src/serve/tests.rs b/src/serve/tests.rs index 7ad44cb..82cfde2 100644 --- a/src/serve/tests.rs +++ b/src/serve/tests.rs @@ -252,7 +252,7 @@ fn a_zero_limit_in_the_config_is_an_error() { /// A process crypto provider, for builds without a crypto backend feature. fn install_provider() { if rustls::crypto::CryptoProvider::get_default().is_none() { - rustls::crypto::CryptoProvider::install_default(rustls_rustcrypto::provider()) + rustls::crypto::CryptoProvider::install_default(sid_crypto::tls::rustcrypto::provider()) .expect("each test runs in its own process"); } } diff --git a/src/shield/resolve/tests.rs b/src/shield/resolve/tests.rs index 64acb0e..5dd6473 100644 --- a/src/shield/resolve/tests.rs +++ b/src/shield/resolve/tests.rs @@ -25,7 +25,9 @@ fn with_tls_provider() { { use rustls::crypto::CryptoProvider; // A concurrent test may have installed it first; either way one is set. - let installed = rustls_rustcrypto::provider().install_default().is_ok(); + let installed = sid_crypto::tls::rustcrypto::provider() + .install_default() + .is_ok(); assert!(installed || CryptoProvider::get_default().is_some()); } } diff --git a/src/tls.rs b/src/tls.rs index 8e5abd0..fc0e6c8 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -126,8 +126,7 @@ fn select_provider( }) } -/// aws-lc wins whenever it is compiled in, as it does for JWT verification: -/// constant-time, and free of the advisories the RustCrypto provider carries. +/// aws-lc wins whenever it is compiled in, as it does for JWT verification. #[cfg(feature = "aws_lc_rs")] fn builtin_provider() -> Option { Some(rustls::crypto::aws_lc_rs::default_provider()) @@ -136,7 +135,7 @@ fn builtin_provider() -> Option { /// The pure-Rust RustCrypto provider, the only backend this build compiled. #[cfg(all(feature = "rust_crypto", not(feature = "aws_lc_rs")))] fn builtin_provider() -> Option { - Some(rustls_rustcrypto::provider()) + Some(sid_crypto::tls::rustcrypto::provider()) } /// No crypto backend is compiled in. diff --git a/src/tls/tests.rs b/src/tls/tests.rs index f42eb17..57f2713 100644 --- a/src/tls/tests.rs +++ b/src/tls/tests.rs @@ -40,13 +40,14 @@ async fn serve( .collect::, _>>() .unwrap(); let key = PrivateKeyDer::from_pem_slice(key.as_bytes()).unwrap(); - let config = - rustls::ServerConfig::builder_with_provider(Arc::new(rustls_rustcrypto::provider())) - .with_protocol_versions(versions) - .unwrap() - .with_no_client_auth() - .with_single_cert(chain, key) - .unwrap(); + let config = rustls::ServerConfig::builder_with_provider(Arc::new( + rustls::crypto::ring::default_provider(), + )) + .with_protocol_versions(versions) + .unwrap() + .with_no_client_auth() + .with_single_cert(chain, key) + .unwrap(); let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config)); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); @@ -80,7 +81,8 @@ async fn serve( /// handshakes below cover the one production uses; a build without a backend /// falls back to RustCrypto, as an embedder would install one. fn client(trusted: &str, host: &str, addr: SocketAddr) -> reqwest::Client { - let provider = Arc::new(builtin_provider().unwrap_or_else(rustls_rustcrypto::provider)); + let provider = + Arc::new(builtin_provider().unwrap_or_else(sid_crypto::tls::rustcrypto::provider)); reqwest::Client::builder() .tls_backend_preconfigured(client_config_with(provider, roots(trusted)).unwrap()) .resolve(host, addr) @@ -97,6 +99,21 @@ async fn fetch(trusted: &str, host: &str, addr: SocketAddr) -> reqwest::Result Box { for cert in CertificateDer::pem_slice_iter(CA.as_bytes()) { roots.add(cert.unwrap()).unwrap(); } - let mut config = - rustls::ClientConfig::builder_with_provider(Arc::new(rustls_rustcrypto::provider())) - .with_safe_default_protocol_versions() - .unwrap() - .with_root_certificates(roots) - .with_no_client_auth(); + let mut config = rustls::ClientConfig::builder_with_provider(Arc::new( + sid_crypto::tls::rustcrypto::provider(), + )) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(roots) + .with_no_client_auth(); config.alpn_protocols = vec![alpn.to_vec()]; let stream = tokio_rustls::TlsConnector::from(Arc::new(config)) .connect(ServerName::try_from("localhost").unwrap(), tcp) diff --git a/tests/tls.rs b/tests/tls.rs index e5fe9b9..b8d9b0a 100644 --- a/tests/tls.rs +++ b/tests/tls.rs @@ -119,7 +119,7 @@ impl tower::Service> for Who { // --- the embedder's TLS server ------------------------------------------------ fn provider() -> Arc { - Arc::new(rustls_rustcrypto::provider()) + Arc::new(sid_crypto::tls::rustcrypto::provider()) } fn chain() -> Vec> { @@ -245,7 +245,7 @@ const TESTDATA: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/tls/testdata"); async fn listen_builtin(listen_yaml: &str) -> SocketAddr { // Builds without a crypto backend feature take the process's provider. if rustls::crypto::CryptoProvider::get_default().is_none() { - rustls::crypto::CryptoProvider::install_default(rustls_rustcrypto::provider()) + rustls::crypto::CryptoProvider::install_default(sid_crypto::tls::rustcrypto::provider()) .expect("each test runs in its own process"); } let pool = pool();