Skip to main content

ttk_server/service/
client.rs

1//! TTKServer HTTP/3 (QUIC) Client implementation.
2//!
3//! Connects to TTKServer running inside an AWS Nitro Enclave (or local testing environment)
4//! over QUIC and HTTP/3 (RFC 9114).
5//!
6//! Handles:
7//! - QUIC transport negotiation via `quinn` with ALPN `h3`
8//! - RA-TLS verification of the enclave's ephemeral self-signed certificate: the embedded
9//!   TEE evidence (AWS Nitro, AMD SEV-SNP, Intel TDX or SGX) is verified against the vendor's
10//!   root and must bind to the SHA-256 of the certificate's public key
11//! - Sending HTTP/3 requests and receiving responses using `h3` and `h3-quinn`
12
13pub use crate::verifier::nitro::AttestationDocument;
14use crate::verifier::{self, Policy, TrustStore, VerifiedEvidence};
15use axum::http::{HeaderMap, Method, Request, StatusCode, Uri};
16use bytes::Buf;
17use log::{debug, info};
18use quinn::Endpoint;
19use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
20use rustls::crypto::WebPkiSupportedAlgorithms;
21use rustls::Error as RustlsError;
22use rustls_pki_types::{CertificateDer, ServerName, UnixTime};
23use sha2::{Digest, Sha256};
24use std::collections::BTreeMap;
25use std::net::SocketAddr;
26use std::sync::{Arc, Mutex};
27use std::time::Duration;
28use x509_parser::prelude::*;
29
30/// Custom certificate verifier for Remote Attestation TLS (RA-TLS).
31///
32/// The server presents an ephemeral self-signed certificate that carries an EAT with nested
33/// TEE evidence (AWS Nitro, AMD SEV-SNP, Intel TDX or Intel SGX) in a custom X.509 extension.
34/// Instead of a Web PKI CA chain, [`verify_server_cert`](ServerCertVerifier::verify_server_cert)
35/// checks:
36///
37/// 1. the certificate itself: well-formed, within its validity period, correctly self-signed;
38/// 2. the evidence: vendor signature chain up to a root in the [`TrustStore`] (see
39///    [`ttk_server::verifier`]), and that the TEE is not in debug mode;
40/// 3. the binding: the evidence's report data equals the SHA-256 of the certificate's
41///    SubjectPublicKeyInfo;
42/// 4. any expected measurements configured with
43///    [`with_expected_measurement`](Self::with_expected_measurement) or
44///    [`with_expected_pcr`](Self::with_expected_pcr).
45///
46/// The TLS handshake signature is verified against the same certificate, proving the peer holds
47/// the attested key.
48#[derive(Debug, Clone)]
49pub struct EnclaveCertVerifier {
50    received_cert: Arc<Mutex<Option<CertificateDer<'static>>>>,
51    verified_evidence: Arc<Mutex<Option<VerifiedEvidence>>>,
52    expected_measurements: BTreeMap<String, Vec<u8>>,
53    policy: Policy,
54    trust: Arc<TrustStore>,
55    algorithms: WebPkiSupportedAlgorithms,
56}
57
58/// Construction, policy configuration and inspection of the verifier.
59impl EnclaveCertVerifier {
60    /// Creates a strict verifier: only genuine, vendor-signed evidence from a non-debug TEE is
61    /// accepted, checked against the built-in vendor roots.
62    pub fn new() -> Self {
63        Self {
64            received_cert: Arc::new(Mutex::new(None)),
65            verified_evidence: Arc::new(Mutex::new(None)),
66            expected_measurements: BTreeMap::new(),
67            policy: Policy::default(),
68            trust: Arc::new(TrustStore::builtin()),
69            algorithms: rustls::crypto::ring::default_provider().signature_verification_algorithms,
70        }
71    }
72
73    /// Requires the evidence measurement `name` to equal `value`.
74    ///
75    /// See [`VerifiedEvidence::measurements`] for the names each TEE reports. Evidence that
76    /// lacks the measurement (e.g. from a different TEE) is rejected.
77    pub fn with_expected_measurement(
78        mut self,
79        name: impl Into<String>,
80        value: impl Into<Vec<u8>>,
81    ) -> Self {
82        self.expected_measurements
83            .insert(name.into().to_lowercase(), value.into());
84        self
85    }
86
87    /// Requires PCR `index` of a Nitro attestation document to equal `value`.
88    pub fn with_expected_pcr(self, index: usize, value: impl Into<Vec<u8>>) -> Self {
89        self.with_expected_measurement(format!("pcr{index}"), value)
90    }
91
92    /// Replaces the built-in vendor roots, e.g. for testing or private deployments.
93    pub fn with_trust_store(mut self, trust: TrustStore) -> Self {
94        self.trust = Arc::new(trust);
95        self
96    }
97
98    /// Accepts unsigned mock attestation documents (for local development only).
99    ///
100    /// Skips the Nitro COSE signature and AWS certificate-chain checks; the certificate checks,
101    /// the key binding and the measurement checks still apply. Never enable this in production.
102    pub fn allow_mock(mut self) -> Self {
103        self.policy.allow_mock = true;
104        self
105    }
106
107    /// Accepts evidence from TEEs running in debug mode, whose memory is not confidential.
108    pub fn allow_debug(mut self) -> Self {
109        self.policy.allow_debug = true;
110        self
111    }
112
113    /// Retrieve the server certificate DER bytes captured during a successful verification.
114    pub fn received_certificate(&self) -> Option<CertificateDer<'static>> {
115        self.received_cert
116            .lock()
117            .ok()
118            .and_then(|guard| guard.clone())
119    }
120
121    /// Returns the evidence accepted during the last successful verification.
122    pub fn verified_evidence(&self) -> Option<VerifiedEvidence> {
123        self.verified_evidence
124            .lock()
125            .ok()
126            .and_then(|guard| guard.clone())
127    }
128
129    /// Returns the Nitro attestation document accepted during the last successful verification,
130    /// if the server attested with AWS Nitro.
131    pub fn verified_attestation(&self) -> Option<AttestationDocument> {
132        self.verified_evidence().and_then(|evidence| evidence.nitro)
133    }
134
135    /// Runs all certificate and attestation checks on `end_entity` at time `now`.
136    fn verify(
137        &self,
138        end_entity: &CertificateDer<'_>,
139        now: UnixTime,
140    ) -> Result<VerifiedEvidence, String> {
141        // 1. The certificate itself
142        let (_, cert) = X509Certificate::from_der(end_entity.as_ref())
143            .map_err(|e| format!("malformed certificate: {e}"))?;
144        let now_secs = now.as_secs() as i64;
145        if now_secs < cert.validity().not_before.timestamp() {
146            return Err("certificate is not valid yet".into());
147        }
148        if now_secs > cert.validity().not_after.timestamp() {
149            return Err("certificate has expired".into());
150        }
151        cert.verify_signature(None)
152            .map_err(|e| format!("certificate is not correctly self-signed: {e}"))?;
153
154        // 2 & 3. The embedded evidence, bound to this certificate's public key
155        let eat_bytes = extract_attestation_doc(end_entity.as_ref())
156            .map_err(|e| format!("missing attestation extension: {e}"))?;
157        let binding = Sha256::digest(cert.public_key().raw);
158        let evidence =
159            verifier::verify_evidence(&eat_bytes, &binding, now, &self.trust, self.policy)?;
160
161        // 4. Reference values
162        for (name, expected) in &self.expected_measurements {
163            match evidence.measurements.get(name) {
164                Some(actual) if actual == expected => {}
165                Some(_) => {
166                    return Err(format!(
167                        "{} does not match the expected value",
168                        name.to_uppercase()
169                    ))
170                }
171                None => {
172                    return Err(format!(
173                        "{} evidence has no measurement '{name}'",
174                        evidence.tee
175                    ))
176                }
177            }
178        }
179
180        Ok(evidence)
181    }
182}
183
184/// Default is equivalent to [`EnclaveCertVerifier::new`].
185impl Default for EnclaveCertVerifier {
186    /// Creates a new strict verifier.
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192/// OID of the X.509 extension carrying the attestation document (placeholder, not a registered PEN).
193const ATTESTATION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 99999, 1];
194
195/// RA-TLS verification: the server is trusted because of its attestation, not a CA chain.
196impl ServerCertVerifier for EnclaveCertVerifier {
197    /// Verifies the RA-TLS certificate and its embedded attestation document.
198    ///
199    /// The hostname is not checked: the server's identity is established by its attestation.
200    fn verify_server_cert(
201        &self,
202        end_entity: &CertificateDer<'_>,
203        _intermediates: &[CertificateDer<'_>],
204        _server_name: &ServerName<'_>,
205        _ocsp_response: &[u8],
206        now: UnixTime,
207    ) -> Result<ServerCertVerified, rustls::Error> {
208        let evidence = self
209            .verify(end_entity, now)
210            .map_err(|e| RustlsError::General(format!("RA-TLS verification failed: {e}")))?;
211        debug!("{} attestation verified", evidence.tee);
212
213        if let Ok(mut guard) = self.received_cert.lock() {
214            *guard = Some(end_entity.clone().into_owned());
215        }
216        if let Ok(mut guard) = self.verified_evidence.lock() {
217            *guard = Some(evidence);
218        }
219        Ok(ServerCertVerified::assertion())
220    }
221
222    /// Verifies the TLS 1.2 handshake signature with the server certificate's key.
223    fn verify_tls12_signature(
224        &self,
225        message: &[u8],
226        cert: &CertificateDer<'_>,
227        dss: &rustls::DigitallySignedStruct,
228    ) -> Result<HandshakeSignatureValid, rustls::Error> {
229        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.algorithms)
230    }
231
232    /// Verifies the TLS 1.3 handshake signature with the server certificate's key.
233    fn verify_tls13_signature(
234        &self,
235        message: &[u8],
236        cert: &CertificateDer<'_>,
237        dss: &rustls::DigitallySignedStruct,
238    ) -> Result<HandshakeSignatureValid, rustls::Error> {
239        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.algorithms)
240    }
241
242    /// Lists the signature schemes the verifier accepts.
243    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
244        self.algorithms.supported_schemes()
245    }
246}
247
248/// Extract raw attestation bytes from the leaf certificate
249pub fn extract_attestation_doc(
250    cert_der: &[u8],
251) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
252    // 1. Parse DER bytes into an X509 certificate
253    let (_, cert) = X509Certificate::from_der(cert_der)?;
254
255    // 2. Search for the custom extension by OID
256    for ext in cert.extensions() {
257        if ext
258            .oid
259            .iter()
260            .into_iter()
261            .flatten()
262            .eq(ATTESTATION_OID.iter().copied())
263        {
264            let raw_value = ext.value;
265
266            // 3. Un-wrap ASN.1 OCTET STRING header if present (Tag 0x04)
267            if !raw_value.is_empty() && raw_value[0] == 0x04 {
268                let (_, octet_string) = der_parser::der::parse_der_octetstring(raw_value)?;
269                return Ok(octet_string.as_slice()?.to_vec());
270            }
271
272            return Ok(raw_value.to_vec());
273        }
274    }
275
276    Err("Attestation extension OID not found in certificate".into())
277}
278
279/// Represents the response received from the HTTP/3 server.
280#[derive(Debug, Clone)]
281pub struct ClientResponse {
282    pub status: StatusCode,
283    pub headers: HeaderMap,
284    pub body: Vec<u8>,
285}
286
287/// Accessors for the response body.
288impl ClientResponse {
289    /// Return the response body interpreted as a UTF-8 string.
290    pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
291        String::from_utf8(self.body.clone())
292    }
293}
294
295/// HTTP/3 client for communicating with TTKServer over QUIC.
296pub struct TtkClient {
297    endpoint: Endpoint,
298    send_request: h3::client::SendRequest<h3_quinn::OpenStreams, axum::body::Bytes>,
299    driver_handle: tokio::task::JoinHandle<Result<(), h3::Error>>,
300    server_addr: SocketAddr,
301    server_name: String,
302    peer_cert: Option<CertificateDer<'static>>,
303}
304
305/// Connecting to the server and issuing requests.
306impl TtkClient {
307    /// Connect to the TTKServer at the specified `server_addr` with the given SNI `server_name`,
308    /// accepting only genuine AWS Nitro attestation (see [`EnclaveCertVerifier::new`]).
309    pub async fn connect(
310        server_addr: SocketAddr,
311        server_name: &str,
312    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
313        Self::connect_with_verifier(server_addr, server_name, EnclaveCertVerifier::new()).await
314    }
315
316    /// Connect to the TTKServer using a custom attestation `verifier` policy.
317    pub async fn connect_with_verifier(
318        server_addr: SocketAddr,
319        server_name: &str,
320        verifier: EnclaveCertVerifier,
321    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
322        // Ensure the default crypto provider is installed
323        let _ = rustls::crypto::ring::default_provider().install_default();
324
325        let cert_verifier = Arc::new(verifier);
326
327        let mut client_crypto = rustls::ClientConfig::builder()
328            .dangerous()
329            .with_custom_certificate_verifier(cert_verifier.clone())
330            .with_no_client_auth();
331
332        // Negotiate HTTP/3 ALPN
333        client_crypto.alpn_protocols = vec![b"h3".to_vec()];
334
335        let quic_client_config = quinn::ClientConfig::new(Arc::new(
336            quinn::crypto::rustls::QuicClientConfig::try_from(client_crypto)?,
337        ));
338
339        // Bind client endpoint to an arbitrary local UDP port
340        let bind_addr: SocketAddr = if server_addr.is_ipv6() {
341            "[::]:0".parse()?
342        } else {
343            "0.0.0.0:0".parse()?
344        };
345        let mut endpoint = Endpoint::client(bind_addr)?;
346        endpoint.set_default_client_config(quic_client_config);
347
348        info!(
349            "Initiating QUIC connection to {} ({})",
350            server_addr, server_name
351        );
352        let connecting = endpoint.connect(server_addr, server_name)?;
353        let connection = connecting.await?;
354        info!("QUIC connection established with {}", server_addr);
355
356        let peer_cert = cert_verifier.received_certificate();
357        if let Some(ref cert) = peer_cert {
358            let hash = Sha256::digest(cert.as_ref());
359            info!(
360                "Server Certificate SHA-256 fingerprint: {}",
361                hex_encode(&hash)
362            );
363        }
364
365        // Establish HTTP/3 on top of the QUIC connection
366        let h3_quic_conn = h3_quinn::Connection::new(connection);
367        let (mut driver, send_request) = h3::client::new(h3_quic_conn).await?;
368
369        // Drive the HTTP/3 connection state machine in the background
370        let driver_handle =
371            tokio::spawn(async move { std::future::poll_fn(|cx| driver.poll_close(cx)).await });
372
373        Ok(Self {
374            endpoint,
375            send_request,
376            driver_handle,
377            server_addr,
378            server_name: server_name.to_string(),
379            peer_cert,
380        })
381    }
382
383    /// Return the server address this client is connected to.
384    pub fn server_addr(&self) -> SocketAddr {
385        self.server_addr
386    }
387
388    /// Return the SNI server name configured for this client.
389    pub fn server_name(&self) -> &str {
390        &self.server_name
391    }
392
393    /// Retrieve the peer's certificate DER bytes if captured.
394    pub fn peer_cert(&self) -> Option<&CertificateDer<'static>> {
395        self.peer_cert.as_ref()
396    }
397
398    /// Retrieve the SHA-256 digest of the peer certificate.
399    pub fn peer_cert_sha256(&self) -> Option<[u8; 32]> {
400        self.peer_cert.as_ref().map(|c| {
401            let mut arr = [0u8; 32];
402            arr.copy_from_slice(&Sha256::digest(c.as_ref()));
403            arr
404        })
405    }
406
407    /// Retrieve the SHA-256 digest of the peer certificate as a hex string.
408    pub fn peer_cert_sha256_hex(&self) -> Option<String> {
409        self.peer_cert_sha256().map(|h| hex_encode(&h))
410    }
411
412    /// Send an HTTP/3 GET request to the specified path.
413    pub async fn get(
414        &mut self,
415        path: &str,
416    ) -> Result<ClientResponse, Box<dyn std::error::Error + Send + Sync>> {
417        let uri: Uri = if path.starts_with('/') {
418            format!("https://{}{}", self.server_name, path).parse()?
419        } else {
420            format!("https://{}/{}", self.server_name, path).parse()?
421        };
422
423        let req = Request::builder()
424            .method(Method::GET)
425            .uri(uri)
426            .header("Host", &self.server_name)
427            .header("User-Agent", "TTKClient/0.6.0")
428            .header("Accept", "*/*")
429            .body(())?;
430
431        self.send(req, None).await
432    }
433
434    /// Send an HTTP/3 POST request with the given body to the specified path.
435    pub async fn post(
436        &mut self,
437        path: &str,
438        body: &[u8],
439    ) -> Result<ClientResponse, Box<dyn std::error::Error + Send + Sync>> {
440        let uri: Uri = if path.starts_with('/') {
441            format!("https://{}{}", self.server_name, path).parse()?
442        } else {
443            format!("https://{}/{}", self.server_name, path).parse()?
444        };
445
446        let req = Request::builder()
447            .method(Method::POST)
448            .uri(uri)
449            .header("Host", &self.server_name)
450            .header("User-Agent", "TTKClient/0.6.0")
451            .header("Content-Type", "application/octet-stream")
452            .header("Content-Length", body.len().to_string())
453            .body(())?;
454
455        self.send(req, Some(body)).await
456    }
457
458    /// Send an HTTP/3 request with an optional payload and receive the response.
459    pub async fn send(
460        &mut self,
461        req: Request<()>,
462        payload: Option<&[u8]>,
463    ) -> Result<ClientResponse, Box<dyn std::error::Error + Send + Sync>> {
464        debug!("Sending HTTP/3 request: {} {}", req.method(), req.uri());
465        let mut stream = self.send_request.send_request(req).await?;
466
467        if let Some(data) = payload {
468            if !data.is_empty() {
469                stream
470                    .send_data(axum::body::Bytes::copy_from_slice(data))
471                    .await?;
472            }
473        }
474        stream.finish().await?;
475
476        let response = stream.recv_response().await?;
477        let status = response.status();
478        let headers = response.headers().clone();
479
480        let mut body = Vec::new();
481        while let Some(mut chunk) = stream.recv_data().await? {
482            while chunk.has_remaining() {
483                let slice = chunk.chunk();
484                body.extend_from_slice(slice);
485                let len = slice.len();
486                chunk.advance(len);
487            }
488        }
489
490        Ok(ClientResponse {
491            status,
492            headers,
493            body,
494        })
495    }
496
497    /// Close the client and wait for underlying QUIC streams and driver to settle.
498    pub async fn close(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
499        drop(self.send_request);
500        // Allow up to 1 second for the driver task to exit gracefully
501        let _ = tokio::time::timeout(Duration::from_secs(1), self.driver_handle).await;
502        self.endpoint.wait_idle().await;
503        Ok(())
504    }
505}
506
507/// Helper function to format bytes as a hex string.
508pub fn hex_encode(bytes: &[u8]) -> String {
509    bytes.iter().map(|b| format!("{:02x}", b)).collect()
510}
511
512/// Usage text of the test-only `client` binary (`test-client` feature).
513pub const CLIENT_USAGE: &str = "\
514Usage: client [OPTIONS] [URL]
515
516Options:
517  -s, --server-name <NAME>  SNI server name (default: localhost)
518  -p, --path <PATH>         Request path (default: /)
519  -a, --addr <ADDR>         Server socket address (default: 127.0.0.1:4433)
520  -h, --help                Print help information
521
522Examples:
523  client
524  client https://127.0.0.1:4433/hello
525  client --addr 127.0.0.1:4433 --server-name enclave.local --path /evidence
526";
527
528/// What a `client` invocation should connect to and request.
529#[derive(Debug, Clone, PartialEq, Eq)]
530pub struct ClientTarget {
531    /// Server socket address.
532    pub server_addr: SocketAddr,
533    /// SNI server name.
534    pub server_name: String,
535    /// Request path, including any query string.
536    pub path: String,
537}
538
539/// Parses `client` arguments (without the program name).
540///
541/// `default_addr` and `default_name` come from `TTK_SERVER_ADDR` and `TTK_SERVER_NAME` in the
542/// binary; they fall back to `127.0.0.1:4433` and `localhost`. An unparsable address falls back
543/// to `127.0.0.1:4433`. Returns `None` if help was requested.
544pub fn parse_client_args(
545    args: &[String],
546    default_addr: Option<String>,
547    default_name: Option<String>,
548) -> Option<ClientTarget> {
549    let mut server_addr_str = default_addr.unwrap_or_else(|| "127.0.0.1:4433".to_string());
550    let mut server_name = default_name.unwrap_or_else(|| "localhost".to_string());
551    let mut path = "/".to_string();
552
553    let mut i = 0;
554    while i < args.len() {
555        let arg = &args[i];
556        if arg == "--help" || arg == "-h" {
557            return None;
558        } else if (arg == "--server-name" || arg == "-s") && i + 1 < args.len() {
559            i += 1;
560            server_name = args[i].clone();
561        } else if (arg == "--path" || arg == "-p") && i + 1 < args.len() {
562            i += 1;
563            path = args[i].clone();
564        } else if (arg == "--addr" || arg == "-a") && i + 1 < args.len() {
565            i += 1;
566            server_addr_str = args[i].clone();
567        } else if !arg.starts_with('-') {
568            // Positional URL or address
569            if let Ok(uri) = arg.parse::<Uri>() {
570                if let Some(host) = uri.host() {
571                    let port = uri.port_u16().unwrap_or(4433);
572                    server_addr_str = format!("{}:{}", host, port);
573                    if host != "127.0.0.1" && host != "0.0.0.0" {
574                        server_name = host.to_string();
575                    }
576                }
577                if !uri.path().is_empty() {
578                    path = uri.path().to_string();
579                    if let Some(query) = uri.query() {
580                        path.push('?');
581                        path.push_str(query);
582                    }
583                }
584            } else {
585                server_addr_str = arg.clone();
586            }
587        }
588        i += 1;
589    }
590
591    let server_addr: SocketAddr = server_addr_str
592        .parse()
593        .unwrap_or_else(|_| "127.0.0.1:4433".parse().unwrap());
594
595    Some(ClientTarget {
596        server_addr,
597        server_name,
598        path,
599    })
600}