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.
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.
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()
andexit()
). - 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.
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
- Open https://codespaces.new/google/wasefire?quickstart=1
- Click the green
Create new codespace
(orResume this codespace
) button - Wait a couple minutes for the codespace to be created
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 usestd
. Instead, you need to usecore
andalloc
. - 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.
- It makes sure the
- In the
tests
module, theuse wasefire_stub as _;
makes sure thewasefire-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 self
1.
// 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(()) }
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 theblinking
shared state isfalse
, 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.
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
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 thecrypto
,store
, andusb::serial
modules of the prelude. It comes with a companion program to interact with the applet (see the documentation in thesrc/lib.rs
of the applet).ctap
implements some simple CTAP-like API using thebutton
,led
,timer
,scheduling
,store
, andusb::serial
modules of the prelude. It describes its usage when connecting to the USB serial interface.memory_game
implements some memory game using theusb::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-dkhost
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
andwasmer
don't support no-stdwasmi
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 interpreterusb-device
andusbd-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)
- (e.g.
- 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
- Website: https://google.github.io/wasefire
- Github: https://github.com/google/wasefire
- Mailing list: https://groups.google.com/g/wasefire
Presentations
- Hardwear.io NL 2023 workshop slides
Documentation
- The FAQ of this document
- Rust prelude: https://docs.rs/wasefire
- Board API: https://docs.rs/wasefire-board-api
- Applet API: https://docs.rs/wasefire-applet-api
Acknowledgments
This project would not be where it is now if it couldn't build upon the following projects1:
Project | Author | Usage |
---|---|---|
defmt | Knurling | Logging for embedded |
probe-run | Knurling | (now replaced by probe-rs) |
probe-rs | probe.rs | Flashing and debugging |
cortex-m | Rust Embedded | Cortex-M support |
riscv | Rust Embedded | RISC-V support |
critical-section | Rust Embedded | Mutex support |
portable-atomic | taiki-e | Atomic support |
usb-device | Rust Embedded Community | Generic USB interface |
usbd-serial | Rust Embedded Community | USB serial implementation |
usbip-device | Sawchord | USB/IP implementation |
nrf-hal | nRF Rust | nRF52840 support |
nrf-usbd | nRF Rust | nRF52840 USB implementation |
hashes | Rust Crypto | SHA-256 and 384 |
MACs | Rust Crypto | HMAC-SHA-256 and 384 |
AEADs | Rust Crypto | AES-128-CCM and 256-GCM |
elliptic-curves | Rust Crypto | NIST P-256 and 384 |
rust-analyzer | Rust | Rust development |
wasm3 | Wasm3 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].
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.