Skip to content
Draft
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
19 changes: 4 additions & 15 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,13 @@
#
# 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.
# RUSTSEC-2023-0071: "Marvin Attack" timing sidechannel in `rsa`, via the
# sid-crypto RustCrypto JWT and rustls providers.
# No fixed `rsa` release exists; RSA only verifies signatures with public keys
# (JWTs, TLS server certificates), and Marvin targets private-key timing.
# (JWTs, TLS peer certificates), RSA private-key operations are refused, 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",
]
5 changes: 2 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
if: matrix.backend.name == 'injected_verifier'
run: |
tree=$(cargo tree -e normal --target all --prefix none --format '{p}' ${{ matrix.backend.flags }})
if printf '%s\n' "$tree" | grep -E '^(rsa|rustls-rustcrypto|paste) v|^rustls-webpki v0\.102\.'; then
if printf '%s\n' "$tree" | grep -E '^rsa v'; then
echo "::error::the build without a crypto backend links a crate with an ignored advisory"
exit 1
fi
Expand Down Expand Up @@ -130,8 +130,7 @@ jobs:
- name: Doc tests
run: cargo test --doc ${{ matrix.backend.flags }}

# With `cli`, so the packaged crate's binary compiles too: without the
# feature cargo verifies the library alone.
# With `cli`, so the packaged binary compiles too.
- name: Publish dry-run
if: matrix.backend.name == 'rust_crypto'
run: cargo publish --dry-run --features cli
Expand Down
18 changes: 9 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,12 @@ 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 rustls and jsonwebtoken providers of the `rust_crypto` backend.
sid-crypto = { version = "0.1", default-features = false, features = ["std", "tls-rustcrypto", "tls12", "jwt-rustcrypto"], optional = true }
webpki-roots = "1"
globset = "0.4"
base64 = "0.23"
sha2 = "0.11"
sha2 = { version = "0.11", features = ["zeroize"] }
url = "2"
getrandom = "0.4"
# Envoy ext_authz `Authorization/Check` messages (External AuthZ).
Expand Down Expand Up @@ -185,7 +181,7 @@ builtin_jwt = ["dep:jsonwebtoken"]
# `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"]
rust_crypto = ["builtin_jwt", "dep:sid-crypto"]
# `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 +214,11 @@ 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", "tls-rustcrypto", "tls12"] }
# The test HTTPS servers' provider: an independent TLS peer that can present
# RSA identities, which the RustCrypto provider refuses. Excluded from product
# dependencies.
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
63 changes: 46 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ tonic services.
capabilities you ask for ([Building the proxy in code](#building-the-proxy-in-code))
- Requests the proxy does not serve go to your own fallback service
- Hooks for auth decisions, token verification, OIDC and extra routes, with no
HTTP framework in your code
HTTP framework in your code. The auth hooks are for applications that leave
authentication to the proxy and an external identity provider; one with its
own authn/authz (such as structured-id) uses none of them
([Applications with their own authentication](#applications-with-their-own-authentication))

## Non-goals

Expand Down Expand Up @@ -309,7 +312,8 @@ shield:
# path never blocks on it; a store outage degrades to per-instance limits.
# sync: { redis_url: "redis://127.0.0.1/", interval_ms: 500 }

# JWT auth
# JWT auth. For an application that leaves authentication to the proxy; one
# with its own authn/authz (such as structured-id) has no `auth:` section.
auth:
mode: "jwt"
# Which traffic needs a token. Default: [transcoded, endpoints]; the
Expand Down Expand Up @@ -973,6 +977,17 @@ returns `application/jwk-set+json` (RFC 7517 §8.5).
command-line dependencies come with the `cli` feature. The library runs on
your tokio runtime and logs through `tracing` to the subscriber you set up.

### Applications with their own authentication

The proxy's auth (JWT verification, route policies and roles, claim→header
forwarding, the forward-auth endpoint, ext_authz and the `with_auth`,
`with_token_verifier` and `with_auth_decider` hooks) is for an application
that leaves authentication and authorization to the proxy and an external
identity provider. An application that verifies tokens and authorizes requests
itself, such as structured-id and the services built on its authn/authz,
embeds the proxy with none of it: no `auth:` section and none of those hooks.
Its own checks are the only ones on the request path.

```rust,no_run
use std::path::Path;
use structured_proxy::ProxyServer;
Expand Down Expand Up @@ -1284,6 +1299,11 @@ Hooks plug your own stateless logic into the proxy. Their traits use `http`,
`bytes` and `serde_json` types and `#[async_trait]`, so your crate implements
them without depending on axum itself.

`with_auth_decider` and `with_token_verifier` are for an application that
leaves authentication and authorization to the proxy. An application with its
own authn/authz, such as structured-id, registers neither (see
[Applications with their own authentication](#applications-with-their-own-authentication)).

```rust
use std::sync::Arc;
use structured_proxy::{config::ProxyConfig, ProxyServer};
Expand Down Expand Up @@ -1331,24 +1351,28 @@ The hooks are:
## JWT verification

The bearer-token check sits behind the `TokenVerifier` hook. A build gets one of
two implementations.
two implementations. Both are for an application that leaves token
verification to the proxy; one that verifies its own tokens, such as
structured-id, uses neither (see
[Applications with their own authentication](#applications-with-their-own-authentication)).

**The built-in verifier** is what a plain config-driven deployment uses: keys
from `auth.jwt` (an Ed25519 PEM file or a JWKS endpoint), verified with
`jsonwebtoken`. Its crypto backend is a Cargo feature:

| 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`). |
| `aws_lc_rs` | aws-lc | Constant-time / FIPS-capable, advisory-free, links aws-lc through C FFI. |
| `rust_crypto` (default) | RustCrypto | Current primitives through jsonwebtoken's provider interface. RSA is public-key verification only; RSA signer and private-key extraction factories refuse requests (see `deny.toml`). |
| `aws_lc_rs` | aws-lc | Explicit optional backend using C FFI. |

Both can be linked at once, as with any pair of Cargo features. `jsonwebtoken`
cannot infer a provider then, so `ProxyServer::from_config` installs `aws_lc_rs`
for the process: it is constant-time and carries no advisory. A build that wants
RustCrypto regardless drops the `aws_lc_rs` feature.
Both can be linked at once, as with any pair of Cargo features. Building the
built-in verifier explicitly installs `aws_lc_rs` when both are enabled, unless
the process installed a provider first. A build that wants RustCrypto omits
`aws_lc_rs`. A proxy without `auth.mode: "jwt"`, or with an injected verifier,
installs no `jsonwebtoken` provider: that choice stays with your application.

If another crate in your process uses `jsonwebtoken` too, it may reach it before
any proxy server is built, and would hit the same ambiguity. It can also turn on
the built-in verifier is built, and would hit the same ambiguity. It can also turn on
the other backend for `jsonwebtoken` directly, which leaves this crate looking
single-backend while `jsonwebtoken` sees two. Settle it once at the top of
`main`:
Expand Down Expand Up @@ -1453,7 +1477,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 @@ -1462,12 +1486,17 @@ or configures a JWKS endpoint or the rate-limit service, installs a provider
before building the proxy (the outbound client needs one even for an
`http://` endpoint); otherwise startup fails with an error that says so.

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`.
The default RustCrypto profile accepts P-256, P-384 and Ed25519 local TLS
identities. RSA private keys are refused, including RSA inside PKCS#8, before
listener activation. RSA CA and peer signatures remain verifiable with
2048..=8192-bit public keys. For example, an ECDSA leaf signed by an RSA CA
works without giving the listener an RSA signing capability.

Rustls owns certificate-path validation. Algorithm identifiers come from
`rustls-pki-types`, without an obsolete webpki dependency. The adapter rejects
all-zero X25519 shared secrets and propagates entropy failures. Its runtime
dependencies are pure Rust; independent backends are confined to development
interoperability tests.

## How it works

Expand Down
6 changes: 3 additions & 3 deletions benches/jwt_verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ use std::sync::Arc;

use axum::body::Body;
use axum::http::{Request, StatusCode};
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use base64::Engine;
use criterion::{criterion_group, criterion_main, Criterion};
use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD};
use criterion::{Criterion, criterion_group, criterion_main};
use ed25519_dalek::{Signer, SigningKey};
use structured_proxy::auth::{middleware, Auth};
use structured_proxy::auth::{Auth, middleware};
use structured_proxy::config::ProxyConfig;
use tower::ServiceExt;

Expand Down
2 changes: 1 addition & 1 deletion benches/transcode_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use std::collections::HashMap;
use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use prost_reflect::{DynamicMessage, MessageDescriptor};
use structured_proxy::transcode::request;

Expand Down
45 changes: 11 additions & 34 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,24 @@
# 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 (the RustCrypto JWT and rustls 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
# DB and covers the `rsa` release this tree resolves (0.10.0-rc.19).
# 2. We keep pure-Rust crypto over C: the RustCrypto JWT backend over
# `aws_lc_rs`, and the RustCrypto rustls provider over `ring` / aws-lc,
# 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 TLS peers. The JWT backend
# refuses RSA signer and private-key factories, and the rustls provider
# admits only P-256, P-384 and Ed25519 local identities. Marvin targets
# private-key timing (decrypt/sign), which never runs on a verify 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 `rsa` 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:[] and covers the resolved 0.10.0-rc.19. We deliberately keep pure-Rust crypto (the RustCrypto JWT backend and rustls provider) over C. RSA only verifies signatures with public keys (JWTs, TLS peer certificates and handshake signatures); RSA private-key operations are refused, and Marvin targets private-key timing, so it is not exploitable on a verify path. Revisit when RustCrypto ships a constant-time `rsa` stable. Tracking: structured-world/structured-proxy#48" },
]
1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
style_edition = "2024"
Loading
Loading