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

nordisk/holger

server/cli/tests/scanner_sigs_bundle.rs

//! ★ **ClamAV's signatures cross the airgap as a signed bundle** (lane HOL-NCSC, NCSC P2/P3),
//! through the real binary: `holger scanner-sigs build` on the online side signs a
//! `holger.scanner-sigs/v1` bundle of `.cvd`/`.cld` files; `holger bundle import` on the secure
//! side verifies it, holds it to the anti-rollback floor (the first one needs the admin's
//! grant), audits it first and installs it where the sandboxed clamd reads its database. A
//! **hotfix** is a bundle with a higher sequence carrying one urgent signature: it lands over
//! the set, the rest stays. A replay of an older bundle, a member that is no signature file
//! and a file a builder may not carry are refused by name. No test here opens a socket.

use std::path::{Path, PathBuf};
use std::process::{Command, Output};

use chrono::{Duration, Utc};
use rcgen::{CertificateParams, DnType, KeyPair, PKCS_ED25519};
use update_bundle::digest::sha256_hex;
use update_bundle::policy::{
    ApprovedSigner, CertificateValidity, ExpiredPosture, TrustMode, TrustPolicy,
};
use update_bundle::schema::POLICY_SCHEMA;

const BIN: &str = env!("CARGO_BIN_EXE_holger");
const PRODUCT: &str = "holger.scanner-sigs/v1";

struct Site {
    dir: tempfile::TempDir,
    signers: Vec<(String, PathBuf, PathBuf)>,
    release: (String, PathBuf, PathBuf),
    policy: TrustPolicy,
    policy_path: PathBuf,
    config: PathBuf,
}

fn mint(dir: &Path, cn: &str) -> (String, PathBuf, PathBuf) {
    let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
    let mut params = CertificateParams::new(vec![]).unwrap();
    params.distinguished_name.push(DnType::CommonName, cn);
    let cert = params.self_signed(&key).unwrap();
    let (c, k) = (dir.join(format!("{cn}.crt")), dir.join(format!("{cn}.key")));
    std::fs::write(&c, cert.der()).unwrap();
    std::fs::write(&k, key.serialize_der()).unwrap();
    (sha256_hex(cert.der()), c, k)
}

fn site() -> Site {
    let dir = tempfile::tempdir().unwrap();
    let pki = dir.path().join("pki");
    std::fs::create_dir_all(&pki).unwrap();
    let reviewer = mint(&pki, "security-reviewer");
    let owner = mint(&pki, "system-owner");
    let release = mint(&pki, "release");
    let policy = TrustPolicy {
        schema_version: POLICY_SCHEMA.into(),
        trust_mode: Some(TrustMode::PinnedSigners),
        trusted_roots: vec![],
        certificate_validity: CertificateValidity::Enforce,
        approved_signers: vec![
            ApprovedSigner {
                fingerprint: reviewer.0.clone(),
                subject: None,
                roles: vec!["security-reviewer".into()],
            },
            ApprovedSigner {
                fingerprint: owner.0.clone(),
                subject: None,
                roles: vec!["system-owner".into()],
            },
        ],
        required_approval_roles: vec!["security-reviewer".into(), "system-owner".into()],
        minimum_distinct_approvers: 2,
        trusted_release_signers: vec![release.0.clone()],
        release_signer_must_be_distinct: true,
        maximum_bundle_age_secs: None,
        require_expiry: true,
        on_expired_metadata: Some(ExpiredPosture::Refuse),
        require_sbom: false,
        require_vex: false,
        require_scan: false,
        allowed_products: vec![PRODUCT.into()],
        allowed_models: vec![],
    };
    let policy_path = dir.path().join("sigs-trust.ron");
    std::fs::write(&policy_path, policy.to_ron().unwrap()).unwrap();
    let root = dir.path().join("store");
    std::fs::create_dir_all(root.join("audit")).unwrap();
    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()
        )
    };
    let config = dir.path().join("holger.ron");
    std::fs::write(
        &config,
        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: [ {incoming} {quarantine} {approved} {production} ],
    audit: (dir: "{root}/audit", enabled: true),
    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",
            trust_policy: Some("{policy}"))),
    )),
)
"#,
            root = root.display(),
            policy = policy_path.display(),
            incoming = repo("incoming"),
            quarantine = repo("quarantine"),
            approved = repo("approved"),
            production = repo("production"),
        ),
    )
    .unwrap();
    Site {
        signers: vec![reviewer, owner],
        release,
        policy,
        policy_path,
        config,
        dir,
    }
}

fn text(o: &Output) -> String {
    format!(
        "{}\n{}",
        String::from_utf8_lossy(&o.stdout),
        String::from_utf8_lossy(&o.stderr)
    )
}

