From e4fb288670297ed87c24c9d9731ddfd770dc3f1a Mon Sep 17 00:00:00 2001 From: Steph Sinyakov Date: Wed, 10 Jun 2026 10:06:50 +0200 Subject: [PATCH] chore: move attestation-provider-server crate from proxy repo --- Cargo.lock | 113 ++++++++++++++++++ Cargo.toml | 1 + crates/attestation-provider-server/Cargo.toml | 25 ++++ crates/attestation-provider-server/build.rs | 50 ++++++++ crates/attestation-provider-server/src/lib.rs | 108 +++++++++++++++++ .../attestation-provider-server/src/main.rs | 111 +++++++++++++++++ readme.md | 2 + 7 files changed, 410 insertions(+) create mode 100644 crates/attestation-provider-server/Cargo.toml create mode 100644 crates/attestation-provider-server/build.rs create mode 100644 crates/attestation-provider-server/src/lib.rs create mode 100644 crates/attestation-provider-server/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index f9a940e..2ef924e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -170,12 +170,56 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -325,6 +369,23 @@ dependencies = [ "x509-parser 0.18.1", ] +[[package]] +name = "attestation-provider-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "attestation", + "axum", + "clap", + "hex", + "parity-scale-codec", + "reqwest", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "attested-tls" version = "0.0.1" @@ -823,6 +884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] @@ -831,8 +893,22 @@ version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ + "anstream", "anstyle", "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -856,6 +932,12 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12170080f3533d6f09a19f81596f836854d0fa4867dc32c8172b8474b4e9de61" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "console" version = "0.16.3" @@ -2254,6 +2336,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -2685,6 +2773,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -4300,6 +4394,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.22" @@ -4310,12 +4414,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -4447,6 +4554,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.22.0" diff --git a/Cargo.toml b/Cargo.toml index 4f699fb..b90c0b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/attestation", "crates/pccs", "crates/mock-tdx", + "crates/attestation-provider-server", ] [workspace.lints.rust] diff --git a/crates/attestation-provider-server/Cargo.toml b/crates/attestation-provider-server/Cargo.toml new file mode 100644 index 0000000..8de3ed3 --- /dev/null +++ b/crates/attestation-provider-server/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "attestation-provider-server" +description = "An HTTP server which provides attestations" +version = "0.1.0" +edition = "2024" +license = "MIT" +publish = false +repository = "https://github.com/flashbots/attested-tls" + +[lints] +workspace = true + +[dependencies] +attestation = { workspace = true } +tokio = { workspace = true } + +axum = "0.8.6" +clap = { version = "4.5.51", features = ["derive", "env"] } +anyhow = "1.0.100" +hex = "0.4.3" +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] } +parity-scale-codec = "3.7.5" +reqwest = { version = "0.12.23", default-features = false } +thiserror = "2.0.17" \ No newline at end of file diff --git a/crates/attestation-provider-server/build.rs b/crates/attestation-provider-server/build.rs new file mode 100644 index 0000000..a233f4a --- /dev/null +++ b/crates/attestation-provider-server/build.rs @@ -0,0 +1,50 @@ +use std::{env, path::PathBuf, process::Command}; + +/// Run a git command and return trimmed stdout +fn git_output(args: &[&str]) -> Option { + let output = Command::new("git").args(args).output().ok()?; + if !output.status.success() { + return None; + } + + let value = String::from_utf8(output.stdout).ok()?; + let value = value.trim(); + if value.is_empty() { None } else { Some(value.to_owned()) } +} + +/// Resolve version as tag then branch-sha then sha then unknown +fn compute_git_rev() -> String { + if let Some(tag) = git_output(&["describe", "--tags", "--exact-match"]) { + return tag; + } + + let Some(sha) = git_output(&["rev-parse", "--short=12", "HEAD"]) else { + return "unknown".to_owned(); + }; + + match git_output(&["rev-parse", "--abbrev-ref", "HEAD"]) { + Some(branch) if branch != "HEAD" => format!("{branch}@{sha}"), + _ => sha, + } +} + +/// Emit build rerun hints for git metadata changes +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")] { + if git_dir.exists() { + println!("cargo:rerun-if-changed={}", git_dir.join("HEAD").display()); + println!("cargo:rerun-if-changed={}", git_dir.join("packed-refs").display()); + break; + } + } + + println!("cargo:rerun-if-env-changed=GIT_DIR"); +} + +fn main() { + println!("cargo:rustc-env=GIT_REV={}", compute_git_rev()); + emit_git_rerun_hints(); +} diff --git a/crates/attestation-provider-server/src/lib.rs b/crates/attestation-provider-server/src/lib.rs new file mode 100644 index 0000000..1db9232 --- /dev/null +++ b/crates/attestation-provider-server/src/lib.rs @@ -0,0 +1,108 @@ +use std::net::SocketAddr; + +pub use attestation::AttestationGenerator; +use attestation::{AttestationError, AttestationExchangeMessage, AttestationVerifier}; +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, +}; +use parity_scale_codec::{Decode, Encode}; +use tokio::net::TcpListener; + +#[derive(Clone)] +struct SharedState { + attestation_generator: AttestationGenerator, +} + +/// An HTTP server which provides attestations +pub async fn attestation_provider_server( + listener: TcpListener, + attestation_generator: AttestationGenerator, +) -> anyhow::Result<()> { + let app = axum::Router::new() + .route("/attest/{input_data}", axum::routing::get(get_attest)) + .with_state(SharedState { attestation_generator }); + + axum::serve(listener, app).await?; + + Ok(()) +} + +/// Handler for the GET `/attest/{input_data}` route +/// Input data should be 64 bytes hex +async fn get_attest( + State(shared_state): State, + Path(input_data): Path, +) -> Result<(StatusCode, Vec), ServerError> { + let input_data: [u8; 64] = + hex::decode(input_data)?.try_into().map_err(|_| ServerError::InvalidLength)?; + + let attestation = shared_state.attestation_generator.generate_attestation(input_data)?.encode(); + + Ok((StatusCode::OK, attestation)) +} + +/// A client helper which makes a request to `/attest` +pub async fn attestation_provider_client( + server_addr: SocketAddr, + attestation_verifier: AttestationVerifier, +) -> anyhow::Result { + let input_data = [0; 64]; + let response = reqwest::get(format!("http://{server_addr}/attest/{}", hex::encode(input_data))) + .await? + .bytes() + .await?; + + let remote_attestation_message = AttestationExchangeMessage::decode(&mut &response[..])?; + let remote_attestation_type = remote_attestation_message.attestation_type; + + println!("Remote attestation type: {remote_attestation_type}"); + + attestation_verifier.verify_attestation(remote_attestation_message.clone(), input_data).await?; + + Ok(remote_attestation_message) +} + +#[derive(Debug, thiserror::Error)] +enum ServerError { + #[error(transparent)] + InvalidHex(#[from] hex::FromHexError), + #[error("Input data must be 64 bytes")] + InvalidLength, + #[error(transparent)] + AttestationFailed(#[from] AttestationError), +} + +impl IntoResponse for ServerError { + fn into_response(self) -> Response { + let (status, message) = match &self { + ServerError::InvalidHex(_) | ServerError::InvalidLength => { + (StatusCode::BAD_REQUEST, self.to_string()) + } + ServerError::AttestationFailed(_) => { + tracing::error!("{self:?}"); + (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string()) + } + }; + (status, message).into_response() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_attestation_provider_server() { + let attestation_generator = AttestationGenerator::with_no_attestation(); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let server_addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + attestation_provider_server(listener, attestation_generator).await.unwrap(); + }); + attestation_provider_client(server_addr, AttestationVerifier::expect_none()).await.unwrap(); + } +} diff --git a/crates/attestation-provider-server/src/main.rs b/crates/attestation-provider-server/src/main.rs new file mode 100644 index 0000000..e4d2b31 --- /dev/null +++ b/crates/attestation-provider-server/src/main.rs @@ -0,0 +1,111 @@ +use std::{net::SocketAddr, path::PathBuf}; + +use attestation::{ + AttestationGenerator, + AttestationType, + AttestationVerifier, + measurements::MeasurementPolicy, +}; +use attestation_provider_server::{attestation_provider_client, attestation_provider_server}; +use clap::{Parser, Subcommand}; +use tokio::net::TcpListener; +use tracing::level_filters::LevelFilter; + +const GIT_REV: &str = match option_env!("GIT_REV") { + Some(rev) => rev, + None => "unknown", +}; + +#[derive(Parser, Debug, Clone)] +#[command(version = GIT_REV, about, long_about = None)] +struct Cli { + #[clap(subcommand)] + command: CliCommand, + /// Log debug messages + #[arg(long, global = true)] + log_debug: bool, + /// Log in JSON format + #[arg(long, global = true)] + log_json: bool, + /// Log DCAP quotes to folder `quotes/` + #[arg(long, global = true)] + log_dcap_quote: bool, +} +#[derive(Subcommand, Debug, Clone)] +enum CliCommand { + Server { + /// Socket address to listen on + #[arg(short, long, default_value = "0.0.0.0:0", env = "LISTEN_ADDR")] + listen_addr: SocketAddr, + /// Type of attestation to present (will attempt to detect if not + /// given) + #[arg(long)] + server_attestation_type: Option, + }, + Client { + /// Socket address of a attestation provider server + server_addr: SocketAddr, + /// Optional path to file containing JSON measurements to be + /// enforced on the remote party + #[arg(long, global = true, env = "MEASUREMENTS_FILE")] + measurements_file: Option, + }, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + let level_filter = if cli.log_debug { LevelFilter::DEBUG } else { LevelFilter::WARN }; + + let env_filter = tracing_subscriber::EnvFilter::builder() + .with_default_directive(level_filter.into()) + .from_env_lossy(); + + let subscriber = tracing_subscriber::fmt::Subscriber::builder().with_env_filter(env_filter); + + if cli.log_json { + subscriber.json().init(); + } else { + subscriber.pretty().init(); + } + + if cli.log_dcap_quote { + tokio::fs::create_dir_all("quotes").await?; + } + + match cli.command { + CliCommand::Server { listen_addr, server_attestation_type } => { + let none_requested = server_attestation_type.as_deref() == Some("none"); + let attestation_generator = + AttestationGenerator::new_with_detection(server_attestation_type, None)?; + + if attestation_generator.attestation_type == AttestationType::None && !none_requested { + anyhow::bail!( + "Failed to detect attestation type. To run without attestation, pass --server-attestation-type none" + ); + } + + let listener = TcpListener::bind(listen_addr).await?; + + println!("Listening on {}", listener.local_addr()?); + attestation_provider_server(listener, attestation_generator).await?; + } + CliCommand::Client { server_addr, measurements_file } => { + let measurement_policy = match measurements_file { + Some(measurements_file) => MeasurementPolicy::from_file(measurements_file).await?, + None => MeasurementPolicy::accept_anything(), + }; + + let attestation_verifier = + AttestationVerifier::new(measurement_policy, None, cli.log_dcap_quote, false); + + let attestation_message = + attestation_provider_client(server_addr, attestation_verifier).await?; + + println!("{attestation_message:?}") + } + } + + Ok(()) +} diff --git a/readme.md b/readme.md index b752422..7a51df0 100644 --- a/readme.md +++ b/readme.md @@ -46,6 +46,8 @@ More details in the individual READMEs of the provided crates: - [`mock-tdx`](./crates/mock-tdx) - generates deterministic mock TDX DCAP quotes, collateral, and trust roots for tests and development on non-TDX hardware. +- [`attestation-provider-server`](./crates/attestation-provider-server) - + HTTP server and client for attestation generation and verification. The included `shell.nix` file can be used with `nix-shell`, `direnv`, or `nix develop` to add the dependencies needed by the optional `azure` feature of the