Introduction

This book is a walk through the Wasefire project.

Vision

Provide a simple-to-use and secure-by-default ecosystem for firmware development.

Simple-to-use

The ecosystem aspires to be accessible to projects and developers regardless of their familiarity with developing secure firmware:

  • No embedded expertise required: Regular software engineers can develop firmware.
  • No security expertise required: The ecosystem provides secure-by-default options.
  • No enforced programming language: Developers may use the language of their choice1.
  • No enforced development environment or build system: Developers may use their usual setup.
1

As long as it compiles to WebAssembly (or the target architecture for native applets).

Secure-by-default

Security is the responsibility of the ecosystem, not the developer. The following security mechanisms are (or are planned to be) in place:

  • Sandboxing of the firmware functions (called applets) from each other and from the firmware runtime (called platform).
  • Secure implementation within the platform boundaries (e.g. side-channel resistance, fault injection protection, etc).
  • Security reviews for the supported boards (e.g. side-channel attacks, fault injection, etc).
  • User documentation (and warnings) when the security must rely on user behavior, for example when a configuration is insecure.

Even though the default is to be secure, the ecosystem will provide options to opt-out from some of the security features for performance or footprint reasons. In other words, the developer is able to make the trade-off between security and performance, defaulting to security without explicit action.

Symbiosis

Being both simple-to-use and secure-by-default actually goes hand in hand. It cannot be expected for humans to never do mistake, even if they have embedded and security expertise.

  • By being simple to use, developers will prefer using the ecosystem solution rather than implementing their own, thus using a secure-by-default solution rather than a possibly insecure implementation.
  • By not being concerned with security and embedded details, developers can be more productive and focus on the actual firmware behavior, following the well-lit path for all security and embedded questions.

Non-goals

The project is not trying to build a self-service applet store. In particular, users are developers. A self-service applet store may come at a later phase or may be done independently by another project. The project will provide anything needed for such applet store to be secure and easy to use.

Disclaimer

The project is still work in progress and many components might change in the future. However, the project follows the usual Rust semantic versioning to avoid unexpected breakages.

Overview

This chapter gives a high-level overview of the project.

Terminology

The project will refer to the following components:

  • A Device is a final product with possibly multiple functions.
  • A User is a person, team, or organization owning the Device.
  • A Board is the hardware of the Device.
  • An Applet is part of the software of the Device implementing a function.
  • The Board API is the hardware abstraction layer.
  • The Applet API is the platform abstraction layer.
  • A Prelude is a library providing language-specific access to the Applet API.
  • A Runner is a binary implementing the Board API and calling the Scheduler.
  • The Scheduler is the software implementing all the platform logic. It sits between the Board API and the Applet API.
  • A Platform is the binary made of a Runner and the Scheduler.

Device

A Device encompasses the following (non-exhaustive list):

  • A hardware on which to run (chip, form factor, external devices, etc).
  • How this hardware is configured and initially provisioned.
  • A set of applets defining the firmware, and their configuration.
  • What is the funtionality expected from the firmware.
  • Should the Device be certified.
  • Where will the Device be installed.
  • Who will/should have access to the Device.

User

Users may delegate part of the Device design to other teams:

  • Developing hardware or selecting an existing hardware.
  • Developing one or more applets and/or selecting one or more applets.
  • Design of the functionality.
  • Lifetime management (provisioning, logging, monitoring, alerting, census, etc).

Users are responsible for all those steps. The project will however support them in those tasks for both development and security aspects. For example (non-exhaustive list):

  • The platform provides a serial (ideally unique) for the device.
  • The platform supports secure updates.
  • The platform provides applet management (with versioning).
  • The platform supports some existing hardware (may add support for more based on demand).

Board

The project provides support for some boards through Runners. Additional boards may be supported depending on User needs.

Boards may be subject to some restrictions:

  • Only ARM Cortex-M and RISC-V architectures are considered for now.
  • Minimum flash and RAM requirements (to be defined).

Applet

The functionality (or business logic) of a Device is implemented as a set of concurrent applets. The project provides example applets in the different supported languages for the User to use as starting point. Eventually, an applet is compiled into WebAssembly which is the only language that the Scheduler supports1.

1

There is currently support for native applets as a work-around performance issues. Such applet is compiled to the target architecture and linked with the platform.

Board API

The Board API is the interface between a Board and the Scheduler and is implemented by a Runner. It is specified by the wasefire-board-api crate and provides cargo features to select the relevant part of the API. This selection may be motivated by different reasons:

  • The Board doesn't support some features in hardware and a software implementation is not feasible or desired.
  • The User knows that some features are not going to be used by any applets that will run on the Device. And the space saved by not implementing them is needed for the Device to properly function.

Applet API

The Applet API is the interface between an Applet and the Scheduler. It is specified by the wasefire-applet-api-desc crate which can generate code for different languages (e.g. the wasefire-applet-api crate for Rust). It also provides cargo features to select the relevant part of the API.

The Board API and Applet API relate by the following points:

  • Together, they provide portability of Applets across different Boards.
  • They are similar interfaces but with major differences.
  • The Board API is low-level and trusted by the Scheduler, while the Applet API may be filtered by the Scheduler based on permissions and capabilities.

Prelude

The Applet API is defined at the WebAssembly level. Applet developers may use it directly, but depending on the language this usage may not be convenient. For example, in Rust, using the Applet API requires the unsafe keyword and thus some good understanding of what's going on. For such languages, a Prelude is provided to give a simpler and more natural interface to the Applet API. For example, in Rust, the wasefire crate provides a safe API with high-level Rust-specific types like &[u8] (instead of the u32 pointer and u32 length that the Applet API expects).

Runner

The project provides Runners for supported Boards. However, Board support doesn't need to be provided by the project. The User (for example the team developing the Board) may develop its own Runner for its own Board. Simplifying the development of Runners (by maximizing the code shared with other Runners) is part of the project vision. The project simplifies development of both Runners and Applets, not just Applets.

Scheduler

Together with the Board API and the Applet API, the Scheduler is the core contribution of the project and where most security properties are implemented. This is completely provided by the project and Users cannot alter it. However, they can configure it when configuring a Platform.

Platform

This is the firmware that runs on the Device. It doesn't provide any business logic, but provides core functionalities through the platform protocol (non-exhaustive list):

  • Secure updates.
  • Applet management.
  • Debugging facilities.

Features

Features are implemented if their checkbox is checked.

If a feature is not implemented, it doesn't mean it will get implemented. It means this is a feature that could be implemented if there is a user need.

If a feature is not listed, it doesn't mean it won't get implemented. We may just not be aware of it, and a user need could justify an implementation.

Supported boards

A board is supported if it has a Runner.

  • Linux (for testing without hardware)
  • nRF52840
  • OpenTitan

Supported applet languages

An applet language is supported if it has a Prelude.

  • Rust
  • C
  • AssemblyScript

Note that when running multiple applets concurrently on the same platform, those applets don't need to be written in the same language to inter-operate.

Developer experience

For applets

  • Development doesn't require hardware (using the Linux board).
  • Testing facilities (using abort() and exit()).
  • Fuzzing facilities.
  • Debugging facilities.

