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

nordisk/Skidbladnir

crates/installer/nordisk-pairing/src/join.rs

//! **The one-time join, over the installer's mutual TLS.** A box presents
//! itself to the front's door ONCE, over the utility network, before any
//! tunnel exists: what kind of box it is, its tunnel public key, what it
//! measured about itself, and a signing request for a key it minted itself.
//! The TLS handshake has already proven WHO is speaking ([`Presented`]): the
//! bootstrap certificate the medium carries, or a box's own certificate from
//! an earlier join. The door knows the box by the utility address it came
//! from (the laptop laid one SLOT per address), admits it, signs its own
//! certificate, and answers with its name and overlay address, its role once
//! the election has spoken, the peers, and the estate values it needs.
//!
//! **The bootstrap certificate is valid during bootstrap only** (owner
//! 2026-09-24), and the ledger is where that is enforced:
//! * it may only JOIN a slot that has not joined — a second bootstrap join on
//!   a taken slot is `spent` (a copied medium);
//! * a box that has joined speaks with its OWN certificate, whose name must
//!   be its slot's — anything else is `forged`;
//! * when every slot has joined, the bootstrap certificate is `withdrawn`.
//!
//! The wire format is TOML, like the seed.

use crate::elect::Capacity;
use crate::seed::Kind;
use crate::Role;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;
use std::net::Ipv4Addr;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JoinRequest {
    pub estate: String,
    pub kind: Kind,
    /// The tunnel public key this box minted at first boot (empty with `tunnel = none`).
    pub tunnel_pubkey: String,
    /// What the box measured about itself — the election's input.
    pub capacity: Option<Capacity>,
    /// A signing request for the box's OWN key (PEM). Present on the first
    /// join; a box already holding its certificate may send it empty.
    #[serde(default)]
    pub csr_pem: String,
}

/// What the door assigned this box: its name and its address in the tunnel.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Assigned {
    pub name: String,
    pub overlay: Ipv4Addr,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Peer {
    pub name: String,
    pub role: Role,
    pub tunnel_pubkey: String,
    pub overlay: Ipv4Addr,
    pub endpoint: Option<String>,
    /// **Where the peer's control plane is on the utility network** (0.4.1):
    /// the front — vm1 on the same medium since 0.4.0 — certifies the twin
    /// against the primary over the utility network, and its reply carried no
    /// twin address. `None` for the front peer and for a 0.4.0 ledger.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub utility: Option<Ipv4Addr>,
}

/// **The front's tunnel identity, as the door FEEDS it** (owner 2026-09-24
/// evening: the medium is constant). The relay key is minted with the front
/// and the endpoint is the front's public address, so both change with every
/// lay; a seed that carried them made every lay a new medium. The laptop
/// writes this into the door's ledger when it prepares the installer, and
/// every join reply lists it as the peer named [`FRONT_PEER`] with role
/// `front` ([`JoinReply::front`]).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrontTunnel {
    /// The relay's public key (WireGuard's base64).
    pub tunnel_pubkey: String,
    /// Where a box dials it: `host:port`.
    pub endpoint: String,
}

/// The name the front goes by in a join reply's peer list.
pub const FRONT_PEER: &str = "front";

impl FrontTunnel {
    /// The front as a peer: the one dialled endpoint, the overlay's `.1`.
    pub fn as_peer(&self) -> Peer {
        Peer { name: FRONT_PEER.to_string(), role: Role::Front, tunnel_pubkey: self.tunnel_pubkey.clone(), overlay: crate::OVERLAY_FRONT, endpoint: Some(self.endpoint.clone()), utility: None }
    }

