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/tests/the_front_renews_every_paired_link.rs

//! ★ **The front renews every paired link before it ends** (lane E2, 2026-10-19) — the
//! installer's door to a REAL Holger box half (`boxside::Installed` with the renewal steps,
//! `renewing::Renewing`), served on loopback exactly as a closed box serves its log door,
//! with the clock injected where a life is judged:
//!
//! 1. a link renews before expiry and keeps serving: not due at once, due at two thirds of
//!    its life, the same key and name with a later end, the fresh bootstrap kept in the
//!    seed, the same open door answering before and after;
//! 2. an expired link without renewal is refused by name (`front-renew-expired`);
//! 3. a failed renewal is retried (10 min, doubling) and, from the third failure in a row,
//!    alerted (`ALERT [front-renew]`); a look that goes well resets it;
//! 4. re-pair after a key loss: the box joins again on the fresh bootstrap its seed kept
//!    and is renewed again; another key's certificate and a box that never joined are
//!    refused by name;
//! 5. the twin is re-certified at two thirds of its life through the same three steps.

use std::collections::BTreeMap;
use std::net::{Ipv4Addr, SocketAddrV4};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use holger_estate_front::renew::{self, Looked};
use holger_estate_front::renewing::Renewing;
use nordisk_pairing::bootlog::BootLog;
use nordisk_pairing::bootstrap::Vm;
use nordisk_pairing::boxside::{self, Installed, Product};
use nordisk_pairing::installer::{Client, Material};
use nordisk_pairing::installer_link::DirectPath;
use nordisk_pairing::join::JoinReply;
use nordisk_pairing::mtls::{self, InstallerCa};
use nordisk_pairing::seed::{Kind, Seed};

const DAY: i64 = 86_400;
const SEED_REL: &str = "pairing/seed.toml";
static LOG: BootLog = BootLog::new(64 * 1024);

struct Box_;
impl Product for Box_ {
    fn kind(&self) -> Kind {
        Kind::Holger
    }
    fn tunnel_pubkey(&self) -> String {
        String::new()
    }
    fn capacity(&self) -> Result<Option<nordisk_pairing::elect::Capacity>, String> {
        Ok(None)
    }
    fn offer(&self, _: Option<&JoinReply>) -> BTreeMap<String, String> {
        BTreeMap::new()
    }
    fn fed(&self, _: &JoinReply) {}
}

fn seed(ca: &InstallerCa) -> Seed {
    let boot = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
    Seed {
        estate: "holger.test".into(),
        tunnel: nordisk_pairing::seed::TunnelKind::Wireguard,
        front: Default::default(),
        kind: 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,
        // The link's keys are only checked for shape here: the test reaches the box's door
        // directly on loopback, as the front reaches its own.
        installer_link: Some(nordisk_pairing::seed::LinkSeed {
            installer_pubkey: "A".repeat(43) + "=",
            vm_private: "B".repeat(43) + "=",
            port: nordisk_pairing::installer_link::LINK_PORT,
            psk: None,
        }),
    }
}

/// A closed Holger box named `main` under `root`: its own key, its own certificate for
/// `life`, the seed on disk, serving its log door on loopback. Answers where.
fn closed_box(root: &Path, ca: &InstallerCa, life: Duration) -> SocketAddrV4 {
    let seed = seed(ca);
    boxside::write_atomic(root, SEED_REL, seed.render().unwrap().as_bytes(), true).unwrap();
    let installed = Arc::new(Installed::open(root, seed.clone(), Box_).unwrap());
    let key = std::fs::read_to_string(root.join(boxside::OWN_KEY_REL)).unwrap();
    let (csr, _) = mtls::box_request_for(&key).unwrap();
    boxside::write_atomic(
        root,
        boxside::OWN_CERT_REL,
        ca.sign_box(&csr, "main", life).unwrap().as_bytes(),
        true,
    )
    .unwrap();
    installed.close().unwrap();
    serve(
        Arc::new(Renewing::new(installed, root, SEED_REL, &seed)),
        &ca.cert_pem,
    )
}

fn serve(vm: Arc<dyn Vm>, ca_pem: &str) -> SocketAddrV4 {
    let listener = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
    let at = match listener.local_addr().unwrap() {
        std::net::SocketAddr::V4(a) => a,
        other => panic!("{other}"),
    };
    let ca_pem = ca_pem.to_string();
    std::thread::spawn(move || {
        nordisk_pairing::bootstrap::serve_after_close(vm.as_ref(), &LOG, &ca_pem, listener, &|_| {})
    });
    at
}