For runners

  • Testing facilities (using the set of example applets suffixed with _test).

Reproducible builds

  • Hermetic development environment for applets.
  • Hermetic development environment for platforms.

Secure platform upgrades

  • The platform can be upgraded.
  • The platform can be downgraded to the extent permitted by the User-configured rollback policy.
  • Platform upgrades are digitally signed and verified.

Applet sandboxing

  • Applets can't tamper with the platform.
  • Applets can't tamper with other applets (this is only missing preemptive concurrency).

Applet capabilities

  • Applets declare their permissions1.
  • Applets declare their capabilities (more dynamic concept of permission).
  • Applets metadata (or manifest) is signed.
1

This is currently done using the set of imported functions in the WebAssembly module of the applet. This will most probably use the manifest mechanism in the future.

Platform side-channel attack testing and resistance

  • Crypto hardware accelerators are leveraged when available (board specific).
    • AES CCM (Bluetooth spec) on nRF52840
  • Software crypto primitives are provided as fallback.
  • Both of those implementations are side-channel attack resilient.

Applet portability

  • Applets are portable at binary level (comes from WebAssembly and APIs).

Applet multiplexing

  • Multiple applets may be installed at the same time.
  • Multiple applets may run simultaneously.
  • Applets can be installed without running.
  • Applets define in their metadata their running condition (e.g. at boot, at USB, at idle, etc).

For now, at most one applet can be installed. It immediately starts running after it has been installed and when the platform boots.

Applet management

  • Applets are identified by a stable id, a version, and a digital signature (verified by the runtime).
  • Applets may be installed if not already present (within the limit of one applet).
  • Applets may be uninstalled in which case all owned resources are deleted.
  • Applets may be upgraded (preserving resources) but not downgraded (probably modulo rollback policy).
  • Installed applets can be listed.

Applets can be uninstalled and upgraded, but the resources are untouched because there is no mapping of resources to applets yet (there is no stable identifier).

Certification

  • The runtime can run on certified hardware (FIPS-140-3 and CC).
  • TBD: The runtime might sustain being part of the security target for certification.

Low power

  • If the runtime is only waiting on external hardware events, the CPU is suspended.

Quick start

Please open an issue if a step doesn't work.

Download the Wasefire CLI

Local machine

Download the latest release of the Wasefire CLI for your platform. The code sample below assumes that your platform is x86_64-unknown-linux-gnu and that your PATH contains ~/.local/bin. You may need to adapt the commands for a different platform or environment.

tar xf wasefire-x86_64-unknown-linux-gnu.tar.gz
rm wasefire-x86_64-unknown-linux-gnu.tar.gz
mkdir -p ~/.local/bin
mv wasefire-x86_64-unknown-linux-gnu ~/.local/bin/wasefire

You can test that the CLI is correctly installed by running wasefire help.

You can also add shell completion with wasefire completion. You need to place the generated script where you shell will interpret it, which depends on your shell and configuration. If you use bash and have root access, you can copy it to /etc/bash_completion.d/wasefire or /usr/share/bash-completion/completions/wasefire.

GitHub Codespace

Start a host platform

You can start a host platform for development with the following command (it will run until interrupted or killed, so you will need a dedicated terminal):

wasefire host --interface=web

You may omit --interface=web if you don't need to interact with buttons and LEDs.

This will create a wasefire/host directory in the current directory to store the state of the host platform.

This will also ask for sudo permissions when using the USB platform protocol, which is the default (except in GitHub Codespace where export WASEFIRE_PROTOCOL=unix is added to the .bashrc file). If you don't want to use sudo, you can use the unix or tcp platform protocol. You'll have to pass --protocol=unix or --protocol=tcp to most wasefire commands or set the WASEFIRE_PROTOCOL environment variable in your shells.

Hello world in Rust

You can create a new Rust applet with:

wasefire rust-applet-new hello
cd hello

This will create a directory called hello with an example "hello world" applet.

You can build this applet (from the hello directory) with:

wasefire rust-applet-build

You can also run the unit tests with:

wasefire rust-applet-test

And you can install it (building it if needed) on a connected platform (for example the host platform started earlier) with:

wasefire rust-applet-install

Regardless of the programming language, if you already built an applet (by default under wasefire/applet.wasm), you can install it with:

wasefire applet-install wasefire/applet.wasm

And you can uninstall an applet (regardless of programming language) with:

wasefire applet-uninstall

Wasefire supports only one applet at a time for now. Once multiple applets can be installed simultaneously, the applet-uninstall command will take an applet ID as argument.

GitHub Codespace tips

Editing an applet

For rust-analyzer to work properly, you need to open the applet in its own workspace, for example:

code examples/rust/exercises/part-1

You can then open the src/lib.rs file and benefit from documentation (hovering a name), auto-completion, diagnostics, and other rust-analyzer features.

Troubleshooting

If you get an error about lsmod not found, you must run exec bash to make sure the WASEFIRE_PROTOCOL environment variable is set. The .bashrc file is modified as part of the Codespace setup and it may happen that the initial terminal was started before the setup finished.

Applet user guide

This chapter describes all you need to know to write an applet. Because we currently only support Rust for writing applets, this guide only describes Rust usage.

You need to have the Wasefire CLI installed according to the quick start instructions to be able to continue.

Create a new applet

You can create a Rust applet called tutorial with the following command:

wasefire rust-applet-new tutorial

This creates a tutorial directory with an example Rust applet. Let's look at Cargo.toml:

  • The wasefire dependency provides a Rust interface for the applet API.
  • The wasefire-stub optional dependency provides support for testing.
  • The test feature enables testing support.

And now let's look at src/lib.rs:

  • The #![no_std] attribute is needed for building WASM modules without WASI. As a consequence, you cannot use std. Instead, you need to use core and alloc.
  • The wasefire::applet!(); macro does a few things:
    • It makes sure the main() function is executed when the applet starts.
    • It imports the alloc crate.
    • It imports all the items of the wasefire crate.
  • In the tests module, the use wasefire_stub as _; makes sure the wasefire-stub crate is linked since it provides symbol definitions for the applet API.

Since most commands assume they are running from the root directory of the Rust applet (unless --crate-dir is provided), you can change the working directory to the crate:

cd tutorial

Run an applet

If you don't have a Wasefire platform, you can start a development platform on your machine according to the quick start instructions.

You can install a Rust applet with the following command:

wasefire rust-applet-install

This command does two things:

  • It builds the Rust applet, like wasefire rust-applet-build would.
  • It installs the built applet, like wasefire applet-install wasefire/applet.wasm would.

API

The applet API is documented here.

Note that you probably don't want to use the API directly, but instead want to use the prelude of your programming language. The API is low-level and corresponds to the interface at WebAssembly level.

Prelude

This chapter illustrates how to use some parts of the prelude.

The prelude documentation is available here.

LEDs

In this section, we will walk through the blink example in Rust. It will blink in order each LED of the board every second in an infinite loop (going back to the first LED after the last LED).

