use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
pub use crate::feedbus;
use crate::bootstrap::Vm;
use crate::join::{JoinReply, JoinRequest};
use crate::mtls::{self, Issued};
use crate::seed::{Kind, Seed};
use crate::signal::Signal;
pub const JOINED_REL: &str = "pairing/joined.toml";
pub const OWN_CERT_REL: &str = "pairing/own.pem";
pub const OWN_KEY_REL: &str = "pairing/own.key";
pub const SIGNAL_REL: &str = "pairing/signal.toml";
pub const CLOSED_REL: &str = "pairing/closed";
pub const PAIRING_STATE: [&str; 5] = [JOINED_REL, OWN_CERT_REL, OWN_KEY_REL, SIGNAL_REL, CLOSED_REL];
pub const ESP_CLEARED: &str = "korp/pairing.cleared";
pub fn write_atomic(root: &Path, rel: &str, bytes: &[u8], secret: bool) -> std::io::Result<()> {
appliance_pid1::durable::write_atomic(&root.join(rel), bytes, secret.then_some(0o600))
}
pub fn mark_esp(path: &Path) -> bool {
let mount = Path::new(appliance_pid1::stop_record::ESP_MOUNT);
mark_esp_on(mount, path, &|m: &Path| appliance_pid1::stop_record::with_esp_writable(m, || write_marker_file(path)))
}
pub fn mark_esp_on(mount: &Path, path: &Path, writable: &dyn Fn(&Path) -> bool) -> bool {
if path.starts_with(mount) {
writable(mount)
} else {
write_marker_file(path)
}
}
fn write_marker_file(path: &Path) -> bool {
appliance_pid1::durable::write_atomic(path, b"the pairing state of the previous installation was cleared\n", None).is_ok()
}
pub fn reset_if_fresh_install(esp: &Path, data: &Path, state: &[&str], write_marker: &dyn Fn(&Path) -> bool) -> Option<Vec<String>> {
if !esp.join("korp").is_dir() || esp.join(ESP_CLEARED).exists() {
return None;
}
if !write_marker(&esp.join(ESP_CLEARED)) {
tracing::error!("pairing: the first boot of this installation and the ESP marker could not be written — the pairing state is NOT cleared; the next boot asks again");
return None;
}
Some(state.iter().filter(|rel| std::fs::remove_file(data.join(rel)).is_ok()).map(|rel| rel.to_string()).collect())
}
pub fn reset_if_fresh_install_of(esp: &Path, data: &Path, state: &[&str], seed: &Seed, write_marker: &dyn Fn(&Path) -> bool) -> Option<Vec<String>> {
if !esp.join("korp").is_dir() || esp.join(ESP_CLEARED).exists() {
return None;
}
let kept = match crate::fed_at_rest::read_checked(data, JOINED_REL) {
Ok(t) => t.and_then(|t| JoinReply::parse(&t).ok()),
Err(e) => {
tracing::warn!(file = JOINED_REL, error = %e, "pairing: the first boot of this installation — the kept feed cannot be read, so the pairing state is cleared");
None
}
};
let ours = kept.as_ref().is_some_and(|r| r.estate == seed.estate && ((seed.kind == Kind::Front) == (r.role == Some(crate::Role::Front))));
if ours {
if !write_marker(&esp.join(ESP_CLEARED)) {
tracing::error!("pairing: a keep boot of this estate and the ESP marker could not be written — the pairing state is KEPT");
}
tracing::info!(estate = %seed.estate, "pairing: a keep boot of the same estate onto its kept data — the installer's feed, this box's key and certificate are KEPT");
return Some(Vec::new());
}
reset_if_fresh_install(esp, data, state, write_marker)
}
pub trait Product: Send + Sync {
fn kind(&self) -> Kind;
fn tunnel_pubkey(&self) -> String;
fn capacity(&self) -> Result<Option<crate::elect::Capacity>, String>;
fn offer(&self, fed: Option<&JoinReply>) -> BTreeMap<String, String>;
fn fed(&self, reply: &JoinReply);
fn may_be_front(&self) -> bool {
false
}
fn live_running(&self) -> Result<crate::live::Running, String> {
Err("REFUSED [live-unsupported] this VM takes no live update".into())
}
fn live_stage(&self, _stage: &crate::live::Stage) -> Result<String, String> {
Err("REFUSED [live-unsupported] this VM takes no live update".into())
}
fn live_stage_declared(&self, stage: &crate::live::Stage, declared: &crate::live::Declared) -> Result<String, String> {
if declared.is_empty() {
self.live_stage(stage)
} else {
Err(crate::live::declared_unsupported())
}
}
}
pub struct Installed<P: Product> {
root: PathBuf,
seed: Seed,
own_key: String,
fed: Mutex<Option<JoinReply>>,
bus: Arc<crate::feedbus::ReplyBus>,
pub product: P,
}
impl<P: Product> Installed<P> {
pub fn open(root: &Path, seed: Seed, product: P) -> Result<Installed<P>, String> {
let own_key = match std::fs::read_to_string(root.join(OWN_KEY_REL)) {
Ok(k) if k.contains("PRIVATE KEY") => k,
_ => {
let (_, key) = mtls::box_request().map_err(|e| e.to_string())?;
write_atomic(root, OWN_KEY_REL, key.as_bytes(), true).map_err(|e| format!("{}: {e}", root.join(OWN_KEY_REL).display()))?;
key
}
};
let fed = crate::fed_at_rest::read(root, JOINED_REL).and_then(|t| JoinReply::parse(&t).ok()).filter(|r| r.estate == seed.estate);
let bus = crate::feedbus::Bus::new();
if let Some(kept) = &fed {
bus.publish(kept.clone());
}
Ok(Installed { root: root.to_path_buf(), seed, own_key, fed: Mutex::new(fed), bus, product })
}
pub fn feed_bus(&self) -> &Arc<crate::feedbus::ReplyBus> {
&self.bus
}
pub fn latest(&self) -> Option<JoinReply> {
self.fed.lock().ok().and_then(|f| f.clone())
}
pub fn closed(&self) -> bool {
self.root.join(CLOSED_REL).exists()
}
pub fn seed(&self) -> &Seed {
&self.seed
}
pub fn installer_ca_pem(&self) -> &str {
&self.seed.installer_ca_pem
}
pub fn own_cert(&self) -> Option<String> {
std::fs::read_to_string(self.root.join(OWN_CERT_REL)).ok().filter(|c| c.contains("BEGIN CERTIFICATE"))
}
pub fn own_identity(&self) -> Option<Issued> {
self.own_cert().map(|cert_pem| Issued { cert_pem, key_pem: self.own_key.clone() })
}
}
pub fn refusal(seed: &Seed, reply: &JoinReply, may_be_front: bool) -> Option<String> {
if reply.estate != seed.estate {
return Some(format!("REFUSED [installer-estate] this VM is of `{}`, the installer fed `{}`", seed.estate, reply.estate));
}
if !may_be_front && reply.role == Some(crate::Role::Front) {
return Some(format!(
"REFUSED [product-medium-is-not-the-front] the installer fed `{}` role `front`, and this VM booted a PRODUCT medium (primary and twin only); vm1 boots the front medium",
reply.me.name
));
}
None
}
impl<P: Product> Vm for Installed<P> {
fn live_running(&self) -> Result<crate::live::Running, String> {
self.product.live_running()
}
fn live_stage(&self, stage: &crate::live::Stage) -> Result<String, String> {
self.product.live_stage(stage)
}
fn live_stage_declared(&self, stage: &crate::live::Stage, declared: &crate::live::Declared) -> Result<String, String> {
self.product.live_stage_declared(stage, declared)
}
fn hello(&self) -> Result<JoinRequest, String> {
let capacity = self.product.capacity()?;
let csr = if self.own_cert().is_some() { String::new() } else { mtls::box_request_for(&self.own_key).map_err(|e| e.to_string())?.0 };
Ok(JoinRequest { estate: self.seed.estate.clone(), kind: self.product.kind(), tunnel_pubkey: self.product.tunnel_pubkey(), capacity, csr_pem: csr })
}
fn fed(&self, reply: &JoinReply) -> Result<(), String> {
if let Some(why) = refusal(&self.seed, reply, self.product.may_be_front()) {
return Err(why);
}
if let Some(cert) = &reply.cert_pem {
write_atomic(&self.root, OWN_CERT_REL, cert.as_bytes(), true).map_err(|e| e.to_string())?;
}
let text = reply.render().map_err(|e| e.to_string())?;
crate::fed_at_rest::write(&self.root, JOINED_REL, &text)?;
*self.fed.lock().map_err(|e| e.to_string())? = Some(reply.clone());
self.product.fed(reply);
self.bus.publish(reply.clone());
Ok(())
}
fn status(&self) -> Signal {
std::fs::read_to_string(self.root.join(SIGNAL_REL))
.ok()
.and_then(|t| Signal::parse(&t).ok())
.unwrap_or_else(|| Signal::of(self.latest().map(|r| r.me.name).as_deref().unwrap_or("?"), self.seed.tunnel, &crate::pair::Machine::default()))
}
fn close(&self) -> Result<(), String> {
write_atomic(&self.root, CLOSED_REL, b"closed by the installer\n", false).map_err(|e| e.to_string())
}
fn identity(&self) -> Issued {
self.own_identity().unwrap_or_else(|| Issued { cert_pem: self.seed.bootstrap_cert_pem.clone(), key_pem: self.seed.bootstrap_key_pem.clone() })
}
fn offer(&self) -> BTreeMap<String, String> {
self.product.offer(self.latest().as_ref())
}
fn refed(&self, reply: &JoinReply) -> Result<(), String> {
if let Some(why) = refusal(&self.seed, reply, self.product.may_be_front()).or_else(|| crate::bootstrap::refeed_refusal(self.latest().as_ref(), reply)) {
return Err(why);
}
self.fed(reply)
}
}
#[cfg(feature = "wireguard")]
pub fn serve_installer(vm: std::sync::Arc<dyn Vm>, seed: Seed, log: &'static crate::bootlog::BootLog, say: std::sync::Arc<dyn Fn(&str) + Send + Sync>) -> std::io::Result<()> {
serve_installer_as(vm, seed, log, false, std::sync::Arc::new(move |_, l: &str| say(l)))
}
#[cfg(feature = "wireguard")]
pub type Say = std::sync::Arc<dyn Fn(crate::bootstrap::Tone, &str) + Send + Sync>;
#[cfg(feature = "wireguard")]
pub fn serve_installer_toned(vm: std::sync::Arc<dyn Vm>, seed: Seed, log: &'static crate::bootlog::BootLog, say: Say) -> std::io::Result<()> {
serve_installer_as(vm, seed, log, false, say)
}
#[cfg(feature = "wireguard")]
pub fn serve_installer_after_close_toned(vm: std::sync::Arc<dyn Vm>, seed: Seed, log: &'static crate::bootlog::BootLog, say: Say) -> std::io::Result<()> {
serve_installer_as(vm, seed, log, true, say)
}
#[cfg(feature = "wireguard")]
pub fn serve_installer_after_close(vm: std::sync::Arc<dyn Vm>, seed: Seed, log: &'static crate::bootlog::BootLog, say: std::sync::Arc<dyn Fn(&str) + Send + Sync>) -> std::io::Result<()> {
serve_installer_as(vm, seed, log, true, std::sync::Arc::new(move |_, l: &str| say(l)))
}
pub fn bind_backoff(n: u32) -> std::time::Duration {
std::time::Duration::from_secs((1u64 << n.saturating_sub(1).min(5)).min(30))
}
pub fn bind_until_open<T>(mut attempt: impl FnMut() -> Result<T, String>, mut sleep: impl FnMut(std::time::Duration), mut say: impl FnMut(&str)) -> T {
let mut n = 0u32;
loop {
match attempt() {
Ok(t) => return t,
Err(why) => {
n = n.saturating_add(1);
let wait = bind_backoff(n);
say(&format!("installer: {why} — asked again in {} s (attempt {n})", wait.as_secs()));
sleep(wait);
}
}
}
}
#[cfg(feature = "wireguard")]
fn serve_installer_as(vm: std::sync::Arc<dyn Vm>, seed: Seed, log: &'static crate::bootlog::BootLog, closed: bool, say: Say) -> std::io::Result<()> {
use crate::bootstrap::Tone;
std::thread::Builder::new().name("installer-listener".into()).spawn(move || {
let (link, listener) = bind_until_open(
|| {
let link = crate::installer_link::vm_link(&seed).map_err(|e| format!("the installer's link cannot be raised — the installer cannot reach this VM yet: {e}"))?;
let listener = std::net::TcpListener::bind((link.bind, crate::bootstrap::PORT)).map_err(|e| format!("the bootstrap listener cannot bind {}:{}: {e}", link.bind, crate::bootstrap::PORT))?;
Ok((link, listener))
},
std::thread::sleep,
|l| say(Tone::Warn, l),
);
say(Tone::Info, &format!("installer: the bootstrap listener is open on {}:{} (the installer's link only; mutual TLS, the installer's certificate only)", link.bind, crate::bootstrap::PORT));
crate::bootstrap::serve_for_life_or_after_close_at(vm.as_ref(), log, &seed.installer_ca_pem, listener, closed, &|t, l| say(t, l));
drop(link);
})?;
Ok(())
}
pub mod renewing;
#[cfg(test)]
mod tests {
#[test]
fn the_listener_bind_is_asked_again_until_it_opens() {
assert_eq!([1, 2, 3, 4, 5, 6, 9].map(|n| bind_backoff(n).as_secs()), [1, 2, 4, 8, 16, 30, 30]);
let mut tries = 0;
let (mut slept, mut said) = (Vec::new(), Vec::new());
let got = bind_until_open(
|| {
tries += 1;
if tries < 4 { Err(format!("cannot bind 10.44.0.2:8474: Cannot assign requested address (try {tries})")) } else { Ok(tries) }
},
|d| slept.push(d.as_secs()),
|l| said.push(l.to_string()),
);
assert_eq!(got, 4);
assert_eq!(slept, [1, 2, 4]);
assert_eq!(said.len(), 3, "{said:?}");
assert!(said[0].contains("try 1") && said[0].contains("again in 1 s"), "{said:?}");
}
use super::*;
use crate::join::Assigned;
struct P;
impl Product for P {
fn kind(&self) -> Kind {
Kind::Holger
}
fn tunnel_pubkey(&self) -> String {
"KH".into()
}
fn capacity(&self) -> Result<Option<crate::elect::Capacity>, String> {
Ok(Some(crate::elect::Capacity { cores: 2, ram_bytes: 1 << 30, data_bytes: 1 << 30, disk_mib_s: 100 }))
}
fn offer(&self, fed: Option<&JoinReply>) -> BTreeMap<String, String> {
BTreeMap::from([("version".into(), fed.map(|r| r.me.name.clone()).unwrap_or_default())])
}
fn fed(&self, _: &JoinReply) {}
}
fn seed(ca: &mtls::InstallerCa) -> Seed {
let boot = ca.issue_bootstrap(std::time::Duration::from_secs(3600)).unwrap();
Seed {
estate: "holger.test".into(),
tunnel: Default::default(),
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,
installer_link: None,
}
}
struct LiveP;
impl Product for LiveP {
fn kind(&self) -> Kind {
Kind::Holger
}
fn tunnel_pubkey(&self) -> String {
String::new()
}
fn capacity(&self) -> Result<Option<crate::elect::Capacity>, String> {
Ok(None)
}
fn offer(&self, _: Option<&JoinReply>) -> BTreeMap<String, String> {
BTreeMap::new()
}
fn fed(&self, _: &JoinReply) {}
fn live_running(&self) -> Result<crate::live::Running, String> {
Ok(crate::live::Running { base: "b".into(), app: "a".into(), slot: "a".into(), live_update: true })
}
fn live_stage(&self, s: &crate::live::Stage) -> Result<String, String> {
Ok(format!("staged {}", s.pin_hex))
}
}
#[test]
fn a_box_answers_the_live_door_with_its_products_slot_or_refuses_by_name() {
let ca = mtls::InstallerCa::mint("holger.test", std::time::Duration::from_secs(3600)).unwrap();
let root = std::env::temp_dir().join(format!("boxside-live-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let live = Installed::open(&root, seed(&ca), LiveP).unwrap();
assert_eq!(live.live_running().unwrap().app, "a");
let st = crate::live::Stage { pin_hex: "c".repeat(64), signature: vec![1], base: "b".into(), archive: vec![2] };
assert_eq!(live.live_stage(&st).unwrap(), format!("staged {}", "c".repeat(64)));
let plain = Installed::open(&root, seed(&ca), P).unwrap();
assert!(plain.live_running().unwrap_err().contains("[live-unsupported]"));
assert!(plain.live_stage(&st).unwrap_err().contains("[live-unsupported]"));
let _ = std::fs::remove_dir_all(&root);
}
fn reply(estate: &str, role: Option<crate::Role>, cert: Option<String>) -> JoinReply {
JoinReply {
estate: estate.into(),
me: Assigned { name: "main".into(), overlay: crate::OVERLAY_BOX_A },
cert_pem: cert,
role,
primary_overlay: None,
primary_utility: None,
peers: Vec::new(),
feed: BTreeMap::new(),
}
}
#[test]
fn a_box_presents_its_kind_keeps_its_feed_and_its_own_certificate_and_refuses_the_front() {
let ca = mtls::InstallerCa::mint("holger.test", std::time::Duration::from_secs(3600)).unwrap();
let root = std::env::temp_dir().join(format!("boxside-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let b = Installed::open(&root, seed(&ca), P).unwrap();
let hello = b.hello().unwrap();
assert_eq!((hello.kind, hello.tunnel_pubkey.as_str()), (Kind::Holger, "KH"));
assert!(!hello.csr_pem.is_empty(), "no certificate yet: it asks for one");
assert!(b.own_identity().is_none(), "the bootstrap certificate is nobody's");
let cert = ca.sign_box(&hello.csr_pem, "main", std::time::Duration::from_secs(3600)).unwrap();
assert!(b.fed(&reply("other.test", Some(crate::Role::Primary), None)).unwrap_err().contains("[installer-estate]"));
assert!(b.fed(&reply("holger.test", Some(crate::Role::Front), None)).unwrap_err().contains("[product-medium-is-not-the-front]"));
b.fed(&reply("holger.test", Some(crate::Role::Primary), Some(cert.clone()))).unwrap();
assert_eq!(b.latest().and_then(|r| r.role), Some(crate::Role::Primary));
assert_eq!(b.own_identity().map(|i| i.cert_pem), Some(cert));
assert!(b.hello().unwrap().csr_pem.is_empty(), "a box with its own certificate asks for no other");
assert_eq!(b.offer()["version"], "main");
let again = Installed::open(&root, seed(&ca), P).unwrap();
assert!(again.own_identity().is_some() && again.latest().is_some());
let other = Installed::open(&root, Seed { estate: "other.test".into(), ..seed(&ca) }, P).unwrap();
assert!(other.latest().is_none());
assert!(!again.closed());
again.close().unwrap();
assert!(again.closed());
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_late_feed_reaches_every_subscriber_of_the_boxs_bus() {
let ca = mtls::InstallerCa::mint("holger.test", std::time::Duration::from_secs(3600)).unwrap();
let root = std::env::temp_dir().join(format!("boxside-bus-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let b = Installed::open(&root, seed(&ca), P).unwrap();
assert!(b.feed_bus().current().is_none(), "nothing fed yet");
let fleet = Arc::new(Mutex::new(Vec::new()));
let f = Arc::clone(&fleet);
b.feed_bus().on_change("fleet", |r: &JoinReply| crate::feedbus::offered(&r.feed, "fleet"), move |v: &Option<String>| f.lock().unwrap().push(v.clone()));
b.fed(&reply("holger.test", Some(crate::Role::Primary), None)).unwrap();
b.close().unwrap();
let mut late = reply("holger.test", Some(crate::Role::Primary), None);
late.feed.insert("offer.front.fleet".into(), "https://one.example".into());
b.refed(&late).unwrap();
b.refed(&late).unwrap();
assert_eq!(*fleet.lock().unwrap(), vec![None, Some("https://one.example".to_string())]);
let again = Installed::open(&root, seed(&ca), P).unwrap();
assert_eq!(again.feed_bus().current().map(|r| r.feed.get("offer.front.fleet").cloned()), Some(Some("https://one.example".into())));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_closed_box_keeps_a_refeed_about_its_peers_and_refuses_one_that_changes_it() {
let ca = mtls::InstallerCa::mint("holger.test", std::time::Duration::from_secs(3600)).unwrap();
let root = std::env::temp_dir().join(format!("boxside-refeed-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let b = Installed::open(&root, seed(&ca), P).unwrap();
assert!(b.refed(&reply("holger.test", Some(crate::Role::Twin), None)).unwrap_err().contains("[refeed-never-fed]"));
b.fed(&reply("holger.test", Some(crate::Role::Twin), None)).unwrap();
b.close().unwrap();
let moved = JoinReply { primary_utility: Some([10, 13, 8, 77].into()), ..reply("holger.test", Some(crate::Role::Twin), None) };
b.refed(&moved).unwrap();
assert_eq!(b.latest().and_then(|r| r.primary_utility), Some([10, 13, 8, 77].into()));
let kept = Installed::open(&root, seed(&ca), P).unwrap();
assert_eq!(kept.latest().and_then(|r| r.primary_utility), Some([10, 13, 8, 77].into()), "kept across a restart");
for bad in [reply("holger.test", Some(crate::Role::Primary), None), reply("holger.test", Some(crate::Role::Twin), Some("C".into())), reply("other.test", Some(crate::Role::Twin), None)] {
assert!(b.refed(&bad).unwrap_err().contains("REFUSED ["));
}
assert_eq!(b.latest(), Some(moved), "a refused re-feed kept nothing");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn the_first_boot_after_an_installation_clears_the_pairing_state_once() {
let esp = std::env::temp_dir().join(format!("boxside-esp-{}", std::process::id()));
let data = std::env::temp_dir().join(format!("boxside-data-{}", std::process::id()));
let _ = (std::fs::remove_dir_all(&esp), std::fs::remove_dir_all(&data));
std::fs::create_dir_all(esp.join("korp")).unwrap();
for rel in PAIRING_STATE {
write_atomic(&data, rel, b"old", false).unwrap();
}
let mark = |p: &Path| std::fs::write(p, "x").is_ok();
assert_eq!(reset_if_fresh_install(&esp, &data, &PAIRING_STATE, &mark).map(|v| v.len()), Some(5));
assert_eq!(reset_if_fresh_install(&esp, &data, &PAIRING_STATE, &mark), None, "once");
write_atomic(&data, JOINED_REL, b"new", false).unwrap();
let _ = (std::fs::remove_dir_all(&esp), std::fs::remove_dir_all(&data));
let esp2 = std::env::temp_dir().join(format!("boxside-esp2-{}", std::process::id()));
std::fs::create_dir_all(esp2.join("korp")).unwrap();
assert_eq!(reset_if_fresh_install(&esp2, &data, &PAIRING_STATE, &|_| false), None);
let _ = std::fs::remove_dir_all(&esp2);
}
fn fed_with_secret(estate: &str) -> JoinReply {
let mut r = reply(estate, Some(crate::Role::Primary), None);
r.feed.insert("console_admin_token".into(), "t0k3n-NEVER-on-disk-in-the-clear".into());
r
}
#[test]
fn a_box_keeps_its_feed_sealed_at_rest_and_reads_it_back_after_a_restart() {
let ca = mtls::InstallerCa::mint("holger.test", std::time::Duration::from_secs(3600)).unwrap();
let root = std::env::temp_dir().join(format!("boxside-sealed-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let b = Installed::open(&root, seed(&ca), P).unwrap();
b.fed(&fed_with_secret("holger.test")).unwrap();
let raw = std::fs::read(root.join(JOINED_REL)).unwrap();
assert!(crate::fed_at_rest::is_sealed(&raw), "joined.toml is sealed");
assert!(!String::from_utf8_lossy(&raw).contains("t0k3n-NEVER"), "the fed token is not readable on disk");
let again = Installed::open(&root, seed(&ca), P).unwrap();
assert_eq!(again.latest().and_then(|r| r.feed.get("console_admin_token").cloned()).as_deref(), Some("t0k3n-NEVER-on-disk-in-the-clear"));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn a_keep_boot_of_the_same_estate_keeps_the_feed_the_key_and_the_certificate() {
let ca = mtls::InstallerCa::mint("holger.test", std::time::Duration::from_secs(3600)).unwrap();
let tag = std::process::id();
let data = std::env::temp_dir().join(format!("boxside-keep-data-{tag}"));
let _ = std::fs::remove_dir_all(&data);
let s = seed(&ca);
let b = Installed::open(&data, s.clone(), P).unwrap();
let cert = ca.sign_box(&b.hello().unwrap().csr_pem, "main", std::time::Duration::from_secs(3600)).unwrap();
b.fed(&reply("holger.test", Some(crate::Role::Primary), Some(cert.clone()))).unwrap();
let own_key = std::fs::read_to_string(data.join(OWN_KEY_REL)).unwrap();
drop(b);
let mark = |p: &Path| std::fs::write(p, "x").is_ok();
let esp = std::env::temp_dir().join(format!("boxside-keep-esp-{tag}"));
let _ = std::fs::remove_dir_all(&esp);
std::fs::create_dir_all(esp.join("korp")).unwrap();
let cleared = reset_if_fresh_install_of(&esp, &data, &PAIRING_STATE, &s, &mark);
assert!(cleared.as_ref().is_none_or(|c| c.is_empty()), "a keep boot of the same estate clears nothing: {cleared:?}");
assert!(esp.join(ESP_CLEARED).exists(), "the boot is marked, so it is decided once");
let after = Installed::open(&data, s.clone(), P).unwrap();
assert!(after.latest().is_some(), "still fed");
assert_eq!(after.own_identity().map(|i| i.cert_pem), Some(cert), "its certificate kept");
assert_eq!(std::fs::read_to_string(data.join(OWN_KEY_REL)).unwrap(), own_key, "its own key kept");
let esp2 = std::env::temp_dir().join(format!("boxside-keep-esp2-{tag}"));
let _ = std::fs::remove_dir_all(&esp2);
std::fs::create_dir_all(esp2.join("korp")).unwrap();
let other = Seed { estate: "other.test".into(), ..s.clone() };
let cleared = reset_if_fresh_install_of(&esp2, &data, &PAIRING_STATE, &other, &mark).expect("a first boot");
assert!(cleared.contains(&JOINED_REL.to_string()) && cleared.contains(&OWN_KEY_REL.to_string()), "{cleared:?}");
write_atomic(&data, JOINED_REL, b"not a reply", true).unwrap();
let esp3 = std::env::temp_dir().join(format!("boxside-keep-esp3-{tag}"));
let _ = std::fs::remove_dir_all(&esp3);
std::fs::create_dir_all(esp3.join("korp")).unwrap();
assert!(reset_if_fresh_install_of(&esp3, &data, &PAIRING_STATE, &s, &mark).expect("a first boot").contains(&JOINED_REL.to_string()));
let front = Seed { kind: Kind::Front, ..s.clone() };
let esp4 = std::env::temp_dir().join(format!("boxside-keep-esp4-{tag}"));
let _ = std::fs::remove_dir_all(&esp4);
std::fs::create_dir_all(esp4.join("korp")).unwrap();
crate::fed_at_rest::write(&data, JOINED_REL, &reply("holger.test", Some(crate::Role::Primary), None).render().unwrap()).unwrap();
assert!(reset_if_fresh_install_of(&esp4, &data, &PAIRING_STATE, &front, &mark).expect("a first boot").contains(&JOINED_REL.to_string()), "a box's feed under a front seed is not the front's");
for d in [&data, &esp, &esp2, &esp3, &esp4] {
let _ = std::fs::remove_dir_all(d);
}
}
#[test]
fn the_clearing_first_boot_says_a_marker_it_could_not_write() {
let tag = format!("clear-said-{}", std::process::id());
let (data, esp) = (std::env::temp_dir().join(format!("boxside-{tag}-data")), std::env::temp_dir().join(format!("boxside-{tag}-esp")));
let _ = std::fs::remove_dir_all(&esp);
std::fs::create_dir_all(esp.join("korp")).unwrap();
let (cleared, said) = crate::heard(|| reset_if_fresh_install(&esp, &data, &PAIRING_STATE, &|_| false));
assert_eq!(cleared, None, "nothing cleared without the marker");
assert!(said.contains("ERROR") && said.contains("the ESP marker could not be written") && said.contains("NOT cleared"), "{said}");
let _ = std::fs::remove_dir_all(&esp);
}
#[test]
fn the_esp_marker_on_a_read_only_boot_is_written_with_the_esp_flipped_read_write_and_back() {
use std::cell::RefCell;
let esp = std::env::temp_dir().join(format!("boxside-ro-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&esp);
let marker = esp.join(ESP_CLEARED);
let flips = RefCell::new(Vec::new());
let ok = mark_esp_on(&esp, &marker, &|m: &Path| {
appliance_pid1::stop_record::with_esp_writable_by(m, &|_| true, &|_, ro| { flips.borrow_mut().push(ro); Ok(()) }, || { assert_eq!(*flips.borrow(), vec![false], "written while read-write"); write_marker_file(&marker) })
});
assert!(ok && marker.exists());
assert_eq!(*flips.borrow(), vec![false, true], "flipped read-write, then back");
let other = std::env::temp_dir().join(format!("boxside-ro-other-{}", std::process::id())).join(ESP_CLEARED);
assert!(mark_esp_on(&esp, &other, &|_| panic!("not on the ESP mount: no flip")));
let _ = std::fs::remove_dir_all(&esp);
let _ = std::fs::remove_dir_all(other.parent().unwrap().parent().unwrap());
}
#[test]
fn the_esp_marker_is_written_whole_and_leaves_nothing_beside_it() {
let esp = std::env::temp_dir().join(format!("boxside-mark-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&esp);
assert!(mark_esp(&esp.join(ESP_CLEARED)));
assert!(std::fs::read_to_string(esp.join(ESP_CLEARED)).unwrap().contains("cleared"));
let beside: Vec<_> = std::fs::read_dir(esp.join("korp")).unwrap().map(|e| e.unwrap().file_name()).collect();
assert_eq!(beside.len(), 1, "{beside:?}");
let _ = std::fs::remove_dir_all(&esp);
}
#[test]
fn the_first_boot_says_a_keep_boot_a_marker_it_could_not_write_and_a_feed_it_could_not_read() {
let ca = mtls::InstallerCa::mint("holger.test", std::time::Duration::from_secs(3600)).unwrap();
let tag = format!("said-{}", std::process::id());
let data = std::env::temp_dir().join(format!("boxside-{tag}-data"));
let _ = std::fs::remove_dir_all(&data);
let s = seed(&ca);
crate::fed_at_rest::write(&data, JOINED_REL, &reply("holger.test", Some(crate::Role::Primary), None).render().unwrap()).unwrap();
let esp = |n: u8| {
let e = std::env::temp_dir().join(format!("boxside-{tag}-esp{n}"));
let _ = std::fs::remove_dir_all(&e);
std::fs::create_dir_all(e.join("korp")).unwrap();
e
};
let (e1, e2, e3) = (esp(1), esp(2), esp(3));
let (kept, said) = crate::heard(|| reset_if_fresh_install_of(&e1, &data, &PAIRING_STATE, &s, &|p| mark_esp(p)));
assert_eq!(kept, Some(Vec::new()));
assert!(said.contains("INFO") && said.contains("a keep boot of the same estate") && said.contains("KEPT"), "{said}");
let (kept, said) = crate::heard(|| reset_if_fresh_install_of(&e2, &data, &PAIRING_STATE, &s, &|_| false));
assert_eq!(kept, Some(Vec::new()));
assert!(said.contains("ERROR") && said.contains("the ESP marker could not be written"), "{said}");
let mut junk = crate::fed_at_rest::SEAL_MAGIC.to_vec();
junk.extend_from_slice(&[9u8; 64]);
write_atomic(&data, JOINED_REL, &junk, true).unwrap();
let (cleared, said) = crate::heard(|| reset_if_fresh_install_of(&e3, &data, &PAIRING_STATE, &s, &|p| mark_esp(p)));
assert!(cleared.expect("a first boot").contains(&JOINED_REL.to_string()));
assert!(said.contains("WARN") && said.contains("the kept feed cannot be read") && said.contains("cleared"), "{said}");
for d in [&data, &e1, &e2, &e3] {
let _ = std::fs::remove_dir_all(d);
}
}
}