fn run(args: &[&str], paths: &[(&str, &Path)]) -> Output {
    let mut c = Command::new(BIN);
    c.args(args);
    for (flag, p) in paths {
        c.arg(flag).arg(p);
    }
    c.env_remove("ESTATE_CLOCK")
        .env_remove("ESTATE_CLOCK_KEY")
        .output()
        .unwrap()
}

impl Site {
    fn p(&self, rel: &str) -> PathBuf {
        self.dir.path().join(rel)
    }

    /// A directory of signature files.
    fn clamav_dir(&self, name: &str, files: &[(&str, &[u8])]) -> PathBuf {
        let d = self.p(name);
        std::fs::create_dir_all(&d).unwrap();
        for (f, b) in files {
            std::fs::write(d.join(f), b).unwrap();
        }
        d
    }

    fn build(&self, clamav: &Path, sequence: u64) -> (Output, PathBuf) {
        let out = self.p(&format!("sigs-{sequence}.holger.znippy"));
        let mut c = Command::new(BIN);
        c.args(["scanner-sigs", "build", "--clamav-dir"])
            .arg(clamav)
            .arg("--policy")
            .arg(&self.policy_path)
            .arg("--release-cert")
            .arg(&self.release.1)
            .arg("--release-key")
            .arg(&self.release.2)
            .args(["--sequence", &sequence.to_string(), "--out"])
            .arg(&out);
        for (i, (_, crt, key)) in self.signers.iter().enumerate() {
            let role = if i == 0 {
                "security-reviewer"
            } else {
                "system-owner"
            };
            c.arg("--approve")
                .arg(format!("{role}={},{}", crt.display(), key.display()));
        }
        (c.env_remove("ESTATE_CLOCK").output().unwrap(), out)
    }

    fn import(&self, bundle: &Path) -> Output {
        run(
            &["bundle", "import"],
            &[
                ("--config", &self.config),
                ("--bundle", bundle),
                ("--policy", &self.policy_path),
            ],
        )
    }

    fn grant(&self, bundle: &Path) {
        let o = run(
            &["bundle", "allow-first-import"],
            &[
                ("--config", &self.config),
                ("--from-bundle", bundle),
                ("--policy", &self.policy_path),
            ],
        );
        assert!(o.status.success(), "the admin's grant: {}", text(&o));
    }

    fn audit(&self) -> String {
        std::fs::read_dir(self.p("store/audit"))
            .unwrap()
            .flatten()
            .filter_map(|e| std::fs::read_to_string(e.path()).ok())
            .collect()
    }

    /// The directory the import said it installed into.
    fn installed_dir(said: &str) -> PathBuf {
        let line = said
            .lines()
            .find(|l| l.contains("scanner-sigs: INSTALLED"))
            .unwrap_or_else(|| panic!("no INSTALLED line: {said}"));
        PathBuf::from(
            line.split(" into ")
                .nth(1)
                .unwrap()
                .split_whitespace()
                .next()
                .unwrap(),
        )
    }
}

#[test]
fn signatures_are_built_signed_imported_and_a_hotfix_lands_over_them() {
    let s = site();
    let full = s.clamav_dir(
        "clamav-full",
        &[
            ("main.cvd", b"ClamAV-VDB:main"),
            ("daily.cld", b"ClamAV-VDB:daily"),
            ("bytecode.cvd", b"ClamAV-VDB:bytecode"),
        ],
    );
    let (b, b1) = s.build(&full, 1);
    assert!(b.status.success(), "{}", text(&b));
    assert!(
        text(&b).contains("BUILT holger-scanner-sigs-1-"),
        "{}",
        text(&b)
    );

    // No floor and no admin's grant: refused by name, nothing installed.
    let i = s.import(&b1);
    assert!(
        !i.status.success() && text(&i).contains("[holger-import-9]"),
        "{}",
        text(&i)
    );

    s.grant(&b1);
    let i = s.import(&b1);
    let said = text(&i);
    assert!(i.status.success(), "{said}");
    assert!(
        said.contains("IMPORTED (new) holger-scanner-sigs-1-"),
        "{said}"
    );
    let db = Site::installed_dir(&said);
    assert!(
        db.ends_with("sandbox/signatures"),
        "the sandboxed clamd's database: {}",
        db.display()
    );
    assert_eq!(
        std::fs::read(db.join("clamav/main.cvd")).unwrap(),
        b"ClamAV-VDB:main"
    );
    assert_eq!(
        std::fs::read(db.join("clamav/daily.cld")).unwrap(),
        b"ClamAV-VDB:daily"
    );
    let audit = s.audit();
    assert!(
        audit.contains("scanner-sigs-import") && audit.contains("seq=1"),
        "audited: {audit}"
    );

    // The hotfix: one urgent signature at the next sequence, over the set.
    let hot = s.clamav_dir(
        "clamav-hotfix",
        &[(
            "hotfix-2026-10-31.ndb",
            b"Holger.Test.Hotfix:0:*:4558414d504c45",
        )],
    );
    let (b, b2) = s.build(&hot, 2);
    assert!(b.status.success(), "{}", text(&b));
    let i = s.import(&b2);
    assert!(
        i.status.success(),
        "the hotfix needs no new grant: {}",
        text(&i)
    );
    assert_eq!(
        std::fs::read(db.join("clamav/hotfix-2026-10-31.ndb")).unwrap(),
        b"Holger.Test.Hotfix:0:*:4558414d504c45"
    );
    assert_eq!(
        std::fs::read(db.join("clamav/main.cvd")).unwrap(),
        b"ClamAV-VDB:main",
        "the rest of the set stays"
    );
    let installed: serde_json::Value =
        serde_json::from_slice(&std::fs::read(db.join("installed.json")).unwrap()).unwrap();
    assert_eq!(installed["sequence"], 2);
    assert_eq!(
        installed["files"].as_array().unwrap().len(),
        4,
        "{installed}"
    );

    // A replay of the older set is a rollback: refused, the hotfix stays.
    let i = s.import(&b1);
    assert!(
        !i.status.success(),
        "an older sequence is refused: {}",
        text(&i)
    );
    assert!(db.join("clamav/hotfix-2026-10-31.ndb").is_file());
}

