nixfleet_control_plane/polling/
signed_fetch.rs

1//! Shared HTTP fetch + Bearer-token primitive; verification stays per-task.
2
3use std::path::Path;
4use std::time::Duration;
5
6use anyhow::{Context, Result};
7use chrono::{DateTime, Utc};
8use nixfleet_proto::TrustedPubkey;
9
10/// 15s timeout.
11pub fn build_client() -> reqwest::Client {
12    reqwest::Client::builder()
13        .use_rustls_tls()
14        .timeout(Duration::from_secs(15))
15        .build()
16        .expect("build signed-fetch client (rustls + 15s timeout)")
17}
18
19/// Re-read each call so trust.json rotation propagates without restart.
20pub fn read_trust_config(path: &Path) -> Result<nixfleet_proto::TrustConfig> {
21    let raw = std::fs::read_to_string(path)
22        .with_context(|| format!("read trust file {}", path.display()))?;
23    serde_json::from_str(&raw).context("parse trust file")
24}
25
26/// Re-read each call so trust.json rotation propagates without restart.
27/// `now` lets the verifier accept successor keys during the rotation
28/// overlap window (`now < retire_at`).
29pub fn read_trust_roots(
30    path: &Path,
31    now: DateTime<Utc>,
32) -> Result<(Vec<TrustedPubkey>, Option<DateTime<Utc>>)> {
33    let trust = read_trust_config(path)?;
34    Ok((
35        trust.ci_release_key.active_keys_at(now),
36        trust.ci_release_key.reject_before,
37    ))
38}
39
40/// Re-read each poll so token rotation propagates; `None` skips auth.
41pub fn read_token(path: Option<&Path>) -> Result<Option<String>> {
42    match path {
43        Some(p) => Ok(Some(
44            std::fs::read_to_string(p)
45                .with_context(|| format!("read token file {}", p.display()))?
46                .trim()
47                .to_string(),
48        )),
49        None => Ok(None),
50    }
51}
52
53/// Non-2xx or network error -> `Err`; caller retains previous state.
54pub async fn fetch_signed_pair(
55    client: &reqwest::Client,
56    artifact_url: &str,
57    signature_url: &str,
58    token: Option<&str>,
59) -> Result<(Vec<u8>, Vec<u8>)> {
60    let artifact = fetch_url(client, artifact_url, token).await?;
61    let signature = fetch_url(client, signature_url, token).await?;
62    Ok((artifact, signature))
63}
64
65async fn fetch_url(client: &reqwest::Client, url: &str, token: Option<&str>) -> Result<Vec<u8>> {
66    let mut req = client.get(url);
67    if let Some(t) = token {
68        req = req.header("Authorization", format!("Bearer {t}"));
69    }
70    let resp = req.send().await.with_context(|| format!("GET {url}"))?;
71    if !resp.status().is_success() {
72        let status = resp.status();
73        let body = resp.text().await.unwrap_or_default();
74        anyhow::bail!("{url}: {status}: {body}");
75    }
76    let bytes = resp
77        .bytes()
78        .await
79        .with_context(|| format!("read body {url}"))?;
80    Ok(bytes.to_vec())
81}