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

nordisk/holger

server/cli/src/secure_update.rs

//! Secure Update Gateway offline operations: verified bundle import (with
//! anti-rollback), integrity scrub, backup/restore, and audit-segment verify.
//!
//! All of these reuse the network-free `holger-update-bundle` crate. Import and
//! scrub key off the target RON config + a raw repository name (whose
//! `ron_data_dir` is the served bundle store); backup packages that store into a
//! single verifiable znippy archive.

use std::path::{Path, PathBuf};

use anyhow::{bail, Context, Result};
use clap::{Args, Subcommand};

use update_bundle::container::{load_bundle, pack_bundle};
use update_bundle::digest::sha256_hex;
use update_bundle::signer::sign_pkcs8;
use update_bundle::verify::{
    verify_detached_by_trusted_signer, verify_member_map_with_time, SignerIdentity, TargetSpec,
};
use update_bundle::{MemberMap, TrustPolicy};

// ───────────────────────── the estate's clock ─────────────────────────

/// **The bundle verifier's time source: the ESTATE's clock** — the one the
/// import door uses too (`server_lib::import_door::EstateTime`), so the CLI and
/// the box answer from one clock with one set of rules.
pub use server_lib::import_door::EstateTime;

/// **Ask the clock before a verdict**: this CLI is its own process, so the
/// running server's view is not here. The source is the medium's / the site's
/// `ESTATE_CLOCK` (+ `ESTATE_CLOCK_KEY`) — on an airgapped downstream a
/// site-local `estate-clock run` — and, on a paired box (feature `paired`), the
/// kept feed (`<root>/pairing/joined.toml`, the front's `clock_peer` +
/// `clock_key`). One measuring sync — the machine's clock is never moved from a
/// CLI — and its line on stderr. Nothing configured: nothing asked.
pub(crate) fn establish_time() {
    #[cfg(feature = "paired")]
    {
        // The kept feed is SEALED at rest (lane G5): read through the box's own reader.
        if let Some(reply) = crate::paired::kept_reply(&crate::paired::appliance_root()) {
            server_lib::clock::set_feed(&reply.feed);
        }
    }
    let config = server_lib::clock::config_now();
    if matches!(config, Ok(server_lib::clock::Config::Sources { .. })) {
        let r = server_lib::clock::measure_once(&config, server_lib::clock::Trigger::Timer);
        eprintln!("clock: {}", r.summary());
    }
}

/// The downstream's expired-metadata posture, or a refusal to start. The owner
/// has not decided between ReadOnly and Refuse (2026-09-20, open); the policy
/// must say which, and nothing here picks one.
fn require_expired_posture(policy: &TrustPolicy) -> Result<update_bundle::policy::ExpiredPosture> {
    policy
        .expired_posture()
        .map_err(|e| anyhow::anyhow!("[{}] {e}", e.code().code))
}

// ───────────────────────── CLI arg types ─────────────────────────

#[derive(Args)]
pub struct BundleArgs {
    #[command(subcommand)]
    pub op: BundleOp,
}

#[derive(Subcommand)]
pub enum BundleOp {
    /// Verify + import a signed update bundle into the protected data root
    /// (anti-rollback enforced; atomic; audited). There is no --force bypass.
    Import(ImportArgs),
    /// ADMIN, on the secure side (lane HOL-CDS, NCSC gap 2): authorize the FIRST import of
    /// one vendor's product at one sequence, so that bundle may establish the absent
    /// anti-rollback floor. Audited; used once by the import it names (the CLI, the
    /// network doors or the `bundle.inbox` drop). Refused when a floor already stands.
    AllowFirstImport(AllowFirstImportArgs),
}

#[derive(Args)]
pub struct AllowFirstImportArgs {
    /// The Holger RON config (its data root and audit sink).
    #[arg(long)]
    pub config: PathBuf,
    /// The bundle's vendor (its `supplier`).
    #[arg(long, required_unless_present = "from_bundle")]
    pub vendor: Option<String>,
    /// The bundle's product.
    #[arg(long, required_unless_present = "from_bundle")]
    pub product: Option<String>,
    /// The bundle's sequence: the one that establishes the floor.
    #[arg(long, required_unless_present = "from_bundle")]
    pub sequence: Option<u64>,
    /// Take vendor, product and sequence from this bundle, verified first under `--policy`
    /// (or the config's `promotion.intake.trust_policy`, then `intel.trust_policy`).
    #[arg(long, conflicts_with_all = ["vendor", "product", "sequence"])]
    pub from_bundle: Option<PathBuf>,
    #[arg(long)]
    pub policy: Option<PathBuf>,
}

#[derive(Args)]
pub struct ImportArgs {
    /// Target Holger RON config (for the audit sink + the store location).
    #[arg(long)]
    pub config: PathBuf,
    /// REMOVED (lane W4): this wrote the bundle straight into a raw repository's
    /// data directory, past the intake chain. Naming it is refused by name
    /// (`holger-import-1`); every bundle now lands through intake.
    #[arg(long, hide = true)]
    pub repo: Option<String>,
    /// The `.holger.znippy` bundle to import.
    #[arg(long)]
    pub bundle: PathBuf,
    /// Trust policy (RON). Default: `promotion.intake.trust_policy` from the config.
    #[arg(long)]
    pub policy: Option<PathBuf>,
    #[arg(long)]
    pub target_product: Option<String>,
    #[arg(long)]
    pub target_model: Option<String>,
    #[arg(long)]
    pub target_revision: Option<String>,
    /// H4: when the bundle carries a sync manifest and it is applied, copy the
    /// signed ack here (the stick). It is always kept in the sync outbox too.
    #[arg(long)]
    pub ack_out: Option<PathBuf>,
}

#[derive(Args)]
pub struct ScrubArgs {
    #[arg(long)]
    pub config: PathBuf,
    #[arg(long)]
    pub repo: String,
    /// Trust policy to re-verify each installed bundle against.
    #[arg(long)]
    pub policy: PathBuf,
}

#[derive(Args)]
pub struct BackupArgs {
    #[command(subcommand)]
    pub op: BackupOp,
}

#[derive(Subcommand)]
pub enum BackupOp {
    /// Package the protected store (bundles + anti-rollback state + receipts) into
    /// a single verifiable `.znippy` backup. Private keys and inline secrets are
    /// never included.
    Create(BackupCreateArgs),
    /// Verify a backup archive offline (detects a single changed byte).
    Verify(BackupVerifyArgs),
    /// Restore a backup into an EMPTY data root (verify-first, atomic activate).
    Restore(BackupRestoreArgs),
}

