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;
pub struct Renewing<V: Vm + ?Sized> {
inner: Arc<V>,
root: PathBuf,
seed_rel: &'static str,
estate: String,
status_of: Option<StatusOf>,
}
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 }
}
pub fn with_status(mut self, f: StatusOf) -> Renewing<V> {
self.status_of = Some(f);
self
}
}
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)
}
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(())
}
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 }),
}
}
#[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]"));
}
}