Gunnar

nordisk/heimdall

crates/heimdall-run/src/wasm.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! The bridge to heimdall-wasm: load a signed guest and run it as a
//! [`GuestRunner`]. Heimdall is a defensive sandbox: the guest is verified
//! (signature chain to a trust root, blake3 pin) before it is compiled, and
//! runs with no WASI under the policy's fuel, wall-clock, memory, table,
//! instance and stack limits.

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

use heimdall_core::Params;
use heimdall_wasm::{Guest, Host, HostConfig, HostError, LoadRefusal};

use crate::job::{GuestRun, GuestRunner, GuestVerdict};
use crate::policy_file::RunLimits;

/// Why a guest was not loaded. Nothing of the guest ran.
#[derive(Debug, thiserror::Error)]
pub enum WasmLoadError {
    #[error("no --trust-root given: every guest must be signed, there is no unsigned mode")]
    NoTrustRoots,
    #[error("wasm host: {0}")]
    Host(#[from] HostError),
    #[error("{0}")]
    Load(#[from] LoadRefusal),
}

/// A verified, compiled guest and the host it was loaded into.
pub struct WasmGuest {
    _host: Host,
    guest: Guest,
}

impl WasmGuest {
    /// The BLAKE3-256 of the verified module bytes.
    pub fn digest_hex(&self) -> &str {
        self.guest.digest_hex()
    }

    /// The certificate common name that signed the module.
    pub fn signer(&self) -> &str {
        self.guest.signer()
    }
}

/// Load and verify the guest at `module` against `trust_roots`; with
/// `pin_blake3`, the module's BLAKE3-256 must also equal that digest.
pub fn load_guest(
    module: &Path,
    trust_roots: &[PathBuf],
    pin_blake3: Option<&str>,
    limits: &RunLimits,
) -> Result<WasmGuest, WasmLoadError> {
    if trust_roots.is_empty() {
        return Err(WasmLoadError::NoTrustRoots);
    }
    let cfg = HostConfig::for_policy(trust_roots.iter().cloned(), &limits.to_core());
    let host = Host::new(cfg)?;
    let guest = match pin_blake3 {
        Some(pin) => host.load_pinned(module, pin)?,
        None => host.load(module)?,
    };
    Ok(WasmGuest { _host: host, guest })
}

impl GuestRunner for WasmGuest {
    fn run_guest(&self, input: &[u8], params: &Params, limits: &RunLimits) -> GuestRun {
        let wl = heimdall_wasm::RunLimits::from(&limits.to_core());
        let out = self.guest.run(input, params, &wl);
        let verdict = match out.verdict {
            heimdall_wasm::Verdict::Accepted { note } => GuestVerdict::Accepted { note },
            heimdall_wasm::Verdict::Refused { code, reason } => GuestVerdict::Refused {
                code: code.to_string(),
                reason,
            },
        };
        GuestRun {
            verdict,
            files: out.files.into_iter().map(|f| (f.name, f.bytes)).collect(),
            fuel_used: out.fuel_used,
        }
    }
}