#[derive(Args)]
pub struct BackupCreateArgs {
    #[arg(long)]
    pub config: PathBuf,
    #[arg(long)]
    pub repo: String,
    #[arg(long)]
    pub output: PathBuf,
    /// X.509 signer certificate (DER) whose key signs the backup manifest.
    #[arg(long)]
    pub signer_cert: Option<PathBuf>,
    /// ed25519 private key (PKCS#8 DER) for `--signer-cert`. Never written into
    /// the backup and never printed.
    #[arg(long)]
    pub signer_key: Option<PathBuf>,
    /// Produce a backup with NO authenticity layer: anyone who can write the file
    /// can rewrite its contents and its hashes together, and `restore` cannot tell.
    #[arg(long)]
    pub allow_unauthenticated_backup: bool,
}

#[derive(Args)]
pub struct BackupVerifyArgs {
    #[arg(long)]
    pub input: PathBuf,
    /// Trust policy (RON) the backup manifest's signer is authorized against.
    #[arg(long)]
    pub policy: Option<PathBuf>,
    /// Accept a backup carrying no signature at all. The manifest then proves only
    /// that the archive is internally consistent — NOT that it is the archive
    /// holger wrote.
    #[arg(long)]
    pub allow_unauthenticated_backup: bool,
}

#[derive(Args)]
pub struct BackupRestoreArgs {
    #[arg(long)]
    pub input: PathBuf,
    /// Empty destination store root to restore into.
    #[arg(long)]
    pub data_root: PathBuf,
    /// Trust policy (RON) the backup manifest's signer is authorized against.
    #[arg(long)]
    pub policy: Option<PathBuf>,
    /// Restore content whose authenticity is unproven into the data root.
    #[arg(long)]
    pub allow_unauthenticated_backup: bool,
}

// ───────────────────────── store layout ─────────────────────────

struct Store {
    /// Where bundle files live (the raw repo's data dir; served over HTTPS).
    bundles: PathBuf,
    /// Under the sibling of `bundles` (everything not served).
    state: PathBuf,
    receipts: PathBuf,
}

fn resolve_store(config: &Path, repo: &str) -> Result<Store> {
    let holger = server_lib::read_ron_config(config)
        .with_context(|| format!("load config {}", config.display()))?;
    let r = holger
        .repositories
        .iter()
        .find(|r| r.ron_name == repo)
        .ok_or_else(|| anyhow::anyhow!("repository {repo:?} not found in config"))?;
    let bundles = r.ron_data_dir.as_ref().map(PathBuf::from).ok_or_else(|| {
        anyhow::anyhow!("repository {repo:?} has no ron_data_dir (must be a raw store)")
    })?;
    let root = bundles
        .parent()
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| bundles.clone());
    Ok(Store {
        state: root.join("holger-update-state"),
        receipts: root.join("holger-import-receipts"),
        bundles,
    })
}

// ───────────────────────── bundle import ─────────────────────────

pub fn run_bundle(a: BundleArgs) -> Result<()> {
    match a.op {
        BundleOp::Import(args) => run_import(args),
        BundleOp::AllowFirstImport(args) => run_allow_first_import(args),
    }
}

/// `bundle allow-first-import`: the admin's authorization, through the import door.
fn run_allow_first_import(a: AllowFirstImportArgs) -> Result<()> {
    let mut holger = server_lib::read_ron_config(&a.config)
        .with_context(|| format!("load config {}", a.config.display()))?;
    for line in holger.banner() {
        println!("{line}");
    }
    #[cfg(feature = "downstream")]
    holger.enter_downstream_mode()?;
    if holger.resolved_network() == server_lib::egress::NetworkMode::Offline {
        server_lib::egress::set_offline();
    }
    holger.instantiate_backends()?;
    let door = server_lib::import_door::ImportDoor::of(&holger)?;
    let (vendor, product, sequence) = match &a.from_bundle {
        None => (
            a.vendor.clone().unwrap_or_default(),
            a.product.clone().unwrap_or_default(),
            a.sequence.unwrap_or(0),
        ),
        Some(bundle) => {
            // The bundle is verified before a field of it is believed: under `--policy`, else
            // under the first of the config's policies (intake, then intel) that verifies it.
            let candidates: Vec<PathBuf> = match &a.policy {
                Some(p) => vec![p.clone()],
                None => [
                    holger
                        .promotion
                        .as_ref()
                        .and_then(|p| p.intake.as_ref())
                        .and_then(|i| i.trust_policy.clone()),
                    holger.intel.as_ref().and_then(|i| i.trust_policy.clone()),
                ]
                .into_iter()
                .flatten()
                .map(PathBuf::from)
                .collect(),
            };
            if candidates.is_empty() {
                bail!("--from-bundle verifies the bundle first: pass --policy, or set promotion.intake.trust_policy / intel.trust_policy");
            }
            let mut why = Vec::new();
            // Verified in a verify-bundle job (lane HOL-SANDBOX-TRAIT): this process never opens it.
            let mut core = None;
            for policy_path in &candidates {
                let policy = TrustPolicy::from_ron(
                    &std::fs::read_to_string(policy_path)
                        .with_context(|| format!("read {}", policy_path.display()))?,
                )?;
                match door.verified_core(bundle, &policy, "cli", "local:cli") {
                    Ok(c) => {
                        core = Some(c);
                        break;
                    }
                    Err(e) => why.push(format!("{}: {}", policy_path.display(), e.tagged())),
                }
            }
            let core = core.ok_or_else(|| {
                anyhow::anyhow!("{} does not verify: {}", bundle.display(), why.join("; "))
            })?;
            println!(
                "bundle {} (verified): vendor {} product {} sequence {}",
                core.bundle_id, core.supplier, core.product, core.sequence
            );
            (core.supplier, core.product, core.sequence)
        }
    };
    let g = door
        .allow_first_import(&vendor, &product, sequence, &os_user(), "local")
        .map_err(|e| anyhow::anyhow!("{}", e.tagged()))?;
    println!(
        "FIRST IMPORT AUTHORIZED vendor {} product {} sequence {} by {} (used once, by the import of exactly that bundle)",
        g.vendor, g.product, g.sequence, g.by
    );
    Ok(())
}

