gunnar-core 3.0.0

gunnar's repository model, identity table, ACL and path sandbox. gunnar-ssh, gunnar-grpc and gunnar-cli all sit on it.
Documentation
//! **An expired certificate on a box with no shell is a brick, and this is the
//! thing that refuses before it becomes one.**
//!
//! # The risk, stated plainly
//!
//! The appliance and the twin ship with `shell = false`. There is no way in to
//! fix a certificate by hand, and **their only door is the very gRPC plane the
//! certificate protects**. A client certificate that lapses does not degrade
//! service; it removes the last way to repair the box. The issuer is ten years
//! ([`DEFAULT_ISSUER_DAYS`](crate::DEFAULT_ISSUER_DAYS)) so it is a planned
//! event, but a leaf is **one year**
//! ([`DEFAULT_LEAF_DAYS`](crate::DEFAULT_LEAF_DAYS)) and one year passes
//! without anybody doing anything at all.
//!
//! # Is WireGuard underneath enough to recover? NO, and it must be said so
//!
//! The tunnel keeps carrying packets whatever the certificate says — it is a
//! different key, a different lifetime and no expiry at all. So after a lapse
//! the box is still *reachable*. It is not *repairable*: the thing listening at
//! the other end of the tunnel is the mutual listener, and it refuses the
//! expired peer at the handshake, before any RPC exists to fix anything with.
//! WireGuard turns "unreachable brick" into "reachable brick". That is worth
//! having and it is not a recovery.
//!
//! The recovery therefore has to be **not lapsing**, and that is what the
//! grades below are for.
//!
//! # Four grades, and why refusal comes EARLY
//!
//! [`Grade::Renew`] fires with a quarter of the lifetime left — 91 days on a
//! 365-day leaf. It is deliberately not "a week before": a box that only
//! notices at seven days has to be rescued inside a week by whoever reads the
//! log, and this estate's boxes are checked on a cadence of months.
//!
//! [`Grade::Refuse`] fires with [`REFUSE_DAYS`] left, **while the certificate
//! is still perfectly valid**. That is the whole idea and it is the estate's
//! own idiom: the refusal happens at a moment when the box can still be talked
//! to, so an operator is forced to act while acting is possible. A warning at
//! the same moment would be a line in a log nobody reads until the box is
//! already dead — the health line that cannot fail, which this codebase has
//! found eight of in a single day. A door that refuses while it can still be
//! opened is worth more than one that reports while it cannot.
//!
//! [`Grade::Expired`] is the state the two above exist to make unreachable.

/// Milliseconds in a day.
const DAY_MS: i64 = 86_400_000;

/// Start asking for a new certificate with a **quarter of the lifetime** left.
///
/// A fraction rather than a day count so a deployment that issues 30-day leaves
/// gets a proportionate warning instead of one that fires before the
/// certificate is installed.
pub const RENEW_AT_FRACTION_REMAINING: f64 = 0.25;

/// Refuse with this many days left — while the certificate still WORKS.
///
/// Fourteen days is two weekly cycles: long enough that a box checked once a
/// week is caught twice before it lapses, short enough that it is not firing
/// for a third of the certificate's life.
pub const REFUSE_DAYS: i64 = 14;

/// How close a certificate is to being the thing that bricks its box.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Grade {
    /// Nothing to do.
    Fine,
    /// Renew now, loudly, by name. Still serving.
    Renew,
    /// **Refuse, while the door can still be opened.** See the module docs.
    Refuse,
    /// Already past `not_after`. The box is a brick unless something below the
    /// control plane can still reach it.
    Expired,
}

/// A certificate's position on that scale, with the numbers that produced it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Expiry {
    /// Whole days from `now` to `not_after`. Negative once it has lapsed.
    pub days_left: i64,
    /// Whole days the certificate was issued for.
    pub lifetime_days: i64,
    /// How close it is to bricking its box.
    pub grade: Grade,
}

