diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7c7af..b5e0aa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,19 @@ 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. +- Replace `Reboot` trait with function pointers in `Data`. +- Make destructive reboot to firmware update optional. + ## [0.3.0] 2026-08-17 [0.3.0]: https://github.com/trussed-dev/admin-app/compare/0.2.0...0.3.0 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" diff --git a/src/admin.rs b/src/admin.rs index f2ee6f7..4304979 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,55 +161,52 @@ struct SetConfigRequest<'a> { value: &'a str, } -pub trait Reboot { - /// Reboots the device. - fn reboot() -> !; +/// Trait indicating that a value can be used as a status +pub trait StatusBytes { + type Serialized: AsRef<[u8]>; + /// Set the flag indicating that the random generator could properly be created (`false`) or not (`true`) + fn set_random_error(&mut self, value: bool); + /// Get the flag indicating that the random generator could properly be created (`false`) or not (`true`) + fn get_random_error(&self) -> bool; + /// Serialize the StatusBytes to raw bytes + 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], + /// 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. - fn reboot_to_firmware_update() -> !; - + 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. - fn reboot_to_firmware_update_destructive() -> !; - + pub reboot_to_firmware_update_destructive: Option !>, /// Is device bootloader locked down? /// E.g., is secure boot enabled? - fn locked() -> bool; + pub locked: fn() -> bool, } -/// Trait indicating that a value can be used as a status -pub trait StatusBytes { - type Serialized: AsRef<[u8]>; - /// Set the flag indicating that the random generator could properly be created (`false`) or not (`true`) - fn set_random_error(&mut self, value: bool); - /// Get the flag indicating that the random generator could properly be created (`false`) or not (`true`) - fn get_random_error(&self) -> bool; - /// Serialize the StatusBytes to raw bytes - fn serialize(&self) -> Self::Serialized; -} - -pub struct App { +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 +impl App where T: TrussedClient, - R: Reboot, S: StatusBytes, C: Config, { @@ -217,22 +214,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 +248,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 +268,16 @@ 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, } } @@ -353,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) @@ -366,9 +325,13 @@ where Command::Update => { if self.user_present() { if input.first().copied() == Some(0x01) { - R::reboot_to_firmware_update_destructive(); + if let Some(f) = self.data.reboot_to_firmware_update_destructive { + f(); + } else { + return Err(Error::UnsupportedCommand); + } } else { - R::reboot_to_firmware_update(); + (self.data.reboot_to_firmware_update)(); } } else { return Err(Error::NotAvailable); @@ -376,16 +339,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 => { @@ -446,7 +411,7 @@ where return Ok(()); } syscall!(self.trussed.factory_reset_device()); - R::reboot(); + (self.data.reboot)(); } #[cfg(feature = "factory-reset")] Command::FactoryResetApp => { @@ -532,10 +497,9 @@ where } } -impl hid::App<'static> for App +impl hid::App<'static> for App where T: TrussedClient, - R: Reboot, S: StatusBytes, C: Config, { @@ -576,10 +540,9 @@ where } } -impl iso7816::App for App +impl iso7816::App for App where T: TrussedClient, - R: Reboot, S: StatusBytes, { // Solo management app @@ -588,10 +551,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 04d897e..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, Reboot, StatusBytes}; +pub use admin::{App, Data, StatusBytes}; pub use config::{ Config, ConfigError, ConfigField, ConfigValueMut, FieldType, ResetConfigResult, ResetSignal, ResetSignalAllocation,