aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGerhard de Clercq <[email protected]>2025-04-15 20:16:09 +0200
committerGerhard de Clercq <[email protected]>2025-05-23 12:40:19 +0200
commit68a45490fc1675f2171131ccbf01f690c4123f01 (patch)
tree42606d9fa51bf50e6d7d84623123616600a70cce
parentf7405493c184ce453ac3f7ba97f7f2689f978194 (diff)
[embassy-usb-dfu] support ed25519 verification
This commit adds the ability to verify that USB DFU updates are correctly signed using ed25519. This required adding support to embassy-boot for reading from the DFU partition.
-rwxr-xr-xci.sh1
-rw-r--r--embassy-boot/src/firmware_updater/asynch.rs11
-rw-r--r--embassy-boot/src/firmware_updater/blocking.rs11
-rw-r--r--embassy-usb-dfu/Cargo.toml5
-rw-r--r--embassy-usb-dfu/README.md6
-rw-r--r--embassy-usb-dfu/src/dfu.rs33
-rw-r--r--examples/boot/application/stm32wb-dfu/memory.x8
-rw-r--r--examples/boot/application/stm32wb-dfu/secrets/key.sec2
-rw-r--r--examples/boot/bootloader/stm32wb-dfu/Cargo.toml1
-rw-r--r--examples/boot/bootloader/stm32wb-dfu/README.md26
-rw-r--r--examples/boot/bootloader/stm32wb-dfu/memory.x8
-rw-r--r--examples/boot/bootloader/stm32wb-dfu/secrets/key.pub.short1
-rw-r--r--examples/boot/bootloader/stm32wb-dfu/src/main.rs12
13 files changed, 114 insertions, 11 deletions
diff --git a/ci.sh b/ci.sh
index 2be84ef6b..dce0d7b13 100755
--- a/ci.sh
+++ b/ci.sh
@@ -282,6 +282,7 @@ cargo batch \
282 --- build --release --manifest-path examples/boot/bootloader/rp/Cargo.toml --target thumbv6m-none-eabi \ 282 --- build --release --manifest-path examples/boot/bootloader/rp/Cargo.toml --target thumbv6m-none-eabi \
283 --- build --release --manifest-path examples/boot/bootloader/stm32/Cargo.toml --target thumbv7em-none-eabi --features embassy-stm32/stm32l496zg \ 283 --- build --release --manifest-path examples/boot/bootloader/stm32/Cargo.toml --target thumbv7em-none-eabi --features embassy-stm32/stm32l496zg \
284 --- build --release --manifest-path examples/boot/bootloader/stm32wb-dfu/Cargo.toml --target thumbv7em-none-eabi --features embassy-stm32/stm32wb55rg \ 284 --- build --release --manifest-path examples/boot/bootloader/stm32wb-dfu/Cargo.toml --target thumbv7em-none-eabi --features embassy-stm32/stm32wb55rg \
285 --- build --release --manifest-path examples/boot/bootloader/stm32wb-dfu/Cargo.toml --target thumbv7em-none-eabi --features embassy-stm32/stm32wb55rg,verify \
285 --- build --release --manifest-path examples/boot/bootloader/stm32-dual-bank/Cargo.toml --target thumbv7em-none-eabi --features embassy-stm32/stm32h743zi \ 286 --- build --release --manifest-path examples/boot/bootloader/stm32-dual-bank/Cargo.toml --target thumbv7em-none-eabi --features embassy-stm32/stm32h743zi \
286 --- build --release --manifest-path examples/wasm/Cargo.toml --target wasm32-unknown-unknown --artifact-dir out/examples/wasm \ 287 --- build --release --manifest-path examples/wasm/Cargo.toml --target wasm32-unknown-unknown --artifact-dir out/examples/wasm \
287 --- build --release --manifest-path tests/stm32/Cargo.toml --target thumbv7m-none-eabi --features stm32f103c8 --artifact-dir out/tests/stm32f103c8 \ 288 --- build --release --manifest-path tests/stm32/Cargo.toml --target thumbv7m-none-eabi --features stm32f103c8 --artifact-dir out/tests/stm32f103c8 \
diff --git a/embassy-boot/src/firmware_updater/asynch.rs b/embassy-boot/src/firmware_updater/asynch.rs
index 0dc09e18d..66e311e38 100644
--- a/embassy-boot/src/firmware_updater/asynch.rs
+++ b/embassy-boot/src/firmware_updater/asynch.rs
@@ -161,6 +161,17 @@ impl<'d, DFU: NorFlash, STATE: NorFlash> FirmwareUpdater<'d, DFU, STATE> {
161 Ok(()) 161 Ok(())
162 } 162 }
163 163
164 /// Read a slice of data from the DFU storage peripheral, starting the read
165 /// operation at the given address offset, and reading `buf.len()` bytes.
166 ///
167 /// # Errors
168 ///
169 /// Returns an error if the arguments are not aligned or out of bounds.
170 pub async fn read_dfu(&mut self, offset: u32, buf: &mut [u8]) -> Result<(), FirmwareUpdaterError> {
171 self.dfu.read(offset, buf).await?;
172 Ok(())
173 }
174
164 /// Mark to trigger firmware swap on next boot. 175 /// Mark to trigger firmware swap on next boot.
165 #[cfg(not(feature = "_verify"))] 176 #[cfg(not(feature = "_verify"))]
166 pub async fn mark_updated(&mut self) -> Result<(), FirmwareUpdaterError> { 177 pub async fn mark_updated(&mut self) -> Result<(), FirmwareUpdaterError> {
diff --git a/embassy-boot/src/firmware_updater/blocking.rs b/embassy-boot/src/firmware_updater/blocking.rs
index 08062b0d0..0fedac1ea 100644
--- a/embassy-boot/src/firmware_updater/blocking.rs
+++ b/embassy-boot/src/firmware_updater/blocking.rs
@@ -196,6 +196,17 @@ impl<'d, DFU: NorFlash, STATE: NorFlash> BlockingFirmwareUpdater<'d, DFU, STATE>
196 Ok(()) 196 Ok(())
197 } 197 }
198 198
199 /// Read a slice of data from the DFU storage peripheral, starting the read
200 /// operation at the given address offset, and reading `buf.len()` bytes.
201 ///
202 /// # Errors
203 ///
204 /// Returns an error if the arguments are not aligned or out of bounds.
205 pub fn read_dfu(&mut self, offset: u32, buf: &mut [u8]) -> Result<(), FirmwareUpdaterError> {
206 self.dfu.read(offset, buf)?;
207 Ok(())
208 }
209
199 /// Mark to trigger firmware swap on next boot. 210 /// Mark to trigger firmware swap on next boot.
200 #[cfg(not(feature = "_verify"))] 211 #[cfg(not(feature = "_verify"))]
201 pub fn mark_updated(&mut self) -> Result<(), FirmwareUpdaterError> { 212 pub fn mark_updated(&mut self) -> Result<(), FirmwareUpdaterError> {
diff --git a/embassy-usb-dfu/Cargo.toml b/embassy-usb-dfu/Cargo.toml
index ca108c1a2..8b2467bca 100644
--- a/embassy-usb-dfu/Cargo.toml
+++ b/embassy-usb-dfu/Cargo.toml
@@ -43,3 +43,8 @@ esp32c3-hal = { version = "0.13.0", optional = true, default-features = false }
43dfu = [] 43dfu = []
44application = [] 44application = []
45defmt = ["dep:defmt", "embassy-boot/defmt", "embassy-usb/defmt"] 45defmt = ["dep:defmt", "embassy-boot/defmt", "embassy-usb/defmt"]
46ed25519-dalek = ["embassy-boot/ed25519-dalek", "_verify"]
47ed25519-salty = ["embassy-boot/ed25519-salty", "_verify"]
48
49# Internal features
50_verify = []
diff --git a/embassy-usb-dfu/README.md b/embassy-usb-dfu/README.md
index bdd5b033a..a81b98279 100644
--- a/embassy-usb-dfu/README.md
+++ b/embassy-usb-dfu/README.md
@@ -4,3 +4,9 @@ An implementation of the USB DFU 1.1 protocol using embassy-boot. It has 2 compo
4 4
5* DFU protocol mode, enabled by the `dfu` feature. This mode corresponds to the transfer phase DFU protocol described by the USB IF. It supports DFU_DNLOAD requests if marked by the user, and will automatically reset the chip once a DFU transaction has been completed. It also responds to DFU_GETSTATUS, DFU_GETSTATE, DFU_ABORT, and DFU_CLRSTATUS with no user intervention. 5* DFU protocol mode, enabled by the `dfu` feature. This mode corresponds to the transfer phase DFU protocol described by the USB IF. It supports DFU_DNLOAD requests if marked by the user, and will automatically reset the chip once a DFU transaction has been completed. It also responds to DFU_GETSTATUS, DFU_GETSTATE, DFU_ABORT, and DFU_CLRSTATUS with no user intervention.
6* DFU runtime mode, enabled by the `application feature`. This mode allows users to expose a DFU interface on their USB device, informing the host of the capability to DFU over USB, and allowing the host to reset the device into its bootloader to complete a DFU operation. Supports DFU_GETSTATUS and DFU_DETACH. When detach/reset is seen by the device as described by the standard, will write a new DFU magic number into the bootloader state in flash, and reset the system. 6* DFU runtime mode, enabled by the `application feature`. This mode allows users to expose a DFU interface on their USB device, informing the host of the capability to DFU over USB, and allowing the host to reset the device into its bootloader to complete a DFU operation. Supports DFU_GETSTATUS and DFU_DETACH. When detach/reset is seen by the device as described by the standard, will write a new DFU magic number into the bootloader state in flash, and reset the system.
7
8## Verification
9
10Embassy-boot provides functionality to verify that an update binary has been correctly signed using ed25519 as described in https://embassy.dev/book/#_verification. Even though the linked procedure describes the signature being concatenated to the end of the update binary, embassy-boot does not force this and is flexible in terms of how the signature for a binary is distributed. The current implementation in embassy-usb-dfu does however assume that the signature is 64 bytes long and concatenated to the end of the update binary since this is the simplest way to make it work with the usb-dfu mechanism. I.e. embassy-usb-dfu does not currently offer the same flexibility as embassy-boot.
11
12To enable verification, you need to enable either the `ed25519-dalek` or the `ed25519-salty` feature with `ed25519-salty` being recommended.
diff --git a/embassy-usb-dfu/src/dfu.rs b/embassy-usb-dfu/src/dfu.rs
index 0f39d906b..9a2f125fb 100644
--- a/embassy-usb-dfu/src/dfu.rs
+++ b/embassy-usb-dfu/src/dfu.rs
@@ -19,11 +19,19 @@ pub struct Control<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_S
19 offset: usize, 19 offset: usize,
20 buf: AlignedBuffer<BLOCK_SIZE>, 20 buf: AlignedBuffer<BLOCK_SIZE>,
21 reset: RST, 21 reset: RST,
22
23 #[cfg(feature = "_verify")]
24 public_key: &'static [u8; 32],
22} 25}
23 26
24impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Control<'d, DFU, STATE, RST, BLOCK_SIZE> { 27impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Control<'d, DFU, STATE, RST, BLOCK_SIZE> {
25 /// Create a new DFU instance to handle DFU transfers. 28 /// Create a new DFU instance to handle DFU transfers.
26 pub fn new(updater: BlockingFirmwareUpdater<'d, DFU, STATE>, attrs: DfuAttributes, reset: RST) -> Self { 29 pub fn new(
30 updater: BlockingFirmwareUpdater<'d, DFU, STATE>,
31 attrs: DfuAttributes,
32 reset: RST,
33 #[cfg(feature = "_verify")] public_key: &'static [u8; 32],
34 ) -> Self {
27 Self { 35 Self {
28 updater, 36 updater,
29 attrs, 37 attrs,
@@ -32,6 +40,9 @@ impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Co
32 offset: 0, 40 offset: 0,
33 buf: AlignedBuffer([0; BLOCK_SIZE]), 41 buf: AlignedBuffer([0; BLOCK_SIZE]),
34 reset, 42 reset,
43
44 #[cfg(feature = "_verify")]
45 public_key,
35 } 46 }
36 } 47 }
37 48
@@ -102,7 +113,23 @@ impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Ha
102 if final_transfer { 113 if final_transfer {
103 debug!("Receiving final transfer"); 114 debug!("Receiving final transfer");
104 115
105 match self.updater.mark_updated() { 116 #[cfg(feature = "_verify")]
117 let update_res: Result<(), FirmwareUpdaterError> = {
118 const SIGNATURE_LEN: usize = 64;
119
120 let mut signature = [0; SIGNATURE_LEN];
121 let update_len = (self.offset - SIGNATURE_LEN) as u32;
122
123 self.updater.read_dfu(update_len, &mut signature).and_then(|_| {
124 self.updater
125 .verify_and_mark_updated(self.public_key, &signature, update_len)
126 })
127 };
128
129 #[cfg(not(feature = "_verify"))]
130 let update_res = self.updater.mark_updated();
131
132 match update_res {
106 Ok(_) => { 133 Ok(_) => {
107 self.status = Status::Ok; 134 self.status = Status::Ok;
108 self.state = State::ManifestSync; 135 self.state = State::ManifestSync;
@@ -168,7 +195,7 @@ impl<'d, DFU: NorFlash, STATE: NorFlash, RST: Reset, const BLOCK_SIZE: usize> Ha
168 Some(InResponse::Accepted(&buf[0..1])) 195 Some(InResponse::Accepted(&buf[0..1]))
169 } 196 }
170 Ok(Request::Upload) if self.attrs.contains(DfuAttributes::CAN_UPLOAD) => { 197 Ok(Request::Upload) if self.attrs.contains(DfuAttributes::CAN_UPLOAD) => {
171 //TODO: FirmwareUpdater does not provide a way of reading the active partition, can't upload. 198 //TODO: FirmwareUpdater provides a way of reading the active partition so we could in theory add functionality to upload firmware.
172 Some(InResponse::Rejected) 199 Some(InResponse::Rejected)
173 } 200 }
174 _ => None, 201 _ => None,
diff --git a/examples/boot/application/stm32wb-dfu/memory.x b/examples/boot/application/stm32wb-dfu/memory.x
index ff1b800d2..f1e6b053c 100644
--- a/examples/boot/application/stm32wb-dfu/memory.x
+++ b/examples/boot/application/stm32wb-dfu/memory.x
@@ -1,10 +1,10 @@
1MEMORY 1MEMORY
2{ 2{
3 /* NOTE 1 K = 1 KiBi = 1024 bytes */ 3 /* NOTE 1 K = 1 KiBi = 1024 bytes */
4 BOOTLOADER : ORIGIN = 0x08000000, LENGTH = 24K 4 BOOTLOADER : ORIGIN = 0x08000000, LENGTH = 48K
5 BOOTLOADER_STATE : ORIGIN = 0x08006000, LENGTH = 4K 5 BOOTLOADER_STATE : ORIGIN = 0x0800C000, LENGTH = 4K
6 FLASH : ORIGIN = 0x08008000, LENGTH = 128K 6 FLASH : ORIGIN = 0x0800D000, LENGTH = 120K
7 DFU : ORIGIN = 0x08028000, LENGTH = 132K 7 DFU : ORIGIN = 0x0802B000, LENGTH = 120K
8 RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K 8 RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K
9} 9}
10 10
diff --git a/examples/boot/application/stm32wb-dfu/secrets/key.sec b/examples/boot/application/stm32wb-dfu/secrets/key.sec
new file mode 100644
index 000000000..52e7f125b
--- /dev/null
+++ b/examples/boot/application/stm32wb-dfu/secrets/key.sec
@@ -0,0 +1,2 @@
1untrusted comment: signify secret key
2RWRCSwAAAAATdHQF3B4jEIoNZrjADRp2LbjJjNdNNzKwTCe4IB6mDNq96pe53nbNxwbdCc/T4hrz7W+Kx1MwrZ0Yz5xebSK5Z0Kh/3Cdf039U5f+eoTDS2fIGbohyUbrtwKzjyE0qXI=
diff --git a/examples/boot/bootloader/stm32wb-dfu/Cargo.toml b/examples/boot/bootloader/stm32wb-dfu/Cargo.toml
index 738afb6ec..0bb93b12e 100644
--- a/examples/boot/bootloader/stm32wb-dfu/Cargo.toml
+++ b/examples/boot/bootloader/stm32wb-dfu/Cargo.toml
@@ -30,6 +30,7 @@ defmt = [
30 "embassy-usb/defmt", 30 "embassy-usb/defmt",
31 "embassy-usb-dfu/defmt" 31 "embassy-usb-dfu/defmt"
32] 32]
33verify = ["embassy-usb-dfu/ed25519-salty"]
33 34
34[profile.dev] 35[profile.dev]
35debug = 2 36debug = 2
diff --git a/examples/boot/bootloader/stm32wb-dfu/README.md b/examples/boot/bootloader/stm32wb-dfu/README.md
index 3c5f268a0..99a7002c4 100644
--- a/examples/boot/bootloader/stm32wb-dfu/README.md
+++ b/examples/boot/bootloader/stm32wb-dfu/README.md
@@ -28,6 +28,32 @@ cargo objcopy --release -- -O binary fw.bin
28dfu-util -d c0de:cafe -w -D fw.bin 28dfu-util -d c0de:cafe -w -D fw.bin
29``` 29```
30 30
31### 3. Sign Updates Before Flashing (Optional)
32
33Currently, embassy-usb-dfu only supports a limited implementation of the generic support for ed25519-based update verfication in embassy-boot. This implementation assumes that a signature is simply concatenated to the end of an update binary. For more details, please see https://embassy.dev/book/#_verification and/or refer to the documentation for embassy-boot-dfu.
34
35To sign (and then verify) application updates, you will first need to generate a key pair:
36
37```
38signify-openbsd -G -n -p secrets/key.pub -s secrets/key.sec
39tail -n1 secrets/key.pub | base64 -d -i - | dd ibs=10 skip=1 > secrets/key.pub.short
40```
41
42Then you will need to sign all you binaries with the private key:
43
44```
45cargo objcopy --release -- -O binary fw.bin
46shasum -a 512 -b fw.bin | head -c128 | xxd -p -r > target/fw-hash.txt
47signify-openbsd -S -s secrets/key.sec -m target/fw-hash.txt -x target/fw-hash.sig
48cp fw.bin fw-signed.bin
49tail -n1 target/fw-hash.sig | base64 -d -i - | dd ibs=10 skip=1 >> fw-signed.bin
50dfu-util -d c0de:cafe -w -D fw-signed.bin
51```
52
53Finally, as shown in this example with the `verify` feature flag enabled, you then need to embed the public key into your bootloader so that it can verify update signatures.
54
55N.B. Please note that the exact steps above are NOT a good example of how to manage your keys securely. In a production environment, you should take great care to ensure that (at least the private key) is protected and not leaked into your version control system.
56
31## Troubleshooting 57## Troubleshooting
32 58
33- Make sure your device is in DFU mode before flashing 59- Make sure your device is in DFU mode before flashing
diff --git a/examples/boot/bootloader/stm32wb-dfu/memory.x b/examples/boot/bootloader/stm32wb-dfu/memory.x
index 858062631..77c4d2ee2 100644
--- a/examples/boot/bootloader/stm32wb-dfu/memory.x
+++ b/examples/boot/bootloader/stm32wb-dfu/memory.x
@@ -1,10 +1,10 @@
1MEMORY 1MEMORY
2{ 2{
3 /* NOTE 1 K = 1 KiBi = 1024 bytes */ 3 /* NOTE 1 K = 1 KiBi = 1024 bytes */
4 FLASH : ORIGIN = 0x08000000, LENGTH = 24K 4 FLASH : ORIGIN = 0x08000000, LENGTH = 48K
5 BOOTLOADER_STATE : ORIGIN = 0x08006000, LENGTH = 4K 5 BOOTLOADER_STATE : ORIGIN = 0x0800C000, LENGTH = 4K
6 ACTIVE : ORIGIN = 0x08008000, LENGTH = 128K 6 ACTIVE : ORIGIN = 0x0800D000, LENGTH = 120K
7 DFU : ORIGIN = 0x08028000, LENGTH = 132K 7 DFU : ORIGIN = 0x0802B000, LENGTH = 120K
8 RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 16K 8 RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 16K
9} 9}
10 10
diff --git a/examples/boot/bootloader/stm32wb-dfu/secrets/key.pub.short b/examples/boot/bootloader/stm32wb-dfu/secrets/key.pub.short
new file mode 100644
index 000000000..7a4de8585
--- /dev/null
+++ b/examples/boot/bootloader/stm32wb-dfu/secrets/key.pub.short
@@ -0,0 +1 @@
gB��p�M�S��z��Kg��!�F���!4�r \ No newline at end of file
diff --git a/examples/boot/bootloader/stm32wb-dfu/src/main.rs b/examples/boot/bootloader/stm32wb-dfu/src/main.rs
index 0b643079f..107f243fd 100644
--- a/examples/boot/bootloader/stm32wb-dfu/src/main.rs
+++ b/examples/boot/bootloader/stm32wb-dfu/src/main.rs
@@ -25,6 +25,12 @@ bind_interrupts!(struct Irqs {
25// N.B. update to a custom GUID for your own device! 25// N.B. update to a custom GUID for your own device!
26const DEVICE_INTERFACE_GUIDS: &[&str] = &["{EAA9A5DC-30BA-44BC-9232-606CDC875321}"]; 26const DEVICE_INTERFACE_GUIDS: &[&str] = &["{EAA9A5DC-30BA-44BC-9232-606CDC875321}"];
27 27
28// This is a randomly generated example key.
29//
30// N.B. Please replace with your own!
31#[cfg(feature = "verify")]
32static PUBLIC_SIGNING_KEY: &[u8; 32] = include_bytes!("../secrets/key.pub.short");
33
28#[entry] 34#[entry]
29fn main() -> ! { 35fn main() -> ! {
30 let mut config = embassy_stm32::Config::default(); 36 let mut config = embassy_stm32::Config::default();
@@ -57,7 +63,13 @@ fn main() -> ! {
57 let mut config_descriptor = [0; 256]; 63 let mut config_descriptor = [0; 256];
58 let mut bos_descriptor = [0; 256]; 64 let mut bos_descriptor = [0; 256];
59 let mut control_buf = [0; 4096]; 65 let mut control_buf = [0; 4096];
66
67 #[cfg(not(feature = "verify"))]
60 let mut state = Control::new(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate); 68 let mut state = Control::new(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate);
69
70 #[cfg(feature = "verify")]
71 let mut state = Control::new(updater, DfuAttributes::CAN_DOWNLOAD, ResetImmediate, PUBLIC_SIGNING_KEY);
72
61 let mut builder = Builder::new( 73 let mut builder = Builder::new(
62 driver, 74 driver,
63 config, 75 config,