fn os_user() -> String {
    std::env::var("USER")
        .or_else(|_| std::env::var("LOGNAME"))
        .unwrap_or_else(|_| "local".into())
}

/// `bundle import`: the box's import door (`server_lib::import_door`), the SAME
/// code `AdminService.ImportBundle`, `POST /-/bundle/import` and the
/// `bundle.inbox` job run. Every artifact lands AVAILABLE through the intake
/// chain; the raw-directory write this verb used to do is refused by name.
pub fn run_import(a: ImportArgs) -> Result<()> {
    if let Some(repo) = &a.repo {
        bail!(
            "{}",
            server_lib::import_door::raw_bypass_refusal(repo).tagged()
        );
    }
    let mut holger = server_lib::read_ron_config(&a.config)
        .with_context(|| format!("load config {}", a.config.display()))?;
    for line in holger.banner() {
        println!("{line}");
    }
    // The secure side of an airgap: `network: Offline` switches the egress guard
    // on for this process too, before the clock or anything else could dial.
    // A downstream (H3) pins this process to its one upstream (or to nothing) first.
    #[cfg(feature = "downstream")]
    holger.enter_downstream_mode()?;
    if holger.resolved_network() == server_lib::egress::NetworkMode::Offline {
        server_lib::egress::set_offline();
    }
    holger.instantiate_backends()?;
    let door = server_lib::import_door::ImportDoor::of(&holger)?;
    std::fs::create_dir_all(door.data_root()).ok();
    // Exclusive store lock (no concurrent import from another process).
    let _guard = LockGuard::acquire(
        &door.data_root().join(".holger-import.lock"),
        "bundle import",
    )?;
    let policy = match &a.policy {
        Some(p) => {
            let policy = TrustPolicy::from_ron(&std::fs::read_to_string(p)?)?;
            require_expired_posture(&policy)?;
            Some(policy)
        }
        None => None,
    };
    let target = TargetSpec {
        product: a.target_product.clone(),
        model: a.target_model.clone(),
        revision: a.target_revision.clone(),
    };
    establish_time();
    let out = door
        .import_bundle(&a.bundle, policy.as_ref(), &target, &os_user(), "local")
        .map_err(|e| anyhow::anyhow!("{}", e.tagged()))?;
    println!(
        "IMPORTED ({}) {} seq {}",
        out.outcome, out.bundle_id, out.sequence
    );
    for art in &out.artifacts {
        if art.outcome == "landed" {
            println!(
                "  LANDED {} {} in '{}' sha256 {}",
                art.artifact, art.state, art.repository, art.sha256
            );
        } else {
            println!("  ALREADY {} AVAILABLE with the same bytes", art.artifact);
        }
    }
    for c in out.checks.iter().filter(|c| c.starts_with("scanner-sigs:")) {
        println!("  {c}");
    }
    println!("  receipt: {}", out.receipt);
    for c in &out.sync_held {
        println!("  SYNC HELD {c}");
    }
    if let Some(ack) = &out.sync_ack {
        println!("  sync ack: {ack}");
        if let Some(dir) = &a.ack_out {
            std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
            let name = std::path::Path::new(ack)
                .file_name()
                .ok_or_else(|| anyhow::anyhow!("the ack path {ack} names no file"))?;
            let to = dir.join(name);
            std::fs::copy(ack, &to).with_context(|| format!("copy the ack to {}", to.display()))?;
            println!("  ACK {}", to.display());
        }
    } else if a.ack_out.is_some() {
        println!(
            "  no sync ack: the bundle carries no sync manifest, or this Holger trusts no upstream"
        );
    }
    if let Some(time) = out.checks.first() {
        println!("  {time}");
    }
    Ok(())
}

// ───────────────────────── the import lock ─────────────────────────
//
// `Drop` removes the lock on every ordinary exit — and on none of the ones that
// matter. SIGKILL runs no destructor, so the OOM kill this store is most likely to
// suffer (a large import on a small appliance) leaves the marker behind forever.
// The old refusal could not tell the operator which case they were in: it said
// "another import is running or a previous one crashed" and left them to guess,
// which is the same as telling them to delete it.
//
// So the lock now carries its holder's IDENTITY, and the refusal states a verdict.

/// The identity written into the lock file when it is acquired.
///
/// `pid` alone is not enough: pids are recycled, and a recycled pid reads as a
/// live holder. `boot_id` settles it across a reboot (every pid from the previous
/// boot is gone, whatever `/proc` says now) and `start_ticks` — the process's own
/// start time from `/proc/<pid>/stat` — settles it within one boot, because a
/// recycled pid cannot have the original's start time.
fn lock_identity(operation: &str) -> serde_json::Value {
    let pid = std::process::id();
    serde_json::json!({
        "pid": pid,
        "boot_id": current_boot_id(),
        "start_ticks": process_start_ticks(pid),
        "acquired_at": chrono::Utc::now().to_rfc3339(),
        "operation": operation,
        "host": hostname(),
    })
}

