crates/installer/nordisk-pairing/src/boxside/renewing.rs
//! **A box keeps the certificate its installer renews** (Holger's lane E2 on Gunnar's
//! lifecycle pattern; shared here since P4c, A28 M11 — Gunnar's box writes the same two
//! steps inline in its own `Vm`, T16).
//!
//! The shared box half ([`super::Installed`], and the front's `frontside::Front`) keeps the
//! certificate the installer signed at the join and nothing after it: asked to renew, it
//! answers `REFUSED [renew-not-kept]`. [`Renewing`] is the box as the installer sees it, with
//! the two renewal steps that half leaves out:
//!
//! * **`renewed`** — the installer re-signed this box's OWN key for a new lifetime
//! (`installer::renew`, at two thirds of the old one's life). The door already checked it
//! (`mtls::renewal_refusal`: the same key, the same name, an end no earlier). It is written
//! atomically over the old one, and the listener shows it from the next connection. Until
//! that write the old certificate is served and stays valid.
//! * **`rebootstrapped`** — the fresh bootstrap pair handed with each renewal is kept in the
//! seed on disk, so a box booted again from its medium joins after the medium's own
//! bootstrap has ended. A seed of another estate is refused by name.
//!
//! Everything else is the wrapped box, unchanged. No I/O but the box's own files.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::bootstrap::{SecretOffer, Vm};
use crate::join::{JoinReply, JoinRequest};
use crate::mtls::Issued;
use crate::seed::Seed;
use crate::signal::Signal;
/// **The box, keeping what its installer renews.** `seed_rel` is where the box's
/// seed lives under `root` (`pairing/seed.toml` on both Holger media).
pub struct Renewing<V: Vm + ?Sized> {
inner: Arc<V>,
root: PathBuf,
seed_rel: &'static str,
estate: String,
/// What the product adds to the pair status the installer reads (lane D10: a box's
/// server start, the front's certify attempts). `None`: the wrapped box's status.
status_of: Option<StatusOf>,
}
/// The product's hand on the pair status: the wrapped box's signal in, the one said out.
pub type StatusOf = Arc<dyn Fn(Signal) -> Signal + Send + Sync>;
impl<V: Vm + ?Sized> Renewing<V> {
pub fn new(inner: Arc<V>, root: &Path, seed_rel: &'static str, seed: &Seed) -> Renewing<V> {
Renewing { inner, root: root.to_path_buf(), seed_rel, estate: seed.estate.clone(), status_of: None }
}
/// The pair status through `f` (lane D10).
pub fn with_status(mut self, f: StatusOf) -> Renewing<V> {
self.status_of = Some(f);
self
}
}
/// Pure: **the seed on disk with the fresh bootstrap pair** — only the pair changes.
/// A seed of another estate is refused by name (it is not this box's).
pub fn rebootstrapped_seed(on_disk: &str, estate: &str, boot: &Issued) -> Result<String, String> {
let mut seed = Seed::parse(on_disk).map_err(|e| format!("REFUSED [rebootstrap-seed] the seed on disk: {e}"))?;
if seed.estate != estate {
return Err(format!("REFUSED [rebootstrap-seed] the seed on disk is of `{}`, this box is of `{estate}`", seed.estate));
}
seed.bootstrap_cert_pem = boot.cert_pem.clone();
seed.bootstrap_key_pem = boot.key_pem.clone();
seed.check().map_err(|e| format!("REFUSED [rebootstrap-seed] the seed with the fresh bootstrap: {e}"))?;
seed.render().map_err(|e| format!("REFUSED [rebootstrap-seed] the seed with the fresh bootstrap: {e}"))
}
impl<V: Vm + ?Sized> Vm for Renewing<V> {
fn hello(&self) -> Result<JoinRequest, String> {
self.inner.hello()
}
fn fed(&self, reply: &JoinReply) -> Result<(), String> {
self.inner.fed(reply)
}
fn status(&self) -> Signal {
let s = self.inner.status();
match &self.status_of {
Some(f) => f(s),
None => s,
}
}
fn close(&self) -> Result<(), String> {
self.inner.close()
}
fn identity(&self) -> Issued {
self.inner.identity()
}
fn offer(&self) -> BTreeMap<String, String> {
self.inner.offer()
}
fn secret_offer(&self) -> SecretOffer {
self.inner.secret_offer()
}
fn refed(&self, reply: &JoinReply) -> Result<(), String> {
self.inner.refed(reply)
}
fn live_running(&self) -> Result<crate::live::Running, String> {
self.inner.live_running()
}
fn live_stage(&self, stage: &crate::live::Stage) -> Result<String, String> {
self.inner.live_stage(stage)
}
fn live_stage_declared(&self, stage: &crate::live::Stage, declared: &crate::live::Declared) -> Result<String, String> {
self.inner.live_stage_declared(stage, declared)
}
/// The renewed certificate over the old one, atomically: the listener reads it
/// from the next connection. A box that holds no certificate of its own yet (it
/// never joined) has nothing to renew.
fn renewed(&self, cert_pem: &str) -> Result<(), String> {
let own = self.root.join(crate::boxside::OWN_CERT_REL);
if !std::fs::read_to_string(&own).is_ok_and(|c| c.contains("BEGIN CERTIFICATE")) {
return Err("REFUSED [renew-not-joined] this box holds no certificate of its own yet; it is renewed after its join".into());
}
crate::boxside::write_atomic(&self.root, crate::boxside::OWN_CERT_REL, cert_pem.as_bytes(), true).map_err(|e| format!("{}: {e}", own.display()))?;
eprintln!("pairing: this box's own certificate was RENEWED (the same key, a new lifetime)");
Ok(())
}
/// The fresh bootstrap pair into the seed on disk (`bootstrap_refusal` already
/// found a bootstrap certificate with its own key).
fn rebootstrapped(&self, boot: &Issued) -> Result<(), String> {
let path = self.root.join(self.seed_rel);
let on_disk = std::fs::read_to_string(&path).map_err(|e| format!("REFUSED [rebootstrap-seed] {}: {e}", path.display()))?;
let rendered = rebootstrapped_seed(&on_disk, &self.estate, boot)?;
crate::boxside::write_atomic(&self.root, self.seed_rel, rendered.as_bytes(), true).map_err(|e| format!("{}: {e}", path.display()))?;
eprintln!("pairing: this box keeps a FRESH bootstrap in its seed for its next join");
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn seed(estate: &str, ca: &crate::mtls::InstallerCa) -> Seed {
let boot = ca.issue_bootstrap(std::time::Duration::from_secs(3600)).unwrap();
Seed {
estate: estate.into(),
tunnel: crate::seed::TunnelKind::Wireguard,
front: Default::default(),
kind: crate::seed::Kind::Holger,
installer_ca_pem: ca.cert_pem.clone(),
bootstrap_cert_pem: boot.cert_pem,
bootstrap_key_pem: boot.key_pem,
peers_expected: 3,
mullvad: None,
tailscale: None,
installer_link: Some(crate::seed::LinkSeed { installer_pubkey: "A".repeat(43) + "=", vm_private: "B".repeat(43) + "=", port: crate::installer_link::LINK_PORT, psk: None }),
}
}
/// ★ Only the bootstrap pair changes; a seed of another estate is refused by name.
#[test]
fn the_seed_keeps_everything_but_its_fresh_bootstrap_and_refuses_another_estate() {
let ca = crate::mtls::InstallerCa::mint("acme.mock.invalid", std::time::Duration::from_secs(86_400)).unwrap();
let on_disk = seed("acme.mock.invalid", &ca).render().unwrap();
let fresh = ca.issue_bootstrap(std::time::Duration::from_secs(7200)).unwrap();
let after = Seed::parse(&rebootstrapped_seed(&on_disk, "acme.mock.invalid", &fresh).unwrap()).unwrap();
assert_eq!((after.bootstrap_cert_pem.as_str(), after.bootstrap_key_pem.as_str()), (fresh.cert_pem.as_str(), fresh.key_pem.as_str()));
assert_eq!((after.estate.as_str(), after.installer_ca_pem.as_str(), after.peers_expected), ("acme.mock.invalid", ca.cert_pem.as_str(), 3));
let other = rebootstrapped_seed(&on_disk, "other.mock.invalid", &fresh).unwrap_err();
assert!(other.starts_with("REFUSED [rebootstrap-seed]") && other.contains("acme.mock.invalid"), "{other}");
assert!(rebootstrapped_seed("not a seed", "acme.mock.invalid", &fresh).unwrap_err().starts_with("REFUSED [rebootstrap-seed]"));
}
}