Skip to content

Commit d8c1305

Browse files
committed
Merge main
2 parents 70bbe55 + baf5188 commit d8c1305

File tree

10 files changed

+894
-304
lines changed

10 files changed

+894
-304
lines changed

Cargo.lock

Lines changed: 303 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[workspace]
2+
members = [".", "dummy-attestation-server"]
3+
14
[package]
25
name = "attested-tls-proxy"
36
version = "0.1.0"
@@ -30,6 +33,7 @@ serde = "1.0.228"
3033
tracing = "0.1.41"
3134
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
3235
parity-scale-codec = "3.7.5"
36+
reqwest = "0.12.23"
3337

3438
[dev-dependencies]
3539
rcgen = "0.14.5"

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ It has three subcommands:
1515

1616
### How it works
1717

18-
1918
This works as follows:
2019
1. The source HTTP client (eg: curl or a web browser) makes an HTTP request to a proxy-client instance running locally.
2120
2. The proxy-client forwards the request to a proxy-server instance over a remote-attested TLS channel.
@@ -139,3 +138,10 @@ Following a successful attestation exchange, the client can make HTTP requests u
139138

140139
As described above, the server will inject measurement data into the request headers before forwarding them to the target service, and the client will inject measurement data into the response headers before forwarding them to the source client.
141140