fn material(ca: &InstallerCa) -> Material {
    Material {
        ca: ca.clone(),
        me: ca.issue_installer(Duration::from_secs(3600)).unwrap(),
        link: None,
    }
}

fn path(at: SocketAddrV4) -> Vec<(String, Box<dyn nordisk_pairing::installer_link::Path>)> {
    vec![("main".to_string(), Box::new(DirectPath { at }))]
}

fn own(root: &Path) -> String {
    std::fs::read_to_string(root.join(boxside::OWN_CERT_REL)).unwrap()
}

fn served(at: SocketAddrV4, m: &Material) -> String {
    Client::new(
        Box::new(DirectPath { at }),
        &m.ca.cert_pem,
        &m.me,
        Duration::from_secs(10),
    )
    .unwrap()
    .cert()
    .unwrap()
}

/// ★ 1. **A link renews before it ends and keeps serving.**
#[test]
fn a_box_link_renews_at_two_thirds_of_its_life_and_keeps_serving() {
    let (dir, front) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
    let ca = InstallerCa::mint("holger.test", renew::BOX_VALID).unwrap();
    let at = closed_box(dir.path(), &ca, renew::BOX_VALID);
    let m = material(&ca);
    let first = own(dir.path());
    let boot_before = Seed::parse(&std::fs::read_to_string(dir.path().join(SEED_REL)).unwrap())
        .unwrap()
        .bootstrap_cert_pem;
    assert_eq!(
        served(at, &m).trim(),
        first.trim(),
        "the box serves its certificate"
    );

    let now = mtls::now_unix();
    let r = renew::look(front.path(), renew::LINK_BOXES, now, || {
        renew::renew_boxes(&m, path(at), renew::BOX_VALID, now)
    })
    .unwrap();
    assert_eq!(
        r,
        Looked::Done("1 box(es) looked at, none due".into()),
        "fresh: not due"
    );
    assert_eq!(
        own(dir.path()),
        first,
        "nothing changed while it was not due"
    );
    assert_eq!(
        renew::look(front.path(), renew::LINK_BOXES, now + 3600, || panic!(
            "asked before the next look"
        ))
        .unwrap(),
        Looked::NotDue,
        "the next look is six hours on"
    );

    // Day 121 of 180: past two thirds.
    let due = now + 121 * DAY;
    let r = renew::look(front.path(), renew::LINK_BOXES, due, || {
        renew::renew_boxes(&m, path(at), renew::BOX_VALID, due)
    })
    .unwrap();
    assert_eq!(r, Looked::Done("renewed: main".into()));
    let renewed = own(dir.path());
    assert_ne!(renewed, first, "a new certificate");
    assert_eq!(
        mtls::renewal_refusal(&first, &renewed),
        None,
        "the same key, the same name, an end no earlier"
    );
    // Renewed seconds after the first was signed (the clock is only advanced for the due
    // check), so the new end is the first one or later — never earlier.
    assert!(
        mtls::cert_left(&renewed, now).unwrap() >= mtls::cert_left(&first, now).unwrap(),
        "an end no earlier"
    );
    assert_eq!(
        served(at, &m).trim(),
        renewed.trim(),
        "the same open door serves the renewed certificate"
    );
    let boot_after =
        Seed::parse(&std::fs::read_to_string(dir.path().join(SEED_REL)).unwrap()).unwrap();
    assert_ne!(
        boot_after.bootstrap_cert_pem, boot_before,
        "a fresh bootstrap is kept in the seed for the next join"
    );
    assert_eq!(
        mtls::cert_cn(&boot_after.bootstrap_cert_pem).unwrap(),
        mtls::BOOTSTRAP_CN
    );
    assert_eq!(boot_after.estate, "holger.test", "only the pair changed");
}

/// ★ 2. **An expired link without renewal is refused by name.**
#[test]
fn an_ended_box_certificate_that_was_never_renewed_is_refused_by_name() {
    let dir = tempfile::tempdir().unwrap();
    let ca = InstallerCa::mint("holger.test", renew::BOX_VALID).unwrap();
    let at = closed_box(dir.path(), &ca, Duration::from_secs(2));
    let m = material(&ca);
    std::thread::sleep(Duration::from_secs(3));
    let e = renew::renew_boxes(&m, path(at), renew::BOX_VALID, mtls::now_unix()).unwrap_err();
    assert!(
        e.contains("front-renew-expired") && e.contains("`main`"),
        "{e}"
    );
    assert!(e.starts_with("none renewed"), "{e}");
}

