Skip to main content

Crate offline_protocol_leaf

Crate offline_protocol_leaf 

Source
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.
LeafDevice
A leaf node.

Enums§

LeafError
What can go wrong on a leaf node.
LeafEvent
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 LeafDevice takes.

Type Aliases§

Result
Result type for this crate.
SharedStore
A shared handle to the device’s store.