diff --git a/README.md b/README.md index 85b1999..a3c470b 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,13 @@ These are the attestation type names used in the HTTP headers, and the measureme - `azure-tdx` - TDX on Azure, with vTPM attestation - `dcap-tdx` - DCAP TDX (platform not specified) +The HTTP and TCP tunnel client/server commands default to `auto` when the local +attestation type is omitted. If no supported attestation platform is detected, +startup fails. To run without local attestation, explicitly pass +`--client-attestation-type none` or `--server-attestation-type none` (or set the +corresponding `CLIENT_ATTESTATION_TYPE` / `SERVER_ATTESTATION_TYPE` environment +variable to `none`). This is separate from the remote verification policy. + ### Other CLI Options - `--pccs-url` selects the PCCS used to retrieve collateral when verifying DCAP attestations. It defaults to Intel PCS. @@ -264,6 +271,7 @@ docker run --rm \ docker run --rm attested-tls-proxy client \ --listen-addr 0.0.0.0:8080 \ target-server:443 \ + --client-attestation-type none \ --allowed-remote-attestation-type none ``` @@ -273,7 +281,10 @@ use the target container's name instead of `host.docker.internal`. ### Testing with Docker Compose -A `docker-compose.yml` is provided to test the full proxy chain: +A `docker-compose.yml` is provided for local testing of the full proxy chain. +It uses test certificates and explicitly disables local attestation on both +proxies, testing TLS forwarding without attestation. For a CVM deployment, +configure local attestation and remote verification policies for your platform. 1. **Generate test certificates:** ```bash diff --git a/crates/attested-tls-proxy/TCP_TUNNEL.md b/crates/attested-tls-proxy/TCP_TUNNEL.md index 667bce1..24d2ff8 100644 --- a/crates/attested-tls-proxy/TCP_TUNNEL.md +++ b/crates/attested-tls-proxy/TCP_TUNNEL.md @@ -84,7 +84,7 @@ settings follow the [HTTP proxy CLI](../../README.md#measurements-file). | Client `--tls-ca-certificate` | Trust the first PEM certificate instead of public roots. | | Client `--allow-self-signed` | Accept a self-signed server certificate; still verify attestation. Cannot combine with `--tls-ca-certificate`. Preserves any supplied client identity. | | Server `--client-auth` | Require a TLS client certificate authenticated against public roots. Private client CAs can be configured through the Rust API. | -| `--client-attestation-type` / `--server-attestation-type` | Automatic local detection when omitted. | +| `--client-attestation-type` / `--server-attestation-type` | Automatic local detection when omitted or set to `auto`; startup fails if no supported platform is detected. Set `none` explicitly to run without local attestation. | | `--pccs-url`, `--dev-dummy-dcap` | Same meaning as in the HTTP proxy. | | `--log-debug`, `--log-json`, `--log-dcap-quote` | Debug logs, structured logs, or DCAP quote dumps in `quotes/`. Logs go to stderr; the default level is info. Payload bytes are never logged. | | `--override-azure-outdated-tcb` | Same Azure verification override as in the HTTP proxy. | diff --git a/crates/attested-tls-proxy/src/cli/attestation.rs b/crates/attested-tls-proxy/src/cli/attestation.rs index c92ff51..f7d6797 100644 --- a/crates/attested-tls-proxy/src/cli/attestation.rs +++ b/crates/attested-tls-proxy/src/cli/attestation.rs @@ -1,6 +1,7 @@ -use anyhow::anyhow; +use anyhow::{anyhow, ensure}; use attested_tls::attestation::{ - AttestationType, AttestationVerifier, CachePolicy, measurements::MeasurementPolicy, + AttestationGenerator, AttestationType, AttestationVerifier, CachePolicy, + measurements::MeasurementPolicy, }; pub(super) async fn build_verifier( @@ -51,3 +52,84 @@ pub(super) async fn build_verifier( } Ok(attestation_verifier_builder.build()) } + +/// Build a local generator, requiring explicit opt-in to run without attestation. +pub(super) fn build_attestation_generator( + requested_type: Option, + provider_url: Option, + type_option: &str, +) -> anyhow::Result { + let generator = AttestationGenerator::new_with_detection(requested_type.clone(), provider_url)?; + validate_platform( + generator.attestation_type, + requested_type.as_deref(), + type_option, + )?; + Ok(generator) +} + +fn validate_platform( + platform: AttestationType, + requested_type: Option<&str>, + type_option: &str, +) -> anyhow::Result<()> { + let automatic = matches!(requested_type, None | Some("auto")); + ensure!( + !automatic || platform != AttestationType::None, + "No supported attestation platform detected. To proceed without local attestation, explicitly set {type_option} none." + ); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn automatic_detection_requires_an_attested_platform() { + for option in ["--client-attestation-type", "--server-attestation-type"] { + for requested in [None, Some("auto")] { + let error = validate_platform(AttestationType::None, requested, option) + .unwrap_err() + .to_string(); + assert!(error.contains("No supported attestation platform detected")); + assert!(error.contains(&format!("{option} none"))); + for platform in [ + AttestationType::DcapTdx, + AttestationType::GcpTdx, + AttestationType::AzureTdx, + ] { + validate_platform(platform, requested, option).unwrap(); + } + } + } + } + + #[test] + fn explicit_none_is_supported() { + let generator = + build_attestation_generator(Some("none".into()), None, "--client-attestation-type") + .unwrap(); + assert_eq!(generator.attestation_type, AttestationType::None); + } + + #[test] + fn upstream_configuration_errors_propagate() { + for (requested, provider) in [ + (Some("invalid"), None), + (None, Some("http://127.0.0.1:1234")), + (Some("auto"), Some("http://127.0.0.1:1234")), + (Some("dcap-tdx"), Some("not a URL")), + ] { + let requested = requested.map(str::to_owned); + let provider = provider.map(str::to_owned); + let expected = + AttestationGenerator::new_with_detection(requested.clone(), provider.clone()) + .unwrap_err(); + let actual = + build_attestation_generator(requested, provider, "--client-attestation-type") + .unwrap_err(); + assert_eq!(actual.to_string(), expected.to_string()); + } + } +} diff --git a/crates/attested-tls-proxy/src/cli/http.rs b/crates/attested-tls-proxy/src/cli/http.rs index 973d176..00497dc 100644 --- a/crates/attested-tls-proxy/src/cli/http.rs +++ b/crates/attested-tls-proxy/src/cli/http.rs @@ -37,7 +37,8 @@ pub(super) struct ClientArgs { /// Maximum in-flight requests, including streaming responses #[arg(long, default_value = "64", value_parser = parse_max_in_flight_requests)] max_in_flight_requests: NonZeroUsize, - /// Type of attestation to present (dafaults to 'auto' for automatic detection) + /// Local attestation type (defaults to auto; errors if no platform is detected). + /// Set none explicitly to run without local attestation. /// If other than None, a TLS key and certicate must also be given #[arg(long, env = "CLIENT_ATTESTATION_TYPE")] client_attestation_type: Option, @@ -84,6 +85,12 @@ impl ClientArgs { .unwrap_or(&target_addr) .to_string(); + let client_attestation_generator = super::attestation::build_attestation_generator( + client_attestation_type, + dev_dummy_dcap, + "--client-attestation-type", + )?; + if let Some(listen_addr_healthcheck) = listen_addr_healthcheck { health_check::server(listen_addr_healthcheck).await?; } @@ -112,9 +119,6 @@ impl ClientArgs { None => None, }; - let client_attestation_generator = - AttestationGenerator::new_with_detection(client_attestation_type, dev_dummy_dcap)?; - let client_tls_config = attested_tls::tls::client_config( tls_cert_and_chain.as_ref(), remote_tls_cert, @@ -150,7 +154,8 @@ pub(super) struct ServerArgs { listen_addr: SocketAddr, /// The hostname:port or ip:port of the target service to forward traffic to target_addr: String, - /// Type of attestation to present (dafaults to 'auto' for automatic detection) + /// Local attestation type (defaults to auto; errors if no platform is detected). + /// Set none explicitly to run without local attestation. /// If other than None, a TLS key and certicate must also be given #[arg(long, env = "SERVER_ATTESTATION_TYPE")] server_attestation_type: Option, @@ -186,6 +191,12 @@ impl ServerArgs { listen_addr_healthcheck, } = self; + let local_attestation_generator = super::attestation::build_attestation_generator( + server_attestation_type, + dev_dummy_dcap, + "--server-attestation-type", + )?; + if let Some(listen_addr_healthcheck) = listen_addr_healthcheck { health_check::server(listen_addr_healthcheck).await?; } @@ -196,9 +207,6 @@ impl ServerArgs { listen_addr.ip(), )?; - let local_attestation_generator = - AttestationGenerator::new_with_detection(server_attestation_type, dev_dummy_dcap)?; - let server = ProxyServer::new( tls_cert_and_chain, listen_addr, diff --git a/crates/attested-tls-proxy/src/cli/tcp_tunnel.rs b/crates/attested-tls-proxy/src/cli/tcp_tunnel.rs index 13d0d06..60701cf 100644 --- a/crates/attested-tls-proxy/src/cli/tcp_tunnel.rs +++ b/crates/attested-tls-proxy/src/cli/tcp_tunnel.rs @@ -1,10 +1,7 @@ use anyhow::{anyhow, ensure}; use attested_tls::self_signed::generate_self_signed_cert; use attested_tls::tls; -use attested_tls::{ - TlsCertAndKey, - attestation::{AttestationGenerator, AttestationVerifier}, -}; +use attested_tls::{TlsCertAndKey, attestation::AttestationVerifier}; use attested_tls_proxy::tcp_tunnel::{TunnelClient, TunnelOptions, TunnelServer}; use clap::Args; use std::{ @@ -25,7 +22,8 @@ pub(super) struct ClientArgs { limits: Limits, #[command(flatten)] identity: Identity, - /// Local attestation type (defaults to automatic detection) + /// Local attestation type (defaults to auto; errors if no platform is detected). + /// Set none explicitly to run without local attestation. #[arg(long, env = "CLIENT_ATTESTATION_TYPE")] client_attestation_type: Option, /// CA certificate to trust instead of public roots @@ -46,7 +44,8 @@ pub(super) struct ServerArgs { limits: Limits, #[command(flatten)] identity: Identity, - /// Local attestation type (defaults to automatic detection) + /// Local attestation type (defaults to auto; errors if no platform is detected). + /// Set none explicitly to run without local attestation. #[arg(long, env = "SERVER_ATTESTATION_TYPE")] server_attestation_type: Option, /// Require a client TLS certificate authenticated against public roots @@ -127,6 +126,12 @@ impl ClientArgs { allow_self_signed, } = self; + let generator = super::attestation::build_attestation_generator( + client_attestation_type, + identity.dev_dummy_dcap.clone(), + "--client-attestation-type", + )?; + let credentials = identity.load()?; let remote_certificate = tls_ca_certificate .as_deref() @@ -135,10 +140,6 @@ impl ClientArgs { .and_then(|certs| certs.into_iter().next()); let config = tls::client_config(credentials.as_ref(), remote_certificate, allow_self_signed)?; - let generator = AttestationGenerator::new_with_detection( - client_attestation_type, - identity.dev_dummy_dcap, - )?; let client = TunnelClient::new_with_tls_config( listen_addr, target_addr, @@ -167,6 +168,12 @@ impl ServerArgs { client_auth, } = self; + let generator = super::attestation::build_attestation_generator( + server_attestation_type, + identity.dev_dummy_dcap.clone(), + "--server-attestation-type", + )?; + let credentials = match identity.load()? { Some(credentials) => credentials, None => { @@ -174,10 +181,6 @@ impl ServerArgs { generate_self_signed_cert(listen_addr.ip())? } }; - let generator = AttestationGenerator::new_with_detection( - server_attestation_type, - identity.dev_dummy_dcap, - )?; let server = TunnelServer::new( listen_addr, target_addr, diff --git a/docker-compose.yml b/docker-compose.yml index 7913884..a08ef74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: - --listen-addr=0.0.0.0:8443 - --tls-private-key-path=/certs/server.key - --tls-certificate-path=/certs/server.crt + - --server-attestation-type=none - --allowed-remote-attestation-type=none - "172.28.0.10:80" volumes: @@ -34,6 +35,7 @@ services: - --listen-addr=0.0.0.0:8080 - proxy-server:8443 - --tls-ca-certificate=/certs/ca.crt + - --client-attestation-type=none - --allowed-remote-attestation-type=none volumes: - ./certs:/certs:ro