server/cli/src/airgap.rs
//! `holger-server export` — the signed airgap export of the approved set. The
//! landing half of an export's import lives in the box's import door
//! (`server_lib::import_door`), which `holger-server bundle import`, the admin
//! RPC, the HTTP route and the `bundle.inbox` job all run.
//!
//! The export takes AVAILABLE artifacts from the intake chain's production
//! repository, with their stored evidence, signed attestations, SBOMs and intake
//! records, and writes a bundle CANDIDATE whose `policy_result.verdict` is
//! computed from that evidence (`server_lib::promote::airgap`). The candidate is
//! then approved and release-signed with `holger-bundle approve` / `finalize`,
//! exactly like every other bundle. Nothing about the verdict is typed in.
use std::path::PathBuf;
use anyhow::{bail, Context, Result};
use clap::Args;
use update_bundle::prepare::write_members_dir;
#[derive(Args)]
pub struct ExportArgs {
/// The online holger's RON config (the one its server starts with).
#[arg(short, long)]
pub config: PathBuf,
/// The bundle candidate directory to write (must not exist).
#[arg(long)]
pub candidate: PathBuf,
#[arg(long)]
pub bundle_id: String,
/// Monotonic per (supplier, product); the secure side's anti-rollback floor.
#[arg(long)]
pub sequence: u64,
#[arg(long)]
pub supplier: String,
#[arg(long)]
pub product: String,
#[arg(long)]
pub release_version: String,
#[arg(long, default_value_t = 30)]
pub expires_days: i64,
/// Export only these artifacts (file name, name or `name@version`; repeatable).
/// Absent: every AVAILABLE artifact in production.
#[arg(long = "artifact")]
pub artifacts: Vec<String>,
}
fn os_user() -> String {
std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.unwrap_or_else(|_| "local".into())
}
/// `holger-server export`: the export door's act ([`server_lib::export_door`], the same one
/// `holger.doors.v1.BundleService.Export` answers over the wire), written to a directory.
pub fn run_export(a: ExportArgs) -> Result<()> {
if a.candidate.exists() {
bail!(
"{} already exists; the export writes a fresh candidate directory",
a.candidate.display()
);
}
let mut holger = server_lib::read_ron_config(&a.config)
.with_context(|| format!("load config {}", a.config.display()))?;
holger.instantiate_backends()?;
let door = server_lib::export_door::ExportDoor::of(&holger)?
.ok_or_else(|| anyhow::anyhow!("config {} declares no intake chain (`promotion.intake`); there is no approved set to export", a.config.display()))?;
let ask = server_lib::export_door::ExportAsk {
bundle_id: a.bundle_id.clone(),
sequence: a.sequence,
supplier: a.supplier.clone(),
product: a.product.clone(),
release_version: a.release_version.clone(),
expires_days: a.expires_days,
artifacts: a.artifacts.clone(),
};
let producer = format!("holger-server export {}", env!("CARGO_PKG_VERSION"));
let exported = door
.export(&ask, &os_user(), "local", &producer)
.map_err(|e| {
if let server_lib::export_door::ExportError::Refused { lines, artifacts } = &e {
for line in lines {
eprintln!("{line}");
}
return anyhow::anyhow!(
"export refused: {} artifact(s) without passing evidence: {}",
artifacts.len(),
artifacts.join(", ")
);
}
anyhow::anyhow!("{}", e.tagged())
})?;
std::fs::create_dir_all(&a.candidate)?;
write_members_dir(&exported.members, &a.candidate)
.map_err(|e| anyhow::anyhow!("write the candidate: {e}"))?;
println!(
"EXPORTED {} seq {} -> {}",
a.bundle_id,
a.sequence,
a.candidate.display()
);
println!(
" verdict: {} (computed from the stored evidence)",
exported.verdict
);
for art in &exported.artifacts {
println!(" {}", art.line());
}
println!(" candidate sha256: {}", exported.candidate_digest);
Ok(())
}
/// **`holger export-push`** (lane HOL-CDS): one push of the `export:` block — each new
/// AVAILABLE artifact, as one signed bundle, written atomically into `export.push_to` (the
/// diode's send side). The started server runs the same push every `export.every`.
/// `--trust-policy-out <file>` writes the pinned trust policy the secure side verifies the
/// pushes with (carry it across once) and pushes nothing.
#[cfg(feature = "export-push")]
#[derive(Args)]
pub struct ExportPushArgs {
#[arg(short, long)]
pub config: PathBuf,
/// Write the pushes' pinned trust policy (RON) here instead of pushing.
#[arg(long)]
pub trust_policy_out: Option<PathBuf>,
}
#[cfg(feature = "export-push")]
pub fn run_export_push(a: ExportPushArgs) -> Result<()> {
let mut holger = server_lib::read_ron_config(&a.config)
.with_context(|| format!("load config {}", a.config.display()))?;
for line in holger.banner() {
println!("{line}");
}
holger.instantiate_backends()?;
let push = server_lib::export_push::ExportPush::of(&holger)?.ok_or_else(|| {
anyhow::anyhow!(
"{}",
server_lib::export_push::not_declared(&a.config.display().to_string())
)
})?;
if let Some(out) = &a.trust_policy_out {
let ron = push
.trust_policy()
.to_ron()
.map_err(|e| anyhow::anyhow!("the trust policy: {e}"))?;
std::fs::write(out, ron).with_context(|| format!("write {}", out.display()))?;
println!(
"TRUST POLICY {} (vendor {} product {}: the pinned signers of this box's pushes)",
out.display(),
push.settings().vendor,
push.settings().product
);
return Ok(());
}
let o = push.push_once()?;
println!("{}", o.line());
Ok(())
}