#[test]
fn only_signature_files_are_built_or_installed() {
    let s = site();
    // The builder refuses a file that is no signature file, by name.
    let bad = s.clamav_dir(
        "clamav-bad",
        &[
            ("main.cvd", b"ClamAV-VDB:main"),
            ("evil.sh", b"#!/bin/sh\n"),
        ],
    );
    let (b, _) = s.build(&bad, 1);
    assert!(
        !b.status.success() && text(&b).contains("evil.sh"),
        "{}",
        text(&b)
    );

    // A bundle signed by the right people that carries one anyway is refused at import.
    use update_bundle::finalize::{add_approval, finalize};
    use update_bundle::prepare::{PayloadInput, Preparation};
    let now = Utc::now();
    let prep = Preparation {
        bundle_id: "holger-scanner-sigs-1-evil".into(),
        sequence: 1,
        created_at: now,
        expires_at: Some(now + Duration::days(30)),
        producer: "test".into(),
        supplier: "holger-scanner-sigs".into(),
        product: PRODUCT.into(),
        release_version: "1".into(),
        supported_hardware_models: vec![],
        supported_hardware_revisions: vec![],
        source_provenance: update_bundle::core::SourceProvenance {
            source_kind: "scanner-signatures".into(),
            source_uri: None,
            source_repository: None,
            source_asset_path: None,
            fetched_at: None,
            reported_size: None,
            vendor_checksum: None,
            vendor_signature_status: None,
            tls_origin: None,
            transfer_receipt_digest: None,
        },
        policy_result: update_bundle::core::PolicyResult {
            policy_digest: sha256_hex(s.policy.to_ron().unwrap().as_bytes()),
            advisory_snapshot_digest: None,
            verdict: "pass".into(),
            non_production: false,
        },
        supersedes: vec![],
        withdraws: vec![],
        payloads: vec![
            PayloadInput {
                relative_path: "clamav/main.cvd".into(),
                bytes: b"ClamAV-VDB:main".to_vec(),
                media_type: "application/octet-stream".into(),
            },
            PayloadInput {
                relative_path: "clamav/evil.sh".into(),
                bytes: b"#!/bin/sh\n".to_vec(),
                media_type: "application/octet-stream".into(),
            },
        ],
        evidence: vec![],
    };
    let mut members = prep.build().unwrap().members;
    for (i, (_, crt, key)) in s.signers.iter().enumerate() {
        let role = if i == 0 {
            "security-reviewer"
        } else {
            "system-owner"
        };
        add_approval(
            &mut members,
            role,
            &std::fs::read(crt).unwrap(),
            &std::fs::read(key).unwrap(),
            None,
            now,
        )
        .unwrap();
    }
    finalize(
        &mut members,
        &s.policy,
        &std::fs::read(&s.release.1).unwrap(),
        &std::fs::read(&s.release.2).unwrap(),
        now,
    )
    .unwrap();
    let evil = s.p("evil.holger.znippy");
    update_bundle::pack_bundle(&members, &evil).unwrap();
    s.grant(&evil);
    let i = s.import(&evil);
    let said = text(&i);
    assert!(
        !i.status.success() && said.contains("[holger-sigs-1]") && said.contains("evil.sh"),
        "{said}"
    );
    let mut stack = vec![s.dir.path().to_path_buf()];
    while let Some(d) = stack.pop() {
        for e in std::fs::read_dir(&d).unwrap().flatten() {
            let p = e.path();
            if p.is_dir() {
                assert!(
                    !p.ends_with("signatures"),
                    "nothing installed: {}",
                    p.display()
                );
                stack.push(p);
            }
        }
    }
}