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

nordisk/heimdall

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