fn current_boot_id() -> Option<String> {
    std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

fn hostname() -> Option<String> {
    std::fs::read_to_string("/proc/sys/kernel/hostname")
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

/// Field 22 (`starttime`) of `/proc/<pid>/stat`, in clock ticks since boot.
///
/// Parsed from after the LAST `)`, because field 2 is the executable name and may
/// itself contain spaces and parentheses — splitting the line from the left is the
/// classic way to read this wrong.
fn process_start_ticks(pid: u32) -> Option<u64> {
    let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
    let after = &stat[stat.rfind(')')? + 1..];
    after.split_whitespace().nth(19)?.parse().ok()
}

/// What can be established about the process named in a lock file.
enum Holder {
    /// The holder is running. Waiting is the only correct action.
    Alive(String),
    /// The holder is PROVABLY gone — a reboot, or no such process, or a pid that
    /// has been recycled into a different process.
    Gone(String),
    /// The lock file is unreadable, malformed, or the liveness of its holder
    /// cannot be established. **Fails closed**: treated exactly like `Alive`.
    Indeterminate(String),
}

fn describe_holder(path: &Path) -> Holder {
    let raw = match std::fs::read(path) {
        Ok(r) => r,
        Err(e) => return Holder::Indeterminate(format!("the lock file cannot be read ({e})")),
    };
    let doc: serde_json::Value = match serde_json::from_slice(&raw) {
        Ok(d) => d,
        Err(_) => {
            return Holder::Indeterminate(
                "the lock file is not valid holger lock JSON (an empty or truncated lock is \
                 what a crash BETWEEN creating and stamping it looks like)"
                    .into(),
            )
        }
    };
    let pid = match doc.get("pid").and_then(|v| v.as_u64()) {
        Some(p) => p as u32,
        None => return Holder::Indeterminate("the lock file names no pid".into()),
    };
    let since = doc
        .get("acquired_at")
        .and_then(|v| v.as_str())
        .unwrap_or("an unrecorded time")
        .to_string();
    let operation = doc
        .get("operation")
        .and_then(|v| v.as_str())
        .unwrap_or("an import")
        .to_string();
    let held = format!("pid {pid} ({operation}, held since {since})");

    // A reboot retires every pid from the previous boot, whatever /proc says now.
    let lock_boot = doc.get("boot_id").and_then(|v| v.as_str());
    match (lock_boot, current_boot_id()) {
        (Some(l), Some(n)) if l != n => {
            return Holder::Gone(format!(
                "{held} belongs to a PREVIOUS boot of this machine, so it cannot be running"
            ))
        }
        (Some(_), Some(_)) => {}
        // Without both boot ids the reboot question is open; fall through to /proc,
        // which can still prove absence, but never claim staleness on this basis.
        _ => {}
    }

    if !Path::new(&format!("/proc/{pid}")).exists() {
        // Absence of /proc entirely (a non-Linux host) must not read as "no such
        // process" — that would turn every lock stale on the wrong platform.
        if !Path::new("/proc/self").exists() {
            return Holder::Indeterminate(format!(
                "{held} cannot be checked: this system exposes no /proc"
            ));
        }
        return Holder::Gone(format!("{held} is not running — there is no /proc/{pid}"));
    }

    match (
        doc.get("start_ticks").and_then(|v| v.as_u64()),
        process_start_ticks(pid),
    ) {
        (Some(recorded), Some(now)) if recorded != now => Holder::Gone(format!(
            "{held} is gone — pid {pid} now belongs to a DIFFERENT process (it started at a \
             different time), so the pid has been recycled"
        )),
        (Some(_), Some(_)) => Holder::Alive(held),
        // The pid exists but its start time cannot be compared. It may well be the
        // holder; refuse rather than guess.
        _ => Holder::Indeterminate(format!(
            "{held}: a process with that pid exists, but its start time could not be compared, \
             so it cannot be told apart from a recycled pid"
        )),
    }
}

/// Exclusive advisory lock via `create_new`, stamped with its holder's identity.
#[derive(Debug)]
struct LockGuard(PathBuf);

impl LockGuard {
    fn acquire(path: &Path, operation: &str) -> Result<Self> {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).ok();
        }
        match std::fs::OpenOptions::new()
            .write(true)
            .create_new(true)
            .open(path)
        {
            Ok(mut f) => {
                use std::io::Write;
                let identity = serde_json::to_vec_pretty(&lock_identity(operation))?;
                // A lock we cannot stamp is worse than no lock: it would be
                // unreadable, and an unreadable lock fails closed FOREVER. Remove
                // it and fail here, where the operator can still see why.
                if let Err(e) = f.write_all(&identity).and_then(|()| f.sync_all()) {
                    drop(f);
                    let _ = std::fs::remove_file(path);
                    bail!("could not stamp the import lock {}: {e}", path.display());
                }
                Ok(LockGuard(path.to_path_buf()))
            }
            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
                // WHY REFUSE RATHER THAN BREAK THE LOCK AUTOMATICALLY:
                //
                // Breaking it would be this process — the one that wants the lock —
                // deciding that the holder of the lock is dead. That is the wrong
                // party to hold that vote, and the evidence it votes on is only as
                // good as a /proc read: a holder inside a different PID namespace,
                // or on a host whose /proc this process does not share, is invisible
                // here and would be declared gone. The cost of being wrong is two
                // concurrent imports writing the protected store.
                //
                // The operator's actual problem was never "the lock is still there",
                // it was "I cannot tell a live holder from a corpse". A definite
                // verdict plus the exact command solves that completely, and costs
                // one paste. So: state the verdict, name the command, change nothing.
                match describe_holder(path) {
                    Holder::Alive(held) => bail!(
                        "store is locked by {held} — that process is RUNNING right now. \
                         Wait for it to finish; do not remove {}.",
                        path.display()
                    ),
                    Holder::Gone(why) => bail!(
                        "store is locked by a holder that is PROVABLY GONE: {why}.\n\
                         No import is running. This lock is a corpse left by a crash or an \
                         OOM kill — `Drop` does not run on SIGKILL.\n\
                         Clear it with:\n    rm {}",
                        path.display()
                    ),
                    Holder::Indeterminate(why) => bail!(
                        "store is locked and holger CANNOT DETERMINE whether the holder is \
                         alive: {why}.\n\
                         Refusing: a lock whose holder cannot be established is treated as HELD, \
                         never as stale.\n\
                         Confirm by hand that no holger import is running, then remove {}.",
                        path.display()
                    ),
                }
            }
            Err(e) => Err(e.into()),
        }
    }
}

impl Drop for LockGuard {
    fn drop(&mut self) {
        let _ = std::fs::remove_file(&self.0);
    }
}

// ───────────────────────── scrub ─────────────────────────

pub fn run_scrub(a: ScrubArgs) -> Result<()> {
    let store = resolve_store(&a.config, &a.repo)?;
    let policy = TrustPolicy::from_ron(&std::fs::read_to_string(&a.policy)?)?;
    require_expired_posture(&policy)?;
    establish_time();
    let mut checked = 0usize;
    let mut bad = 0usize;
    let bundles = collect_bundles(&store.bundles)?;
    for path in &bundles {
        checked += 1;
        match scrub_one(path, &policy) {
            Ok(()) => println!("ok   {}", path.display()),
            Err(e) => {
                bad += 1;
                println!("FAIL {} — {e}", path.display());
            }
        }
    }
    println!("scrub: {checked} bundle(s) checked, {bad} failed");
    if bad > 0 {
        bail!("integrity scrub found {bad} corrupt bundle(s)");
    }
    Ok(())
}

