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.
297///
298/// Requests take `&self` and run as independent HTTP/3 streams, so one client (e.g. shared
299/// behind an `Arc`) can carry concurrent requests over the same QUIC connection.
300pub struct TtkClient {
301    endpoint: Endpoint,
302    connection: quinn::Connection,
303    send_request: h3::client::SendRequest<h3_quinn::OpenStreams, axum::body::Bytes>,
304    driver_handle: tokio::task::JoinHandle<Result<(), h3::Error>>,
305    server_addr: SocketAddr,
306    server_name: String,
307    peer_cert: Option<CertificateDer<'static>>,
308}
309
310/// Connecting to the server and issuing requests.
311impl TtkClient {
312    /// Connect to the TTKServer at the specified `server_addr` with the given SNI `server_name`,
313    /// accepting only genuine AWS Nitro attestation (see [`EnclaveCertVerifier::new`]).
314    pub async fn connect(
315        server_addr: SocketAddr,
316        server_name: &str,
317    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
318        Self::connect_with_verifier(server_addr, server_name, EnclaveCertVerifier::new()).await
319    }
320
321    /// Connect to the TTKServer using a custom attestation `verifier` policy.
322    pub async fn connect_with_verifier(
323        server_addr: SocketAddr,
324        server_name: &str,
325        verifier: EnclaveCertVerifier,
326    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
327        // Ensure the default crypto provider is installed
328        let _ = rustls::crypto::ring::default_provider().install_default();
329
330        let cert_verifier = Arc::new(verifier);
331
332        let mut client_crypto = rustls::ClientConfig::builder()
333            .dangerous()
334            .with_custom_certificate_verifier(cert_verifier.clone())
335            .with_no_client_auth();
336
337        // Negotiate HTTP/3 ALPN
338        client_crypto.alpn_protocols = vec![b"h3".to_vec()];
339
340        let quic_client_config = quinn::ClientConfig::new(Arc::new(
341            quinn::crypto::rustls::QuicClientConfig::try_from(client_crypto)?,
342        ));
343
344        // Bind client endpoint to an arbitrary local UDP port
345        let bind_addr: SocketAddr = if server_addr.is_ipv6() {
346            "[::]:0".parse()?
347        } else {
348            "0.0.0.0:0".parse()?
349        };
350        let mut endpoint = Endpoint::client(bind_addr)?;
351        endpoint.set_default_client_config(quic_client_config);
352
353        info!(
354            "Initiating QUIC connection to {} ({})",
355            server_addr, server_name
356        );
357        let connecting = endpoint.connect(server_addr, server_name)?;
358        let connection = connecting.await?;
359        info!("QUIC connection established with {}", server_addr);
360
361        let peer_cert = cert_verifier.received_certificate();
362        if let Some(ref cert) = peer_cert {
363            let hash = Sha256::digest(cert.as_ref());
364            info!(
365                "Server Certificate SHA-256 fingerprint: {}",
366                hex_encode(&hash)
367            );
368        }
369
370        // Establish HTTP/3 on top of the QUIC connection
371        let h3_quic_conn = h3_quinn::Connection::new(connection.clone());
372        let (mut driver, send_request) = h3::client::new(h3_quic_conn).await?;
373
374        // Drive the HTTP/3 connection state machine in the background
375        let driver_handle =
376            tokio::spawn(async move { std::future::poll_fn(|cx| driver.poll_close(cx)).await });
377
378        Ok(Self {
379            endpoint,
380            connection,
381            send_request,
382            driver_handle,
383            server_addr,
384            server_name: server_name.to_string(),
385            peer_cert,
386        })
387    }
388
389    /// Return the server address this client is connected to.
390    pub fn server_addr(&self) -> SocketAddr {
391        self.server_addr
392    }
393
394    /// Return the SNI server name configured for this client.
395    pub fn server_name(&self) -> &str {
396        &self.server_name
397    }
398
399    /// Returns `true` once the QUIC connection has closed (e.g. idle timeout or peer close);
400    /// a closed client can no longer send requests.
401    pub fn is_closed(&self) -> bool {
402        self.connection.close_reason().is_some()
403    }
404
405    /// Retrieve the peer's certificate DER bytes if captured.
406    pub fn peer_cert(&self) -> Option<&CertificateDer<'static>> {
407        self.peer_cert.as_ref()
408    }
409
410    /// Retrieve the SHA-256 digest of the peer certificate.
411    pub fn peer_cert_sha256(&self) -> Option<[u8; 32]> {
412        self.peer_cert.as_ref().map(|c| {
413            let mut arr = [0u8; 32];
414            arr.copy_from_slice(&Sha256::digest(c.as_ref()));
415            arr
416        })
417    }
418
419    /// Retrieve the SHA-256 digest of the peer certificate as a hex string.
420    pub fn peer_cert_sha256_hex(&self) -> Option<String> {
421        self.peer_cert_sha256().map(|h| hex_encode(&h))
422    }
423
424    /// Send an HTTP/3 GET request to the specified path.
425    pub async fn get(
426        &self,
427        path: &str,
428    ) -> Result<ClientResponse, Box<dyn std::error::Error + Send + Sync>> {
429        let uri: Uri = if path.starts_with('/') {
430            format!("https://{}{}", self.server_name, path).parse()?
431        } else {
432            format!("https://{}/{}", self.server_name, path).parse()?
433        };
434
435        let req = Request::builder()
436            .method(Method::GET)
437            .uri(uri)
438            .header("Host", &self.server_name)
439            .header("User-Agent", "TTKClient/0.6.0")
440            .header("Accept", "*/*")
441            .body(())?;
442
443        self.send(req, None).await
444    }
445
446    /// Send an HTTP/3 POST request with the given body to the specified path.
447    pub async fn post(
448        &self,
449        path: &str,
450        body: &[u8],
451    ) -> Result<ClientResponse, Box<dyn std::error::Error + Send + Sync>> {
452        self.post_with_content_type(path, "application/octet-stream", body)
453            .await
454    }
455
456    /// Send an HTTP/3 POST request with `value` serialized as a JSON body to the specified path.
457    pub async fn post_json<T: Serialize + ?Sized>(
458        &self,
459        path: &str,
460        value: &T,
461    ) -> Result<ClientResponse, Box<dyn std::error::Error + Send + Sync>> {
462        let body = serde_json::to_vec(value)?;
463        self.post_with_content_type(path, "application/json", &body)
464            .await
465    }
466
467    /// Send an HTTP/3 POST request with the given body and `Content-Type`.
468    async fn post_with_content_type(
469        &self,
470        path: &str,
471        content_type: &str,
472        body: &[u8],
473    ) -> Result<ClientResponse, Box<dyn std::error::Error + Send + Sync>> {
474        let uri: Uri = if path.starts_with('/') {
475            format!("https://{}{}", self.server_name, path).parse()?
476        } else {
477            format!("https://{}/{}", self.server_name, path).parse()?
478        };
479
480        let req = Request::builder()
481            .method(Method::POST)
482            .uri(uri)
483            .header("Host", &self.server_name)
484            .header("User-Agent", "TTKClient/0.6.0")
485            .header("Content-Type", content_type)
486            .header("Content-Length", body.len().to_string())
487            .body(())?;
488
489        self.send(req, Some(body)).await
490    }
491
492    /// Send an HTTP/3 request with an optional payload and receive the response.
493    pub async fn send(
494        &self,
495        req: Request<()>,
496        payload: Option<&[u8]>,
497    ) -> Result<ClientResponse, Box<dyn std::error::Error + Send + Sync>> {
498        debug!("Sending HTTP/3 request: {} {}", req.method(), req.uri());
499        let mut stream = self.send_request.clone().send_request(req).await?;
500
501        if let Some(data) = payload {
502            if !data.is_empty() {
503                stream
504                    .send_data(axum::body::Bytes::copy_from_slice(data))
505                    .await?;
506            }
507        }
508        stream.finish().await?;
509
510        let response = stream.recv_response().await?;
511        let status = response.status();
512        let headers = response.headers().clone();
513
514        let mut body = Vec::new();
515        while let Some(mut chunk) = stream.recv_data().await? {
516            while chunk.has_remaining() {
517                let slice = chunk.chunk();
518                body.extend_from_slice(slice);
519                let len = slice.len();
520                chunk.advance(len);
521            }
522        }
523
524        Ok(ClientResponse {
525            status,
526            headers,
527            body,
528        })
529    }
530
531    /// Close the client and wait for underlying QUIC streams and driver to settle.
532    pub async fn close(self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
533        drop(self.send_request);
534        // Allow up to 1 second for the driver task to exit gracefully
535        let _ = tokio::time::timeout(Duration::from_secs(1), self.driver_handle).await;
536        self.endpoint.wait_idle().await;
537        Ok(())
538    }
539}
540
541/// Helper function to format bytes as a hex string.
542pub fn hex_encode(bytes: &[u8]) -> String {
543    bytes.iter().map(|b| format!("{:02x}", b)).collect()
544}