Skip to content

tegmentum/uefi-wasmtime

Repository files navigation

uefi-wasmtime

Run the wasmtime WebAssembly runtime — including the component model — as a UEFI application, so WebAssembly components can execute at the firmware level and reach UEFI services through typed WIT interfaces.

This is a sibling to uefi-wamr, which embeds the WAMR interpreter. WAMR has no component-model runtime; wasmtime does. The motivating workload is running the Citadel TPM (libtpms compiled to a WASM component) under UEFI, with the host providing the tegmentum:tpm storage/io imports and a modular tegmentum:uefi interface for firmware services.

Approach

  • Written in Rust against the first-class x86_64-unknown-uefi target, using the uefi crate for firmware services — this produces a valid PE32+ EFI application directly (no EDK2/objcopy dance).
  • wasmtime is built no_std with the Pulley portable interpreter (no JIT — UEFI has no W^X mmap), with a minimal platform shim mapping wasmtime's platform needs onto UEFI Boot Services.
  • The component model is enabled so true components (e.g. libtpms) run directly.

Status / milestones

  1. Boot — minimal Rust UEFI app boots in QEMU/OVMF, heap allocator works.

  2. Core wasm — wasmtime (Pulley, no_std) runs a core module under UEFI: add(20,22)=42 from an AOT-compiled Pulley artifact loaded via Module::deserialize.

  3. Component model — a WASM component (examples/uefi-demo-component, imports tegmentum:uefi/console-out + time, exports run) runs under UEFI and calls the host imports, which are implemented over UEFI services.

  4. TPM — the real libtpms TPM 2.0 component (3.7 MB, OpenSSL 3.3 + libtpms 0.10) runs under UEFI: choose-version(2.0), init, TPM2_Startup (rc=0), TPM2_GetRandom (rc=0). Driven through a hand-written no_std WASI preview2 host shim (src/tpm.rs, 19 interfaces) — real random/clocks/stdio→console, empty environment/preopens, traps for the filesystem/terminal/poll surfaces the ephemeral TPM never uses. Resource stubs use Resource::new_own (no ResourceTable needed). The component is AOT-compiled to a 13 MB Pulley artifact and embedded.

    Build the artifact: aot-compile <tpm-ephemeral.component.wasm> assets/tpm.cwasm pulley64.

Building a guest component

tools/aot-compile auto-detects components vs core modules. Build a component guest with wit-bindgen (no_std) + wasm-tools component new:

cargo build --release --target wasm32-unknown-unknown \
  --manifest-path examples/uefi-demo-component/Cargo.toml
wasm-tools component new <core>.wasm -o assets/demo.component.wasm
tools/aot-compile/target/release/aot-compile assets/demo.component.wasm assets/demo.cwasm pulley64

Host imports are implemented via wasmtime::component::bindgen! + a HostState that implements the generated Host traits, added with add_to_linker::<_, HasSelf<_>>.