fn scrub_one(path: &Path, policy: &TrustPolicy) -> Result<()> {
    let members = load_bundle(path).map_err(|e| anyhow::anyhow!("{e}"))?;
    verify_member_map_with_time(&members, policy, &TargetSpec::default(), None, &EstateTime)
        .map_err(|e| anyhow::anyhow!("{e}"))?;
    Ok(())
}

fn collect_bundles(dir: &Path) -> Result<Vec<PathBuf>> {
    let mut out = Vec::new();
    if !dir.exists() {
        return Ok(out);
    }
    walk(dir, &mut out)?;
    out.sort();
    Ok(out)
}

fn walk(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
    for de in std::fs::read_dir(dir)? {
        let de = de?;
        let ft = de.file_type()?;
        if ft.is_symlink() {
            bail!("unexpected symlink in store: {}", de.path().display());
        }
        if ft.is_dir() {
            walk(&de.path(), out)?;
        } else if de.path().to_string_lossy().ends_with(".holger.znippy") {
            out.push(de.path());
        }
    }
    Ok(())
}

// ───────────────────────── backup ─────────────────────────

pub fn run_backup(a: BackupArgs) -> Result<()> {
    match a.op {
        BackupOp::Create(args) => backup_create(args),
        BackupOp::Verify(args) => backup_verify(args),
        BackupOp::Restore(args) => backup_restore(args),
    }
}

const BACKUP_MANIFEST: &str = "backup-manifest.json";
/// Detached ed25519 signature over the EXACT `backup-manifest.json` bytes.
const BACKUP_MANIFEST_SIG: &str = "backup-manifest.sig";
/// The X.509 signer certificate (DER) for [`BACKUP_MANIFEST_SIG`].
const BACKUP_MANIFEST_CRT: &str = "backup-manifest.crt";
/// The policy role a backup signer must hold (a `trusted_release_signer` is also
/// accepted, so an operator need not invent a second key to get a signed backup).
const BACKUP_SIGNER_ROLE: &str = "backup-signer";

/// The three members that carry the backup's own trust decision. They are not
/// listed in the manifest — the manifest cannot cover itself, and the signature
/// and certificate are what prove the manifest instead.
fn is_backup_control(path: &str) -> bool {
    path == BACKUP_MANIFEST || path == BACKUP_MANIFEST_SIG || path == BACKUP_MANIFEST_CRT
}

/// Authenticate a backup's manifest before ANY of its member hashes are believed.
///
/// The finding this answers: the manifest lists a sha256 per member and lives
/// INSIDE the archive it describes, so anyone who can write the backup file
/// rewrites a member and its hash together and both `verify` and `restore` report
/// success. Hashes detect corruption; only a signature over those hashes, made by
/// a key the attacker does not have, detects tampering.
///
/// Returns the signer's identity, or `None` for a backup that carries no signature
/// AND whose operator has explicitly accepted that.
fn authenticate_backup_manifest(
    members: &MemberMap,
    policy: Option<&TrustPolicy>,
    allow_unauthenticated: bool,
) -> Result<Option<SignerIdentity>> {
    let manifest = members
        .get(BACKUP_MANIFEST)
        .ok_or_else(|| anyhow::anyhow!("not a holger backup (no {BACKUP_MANIFEST})"))?;
    let sig = members.get(BACKUP_MANIFEST_SIG);
    let crt = members.get(BACKUP_MANIFEST_CRT);

    match (sig, crt) {
        (Some(sig), Some(crt)) => {
            // A signature is PRESENT: it gets checked. Without a policy there is
            // nothing to authorize it against, and skipping it because the operator
            // forgot a flag is precisely the silent pass this fix exists to remove.
            let policy = policy.ok_or_else(|| {
                anyhow::anyhow!(
                    "this backup is signed but no --policy was given — refusing to \
                     ignore a signature. Pass --policy <trust-policy.ron>."
                )
            })?;
            let who =
                verify_detached_by_trusted_signer(manifest, sig, crt, policy, BACKUP_SIGNER_ROLE)
                    .map_err(|e| anyhow::anyhow!("backup manifest signature: {}", e.tagged()))?;
            Ok(Some(who))
        }
        (None, None) => {
            if !allow_unauthenticated {
                bail!(
                    "this backup carries no signature ({BACKUP_MANIFEST_SIG} / \
                     {BACKUP_MANIFEST_CRT} are absent), so its manifest proves only that the \
                     archive agrees with itself — anyone who could write the file could have \
                     rewritten a member and its hash together.\n\
                     Re-create it with --signer-cert/--signer-key, or, if you accept that this \
                     content's origin is unproven, pass --allow-unauthenticated-backup."
                )
            }
            eprintln!(
                "WARNING: --allow-unauthenticated-backup — this backup's contents are NOT \
                 authenticated. Its hashes prove only internal consistency; a tampered archive \
                 whose member and hash were changed together passes every check below. Do not \
                 restore it into a protected data root unless you trust the whole path the file \
                 travelled."
            );
            Ok(None)
        }
        _ => bail!(
            "malformed backup: exactly one of {BACKUP_MANIFEST_SIG} / {BACKUP_MANIFEST_CRT} is \
             present. A half-signed backup is refused."
        ),
    }
}

/// Refuse any member the manifest does not list.
///
/// `restore` used to write EVERY member except the manifest into the data root, so
/// a member simply added to the archive was never hash-checked and landed in the
/// store regardless. Signing the manifest does not fix that on its own: the
/// signature covers what the manifest names, and a stowaway is by definition not
/// named.
fn refuse_unlisted_members(members: &MemberMap, listed: &[(String, String)]) -> Result<()> {
    for path in members.keys() {
        if is_backup_control(path) {
            continue;
        }
        if !listed.iter().any(|(p, _)| p == path) {
            bail!("backup contains {path} which its manifest does not list — refusing");
        }
    }
    Ok(())
}

