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 same kind of considered deviation as the hand-written
47//! MAVLink dialect, and it is recorded here rather than left to be discovered.
48//!
49//! # What it defends against
50//!
51//! RFC 9124 enumerates the threats a firmware update mechanism has to answer.
52//! Each one this crate answers is answered by a rule with a test naming it:
53//!
54//! | Threat | Answered by |
55//! | --- | --- |
56//! | `THREAT.IMG.NON_AUTH`, unauthorised firmware | the author's signature, checked before the manifest is parsed |
57//! | `THREAT.IMG.EXPIRED`, a replayed older release | a sequence number that must beat every slot, failed ones included |
58//! | `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 |
59//! | `THREAT.IMG.INCOMPATIBLE`, firmware for another device | authenticated vendor and class identifiers |
60//! | `THREAT.IMG.FORMAT`, a misread payload type | the payload format sits inside the signed body |
61//!
62//! Two it does not answer. `THREAT.IMG.DISCLOSURE`, an attacker reading the
63//! firmware to hunt for flaws, wants payload encryption. `THREAT.UPD.WRONG_PRECURSOR`
64//! only arises for differential updates, which this crate does not do.
65//!
66//! # Known limits
67//!
68//! **A retired key stays trusted until the device hears otherwise.** Rotation
69//! takes effect when a device adopts the new delegation, and a device that has
70//! been out of contact since a key was compromised still honours that key until
71//! it is reached. There is no way to revoke faster than you can deliver, which
72//! RFC 9124 acknowledges by leaving revocation outside the manifest format.
73//! Setting an expiry on a delegation bounds the exposure for devices that have a
74//! clock.
75//!
76//! **Delegation is one level deep.** The anchor appoints a release key, and the
77//! chain stops there. RFC 9124 allows longer chains for delegated authority
78//! between several parties; that is not implemented, and a release key cannot
79//! appoint a successor.
80//!
81//! **The sequence number is only as trustworthy as the slot records.** It is
82//! derived from what [`SlotStore`] reports, so an implementation that loses or
83//! exposes those records weakens rollback protection. Hardware that can keep a
84//! monotonic counter should be used where it exists.
85//!
86//! **It does not fetch, and it does not write to flash.** There is no transport
87//! and no driver: the image arrives however the caller arranges, and
88//! [`SlotStore`] is the seam to real storage.
89//!
90//! Also absent: attestation and secure boot, delta updates, encrypted payloads,
91//! multi-payload dependency manifests, and the optional RFC 9124 elements for
92//! multi-component devices, payload URIs, and execute-in-place metadata.
93//!
94//! # Resuming an interrupted transfer
95//!
96//! A slow radio can spend half an hour on one image, so a link that drops near the
97//! end must not mean starting again. Progress is recorded as it is made, and
98//! [`Updater::resume_at`] continues from there when the slot already holds part of
99//! exactly the same image. Anything else starts over, because two images spliced
100//! together are neither.
101//!
102//! Resuming does not make the earlier bytes trusted. A hash cannot be carried
103//! across a reset, so it is rebuilt by reading back what the slot holds, and the
104//! whole image is still settled by the digest check at the end. A resumed transfer
105//! that completes with the wrong bytes fails exactly as a fresh one would.
106//!
107//! How often progress is recorded is the caller's to choose through its chunk
108//! size: larger chunks mean fewer writes and less flash wear, but more to redo
109//! after a reset.
110//!
111//! # Who may sign
112//!
113//! A device anchors its trust in one key. That anchor can sign releases itself,
114//! which is the simple arrangement, or it can sign a [`Delegation`] naming a
115//! separate release key and then stay somewhere hard to reach.
116//!
117//! The second is worth the extra step. The key that signs releases has to be
118//! available every time you cut one, and availability is what eventually gets a
119//! key stolen; an anchor that only comes out to authorise a rotation can live in a
120//! safe. Rotating means issuing a delegation with a higher epoch, which retires
121//! the previous key rather than adding to it.
122//!
123//! # How it boots
124//!
125//! An image is run from whichever slot holds it, and slots are never swapped.
126//! That is the model MCUboot calls direct-XIP, chosen because a swap can be
127//! interrupted halfway and then has to be recovered; here there is nothing to
128//! recover, because nothing moves.
129//!
130//! # Examples
131//!
132//! An update is released, carried to a device, tried, and confirmed:
133//!
134//! ```
135//! use pamoja_security::DeviceIdentity;
136//! use pamoja_update::{
137//! Boot, Device, Manifest, MemoryStore, PayloadFormat, Updater, ENVELOPE_MAX,
138//! STRUCTURE_VERSION,
139//! };
140//! use sha2::{Digest, Sha256};
141//!
142//! let author = DeviceIdentity::from_seed(&[1u8; 32]);
143//! let image = b"version two of the firmware";
144//!
145//! let manifest = Manifest {
146//! structure_version: STRUCTURE_VERSION,
147//! sequence: 2,
148//! vendor_id: [0xab; 16],
149//! class_id: [0xcd; 16],
150//! format: PayloadFormat::Raw,
151//! storage: 1,
152//! digest: Sha256::digest(image).into(),
153//! size: image.len() as u32,
154//! expires: 0,
155//! };
156//! let mut envelope = [0u8; ENVELOPE_MAX];
157//! let written = manifest.sign(&author, &mut envelope).unwrap();
158//!
159//! // The device trusts one author and knows what it is.
160//! let device = Device {
161//! vendor_id: [0xab; 16],
162//! class_id: [0xcd; 16],
163//! anchor: author.public(),
164//! };
165//! let mut updater = Updater::new(device, MemoryStore::new(2, 4096));
166//! updater.provision(0, 1).unwrap(); // the image it shipped with
167//!
168//! updater.stage(&envelope[..written], image).unwrap();
169//! assert_eq!(updater.on_boot().unwrap(), Boot::Trying(1));
170//! assert_eq!(updater.confirm().unwrap(), 1);
171//!
172//! // The next boot simply runs it.
173//! assert_eq!(updater.on_boot().unwrap(), Boot::Confirmed(1));
174//! ```
175
176extern crate alloc;
177
178mod cbor;
179mod error;
180mod manifest;
181mod slots;
182mod trust;
183mod update;
184mod verify;
185
186pub use error::{Refusal, Result};
187pub use manifest::{
188 Envelope, Manifest, PayloadFormat, DIGEST_LEN, ENVELOPE_MAX, ID_LEN, MANIFEST_MAX,
189 STRUCTURE_VERSION,
190};
191pub use slots::{MemoryStore, SlotRecord, SlotState, SlotStore};
192pub use trust::{Delegation, DELEGATION_MAX};
193pub use update::{Boot, Device, Staging, Updater};
194pub use verify::{image_digest, ImageVerifier, Verified};