crates/installer/nordisk-pairing/src/live.rs
//! **A live update over the installer's door** (owner 2026-10-12): the kit reads what a box
//! runs and hands it a new signed app — the same link, the same mutual TLS, the same one-role
//! rule as the log door (the installer's own certificate and nobody else). The box decides
//! (korp-installer `live`): refused by name, or staged into its other app slot, then re-exec.
//!
//! * `GET` [`RUNNING_PATH`] — what the box runs: `base`, `app`, `slot`, `live_update`
//! (TOML; [`Running`]).
//! * `POST` [`STAGE_PATH`] — a new app: [`Stage`] as [`Stage::encode`] writes it, with what
//! the release declares beside it ([`Declared`], [`Stage::encode_declared`], lane B5).
use serde::{Deserialize, Serialize};
pub const RUNNING_PATH: &str = "/live/running";
pub const STAGE_PATH: &str = "/live/stage";
/// The largest live payload taken (a stage request, base64 included).
pub const MAX_STAGE_BYTES: usize = 256 * 1024 * 1024;
/// What a box reports it runs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Running {
/// The BASE hash (korp-installer `live::running_base`).
pub base: String,
/// The running app's `app.blake3`.
pub app: String,
/// `a` | `b`, and the trial state (`a`, `b trial`, `b booted`).
pub slot: String,
/// `korp.live_update` on this box.
pub live_update: bool,
}
impl Running {
pub fn render(&self) -> String {
toml::to_string(self).unwrap_or_default()
}
pub fn parse(text: &str) -> Result<Running, String> {
toml::from_str(text).map_err(|e| format!("not a running report: {e}"))
}
}
/// A new app for the box's other slot.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stage {
/// `app.blake3`, hex.
pub pin_hex: String,
/// `app.sig` (the framed, versioned signature), raw.
pub signature: Vec<u8>,
/// The BASE the app was built against.
pub base: String,
/// The app `.znippy`.
pub archive: Vec<u8>,
}
/// **What a live update declares beside its app** (lane B5) — korp-installer
/// `live::Declaration` on the wire. Both parts are optional: an older ONE box or front sends
/// neither, and the box says so.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Declared {
/// The release declaration: its bundle (korp-installer `release`) and its signature over
/// the app's root (empty: unsigned, an unenrolled box). `None`: none rides this update.
pub release: Option<(Vec<u8>, Vec<u8>)>,
/// The release's own `korp.env.*` tokens (`korp.env.KEY=VALUE`, korp-installer
/// `live::release_env`). `None`: undeclared.
pub env: Option<Vec<String>>,
}
impl Declared {
/// Nothing declared: the plain stage of before.
pub fn is_empty(&self) -> bool {
self.release.is_none() && self.env.is_none()
}
}
/// **The refusals a front answers with a KEEP boot** (lane B5), never a failure: the release
/// moved what only a keep boot lays — the env on the cmdline (`[live-env-moved]`), a
/// declaration the box keeps that the update does not carry (`[live-release-absent]`) — or the
/// box takes no declaration live at all (`[live-declared-unsupported]`). A declaration that
/// does not hold (`[release-sig]`) is a failure: the release itself is wrong.
pub const KEEP_REFUSALS: &[&str] = &["[live-env-moved]", "[live-release-absent]", "[live-declared-unsupported]"];
/// Whether a box's refusal of a stage is one [`KEEP_REFUSALS`] names.
pub fn falls_to_keep(refusal: &str) -> bool {
KEEP_REFUSALS.iter().any(|t| refusal.contains(t))
}
/// The refusal of a box (or product) that takes the plain stage only.
pub fn declared_unsupported() -> String {
"REFUSED [live-declared-unsupported] this box takes no release declaration or release env live: a keep boot lays them".into()
}
fn hex(b: &[u8]) -> String {
b.iter().map(|b| format!("{b:02x}")).collect()
}
fn unhex(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(s.get(i..i + 2)?, 16).ok()).collect()
}
impl Stage {
/// The request body: `pin`, `sig` (hex), `base`, one line each, a blank line, the archive
/// base64.
pub fn encode(&self) -> String {
self.encode_declared(&Declared::default())
}
/// [`Stage::encode`] with what the release declares (lane B5): after `base`, `release`
/// (the bundle, base64) and `release_sig` (hex), and `env` (the tokens, space-separated),
/// each only when declared. A reader before B5 reads the three lines it knows and ignores
/// these.
pub fn encode_declared(&self, d: &Declared) -> String {
let mut head = format!("pin {}\nsig {}\nbase {}\n", self.pin_hex.trim(), hex(&self.signature), self.base.trim());
if let Some((bundle, sig)) = &d.release {
head.push_str(&format!("release {}\nrelease_sig {}\n", b64_encode(bundle), hex(sig)));
}
if let Some(env) = &d.env {
head.push_str(&format!("env {}\n", env.join(" ")));
}
format!("{head}\n{}", b64_encode(&self.archive))
}
pub fn decode(body: &str) -> Result<Stage, String> {
Self::decode_declared(body).map(|(s, _)| s)
}
/// [`Stage::decode`] with what the release declares. A line this reader does not know is
/// refused by name: what a release declares is never dropped silently.
pub fn decode_declared(body: &str) -> Result<(Stage, Declared), String> {
let bad = |w: &str| format!("[live-stage-body] {w}");
let (head, archive) = body.split_once("\n\n").ok_or_else(|| bad("no blank line before the archive"))?;
let mut lines = head.lines();
let mut field = |k: &str| -> Result<String, String> {
let l = lines.next().ok_or_else(|| bad(&format!("no `{k}` line")))?;
l.strip_prefix(&format!("{k} ")).map(|v| v.trim().to_string()).ok_or_else(|| bad(&format!("`{k}` expected, `{l:.20}` found")))
};
let pin_hex = field("pin")?;
let sig = field("sig")?;
let base = field("base")?;
let signature = unhex(&sig).ok_or_else(|| bad("the signature is not hex"))?;
let mut d = Declared::default();
let (mut bundle, mut bundle_sig) = (None, None);
for l in lines {
let (k, v) = l.split_once(' ').unwrap_or((l, ""));
let v = v.trim();
match k {
"release" if bundle.is_none() => bundle = Some(b64_decode(v).ok_or_else(|| bad("the release declaration is not base64"))?),
"release_sig" if bundle_sig.is_none() => bundle_sig = Some(unhex(v).ok_or_else(|| bad("the release signature is not hex"))?),
"env" if d.env.is_none() => d.env = Some(v.split_whitespace().map(str::to_string).collect()),
_ => return Err(bad(&format!("`{k:.20}`: a line this box does not know, or one given twice — never dropped"))),
}
}
d.release = match (bundle, bundle_sig) {
(Some(b), s) => Some((b, s.unwrap_or_default())),
(None, None) => None,
(None, Some(_)) => return Err(bad("a release signature without its declaration")),
};
let archive = b64_decode(archive.trim_end()).ok_or_else(|| bad("the archive is not base64"))?;
Ok((Stage { pin_hex, signature, base, archive }, d))
}
}
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub(crate) fn b64_encode(b: &[u8]) -> String {
let mut out = String::with_capacity(b.len().div_ceil(3) * 4);
for c in b.chunks(3) {
let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
for (i, shift) in [18, 12, 6, 0].iter().enumerate() {
out.push(if i <= c.len() { B64[((n >> shift) & 63) as usize] as char } else { '=' });
}
}
out
}
fn b64_decode(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(4) {
return None;
}
let val = |c: u8| B64.iter().position(|x| *x == c).map(|p| p as u32);
let mut out = Vec::with_capacity(s.len() / 4 * 3);
for q in s.as_bytes().chunks(4) {
let pad = q.iter().rev().take_while(|c| **c == b'=').count();
if pad > 2 {
return None;
}
let mut n = 0u32;
for (i, c) in q.iter().enumerate() {
let v = if i >= 4 - pad { 0 } else { val(*c)? };
n = n << 6 | v;
}
let bytes = [(n >> 16) as u8, (n >> 8) as u8, n as u8];
out.extend_from_slice(&bytes[..3 - pad]);
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_stage_round_trips_and_a_broken_one_is_refused_by_name() {
let s = Stage { pin_hex: "ab".repeat(32), signature: vec![1, 2, 3, 250], base: "cd".repeat(32), archive: (0..70_000u32).map(|i| (i % 251) as u8).collect() };
let body = s.encode();
assert!(body.starts_with("pin "), "{:.40}", body);
assert_eq!(Stage::decode(&body).unwrap(), s);
for bad in ["", "pin x\n\nAAAA", &body.replacen("base ", "bass ", 1), &format!("{body}!")] {
assert!(Stage::decode(bad).unwrap_err().contains("[live-stage-body]"), "{bad:.60}");
}
let r = Running { base: "b".into(), app: "a".into(), slot: "b trial".into(), live_update: true };
assert_eq!(Running::parse(&r.render()).unwrap(), r);
}
/// ★ **What a live update declares rides the stage** (lane B5): the release declaration
/// (bundle and signature) and the release's env round-trip; an older body declares
/// nothing; an older reader (`decode`) still reads the stage under the new lines; an
/// unknown line is refused by name, never ignored.
#[test]
fn a_stage_carries_its_declaration_and_an_unknown_line_is_refused() {
let s = Stage { pin_hex: "ab".repeat(32), signature: vec![1, 2], base: "cd".repeat(32), archive: vec![5; 4000] };
let d = Declared { release: Some((b"korp-release-v1\nholger.ron 5\n(two)\n".to_vec(), vec![7; 72])), env: Some(vec!["korp.env.A=1".into(), "korp.env.B=x:y".into()]) };
let body = s.encode_declared(&d);
assert_eq!(Stage::decode_declared(&body).unwrap(), (s.clone(), d.clone()));
assert_eq!(Stage::decode(&body).unwrap(), s, "the stage reads under the new lines");
assert_eq!(Stage::decode_declared(&s.encode()).unwrap(), (s.clone(), Declared::default()), "an older body declares nothing");
assert!(Declared::default().is_empty() && !d.is_empty());
// An unsigned declaration (an unenrolled box) and an empty env are declared too.
let bare = Declared { release: Some((b"korp-release-v1\n".to_vec(), vec![])), env: Some(vec![]) };
assert_eq!(Stage::decode_declared(&s.encode_declared(&bare)).unwrap().1, bare);
let unknown = body.replacen("\nbase ", "\nwhat 1\nbase ", 1);
assert!(Stage::decode_declared(&body.replacen("\n\n", "\nwhat 1\n\n", 1)).unwrap_err().contains("[live-stage-body]"));
assert!(Stage::decode_declared(&unknown).is_err());
// The refusals a front answers with a keep boot.
for w in ["REFUSED [live-release-absent] x", "[live-env-moved] y", "REFUSED [live-declared-unsupported] z"] {
assert!(falls_to_keep(w), "{w}");
}
assert!(!falls_to_keep("[release-sig] the signature does not hold"), "a bad declaration is a failure, never a keep");
}
}