Skip to main content

ttk_server/service/
router.rs

1//! HTTP routes of the RA-TLS server, served over HTTP/3 by [`super::server`]:
2//!
3//! | Route               | Response                                                        |
4//! |---------------------|-----------------------------------------------------------------|
5//! | `GET /`             | Greeting text                                                   |
6//! | `GET /evidence.eat` | Base64-encoded EAT carrying this server's [`Evidence`]          |
7//! | `POST /faf`         | Forwards a [`FafRequest`] to its relay; see [`FafRequest`]      |
8//!
9//! When forwarding a [`FafRequest`], this server is itself a RATS (RFC 9334) Relying Party: it
10//! only sends the message and key to a relay whose RA-TLS attestation verifies.
11
12use crate::client::{EnclaveCertVerifier, TtkClient};
13use axum::extract::State;
14use axum::http::StatusCode;
15use axum::routing::{get, post};
16use axum::{Json, Router};
17use base64::{engine::general_purpose::STANDARD, Engine as _};
18use log::{info, warn};
19use serde::{Deserialize, Serialize};
20use std::sync::Arc;
21
22/// Boxed error type that can cross task boundaries.
23type SendError = Box<dyn std::error::Error + Send + Sync>;
24
25/// Evidence for this instance, in the formats served over HTTP.
26#[derive(Clone, Debug)]
27pub struct Evidence {
28    /// Raw NSM Attestation Document (COSE_Sign1).
29    pub nitro: Vec<u8>,
30    /// The same document, wrapped as an RFC 9711 EAT claims-set.
31    pub eat: Vec<u8>,
32}
33
34/// Port assumed for a `relay_server` that does not name one.
35pub const DEFAULT_RELAY_PORT: u16 = 4433;
36
37/// Time allowed for forwarding a `/faf` request to its relay, RA-TLS handshake included.
38pub const RELAY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
39
40/// Time allowed for the RA-TLS handshake with each resolved address of a relay.
41pub const RELAY_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
42
43/// Environment variable that makes [`run`](super::server::run) accept mock attestation from relay servers.
44pub const ALLOW_MOCK_RELAY_ENV: &str = "TTK_ALLOW_MOCK_ATTESTATION";
45
46/// Builds the verifier used to attest a relay server. Called once per forwarded request, so
47/// each connection gets its own verifier state.
48pub type RelayVerifierFactory = Arc<dyn Fn() -> EnclaveCertVerifier + Send + Sync>;
49
50/// Body of `POST /faf`: a message and key to hand to `relay_server`.
51///
52/// The relay runs this same server. The message and key are forwarded to its `POST /faf`
53/// without `relay_server`, which tells the relay it is the last hop: it accepts the request and
54/// answers `200 OK`. This server answers `200 OK` once the relay has.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct FafRequest {
57    /// Relay to forward to, as `host[:port]` or `https://host[:port]` (default port
58    /// [`DEFAULT_RELAY_PORT`]). Absent when this server is the last hop.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub relay_server: Option<String>,
61    /// The message to transmit.
62    pub message: String,
63    /// The key to transmit alongside the message.
64    pub key: String,
65}
66
67/// Builds the Axum router: the greeting, the evidence endpoint and the `/faf` relay.
68pub(crate) fn build_router(
69    evidence: Arc<Evidence>,
70    relay_verifier: RelayVerifierFactory,
71) -> Router {
72    let eat_b64 = STANDARD.encode(&evidence.eat);
73
74    Router::new()
75        .route("/", get(|| async { "Hello from Enclave over HTTP/3!" }))
76        .route("/evidence.eat", get(move || async move { eat_b64 }))
77        .route("/faf", post(faf))
78        .with_state(relay_verifier)
79}
80
81/// `POST /faf`: forwards the message and key to the relay and answers `200 OK` once the relay
82/// has; with no `relay_server`, this server is the last hop and accepts the request directly.
83///
84/// Answers `400` for an unparsable `relay_server`, `502` if the relay can't be reached, fails
85/// attestation or answers anything but `200`, and `504` if it doesn't answer within
86/// [`RELAY_TIMEOUT`].
87async fn faf(
88    State(relay_verifier): State<RelayVerifierFactory>,
89    Json(request): Json<FafRequest>,
90) -> (StatusCode, String) {
91    let FafRequest {
92        relay_server,
93        message,
94        key,
95    } = request;
96
97    let Some(relay_server) = relay_server else {
98        // Never log the key or the message itself.
99        info!(
100            "/faf: accepted a {}-byte message as the last hop",
101            message.len()
102        );
103        return (StatusCode::OK, "delivered".to_string());
104    };
105
106    let (host, port) = match parse_relay_server(&relay_server) {
107        Ok(target) => target,
108        Err(e) => {
109            return (
110                StatusCode::BAD_REQUEST,
111                format!("invalid relay_server: {e}"),
112            )
113        }
114    };
115    let forward = FafRequest {
116        relay_server: None,
117        message,
118        key,
119    };
120
121    let relayed = tokio::time::timeout(
122        RELAY_TIMEOUT,
123        forward_to_relay(&host, port, relay_verifier(), &forward),
124    )
125    .await;
126    match relayed {
127        Ok(Ok(StatusCode::OK)) => {
128            info!("/faf: relayed to {host}:{port}");
129            (StatusCode::OK, "relayed".to_string())
130        }
131        Ok(Ok(status)) => {
132            warn!("/faf: relay {host}:{port} answered {status}");
133            (StatusCode::BAD_GATEWAY, format!("relay answered {status}"))
134        }
135        Ok(Err(e)) => {
136            warn!("/faf: relaying to {host}:{port} failed: {e}");
137            (StatusCode::BAD_GATEWAY, format!("relay failed: {e}"))
138        }
139        Err(_) => {
140            warn!("/faf: relay {host}:{port} timed out");
141            (StatusCode::GATEWAY_TIMEOUT, "relay timed out".to_string())
142        }
143    }
144}
145
146/// Splits a `relay_server` value (`host[:port]` or `https://host[:port][/...]`) into its host
147/// (without IPv6 brackets) and port.
148pub fn parse_relay_server(relay_server: &str) -> Result<(String, u16), String> {
149    let uri: axum::http::Uri = relay_server
150        .trim()
151        .parse()
152        .map_err(|e| format!("{relay_server:?}: {e}"))?;
153    if let Some(scheme) = uri.scheme_str() {
154        if scheme != "https" {
155            return Err(format!(
156                "unsupported scheme {scheme:?}; the relay speaks HTTP/3"
157            ));
158        }
159    }
160    let authority = uri
161        .authority()
162        .ok_or_else(|| format!("{relay_server:?} has no host"))?;
163    if authority.as_str().contains('@') {
164        return Err("user info is not allowed".to_string());
165    }
166    let host = authority
167        .host()
168        .trim_start_matches('[')
169        .trim_end_matches(']');
170    if host.is_empty() {
171        return Err(format!("{relay_server:?} has no host"));
172    }
173    let port = authority.port_u16().unwrap_or(DEFAULT_RELAY_PORT);
174    Ok((host.to_string(), port))
175}
176
177/// Resolves `host` and connects to the first of its addresses that completes an RA-TLS
178/// handshake within [`RELAY_CONNECT_TIMEOUT`], e.g. falling back from `::1` to `127.0.0.1`
179/// for `localhost`.
180async fn connect_to_relay(
181    host: &str,
182    port: u16,
183    verifier: EnclaveCertVerifier,
184) -> Result<TtkClient, SendError> {
185    let mut last_error: SendError = format!("{host} did not resolve").into();
186    for addr in tokio::net::lookup_host((host, port)).await? {
187        let connecting = TtkClient::connect_with_verifier(addr, host, verifier.clone());
188        match tokio::time::timeout(RELAY_CONNECT_TIMEOUT, connecting).await {
189            Ok(Ok(client)) => return Ok(client),
190            Ok(Err(e)) => last_error = e,
191            Err(_) => last_error = format!("connecting to {addr} timed out").into(),
192        }
193    }
194    Err(last_error)
195}
196
197/// Resolves the relay, connects over RA-TLS (verified by `verifier`), posts `request` to its
198/// `/faf` and returns the relay's status.
199async fn forward_to_relay(
200    host: &str,
201    port: u16,
202    verifier: EnclaveCertVerifier,
203    request: &FafRequest,
204) -> Result<StatusCode, SendError> {
205    let mut client = connect_to_relay(host, port, verifier).await?;
206    let response = client.post_json("/faf", request).await;
207    let _ = client.close().await;
208    Ok(response?.status)
209}