Skip to content

Commit

Permalink
Allows initialization without Reset permission (#673)
Browse files Browse the repository at this point in the history
* Allows initialization without Reset permission

This PR is useful for all implementations that can trigger a reboot
without user intervention. In these cases, we don't want to allow the
Reset command. It should only be allowed after a user initiated power
cycle.

Adds tests to the new functionality and a few other coverage holes.

* Moves soft reset parameters into Env
  • Loading branch information
kaczmarczyck authored Jan 9, 2024
1 parent 0185d1e commit ba0d717
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 8 deletions.
11 changes: 11 additions & 0 deletions libraries/opensk/src/api/attestation_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,14 @@ impl From<StoreError> for Error {
}
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_store_error() {
assert_eq!(Error::from(StoreError::StorageError), Error::Storage);
assert_eq!(Error::from(StoreError::InvalidStorage), Error::Internal);
}
}
16 changes: 15 additions & 1 deletion libraries/opensk/src/api/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

use core::convert::TryFrom;

#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UsbEndpoint {
MainHid = 1,
#[cfg(feature = "vendor_hid")]
Expand All @@ -40,10 +40,24 @@ pub enum SendOrRecvStatus {
Received(UsbEndpoint),
}

#[derive(Debug, PartialEq, Eq)]
pub struct SendOrRecvError;

pub type SendOrRecvResult = Result<SendOrRecvStatus, SendOrRecvError>;

pub trait HidConnection {
fn send_and_maybe_recv(&mut self, buf: &mut [u8; 64], timeout_ms: usize) -> SendOrRecvResult;
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_endpoint_num() {
assert_eq!(UsbEndpoint::try_from(1), Ok(UsbEndpoint::MainHid));
#[cfg(feature = "vendor_hid")]
assert_eq!(UsbEndpoint::try_from(2), Ok(UsbEndpoint::VendorHid));
assert_eq!(UsbEndpoint::try_from(3), Err(SendOrRecvError));
}
}
43 changes: 43 additions & 0 deletions libraries/opensk/src/api/customization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,4 +457,47 @@ mod test {
fn test_invariants() {
assert!(is_valid(&DEFAULT_CUSTOMIZATION));
}

#[test]
fn test_accessors() {
let customization = CustomizationImpl {
aaguid: &[0; AAGUID_LENGTH],
allows_pin_protocol_v1: true,
default_cred_protect: None,
default_min_pin_length: 4,
default_min_pin_length_rp_ids: &["example.com"],
enforce_always_uv: false,
enterprise_attestation_mode: None,
enterprise_rp_id_list: &[],
max_msg_size: 7609,
max_pin_retries: 8,
use_batch_attestation: true,
use_signature_counter: true,
max_cred_blob_length: 32,
max_credential_count_in_list: Some(3),
max_large_blob_array_size: 2048,
max_rp_ids_length: 8,
max_supported_resident_keys: 150,
};
assert_eq!(customization.aaguid(), &[0; AAGUID_LENGTH]);
assert!(customization.allows_pin_protocol_v1());
assert!(customization.default_cred_protect().is_none());
assert_eq!(customization.default_min_pin_length(), 4);
assert_eq!(
customization.default_min_pin_length_rp_ids(),
vec![String::from("example.com")]
);
assert!(!customization.enforce_always_uv());
assert!(customization.enterprise_attestation_mode().is_none());
assert!(customization.enterprise_rp_id_list().is_empty());
assert_eq!(customization.max_msg_size(), 7609);
assert_eq!(customization.max_pin_retries(), 8);
assert!(customization.use_batch_attestation());
assert!(customization.use_signature_counter());
assert_eq!(customization.max_cred_blob_length(), 32);
assert_eq!(customization.max_credential_count_in_list(), Some(3));
assert_eq!(customization.max_large_blob_array_size(), 2048);
assert_eq!(customization.max_rp_ids_length(), 8);
assert_eq!(customization.max_supported_resident_keys(), 150);
}
}
26 changes: 23 additions & 3 deletions libraries/opensk/src/ctap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,11 +410,26 @@ pub struct StatefulPermission<E: Env> {
channel: Option<Channel>,
}

impl<E: Env> Default for StatefulPermission<E> {
/// Creates the command state at device startup without user action.
///
/// Reset is not granted after a forced reboot. The user replugging the device is a required
/// to avoid accidental data loss.
fn default() -> StatefulPermission<E> {
StatefulPermission {
permission: <E::Clock as Clock>::Timer::default(),
command_type: None,
channel: None,
}
}
}

