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

nordisk/holger

update-bundle/src/cli/verify.rs

//! `holger-cli verify` and `holger-cli bundle extract` — the zero-egress offline verifier
//! and extractor for holger update bundles (this was the `holger-verify` program). A verification failure is an
//! error. There is no `--skip-verification`: `extract` always verifies before writing.

use std::path::PathBuf;

use chrono::Utc;
use clap::Args;

use crate::container::load_bundle;
use crate::core::BundleCore;
use crate::rollback::RollbackStore;
use crate::schema::CORE_JSON;
use crate::verify::verify_member_map;
use crate::{extract_bundle_file, TargetSpec, TrustPolicy};

#[derive(Args, Debug)]
pub struct VerifyArgs {
    /// The `.holger.znippy` bundle file.
    bundle: PathBuf,
    #[arg(long)]
    policy: PathBuf,
    #[arg(long)]
    target_product: Option<String>,
    #[arg(long)]
    target_model: Option<String>,
    #[arg(long)]
    target_revision: Option<String>,
    /// Anti-rollback state directory (checks the sequence against accepted state).
    #[arg(long)]
    state_dir: Option<PathBuf>,
    #[arg(long)]
    json: bool,
}

#[derive(Args, Debug)]
pub struct ExtractArgs {
    bundle: PathBuf,
    #[arg(long)]
    policy: PathBuf,
    #[arg(long)]
    dest: PathBuf,
    #[arg(long)]
    target_product: Option<String>,
    #[arg(long)]
    target_model: Option<String>,
    #[arg(long)]
    target_revision: Option<String>,
    #[arg(long)]
    json: bool,
}

fn load_policy(path: &PathBuf) -> anyhow::Result<TrustPolicy> {
    let text = std::fs::read_to_string(path)?;
    Ok(TrustPolicy::from_ron(&text)?)
}

/// `holger-cli verify`: verify a bundle against a trust policy, offline. A failure is an
/// error (a non-zero exit).
pub fn verify(a: VerifyArgs) -> anyhow::Result<()> {
    let policy = load_policy(&a.policy)?;
    let target = TargetSpec {
        product: a.target_product,
        model: a.target_model,
        revision: a.target_revision,
    };
    let members = load_bundle(&a.bundle)?;
    let state = match &a.state_dir {
        Some(dir) => {
            let core_bytes = members
                .get(CORE_JSON)
                .ok_or_else(|| anyhow::anyhow!("bundle is missing core.json"))?;
            let core = BundleCore::from_json_bytes(core_bytes)?;
            Some(RollbackStore::open(dir).load(&core.supplier, &core.product)?)
        }
        None => None,
    };
    let report = verify_member_map(&members, &policy, &target, state.as_ref(), Utc::now())?;
    if a.json {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        print_human(&report);
    }
    Ok(())
}

/// `holger-cli bundle extract`: verify, then extract into an empty directory.
pub fn extract(a: ExtractArgs) -> anyhow::Result<()> {
    let policy = load_policy(&a.policy)?;
    let target = TargetSpec {
        product: a.target_product,
        model: a.target_model,
        revision: a.target_revision,
    };
    let report = extract_bundle_file(&a.bundle, &policy, &target, &a.dest)?;
    if a.json {
        println!("{}", serde_json::to_string_pretty(&report)?);
    } else {
        print_human(&report);
        println!("extracted to {}", a.dest.display());
    }
    Ok(())
}

fn print_human(r: &crate::VerifyReport) {
    println!(
        "VERIFIED  {} {} (seq {})",
        r.product, r.release_version, r.sequence
    );
    println!("  bundle-id:        {}", r.bundle_id);
    println!("  supplier:         {}", r.supplier);
    println!("  candidate-digest: {}", r.candidate_digest);
    println!(
        "  release-signer:   {} [{}]",
        r.release_signer_subject, r.release_signer_fingerprint
    );
    for a in &r.approvals {
        println!(
            "  approval:         {} by {} [{}]",
            a.role, a.subject, a.fingerprint
        );
    }
    if r.non_production_evidence {
        println!("  NOTE: advisory evidence is marked NON-PRODUCTION");
    }
    for c in &r.checks {
        println!("  ok: {c}");
    }
}