Expand description
A constrained device that speaks the Offline Protocol as a real peer.
A leaf node is a door lock, a sensor, or a mains-powered relay: something with a radio, a few hundred kilobytes of flash and no operating system. It parses frames, validates addressing, runs RFC 9420 MLS, and holds an end-to-end encrypted conversation with a phone under the same guarantees a phone gets. Same frames, same envelope, same trust gates, no second sealing path and no reduced properties.
That is the decision in
ADR 0021,
and it is affordable because a phone paired with one device is a
two-member group: the ratchet tree is three nodes, per-commit cost is
two elliptic-curve operations, and per-message cost is symmetric only. What
both ends must agree on lives in
offline_protocol_sealed, so the two MLS implementations never disagree
about a byte outside themselves.
§What this crate is
LeafDevice is a frame-level state machine, not a bag of primitives. It
takes an inbound Message and hands back
the frames to send and what happened. The choreography it implements is
security-critical and easy to get subtly wrong: the derive-and-compare gate
at every site that accepts an identity claim, the binding of a Welcome to
the key package this device minted for the peer that sent it (a package
travels unencrypted, so whoever copies one off the air satisfies every
other gate honestly), the roster re-read at every use of the group that
keeps a pair a pair once commits start arriving, the confirmation that has
to be a group-aware decrypt, and the reset sequence that a driven rekey
arrives as.
use offline_protocol_leaf::{shared_store, LeafDevice, MemoryStore};
// A real device implements `LeafStore` over its secure key storage.
let store = shared_store(MemoryStore::new());
let mut device = LeafDevice::open(store, "com.example.lock")?;
// `now` comes from the radio stack, the commissioner, or the pairing
// exchange. It is a parameter because a device has no clock, and an MLS
// implementation that reads one it does not have stamps 1970.
let now = 1_787_314_332;
let peer = device.address().to_string();
let advertisement = device.key_package_frame(&peer, now)?;
assert!(advertisement.content.starts_with("__MLS_KEY_PKG__"));§Four obligations this crate cannot discharge for you
A time source at pairing. Every entry point that needs a clock takes
now_unix_secs. A device that supplies something wrong emits a key package
the peer refuses as expired, and it never pairs at all. Validity is a
freshness bound rather than an authentication mechanism, so a wrong clock
costs availability, not confidentiality.
Real entropy. This crate registers no getrandom backend, on purpose:
doing so would let firmware link and run with randomness this crate
invented, and MLS key generation is exactly as strong as what that symbol
returns. Wire it to the part’s hardware entropy source.
Durable storage. LeafStore must be durable and atomic per entry.
This crate orders every persist before the emit it belongs to, so a store
that lies is the one remaining way to reuse an AEAD nonce after a power
cut.
Authorization. A session proves who a peer is and never that they may
do anything. Every gate in this crate answers the first question, and any
address in radio range can complete a pairing, because producing a key that
derives to its own address costs nothing. So firmware decides when the radio
accepts a new pairing, and firmware decides what a message from a given peer
may actuate, by the address on the event. A lock that opens for whatever
arrives on an established session opens for anyone patient enough to pair
with it. LeafDevice::peers is how firmware audits what a device
accumulated and LeafDevice::unpair is how it removes one.
§One writer
Every operation that advances state takes &mut self, so a device is one
value exclusively held rather than a handle to share between tasks. Two
seals at once would reuse an AEAD nonce without any power cut being
involved; see LeafDevice for that argument and for what a replayed
control frame can still do.
§Bare metal
Builds with --no-default-features for a target with no std, which is
the configuration the CI job gates. The std build is the same code with
its dependencies’ std features on, and is what the unit tests run under.
CI builds and lints it for a Cortex-M33 (thumbv8m.main-none-eabihf), for
RISC-V with the atomic extension (riscv32imac-unknown-none-elf: ESP32-C6
and ESP32-H2), and for the two classes with no compare-and-swap at all
(riscv32imc-unknown-none-elf: ESP32-C3 and ESP32-C2; thumbv6m-none-eabi:
every Cortex-M0). On those last two the firmware must link a
critical-section implementation, because the store handle’s reference count
and mls-rs’s own locks are built on one; see SharedStore.
Compiling is all that is claimed. Nothing here is linked into an image or
run on a board by CI. ESP32-C3 and ESP32-C2 cannot yet be built on esp-hal
at all: mls-rs selects portable-atomic’s critical-section backend on them,
esp-hal selects unsafe-assume-single-core, and portable-atomic refuses the
pair.
Re-exports§
pub use store::LeafStore;pub use store::StoreError;pub use store::MemoryStore;
Modules§
- store
- The storage seam, and the one rule that makes it safe.
Structs§
- Handled
- What a frame produced.
- Leaf
Device - A leaf node.
Enums§
- Leaf
Error - What can go wrong on a leaf node.
- Leaf
Event - Something that happened, for the firmware to act on.
Constants§
- CIPHERSUITE
- The one ciphersuite this protocol uses, and never negotiates.
Functions§
- shared_
store - Wraps a store in the handle a
LeafDevicetakes.
Type Aliases§
- Result
- Result type for this crate.
- Shared
Store - A shared handle to the device’s store.