Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 5 additions & 19 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
21 changes: 11 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"]`.
Expand Down Expand Up @@ -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
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
47 changes: 11 additions & 36 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
2 changes: 1 addition & 1 deletion src/auth/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 2 additions & 5 deletions src/auth/crypto/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
Expand All @@ -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()
));
}

Expand Down
2 changes: 1 addition & 1 deletion src/serve/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/shield/resolve/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<CryptoProvider> {
Some(rustls::crypto::aws_lc_rs::default_provider())
Expand All @@ -136,7 +135,7 @@ fn builtin_provider() -> Option<CryptoProvider> {
/// 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<CryptoProvider> {
Some(rustls_rustcrypto::provider())
Some(sid_crypto::tls::rustcrypto::provider())
}

/// No crypto backend is compiled in.
Expand Down
49 changes: 36 additions & 13 deletions src/tls/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ async fn serve(
.collect::<Result<Vec<_>, _>>()
.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(),
))
Comment thread
polaz marked this conversation as resolved.
Resolved
.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();
Expand Down Expand Up @@ -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)
Expand All @@ -97,6 +99,21 @@ async fn fetch(trusted: &str, host: &str, addr: SocketAddr) -> reqwest::Result<S
.await
}

/// The pure-Rust provider only verifies with RSA, so a listener on it refuses
/// an RSA key at startup, naming the file, rather than failing each handshake.
#[cfg(all(feature = "rust_crypto", not(feature = "aws_lc_rs")))]
#[test]
fn a_listener_on_the_pure_rust_provider_refuses_an_rsa_key() {
let testdata = concat!(env!("CARGO_MANIFEST_DIR"), "/src/tls/testdata");
let config = crate::config::ListenTlsConfig::new(
format!("{testdata}/rsa.pem"),
format!("{testdata}/rsa.key.pem"),
);
let refused = server_config(&config).unwrap_err();
assert!(refused.contains("rsa.pem"), "{refused}");
assert!(refused.contains("private keys"), "{refused}");
}

#[tokio::test]
async fn jwks_fetch_over_tls13_with_an_ecdsa_certificate() {
let (addr, server) = serve(ECDSA_CERT, ECDSA_KEY, &[&rustls::version::TLS13]).await;
Expand Down Expand Up @@ -194,14 +211,20 @@ fn the_aws_lc_backend_brings_the_aws_lc_provider() {
kind(&builtin),
kind(&rustls::crypto::aws_lc_rs::default_provider())
);
assert_ne!(kind(&builtin), kind(&rustls_rustcrypto::provider()));
assert_ne!(
kind(&builtin),
kind(&sid_crypto::tls::rustcrypto::provider())
);
}

#[cfg(all(feature = "rust_crypto", not(feature = "aws_lc_rs")))]
#[test]
fn the_rust_crypto_backend_brings_the_rustcrypto_provider() {
let builtin = builtin_provider().unwrap();
assert_eq!(kind(&builtin), kind(&rustls_rustcrypto::provider()));
assert_eq!(
kind(&builtin),
kind(&sid_crypto::tls::rustcrypto::provider())
);
}

#[cfg(not(any(feature = "rust_crypto", feature = "aws_lc_rs")))]
Expand All @@ -213,7 +236,7 @@ fn without_a_crypto_backend_no_provider_is_linked() {

#[test]
fn the_installed_provider_wins_over_the_builtin_one() {
let installed = Arc::new(rustls_rustcrypto::provider());
let installed = Arc::new(sid_crypto::tls::rustcrypto::provider());
let chosen = select_provider(Some(&installed), || {
panic!("the builtin provider is not built when one is installed")
})
Expand All @@ -223,7 +246,7 @@ fn the_installed_provider_wins_over_the_builtin_one() {

#[test]
fn without_an_installed_provider_the_builtin_one_is_used() {
let chosen = select_provider(None, || Some(rustls_rustcrypto::provider())).unwrap();
let chosen = select_provider(None, || Some(sid_crypto::tls::rustcrypto::provider())).unwrap();
assert!(!chosen.cipher_suites.is_empty());
}

Expand All @@ -245,7 +268,7 @@ fn a_provider_without_tls12_or_tls13_suites_is_refused() {
// safe version is an error, not a panic.
let provider = rustls::crypto::CryptoProvider {
cipher_suites: Vec::new(),
..rustls_rustcrypto::provider()
..sid_crypto::tls::rustcrypto::provider()
};
let err = client_config_with(Arc::new(provider), roots(CA)).unwrap_err();
assert!(err.contains("TLS 1.2 or 1.3"), "{err}");
Expand Down
Loading
Loading