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/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(())
}