/// ★ 3. **A failed renewal is retried, then alerted; a good look resets it.**
#[test]
fn a_failed_renewal_is_retried_with_a_growing_wait_and_alerted_from_the_third_failure() {
    let front = tempfile::tempdir().unwrap();
    let ca = InstallerCa::mint("holger.test", renew::BOX_VALID).unwrap();
    let m = material(&ca);
    // A box nobody answers for.
    let gone = {
        let l = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
        match l.local_addr().unwrap() {
            std::net::SocketAddr::V4(a) => a,
            other => panic!("{other}"),
        }
    };
    let now = mtls::now_unix();
    let fail = |t: i64| {
        renew::look(front.path(), renew::LINK_BOXES, t, || {
            renew::renew_boxes(&m, path(gone), renew::BOX_VALID, t)
        })
    };
    assert!(
        fail(now).unwrap_err().contains("`main`"),
        "the box is named"
    );
    let s = renew::State::load(front.path()).links[renew::LINK_BOXES].clone();
    assert_eq!(
        (s.failures, s.next),
        (1, now + 600),
        "retried in ten minutes"
    );
    assert!(
        s.alert(renew::LINK_BOXES).is_none(),
        "one failure is no alert"
    );
    assert_eq!(
        renew::look(front.path(), renew::LINK_BOXES, now + 599, || panic!(
            "asked before the retry"
        ))
        .unwrap(),
        Looked::NotDue
    );
    assert!(fail(now + 600).is_err());
    assert_eq!(
        renew::State::load(front.path()).links[renew::LINK_BOXES].next,
        now + 600 + 1200,
        "the wait doubles"
    );
    assert!(fail(now + 1800).is_err());
    let s = renew::State::load(front.path()).links[renew::LINK_BOXES].clone();
    let alert = s
        .alert(renew::LINK_BOXES)
        .expect("the third failure in a row is an alert");
    assert!(
        alert.contains("ALERT [front-renew]")
            && alert.contains("3 times")
            && alert.contains("`main`"),
        "{alert}"
    );
    // The box answers again: the next look renews (nothing due) and the alert is gone.
    let r = renew::look(front.path(), renew::LINK_BOXES, now + 1800 + 2400, || {
        Ok("renewed: main".into())
    })
    .unwrap();
    assert_eq!(r, Looked::Done("renewed: main".into()));
    let s = renew::State::load(front.path()).links[renew::LINK_BOXES].clone();
    assert_eq!((s.failures, s.alert(renew::LINK_BOXES)), (0, None));
    assert_eq!(
        s.next,
        now + 1800 + 2400 + renew::RENEW_EVERY.as_secs() as i64
    );
}

/// ★ 4. **Re-pair after a key loss**; a stranger's certificate is refused by name.
#[test]
fn a_box_that_lost_its_key_re_pairs_on_the_fresh_bootstrap_and_a_stranger_is_refused() {
    let dir = tempfile::tempdir().unwrap();
    let ca = InstallerCa::mint("holger.test", renew::BOX_VALID).unwrap();
    let at = closed_box(dir.path(), &ca, renew::BOX_VALID);
    let m = material(&ca);
    let due = mtls::now_unix() + 121 * DAY;
    renew::renew_boxes(&m, path(at), renew::BOX_VALID, due).unwrap();
    let handed = Seed::parse(&std::fs::read_to_string(dir.path().join(SEED_REL)).unwrap()).unwrap();

    // Another key's certificate under the box's name is no renewal: refused by name.
    let mut c = Client::new(
        Box::new(DirectPath { at }),
        &m.ca.cert_pem,
        &m.me,
        Duration::from_secs(10),
    )
    .unwrap();
    let (other_csr, _) = mtls::box_request().unwrap();
    let stranger = ca.sign_box(&other_csr, "main", renew::BOX_VALID).unwrap();
    let e = c.renew(&stranger).unwrap_err();
    assert!(e.contains("renew-other-key"), "{e}");

    // The box loses its key (its pairing state is gone, the seed it kept stays).
    for rel in boxside::PAIRING_STATE {
        let _ = std::fs::remove_file(dir.path().join(rel));
    }
    let kept_seed =
        Seed::parse(&std::fs::read_to_string(dir.path().join(SEED_REL)).unwrap()).unwrap();
    let again = Arc::new(Installed::open(dir.path(), kept_seed.clone(), Box_).unwrap());
    // It joins on the FRESH bootstrap its seed kept (the medium's may have ended).
    assert_eq!(
        again.identity().cert_pem,
        handed.bootstrap_cert_pem,
        "the fresh bootstrap is what it presents"
    );
    assert!(
        mtls::bootstrap_refusal(&again.identity()).is_none(),
        "a bootstrap pair with its own key"
    );
    let hello = again.hello().unwrap();
    assert!(!hello.csr_pem.is_empty(), "a request for its NEW key");
    // A box that has not joined yet has nothing to renew: refused by name.
    let unjoined = Renewing::new(again.clone(), dir.path(), SEED_REL, &kept_seed);
    assert!(unjoined
        .renewed("x")
        .unwrap_err()
        .contains("renew-not-joined"));
    // The installer signs the request (the join), and the box is renewed again later.
    let signed = ca
        .sign_box(&hello.csr_pem, "main", renew::BOX_VALID)
        .unwrap();
    boxside::write_atomic(dir.path(), boxside::OWN_CERT_REL, signed.as_bytes(), true).unwrap();
    again.close().unwrap();
    let at2 = serve(
        Arc::new(Renewing::new(again, dir.path(), SEED_REL, &kept_seed)),
        &ca.cert_pem,
    );
    assert_eq!(
        renew::renew_boxes(&m, path(at2), renew::BOX_VALID, due).unwrap(),
        "renewed: main",
        "the re-paired box renews"
    );
    assert_eq!(
        mtls::renewal_refusal(&signed, &own(dir.path())),
        None,
        "its new key, renewed"
    );
}

