Skip to main content

admin_app/
admin.rs

1use super::Client as TrussedClient;
2use apdu_app::{CommandView, Interface};
3use cbor_smol::{cbor_deserialize, cbor_serialize_to};
4use core::{convert::TryInto, time::Duration};
5use ctaphid_app::{self as hid, Command as HidCommand, VendorCommand};
6use heapless::VecView;
7use heapless_bytes::BytesView;
8use iso7816::Status;
9#[cfg(feature = "factory-reset")]
10use littlefs2_core::PathBuf;
11use serde::Deserialize;
12use trussed::store::{Filestore, Store};
13use trussed_core::{syscall, try_syscall, InterruptFlag};
14
15use crate::config::{self, Config, ConfigError};
16use crate::migrations::Migrator;
17
18pub const USER_PRESENCE_TIMEOUT_SECS: u32 = 15;
19
20// New commands are only available over this vendor command (acting as a namespace for this
21// application).  The actual application command is stored in the first byte of the packet data.
22const ADMIN: VendorCommand = VendorCommand::H72;
23const STATUS: u8 = 0x80;
24const TEST_SE050: u8 = 0x81;
25const GET_CONFIG: u8 = 0x82;
26const SET_CONFIG: u8 = 0x83;
27#[cfg(feature = "factory-reset")]
28const FACTORY_RESET: u8 = 0x84;
29#[cfg(feature = "factory-reset")]
30const FACTORY_RESET_APP: u8 = 0x85;
31const LIST_AVAILABLE_FIELDS: u8 = 0x86;
32
33// For compatibility, old commands are also available directly as separate vendor commands.
34const UPDATE: VendorCommand = VendorCommand::H51;
35const REBOOT: VendorCommand = VendorCommand::H53;
36const RNG: VendorCommand = VendorCommand::H60;
37const VERSION: VendorCommand = VendorCommand::H61;
38const UUID: VendorCommand = VendorCommand::H62;
39const LOCKED: VendorCommand = VendorCommand::H63;
40
41// We also handle the standard wink command.
42const WINK: HidCommand = HidCommand::Wink; // 0x08
43
44const RNG_DATA_LEN: usize = 57;
45
46const CONFIG_OK: u8 = 0x00;
47#[cfg(feature = "factory-reset")]
48const FACTORY_RESET_OK: u8 = 0x00;
49#[cfg(feature = "factory-reset")]
50const FACTORY_RESET_NOT_CONFIRMED: u8 = 0x01;
51#[cfg(feature = "factory-reset")]
52const FACTORY_RESET_APP_NOT_ALLOWED: u8 = 0x02;
53#[cfg(feature = "factory-reset")]
54const FACTORY_RESET_APP_FAILED_PARSE: u8 = 0x03;
55
56#[derive(PartialEq, Debug)]
57enum Command {
58    Update,
59    Reboot,
60    Rng,
61    Version,
62    Uuid,
63    Locked,
64    Wink,
65    Status,
66    TestSe05X,
67    GetConfig,
68    SetConfig,
69    #[cfg(feature = "factory-reset")]
70    FactoryReset,
71    #[cfg(feature = "factory-reset")]
72    FactoryResetApp,
73    ListAvailableFields,
74}
75
76impl TryFrom<u8> for Command {
77    type Error = Error;
78
79    fn try_from(command: u8) -> Result<Self, Self::Error> {
80        // First, check the old commands.
81        if let Ok(command) = HidCommand::try_from(command) {
82            if let Ok(command) = command.try_into() {
83                return Ok(command);
84            }
85        }
86
87        // Now check the new commands.
88        match command {
89            STATUS => Ok(Command::Status),
90            TEST_SE050 => Ok(Command::TestSe05X),
91            GET_CONFIG => Ok(Command::GetConfig),
92            SET_CONFIG => Ok(Command::SetConfig),
93            #[cfg(feature = "factory-reset")]
94            FACTORY_RESET => Ok(Command::FactoryReset),
95            #[cfg(feature = "factory-reset")]
96            FACTORY_RESET_APP => Ok(Command::FactoryResetApp),
97            LIST_AVAILABLE_FIELDS => Ok(Command::ListAvailableFields),
98            _ => Err(Error::UnsupportedCommand),
99        }
100    }
101}
102
103impl TryFrom<HidCommand> for Command {
104    type Error = Error;
105
106    fn try_from(command: HidCommand) -> Result<Self, Self::Error> {
107        match command {
108            WINK => Ok(Command::Wink),
109            HidCommand::Vendor(command) => command.try_into(),
110            _ => Err(Error::UnsupportedCommand),
111        }
112    }
113}
114
115impl TryFrom<VendorCommand> for Command {
116    type Error = Error;
117
118    fn try_from(command: VendorCommand) -> Result<Self, Self::Error> {
119        match command {
120            UPDATE => Ok(Command::Update),
121            REBOOT => Ok(Command::Reboot),
122            RNG => Ok(Command::Rng),
123            VERSION => Ok(Command::Version),
124            UUID => Ok(Command::Uuid),
125            LOCKED => Ok(Command::Locked),
126            _ => Err(Error::UnsupportedCommand),
127        }
128    }
129}
130
131enum Error {
132    InvalidLength,
133    NotAvailable,
134    UnsupportedCommand,
135}
136
137impl From<Error> for hid::Error {
138    fn from(error: Error) -> Self {
139        match error {
140            Error::InvalidLength => Self::InvalidLength,
141            // TODO: use more appropriate error code
142            Error::NotAvailable => Self::InvalidLength,
143            Error::UnsupportedCommand => Self::InvalidCommand,
144        }
145    }
146}
147
148impl From<Error> for Status {
149    fn from(error: Error) -> Self {
150        match error {
151            Error::InvalidLength => Self::WrongLength,
152            Error::NotAvailable => Self::ConditionsOfUseNotSatisfied,
153            Error::UnsupportedCommand => Self::InstructionNotSupportedOrInvalid,
154        }
155    }
156}
157
158#[derive(Debug, Deserialize)]
159struct SetConfigRequest<'a> {
160    key: &'a str,
161    value: &'a str,
162}
163
164/// Trait indicating that a value can be used as a status
165pub trait StatusBytes {
166    type Serialized: AsRef<[u8]>;
167    /// Set the flag indicating that the random generator could properly be created (`false`) or not (`true`)
168    fn set_random_error(&mut self, value: bool);
169    /// Get the flag indicating that the random generator could properly be created (`false`) or not (`true`)
170    fn get_random_error(&self) -> bool;
171    /// Serialize the StatusBytes to raw bytes
172    fn serialize(&self) -> Self::Serialized;
173}
174
175#[derive(Clone, Copy)]
176pub struct Data {
177    pub uuid: [u8; 16],
178    pub version: u32,
179    pub full_version: &'static str,
180    pub migrations: &'static [Migrator],
181    /// Reboots the device.
182    pub reboot: fn() -> !,
183    /// Reboots the device.
184    ///
185    /// Presuming the device has a separate mode of operation that
186    /// allows updating its firmware (for instance, a bootloader),
187    /// reboots the device into this mode.
188    pub reboot_to_firmware_update: fn(),
189    /// Reboots the device.
190    ///
191    /// Presuming the device has a separate destructive but more
192    /// reliable way of rebooting into the firmware mode of operation,
193    /// does so.
194    pub reboot_to_firmware_update_destructive: Option<fn() -> !>,
195    /// Is device bootloader locked down?
196    /// E.g., is secure boot enabled?
197    pub locked: fn() -> bool,
198}
199
200pub struct App<T, S, C = ()> {
201    trussed: T,
202    data: Data,
203    status: S,
204    config: C,
205}
206
207impl<T, S, C> App<T, S, C>
208where
209    T: TrussedClient,
210    S: StatusBytes,
211    C: Config,
212{
213    /// Create an admin app instance, loading the configuration from the filesystem.
214    pub fn load_config<F: Filestore>(
215        client: T,
216        filestore: &mut F,
217        data: Data,
218        status: S,
219    ) -> Result<Self, (T, ConfigError)> {
220        match config::load(filestore) {
221            Ok(config) => Ok(Self::new(client, data, status, config)),
222            Err(err) => {
223                error!("failed to load configuration: {:?}", err);
224                Err((client, err))
225            }
226        }
227    }
228
229    pub fn migrate<F: Filestore>(
230        &mut self,
231        to_version: u32,
232        store: impl Store,
233        filestore: &mut F,
234    ) -> Result<(), ConfigError> {
235        let Some(current_version) = self.config.migration_version() else {
236            // Migrate cannot be done for configurations that don't provide storage of the filesystem version
237            return Err(ConfigError::InvalidValue);
238        };
239
240        if current_version == to_version {
241            return Ok(());
242        }
243
244        if to_version < current_version {
245            return Err(ConfigError::InvalidValue);
246        }
247
248        let internal = store.ifs();
249        let external = store.efs();
250
251        for migration in self.data.migrations {
252            if migration.version > current_version && migration.version <= to_version {
253                (migration.migrate)(internal, external).map_err(|_err| {
254                    error_now!("Migration failed: {_err:?}");
255                    ConfigError::WriteFailed
256                })?;
257            }
258        }
259
260        if !self.config.set_migration_version(to_version) {
261            return Err(ConfigError::InvalidValue);
262        }
263        config::save_filestore(filestore, &self.config)
264    }
265
266    /// Create an admin app instance without the configuration mechanism, using the default config
267    /// values.
268    ///
269    /// This is only intended for debugging, testing and example code.  In production,
270    /// [`App::load_config`][] should be used.
271    pub fn with_default_config(client: T, data: Data, status: S) -> Self {
272        Self::new(client, data, status, Default::default())
273    }
274
275    fn new(client: T, data: Data, status: S, config: C) -> Self {
276        Self {
277            trussed: client,
278            data,
279            status,
280            config,
281        }
282    }
283
284    pub fn config(&self) -> &C {
285        &self.config
286    }
287
288    pub fn config_mut(&mut self) -> &mut C {
289        &mut self.config
290    }
291
292    pub fn save_config_filestore<F: Filestore>(
293        &mut self,
294        filestore: &mut F,
295    ) -> Result<(), ConfigError> {
296        config::save_filestore(filestore, &self.config)
297    }
298
299    fn user_present(&mut self) -> bool {
300        let user_present = syscall!(self
301            .trussed
302            .confirm_user_present(USER_PRESENCE_TIMEOUT_SECS * 1000))
303        .result;
304        user_present.is_ok()
305    }
306
307    fn exec(
308        &mut self,
309        command: Command,
310        input: &[u8],
311        response: &mut VecView<u8>,
312    ) -> Result<(), Error> {
313        debug_now!("Executing command: {command:?}");
314        match command {
315            Command::Reboot => (self.data.reboot)(),
316            Command::Locked => {
317                response.push((self.data.locked)().into()).ok();
318            }
319            Command::Rng => {
320                // Fill the HID packet (57 bytes)
321                response
322                    .extend_from_slice(&syscall!(self.trussed.random_bytes(RNG_DATA_LEN)).bytes)
323                    .ok();
324            }
325            Command::Update => {
326                if self.user_present() {
327                    if input.first().copied() == Some(0x01) {
328                        if let Some(f) = self.data.reboot_to_firmware_update_destructive {
329                            f();
330                        } else {
331                            return Err(Error::UnsupportedCommand);
332                        }
333                    } else {
334                        (self.data.reboot_to_firmware_update)();
335                    }
336                } else {
337                    return Err(Error::NotAvailable);
338                }
339            }
340            Command::Uuid => {
341                // Get UUID
342                response.extend_from_slice(&self.data.uuid).ok();
343            }
344            Command::Version => {
345                // GET VERSION
346                if input.first().copied() == Some(0x01) {
347                    response
348                        .extend_from_slice(self.data.full_version.as_bytes())
349                        .ok();
350                } else {
351                    response
352                        .extend_from_slice(&self.data.version.to_be_bytes())
353                        .ok();
354                }
355            }
356            Command::Wink => {
357                debug_now!("winking");
358                syscall!(self.trussed.wink(Duration::from_secs(10)));
359            }
360            Command::Status => {
361                if !self.status.get_random_error() {
362                    let is_random_working = try_syscall!(self.trussed.random_bytes(1)).is_ok();
363                    self.status.set_random_error(!is_random_working);
364                }
365                response
366                    .extend_from_slice(self.status.serialize().as_ref())
367                    .ok();
368            }
369            Command::TestSe05X => {
370                #[cfg(feature = "se050")]
371                {
372                    let rep = syscall!(self.trussed.test_se050());
373                    response.extend_from_slice(&rep.reply).ok();
374                    return Ok(());
375                }
376                #[cfg(not(feature = "se050"))]
377                {
378                    return Err(Error::UnsupportedCommand);
379                }
380            }
381            Command::GetConfig => {
382                // Response: 1 status byte, then data if status == 0
383                response.push(CONFIG_OK).ok();
384                if let Err(error) = self.get_config(input, response) {
385                    response.clear();
386                    response.push(error.into()).ok();
387                }
388            }
389            Command::SetConfig => {
390                // Response: 1 status byte
391                let status = match self.set_config(input) {
392                    Ok(()) => CONFIG_OK,
393                    Err(error) => error.into(),
394                };
395                response.push(status).ok();
396            }
397            Command::ListAvailableFields => {
398                cbor_serialize_to::<_, &mut VecView<u8>>(
399                    &self.config.list_available_fields(),
400                    response,
401                )
402                .ok();
403                return Ok(());
404            }
405            #[cfg(feature = "factory-reset")]
406            Command::FactoryReset => {
407                debug_now!("Factory resetting the device");
408                if let Err(_err) = syscall!(self.trussed.confirm_user_present(15 * 1000)).result {
409                    debug_now!("Failed to verify user presence: {_err:?}");
410                    response.push(FACTORY_RESET_NOT_CONFIRMED).ok();
411                    return Ok(());
412                }
413                syscall!(self.trussed.factory_reset_device());
414                (self.data.reboot)();
415            }
416            #[cfg(feature = "factory-reset")]
417            Command::FactoryResetApp => {
418                let Ok(client) = core::str::from_utf8(input) else {
419                    response.push(FACTORY_RESET_APP_FAILED_PARSE).ok();
420                    return Ok(());
421                };
422                let Ok(path) = PathBuf::try_from(client) else {
423                    response.push(FACTORY_RESET_APP_FAILED_PARSE).ok();
424                    return Ok(());
425                };
426
427                let Some((_, flag)) = self.config().reset_client_id(client) else {
428                    response.push(FACTORY_RESET_APP_NOT_ALLOWED).ok();
429                    return Ok(());
430                };
431
432                if let Err(_err) = syscall!(self.trussed.confirm_user_present(15 * 1000)).result {
433                    debug_now!("Failed to verify user presence: {_err:?}");
434                    response.push(FACTORY_RESET_NOT_CONFIRMED).ok();
435                    return Ok(());
436                }
437
438                match self.config.reset_client_config(client) {
439                    crate::config::ResetConfigResult::Changed => {
440                        flag.set_config_changed();
441                        config::save(&mut self.trussed, &self.config).map_err(|_err| {
442                            error_now!("Failed to save config: {_err:?}");
443                            Error::InvalidLength
444                        })?;
445                        syscall!(self.trussed.factory_reset_client(&path));
446                    }
447                    crate::config::ResetConfigResult::Unchanged => {
448                        // No need to factory reset if already factory reset
449                        if flag.set_factory_reset() {
450                            syscall!(self.trussed.factory_reset_client(&path));
451                        }
452                    }
453                    crate::config::ResetConfigResult::WrongKey => {
454                        response.push(FACTORY_RESET_APP_NOT_ALLOWED).ok();
455                        return Ok(());
456                    }
457                }
458
459                response.push(FACTORY_RESET_OK).ok();
460            }
461        }
462        Ok(())
463    }
464
465    fn get_config(&mut self, input: &[u8], response: &mut VecView<u8>) -> Result<(), ConfigError> {
466        let key = core::str::from_utf8(input).map_err(|_| ConfigError::InvalidKey)?;
467        config::get(&mut self.config, key, response)
468    }
469
470    fn set_config(&mut self, input: &[u8]) -> Result<(), ConfigError> {
471        let request: SetConfigRequest<'_> =
472            cbor_deserialize(input).map_err(|_| ConfigError::DeserializationFailed)?;
473        let reset_client_id = self.config.reset_client_id(request.key);
474
475        if reset_client_id.is_some() {
476            if let Err(_err) = syscall!(self.trussed.confirm_user_present(15 * 1000)).result {
477                debug_now!("Failed to verify user presence: {_err:?}");
478                return Err(ConfigError::NotConfirmed);
479            }
480        }
481
482        config::set(&mut self.config, request.key, request.value)?;
483        if let Some((client, signal)) = reset_client_id {
484            signal.set_config_changed();
485            syscall!(self.trussed.factory_reset_client(client));
486        }
487
488        config::save(&mut self.trussed, &self.config)
489    }
490
491    pub fn status(&self) -> &S {
492        &self.status
493    }
494
495    pub fn status_mut(&mut self) -> &mut S {
496        &mut self.status
497    }
498}
499
500impl<T, S, C> hid::App<'static> for App<T, S, C>
501where
502    T: TrussedClient,
503    S: StatusBytes,
504    C: Config,
505{
506    fn commands(&self) -> &'static [HidCommand] {
507        &[
508            HidCommand::Wink,
509            HidCommand::Vendor(ADMIN),
510            HidCommand::Vendor(UPDATE),
511            HidCommand::Vendor(REBOOT),
512            HidCommand::Vendor(RNG),
513            HidCommand::Vendor(VERSION),
514            HidCommand::Vendor(UUID),
515            HidCommand::Vendor(LOCKED),
516        ]
517    }
518
519    fn call(
520        &mut self,
521        command: HidCommand,
522        input_data: &[u8],
523        response: &mut BytesView,
524    ) -> Result<(), hid::Error> {
525        let (command, input) = if command == HidCommand::Vendor(ADMIN) {
526            // new mode: first input byte specifies the actual command
527            let (command, input) = input_data.split_first().ok_or(Error::InvalidLength)?;
528            let command = Command::try_from(*command)?;
529            (command, input)
530        } else {
531            // old mode: directly use vendor commands + wink
532            (Command::try_from(command)?, input_data)
533        };
534        self.exec(command, input, response.as_mut())
535            .map_err(From::from)
536    }
537
538    fn interrupt(&self) -> Option<&'static InterruptFlag> {
539        self.trussed.interrupt()
540    }
541}
542
543impl<T, S, C> iso7816::App for App<T, S, C>
544where
545    T: TrussedClient,
546    S: StatusBytes,
547{
548    // Solo management app
549    fn aid(&self) -> iso7816::Aid {
550        iso7816::Aid::new(&[0xA0, 0x00, 0x00, 0x08, 0x47, 0x00, 0x00, 0x00, 0x01])
551    }
552}
553
554impl<T, S, C> apdu_app::App for App<T, S, C>
555where
556    T: TrussedClient,
557    S: StatusBytes,
558    C: Config,
559{
560    fn select(
561        &mut self,
562        _interface: Interface,
563        _apdu: CommandView<'_>,
564        _reply: &mut heapless::VecView<u8>,
565    ) -> apdu_app::Result {
566        Ok(())
567    }
568
569    fn deselect(&mut self) {}
570
571    fn call(
572        &mut self,
573        interface: Interface,
574        apdu: CommandView<'_>,
575        reply: &mut heapless::VecView<u8>,
576    ) -> apdu_app::Result {
577        let instruction: u8 = apdu.instruction().into();
578        let command = Command::try_from(instruction)?;
579
580        // Reboot may only be called over USB
581        if command == Command::Reboot && interface != Interface::Contact {
582            return Err(Status::ConditionsOfUseNotSatisfied);
583        }
584
585        // The Update and Version commands use the P1 value to select an operation mode. As we
586        // cannot model this in the CTAPHID application, we pretend that we received the flag as
587        // the command payload.
588        if command == Command::Update || command == Command::Version {
589            self.exec(command, &[apdu.p1], reply)
590        } else {
591            self.exec(command, apdu.data(), reply)
592        }
593        .map_err(From::from)
594    }
595}