server/cli/tests/bundle_time.rs
//! **`holger-server bundle import` decides expiry by the estate's ONE clock.**
//!
//! There is one signed-time format in the house: edda `estate-clock`'s signed
//! peer (`ESTATE-TIME/1`, ed25519 SSHSIG over a nonce the box chose). holger's
//! front serves it on the utility network; an airgapped downstream with no front
//! serves it with `estate-clock run` on its own clock host. These tests run the
//! REAL binary against the REAL peer half of that protocol
//! (`estate_clock::peer::answer` / `peer::serve`), answering with a chosen skew,
//! so a box two hours behind its estate can be made on a machine whose clock is
//! right. Every import is its own process, so the process-global clock cannot
//! leak between tests; the machine's clock is never moved (a CLI only measures).
//!
//! The bundles are real: minted signers, two approvals, a release signature
//! (`bench_scenarios::secure_update::finalized_bundle_expiring_at`).
use std::io::{Read as _, Write as _};
use std::net::{SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use chrono::{Duration, Utc};
const BIN: &str = env!("CARGO_BIN_EXE_holger");
const HOUR_MS: i64 = 3_600_000;
/// A signed-time peer whose clock is `skew_ms` ahead of this machine's, signing
/// with `signer`. The front half exactly as holger's front and `estate-clock run`
/// answer (`estate_clock::peer::answer`).
fn peer(signer: estate_clock::peer::ssh_key::PrivateKey, skew_ms: i64) -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
let mut request = Vec::new();
let mut byte = [0u8; 1];
while request.len() < estate_clock::peer::MAX_REQUEST_BYTES {
match stream.read(&mut byte) {
Ok(1) => {
request.push(byte[0]);
if byte[0] == b'\n' {
break;
}
}
_ => break,
}
}
let now = estate_clock::now_unix_ms() + skew_ms;
let _ = stream.write_all(&estate_clock::peer::answer(&request, &signer, now, Ok(())));
}
});
addr
}
/// A fresh time key and its OpenSSH public line (what a box pins as
/// `ESTATE_CLOCK_KEY`).
fn time_key() -> (estate_clock::peer::ssh_key::PrivateKey, String) {
let k = estate_clock::peer::generate_key().unwrap();
let line = k.public_key().to_openssh().unwrap();
(k, line)
}
struct Downstream {
_dir: tempfile::TempDir,
config: PathBuf,
bundles: PathBuf,
root: PathBuf,
}
/// A downstream box's store: the intake chain (W4), a raw `bundles` repository
/// nothing may land in any more, and its audit dir. The
/// audit log is ON: a bundle import with no audit record is refused (B3).
fn downstream() -> Downstream {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("store");
let bundles = root.join("bundles");
let audit = root.join("audit");
std::fs::create_dir_all(&bundles).unwrap();
std::fs::create_dir_all(&audit).unwrap();
let config = dir.path().join("holger.ron");
let audit_line = format!("( dir: \"{}\", enabled: true )", audit.display());
std::fs::write(&config, intake_ron(&root, &bundles, &audit_line)).unwrap();
Downstream {
_dir: dir,
config,
bundles,
root,
}
}
/// A signed bundle expiring at `now + expires_in`, and its policy file. `posture`:
/// whether the policy states `on_expired_metadata` (a fixture's choice).
fn bundle(d: &Downstream, expires_in: Duration, posture: bool) -> (PathBuf, PathBuf) {
let out = d.root.parent().unwrap().join("candidate.holger.znippy");
let mut policy = bench_scenarios::secure_update::finalized_bundle_expiring_at(
7,
"bundle-7",
Utc::now() + expires_in,
&out,
)
.expect("a signed bundle");
if !posture {
policy.on_expired_metadata = None;
}
let policy_path = d.root.parent().unwrap().join("trust-policy.ron");
std::fs::write(&policy_path, policy.to_ron().unwrap()).unwrap();
(out, policy_path)
}
/// `holger-server bundle import`, with the clock environment set to exactly
/// `clock` (nothing inherited from the shell running the tests).
fn import(d: &Downstream, bundle: &Path, policy: &Path, clock: &[(&str, String)]) -> Output {
allow_first_import(BIN, &d.config, bundle, Some(policy));
let mut cmd = Command::new(BIN);
cmd.args(["bundle", "import", "--config"])
.arg(&d.config)
.arg("--bundle")
.arg(bundle)
.arg("--policy")
.arg(policy)
.env_remove("ESTATE_CLOCK")
.env_remove("ESTATE_CLOCK_KEY");
for (k, v) in clock {
cmd.env(k, v);
}
cmd.output().expect("run holger-server")
}
/// The downstream's config: a raw repository `bundles` (where the REMOVED
/// raw-directory import used to write, kept to prove nothing lands there any
/// more), the four intake repositories every bundle now lands through, and the
/// audit block `audit_line`.
fn intake_ron(root: &Path, bundles: &Path, audit_line: &str) -> String {
let repo = |name: &str| {
format!(
r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "e")),
ron_data_dir: Some("{}/{name}")),"#,
root.display()
)
};
format!(
r#"(
exposed_endpoints: [ ( ron_name: "e", ron_url: "127.0.0.1:0", ron_http_url: None ) ],
storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{root}") ],
repositories: [
( ron_name: "bundles", ron_repo_type: "raw", ron_data_dir: Some("{bundles}"), ron_upstreams: [], ron_in: None, ron_out: None ),
{incoming} {quarantine} {approved} {production}
],
audit: {audit_line},
promotion: Some((
ladder_name: "intake",
stages: [
(name: "incoming", repo: "incoming"),
(name: "quarantine", repo: "quarantine"),
(name: "approved", repo: "approved"),
(name: "production", repo: "production"),
],
intake: Some((rules: (sandbox: off), incoming: "incoming", quarantine: "quarantine", approved: "approved", production: "production")),
)),
)
"#,
root = root.display(),
bundles = bundles.display(),
incoming = repo("incoming"),
quarantine = repo("quarantine"),
approved = repo("approved"),
production = repo("production"),
)
}
fn follow(addr: SocketAddr, key_line: &str) -> Vec<(&'static str, String)> {
vec![
("ESTATE_CLOCK", format!("peer://{addr}")),
("ESTATE_CLOCK_KEY", key_line.to_string()),
]
}
fn text(o: &Output) -> String {
format!(
"{}\n{}",
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
)
}
/// Nothing reached the served store: no bundle, no receipt, no floor.
fn nothing_written(d: &Downstream) {
let files: Vec<_> = walk(&d.bundles);
assert!(
files.is_empty(),
"a refused import wrote into the served store: {files:?}"
);
assert!(
!d.root.join("holger-import-receipts").exists()
|| walk(&d.root.join("holger-import-receipts")).is_empty()
);
assert!(
!d.root.join("holger-update-state").exists()
|| walk(&d.root.join("holger-update-state")).is_empty()
);
}
fn walk(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
for e in std::fs::read_dir(&d).into_iter().flatten().flatten() {
let p = e.path();
if p.is_dir() {
stack.push(p);
} else {
out.push(p);
}
}
}
out
}
/// **An expired bundle is refused by the synced clock.** The bundle expires in
/// one hour by this machine's clock; the estate's clock (the front's signed
/// time) is two hours ahead, so by the estate it expired an hour ago. The import
/// measures the front, and refuses — with the machine's clock still saying the
/// bundle is fresh. The same bundle, with the front agreeing with the machine,
/// imports and its verdict says the time was verified.
#[test]
fn an_expired_bundle_is_refused_by_the_synced_clock() {
let d = downstream();
let (b, p) = bundle(&d, Duration::hours(1), true);
let (signer, line) = time_key();
let out = import(&d, &b, &p, &follow(peer(signer, 2 * HOUR_MS), &line));
let t = text(&out);
assert!(
!out.status.success(),
"an import of a bundle expired by the estate's clock must fail:\n{t}"
);
assert!(
t.contains("bundle has expired"),
"refused as EXPIRED, not for another reason:\n{t}"
);
assert!(
t.contains("clock:"),
"the measuring sync is on stderr:\n{t}"
);
nothing_written(&d);
// Another box, whose front agrees with the machine (this box keeps the time its verified
// clock said, two hours ahead, as its time floor: HOL-NCSC, `a_rolled_back_clock_..`).
let d = downstream();
let (b, p) = bundle(&d, Duration::hours(1), true);
let (signer, line) = time_key();
let out = import(&d, &b, &p, &follow(peer(signer, 0), &line));
let t = text(&out);
assert!(
out.status.success(),
"the front agrees with the machine: fresh, imported:\n{t}"
);
assert!(
t.contains("time source: estate-clock (verified)"),
"the verdict names the verified estate clock:\n{t}"
);
}
/// **A skewed box does not accept an expired bundle** — not when its clock
/// source answers (above), and not when it does not. This box is two hours
/// behind the estate and was TOLD to follow a clock; the answer it gets is
/// forged (signed by a key that is not the pinned one), or nothing answers at
/// all. Before 2026-09-28 the verifier then read the box's own RTC — which says
/// the bundle is fresh — and imported it, marked UNTRUSTED. Now a configured
/// clock that did not verify is a refusal: the box has no trusted time, and it
/// does not borrow the skewed one.
#[test]
fn a_skewed_box_does_not_accept_an_expired_bundle() {
let d = downstream();
let (b, p) = bundle(&d, Duration::hours(1), true);
// A forged answer: the impostor's clock is honest-looking (+2 h), the key is not pinned.
let (_pinned, pinned_line) = time_key();
let (impostor, _) = time_key();
let out = import(
&d,
&b,
&p,
&follow(peer(impostor, 2 * HOUR_MS), &pinned_line),
);
let t = text(&out);
assert!(
!out.status.success(),
"a forged clock answer must not let the RTC decide:\n{t}"
);
assert!(
t.contains("no verified answer stands behind"),
"refused for having no verified time:\n{t}"
);
assert!(
t.contains("not the configured peer key"),
"the forged key is named:\n{t}"
);
nothing_written(&d);
// A silent clock source.
let closed = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap();
let (_, line) = time_key();
let out = import(&d, &b, &p, &follow(closed, &line));
let t = text(&out);
assert!(
!out.status.success(),
"a silent clock source must not let the RTC decide:\n{t}"
);
assert!(t.contains("no verified answer stands behind"), "{t}");
nothing_written(&d);
}
/// **An airgapped import still works.** Two downstreams with no estate front:
///
/// * one whose site runs the estate's clock itself (`estate-clock run` with
/// `ESTATE_CLOCK_TRUST_SYSTEM=1` on its clock host — here the same peer half,
/// `estate_clock::peer::serve`, signing unconditionally as that spelling does):
/// the import follows it over the site network and its verdict is VERIFIED;
/// * one with no clock host at all (`ESTATE_CLOCK` unset): the import still
/// works, against the system clock, and its verdict says UNTRUSTED.
#[test]
fn an_airgapped_import_still_works() {
// A site-local clock host.
let d = downstream();
let (b, p) = bundle(&d, Duration::days(30), true);
let (signer, line) = time_key();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
std::thread::spawn(move || {
estate_clock::peer::serve(listener, &signer, &|| Ok(()), &|_, _| {})
});
let out = import(&d, &b, &p, &follow(addr, &line));
let t = text(&out);
assert!(
out.status.success(),
"an airgapped downstream following its site's clock imports:\n{t}"
);
assert!(t.contains("IMPORTED (new)"), "{t}");
assert!(t.contains("time source: estate-clock (verified)"), "{t}");
// No clock host at all.
let d = downstream();
let (b, p) = bundle(&d, Duration::days(30), true);
let out = import(&d, &b, &p, &[]);
let t = text(&out);
assert!(
out.status.success(),
"a downstream with no clock at all still imports:\n{t}"
);
assert!(t.contains("IMPORTED (new)"), "{t}");
assert!(
t.contains("time source: estate-clock (UNTRUSTED"),
"and says its time was not verified:\n{t}"
);
}
/// **An unstated expired-metadata posture is ReadOnly** (the owner's ruling,
/// 2026-09-28): a downstream whose policy states none starts and imports a
/// fresh bundle as if it said `Some(ReadOnly)`.
#[test]
fn a_downstream_without_a_stated_posture_runs_read_only() {
let d = downstream();
let (b, p) = bundle(&d, Duration::days(30), false);
let out = import(&d, &b, &p, &[]);
let t = text(&out);
assert!(
out.status.success(),
"an unstated posture is ReadOnly, not a refusal:\n{t}"
);
assert!(t.contains("IMPORTED (new)"), "{t}");
assert!(!t.contains("[holger-update-39]"), "{t}");
}
// ─── the time floor (lane HOL-NCSC, NCSC P3) ──────────────────────────────────
const DAY_MS: i64 = 24 * HOUR_MS;
fn floor_of(d: &Downstream) -> i64 {
let p = d.root.join("holger-time").join("floor");
let v: serde_json::Value = serde_json::from_slice(
&std::fs::read(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display())),
)
.unwrap();
v["unix_ms"].as_i64().expect("the floor names its unix_ms")
}
fn audit_text(d: &Downstream) -> String {
walk(&d.root.join("audit"))
.iter()
.filter_map(|p| std::fs::read_to_string(p).ok())
.collect()
}
/// **An accepted bundle raises the floor** to its signed `created_at`.
#[test]
fn an_accepted_bundle_raises_the_time_floor() {
let d = downstream();
let (b, p) = bundle(&d, Duration::days(30), true);
let before = Utc::now().timestamp_millis();
let out = import(&d, &b, &p, &[]);
assert!(out.status.success(), "{}", text(&out));
let floor = floor_of(&d);
assert!(
floor >= before - HOUR_MS && floor <= Utc::now().timestamp_millis() + 60_000,
"the floor is the bundle's signed time: {floor} vs {before}"
);
}
/// **A rolled-back clock does not make an expired bundle fresh.** The box once saw the time
/// 30 days from now (its floor); its clock now says today; a bundle that expires in 10 days
/// is judged at the floor, so it is expired, and the rollback is audited by name.
#[test]
fn a_rolled_back_clock_is_judged_at_the_time_floor() {
let d = downstream();
let seeded = Utc::now().timestamp_millis() + 30 * DAY_MS;
std::fs::create_dir_all(d.root.join("holger-time")).unwrap();
std::fs::write(
d.root.join("holger-time").join("floor"),
serde_json::json!({ "unix_ms": seeded, "source": "a bundle the box accepted" }).to_string(),
)
.unwrap();
let (b, p) = bundle(&d, Duration::days(10), true);
let out = import(&d, &b, &p, &[]);
let t = text(&out);
assert!(
!out.status.success(),
"a bundle expired by the floor must be refused:\n{t}"
);
assert!(t.contains("bundle has expired"), "refused as EXPIRED:\n{t}");
nothing_written(&d);
assert!(
audit_text(&d).contains("[holger-time-1]"),
"the clock behind the floor is audited: {}",
audit_text(&d)
);
assert_eq!(floor_of(&d), seeded, "the floor did not move back");
}
/// `holger time-token make` (the online side): a signed `holger.time-token/v1` at `at`.
fn make_token(dir: &Path, signer_dir: &Path, at: chrono::DateTime<Utc>) -> (PathBuf, PathBuf) {
let (token, policy) = (
dir.join(format!("time-{}.holger.znippy", at.timestamp())),
dir.join("time-trust.ron"),
);
let out = Command::new(BIN)
.args(["time-token", "make", "--out"])
.arg(&token)
.arg("--signer-dir")
.arg(signer_dir)
.arg("--at")
.arg(at.to_rfc3339())
.arg("--trust-policy-out")
.arg(&policy)
.output()
.expect("run holger");
assert!(out.status.success(), "{}", text(&out));
(token, policy)
}
/// **A signed time token raises the floor**, carried in like any bundle (`bundle import`, no
/// first-import grant: it sets no anti-rollback floor of its own). Then a bundle that expires
/// before the token's time is refused. A token from a signer the policy does not trust is
/// refused, audited, and moves nothing.
#[test]
fn a_signed_time_token_raises_the_time_floor() {
let d = downstream();
let online = tempfile::tempdir().unwrap();
let at = Utc::now() + Duration::days(20);
let (token, policy) = make_token(online.path(), &online.path().join("signer"), at);
let mut cmd = Command::new(BIN);
let out = cmd
.args(["bundle", "import", "--config"])
.arg(&d.config)
.arg("--bundle")
.arg(&token)
.arg("--policy")
.arg(&policy)
.env_remove("ESTATE_CLOCK")
.env_remove("ESTATE_CLOCK_KEY")
.output()
.unwrap();
let t = text(&out);
assert!(
out.status.success(),
"the token is carried in like any bundle:\n{t}"
);
assert!(
t.contains("TIME FLOOR"),
"the import says the floor moved:\n{t}"
);
assert!(
floor_of(&d) >= at.timestamp_millis() - 1_000,
"the floor is the token's time"
);
// A token from another signer, verified under this policy: refused, the floor stays.
let other = tempfile::tempdir().unwrap();
let (forged, _) = make_token(
other.path(),
&other.path().join("signer"),
at + Duration::days(100),
);
let floor = floor_of(&d);
let out = Command::new(BIN)
.args(["bundle", "import", "--config"])
.arg(&d.config)
.arg("--bundle")
.arg(&forged)
.arg("--policy")
.arg(&policy)
.env_remove("ESTATE_CLOCK")
.env_remove("ESTATE_CLOCK_KEY")
.output()
.unwrap();
assert!(
!out.status.success(),
"an untrusted token must be refused:\n{}",
text(&out)
);
assert_eq!(floor_of(&d), floor, "the untrusted token moved nothing");
assert!(audit_text(&d).contains("refused"), "the refusal is audited");
// The floor now judges a bundle that expires before the token's time.
let (b, p) = bundle(&d, Duration::days(10), true);
let out = import(&d, &b, &p, &[]);
assert!(
!out.status.success() && text(&out).contains("bundle has expired"),
"{}",
text(&out)
);
}
/// The admin's first-import authorization on the secure side (lane HOL-CDS, NCSC gap 2), for
/// the vendor, product and sequence of `bundle` (verified under `policy`). Its refusal (a
/// floor that already stands) is the import's business, so it is not asserted here.
fn allow_first_import(bin: &str, config: &Path, bundle: &Path, policy: Option<&Path>) {
let mut c = Command::new(bin);
c.args(["bundle", "allow-first-import", "--config"])
.arg(config)
.arg("--from-bundle")
.arg(bundle);
if let Some(p) = policy {
c.arg("--policy").arg(p);
}
let _ = c
.env_remove("ESTATE_CLOCK")
.env_remove("ESTATE_CLOCK_KEY")
.output();
}