holger-estate-front/src/console_key.rs
//! **The console's keys** (A7 row 14; Gunnar d1b12b15 / 07360814 / 8f502b01: the front mints
//! the console's keys and offers their public halves, the boxes enrol them, and no shared
//! secret goes from the shop to every box). Holger's own form of that pattern:
//!
//! | who | holds | offers (`offer.<vm>.<key>`) |
//! |---|---|---|
//! | the front | an x25519 key under `<data>/console/` (0600, minted once, kept across boots) | [`OFFER_CONSOLE_PUBKEY`] |
//! | each box (main, twin) | its own x25519 key under `<appliance>/console/` | [`OFFER_BOX_PUBKEY`] |
//!
//! The console's bearer to ONE box is derived on both ends from that pair
//! ([`bearer`]: x25519, then blake3 `derive_key` under [`BEARER_CONTEXT`]). The box enrols the
//! front's offered public half and writes the bearer it derives as its static-token login
//! (holger `server/cli/src/paired.rs` `install_console_token`); the front derives the same
//! bearer for that box and presents it. Every box has its own bearer, the shop relays only
//! public halves, and a recharged front or box mints a new key, so the old bearer dies with
//! the old machine. Until 2026-10-22 the shop fed one `holger_console_token` to the front AND
//! every box (monetize 13a2fae1).
use std::collections::BTreeMap;
use std::net::Ipv4Addr;
use std::path::Path;
use std::sync::RwLock;
use nordisk_pairing::join::JoinReply;
use nordisk_pairing::Role;
/// The key's directory under the front's data root (the ceremony's `private.key` in it).
pub const KEY_DIR: &str = "console";
/// What the front offers every box: the public half of its console key (base64).
pub const OFFER_CONSOLE_PUBKEY: &str = "console_pubkey";
/// What every box offers the front: the public half of its own console key (base64).
pub const OFFER_BOX_PUBKEY: &str = "console_box_pubkey";
/// The derivation's context (blake3 `derive_key`); the box derives under the same words.
pub const BEARER_CONTEXT: &str =
"holger 2026-10-22 console bearer from the front's and the box's x25519 keys";
static KEY: RwLock<Option<[u8; 32]>> = RwLock::new(None);
/// **The front's console key**, minted once under `root` and kept: answers its public half
/// (base64) and adopts the private half for [`bearers`].
pub fn adopt(root: &Path) -> Result<String, String> {
let id = wg_appliance_core::ceremony::load_or_mint_identity(&root.join(KEY_DIR))
.map_err(|e| format!("the console's key under {}: {e}", root.display()))?;
*KEY.write().unwrap_or_else(|p| p.into_inner()) = Some(id.private);
Ok(id.public_b64)
}
/// The adopted private half, `None` before [`adopt`].
pub fn adopted() -> Option<[u8; 32]> {
*KEY.read().unwrap_or_else(|p| p.into_inner())
}
/// Pure: **the console's bearer between `my_private` and the other end's `their_public`
/// (base64)**: 64 hex characters; the two ends of a pair derive the same one.
pub fn bearer(my_private: &[u8; 32], their_public_b64: &str) -> Result<String, String> {
let theirs = wg_appliance_core::keys::decode_key(their_public_b64.trim()).map_err(|e| format!("REFUSED [holger-console-key-bad] the offered console key is not a 32-byte base64 key: {e:?}"))?;
let shared = boringtun::x25519::StaticSecret::from(*my_private)
.diffie_hellman(&boringtun::x25519::PublicKey::from(theirs));
if !shared.was_contributory() {
return Err("REFUSED [holger-console-key-bad] the offered console key is a low-order point: no bearer is derived from it".to_string());
}
Ok(blake3::derive_key(BEARER_CONTEXT, shared.as_bytes())
.iter()
.map(|b| format!("{b:02x}"))
.collect())
}
/// **The console's bearer per box**, by every address the front reaches it at.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Bearers {
by_host: BTreeMap<Ipv4Addr, String>,
any: Option<String>,
}
impl Bearers {
/// Pure: the bearers of every box in `reply` that offered its key ([`OFFER_BOX_PUBKEY`]),
/// at its overlay and its utility address. A box whose key does not derive is left out
/// (and named in the second half).
pub fn of(my_private: &[u8; 32], reply: &JoinReply) -> (Bearers, Vec<String>) {
let mut b = Bearers::default();
let mut refused = Vec::new();
for p in reply
.peers
.iter()
.filter(|p| matches!(p.role, Role::Primary | Role::Twin))
{
let Some(theirs) = reply
.feed
.get(&nordisk_pairing::bootstrap::fed_offer_key(
&p.name,
OFFER_BOX_PUBKEY,
))
.filter(|v| !v.trim().is_empty())
else {
continue;
};
match bearer(my_private, theirs) {
Ok(t) => {
b.by_host.insert(p.overlay, t.clone());
if let Some(u) = p.utility {
b.by_host.insert(u, t);
}
}
Err(e) => refused.push(format!("{}: {e}", p.name)),
}
}
(b, refused)
}
/// One bearer for every address (a test's boxes, which share one login).
pub fn every(bearer: impl Into<String>) -> Bearers {
Bearers {
by_host: BTreeMap::new(),
any: Some(bearer.into()),
}
}
/// The bearer for the box at `ctl` (`<ip>:<port>` or a bare address).
pub fn at(&self, ctl: &str) -> Option<&str> {
let host = ctl
.rsplit_once(':')
.map_or(ctl, |(h, _)| h)
.trim_matches(['[', ']']);
host.parse::<Ipv4Addr>()
.ok()
.and_then(|ip| self.by_host.get(&ip))
.or(self.any.as_ref())
.map(String::as_str)
}
pub fn is_empty(&self) -> bool {
self.by_host.is_empty() && self.any.is_none()
}
}
/// **The bearers a running courier reads at every call** (gate A12, 2026-10-23): the
/// certify loop is started once per pair, when the verdict first names it, and the installer
/// may hand the front that verdict BEFORE the twin's own console key is relayed (its offer
/// rides the twin's next join). A loop that held the bearers of that first feed asked the
/// twin with no bearer for ever ("the box has not offered its console key"); this one is
/// followed by every later feed ([`LiveBearers::follow`]).
#[derive(Debug, Clone, Default)]
pub struct LiveBearers(std::sync::Arc<RwLock<Bearers>>);
impl LiveBearers {
pub fn new(b: Bearers) -> LiveBearers {
LiveBearers(std::sync::Arc::new(RwLock::new(b)))
}
/// The bearer for the box at `ctl` now.
pub fn at(&self, ctl: &str) -> Option<String> {
self.0
.read()
.unwrap_or_else(|p| p.into_inner())
.at(ctl)
.map(str::to_string)
}
/// The bearers now.
pub fn now(&self) -> Bearers {
self.0.read().unwrap_or_else(|p| p.into_inner()).clone()
}
/// Take the bearers of a later feed `reply` under the front's key `key`. A feed that
/// derives none (no key adopted, nothing offered, every offer refused) keeps what is
/// held. True when the bearers changed.
pub fn follow(&self, key: Option<[u8; 32]>, reply: &JoinReply) -> bool {
let Ok(Some(b)) = bearers_with(key, reply) else {
return false;
};
let mut g = self.0.write().unwrap_or_else(|p| p.into_inner());
if *g == b {
return false;
}
*g = b;
true
}
}
impl From<Bearers> for LiveBearers {
fn from(b: Bearers) -> LiveBearers {
LiveBearers::new(b)
}
}
/// **The console's bearers for the verdict in `reply`**, from the adopted key: `Ok(None)`
/// while the key is not adopted or no box has offered its key yet.
pub fn bearers(reply: &JoinReply) -> Result<Option<Bearers>, String> {
bearers_with(adopted(), reply)
}
/// Pure: [`bearers`] with the front's private half `key`.
pub fn bearers_with(key: Option<[u8; 32]>, reply: &JoinReply) -> Result<Option<Bearers>, String> {
let Some(k) = key else { return Ok(None) };
let (b, refused) = Bearers::of(&k, reply);
if b.is_empty() {
return match refused.first() {
Some(e) => Err(e.clone()),
None => Ok(None),
};
}
Ok(Some(b))
}
#[cfg(test)]
mod tests {
use super::*;
/// The fixed pair both ends test against (holger `server/cli/src/paired.rs` holds the
/// same vector): the front's seed 0x11…, the box's seed 0x22….
pub const FRONT_SEED: [u8; 32] = [0x11; 32];
pub const BOX_SEED: [u8; 32] = [0x22; 32];
fn public(seed: &[u8; 32]) -> String {
wg_appliance_core::keys::encode_key(&wg_appliance_core::keys::public_from_private_bytes(
seed,
))
}
/// ★ Row 14: the front and the box derive ONE bearer from their two keys, each from its
/// own private half and the other's offered public half; another box gets another one.
#[test]
fn the_front_and_a_box_derive_the_same_bearer_and_no_other_box_does() {
let at_front = bearer(&FRONT_SEED, &public(&BOX_SEED)).unwrap();
let at_box = bearer(&BOX_SEED, &public(&FRONT_SEED)).unwrap();
assert_eq!(at_front, at_box);
assert_eq!(at_front.len(), 64);
assert_eq!(at_front, KNOWN, "the vector the box side holds too");
assert_ne!(bearer(&FRONT_SEED, &public(&[0x33; 32])).unwrap(), at_front);
assert!(bearer(&FRONT_SEED, "not a key")
.unwrap_err()
.contains("[holger-console-key-bad]"));
assert!(bearer(
&FRONT_SEED,
&wg_appliance_core::keys::encode_key(&[0u8; 32])
)
.unwrap_err()
.contains("low-order"));
}
/// The bearer of the fixed pair.
const KNOWN: &str = "52140321f476b69df5fde703935f0682beaae18f61d8f12591d3bc0784485336";
/// ★ The bearers come from each box's own offer in the feed, at its overlay and utility.
#[test]
fn each_box_gets_the_bearer_of_its_own_offer() {
use nordisk_pairing::join::{Assigned, Peer};
let peer = |name: &str, role, o: [u8; 4], u: Option<[u8; 4]>| Peer {
name: name.into(),
role,
tunnel_pubkey: String::new(),
overlay: o.into(),
endpoint: None,
utility: u.map(Into::into),
};
let mut feed = BTreeMap::new();
feed.insert(
nordisk_pairing::bootstrap::fed_offer_key("main", OFFER_BOX_PUBKEY),
public(&BOX_SEED),
);
feed.insert(
nordisk_pairing::bootstrap::fed_offer_key("twin", OFFER_BOX_PUBKEY),
public(&[0x33; 32]),
);
let reply = JoinReply {
estate: "holger.test".into(),
me: Assigned {
name: "front".into(),
overlay: [10, 99, 0, 1].into(),
},
cert_pem: None,
role: Some(Role::Front),
primary_overlay: None,
primary_utility: None,
peers: vec![
peer("front", Role::Front, [10, 99, 0, 1], None),
peer("main", Role::Primary, [10, 99, 0, 2], Some([10, 0, 0, 2])),
peer("twin", Role::Twin, [10, 99, 0, 3], None),
],
feed,
};
let (b, refused) = Bearers::of(&FRONT_SEED, &reply);
assert!(refused.is_empty(), "{refused:?}");
let main = bearer(&FRONT_SEED, &public(&BOX_SEED)).unwrap();
assert_eq!(b.at("10.99.0.2:50051"), Some(main.as_str()));
assert_eq!(
b.at("10.0.0.2:8443"),
Some(main.as_str()),
"the utility address too"
);
assert_eq!(
b.at("10.99.0.3:50051"),
Some(bearer(&FRONT_SEED, &public(&[0x33; 32])).unwrap().as_str())
);
assert_eq!(b.at("10.99.0.9:50051"), None, "no offer, no bearer");
assert_eq!(Bearers::every("t").at("10.99.0.9:1"), Some("t"));
}
}