pamoja_update/lib.rs
1#![cfg_attr(not(test), no_std)]
2
3//! Signed firmware updates for the pamoja SDK.
4//!
5//! A device in a clinic, on a pump, or under a solar panel is expensive to reach
6//! and sometimes impossible. If it cannot be updated in place, every bug in it is
7//! permanent and every fix is a journey. This crate is what makes updating one
8//! safe enough to do remotely:
9//!
10//! - [`Manifest`] - what an update claims about itself: which device it is for,
11//! where it goes, how big it is, what it hashes to, and where it sits in the
12//! release order.
13//! - [`Envelope`] - that manifest next to a signature over it, so a device can
14//! tell an author's release from anyone else's bytes.
15//! - [`ImageVerifier`] - hashes the image as it arrives, so a device with
16//! kilobytes of memory can check a payload of megabytes.
17//! - [`SlotStore`] and [`MemoryStore`] - where images live, with an in-memory
18//! implementation so the whole flow runs in a test with no hardware.
19//! - [`Delegation`] - the anchor's signed statement of which key may sign
20//! releases, so that key can be rotated without visiting the devices.
21//! - [`Updater`] - the rules: verify, then stage, then try, then confirm or fall
22//! back. A transfer cut off by a dead link resumes where it stopped rather than
23//! starting over, which is what makes a large image installable over a slow
24//! radio at all.
25//!
26//! # Why this is safe over an untrusted link
27//!
28//! The signature covers the manifest, and the manifest commits to the image's
29//! digest. Authenticity therefore reaches the image without the carrier being
30//! trusted at all. An update can ride a LoRa mesh, a passing phone, or a USB stick
31//! left in a village, and a device will still only run what its author released.
32//! That matters more here than a secure channel would, because the deployments
33//! this SDK targets frequently have no certificate authority, and often no
34//! internet.
35//!
36//! # Relationship to the SUIT specifications
37//!
38//! The information model is the one RFC 9124 defines, and the architecture and
39//! terminology are RFC 9019's. Both are published standards. The concrete CBOR
40//! serialization, `draft-ietf-suit-manifest`, is not: at the time of writing it
41//! remains an Internet-Draft awaiting publication.
42//!
43//! So this crate implements the settled part and serializes it itself, rather
44//! than pinning the SDK to a wire format that can still change. The encoding is
45//! deliberately kept separate from the model, so a SUIT reader can later produce
46//! the same [`Manifest`] without any of the rules around it moving. This is the
47//! same kind of considered deviation as the hand-written MAVLink dialect, and it
48//! is recorded here rather than left to be discovered.
49//!
50//! # What it defends against
51//!
52//! RFC 9124 enumerates the threats a firmware update mechanism has to answer.
53//! Each one this crate answers is answered by a rule with a test naming it:
54//!
55//! | Threat | Answered by |
56//! | --- | --- |
57//! | `THREAT.IMG.NON_AUTH`, unauthorized firmware | the author's signature, checked before the manifest is parsed |
58//! | `THREAT.IMG.EXPIRED`, a replayed older release | a sequence number that must beat every slot, failed ones included |
59//! | `THREAT.IMG.EXPIRED.OFFLINE`, a stale release aimed at a device that has been out of contact | [`Manifest::expires`], which bounds how long a release stays usable |
60//! | `THREAT.IMG.INCOMPATIBLE`, firmware for another device | authenticated vendor and class identifiers |
61//! | `THREAT.IMG.FORMAT`, a misread payload type | the payload format sits inside the signed body |
62//!
63//! Two it does not answer. `THREAT.IMG.DISCLOSURE`, an attacker reading the
64//! firmware to hunt for flaws, wants payload encryption. `THREAT.UPD.WRONG_PRECURSOR`
65//! only arises for differential updates, which this crate does not do.
66//!
67//! # Known limits
68//!
69//! **A retired key stays trusted until the device hears otherwise.** Rotation
70//! takes effect when a device adopts the new delegation, and a device that has
71//! been out of contact since a key was compromised still honors that key until
72//! it is reached. There is no way to revoke faster than you can deliver, which
73//! RFC 9124 acknowledges by leaving revocation outside the manifest format.
74//! Setting an expiry on a delegation bounds the exposure for devices that have a
75//! clock.
76//!
77//! **Delegation is one level deep.** The anchor appoints a release key, and the
78//! chain stops there. RFC 9124 allows longer chains for delegated authority
79//! between several parties; that is not implemented, and a release key cannot
80//! appoint a successor.
81//!
82//! **The sequence number is only as trustworthy as the slot records.** It is
83//! derived from what [`SlotStore`] reports, so an implementation that loses or
84//! exposes those records weakens rollback protection. Hardware that can keep a
85//! monotonic counter should be used where it exists.
86//!
87//! **It does not fetch, and it does not write to flash.** There is no transport
88//! and no driver: the image arrives however the caller arranges, and
89//! [`SlotStore`] is the seam to real storage.
90//!
91//! Also absent: attestation and secure boot, delta updates, encrypted payloads,
92//! multi-payload dependency manifests, and the optional RFC 9124 elements for
93//! multi-component devices, payload URIs, and execute-in-place metadata.
94//!
95//! # Resuming an interrupted transfer
96//!
97//! A slow radio can spend half an hour on one image, so a link that drops near the
98//! end must not mean starting again. Progress is recorded as it is made, and
99//! [`Updater::resume_at`] continues from there when the slot already holds part of
100//! exactly the same image. Anything else starts over, because two images spliced
101//! together are neither.
102//!
103//! Resuming does not make the earlier bytes trusted. A hash cannot be carried
104//! across a reset, so it is rebuilt by reading back what the slot holds, and the
105//! whole image is still settled by the digest check at the end. A resumed transfer
106//! that completes with the wrong bytes fails exactly as a fresh one would.
107//!
108//! How often progress is recorded is the caller's to choose through its chunk
109//! size: larger chunks mean fewer writes and less flash wear, but more to redo
110//! after a reset.
111//!
112//! Code that takes each piece in a call of its own, rather than in one loop over
113//! a [`Staging`], [`detach`](Staging::detach)es the transfer between pieces and
114//! continues with [`Updater::resume_from`]. The hash of what has arrived travels
115//! with the [`Transfer`], so nothing is read back unless the slot was opened for
116//! something else in between.
117//!
118//! # Who may sign
119//!
120//! A device anchors its trust in one key. That anchor can sign releases itself,
121//! which is the simple arrangement, or it can sign a [`Delegation`] naming a
122//! separate release key and then stay somewhere hard to reach.
123//!
124//! The second is worth the extra step. The key that signs releases has to be
125//! available every time you cut one, and availability is what eventually gets a
126//! key stolen; an anchor that only comes out to authorize a rotation can live in a
127//! safe. Rotating means issuing a delegation with a higher epoch, which retires
128//! the previous key rather than adding to it.
129//!
130//! # How it boots
131//!
132//! An image is run from whichever slot holds it, and slots are never swapped.
133//! That is the model MCUboot calls direct-XIP, chosen because a swap can be
134//! interrupted halfway and then has to be recovered; here there is nothing to
135//! recover, because nothing moves.
136//!
137//! # Examples
138//!
139//! An update is released, carried to a device, tried, and confirmed:
140//!
141//! ```
142//! use pamoja_security::DeviceIdentity;
143//! use pamoja_update::{
144//! image_digest, Boot, Device, Manifest, MemoryStore, PayloadFormat, Updater,
145//! ENVELOPE_MAX, STRUCTURE_VERSION,
146//! };
147//!
148//! // Sixteen bytes each that a vendor assigns itself, naming who builds the
149//! // firmware and which kind of device it runs on.
150//! const VENDOR: [u8; 16] = [10; 16];
151//! const WATER_PUMP: [u8; 16] = [12; 16];
152//!
153//! let author = DeviceIdentity::from_seed(&[1u8; 32]);
154//! let image = b"version two of the firmware";
155//!
156//! let manifest = Manifest {
157//! structure_version: STRUCTURE_VERSION,
158//! sequence: 2,
159//! vendor_id: VENDOR,
160//! class_id: WATER_PUMP,
161//! format: PayloadFormat::Raw,
162//! storage: 1,
163//! digest: image_digest(image),
164//! size: image.len() as u32,
165//! expires: 0,
166//! };
167//! let mut envelope = [0u8; ENVELOPE_MAX];
168//! let written = manifest.sign(&author, &mut envelope).unwrap();
169//!
170//! // The device trusts one author and knows what it is.
171//! let device = Device {
172//! vendor_id: VENDOR,
173//! class_id: WATER_PUMP,
174//! anchor: author.public(),
175//! };
176//! let mut updater = Updater::new(device, MemoryStore::new(2, 4096));
177//! updater.provision(0, 1).unwrap(); // the image it shipped with
178//!
179//! updater.stage(&envelope[..written], image).unwrap();
180//! assert_eq!(updater.on_boot().unwrap(), Boot::Trying(1));
181//! assert_eq!(updater.confirm().unwrap(), 1);
182//!
183//! // The next boot simply runs it.
184//! assert_eq!(updater.on_boot().unwrap(), Boot::Confirmed(1));
185//! ```
186
187extern crate alloc;
188
189pub mod block;
190mod cbor;
191mod error;
192mod manifest;
193mod slots;
194mod trust;
195mod update;
196mod verify;
197
198pub use error::{Refusal, Result};
199pub use manifest::{
200 Envelope, Manifest, PayloadFormat, DIGEST_LEN, ENVELOPE_MAX, ID_LEN, MANIFEST_MAX,
201 STRUCTURE_VERSION,
202};
203pub use slots::{MemoryStore, SlotRecord, SlotState, SlotStore};
204pub use trust::{Delegation, DELEGATION_MAX};
205pub use update::{Boot, Device, Staging, Transfer, Updater};
206pub use verify::{image_digest, ImageVerifier, Verified};