diff --git a/.dockerignore b/.dockerignore index 3c429d9..f5d5c9a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ target/ .git/ *.md +!README.md certs/ diff --git a/Cargo.toml b/Cargo.toml index 95317a5..3e46b57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,82 +1,23 @@ [workspace] -members = [".", "attested-tls"] +members = ["crates/attested-tls", "crates/attested-tls-proxy"] +default-members = ["crates/attested-tls-proxy"] +resolver = "3" -[package] -name = "attested-tls-proxy" -version = "1.1.1" -edition = "2024" -license = "MIT" -description = "An HTTP attested TLS proxy server and client for secure communication with CVM services" -repository = "https://github.com/flashbots/attested-tls-proxy" -keywords = ["attested-TLS", "CVM", "TDX"] - -[dependencies] -attested-tls = { path = "attested-tls", default-features = false } -tokio = { version = "1.48.0", features = ["full"] } -tokio-rustls = { version = "0.26.4", default-features = false, features = [ - "aws_lc_rs", -] } -x509-parser = { version = "0.18.0", features = ["verify"] } -thiserror = "2.0.17" -clap = { version = "4.5.51", features = ["derive", "env"] } -rustls-pemfile = "2.2.0" -anyhow = "1.0.100" -pem-rfc7468 = { version = "0.7.0", features = ["std"] } -hyper = { version = "1.7.0", features = ["server", "http2"] } -h2 = "0.4.12" -hyper-util = { version = "0.1.17", features = ["tokio"] } -http-body-util = "0.1.3" +[workspace.dependencies] bytes = "1.11.1" http = "1.3.1" -serde_json = "1.0.145" -serde = "1.0.228" -reqwest = { version = "0.13.4", default-features = false, features = [ - "rustls-no-provider", -] } -webpki-roots = "1.0.4" -tracing = "0.1.41" -tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } -axum = "0.8.8" -tower-http = { version = "0.6.7", features = ["fs"] } -rsa = { version = "0.9", default-features = false } -p256 = { version = "0.13.2", features = ["pkcs8"] } -pkcs1 = "0.7.5" -pkcs8 = "0.10.2" +http-body-util = "0.1.3" +hyper = "1.7.0" +hyper-util = "0.1.17" rcgen = "0.14.5" -pin-project-lite = "0.2.16" -pccs = { git = "https://github.com/flashbots/attested-tls", branch = "main" } - -[dev-dependencies] +serde_json = "1.0.145" tempfile = "3.23.0" -tdx-quote = { version = "0.0.5", features = ["mock"] } -attested-tls = { path = "attested-tls", features = ["test-helpers", "mock"] } -jsonrpsee = { version = "0.26.0", features = ["server"] } - -[features] -default = [] - -# Adds support for Microsoft Azure attestation generation and verification -azure = ["attested-tls/azure"] - -[package.metadata.deb] -maintainer = "Flashbots Team " -depends = "$auto" -section = "network" -priority = "optional" -maintainer-scripts = "pkg/debian" -assets = [ - [ - "target/reproducible/attested-tls-proxy", - "usr/bin/", - "755", - ], - [ - "LICENSE", - "usr/share/doc/attested-tls-proxy/", - "644", - ], -] -systemd-units = { enable = false, start = false, unit-name = "attested-tls-proxy" } +thiserror = "2.0.17" +tokio = "1.48.0" +tokio-rustls = { version = "0.26.4", default-features = false } +tracing = "0.1.41" +webpki-roots = "1.0.4" +x509-parser = "0.18.0" [profile.reproducible] inherits = "release" diff --git a/Dockerfile b/Dockerfile index 5c20aad..2e5d918 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,9 +26,9 @@ RUN build_features="$FEATURES"; \ fi; \ fi; \ if [ -n "$build_features" ]; then \ - cargo build --release --no-default-features --features "$build_features"; \ + cargo build -p attested-tls-proxy --locked --release --no-default-features --features "$build_features"; \ else \ - cargo build --release --no-default-features; \ + cargo build -p attested-tls-proxy --locked --release --no-default-features; \ fi # Runtime stage diff --git a/Makefile b/Makefile index eddc1c8..4182a84 100644 --- a/Makefile +++ b/Makefile @@ -72,11 +72,11 @@ endif .PHONY: build build: ## Build (release version) - $(BUILD_ENV) cargo build $(FEATURE_ARGS) --locked $(if $(BUILD_TARGET),--target $(BUILD_TARGET)) --profile $(BUILD_PROFILE) + $(BUILD_ENV) cargo build -p attested-tls-proxy $(FEATURE_ARGS) --locked $(if $(BUILD_TARGET),--target $(BUILD_TARGET)) --profile $(BUILD_PROFILE) .PHONY: build-dev build-dev: ## Build (debug version) - cargo build $(FEATURE_ARGS) + cargo build -p attested-tls-proxy $(FEATURE_ARGS) ##@ Debian Packages @@ -95,18 +95,18 @@ build-deb: install-cargo-deb ## Build Debian package .PHONY: lint lint: ## Run the linters - cargo fmt -- --check + cargo fmt --all -- --check cargo clippy --workspace $(FEATURE_ARGS) -- -D warnings .PHONY: test test: - cargo test --verbose $(FEATURE_ARGS) + cargo test --workspace --verbose $(FEATURE_ARGS) .PHONY: lt lt: lint test ## Run "lint" and "test" .PHONY: fmt fmt: ## Format the code - cargo fmt - cargo fix --allow-staged - cargo clippy $(FEATURE_ARGS) --fix --allow-staged + cargo fmt --all + cargo fix --workspace --allow-staged + cargo clippy --workspace $(FEATURE_ARGS) --fix --allow-staged diff --git a/README.md b/README.md index 0d03647..218cc8b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This is a reverse HTTP proxy allowing a normal HTTP client to communicate with a This is designed to be an alternative to [`cvm-reverse-proxy`](https://github.com/flashbots/cvm-reverse-proxy). Unlike `cvm-reverse-proxy` this uses post-handshake remote-attested TLS, meaning regular CA-signed TLS certificates can be used. -Details of the remote-attested TLS protocol are in [attested-tls/README.md](attested-tls/README.md). This is provided as a separate crate for other uses than HTTP proxying. +Details of the remote-attested TLS protocol are in [crates/attested-tls/README.md](crates/attested-tls/README.md). This is provided as a separate crate for other uses than HTTP proxying. The proxy-client, on starting, immediately connects to the proxy-server and an attestation-verification exchange is made. This attested-TLS channel is then re-used for requests from that proxy-client instance. If the channel is lost, the client reconnects automatically and repeats the attestation exchange before forwarding subsequent requests. @@ -94,7 +94,7 @@ Proxy-client to proxy-server connections use TLS 1.3. The protocol name `flashbots-ratls/1` must be given in the TLS configuration for ALPN protocol negotiation during the TLS handshake. Future versions of this protocol will use incrementing version numbers, eg: `flashbots-ratls/2`. -Immediately after the TLS handshake, an attestation exchange is made. Details of how this works are in the [attested-tls protocol specification](attested-tls/README.md#protocol-specification). +Immediately after the TLS handshake, an attestation exchange is made. Details of how this works are in the [attested-tls protocol specification](crates/attested-tls/README.md#protocol-specification). Following a successful attestation exchange, the client can make HTTP requests, and the server will forward them to the target service. @@ -102,6 +102,27 @@ As described above, the server will inject measurement data into the request hea The proxy client and proxy server support HTTP/2 and HTTP/1.1 over their attested-TLS channel, with HTTP/2 preferred. The HTTP protocol is combined with the attested-TLS protocol version in ALPN, producing `flashbots-ratls/1+h2` or `flashbots-ratls/1+http/1.1`. A negotiated `flashbots-ratls/1` value without an HTTP suffix falls back to HTTP/1.1. +## Repository layout and development + +- `crates/attested-tls`: the attested TLS protocol library. +- `crates/attested-tls-proxy`: the HTTP proxy library and CLI. HTTP forwarding + lives in `src/http`; its public API is also re-exported at the crate root. + +Run the commands below from the repository root. The proxy is the default +workspace member, so existing `cargo run -- ...` commands still work. Build +artifacts remain in the root `target/` directory. + +```sh +cargo build -p attested-tls-proxy --locked +cargo test --workspace --features azure --all-targets --locked +cargo clippy --workspace --features azure --locked -- -D warnings +cargo fmt --all -- --check +``` + +Omit `--features azure` on systems without the TPM dependencies described below. +To install from a local checkout, use `cargo install --path crates/attested-tls-proxy --locked`. +Docker and Compose commands also run from the repository root. + ## Dependencies and feature flags The `azure` feature, for Microsoft Azure attestation requires [tpm2](https://tpm2-software.github.io) to be installed. On Debian-based systems this is provided by [`libtss2-dev`](https://packages.debian.org/trixie/libtss2-dev), and on nix `tpm2-tss`. This dependency is currently not packaged for MacOS, meaning currently it is not possible to compile or run with the `azure` feature on MacOS. diff --git a/crates/attested-tls-proxy/Cargo.toml b/crates/attested-tls-proxy/Cargo.toml new file mode 100644 index 0000000..2b0e4f0 --- /dev/null +++ b/crates/attested-tls-proxy/Cargo.toml @@ -0,0 +1,77 @@ +[package] +name = "attested-tls-proxy" +version = "1.1.1" +edition = "2024" +license = "MIT" +readme = "../../README.md" +description = "An HTTP attested TLS proxy server and client for secure communication with CVM services" +repository = "https://github.com/flashbots/attested-tls-proxy" +keywords = ["attested-TLS", "CVM", "TDX"] + +[dependencies] +attested-tls = { path = "../attested-tls", default-features = false } +tokio = { workspace = true, features = ["full"] } +tokio-rustls = { workspace = true, features = ["aws_lc_rs"] } +x509-parser = { workspace = true, features = ["verify"] } +thiserror.workspace = true +clap = { version = "4.5.51", features = ["derive", "env"] } +rustls-pemfile = "2.2.0" +anyhow = "1.0.100" +pem-rfc7468 = { version = "0.7.0", features = ["std"] } +hyper = { workspace = true, features = ["server", "http2"] } +h2 = "0.4.12" +hyper-util = { workspace = true, features = ["tokio"] } +http-body-util.workspace = true +bytes.workspace = true +http.workspace = true +serde_json.workspace = true +serde = "1.0.228" +reqwest = { version = "0.13.4", default-features = false, features = [ + "rustls-no-provider", +] } +webpki-roots.workspace = true +tracing.workspace = true +tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } +axum = "0.8.8" +tower-http = { version = "0.6.7", features = ["fs"] } +rsa = { version = "0.9", default-features = false } +p256 = { version = "0.13.2", features = ["pkcs8"] } +pkcs1 = "0.7.5" +pkcs8 = "0.10.2" +rcgen.workspace = true +pin-project-lite = "0.2.16" +pccs = { git = "https://github.com/flashbots/attested-tls", branch = "main" } + +[dev-dependencies] +tempfile.workspace = true +tdx-quote = { version = "0.0.5", features = ["mock"] } +attested-tls = { path = "../attested-tls", features = ["test-helpers", "mock"] } +jsonrpsee = { version = "0.26.0", features = ["server"] } + +[features] +default = [] + +# Adds support for Microsoft Azure attestation generation and verification +azure = ["attested-tls/azure"] + +[package.metadata.deb] +maintainer = "Flashbots Team " +depends = "$auto" +section = "network" +priority = "optional" +maintainer-scripts = "../../pkg/debian" +assets = [ + [ + # cargo-deb resolves this prefix to the workspace target and selected profile. + "target/release/attested-tls-proxy", + "usr/bin/", + "755", + ], + [ + "../../LICENSE", + "usr/share/doc/attested-tls-proxy/", + "644", + ], +] +systemd-units = { enable = false, start = false, unit-name = "attested-tls-proxy" } + diff --git a/build.rs b/crates/attested-tls-proxy/build.rs similarity index 93% rename from build.rs rename to crates/attested-tls-proxy/build.rs index beba533..564a48f 100644 --- a/build.rs +++ b/crates/attested-tls-proxy/build.rs @@ -37,10 +37,8 @@ fn emit_git_rerun_hints() { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_owned())); - for git_dir in [ - manifest_dir.join(".git"), - manifest_dir.join("..").join(".git"), - ] { + for ancestor in manifest_dir.ancestors() { + let git_dir = ancestor.join(".git"); if git_dir.exists() { println!("cargo:rerun-if-changed={}", git_dir.join("HEAD").display()); println!( diff --git a/src/attested_get.rs b/crates/attested-tls-proxy/src/http/attested_get.rs similarity index 98% rename from src/attested_get.rs rename to crates/attested-tls-proxy/src/http/attested_get.rs index df6ef1e..f543c30 100644 --- a/src/attested_get.rs +++ b/crates/attested-tls-proxy/src/http/attested_get.rs @@ -1,5 +1,5 @@ //! A one-shot attested TLS proxy client which sends a single GET request and returns the response -use crate::{AttestationGenerator, AttestationVerifier, ProxyClient, ProxyError}; +use crate::http::{AttestationGenerator, AttestationVerifier, ProxyClient, ProxyError}; use tokio_rustls::rustls::pki_types::CertificateDer; /// Split an `attested-get` target into a proxy target and an optional request path. @@ -90,7 +90,7 @@ async fn attested_get_with_client( #[cfg(test)] mod tests { use super::*; - use crate::{ + use crate::http::{ ProxyServer, attestation::AttestationType, file_server::static_file_server, diff --git a/src/client_request/http2.rs b/crates/attested-tls-proxy/src/http/client_request/http2.rs similarity index 88% rename from src/client_request/http2.rs rename to crates/attested-tls-proxy/src/http/client_request/http2.rs index c4a4388..92db87a 100644 --- a/src/client_request/http2.rs +++ b/crates/attested-tls-proxy/src/http/client_request/http2.rs @@ -18,7 +18,7 @@ use hyper::body::{Body, Frame, SizeHint}; use tokio::io::{AsyncRead, AsyncWrite}; use super::{BoxError, ProxyResponse, RequestBody}; -use crate::ProxyError; +use crate::http::ProxyError; pub(crate) type Connection = Pin> + Send>>; @@ -65,9 +65,9 @@ where async fn keep_alive(mut ping: h2::PingPong) -> Result<(), ProxyError> { loop { - tokio::time::sleep(Duration::from_secs(crate::KEEP_ALIVE_INTERVAL)).await; + tokio::time::sleep(Duration::from_secs(crate::http::KEEP_ALIVE_INTERVAL)).await; tokio::time::timeout( - Duration::from_secs(crate::KEEP_ALIVE_TIMEOUT), + Duration::from_secs(crate::http::KEEP_ALIVE_TIMEOUT), ping.ping(h2::Ping::opaque()), ) .await @@ -93,7 +93,7 @@ impl Sender { pub(crate) async fn send_request( &mut self, - request: http::Request, + request: ::http::Request, ) -> Result { let (mut parts, body) = request.into_parts(); strip_connection_headers(&mut parts.headers); @@ -101,18 +101,18 @@ impl Sender { && (length != 0 || matches!( parts.method, - http::Method::POST | http::Method::PUT | http::Method::PATCH + ::http::Method::POST | ::http::Method::PUT | ::http::Method::PATCH )) { parts .headers - .entry(http::header::CONTENT_LENGTH) + .entry(::http::header::CONTENT_LENGTH) .or_insert(length.into()); } let end = body.is_end_stream(); let (response, stream) = self .inner - .send_request(http::Request::from_parts(parts, ()), end)?; + .send_request(::http::Request::from_parts(parts, ()), end)?; if !end { body.send_http2(stream); } @@ -127,15 +127,15 @@ impl Sender { } } -fn strip_connection_headers(headers: &mut http::HeaderMap) { - let connection_headers: Vec = headers - .get_all(http::header::CONNECTION) +fn strip_connection_headers(headers: &mut ::http::HeaderMap) { + let connection_headers: Vec<::http::header::HeaderName> = headers + .get_all(::http::header::CONNECTION) .iter() .filter_map(|value| value.to_str().ok()) .flat_map(|value| value.split(',')) .filter_map(|name| name.trim().parse().ok()) .collect(); - headers.remove(http::header::CONNECTION); + headers.remove(::http::header::CONNECTION); for name in connection_headers { headers.remove(name); } @@ -148,10 +148,10 @@ fn strip_connection_headers(headers: &mut http::HeaderMap) { headers.remove(name); } if headers - .get(http::header::TE) + .get(::http::header::TE) .is_some_and(|value| value != "trailers") { - headers.remove(http::header::TE); + headers.remove(::http::header::TE); } } @@ -201,7 +201,7 @@ mod tests { /// Removes headers nominated by every Connection field, including comma-separated names. #[test] fn strips_all_connection_header_values() { - let mut headers = http::HeaderMap::new(); + let mut headers = ::http::HeaderMap::new(); headers.append("connection", "x-first, connection".parse().unwrap()); headers.append("connection", " X-Second, x-third ".parse().unwrap()); for name in ["x-first", "x-second", "x-third", "x-end-to-end"] { diff --git a/src/client_request/mod.rs b/crates/attested-tls-proxy/src/http/client_request/mod.rs similarity index 97% rename from src/client_request/mod.rs rename to crates/attested-tls-proxy/src/http/client_request/mod.rs index 107a8b4..80e7040 100644 --- a/src/client_request/mod.rs +++ b/crates/attested-tls-proxy/src/http/client_request/mod.rs @@ -23,7 +23,7 @@ use tokio::{ time::Instant, }; -use crate::{ +use crate::http::{ ATTESTATION_TYPE_HEADER, MEASUREMENT_HEADER, attestation::{AttestationType, measurements::MultiMeasurements}, full, @@ -60,7 +60,7 @@ pub(crate) type ProxyResponse = Response>; pub(crate) struct PendingRequest { - pub request: http::Request, + pub request: ::http::Request, pub response_tx: oneshot::Sender, pub deadline: Instant, pub permit: OwnedSemaphorePermit, @@ -73,7 +73,7 @@ pub(crate) fn gateway_timeout() -> ProxyResponse { .map_err(Into::into) .boxed(), ); - *response.status_mut() = http::StatusCode::GATEWAY_TIMEOUT; + *response.status_mut() = ::http::StatusCode::GATEWAY_TIMEOUT; response } @@ -141,7 +141,7 @@ pub(crate) async fn forward( let permit = Arc::new(permit); let (parts, body) = request.into_parts(); let (body, upload_guard, mut upload_finished) = RequestBody::new(body, permit.clone()); - let request = http::Request::from_parts(parts, body); + let request = ::http::Request::from_parts(parts, body); let response = tokio::select! { biased; @@ -165,7 +165,7 @@ pub(crate) async fn forward( .map_err(Into::into) .boxed(), ); - *response.status_mut() = http::StatusCode::BAD_GATEWAY; + *response.status_mut() = ::http::StatusCode::BAD_GATEWAY; let _ = response_tx.send(response); } // HTTP/2 stream failures/cancellations must not interrupt other streams. diff --git a/src/client_request/response_idle.rs b/crates/attested-tls-proxy/src/http/client_request/response_idle.rs similarity index 100% rename from src/client_request/response_idle.rs rename to crates/attested-tls-proxy/src/http/client_request/response_idle.rs diff --git a/src/client_request/tests.rs b/crates/attested-tls-proxy/src/http/client_request/tests.rs similarity index 95% rename from src/client_request/tests.rs rename to crates/attested-tls-proxy/src/http/client_request/tests.rs index 868a503..57df680 100644 --- a/src/client_request/tests.rs +++ b/crates/attested-tls-proxy/src/http/client_request/tests.rs @@ -19,7 +19,7 @@ use tokio::{ }; use super::ProxyClientOptions; -use crate::{ +use crate::http::{ AttestationGenerator, AttestationVerifier, ProxyClient, ProxyServer, http_version::{ALPN_H2, ALPN_HTTP11}, test_helpers::{generate_certificate_chain, generate_tls_config}, @@ -117,16 +117,16 @@ fn http_client() -> reqwest::Client { // Real Hyper senders over an in-memory connection make worker failures and // connection closure deterministic, without racing TCP shutdown against dispatch. -async fn sender_for_test(http2: bool) -> (crate::http_version::HttpSender, JoinSet<()>) { +async fn sender_for_test(http2: bool) -> (crate::http::http_version::HttpSender, JoinSet<()>) { use hyper_util::rt::TokioIo; let (client, server) = tokio::io::duplex(4096); let mut tasks = JoinSet::new(); let service = hyper::service::service_fn(|_| async { - Ok::<_, std::convert::Infallible>(hyper::Response::new(crate::full("ok"))) + Ok::<_, std::convert::Infallible>(hyper::Response::new(crate::http::full("ok"))) }); let sender = if http2 { tasks.spawn(async move { - let _ = hyper::server::conn::http2::Builder::new(crate::TokioExecutor) + let _ = hyper::server::conn::http2::Builder::new(crate::http::TokioExecutor) .serve_connection(TokioIo::new(server), service) .await; }); @@ -221,7 +221,7 @@ async fn http2_stalled_request_does_not_block_fast_request() { assert_eq!(fast.text().await.unwrap(), "fast"); assert_eq!( slow.await.unwrap().unwrap().status(), - http::StatusCode::GATEWAY_TIMEOUT + ::http::StatusCode::GATEWAY_TIMEOUT ); assert_eq!(fixture.connections.load(Ordering::SeqCst), 1); } @@ -249,7 +249,7 @@ async fn http1_timeout_reconnects_without_replaying_post() { .send() .await .unwrap(); - assert_eq!(slow.status(), http::StatusCode::GATEWAY_TIMEOUT); + assert_eq!(slow.status(), ::http::StatusCode::GATEWAY_TIMEOUT); let fast = client .get(format!("{}/fast", fixture.url)) .send() @@ -298,13 +298,13 @@ async fn streaming_bodies_hold_capacity_and_expired_requests_are_not_forwarded() .send() .await .unwrap(); - assert_eq!(stream.status(), http::StatusCode::OK); + assert_eq!(stream.status(), ::http::StatusCode::OK); let blocked = client .get(format!("{}/fast", fixture.url)) .send() .await .unwrap(); - assert_eq!(blocked.status(), http::StatusCode::GATEWAY_TIMEOUT); + assert_eq!(blocked.status(), ::http::StatusCode::GATEWAY_TIMEOUT); assert_eq!(calls.load(Ordering::SeqCst), 0); drop(body_tx); assert!(stream.bytes().await.unwrap().is_empty()); @@ -438,7 +438,7 @@ async fn dropping_streaming_response_releases_slot() { .send() .await .unwrap(); - assert_eq!(response.status(), http::StatusCode::OK); + assert_eq!(response.status(), ::http::StatusCode::OK); drop(response); let fast = timeout( Duration::from_secs(1), @@ -460,7 +460,7 @@ async fn dropping_streaming_response_releases_slot() { async fn http1_clean_close_preserves_response() { let app = Router::new().route( "/", - get(|| async { ([(http::header::CONNECTION, "close")], "ok") }), + get(|| async { ([(::http::header::CONNECTION, "close")], "ok") }), ); let fixture = proxy(app, ALPN_HTTP11, 2, Duration::from_secs(3)).await; let client = http_client(); @@ -472,7 +472,7 @@ async fn http1_clean_close_preserves_response() { .unwrap(); let status = response.status(); let body = response.text().await.unwrap(); - assert_eq!(status, http::StatusCode::OK, "request {i}: {body}"); + assert_eq!(status, ::http::StatusCode::OK, "request {i}: {body}"); assert_eq!(body, "ok"); } } @@ -491,7 +491,7 @@ async fn early_response_keeps_upload_bounded() { let _ = axum::body::to_bytes(request.into_body(), 1024).await; counter.fetch_sub(1, Ordering::SeqCst); }); - http::StatusCode::OK + ::http::StatusCode::OK } }), ); @@ -544,7 +544,7 @@ async fn early_response_allows_upload_to_finish_before_releasing_slot() { .unwrap(); uploaded_tx.lock().await.take().unwrap().send(body).unwrap(); }); - http::StatusCode::OK + ::http::StatusCode::OK } }), ) @@ -675,7 +675,7 @@ async fn silent_response_body_times_out_and_releases_capacity() { .send() .await .unwrap(); - assert_eq!(response.status(), http::StatusCode::OK); + assert_eq!(response.status(), ::http::StatusCode::OK); assert!( timeout(Duration::from_secs(2), response.bytes()) .await @@ -750,7 +750,7 @@ async fn flow_control_blocked_upload_deadline_releases_capacity() { tokio::time::sleep(Duration::from_secs(3)).await; drop(request); }); - http::StatusCode::OK + ::http::StatusCode::OK }), ) .route("/fast", get(|| async { "fast" })); @@ -778,7 +778,7 @@ async fn flow_control_blocked_upload_deadline_releases_capacity() { .send() .await .unwrap(); - assert_eq!(response.status(), http::StatusCode::OK); + assert_eq!(response.status(), ::http::StatusCode::OK); assert_eq!(response.text().await.unwrap(), "fast"); assert_eq!(fixture.connections.load(Ordering::SeqCst), 1); } @@ -788,7 +788,7 @@ async fn flow_control_blocked_upload_deadline_releases_capacity() { async fn incoming_request( raw: Vec, tasks: &mut JoinSet<()>, -) -> http::Request { +) -> ::http::Request { let (mut source, server) = tokio::io::duplex(4096); let (tx, rx) = tokio::sync::oneshot::channel(); let tx = Arc::new(std::sync::Mutex::new(Some(tx))); @@ -797,7 +797,7 @@ async fn incoming_request( tx.lock().unwrap().take().unwrap().send(request).unwrap(); std::future::pending::< Result< - http::Response>, + ::http::Response>, std::convert::Infallible, >, >() @@ -838,7 +838,7 @@ async fn blocked_uploads_reset_streams_and_preserve_other_http2_responses() { } else { Some( respond - .send_response(http::Response::new(()), !other) + .send_response(::http::Response::new(()), !other) .unwrap(), ) }; @@ -868,7 +868,7 @@ async fn blocked_uploads_reset_streams_and_preserve_other_http2_responses() { tasks.spawn(async move { let _ = connection.await; }); - let mut sender = Some(crate::http_version::HttpSender::Http2(sender)); + let mut sender = Some(crate::http::http_version::HttpSender::Http2(sender)); let slots = Arc::new(tokio::sync::Semaphore::new(2)); let (tx, rx) = tokio::sync::oneshot::channel(); let other_request = incoming_request( @@ -885,7 +885,7 @@ async fn blocked_uploads_reset_streams_and_preserve_other_http2_responses() { permit: slots.clone().acquire_owned().await.unwrap(), }, None, - crate::attestation::AttestationType::None, + crate::http::attestation::AttestationType::None, )); let response = timeout(Duration::from_secs(1), rx).await.unwrap().unwrap(); // Cover both completed and partial source bodies, with and without early @@ -911,14 +911,14 @@ async fn blocked_uploads_reset_streams_and_preserve_other_http2_responses() { permit: slots.clone().acquire_owned().await.unwrap(), }, None, - crate::attestation::AttestationType::None, + crate::http::attestation::AttestationType::None, )); assert_eq!( rx.await.unwrap().status(), if path == "/late" { - http::StatusCode::GATEWAY_TIMEOUT + ::http::StatusCode::GATEWAY_TIMEOUT } else { - http::StatusCode::OK + ::http::StatusCode::OK }, ); let result = timeout(Duration::from_secs(1), worker) @@ -960,7 +960,7 @@ async fn http2_upload_and_response_preserve_trailers() { assert!(!request.headers().contains_key("transfer-encoding")); assert_eq!(request.headers()["te"], "trailers"); let mut response = respond - .send_response(http::Response::new(()), false) + .send_response(::http::Response::new(()), false) .unwrap(); let mut body = request.into_body(); let mut received = Vec::new(); @@ -990,7 +990,7 @@ async fn http2_upload_and_response_preserve_trailers() { let slots = Arc::new(tokio::sync::Semaphore::new(1)); let (tx, rx) = tokio::sync::oneshot::channel(); let worker = tokio::spawn(super::forward( - crate::http_version::HttpSender::Http2(sender), + crate::http::http_version::HttpSender::Http2(sender), super::PendingRequest { request, response_tx: tx, @@ -998,7 +998,7 @@ async fn http2_upload_and_response_preserve_trailers() { permit: slots.clone().acquire_owned().await.unwrap(), }, None, - crate::attestation::AttestationType::None, + crate::http::attestation::AttestationType::None, )); let response = timeout(Duration::from_secs(2), rx).await.unwrap().unwrap(); let body = timeout(Duration::from_secs(2), response.into_body().collect()) @@ -1023,7 +1023,7 @@ async fn finite_idle_response(body: bytes::Bytes) -> (TcpStream, JoinSet<()>) { tasks.spawn(async move { let service = hyper::service::service_fn(move |_| { let response = activity.track(hyper::Response::new(http_body_util::BodyExt::boxed( - http_body_util::BodyExt::map_err(crate::full(body.clone()), Into::into), + http_body_util::BodyExt::map_err(crate::http::full(body.clone()), Into::into), ))); async { Ok::<_, std::convert::Infallible>(response) } }); @@ -1070,7 +1070,7 @@ async fn flushed_responses_leave_source_keep_alive() { }); for _ in 0..2 { let response = sender - .send_request(http::Request::new(crate::full(""))) + .send_request(::http::Request::new(crate::http::full(""))) .await .unwrap(); assert_eq!( @@ -1102,7 +1102,7 @@ async fn check_http2_drain(end: DrainEnd) { let (mut server_config, mut client_config) = generate_tls_config(certs.clone(), key); server_config.alpn_protocols = vec![ALPN_H2.to_vec()]; client_config.alpn_protocols = vec![ALPN_H2.to_vec()]; - let server = crate::AttestedTlsServer::new_with_tls_config( + let server = crate::http::AttestedTlsServer::new_with_tls_config( certs, server_config, AttestationGenerator::with_no_attestation(), @@ -1123,7 +1123,7 @@ async fn check_http2_drain(end: DrainEnd) { let (request, mut respond) = connection.accept().await.unwrap().unwrap(); assert_eq!(request.uri().path(), "/old"); let response = respond - .send_response(http::Response::new(()), matches!(end, DrainEnd::Empty)) + .send_response(::http::Response::new(()), matches!(end, DrainEnd::Empty)) .unwrap(); drop(respond); drop(request); @@ -1162,7 +1162,7 @@ async fn check_http2_drain(end: DrainEnd) { let (request, mut respond) = replacement.accept().await.unwrap().unwrap(); assert_eq!(request.uri().path(), "/fresh"); respond - .send_response(http::Response::new(()), true) + .send_response(::http::Response::new(()), true) .unwrap(); drop(respond); drop(request); @@ -1207,7 +1207,7 @@ async fn check_http2_drain(end: DrainEnd) { .send() .await .unwrap(); - assert_eq!(old.status(), http::StatusCode::OK); + assert_eq!(old.status(), ::http::StatusCode::OK); shutdown_tx.send(()).unwrap(); timeout(Duration::from_secs(2), retired_rx) .await @@ -1219,15 +1219,15 @@ async fn check_http2_drain(end: DrainEnd) { .send() .await .unwrap(); - if probe.status() == http::StatusCode::BAD_GATEWAY { + if probe.status() == ::http::StatusCode::BAD_GATEWAY { let fresh = http_client() .get(format!("{url}/fresh")) .send() .await .unwrap(); - assert_eq!(fresh.status(), http::StatusCode::OK); + assert_eq!(fresh.status(), ::http::StatusCode::OK); } else { - assert_eq!(probe.status(), http::StatusCode::OK); + assert_eq!(probe.status(), ::http::StatusCode::OK); } finish_tx.send(()).unwrap(); let result = old.text().await; diff --git a/src/client_request/upload.rs b/crates/attested-tls-proxy/src/http/client_request/upload.rs similarity index 100% rename from src/client_request/upload.rs rename to crates/attested-tls-proxy/src/http/client_request/upload.rs diff --git a/src/file_server.rs b/crates/attested-tls-proxy/src/http/file_server.rs similarity index 95% rename from src/file_server.rs rename to crates/attested-tls-proxy/src/http/file_server.rs index bce4804..fcbff2a 100644 --- a/src/file_server.rs +++ b/crates/attested-tls-proxy/src/http/file_server.rs @@ -1,5 +1,7 @@ //! Static HTTP file server provided by an attested TLS proxy server -use crate::{AttestationGenerator, AttestationVerifier, ProxyError, ProxyServer, TlsCertAndKey}; +use crate::http::{ + AttestationGenerator, AttestationVerifier, ProxyError, ProxyServer, TlsCertAndKey, +}; use std::{net::SocketAddr, path::PathBuf}; use tokio::net::ToSocketAddrs; use tower_http::services::ServeDir; @@ -52,10 +54,10 @@ pub(crate) async fn static_file_server(path: PathBuf) -> Result, + request: ::http::Request, ) -> Result { match self { Self::Http1(sender) => sender diff --git a/src/lib.rs b/crates/attested-tls-proxy/src/http/mod.rs similarity index 99% rename from src/lib.rs rename to crates/attested-tls-proxy/src/http/mod.rs index 9e2db67..14a57da 100644 --- a/src/lib.rs +++ b/crates/attested-tls-proxy/src/http/mod.rs @@ -1,9 +1,8 @@ -//! An attested TLS protocol and HTTPS proxy +//! HTTP forwarding over attested TLS. pub mod attested_get; pub mod file_server; pub mod health_check; -pub mod normalize_pem; -pub mod self_signed; +use crate::self_signed; pub use attested_tls; pub use attested_tls::attestation; @@ -15,10 +14,10 @@ pub use client_request::ProxyClientOptions; use client_request::{PendingRequest, forward, gateway_timeout, take_sender, worker_finished}; #[cfg(test)] -mod test_helpers; +pub(crate) mod test_helpers; +use ::http::{HeaderMap, HeaderName, HeaderValue}; use bytes::Bytes; -use http::{HeaderMap, HeaderName, HeaderValue}; use http_body_util::{BodyExt, combinators::BoxBody}; use hyper::{Response, service::service_fn}; use hyper_util::rt::TokioIo; @@ -33,7 +32,7 @@ use tokio_rustls::rustls::{ }; use tracing::{debug, error, warn}; -use crate::http_version::{ALPN_H2, ALPN_HTTP11, HttpConnection, HttpSender, HttpVersion}; +use crate::http::http_version::{ALPN_H2, ALPN_HTTP11, HttpConnection, HttpSender, HttpVersion}; use attested_tls::{ AttestedTlsClient, AttestedTlsError, AttestedTlsServer, TlsCertAndKey, attestation::{ @@ -231,7 +230,7 @@ impl ProxyServer { let headers = req.headers_mut(); // Add or update the HOST header - let old_value = update_header(headers, &http::header::HOST, &target); + let old_value = update_header(headers, &::http::header::HOST, &target); debug!("Updating Host header - old value: {old_value:?} new value: {target}",); // Add the x-real-ip header @@ -751,7 +750,7 @@ fn update_header( header_value: &str, ) -> Option where - K: http::header::IntoHeaderName + std::fmt::Display, + K: ::http::header::IntoHeaderName + std::fmt::Display, { if let Ok(value) = HeaderValue::from_str(header_value) { headers.insert(header_name, value) @@ -827,7 +826,7 @@ where #[cfg(test)] mod tests { - use crate::{ + use crate::http::{ attestation::{PccsMode, measurements::MeasurementPolicy}, attested_tls::get_tls_cert_with_config, }; @@ -1013,7 +1012,7 @@ mod tests { .send() .await .unwrap(); - assert_eq!(response.status(), http::StatusCode::OK); + assert_eq!(response.status(), ::http::StatusCode::OK); let response_measurements: Vec<_> = response .headers() .get_all(MEASUREMENT_HEADER) diff --git a/src/test_helpers.rs b/crates/attested-tls-proxy/src/http/test_helpers.rs similarity index 97% rename from src/test_helpers.rs rename to crates/attested-tls-proxy/src/http/test_helpers.rs index cb6e79c..eeacc5e 100644 --- a/src/test_helpers.rs +++ b/crates/attested-tls-proxy/src/http/test_helpers.rs @@ -14,7 +14,7 @@ use tracing_subscriber::{EnvFilter, fmt}; static INIT: Once = Once::new(); -use crate::MEASUREMENT_HEADER; +use crate::http::MEASUREMENT_HEADER; pub use attested_tls::attestation::measurements::mock_dcap_measurements; @@ -131,7 +131,7 @@ pub async fn example_http_service() -> SocketAddr { addr } -async fn get_handler(headers: http::HeaderMap) -> impl IntoResponse { +async fn get_handler(headers: ::http::HeaderMap) -> impl IntoResponse { headers .get(MEASUREMENT_HEADER) .and_then(|v| v.to_str().ok()) diff --git a/crates/attested-tls-proxy/src/lib.rs b/crates/attested-tls-proxy/src/lib.rs new file mode 100644 index 0000000..fa48510 --- /dev/null +++ b/crates/attested-tls-proxy/src/lib.rs @@ -0,0 +1,7 @@ +//! An attested TLS protocol and HTTPS proxy. +pub mod http; +pub mod normalize_pem; +pub mod self_signed; + +// Preserve the original HTTP proxy API at the crate root. +pub use http::*; diff --git a/src/main.rs b/crates/attested-tls-proxy/src/main.rs similarity index 100% rename from src/main.rs rename to crates/attested-tls-proxy/src/main.rs diff --git a/src/normalize_pem.rs b/crates/attested-tls-proxy/src/normalize_pem.rs similarity index 100% rename from src/normalize_pem.rs rename to crates/attested-tls-proxy/src/normalize_pem.rs diff --git a/src/self_signed.rs b/crates/attested-tls-proxy/src/self_signed.rs similarity index 99% rename from src/self_signed.rs rename to crates/attested-tls-proxy/src/self_signed.rs index 309803f..6d6322a 100644 --- a/src/self_signed.rs +++ b/crates/attested-tls-proxy/src/self_signed.rs @@ -203,7 +203,7 @@ mod tests { AttestationGenerator, attestation::{AttestationType, AttestationVerifier}, attested_tls::{AttestedTlsClient, AttestedTlsServer}, - test_helpers::{generate_certificate_chain, generate_tls_config}, + http::test_helpers::{generate_certificate_chain, generate_tls_config}, }; use tokio::net::TcpListener; use tokio_rustls::rustls::pki_types::ServerName; diff --git a/tests/attested_get_redirect.rs b/crates/attested-tls-proxy/tests/attested_get_redirect.rs similarity index 100% rename from tests/attested_get_redirect.rs rename to crates/attested-tls-proxy/tests/attested_get_redirect.rs diff --git a/attested-tls/Cargo.toml b/crates/attested-tls/Cargo.toml similarity index 72% rename from attested-tls/Cargo.toml rename to crates/attested-tls/Cargo.toml index ef4930d..9b4f237 100644 --- a/attested-tls/Cargo.toml +++ b/crates/attested-tls/Cargo.toml @@ -8,15 +8,15 @@ repository = "https://github.com/flashbots/attested-tls-proxy" keywords = ["attested-TLS", "CVM", "TDX"] [dependencies] -tokio = { version = "1.48.0", features = ["full"] } -tokio-rustls = { version = "0.26.4", default-features = false } +tokio = { workspace = true, features = ["full"] } +tokio-rustls.workspace = true sha2 = "0.10.9" -x509-parser = "0.18.0" -thiserror = "2.0.17" -webpki-roots = "1.0.4" -http = "1.3.1" -serde_json = "1.0.145" -tracing = "0.1.41" +x509-parser.workspace = true +thiserror.workspace = true +webpki-roots.workspace = true +http.workspace = true +serde_json.workspace = true +tracing.workspace = true parity-scale-codec = "3.7.5" attestation = { git = "https://github.com/flashbots/attested-tls", branch = "main" } @@ -29,17 +29,17 @@ alloy-rpc-client = { version = "1.1.3", optional = true } tower-service = { version = "0.3.3", optional = true } alloy-transport-http = { version = "1.4.3", features = ["hyper"], optional = true } url = { version = "2.5.7", optional = true } -hyper = { version = "1.7.0", features = ["client", "http2"], optional = true } -hyper-util = { version = "0.1.17", features = ["tokio"], optional = true } -bytes = { version = "1.11.1", optional = true } -http-body-util = { version = "0.1.3", optional = true } +hyper = { workspace = true, features = ["client", "http2"], optional = true } +hyper-util = { workspace = true, features = ["tokio"], optional = true } +bytes = { workspace = true, optional = true } +http-body-util = { workspace = true, optional = true } # Used by test helpers -rcgen = { version = "0.14.5", optional = true } +rcgen = { workspace = true, optional = true } [dev-dependencies] -rcgen = "0.14.5" -tempfile = "3.23.0" +rcgen.workspace = true +tempfile.workspace = true attestation = { git = "https://github.com/flashbots/attested-tls", branch = "main", features = ["mock"] } [features] diff --git a/attested-tls/README.md b/crates/attested-tls/README.md similarity index 96% rename from attested-tls/README.md rename to crates/attested-tls/README.md index d699e5b..6abd7a7 100644 --- a/attested-tls/README.md +++ b/crates/attested-tls/README.md @@ -24,7 +24,7 @@ Immediately after the TLS handshake, an attestation exchange is made. The server Attestation exchange messages are formatted as follows: - A 4 byte length prefix - a big endian encoded unsigned 32 bit integer -- A SCALE (Simple Concatenated Aggregate Little-Endian) encoded [struct](./src/attestation/mod.rs) with the following fields: +- A SCALE (Simple Concatenated Aggregate Little-Endian) encoded [struct in the attestation crate](https://github.com/flashbots/attested-tls/blob/main/crates/attestation/src/lib.rs) with the following fields: - `attestation_type` - a string with one of the attestation types (described above) including `none`. - `attestation` - the actual attestation data. In the case of DCAP this is a binary quote report. In the case of `none` this is an empty byte array. diff --git a/attested-tls/src/attested_rpc.rs b/crates/attested-tls/src/attested_rpc.rs similarity index 100% rename from attested-tls/src/attested_rpc.rs rename to crates/attested-tls/src/attested_rpc.rs diff --git a/attested-tls/src/lib.rs b/crates/attested-tls/src/lib.rs similarity index 100% rename from attested-tls/src/lib.rs rename to crates/attested-tls/src/lib.rs diff --git a/attested-tls/src/test_helpers.rs b/crates/attested-tls/src/test_helpers.rs similarity index 100% rename from attested-tls/src/test_helpers.rs rename to crates/attested-tls/src/test_helpers.rs diff --git a/attested-tls/src/websockets.rs b/crates/attested-tls/src/websockets.rs similarity index 100% rename from attested-tls/src/websockets.rs rename to crates/attested-tls/src/websockets.rs