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