Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ These are the attestation type names used in the HTTP headers, and the measureme
- `client`, `get-tls-cert`, and `attested-get` accept `--allow-self-signed` to permit a self-signed remote TLS certificate.
- `client` and `server` accept `--listen-addr-healthcheck` to start a separate HTTP health-check listener.
- `get-tls-cert --out-measurements <PATH>` writes the verified remote measurements as JSON in addition to writing the certificate chain to standard output.
- `attested-get` does not follow HTTP redirects and exits with an error on 3xx responses. Its library API returns the original response for inspection. The loopback request ignores environment proxy settings.
- If `server` is started without `--tls-private-key-path` and `--tls-certificate-path`, it generates a self-signed certificate for its listening IP address.

## Protocol Specification
Expand Down
67 changes: 56 additions & 11 deletions attested-tls/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,14 @@ impl AttestedTlsServer {
let remote_attestation_message = AttestationExchangeMessage::decode(&mut &buf[..])?;
let remote_attestation_type = remote_attestation_message.attestation_type();

// If we expect an attestaion from the client, verify it and get measurements
let measurements = if self.attestation_verifier.has_remote_attestation() {
let remote_input_data = compute_report_input(remote_cert_chain.as_deref(), exporter)?;

self.attestation_verifier
.verify_attestation(remote_attestation_message, remote_input_data)
.await?
.map(|verified| verified.measurements)
} else {
None
};
// Validate every exchange, including policies that only accept no attestation,
// before exposing the peer's attestation type to callers.
let remote_input_data = compute_report_input(remote_cert_chain.as_deref(), exporter)?;
let measurements = self
.attestation_verifier
.verify_attestation(remote_attestation_message, remote_input_data)
.await?
.map(|verified| verified.measurements);

Ok((tls_stream, measurements, remote_attestation_type))
}
Expand Down Expand Up @@ -672,6 +669,54 @@ mod tests {
client.connect_tcp(&server_addr.to_string()).await.unwrap();
}

#[tokio::test]
async fn server_enforces_no_attestation_policy() {
for client_type in [AttestationType::None, AttestationType::DcapTdx] {
let (cert_chain, private_key) =
generate_certificate_chain("127.0.0.1".parse().unwrap());
let (server_config, client_config) =
generate_tls_config(cert_chain.clone(), private_key);
let server = AttestedTlsServer::new_with_tls_config(
cert_chain,
server_config,
AttestationGenerator::with_no_attestation(),
AttestationVerifier::expect_none(),
)
.unwrap();
let client = AttestedTlsClient::new_with_tls_config(
client_config,
AttestationGenerator::new(client_type, None).unwrap(),
AttestationVerifier::expect_none(),
None,
)
.unwrap();
let (server_io, client_io) = tokio::io::duplex(128 * 1024);
let (server_result, client_result) =
tokio::time::timeout(std::time::Duration::from_secs(5), async {
tokio::join!(
server.handle_connection(server_io),
client.connect("127.0.0.1", client_io),
)
})
.await
.unwrap();
// The client finishes sending before the server checks its evidence.
let _client_connection = client_result.unwrap();
if client_type == AttestationType::None {
let (_stream, measurements, attestation_type) = server_result.unwrap();
assert!(measurements.is_none());
assert_eq!(attestation_type, AttestationType::None);
} else {
assert!(matches!(
server_result,
Err(AttestedTlsError::Attestation(
AttestationError::MeasurementsNotAccepted
))
));
}
}
}

