Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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