141+
### CLI differences from `cvm-reverse-proxy`
142+
143+
This aims to have a similar command line interface to `cvm-reverse-proxy` but there are some differences:
144+
145+
- The measurements file path is specified with `--measurements-file` rather than `--server-measurements` or `--client-measurements`.
146+
- If no measurements file is specified, `--allowed-remote-attestation-type` must be given.
147+
- `--log-dcap-quote` logs all attestation data (not only DCAP), but [currently] only remote attestation data, not locally-generated data.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "dummy-attestation-server"
3+
version = "0.1.0"
4+
edition = "2024"
5+
license = "MIT"
6+
publish = false
7+
8+
[dependencies]
9+
attested-tls-proxy = { path = ".." }
10+
tokio = { version = "1.48.0", features = ["full"] }
11+
axum = "0.8.6"
12+
clap = { version = "4.5.51", features = ["derive", "env"] }
13+
anyhow = "1.0.100"
14+
hex = "0.4.3"
15+
serde_json = "1.0.145"
16+
tracing = "0.1.41"
17+
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "json"] }
18+
parity-scale-codec = "3.7.5"
19+
reqwest = { version = "0.12.23", default-features = false }
20+
21+
[dev-dependencies]
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
pub use attested_tls_proxy::attestation::AttestationGenerator;
2+
use std::net::SocketAddr;
3+
4+
use anyhow::anyhow;
5+
use attested_tls_proxy::attestation::{AttestationExchangeMessage, AttestationVerifier};
6+
use axum::{
7+
extract::{Path, State},
8+
http::StatusCode,
9+
response::{IntoResponse, Response},
10+
};
11+
use parity_scale_codec::{Decode, Encode};
12+
use tokio::net::TcpListener;
13+
14+
#[derive(Clone)]
15+
struct SharedState {
16+
attestation_generator: AttestationGenerator,
17+
}
18+
19+
/// An HTTP server which produces test attestations
20+
pub async fn dummy_attestation_server(
21+
listener: TcpListener,
22+
attestation_generator: AttestationGenerator,
23+
) -> anyhow::Result<()> {
24+
let app = axum::Router::new()
25+
.route("/attest/{input_data}", axum::routing::get(get_attest))
26+
.with_state(SharedState {
27+
attestation_generator,
28+
});
29+
30+
axum::serve(listener, app).await?;
31+
32+
Ok(())
33+
}
34+
35+
/// Handler for the GET `/attest/{input_data}` route
36+
/// Input data should be 64 bytes hex
37+
async fn get_attest(
38+
State(shared_state): State<SharedState>,
39+
Path(input_data): Path<String>,
40+
) -> Result<(StatusCode, Vec<u8>), ServerError> {
41+
let input_data: [u8; 64] = hex::decode(input_data)?
42+
.try_into()
43+
.map_err(|_| anyhow!("Input data must be 64 bytes"))?;
44+
45+
let attestation = shared_state
46+
.attestation_generator
47+
.generate_attestation(input_data)
48+
.await?
49+
.encode();
50+
51+
Ok((StatusCode::OK, attestation))
52+
}
53+
54+
/// A client helper which makes a request to `/attest`
55+
pub async fn dummy_attestation_client(
56+
server_addr: SocketAddr,
57+
attestation_verifier: AttestationVerifier,
58+
) -> anyhow::Result<AttestationExchangeMessage> {
59+
let input_data = [0; 64];
60+
let response = reqwest::get(format!(
61+
"http://{server_addr}/attest/{}",
62+
hex::encode(input_data)
63+
))
64+
.await?
65+
.bytes()
66+
.await?;
67+
68+
let remote_attestation_message = AttestationExchangeMessage::decode(&mut &response[..])?;
69+
let remote_attestation_type = remote_attestation_message.attestation_type;
70+
71+
println!("Remote attestation type: {remote_attestation_type}");
72+
73+
attestation_verifier
74+
.verify_attestation(remote_attestation_message.clone(), input_data)
75+
.await?;
76+
77+
Ok(remote_attestation_message)
78+
}
79+
80+
struct ServerError(pub anyhow::Error);
81+
82+
impl<E> From<E> for ServerError
83+
where
84+
E: Into<anyhow::Error>,
85+
{
86+
fn from(err: E) -> Self {
87+
ServerError(err.into())
88+
}
89+
}
90+
91+
impl IntoResponse for ServerError {
92+
fn into_response(self) -> Response {
93+
eprintln!("{:?}", self.0);
94+
(StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self.0)).into_response()
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use super::*;
101+
102+
#[tokio::test]
103+
async fn test_dummy_server() {
104+
let attestation_generator = AttestationGenerator::with_no_attestation();
105+
106+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
107+
let server_addr = listener.local_addr().unwrap();
108+
109+
tokio::spawn(async move {
110+
dummy_attestation_server(listener, attestation_generator)
111+
.await
112+
.unwrap();
113+
});
114+
dummy_attestation_client(server_addr, AttestationVerifier::expect_none())
115+
.await
116+
.unwrap();
117+
}
118+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use attested_tls_proxy::attestation::{
2+
measurements::MeasurementPolicy, AttestationGenerator, AttestationType, AttestationVerifier,
3+
};
4+
use clap::{Parser, Subcommand};
5+
use dummy_attestation_server::{dummy_attestation_client, dummy_attestation_server};
6+
use std::{net::SocketAddr, path::PathBuf};
7+
use tokio::net::TcpListener;
8+
use tracing::level_filters::LevelFilter;
9+
10+
#[derive(Parser, Debug, Clone)]
11+
#[clap(version, about, long_about = None)]
12+
struct Cli {
13+
#[clap(subcommand)]
14+
command: CliCommand,
15+
/// Log debug messages
16+
#[arg(long, global = true)]
17+
log_debug: bool,
18+
/// Log in JSON format
19+
#[arg(long, global = true)]
20+
log_json: bool,
21+
/// Log DCAP quotes to folder `quotes/`
22+
#[arg(long, global = true)]
23+
log_dcap_quote: bool,
24+
}
25+
#[derive(Subcommand, Debug, Clone)]
26+
enum CliCommand {
27+
Server {
28+
/// Socket address to listen on
29+
#[arg(short, long, default_value = "0.0.0.0:0", env = "LISTEN_ADDR")]
30+
listen_addr: SocketAddr,
31+
/// Type of attestation to present (defaults to none)
32+
#[arg(long)]
33+
server_attestation_type: Option<String>,
34+
},
35+
Client {
36+
/// Socket address of a dummy attestation server
37+
server_addr: SocketAddr,
38+
/// Optional path to file containing JSON measurements to be enforced on the remote party
39+
#[arg(long, global = true, env = "MEASUREMENTS_FILE")]
40+
measurements_file: Option<PathBuf>,
41+
},
42+
}
43+
44+
#[tokio::main]
45+
async fn main() -> anyhow::Result<()> {
46+
let cli = Cli::parse();
47+
48+
let level_filter = if cli.log_debug {
49+
LevelFilter::DEBUG
50+
} else {
51+
LevelFilter::WARN
52+
};
53+
54+
let env_filter = tracing_subscriber::EnvFilter::builder()
55+
.with_default_directive(level_filter.into())
56+
.from_env_lossy();
57+
58+
let subscriber = tracing_subscriber::fmt::Subscriber::builder().with_env_filter(env_filter);
59+
60+
if cli.log_json {
61+
subscriber.json().init();
62+
} else {
63+
subscriber.pretty().init();
64+
}
65+
66+
if cli.log_dcap_quote {
67+
tokio::fs::create_dir_all("quotes").await?;
68+
}
69+
70+
match cli.command {
71+
CliCommand::Server {
72+
listen_addr,
73+
server_attestation_type,
74+
} => {
75+
let server_attestation_type: AttestationType = serde_json::from_value(
76+
serde_json::Value::String(server_attestation_type.unwrap_or("none".to_string())),
77+
)?;
78+
79+
let attestation_generator =
80+
AttestationGenerator::new_not_dummy(server_attestation_type)?;
81+
82+
let listener = TcpListener::bind(listen_addr).await?;
83+
84+
println!("Listening on {}", listener.local_addr()?);
85+
dummy_attestation_server(listener, attestation_generator).await?;
86+
}
87+
CliCommand::Client {
88+
server_addr,
89+
measurements_file,
90+
} => {
91+
let measurement_policy = match measurements_file {
92+
Some(measurements_file) => MeasurementPolicy::from_file(measurements_file).await?,
93+
None => MeasurementPolicy::accept_anything(),
94+
};
95+
96+
let attestation_verifier = AttestationVerifier {
97+
measurement_policy,
98+
pccs_url: None,
99+
log_dcap_quote: cli.log_dcap_quote,
100+
};
101+
102+
let attestation_message =
103+
dummy_attestation_client(server_addr, attestation_verifier).await?;
104+
105+
println!("{attestation_message:?}")
106+
}
107+
}
108+
109+
Ok(())
110+
}

src/attestation/dcap.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//! Data Center Attestation Primitives (DCAP) evidence generation and verification
2+
use crate::attestation::{
3+
measurements::{CvmImageMeasurements, Measurements, PlatformMeasurements},
4+
AttestationError,
5+
};
6+
7+
use configfs_tsm::QuoteGenerationError;
8+
use dcap_qvl::{
9+
collateral::get_collateral_for_fmspc,
10+
quote::{Quote, Report},
11+
};
12+
13+
/// For fetching collateral directly from Intel, if no PCCS is specified
14+
pub const PCS_URL: &str = "https://api.trustedservices.intel.com";
15+
16+
/// Quote generation using configfs_tsm
17+
pub async fn create_dcap_attestation(input_data: [u8; 64]) -> Result<Vec<u8>, AttestationError> {
18+
Ok(generate_quote(input_data)?)
19+
}
20+
21+
/// Verify a DCAP TDX quote, and return the measurement values
22+
pub async fn verify_dcap_attestation(
23+
input: Vec<u8>,
24+
expected_input_data: [u8; 64],
25+
pccs_url: Option<String>,
26+
) -> Result<Measurements, AttestationError> {
27+
let (platform_measurements, image_measurements) = if cfg!(not(test)) {
28+
let now = std::time::SystemTime::now()
29+
.duration_since(std::time::UNIX_EPOCH)?
30+
.as_secs();
31+
let quote = Quote::parse(&input)?;
32+
33+
let ca = quote.ca()?;
34+
let fmspc = hex::encode_upper(quote.fmspc()?);
35+
let collateral = get_collateral_for_fmspc(
36+
&pccs_url.clone().unwrap_or(PCS_URL.to_string()),
37+
fmspc,
38+
ca,
39+
false, // Indicates not SGX
40+
)
41+
.await?;
42+
43+
let _verified_report = dcap_qvl::verify::verify(&input, &collateral, now)?;
44+
45+
let measurements = (
46+
PlatformMeasurements::from_dcap_qvl_quote(&quote)?,
47+
CvmImageMeasurements::from_dcap_qvl_quote(&quote)?,
48+
);
49+
if get_quote_input_data(quote.report) != expected_input_data {
50+
return Err(AttestationError::InputMismatch);
51+
}
52+
measurements
53+
} else {
54+
// In tests we use mock quotes which will fail to verify
55+
let quote = tdx_quote::Quote::from_bytes(&input)?;
56+
if quote.report_input_data() != expected_input_data {
57+
return Err(AttestationError::InputMismatch);
58+
}
59+
60+
(
61+
PlatformMeasurements::from_tdx_quote(&quote),
62+
CvmImageMeasurements::from_tdx_quote(&quote),
63+
)
64+
};
65+
66+
Ok(Measurements {
67+
platform: platform_measurements,
68+
cvm_image: image_measurements,
69+
})
70+
}
71+
72+
/// Create a mock quote for testing on non-confidential hardware
73+
#[cfg(test)]
74+
fn generate_quote(input: [u8; 64]) -> Result<Vec<u8>, QuoteGenerationError> {
75+
let attestation_key = tdx_quote::SigningKey::random(&mut rand_core::OsRng);
76+
let provisioning_certification_key = tdx_quote::SigningKey::random(&mut rand_core::OsRng);
77+
Ok(tdx_quote::Quote::mock(
78+
attestation_key.clone(),
79+
provisioning_certification_key.clone(),
80+
input,
81+
b"Mock cert chain".to_vec(),
82+
)
83+
.as_bytes())
84+
}
85+
86+
/// Create a quote
87+
#[cfg(not(test))]
88+
fn generate_quote(input: [u8; 64]) -> Result<Vec<u8>, QuoteGenerationError> {
89+
configfs_tsm::create_quote(input)
90+
}
91+
92+
/// Given a [Report] get the input data regardless of report type
93+
pub fn get_quote_input_data(report: Report) -> [u8; 64] {
94+
match report {
95+
Report::TD10(r) => r.report_data,
96+
Report::TD15(r) => r.base.report_data,
97+
Report::SgxEnclave(r) => r.report_data,
98+
}
99+
}

0 commit comments

Comments
 (0)