fn backup_create(a: BackupCreateArgs) -> Result<()> {
    let store = resolve_store(&a.config, &a.repo)?;
    let mut members = MemberMap::new();
    // Collect bundles, state, and receipts under stable relative prefixes.
    read_tree(&store.bundles, "bundles", &mut members)?;
    read_tree(&store.state, "state", &mut members)?;
    read_tree(&store.receipts, "receipts", &mut members)?;
    // Manifest of hashes for every included file (never a private key — the store
    // holds none; config with inline secrets is not included).
    let mut manifest = serde_json::Map::new();
    for (path, bytes) in &members {
        manifest.insert(
            path.clone(),
            serde_json::json!({ "sha256": sha256_hex(bytes) }),
        );
    }
    let manifest_bytes = serde_json::to_vec_pretty(&serde_json::json!({
        "schema": "holger.backup/v1",
        "files": manifest,
    }))?;
    let file_count = members.len();
    members.insert(BACKUP_MANIFEST.to_string(), manifest_bytes.clone());

    // Sign the manifest, so that the hash list is worth something against an
    // attacker rather than only against a failing disk.
    let signed_by = match (&a.signer_cert, &a.signer_key) {
        (Some(cert_path), Some(key_path)) => {
            let cert_der = std::fs::read(cert_path)
                .with_context(|| format!("read signer certificate {}", cert_path.display()))?;
            let key_pkcs8 = server_lib::keyfile::read_private(
                std::path::Path::new(key_path),
                "the signing key",
            )
            .with_context(|| format!("read signer key {}", key_path.display()))?;
            let sig = sign_pkcs8(&key_pkcs8, &manifest_bytes)
                .map_err(|e| anyhow::anyhow!("sign backup manifest: {}", e.tagged()))?;
            // The private key stays in this scope and is never a member.
            drop(key_pkcs8);
            members.insert(BACKUP_MANIFEST_SIG.to_string(), sig);
            members.insert(BACKUP_MANIFEST_CRT.to_string(), cert_der);
            Some(cert_path.display().to_string())
        }
        (None, None) => {
            if !a.allow_unauthenticated_backup {
                bail!(
                    "refusing to write a backup with no authenticity layer. Its manifest would \
                     list a hash per file and live inside the archive it describes, so anyone \
                     who can write the file could rewrite a member and its hash together and \
                     `backup verify` would still say ok.\n\
                     Pass --signer-cert <cert.der> --signer-key <key.pk8>, or \
                     --allow-unauthenticated-backup to accept that."
                )
            }
            eprintln!(
                "WARNING: --allow-unauthenticated-backup — writing an UNSIGNED backup. Anyone \
                 who can write {} can alter its contents undetectably, and `backup restore` \
                 will write whatever it then contains into a data root.",
                a.output.display()
            );
            None
        }
        _ => bail!("--signer-cert and --signer-key must be given together"),
    };

    pack_bundle(&members, &a.output).map_err(|e| anyhow::anyhow!("{e}"))?;
    println!(
        "backup created: {} ({file_count} files)",
        a.output.display()
    );
    match signed_by {
        Some(c) => println!("  manifest signed with: {c}"),
        None => println!("  manifest: UNSIGNED (authenticity not provable)"),
    }
    Ok(())
}

fn backup_verify(a: BackupVerifyArgs) -> Result<()> {
    let members = load_bundle(&a.input).map_err(|e| anyhow::anyhow!("{e}"))?;
    let policy = load_optional_policy(a.policy.as_deref())?;
    // AUTHENTICITY FIRST: the member hashes below are only worth checking once the
    // document listing them is known to be the one holger signed.
    let signer =
        authenticate_backup_manifest(&members, policy.as_ref(), a.allow_unauthenticated_backup)?;
    let (_, files) = read_backup_manifest(&members)?;
    refuse_unlisted_members(&members, &files)?;
    let mut bad = 0usize;
    for (path, expect_sha) in &files {
        match members.get(path) {
            Some(bytes) if &sha256_hex(bytes) == expect_sha => {}
            Some(_) => {
                bad += 1;
                println!("FAIL {path} — digest mismatch");
            }
            None => {
                bad += 1;
                println!("FAIL {path} — missing from backup");
            }
        }
    }
    println!("backup verify: {} file(s), {bad} failed", files.len());
    match &signer {
        Some(w) => println!("  manifest signed by: {} [{}]", w.subject, w.fingerprint),
        None => println!("  manifest: UNSIGNED — authenticity NOT checked"),
    }
    if bad > 0 {
        bail!("backup verification failed for {bad} file(s)");
    }
    Ok(())
}

/// Read a trust policy if one was given.
fn load_optional_policy(path: Option<&Path>) -> Result<Option<TrustPolicy>> {
    match path {
        Some(p) => {
            let text = std::fs::read_to_string(p)
                .with_context(|| format!("read trust policy {}", p.display()))?;
            Ok(Some(TrustPolicy::from_ron(&text)?))
        }
        None => Ok(None),
    }
}

fn backup_restore(a: BackupRestoreArgs) -> Result<()> {
    // Verify before writing anything.
    let members = load_bundle(&a.input).map_err(|e| anyhow::anyhow!("{e}"))?;
    let policy = load_optional_policy(a.policy.as_deref())?;
    // AUTHENTICATE FIRST — this command writes into the protected data root, so it
    // is the one that must not act on a hash list an attacker could have rewritten.
    let signer =
        authenticate_backup_manifest(&members, policy.as_ref(), a.allow_unauthenticated_backup)?;
    let (_, files) = read_backup_manifest(&members)?;
    refuse_unlisted_members(&members, &files)?;
    for (path, expect_sha) in &files {
        let bytes = members
            .get(path)
            .ok_or_else(|| anyhow::anyhow!("backup missing {path}"))?;
        if &sha256_hex(bytes) != expect_sha {
            bail!("backup digest mismatch for {path} — refusing to restore");
        }
    }
    // Restore only into an empty target (verify-first, atomic activate). The three
    // control members are the backup's own trust wrapper, not store content, so
    // they are not written into the data root.
    let mut to_write = members.clone();
    to_write.retain(|path, _| !is_backup_control(path));
    update_bundle::api::write_members_to_empty_dir(&to_write, &a.data_root)
        .map_err(|e| anyhow::anyhow!("{e}"))?;
    println!(
        "restored {} file(s) into {}",
        files.len(),
        a.data_root.display()
    );
    match &signer {
        Some(w) => println!("  manifest signed by: {} [{}]", w.subject, w.fingerprint),
        None => println!("  manifest: UNSIGNED — restored content's origin is unproven"),
    }
    println!("NOTE: private signing/client keys and inline config secrets are NOT in the backup — restore them separately.");
    Ok(())
}

