server/lib/src/cert_health.rs
//! **How long this box's paired certificates have left** (lane E2, 2026-10-19;
//! Gunnar's renewal lifecycle, Holger's own code).
//!
//! Every link of a Holger estate rides a certificate with an end:
//!
//! | link | certificate | who renews it, at two thirds of its life |
//! |---|---|---|
//! | front → main, twin (the installer's door) | the box's own (`pairing/own.pem`, 180 d) | the front, from the kept installer material (`front.renew.boxes`) |
//! | twin → primary (replication) | the twin's client leaf (`holger-twin-tls/twin.crt`, 365 d) | the front re-certifies the twin (`front.renew.twin`) |
//! | anyone → the replication door | the door's server identity (365 d) | the box itself (`box.renew.door`, [`crate::twin_tls`]) |
//!
//! A renewal that keeps failing is retried by the job that owns it; this module is
//! the ALARM on top: the box reports each certificate's grade in its health words
//! (`Fleet.Report`, the words the ONE box's guardian reads), so a renewal that has
//! failed long enough for the end to come near is said by the box itself, whoever
//! failed to renew it.
//!
//! | grade | when | meaning |
//! |---|---|---|
//! | `ok` | before two thirds of the life | nothing to do |
//! | `due` | past two thirds | the renewal should have happened; it is retried |
//! | `expiring` | past five sixths | the renewal has failed for half the time it had: ALERT |
//! | `expired` | past the end | the link is refused by name (`holger-twin-18`) |
use crate::lock::RwLockOrRecover as _;
use std::collections::BTreeMap;
use std::path::Path;
use std::sync::RwLock;
/// The share of a life after which a certificate is renewed: two thirds
/// (nordisk-pairing's `mtls::RENEW_AT`, the same rule on every link).
pub const RENEW_AT: (i64, i64) = (2, 3);
/// The share after which a renewal that did not happen is an alert: five sixths —
/// half the time the renewal had is gone.
pub const ALERT_AT: (i64, i64) = (5, 6);
/// A certificate's grade at a moment.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Grade {
Ok,
Due,
Expiring,
Expired,
}
impl Grade {
pub fn word(self) -> &'static str {
match self {
Grade::Ok => "ok",
Grade::Due => "due",
Grade::Expiring => "expiring",
Grade::Expired => "expired",
}
}
/// The grades an operator must hear about.
pub fn alerts(self) -> bool {
self >= Grade::Expiring
}
}
fn at(from: i64, to: i64, share: (i64, i64)) -> i64 {
from + (to - from).max(0) * share.0 / share.1
}
/// Pure: **the grade of a life `[from, to)` at `now`** (Unix seconds).
pub fn grade(from: i64, to: i64, now: i64) -> Grade {
if now >= to {
Grade::Expired
} else if now >= at(from, to, ALERT_AT) {
Grade::Expiring
} else if now >= at(from, to, RENEW_AT) {
Grade::Due
} else {
Grade::Ok
}
}
/// Pure: **is the life `[from, to)` due for renewal at `now`?** (two thirds, or past its end).
pub fn due(from: i64, to: i64, now: i64) -> bool {
grade(from, to, now) >= Grade::Due
}
/// Pure: the grade and whole days left of a certificate (PEM) at `now`.
pub fn of_pem(pem: &str, now: i64) -> Result<(Grade, i64), String> {
let (from, to) = nordisk_twin::pki::validity(pem).map_err(|e| e.to_string())?;
Ok((grade(from, to, now), (to - now).div_euclid(86_400)))
}
/// Seconds since the epoch, now.
pub fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
type Source = Box<dyn Fn() -> Option<String> + Send + Sync>;
static PAIRING_CERT: RwLock<Option<Source>> = RwLock::new(None);
/// **Where this box's own pairing certificate is read** (holger-server's `paired`
/// lane sets it: the library does not own the pairing root).
pub fn set_pairing_cert(f: impl Fn() -> Option<String> + Send + Sync + 'static) {
{
let mut s = PAIRING_CERT.write_or_recover();
*s = Some(Box::new(f));
}
}
fn pairing_cert() -> Option<String> {
PAIRING_CERT.read_or_recover().as_ref().and_then(|f| f())
}
/// **The end of this box's own pairing certificate** (Unix s), read live; `None` before it
/// joined or when nobody set the source.
pub fn pairing_not_after() -> Option<i64> {
nordisk_twin::pki::validity(&pairing_cert()?)
.ok()
.map(|(_, to)| to)
}
/// Pure: **the words for one certificate** — `<name>_cert` (its grade) and
/// `<name>_cert_days` — and an unreadable one says so.
pub fn put(h: &mut BTreeMap<String, String>, name: &str, pem: Option<&str>, now: i64) {
let Some(pem) = pem else { return };
match of_pem(pem, now) {
Ok((g, days)) => {
h.insert(format!("{name}_cert"), g.word().into());
h.insert(format!("{name}_cert_days"), days.to_string());
}
Err(e) => {
h.insert(format!("{name}_cert"), format!("unreadable: {e}"));
}
}
}
/// **The certificate words of this box** at `now`: its pairing certificate, its
/// twin credential (a twin), its recharge credential (a fresh main), its replication
/// door's identity — each that exists — and `cert_alert` naming every one that is
/// `expiring` or `expired`.
pub fn words(data_root: Option<&Path>, now: i64) -> BTreeMap<String, String> {
let mut h = BTreeMap::new();
put(&mut h, "pairing", pairing_cert().as_deref(), now);
if let Some(root) = data_root {
for (name, purpose) in [
("twin", crate::twin_tls::Purpose::Twin),
("recharge", crate::twin_tls::Purpose::Recharge),
] {
let cert =
std::fs::read_to_string(purpose.dir(root).join(nordisk_twin::credential::CERT))
.ok();
put(&mut h, name, cert.as_deref(), now);
}
}
put(
&mut h,
"door",
crate::twin_tls::door_identity_pem().as_deref(),
now,
);
let alerts: Vec<String> = h
.iter()
.filter(|(k, v)| {
k.ends_with("_cert")
&& (v.as_str() == Grade::Expiring.word() || v.as_str() == Grade::Expired.word())
})
.map(|(k, v)| format!("{} {v}", k.trim_end_matches("_cert")))
.collect();
if !alerts.is_empty() {
h.insert(
"cert_alert".into(),
format!(
"{} — the certificate renewal has failed long enough for the end to come near",
alerts.join(", ")
),
);
}
h
}
#[cfg(test)]
mod tests {
use super::*;
const D: i64 = 86_400;
#[test]
fn a_life_is_ok_then_due_at_two_thirds_then_an_alert_at_five_sixths_then_expired() {
let (from, to) = (1_000 * D, 1_180 * D);
assert_eq!(grade(from, to, from), Grade::Ok);
assert_eq!(grade(from, to, from + 119 * D), Grade::Ok);
assert_eq!(
grade(from, to, from + 120 * D),
Grade::Due,
"two thirds of 180 days"
);
assert_eq!(
grade(from, to, from + 150 * D),
Grade::Expiring,
"five sixths"
);
assert!(grade(from, to, from + 150 * D).alerts() && !Grade::Due.alerts());
assert_eq!(grade(from, to, to), Grade::Expired);
assert!(due(from, to, from + 120 * D) && !due(from, to, from + 10 * D));
}
#[test]
fn the_words_name_every_certificate_near_its_end() {
let dir = tempfile::tempdir().unwrap();
let issuer =
nordisk_twin::pki::Issuer::load_or_mint(&dir.path().join("i"), "issuer").unwrap();
let (csr_key, csr) = nordisk_twin::pki::mint_request("holger-twin").unwrap();
let _ = csr_key;
let leaf = issuer.issue_from_csr("holger-twin", &csr, 30).unwrap();
let twin_dir = crate::twin_tls::Purpose::Twin.dir(dir.path());
std::fs::create_dir_all(&twin_dir).unwrap();
std::fs::write(
twin_dir.join(nordisk_twin::credential::CERT),
&leaf.cert_pem,
)
.unwrap();
let now = now_unix();
let fresh = words(Some(dir.path()), now);
assert_eq!(
fresh.get("twin_cert").map(String::as_str),
Some("ok"),
"{fresh:?}"
);
assert!(!fresh.contains_key("cert_alert"), "{fresh:?}");
let late = words(Some(dir.path()), now + 27 * D);
assert_eq!(
late.get("twin_cert").map(String::as_str),
Some("expiring"),
"{late:?}"
);
assert!(
late.get("cert_alert")
.is_some_and(|a| a.contains("twin expiring")),
"{late:?}"
);
let ended = words(Some(dir.path()), now + 31 * D);
assert_eq!(
ended.get("twin_cert").map(String::as_str),
Some("expired"),
"{ended:?}"
);
}
}