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/sigs_cli.rs

//! `holger scanner-sigs …` — scanner signatures across the airgap (lane HOL-NCSC).
//!
//! ```text
//! ONLINE   scanner-sigs build --clamav-dir DIR [--yara-dir DIR] --out sigs.holger.znippy --policy trust.ron
//!                             --release-cert … --release-key … [--approve ROLE=CERT,KEY]… [--sequence N]
//!   ── USB ──
//! SECURE   bundle allow-first-import --config holger.ron --from-bundle sigs.holger.znippy   (the first one only)
//!          bundle import --config holger.ron --bundle sigs.holger.znippy
//!          scanner-sigs status --config holger.ron
//! ```
//!
//! The bundle is a `holger.update-bundle/v1` with product `holger.scanner-sigs/v1`
//! (`holger_intel::sigs`): every signature file a payload, hashed by the release manifest and
//! signed. `bundle import` (and every import door: HTTP, gRPC, the `bundle.inbox`) verifies it,
//! holds it to the anti-rollback floor, audits it first and installs it where the sandboxed
//! clamd reads its database. A hotfix is the same bundle with the next sequence and one file.

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

use anyhow::{Context, Result};
use chrono::{Duration, Utc};
use clap::{Args, Subcommand};

use holger_intel::sigs::{build_sigs_bundle, read_dirs, SigsBuild, SigsDb, DEFAULT_SIGS_SUPPLIER};
use holger_intel::SignerFiles;
use update_bundle::TrustPolicy;

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

#[derive(Subcommand)]
pub enum SigsOp {
    /// ONLINE side: sign the signature files of `--clamav-dir` (and `--yara-dir`) into a
    /// `holger.scanner-sigs/v1` bundle for the USB stick. Every file must be a signature
    /// file (a ClamAV database extension, or .yar/.yara); anything else refuses the build.
    Build(BuildArgs),
    /// What the box's signature database holds (the image's own until the first bundle).
    Status(StatusArgs),
}

#[derive(Args)]
pub struct BuildArgs {
    /// ClamAV database files (main/daily/bytecode .cvd/.cld, a hotfix .ndb/.hdb/.ldb …).
    #[arg(long)]
    pub clamav_dir: Option<PathBuf>,
    /// YARA rule files (.yar, .yara).
    #[arg(long)]
    pub yara_dir: Option<PathBuf>,
    #[arg(long)]
    pub out: PathBuf,
    /// The trust policy (RON) the secure side verifies with.
    #[arg(long)]
    pub policy: PathBuf,
    #[arg(long)]
    pub release_cert: PathBuf,
    #[arg(long)]
    pub release_key: PathBuf,
    /// An approval, `ROLE=CERT,KEY` (DER files). Repeat for the two-person rule.
    #[arg(long = "approve", value_name = "ROLE=CERT,KEY")]
    pub approve: Vec<String>,
    /// The monotonic sequence (a hotfix takes the next). Default: the build time as
    /// YYYYMMDDHHMMSS.
    #[arg(long)]
    pub sequence: Option<u64>,
    #[arg(long, default_value = DEFAULT_SIGS_SUPPLIER)]
    pub supplier: String,
    #[arg(long, default_value_t = 30)]
    pub valid_days: i64,
}

#[derive(Args)]
pub struct StatusArgs {
    #[arg(long)]
    pub config: PathBuf,
}

pub fn run(a: SigsArgs) -> Result<()> {
    match a.op {
        SigsOp::Build(b) => run_build(b),
        SigsOp::Status(s) => run_status(s),
    }
}

fn parse_approval(s: &str) -> Result<(String, SignerFiles)> {
    let (role, files) = s
        .split_once('=')
        .ok_or_else(|| anyhow::anyhow!("--approve {s:?}: expected ROLE=CERT,KEY"))?;
    let (cert, key) = files
        .split_once(',')
        .ok_or_else(|| anyhow::anyhow!("--approve {s:?}: expected ROLE=CERT,KEY"))?;
    Ok((
        role.to_string(),
        SignerFiles::read(Path::new(cert), Path::new(key))?,
    ))
}

fn run_build(a: BuildArgs) -> Result<()> {
    let policy = TrustPolicy::from_ron(
        &std::fs::read_to_string(&a.policy)
            .with_context(|| format!("read {}", a.policy.display()))?,
    )?;
    let files = read_dirs(a.clamav_dir.as_deref(), a.yara_dir.as_deref())?;
    let created_at = Utc::now();
    let approvals = a
        .approve
        .iter()
        .map(|s| parse_approval(s))
        .collect::<Result<Vec<_>>>()?;
    let built = build_sigs_bundle(
        &files,
        SigsBuild {
            sequence: a
                .sequence
                .unwrap_or_else(|| holger_intel::bundle::sequence_for(created_at)),
            created_at,
            expires_at: Some(created_at + Duration::days(a.valid_days)),
            supplier: a.supplier.clone(),
            producer: format!("holger scanner-sigs build {}", env!("CARGO_PKG_VERSION")),
            policy,
            approvals,
            release: SignerFiles::read(&a.release_cert, &a.release_key)?,
            out: a.out.clone(),
        },
    )?;
    println!(
        "BUILT {} seq {}: {} signature file(s) -> {}",
        built.bundle_id,
        built.sequence,
        built.files.len(),
        built.path.display()
    );
    for f in &built.files {
        println!("  {f}");
    }
    Ok(())
}

fn run_status(a: StatusArgs) -> Result<()> {
    let holger = server_lib::read_ron_config(&a.config)?;
    let db = SigsDb::open(holger_sandbox_layout(&holger.data_root()));
    match db.installed()? {
        None => println!("scanner signatures: none installed under {}; the sandboxed clamd reads the image's own database", db.root().display()),
        Some(i) => {
            println!("scanner signatures: {} seq {} from {} installed {} ({} file(s)) under {}", i.bundle_id, i.sequence, i.supplier, i.installed_at.format("%Y-%m-%d %H:%M"), i.files.len(), db.root().display());
            for f in &i.files {
                println!("  {} sha256 {} ({} bytes, seq {})", f.path, f.sha256, f.size, f.sequence);
            }
        }
    }
    Ok(())
}

/// `<data_root>/sandbox/signatures` (`holger_sandbox::media::Layout::signatures`).
fn holger_sandbox_layout(data_root: &Path) -> PathBuf {
    data_root.join("sandbox").join("signatures")
}