Skip to main content

ttk_server/service/
server.rs

1//! RATS (RFC 9334) role mapping for this server:
2//!
3//! - **Attester**: this process, running inside a Nitro Enclave. It holds a
4//!   hardware-rooted identity via the Nitro Security Module (NSM).
5//! - **Evidence**: the NSM Attestation Document produced by [`attestation::detect`],
6//!   with `user_data` bound to the SHA-256 hash of the ephemeral TLS key's
7//!   SubjectPublicKeyInfo so a Relying Party can tie the Evidence to the TLS session.
8//! - **Endorsements**: the AWS Nitro certificate chain embedded in the Attestation
9//!   Document, rooted at the AWS Nitro Enclaves root certificate.
10//! - **Verifier** / **Relying Party**: the external client fetching Evidence over
11//!   `/evidence` (RATS-standard name; `/attestation` kept as an alias), or as an
12//!   RFC 9711 EAT over `/evidence.eat`. Appraisal against Reference Values (expected
13//!   PCR measurements) and issuance of an Attestation Result happen outside this
14//!   server.
15
16use crate::{attestation, AttestationParams};
17use axum::{routing::get, Router};
18use base64::{engine::general_purpose::STANDARD, Engine as _};
19use log::info;
20use quinn::{Endpoint, ServerConfig};
21use rcgen::{CertificateParams, CustomExtension, KeyPair, SanType};
22use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
23use std::net::SocketAddr;
24use std::sync::Arc;
25use time::{Duration, OffsetDateTime};
26use tower_service::Service;
27
28/// OID of the X.509 extension carrying the attestation document (placeholder, not a registered PEN).
29const ATTESTATION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 99999, 1];
30
31/// Evidence for this instance, in the formats served over HTTP.
32#[derive(Clone, Debug)]
33pub struct Evidence {
34    /// Raw NSM Attestation Document (COSE_Sign1).
35    pub nitro: Vec<u8>,
36    /// The same document, wrapped as an RFC 9711 EAT claims-set.
37    pub eat: Vec<u8>,
38}
39
40/// Creates a self-signed certificate for `key_pair` with `attestation_doc` embedded as a
41/// non-critical X.509 extension, and returns it PEM-encoded.
42pub fn create_cert_with_attestation(
43    key_pair: &KeyPair,
44    common_name: &str,
45    attestation_doc: &[u8], // <--- Attestation document passed as a parameter
46    validity_days: i64,
47) -> Result<String, Box<dyn std::error::Error>> {
48    let mut params = CertificateParams::default();
49
50    // Set Subject Name
51    // let mut dn = DistinguishName::new();
52    // dn.push(DnType::CommonName, common_name);
53    // params.distinguished_name = dn;
54
55    // Set Validity Period
56    let now = OffsetDateTime::now_utc();
57    params.not_before = now;
58    params.not_after = now + Duration::days(validity_days);
59
60    // Subject Alternative Name
61    params.subject_alt_names = vec![SanType::DnsName(common_name.try_into()?)];
62
63    // --------------------------------------------------------------------
64    // WRAP AND ATTACH ATTESTATION DOCUMENT AS X.509 EXTENSION
65    // --------------------------------------------------------------------
66    // Wrap the raw attestation bytes into an ASN.1 OCTET STRING header
67    let der_encoded_payload = wrap_in_asn1_octet_string(attestation_doc);
68
69    let mut attestation_ext =
70        CustomExtension::from_oid_content(ATTESTATION_OID, der_encoded_payload);
71
72    // Set to false unless you want parsers to fail if they don't recognize the OID
73    attestation_ext.set_criticality(false);
74
75    params.custom_extensions.push(attestation_ext);
76
77    // Sign the certificate
78    let cert = params.self_signed(key_pair)?;
79    Ok(cert.pem())
80}
81
82/// Helper to wrap raw binary bytes in an ASN.1 OCTET STRING TLV header
83fn wrap_in_asn1_octet_string(data: &[u8]) -> Vec<u8> {
84    let mut encoded = Vec::new();
85    encoded.push(0x04); // ASN.1 Tag for OCTET STRING
86
87    let len = data.len();
88    if len < 128 {
89        encoded.push(len as u8);
90    } else if len <= 0xFF {
91        encoded.push(0x81);
92        encoded.push(len as u8);
93    } else if len <= 0xFFFF {
94        encoded.push(0x82);
95        encoded.extend_from_slice(&(len as u16).to_be_bytes());
96    } else {
97        encoded.push(0x84);
98        encoded.extend_from_slice(&(len as u32).to_be_bytes());
99    }
100
101    encoded.extend_from_slice(data);
102    encoded
103}
104
105/// Boxed error type used by the server functions.
106type BoxError = Box<dyn std::error::Error>;
107
108/// Address the QUIC endpoint binds to.
109const LISTEN_ADDR: &str = "0.0.0.0:4433";
110
111/// Runs the server: attests, builds the RA-TLS identity, then serves HTTP/3 on
112/// `0.0.0.0:4433` until the endpoint closes.
113pub async fn run() -> Result<(), BoxError> {
114    let server = Server::bind(LISTEN_ADDR.parse()?)?;
115    info!("Server listening on {} (QUIC/HTTP/3)", server.local_addr()?);
116    server.serve().await;
117    Ok(())
118}
119
120/// An attested HTTP/3 server bound to a QUIC endpoint.
121pub struct Server {
122    endpoint: Endpoint,
123    app: Router,
124}
125
126/// Setup and serving.
127impl Server {
128    /// Attests, builds the RA-TLS identity and binds the QUIC endpoint to `addr`.
129    ///
130    /// Must be called within a Tokio runtime. Binding port 0 picks a free port; see
131    /// [`local_addr`](Self::local_addr).
132    pub fn bind(addr: SocketAddr) -> Result<Self, BoxError> {
133        info!("Initializing Nitro Enclave HTTP/3 Server...");
134
135        // Install the default cryptographic provider for rustls 0.23
136        let _ = rustls::crypto::ring::default_provider().install_default();
137
138        let key_pair = KeyPair::generate()?;
139        info!("Generated ephemeral TLS certificate.");
140
141        let eat_bytes = generate_evidence(&key_pair)?;
142        let tls_config = build_tls_config(&key_pair, &eat_bytes)?;
143        let app = build_router(Arc::new(Evidence {
144            nitro: eat_bytes.clone(),
145            eat: eat_bytes,
146        }));
147
148        let quic_config = ServerConfig::with_crypto(Arc::new(
149            quinn::crypto::rustls::QuicServerConfig::try_from(tls_config)?,
150        ));
151        let endpoint = Endpoint::server(quic_config, addr)?;
152        Ok(Self { endpoint, app })
153    }
154
155    /// Returns the address the endpoint is bound to.
156    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
157        self.endpoint.local_addr()
158    }
159
160    /// Accepts QUIC connections and serves HTTP/3 until the endpoint closes.
161    pub async fn serve(self) {
162        while let Some(incoming) = self.endpoint.accept().await {
163            tokio::spawn(handle_connection(incoming, self.app.clone()));
164        }
165    }
166}
167
168/// Requests evidence from the detected attestation provider, bound to the TLS public key,
169/// and returns it as CBOR-encoded RFC 9711 EAT bytes.
170fn generate_evidence(key_pair: &KeyPair) -> Result<Vec<u8>, BoxError> {
171    let params = AttestationParams::new().with_user_data_hash(&key_pair.public_key_der());
172
173    let provider = attestation::detect()?;
174    info!("Using attestation provider: {}", provider.name());
175    let eat_bytes = provider.generate_document(&params)?.to_cbor_bytes()?;
176    info!(
177        "Wrapped Attestation Document as RFC 9711 EAT token ({} bytes).",
178        eat_bytes.len()
179    );
180    Ok(eat_bytes)
181}
182
183/// Builds the rustls config using a self-signed RA-TLS certificate carrying `eat_bytes`.
184fn build_tls_config(
185    key_pair: &KeyPair,
186    eat_bytes: &[u8],
187) -> Result<rustls::ServerConfig, BoxError> {
188    let cert_pem = create_cert_with_attestation(key_pair, "enclave.internal", eat_bytes, 30)?;
189    let key_pem = key_pair.serialize_pem();
190
191    let certs =
192        CertificateDer::pem_slice_iter(cert_pem.as_bytes()).collect::<Result<Vec<_>, _>>()?;
193    let key = PrivateKeyDer::from_pem_slice(key_pem.as_bytes())?;
194
195    let mut config = rustls::ServerConfig::builder()
196        .with_no_client_auth()
197        .with_single_cert(certs, key)?;
198    // Enable ALPN for HTTP/3 ("h3")
199    config.alpn_protocols = vec![b"h3".to_vec()];
200    Ok(config)
201}
202
203/// Builds the Axum router serving the evidence endpoints.
204fn build_router(evidence: Arc<Evidence>) -> Router {
205    let nitro_b64 = STANDARD.encode(&evidence.nitro);
206    let eat_b64 = STANDARD.encode(&evidence.eat);
207
208    let text = |body: String| get(move || async move { body });
209
210    Router::new()
211        .route("/", get(|| async { "Hello from Enclave over HTTP/3!" }))
212        .route("/hello", get(|| async { "Hello from inside the Enclave!" }))
213        .route("/evidence", text(nitro_b64.clone()))
214        .route("/attestation", text(nitro_b64))
215        .route("/evidence.eat", text(eat_b64))
216}
217
218/// Drives a single QUIC connection, dispatching each HTTP/3 request to `app`.
219async fn handle_connection(incoming: quinn::Incoming, app: Router) {
220    let conn = match incoming.await {
221        Ok(conn) => conn,
222        Err(err) => return eprintln!("Handshake failed: {err}"),
223    };
224
225    let mut h3_conn =
226        match h3::server::Connection::<_, axum::body::Bytes>::new(h3_quinn::Connection::new(conn))
227            .await
228        {
229            Ok(h3) => h3,
230            Err(e) => return eprintln!("H3 setup failed: {e}"),
231        };
232
233    while let Ok(Some((req, stream))) = h3_conn.accept().await {
234        let app = app.clone();
235        tokio::spawn(async move {
236            let req = req.map(|_| axum::body::Body::empty());
237            respond(app, req, stream).await;
238        });
239    }
240}
241
242/// Runs `req` through `app` and streams the response back over the HTTP/3 `stream`.
243async fn respond(
244    mut app: Router,
245    req: axum::http::Request<axum::body::Body>,
246    mut stream: h3::server::RequestStream<
247        h3_quinn::BidiStream<axum::body::Bytes>,
248        axum::body::Bytes,
249    >,
250) {
251    let response = match app.call(req).await {
252        Ok(response) => response,
253        Err(e) => return eprintln!("App call error: {e}"),
254    };
255
256    let (parts, body) = response.into_parts();
257    if let Err(e) = stream
258        .send_response(axum::http::Response::from_parts(parts, ()))
259        .await
260    {
261        return eprintln!("Failed to send response headers: {e}");
262    }
263    match axum::body::to_bytes(body, usize::MAX).await {
264        Ok(bytes) if !bytes.is_empty() => {
265            if let Err(e) = stream.send_data(bytes).await {
266                return eprintln!("Failed to send response body: {e}");
267            }
268        }
269        Ok(_) => {}
270        Err(e) => eprintln!("Failed to read response body: {e}"),
271    }
272    if let Err(e) = stream.finish().await {
273        eprintln!("Failed to finish stream: {e}");
274    }
275}