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

nordisk/holger

server/lib/tests/the_time_floor_never_moves_back.rs

//! ★ **The time floor never moves back** (lane HOL-NCSC, NCSC P3): an airgap box judges
//! every expiry and validity window at `max(clock, floor)`, where the floor
//! (`<data_root>/holger-time/floor`) is the highest TRUSTED time it has seen: a verified
//! estate clock, the signed `created_at` of every bundle it accepted, a signed time token. A
//! clock found behind the floor (rolled back from the console or the BIOS) is judged at the
//! floor, said by name and audited (`holger-time-1`); an unreadable floor refuses
//! (`holger-time-3`), it never reads as zero.

use std::sync::Arc;

use chrono::{DateTime, TimeZone, Utc};
use holger_server_lib::audit::MemoryAuditLog;
use holger_server_lib::time_floor::{FlooredTime, TimeFloor, DIR, FILE};
use update_bundle::time::{TimeError, TrustedTime};

const DAY_MS: i64 = 86_400_000;
const T: i64 = 1_900_000_000_000;

struct Fixed(i64, bool);

impl TrustedTime for Fixed {
    fn source_name(&self) -> &'static str {
        "fixed"
    }
    fn is_trusted(&self) -> bool {
        self.1
    }
    fn now(&self) -> Result<DateTime<Utc>, TimeError> {
        Ok(Utc.timestamp_millis_opt(self.0).unwrap())
    }
}

#[test]
fn the_floor_only_rises_and_survives_a_restart() {
    let dir = tempfile::tempdir().unwrap();
    let f = TimeFloor::open(dir.path()).unwrap();
    assert_eq!(f.floor_ms(), 0, "a fresh box has no floor");
    assert!(
        f.raise(T, "a bundle").unwrap(),
        "the first trusted time sets it"
    );
    assert!(
        !f.raise(T - DAY_MS, "an older bundle").unwrap(),
        "an older time never lowers it"
    );
    assert_eq!(f.floor_ms(), T);
    assert!(f.raise(T + DAY_MS, "a newer bundle").unwrap());
    drop(f);
    let again = TimeFloor::open(dir.path()).unwrap();
    assert_eq!(again.floor_ms(), T + DAY_MS, "kept on the data disk");
    assert!(
        again.path().ends_with(format!("{DIR}/{FILE}")),
        "{}",
        again.path().display()
    );
}

#[test]
fn a_clock_behind_the_floor_is_judged_at_the_floor_and_audited() {
    let dir = tempfile::tempdir().unwrap();
    let f = TimeFloor::open(dir.path()).unwrap();
    f.raise(T, "a bundle").unwrap();
    let log = Arc::new(MemoryAuditLog::new());
    let rolled_back = Fixed(T - 30 * DAY_MS, false);
    let t = FlooredTime::new(&f, &rolled_back, Some(log.clone()));
    assert_eq!(
        t.now().unwrap().timestamp_millis(),
        T,
        "judged at the floor, not at the rolled-back clock"
    );
    let rows = log.events();
    assert!(
        rows.iter().any(|r| r.detail.contains("[holger-time-1]")),
        "the rollback is audited: {rows:?}"
    );
    // A clock ahead of the floor is the time, and nothing is said.
    let log = Arc::new(MemoryAuditLog::new());
    let ahead = Fixed(T + DAY_MS, false);
    assert_eq!(
        FlooredTime::new(&f, &ahead, Some(log.clone()))
            .now()
            .unwrap()
            .timestamp_millis(),
        T + DAY_MS
    );
    assert!(log.events().is_empty(), "{:?}", log.events());
}

#[test]
fn only_a_trusted_clock_raises_the_floor() {
    let dir = tempfile::tempdir().unwrap();
    let f = TimeFloor::open(dir.path()).unwrap();
    f.raise(T, "a bundle").unwrap();
    // An untrusted RTC set into the future would raise a floor that never comes down:
    // only a verified time moves it.
    FlooredTime::new(&f, &Fixed(T + 365 * DAY_MS, false), None)
        .now()
        .unwrap();
    assert_eq!(
        f.floor_ms(),
        T,
        "an untrusted clock does not move the floor"
    );
    FlooredTime::new(&f, &Fixed(T + DAY_MS, true), None)
        .now()
        .unwrap();
    assert_eq!(f.floor_ms(), T + DAY_MS, "a verified clock does");
}

#[test]
fn an_unreadable_floor_refuses() {
    let dir = tempfile::tempdir().unwrap();
    std::fs::create_dir_all(dir.path().join(DIR)).unwrap();
    std::fs::write(dir.path().join(DIR).join(FILE), b"not a floor").unwrap();
    let e = TimeFloor::open(dir.path())
        .err()
        .expect("a floor that cannot be read is no floor of zero");
    assert!(e.contains("[holger-time-3]"), "{e}");
}