Skip to content
Open
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
228 changes: 146 additions & 82 deletions src/admin.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,115 @@
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;
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<u8> for Command {
type Error = Error;

fn try_from(command: u8) -> Result<Self, Self::Error> {
// 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<HidCommand> for Command {
type Error = Error;

fn try_from(command: HidCommand) -> Result<Self, Self::Error> {
match command {
WINK => Ok(Command::Wink),
HidCommand::Vendor(command) => command.try_into(),
_ => Err(Error::UnsupportedCommand)
}
}
}

impl TryFrom<VendorCommand> for Command {
type Error = Error;

fn try_from(command: VendorCommand) -> Result<Self, Self::Error> {
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<Error> 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<Error> 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() -> !;
Expand Down Expand Up @@ -63,70 +156,77 @@ where T: TrussedClient,
user_present.is_ok()
}


}

impl<T, R> hid::App for App<T, R>
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<const N: usize>(&mut self, command: Command, flag: Option<u8>, response: &mut Vec<u8, N>) -> 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<T, R> hid::App for App<T, R>
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<T, R> iso7816::App for App<T, R>
where T: TrussedClient,
R: Reboot
Expand All @@ -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)
}
}