nixfleet_control_plane/server/routes/
fleet.rs

1//! Stateless distributor for the signed `fleet.resolved.json` artifact.
2//!
3//! Serves the canonical signed bytes from `state.verified_fleet` (the
4//! `cp_manifest_poll` worker's in-memory snapshot). The bytes returned
5//! are exactly the bytes CP fetched from the channel-refs source and
6//! verified against the trust roots; signature verification + the
7//! rollout-anchored `fleet_resolved_hash` discriminator happen at the
8//! consumer (per RFC-0004 §1 invariant #1 — single signed source of
9//! truth + defense-in-depth at the verification gate).
10
11use std::sync::Arc;
12
13use axum::body::Bytes;
14use axum::extract::State;
15use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
16use axum::response::IntoResponse;
17
18use super::super::state::AppState;
19
20/// `GET /v1/fleet.resolved` - canonical signed bytes; mTLS via the
21/// router-level `require_cn_layer`.
22pub(in crate::server) async fn artifact(
23    State(state): State<Arc<AppState>>,
24) -> Result<impl IntoResponse, StatusCode> {
25    let snapshot_guard = state.verified_fleet.read().await;
26    let snapshot = snapshot_guard
27        .as_ref()
28        .ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
29    let bytes = Bytes::copy_from_slice(&snapshot.artifact_bytes);
30    let mut headers = HeaderMap::new();
31    headers.insert(
32        header::CONTENT_TYPE,
33        HeaderValue::from_static("application/json"),
34    );
35    Ok((StatusCode::OK, headers, bytes))
36}
37
38/// `GET /v1/fleet.resolved/sig` - raw signature bytes.
39pub(in crate::server) async fn signature(
40    State(state): State<Arc<AppState>>,
41) -> Result<impl IntoResponse, StatusCode> {
42    let snapshot_guard = state.verified_fleet.read().await;
43    let snapshot = snapshot_guard
44        .as_ref()
45        .ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
46    let bytes = Bytes::copy_from_slice(&snapshot.signature_bytes);
47    let mut headers = HeaderMap::new();
48    headers.insert(
49        header::CONTENT_TYPE,
50        HeaderValue::from_static("application/octet-stream"),
51    );
52    Ok((StatusCode::OK, headers, bytes))
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use crate::server::state::VerifiedFleetSnapshot;
59    use nixfleet_proto::FleetResolved;
60    use std::sync::Arc;
61
62    fn state_with_snapshot(snapshot: Option<VerifiedFleetSnapshot>) -> Arc<AppState> {
63        let state = AppState {
64            verified_fleet: Arc::new(tokio::sync::RwLock::new(snapshot)),
65            ..Default::default()
66        };
67        Arc::new(state)
68    }
69
70    fn sample_snapshot(artifact: &[u8], signature: &[u8]) -> VerifiedFleetSnapshot {
71        // Minimal valid FleetResolved; the route tests only exercise the
72        // bytes path so the fleet's content is opaque here.
73        let fleet: FleetResolved = serde_json::from_str(
74            r#"{"schemaVersion":1,"hosts":{},"channels":{},"waves":{},"meta":{"schemaVersion":1}}"#,
75        )
76        .expect("minimal FleetResolved JSON valid");
77        VerifiedFleetSnapshot {
78            fleet: Arc::new(fleet),
79            fleet_resolved_hash: "0".repeat(64),
80            artifact_bytes: artifact.to_vec(),
81            signature_bytes: signature.to_vec(),
82        }
83    }
84
85    #[tokio::test]
86    async fn artifact_returns_503_when_verified_fleet_unset() {
87        let state = state_with_snapshot(None);
88        match artifact(State(state)).await {
89            Err(status) => assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE),
90            Ok(_) => panic!("expected SERVICE_UNAVAILABLE when verified_fleet is None"),
91        }
92    }
93
94    #[tokio::test]
95    async fn signature_returns_503_when_verified_fleet_unset() {
96        let state = state_with_snapshot(None);
97        match signature(State(state)).await {
98            Err(status) => assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE),
99            Ok(_) => panic!("expected SERVICE_UNAVAILABLE when verified_fleet is None"),
100        }
101    }
102
103    #[tokio::test]
104    async fn artifact_returns_snapshot_bytes_on_200() {
105        let snap = sample_snapshot(br#"{"hello":"fleet"}"#, &[0xDE, 0xAD]);
106        let state = state_with_snapshot(Some(snap));
107        let resp = artifact(State(state))
108            .await
109            .expect("artifact returns Ok")
110            .into_response();
111        assert_eq!(resp.status(), StatusCode::OK);
112        let body = axum::body::to_bytes(resp.into_body(), 4096).await.unwrap();
113        assert_eq!(&body[..], br#"{"hello":"fleet"}"#);
114    }
115
116    #[tokio::test]
117    async fn signature_returns_snapshot_bytes_on_200() {
118        let snap = sample_snapshot(b"{}", &[0xDE, 0xAD, 0xBE, 0xEF]);
119        let state = state_with_snapshot(Some(snap));
120        let resp = signature(State(state))
121            .await
122            .expect("signature returns Ok")
123            .into_response();
124        assert_eq!(resp.status(), StatusCode::OK);
125        let body = axum::body::to_bytes(resp.into_body(), 4096).await.unwrap();
126        assert_eq!(&body[..], &[0xDE, 0xAD, 0xBE, 0xEF]);
127    }
128}