fn read_backup_manifest(members: &MemberMap) -> Result<(String, Vec<(String, String)>)> {
    let raw = members
        .get(BACKUP_MANIFEST)
        .ok_or_else(|| anyhow::anyhow!("not a holger backup (no {BACKUP_MANIFEST})"))?;
    let doc: serde_json::Value = serde_json::from_slice(raw)?;
    let schema = doc
        .get("schema")
        .and_then(|v| v.as_str())
        .unwrap_or_default()
        .to_string();
    let mut out = Vec::new();
    if let Some(files) = doc.get("files").and_then(|v| v.as_object()) {
        for (path, meta) in files {
            let sha = meta
                .get("sha256")
                .and_then(|v| v.as_str())
                .ok_or_else(|| anyhow::anyhow!("backup manifest entry {path} missing sha256"))?;
            out.push((path.clone(), sha.to_string()));
        }
    }
    Ok((schema, out))
}

/// Read a directory tree into `members` under `prefix`, rejecting symlinks.
fn read_tree(dir: &Path, prefix: &str, members: &mut MemberMap) -> Result<()> {
    if !dir.exists() {
        return Ok(());
    }
    let mut paths = Vec::new();
    walk_all(dir, &mut paths)?;
    for p in paths {
        let rel = p
            .strip_prefix(dir)
            .unwrap()
            .to_string_lossy()
            .replace('\\', "/");
        let key = format!("{prefix}/{rel}");
        members.insert(key, std::fs::read(&p)?);
    }
    Ok(())
}

fn walk_all(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
    for de in std::fs::read_dir(dir)? {
        let de = de?;
        let ft = de.file_type()?;
        if ft.is_symlink() {
            bail!("refusing to back up a symlink: {}", de.path().display());
        }
        if ft.is_dir() {
            walk_all(&de.path(), out)?;
        } else if ft.is_file() {
            out.push(de.path());
        }
    }
    Ok(())
}

// ───────────────────────── tests ─────────────────────────

#[cfg(test)]
mod tests {
    //! Two security properties the Secure Update Gateway's OFFLINE side owes an
    //! operator, neither of which the code held before:
    //!
    //! * a lock that outlives its holder must say so, and a lock whose holder
    //!   cannot be established must never be called stale;
    //! * a backup archive must prove WHO wrote it, not merely that it agrees with
    //!   itself.
    //!
    //! The crypto half of the second property — a real certificate, a real
    //! signature, a real untrusted signer — is proven in `holger-update-bundle`'s
    //! own suite, where `rcgen` is available to mint throwaway keys in-process.
    //! What is provable here without a PKI is the POLICY around it, which is where
    //! the silent pass lived.

    use super::*;

    // ── finding 2: the import lock ───────────────────────────────────────────

    fn lock_path(dir: &tempfile::TempDir) -> PathBuf {
        dir.path().join(".holger-import.lock")
    }

    #[test]
    fn acquiring_the_import_lock_stamps_the_holders_identity_into_it() {
        let dir = tempfile::tempdir().unwrap();
        let path = lock_path(&dir);
        let _guard = LockGuard::acquire(&path, "bundle import").expect("a free lock is acquirable");

        let doc: serde_json::Value =
            serde_json::from_slice(&std::fs::read(&path).unwrap()).expect("the lock must be JSON");
        assert_eq!(
            doc["pid"].as_u64().unwrap() as u32,
            std::process::id(),
            "the lock must name its holder"
        );
        assert!(
            doc["acquired_at"].as_str().is_some_and(|s| s.contains('T')),
            "the lock must carry an ISO-8601 acquisition time: {doc}"
        );
        assert_eq!(doc["operation"].as_str(), Some("bundle import"));
        // pid alone cannot survive recycling; one of these must pin it further.
        assert!(
            !doc["boot_id"].is_null() || !doc["start_ticks"].is_null(),
            "a pid with neither a boot id nor a start time cannot be told from a recycled \
             pid: {doc}"
        );
    }

    #[test]
    fn an_import_lock_whose_holder_is_gone_names_the_command_that_clears_it() {
        let dir = tempfile::tempdir().unwrap();
        let path = lock_path(&dir);
        // A holder from a PREVIOUS BOOT: whatever /proc says now, every pid from
        // that boot is retired. This is exactly the corpse an OOM kill leaves.
        let stale = serde_json::json!({
            "pid": 1u32,
            "boot_id": "00000000-0000-0000-0000-000000000000",
            "start_ticks": 1u64,
            "acquired_at": "2026-09-10T04:00:00+00:00",
            "operation": "bundle import",
        });
        std::fs::write(&path, serde_json::to_vec_pretty(&stale).unwrap()).unwrap();

        let err = LockGuard::acquire(&path, "bundle import")
            .expect_err("a lock must never be taken while the file is there");
        let msg = format!("{err}");
        assert!(
            msg.contains("PROVABLY GONE"),
            "the operator must be told the holder is dead, not asked to guess: {msg}"
        );
        assert!(
            msg.contains(&format!("rm {}", path.display())),
            "the refusal must name the exact command that clears it: {msg}"
        );
        // Refusing is the choice; it must not have broken the lock behind our back.
        assert!(path.exists(), "refusing must leave the lock in place");
    }

    #[test]
    fn an_import_lock_whose_holder_is_this_running_process_is_refused_as_live() {
        let dir = tempfile::tempdir().unwrap();
        let path = lock_path(&dir);
        // This very process: pid, boot id and start time all agree, because they
        // are ours. No liveness check may call this a corpse.
        std::fs::write(
            &path,
            serde_json::to_vec_pretty(&lock_identity("bundle import")).unwrap(),
        )
        .unwrap();

        let err = LockGuard::acquire(&path, "bundle import").expect_err("a held lock is refused");
        let msg = format!("{err}");
        assert!(
            msg.contains("RUNNING"),
            "a live holder must be reported as live: {msg}"
        );
        assert!(
            !msg.contains("PROVABLY GONE"),
            "a running holder must never be reported as gone: {msg}"
        );
        assert!(
            !msg.contains("rm "),
            "holger must not invite an operator to delete a LIVE holder's lock: {msg}"
        );
    }

