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