// Negative test - server does not provide attestation but client requires it
// Server has no attestation, client has no attestation and no client auth
#[tokio::test]
Expand Down
25 changes: 14 additions & 11 deletions src/attested_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub fn split_target_and_path(target: &str) -> (String, Option<String>) {

/// Start a proxy-client, send a single HTTP GET request to the given path and return the
/// [reqwest::Response]
///
/// Redirects are returned without following them, so requests stay on the attested channel.
pub async fn attested_get(
target_addr: String,
url_path: &str,
Expand Down Expand Up @@ -62,7 +64,17 @@ async fn attested_get_with_client(
proxy_client: ProxyClient,
url_path: &str,
) -> Result<reqwest::Response, ProxyError> {
let proxy_client_addr = proxy_client.local_addr().unwrap();
let proxy_client_addr = proxy_client.local_addr()?;

// Keep the request on the local proxy and return redirects without following them.
let client = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.no_proxy()
.build()?;
let url_path = url_path.strip_prefix("/").unwrap_or(url_path);
let request = client
.get(format!("http://{proxy_client_addr}/{url_path}"))
.build()?;

// Accept a single connection in a separate task
tokio::spawn(async move {
Expand All @@ -71,16 +83,7 @@ async fn attested_get_with_client(
}
});

// Remove leading '/' if present
let url_path = url_path.strip_prefix("/").unwrap_or(url_path);

// Make a GET request
let request = reqwest::Request::new(
reqwest::Method::GET,
reqwest::Url::parse(&format!("http://{proxy_client_addr}/{url_path}")).unwrap(),
);
let client = reqwest::Client::new();
let response = client.execute(request).await.unwrap();
let response = client.execute(request).await?;
Ok(response)
}

Expand Down
121 changes: 120 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ impl ProxyServer {

update_header(headers, &X_FORWARDED_FOR, &new_x_forwarded_for);

// If we have measurements, from the remote peer, add them to the request header
// Discard untrusted measurements before inserting verified peer measurements.
headers.remove(MEASUREMENT_HEADER);
let measurements = measurements.clone();
if let Some(measurements) = measurements {
match measurements.to_header_format() {
Expand Down Expand Up @@ -499,6 +500,8 @@ impl ProxyClient {
// If we have measurements from the proxy-server, inject them into the
// response header
let headers = resp.headers_mut();
// Never forward measurements supplied by the target service.
headers.remove(MEASUREMENT_HEADER);
if let Some(measurements) = measurements.clone() {
match measurements.to_header_format() {
Ok(header_value) => {
Expand Down Expand Up @@ -749,6 +752,8 @@ pub enum ProxyError {
BadDnsName(#[from] tokio_rustls::rustls::pki_types::InvalidDnsNameError),
#[error("HTTP: {0}")]
Hyper(#[from] hyper::Error),
#[error("HTTP request: {0}")]
Reqwest(#[from] reqwest::Error),
Comment thread
ameba23 marked this conversation as resolved.
#[error("JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("Could not forward response - sender was dropped")]
Expand Down Expand Up @@ -886,6 +891,120 @@ mod tests {
assert_eq!(res_body, "No measurements");
}

#[tokio::test]
async fn http_proxy_sanitizes_measurement_headers() {
for protocol in [ALPN_HTTP11, ALPN_H2] {
for attested in [false, true] {
let app = axum::Router::new().route(
"/",
axum::routing::get(|headers: HeaderMap| async move {
let received: Vec<String> = headers
.get_all(MEASUREMENT_HEADER)
.iter()
.map(|value| value.to_str().unwrap().to_owned())
.collect();
let mut headers = HeaderMap::new();
headers.append(
MEASUREMENT_HEADER,
HeaderValue::from_static("forged-server"),
);
headers.append(
MEASUREMENT_HEADER,
HeaderValue::from_static("another-forgery"),
);
(headers, axum::Json(received))
}),
);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let target_addr = listener.local_addr().unwrap();
let backend = tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});

let (cert_chain, private_key) =
generate_certificate_chain("127.0.0.1".parse().unwrap());
let (mut server_config, mut client_config) =
generate_tls_config(cert_chain.clone(), private_key);
server_config.alpn_protocols = vec![protocol.to_vec()];
client_config.alpn_protocols = vec![protocol.to_vec()];

let (generator, verifier, expected) = if attested {
(
AttestationGenerator::new(AttestationType::DcapTdx, None).unwrap(),
AttestationVerifier::mock(),
vec![
serde_json::from_str::<serde_json::Value>(
mock_dcap_measurements()
.to_header_format()
.unwrap()
.to_str()
.unwrap(),
)
.unwrap(),
],
)
} else {
(
AttestationGenerator::with_no_attestation(),
AttestationVerifier::expect_none(),
Vec::new(),
)
};
let server = ProxyServer::new_with_tls_config(
cert_chain,
server_config,
"127.0.0.1:0",
target_addr.to_string(),
generator.clone(),
verifier.clone(),
)
.await
.unwrap();
let server_addr = server.local_addr().unwrap();
tokio::spawn(async move { server.accept().await.unwrap() });

let client = ProxyClient::new_with_tls_config(
client_config,
"127.0.0.1:0",
server_addr.to_string(),
generator,
verifier,
None,
)
.await
.unwrap();
let client_addr = client.local_addr().unwrap();
tokio::spawn(async move { client.accept().await.unwrap() });

let response = reqwest::Client::new()
.get(format!("http://{client_addr}/"))
.header(MEASUREMENT_HEADER, "forged-client")
.header(MEASUREMENT_HEADER, "another-forgery")
.send()
.await
.unwrap();
assert_eq!(response.status(), http::StatusCode::OK);
let response_measurements: Vec<_> = response
.headers()
.get_all(MEASUREMENT_HEADER)
.iter()
.map(|value| {
serde_json::from_str::<serde_json::Value>(value.to_str().unwrap()).unwrap()
})
.collect();
assert_eq!(response_measurements, expected);
let request_measurements: Vec<String> =
serde_json::from_slice(&response.bytes().await.unwrap()).unwrap();
let request_measurements: Vec<_> = request_measurements
.iter()
.map(|value| serde_json::from_str::<serde_json::Value>(value).unwrap())
.collect();
assert_eq!(request_measurements, expected);
backend.abort();
}
}
}

// Server has mock DCAP, client has no attestation and no client auth
#[tokio::test]
async fn http_proxy_with_server_attestation() {
Expand Down
6 changes: 6 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@ async fn main() -> anyhow::Result<()> {
)
.await?;

ensure!(
!response.status().is_redirection(),
"Attested GET returned {}; redirects are not followed because the destination has not been attested",
response.status()
);

// Write response body to standard output
let mut stdout = tokio::io::stdout();

Expand Down
Loading
Loading