    /// Refused by name when it cannot be dialled.
    pub fn check(&self) -> Result<(), JoinError> {
        if self.tunnel_pubkey.trim().is_empty() {
            return Err(JoinError::Wire("the door's front tunnel key is empty".into()));
        }
        if self.endpoint.rsplit_once(':').and_then(|(_, p)| p.parse::<u16>().ok()).is_none() {
            return Err(JoinError::Wire(format!("the door's front endpoint `{}` is not host:port", self.endpoint)));
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JoinReply {
    pub estate: String,
    /// This box, as the door knows it.
    pub me: Assigned,
    /// The box's own certificate (PEM), signed from its request at the first
    /// admission. From now on the box speaks with it; the bootstrap
    /// certificate is refused for its slot.
    #[serde(default)]
    pub cert_pem: Option<String>,
    /// What this box IS, decided once. `None` = admitted, and the election has
    /// not spoken yet because the other gunnar box has not joined: ask again,
    /// with the box's own certificate.
    pub role: Option<Role>,
    /// Where the primary is inside the tunnel — the address a twin pairs against.
    pub primary_overlay: Option<Ipv4Addr>,
    /// Where the primary's CONTROL PLANE listens: its utility address. gunnar
    /// binds :50051 on the private (utility) interface only, so a twin that
    /// becomes the twin of the primary's OVERLAY address reaches nothing there
    /// (MEASURED 2026-09-24, read in the audit). Replication still prefers the
    /// tunnel through its own host list; the control plane is always utility.
    #[serde(default)]
    pub primary_utility: Option<Ipv4Addr>,
    pub peers: Vec<Peer>,
    /// **The estate values the installer feeds** — the monetize verifying key,
    /// the clock key, the admin keys, … — so the medium carries none of them.
    #[serde(default)]
    pub feed: BTreeMap<String, String>,
}

impl JoinRequest {
    pub fn render(&self) -> Result<String, JoinError> {
        toml::to_string(self).map_err(|e| JoinError::Wire(e.to_string()))
    }

    pub fn parse(text: &str) -> Result<JoinRequest, JoinError> {
        toml::from_str(text).map_err(|e| JoinError::Wire(format!("the body is not a join request: {e}")))
    }
}

impl JoinReply {
    pub fn parse(text: &str) -> Result<JoinReply, JoinError> {
        toml::from_str(text).map_err(|e| JoinError::Wire(format!("the join reply is not a join reply: {e}")))
    }

    /// **The front, as the door fed it** — the peer with role `front`: its
    /// tunnel key and the endpoint to dial. `None` from a door that was given
    /// no front tunnel (a `tunnel = none` estate, or a ledger laid without it).
    pub fn front(&self) -> Option<&Peer> {
        self.peers.iter().find(|p| p.role == Role::Front)
    }

    pub fn render(&self) -> Result<String, JoinError> {
        toml::to_string(self).map_err(|e| JoinError::Wire(e.to_string()))
    }
}

/// **Who is speaking**, as the TLS handshake proved it: the bootstrap
/// certificate the medium carries, or a box's own certificate (its CN).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Presented {
    Bootstrap,
    Box(String),
}

impl Presented {
    /// From the certificate's CN (`crate::mtls::peer_cn`).
    pub fn from_cn(cn: &str) -> Presented {
        if cn == crate::mtls::BOOTSTRAP_CN {
            Presented::Bootstrap
        } else {
            Presented::Box(cn.to_string())
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JoinError {
    Wire(String),
    /// The address is not a slot this estate laid.
    Unknown(Ipv4Addr),
    /// The slot has joined, and the bootstrap certificate came again — a copied medium.
    Spent { name: String, from: Ipv4Addr },
    /// The certificate does not belong to this slot, or the box is not the kind its slot is.
    Forged(String),
    /// Every slot has joined: the bootstrap certificate is no longer accepted anywhere.
    Withdrawn,
    WrongEstate { want: String, got: String },
    /// The door could not sign the box's certificate.
    Sign(String),
    /// `Door::forget` was asked about a name the estate never laid.
    NotLaid(String),
}

impl fmt::Display for JoinError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            JoinError::Wire(e) => write!(f, "REFUSED [join-wire] the join message could not be rendered or read: {e}"),
            JoinError::Unknown(a) => write!(f, "REFUSED [join-unknown] {a} is not an address this estate laid a box on"),
            JoinError::Spent { name, from } => write!(f, "REFUSED [join-spent] `{name}` ({from}) has already joined; the bootstrap certificate is not accepted for it again — a copied medium"),
            JoinError::Forged(why) => write!(f, "REFUSED [join-forged] {why}"),
            JoinError::Withdrawn => write!(f, "REFUSED [join-withdrawn] every box has joined; the bootstrap certificate is valid during bootstrap only"),
            JoinError::WrongEstate { want, got } => write!(f, "REFUSED [join-estate] this is `{want}`, the request says `{got}`"),
            JoinError::Sign(e) => write!(f, "REFUSED [join-sign] {e}"),
            JoinError::NotLaid(n) => write!(f, "REFUSED [door-forget-unknown] `{n}` is not a box this estate laid; there is no admission to clear"),
        }
    }
}

impl std::error::Error for JoinError {}

/// One box the laptop laid: what the door will call it and where it goes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Slot {
    pub name: String,
    pub kind: Kind,
    pub overlay: Ipv4Addr,
    /// The front's own tunnel endpoint, for the peer list (only the front has one).
    #[serde(default)]
    pub endpoint: Option<String>,
    /// **Where the box is on the utility network** (0.3.0). Since the
    /// installer connects OUT to each VM, a slot is keyed by the address the
    /// installer DIALS — the VM's public one — while the boxes reach each
    /// other's control planes on the utility network. `None` = the key is the
    /// utility address (the 0.2 join, where the box dialled the door).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub utility: Option<Ipv4Addr>,
}

impl Slot {
    /// The slot's utility address: its own, or the key it is laid under.
    pub fn utility_or(&self, key: Ipv4Addr) -> Ipv4Addr {
        self.utility.unwrap_or(key)
    }
}

/// What a box presented when it was admitted.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Admitted {
    pub from: Ipv4Addr,
    pub tunnel_pubkey: String,
    pub capacity: Option<Capacity>,
}