/// Grade a certificate from its validity window and the current time, all in
/// Unix milliseconds.
///
/// Pure arithmetic on three numbers: no clock, no filesystem and no store, so
/// every grade can be driven from a test rather than waited for. A predicate
/// that can only be exercised by letting a year pass is a predicate that is
/// never exercised.
///
/// A `not_before` at or after `not_after` — a certificate valid for zero days,
/// which the issuer already refuses to mint — grades [`Grade::Expired`]
/// whatever the clock says, because a lifetime of zero cannot be a fraction of
/// anything and guessing would be worse than refusing.
pub fn grade(now_unix_ms: i64, not_before_unix_ms: i64, not_after_unix_ms: i64) -> Expiry {
    let lifetime_days = (not_after_unix_ms - not_before_unix_ms).div_euclid(DAY_MS);
    let days_left = (not_after_unix_ms - now_unix_ms).div_euclid(DAY_MS);

    if lifetime_days <= 0 {
        return Expiry {
            days_left,
            lifetime_days,
            grade: Grade::Expired,
        };
    }
    let renew_at = (lifetime_days as f64 * RENEW_AT_FRACTION_REMAINING).floor() as i64;
    // **The refusal must never fire before the renewal was asked for.** On a
    // leaf shorter than `REFUSE_DAYS / RENEW_AT_FRACTION_REMAINING` (56 days)
    // the absolute threshold overtakes the proportional one, and a box would go
    // straight from `Fine` to `Refuse` having never once been told to renew.
    // That is a refusal nobody could have acted on, which is the opposite of
    // this module's whole argument — so the absolute figure yields, and the
    // refusal always sits at least a day inside the renewal window.
    let refuse_at = REFUSE_DAYS.min((renew_at - 1).max(0));

    let grade = if days_left < 0 {
        Grade::Expired
    } else if days_left <= refuse_at {
        Grade::Refuse
    } else if days_left <= renew_at {
        Grade::Renew
    } else {
        Grade::Fine
    };
    Expiry {
        days_left,
        lifetime_days,
        grade,
    }
}

impl Expiry {
    /// The line an operator has to be able to act on, naming the box's role,
    /// the number of days, and what happens if nothing is done.
    ///
    /// `who` is what the certificate is for — `"this twin's client certificate"`,
    /// `"the control plane's own listener certificate"` — because the recovery
    /// is different for each and a message that does not say which one it is
    /// about sends the operator to the wrong file.
    pub fn line(&self, who: &str) -> String {
        match self.grade {
            Grade::Fine => format!(
                "{who} is good for {} more day(s) of its {}-day life.",
                self.days_left, self.lifetime_days
            ),
            Grade::Renew => format!(
                "RE-ISSUE NOW: {who} expires in {} day(s) of its {}-day life. This box has no shell \
                 and its only door is the control plane this certificate opens, so a lapse is \
                 not a degraded service — it is a box nobody can repair. WireGuard underneath \
                 keeps it REACHABLE and cannot make it repairable.",
                self.days_left, self.lifetime_days
            ),
            Grade::Refuse => format!(
                "REFUSED WITH {} DAY(S) STILL VALID: {who} expires in {} day(s). It is refused \
                 NOW, while the certificate still works and this box can still be talked to, \
                 because in {} day(s) it could not be. Re-enrol it — the control plane is open \
                 to you today and will not be. A warning here would be a line in a log read \
                 after the box was already dead.",
                self.days_left, self.days_left, self.days_left
            ),
            Grade::Expired => format!(
                "EXPIRED: {who} lapsed {} day(s) ago. The mutual listener refuses this box at \
                 the handshake, before any RPC it could be repaired with. If it has no shell, \
                 the way back is re-imaging it from the medium and re-enrolling — WireGuard \
                 still carries packets to it and there is nothing at the other end that will \
                 take them.",
                -self.days_left
            ),
        }
    }

    /// Should this stop a box from starting or from following?
    pub fn is_refusal(&self) -> bool {
        matches!(self.grade, Grade::Refuse | Grade::Expired)
    }
}

/// Grade a DER certificate against `now`.
///
/// The parse failing is **not** graded as fine: bytes that are not a
/// certificate are the same operational fact as a certificate that cannot be
/// used, and grading them `Fine` is how a broken file becomes a silent one.
pub fn grade_der(now_unix_ms: i64, der: &[u8]) -> Result<Expiry, crate::Error> {
    let (_rest, cert) = x509_parser::parse_x509_certificate(der).map_err(|err| {
        crate::Error::Pki(format!("this is not a usable X.509 certificate: {err}"))
    })?;
    let validity = cert.validity();
    let ms = |t: x509_parser::time::ASN1Time| t.timestamp() * 1000;
    Ok(grade(
        now_unix_ms,
        ms(validity.not_before),
        ms(validity.not_after),
    ))
}

/// Grade the **first** certificate in a PEM bundle against `now`.
///
/// First and not last: a chain is written leaf-first, and the leaf is the one
/// with the short life and therefore the one that bricks the box. Grading the
/// issuer instead would report ten comfortable years while the credential that
/// actually opens the door had a fortnight left.
pub fn grade_pem(now_unix_ms: i64, pem: &[u8]) -> Result<Expiry, crate::Error> {
    let (_rest, block) = x509_parser::pem::parse_x509_pem(pem)
        .map_err(|err| crate::Error::Pki(format!("this is not a usable PEM certificate: {err}")))?;
    grade_der(now_unix_ms, &block.contents)
}

