Allows initialization without Reset permission (#673)
* 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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")]
|
||||
@@ -40,6 +40,7 @@ pub enum SendOrRecvStatus {
|
||||
Received(UsbEndpoint),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SendOrRecvError;
|
||||
|
||||
pub type SendOrRecvResult = Result<SendOrRecvStatus, SendOrRecvError>;
|
||||
@@ -47,3 +48,16 @@ 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
3
libraries/opensk/src/env/mod.rs
vendored
3
libraries/opensk/src/env/mod.rs
vendored
@@ -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
|
||||
|
||||
20
libraries/opensk/src/env/test/mod.rs
vendored
20
libraries/opensk/src/env/test/mod.rs
vendored
@@ -34,6 +34,7 @@ pub struct TestEnv {
|
||||
store: Store<BufferStorage>,
|
||||
customization: TestCustomization,
|
||||
clock: TestClock,
|
||||
soft_reset: bool,
|
||||
}
|
||||
|
||||
pub type TestRng = StdRng;
|
||||
@@ -127,6 +128,7 @@ impl Default for TestEnv {
|
||||
store,
|
||||
customization,
|
||||
clock,
|
||||
soft_reset: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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() {
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user