diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 9cb9818..9feb315 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -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", ] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6259f04..3b8c7ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 6cce53a..1776c4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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). @@ -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"]`. @@ -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 diff --git a/README.md b/README.md index 8ce19a8..1cc0650 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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; @@ -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}; @@ -1331,7 +1351,10 @@ 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 @@ -1339,16 +1362,17 @@ 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`). | -| `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`: @@ -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 @@ -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 diff --git a/benches/jwt_verify.rs b/benches/jwt_verify.rs index c5e63b1..09df72b 100644 --- a/benches/jwt_verify.rs +++ b/benches/jwt_verify.rs @@ -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; diff --git a/benches/transcode_request.rs b/benches/transcode_request.rs index 3a91da8..1212289 100644 --- a/benches/transcode_request.rs +++ b/benches/transcode_request.rs @@ -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; diff --git a/deny.toml b/deny.toml index 2a1dc60..3a5ba51 100644 --- a/deny.toml +++ b/deny.toml @@ -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" }, ] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..3501136 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +style_edition = "2024" diff --git a/src/auth/authz.rs b/src/auth/authz.rs index fafe258..efa4908 100644 --- a/src/auth/authz.rs +++ b/src/auth/authz.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use std::time::Duration; use axum::extract::State; -use axum::http::header::{HeaderName, HeaderValue, HOST}; +use axum::http::header::{HOST, HeaderName, HeaderValue}; use axum::http::{HeaderMap, StatusCode, Uri}; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; @@ -18,10 +18,10 @@ use tonic::transport::Channel; use envoy_types::pb::envoy::config::core::v3::HeaderValueOption; use envoy_types::pb::envoy::service::auth::v3::{ + AttributeContext, CheckRequest, CheckResponse, DeniedHttpResponse, attribute_context::{HttpRequest, Request as AttrRequest}, authorization_client::AuthorizationClient, check_response::HttpResponse as EnvoyHttpResponse, - AttributeContext, CheckRequest, CheckResponse, DeniedHttpResponse, }; use crate::config::AuthzConfig; @@ -233,8 +233,8 @@ fn denied_to_response(denied: DeniedHttpResponse) -> Response { mod tests { use super::*; use envoy_types::pb::envoy::config::core::v3::HeaderValue as EnvoyHeaderValue; - use envoy_types::pb::envoy::r#type::v3::HttpStatus; use envoy_types::pb::envoy::service::auth::v3::{CheckResponse, OkHttpResponse}; + use envoy_types::pb::envoy::r#type::v3::HttpStatus; use envoy_types::pb::google::rpc::Status as RpcStatus; fn hvo(key: &str, value: &str) -> HeaderValueOption { @@ -395,8 +395,8 @@ mod tests { #[tokio::test] async fn fail_closed_returns_503_when_authz_unreachable() { - use axum::routing::get; use axum::Router; + use axum::routing::get; use tower::ServiceExt; let authz = Authz::build(&AuthzConfig { @@ -424,8 +424,8 @@ mod tests { #[tokio::test] async fn fail_open_passes_through_when_authz_unreachable() { - use axum::routing::get; use axum::Router; + use axum::routing::get; use tower::ServiceExt; let authz = Authz::build(&AuthzConfig { diff --git a/src/auth/cache.rs b/src/auth/cache.rs index df20350..8ebcd03 100644 --- a/src/auth/cache.rs +++ b/src/auth/cache.rs @@ -11,8 +11,8 @@ //! insert that finds the cache full sweeps them at most once per second, so it //! may leave a new token uncached until the next sweep frees room. -use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; // no-std: caller-provided monotonic clock use std::time::Instant; diff --git a/src/auth/crypto.rs b/src/auth/crypto.rs index be3728a..52e8cda 100644 --- a/src/auth/crypto.rs +++ b/src/auth/crypto.rs @@ -1,22 +1,16 @@ //! Process-wide `jsonwebtoken` crypto provider selection. //! -//! `jsonwebtoken` infers its provider from its own two backend features, and -//! with both on it cannot: it falls back to a provider that panics on first -//! use. Cargo features being additive, that combination arrives on its own. -//! Either this crate's `rust_crypto` and `aws_lc_rs` are both enabled (two -//! dependents asking for different backends, or `--all-features`, which is what -//! docs.rs and `cargo-semver-checks` use), or one of ours is enabled while -//! another crate in the graph turns on the other `jsonwebtoken` feature -//! directly. The second case looks single-backend from here, so the provider is -//! installed explicitly whichever backend this crate compiled with. +//! The RustCrypto backend is the `sid-crypto` jsonwebtoken adapter over current +//! primitives. The optional aws-lc backend retains its existing precedence. +//! Explicit installation avoids feature inference when dependencies enable +//! different jsonwebtoken backends in one process. #[cfg(test)] mod tests; /// The provider this crate installs. /// -/// `aws_lc_rs` wins whenever it is compiled in: it is constant-time and -/// advisory-free, while `rust_crypto` pulls in `rsa` (RUSTSEC-2023-0071). +/// `aws_lc_rs` retains precedence when both optional backends are compiled in. #[cfg(feature = "aws_lc_rs")] pub(crate) fn preferred_provider() -> &'static jsonwebtoken::crypto::CryptoProvider { &jsonwebtoken::crypto::aws_lc::DEFAULT_PROVIDER @@ -26,7 +20,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. @@ -35,16 +29,16 @@ pub(crate) fn preferred_provider() -> &'static jsonwebtoken::crypto::CryptoProvi /// JWT. It is idempotent, and installs the backend this crate was built with, /// so `jsonwebtoken` never has to infer one. /// -/// A plain [`ProxyServer`](crate::ProxyServer) deployment needs no call: the -/// server does this while it is being built. It is public for the case the -/// server cannot cover, which is also how the ambiguity arises in the first +/// A plain [`ProxyServer`](crate::ProxyServer) deployment with `auth.mode: +/// "jwt"` needs no call: building the built-in verifier does this. A proxy +/// without it installs nothing and leaves the provider to its host. It is +/// public for the case the server cannot cover, which is also how the ambiguity arises in the first /// place: another crate in the graph uses `jsonwebtoken` too and may reach it /// first. Call this at the top of `main` and every consumer is covered, /// whichever runs first. /// -/// With both backends linked the choice is `aws_lc_rs`: constant-time, and free -/// of the `rsa` advisory `rust_crypto` carries. A process that wants a different -/// one installs it through +/// With both backends linked the choice is `aws_lc_rs`. A process that wants +/// a different one installs it through /// [`CryptoProvider::install_default`](jsonwebtoken::crypto::CryptoProvider::install_default) /// before calling this, and the earlier choice stands. pub fn install_default_crypto_provider() { diff --git a/src/auth/crypto/tests.rs b/src/auth/crypto/tests.rs index 56c4f01..ded9a1e 100644 --- a/src/auth/crypto/tests.rs +++ b/src/auth/crypto/tests.rs @@ -7,17 +7,12 @@ #[cfg(all(feature = "rust_crypto", feature = "aws_lc_rs"))] #[test] fn tie_break_selects_aws_lc_rs() { - let selected = super::preferred_provider(); assert!( - std::ptr::eq(selected, &jsonwebtoken::crypto::aws_lc::DEFAULT_PROVIDER), - "the tie-break must select the aws_lc_rs provider" - ); - assert!( - !std::ptr::eq( - selected, - &jsonwebtoken::crypto::rust_crypto::DEFAULT_PROVIDER + std::ptr::eq( + super::preferred_provider(), + &jsonwebtoken::crypto::aws_lc::DEFAULT_PROVIDER ), - "the tie-break must not fall back to RustCrypto" + "the tie-break must select the aws_lc_rs provider" ); } @@ -40,7 +35,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() )); } @@ -51,3 +46,55 @@ fn installing_twice_is_harmless() { super::install_default_crypto_provider(); super::install_default_crypto_provider(); } + +/// A proxy without `auth:` uses no JWT and leaves the process-wide jsonwebtoken +/// provider to the host: an application with its own token checks must keep +/// its own choice. The probe install succeeds only if nothing installed one +/// first (each test runs in its own process). +#[test] +fn a_proxy_without_auth_installs_no_jwt_provider() { + let service = crate::ProxyServer::from_yaml_str("service:\n name: demo\n") + .unwrap() + .service(tonic::service::Routes::default()) + .unwrap(); + drop(service); + assert!( + super::preferred_provider().install_default().is_ok(), + "building a proxy without auth must not install a jsonwebtoken provider" + ); +} + +/// The built-in verifier, once built, has the provider it verifies with. +#[test] +fn building_the_builtin_verifier_installs_the_provider() { + let jwt = crate::config::JwtConfig { + jwks_uri: Some("https://issuer.example/jwks".into()), + jwks_max_age_secs: 300, + issuer: None, + audience: None, + public_key_pem_file: None, + claims_headers: Default::default(), + roles_claim: "roles".into(), + cache: Default::default(), + }; + super::super::verifier::ConfigVerifier::build(&jwt).unwrap(); + assert!(super::preferred_provider().install_default().is_err()); +} + +/// RSA public verification does not authorize any local RSA signer factory. +#[cfg(all(feature = "rust_crypto", not(feature = "aws_lc_rs")))] +#[test] +fn rust_crypto_refuses_rsa_signer_factories() { + use jsonwebtoken::{Algorithm, EncodingKey}; + let key = EncodingKey::from_rsa_der(&[]); + for algorithm in [ + Algorithm::RS256, + Algorithm::RS384, + Algorithm::RS512, + Algorithm::PS256, + Algorithm::PS384, + Algorithm::PS512, + ] { + assert!((super::preferred_provider().signer_factory)(&algorithm, &key).is_err()); + } +} diff --git a/src/auth/forward.rs b/src/auth/forward.rs index 0d8be17..d5f6d2e 100644 --- a/src/auth/forward.rs +++ b/src/auth/forward.rs @@ -7,14 +7,14 @@ use std::sync::Arc; +use axum::Router; use axum::extract::Request; use axum::http::header::{HeaderValue, LOCATION}; use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::routing::any; -use axum::Router; -use super::{forbidden, unauthorized, Auth, AuthDecision}; +use super::{Auth, AuthDecision, forbidden, unauthorized}; use crate::config::AuthConfig; /// A verification endpoint backed by the shared [`Auth`] machinery. diff --git a/src/auth/forward/tests.rs b/src/auth/forward/tests.rs index 557c3dc..32cd441 100644 --- a/src/auth/forward/tests.rs +++ b/src/auth/forward/tests.rs @@ -21,8 +21,8 @@ fn keypair() -> (SigningKey, String) { } fn sign(sk: &SigningKey, claims: &serde_json::Value) -> String { - use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; + use base64::engine::general_purpose::URL_SAFE_NO_PAD; let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"EdDSA","typ":"JWT"}"#); let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims).unwrap()); let signing_input = format!("{header}.{payload}"); diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 5e9223c..79846ab 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -10,6 +10,11 @@ //! [`ProxyServer::with_token_verifier`](crate::ProxyServer::with_token_verifier). //! Everything else here — policies, roles, claim headers — is independent of //! which one verified the token. +//! +//! This module is for an application that leaves authentication and +//! authorization to the proxy. One with its own authn/authz, such as +//! structured-id, uses none of it (see +//! [the crate docs](crate#applications-with-their-own-authentication)). pub mod authz; #[cfg(feature = "builtin_jwt")] @@ -31,13 +36,13 @@ use std::collections::HashSet; use std::sync::Arc; use axum::extract::State; -use axum::http::header::{HeaderName, HeaderValue}; use axum::http::HeaderMap; +use axum::http::header::{HeaderName, HeaderValue}; use axum::middleware::Next; use axum::response::Response; use serde_json::Value; -use crate::config::{default_roles_claim, AuthConfig}; +use crate::config::{AuthConfig, default_roles_claim}; use crate::hooks::TokenVerifier; use policy::Policies; diff --git a/src/auth/tests.rs b/src/auth/tests.rs index cb553d0..fdd5731 100644 --- a/src/auth/tests.rs +++ b/src/auth/tests.rs @@ -334,7 +334,7 @@ fn jwt_mode_without_a_verifier_is_rejected() { #[cfg(feature = "builtin_jwt")] mod builtin { use super::*; - use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; + use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; use std::sync::atomic::{AtomicU32, Ordering}; // Ed25519 test keypair (generated for tests only; not a secret). diff --git a/src/auth/verifier.rs b/src/auth/verifier.rs index 61294d7..e7421a9 100644 --- a/src/auth/verifier.rs +++ b/src/auth/verifier.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::time::Duration; -use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header}; use serde_json::Value; use super::jwks::{JwksCache, MIN_REFRESH_INTERVAL}; @@ -43,9 +43,8 @@ impl ConfigVerifier { /// max age is below the refresh interval, the PEM file cannot be read, or /// it is not a valid Ed25519 public key. pub(crate) fn build(jwt: &JwtConfig) -> Result { - // Normally settled in `ProxyServer::from_config` already; repeated here - // because a verifier can also be built without going through the - // server, and the call costs one atomic once the provider is in place. + // The only place the proxy settles the jsonwebtoken provider: a proxy + // that builds no built-in verifier leaves that choice to its host. super::crypto::install_default_crypto_provider(); let keys = if let Some(uri) = &jwt.jwks_uri { diff --git a/src/client_address.rs b/src/client_address.rs index fafed73..39a3ab1 100644 --- a/src/client_address.rs +++ b/src/client_address.rs @@ -98,7 +98,7 @@ use std::sync::Arc; use std::task::{Context, Poll}; use axum::extract::ConnectInfo; -use http::header::{HeaderName, FORWARDED}; +use http::header::{FORWARDED, HeaderName}; use http::{HeaderMap, HeaderValue}; use ipnet::IpNet; use tower::{Layer, Service}; @@ -633,7 +633,7 @@ fn parse_trusted(entry: &str) -> Result { Err(_) => { return Err(format!( "trusted_proxies entry {entry:?} is neither a CIDR range nor an IP address" - )) + )); } }, }; diff --git a/src/client_address/tests.rs b/src/client_address/tests.rs index a1bd0dc..508e66b 100644 --- a/src/client_address/tests.rs +++ b/src/client_address/tests.rs @@ -726,12 +726,14 @@ fn an_unresolved_address_leaves_no_forwarding_header() { behind_lb().apply(&mut request, from); assert!(request.headers().get("x-forwarded-for").is_none()); assert!(request.headers().get("x-real-ip").is_none()); - assert!(request - .extensions() - .get::() - .unwrap() - .ip() - .is_none()); + assert!( + request + .extensions() + .get::() + .unwrap() + .ip() + .is_none() + ); } } diff --git a/src/config.rs b/src/config.rs index eb40b6a..1f58e0f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1801,7 +1801,9 @@ impl ProxyConfig { anyhow::bail!("endpoint path {path:?} ({label}) must start with '/'"); } if !seen.insert(path.to_string()) { - anyhow::bail!("duplicate endpoint path {path:?} ({label}); each built-in endpoint must have a distinct path"); + anyhow::bail!( + "duplicate endpoint path {path:?} ({label}); each built-in endpoint must have a distinct path" + ); } Ok(()) }; diff --git a/src/config/tests.rs b/src/config/tests.rs index 54092c9..c9d6291 100644 --- a/src/config/tests.rs +++ b/src/config/tests.rs @@ -769,9 +769,11 @@ error_details: let options = transcode_options(yaml).unwrap(); assert!(options.ndjson_envelope); assert!(options.error_details.enabled_for("/v1/public/items")); - assert!(!options - .error_details - .enabled_for("/v1/public/internal/{id}")); + assert!( + !options + .error_details + .enabled_for("/v1/public/internal/{id}") + ); assert!(!options.error_details.enabled_for("/v1/admin/items")); // The ProxyConfig still reads its own streaming key from the same section. let config: ProxyConfig = parse_yaml(yaml).unwrap(); diff --git a/src/cors.rs b/src/cors.rs index f7f9982..6c92ea4 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -14,7 +14,7 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use std::task::{ready, Context, Poll}; +use std::task::{Context, Poll, ready}; use axum::http::header::{ ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, diff --git a/src/cors/tests.rs b/src/cors/tests.rs index f415ba3..cd3fd28 100644 --- a/src/cors/tests.rs +++ b/src/cors/tests.rs @@ -1,9 +1,9 @@ use super::*; +use axum::Router; use axum::body::Body; use axum::extract::Request; use axum::http::StatusCode; use axum::routing::any; -use axum::Router; use tower::ServiceExt; /// A router whose only route answers every method with the method it saw, a diff --git a/src/embed.rs b/src/embed.rs index 2af5524..fa30f9d 100644 --- a/src/embed.rs +++ b/src/embed.rs @@ -16,7 +16,7 @@ use axum::http::header::{CONTENT_TYPE, LOCATION, WWW_AUTHENTICATE}; use axum::http::{HeaderMap, StatusCode}; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; -use axum::routing::{get, on, MethodFilter, MethodRouter}; +use axum::routing::{MethodFilter, MethodRouter, get, on}; use axum::{Json, Router}; use crate::client_address::ClientAddress; diff --git a/src/embed/tests.rs b/src/embed/tests.rs index 93cde96..c389a37 100644 --- a/src/embed/tests.rs +++ b/src/embed/tests.rs @@ -3,10 +3,10 @@ use std::sync::Arc; use async_trait::async_trait; +use axum::Router; use axum::body::Body; use axum::http::{HeaderMap, Method, Request, StatusCode}; use axum::routing::get; -use axum::Router; use bytes::Bytes; use tower::ServiceExt; diff --git a/src/guard/gate.rs b/src/guard/gate.rs index 943a966..0b96e53 100644 --- a/src/guard/gate.rs +++ b/src/guard/gate.rs @@ -11,13 +11,13 @@ use std::task::{Context, Poll}; use axum::extract::Request; use axum::response::Response; -use http::header::RETRY_AFTER; use http::HeaderValue; +use http::header::RETRY_AFTER; use pin_project_lite::pin_project; use tokio::sync::OwnedSemaphorePermit; use tower::{Layer, Service}; -use super::{reject, Class, Concurrency, Guards, Maintenance, Scope}; +use super::{Class, Concurrency, Guards, Maintenance, Scope, reject}; use crate::client_address::{ClientAddress, Resolution}; use crate::shield::matcher::Phase; use crate::shield::{Decision, Report, Shield}; diff --git a/src/guard/grpc.rs b/src/guard/grpc.rs index daedebf..228dc3e 100644 --- a/src/guard/grpc.rs +++ b/src/guard/grpc.rs @@ -3,15 +3,15 @@ use std::convert::Infallible; use std::future::Future; use std::pin::Pin; -use std::task::{ready, Context, Poll}; +use std::task::{Context, Poll, ready}; use axum::extract::Request; use axum::response::Response; use pin_project_lite::pin_project; use tower::Service; -use super::{http_to_grpc_code, BoxedService, Rejection}; -use crate::upstream::{grpc_protocol, trailers_only, GrpcProtocol}; +use super::{BoxedService, Rejection, http_to_grpc_code}; +use crate::upstream::{GrpcProtocol, grpc_protocol, trailers_only}; /// The guarded gRPC path of one protocol: a guard's HTTP rejection becomes a /// trailers-only status in `protocol` (gRPC PROTOCOL-HTTP2, "Responses"), the diff --git a/src/hooks.rs b/src/hooks.rs index 63ed7c2..18ef421 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -8,6 +8,11 @@ //! `axum` type), so `cargo tree -i axum` in an embedder shows axum only under //! `structured-proxy`. //! +//! [`AuthDecider`] and [`TokenVerifier`] are for an application that leaves +//! authentication and authorization to the proxy. One with its own authn/authz, +//! such as structured-id, implements neither (see +//! [the crate docs](crate#applications-with-their-own-authentication)). +//! //! Stateful concerns (BFF sessions, OIDC `authorize`/`token`) are deliberately //! absent: the default build is a stateless data plane (see the crate README //! Non-goals). They are planned behind an opt-in `bff` feature. @@ -111,6 +116,10 @@ pub enum Decision { /// (e.g. JWT verification + a policy engine + header translation). Called inline /// on every proxied request *and* by the `/verify` forward-auth endpoint: same /// trait, two call sites. +/// +/// Only for an application that leaves authorization to the proxy; one with its +/// own authn/authz, such as structured-id, does not implement it (see +/// [the crate docs](crate#applications-with-their-own-authentication)). #[async_trait] pub trait AuthDecider: Send + Sync { /// Decide whether to allow, deny, or redirect the request. @@ -136,6 +145,10 @@ pub trait AuthDecider: Send + Sync { /// Everything around verification stays with the proxy: route policies /// (`require_auth` / `required_roles`), the roles claim, and the claim→header /// forwarding all operate on the returned claims. +/// +/// Only for an application that leaves token verification to the proxy; one +/// that verifies its own tokens, such as structured-id, does not implement it +/// (see [the crate docs](crate#applications-with-their-own-authentication)). #[async_trait] pub trait TokenVerifier: Send + Sync { /// Verify `token` and return its claims, or `None` to reject the request diff --git a/src/lib.rs b/src/lib.rs index 06b6aa7..92efa11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,10 +10,24 @@ //! structured-proxy --config sflow-proxy.yaml //! ``` //! +//! ## Applications with their own authentication +//! +//! The proxy's auth (JWT verification, route policies and roles, claim→header +//! forwarding, forward-auth, ext_authz, and the [`ProxyServer::with_auth`], +//! [`ProxyServer::with_token_verifier`] and [`ProxyServer::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. +//! //! ## JWT verification //! -//! The bearer-token check sits behind [`hooks::TokenVerifier`]. A build gets one -//! of two: +//! The bearer-token check sits behind [`hooks::TokenVerifier`], for an +//! application that leaves token verification to the proxy (see +//! [above](#applications-with-their-own-authentication)). A build gets one of +//! two: //! //! - the **built-in** verifier (keys from `auth.jwt`), whose crypto backend is //! picked by a feature: `rust_crypto` (default, pure Rust) or `aws_lc_rs` @@ -21,10 +35,12 @@ //! compiled in at once — Cargo features are additive, so a dependency graph //! with two dependents asking for different backends unifies into exactly that //! build. `aws_lc_rs` then wins: it is constant-time and free of the `rsa` -//! advisory `rust_crypto` carries. [`ProxyServer::from_config`] settles that -//! choice for the process; a process where another crate may reach -//! `jsonwebtoken` before any server exists calls -//! [`install_default_crypto_provider`] from `main` instead. +//! advisory `rust_crypto` carries. Building the built-in verifier settles +//! that choice for the process, unless the host installed a provider first; +//! a proxy without `auth.mode: "jwt"`, or with an injected verifier, leaves it +//! to the host. A process where another crate may reach `jsonwebtoken` before +//! the verifier is built calls [`install_default_crypto_provider`] from `main` +//! instead. //! - an **injected** one, supplied by the embedder through //! [`ProxyServer::with_token_verifier`]. Since Cargo unifies features across a //! whole dependency graph, a backend feature cannot be chosen per binary — @@ -82,7 +98,7 @@ pub mod upstream; pub use auth::crypto::install_default_crypto_provider; pub use client_address::ClientAddress; pub use received::ReceivedRequest; -pub use serve::{serve, serve_with, serve_with_shutdown, ServeOptions}; +pub use serve::{ServeOptions, serve, serve_with, serve_with_shutdown}; pub use service::{ConnectionInfo, ProxyService}; use axum::extract::State; @@ -163,13 +179,6 @@ impl Default for ProxyServer { impl ProxyServer { /// Create from YAML config file. pub fn from_config(config: ProxyConfig) -> Self { - // Earliest point this crate owns: settle the JWT crypto provider here, - // long before the first token arrives. A process whose other crates - // reach jsonwebtoken before any server exists calls - // `install_default_crypto_provider` from `main` instead. - #[cfg(feature = "builtin_jwt")] - auth::crypto::install_default_crypto_provider(); - Self { config, descriptor_pool: None, @@ -396,6 +405,10 @@ impl ProxyServer { /// JWT auth, route policies, forward-auth and ext_authz (`auth:`); build /// the section with [`config::from_yaml`]. + /// + /// Only for an application that leaves authentication to the proxy. One + /// with its own authn/authz, such as structured-id, does not call this (see + /// [the crate docs](crate#applications-with-their-own-authentication)). pub fn with_auth(mut self, auth: config::AuthConfig) -> Self { self.config.auth = Some(auth); self @@ -414,6 +427,10 @@ impl ProxyServer { /// backs the `/verify` forward-auth endpoint. Its signature is `axum`-free /// (see [`hooks::AuthDecider`]), so the embedder never names an HTTP /// framework. + /// + /// Only for an application that leaves authorization to the proxy. One + /// with its own authn/authz, such as structured-id, does not call this (see + /// [the crate docs](crate#applications-with-their-own-authentication)). pub fn with_auth_decider(mut self, decider: Arc) -> Self { self.auth_decider = Some(decider); self @@ -485,6 +502,11 @@ impl ProxyServer { /// `auth.mode` must still be `"jwt"` for the middleware to run; `auth.jwt` /// then only configures claim forwarding, and any key source in it is /// ignored (with a warning), since the verifier owns its own keys. + /// + /// Only for an application that leaves token verification to the proxy. + /// One that verifies its own tokens, such as structured-id, does not call + /// this and has no `auth:` section (see + /// [the crate docs](crate#applications-with-their-own-authentication)). pub fn with_token_verifier(mut self, verifier: Arc) -> Self { self.token_verifier = Some(verifier); self diff --git a/src/main.rs b/src/main.rs index e0258f1..0e7ea53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,7 +60,7 @@ fn main() -> anyhow::Result<()> { async fn stop_requested() { #[cfg(unix)] { - use tokio::signal::unix::{signal, SignalKind}; + use tokio::signal::unix::{SignalKind, signal}; match signal(SignalKind::terminate()) { Ok(mut term) => { tokio::select! { diff --git a/src/oidc/mod.rs b/src/oidc/mod.rs index 74d03d8..a14f829 100644 --- a/src/oidc/mod.rs +++ b/src/oidc/mod.rs @@ -10,9 +10,9 @@ use std::path::Path; use axum::routing::get; use axum::{Json, Router}; -use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use base64::Engine; -use serde_json::{json, Map, Value}; +use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; +use serde_json::{Map, Value, json}; use sha2::{Digest, Sha256}; use crate::config::OidcDiscoveryConfig; diff --git a/src/openapi.rs b/src/openapi.rs index bfe1d91..a9865e4 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -8,12 +8,12 @@ use std::collections::HashSet; use axum::http::Method; use prost_reflect::{DescriptorPool, FieldDescriptor, Kind, MessageDescriptor, MethodDescriptor}; -use serde_json::{json, Map, Value}; +use serde_json::{Map, Value, json}; use crate::config::{AliasConfig, OpenApiConfig}; +use crate::transcode::RpcSelection; use crate::transcode::request::{self, BodyMapping}; use crate::transcode::rule::{self, HttpBinding, RouteMethod}; -use crate::transcode::RpcSelection; use crate::transcode::{self, httpbody, path}; /// How a field's value travels, which decides its schema where the two differ. diff --git a/src/openapi/tests.rs b/src/openapi/tests.rs index ae83a24..8102808 100644 --- a/src/openapi/tests.rs +++ b/src/openapi/tests.rs @@ -364,11 +364,12 @@ fn a_template_the_router_cannot_serve_is_not_documented() { let spec = spec(&[]); let paths = spec["paths"].as_object().unwrap(); assert!(!paths.contains_key("/v1/broken/a{name}b"), "{paths:?}"); - assert!(!paths.values().any(|item| item - .as_object() - .unwrap() - .values() - .any(|op| op["operationId"] == "Api.Broken"))); + assert!(!paths.values().any(|item| { + item.as_object() + .unwrap() + .values() + .any(|op| op["operationId"] == "Api.Broken") + })); } #[test] @@ -710,8 +711,8 @@ fn every_response_validates_against_its_schema() { let pool = test_pool(); let spec = spec(&[]); let components = &spec["components"]["schemas"]; - let schema = &spec["paths"]["/v1/wire"]["post"]["responses"]["200"]["content"] - ["application/json"]["schema"]; + let schema = &spec["paths"]["/v1/wire"]["post"]["responses"]["200"]["content"]["application/json"] + ["schema"]; let wire = pool.get_message_by_name("test.v1.Wire").unwrap(); let choices = wire.oneofs().map(|o| o.fields().len()).max().unwrap(); // `Value.kind` has six members: every one of them is sampled too. diff --git a/src/received.rs b/src/received.rs index 0c66e89..1a88553 100644 --- a/src/received.rs +++ b/src/received.rs @@ -8,8 +8,8 @@ //! request extensions. The proxy records them and checks nothing about them. //! A remote upstream never sees request extensions. -use axum::http::uri::PathAndQuery; use axum::http::Method; +use axum::http::uri::PathAndQuery; /// The HTTP request a transcoded gRPC call was made from, as the proxy /// received it: its method, its path and query, and the gRPC path of the RPC diff --git a/src/serve.rs b/src/serve.rs index b9de352..5a5d18b 100644 --- a/src/serve.rs +++ b/src/serve.rs @@ -15,7 +15,7 @@ use hyper_util::service::TowerToHyperService; use pin_project_lite::pin_project; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio::net::TcpListener; -use tokio::sync::{watch, OwnedSemaphorePermit, Semaphore}; +use tokio::sync::{OwnedSemaphorePermit, Semaphore, watch}; use tokio::task::JoinSet; use tonic::transport::server::Connected; diff --git a/src/serve/idle.rs b/src/serve/idle.rs index 96e4931..d7616d8 100644 --- a/src/serve/idle.rs +++ b/src/serve/idle.rs @@ -3,9 +3,9 @@ use std::convert::Infallible; use std::future::Future; use std::pin::Pin; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use std::task::{ready, Context, Poll}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::task::{Context, Poll, ready}; use std::time::Duration; use axum::body::Body; diff --git a/src/serve/tests.rs b/src/serve/tests.rs index 7ad44cb..e556a7b 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"); } } @@ -291,6 +291,41 @@ fn client_auth_without_a_client_ca_is_an_error() { } } +/// The listener YAML for the RSA identity in the TLS fixtures. +fn rsa_listener_yaml() -> String { + let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src/tls/testdata"); + format!("listen:\n tls:\n cert_file: {dir}/rsa.pem\n key_file: {dir}/rsa.key.pem\n") +} + +/// The RustCrypto provider presents no RSA identity, and the error names the +/// key file it refused. +#[test] +fn the_rustcrypto_provider_refuses_an_rsa_listener_key() { + rustls::crypto::CryptoProvider::install_default(sid_crypto::tls::rustcrypto::provider()) + .expect("each test runs in its own process"); + let err = crate::ProxyServer::from_yaml_str(&rsa_listener_yaml()) + .unwrap() + .serve_options() + .unwrap_err() + .to_string(); + assert!(err.contains("rsa.key.pem"), "{err}"); + assert!(err.contains("P-256, P-384 or Ed25519"), "{err}"); +} + +/// A provider the host installed keeps its own key contract, whichever +/// backend features the dependency graph unified: one that signs with RSA +/// serves an RSA listener. +#[test] +fn a_host_provider_with_rsa_serves_an_rsa_listener_key() { + rustls::crypto::CryptoProvider::install_default(rustls::crypto::ring::default_provider()) + .expect("each test runs in its own process"); + let options = crate::ProxyServer::from_yaml_str(&rsa_listener_yaml()) + .unwrap() + .serve_options() + .unwrap(); + assert!(options.tls.is_some()); +} + #[test] fn a_missing_tls_file_names_itself() { install_provider(); diff --git a/src/service.rs b/src/service.rs index 1146339..9b11373 100644 --- a/src/service.rs +++ b/src/service.rs @@ -29,7 +29,7 @@ pub type TlsConnectInfo = as Connected>::ConnectInfo; use crate::upstream::{ - grpc_protocol, is_grpc_web_preflight, BoxError, GrpcProtocol, PassThrough, Upstream, + BoxError, GrpcProtocol, PassThrough, Upstream, grpc_protocol, is_grpc_web_preflight, }; /// The proxy as a tower service, built by diff --git a/src/shield/matcher.rs b/src/shield/matcher.rs index 6ab1523..1e5b21c 100644 --- a/src/shield/matcher.rs +++ b/src/shield/matcher.rs @@ -274,16 +274,20 @@ mod tests { #[test] fn zero_rate_or_burst_is_rejected() { - assert!(compile_profile(&LimitProfileConfig { - rate: "0/min".to_string(), - burst: None, - }) - .is_err()); - assert!(compile_profile(&LimitProfileConfig { - rate: "100/min".to_string(), - burst: Some(0), - }) - .is_err()); + assert!( + compile_profile(&LimitProfileConfig { + rate: "0/min".to_string(), + burst: None, + }) + .is_err() + ); + assert!( + compile_profile(&LimitProfileConfig { + rate: "100/min".to_string(), + burst: Some(0), + }) + .is_err() + ); } #[test] 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/shield/tests.rs b/src/shield/tests.rs index bbcc5ae..f642421 100644 --- a/src/shield/tests.rs +++ b/src/shield/tests.rs @@ -3,10 +3,10 @@ use super::*; use crate::config::{KeySourceConfig, LimitProfileConfig, RateRuleConfig, ShieldConfig}; +use axum::Router; use axum::body::Body; use axum::http::{Request, StatusCode}; use axum::routing::get; -use axum::Router; use std::collections::HashMap; use tower::ServiceExt; @@ -415,8 +415,8 @@ mod two_phase { } fn token(sk: &SigningKey, sub: &str) -> String { - use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; + use base64::engine::general_purpose::URL_SAFE_NO_PAD; let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"EdDSA","typ":"JWT"}"#); // Far-future exp so default expiry validation passes. let claims = serde_json::json!({ "sub": sub, "exp": 9_999_999_999u64 }); diff --git a/src/tls.rs b/src/tls.rs index 8e5abd0..82a9d80 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -92,9 +92,15 @@ pub(crate) fn server_config( builder.with_client_cert_verifier(verifier) } }; - let mut server = builder - .with_single_cert(chain, key) - .map_err(|e| format!("{}: {e}", config.cert_file.display()))?; + // The provider's own key policy decides which identities it presents (the + // RustCrypto one refuses RSA), and the error may concern either file. + let mut server = builder.with_single_cert(chain, key).map_err(|e| { + format!( + "{} / {}: {e}", + config.cert_file.display(), + config.key_file.display() + ) + })?; server.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; Ok(server) } @@ -126,8 +132,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. +/// The explicitly enabled aws-lc backend retains precedence, as for JWT verification. #[cfg(feature = "aws_lc_rs")] fn builtin_provider() -> Option { Some(rustls::crypto::aws_lc_rs::default_provider()) @@ -136,7 +141,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..4abe4ef 100644 --- a/src/tls/tests.rs +++ b/src/tls/tests.rs @@ -40,13 +40,16 @@ 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(); + // An independent peer: the client under test is the proxy's, and the + // server must present RSA identities, which the RustCrypto provider refuses. + 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 +83,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) @@ -172,11 +176,13 @@ fn production_config_trusts_the_bundled_mozilla_roots() { // The backend's provider, and the bundled root store is not empty. let config = client_config().unwrap(); assert!(!webpki_roots::TLS_SERVER_ROOTS.is_empty()); - assert!(config - .crypto_provider() - .cipher_suites - .iter() - .any(|suite| suite.version() == &rustls::version::TLS13)); + assert!( + config + .crypto_provider() + .cipher_suites + .iter() + .any(|suite| suite.version() == &rustls::version::TLS13) + ); } /// Which provider `p` is: its key provider's type, named by `Debug`. @@ -194,14 +200,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")))] @@ -213,7 +225,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") }) @@ -223,7 +235,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()); } @@ -245,7 +257,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}"); diff --git a/src/transcode/codec.rs b/src/transcode/codec.rs index 2385e5d..04729fc 100644 --- a/src/transcode/codec.rs +++ b/src/transcode/codec.rs @@ -5,8 +5,8 @@ use prost::Message; use prost_reflect::{Cardinality, DynamicMessage, Kind, MessageDescriptor, ReflectMessage, Value}; -use tonic::codec::{BufferSettings, Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}; use tonic::Status; +use tonic::codec::{BufferSettings, Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}; /// Encoder for `DynamicMessage` → wire bytes. #[derive(Debug, Clone)] diff --git a/src/transcode/error.rs b/src/transcode/error.rs index 11ff761..fb6ecc6 100644 --- a/src/transcode/error.rs +++ b/src/transcode/error.rs @@ -5,9 +5,9 @@ //! renders the typed `google.rpc.Status` details the upstream attached in the //! `grpc-status-details-bin` trailer. +use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; -use axum::Json; use base64::Engine as _; use globset::GlobMatcher; use prost::Message as _; diff --git a/src/transcode/error/tests.rs b/src/transcode/error/tests.rs index 465509b..443a43c 100644 --- a/src/transcode/error/tests.rs +++ b/src/transcode/error/tests.rs @@ -346,7 +346,7 @@ fn object_valued_well_known_details_go_under_value() { // under `value`, decided by the type, not by the shape of its JSON: a // Struct is a JSON object yet still goes under `value`, and its own // `@type` key cannot overwrite the detail's. - use prost_reflect::prost_types::{value::Kind, Struct, Value as PbValue}; + use prost_reflect::prost_types::{Struct, Value as PbValue, value::Kind}; let mut fields = std::collections::BTreeMap::new(); fields.insert( "@type".to_string(), @@ -674,7 +674,7 @@ fn struct_keys_that_look_like_debug_info_are_kept() { // DebugInfo is withheld where an Any packs it. A Struct is data: a key // `@type` naming DebugInfo is just a string there, and the Struct must // stay whole and validly wrapped. - use prost_reflect::prost_types::{value::Kind, Struct, Value as PbValue}; + use prost_reflect::prost_types::{Struct, Value as PbValue, value::Kind}; let mut fields = std::collections::BTreeMap::new(); fields.insert( "@type".to_string(), diff --git a/src/transcode/mod.rs b/src/transcode/mod.rs index dc550d9..32a9d35 100644 --- a/src/transcode/mod.rs +++ b/src/transcode/mod.rs @@ -24,13 +24,13 @@ mod table; pub use path::proto_path_to_axum; pub use select::RpcSelection; +use axum::Router; use axum::body::{Body, Bytes}; use axum::extract::{FromRequest, FromRequestParts, OriginalUri, RawPathParams, Request, State}; use axum::http::header::{ALLOW, CONTENT_TYPE}; use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri}; use axum::response::sse::{Event, KeepAlive, Sse}; use axum::response::{IntoResponse, Response}; -use axum::Router; use futures::{StreamExt, TryStreamExt}; use prost_reflect::{ DescriptorPool, DynamicMessage, FieldDescriptor, MessageDescriptor, MethodDescriptor, @@ -720,7 +720,7 @@ async fn dispatch( let (table, index) = match choice { Choice::Route { table, index } => (table, index), Choice::MethodNotAllowed { allow, .. } => { - return (StatusCode::METHOD_NOT_ALLOWED, [(ALLOW, allow)]).into_response() + return (StatusCode::METHOD_NOT_ALLOWED, [(ALLOW, allow)]).into_response(); } // Only routes served without a `Chooser` get here. Choice::NotFound => return StatusCode::NOT_FOUND.into_response(), diff --git a/src/transcode/request.rs b/src/transcode/request.rs index 2ddc6ca..3cc5596 100644 --- a/src/transcode/request.rs +++ b/src/transcode/request.rs @@ -14,8 +14,8 @@ use std::collections::HashMap; use prost_reflect::{ DynamicMessage, FieldDescriptor, Kind, MessageDescriptor, ReflectMessage, Value, }; -use serde::de::value::StrDeserializer; use serde::Deserializer; +use serde::de::value::StrDeserializer; use serde_json::Value as JsonValue; pub(crate) use presence::has_special_json; diff --git a/src/transcode/request/tests.rs b/src/transcode/request/tests.rs index a9410f6..8ec199c 100644 --- a/src/transcode/request/tests.rs +++ b/src/transcode/request/tests.rs @@ -1,10 +1,10 @@ use super::*; +use prost_reflect::DescriptorPool; use prost_reflect::prost::Message; use prost_reflect::prost_types::{ - field_descriptor_proto::{Label, Type}, DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet, + field_descriptor_proto::{Label, Type}, }; -use prost_reflect::DescriptorPool; use serde_json::Value; fn field( diff --git a/src/transcode/response.rs b/src/transcode/response.rs index 4062967..d1b0a25 100644 --- a/src/transcode/response.rs +++ b/src/transcode/response.rs @@ -7,7 +7,7 @@ //! (grpc-gateway's convention). use axum::body::Body; -use axum::http::header::{Entry, OccupiedEntry, CONTENT_TYPE}; +use axum::http::header::{CONTENT_TYPE, Entry, OccupiedEntry}; use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; use axum::response::Response; use tonic::metadata::MetadataMap; diff --git a/src/transcode/rule/tests.rs b/src/transcode/rule/tests.rs index eb6203c..0da295f 100644 --- a/src/transcode/rule/tests.rs +++ b/src/transcode/rule/tests.rs @@ -7,8 +7,8 @@ use prost_reflect::DescriptorPool; fn http_rule_descriptor() -> prost_reflect::MessageDescriptor { use prost_reflect::prost::Message; use prost_reflect::prost_types::{ - field_descriptor_proto::{Label, Type}, DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet, + field_descriptor_proto::{Label, Type}, }; let str_field = |name: &str, num: i32| FieldDescriptorProto { diff --git a/src/transcode/table.rs b/src/transcode/table.rs index 2bca486..489f960 100644 --- a/src/transcode/table.rs +++ b/src/transcode/table.rs @@ -16,7 +16,7 @@ use std::ops::ControlFlow; use axum::http::{HeaderValue, Method}; use rustc_hash::FxHashMap; -use super::path::{decode_multi_segment, Composite, MountedPath, Part}; +use super::path::{Composite, MountedPath, Part, decode_multi_segment}; use super::rule::RouteMethod; use super::{PathParams, RouteEntry}; @@ -881,7 +881,7 @@ impl PathTable { match &binding.entry.http_method { RouteMethod::Any => return Some(Choice::Route { table, index }), RouteMethod::One(bound) if bound == method => { - return Some(Choice::Route { table, index }) + return Some(Choice::Route { table, index }); } // A GET binding answers HEAD too, after one bound to HEAD. RouteMethod::One(bound) if *bound == Method::GET && *method == Method::HEAD => { diff --git a/src/transcode/tests.rs b/src/transcode/tests.rs index 2f5ebec..3fe996b 100644 --- a/src/transcode/tests.rs +++ b/src/transcode/tests.rs @@ -134,19 +134,25 @@ fn non_terminal_field_template_is_spelled_out_in_the_path() { ); let path = proto_path_to_axum("/v1/{name=projects/*}/topics"); let _router: Router<()> = Router::new().route(&path, get(|| async { "ok" })); - assert!(path::MountedPath::new("/v1/{name=projects/*}/topics") - .unsupported - .is_none()); + assert!( + path::MountedPath::new("/v1/{name=projects/*}/topics") + .unsupported + .is_none() + ); // `**` before the last segment has no faithful form: the binding is // left out rather than widened to one segment. A terminal one is still // a real catch-all. - assert!(path::MountedPath::new("/v1/{rest=**}/tail") - .unsupported - .is_some()); - assert!(path::MountedPath::new("/v1/{rest=a/**}/tail") - .unsupported - .is_some()); + assert!( + path::MountedPath::new("/v1/{rest=**}/tail") + .unsupported + .is_some() + ); + assert!( + path::MountedPath::new("/v1/{rest=a/**}/tail") + .unsupported + .is_some() + ); assert_eq!( proto_path_to_axum("/v1/files/{rest=**}"), "/v1/files/{*rest}" @@ -1059,8 +1065,8 @@ fn item_message() -> DynamicMessage { fn item_message_named(name: &str, count: i64) -> DynamicMessage { use prost_reflect::prost::Message; use prost_reflect::prost_types::{ - field_descriptor_proto::{Label, Type}, DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet, + field_descriptor_proto::{Label, Type}, }; let item = DescriptorProto { @@ -1191,8 +1197,8 @@ async fn ndjson_terminal_frame_carries_status_details() { /// no pool knows, so it cannot be serialized to JSON. fn unserializable_message() -> DynamicMessage { use prost_reflect::prost_types::{ - field_descriptor_proto::{Label, Type}, DescriptorProto, FieldDescriptorProto, FileDescriptorProto, + field_descriptor_proto::{Label, Type}, }; let wrapper = DescriptorProto { diff --git a/src/upstream.rs b/src/upstream.rs index 0382304..810cb16 100644 --- a/src/upstream.rs +++ b/src/upstream.rs @@ -9,7 +9,7 @@ use std::convert::Infallible; use std::future::Future; use std::pin::Pin; -use std::task::{ready, Context, Poll}; +use std::task::{Context, Poll, ready}; use bytes::Bytes; use pin_project_lite::pin_project; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 80da3cd..6c7385c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -8,10 +8,10 @@ use std::convert::Infallible; use axum::body::Body; use http::StatusCode; use prost_reflect::DescriptorPool; -use structured_proxy::transcode::error::ErrorDetailsPolicy; use structured_proxy::ProxyServer; -use tower::util::BoxCloneSyncService; +use structured_proxy::transcode::error::ErrorDetailsPolicy; use tower::ServiceExt; +use tower::util::BoxCloneSyncService; mod protos; diff --git a/tests/custom_verbs.rs b/tests/custom_verbs.rs index 9f7d319..7b4179e 100644 --- a/tests/custom_verbs.rs +++ b/tests/custom_verbs.rs @@ -11,7 +11,7 @@ mod common; use std::convert::Infallible; -use std::future::{ready, Ready}; +use std::future::{Ready, ready}; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; @@ -19,7 +19,7 @@ use std::task::{Context, Poll}; use axum::body::Body; use http::{Method, StatusCode}; use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor, Value as PbValue}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use structured_proxy::hooks::{ExtraRoute, ExtraRouteHandler, RouteRequest, RouteResponse}; use structured_proxy::transcode::codec::DynamicCodec; use tower::ServiceExt; diff --git a/tests/dynamic_codec.rs b/tests/dynamic_codec.rs index 698e1b5..7cf6afc 100644 --- a/tests/dynamic_codec.rs +++ b/tests/dynamic_codec.rs @@ -7,14 +7,14 @@ //! `Missing request message` and leaves the client without its response. use std::convert::Infallible; -use std::future::{ready, Ready}; +use std::future::{Ready, ready}; use std::pin::Pin; use std::task::{Context, Poll}; use prost::Message as _; use prost_reflect::prost_types::{ - field_descriptor_proto::{Label, Type}, DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet, + field_descriptor_proto::{Label, Type}, }; use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor, ReflectMessage}; use structured_proxy::transcode::codec::DynamicCodec; diff --git a/tests/edge.rs b/tests/edge.rs index 0b8ecb7..f7d4c17 100644 --- a/tests/edge.rs +++ b/tests/edge.rs @@ -16,8 +16,8 @@ use axum::body::Body; use http::StatusCode; use prost_reflect::{DescriptorPool, DynamicMessage, Value as PbValue}; use serde_json::Value; -use structured_proxy::transcode::codec::DynamicCodec; use structured_proxy::ProxyServer; +use structured_proxy::transcode::codec::DynamicCodec; use tokio::io::{AsyncReadExt, AsyncWriteExt}; const EDGE_PROTO: &str = r#" @@ -802,10 +802,12 @@ async fn a_grpc_web_call_from_an_unlisted_origin_gets_no_allowance() { let response = tower::ServiceExt::oneshot(grpc_web_proxy_with(CORS_YAML), request) .await .unwrap(); - assert!(response - .headers() - .get("access-control-allow-origin") - .is_none()); + assert!( + response + .headers() + .get("access-control-allow-origin") + .is_none() + ); } #[tokio::test] diff --git a/tests/embedded.rs b/tests/embedded.rs index 4f6cd67..c70578f 100644 --- a/tests/embedded.rs +++ b/tests/embedded.rs @@ -8,11 +8,11 @@ //! it fails if any of them can no longer be built from its default or its //! constructor. +use structured_proxy::ProxyServer; use structured_proxy::config::{ AliasConfig, ClientAddressConfig, ConcurrencyConfig, DescriptorSource, ListenTlsConfig, ProxyConfig, ScopeConfig, Traffic, UpstreamConfig, }; -use structured_proxy::ProxyServer; #[test] fn embedded_config_is_constructible() { @@ -33,10 +33,12 @@ fn embedded_config_is_constructible() { client_address.trusted_proxies = vec!["10.0.0.0/8".into()]; config.client_address = client_address; // The default list, unlike a literal, keeps the serde defaults. - assert!(config - .forwarded_headers - .iter() - .any(|h| h == "authorization")); + assert!( + config + .forwarded_headers + .iter() + .any(|h| h == "authorization") + ); // The server accepts a programmatically-built config (the embedded path). let _server = ProxyServer::from_config(config); } diff --git a/tests/error_details.rs b/tests/error_details.rs index 5075aac..2c36c03 100644 --- a/tests/error_details.rs +++ b/tests/error_details.rs @@ -11,7 +11,7 @@ mod common; use std::convert::Infallible; -use std::future::{ready, Ready}; +use std::future::{Ready, ready}; use std::pin::Pin; use std::task::{Context, Poll}; @@ -20,7 +20,7 @@ use futures::stream::BoxStream; use http::StatusCode; use prost::Message as _; use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use structured_proxy::transcode::codec::DynamicCodec; use structured_proxy::transcode::error::ErrorDetailsPolicy; use tonic_types::{BadRequest, DebugInfo, ErrorDetail, ErrorInfo, FieldViolation, StatusExt}; diff --git a/tests/forwarded_headers.rs b/tests/forwarded_headers.rs index 60bd4dc..9c78c3b 100644 --- a/tests/forwarded_headers.rs +++ b/tests/forwarded_headers.rs @@ -5,7 +5,7 @@ mod common; use std::convert::Infallible; -use std::future::{ready, Ready}; +use std::future::{Ready, ready}; use std::pin::Pin; use std::task::{Context, Poll}; diff --git a/tests/hooks.rs b/tests/hooks.rs index ba92521..3ea0ee1 100644 --- a/tests/hooks.rs +++ b/tests/hooks.rs @@ -11,12 +11,12 @@ use std::sync::Arc; use async_trait::async_trait; use http::{HeaderMap, Method, StatusCode}; +use structured_proxy::ProxyServer; use structured_proxy::config::ProxyConfig; use structured_proxy::hooks::{ AuthDecider, Decision, ExtraRoute, ExtraRouteHandler, MetadataDocument, OidcBackend, RequestParts, RouteRequest, RouteResponse, TokenVerifier, }; -use structured_proxy::ProxyServer; // --- embedder-supplied hook impls (axum-free) ---------------------------- @@ -195,9 +195,11 @@ async fn options_verify_request_is_decided_not_answered_by_cors() { .await .unwrap(); assert_eq!(preflight.status(), StatusCode::OK); - assert!(preflight - .headers() - .contains_key("access-control-allow-methods")); + assert!( + preflight + .headers() + .contains_key("access-control-allow-methods") + ); } #[tokio::test] @@ -399,10 +401,12 @@ auth: let result = ProxyServer::from_config(config).router(); let _ = std::fs::remove_file(&pem_path); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("registered by more than one endpoint")); + assert!( + result + .unwrap_err() + .to_string() + .contains("registered by more than one endpoint") + ); } #[tokio::test] @@ -423,10 +427,12 @@ openapi: .with_verify_path("/docs") .router(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("registered by more than one endpoint")); + assert!( + result + .unwrap_err() + .to_string() + .contains("registered by more than one endpoint") + ); } #[tokio::test] @@ -441,10 +447,12 @@ async fn verify_path_colliding_with_oidc_route_is_a_clean_error() { .with_verify_path("/.well-known/openid-configuration") .router(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("registered by more than one endpoint")); + assert!( + result + .unwrap_err() + .to_string() + .contains("registered by more than one endpoint") + ); } #[tokio::test] @@ -461,10 +469,12 @@ async fn extra_route_colliding_with_builtin_is_a_clean_error() { )]) .router(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("registered by more than one endpoint")); + assert!( + result + .unwrap_err() + .to_string() + .contains("registered by more than one endpoint") + ); } #[tokio::test] @@ -514,10 +524,12 @@ async fn structurally_identical_dynamic_routes_are_a_clean_error() { ]) .router(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("registered by more than one endpoint")); + assert!( + result + .unwrap_err() + .to_string() + .contains("registered by more than one endpoint") + ); } #[tokio::test] @@ -530,10 +542,12 @@ async fn malformed_extra_route_path_is_a_clean_error() { .with_extra_routes([ExtraRoute::new(Method::GET, "ping", Arc::new(PingHandler))]) .router(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("must start with '/'")); + assert!( + result + .unwrap_err() + .to_string() + .contains("must start with '/'") + ); } #[tokio::test] @@ -545,10 +559,12 @@ async fn malformed_verify_path_is_a_clean_error() { .with_verify_path("auth/verify") // missing leading '/' .router(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("must start with '/'")); + assert!( + result + .unwrap_err() + .to_string() + .contains("must start with '/'") + ); } #[tokio::test] @@ -587,10 +603,12 @@ auth: .router(); let _ = std::fs::remove_file(&pem_path); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("registered by more than one endpoint")); + assert!( + result + .unwrap_err() + .to_string() + .contains("registered by more than one endpoint") + ); } #[tokio::test] @@ -604,10 +622,12 @@ async fn verify_path_colliding_with_probe_is_a_clean_error() { .with_verify_path("/health") .router(); assert!(result.is_err()); - assert!(result - .unwrap_err() - .to_string() - .contains("registered by more than one endpoint")); + assert!( + result + .unwrap_err() + .to_string() + .contains("registered by more than one endpoint") + ); } #[tokio::test] diff --git a/tests/request_mapping.rs b/tests/request_mapping.rs index 67e906b..d9870a9 100644 --- a/tests/request_mapping.rs +++ b/tests/request_mapping.rs @@ -9,14 +9,14 @@ mod common; use std::convert::Infallible; -use std::future::{ready, Ready}; +use std::future::{Ready, ready}; use std::pin::Pin; use std::task::{Context, Poll}; use axum::body::Body; use http::StatusCode; use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use structured_proxy::transcode::codec::DynamicCodec; const ITEMS_PROTO: &str = r#" diff --git a/tests/shutdown.rs b/tests/shutdown.rs index 4a4a9f2..200e3cd 100644 --- a/tests/shutdown.rs +++ b/tests/shutdown.rs @@ -239,7 +239,7 @@ impl Proxy { 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"); } } @@ -253,12 +253,13 @@ async fn connect(addr: SocketAddr, tls: bool, alpn: &[u8]) -> 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/streaming_request.rs b/tests/streaming_request.rs index 17d23e1..4ae4972 100644 --- a/tests/streaming_request.rs +++ b/tests/streaming_request.rs @@ -9,7 +9,7 @@ mod common; use std::convert::Infallible; -use std::future::{ready, Ready}; +use std::future::{Ready, ready}; use std::pin::Pin; use std::task::{Context, Poll}; @@ -17,7 +17,7 @@ use axum::body::Body; use futures::stream::BoxStream; use http::StatusCode; use prost_reflect::{DescriptorPool, DynamicMessage, MessageDescriptor}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use structured_proxy::transcode::codec::DynamicCodec; const THINGS_PROTO: &str = r#" diff --git a/tests/tls.rs b/tests/tls.rs index e5fe9b9..c803084 100644 --- a/tests/tls.rs +++ b/tests/tls.rs @@ -8,7 +8,7 @@ mod protos; use std::convert::Infallible; -use std::future::{ready, Ready}; +use std::future::{Ready, ready}; use std::net::SocketAddr; use std::pin::Pin; use std::sync::Arc; @@ -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(); diff --git a/tests/upstream_controls.rs b/tests/upstream_controls.rs index 4d59ea8..429c556 100644 --- a/tests/upstream_controls.rs +++ b/tests/upstream_controls.rs @@ -12,7 +12,7 @@ mod common; use std::convert::Infallible; -use std::future::{ready, Ready}; +use std::future::{Ready, ready}; use std::pin::Pin; use std::task::{Context, Poll}; @@ -23,10 +23,10 @@ use http::{HeaderMap, HeaderName, Method, StatusCode}; use http_body_util::BodyExt; use prost::Message as _; use prost_reflect::{DescriptorPool, DynamicMessage, Value as PbValue}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; +use structured_proxy::ProxyServer; use structured_proxy::transcode::codec::DynamicCodec; use structured_proxy::transcode::error::ErrorDetailsPolicy; -use structured_proxy::ProxyServer; use tonic::metadata::{AsciiMetadataValue, BinaryMetadataValue}; use tower::ServiceExt;