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, marker::PhantomData, 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
164pub trait Reboot {
165    /// Reboots the device.
166    fn reboot() -> !;
167
168    /// Reboots the device.
169    ///
170    /// Presuming the device has a separate mode of operation that
171    /// allows updating its firmware (for instance, a bootloader),
172    /// reboots the device into this mode.
173    fn reboot_to_firmware_update() -> !;
174
175    /// Reboots the device.
176    ///
177    /// Presuming the device has a separate destructive but more
178    /// reliable way of rebooting into the firmware mode of operation,
179    /// does so.
180    fn reboot_to_firmware_update_destructive() -> !;
181
182    /// Is device bootloader locked down?
183    /// E.g., is secure boot enabled?
184    fn locked() -> bool;
185}
186
187/// Trait indicating that a value can be used as a status
188pub trait StatusBytes {
189    type Serialized: AsRef<[u8]>;
190    /// Set the flag indicating that the random generator could properly be created (`false`) or not (`true`)
191    fn set_random_error(&mut self, value: bool);
192    /// Get the flag indicating that the random generator could properly be created (`false`) or not (`true`)
193    fn get_random_error(&self) -> bool;
194    /// Serialize the StatusBytes to raw bytes
195    fn serialize(&self) -> Self::Serialized;
196}
197
198pub struct App<T, R, S, C = ()> {
199    trussed: T,
200    uuid: [u8; 16],
201    version: u32,
202    full_version: &'static str,
203    status: S,
204    boot_interface: PhantomData<R>,
205    config: C,
206    migrations: &'static [Migrator],
207}
208
209impl<T, R, S, C> App<T, R, S, C>
210where
211    T: TrussedClient,
212    R: Reboot,
213    S: StatusBytes,
214    C: Config,
215{
216    /// Create an admin app instance, loading the configuration from the filesystem.
217    pub fn load_config<F: Filestore>(
218        client: T,
219        filestore: &mut F,
220        uuid: [u8; 16],
221        version: u32,
222        full_version: &'static str,
223        status: S,
224        migrations: &'static [Migrator],
225    ) -> Result<Self, (T, ConfigError)> {
226        match config::load(filestore) {
227            Ok(config) => Ok(Self::new(
228                client,
229                uuid,
230                version,
231                full_version,
232                status,
233                config,
234                migrations,
235            )),
236            Err(err) => {
237                error!("failed to load configuration: {:?}", err);
238                Err((client, err))
239            }
240        }
241    }
242
243    pub fn migrate<F: Filestore>(
244        &mut self,
245        to_version: u32,
246        store: impl Store,
247        filestore: &mut F,
248    ) -> Result<(), ConfigError> {
249        let Some(current_version) = self.config.migration_version() else {
250            // Migrate cannot be done for configurations that don't provide storage of the filesystem version
251            return Err(ConfigError::InvalidValue);
252        };
253
254        if current_version == to_version {
255            return Ok(());
256        }
257
258        if to_version < current_version {
259            return Err(ConfigError::InvalidValue);
260        }
261
262        let internal = store.ifs();
263        let external = store.efs();
264
265        for migration in self.migrations {
266            if migration.version > current_version && migration.version <= to_version {
267                (migration.migrate)(internal, external).map_err(|_err| {
268                    error_now!("Migration failed: {_err:?}");
269                    ConfigError::WriteFailed
270                })?;
271            }
272        }
273
274        if !self.config.set_migration_version(to_version) {
275            return Err(ConfigError::InvalidValue);
276        }
277        config::save_filestore(filestore, &self.config)
278    }
279
280    /// Create an admin app instance without the configuration mechanism, using the default config
281    /// values.
282    ///
283    /// This is only intended for debugging, testing and example code.  In production,
284    /// [`App::load_config`][] should be used.
285    pub fn with_default_config(
286        client: T,
287        uuid: [u8; 16],
288        version: u32,
289        full_version: &'static str,
290        status: S,
291        migrations: &'static [Migrator],
292    ) -> Self {
293        Self::new(
294            client,
295            uuid,
296            version,
297            full_version,
298            status,
299            Default::default(),
300            migrations,
301        )
302    }
303
304    fn new(
305        client: T,
306        uuid: [u8; 16],
307        version: u32,
308        full_version: &'static str,
309        status: S,
310        config: C,
311        migrations: &'static [Migrator],
312    ) -> Self {
313        Self {
314            trussed: client,
315            uuid,
316            version,
317            full_version,
318            status,
319            boot_interface: PhantomData,
320            config,
321            migrations,
322        }
323    }
324
325    pub fn config(&self) -> &C {
326        &self.config
327    }
328
329    pub fn config_mut(&mut self) -> &mut C {
330        &mut self.config
331    }
332
333    pub fn save_config_filestore<F: Filestore>(
334        &mut self,
335        filestore: &mut F,
336    ) -> Result<(), ConfigError> {
337        config::save_filestore(filestore, &self.config)
338    }
339
340    fn user_present(&mut self) -> bool {
341        let user_present = syscall!(self
342            .trussed
343            .confirm_user_present(USER_PRESENCE_TIMEOUT_SECS * 1000))
344        .result;
345        user_present.is_ok()
346    }
347
348    fn exec(
349        &mut self,
350        command: Command,
351        input: &[u8],
352        response: &mut VecView<u8>,
353    ) -> Result<(), Error> {
354        debug_now!("Executing command: {command:?}");
355        match command {
356            Command::Reboot => R::reboot(),
357            Command::Locked => {
358                response.push(R::locked().into()).ok();
359            }
360            Command::Rng => {
361                // Fill the HID packet (57 bytes)
362                response
363                    .extend_from_slice(&syscall!(self.trussed.random_bytes(RNG_DATA_LEN)).bytes)
364                    .ok();
365            }
366            Command::Update => {
367                if self.user_present() {
368                    if input.first().copied() == Some(0x01) {
369                        R::reboot_to_firmware_update_destructive();
370                    } else {
371                        R::reboot_to_firmware_update();
372                    }
373                } else {
374                    return Err(Error::NotAvailable);
375                }
376            }
377            Command::Uuid => {
378                // Get UUID
379                response.extend_from_slice(&self.uuid).ok();
380            }
381            Command::Version => {
382                // GET VERSION
383                if input.first().copied() == Some(0x01) {
384                    response
385                        .extend_from_slice(self.full_version.as_bytes())
386                        .ok();
387                } else {
388                    response.extend_from_slice(&self.version.to_be_bytes()).ok();
389                }
390            }
391            Command::Wink => {
392                debug_now!("winking");
393                syscall!(self.trussed.wink(Duration::from_secs(10)));
394            }
395            Command::Status => {
396                if !self.status.get_random_error() {
397                    let is_random_working = try_syscall!(self.trussed.random_bytes(1)).is_ok();
398                    self.status.set_random_error(!is_random_working);
399                }
400                response
401                    .extend_from_slice(self.status.serialize().as_ref())
402                    .ok();
403            }
404            Command::TestSe05X => {
405                #[cfg(feature = "se050")]
406                {
407                    let rep = syscall!(self.trussed.test_se050());
408                    response.extend_from_slice(&rep.reply).ok();
409                    return Ok(());
410                }
411                #[cfg(not(feature = "se050"))]
412                {
413                    return Err(Error::UnsupportedCommand);
414                }
415            }
416            Command::GetConfig => {
417                // Response: 1 status byte, then data if status == 0
418                response.push(CONFIG_OK).ok();
419                if let Err(error) = self.get_config(input, response) {
420                    response.clear();
421                    response.push(error.into()).ok();
422                }
423            }
424            Command::SetConfig => {
425                // Response: 1 status byte
426                let status = match self.set_config(input) {
427                    Ok(()) => CONFIG_OK,
428                    Err(error) => error.into(),
429                };
430                response.push(status).ok();
431            }
432            Command::ListAvailableFields => {
433                cbor_serialize_to::<_, &mut VecView<u8>>(
434                    &self.config.list_available_fields(),
435                    response,
436                )
437                .ok();
438                return Ok(());
439            }
440            #[cfg(feature = "factory-reset")]
441            Command::FactoryReset => {
442                debug_now!("Factory resetting the device");
443                if let Err(_err) = syscall!(self.trussed.confirm_user_present(15 * 1000)).result {
444                    debug_now!("Failed to verify user presence: {_err:?}");
445                    response.push(FACTORY_RESET_NOT_CONFIRMED).ok();
446                    return Ok(());
447                }
448                syscall!(self.trussed.factory_reset_device());
449                R::reboot();
450            }
451            #[cfg(feature = "factory-reset")]
452            Command::FactoryResetApp => {
453                let Ok(client) = core::str::from_utf8(input) else {
454                    response.push(FACTORY_RESET_APP_FAILED_PARSE).ok();
455                    return Ok(());
456                };
457                let Ok(path) = PathBuf::try_from(client) else {
458                    response.push(FACTORY_RESET_APP_FAILED_PARSE).ok();
459                    return Ok(());
460                };
461
462                let Some((_, flag)) = self.config().reset_client_id(client) else {
463                    response.push(FACTORY_RESET_APP_NOT_ALLOWED).ok();
464                    return Ok(());
465                };
466
467                if let Err(_err) = syscall!(self.trussed.confirm_user_present(15 * 1000)).result {
468                    debug_now!("Failed to verify user presence: {_err:?}");
469                    response.push(FACTORY_RESET_NOT_CONFIRMED).ok();
470                    return Ok(());
471                }
472
473                match self.config.reset_client_config(client) {
474                    crate::config::ResetConfigResult::Changed => {
475                        flag.set_config_changed();
476                        config::save(&mut self.trussed, &self.config).map_err(|_err| {
477                            error_now!("Failed to save config: {_err:?}");
478                            Error::InvalidLength
479                        })?;
480                        syscall!(self.trussed.factory_reset_client(&path));
481                    }
482                    crate::config::ResetConfigResult::Unchanged => {
483                        // No need to factory reset if already factory reset
484                        if flag.set_factory_reset() {
485                            syscall!(self.trussed.factory_reset_client(&path));
486                        }
487                    }
488                    crate::config::ResetConfigResult::WrongKey => {
489                        response.push(FACTORY_RESET_APP_NOT_ALLOWED).ok();
490                        return Ok(());
491                    }
492                }
493
494                response.push(FACTORY_RESET_OK).ok();
495            }
496        }
497        Ok(())
498    }
499
500    fn get_config(&mut self, input: &[u8], response: &mut VecView<u8>) -> Result<(), ConfigError> {
501        let key = core::str::from_utf8(input).map_err(|_| ConfigError::InvalidKey)?;
502        config::get(&mut self.config, key, response)
503    }
504
505    fn set_config(&mut self, input: &[u8]) -> Result<(), ConfigError> {
506        let request: SetConfigRequest<'_> =
507            cbor_deserialize(input).map_err(|_| ConfigError::DeserializationFailed)?;
508        let reset_client_id = self.config.reset_client_id(request.key);
509
510        if reset_client_id.is_some() {
511            if let Err(_err) = syscall!(self.trussed.confirm_user_present(15 * 1000)).result {
512                debug_now!("Failed to verify user presence: {_err:?}");
513                return Err(ConfigError::NotConfirmed);
514            }
515        }
516
517        config::set(&mut self.config, request.key, request.value)?;
518        if let Some((client, signal)) = reset_client_id {
519            signal.set_config_changed();
520            syscall!(self.trussed.factory_reset_client(client));
521        }
522
523        config::save(&mut self.trussed, &self.config)
524    }
525
526    pub fn status(&self) -> &S {
527        &self.status
528    }
529
530    pub fn status_mut(&mut self) -> &mut S {
531        &mut self.status
532    }
533}
534
535impl<T, R, S, C> hid::App<'static> for App<T, R, S, C>
536where
537    T: TrussedClient,
538    R: Reboot,
539    S: StatusBytes,
540    C: Config,
541{
542    fn commands(&self) -> &'static [HidCommand] {
543        &[
544            HidCommand::Wink,
545            HidCommand::Vendor(ADMIN),
546            HidCommand::Vendor(UPDATE),
547            HidCommand::Vendor(REBOOT),
548            HidCommand::Vendor(RNG),
549            HidCommand::Vendor(VERSION),
550            HidCommand::Vendor(UUID),
551            HidCommand::Vendor(LOCKED),
552        ]
553    }
554
555    fn call(
556        &mut self,
557        command: HidCommand,
558        input_data: &[u8],
559        response: &mut BytesView,
560    ) -> Result<(), hid::Error> {
561        let (command, input) = if command == HidCommand::Vendor(ADMIN) {
562            // new mode: first input byte specifies the actual command
563            let (command, input) = input_data.split_first().ok_or(Error::InvalidLength)?;
564            let command = Command::try_from(*command)?;
565            (command, input)
566        } else {
567            // old mode: directly use vendor commands + wink
568            (Command::try_from(command)?, input_data)
569        };
570        self.exec(command, input, response.as_mut())
571            .map_err(From::from)
572    }
573
574    fn interrupt(&self) -> Option<&'static InterruptFlag> {
575        self.trussed.interrupt()
576    }
577}
578
579impl<T, R, S, C> iso7816::App for App<T, R, S, C>
580where
581    T: TrussedClient,
582    R: Reboot,
583    S: StatusBytes,
584{
585    // Solo management app
586    fn aid(&self) -> iso7816::Aid {
587        iso7816::Aid::new(&[0xA0, 0x00, 0x00, 0x08, 0x47, 0x00, 0x00, 0x00, 0x01])
588    }
589}
590
591impl<T, R, S, C> apdu_app::App for App<T, R, S, C>
592where
593    T: TrussedClient,
594    R: Reboot,
595    S: StatusBytes,
596    C: Config,
597{
598    fn select(
599        &mut self,
600        _interface: Interface,
601        _apdu: CommandView<'_>,
602        _reply: &mut heapless::VecView<u8>,
603    ) -> apdu_app::Result {
604        Ok(())
605    }
606
607    fn deselect(&mut self) {}
608
609    fn call(
610        &mut self,
611        interface: Interface,
612        apdu: CommandView<'_>,
613        reply: &mut heapless::VecView<u8>,
614    ) -> apdu_app::Result {
615        let instruction: u8 = apdu.instruction().into();
616        let command = Command::try_from(instruction)?;
617
618        // Reboot may only be called over USB
619        if command == Command::Reboot && interface != Interface::Contact {
620            return Err(Status::ConditionsOfUseNotSatisfied);
621        }
622
623        // The Update and Version commands use the P1 value to select an operation mode. As we
624        // cannot model this in the CTAPHID application, we pretend that we received the flag as
625        // the command payload.
626        if command == Command::Update || command == Command::Version {
627            self.exec(command, &[apdu.p1], reply)
628        } else {
629            self.exec(command, apdu.data(), reply)
630        }
631        .map_err(From::from)
632    }
633}