/// What an admission did.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Admission {
    /// The first join of this slot: sign the box's request.
    First(String),
    /// The box, speaking with its own certificate: a read.
    Again(String),
}

impl Admission {
    pub fn name(&self) -> &str {
        match self {
            Admission::First(n) | Admission::Again(n) => n,
        }
    }
}

/// **The front's ledger**: one slot per utility address, each joined once.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ledger {
    pub estate: String,
    /// utility address → the box laid there
    pub slots: BTreeMap<Ipv4Addr, Slot>,
    /// name → what it presented, once admitted
    pub admitted: BTreeMap<String, Admitted>,
}

impl Ledger {
    pub fn new(estate: &str) -> Ledger {
        Ledger { estate: estate.to_string(), ..Default::default() }
    }

    pub fn lay(&mut self, utility: Ipv4Addr, slot: Slot) {
        self.slots.insert(utility, slot);
    }

    /// **The rule of the bootstrap certificate**, in one place. The checks are
    /// in the order that leaks the least.
    pub fn admit(&mut self, req: &JoinRequest, who: &Presented, from: Ipv4Addr) -> Result<Admission, JoinError> {
        if req.estate != self.estate {
            return Err(JoinError::WrongEstate { want: self.estate.clone(), got: req.estate.clone() });
        }
        if *who == Presented::Bootstrap && self.complete() {
            return Err(JoinError::Withdrawn);
        }
        let slot = self.slots.get(&from).ok_or(JoinError::Unknown(from))?.clone();
        // 0.4.0 (owner 2026-09-25, the one-medium ruling): the FRONT's role is
        // an install parameter — vm1 boots the same medium as the boxes, and
        // the slot the installer laid makes it the front. Any other slot still
        // demands its own kind.
        if req.kind != slot.kind && slot.kind != Kind::Front {
            return Err(JoinError::Forged(format!("{from} presents kind {:?}; the slot `{}` laid there is {:?}", req.kind, slot.name, slot.kind)));
        }
        let joined = self.admitted.contains_key(&slot.name);
        match (who, joined) {
            (Presented::Bootstrap, true) => Err(JoinError::Spent { name: slot.name, from }),
            (Presented::Bootstrap, false) => {
                if req.csr_pem.trim().is_empty() {
                    return Err(JoinError::Forged(format!("the first join from {from} carries no signing request; a box must mint its own key")));
                }
                self.admitted.insert(slot.name.clone(), Admitted { from, tunnel_pubkey: req.tunnel_pubkey.clone(), capacity: req.capacity });
                Ok(Admission::First(slot.name))
            }
            (Presented::Box(cn), true) if *cn == slot.name && self.admitted[&slot.name].from == from => {
                // ★ **A tunnel key learned late is recorded — once, and only
                // into an empty field.** MEASURED 2026-09-24 on gunnar.rs: the
                // boxes' first join carried NO tunnel key (the loop minted it
                // when it raised the tunnel, after the join), so the relay had
                // nobody to list and the twin never reached the primary's
                // overlay address. The box asking again AS ITSELF (its own
                // certificate) may fill the empty field; a key already recorded
                // is never replaced — that would let a box re-key itself away
                // from what the relay holds.
                let entry = self.admitted.get_mut(&slot.name).expect("joined was just checked");
                if entry.tunnel_pubkey.is_empty() && !req.tunnel_pubkey.is_empty() {
                    entry.tunnel_pubkey = req.tunnel_pubkey.clone();
                }
                Ok(Admission::Again(slot.name))
            }
            (Presented::Box(cn), _) => Err(JoinError::Forged(format!("{from} is `{}`'s address, and it presents the certificate of `{cn}`", slot.name))),
        }
    }

