diff --git a/Cargo.lock b/Cargo.lock index e849e1c..57cf617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,13 +572,12 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "attest-measure" version = "0.0.1" -source = "git+https://github.com/easy-tee/attest.git?rev=8206cd19d9dcb1978d85a3d8dece06a3ee7a1206#8206cd19d9dcb1978d85a3d8dece06a3ee7a1206" +source = "git+https://github.com/easy-tee/attest.git?rev=a64f147362b8948e2288015e476c40d04b11b661#a64f147362b8948e2288015e476c40d04b11b661" dependencies = [ + "anyhow", "attest-types", "authenticode", "crc32fast", - "fatfs", - "flate2", "hex", "hex-literal", "object", @@ -588,7 +587,6 @@ dependencies = [ "serde_json", "serde_with", "sha2", - "tar", "thiserror 2.0.17", "ureq 3.3.0", "x509-parser", @@ -597,7 +595,7 @@ dependencies = [ [[package]] name = "attest-types" version = "0.0.1" -source = "git+https://github.com/easy-tee/attest.git?rev=8206cd19d9dcb1978d85a3d8dece06a3ee7a1206#8206cd19d9dcb1978d85a3d8dece06a3ee7a1206" +source = "git+https://github.com/easy-tee/attest.git?rev=a64f147362b8948e2288015e476c40d04b11b661#a64f147362b8948e2288015e476c40d04b11b661" dependencies = [ "parity-scale-codec", "serde", @@ -608,7 +606,7 @@ dependencies = [ [[package]] name = "attestation" version = "0.0.1" -source = "git+https://github.com/flashbots/attested-tls?branch=main#bf8b99507d09d6ab79d675346bc604989b491ef9" +source = "git+https://github.com/flashbots/attested-tls?branch=peg%2Fverifier-returns-expected-measurements#2eb731ae6b9a2a2c2326d2420fd4516451c5b180" dependencies = [ "anyhow", "attest-measure", @@ -685,7 +683,6 @@ dependencies = [ "hyper-util", "jsonrpsee", "p256", - "pccs", "pem-rfc7468", "pin-project-lite", "pkcs1", @@ -1898,17 +1895,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "fatfs" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05669f8e7e2d7badc545c513710f0eba09c2fbef683eb859fd79c46c355048e0" -dependencies = [ - "bitflags 1.3.2", - "byteorder", - "log", -] - [[package]] name = "ff" version = "0.13.1" @@ -1925,16 +1911,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -3070,7 +3046,7 @@ dependencies = [ [[package]] name = "mock-tdx" version = "0.0.1" -source = "git+https://github.com/flashbots/attested-tls?branch=main#bf8b99507d09d6ab79d675346bc604989b491ef9" +source = "git+https://github.com/flashbots/attested-tls?branch=peg%2Fverifier-returns-expected-measurements#2eb731ae6b9a2a2c2326d2420fd4516451c5b180" dependencies = [ "axum", "dcap-qvl", @@ -3482,7 +3458,7 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pccs" version = "0.0.1" -source = "git+https://github.com/flashbots/attested-tls?branch=main#bf8b99507d09d6ab79d675346bc604989b491ef9" +source = "git+https://github.com/flashbots/attested-tls?branch=peg%2Fverifier-returns-expected-measurements#2eb731ae6b9a2a2c2326d2420fd4516451c5b180" dependencies = [ "anyhow", "dcap-qvl", @@ -4990,17 +4966,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tar" -version = "0.4.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "target-lexicon" version = "0.12.16" @@ -6286,16 +6251,6 @@ dependencies = [ "x509-ocsp", ] -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - [[package]] name = "yasna" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 545bf7c..6220aea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ pkcs1 = "0.7.5" pkcs8 = "0.10.2" rcgen = "0.14.5" pin-project-lite = "0.2.16" -pccs = { git = "https://github.com/flashbots/attested-tls", branch = "main" } [dev-dependencies] tempfile = "3.23.0" diff --git a/README.md b/README.md index 7d97492..9f9baf6 100644 --- a/README.md +++ b/README.md @@ -37,27 +37,65 @@ Details and examples of the measurements file format are [in the `attestation` c Exactly one verification policy must be provided: either `--measurements-file` or `--allowed-remote-attestation-type`. The latter may be `none` for cases where the remote party is not running in a CVM, but that must be explicitly specified. +When `--dynamic-measurement-policy` is enabled, the policy is reloaded from the path or URL supplied via `--measurements-file` after a measurement mismatch. This option requires `--measurements-file`. + As an alternative to specifying measurement values, OS image hashes can be specified. See [portable measurement policies](https://github.com/flashbots/attested-tls/tree/main/crates/attestation#portable-measurement-policies) for details. ### Measurement Headers -When attestation is validated successfully, the following headers are injected into the HTTP request / response making them available to the source client and/or target service. - -These aim to match the header formatting used by `cvm-reverse-proxy`. +When the remote party satisfies the configured measurement policy, the following headers are injected into the HTTP request / response, making the matched policy values available to the source client and/or target service. Header name: `X-Flashbots-Measurement` -Header value: +The value is compact, self-describing JSON containing the expected measurements from the matching policy record, rather than the measurements reported by the remote party. Hashes are lowercase hexadecimal strings. DCAP and Azure register values are arrays because a policy can accept multiple values for each register. + +For DCAP measurements, keys `"0"` through `"4"` represent MRTD and RTMR0 through RTMR3 respectively: + ```json { - "0": "48 byte MRTD value encoded as hex", - "1": "48 byte RTMR0 value encoded as hex", - "2": "48 byte RTMR1 value encoded as hex", - "3": "48 byte RTMR2 value encoded as hex", - "4": "48 byte RTMR3 value encoded as hex" + "type": "dcap", + "measurements": { + "0": ["<96 hex characters>"], + "3": ["<96 hex characters>", "<96 hex characters>"] + } } ``` +For Azure measurements, each key is a PCR index: + +```json +{ + "type": "azure", + "measurements": { + "4": ["<64 hex characters>"], + "11": ["<64 hex characters>"] + } +} +``` + +Portable image-hash policies produce: + +```json +{ + "type": "image", + "measurements": { + "uki_authenticode": "<96 hex characters>", + "kernel_authenticode": "<96 hex characters>", + "cmdline_hash": "<96 hex characters>", + "initrd_hash": "<96 hex characters>", + "gpt_disk_guid_hash": "<96 hex characters>" + } +} +``` + +When the matching policy permits no attestation, the value is: + +```json +{"type":"no_attestation"} +``` + +The actual HTTP header value is serialized as compact JSON on one line. + Header name: `X-Flashbots-Attestation-Type` Header value: an attestation type given as a string as described below. @@ -77,7 +115,7 @@ These are the attestation type names used in the HTTP headers, and the measureme - `--pccs-url` selects the PCCS used to retrieve collateral when verifying DCAP attestations. It defaults to Intel PCS. - `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 ` writes the verified remote measurements as JSON in addition to writing the certificate chain to standard output. +- `get-tls-cert --out-measurements ` writes the matched expected measurements as JSON in addition to writing the certificate chain to standard output. - 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 @@ -92,7 +130,7 @@ Immediately after the TLS handshake, an attestation exchange is made. Details of Following a successful attestation exchange, the client can make HTTP requests, and the server will forward them to the target service. -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. +As described above, the server will inject matched expected measurement data into the request headers before forwarding them to the target service, and the client will inject the same kind of data into the response headers before forwarding them to the source client. 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. diff --git a/attested-tls/Cargo.toml b/attested-tls/Cargo.toml index ef4930d..652014b 100644 --- a/attested-tls/Cargo.toml +++ b/attested-tls/Cargo.toml @@ -18,7 +18,7 @@ http = "1.3.1" serde_json = "1.0.145" tracing = "0.1.41" parity-scale-codec = "3.7.5" -attestation = { git = "https://github.com/flashbots/attested-tls", branch = "main" } +attestation = { git = "https://github.com/flashbots/attested-tls", branch = "peg/verifier-returns-expected-measurements" } # Used for websocket support tokio-tungstenite = { version = "0.28.0", optional = true } @@ -40,7 +40,7 @@ rcgen = { version = "0.14.5", optional = true } [dev-dependencies] rcgen = "0.14.5" tempfile = "3.23.0" -attestation = { git = "https://github.com/flashbots/attested-tls", branch = "main", features = ["mock"] } +attestation = { git = "https://github.com/flashbots/attested-tls", branch = "peg/verifier-returns-expected-measurements", features = ["mock"] } [features] default = ["ws", "rpc"] diff --git a/attested-tls/src/attested_rpc.rs b/attested-tls/src/attested_rpc.rs index 0e19c6d..256cd52 100644 --- a/attested-tls/src/attested_rpc.rs +++ b/attested-tls/src/attested_rpc.rs @@ -1,6 +1,7 @@ //! Provides an attested JSON RPC client based on [alloy_rpc_client::RpcClient] use alloy_rpc_client::RpcClient; use alloy_transport_http::{Http, HyperClient}; +use attestation::measurements::ExpectedMeasurements; use hyper::{Request, Response, client::conn}; use hyper_util::rt::TokioIo; use std::{ @@ -12,10 +13,7 @@ use std::{ use thiserror::Error; use tower_service::Service; -use crate::{ - AttestedTlsClient, AttestedTlsError, - attestation::{AttestationType, measurements::MultiMeasurements}, -}; +use crate::{AttestedTlsClient, AttestedTlsError, attestation::AttestationType}; /// Supported HTTP versions for RPC connection bootstrapping pub enum HttpVersion { @@ -54,7 +52,7 @@ impl AttestedRpcClient { &self, server: &str, is_local: bool, - ) -> Result<(RpcClient, Option, AttestationType), AttestedRpcError> { + ) -> Result<(RpcClient, ExpectedMeasurements, AttestationType), AttestedRpcError> { let (stream, measurements, attestation_type) = self.inner.connect_tcp(server).await?; let io = TokioIo::new(stream); diff --git a/attested-tls/src/lib.rs b/attested-tls/src/lib.rs index 3fdfe2c..ab9eacb 100644 --- a/attested-tls/src/lib.rs +++ b/attested-tls/src/lib.rs @@ -12,7 +12,7 @@ pub use attestation; use attestation::{ AttestationError, AttestationExchangeMessage, AttestationGenerator, AttestationType, - AttestationVerifier, measurements::MultiMeasurements, + AttestationVerifier, measurements::ExpectedMeasurements, }; use parity_scale_codec::{Decode, Encode}; use sha2::{Digest, Sha256}; @@ -137,7 +137,7 @@ impl AttestedTlsServer { ) -> Result< ( tokio_rustls::server::TlsStream, - Option, + ExpectedMeasurements, AttestationType, ), AttestedTlsError, @@ -197,7 +197,7 @@ 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 + // If we expect an attestation 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)?; @@ -205,7 +205,7 @@ impl AttestedTlsServer { .verify_attestation(remote_attestation_message, remote_input_data) .await? } else { - None + ExpectedMeasurements::NoAttestation }; Ok((tls_stream, measurements, remote_attestation_type)) @@ -330,7 +330,7 @@ impl AttestedTlsClient { ) -> Result< ( tokio_rustls::client::TlsStream, - Option, + ExpectedMeasurements, AttestationType, ), AttestedTlsError, @@ -414,7 +414,7 @@ impl AttestedTlsClient { ) -> Result< ( tokio_rustls::client::TlsStream, - Option, + ExpectedMeasurements, AttestationType, ), AttestedTlsError, @@ -427,7 +427,7 @@ impl AttestedTlsClient { pub async fn get_tls_cert( &self, server_name: &str, - ) -> Result<(Vec>, Option), AttestedTlsError> { + ) -> Result<(Vec>, ExpectedMeasurements), AttestedTlsError> { let (mut tls_stream, measurements, _attestation_type) = self.connect_tcp(server_name).await?; @@ -449,7 +449,7 @@ pub async fn get_tls_cert( server_name: String, attestation_verifier: AttestationVerifier, remote_certificate: Option>, -) -> Result<(Vec>, Option), AttestedTlsError> { +) -> Result<(Vec>, ExpectedMeasurements), AttestedTlsError> { tracing::debug!("Getting remote TLS cert"); let attested_tls_client = AttestedTlsClient::new( None, @@ -467,7 +467,7 @@ pub async fn get_tls_cert_with_config( server_name: &str, attestation_verifier: AttestationVerifier, client_config: ClientConfig, -) -> Result<(Vec>, Option), AttestedTlsError> { +) -> Result<(Vec>, ExpectedMeasurements), AttestedTlsError> { let attested_tls_client = AttestedTlsClient::new_with_tls_config( client_config, AttestationGenerator::with_no_attestation(), @@ -752,8 +752,9 @@ mod tests { ) .unwrap(); - let mut attestation_verifier = AttestationVerifier::mock(); - attestation_verifier.measurement_policy = measurement_policy; + let attestation_verifier = AttestationVerifier::builder(measurement_policy) + .with_no_internal_pccs() + .build(); let client = AttestedTlsClient::new_with_tls_config( client_config, diff --git a/attested-tls/src/websockets.rs b/attested-tls/src/websockets.rs index 7669787..f22b15a 100644 --- a/attested-tls/src/websockets.rs +++ b/attested-tls/src/websockets.rs @@ -1,13 +1,11 @@ //! An attested Websocket server and client +use attestation::measurements::ExpectedMeasurements; use std::{net::SocketAddr, sync::Arc}; use thiserror::Error; use tokio::net::{TcpListener, ToSocketAddrs}; use tokio_tungstenite::{WebSocketStream, tungstenite::protocol::WebSocketConfig}; -use crate::{ - AttestedTlsClient, AttestedTlsError, AttestedTlsServer, - attestation::{AttestationType, measurements::MultiMeasurements}, -}; +use crate::{AttestedTlsClient, AttestedTlsError, AttestedTlsServer, attestation::AttestationType}; /// Websocket message type re-exported for convenience pub use tokio_tungstenite::tungstenite::protocol::Message; @@ -42,7 +40,7 @@ impl AttestedWsServer { ) -> Result< ( WebSocketStream>, - Option, + ExpectedMeasurements, AttestationType, ), AttestedWsError, @@ -80,7 +78,7 @@ impl AttestedWsClient { ) -> Result< ( WebSocketStream>, - Option, + ExpectedMeasurements, AttestationType, ), AttestedWsError, diff --git a/src/lib.rs b/src/lib.rs index 3eab66d..c683f70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ mod http_version; #[cfg(test)] mod test_helpers; +use attested_tls::attestation::measurements::ExpectedMeasurements; use bytes::Bytes; use http::{HeaderMap, HeaderName, HeaderValue}; use http_body_util::{BodyExt, combinators::BoxBody}; @@ -33,9 +34,7 @@ use tracing::{debug, error, warn}; use crate::http_version::{ALPN_H2, ALPN_HTTP11, HttpConnection, HttpSender, HttpVersion}; use attested_tls::{ AttestedTlsClient, AttestedTlsError, AttestedTlsServer, TlsCertAndKey, - attestation::{ - AttestationError, AttestationType, AttestationVerifier, measurements::MultiMeasurements, - }, + attestation::{AttestationError, AttestationType, AttestationVerifier}, }; /// The header name for giving attestation type @@ -78,7 +77,7 @@ pub async fn get_tls_cert( attestation_verifier: AttestationVerifier, remote_certificate: Option>, allow_self_signed: bool, -) -> Result<(Vec>, Option), AttestedTlsError> { +) -> Result<(Vec>, ExpectedMeasurements), AttestedTlsError> { let (cert, measurements) = if allow_self_signed { let client_tls_config = self_signed::client_tls_config_allow_self_signed()?; attested_tls::get_tls_cert_with_config( @@ -218,7 +217,7 @@ impl ProxyServer { /// Handle an incoming connection from a proxy-client async fn handle_connection( tls_stream: tokio_rustls::server::TlsStream, - measurements: Option, + measurements: ExpectedMeasurements, remote_attestation_type: AttestationType, target: String, client_addr: SocketAddr, @@ -251,18 +250,16 @@ 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 + // Add the expected measurements matched for the remote peer to the request header. let measurements = measurements.clone(); - if let Some(measurements) = measurements { - match measurements.to_header_format() { - Ok(header_value) => { - headers.insert(MEASUREMENT_HEADER, header_value); - } - Err(e) => { - // This error is highly unlikely - that the measurement values fail to - // encode to JSON or fit in an HTTP header - error!("Failed to encode measurement values: {e}"); - } + match measurements.to_header_format() { + Ok(header_value) => { + headers.insert(MEASUREMENT_HEADER, header_value); + } + Err(e) => { + // This error is highly unlikely - that the measurement values fail to + // encode to JSON or fit in an HTTP header + error!("Failed to encode measurement values: {e}"); } } @@ -496,19 +493,17 @@ impl ProxyClient { let (response, should_reconnect) = match sender.send_request(req).await { Ok(mut resp) => { debug!("[proxy-client] Read response from proxy-server: {resp:?}"); - // If we have measurements from the proxy-server, inject them into the - // response header + // Add the expected measurements matched for the proxy-server to the + // response header. let headers = resp.headers_mut(); - if let Some(measurements) = measurements.clone() { - match measurements.to_header_format() { - Ok(header_value) => { - headers.insert(MEASUREMENT_HEADER, header_value); - } - Err(e) => { - // This error is highly unlikely - that the measurement values fail to - // encode to JSON or fit in an HTTP header - error!("Failed to encode measurement values: {e}"); - } + match measurements.to_header_format() { + Ok(header_value) => { + headers.insert(MEASUREMENT_HEADER, header_value); + } + Err(e) => { + // This error is highly unlikely - that the measurement values fail to + // encode to JSON or fit in an HTTP header + error!("Failed to encode measurement values: {e}"); } } @@ -628,7 +623,7 @@ impl ProxyClient { ( HttpSender, HttpConnection, - Option, + ExpectedMeasurements, AttestationType, ), ProxyError, @@ -665,7 +660,7 @@ impl ProxyClient { ( HttpSender, HttpConnection, - Option, + ExpectedMeasurements, AttestationType, ), ProxyError, @@ -799,9 +794,16 @@ mod tests { use super::*; use test_helpers::{ example_http_service, generate_certificate_chain, generate_tls_config, - generate_tls_config_with_client_auth, init_tracing, mock_dcap_measurements, + generate_tls_config_with_client_auth, init_tracing, mock_expected_measurements, }; + fn assert_no_attestation_header(value: &str) { + assert_eq!( + ExpectedMeasurements::from_header_format(value).unwrap(), + ExpectedMeasurements::NoAttestation + ); + } + #[test] fn proxy_alpn_protocols_prefer_http2() { let mut protocols = Vec::new(); @@ -876,13 +878,11 @@ mod tests { assert_eq!(attestation_type, AttestationType::DcapTdx.as_str()); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = - MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) - .unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(measurements_json).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); let res_body = res.text().await.unwrap(); - assert_eq!(res_body, "No measurements"); + assert_no_attestation_header(&res_body); } // Server has mock DCAP, client has no attestation and no client auth @@ -942,13 +942,11 @@ mod tests { assert_eq!(attestation_type, AttestationType::DcapTdx.as_str()); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = - MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) - .unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(measurements_json).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); let res_body = res.text().await.unwrap(); - assert_eq!(res_body, "No measurements"); + assert_no_attestation_header(&res_body); } // Server has no attestation, client has mock DCAP and client auth @@ -1012,9 +1010,9 @@ mod tests { .await .unwrap(); - // We expect no measurements from the server + // The server matched the policy record allowing no attestation. let headers = res.headers(); - assert!(headers.get(MEASUREMENT_HEADER).is_none()); + assert_no_attestation_header(headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1027,9 +1025,8 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = - MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(&res_body).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); } // Server has no attestation, client has mock DCAP but no client auth @@ -1083,9 +1080,9 @@ mod tests { .await .unwrap(); - // We expect no measurements from the server + // The server matched the policy record allowing no attestation. let headers = res.headers(); - assert!(headers.get(MEASUREMENT_HEADER).is_none()); + assert_no_attestation_header(headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1098,9 +1095,8 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = - MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(&res_body).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); } // Server has mock DCAP, client has mock DCAP and client auth @@ -1166,10 +1162,8 @@ mod tests { let headers = res.headers(); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = - MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) - .unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(measurements_json).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1182,9 +1176,8 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = - MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(&res_body).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); // Now do another request - to check that the connection has stayed open let res = reqwest::get(format!("http://{}", proxy_client_addr.to_string())) @@ -1193,10 +1186,8 @@ mod tests { let headers = res.headers(); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = - MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) - .unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(measurements_json).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); let attestation_type = headers .get(ATTESTATION_TYPE_HEADER) @@ -1209,9 +1200,8 @@ mod tests { // The response body shows us what was in the request header (as the test http server // handler puts them there) - let measurements = - MultiMeasurements::from_header_format(&res_body, AttestationType::DcapTdx).unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(&res_body).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); } // Server has mock DCAP, client no attestation - just get the server certificate @@ -1338,8 +1328,9 @@ mod tests { ) .unwrap(); - let mut attestation_verifier = AttestationVerifier::mock(); - attestation_verifier.measurement_policy = measurement_policy; + let attestation_verifier = AttestationVerifier::builder(measurement_policy) + .with_no_internal_pccs() + .build(); let proxy_client_result = ProxyClient::new_with_tls_config( client_config, @@ -1436,13 +1427,11 @@ mod tests { assert_eq!(attestation_type, AttestationType::DcapTdx.as_str()); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = - MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) - .unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(measurements_json).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); let res_body = res.text().await.unwrap(); - assert_eq!(res_body, "No measurements"); + assert_no_attestation_header(&res_body); } // Use HTTP 1.1 @@ -1509,12 +1498,10 @@ mod tests { assert_eq!(attestation_type, AttestationType::DcapTdx.as_str()); let measurements_json = headers.get(MEASUREMENT_HEADER).unwrap().to_str().unwrap(); - let measurements = - MultiMeasurements::from_header_format(measurements_json, AttestationType::DcapTdx) - .unwrap(); - assert_eq!(measurements, mock_dcap_measurements()); + let measurements = ExpectedMeasurements::from_header_format(measurements_json).unwrap(); + assert_eq!(measurements, mock_expected_measurements()); let res_body = res.text().await.unwrap(); - assert_eq!(res_body, "No measurements"); + assert_no_attestation_header(&res_body); } } diff --git a/src/main.rs b/src/main.rs index 449fe69..d9a79ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, ensure}; -use attested_tls::attestation::measurements::MultiMeasurements; use clap::{Parser, Subcommand}; use std::{ fs::File, @@ -35,6 +34,9 @@ struct Cli { /// Path to file, or URL, containing JSON measurements to be enforced on the remote party #[arg(long, global = true, env = "MEASUREMENTS_FILE")] measurements_file: Option, + /// Reload the measurement policy from --measurements-file after a measurement mismatch + #[arg(long, global = true, requires = "measurements_file")] + dynamic_measurement_policy: bool, /// If no measurements file is specified, a single attestion type to allow #[arg(long, global = true)] allowed_remote_attestation_type: Option, @@ -184,6 +186,16 @@ async fn main() -> anyhow::Result<()> { cli.allowed_remote_attestation_type.is_some() != cli.measurements_file.is_some(), "Exactly one of --measurements-file or --allowed-remote-attestation-type must be provided" ); + ensure!( + !cli.dynamic_measurement_policy || cli.measurements_file.is_some(), + "--dynamic-measurement-policy requires --measurements-file" + ); + + let dynamic_measurements_file_or_url = if cli.dynamic_measurement_policy { + cli.measurements_file.clone() + } else { + None + }; let crate_name = env!("CARGO_CRATE_NAME"); @@ -230,13 +242,23 @@ async fn main() -> anyhow::Result<()> { } }; - let mut attestation_verifier = AttestationVerifier::new( - measurement_policy, - None, - cli.log_dcap_quote, - cli.override_azure_outdated_tcb, - ); - attestation_verifier.internal_pccs = Some(pccs::Pccs::new_without_prewarm(cli.pccs_url)); + let mut attestation_verifier_builder = + AttestationVerifier::builder(measurement_policy).with_pccs_not_prewarmed(); + if let Some(file_or_url) = dynamic_measurements_file_or_url { + attestation_verifier_builder = + attestation_verifier_builder.with_dynamic_measurements_file_or_url(file_or_url); + } + if let Some(pccs_url) = cli.pccs_url { + attestation_verifier_builder = attestation_verifier_builder.pccs_url(pccs_url); + } + if cli.log_dcap_quote { + attestation_verifier_builder = attestation_verifier_builder.dump_dcap_quotes(); + } + #[cfg(feature = "azure")] + if cli.override_azure_outdated_tcb { + attestation_verifier_builder = attestation_verifier_builder.override_azure_outdated_tcb(); + } + let attestation_verifier = attestation_verifier_builder.build(); match cli.command { CliCommand::Client { @@ -382,10 +404,7 @@ async fn main() -> anyhow::Result<()> { if let Some(path_to_write_measurements) = out_measurements { std::fs::write( path_to_write_measurements, - measurements - .unwrap_or(MultiMeasurements::NoAttestation) - .to_header_format()? - .as_bytes(), + measurements.to_header_format()?.as_bytes(), )?; } println!("{}", certs_to_pem_string(&cert_chain)?); @@ -514,3 +533,38 @@ fn certs_to_pem_string(certs: &[CertificateDer<'_>]) -> Result ExpectedMeasurements { + MeasurementPolicy::mock() + .check_measurement( + &attested_tls::attestation::measurements::mock_dcap_measurements(), + None, + ) + .unwrap() +} /// Helper to generate a self-signed certificate for testing pub fn generate_certificate_chain(