#[cfg(test)]
mod tests {
    use super::*;

    const YEAR: i64 = 365;

    fn at(day_of_life: i64, lifetime: i64) -> Expiry {
        let not_before = 1_700_000_000_000;
        let not_after = not_before + lifetime * DAY_MS;
        grade(not_before + day_of_life * DAY_MS, not_before, not_after)
    }

    /// **All four grades are reachable.** Without this the whole module could be
    /// a function that returns `Fine` and every other test would still pass.
    #[test]
    fn every_grade_can_happen() {
        assert_eq!(at(0, YEAR).grade, Grade::Fine);
        assert_eq!(at(280, YEAR).grade, Grade::Renew);
        assert_eq!(at(360, YEAR).grade, Grade::Refuse);
        assert_eq!(at(400, YEAR).grade, Grade::Expired);
    }

    /// The refusal fires while the certificate is STILL VALID. This is the
    /// property the module exists for: a door that refuses while it can still
    /// be opened.
    #[test]
    fn the_refusal_comes_before_the_lapse() {
        let e = at(YEAR - REFUSE_DAYS, YEAR);
        assert_eq!(e.grade, Grade::Refuse);
        assert!(
            e.days_left > 0,
            "refusing only once it has lapsed is refusing too late"
        );
        assert!(e.is_refusal());
        let text = e.line("this twin's client certificate");
        assert!(text.contains("STILL VALID"), "{text}");
        assert!(text.contains("this twin's client certificate"), "{text}");
    }

    /// The renewal threshold is a FRACTION, so a short-lived leaf is not warned
    /// about from the day it is installed.
    #[test]
    fn a_short_lifetime_gets_a_proportionate_warning() {
        // A 60-day leaf: renew at 15 days left, not at 91 (which is before it
        // was even issued) and not at 274.
        assert_eq!(at(0, 60).grade, Grade::Fine);
        assert_eq!(at(30, 60).grade, Grade::Fine);
        assert_eq!(
            at(45, 60).grade,
            Grade::Renew,
            "a quarter of 60 days left is the renewal edge"
        );
        assert_eq!(
            at(46, 60).grade,
            Grade::Refuse,
            "14 days left is inside REFUSE_DAYS"
        );
        // A 365-day leaf renews at 91 days left = day 274.
        assert_eq!(at(273, YEAR).grade, Grade::Fine);
        assert_eq!(at(274, YEAR).grade, Grade::Renew);
    }

    /// **A box is never refused without first having been told to renew.**
    ///
    /// On a leaf shorter than 56 days the absolute [`REFUSE_DAYS`] would
    /// overtake the proportional renewal threshold, and the box would go from
    /// `Fine` straight to `Refuse` having never been asked for anything. This
    /// walks every day of several lifetimes and asserts the order holds.
    #[test]
    fn the_refusal_never_overtakes_the_renewal() {
        for lifetime in [7, 20, 30, 56, 60, 90, 365, 3650] {
            let mut seen_renew = false;
            let mut refused_on = None;
            for day in 0..=lifetime {
                match at(day, lifetime).grade {
                    Grade::Renew => seen_renew = true,
                    Grade::Refuse if refused_on.is_none() => refused_on = Some(day),
                    _ => {}
                }
            }
            assert!(
                refused_on.is_some(),
                "a {lifetime}-day leaf must eventually be refused, or the guard is decoration"
            );
            assert!(
                seen_renew,
                "a {lifetime}-day leaf was refused on day {:?} without ever being told to renew",
                refused_on
            );
        }
    }

    /// A certificate with no lifetime is never graded usable.
    #[test]
    fn a_zero_length_certificate_is_expired_whatever_the_clock_says() {
        let t = 1_700_000_000_000;
        assert_eq!(grade(t, t, t).grade, Grade::Expired);
        assert_eq!(grade(t - 100 * DAY_MS, t, t).grade, Grade::Expired);
    }

    /// The expired line says what the way back is, and that WireGuard is not it.
    #[test]
    fn the_expired_line_is_honest_about_wireguard() {
        let text = at(400, YEAR).line("this twin's client certificate");
        assert!(text.contains("EXPIRED"), "{text}");
        assert!(
            text.contains("re-imaging"),
            "the way back must be named: {text}"
        );
        assert!(
            text.contains("WireGuard still carries packets"),
            "it must say plainly that the tunnel does not rescue this: {text}"
        );
    }

    /// Bytes that are not a certificate are an error, never a pass.
    #[test]
    fn unparseable_bytes_are_not_graded_fine() {
        assert!(grade_der(1_700_000_000_000, b"not a certificate at all").is_err());
    }
}