crates/installer/nordisk-narrate/src/wire.rs
//! **The datagrams.** Four kinds, each a 4-byte tag first:
//!
//! | tag | direction | sealed | what |
//! |---|---|---|---|
//! | `NRK1` KNOCK | installer → box | no | "who is there?" — a nonce, padded to [`KNOCK_LEN`] |
//! | `NRW1` WHO | box → installer | box's ephemeral → installer's key | the box id, its boot, the knock's nonce echoed |
//! | `NRH1` HELLO | installer → box | installer's key → box's ephemeral | "stream to me, from line N" |
//! | `NRD1` DATA | box → installer | box's ephemeral → installer's key | lines `first..`, and how many were dropped before them |
//!
//! Box → installer: `tag ‖ box ephemeral public (32) ‖ nonce (24) ‖ crypto_box`.
//! Installer → box: `tag ‖ nonce (24) ‖ crypto_box`.
//!
//! The plaintext begins with a KIND byte, so a datagram sealed for one purpose
//! never opens as another (the two directions share one shared key).
use crypto_box::aead::{Aead, AeadCore, OsRng};
use crypto_box::{PublicKey, SalsaBox, SecretKey};
/// The largest datagram either side sends: under every path MTU a VM meets
/// (IPv4 576 is not a concern for a cloud VM; 1280 is IPv6's floor).
pub const MAX_DATAGRAM: usize = 1200;
/// A knock is at least this long, and a WHO answer is always shorter: a box
/// answering a knock from a forged source address amplifies nothing.
pub const KNOCK_LEN: usize = 256;
pub const TAG_KNOCK: &[u8; 4] = b"NRK1";
pub const TAG_WHO: &[u8; 4] = b"NRW1";
pub const TAG_HELLO: &[u8; 4] = b"NRH1";
pub const TAG_DATA: &[u8; 4] = b"NRD1";
const KEY: usize = 32;
const NONCE: usize = 24;
const MAC: usize = 16;
const KIND_WHO: u8 = 1;
const KIND_DATA: u8 = 2;
const KIND_HELLO: u8 = 3;
/// **Who is narrating**: the product, the box's first NIC address (MAC), and a
/// random id minted once per process start. A new boot is a new stream.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoxId {
pub product: String,
pub mac: String,
pub boot: u64,
}
/// What a box seals to the installer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Up {
Who { id: BoxId, echo: [u8; 16] },
/// Lines `first ..`; `dropped` lines immediately before `first` are gone
/// (the box's log evicted them before they were sent).
Data { id: BoxId, first: u64, dropped: u64, lines: Vec<String> },
}
/// What the installer seals to a box.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hello {
/// The box's boot this hello is for (from its WHO).
pub boot: u64,
/// Strictly increasing per installer run: a replayed hello is refused.
pub counter: u64,
/// The next line the installer needs.
pub from: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WireError {
/// Not one of ours (wrong tag, too short).
NotOurs,
/// Does not open: forged, corrupted, or sealed to another key.
Forged,
/// Opened, and the payload is not what the tag says.
Malformed(&'static str),
}
impl std::fmt::Display for WireError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WireError::NotOurs => f.write_str("not a narrate datagram"),
WireError::Forged => f.write_str("does not open under the installer's key (forged, corrupted or for another installer)"),
WireError::Malformed(w) => write!(f, "opened, but malformed: {w}"),
}
}
}
impl std::error::Error for WireError {}
/// A fresh X25519 key (the box's per-boot ephemeral key, a test's installer).
pub fn generate() -> SecretKey {
SecretKey::generate(&mut OsRng)
}
/// 8 random bytes.
pub fn random_u64() -> u64 {
let n = SalsaBox::generate_nonce(&mut OsRng);
u64::from_le_bytes(n[..8].try_into().expect("24 >= 8"))
}
/// 16 random bytes (a knock's nonce).
pub fn random_16() -> [u8; 16] {
let n = SalsaBox::generate_nonce(&mut OsRng);
n[..16].try_into().expect("24 >= 16")
}
// ── the knock ────────────────────────────────────────────────────────────────
/// A knock: the tag, a nonce the WHO must echo, zeros to [`KNOCK_LEN`].
pub fn knock(nonce: [u8; 16]) -> Vec<u8> {
let mut d = Vec::with_capacity(KNOCK_LEN);
d.extend_from_slice(TAG_KNOCK);
d.extend_from_slice(&nonce);
d.resize(KNOCK_LEN, 0);
d
}
/// The knock's nonce, if `d` is a knock of full length.
pub fn parse_knock(d: &[u8]) -> Option<[u8; 16]> {
if d.len() < KNOCK_LEN || &d[..4] != TAG_KNOCK {
return None;
}
d[4..20].try_into().ok()
}
// ── the plaintext ────────────────────────────────────────────────────────────
struct W(Vec<u8>);
impl W {
fn u8(&mut self, v: u8) {
self.0.push(v);
}
fn u16(&mut self, v: u16) {
self.0.extend_from_slice(&v.to_le_bytes());
}
fn u64(&mut self, v: u64) {
self.0.extend_from_slice(&v.to_le_bytes());
}
fn str(&mut self, s: &str) {
let b = &s.as_bytes()[..s.len().min(u16::MAX as usize)];
self.u16(b.len() as u16);
self.0.extend_from_slice(b);
}
fn id(&mut self, id: &BoxId) {
self.str(&id.product);
self.str(&id.mac);
self.u64(id.boot);
}
}
struct R<'a>(&'a [u8]);
impl<'a> R<'a> {
fn take(&mut self, n: usize) -> Result<&'a [u8], WireError> {
if self.0.len() < n {
return Err(WireError::Malformed("short"));
}
let (h, t) = self.0.split_at(n);
self.0 = t;
Ok(h)
}
fn u8(&mut self) -> Result<u8, WireError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, WireError> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().expect("2")))
}
fn u64(&mut self) -> Result<u64, WireError> {
Ok(u64::from_le_bytes(self.take(8)?.try_into().expect("8")))
}
fn str(&mut self) -> Result<String, WireError> {
let n = self.u16()? as usize;
String::from_utf8(self.take(n)?.to_vec()).map_err(|_| WireError::Malformed("not utf-8"))
}
fn id(&mut self) -> Result<BoxId, WireError> {
Ok(BoxId { product: self.str()?, mac: self.str()?, boot: self.u64()? })
}
fn end(&self) -> Result<(), WireError> {
if self.0.is_empty() { Ok(()) } else { Err(WireError::Malformed("trailing bytes")) }
}
}
fn encode_up(up: &Up) -> Vec<u8> {
let mut w = W(Vec::with_capacity(MAX_DATAGRAM));
match up {
Up::Who { id, echo } => {
w.u8(KIND_WHO);
w.id(id);
w.0.extend_from_slice(echo);
}
Up::Data { id, first, dropped, lines } => {
w.u8(KIND_DATA);
w.id(id);
w.u64(*first);
w.u64(*dropped);
w.u16(lines.len().min(u16::MAX as usize) as u16);
for l in lines.iter().take(u16::MAX as usize) {
w.str(l);
}
}
}
w.0
}
fn decode_up(p: &[u8]) -> Result<Up, WireError> {
let mut r = R(p);
let up = match r.u8()? {
KIND_WHO => {
let id = r.id()?;
let echo = r.take(16)?.try_into().expect("16");
Up::Who { id, echo }
}
KIND_DATA => {
let id = r.id()?;
let first = r.u64()?;
let dropped = r.u64()?;
let n = r.u16()? as usize;
let mut lines = Vec::with_capacity(n);
for _ in 0..n {
lines.push(r.str()?);
}
Up::Data { id, first, dropped, lines }
}
_ => return Err(WireError::Malformed("not a box → installer kind")),
};
r.end()?;
Ok(up)
}
// ── sealing ─────────────────────────────────────────────────────────────────
/// **Seal what a box says** to the installer's key, from the box's per-boot
/// ephemeral key.
pub fn seal_up(eph: &SecretKey, installer: &PublicKey, up: &Up) -> Vec<u8> {
let tag = match up {
Up::Who { .. } => TAG_WHO,
Up::Data { .. } => TAG_DATA,
};
let b = SalsaBox::new(installer, eph);
let nonce = SalsaBox::generate_nonce(&mut OsRng);
let ct = b.encrypt(&nonce, encode_up(up).as_slice()).expect("crypto_box encrypt does not fail on a Vec");
let mut d = Vec::with_capacity(4 + KEY + NONCE + ct.len());
d.extend_from_slice(tag);
d.extend_from_slice(eph.public_key().as_bytes());
d.extend_from_slice(&nonce);
d.extend_from_slice(&ct);
d
}
/// **Open what a box said**, with the installer's private key. Returns the
/// box's ephemeral public key (the stream's identity) and the payload. A tag
/// that disagrees with the payload's kind is malformed.
pub fn open_up(installer: &SecretKey, d: &[u8]) -> Result<([u8; 32], Up), WireError> {
if d.len() < 4 + KEY + NONCE + MAC {
return Err(WireError::NotOurs);
}
let tag: &[u8; 4] = d[..4].try_into().expect("4");
if tag != TAG_WHO && tag != TAG_DATA {
return Err(WireError::NotOurs);
}
let eph: [u8; 32] = d[4..4 + KEY].try_into().expect("32");
let nonce = crypto_box::Nonce::from_slice(&d[4 + KEY..4 + KEY + NONCE]);
let b = SalsaBox::new(&PublicKey::from(eph), installer);
let pt = b.decrypt(nonce, &d[4 + KEY + NONCE..]).map_err(|_| WireError::Forged)?;
let up = decode_up(&pt)?;
let agrees = matches!((&up, tag), (Up::Who { .. }, t) if t == TAG_WHO) || matches!((&up, tag), (Up::Data { .. }, t) if t == TAG_DATA);
if !agrees {
return Err(WireError::Malformed("the tag and the sealed kind disagree"));
}
Ok((eph, up))
}
/// **Seal a hello** from the installer's private key to a box's ephemeral key.
pub fn seal_hello(installer: &SecretKey, box_eph: &PublicKey, h: &Hello) -> Vec<u8> {
let mut w = W(Vec::with_capacity(32));
w.u8(KIND_HELLO);
w.u64(h.boot);
w.u64(h.counter);
w.u64(h.from);
let b = SalsaBox::new(box_eph, installer);
let nonce = SalsaBox::generate_nonce(&mut OsRng);
let ct = b.encrypt(&nonce, w.0.as_slice()).expect("crypto_box encrypt does not fail on a Vec");
let mut d = Vec::with_capacity(4 + NONCE + ct.len());
d.extend_from_slice(TAG_HELLO);
d.extend_from_slice(&nonce);
d.extend_from_slice(&ct);
d
}
/// **Open a hello** on the box: only the installer's private key seals one
/// that opens under the box's ephemeral key and the installer's public key.
pub fn open_hello(eph: &SecretKey, installer: &PublicKey, d: &[u8]) -> Result<Hello, WireError> {
if d.len() < 4 + NONCE + MAC || &d[..4] != TAG_HELLO {
return Err(WireError::NotOurs);
}
let nonce = crypto_box::Nonce::from_slice(&d[4..4 + NONCE]);
let b = SalsaBox::new(installer, eph);
let pt = b.decrypt(nonce, &d[4 + NONCE..]).map_err(|_| WireError::Forged)?;
let mut r = R(&pt);
if r.u8()? != KIND_HELLO {
return Err(WireError::Malformed("not a hello"));
}
let h = Hello { boot: r.u64()?, counter: r.u64()?, from: r.u64()? };
r.end()?;
Ok(h)
}
// ── packing lines ───────────────────────────────────────────────────────────
/// The sealed overhead of a DATA datagram around its plaintext.
pub const DATA_OVERHEAD: usize = 4 + KEY + NONCE + MAC;
/// A line cut to fit a datagram says so.
pub const CUT: &str = " … (cut to fit one narrate datagram; the log door has it whole)";
/// **As many of `lines` as fit one DATA datagram** (at least one: a line too
/// long for a datagram alone is cut on a character boundary and says so).
/// Returns the payload and how many lines it took.
pub fn pack(id: &BoxId, first: u64, dropped: u64, lines: &[String]) -> (Up, usize) {
let head = 1 + (2 + id.product.len()) + (2 + id.mac.len()) + 8 + 8 + 8 + 2;
let budget = MAX_DATAGRAM.saturating_sub(DATA_OVERHEAD + head);
let mut used = 0usize;
let mut out: Vec<String> = Vec::new();
for l in lines {
let need = 2 + l.len();
if used + need <= budget {
used += need;
out.push(l.clone());
continue;
}
if out.is_empty() {
let room = budget.saturating_sub(2 + CUT.len());
let mut cut = room.min(l.len());
while cut > 0 && !l.is_char_boundary(cut) {
cut -= 1;
}
out.push(format!("{}{CUT}", &l[..cut]));
}
break;
}
let n = out.len();
(Up::Data { id: id.clone(), first, dropped, lines: out }, n)
}
#[cfg(test)]
mod tests {
use super::*;
fn id() -> BoxId {
BoxId { product: "gunnar-server".into(), mac: "52:54:00:12:34:56".into(), boot: 0xfeed }
}
/// **Only the installer reads it.** A WHO and a DATA sealed to the
/// installer's key open with its private key — and with no other.
#[test]
fn a_box_datagram_opens_under_the_installers_key_and_no_other() {
let installer = generate();
let eph = generate();
let up = Up::Data { id: id(), first: 7, dropped: 0, lines: vec!["korp-start: STAGE console".into(), "kernel: xfs mounted".into()] };
let d = seal_up(&eph, &installer.public_key(), &up);
assert!(d.len() <= MAX_DATAGRAM);
assert_eq!(&d[..4], TAG_DATA);
let (e, back) = open_up(&installer, &d).unwrap();
assert_eq!((e, back), (*eph.public_key().as_bytes(), up));
let stranger = generate();
assert_eq!(open_up(&stranger, &d), Err(WireError::Forged), "another installer cannot read it");
// Nothing of the line text is in the clear.
assert!(!d.windows(10).any(|w| w == b"korp-start"), "the plaintext leaked");
}
/// **Forged or tampered datagrams are dropped.** One flipped bit anywhere
/// in the sealed part, a swapped ephemeral key, or a DATA re-tagged as a
/// WHO does not open as what it claims.
#[test]
fn a_tampered_or_re_tagged_datagram_does_not_open() {
let installer = generate();
let eph = generate();
let d = seal_up(&eph, &installer.public_key(), &Up::Who { id: id(), echo: [9; 16] });
for i in 4..d.len() {
let mut t = d.clone();
t[i] ^= 0x01;
assert!(open_up(&installer, &t).is_err(), "a flip at byte {i} still opened");
}
let mut retag = seal_up(&eph, &installer.public_key(), &Up::Data { id: id(), first: 0, dropped: 0, lines: vec![] });
retag[..4].copy_from_slice(TAG_WHO);
assert!(matches!(open_up(&installer, &retag), Err(WireError::Malformed(_))));
assert_eq!(open_up(&installer, b"NRX1 garbage"), Err(WireError::NotOurs));
}
/// **Only the installer can point the stream.** A hello sealed with the
/// installer's private key to the box's ephemeral key opens on the box; a
/// hello from anyone else — including a holder of the box's WHO — does not;
/// and a box's own DATA reflected back as a hello is not a hello.
#[test]
fn only_the_installers_key_seals_a_hello_the_box_accepts() {
let installer = generate();
let eph = generate();
let h = Hello { boot: 0xfeed, counter: 3, from: 120 };
let d = seal_hello(&installer, &eph.public_key(), &h);
assert_eq!(open_hello(&eph, &installer.public_key(), &d), Ok(h));
let mallory = generate();
let forged = seal_hello(&mallory, &eph.public_key(), &h);
assert_eq!(open_hello(&eph, &installer.public_key(), &forged), Err(WireError::Forged));
// The two directions share one key: a reflected DATA is refused by its kind.
let data = seal_up(&eph, &installer.public_key(), &Up::Data { id: id(), first: 0, dropped: 0, lines: vec![] });
let mut reflected = TAG_HELLO.to_vec();
reflected.extend_from_slice(&data[4 + 32..]);
assert!(matches!(open_hello(&eph, &installer.public_key(), &reflected), Err(WireError::Malformed(_))));
}
/// **No amplification.** A knock is [`KNOCK_LEN`] long; the WHO a box
/// answers it with is shorter, whatever the product and MAC say.
#[test]
fn a_who_is_shorter_than_the_knock_it_answers() {
let k = knock([7; 16]);
assert_eq!(k.len(), KNOCK_LEN);
assert_eq!(parse_knock(&k), Some([7; 16]));
assert_eq!(parse_knock(&k[..KNOCK_LEN - 1]), None, "a short knock is not answered");
let who = seal_up(&generate(), &generate().public_key(), &Up::Who { id: BoxId { product: "gunnar-front".into(), mac: "52:54:00:aa:bb:cc".into(), boot: u64::MAX }, echo: [7; 16] });
assert!(who.len() < KNOCK_LEN, "{} >= {KNOCK_LEN}", who.len());
}
/// **Lines are packed to fit one datagram**, in order, and a line longer
/// than a datagram is cut on a character boundary and says so.
#[test]
fn lines_pack_into_datagrams_in_order_and_a_giant_line_is_cut() {
let installer = generate();
let eph = generate();
let lines: Vec<String> = (0..500).map(|i| format!("line {i}: {}", "ä".repeat(i % 37))).collect();
let (mut at, mut seen) = (0usize, Vec::new());
while at < lines.len() {
let (up, n) = pack(&id(), at as u64, 0, &lines[at..]);
assert!(n >= 1);
let d = seal_up(&eph, &installer.public_key(), &up);
assert!(d.len() <= MAX_DATAGRAM, "{}", d.len());
let (_, Up::Data { first, lines: got, .. }) = open_up(&installer, &d).unwrap() else { panic!() };
assert_eq!(first, at as u64);
seen.extend(got);
at += n;
}
assert_eq!(seen, lines);
let giant = vec!["å".repeat(3000), "next".to_string()];
let (up, n) = pack(&id(), 0, 0, &giant);
assert_eq!(n, 1, "the giant goes alone");
let Up::Data { lines: got, .. } = &up else { panic!() };
assert!(got[0].ends_with(CUT) && got[0].starts_with("åå"));
assert!(seal_up(&eph, &installer.public_key(), &up).len() <= MAX_DATAGRAM);
}
}