/// The twin's three steps, recorded.
#[derive(Default)]
struct Steps(Mutex<Vec<String>>);

impl nordisk_twin::certify::Boxes for Steps {
    fn become_twin(&self, _: &str, _: &str) -> Result<String, String> {
        Err("never asked".into())
    }
    fn request(&self, reader: &str, purpose: &str) -> Result<String, String> {
        self.0
            .lock()
            .unwrap()
            .push(format!("request {reader} {purpose}"));
        Ok("CSR".into())
    }
    fn issue(
        &self,
        source: &str,
        principal: &str,
        _: &str,
    ) -> Result<nordisk_twin::certify::Issued, String> {
        self.0
            .lock()
            .unwrap()
            .push(format!("issue {source} {principal}"));
        Ok(nordisk_twin::certify::Issued {
            cert_pem: "LEAF".into(),
            issuer_pem: "CA".into(),
            port: 50052,
            hosts: vec!["10.44.0.2".into()],
        })
    }
    fn install(
        &self,
        reader: &str,
        _: &str,
        _: &nordisk_twin::certify::Issued,
    ) -> Result<i64, String> {
        self.0.lock().unwrap().push(format!("install {reader}"));
        Ok(365)
    }
}

/// ★ 5. **The twin is re-certified at two thirds of its life**, through the same three steps.
#[test]
fn the_twin_is_re_certified_at_two_thirds_of_its_life() {
    let front = tempfile::tempdir().unwrap();
    let t = nordisk_twin::certify::Target {
        reader_ctl: "10.44.0.3:50051".into(),
        source_ctl: "10.44.0.2:50051".into(),
        become_twin_of: None,
        principal: holger_estate_front::certify::TWIN_PRINCIPAL.into(),
        purpose: holger_estate_front::certify::PURPOSE.into(),
    };
    let steps = Steps::default();
    let now = mtls::now_unix();
    assert!(
        renew::renew_twin(front.path(), &steps, &t, now)
            .unwrap()
            .contains("not certified yet"),
        "the first certification is the role loop's"
    );
    holger_estate_front::certify::run_at(front.path(), &steps, &t, std::iter::empty(), now)
        .unwrap();
    steps.0.lock().unwrap().clear();
    assert!(renew::renew_twin(front.path(), &steps, &t, now + 200 * DAY)
        .unwrap()
        .contains("not due"));
    assert!(
        steps.0.lock().unwrap().is_empty(),
        "nothing asked while not due"
    );
    let later = now + 244 * DAY;
    let said = renew::renew_twin(front.path(), &steps, &t, later).unwrap();
    assert!(said.contains("re-certified"), "{said}");
    assert_eq!(
        *steps.0.lock().unwrap(),
        [
            "request 10.44.0.3:50051 twin",
            "issue 10.44.0.2:50051 holger-twin",
            "install 10.44.0.3:50051"
        ]
    );
    let rec = holger_estate_front::certify::Record::load(front.path()).unwrap();
    assert_eq!(
        (rec.certified_at, rec.not_after),
        (later, later + 365 * DAY),
        "the record moves to the new life"
    );
}