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

nordisk/holger

xtask/src/sigstore_trust.rs

//! `cargo xtask sigstore-trust stage [--from <trusted_root.json>] [--out <dir>] [--signer-dir <dir>] [--valid-days N]`
//! (lane HOL-OCI-EVIDENCE): **the release's Sigstore trust root**, staged beside the sandbox
//! entries so the forge packs it into the app archive (`holger-recipe`
//! `sandbox_payload_entries`: `sandbox/sigstore-trust.holger.znippy`, `sandbox/bundle-trust.ron`).
//!
//! 1. Sigstore's public-good `trusted_root.json` is fetched from its TUF target by the pinned
//!    digest (`holger_recipe::packaging::SIGSTORE_TRUSTED_ROOT_SHA256`; `--from` a file, or the
//!    cache `~/.cache/holger/<pin>.trusted_root.json`) and held to it; anything else is refused.
//! 2. `holger sigstore-trust make` signs it with the release trust identities
//!    (`--signer-dir`, `$HOLGER_TRUST_SIGNER_DIR`, default `~/.holger/release-trust-signer`; the
//!    release runner keeps them: every medium and every refresh is signed by the same three, so
//!    the box's seeded policy verifies the next refresh), sequence = the build's clock.
//!
//! The forge door stages it when a row's payload names it and it is absent or not the pin
//! (`forge_door::ensure_payload_entries_staged`).

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

use anyhow::{bail, Context, Result};
use sha2::Digest as _;

use holger_recipe::packaging::{
    SIGSTORE_TRUSTED_ROOT_SHA256 as PIN, SIGSTORE_TRUST_BUNDLE, SIGSTORE_TRUST_POLICY,
};

/// The pinned root's URL (Sigstore's TUF repository, consistent snapshots).
pub fn pinned_url() -> String {
    format!("https://tuf-repo-cdn.sigstore.dev/targets/{PIN}.trusted_root.json")
}

/// The release trust identities' directory.
pub fn signer_dir() -> PathBuf {
    match std::env::var_os("HOLGER_TRUST_SIGNER_DIR").filter(|v| !v.is_empty()) {
        Some(d) => PathBuf::from(d),
        None => PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
            .join(".holger/release-trust-signer"),
    }
}

fn sha256_hex(b: &[u8]) -> String {
    hex_of(&sha2::Sha256::digest(b))
}

fn hex_of(b: &[u8]) -> String {
    b.iter().map(|x| format!("{x:02x}")).collect()
}

/// Is `json` the pinned root? A refusal names the pin.
pub fn check_pinned(json: &[u8]) -> Result<()> {
    let got = sha256_hex(json);
    if got != PIN {
        bail!("[holger-trust-1] the trusted_root.json hashes to sha256:{got}, and the pinned root is sha256:{PIN}; moving the pin is a commit (holger-recipe SIGSTORE_TRUSTED_ROOT_SHA256)");
    }
    Ok(())
}

/// The pinned root: `from`, else the cache, else fetched by its digest (and cached).
pub fn fetch_pinned(from: Option<&Path>) -> Result<Vec<u8>> {
    if let Some(f) = from {
        let b = std::fs::read(f).with_context(|| f.display().to_string())?;
        check_pinned(&b)?;
        return Ok(b);
    }
    let cache = PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
        .join(".cache/holger")
        .join(format!("{PIN}.trusted_root.json"));
    if let Ok(b) = std::fs::read(&cache) {
        if check_pinned(&b).is_ok() {
            return Ok(b);
        }
    }
    let url = pinned_url();
    println!(
        "fetching Sigstore's trusted_root.json (pinned sha256 {}…) from {url}",
        &PIN[..16]
    );
    let mut resp = ureq::get(&url).call().with_context(|| url.clone())?;
    let b = resp
        .body_mut()
        .with_config()
        .limit(8 << 20)
        .read_to_vec()
        .with_context(|| url.clone())?;
    check_pinned(&b)?;
    if let Some(p) = cache.parent() {
        std::fs::create_dir_all(p).ok();
    }
    std::fs::write(&cache, &b).ok();
    Ok(b)
}