How the no_std build works

  • A derived hard-float target spec (x86_64-unknown-uefi.json, SSE2 on) + nightly -Zbuild-std — the stock UEFI target's soft-float ABI breaks sha2/libm.
  • A tiny platform shim (wasmtime_tls_get/set) — with no virtual memory and no native signals configured, wasmtime needs nothing else (Pulley traps in-interp, memories are malloc'd).
  • Engine config must match the AOT artifact: target("pulley64"), signals_based_traps(false), memory_init_cow(false), and the same wasm feature set (no gc/threads) on both tools/aot-compile and the runtime.

Producing the embedded module

cargo build --release --manifest-path tools/aot-compile/Cargo.toml
tools/aot-compile/target/release/aot-compile <in.wasm> assets/add.cwasm pulley64

Build & run

bash scripts/build.sh            # builds both bins (PROFILE=release by default)
bash scripts/run-qemu.sh         # boot the demo in QEMU/OVMF, serial to stdout
bash scripts/test-qemu.sh        # smoke test: boot each bin, assert serial markers

# boot the attestation app instead of the demo:
EFI=target/x86_64-unknown-uefi/release/citadel-attest.efi bash scripts/run-qemu.sh
# PROFILE=debug bash scripts/build.sh   # fast iteration on non-TPM paths only
# WASM=path/to/module.wasm bash scripts/run-qemu.sh   # stage a module on the ESP

Build --release. A debug build runs the Pulley interpreter unoptimized, which makes the TPM's asymmetric ops impractically slow (CreatePrimary did not finish in 40 min). Release (opt-level = 3) brings the whole attestation to ~3 s; the scripts default to PROFILE=release. See Performance below.

Requirements: a nightly Rust toolchain with rust-src (for -Zbuild-std; see rust-toolchain.toml), QEMU, OVMF firmware, and mtools.

Binaries

  • uefi-wasmtime (src/main.rs) — the demo: boots, runs a core module, a component calling tegmentum:uefi host imports, then the full TPM 2.0 attestation flow.
  • citadel-attest (src/bin/citadel-attest.rs) — a firmware-level measured-boot attestation app: boot → vTPM → measure the running image into PCR[10] → mint a restricted ECC P-256 AK → TPM2_Quote over a verifier nonce → emit evidence (AK public + quoted PCR + attest + signature) to serial (between ---BEGIN/END CITADEL-ATTEST--- markers) and to \citadel-attest.bin on the ESP. Evidence::encode() is a self-describing blob (magic CATT1) a Citadel verifier can appraise against the tegmentum:tpm contract.

TPM stack: the shared driver

The TPM command/attestation protocol is not in this repo — it lives in tpm2-driver, a host-agnostic crate shared with Citadel's Linux vTPM backend. The seam is one trait, TpmTransport { process(cmd)->resp }:

   citadel (Linux, std) ──EngineTransport (wasmtime JIT)──┐
                                                          ├─▶ tpm2-driver ─▶ libtpms guest
   uefi-wasmtime (UEFI, no_std) ─ComponentTransport (Pulley)─┘

src/tpm.rs provides ComponentTransport (each process is a tegmentum:tpm/commands.process call into the wasmtime component), the WASI p2 host shim, setup() (component bring-up + lifecycle), and attest() / Evidence. The same driver code drives the TPM on Linux and at firmware level.

Attestation flow (D1)

PCR_Read → PCR_Extend(PCR[10]) → PCR_Read → CreatePrimary (restricted ECC P-256 signing AK, owner hierarchy) → TPM2_Quote(PCR[10], nonce) → FlushContext. The first asymmetric op makes libtpms defer for an algorithm self-test (TPM_RC_RETRY 0x922 / TPM_RC_TESTING 0x90A); send_command resubmits transparently. Verified live in QEMU: 129-byte TPMS_ATTEST + ECDSA signature, rc=0, state persisted on Shutdown(STATE).

Performance (D2)

Under the Pulley interpreter (no JIT in UEFI), latency is dominated by the host build profile and the asymmetric ops, not the PCR/measured-boot path:

op debug release (opt-level 3) native (ref)
PCR extend/read fast fast sub-ms
CreatePrimary (ECC keygen + first-asym self-test) >40 min (DNF) ~2 s ~6 ms
TPM2_Quote ~1 s ~1.8 ms

So Pulley overhead is a sane ~300–1000× once optimized. Further levers if needed: cache the AK in persistent NV (pay keygen once), a curated libtpms algorithm profile to shrink the first-asym self-test, a trimmed OpenSSL.

Testing (D3)

  • tpm2-driver has host-side unit tests (builder golden bytes, send_command retry via a mock transport, response parsers): cargo test in ../tpm2-driver.
  • scripts/test-qemu.sh builds release and boots each bin in QEMU, asserting the expected serial markers (Startup rc=0, the Quote line, the attestation-complete / evidence markers).

WIT interfaces

The modular UEFI WIT package (tegmentum:uefi@0.1.0) lives in ../uefi-wamr/wit: small standalone interfaces (console, time, memory, variables, filesystem, protocols, boot, runtime, system, events) that a guest composes into exactly the world it needs. The TPM interfaces are tegmentum:tpm@0.1.0 (see ~/git/tpm-wit).

About

Run the wasmtime component-model runtime as a UEFI application (Pulley, no_std); runs a real TPM 2.0 in firmware

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors