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);
}
}
}
}