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