From e0775fbc8ab1ecc83bce42fe6e11accf481bc9e1 Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Thu, 11 Jan 2024 18:55:59 +0100 Subject: Flatten embassy-boot dir tree --- embassy-boot/src/boot_loader.rs | 411 ++++++++++++++++++++++ embassy-boot/src/digest_adapters/ed25519_dalek.rs | 30 ++ embassy-boot/src/digest_adapters/mod.rs | 5 + embassy-boot/src/digest_adapters/salty.rs | 29 ++ embassy-boot/src/firmware_updater/asynch.rs | 329 +++++++++++++++++ embassy-boot/src/firmware_updater/blocking.rs | 340 ++++++++++++++++++ embassy-boot/src/firmware_updater/mod.rs | 49 +++ embassy-boot/src/fmt.rs | 258 ++++++++++++++ embassy-boot/src/lib.rs | 323 +++++++++++++++++ embassy-boot/src/mem_flash.rs | 170 +++++++++ embassy-boot/src/test_flash/asynch.rs | 64 ++++ embassy-boot/src/test_flash/blocking.rs | 68 ++++ embassy-boot/src/test_flash/mod.rs | 5 + 13 files changed, 2081 insertions(+) create mode 100644 embassy-boot/src/boot_loader.rs create mode 100644 embassy-boot/src/digest_adapters/ed25519_dalek.rs create mode 100644 embassy-boot/src/digest_adapters/mod.rs create mode 100644 embassy-boot/src/digest_adapters/salty.rs create mode 100644 embassy-boot/src/firmware_updater/asynch.rs create mode 100644 embassy-boot/src/firmware_updater/blocking.rs create mode 100644 embassy-boot/src/firmware_updater/mod.rs create mode 100644 embassy-boot/src/fmt.rs create mode 100644 embassy-boot/src/lib.rs create mode 100644 embassy-boot/src/mem_flash.rs create mode 100644 embassy-boot/src/test_flash/asynch.rs create mode 100644 embassy-boot/src/test_flash/blocking.rs create mode 100644 embassy-boot/src/test_flash/mod.rs (limited to 'embassy-boot/src') diff --git a/embassy-boot/src/boot_loader.rs b/embassy-boot/src/boot_loader.rs new file mode 100644 index 000000000..e568001bc --- /dev/null +++ b/embassy-boot/src/boot_loader.rs @@ -0,0 +1,411 @@ +use core::cell::RefCell; + +use embassy_embedded_hal::flash::partition::BlockingPartition; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::Mutex; +use embedded_storage::nor_flash::{NorFlash, NorFlashError, NorFlashErrorKind}; + +use crate::{State, BOOT_MAGIC, DFU_DETACH_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC}; + +/// Errors returned by bootloader +#[derive(PartialEq, Eq, Debug)] +pub enum BootError { + /// Error from flash. + Flash(NorFlashErrorKind), + /// Invalid bootloader magic + BadMagic, +} + +#[cfg(feature = "defmt")] +impl defmt::Format for BootError { + fn format(&self, fmt: defmt::Formatter) { + match self { + BootError::Flash(_) => defmt::write!(fmt, "BootError::Flash(_)"), + BootError::BadMagic => defmt::write!(fmt, "BootError::BadMagic"), + } + } +} + +impl From for BootError +where + E: NorFlashError, +{ + fn from(error: E) -> Self { + BootError::Flash(error.kind()) + } +} + +/// Bootloader flash configuration holding the three flashes used by the bootloader +/// +/// If only a single flash is actually used, then that flash should be partitioned into three partitions before use. +/// The easiest way to do this is to use [`BootLoaderConfig::from_linkerfile_blocking`] which will partition +/// the provided flash according to symbols defined in the linkerfile. +pub struct BootLoaderConfig { + /// Flash type used for the active partition - the partition which will be booted from. + pub active: ACTIVE, + /// Flash type used for the dfu partition - the partition which will be swapped in when requested. + pub dfu: DFU, + /// Flash type used for the state partition. + pub state: STATE, +} + +impl<'a, FLASH: NorFlash> + BootLoaderConfig< + BlockingPartition<'a, NoopRawMutex, FLASH>, + BlockingPartition<'a, NoopRawMutex, FLASH>, + BlockingPartition<'a, NoopRawMutex, FLASH>, + > +{ + /// Create a bootloader config from the flash and address symbols defined in the linkerfile + // #[cfg(target_os = "none")] + pub fn from_linkerfile_blocking(flash: &'a Mutex>) -> Self { + extern "C" { + static __bootloader_state_start: u32; + static __bootloader_state_end: u32; + static __bootloader_active_start: u32; + static __bootloader_active_end: u32; + static __bootloader_dfu_start: u32; + static __bootloader_dfu_end: u32; + } + + let active = unsafe { + let start = &__bootloader_active_start as *const u32 as u32; + let end = &__bootloader_active_end as *const u32 as u32; + trace!("ACTIVE: 0x{:x} - 0x{:x}", start, end); + + BlockingPartition::new(flash, start, end - start) + }; + let dfu = unsafe { + let start = &__bootloader_dfu_start as *const u32 as u32; + let end = &__bootloader_dfu_end as *const u32 as u32; + trace!("DFU: 0x{:x} - 0x{:x}", start, end); + + BlockingPartition::new(flash, start, end - start) + }; + let state = unsafe { + let start = &__bootloader_state_start as *const u32 as u32; + let end = &__bootloader_state_end as *const u32 as u32; + trace!("STATE: 0x{:x} - 0x{:x}", start, end); + + BlockingPartition::new(flash, start, end - start) + }; + + Self { active, dfu, state } + } +} + +/// BootLoader works with any flash implementing embedded_storage. +pub struct BootLoader { + active: ACTIVE, + dfu: DFU, + /// The state partition has the following format: + /// All ranges are in multiples of WRITE_SIZE bytes. + /// | Range | Description | + /// | 0..1 | Magic indicating bootloader state. BOOT_MAGIC means boot, SWAP_MAGIC means swap. | + /// | 1..2 | Progress validity. ERASE_VALUE means valid, !ERASE_VALUE means invalid. | + /// | 2..2 + N | Progress index used while swapping or reverting + state: STATE, +} + +impl BootLoader { + /// Get the page size which is the "unit of operation" within the bootloader. + const PAGE_SIZE: u32 = if ACTIVE::ERASE_SIZE > DFU::ERASE_SIZE { + ACTIVE::ERASE_SIZE as u32 + } else { + DFU::ERASE_SIZE as u32 + }; + + /// Create a new instance of a bootloader with the flash partitions. + /// + /// - All partitions must be aligned with the PAGE_SIZE const generic parameter. + /// - The dfu partition must be at least PAGE_SIZE bigger than the active partition. + pub fn new(config: BootLoaderConfig) -> Self { + Self { + active: config.active, + dfu: config.dfu, + state: config.state, + } + } + + /// Perform necessary boot preparations like swapping images. + /// + /// The DFU partition is assumed to be 1 page bigger than the active partition for the swap + /// algorithm to work correctly. + /// + /// The provided aligned_buf argument must satisfy any alignment requirements + /// given by the partition flashes. All flash operations will use this buffer. + /// + /// ## SWAPPING + /// + /// Assume a flash size of 3 pages for the active partition, and 4 pages for the DFU partition. + /// The swap index contains the copy progress, as to allow continuation of the copy process on + /// power failure. The index counter is represented within 1 or more pages (depending on total + /// flash size), where a page X is considered swapped if index at location (`X + WRITE_SIZE`) + /// contains a zero value. This ensures that index updates can be performed atomically and + /// avoid a situation where the wrong index value is set (page write size is "atomic"). + /// + /// + /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 | + /// |-----------|------------|--------|--------|--------|--------| + /// | Active | 0 | 1 | 2 | 3 | - | + /// | DFU | 0 | 3 | 2 | 1 | X | + /// + /// The algorithm starts by copying 'backwards', and after the first step, the layout is + /// as follows: + /// + /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 | + /// |-----------|------------|--------|--------|--------|--------| + /// | Active | 1 | 1 | 2 | 1 | - | + /// | DFU | 1 | 3 | 2 | 1 | 3 | + /// + /// The next iteration performs the same steps + /// + /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 | + /// |-----------|------------|--------|--------|--------|--------| + /// | Active | 2 | 1 | 2 | 1 | - | + /// | DFU | 2 | 3 | 2 | 2 | 3 | + /// + /// And again until we're done + /// + /// | Partition | Swap Index | Page 0 | Page 1 | Page 3 | Page 4 | + /// |-----------|------------|--------|--------|--------|--------| + /// | Active | 3 | 3 | 2 | 1 | - | + /// | DFU | 3 | 3 | 1 | 2 | 3 | + /// + /// ## REVERTING + /// + /// The reverting algorithm uses the swap index to discover that images were swapped, but that + /// the application failed to mark the boot successful. In this case, the revert algorithm will + /// run. + /// + /// The revert index is located separately from the swap index, to ensure that revert can continue + /// on power failure. + /// + /// The revert algorithm works forwards, by starting copying into the 'unused' DFU page at the start. + /// + /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 | + /// |-----------|--------------|--------|--------|--------|--------| + /// | Active | 3 | 1 | 2 | 1 | - | + /// | DFU | 3 | 3 | 1 | 2 | 3 | + /// + /// + /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 | + /// |-----------|--------------|--------|--------|--------|--------| + /// | Active | 3 | 1 | 2 | 1 | - | + /// | DFU | 3 | 3 | 2 | 2 | 3 | + /// + /// | Partition | Revert Index | Page 0 | Page 1 | Page 3 | Page 4 | + /// |-----------|--------------|--------|--------|--------|--------| + /// | Active | 3 | 1 | 2 | 3 | - | + /// | DFU | 3 | 3 | 2 | 1 | 3 | + /// + pub fn prepare_boot(&mut self, aligned_buf: &mut [u8]) -> Result { + // Ensure we have enough progress pages to store copy progress + assert_eq!(0, Self::PAGE_SIZE % aligned_buf.len() as u32); + assert_eq!(0, Self::PAGE_SIZE % ACTIVE::WRITE_SIZE as u32); + assert_eq!(0, Self::PAGE_SIZE % ACTIVE::ERASE_SIZE as u32); + assert_eq!(0, Self::PAGE_SIZE % DFU::WRITE_SIZE as u32); + assert_eq!(0, Self::PAGE_SIZE % DFU::ERASE_SIZE as u32); + assert!(aligned_buf.len() >= STATE::WRITE_SIZE); + assert_eq!(0, aligned_buf.len() % ACTIVE::WRITE_SIZE); + assert_eq!(0, aligned_buf.len() % DFU::WRITE_SIZE); + + // Ensure our partitions are able to handle boot operations + assert_partitions(&self.active, &self.dfu, &self.state, Self::PAGE_SIZE); + + // Copy contents from partition N to active + let state = self.read_state(aligned_buf)?; + if state == State::Swap { + // + // Check if we already swapped. If we're in the swap state, this means we should revert + // since the app has failed to mark boot as successful + // + if !self.is_swapped(aligned_buf)? { + trace!("Swapping"); + self.swap(aligned_buf)?; + trace!("Swapping done"); + } else { + trace!("Reverting"); + self.revert(aligned_buf)?; + + let state_word = &mut aligned_buf[..STATE::WRITE_SIZE]; + + // Invalidate progress + state_word.fill(!STATE_ERASE_VALUE); + self.state.write(STATE::WRITE_SIZE as u32, state_word)?; + + // Clear magic and progress + self.state.erase(0, self.state.capacity() as u32)?; + + // Set magic + state_word.fill(BOOT_MAGIC); + self.state.write(0, state_word)?; + } + } + Ok(state) + } + + fn is_swapped(&mut self, aligned_buf: &mut [u8]) -> Result { + let page_count = self.active.capacity() / Self::PAGE_SIZE as usize; + let progress = self.current_progress(aligned_buf)?; + + Ok(progress >= page_count * 2) + } + + fn current_progress(&mut self, aligned_buf: &mut [u8]) -> Result { + let write_size = STATE::WRITE_SIZE as u32; + let max_index = ((self.state.capacity() - STATE::WRITE_SIZE) / STATE::WRITE_SIZE) - 2; + let state_word = &mut aligned_buf[..write_size as usize]; + + self.state.read(write_size, state_word)?; + if state_word.iter().any(|&b| b != STATE_ERASE_VALUE) { + // Progress is invalid + return Ok(max_index); + } + + for index in 0..max_index { + self.state.read((2 + index) as u32 * write_size, state_word)?; + + if state_word.iter().any(|&b| b == STATE_ERASE_VALUE) { + return Ok(index); + } + } + Ok(max_index) + } + + fn update_progress(&mut self, progress_index: usize, aligned_buf: &mut [u8]) -> Result<(), BootError> { + let state_word = &mut aligned_buf[..STATE::WRITE_SIZE]; + state_word.fill(!STATE_ERASE_VALUE); + self.state + .write((2 + progress_index) as u32 * STATE::WRITE_SIZE as u32, state_word)?; + Ok(()) + } + + fn copy_page_once_to_active( + &mut self, + progress_index: usize, + from_offset: u32, + to_offset: u32, + aligned_buf: &mut [u8], + ) -> Result<(), BootError> { + if self.current_progress(aligned_buf)? <= progress_index { + let page_size = Self::PAGE_SIZE as u32; + + self.active.erase(to_offset, to_offset + page_size)?; + + for offset_in_page in (0..page_size).step_by(aligned_buf.len()) { + self.dfu.read(from_offset + offset_in_page as u32, aligned_buf)?; + self.active.write(to_offset + offset_in_page as u32, aligned_buf)?; + } + + self.update_progress(progress_index, aligned_buf)?; + } + Ok(()) + } + + fn copy_page_once_to_dfu( + &mut self, + progress_index: usize, + from_offset: u32, + to_offset: u32, + aligned_buf: &mut [u8], + ) -> Result<(), BootError> { + if self.current_progress(aligned_buf)? <= progress_index { + let page_size = Self::PAGE_SIZE as u32; + + self.dfu.erase(to_offset as u32, to_offset + page_size)?; + + for offset_in_page in (0..page_size).step_by(aligned_buf.len()) { + self.active.read(from_offset + offset_in_page as u32, aligned_buf)?; + self.dfu.write(to_offset + offset_in_page as u32, aligned_buf)?; + } + + self.update_progress(progress_index, aligned_buf)?; + } + Ok(()) + } + + fn swap(&mut self, aligned_buf: &mut [u8]) -> Result<(), BootError> { + let page_count = self.active.capacity() as u32 / Self::PAGE_SIZE; + for page_num in 0..page_count { + let progress_index = (page_num * 2) as usize; + + // Copy active page to the 'next' DFU page. + let active_from_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE; + let dfu_to_offset = (page_count - page_num) * Self::PAGE_SIZE; + //trace!("Copy active {} to dfu {}", active_from_offset, dfu_to_offset); + self.copy_page_once_to_dfu(progress_index, active_from_offset, dfu_to_offset, aligned_buf)?; + + // Copy DFU page to the active page + let active_to_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE; + let dfu_from_offset = (page_count - 1 - page_num) * Self::PAGE_SIZE; + //trace!("Copy dfy {} to active {}", dfu_from_offset, active_to_offset); + self.copy_page_once_to_active(progress_index + 1, dfu_from_offset, active_to_offset, aligned_buf)?; + } + + Ok(()) + } + + fn revert(&mut self, aligned_buf: &mut [u8]) -> Result<(), BootError> { + let page_count = self.active.capacity() as u32 / Self::PAGE_SIZE; + for page_num in 0..page_count { + let progress_index = (page_count * 2 + page_num * 2) as usize; + + // Copy the bad active page to the DFU page + let active_from_offset = page_num * Self::PAGE_SIZE; + let dfu_to_offset = page_num * Self::PAGE_SIZE; + self.copy_page_once_to_dfu(progress_index, active_from_offset, dfu_to_offset, aligned_buf)?; + + // Copy the DFU page back to the active page + let active_to_offset = page_num * Self::PAGE_SIZE; + let dfu_from_offset = (page_num + 1) * Self::PAGE_SIZE; + self.copy_page_once_to_active(progress_index + 1, dfu_from_offset, active_to_offset, aligned_buf)?; + } + + Ok(()) + } + + fn read_state(&mut self, aligned_buf: &mut [u8]) -> Result { + let state_word = &mut aligned_buf[..STATE::WRITE_SIZE]; + self.state.read(0, state_word)?; + + if !state_word.iter().any(|&b| b != SWAP_MAGIC) { + Ok(State::Swap) + } else if !state_word.iter().any(|&b| b != DFU_DETACH_MAGIC) { + Ok(State::DfuDetach) + } else { + Ok(State::Boot) + } + } +} + +fn assert_partitions( + active: &ACTIVE, + dfu: &DFU, + state: &STATE, + page_size: u32, +) { + assert_eq!(active.capacity() as u32 % page_size, 0); + assert_eq!(dfu.capacity() as u32 % page_size, 0); + // DFU partition has to be bigger than ACTIVE partition to handle swap algorithm + assert!(dfu.capacity() as u32 - active.capacity() as u32 >= page_size); + assert!(2 + 2 * (active.capacity() as u32 / page_size) <= state.capacity() as u32 / STATE::WRITE_SIZE as u32); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mem_flash::MemFlash; + + #[test] + #[should_panic] + fn test_range_asserts() { + const ACTIVE_SIZE: usize = 4194304 - 4096; + const DFU_SIZE: usize = 4194304; + const STATE_SIZE: usize = 4096; + static ACTIVE: MemFlash = MemFlash::new(0xFF); + static DFU: MemFlash = MemFlash::new(0xFF); + static STATE: MemFlash = MemFlash::new(0xFF); + assert_partitions(&ACTIVE, &DFU, &STATE, 4096); + } +} diff --git a/embassy-boot/src/digest_adapters/ed25519_dalek.rs b/embassy-boot/src/digest_adapters/ed25519_dalek.rs new file mode 100644 index 000000000..2e4e03da3 --- /dev/null +++ b/embassy-boot/src/digest_adapters/ed25519_dalek.rs @@ -0,0 +1,30 @@ +use digest::typenum::U64; +use digest::{FixedOutput, HashMarker, OutputSizeUser, Update}; +use ed25519_dalek::Digest; + +pub struct Sha512(ed25519_dalek::Sha512); + +impl Default for Sha512 { + fn default() -> Self { + Self(ed25519_dalek::Sha512::new()) + } +} + +impl Update for Sha512 { + fn update(&mut self, data: &[u8]) { + Digest::update(&mut self.0, data) + } +} + +impl FixedOutput for Sha512 { + fn finalize_into(self, out: &mut digest::Output) { + let result = self.0.finalize(); + out.as_mut_slice().copy_from_slice(result.as_slice()) + } +} + +impl OutputSizeUser for Sha512 { + type OutputSize = U64; +} + +impl HashMarker for Sha512 {} diff --git a/embassy-boot/src/digest_adapters/mod.rs b/embassy-boot/src/digest_adapters/mod.rs new file mode 100644 index 000000000..9b4b4b60c --- /dev/null +++ b/embassy-boot/src/digest_adapters/mod.rs @@ -0,0 +1,5 @@ +#[cfg(feature = "ed25519-dalek")] +pub(crate) mod ed25519_dalek; + +#[cfg(feature = "ed25519-salty")] +pub(crate) mod salty; diff --git a/embassy-boot/src/digest_adapters/salty.rs b/embassy-boot/src/digest_adapters/salty.rs new file mode 100644 index 000000000..2b5dcf3af --- /dev/null +++ b/embassy-boot/src/digest_adapters/salty.rs @@ -0,0 +1,29 @@ +use digest::typenum::U64; +use digest::{FixedOutput, HashMarker, OutputSizeUser, Update}; + +pub struct Sha512(salty::Sha512); + +impl Default for Sha512 { + fn default() -> Self { + Self(salty::Sha512::new()) + } +} + +impl Update for Sha512 { + fn update(&mut self, data: &[u8]) { + self.0.update(data) + } +} + +impl FixedOutput for Sha512 { + fn finalize_into(self, out: &mut digest::Output) { + let result = self.0.finalize(); + out.as_mut_slice().copy_from_slice(result.as_slice()) + } +} + +impl OutputSizeUser for Sha512 { + type OutputSize = U64; +} + +impl HashMarker for Sha512 {} diff --git a/embassy-boot/src/firmware_updater/asynch.rs b/embassy-boot/src/firmware_updater/asynch.rs new file mode 100644 index 000000000..2e43e1cc1 --- /dev/null +++ b/embassy-boot/src/firmware_updater/asynch.rs @@ -0,0 +1,329 @@ +use digest::Digest; +#[cfg(target_os = "none")] +use embassy_embedded_hal::flash::partition::Partition; +#[cfg(target_os = "none")] +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embedded_storage_async::nor_flash::NorFlash; + +use super::FirmwareUpdaterConfig; +use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, DFU_DETACH_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC}; + +/// FirmwareUpdater is an application API for interacting with the BootLoader without the ability to +/// 'mess up' the internal bootloader state +pub struct FirmwareUpdater<'d, DFU: NorFlash, STATE: NorFlash> { + dfu: DFU, + state: FirmwareState<'d, STATE>, +} + +#[cfg(target_os = "none")] +impl<'a, FLASH: NorFlash> + FirmwareUpdaterConfig, Partition<'a, NoopRawMutex, FLASH>> +{ + /// Create a firmware updater config from the flash and address symbols defined in the linkerfile + pub fn from_linkerfile(flash: &'a embassy_sync::mutex::Mutex) -> Self { + extern "C" { + static __bootloader_state_start: u32; + static __bootloader_state_end: u32; + static __bootloader_dfu_start: u32; + static __bootloader_dfu_end: u32; + } + + let dfu = unsafe { + let start = &__bootloader_dfu_start as *const u32 as u32; + let end = &__bootloader_dfu_end as *const u32 as u32; + trace!("DFU: 0x{:x} - 0x{:x}", start, end); + + Partition::new(flash, start, end - start) + }; + let state = unsafe { + let start = &__bootloader_state_start as *const u32 as u32; + let end = &__bootloader_state_end as *const u32 as u32; + trace!("STATE: 0x{:x} - 0x{:x}", start, end); + + Partition::new(flash, start, end - start) + }; + + Self { dfu, state } + } +} + +impl<'d, DFU: NorFlash, STATE: NorFlash> FirmwareUpdater<'d, DFU, STATE> { + /// Create a firmware updater instance with partition ranges for the update and state partitions. + pub fn new(config: FirmwareUpdaterConfig, aligned: &'d mut [u8]) -> Self { + Self { + dfu: config.dfu, + state: FirmwareState::new(config.state, aligned), + } + } + + /// Obtain the current state. + /// + /// This is useful to check if the bootloader has just done a swap, in order + /// to do verifications and self-tests of the new image before calling + /// `mark_booted`. + pub async fn get_state(&mut self) -> Result { + self.state.get_state().await + } + + /// Verify the DFU given a public key. If there is an error then DO NOT + /// proceed with updating the firmware as it must be signed with a + /// corresponding private key (otherwise it could be malicious firmware). + /// + /// Mark to trigger firmware swap on next boot if verify suceeds. + /// + /// If the "ed25519-salty" feature is set (or another similar feature) then the signature is expected to have + /// been generated from a SHA-512 digest of the firmware bytes. + /// + /// If no signature feature is set then this method will always return a + /// signature error. + #[cfg(feature = "_verify")] + pub async fn verify_and_mark_updated( + &mut self, + _public_key: &[u8; 32], + _signature: &[u8; 64], + _update_len: u32, + ) -> Result<(), FirmwareUpdaterError> { + assert!(_update_len <= self.dfu.capacity() as u32); + + self.state.verify_booted().await?; + + #[cfg(feature = "ed25519-dalek")] + { + use ed25519_dalek::{Signature, SignatureError, Verifier, VerifyingKey}; + + use crate::digest_adapters::ed25519_dalek::Sha512; + + let into_signature_error = |e: SignatureError| FirmwareUpdaterError::Signature(e.into()); + + let public_key = VerifyingKey::from_bytes(_public_key).map_err(into_signature_error)?; + let signature = Signature::from_bytes(_signature); + + let mut chunk_buf = [0; 2]; + let mut message = [0; 64]; + self.hash::(_update_len, &mut chunk_buf, &mut message).await?; + + public_key.verify(&message, &signature).map_err(into_signature_error)? + } + #[cfg(feature = "ed25519-salty")] + { + use salty::{PublicKey, Signature}; + + use crate::digest_adapters::salty::Sha512; + + fn into_signature_error(_: E) -> FirmwareUpdaterError { + FirmwareUpdaterError::Signature(signature::Error::default()) + } + + let public_key = PublicKey::try_from(_public_key).map_err(into_signature_error)?; + let signature = Signature::try_from(_signature).map_err(into_signature_error)?; + + let mut message = [0; 64]; + let mut chunk_buf = [0; 2]; + self.hash::(_update_len, &mut chunk_buf, &mut message).await?; + + let r = public_key.verify(&message, &signature); + trace!( + "Verifying with public key {}, signature {} and message {} yields ok: {}", + public_key.to_bytes(), + signature.to_bytes(), + message, + r.is_ok() + ); + r.map_err(into_signature_error)? + } + + self.state.mark_updated().await + } + + /// Verify the update in DFU with any digest. + pub async fn hash( + &mut self, + update_len: u32, + chunk_buf: &mut [u8], + output: &mut [u8], + ) -> Result<(), FirmwareUpdaterError> { + let mut digest = D::new(); + for offset in (0..update_len).step_by(chunk_buf.len()) { + self.dfu.read(offset, chunk_buf).await?; + let len = core::cmp::min((update_len - offset) as usize, chunk_buf.len()); + digest.update(&chunk_buf[..len]); + } + output.copy_from_slice(digest.finalize().as_slice()); + Ok(()) + } + + /// Mark to trigger firmware swap on next boot. + #[cfg(not(feature = "_verify"))] + pub async fn mark_updated(&mut self) -> Result<(), FirmwareUpdaterError> { + self.state.mark_updated().await + } + + /// Mark to trigger USB DFU on next boot. + pub async fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> { + self.state.verify_booted().await?; + self.state.mark_dfu().await + } + + /// Mark firmware boot successful and stop rollback on reset. + pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { + self.state.mark_booted().await + } + + /// Write data to a flash page. + /// + /// The buffer must follow alignment requirements of the target flash and a multiple of page size big. + /// + /// # Safety + /// + /// Failing to meet alignment and size requirements may result in a panic. + pub async fn write_firmware(&mut self, offset: usize, data: &[u8]) -> Result<(), FirmwareUpdaterError> { + assert!(data.len() >= DFU::ERASE_SIZE); + + self.state.verify_booted().await?; + + self.dfu.erase(offset as u32, (offset + data.len()) as u32).await?; + + self.dfu.write(offset as u32, data).await?; + + Ok(()) + } + + /// Prepare for an incoming DFU update by erasing the entire DFU area and + /// returning its `Partition`. + /// + /// Using this instead of `write_firmware` allows for an optimized API in + /// exchange for added complexity. + pub async fn prepare_update(&mut self) -> Result<&mut DFU, FirmwareUpdaterError> { + self.state.verify_booted().await?; + self.dfu.erase(0, self.dfu.capacity() as u32).await?; + + Ok(&mut self.dfu) + } +} + +/// Manages the state partition of the firmware update. +/// +/// Can be used standalone for more fine grained control, or as part of the updater. +pub struct FirmwareState<'d, STATE> { + state: STATE, + aligned: &'d mut [u8], +} + +impl<'d, STATE: NorFlash> FirmwareState<'d, STATE> { + /// Create a firmware state instance from a FirmwareUpdaterConfig with a buffer for magic content and state partition. + /// + /// # Safety + /// + /// The `aligned` buffer must have a size of STATE::WRITE_SIZE, and follow the alignment rules for the flash being read from + /// and written to. + pub fn from_config(config: FirmwareUpdaterConfig, aligned: &'d mut [u8]) -> Self { + Self::new(config.state, aligned) + } + + /// Create a firmware state instance with a buffer for magic content and state partition. + /// + /// # Safety + /// + /// The `aligned` buffer must have a size of maximum of STATE::WRITE_SIZE and STATE::READ_SIZE, + /// and follow the alignment rules for the flash being read from and written to. + pub fn new(state: STATE, aligned: &'d mut [u8]) -> Self { + assert_eq!(aligned.len(), STATE::WRITE_SIZE.max(STATE::READ_SIZE)); + Self { state, aligned } + } + + // Make sure we are running a booted firmware to avoid reverting to a bad state. + async fn verify_booted(&mut self) -> Result<(), FirmwareUpdaterError> { + if self.get_state().await? == State::Boot { + Ok(()) + } else { + Err(FirmwareUpdaterError::BadState) + } + } + + /// Obtain the current state. + /// + /// This is useful to check if the bootloader has just done a swap, in order + /// to do verifications and self-tests of the new image before calling + /// `mark_booted`. + pub async fn get_state(&mut self) -> Result { + self.state.read(0, &mut self.aligned).await?; + + if !self.aligned.iter().any(|&b| b != SWAP_MAGIC) { + Ok(State::Swap) + } else { + Ok(State::Boot) + } + } + + /// Mark to trigger firmware swap on next boot. + pub async fn mark_updated(&mut self) -> Result<(), FirmwareUpdaterError> { + self.set_magic(SWAP_MAGIC).await + } + + /// Mark to trigger USB DFU on next boot. + pub async fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> { + self.set_magic(DFU_DETACH_MAGIC).await + } + + /// Mark firmware boot successful and stop rollback on reset. + pub async fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { + self.set_magic(BOOT_MAGIC).await + } + + async fn set_magic(&mut self, magic: u8) -> Result<(), FirmwareUpdaterError> { + self.state.read(0, &mut self.aligned).await?; + + if self.aligned.iter().any(|&b| b != magic) { + // Read progress validity + self.state.read(STATE::WRITE_SIZE as u32, &mut self.aligned).await?; + + if self.aligned.iter().any(|&b| b != STATE_ERASE_VALUE) { + // The current progress validity marker is invalid + } else { + // Invalidate progress + self.aligned.fill(!STATE_ERASE_VALUE); + self.state.write(STATE::WRITE_SIZE as u32, &self.aligned).await?; + } + + // Clear magic and progress + self.state.erase(0, self.state.capacity() as u32).await?; + + // Set magic + self.aligned.fill(magic); + self.state.write(0, &self.aligned).await?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use embassy_embedded_hal::flash::partition::Partition; + use embassy_sync::blocking_mutex::raw::NoopRawMutex; + use embassy_sync::mutex::Mutex; + use futures::executor::block_on; + use sha1::{Digest, Sha1}; + + use super::*; + use crate::mem_flash::MemFlash; + + #[test] + fn can_verify_sha1() { + let flash = Mutex::::new(MemFlash::<131072, 4096, 8>::default()); + let state = Partition::new(&flash, 0, 4096); + let dfu = Partition::new(&flash, 65536, 65536); + let mut aligned = [0; 8]; + + let update = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66]; + let mut to_write = [0; 4096]; + to_write[..7].copy_from_slice(update.as_slice()); + + let mut updater = FirmwareUpdater::new(FirmwareUpdaterConfig { dfu, state }, &mut aligned); + block_on(updater.write_firmware(0, to_write.as_slice())).unwrap(); + let mut chunk_buf = [0; 2]; + let mut hash = [0; 20]; + block_on(updater.hash::(update.len() as u32, &mut chunk_buf, &mut hash)).unwrap(); + + assert_eq!(Sha1::digest(update).as_slice(), hash); + } +} diff --git a/embassy-boot/src/firmware_updater/blocking.rs b/embassy-boot/src/firmware_updater/blocking.rs new file mode 100644 index 000000000..f1368540d --- /dev/null +++ b/embassy-boot/src/firmware_updater/blocking.rs @@ -0,0 +1,340 @@ +use digest::Digest; +#[cfg(target_os = "none")] +use embassy_embedded_hal::flash::partition::BlockingPartition; +#[cfg(target_os = "none")] +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embedded_storage::nor_flash::NorFlash; + +use super::FirmwareUpdaterConfig; +use crate::{FirmwareUpdaterError, State, BOOT_MAGIC, DFU_DETACH_MAGIC, STATE_ERASE_VALUE, SWAP_MAGIC}; + +/// Blocking FirmwareUpdater is an application API for interacting with the BootLoader without the ability to +/// 'mess up' the internal bootloader state +pub struct BlockingFirmwareUpdater<'d, DFU: NorFlash, STATE: NorFlash> { + dfu: DFU, + state: BlockingFirmwareState<'d, STATE>, +} + +#[cfg(target_os = "none")] +impl<'a, FLASH: NorFlash> + FirmwareUpdaterConfig, BlockingPartition<'a, NoopRawMutex, FLASH>> +{ + /// Create a firmware updater config from the flash and address symbols defined in the linkerfile + pub fn from_linkerfile_blocking( + flash: &'a embassy_sync::blocking_mutex::Mutex>, + ) -> Self { + extern "C" { + static __bootloader_state_start: u32; + static __bootloader_state_end: u32; + static __bootloader_dfu_start: u32; + static __bootloader_dfu_end: u32; + } + + let dfu = unsafe { + let start = &__bootloader_dfu_start as *const u32 as u32; + let end = &__bootloader_dfu_end as *const u32 as u32; + trace!("DFU: 0x{:x} - 0x{:x}", start, end); + + BlockingPartition::new(flash, start, end - start) + }; + let state = unsafe { + let start = &__bootloader_state_start as *const u32 as u32; + let end = &__bootloader_state_end as *const u32 as u32; + trace!("STATE: 0x{:x} - 0x{:x}", start, end); + + BlockingPartition::new(flash, start, end - start) + }; + + Self { dfu, state } + } +} + +impl<'d, DFU: NorFlash, STATE: NorFlash> BlockingFirmwareUpdater<'d, DFU, STATE> { + /// Create a firmware updater instance with partition ranges for the update and state partitions. + /// + /// # Safety + /// + /// The `aligned` buffer must have a size of STATE::WRITE_SIZE, and follow the alignment rules for the flash being read from + /// and written to. + pub fn new(config: FirmwareUpdaterConfig, aligned: &'d mut [u8]) -> Self { + Self { + dfu: config.dfu, + state: BlockingFirmwareState::new(config.state, aligned), + } + } + + /// Obtain the current state. + /// + /// This is useful to check if the bootloader has just done a swap, in order + /// to do verifications and self-tests of the new image before calling + /// `mark_booted`. + pub fn get_state(&mut self) -> Result { + self.state.get_state() + } + + /// Verify the DFU given a public key. If there is an error then DO NOT + /// proceed with updating the firmware as it must be signed with a + /// corresponding private key (otherwise it could be malicious firmware). + /// + /// Mark to trigger firmware swap on next boot if verify suceeds. + /// + /// If the "ed25519-salty" feature is set (or another similar feature) then the signature is expected to have + /// been generated from a SHA-512 digest of the firmware bytes. + /// + /// If no signature feature is set then this method will always return a + /// signature error. + #[cfg(feature = "_verify")] + pub fn verify_and_mark_updated( + &mut self, + _public_key: &[u8; 32], + _signature: &[u8; 64], + _update_len: u32, + ) -> Result<(), FirmwareUpdaterError> { + assert!(_update_len <= self.dfu.capacity() as u32); + + self.state.verify_booted()?; + + #[cfg(feature = "ed25519-dalek")] + { + use ed25519_dalek::{Signature, SignatureError, Verifier, VerifyingKey}; + + use crate::digest_adapters::ed25519_dalek::Sha512; + + let into_signature_error = |e: SignatureError| FirmwareUpdaterError::Signature(e.into()); + + let public_key = VerifyingKey::from_bytes(_public_key).map_err(into_signature_error)?; + let signature = Signature::from_bytes(_signature); + + let mut message = [0; 64]; + let mut chunk_buf = [0; 2]; + self.hash::(_update_len, &mut chunk_buf, &mut message)?; + + public_key.verify(&message, &signature).map_err(into_signature_error)? + } + #[cfg(feature = "ed25519-salty")] + { + use salty::{PublicKey, Signature}; + + use crate::digest_adapters::salty::Sha512; + + fn into_signature_error(_: E) -> FirmwareUpdaterError { + FirmwareUpdaterError::Signature(signature::Error::default()) + } + + let public_key = PublicKey::try_from(_public_key).map_err(into_signature_error)?; + let signature = Signature::try_from(_signature).map_err(into_signature_error)?; + + let mut message = [0; 64]; + let mut chunk_buf = [0; 2]; + self.hash::(_update_len, &mut chunk_buf, &mut message)?; + + let r = public_key.verify(&message, &signature); + trace!( + "Verifying with public key {}, signature {} and message {} yields ok: {}", + public_key.to_bytes(), + signature.to_bytes(), + message, + r.is_ok() + ); + r.map_err(into_signature_error)? + } + + self.state.mark_updated() + } + + /// Verify the update in DFU with any digest. + pub fn hash( + &mut self, + update_len: u32, + chunk_buf: &mut [u8], + output: &mut [u8], + ) -> Result<(), FirmwareUpdaterError> { + let mut digest = D::new(); + for offset in (0..update_len).step_by(chunk_buf.len()) { + self.dfu.read(offset, chunk_buf)?; + let len = core::cmp::min((update_len - offset) as usize, chunk_buf.len()); + digest.update(&chunk_buf[..len]); + } + output.copy_from_slice(digest.finalize().as_slice()); + Ok(()) + } + + /// Mark to trigger firmware swap on next boot. + #[cfg(not(feature = "_verify"))] + pub fn mark_updated(&mut self) -> Result<(), FirmwareUpdaterError> { + self.state.mark_updated() + } + + /// Mark to trigger USB DFU device on next boot. + pub fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> { + self.state.verify_booted()?; + self.state.mark_dfu() + } + + /// Mark firmware boot successful and stop rollback on reset. + pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { + self.state.mark_booted() + } + + /// Write data to a flash page. + /// + /// The buffer must follow alignment requirements of the target flash and a multiple of page size big. + /// + /// # Safety + /// + /// Failing to meet alignment and size requirements may result in a panic. + pub fn write_firmware(&mut self, offset: usize, data: &[u8]) -> Result<(), FirmwareUpdaterError> { + assert!(data.len() >= DFU::ERASE_SIZE); + self.state.verify_booted()?; + + self.dfu.erase(offset as u32, (offset + data.len()) as u32)?; + + self.dfu.write(offset as u32, data)?; + + Ok(()) + } + + /// Prepare for an incoming DFU update by erasing the entire DFU area and + /// returning its `Partition`. + /// + /// Using this instead of `write_firmware` allows for an optimized API in + /// exchange for added complexity. + pub fn prepare_update(&mut self) -> Result<&mut DFU, FirmwareUpdaterError> { + self.state.verify_booted()?; + self.dfu.erase(0, self.dfu.capacity() as u32)?; + + Ok(&mut self.dfu) + } +} + +/// Manages the state partition of the firmware update. +/// +/// Can be used standalone for more fine grained control, or as part of the updater. +pub struct BlockingFirmwareState<'d, STATE> { + state: STATE, + aligned: &'d mut [u8], +} + +impl<'d, STATE: NorFlash> BlockingFirmwareState<'d, STATE> { + /// Creates a firmware state instance from a FirmwareUpdaterConfig, with a buffer for magic content and state partition. + /// + /// # Safety + /// + /// The `aligned` buffer must have a size of STATE::WRITE_SIZE, and follow the alignment rules for the flash being read from + /// and written to. + pub fn from_config(config: FirmwareUpdaterConfig, aligned: &'d mut [u8]) -> Self { + Self::new(config.state, aligned) + } + + /// Create a firmware state instance with a buffer for magic content and state partition. + /// + /// # Safety + /// + /// The `aligned` buffer must have a size of STATE::WRITE_SIZE, and follow the alignment rules for the flash being read from + /// and written to. + pub fn new(state: STATE, aligned: &'d mut [u8]) -> Self { + assert_eq!(aligned.len(), STATE::WRITE_SIZE); + Self { state, aligned } + } + + // Make sure we are running a booted firmware to avoid reverting to a bad state. + fn verify_booted(&mut self) -> Result<(), FirmwareUpdaterError> { + if self.get_state()? == State::Boot || self.get_state()? == State::DfuDetach { + Ok(()) + } else { + Err(FirmwareUpdaterError::BadState) + } + } + + /// Obtain the current state. + /// + /// This is useful to check if the bootloader has just done a swap, in order + /// to do verifications and self-tests of the new image before calling + /// `mark_booted`. + pub fn get_state(&mut self) -> Result { + self.state.read(0, &mut self.aligned)?; + + if !self.aligned.iter().any(|&b| b != SWAP_MAGIC) { + Ok(State::Swap) + } else if !self.aligned.iter().any(|&b| b != DFU_DETACH_MAGIC) { + Ok(State::DfuDetach) + } else { + Ok(State::Boot) + } + } + + /// Mark to trigger firmware swap on next boot. + pub fn mark_updated(&mut self) -> Result<(), FirmwareUpdaterError> { + self.set_magic(SWAP_MAGIC) + } + + /// Mark to trigger USB DFU on next boot. + pub fn mark_dfu(&mut self) -> Result<(), FirmwareUpdaterError> { + self.set_magic(DFU_DETACH_MAGIC) + } + + /// Mark firmware boot successful and stop rollback on reset. + pub fn mark_booted(&mut self) -> Result<(), FirmwareUpdaterError> { + self.set_magic(BOOT_MAGIC) + } + + fn set_magic(&mut self, magic: u8) -> Result<(), FirmwareUpdaterError> { + self.state.read(0, &mut self.aligned)?; + + if self.aligned.iter().any(|&b| b != magic) { + // Read progress validity + self.state.read(STATE::WRITE_SIZE as u32, &mut self.aligned)?; + + if self.aligned.iter().any(|&b| b != STATE_ERASE_VALUE) { + // The current progress validity marker is invalid + } else { + // Invalidate progress + self.aligned.fill(!STATE_ERASE_VALUE); + self.state.write(STATE::WRITE_SIZE as u32, &self.aligned)?; + } + + // Clear magic and progress + self.state.erase(0, self.state.capacity() as u32)?; + + // Set magic + self.aligned.fill(magic); + self.state.write(0, &self.aligned)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use core::cell::RefCell; + + use embassy_embedded_hal::flash::partition::BlockingPartition; + use embassy_sync::blocking_mutex::raw::NoopRawMutex; + use embassy_sync::blocking_mutex::Mutex; + use sha1::{Digest, Sha1}; + + use super::*; + use crate::mem_flash::MemFlash; + + #[test] + fn can_verify_sha1() { + let flash = Mutex::::new(RefCell::new(MemFlash::<131072, 4096, 8>::default())); + let state = BlockingPartition::new(&flash, 0, 4096); + let dfu = BlockingPartition::new(&flash, 65536, 65536); + let mut aligned = [0; 8]; + + let update = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66]; + let mut to_write = [0; 4096]; + to_write[..7].copy_from_slice(update.as_slice()); + + let mut updater = BlockingFirmwareUpdater::new(FirmwareUpdaterConfig { dfu, state }, &mut aligned); + updater.write_firmware(0, to_write.as_slice()).unwrap(); + let mut chunk_buf = [0; 2]; + let mut hash = [0; 20]; + updater + .hash::(update.len() as u32, &mut chunk_buf, &mut hash) + .unwrap(); + + assert_eq!(Sha1::digest(update).as_slice(), hash); + } +} diff --git a/embassy-boot/src/firmware_updater/mod.rs b/embassy-boot/src/firmware_updater/mod.rs new file mode 100644 index 000000000..4814786bf --- /dev/null +++ b/embassy-boot/src/firmware_updater/mod.rs @@ -0,0 +1,49 @@ +mod asynch; +mod blocking; + +pub use asynch::{FirmwareState, FirmwareUpdater}; +pub use blocking::{BlockingFirmwareState, BlockingFirmwareUpdater}; +use embedded_storage::nor_flash::{NorFlashError, NorFlashErrorKind}; + +/// Firmware updater flash configuration holding the two flashes used by the updater +/// +/// If only a single flash is actually used, then that flash should be partitioned into two partitions before use. +/// The easiest way to do this is to use [`FirmwareUpdaterConfig::from_linkerfile`] or [`FirmwareUpdaterConfig::from_linkerfile_blocking`] which will partition +/// the provided flash according to symbols defined in the linkerfile. +pub struct FirmwareUpdaterConfig { + /// The dfu flash partition + pub dfu: DFU, + /// The state flash partition + pub state: STATE, +} + +/// Errors returned by FirmwareUpdater +#[derive(Debug)] +pub enum FirmwareUpdaterError { + /// Error from flash. + Flash(NorFlashErrorKind), + /// Signature errors. + Signature(signature::Error), + /// Bad state. + BadState, +} + +#[cfg(feature = "defmt")] +impl defmt::Format for FirmwareUpdaterError { + fn format(&self, fmt: defmt::Formatter) { + match self { + FirmwareUpdaterError::Flash(_) => defmt::write!(fmt, "FirmwareUpdaterError::Flash(_)"), + FirmwareUpdaterError::Signature(_) => defmt::write!(fmt, "FirmwareUpdaterError::Signature(_)"), + FirmwareUpdaterError::BadState => defmt::write!(fmt, "FirmwareUpdaterError::BadState"), + } + } +} + +impl From for FirmwareUpdaterError +where + E: NorFlashError, +{ + fn from(error: E) -> Self { + FirmwareUpdaterError::Flash(error.kind()) + } +} diff --git a/embassy-boot/src/fmt.rs b/embassy-boot/src/fmt.rs new file mode 100644 index 000000000..78e583c1c --- /dev/null +++ b/embassy-boot/src/fmt.rs @@ -0,0 +1,258 @@ +#![macro_use] +#![allow(unused_macros)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unreachable { + ($($x:tt)*) => { + ::core::unreachable!($($x)*) + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unreachable { + ($($x:tt)*) => { + ::defmt::unreachable!($($x)*) + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::defmt::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[allow(unused)] +pub(crate) struct Bytes<'a>(pub &'a [u8]); + +impl<'a> Debug for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> Display for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> LowerHex for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for Bytes<'a> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} diff --git a/embassy-boot/src/lib.rs b/embassy-boot/src/lib.rs new file mode 100644 index 000000000..b4f03e01e --- /dev/null +++ b/embassy-boot/src/lib.rs @@ -0,0 +1,323 @@ +#![no_std] +#![allow(async_fn_in_trait)] +#![warn(missing_docs)] +#![doc = include_str!("../README.md")] +mod fmt; + +mod boot_loader; +mod digest_adapters; +mod firmware_updater; +#[cfg(test)] +mod mem_flash; +#[cfg(test)] +mod test_flash; + +// The expected value of the flash after an erase +// TODO: Use the value provided by NorFlash when available +pub(crate) const STATE_ERASE_VALUE: u8 = 0xFF; +pub use boot_loader::{BootError, BootLoader, BootLoaderConfig}; +pub use firmware_updater::{ + BlockingFirmwareState, BlockingFirmwareUpdater, FirmwareState, FirmwareUpdater, FirmwareUpdaterConfig, + FirmwareUpdaterError, +}; + +pub(crate) const BOOT_MAGIC: u8 = 0xD0; +pub(crate) const SWAP_MAGIC: u8 = 0xF0; +pub(crate) const DFU_DETACH_MAGIC: u8 = 0xE0; + +/// The state of the bootloader after running prepare. +#[derive(PartialEq, Eq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum State { + /// Bootloader is ready to boot the active partition. + Boot, + /// Bootloader has swapped the active partition with the dfu partition and will attempt boot. + Swap, + /// Application has received a request to reboot into DFU mode to apply an update. + DfuDetach, +} + +/// Buffer aligned to 32 byte boundary, largest known alignment requirement for embassy-boot. +#[repr(align(32))] +pub struct AlignedBuffer(pub [u8; N]); + +impl AsRef<[u8]> for AlignedBuffer { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsMut<[u8]> for AlignedBuffer { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +#[cfg(test)] +mod tests { + #![allow(unused_imports)] + + use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; + use embedded_storage_async::nor_flash::NorFlash as AsyncNorFlash; + use futures::executor::block_on; + + use super::*; + use crate::boot_loader::BootLoaderConfig; + use crate::firmware_updater::FirmwareUpdaterConfig; + use crate::mem_flash::MemFlash; + use crate::test_flash::{AsyncTestFlash, BlockingTestFlash}; + + /* + #[test] + fn test_bad_magic() { + let mut flash = MemFlash([0xff; 131072]); + let mut flash = SingleFlashConfig::new(&mut flash); + + let mut bootloader = BootLoader::<4096>::new(ACTIVE, DFU, STATE); + + assert_eq!( + bootloader.prepare_boot(&mut flash), + Err(BootError::BadMagic) + ); + } + */ + + #[test] + fn test_boot_state() { + let flash = BlockingTestFlash::new(BootLoaderConfig { + active: MemFlash::<57344, 4096, 4>::default(), + dfu: MemFlash::<61440, 4096, 4>::default(), + state: MemFlash::<4096, 4096, 4>::default(), + }); + + flash.state().write(0, &[BOOT_MAGIC; 4]).unwrap(); + + let mut bootloader = BootLoader::new(BootLoaderConfig { + active: flash.active(), + dfu: flash.dfu(), + state: flash.state(), + }); + + let mut page = [0; 4096]; + assert_eq!(State::Boot, bootloader.prepare_boot(&mut page).unwrap()); + } + + #[test] + #[cfg(not(feature = "_verify"))] + fn test_swap_state() { + const FIRMWARE_SIZE: usize = 57344; + let flash = AsyncTestFlash::new(BootLoaderConfig { + active: MemFlash::::default(), + dfu: MemFlash::<61440, 4096, 4>::default(), + state: MemFlash::<4096, 4096, 4>::default(), + }); + + const ORIGINAL: [u8; FIRMWARE_SIZE] = [0x55; FIRMWARE_SIZE]; + const UPDATE: [u8; FIRMWARE_SIZE] = [0xAA; FIRMWARE_SIZE]; + let mut aligned = [0; 4]; + + block_on(flash.active().erase(0, ORIGINAL.len() as u32)).unwrap(); + block_on(flash.active().write(0, &ORIGINAL)).unwrap(); + + let mut updater = FirmwareUpdater::new( + FirmwareUpdaterConfig { + dfu: flash.dfu(), + state: flash.state(), + }, + &mut aligned, + ); + block_on(updater.write_firmware(0, &UPDATE)).unwrap(); + block_on(updater.mark_updated()).unwrap(); + + // Writing after marking updated is not allowed until marked as booted. + let res: Result<(), FirmwareUpdaterError> = block_on(updater.write_firmware(0, &UPDATE)); + assert!(matches!(res, Err::<(), _>(FirmwareUpdaterError::BadState))); + + let flash = flash.into_blocking(); + let mut bootloader = BootLoader::new(BootLoaderConfig { + active: flash.active(), + dfu: flash.dfu(), + state: flash.state(), + }); + + let mut page = [0; 1024]; + assert_eq!(State::Swap, bootloader.prepare_boot(&mut page).unwrap()); + + let mut read_buf = [0; FIRMWARE_SIZE]; + flash.active().read(0, &mut read_buf).unwrap(); + assert_eq!(UPDATE, read_buf); + // First DFU page is untouched + flash.dfu().read(4096, &mut read_buf).unwrap(); + assert_eq!(ORIGINAL, read_buf); + + // Running again should cause a revert + assert_eq!(State::Swap, bootloader.prepare_boot(&mut page).unwrap()); + + let mut read_buf = [0; FIRMWARE_SIZE]; + flash.active().read(0, &mut read_buf).unwrap(); + assert_eq!(ORIGINAL, read_buf); + // Last DFU page is untouched + flash.dfu().read(0, &mut read_buf).unwrap(); + assert_eq!(UPDATE, read_buf); + + // Mark as booted + let flash = flash.into_async(); + let mut updater = FirmwareUpdater::new( + FirmwareUpdaterConfig { + dfu: flash.dfu(), + state: flash.state(), + }, + &mut aligned, + ); + block_on(updater.mark_booted()).unwrap(); + + let flash = flash.into_blocking(); + let mut bootloader = BootLoader::new(BootLoaderConfig { + active: flash.active(), + dfu: flash.dfu(), + state: flash.state(), + }); + assert_eq!(State::Boot, bootloader.prepare_boot(&mut page).unwrap()); + } + + #[test] + #[cfg(not(feature = "_verify"))] + fn test_swap_state_active_page_biggest() { + const FIRMWARE_SIZE: usize = 12288; + let flash = AsyncTestFlash::new(BootLoaderConfig { + active: MemFlash::<12288, 4096, 8>::random(), + dfu: MemFlash::<16384, 2048, 8>::random(), + state: MemFlash::<2048, 128, 4>::random(), + }); + + const ORIGINAL: [u8; FIRMWARE_SIZE] = [0x55; FIRMWARE_SIZE]; + const UPDATE: [u8; FIRMWARE_SIZE] = [0xAA; FIRMWARE_SIZE]; + let mut aligned = [0; 4]; + + block_on(flash.active().erase(0, ORIGINAL.len() as u32)).unwrap(); + block_on(flash.active().write(0, &ORIGINAL)).unwrap(); + + let mut updater = FirmwareUpdater::new( + FirmwareUpdaterConfig { + dfu: flash.dfu(), + state: flash.state(), + }, + &mut aligned, + ); + block_on(updater.write_firmware(0, &UPDATE)).unwrap(); + block_on(updater.mark_updated()).unwrap(); + + let flash = flash.into_blocking(); + let mut bootloader = BootLoader::new(BootLoaderConfig { + active: flash.active(), + dfu: flash.dfu(), + state: flash.state(), + }); + + let mut page = [0; 4096]; + assert_eq!(State::Swap, bootloader.prepare_boot(&mut page).unwrap()); + + let mut read_buf = [0; FIRMWARE_SIZE]; + flash.active().read(0, &mut read_buf).unwrap(); + assert_eq!(UPDATE, read_buf); + // First DFU page is untouched + flash.dfu().read(4096, &mut read_buf).unwrap(); + assert_eq!(ORIGINAL, read_buf); + } + + #[test] + #[cfg(not(feature = "_verify"))] + fn test_swap_state_dfu_page_biggest() { + const FIRMWARE_SIZE: usize = 12288; + let flash = AsyncTestFlash::new(BootLoaderConfig { + active: MemFlash::::random(), + dfu: MemFlash::<16384, 4096, 8>::random(), + state: MemFlash::<2048, 128, 4>::random(), + }); + + const ORIGINAL: [u8; FIRMWARE_SIZE] = [0x55; FIRMWARE_SIZE]; + const UPDATE: [u8; FIRMWARE_SIZE] = [0xAA; FIRMWARE_SIZE]; + let mut aligned = [0; 4]; + + block_on(flash.active().erase(0, ORIGINAL.len() as u32)).unwrap(); + block_on(flash.active().write(0, &ORIGINAL)).unwrap(); + + let mut updater = FirmwareUpdater::new( + FirmwareUpdaterConfig { + dfu: flash.dfu(), + state: flash.state(), + }, + &mut aligned, + ); + block_on(updater.write_firmware(0, &UPDATE)).unwrap(); + block_on(updater.mark_updated()).unwrap(); + + let flash = flash.into_blocking(); + let mut bootloader = BootLoader::new(BootLoaderConfig { + active: flash.active(), + dfu: flash.dfu(), + state: flash.state(), + }); + let mut page = [0; 4096]; + assert_eq!(State::Swap, bootloader.prepare_boot(&mut page).unwrap()); + + let mut read_buf = [0; FIRMWARE_SIZE]; + flash.active().read(0, &mut read_buf).unwrap(); + assert_eq!(UPDATE, read_buf); + // First DFU page is untouched + flash.dfu().read(4096, &mut read_buf).unwrap(); + assert_eq!(ORIGINAL, read_buf); + } + + #[test] + #[cfg(feature = "_verify")] + fn test_verify() { + // The following key setup is based on: + // https://docs.rs/ed25519-dalek/latest/ed25519_dalek/#example + + use ed25519_dalek::{Digest, Sha512, Signature, Signer, SigningKey, VerifyingKey}; + use rand::rngs::OsRng; + + let mut csprng = OsRng {}; + let keypair = SigningKey::generate(&mut csprng); + + let firmware: &[u8] = b"This are bytes that would otherwise be firmware bytes for DFU."; + let mut digest = Sha512::new(); + digest.update(&firmware); + let message = digest.finalize(); + let signature: Signature = keypair.sign(&message); + + let public_key = keypair.verifying_key(); + + // Setup flash + let flash = BlockingTestFlash::new(BootLoaderConfig { + active: MemFlash::<0, 0, 0>::default(), + dfu: MemFlash::<4096, 4096, 4>::default(), + state: MemFlash::<4096, 4096, 4>::default(), + }); + + let firmware_len = firmware.len(); + + let mut write_buf = [0; 4096]; + write_buf[0..firmware_len].copy_from_slice(firmware); + flash.dfu().write(0, &write_buf).unwrap(); + + // On with the test + let flash = flash.into_async(); + let mut aligned = [0; 4]; + let mut updater = FirmwareUpdater::new( + FirmwareUpdaterConfig { + dfu: flash.dfu(), + state: flash.state(), + }, + &mut aligned, + ); + + assert!(block_on(updater.verify_and_mark_updated( + &public_key.to_bytes(), + &signature.to_bytes(), + firmware_len as u32, + )) + .is_ok()); + } +} diff --git a/embassy-boot/src/mem_flash.rs b/embassy-boot/src/mem_flash.rs new file mode 100644 index 000000000..40f352c8d --- /dev/null +++ b/embassy-boot/src/mem_flash.rs @@ -0,0 +1,170 @@ +#![allow(unused)] + +use core::ops::{Bound, Range, RangeBounds}; + +use embedded_storage::nor_flash::{ErrorType, NorFlash, NorFlashError, NorFlashErrorKind, ReadNorFlash}; +use embedded_storage_async::nor_flash::{NorFlash as AsyncNorFlash, ReadNorFlash as AsyncReadNorFlash}; + +pub struct MemFlash { + pub mem: [u8; SIZE], + pub pending_write_successes: Option, +} + +#[derive(Debug)] +pub struct MemFlashError; + +impl MemFlash { + pub const fn new(fill: u8) -> Self { + Self { + mem: [fill; SIZE], + pending_write_successes: None, + } + } + + #[cfg(test)] + pub fn random() -> Self { + let mut mem = [0; SIZE]; + for byte in mem.iter_mut() { + *byte = rand::random::(); + } + Self { + mem, + pending_write_successes: None, + } + } + + fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), MemFlashError> { + let len = bytes.len(); + bytes.copy_from_slice(&self.mem[offset as usize..offset as usize + len]); + Ok(()) + } + + fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), MemFlashError> { + let offset = offset as usize; + assert!(bytes.len() % WRITE_SIZE == 0); + assert!(offset % WRITE_SIZE == 0); + assert!(offset + bytes.len() <= SIZE); + + if let Some(pending_successes) = self.pending_write_successes { + if pending_successes > 0 { + self.pending_write_successes = Some(pending_successes - 1); + } else { + return Err(MemFlashError); + } + } + + for ((offset, mem_byte), new_byte) in self + .mem + .iter_mut() + .enumerate() + .skip(offset) + .take(bytes.len()) + .zip(bytes) + { + assert_eq!(0xFF, *mem_byte, "Offset {} is not erased", offset); + *mem_byte = *new_byte; + } + + Ok(()) + } + + fn erase(&mut self, from: u32, to: u32) -> Result<(), MemFlashError> { + let from = from as usize; + let to = to as usize; + assert!(from % ERASE_SIZE == 0); + assert!(to % ERASE_SIZE == 0, "To: {}, erase size: {}", to, ERASE_SIZE); + for i in from..to { + self.mem[i] = 0xFF; + } + Ok(()) + } + + pub fn program(&mut self, offset: u32, bytes: &[u8]) -> Result<(), MemFlashError> { + let offset = offset as usize; + assert!(bytes.len() % WRITE_SIZE == 0); + assert!(offset % WRITE_SIZE == 0); + assert!(offset + bytes.len() <= SIZE); + + self.mem[offset..offset + bytes.len()].copy_from_slice(bytes); + + Ok(()) + } +} + +impl Default + for MemFlash +{ + fn default() -> Self { + Self::new(0xFF) + } +} + +impl ErrorType + for MemFlash +{ + type Error = MemFlashError; +} + +impl NorFlashError for MemFlashError { + fn kind(&self) -> NorFlashErrorKind { + NorFlashErrorKind::Other + } +} + +impl ReadNorFlash + for MemFlash +{ + const READ_SIZE: usize = 1; + + fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { + self.read(offset, bytes) + } + + fn capacity(&self) -> usize { + SIZE + } +} + +impl NorFlash + for MemFlash +{ + const WRITE_SIZE: usize = WRITE_SIZE; + const ERASE_SIZE: usize = ERASE_SIZE; + + fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { + self.write(offset, bytes) + } + + fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + self.erase(from, to) + } +} + +impl AsyncReadNorFlash + for MemFlash +{ + const READ_SIZE: usize = 1; + + async fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { + self.read(offset, bytes) + } + + fn capacity(&self) -> usize { + SIZE + } +} + +impl AsyncNorFlash + for MemFlash +{ + const WRITE_SIZE: usize = WRITE_SIZE; + const ERASE_SIZE: usize = ERASE_SIZE; + + async fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { + self.write(offset, bytes) + } + + async fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + self.erase(from, to) + } +} diff --git a/embassy-boot/src/test_flash/asynch.rs b/embassy-boot/src/test_flash/asynch.rs new file mode 100644 index 000000000..3ac9e71ab --- /dev/null +++ b/embassy-boot/src/test_flash/asynch.rs @@ -0,0 +1,64 @@ +use embassy_embedded_hal::flash::partition::Partition; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::mutex::Mutex; +use embedded_storage_async::nor_flash::NorFlash; + +use crate::BootLoaderConfig; + +pub struct AsyncTestFlash +where + ACTIVE: NorFlash, + DFU: NorFlash, + STATE: NorFlash, +{ + active: Mutex, + dfu: Mutex, + state: Mutex, +} + +impl AsyncTestFlash +where + ACTIVE: NorFlash, + DFU: NorFlash, + STATE: NorFlash, +{ + pub fn new(config: BootLoaderConfig) -> Self { + Self { + active: Mutex::new(config.active), + dfu: Mutex::new(config.dfu), + state: Mutex::new(config.state), + } + } + + pub fn active(&self) -> Partition { + Self::create_partition(&self.active) + } + + pub fn dfu(&self) -> Partition { + Self::create_partition(&self.dfu) + } + + pub fn state(&self) -> Partition { + Self::create_partition(&self.state) + } + + fn create_partition(mutex: &Mutex) -> Partition { + Partition::new(mutex, 0, mutex.try_lock().unwrap().capacity() as u32) + } +} + +impl AsyncTestFlash +where + ACTIVE: NorFlash + embedded_storage::nor_flash::NorFlash, + DFU: NorFlash + embedded_storage::nor_flash::NorFlash, + STATE: NorFlash + embedded_storage::nor_flash::NorFlash, +{ + pub fn into_blocking(self) -> super::BlockingTestFlash { + let config = BootLoaderConfig { + active: self.active.into_inner(), + dfu: self.dfu.into_inner(), + state: self.state.into_inner(), + }; + super::BlockingTestFlash::new(config) + } +} diff --git a/embassy-boot/src/test_flash/blocking.rs b/embassy-boot/src/test_flash/blocking.rs new file mode 100644 index 000000000..5ec476c65 --- /dev/null +++ b/embassy-boot/src/test_flash/blocking.rs @@ -0,0 +1,68 @@ +use core::cell::RefCell; + +use embassy_embedded_hal::flash::partition::BlockingPartition; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::Mutex; +use embedded_storage::nor_flash::NorFlash; + +use crate::BootLoaderConfig; + +pub struct BlockingTestFlash +where + ACTIVE: NorFlash, + DFU: NorFlash, + STATE: NorFlash, +{ + active: Mutex>, + dfu: Mutex>, + state: Mutex>, +} + +impl BlockingTestFlash +where + ACTIVE: NorFlash, + DFU: NorFlash, + STATE: NorFlash, +{ + pub fn new(config: BootLoaderConfig) -> Self { + Self { + active: Mutex::new(RefCell::new(config.active)), + dfu: Mutex::new(RefCell::new(config.dfu)), + state: Mutex::new(RefCell::new(config.state)), + } + } + + pub fn active(&self) -> BlockingPartition { + Self::create_partition(&self.active) + } + + pub fn dfu(&self) -> BlockingPartition { + Self::create_partition(&self.dfu) + } + + pub fn state(&self) -> BlockingPartition { + Self::create_partition(&self.state) + } + + pub fn create_partition( + mutex: &Mutex>, + ) -> BlockingPartition { + BlockingPartition::new(mutex, 0, mutex.lock(|f| f.borrow().capacity()) as u32) + } +} + +impl BlockingTestFlash +where + ACTIVE: NorFlash + embedded_storage_async::nor_flash::NorFlash, + DFU: NorFlash + embedded_storage_async::nor_flash::NorFlash, + STATE: NorFlash + embedded_storage_async::nor_flash::NorFlash, +{ + pub fn into_async(self) -> super::AsyncTestFlash { + let config = BootLoaderConfig { + active: self.active.into_inner().into_inner(), + dfu: self.dfu.into_inner().into_inner(), + state: self.state.into_inner().into_inner(), + }; + super::AsyncTestFlash::new(config) + } +} diff --git a/embassy-boot/src/test_flash/mod.rs b/embassy-boot/src/test_flash/mod.rs new file mode 100644 index 000000000..79b15a081 --- /dev/null +++ b/embassy-boot/src/test_flash/mod.rs @@ -0,0 +1,5 @@ +mod asynch; +mod blocking; + +pub(crate) use asynch::AsyncTestFlash; +pub(crate) use blocking::BlockingTestFlash; -- cgit