/// **`stage`**: the pinned root, signed into `out` (the bundle and its policy).
pub fn stage(
    root: &Path,
    out: &Path,
    from: Option<&Path>,
    signer: &Path,
    valid_days: i64,
) -> Result<()> {
    let json = fetch_pinned(from)?;
    std::fs::create_dir_all(out).with_context(|| out.display().to_string())?;
    let rootfile = out.join(".trusted_root.json");
    std::fs::write(&rootfile, &json)?;
    let st = Command::new("cargo")
        .current_dir(root)
        .args([
            "run",
            "-q",
            "--locked",
            "-p",
            "holger",
            "--bin",
            "holger",
            "--",
            "sigstore-trust",
            "make",
            "--root",
        ])
        .arg(&rootfile)
        .arg("--signer-dir")
        .arg(signer)
        .arg("--out")
        .arg(out.join(SIGSTORE_TRUST_BUNDLE))
        .arg("--policy-out")
        .arg(out.join(SIGSTORE_TRUST_POLICY))
        .args(["--valid-days", &valid_days.to_string()])
        .status()
        .context("cargo run -p holger -- sigstore-trust make")?;
    let _ = std::fs::remove_file(&rootfile);
    if !st.success() {
        bail!("holger sigstore-trust make failed ({st})");
    }
    staged_ok(out)?;
    println!("staged {}/{SIGSTORE_TRUST_BUNDLE} and {SIGSTORE_TRUST_POLICY} (the pinned root sha256 {}…, signer {})", out.display(), &PIN[..16], signer.display());
    Ok(())
}

/// Is `out` a complete trust staging: the bundle verifies against the policy beside it and
/// carries the pinned root?
pub fn staged_ok(out: &Path) -> Result<()> {
    let (b, p) = (
        out.join(SIGSTORE_TRUST_BUNDLE),
        out.join(SIGSTORE_TRUST_POLICY),
    );
    let text = std::fs::read_to_string(&p)
        .with_context(|| format!("{}: run `cargo xtask sigstore-trust stage`", p.display()))?;
    let policy = update_bundle::TrustPolicy::from_ron(&text)
        .map_err(|e| anyhow::anyhow!("{}: {e}", p.display()))?;
    let members = update_bundle::container::load_bundle_verified(&b, &policy).map_err(|e| {
        anyhow::anyhow!(
            "{}: {e} (run `cargo xtask sigstore-trust stage`)",
            b.display()
        )
    })?;
    let root = members
        .get("payload/trusted_root.json")
        .with_context(|| format!("{} carries no trusted_root.json", b.display()))?;
    check_pinned(root)
}

/// The verb.
pub fn run(root: &Path, args: &[String]) -> Result<()> {
    let mut it = args.iter();
    match it.next().map(String::as_str) {
        Some("stage") => {}
        _ => bail!("sigstore-trust: takes `stage [--from <trusted_root.json>] [--out <dir>] [--signer-dir <dir>] [--valid-days N]`"),
    }
    let (mut from, mut out, mut signer, mut days) = (
        None,
        root.join(crate::scan_worker::STAGE_OUT),
        signer_dir(),
        730i64,
    );
    while let Some(a) = it.next() {
        let mut val = || {
            it.next()
                .cloned()
                .with_context(|| format!("{a} takes a value"))
        };
        match a.as_str() {
            "--from" => from = Some(PathBuf::from(val()?)),
            "--out" => out = PathBuf::from(val()?),
            "--signer-dir" => signer = PathBuf::from(val()?),
            "--valid-days" => days = val()?.parse().context("--valid-days")?,
            other => bail!("sigstore-trust stage: unknown word '{other}'"),
        }
    }
    stage(root, &out, from.as_deref(), &signer, days)
}

#[cfg(test)]
mod tests {
    use super::*;

    const FIXTURE: &str = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/../provenance/tests/fixtures/sigstore/trusted_root.json"
    );

    #[test]
    fn the_pinned_root_is_taken_and_any_other_is_refused_by_name() {
        assert!(
            fetch_pinned(Some(Path::new(FIXTURE))).is_ok(),
            "the fixture is the pinned TUF target"
        );
        let d = tempfile_dir();
        let other = d.join("other.json");
        std::fs::write(&other, b"{}").unwrap();
        let e = fetch_pinned(Some(&other)).unwrap_err().to_string();
        assert!(e.contains("[holger-trust-1]") && e.contains(PIN), "{e}");
        assert_eq!(
            pinned_url(),
            format!("https://tuf-repo-cdn.sigstore.dev/targets/{PIN}.trusted_root.json")
        );
        let e = staged_ok(&d).unwrap_err().to_string();
        assert!(
            e.contains("cargo xtask sigstore-trust stage"),
            "an empty staging says what to run: {e}"
        );
    }

    fn tempfile_dir() -> PathBuf {
        let d = std::env::temp_dir().join(format!("xtask-sigstore-{}", std::process::id()));
        std::fs::create_dir_all(&d).unwrap();
        d
    }
}