The number of LEDs available on the board is advertised by the led::count() function. We want to make sure there is at least one LED available:

    // Make sure there is at least one LED.
    let num_leds = led::count();
    assert!(num_leds > 0, "Board has no LEDs.");

We start the infinite loop cycling through all LEDs in order:

    // Cycle indefinitely through all LEDs in order.
    for led_index in (0 .. num_leds).cycle() {

Within the infinite loop (notice the indentation), we first turn on the current LED using the led::set() function:

        // Turn on the current LED.
        led::set(led_index, led::On);

We now wait for half a second because we want to blink each LED for one second which means half a second on and half a second off:

        // Wait before turning it off.
        timer::sleep(Duration::from_millis(500));

Finally, we repeat the same process but turning the LED off before looping to the next LED:

        // Turn it off and wait before turning on the next LED.
        led::set(led_index, led::Off);
        timer::sleep(Duration::from_millis(500));

The final code looks like this:

#![no_std]
wasefire::applet!();

use core::time::Duration;

fn main() {
    // Make sure there is at least one LED.
    let num_leds = led::count();
    assert!(num_leds > 0, "Board has no LEDs.");

    // Cycle indefinitely through all LEDs in order.
    for led_index in (0 .. num_leds).cycle() {
        // Turn on the current LED.
        led::set(led_index, led::On);
        // Wait before turning it off.
        timer::sleep(Duration::from_millis(500));

        // Turn it off and wait before turning on the next LED.
        led::set(led_index, led::Off);
        timer::sleep(Duration::from_millis(500));
    }
}

Testing

The host runner currently has 1 LED and, either prints its state to stdout (the default) or blinks the LED in the Web UI (with --interface=web). Eventually, the number of LEDs will be configurable.

To build and install the applet on a connected platform:

wasefire rust-applet-install

The output in a host platform without Web UI (i.e. wasefire host) should look like this:

Applet running.
Led is on
Led is off
Led is on
Led is off
Led is on
Led is off
Led is on
Led is off
Led is on
Led is off
Led is on
Led is off
Led is on
Led is off
Led is on
The applet was killed.

Where the applet was uninstalled (and thus killed) with wasefire applet-uninstall.

Buttons

In this section, we will walk through 2 Rust examples:

  • The button example illustrates stateless usage but lets us introduce how to handle events with callbacks.
  • The led example illustrates stateful usage and thus how to access state in callbacks.

Stateless usage

This example prints to the debug output the new state of a button each time that button changed state and so for all buttons.

Similarly to LEDs, there is a button::count() function to discover the number of buttons available on the board:

    // Make sure there is at least one button.
    let count = button::count();
    assert!(count > 0, "Board has no buttons.");

We want to listen on events for all available buttons, so we loop over all button indices (starting at 0):

    // For each button on the board.
    for index in 0 .. count {

For each button, we define a handler that prints the new button state to the debug output. The handler takes the new button state as argument. Since button::State implements Debug, we simply use {state:?} to print the new state.

        // We define a button handler printing the new state.
        let handler = move |state| debug!("Button {index} has been {state:?}.");

We can now start listening for events. This is done by creating a button::Listener which will call the provided handler each time the button changes state. We specify the button we want to listen to by its index and the handler as a closure.

        // We start listening for state changes with the handler.
        let listener = button::Listener::new(index, handler)?;

A listener continues to listen for events until it is dropped. Since we want to indefinitely listen, we must not drop the listener. A simple way to do that is to leak it. This is equivalent to calling core::mem::forget().

        // We leak the listener to continue listening for events.
        listener.leak();

Finally, we just endlessly wait for callbacks. This step is optional: waiting for callbacks indefinitely is the implicit behavior when main exits. In some way, main can be seen as a callback setup procedure. The scheduling::wait_for_callback() function puts the applet to sleep until a callback is scheduled and scheduled::wait_indefinitely() is just an infinite loop around wait_for_callback().

    // We indefinitely wait for callbacks.
    scheduling::wait_indefinitely();

The final code looks like this:

#![no_std]
wasefire::applet!();

fn main() -> Result<(), Error> {
    // Make sure there is at least one button.
    let count = button::count();
    assert!(count > 0, "Board has no buttons.");

    // For each button on the board.
    for index in 0 .. count {
        // We define a button handler printing the new state.
        let handler = move |state| debug!("Button {index} has been {state:?}.");

        // We start listening for state changes with the handler.
        let listener = button::Listener::new(index, handler)?;

        // We leak the listener to continue listening for events.
        listener.leak();
    }

    // We indefinitely wait for callbacks.
    scheduling::wait_indefinitely();
}

Testing

The host runner currently has 1 button and is either controlled by typing button on a line on stdin (the default) or by clicking the button in the Web UI (with --interface=web). With --interface=stdin (the default), one can also type press and release to control those events independently. Eventually, the number of buttons will be configurable.

The input and output of a host platform with --interface=stdio could look like this (using <- for input and -> for output):

-> Applet running.
<- button
-> 0.580263: Button 0 has been Pressed.
-> 0.580633: Button 0 has been Released.
<- button
-> 3.308765: Button 0 has been Pressed.
-> 3.309006: Button 0 has been Released.
<- press
-> 4.380741: Button 0 has been Pressed.
<- release
-> 5.436308: Button 0 has been Released.

Stateful usage

This example combines all the LEDs and buttons available on the board by maintaining a dynamic mapping between them. Initially, all buttons map to the first LED. Each time a button is pressed or released, the LED it is mapped to is toggled. And when a button is released, it maps to the next LED (or the first one if it was mapping to the last one).

In particular:

  • A single button can toggle all LEDs.
  • Multiple buttons can toggle the same LED.
  • A button may stay pressed while another button is pressed.
  • All buttons eventually toggle all LEDs.

We skip over the setup which doesn't illustrate anything new:

    // Make sure there is at least one button.
    let num_buttons = button::count();
    assert!(num_buttons > 0, "Board has no buttons.");

    // Make sure there is at least one LED.
    let num_leds = led::count();
    assert!(num_leds > 0, "Board has no LEDs.");

    // For each button on the board.
    for button_index in 0 .. num_buttons {

Because buttons dynamically map to LEDs, we need some state to store this information. For each button, we'll store the index of the LED to which it maps to in its handler. We have to use interior mutability (in this case Cell) because the handler is called as &self not &mut self1.

        // We create the state containing the LED to which this button maps to.
        let led_state = Cell::new(0);

When defining the button handler, we must move (and thus transfer ownership of) the state we just created to the handler, such that the handler can read and write the state when called.

        // We define the button handler and move the state to there.
        let handler = move |button_state| {

When the handler is called, we first toggle the associated LED:

            // We toggle the LED.
            let led_index = led_state.get();
            led::set(led_index, !led::get(led_index));

And then if the button is released, we update the dynamic mapping to point to the next LED (wrapping if needed):

            // If the button is released, we point it to the next LED.
            if matches!(button_state, button::Released) {
                led_state.set((led_index + 1) % num_leds);
            }

Finally, we create a button listener with the defined handler. And we leak it to continue listening after it goes out of scope and in particular after main returns.

        // We indefinitely listen by creating and leaking a listener.
        button::Listener::new(button_index, handler)?.leak();

The final code looks like this:

#![no_std]
wasefire::applet!();

use core::cell::Cell;

fn main() -> Result<(), Error> {
    // Make sure there is at least one button.
    let num_buttons = button::count();
    assert!(num_buttons > 0, "Board has no buttons.");

    // Make sure there is at least one LED.
    let num_leds = led::count();
    assert!(num_leds > 0, "Board has no LEDs.");

    // For each button on the board.
    for button_index in 0 .. num_buttons {
        // We create the state containing the LED to which this button maps to.
        let led_state = Cell::new(0);

        // We define the button handler and move the state to there.
        let handler = move |button_state| {
            // We toggle the LED.
            let led_index = led_state.get();
            led::set(led_index, !led::get(led_index));

            // If the button is released, we point it to the next LED.
            if matches!(button_state, button::Released) {
                led_state.set((led_index + 1) % num_leds);
            }
        };

        // We indefinitely listen by creating and leaking a listener.
        button::Listener::new(button_index, handler)?.leak();
    }

    Ok(())
}
1

This is because the handler could wait for callbacks itself and thus the handler may be reentered. This would essentially copy a mutable reference which is unsound.

Timers

In this section, we will walk through the button_abort example in Rust. It uses the first button and the first LED of the board. On a short press, the LED will start blinking. On a long press, the LED will stop blinking. While the button is pressed, the LED indicates whether the press is short or long:

  • The LED is on while the press is short.
  • The LED turns off once the press is long.

This applet will need a shared state to know whether the LED must be blinking or not. We cannot simply use a boolean because the state will be shared. We cannot use Cell<bool> neither because the state must be in the heap1. So we use Rc<Cell<bool>> which is a common pattern when using callbacks:

    // We define a shared state to decide whether we must blink.
    let blinking = Rc::new(Cell::new(false));

We can now allocate a timer for the blinking behavior using timer::Timer::new. This function takes the handler that will be called each time the timer fires. The handler simply toggles the LED if we must be blinking. Note how we must move a clone of the state to the callback. This is also a common pattern when using callbacks:

    // Allocate a timer for blinking the LED.
    let blink = timer::Timer::new({
        // Move a clone of the state to the callback.
        let blinking = blinking.clone();
        move || {
            // Toggle the LED if blinking.
            if blinking.get() {
                led::set(0, !led::get(0));
            }
        }
    });

The rest of the code is done in an infinite loop:

    loop {

At each iteration, we start by setting up a button::Listener to record whether the button was pressed and then released. The logic is similar to the callback setup for the timer. The small difference is that we won't need to call any function on the listener so we prefix its variable name _button with an underscore. We cannot simply omit the variable name because we don't want to drop it until the end of the loop iteration, otherwise we would stop listening to button events.

        // Setup button listeners.
        let pressed = Rc::new(Cell::new(false));
        let released = Rc::new(Cell::new(false));
        let _button = button::Listener::new(0, {
            let pressed = pressed.clone();
            let released = released.clone();
            move |state| match state {
                button::Pressed => pressed.set(true),
                button::Released if pressed.get() => released.set(true),
                button::Released => (),
            }
        })?;

We then wait until the button is pressed using scheduling::wait_until(). This function takes a condition as argument and only executes callbacks until the condition is satisfied.

        // Wait for the button to be pressed.
        scheduling::wait_until(|| pressed.get());

According to the specification of this example applet, when the button is pressed we must turn on the LED (and stop blinking if we were blinking) to signal a possible short press. Note that callbacks can only executed when a scheduling function is called, so we are essentially in a critical section. As such, the order in which we do those 3 lines doesn't matter. However, a callback might be scheduled before we stop the blink timer. It will execute next time we call a scheduling function. This is ok because when that will happen, the blinking state will be false and the blink handler will do nothing.

        // Turn the LED on.
        blink.stop();
        blinking.set(false);
        led::set(0, led::On);

To detect a long press, we need to start a timer. There is nothing new here except the Timer::start() function which takes the timer mode (one-shot or periodic) and its duration.

        // Start the timeout to decide between short and long press.
        let timed_out = Rc::new(Cell::new(false));
        let timer = timer::Timer::new({
            let timed_out = timed_out.clone();
            move || timed_out.set(true)
        });
        timer.start(timer::Oneshot, Duration::from_secs(1));

We now wait for the first event between the button being released and the timeout firing. This is simply done by using a condition which is a disjunction of the events of interest. This is a common pattern when implementing behavior with a timeout.

        // Wait for the button to be released or the timeout to fire.
        scheduling::wait_until(|| released.get() || timed_out.get());

To signal that the timeout was reached or the button was released, we turn off the LED.

        // Turn the LED off.
        led::set(0, led::Off);

Finally, if the press was short (i.e. the button was released before the timeout), we start blinking. This demonstrates the use of periodic timers.

        // Start blinking if short press.
        if !timed_out.get() {
            blinking.set(true);
            blink.start(timer::Periodic, Duration::from_millis(200));
        }

There are a few things to note:

  • The code is implicit in Rust, but the button handler and the timer handler within the loop iteration are dropped before the next iteration. This means that their callbacks are unregistered. This could be done explicitly by calling core::mem::drop() on their variable if needed.
  • It is not needed to start and stop the blink timer within the loop as long as it is started before entering the loop. This is just an optimization to avoid calling the handler when we know that the blinking shared state is false, because the handler would do nothing in that case.

The final code looks like this:

#![no_std]
wasefire::applet!();

use alloc::rc::Rc;
use core::cell::Cell;
use core::time::Duration;

fn main() -> Result<(), Error> {
    assert!(button::count() > 0, "Board has no buttons.");
    assert!(led::count() > 0, "Board has no LEDs.");

    // We define a shared state to decide whether we must blink.
    let blinking = Rc::new(Cell::new(false));

    // Allocate a timer for blinking the LED.
    let blink = timer::Timer::new({
        // Move a clone of the state to the callback.
        let blinking = blinking.clone();
        move || {
            // Toggle the LED if blinking.
            if blinking.get() {
                led::set(0, !led::get(0));
            }
        }
    });

    loop {
        // Setup button listeners.
        let pressed = Rc::new(Cell::new(false));
        let released = Rc::new(Cell::new(false));
        let _button = button::Listener::new(0, {
            let pressed = pressed.clone();
            let released = released.clone();
            move |state| match state {
                button::Pressed => pressed.set(true),
                button::Released if pressed.get() => released.set(true),
                button::Released => (),
            }
        })?;

        // Wait for the button to be pressed.
        scheduling::wait_until(|| pressed.get());

        // Turn the LED on.
        blink.stop();
        blinking.set(false);
        led::set(0, led::On);

        // Start the timeout to decide between short and long press.
        let timed_out = Rc::new(Cell::new(false));
        let timer = timer::Timer::new({
            let timed_out = timed_out.clone();
            move || timed_out.set(true)
        });
        timer.start(timer::Oneshot, Duration::from_secs(1));

        // Wait for the button to be released or the timeout to fire.
        scheduling::wait_until(|| released.get() || timed_out.get());

        // Turn the LED off.
        led::set(0, led::Off);

        // Start blinking if short press.
        if !timed_out.get() {
            blinking.set(true);
            blink.start(timer::Periodic, Duration::from_millis(200));
        }
    }
}

Testing

This is best tested with the Web UI.

1

If the state were on the stack and a callback were pointing to that state, it would become a safety requirement to unregister the callback before popping the state from the stack. However, it is safe to leak a callback with core::mem::forget() and thus not drop it. So we enforce callbacks to be 'static and thus not depend on references to the stack.

USB

For now only USB serial is supported. Eventually, the idea would be for applets to describe the USB interfaces they need in some init function. The scheduler would then create the USB device based on those information. And only then start the applets with capabilities to the interfaces they asked for.

In this section, we will illustrate USB serial usage by walking through the memory_game example. The game is essentially an infinite loop of memory questions. The player has 3 seconds to memorize a random base32 string (the length is the current level in the game and thus represents the difficulty). The player then has 7 seconds to type it back. On success they go to the next level, otherwise to the previous level.

The applet has only 2 states across loop iterations:

  • The level of the game (and thus the length of the string to remember) starting at 3.
  • The next prompt to show to the player while they get ready for the next question.
    let mut level = 3; // length of the string to remember
    let mut prompt = "Press ENTER when you are ready.";

Everything else is in the infinite loop:

    loop {

First thing we do is print the prompt and wait for the player to press Enter. We use ANSI escape codes to overwrite whatever was there before. As an invariant throughout the game, we always use a single line of the terminal. This is particularly important to overwrite the question since the player has to guess it. We write to the USB serial using serial::write_all(). This function is generic over objects implementing serial::Serial, in this case usb::serial::UsbSerial.

        serial::write_all(&UsbSerial, format!("\r\x1b[K{prompt}").as_bytes()).unwrap();

We then wait until the player presses Enter. We can read a single byte from the USB serial using serial::read_byte(). The terminal sends 0x0d when Enter is pressed.

        // Make sure the player is ready.
        while serial::read_byte(&UsbSerial).unwrap() != 0x0d {}

To generate the next question, we use rng::fill_bytes() which fills a buffer with random bytes. We provide a buffer with the length of the current level. For the string to be printable we truncate the entropy of each byte from 8 to 5 bits and convert it to a base32 symbol.

        // Generate a question for this level.
        let mut question = vec![0; level];
        rng::fill_bytes(&mut question).unwrap();
        for byte in &mut question {
            const BASE32: [u8; 32] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
            *byte = BASE32[(*byte & 0x1f) as usize];
        }
        let mut question = String::from_utf8(question).unwrap();

We can now show the question to the player. We do so using a process helper function that we will also use for the answer. We instantiate this function such that the player has 3 seconds to memorize the question and may hit Enter at any time to start answering.

        // Display the question.
        process(3, "Memorize this", &mut question, |_, x| x == 0x0d);

After 3 seconds have elapsed or if the player hit Enter, we read the answer from the player. We give them 7 seconds to type the answer. We also convert lower-case letters to upper-case for convenience (it's easier to read upper-case but easier to type lower-case). We also support backspace which the terminal sends as 0x7f. And same as for the question, we let the player exit early with Enter to avoid waiting until the timeout.

        // Read the answer.
        let mut answer = String::new();
        process(7, "Type what you remember", &mut answer, |answer, byte| {
            match byte {
                b'A' ..= b'Z' | b'2' ..= b'7' => answer.push(byte as char),
                b'a' ..= b'z' => answer.push(byte.to_ascii_uppercase() as char),
                0x7f => drop(answer.pop()),
                0x0d => return true,
                _ => (),
            }
            false
        });

Once we have the answer, we check if it matches the question. If it does, we promote the player to the next level. If it doesn't, we demote the player to the previous level. However, if there are no previous level because the player is at level 1, then we let them retry the level to show our support. We use ANSI escape codes to highlight the result.

        // Check the answer.
        if answer == question {
            level += 1;
            prompt = "\x1b[1;32mPromotion!\x1b[m Press ENTER for next level.";
        } else if level > 1 {
            level -= 1;
            prompt = "\x1b[1;31mDemotion...\x1b[m Press ENTER for previous level.";
        } else {
            prompt = "\x1b[1;41mRetry?\x1b[m Press ENTER to retry.";
        }

Now that we're done with the main loop, let's look at the process helper. It takes 4 arguments:

  • max_secs: usize: the maximum display time in seconds.
  • prompt: &str: the message shown at the beginning of the line.
  • data: &mut String: the data shown after the prompt, which may be updated (see below).
  • update: impl Fn(&mut String, u8) -> bool: the closure called on each input byte possibly updating the data and returning whether processing should end immediately without waiting for the maximum display time.
fn process(
    max_secs: usize, prompt: &str, data: &mut String, update: impl Fn(&mut String, u8) -> bool,
) {

The helper counts the number of elapsed seconds in shared variable secs and updates it using a periodic timer every second.

    let secs = Rc::new(Cell::new(0));
    let timer = timer::Timer::new({
        let time = secs.clone();
        move || time.set(time.get() + 1)
    });
    timer.start(timer::Periodic, Duration::from_secs(1));

The helper loops as long as the update function didn't say to stop (tracked by the done variable) and there is still time available.

    let mut done = false;
    while !done && secs.get() < max_secs {

We update the line in the terminal with the prompt, time left, and current data. We use ANSI escape codes to highlight the data and help readability.

        let secs = max_secs - secs.get();
        let message = format!("\r\x1b[K{prompt} ({secs} seconds remaining): \x1b[1m{data}\x1b[m");
        serial::write_all(&UsbSerial, message.as_bytes()).unwrap();

To be able to update the time left in the terminal we must read from the USB serial asynchronously using serial::Reader. We create a reader by providing a mutable buffer to which the reader will write the received bytes.

        let mut buffer = [0; 8];
        let reader = serial::Reader::new(&UsbSerial, &mut buffer);

We then sleep until a callback is executed using scheduling::wait_for_callback(). This callback may either be the timer firing the next second or the reader getting input from the USB serial.

        scheduling::wait_for_callback();

We call Reader::result() to know how many bytes were read from USB serial and written to the buffer (or if an error occurred). We then simply iterate over the received bytes and update the data and early exit status according to the provided closure. Same as with timers, when a reader is dropped, its callback is canceled.

        let len = reader.result().unwrap();
        for &byte in &buffer[.. len] {
            done |= update(data, byte);
        }

The final code looks like this:

#![no_std]
wasefire::applet!();

use alloc::rc::Rc;
use alloc::string::String;
use alloc::{format, vec};
use core::cell::Cell;
use core::time::Duration;

use wasefire::usb::serial::UsbSerial;

fn main() {
    let mut level = 3; // length of the string to remember
    let mut prompt = "Press ENTER when you are ready.";
    loop {
        serial::write_all(&UsbSerial, format!("\r\x1b[K{prompt}").as_bytes()).unwrap();

        // Make sure the player is ready.
        while serial::read_byte(&UsbSerial).unwrap() != 0x0d {}

        // Generate a question for this level.
        let mut question = vec![0; level];
        rng::fill_bytes(&mut question).unwrap();
        for byte in &mut question {
            const BASE32: [u8; 32] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
            *byte = BASE32[(*byte & 0x1f) as usize];
        }
        let mut question = String::from_utf8(question).unwrap();

        // Display the question.
        process(3, "Memorize this", &mut question, |_, x| x == 0x0d);

        // Read the answer.
        let mut answer = String::new();
        process(7, "Type what you remember", &mut answer, |answer, byte| {
            match byte {
                b'A' ..= b'Z' | b'2' ..= b'7' => answer.push(byte as char),
                b'a' ..= b'z' => answer.push(byte.to_ascii_uppercase() as char),
                0x7f => drop(answer.pop()),
                0x0d => return true,
                _ => (),
            }
            false
        });

        // Check the answer.
        if answer == question {
            level += 1;
            prompt = "\x1b[1;32mPromotion!\x1b[m Press ENTER for next level.";
        } else if level > 1 {
            level -= 1;
            prompt = "\x1b[1;31mDemotion...\x1b[m Press ENTER for previous level.";
        } else {
            prompt = "\x1b[1;41mRetry?\x1b[m Press ENTER to retry.";
        }
    }
}

fn process(
    max_secs: usize, prompt: &str, data: &mut String, update: impl Fn(&mut String, u8) -> bool,
) {
    let secs = Rc::new(Cell::new(0));
    let timer = timer::Timer::new({
        let time = secs.clone();
        move || time.set(time.get() + 1)
    });
    timer.start(timer::Periodic, Duration::from_secs(1));
    let mut done = false;
    while !done && secs.get() < max_secs {
        let secs = max_secs - secs.get();
        let message = format!("\r\x1b[K{prompt} ({secs} seconds remaining): \x1b[1m{data}\x1b[m");
        serial::write_all(&UsbSerial, message.as_bytes()).unwrap();
        let mut buffer = [0; 8];
        let reader = serial::Reader::new(&UsbSerial, &mut buffer);
        scheduling::wait_for_callback();
        let len = reader.result().unwrap();
        for &byte in &buffer[.. len] {
            done |= update(data, byte);
        }
    }
}

Testing

The host platform doesn't enable USB serial by default. Pass --usb-serial to enable it:

wasefire host --usb-serial

Once the applet is running, you can connect to the USB serial with the following command:

picocom -q /dev/ttyACM0

You can install picocom with sudo apt-get install picocom.

UART

Using the UART is similar to using the USB serial, because the uart::Uart::new(uart_id) object implements serial::Serial, where uart_id is the UART index. The uart::count() function returns how many UARTs are available on the device. UART indices must be smaller than this count.

It is usually a good idea to write code that is generic over the serial implementation. This can be done by using a serial variable implementing serial::Serial. This variable may be instantiated differently based on a compilation feature:

#[cfg(feature = "serial_uart")]
let serial = uart::Uart::new(0).unwrap();
#[cfg(feature = "serial_usb")]
let serial = usb::serial::UsbSerial;

serial::write_all(&serial, b"hello").unwrap();

Host platforms don't have a real UART. Instead they create a UNIX socket. You can connect to such a UART by connecting to the UNIX socket. In a terminal dedicated for the connection (you will need to close the terminal to close the connection) and from the directory where the host platform is running, you can run:

socat -,cfmakeraw UNIX-CONNECT:wasefire/host/uart0

You can install socat with sudo apt-get install socat.

RPC

In this section, we will walk through the protocol example in Rust. It converts the case of alphabetic ASCII characters from its request to its response and switches the letters I and O (similarly for lower-case the letters i and o).

The platform protocol (used to install applets, update the platform, reboot the platform, etc) also provides a way to call into an applet by sending a request and reading the response. The applet can define a handler taking a Vec<u8> request as argument and returning a Vec<u8> response (which can reuse the backing storage of the request).

In this example, the handler simply converts all characters of the request, prints how many times it was called, and returns the updated request as its response1.

    let mut counter = 0;
    let handler = move |mut data: Vec<u8>| {
        data.iter_mut().for_each(convert);
        counter += 1;
        debug!("Converted {counter} lines.");
        data
    };

A listener can be created and leaked as usual.

    rpc::Listener::new(&platform::protocol::RpcProtocol, handler).leak();

The conversion function is straightforward:

fn convert(x: &mut u8) {
    if x.is_ascii_alphabetic() {
        *x ^= 0x20; // switch case
    }
    if matches!(*x, b'I' | b'O' | b'i' | b'o') {
        *x ^= 0x6; // switch between I and O preserving case
    }
}

The final code looks like this:

#![no_std]
wasefire::applet!();

use alloc::vec::Vec;

fn main() {
    let mut counter = 0;
    let handler = move |mut data: Vec<u8>| {
        data.iter_mut().for_each(convert);
        counter += 1;
        debug!("Converted {counter} lines.");
        data
    };
    rpc::Listener::new(&platform::protocol::RpcProtocol, handler).leak();
}

fn convert(x: &mut u8) {
    if x.is_ascii_alphabetic() {
        *x ^= 0x20; // switch case
    }
    if matches!(*x, b'I' | b'O' | b'i' | b'o') {
        *x ^= 0x6; // switch between I and O preserving case
    }
}

Testing

You can use the wasefire applet-rpc command to send an RPC to an applet. By default it reads from standard input and write to standard output.

% echo HELLO ping | wasefire applet-rpc
helli PONG
1

Note that we don't need interior mutability here, compared to buttons for example. This is because the RPC handler is not re-entrant. The platform won't send a new request if the previous request was not responded.

Storage

For now only a key-value store is supported for persistent storage. Eventually, additional facilities may be added: a cyclic logging journal, a file-system, raw flash access, etc.

In this section, we will illustrate the key-value store usage by walking through the store example which provides direct store access with a text-based interface through the platform protocol applet RPC mechanism.

The key-value store has 2 restrictions:

  • Keys must be between 0 and 4095
  • Values must be at most 1023 bytes

The second restriction can be worked around by storing large entries as multiple fragments of at most 1023 bytes each using multiple keys. To support those large entries, we define an abstract notion of keys. An abstract key is either exactly one key, or a contiguous range of keys.

enum Key {
    Exact(usize),
    Range(Range<usize>),
}

The store provides 3 operations: insert, find, and remove. Each of those operation comes with a fragmented variant (working on a range of keys for large values). We define helpers to dispatch to the regular or fragmented version based on the abstract key.

fn insert(key: &Key, value: &[u8]) -> Result<(), Error> {
    match key {
        Key::Exact(key) => store::insert(*key, value),
        Key::Range(keys) => store::fragment::insert(keys.clone(), value),
    }
}

fn find(key: &Key) -> Result<Option<Box<[u8]>>, Error> {
    match key {
        Key::Exact(key) => store::find(*key),
        Key::Range(keys) => store::fragment::find(keys.clone()),
    }
}

fn remove(key: &Key) -> Result<(), Error> {
    match key {
        Key::Exact(key) => store::remove(*key),
        Key::Range(keys) => store::fragment::remove(keys.clone()),
    }
}

To ease parsing and processing of RPC requests, we define a straightforward type for commands.

enum Command<'a> {
    Help,
    Insert { key: Key, value: &'a str },
    Find { key: Key },
    Remove { key: Key },
}

The parsing function is also straightforward.

impl<'a> Command<'a> {
    fn parse(input: &'a str) -> Result<Self, String> {
        Ok(match *input.split_whitespace().collect::<Vec<_>>().as_slice() {
            [] | ["help"] => Command::Help,
            ["insert", key, value] => Command::Insert { key: Key::parse(key)?, value },
            ["find", key] => Command::Find { key: Key::parse(key)? },
            ["remove", key] => Command::Remove { key: Key::parse(key)? },
            [command, ..] => return Err(format!("Invalid command {command:?}")),
        })
    }

The process function is more interesting as we'll describe how the store operations behave. When processing a command, we may either succeed with an output, or fail with an error. We use strings because we implement a text-based interface.

    fn process(&self) -> Result<String, String> {

For the help command, we simply output the grammar for commands.

        match self {
            Command::Help => Ok("\
Usage: insert <key>[..<key>] <value>
Usage: find <key>[..<key>]
Usage: remove <key>[..<key>]"
                .to_string()),

For insert commands, we use our helper function for abstract keys and format the output and error appropriately. When inserting to the store, if the key (or range of keys) is already present, then its value is overwritten.

            Command::Insert { key, value } => match insert(key, value.as_bytes()) {
                Ok(()) => Ok("Done".to_string()),
                Err(error) => Err(format!("{error}")),
            },

Remove commands are similar. When removing from the store, it is not an error if the key (or range of keys) is absent.

            Command::Remove { key } => match remove(key) {
                Ok(()) => Ok("Done".to_string()),
                Err(error) => Err(format!("{error}")),
            },

Find commands also use the helper function, however they have 2 possible outputs (outside errors):

  • If the key is absent from the store, then None is returned.
  • If the key is present, then Some is returned with the bytes of the value. Because we implement a text-based interface, we try to convert the byte slice to a string slice for the output.
            Command::Find { key } => match find(key) {
                Ok(None) => Ok("Not found".to_string()),
                Ok(Some(value)) => match core::str::from_utf8(&value) {
                    Ok(value) => Ok(format!("Found: {value}")),
                    Err(_) => Ok(format!("Found (not UTF-8): {value:02x?}")),
                },
                Err(error) => Err(format!("{error}")),
            },

We can finally write our handler function taking a request as argument and returning a response.

fn handler(request: Vec<u8>) -> Vec<u8> {
    // Parse and process the request.
    let result: Result<String, String> = try {
        let request = String::from_utf8(request).map_err(|_| "Request is not UTF-8")?;
        Command::parse(&request)?.process()?
    };
    // Format output including error and next prompt.
    let mut output = result.unwrap_or_else(|error| format!("Error: {error}"));
    output.push_str("\n> ");
    output.into_bytes()
}

The main function simply registers an RPC listener with the handler above.

fn main() {
    rpc::Listener::new(&platform::protocol::RpcProtocol, handler).leak();
}

The final code looks like this:

#![no_std]
#![feature(try_blocks)]
wasefire::applet!();

use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::num::ParseIntError;
use core::ops::Range;
use core::str::FromStr;

fn main() {
    rpc::Listener::new(&platform::protocol::RpcProtocol, handler).leak();
}

fn handler(request: Vec<u8>) -> Vec<u8> {
    // Parse and process the request.
    let result: Result<String, String> = try {
        let request = String::from_utf8(request).map_err(|_| "Request is not UTF-8")?;
        Command::parse(&request)?.process()?
    };
    // Format output including error and next prompt.
    let mut output = result.unwrap_or_else(|error| format!("Error: {error}"));
    output.push_str("\n> ");
    output.into_bytes()
}

enum Command<'a> {
    Help,
    Insert { key: Key, value: &'a str },
    Find { key: Key },
    Remove { key: Key },
}

impl<'a> Command<'a> {
    fn parse(input: &'a str) -> Result<Self, String> {
        Ok(match *input.split_whitespace().collect::<Vec<_>>().as_slice() {
            [] | ["help"] => Command::Help,
            ["insert", key, value] => Command::Insert { key: Key::parse(key)?, value },
            ["find", key] => Command::Find { key: Key::parse(key)? },
            ["remove", key] => Command::Remove { key: Key::parse(key)? },
            [command, ..] => return Err(format!("Invalid command {command:?}")),
        })
    }

    fn process(&self) -> Result<String, String> {
        match self {
            Command::Help => Ok("\
Usage: insert <key>[..<key>] <value>
Usage: find <key>[..<key>]
Usage: remove <key>[..<key>]"
                .to_string()),
            Command::Insert { key, value } => match insert(key, value.as_bytes()) {
                Ok(()) => Ok("Done".to_string()),
                Err(error) => Err(format!("{error}")),
            },
            Command::Find { key } => match find(key) {
                Ok(None) => Ok("Not found".to_string()),
                Ok(Some(value)) => match core::str::from_utf8(&value) {
                    Ok(value) => Ok(format!("Found: {value}")),
                    Err(_) => Ok(format!("Found (not UTF-8): {value:02x?}")),
                },
                Err(error) => Err(format!("{error}")),
            },
            Command::Remove { key } => match remove(key) {
                Ok(()) => Ok("Done".to_string()),
                Err(error) => Err(format!("{error}")),
            },
        }
    }
}

enum Key {
    Exact(usize),
    Range(Range<usize>),
}

impl FromStr for Key {
    type Err = ParseIntError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.split_once("..") {
            Some((start, end)) => Ok(Key::Range(start.parse()? .. end.parse()?)),
            None => Ok(Key::Exact(s.parse()?)),
        }
    }
}

impl Key {
    fn parse(key: &str) -> Result<Self, String> {
        let key: Key = key.parse().map_err(|_| "Failed to parse key")?;
        let valid = match &key {
            Key::Exact(key) => *key < 4096,
            Key::Range(keys) => !keys.is_empty() && keys.end < 4096,
        };
        if !valid {
            return Err("Invalid key".to_string());
        }
        Ok(key)
    }
}

fn insert(key: &Key, value: &[u8]) -> Result<(), Error> {
    match key {
        Key::Exact(key) => store::insert(*key, value),
        Key::Range(keys) => store::fragment::insert(keys.clone(), value),
    }
}

fn find(key: &Key) -> Result<Option<Box<[u8]>>, Error> {
    match key {
        Key::Exact(key) => store::find(*key),
        Key::Range(keys) => store::fragment::find(keys.clone()),
    }
}

fn remove(key: &Key) -> Result<(), Error> {
    match key {
        Key::Exact(key) => store::remove(*key),
        Key::Range(keys) => store::fragment::remove(keys.clone()),
    }
}

Testing

We can use a one-liner REPL in shell around wasefire applet-rpc:

while read line; do echo "$line" | wasefire applet-rpc; done

An example interaction could look like this:

help
Usage: insert <key>[..<key>] <value>
Usage: find <key>[..<key>]
Usage: remove <key>[..<key>]
> insert 0 hello world
Error: Invalid command "insert"
> insert 0 hello-world
Done
> find 0
Found: hello-world
> find 1
Not found
> remove 0
Done
> find 0
Not found

Exercises

The examples/rust/exercises of the Wasefire repository contains exercises to implement an applet that behaves like a security key over UART.

The part-<n> directories contain the successive parts towards the final applet. You will need to modify those applets by fixing the different TODO comments. The exercise description is at the top of the src/lib.rs file.

The part-<n>-sol directories contain the solution for each part. You don't need to modify those applets. You can look at them for hints while working part-<n>.

The client directory contains a binary to communicate with the applet. You don't need to modify this binary. You can build it with cargo run --release from the client directory, then execute it from the host platform directory with $WASEFIRE_REPO/target/release/client. You can also copy the executable to the host platform directory and use ./client. You need to run it from the host platform directory because it assumes the UART socket to be at wasefire/host/uart0 (which is the default for the host platform).

The interface directory contains a library defining the interface between the applet and the client. You don't need to modify this library but you need to read its documentation. You will use it from the applet.

Examples

There are a few existing applets that demonstrate simple usage of the prelude. Each example starts with a short documentation in its src/lib.rs file.

Noticeable examples are:

  • hsm implements some simple HSM-like API using the crypto, store, and usb::serial modules of the prelude. It comes with a companion program to interact with the applet (see the documentation in the src/lib.rs of the applet).
  • ctap implements some simple CTAP-like API using the button, led, timer,scheduling, store, and usb::serial modules of the prelude. It describes its usage when connecting to the USB serial interface.
  • memory_game implements some memory game using the usb::serial module of the prelude. It describes its usage when connecting to the USB serial interface.

Runner user guide

This chapter will describe how you can add support for a board. It is not yet written though and only provides a link to the API to implement.

There's currently only 2 supported boards:

  • nordic for nRF52840-dk
  • host for Linux (not clear if more or less is actually supported)

API

The board API is documented here.

Frequently asked questions

This section tries to answer common questions. If your question is not listed here, please open an issue.

Are applets trusted to be correct?

No. The platform does not trust applets and applets do not trust each other. However, if an applet has a valid signature, then the platform trusts the permissions required by the applet.

Are applets executed as native code?

No. Applets are installed as WebAssembly byte-code. This is required since the static guarantees provided by WebAssembly apply to the byte-code and the platform checks those guarantees. For execution, applets are interpreted: either directly from the byte-code, or for performance purposes from an optimized representation in flash or RAM which may be computed ahead-of-time or on-demand.

Why is performance not an issue?

The main bets are:

  • Computing intensive code (like cryptography) is done in hardware or native code (in the platform).
  • Applets are supposed to only do business logic which is assumed to not be computing intensive.
  • The platform targets users who can't write embedded code, so the main concern is not performance but making firmware development accessible.

How does this fit on micro-controllers?

The interpreter currently fits in 22kB when optimized for size and 66kB when optimized for speed. The interpreter is also designed to have minimal RAM overhead. However, most optimizations (for both performance and overhead) are not yet implemented, which may increase the binary size.

Why implement a new interpreter?

The following runtimes have been quickly rejected:

  • wasmtime and wasmer don't support no-std
  • wasmi consumes too much RAM for embedded

wasm3 has been used during the initial phase of the project but got eventually replaced with a custom interpreter for the following reasons:

  • It doesn't perform validation yet. We probably need proper validation.
  • It only compiles to RAM (not flash). We want to be able to preprocess a module and persist the pre-computation in flash such that it is only done when a module is installed and not each time it is instantiated.
  • It takes control of the execution flow. All runtimes I'm aware of behave like that. To simplify scheduling, we want the interpreter to give back control when asked or when a host function is called.
  • It is written in C. Although the code has tests and fuzzing, we want additional security provided by the language.

The interpreter we implemented is written in Rust, doesn't take control of the execution thread, doesn't pre-compute anything yet (but will be able to pre-compute to flash), and performs validation.

Applet footprint compared to native code?

WebAssembly byte-code is compact so there should be a footprint benefit compared to native code. However, no benchmarks have been done in that regard.

Is it possible to share code between applets?

Yes (although not yet implemented). Applets are represented at runtime by a WebAssembly store which is unique per applet. Applets behavior is defined by a set of WebAssembly modules which are instantiated to the applet store. Applets may share those modules. A typical example would be an allocator module. Multiple applets may use the same allocator byte-code (from the module) to manage their own linear memory (from the module instance in the applet store).

What third-party dependencies are used?

The minimum set of third-party dependencies is currently:

  • num_enum for the interpreter
  • usb-device and usbd-serial for the board API

Additional dependencies are used by:

  • the actual board implementation:
    • (e.g. cortex-m-rt, nrf52840-hal, panic-abort for nordic)
    • (e.g. tokio, usbip-device, aes, rand for linux)
  • compilation (e.g. proc-macro2, quote)
  • debugging (e.g. defmt, defmt-rtt, log, env_logger)
  • tooling (e.g. anyhow, clap)

In particular, the project doesn't need any operating system (e.g. TockOS) but may use one as part of a board implementation.

Links

Project

Presentations

  • Hardwear.io NL 2023 workshop slides

Documentation

Acknowledgments

This project would not be where it is now if it couldn't build upon the following projects1:

ProjectAuthorUsage
defmtKnurlingLogging for embedded
probe-runKnurling(now replaced by probe-rs)
probe-rsprobe.rsFlashing and debugging
cortex-mRust EmbeddedCortex-M support
riscvRust EmbeddedRISC-V support
critical-sectionRust EmbeddedMutex support
portable-atomictaiki-eAtomic support
usb-deviceRust Embedded CommunityGeneric USB interface
usbd-serialRust Embedded CommunityUSB serial implementation
usbip-deviceSawchordUSB/IP implementation
nrf-halnRF RustnRF52840 support
nrf-usbdnRF RustnRF52840 USB implementation
hashesRust CryptoSHA-256 and 384
MACsRust CryptoHMAC-SHA-256 and 384
AEADsRust CryptoAES-128-CCM and 256-GCM
elliptic-curvesRust CryptoNIST P-256 and 384
rust-analyzerRustRust development
wasm3Wasm3 Labs(not used anymore)

We would like to thank all authors and contributors of those projects (as well as those we use but we forgot to mention) and more generally the community around Rust for embedded development. We are also grateful to those who answered our issues [1, 2, 3, 4, 5, 6, 7, 8, 9] and reviewed our pull requests [10, 11, 12, 13].

1

We tried to focus on the projects that were critical to get where we are now. Some projects are thus deliberately left out (and some projects that we don't use anymore are still listed). But it is also possible that we forgot to list some, in which case we apologize and would be happy to fix our oversight if notified.