From 028dcc5a089c7bdcd927feac6db8c6a4007ce1a8 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Tue, 31 Jan 2023 12:08:39 +0100 Subject: [PATCH] Move CTAPHID commands under a single vendor command Previously, we used one CTAPHID vendor command per application command. This makes it hard to extend the functionality because we have to synchronize the used vendor commands over all applications in the Trussed ecosystem. As an alternative, this patch introduces a new vendor command that acts as a namespace for the admin application. The actual application command is encoded in the first byte of the payload. In the future, we might want to make this configurable so that the runner can decide the namespace used by CTAPHID apps. For compatibility, the CTAPHID vendor commands are still available. This change also allows us to combine the CTAPHID and APDU command handling into a single dispatch function. --- src/admin.rs | 228 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 146 insertions(+), 82 deletions(-) diff --git a/src/admin.rs b/src/admin.rs index 56cf138..a9a0775 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,15 +1,21 @@ -use core::{convert::TryInto, marker::PhantomData}; +use core::{convert::TryInto, marker::PhantomData, time::Duration}; use ctaphid_dispatch::app::{self as hid, Command as HidCommand, Message}; use ctaphid_dispatch::command::VendorCommand; -use apdu_dispatch::{Command, command, response, app as apdu}; +use apdu_dispatch::{Command as ApduCommand, command, response, app as apdu}; use apdu_dispatch::iso7816::Status; use trussed::{ + types::Vec, syscall, Client as TrussedClient, }; pub const USER_PRESENCE_TIMEOUT_SECS: u32 = 15; +// New commands are only available over this vendor command (acting as a namespace for this +// application). The actual application command is stored in the first byte of the packet data. +const ADMIN: VendorCommand = VendorCommand::H72; + +// For compatibility, old commands are also available directly as separate vendor commands. const UPDATE: VendorCommand = VendorCommand::H51; const REBOOT: VendorCommand = VendorCommand::H53; const RNG: VendorCommand = VendorCommand::H60; @@ -17,6 +23,93 @@ const VERSION: VendorCommand = VendorCommand::H61; const UUID: VendorCommand = VendorCommand::H62; const LOCKED: VendorCommand = VendorCommand::H63; +// We also handle the standard wink command. +const WINK: HidCommand = HidCommand::Wink; // 0x08 + +const RNG_DATA_LEN: usize = 57; + +#[derive(PartialEq)] +enum Command { + Update, + Reboot, + Rng, + Version, + Uuid, + Locked, + Wink, +} + +impl TryFrom for Command { + type Error = Error; + + fn try_from(command: u8) -> Result { + // First, check the old commands. + if let Ok(command) = HidCommand::try_from(command) { + if let Ok(command) = command.try_into() { + return Ok(command); + } + } + + // Now check the new commands (none yet). + Err(Error::UnsupportedCommand) + } +} + +impl TryFrom for Command { + type Error = Error; + + fn try_from(command: HidCommand) -> Result { + match command { + WINK => Ok(Command::Wink), + HidCommand::Vendor(command) => command.try_into(), + _ => Err(Error::UnsupportedCommand) + } + } +} + +impl TryFrom for Command { + type Error = Error; + + fn try_from(command: VendorCommand) -> Result { + match command { + UPDATE => Ok(Command::Update), + REBOOT => Ok(Command::Reboot), + RNG => Ok(Command::Rng), + VERSION => Ok(Command::Version), + UUID => Ok(Command::Uuid), + LOCKED => Ok(Command::Locked), + _ => Err(Error::UnsupportedCommand), + } + } +} + +enum Error { + InvalidLength, + NotAvailable, + UnsupportedCommand, +} + +impl From for hid::Error { + fn from(error: Error) -> Self { + match error { + Error::InvalidLength => Self::InvalidLength, + // TODO: use more appropriate error code + Error::NotAvailable => Self::InvalidLength, + Error::UnsupportedCommand => Self::InvalidCommand, + } + } +} + +impl From for Status { + fn from(error: Error) -> Self { + match error { + Error::InvalidLength => Self::WrongLength, + Error::NotAvailable => Self::ConditionsOfUseNotSatisfied, + Error::UnsupportedCommand => Self::InstructionNotSupportedOrInvalid, + } + } +} + pub trait Reboot { /// Reboots the device. fn reboot() -> !; @@ -63,70 +156,77 @@ where T: TrussedClient, user_present.is_ok() } - -} - -impl hid::App for App -where T: TrussedClient, - R: Reboot -{ - fn commands(&self) -> &'static [HidCommand] { - &[ - HidCommand::Wink, - HidCommand::Vendor(UPDATE), - HidCommand::Vendor(REBOOT), - HidCommand::Vendor(RNG), - HidCommand::Vendor(VERSION), - HidCommand::Vendor(UUID), - HidCommand::Vendor(LOCKED), - ] - } - - fn call(&mut self, command: HidCommand, input_data: &Message, response: &mut Message) -> hid::AppResult { + fn exec(&mut self, command: Command, flag: Option, response: &mut Vec) -> Result<(), Error> { match command { - HidCommand::Vendor(REBOOT) => R::reboot(), - HidCommand::Vendor(LOCKED) => { - response.extend_from_slice( - &[R::locked() as u8] - ).ok(); + Command::Reboot => R::reboot(), + Command::Locked => { + response.push(R::locked().into()).ok(); } - HidCommand::Vendor(RNG) => { + Command::Rng => { // Fill the HID packet (57 bytes) response.extend_from_slice( - &syscall!(self.trussed.random_bytes(57)).bytes.as_slice() + &syscall!(self.trussed.random_bytes(RNG_DATA_LEN)).bytes, ).ok(); } - HidCommand::Vendor(UPDATE) => { + Command::Update => { if self.user_present() { - if input_data.len() > 0 && input_data[0] == 0x01 { + if flag == Some(0x01) { R::reboot_to_firmware_update_destructive(); } else { R::reboot_to_firmware_update(); } } else { - return Err(hid::Error::InvalidLength); + return Err(Error::NotAvailable); } } - HidCommand::Vendor(UUID) => { + Command::Uuid => { // Get UUID response.extend_from_slice(&self.uuid).ok(); } - HidCommand::Vendor(VERSION) => { + Command::Version => { // GET VERSION response.extend_from_slice(&self.version.to_be_bytes()).ok(); } - HidCommand::Wink => { + Command::Wink => { debug_now!("winking"); - syscall!(self.trussed.wink(core::time::Duration::from_secs(10))); - } - _ => { - return Err(hid::Error::InvalidCommand); + syscall!(self.trussed.wink(Duration::from_secs(10))); } } Ok(()) } } +impl hid::App for App +where T: TrussedClient, + R: Reboot +{ + fn commands(&self) -> &'static [HidCommand] { + &[ + HidCommand::Wink, + HidCommand::Vendor(ADMIN), + HidCommand::Vendor(UPDATE), + HidCommand::Vendor(REBOOT), + HidCommand::Vendor(RNG), + HidCommand::Vendor(VERSION), + HidCommand::Vendor(UUID), + HidCommand::Vendor(LOCKED), + ] + } + + fn call(&mut self, command: HidCommand, input_data: &Message, response: &mut Message) -> hid::AppResult { + let (command, flag) = if command == HidCommand::Vendor(ADMIN) { + // new mode: first input byte specifies the actual command + let (command, input) = input_data.split_first().ok_or(Error::InvalidLength)?; + let command = Command::try_from(*command)?; + (command, input.first()) + } else { + // old mode: directly use vendor commands + wink + (Command::try_from(command)?, input_data.first()) + }; + self.exec(command, flag.copied(), response).map_err(From::from) + } +} + impl iso7816::App for App where T: TrussedClient, R: Reboot @@ -142,58 +242,22 @@ where T: TrussedClient, R: Reboot { - fn select(&mut self, _apdu: &Command, _reply: &mut response::Data) -> apdu::Result { + fn select(&mut self, _apdu: &ApduCommand, _reply: &mut response::Data) -> apdu::Result { Ok(()) } fn deselect(&mut self) {} - fn call(&mut self, interface: apdu::Interface, apdu: &Command, reply: &mut response::Data) -> apdu::Result { + fn call(&mut self, interface: apdu::Interface, apdu: &ApduCommand, reply: &mut response::Data) -> apdu::Result { let instruction: u8 = apdu.instruction().into(); + let command = Command::try_from(instruction)?; - if instruction == 0x08 { - syscall!(self.trussed.wink(core::time::Duration::from_secs(10))); - return Ok(()); + // Reboot may only be called over USB + if command == Command::Reboot && interface != apdu::Interface::Contact { + return Err(Status::ConditionsOfUseNotSatisfied); } - let command: VendorCommand = instruction.try_into().map_err(|_e| Status::InstructionNotSupportedOrInvalid)?; - - match command { - REBOOT => R::reboot(), - LOCKED => { - // Random bytes - reply.extend_from_slice(&[R::locked() as u8]).ok(); - } - RNG => { - // Random bytes - reply.extend_from_slice(&syscall!(self.trussed.random_bytes(57)).bytes.as_slice()).ok(); - } - UPDATE => { - // Boot to mcuboot (only when contact interface) - if interface == apdu::Interface::Contact && self.user_present() - { - if apdu.p1 == 0x01 { - R::reboot_to_firmware_update_destructive(); - } else { - R::reboot_to_firmware_update(); - } - } - return Err(Status::ConditionsOfUseNotSatisfied); - } - UUID => { - // Get UUID - reply.extend_from_slice(&self.uuid).ok(); - } - VERSION => { - // Get version - reply.extend_from_slice(&self.version.to_be_bytes()[..]).ok(); - } - - _ => return Err(Status::InstructionNotSupportedOrInvalid), - - } - Ok(()) - + self.exec(command, Some(apdu.p1), reply).map_err(From::from) } }