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 choice, as long as it compiles to WebAssembly or the target architecture.
- No enforced development environment or build system: Developers may use their usual setup.
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 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 choose 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 is split in the following components:
- A Device is a final product.
- 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.
- 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 running the Scheduler.
- The Scheduler is the software implementing all the platform logic and fits 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 unique ids per 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 compilet into WebAssembly which is the only language that the Scheduler supports.
Board API
The Board API may be partially implemented for 2 reasons:
- The Board doesn't support some features in hardware and a software implementation is not feasible or desired.
- The User knows that those features are not going to be used by any applets that will run on the Device. And the space saved by not implementing or building them is needed for the Device to properly function.
Applet API
The Applet API is currently the same as (or very close to) the Board API. This may change in the future, for example if capabilities require it. The Board API is low-level and doesn't have a notion of capabilities. It fully trusts the Scheduler. The Applet API on the other hand may need to prove to the Scheduler that it is allowed to access some dynamically-allocated resource.
The Board API and the Applet API provide portability of Applets across different Boards.
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 Prelude 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 some core functionalities like (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
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 (probably on any board).
- Fuzzing facilities (probably on Linux board only).
- Rich debugger experience (probably on any board).
For runners
- Testing facilities (probably a set of test applets).
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 permissions (i.e. function imports).
- Applets declare their capabilities (more dynamic concept of permission).
- Applets metadata (or manifest) is signed.
Platform side-channel attack testing and resistance
-
Crypto hardware accelerators are leveraged when available.
- AES CCM (Bluetooth spec) on nRF52840
- Otherwise fallback software crypto primitives are provided for main algorithms.
- Both of those implementations are side-channel attack resilient.
Applet portability
- Applets are portable at binary level (comes from Wasm and APIs).
Applet multiplexing
- Multiple applets may be installed at the same time.
- Multiple applets may run simultaneously (not in early versions).
- 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, a single applet is baked at compile-time in the platform.
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.
- 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.
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.
Repository setup
Local machine
Clone the repository and run the setup script1:
git clone https://github.com/google/wasefire
cd wasefire
./scripts/setup.sh
Github codespace
- Open https://codespaces.new/google/wasefire?quickstart=1
- Click the green
Create new codespace
(orResume this codespace
) button - Wait 2 minutes for the codespace to be created (or a 10 seconds to be resumed)
Run an applet
Depending on your hardware, you can run the hello applet with:
cargo xtask applet rust hello runner nordic
to run on an nRF52840 dev-kit.cargo xtask applet rust hello runner host
to run on your desktop.cargo xtask applet rust hello runner host --web
to run on your desktop with a web UI.
The general format is cargo xtask applet LANGUAGE NAME runner BOARD
. Example
applets are listed in the examples
directory by language then name.
Boards are listed under the crates
directory and are prefixed by runner-
.
You can find more details using cargo xtask help
.
Feel free to stop and play around by directly editing the examples. Or continue reading for a deeper tutorial on how to write applets in Rust.
The setup script is best effort and is only meant for Unix systems so far. On Debian-based Linux systems, the script will directly invoke installation commands, possibly asking for sudo password. On other systems, the script will return an error and print a message each time a binary or library must be installed. Once the package is installed, the script must be run again. The script will eventually print nothing and return a success code indicating that no manual step is required and everything seems in order.
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
However, this will also modify the working directory of the terminal. To be able
to run cargo xtask
, you will need to go back to the root of the git
repository:
cd /workspaces/wasefire
You can then open the src/lib.rs
file and benefit from documentation (hovering
a name), auto-completion, diagnostics, and other rust-analyzer features.
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 set the repository up according to the quick start instructions to be able to continue.
Create a new applet
This step will eventually be a
simple wasefire new <applet-name>
command out of tree. But for now we will
build the applet within the project repository as an example applet. We'll use
tutorial
as the applet name throughout this tutorial.
You have 2 options to create and populate the applet directory. We'll go over both for pedagogical reasons.
Copy the hello
applet
The first step is to copy the hello
directory to the tutorial
directory:
cp -r examples/rust/hello examples/rust/tutorial
The second step is to update the applet name in the Cargo.toml
:
sed -i 's/hello/tutorial/' examples/rust/tutorial/Cargo.toml
Create the applet from scratch
Create the tutorial
directory:
mkdir examples/rust/tutorial
Create the Cargo.toml
file in the created directory with the following
content:
[package]
name = "tutorial"
version = "0.1.0"
edition = "2021"
[dependencies]
wasefire = "*" # use the latest version
The wasefire
dependency provides a high-level interface to the Applet API.
Then create the src/lib.rs
file in the created directory with the following
content:
#![no_std] // needed for building wasm (without wasi) wasefire::applet!(); // imports the prelude and defines main as entry point fn main() { debug!("hello world"); }
Note that because you need to use core
or alloc
instead of std
.
Run an applet
We currently use cargo xtask
as an alias to the local xtask
crate to build,
flash, and run platforms and applets. Eventually, this will be a wasefire
command and will work out-of-tree. You can use cargo xtask help
to discover
the tool.
On host
We can run an applet (here the tutorial
applet) on the host
runner with the
following command:
cargo xtask applet rust tutorial runner host
Type your password when asked. The host
runner needs sudo
to set USB/IP up,
which is needed for applets that use USB. This is disabled in Github Codespace.
After a bunch of compilation steps, you should see something that ends like:
Running `.../wasefire/target/release/runner-host`
Executing: sudo modprobe vhci-hcd
[sudo] password for user:
Executing: sudo usbip attach -r localhost -b 1-1
Board initialized. Starting scheduler.
00000.000: hello world
The first line is output by cargo
. The last 2 lines are output by the host
runner. The last one was triggered by the applet. Debugging output is prefixed
with a timestamp.
The host runner (like all runners) doesn't stop, even if all applets have completed. Instead, it goes to sleep. This is because all known use-cases are reactor-like (they react to external input). Besides, if the platform has applet management enabled, then the platform is ready to execute applet management commands. However, if there is a use-case that needs to shutdown, then the API or scheduler will be extended to provide this functionality.
Use Ctrl-C to terminate the runner. For hardware boards, you can just remove the power or let it run. The device is in sleep state (although if USB is enabled, then it wakes up every millisecond to keep the connection active).
On board
We currently only support the nRF52840-dk board from Nordic. If you have such a
dev board, you can run an applet (here the tutorial
applet) on the nordic
runner with the following command:
cargo xtask applet rust tutorial runner nordic
After a bunch of compilation steps, you should see something that ends like:
".../wrapper.sh" "probe-rs" "run" "--chip=nRF52840_xxAA" "target/.../runner-nordic"
Erasing sectors [00:00:05] [################################]
Programming pages [00:00:04] [################################]
0.090527: hello world
The first line is from cargo xtask
. The rest is from probe-rs run
. The last
line is triggered by the applet. Debugging output is prefixed by a timestamp in
seconds.
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 prints its state on stdout
as an
info-level log. Eventually, the number of LEDs will be configurable and how they
are represented will be improved (for example through some graphical interface).
To test the applet on the host
runner, you'll thus need to use:
cargo xtask applet rust blink runner host --log=info
The --log=info
flag specifies that we want info-level (or more severe)
logging. By default, only errors are printed.
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 controlled by typing button
on
a line on stdin
. Eventually, the number of buttons will be configurable and
how they are controlled will be improved (for example through some graphical
interface).
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 a LED, we need a state to store this
information. We create this state on the heap because we will eventually leak
the listener and exit the main
function to indefinitely listen for button
events. This state simply contains the index of the LED to which this buttons
maps to. We have to use Cell
because the handler is called as &self
(and
thus closures must be Fn
not FnMut
)1.
// We create the state containing the LED to which this button maps to.
let led_pointer = Box::new(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_pointer.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_pointer.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 going 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 alloc::boxed::Box; 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_pointer = Box::new(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_pointer.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_pointer.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 (which the prelude has no way to know, or is there?) 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, because callbacks must be 'static
1:
// 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
As for the LEDs and buttons examples, to test the applet on the host
runner,
you'll need to use:
cargo xtask applet rust button_abort runner host --log=info
However, in addition to button
which does a press and release sequence, you
can use press
and release
to independently press and release the button. In
particular, button
may be used to start blinking and press
may be used to
stop blinking. There's no need to explicitly release because the applet supports
missing callbacks for robustness.
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 than 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 press 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); } } }
You can connect to the USB serial with the following command:
picocom -q /dev/ttyACM1
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 id. The uart::count()
function returns how many UARTs are
available on the device. UART ids must be smaller than this count.
It is usually a good idea to write generic code over any serial without assuming
a particular 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();
When using the host runner, you can connect to the UART with:
socat -,cfmakeraw UNIX-CONNECT:target/wasefire/uart0
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 through USB serial.
We first define a helper to write a line to the USB serial.
fn writeln(buf: &[u8]) {
serial::write_all(&UsbSerial, buf).unwrap();
serial::write_all(&UsbSerial, b"\r\n").unwrap();
}
Because values may be at most 1023 bytes, there is a system to store 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>),
}
We then define helpers to dispatch to the regular or fragmented version based on
the abstract key. The insert
, find
, and remove
functions will be explained
later.
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()),
}
}
The first thing we do when the applet starts is print a short help describing how to use the applet.
fn main() {
writeln(b"Usage: insert <key>[..<key>] <value>");
writeln(b"Usage: find <key>[..<key>]");
writeln(b"Usage: remove <key>[..<key>]");
We can then start the infinite loop processing exactly one command per iteration.
loop {
We start a loop iteration by reading a command from the user. Only space, lower-case alphabetic characters, digits, and backspace are supported. We exit as soon as the user hits Enter.
// Read the command.
let mut command = String::new();
loop {
serial::write_all(&UsbSerial, format!("\r\x1b[K> {command}").as_bytes()).unwrap();
match serial::read_byte(&UsbSerial).unwrap() {
c @ (b' ' | b'.' | b'a' ..= b'z' | b'0' ..= b'9') => command.push(c as char),
0x7f => drop(command.pop()),
0x0d => break,
_ => (),
}
}
serial::write_all(&UsbSerial, b"\r\n").unwrap();
We then parse the command (described later). If the command is invalid, we print a message and continue to the next loop iteration.
// Parse the command.
let command = match Command::parse(&command) {
Some(x) => x,
None => {
writeln(b"Failed: InvalidCommand");
continue;
}
};
And we finally process the command (described later). If processing failed, we print a message with the error. Regardless of error, this is the end of the loop (and thus the main function) and we continue to the next iteration.
// Process the command.
if let Err(error) = command.process() {
writeln(format!("Failed: {error:?}").as_bytes());
}
To ease parsing and processing, we define a straightforward type for commands.
enum Command<'a> {
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) -> Option<Self> {
Some(match *input.split_whitespace().collect::<Vec<_>>().as_slice() {
["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)? },
_ => return None,
})
}
The process function is a Command
method which may return a store error.
fn process(&self) -> Result<(), Error> {
For insert commands, we simply forward to the store::insert()
function (resp.
store::fragment::insert()
for fragmented entries) which maps a key (resp. a
range of keys) to a value. If the key (resp. range of keys) was already mapped,
it is overwritten. A key must be a number smaller than 4096. A range of keys
must be non-empty. A value must be a slice of at most 1023 bytes (resp. 1023
bytes times the number of fragments).
match self {
Command::Insert { key, value } => insert(key, value.as_bytes()),
Remove commands are also straightforward. We use store::remove()
which maps a
key to nothing. It's not an error if the key wasn't mapped before.
Command::Remove { key } => remove(key),
Finally, find commands are implemented using store::find()
which takes a key
and return the mapped value if any.
Command::Find { key } => {
match find(key)? {
We print a message if no value was found.
None => writeln(b"Not found."),
Otherwise, we try to convert the byte slice to a string slice. This should succeed for values that were inserted by this applet since we only accept alphanumeric characters. In that case, we simply print the value.
Some(value) => match core::str::from_utf8(&value) {
Ok(value) => writeln(format!("Found: {value}").as_bytes()),
However, because the store is persistent and keys are not yet partitioned by applets, we could read the values written by a previous applet for that key. And those values don't need to be valid UTF-8. In those cases, we print the value as a byte slice.
Err(_) => writeln(format!("Found (not UTF-8): {value:02x?}").as_bytes()),
The final code looks like this:
#![no_std] wasefire::applet!(); use alloc::boxed::Box; use alloc::format; use alloc::string::String; use alloc::vec::Vec; use core::num::ParseIntError; use core::ops::Range; use core::str::FromStr; use wasefire::usb::serial::UsbSerial; fn main() { writeln(b"Usage: insert <key>[..<key>] <value>"); writeln(b"Usage: find <key>[..<key>]"); writeln(b"Usage: remove <key>[..<key>]"); loop { // Read the command. let mut command = String::new(); loop { serial::write_all(&UsbSerial, format!("\r\x1b[K> {command}").as_bytes()).unwrap(); match serial::read_byte(&UsbSerial).unwrap() { c @ (b' ' | b'.' | b'a' ..= b'z' | b'0' ..= b'9') => command.push(c as char), 0x7f => drop(command.pop()), 0x0d => break, _ => (), } } serial::write_all(&UsbSerial, b"\r\n").unwrap(); // Parse the command. let command = match Command::parse(&command) { Some(x) => x, None => { writeln(b"Failed: InvalidCommand"); continue; } }; // Process the command. if let Err(error) = command.process() { writeln(format!("Failed: {error:?}").as_bytes()); } } } enum Command<'a> { Insert { key: Key, value: &'a str }, Find { key: Key }, Remove { key: Key }, } impl<'a> Command<'a> { fn parse(input: &'a str) -> Option<Self> { Some(match *input.split_whitespace().collect::<Vec<_>>().as_slice() { ["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)? }, _ => return None, }) } fn process(&self) -> Result<(), Error> { match self { Command::Insert { key, value } => insert(key, value.as_bytes()), Command::Find { key } => { match find(key)? { None => writeln(b"Not found."), Some(value) => match core::str::from_utf8(&value) { Ok(value) => writeln(format!("Found: {value}").as_bytes()), Err(_) => writeln(format!("Found (not UTF-8): {value:02x?}").as_bytes()), }, } Ok(()) } Command::Remove { key } => remove(key), } } } 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) -> Option<Self> { let key: Key = key.parse().ok()?; let valid = match &key { Key::Exact(key) => *key < 4096, Key::Range(keys) => !keys.is_empty() && keys.end < 4096, }; if !valid { return None; } Some(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()), } } fn writeln(buf: &[u8]) { serial::write_all(&UsbSerial, buf).unwrap(); serial::write_all(&UsbSerial, b"\r\n").unwrap(); }
Exercises
The
examples/rust/exercises
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 use it by running cargo run
in that
directory. In particular you can get help with cargo run -- help
and run send
specific requests to the applet with cargo run -- register foo
for example.
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.
You can run an applet on the host runner with:
cargo xtask applet rust exercises/part-1 runner host --web
Examples
There are a few existing
applets that
demonstrate simple usage of the prelude and should cover all functionalities in
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.