    #[test]
    fn a_malformed_import_lock_is_never_treated_as_stale() {
        // Every shape a crash or a hostile writer can leave behind. None of them
        // may resolve to "stale" — that direction fails closed.
        for (name, body) in [
            ("empty", b"".to_vec()),
            ("truncated json", b"{\"pid\": 12".to_vec()),
            (
                "not json at all",
                b"a previous holger wrote nothing useful".to_vec(),
            ),
            (
                "json without a pid",
                br#"{"operation":"bundle import"}"#.to_vec(),
            ),
        ] {
            let dir = tempfile::tempdir().unwrap();
            let path = lock_path(&dir);
            std::fs::write(&path, &body).unwrap();

            let err = match LockGuard::acquire(&path, "bundle import") {
                Ok(_) => panic!("[{name}] a lock file that exists is never taken"),
                Err(e) => e,
            };
            let msg = format!("{err}");
            assert!(
                msg.contains("CANNOT DETERMINE"),
                "[{name}] an unreadable lock must say so: {msg}"
            );
            assert!(
                !msg.contains("PROVABLY GONE"),
                "[{name}] an unreadable lock must NOT be called stale: {msg}"
            );
            assert!(path.exists(), "[{name}] refusing must leave the lock alone");
        }
    }

    // ── finding 3: backup authenticity ───────────────────────────────────────

    /// A backup member map with a well-formed manifest over its own files.
    fn backup_members(files: &[(&str, &[u8])]) -> MemberMap {
        let mut members = MemberMap::new();
        let mut manifest = serde_json::Map::new();
        for (path, bytes) in files {
            members.insert((*path).to_string(), bytes.to_vec());
            manifest.insert(
                (*path).to_string(),
                serde_json::json!({ "sha256": sha256_hex(bytes) }),
            );
        }
        let manifest_bytes = serde_json::to_vec_pretty(&serde_json::json!({
            "schema": "holger.backup/v1",
            "files": manifest,
        }))
        .unwrap();
        members.insert(BACKUP_MANIFEST.to_string(), manifest_bytes);
        members
    }

    #[test]
    fn an_unsigned_backup_is_refused_unless_the_operator_says_what_that_gives_up() {
        let members = backup_members(&[("bundles/a.holger.znippy", b"content")]);

        let err = authenticate_backup_manifest(&members, None, false)
            .expect_err("an unsigned backup must not silently pass");
        let msg = format!("{err}");
        assert!(
            msg.contains("carries no signature"),
            "the refusal must say what is missing: {msg}"
        );
        assert!(
            msg.contains("--allow-unauthenticated-backup"),
            "the refusal must name the opt-out flag: {msg}"
        );

        // With the opt-out it proceeds — and reports no signer, so nothing
        // downstream can mistake it for an authenticated backup.
        let who = authenticate_backup_manifest(&members, None, true)
            .expect("the explicit opt-out is honoured");
        assert!(who.is_none(), "an unsigned backup has no signer to report");
    }

    #[test]
    fn a_signed_backup_is_never_accepted_without_a_policy_to_check_it_against() {
        let mut members = backup_members(&[("bundles/a.holger.znippy", b"content")]);
        members.insert(BACKUP_MANIFEST_SIG.to_string(), vec![0u8; 64]);
        members.insert(BACKUP_MANIFEST_CRT.to_string(), b"a certificate".to_vec());

        // No policy: the signature cannot be authorized, so it must not be skipped.
        let err = authenticate_backup_manifest(&members, None, false)
            .expect_err("a present signature must be checked, not ignored");
        assert!(
            format!("{err}").contains("--policy"),
            "the refusal must name what is needed: {err}"
        );

        // And the opt-out must NOT become a way to skip a signature that is there.
        let err = authenticate_backup_manifest(&members, None, true)
            .expect_err("--allow-unauthenticated-backup must not bypass a present signature");
        assert!(
            format!("{err}").contains("--policy"),
            "the opt-out is for backups with NO signature, not for skipping one: {err}"
        );
    }

    #[test]
    fn a_half_signed_backup_is_refused() {
        let mut members = backup_members(&[("bundles/a.holger.znippy", b"content")]);
        members.insert(BACKUP_MANIFEST_SIG.to_string(), vec![0u8; 64]);
        // Signature present, certificate absent: an attacker stripping the cert
        // must not land in the "unsigned, warn and continue" path.
        let err = authenticate_backup_manifest(&members, None, true)
            .expect_err("a half-signed backup must be refused");
        assert!(
            format!("{err}").contains("half-signed"),
            "the refusal must name the shape: {err}"
        );
    }

    #[test]
    fn a_backup_member_the_manifest_does_not_list_is_refused() {
        let mut members = backup_members(&[("bundles/a.holger.znippy", b"content")]);
        // A stowaway: never hash-checked, and `restore` used to write it straight
        // into the data root.
        members.insert("bundles/evil.holger.znippy".into(), b"payload".to_vec());

        let (_, files) = read_backup_manifest(&members).unwrap();
        let err = refuse_unlisted_members(&members, &files)
            .expect_err("a member outside the manifest must be refused");
        assert!(
            format!("{err}").contains("bundles/evil.holger.znippy"),
            "the refusal must name the stowaway: {err}"
        );
    }
}

#[cfg(test)]
mod estate_time_tests {
    //! The bundle verifier's time source is the ESTATE's clock: named
    //! `estate-clock`, UNTRUSTED until the front's signed time answered, and
    //! reading that time once it has — the instant `core.expires_at`, the policy's
    //! maximum bundle age and the signer certificates' windows are checked against.
    use super::EstateTime;
    use update_bundle::time::TrustedTime as _;

    #[test]
    fn the_bundle_verifier_reads_the_estate_clock_and_says_whether_it_is_verified() {
        use server_lib::clock::{self, Outcome, Report, SourceReport, Trigger};
        clock::reset();
        assert_eq!(EstateTime.source_name(), "estate-clock");
        assert!(
            !EstateTime.is_trusted(),
            "nothing answered: the verdict says UNTRUSTED"
        );
        let system = chrono::Utc::now();
        assert!((EstateTime.now().unwrap() - system).num_seconds().abs() < 2);

        // The front's verified answer: this box is two hours behind it.
        let mut r = Report::without_sources(Trigger::Timer, Outcome::ApplyFailed, "measured");
        r.offset_ms = 7_200_000;
        r.sources = vec![SourceReport {
            source: "peer://10.13.8.1:50123".into(),
            offset_ms: Some(7_200_000),
            error: None,
        }];
        clock::settle(&r);
        assert!(EstateTime.is_trusted());
        let ahead = (EstateTime.now().unwrap() - chrono::Utc::now()).num_seconds();
        assert!(
            (ahead - 7_200).abs() < 2,
            "the verifier's now is the front's time: {ahead} s"
        );
        clock::reset();
    }
}