Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "admin-app"
version = "0.3.0"
version = "0.4.0"
authors = ["Conor Patrick <conor@solokeys.com>", "Nicolas Stalder <nicolas@solokeys.com>"]
repository = "https://github.com/solokeys/admin-app"
edition = "2021"
Expand Down
136 changes: 49 additions & 87 deletions src/admin.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -161,78 +161,64 @@ 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<fn() -> !>,
/// 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<T, R, S, C = ()> {
pub struct App<T, S, C = ()> {
trussed: T,
uuid: [u8; 16],
version: u32,
full_version: &'static str,
data: Data,
status: S,
boot_interface: PhantomData<R>,
config: C,
migrations: &'static [Migrator],
}

impl<T, R, S, C> App<T, R, S, C>
impl<T, S, C> App<T, S, C>
where
T: TrussedClient,
R: Reboot,
S: StatusBytes,
C: Config,
{
/// Create an admin app instance, loading the configuration from the filesystem.
pub fn load_config<F: Filestore>(
client: T,
filestore: &mut F,
uuid: [u8; 16],
version: u32,
full_version: &'static str,
data: Data,
status: S,
migrations: &'static [Migrator],
) -> Result<Self, (T, ConfigError)> {
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))
Expand Down Expand Up @@ -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:?}");
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -366,26 +325,32 @@ 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);
}
}
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 => {
Expand Down Expand Up @@ -446,7 +411,7 @@ where
return Ok(());
}
syscall!(self.trussed.factory_reset_device());
R::reboot();
(self.data.reboot)();
}
#[cfg(feature = "factory-reset")]
Command::FactoryResetApp => {
Expand Down Expand Up @@ -532,10 +497,9 @@ where
}
}

impl<T, R, S, C> hid::App<'static> for App<T, R, S, C>
impl<T, S, C> hid::App<'static> for App<T, S, C>
where
T: TrussedClient,
R: Reboot,
S: StatusBytes,
C: Config,
{
Expand Down Expand Up @@ -576,10 +540,9 @@ where
}
}

impl<T, R, S, C> iso7816::App for App<T, R, S, C>
impl<T, S, C> iso7816::App for App<T, S, C>
where
T: TrussedClient,
R: Reboot,
S: StatusBytes,
{
// Solo management app
Expand All @@ -588,10 +551,9 @@ where
}
}

impl<T, R, S, C> apdu_app::App for App<T, R, S, C>
impl<T, S, C> apdu_app::App for App<T, S, C>
where
T: TrussedClient,
R: Reboot,
S: StatusBytes,
C: Config,
{
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading