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) } }