From 2b814456c2321ff6b63baa0d0d73d1729f5d6e42 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 15 Sep 2026 19:56:16 +0200 Subject: [PATCH 1/5] Make Reboot::reboot_to_firmware_update non-diverging On the lpc55, the reboot must be triggered from the idle task so the reboot function needs to return to yield back. --- CHANGELOG.md | 2 +- src/admin.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7c7af..42760eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased]: https://github.com/trussed-dev/admin-app/compare/0.3.0...HEAD -- +- Make `Reboot::reboot_to_firmware_update` non-diverging. ## [0.3.0] 2026-08-17 diff --git a/src/admin.rs b/src/admin.rs index f2ee6f7..0f41baa 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -170,7 +170,7 @@ pub trait Reboot { /// Presuming the device has a separate mode of operation that /// allows updating its firmware (for instance, a bootloader), /// reboots the device into this mode. - fn reboot_to_firmware_update() -> !; + fn reboot_to_firmware_update(); /// Reboots the device. /// From 9c7c23b298662ce42b9a58179e749356be2fd857 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 16 Sep 2026 12:39:30 +0200 Subject: [PATCH 2/5] Add Data struct and use it to simplify constructors --- CHANGELOG.md | 1 + src/admin.rs | 72 +++++++++++++++------------------------------------- src/lib.rs | 2 +- 3 files changed, 23 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42760eb..7377bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [Unreleased]: https://github.com/trussed-dev/admin-app/compare/0.3.0...HEAD - Make `Reboot::reboot_to_firmware_update` non-diverging. +- Add `Data` struct and change `App::load_config` and `App::with_default_config` to use it. ## [0.3.0] 2026-08-17 diff --git a/src/admin.rs b/src/admin.rs index 0f41baa..19c242c 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -195,15 +195,20 @@ pub trait StatusBytes { fn serialize(&self) -> Self::Serialized; } +#[derive(Clone, Copy)] +pub struct Data { + pub uuid: [u8; 16], + pub version: u32, + pub full_version: &'static str, + pub migrations: &'static [Migrator], +} + pub struct App { trussed: T, - uuid: [u8; 16], - version: u32, - full_version: &'static str, + data: Data, status: S, boot_interface: PhantomData, config: C, - migrations: &'static [Migrator], } impl App @@ -217,22 +222,11 @@ where pub fn load_config( client: T, filestore: &mut F, - uuid: [u8; 16], - version: u32, - full_version: &'static str, + data: Data, status: S, - migrations: &'static [Migrator], ) -> Result { match config::load(filestore) { - Ok(config) => Ok(Self::new( - client, - uuid, - version, - full_version, - status, - config, - migrations, - )), + Ok(config) => Ok(Self::new(client, data, status, config)), Err(err) => { error!("failed to load configuration: {:?}", err); Err((client, err)) @@ -262,7 +256,7 @@ where let internal = store.ifs(); let external = store.efs(); - for migration in self.migrations { + for migration in self.data.migrations { if migration.version > current_version && migration.version <= to_version { (migration.migrate)(internal, external).map_err(|_err| { error_now!("Migration failed: {_err:?}"); @@ -282,43 +276,17 @@ where /// /// This is only intended for debugging, testing and example code. In production, /// [`App::load_config`][] should be used. - pub fn with_default_config( - client: T, - uuid: [u8; 16], - version: u32, - full_version: &'static str, - status: S, - migrations: &'static [Migrator], - ) -> Self { - Self::new( - client, - uuid, - version, - full_version, - status, - Default::default(), - migrations, - ) + pub fn with_default_config(client: T, data: Data, status: S) -> Self { + Self::new(client, data, status, Default::default()) } - fn new( - client: T, - uuid: [u8; 16], - version: u32, - full_version: &'static str, - status: S, - config: C, - migrations: &'static [Migrator], - ) -> Self { + fn new(client: T, data: Data, status: S, config: C) -> Self { Self { trussed: client, - uuid, - version, - full_version, + data, status, boot_interface: PhantomData, config, - migrations, } } @@ -376,16 +344,18 @@ where } Command::Uuid => { // Get UUID - response.extend_from_slice(&self.uuid).ok(); + response.extend_from_slice(&self.data.uuid).ok(); } Command::Version => { // GET VERSION if input.first().copied() == Some(0x01) { response - .extend_from_slice(self.full_version.as_bytes()) + .extend_from_slice(self.data.full_version.as_bytes()) .ok(); } else { - response.extend_from_slice(&self.version.to_be_bytes()).ok(); + response + .extend_from_slice(&self.data.version.to_be_bytes()) + .ok(); } } Command::Wink => { diff --git a/src/lib.rs b/src/lib.rs index 04d897e..6169084 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ mod admin; mod config; pub mod migrations; -pub use admin::{App, Reboot, StatusBytes}; +pub use admin::{App, Data, Reboot, StatusBytes}; pub use config::{ Config, ConfigError, ConfigField, ConfigValueMut, FieldType, ResetConfigResult, ResetSignal, ResetSignalAllocation, From 2caa67ee6f378f38bb6bb9c347b1e926486a5252 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 16 Sep 2026 12:43:53 +0200 Subject: [PATCH 3/5] Replace Reboot trait with function pointers in Data This simplifies the type signature for App and also makes it possible to make some functions optional. --- CHANGELOG.md | 1 + src/admin.rs | 68 ++++++++++++++++++++++------------------------------ src/lib.rs | 2 +- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7377bc0..1e91516 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Make `Reboot::reboot_to_firmware_update` non-diverging. - Add `Data` struct and change `App::load_config` and `App::with_default_config` to use it. +- Replace `Reboot` trait with function pointers in `Data`. ## [0.3.0] 2026-08-17 diff --git a/src/admin.rs b/src/admin.rs index 19c242c..caa09ac 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,7 +1,7 @@ use super::Client as TrussedClient; use apdu_app::{CommandView, Interface}; use cbor_smol::{cbor_deserialize, cbor_serialize_to}; -use core::{convert::TryInto, marker::PhantomData, time::Duration}; +use core::{convert::TryInto, time::Duration}; use ctaphid_app::{self as hid, Command as HidCommand, VendorCommand}; use heapless::VecView; use heapless_bytes::BytesView; @@ -161,29 +161,6 @@ struct SetConfigRequest<'a> { value: &'a str, } -pub trait Reboot { - /// Reboots the device. - fn reboot() -> !; - - /// Reboots the device. - /// - /// Presuming the device has a separate mode of operation that - /// allows updating its firmware (for instance, a bootloader), - /// reboots the device into this mode. - fn reboot_to_firmware_update(); - - /// Reboots the device. - /// - /// Presuming the device has a separate destructive but more - /// reliable way of rebooting into the firmware mode of operation, - /// does so. - fn reboot_to_firmware_update_destructive() -> !; - - /// Is device bootloader locked down? - /// E.g., is secure boot enabled? - fn locked() -> bool; -} - /// Trait indicating that a value can be used as a status pub trait StatusBytes { type Serialized: AsRef<[u8]>; @@ -201,20 +178,35 @@ pub struct Data { pub version: u32, pub full_version: &'static str, pub migrations: &'static [Migrator], + /// Reboots the device. + pub reboot: fn() -> !, + /// Reboots the device. + /// + /// Presuming the device has a separate mode of operation that + /// allows updating its firmware (for instance, a bootloader), + /// reboots the device into this mode. + pub reboot_to_firmware_update: fn(), + /// Reboots the device. + /// + /// Presuming the device has a separate destructive but more + /// reliable way of rebooting into the firmware mode of operation, + /// does so. + pub reboot_to_firmware_update_destructive: fn() -> !, + /// Is device bootloader locked down? + /// E.g., is secure boot enabled? + pub locked: fn() -> bool, } -pub struct App { +pub struct App { trussed: T, data: Data, status: S, - boot_interface: PhantomData, config: C, } -impl App +impl App where T: TrussedClient, - R: Reboot, S: StatusBytes, C: Config, { @@ -285,7 +277,6 @@ where trussed: client, data, status, - boot_interface: PhantomData, config, } } @@ -321,9 +312,9 @@ where ) -> Result<(), Error> { debug_now!("Executing command: {command:?}"); match command { - Command::Reboot => R::reboot(), + Command::Reboot => (self.data.reboot)(), Command::Locked => { - response.push(R::locked().into()).ok(); + response.push((self.data.locked)().into()).ok(); } Command::Rng => { // Fill the HID packet (57 bytes) @@ -334,9 +325,9 @@ where Command::Update => { if self.user_present() { if input.first().copied() == Some(0x01) { - R::reboot_to_firmware_update_destructive(); + (self.data.reboot_to_firmware_update_destructive)(); } else { - R::reboot_to_firmware_update(); + (self.data.reboot_to_firmware_update)(); } } else { return Err(Error::NotAvailable); @@ -416,7 +407,7 @@ where return Ok(()); } syscall!(self.trussed.factory_reset_device()); - R::reboot(); + (self.data.reboot)(); } #[cfg(feature = "factory-reset")] Command::FactoryResetApp => { @@ -502,10 +493,9 @@ where } } -impl hid::App<'static> for App +impl hid::App<'static> for App where T: TrussedClient, - R: Reboot, S: StatusBytes, C: Config, { @@ -546,10 +536,9 @@ where } } -impl iso7816::App for App +impl iso7816::App for App where T: TrussedClient, - R: Reboot, S: StatusBytes, { // Solo management app @@ -558,10 +547,9 @@ where } } -impl apdu_app::App for App +impl apdu_app::App for App where T: TrussedClient, - R: Reboot, S: StatusBytes, C: Config, { diff --git a/src/lib.rs b/src/lib.rs index 6169084..7e5ec23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ mod admin; mod config; pub mod migrations; -pub use admin::{App, Data, Reboot, StatusBytes}; +pub use admin::{App, Data, StatusBytes}; pub use config::{ Config, ConfigError, ConfigField, ConfigValueMut, FieldType, ResetConfigResult, ResetSignal, ResetSignalAllocation, From 23ad55968605bc2ce526fa2ebf5e198621ac057e Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 16 Sep 2026 12:46:21 +0200 Subject: [PATCH 4/5] Make destructive reboot to firmware update optional This feature is only available on some hardware variants. If it is not enabled, the command will return an error and normal reboot to firmware update can still be used. --- CHANGELOG.md | 1 + src/admin.rs | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e91516..ac87aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Make `Reboot::reboot_to_firmware_update` non-diverging. - Add `Data` struct and change `App::load_config` and `App::with_default_config` to use it. - Replace `Reboot` trait with function pointers in `Data`. +- Make destructive reboot to firmware update optional. ## [0.3.0] 2026-08-17 diff --git a/src/admin.rs b/src/admin.rs index caa09ac..4304979 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -191,7 +191,7 @@ pub struct Data { /// Presuming the device has a separate destructive but more /// reliable way of rebooting into the firmware mode of operation, /// does so. - pub reboot_to_firmware_update_destructive: fn() -> !, + pub reboot_to_firmware_update_destructive: Option !>, /// Is device bootloader locked down? /// E.g., is secure boot enabled? pub locked: fn() -> bool, @@ -325,7 +325,11 @@ where Command::Update => { if self.user_present() { if input.first().copied() == Some(0x01) { - (self.data.reboot_to_firmware_update_destructive)(); + if let Some(f) = self.data.reboot_to_firmware_update_destructive { + f(); + } else { + return Err(Error::UnsupportedCommand); + } } else { (self.data.reboot_to_firmware_update)(); } From 8bf2fc9885ad828fdae997e1433f1dcc385e0dee Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 16 Sep 2026 12:50:22 +0200 Subject: [PATCH 5/5] Release v0.4.0 --- CHANGELOG.md | 8 +++++++- Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac87aea..b5e0aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -[Unreleased]: https://github.com/trussed-dev/admin-app/compare/0.3.0...HEAD +[Unreleased]: https://github.com/trussed-dev/admin-app/compare/0.4.0...HEAD + +- + +## [0.4.0] 2026-09-16 + +[0.4.0]: https://github.com/trussed-dev/admin-app/compare/0.3.0...0.4.0 - Make `Reboot::reboot_to_firmware_update` non-diverging. - Add `Data` struct and change `App::load_config` and `App::with_default_config` to use it. diff --git a/Cargo.toml b/Cargo.toml index 0ddcbe8..9e16cbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "admin-app" -version = "0.3.0" +version = "0.4.0" authors = ["Conor Patrick ", "Nicolas Stalder "] repository = "https://github.com/solokeys/admin-app" edition = "2021"