    /// The slot of a box, by name.
    pub fn slot_of(&self, name: &str) -> Option<(&Ipv4Addr, &Slot)> {
        self.slots.iter().find(|(_, s)| s.name == name)
    }

    /// Every laid slot has a box in it.
    pub fn complete(&self) -> bool {
        !self.slots.is_empty() && self.slots.values().all(|s| self.admitted.contains_key(&s.name))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const MAIN_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 90);
    const TWIN_UTIL: Ipv4Addr = Ipv4Addr::new(10, 13, 9, 87);

    fn ledger() -> Ledger {
        let mut l = Ledger::new("gunnar.rs");
        l.lay(MAIN_UTIL, Slot { name: "main".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_A, endpoint: None, utility: None });
        l.lay(TWIN_UTIL, Slot { name: "twin".into(), kind: Kind::Gunnar, overlay: crate::OVERLAY_BOX_B, endpoint: None, utility: None });
        l
    }

    fn req() -> JoinRequest {
        JoinRequest {
            estate: "gunnar.rs".into(),
            kind: Kind::Gunnar,
            tunnel_pubkey: "K1".into(),
            capacity: Some(Capacity { cores: 2, ram_bytes: 4 << 30, data_bytes: 10 << 30, disk_mib_s: 400 }),
            csr_pem: "-----BEGIN CERTIFICATE REQUEST-----".into(),
        }
    }

    #[test]
    fn the_bootstrap_certificate_joins_once_then_the_box_speaks_as_itself_and_at_completion_bootstrap_is_withdrawn() {
        let mut l = ledger();
        assert_eq!(l.admit(&req(), &Presented::Bootstrap, MAIN_UTIL), Ok(Admission::First("main".into())));
        assert_eq!(l.admit(&req(), &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Spent { name: "main".into(), from: MAIN_UTIL }), "a copied medium");
        assert_eq!(l.admit(&req(), &Presented::Box("main".into()), MAIN_UTIL), Ok(Admission::Again("main".into())));
        assert!(matches!(l.admit(&req(), &Presented::Box("main".into()), TWIN_UTIL), Err(JoinError::Forged(_))), "main's certificate from twin's address");
        assert!(matches!(l.admit(&req(), &Presented::Box("twin".into()), TWIN_UTIL), Err(JoinError::Forged(_))), "a certificate for a slot that never joined");
        assert_eq!(l.admit(&req(), &Presented::Bootstrap, TWIN_UTIL), Ok(Admission::First("twin".into())));
        assert!(l.complete());
        assert_eq!(l.admit(&req(), &Presented::Bootstrap, Ipv4Addr::new(10, 13, 9, 1)), Err(JoinError::Withdrawn), "bootstrap only during bootstrap");
        assert_eq!(l.admit(&req(), &Presented::Box("twin".into()), TWIN_UTIL), Ok(Admission::Again("twin".into())), "a joined box still reads");
    }

