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.
- Written in Rust against the first-class
x86_64-unknown-uefitarget, using theueficrate for firmware services — this produces a valid PE32+ EFI application directly (no EDK2/objcopy dance). - wasmtime is built
no_stdwith 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.
-
✅ Boot — minimal Rust UEFI app boots in QEMU/OVMF, heap allocator works.
-
✅ Core wasm — wasmtime (Pulley,
no_std) runs a core module under UEFI:add(20,22)=42from an AOT-compiled Pulley artifact loaded viaModule::deserialize. -
✅ Component model — a WASM component (
examples/uefi-demo-component, importstegmentum:uefi/console-out+time, exportsrun) runs under UEFI and calls the host imports, which are implemented over UEFI services. -
✅ 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-writtenno_stdWASI 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 useResource::new_own(noResourceTableneeded). 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.
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 pulley64Host imports are implemented via wasmtime::component::bindgen! + a HostState
that implements the generated Host traits, added with
add_to_linker::<_, HasSelf<_>>.
- A derived hard-float target spec (
x86_64-unknown-uefi.json, SSE2 on) + nightly-Zbuild-std— the stock UEFI target's soft-float ABI breakssha2/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 bothtools/aot-compileand the runtime.
cargo build --release --manifest-path tools/aot-compile/Cargo.toml
tools/aot-compile/target/release/aot-compile <in.wasm> assets/add.cwasm pulley64bash 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 ESPBuild --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.
uefi-wasmtime(src/main.rs) — the demo: boots, runs a core module, a component callingtegmentum:uefihost 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_Quoteover a verifier nonce → emit evidence (AK public + quoted PCR + attest + signature) to serial (between---BEGIN/END CITADEL-ATTEST---markers) and to\citadel-attest.binon the ESP.Evidence::encode()is a self-describing blob (magicCATT1) a Citadel verifier can appraise against thetegmentum:tpmcontract.
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.
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).
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.
tpm2-driverhas host-side unit tests (builder golden bytes,send_commandretry via a mock transport, response parsers):cargo testin../tpm2-driver.scripts/test-qemu.shbuilds release and boots each bin in QEMU, asserting the expected serial markers (Startup rc=0, the Quote line, the attestation-complete / evidence markers).
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).