impl<E: Env> StatefulPermission<E> {
/// Creates the command state at device startup.
///
/// Resets are only possible after a power cycle. Therefore, initialization
/// means allowing Reset, and Reset cannot be granted later.
/// Resets are only possible after a power cycle. Therefore, there is no way to grant the Reset
/// permission outside of this function. If you initialize the app without a power cycle
/// (potentially after waking up from sleep), call `default` instead.
pub fn new_reset(env: &mut E) -> StatefulPermission<E> {
StatefulPermission {
permission: env.clock().make_timer(RESET_TIMEOUT_DURATION_MS),
Expand Down Expand Up @@ -543,11 +558,16 @@ impl<E: Env> CtapState<E> {
pub fn new(env: &mut E) -> Self {
storage::init(env).ok().unwrap();
let client_pin = ClientPin::new(env);
let stateful_command_permission = if env.boots_after_soft_reset() {
StatefulPermission::default()
} else {
StatefulPermission::new_reset(env)
};
CtapState {
client_pin,
#[cfg(feature = "with_ctap1")]
u2f_up_state: U2fUserPresenceState::new(),
stateful_command_permission: StatefulPermission::new_reset(env),
stateful_command_permission,
large_blobs: LargeBlobs::new(),
}
}
Expand Down
3 changes: 3 additions & 0 deletions libraries/opensk/src/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ pub trait Env {
#[cfg(feature = "vendor_hid")]
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection;

/// Indicates that the last power cycle was not caused by user action.
fn boots_after_soft_reset(&self) -> bool;

/// Option to return a firmware version that is shown as device info.
fn firmware_version(&self) -> Option<u64> {
None
Expand Down
20 changes: 20 additions & 0 deletions libraries/opensk/src/env/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub struct TestEnv {
store: Store<BufferStorage>,
customization: TestCustomization,
clock: TestClock,
soft_reset: bool,
}

pub type TestRng = StdRng;
Expand Down Expand Up @@ -127,6 +128,7 @@ impl Default for TestEnv {
store,
customization,
clock,
soft_reset: false,
}
}
}
Expand All @@ -139,6 +141,10 @@ impl TestEnv {
pub fn seed_rng_from_u64(&mut self, seed: u64) {
self.rng = StdRng::seed_from_u64(seed);
}

pub fn set_boots_after_soft_reset(&mut self, value: bool) {
self.soft_reset = value;
}
}

impl TestUserPresence {
Expand Down Expand Up @@ -227,6 +233,10 @@ impl Env for TestEnv {
self
}

fn boots_after_soft_reset(&self) -> bool {
self.soft_reset
}

fn firmware_version(&self) -> Option<u64> {
Some(0)
}
Expand All @@ -247,4 +257,14 @@ mod test {
clock.advance(1);
assert!(clock.is_elapsed(&timer));
}

#[test]
fn test_soft_reset() {
let mut env = TestEnv::default();
assert!(!env.boots_after_soft_reset());
env.set_boots_after_soft_reset(true);
assert!(env.boots_after_soft_reset());
env.set_boots_after_soft_reset(false);
assert!(!env.boots_after_soft_reset());
}
}
65 changes: 61 additions & 4 deletions libraries/opensk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ impl<E: Env> Ctap<E> {
&mut self.state
}

pub fn hid(&mut self) -> &mut MainHid<E> {
&mut self.hid
}

pub fn env(&mut self) -> &mut E {
&mut self.env
}
Expand Down Expand Up @@ -134,6 +130,7 @@ impl<E: Env> Ctap<E> {
#[cfg(test)]
mod test {
use super::*;
use crate::ctap::status_code::Ctap2StatusCode;
use crate::env::test::TestEnv;

/// Assembles a packet for a payload that fits into one packet.
Expand Down Expand Up @@ -200,6 +197,56 @@ mod test {
assert_eq!(response_packet[4], 0xBF);
}

#[test]
fn test_hard_reset() {
let env = TestEnv::default();
let mut ctap = Ctap::<TestEnv>::new(env);

// Send Init, receive Init response.
let mut init_response = ctap.process_hid_packet(&init_packet(), Transport::MainHid);
let response_packet = init_response.next().unwrap();
assert_eq!(response_packet[4], 0x86);
let cid = *array_ref!(response_packet, 15, 4);

// Send Reset, get Ok.
let reset_packet = assemble_packet(&cid, 0x10, &[0x07]);
let mut reset_response = ctap.process_hid_packet(&reset_packet, Transport::MainHid);
let response_packet = reset_response.next().unwrap();
let status_byte = Ctap2StatusCode::CTAP2_OK as u8;
let expected_data = [0x90, 0x00, 0x01, status_byte];
assert_eq!(response_packet[..4], cid);
assert_eq!(response_packet[4..8], expected_data);
}

#[test]
fn test_soft_reset() {
let mut env = TestEnv::default();
env.set_boots_after_soft_reset(true);
let mut ctap = Ctap::<TestEnv>::new(env);

// Send Init, receive Init response.
let mut init_response = ctap.process_hid_packet(&init_packet(), Transport::MainHid);
let response_packet = init_response.next().unwrap();
assert_eq!(response_packet[4], 0x86);
let cid = *array_ref!(response_packet, 15, 4);

// Send Reset, get error.
let reset_packet = assemble_packet(&cid, 0x10, &[0x07]);
let mut reset_response = ctap.process_hid_packet(&reset_packet, Transport::MainHid);
let response_packet = reset_response.next().unwrap();
let status_byte = Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED as u8;
let expected_data = [0x90, 0x00, 0x01, status_byte];
assert_eq!(response_packet[..4], cid);
assert_eq!(response_packet[4..8], expected_data);
}

#[test]
fn test_env_api() {
let env = TestEnv::default();
let mut ctap = Ctap::<TestEnv>::new(env);
assert_eq!(ctap.env().firmware_version(), Some(0));
}

#[test]
#[cfg(feature = "vendor_hid")]
fn test_locked_transport() {
Expand All @@ -222,4 +269,14 @@ mod test {
let response_packet = init_response.next().unwrap();
assert_eq!(response_packet[4], 0xBF);
}

#[test]
#[cfg(feature = "with_ctap1")]
fn test_ctap1_initial_state() {
let env = TestEnv::default();
let mut ctap = Ctap::<TestEnv>::new(env);
// Granting doesn't work until a CTAP1 request was processed.
ctap.u2f_grant_user_presence();
assert!(!ctap.u2f_needs_user_presence());
}
}
4 changes: 4 additions & 0 deletions src/env/tock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ impl<S: Syscalls, C: platform::subscribe::Config + platform::allow_ro::Config> E
commands::process_vendor_command(self, bytes, channel)
}

fn boots_after_soft_reset(&self) -> bool {
false
}

fn firmware_version(&self) -> Option<u64> {
self.upgrade_storage
.as_ref()
Expand Down

0 comments on commit ba0d717

Please sign in to comment.