Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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"));
    }
}