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:
kaczmarczyck
2024-01-09 18:30:35 +01:00
committed by GitHub
parent 0185d1e443
commit ba0d717d88
8 changed files with 180 additions and 8 deletions

View File

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

View File

@@ -14,7 +14,7 @@
use core::convert::TryFrom; use core::convert::TryFrom;
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UsbEndpoint { pub enum UsbEndpoint {
MainHid = 1, MainHid = 1,
#[cfg(feature = "vendor_hid")] #[cfg(feature = "vendor_hid")]
@@ -40,6 +40,7 @@ pub enum SendOrRecvStatus {
Received(UsbEndpoint), Received(UsbEndpoint),
} }
#[derive(Debug, PartialEq, Eq)]
pub struct SendOrRecvError; pub struct SendOrRecvError;
pub type SendOrRecvResult = Result<SendOrRecvStatus, SendOrRecvError>; pub type SendOrRecvResult = Result<SendOrRecvStatus, SendOrRecvError>;
@@ -47,3 +48,16 @@ pub type SendOrRecvResult = Result<SendOrRecvStatus, SendOrRecvError>;
pub trait HidConnection { pub trait HidConnection {
fn send_and_maybe_recv(&mut self, buf: &mut [u8; 64], timeout_ms: usize) -> SendOrRecvResult; 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));
}
}

View File

@@ -457,4 +457,47 @@ mod test {
fn test_invariants() { fn test_invariants() {
assert!(is_valid(&DEFAULT_CUSTOMIZATION)); 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);
}
} }

View File

@@ -410,11 +410,26 @@ pub struct StatefulPermission<E: Env> {
channel: Option<Channel>, 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> { impl<E: Env> StatefulPermission<E> {
/// Creates the command state at device startup. /// Creates the command state at device startup.
/// ///
/// Resets are only possible after a power cycle. Therefore, initialization /// Resets are only possible after a power cycle. Therefore, there is no way to grant the Reset
/// means allowing Reset, and Reset cannot be granted later. /// 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> { pub fn new_reset(env: &mut E) -> StatefulPermission<E> {
StatefulPermission { StatefulPermission {
permission: env.clock().make_timer(RESET_TIMEOUT_DURATION_MS), 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 { pub fn new(env: &mut E) -> Self {
storage::init(env).ok().unwrap(); storage::init(env).ok().unwrap();
let client_pin = ClientPin::new(env); let client_pin = ClientPin::new(env);
let stateful_command_permission = if env.boots_after_soft_reset() {
StatefulPermission::default()
} else {
StatefulPermission::new_reset(env)
};
CtapState { CtapState {
client_pin, client_pin,
#[cfg(feature = "with_ctap1")] #[cfg(feature = "with_ctap1")]
u2f_up_state: U2fUserPresenceState::new(), u2f_up_state: U2fUserPresenceState::new(),
stateful_command_permission: StatefulPermission::new_reset(env), stateful_command_permission,
large_blobs: LargeBlobs::new(), large_blobs: LargeBlobs::new(),
} }
} }

View File

@@ -76,6 +76,9 @@ pub trait Env {
#[cfg(feature = "vendor_hid")] #[cfg(feature = "vendor_hid")]
fn vendor_hid_connection(&mut self) -> &mut Self::HidConnection; 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. /// Option to return a firmware version that is shown as device info.
fn firmware_version(&self) -> Option<u64> { fn firmware_version(&self) -> Option<u64> {
None None

View File

@@ -34,6 +34,7 @@ pub struct TestEnv {
store: Store<BufferStorage>, store: Store<BufferStorage>,
customization: TestCustomization, customization: TestCustomization,
clock: TestClock, clock: TestClock,
soft_reset: bool,
} }
pub type TestRng = StdRng; pub type TestRng = StdRng;
@@ -127,6 +128,7 @@ impl Default for TestEnv {
store, store,
customization, customization,
clock, clock,
soft_reset: false,
} }
} }
} }
@@ -139,6 +141,10 @@ impl TestEnv {
pub fn seed_rng_from_u64(&mut self, seed: u64) { pub fn seed_rng_from_u64(&mut self, seed: u64) {
self.rng = StdRng::seed_from_u64(seed); self.rng = StdRng::seed_from_u64(seed);
} }
pub fn set_boots_after_soft_reset(&mut self, value: bool) {
self.soft_reset = value;
}
} }
impl TestUserPresence { impl TestUserPresence {
@@ -227,6 +233,10 @@ impl Env for TestEnv {
self self
} }
fn boots_after_soft_reset(&self) -> bool {
self.soft_reset
}
fn firmware_version(&self) -> Option<u64> { fn firmware_version(&self) -> Option<u64> {
Some(0) Some(0)
} }
@@ -247,4 +257,14 @@ mod test {
clock.advance(1); clock.advance(1);
assert!(clock.is_elapsed(&timer)); 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());
}
} }

View File

@@ -81,10 +81,6 @@ impl<E: Env> Ctap<E> {
&mut self.state &mut self.state
} }
pub fn hid(&mut self) -> &mut MainHid<E> {
&mut self.hid
}
pub fn env(&mut self) -> &mut E { pub fn env(&mut self) -> &mut E {
&mut self.env &mut self.env
} }
@@ -134,6 +130,7 @@ impl<E: Env> Ctap<E> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::ctap::status_code::Ctap2StatusCode;
use crate::env::test::TestEnv; use crate::env::test::TestEnv;
/// Assembles a packet for a payload that fits into one packet. /// Assembles a packet for a payload that fits into one packet.
@@ -200,6 +197,56 @@ mod test {
assert_eq!(response_packet[4], 0xBF); 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] #[test]
#[cfg(feature = "vendor_hid")] #[cfg(feature = "vendor_hid")]
fn test_locked_transport() { fn test_locked_transport() {
@@ -222,4 +269,14 @@ mod test {
let response_packet = init_response.next().unwrap(); let response_packet = init_response.next().unwrap();
assert_eq!(response_packet[4], 0xBF); 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
src/env/tock/mod.rs vendored
View File

@@ -452,6 +452,10 @@ impl<S: Syscalls, C: platform::subscribe::Config + platform::allow_ro::Config> E
commands::process_vendor_command(self, bytes, channel) commands::process_vendor_command(self, bytes, channel)
} }
fn boots_after_soft_reset(&self) -> bool {
false
}
fn firmware_version(&self) -> Option<u64> { fn firmware_version(&self) -> Option<u64> {
self.upgrade_storage self.upgrade_storage
.as_ref() .as_ref()