From 1e9579d55d71d35d14ca997d0d5fdcde295c538f Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 9 Jan 2024 15:30:24 +0100 Subject: [PATCH] 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. --- libraries/opensk/fuzz/fuzz_helper/src/lib.rs | 4 +- libraries/opensk/src/api/attestation_store.rs | 11 +++ libraries/opensk/src/api/connection.rs | 16 +++- libraries/opensk/src/api/customization.rs | 43 ++++++++++ libraries/opensk/src/ctap/mod.rs | 11 ++- libraries/opensk/src/lib.rs | 78 ++++++++++++++++--- src/main.rs | 2 +- 7 files changed, 151 insertions(+), 14 deletions(-) diff --git a/libraries/opensk/fuzz/fuzz_helper/src/lib.rs b/libraries/opensk/fuzz/fuzz_helper/src/lib.rs index cab68a59..4fcb5c57 100644 --- a/libraries/opensk/fuzz/fuzz_helper/src/lib.rs +++ b/libraries/opensk/fuzz/fuzz_helper/src/lib.rs @@ -141,7 +141,7 @@ pub fn process_ctap_any_type(data: &[u8]) -> arbitrary::Result<()> { let data = unstructured.take_rest(); // Initialize ctap state and hid and get the allocated cid. - let mut ctap = Ctap::new(env); + let mut ctap = Ctap::new(env, false); let cid = initialize(&mut ctap); // Wrap input as message with the allocated cid. let mut command = cid.to_vec(); @@ -191,7 +191,7 @@ pub fn process_ctap_specific_type(data: &[u8], input_type: InputType) -> arbitra return Ok(()); } // Initialize ctap state and hid and get the allocated cid. - let mut ctap = Ctap::new(env); + let mut ctap = Ctap::new(env, false); let cid = initialize(&mut ctap); // Wrap input as message with allocated cid and command type. let mut command = cid.to_vec(); diff --git a/libraries/opensk/src/api/attestation_store.rs b/libraries/opensk/src/api/attestation_store.rs index f4cfb894..68dcee27 100644 --- a/libraries/opensk/src/api/attestation_store.rs +++ b/libraries/opensk/src/api/attestation_store.rs @@ -113,3 +113,14 @@ impl From 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); + } +} diff --git a/libraries/opensk/src/api/connection.rs b/libraries/opensk/src/api/connection.rs index 348c5f4d..b3a577ce 100644 --- a/libraries/opensk/src/api/connection.rs +++ b/libraries/opensk/src/api/connection.rs @@ -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; @@ -47,3 +48,16 @@ pub type SendOrRecvResult = Result; 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)); + } +} diff --git a/libraries/opensk/src/api/customization.rs b/libraries/opensk/src/api/customization.rs index 98b367fc..38c6a426 100644 --- a/libraries/opensk/src/api/customization.rs +++ b/libraries/opensk/src/api/customization.rs @@ -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); + } } diff --git a/libraries/opensk/src/ctap/mod.rs b/libraries/opensk/src/ctap/mod.rs index 748f2566..d3142dda 100644 --- a/libraries/opensk/src/ctap/mod.rs +++ b/libraries/opensk/src/ctap/mod.rs @@ -413,7 +413,9 @@ pub struct StatefulPermission { impl StatefulPermission { /// 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 + /// permission outside of this function. If you initialize the app without a power cycle + /// (potentially after waking up from sleep), `clear` the permissions. /// means allowing Reset, and Reset cannot be granted later. pub fn new_reset(env: &mut E) -> StatefulPermission { StatefulPermission { @@ -552,6 +554,13 @@ impl CtapState { } } + /// Creates new CTAP state that doesn't assume a user intended power cycle. + pub fn new_soft_reset(env: &mut E) -> Self { + let mut ctap_state = CtapState::new(env); + ctap_state.stateful_command_permission.clear(); + ctap_state + } + pub fn increment_global_signature_counter( &mut self, env: &mut E, diff --git a/libraries/opensk/src/lib.rs b/libraries/opensk/src/lib.rs index 1412a95a..adbf5ef9 100644 --- a/libraries/opensk/src/lib.rs +++ b/libraries/opensk/src/lib.rs @@ -63,8 +63,12 @@ impl Ctap { /// Instantiates a CTAP implementation given its environment. // This should only take the environment, but it temporarily takes the boot time until the // clock is part of the environment. - pub fn new(mut env: E) -> Self { - let state = CtapState::::new(&mut env); + pub fn new(mut env: E, is_soft_reset: bool) -> Self { + let state = if is_soft_reset { + CtapState::::new_soft_reset(&mut env) + } else { + CtapState::::new(&mut env) + }; let hid = MainHid::default(); #[cfg(feature = "vendor_hid")] let vendor_hid = VendorHid::default(); @@ -81,10 +85,6 @@ impl Ctap { &mut self.state } - pub fn hid(&mut self) -> &mut MainHid { - &mut self.hid - } - pub fn env(&mut self) -> &mut E { &mut self.env } @@ -134,6 +134,7 @@ impl Ctap { #[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. @@ -162,7 +163,7 @@ mod test { #[test] fn test_wink() { let env = TestEnv::default(); - let mut ctap = Ctap::::new(env); + let mut ctap = Ctap::::new(env, false); // Send Init, receive Init response and check wink if disabled. let mut init_response = ctap.process_hid_packet(&init_packet(), Transport::MainHid); @@ -181,7 +182,7 @@ mod test { #[test] fn test_locked_channel_id() { let env = TestEnv::default(); - let mut ctap = Ctap::::new(env); + let mut ctap = Ctap::::new(env, false); // Send Init, receive Init response. let mut init_response = ctap.process_hid_packet(&init_packet(), Transport::MainHid); @@ -200,11 +201,60 @@ mod test { assert_eq!(response_packet[4], 0xBF); } + #[test] + fn test_hard_reset() { + let env = TestEnv::default(); + let mut ctap = Ctap::::new(env, false); + + // 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 env = TestEnv::default(); + let mut ctap = Ctap::::new(env, true); + + // 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::::new(env, true); + assert_eq!(ctap.env().firmware_version(), Some(0)); + } + #[test] #[cfg(feature = "vendor_hid")] fn test_locked_transport() { let env = TestEnv::default(); - let mut ctap = Ctap::::new(env); + let mut ctap = Ctap::::new(env, false); // Send Init, receive Init response. let mut init_response = ctap.process_hid_packet(&init_packet(), Transport::MainHid); @@ -222,4 +272,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::::new(env, false); + // Granting doesn't work until a CTAP1 request was processed. + ctap.u2f_grant_user_presence(); + assert!(!ctap.u2f_needs_user_presence()); + } } diff --git a/src/main.rs b/src/main.rs index a71c8ff9..0676b553 100644 --- a/src/main.rs +++ b/src/main.rs @@ -137,7 +137,7 @@ fn main() { } let env = TockEnv::::default(); - let mut ctap = opensk::Ctap::new(env); + let mut ctap = opensk::Ctap::new(env, false); let mut led_counter = 0; let mut led_blink_timer =