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