guests/verify-bundle/tools/release.rs
// SPDX-License-Identifier: AGPL-3.0-only
//! The release tools' logic: [`dist`] builds the three shipped components
//! (`heimdall-verify-bundle{,-verifier,-decoder}.wasm`) with their
//! `.wasm.blake3`; [`sign`] writes each `.wasm.p7s` the way Holger's own wasm
//! plugins are signed (the forge's plugin-signing certificate under the korp
//! root, `dwarves sign-cert`; the one CMS writer
//! `guest_host::verify::sign::sign_module_file`), verifies it against the
//! root and loads all three in the real Heimdall host with that root as its
//! only trust root. Heimdall is a defensive sandbox.
//!
//! Used by examples/dist.rs, examples/sign.rs and tests/release_dist.rs.
#![allow(dead_code)]
use std::path::{Path, PathBuf};
#[path = "guest_build.rs"]
pub mod guest_build;
use guest_build::RELEASE;
/// The plugin-signing material's file names, as `dwarves sign-cert issue
/// --purpose plugin` writes them beside the root seed (t14s, oden).
pub const PLUGIN_KEY_FILE: &str = "plugin-signing.pk8";
pub const PLUGIN_CERT_FILE: &str = "plugin-signing.der";
pub const ROOT_CA_FILE: &str = "korp-root-ca.der";
/// One shipped component.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Artifact {
pub name: &'static str,
pub path: PathBuf,
pub size: usize,
pub blake3: String,
pub sha256: String,
}
/// One signed component.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signed {
pub name: &'static str,
pub signature: PathBuf,
pub blake3: String,
pub sha256: String,
/// The verified signer's common name.
pub signer: String,
}
fn sha256_hex(bytes: &[u8]) -> String {
use sha2::Digest as _;
sha2::Sha256::digest(bytes)
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
fn sidecar(module: &Path, ext: &str) -> PathBuf {
let mut n = module.file_name().unwrap_or_default().to_os_string();
n.push(".");
n.push(ext);
module.with_file_name(n)
}
/// Build the three release components into `out` (created), each with its
/// `.wasm.blake3` (64 lowercase hex and a newline). `target_root` holds the
/// cargo target directories.
pub fn dist(out: &Path, target_root: &Path) -> Result<Vec<Artifact>, String> {
std::fs::create_dir_all(out).map_err(|e| format!("{}: {e}", out.display()))?;
let mut done = Vec::new();
for b in RELEASE {
let name = b.release_name().expect("a release build has a name");
let component = guest_build::componentize(&guest_build::build_core(b, target_root));
let path = out.join(name);
std::fs::write(&path, &component).map_err(|e| format!("{}: {e}", path.display()))?;
let blake3 = blake3::hash(&component).to_hex().to_string();
let pin = sidecar(&path, "blake3");
std::fs::write(&pin, format!("{blake3}\n"))
.map_err(|e| format!("{}: {e}", pin.display()))?;
done.push(Artifact {
name,
path,
size: component.len(),
blake3,
sha256: sha256_hex(&component),
});
}
Ok(done)
}
/// Sign the three components in `dist_dir` with the plugin-signing material
/// in `material`, verify every signature against `material`/korp-root-ca.der
/// and load all three in the real host with that root as its only trust
/// root (the job guest, the decoder, the verifier in front of the decoder;
/// each pinned to its `.wasm.blake3`). Any failure removes every `.wasm.p7s`
/// this call wrote: a release is signed whole or not at all.
pub fn sign(material: &Path, dist_dir: &Path) -> Result<Vec<Signed>, String> {
let mut written = Vec::new();
let r = sign_inner(material, dist_dir, &mut written);
if r.is_err() {
for p in &written {
let _ = std::fs::remove_file(p);
}
}
r
}
fn sign_inner(
material: &Path,
dist_dir: &Path,
written: &mut Vec<PathBuf>,
) -> Result<Vec<Signed>, String> {
let key_path = material.join(PLUGIN_KEY_FILE);
let cert_path = material.join(PLUGIN_CERT_FILE);
let root = material.join(ROOT_CA_FILE);
let read = |p: &Path| std::fs::read(p).map_err(|e| format!("{}: {e}", p.display()));
// The key's custody, checked the way the forge checks it: owner only.
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt as _;
let mode = std::fs::metadata(&key_path)
.map_err(|e| format!("{}: {e}", key_path.display()))?
.mode();
if mode & 0o077 != 0 {
return Err(format!(
"{}: mode {:o} lets others read the signing key; it must be 0600",
key_path.display(),
mode & 0o777
));
}
}
let signer = znippy_common::sign::signer_from_pkcs8(
znippy_common::sign::SigAlg::Ed25519,
&read(&key_path)?,
&read(&cert_path)?,
)
.map_err(|e| format!("{}: {e}", key_path.display()))?;
let trust = guest_host::ModuleTrust::required([root.clone()]);
let mut out = Vec::new();
for b in RELEASE {
let name = b.release_name().expect("a release build has a name");
let module = dist_dir.join(name);
let bytes = read(&module)?;
let blake3 = blake3::hash(&bytes).to_hex().to_string();
let pin_path = sidecar(&module, "blake3");
let pin = read(&pin_path)?;
if String::from_utf8_lossy(&pin).trim() != blake3 {
return Err(format!(
"{}: the module's blake3 is {blake3}, not the pinned {}; refusing to sign",
module.display(),
String::from_utf8_lossy(&pin).trim()
));
}
let signature = sidecar(&module, "p7s");
if signature.exists() {
return Err(format!(
"{} exists already; a release is signed once",
signature.display()
));
}
let wrote = guest_host::verify::sign::sign_module_file(&module, signer.as_ref())?;
written.push(wrote.clone());
let cn = trust
.verify(&module, &bytes)
.map_err(|e| format!("the signature just written does not verify: {e}"))?
.unwrap_or_default();
out.push(Signed {
name,
signature: wrote,
blake3,
sha256: sha256_hex(&bytes),
signer: cn,
});
}
// Load all three as Holger will: the root is the host's only trust root.
let host = heimdall_wasm::Host::new(heimdall_wasm::HostConfig::new([root]))
.map_err(|e| format!("the host did not start: {e}"))?;
let path = |i: usize| dist_dir.join(out[i].name);
host.load_pinned(&path(0), &out[0].blake3)
.map_err(|e| format!("the host refused {}: {e}", out[0].name))?;
let decoder = host
.load_decoder_pinned(&path(2), &out[2].blake3)
.map_err(|e| format!("the host refused {}: {e}", out[2].name))?;
host.load_split_pinned(&path(1), &out[1].blake3, decoder)
.map_err(|e| format!("the host refused {}: {e}", out[1].name))?;
Ok(out)
}