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
}
}