crates/gunnar-front/src/downloads.rs
//! **The `gunnar` CLI, downloadable from the estate's own site** (E4, owner
//! 2026-10-11): every encrypted-repository flow and the browser decrypt need
//! the CLI, so every Gunnar site serves it — gunnar.rs, test2, every sold
//! `<company>.gunnar.rs` — from the SAME release build that made the estate.
//!
//! **Where the bytes come from.** The Build Thing's `gunnar-musl` row builds the
//! client-only `gunnar` for `x86_64-unknown-linux-musl` and declares `sign`, so
//! the forge's signing step writes `gunnar.p7s` (a detached CMS over the
//! BLAKE3 digest, under the plugin-signing certificate issued by the owner's
//! root key) and `gunnar.blake3` beside it. `build.rs` embeds the three files
//! and the published trust root (`korp-root-ca.der`) into this binary — the
//! front medium stays ONE file — and this module serves them:
//!
//! | path | what |
//! |---|---|
//! | `/download/` | a 303 to the console's page (`/cli`), which renders the manifest |
//! | `/download/index.json` | the [`Manifest`]: sizes, sha256, blake3, the verify line, `cargo install` |
//! | `/download/<file>` | the binary, `<file>.p7s`, `<file>.blake3`, `<file>.sha256`, the root |
//!
//! **Only a SIGNED binary is offered**: a binary the build carries without its
//! `.p7s` is left out, and a medium without one says so by name in the
//! manifest (`absent`) instead of serving an unsigned file.
use std::sync::Arc;
use axum::extract::Path as UrlPath;
use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use axum::routing::get;
use axum::Router;
use serde::Serialize;
mod embedded {
include!(concat!(env!("OUT_DIR"), "/cli_downloads.rs"));
}
pub use embedded::{CLI_SOURCE, FILES};
/// The path every download sits under.
pub const PREFIX: &str = "/download";
/// The console's page that renders the manifest (gunnar-ui `/cli`).
pub const PAGE: &str = "/cli";
/// The published trust root's file name (dwarves `trust/korp-root-ca.der`).
pub const ROOT_CA: &str = "korp-root-ca.der";
/// **The Gunnar mark, at a stable public path on every site** — what the crates'
/// READMEs (crates.io) and `#![doc(html_logo_url, html_favicon_url)]` (docs.rs)
/// point at, absolute: `https://gunnar.rs/download/gunnar.webp`. Copies of the
/// console's own (gunnar-ui-web `assets/gunnar_light.webp`, `styles/favicon.svg`).
pub const BRAND: &[(&str, &[u8], &str)] = &[
(
"gunnar.webp",
include_bytes!("../assets/gunnar.webp"),
"image/webp",
),
(
"favicon.svg",
include_bytes!("../assets/favicon.svg"),
"image/svg+xml",
),
];
/// The second way to get the CLI (owner 2026-10-11, decision (c)): crates.io.
pub const CARGO_INSTALL: &str =
"cargo install gunnar-cli --locked --no-default-features --features client";
/// One signed binary, as the page shows it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Binary {
/// The file name served under [`PREFIX`].
pub file: String,
/// The target triple it runs on.
pub target: String,
pub bytes: u64,
pub sha256: String,
pub blake3: String,
/// `<file>.p7s`.
pub signature: String,
/// The one line that checks the sha256 AND the signature against the root.
pub verify: String,
}
/// The trust root a signature chains to.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Root {
pub file: String,
pub sha256: String,
}
/// **What `/download/index.json` says.**
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Manifest {
/// The release these binaries are from.
pub version: String,
pub binaries: Vec<Binary>,
pub root: Option<Root>,
/// The crates.io way.
pub cargo_install: String,
/// Why nothing is offered, by name; `None` when a signed binary is.
pub absent: Option<String>,
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
/// Pure: the sha256 of `bytes`, lowercase hex.
pub fn sha256_hex(bytes: &[u8]) -> String {
use sha2::Digest;
hex(&sha2::Sha256::digest(bytes))
}
/// Pure: **the one verify line** for `file` — the sha256, then the detached CMS
/// over the BLAKE3 digest against the published root (openssl ≥ 3, b3sum).
pub fn verify_line(file: &str) -> String {
format!(
"sha256sum -c {file}.sha256 && b3sum --no-names {file} | xxd -r -p > {file}.b3 && openssl cms -verify -binary -inform DER -in {file}.p7s -content {file}.b3 -CAfile <(openssl x509 -inform DER -in {ROOT_CA}) -purpose any -out /dev/null"
)
}
/// The target a served binary's name ends in (`gunnar-<target>`).
fn target_of(file: &str) -> Option<&str> {
file.strip_prefix("gunnar-")
.filter(|t| !t.is_empty() && !t.contains('.'))
}
/// Pure: **the manifest of `files`** (`(name, bytes)` as [`FILES`] holds them).
pub fn manifest(files: &[(&str, &[u8])], version: &str) -> Manifest {
let get = |n: &str| files.iter().find(|(f, _)| *f == n).map(|(_, b)| *b);
let mut binaries = Vec::new();
let mut unsigned = Vec::new();
for (name, bytes) in files {
let Some(target) = target_of(name) else {
continue;
};
if get(&format!("{name}.p7s")).is_none() {
unsigned.push(name.to_string());
continue;
}
let blake3 = get(&format!("{name}.blake3"))
.map(|b| String::from_utf8_lossy(b).trim().to_string())
.unwrap_or_else(|| blake3::hash(bytes).to_hex().to_string());
binaries.push(Binary {
file: name.to_string(),
target: target.to_string(),
bytes: bytes.len() as u64,
sha256: sha256_hex(bytes),
blake3,
signature: format!("{name}.p7s"),
verify: verify_line(name),
});
}
let root = get(ROOT_CA).map(|b| Root {
file: ROOT_CA.to_string(),
sha256: sha256_hex(b),
});
let absent = if binaries.is_empty() {
Some(if unsigned.is_empty() {
"REFUSED [cli-not-on-medium] this medium was composed without the signed Gunnar CLI (the Build Thing's `gunnar-musl` row); compose it again with it".to_string()
} else {
format!(
"REFUSED [cli-unsigned] {} came without a signature and is not offered",
unsigned.join(", ")
)
})
} else if root.is_none() {
binaries.clear();
Some(format!("REFUSED [cli-no-root] the trust root `{ROOT_CA}` is not on this medium: a signature nobody can check is not offered"))
} else {
None
};
Manifest {
version: version.to_string(),
binaries,
root,
cargo_install: CARGO_INSTALL.to_string(),
absent,
}
}
/// Pure: the bytes served at `/download/<name>` — an embedded file (only those
/// the manifest offers, their sidecars and the root), or `<binary>.sha256`
/// made from it in the `sha256sum -c` form.
pub fn file(files: &[(&str, &[u8])], m: &Manifest, name: &str) -> Option<Vec<u8>> {
if let Some(b) = name
.strip_suffix(".sha256")
.and_then(|bin| m.binaries.iter().find(|x| x.file == bin))
{
return Some(format!("{} {}\n", b.sha256, b.file).into_bytes());
}
let offered = m.root.as_ref().is_some_and(|r| r.file == name)
|| m.binaries
.iter()
.any(|b| name == b.file || name == b.signature || name == format!("{}.blake3", b.file));
if !offered {
return None;
}
files
.iter()
.find(|(f, _)| *f == name)
.map(|(_, b)| b.to_vec())
}
/// **The `/download` router** over `files`.
pub fn router_over(files: &'static [(&'static str, &'static [u8])], version: &str) -> Router {
let m = Arc::new(manifest(files, version));
let index = m.clone();
Router::new()
.route(&format!("{PREFIX}/"), get(|| async { (StatusCode::SEE_OTHER, [(header::LOCATION, PAGE)]).into_response() }))
.route(PREFIX, get(|| async { (StatusCode::SEE_OTHER, [(header::LOCATION, PAGE)]).into_response() }))
.route(&format!("{PREFIX}/index.json"), get(move || async move { axum::Json((*index).clone()).into_response() }))
.route(
&format!("{PREFIX}/{{name}}"),
get(move |UrlPath(name): UrlPath<String>| async move {
if let Some((_, bytes, ct)) = BRAND.iter().find(|(n, _, _)| *n == name) {
return ([(header::CONTENT_TYPE, *ct), (header::CACHE_CONTROL, "public, max-age=86400")], *bytes).into_response();
}
match file(files, &m, &name) {
Some(b) => {
let ct = if name.ends_with(".sha256") || name.ends_with(".blake3") { "text/plain; charset=utf-8" } else { "application/octet-stream" };
([(header::CONTENT_TYPE, ct.to_string()), (header::CONTENT_DISPOSITION, format!("attachment; filename=\"{name}\""))], b).into_response()
}
None => (StatusCode::NOT_FOUND, format!("REFUSED [cli-download-unknown] {name:?} is not a download of this medium\n")).into_response(),
}
}),
)
}
/// The router over THIS medium's embedded files.
pub fn router() -> Router {
router_over(FILES, env!("CARGO_PKG_VERSION"))
}
/// **`face`, with `/download/…` served by this module** and every other path
/// left to it (the console).
pub fn serving(face: Router) -> Router {
router().fallback_service(face)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use tower::ServiceExt;
const BIN: &[u8] = b"\x7fELF a gunnar";
const SIG: &[u8] = b"0\x82 cms";
const ROOT: &[u8] = b"0\x82 root";
fn signed() -> &'static [(&'static str, &'static [u8])] {
&[
("gunnar-x86_64-unknown-linux-musl", BIN),
("gunnar-x86_64-unknown-linux-musl.p7s", SIG),
("korp-root-ca.der", ROOT),
]
}
#[test]
fn a_signed_binary_is_offered_with_its_sha256_blake3_signature_and_verify_line() {
let m = manifest(signed(), "1.5.0");
assert_eq!(m.absent, None);
assert_eq!(m.binaries.len(), 1);
let b = &m.binaries[0];
assert_eq!(b.target, "x86_64-unknown-linux-musl");
assert_eq!(b.bytes, BIN.len() as u64);
assert_eq!(b.sha256, sha256_hex(BIN));
assert_eq!(b.sha256.len(), 64);
assert_eq!(b.blake3, blake3::hash(BIN).to_hex().to_string());
assert_eq!(b.signature, "gunnar-x86_64-unknown-linux-musl.p7s");
assert!(
b.verify
.contains("sha256sum -c gunnar-x86_64-unknown-linux-musl.sha256"),
"{}",
b.verify
);
assert!(
b.verify.contains("openssl cms -verify") && b.verify.contains(ROOT_CA),
"{}",
b.verify
);
assert_eq!(
m.root,
Some(Root {
file: ROOT_CA.into(),
sha256: sha256_hex(ROOT)
})
);
assert!(
m.cargo_install.starts_with("cargo install gunnar-cli"),
"{}",
m.cargo_install
);
}
#[test]
fn an_unsigned_binary_is_never_offered() {
let files: &[(&str, &[u8])] = &[
("gunnar-x86_64-unknown-linux-musl", BIN),
("korp-root-ca.der", ROOT),
];
let m = manifest(files, "1.5.0");
assert!(m.binaries.is_empty());
assert!(
m.absent.as_deref().unwrap().contains("[cli-unsigned]"),
"{:?}",
m.absent
);
assert_eq!(file(files, &m, "gunnar-x86_64-unknown-linux-musl"), None);
}
#[test]
fn a_medium_without_the_cli_or_its_root_says_so_by_name() {
assert!(manifest(&[], "1.5.0")
.absent
.unwrap()
.contains("[cli-not-on-medium]"));
let no_root: &[(&str, &[u8])] = &[
("gunnar-x86_64-unknown-linux-musl", BIN),
("gunnar-x86_64-unknown-linux-musl.p7s", SIG),
];
let m = manifest(no_root, "1.5.0");
assert!(m.binaries.is_empty() && m.absent.unwrap().contains("[cli-no-root]"));
}
#[test]
fn the_sha256_file_is_in_the_form_sha256sum_checks() {
let m = manifest(signed(), "1.5.0");
let got = file(signed(), &m, "gunnar-x86_64-unknown-linux-musl.sha256").unwrap();
assert_eq!(
String::from_utf8(got).unwrap(),
format!("{} gunnar-x86_64-unknown-linux-musl\n", sha256_hex(BIN))
);
}
async fn get_path(r: Router, p: &str) -> (StatusCode, Option<String>, Vec<u8>) {
let res = r
.oneshot(axum::http::Request::get(p).body(Body::empty()).unwrap())
.await
.unwrap();
let loc = res
.headers()
.get(header::LOCATION)
.map(|v| v.to_str().unwrap().to_string());
let st = res.status();
let body = axum::body::to_bytes(res.into_body(), usize::MAX)
.await
.unwrap()
.to_vec();
(st, loc, body)
}
#[tokio::test]
async fn the_download_paths_are_served_ahead_of_the_console() {
let console = Router::new().fallback(|| async { "console" });
let r = router_over(signed(), "1.5.0").fallback_service(console);
let (st, _, body) = get_path(r.clone(), "/download/gunnar-x86_64-unknown-linux-musl").await;
assert_eq!((st, body.as_slice()), (StatusCode::OK, BIN));
let (st, _, body) =
get_path(r.clone(), "/download/gunnar-x86_64-unknown-linux-musl.p7s").await;
assert_eq!((st, body.as_slice()), (StatusCode::OK, SIG));
let (st, _, body) = get_path(r.clone(), "/download/korp-root-ca.der").await;
assert_eq!((st, body.as_slice()), (StatusCode::OK, ROOT));
let (st, _, body) = get_path(r.clone(), "/download/index.json").await;
assert_eq!(st, StatusCode::OK);
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(v["binaries"][0]["file"], "gunnar-x86_64-unknown-linux-musl");
assert_eq!(v["version"], "1.5.0");
let (st, loc, _) = get_path(r.clone(), "/download/").await;
assert_eq!((st, loc.as_deref()), (StatusCode::SEE_OTHER, Some(PAGE)));
// The mark the crates' READMEs and docs.rs name, absolute, on every site.
let (st, _, body) = get_path(r.clone(), "/download/gunnar.webp").await;
assert_eq!(
(st, &body[..4], &body[8..12]),
(StatusCode::OK, &b"RIFF"[..], &b"WEBP"[..])
);
let (st, _, body) = get_path(r.clone(), "/download/favicon.svg").await;
assert!(st == StatusCode::OK && String::from_utf8_lossy(&body).contains("<svg"));
let (st, _, _) = get_path(r.clone(), "/download/etc-passwd").await;
assert_eq!(st, StatusCode::NOT_FOUND);
let (st, _, body) = get_path(r, "/repos").await;
assert_eq!((st, body.as_slice()), (StatusCode::OK, &b"console"[..]));
}
}