    #[test]
    fn a_box_asking_as_itself_fills_an_empty_tunnel_key_once_and_never_replaces_one() {
        let mut l = ledger();
        let mut first = req();
        first.tunnel_pubkey.clear();
        l.admit(&first, &Presented::Bootstrap, MAIN_UTIL).unwrap();
        assert_eq!(l.admitted["main"].tunnel_pubkey, "");
        l.admit(&req(), &Presented::Box("main".into()), MAIN_UTIL).unwrap();
        assert_eq!(l.admitted["main"].tunnel_pubkey, "K1", "learned late, recorded");
        let mut other = req();
        other.tunnel_pubkey = "K2".into();
        l.admit(&other, &Presented::Box("main".into()), MAIN_UTIL).unwrap();
        assert_eq!(l.admitted["main"].tunnel_pubkey, "K1", "never replaced");
    }

    #[test]
    fn an_unlaid_address_a_wrong_kind_another_estate_and_a_join_without_a_request_are_refused_by_name() {
        let mut l = ledger();
        assert_eq!(l.admit(&req(), &Presented::Bootstrap, Ipv4Addr::new(10, 13, 9, 1)), Err(JoinError::Unknown(Ipv4Addr::new(10, 13, 9, 1))));
        let mut front = req();
        front.kind = Kind::Front;
        assert!(matches!(l.admit(&front, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Forged(_))));
        let mut other = req();
        other.estate = "holger.rs".into();
        assert!(matches!(l.admit(&other, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::WrongEstate { .. })));
        let mut bare = req();
        bare.csr_pem.clear();
        assert!(matches!(l.admit(&bare, &Presented::Bootstrap, MAIN_UTIL), Err(JoinError::Forged(_))));
        assert!(!l.complete());
        assert_eq!(Presented::from_cn("bootstrap"), Presented::Bootstrap);
        assert_eq!(Presented::from_cn("main"), Presented::Box("main".into()));
        assert!(JoinError::Withdrawn.to_string().starts_with("REFUSED [join-withdrawn]"));
    }

    #[test]
    fn the_front_tunnel_is_a_dialled_peer_and_an_undialable_one_is_refused() {
        let f = FrontTunnel { tunnel_pubkey: "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=".into(), endpoint: "95.111.211.189:51820".into() };
        assert_eq!(f.check(), Ok(()));
        let p = f.as_peer();
        assert_eq!((p.name.as_str(), p.role, p.overlay, p.endpoint.as_deref()), (FRONT_PEER, Role::Front, crate::OVERLAY_FRONT, Some("95.111.211.189:51820")));
        let reply = JoinReply { estate: "e".into(), me: Assigned { name: "main".into(), overlay: crate::OVERLAY_BOX_A }, cert_pem: None, role: None, primary_overlay: None, primary_utility: None, peers: vec![p.clone()], feed: BTreeMap::new() };
        assert_eq!(reply.front(), Some(&p));
        assert!(FrontTunnel { tunnel_pubkey: String::new(), ..f.clone() }.check().is_err());
        assert!(FrontTunnel { endpoint: "nowhere".into(), ..f }.check().is_err());
    }

    #[test]
    fn the_messages_round_trip() {
        let r = req();
        assert_eq!(JoinRequest::parse(&r.render().unwrap()).unwrap(), r);
        let mut feed = BTreeMap::new();
        feed.insert("monetize_pubkey".to_string(), "eb16…".to_string());
        let reply = JoinReply { estate: "gunnar.rs".into(), me: Assigned { name: "main".into(), overlay: crate::OVERLAY_BOX_A }, cert_pem: Some("PEM".into()), role: None, primary_overlay: None, primary_utility: None, peers: vec![], feed };
        assert_eq!(JoinReply::parse(&reply.render().unwrap()).unwrap(), reply);
    }
}