From d6a2080cd587972e42ba302a3f3524c7feac9b9b Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 15 Mar 2022 13:16:35 +0100 Subject: [PATCH] adds channels to CTAP logic --- src/ctap/credential_management.rs | 34 +- src/ctap/mod.rs | 643 +++++++++++++++++++++++------- src/lib.rs | 4 +- 3 files changed, 519 insertions(+), 162 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 61458aa..0d51a03 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -22,7 +22,7 @@ use super::data_formats::{ }; use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; -use super::{StatefulCommand, StatefulPermission}; +use super::{Channel, StatefulCommand, StatefulPermission}; use crate::ctap::storage; use crate::env::Env; use alloc::collections::BTreeSet; @@ -137,13 +137,14 @@ fn process_get_creds_metadata( fn process_enumerate_rps_begin( env: &mut impl Env, stateful_command_permission: &mut StatefulPermission, + channel: Channel, now: CtapInstant, ) -> Result { let rp_set = get_stored_rp_ids(env)?; let total_rps = rp_set.len(); if total_rps > 1 { - stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1)); + stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1), channel); } // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() let rp_id = rp_set @@ -174,6 +175,7 @@ fn process_enumerate_credentials_begin( stateful_command_permission: &mut StatefulPermission, client_pin: &mut ClientPin, sub_command_params: CredentialManagementSubCommandParameters, + channel: Channel, now: CtapInstant, ) -> Result { let rp_id_hash = sub_command_params @@ -199,8 +201,11 @@ fn process_enumerate_credentials_begin( .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; let credential = storage::get_credential(env, current_key)?; if total_credentials > 1 { - stateful_command_permission - .set_command(now, StatefulCommand::EnumerateCredentials(rp_credentials)); + stateful_command_permission.set_command( + now, + StatefulCommand::EnumerateCredentials(rp_credentials), + channel, + ); } enumerate_credentials_response(credential, Some(total_credentials as u64)) } @@ -252,6 +257,7 @@ pub fn process_credential_management( stateful_command_permission: &mut StatefulPermission, client_pin: &mut ClientPin, cred_management_params: AuthenticatorCredentialManagementParameters, + channel: Channel, now: CtapInstant, ) -> Result { let AuthenticatorCredentialManagementParameters { @@ -269,7 +275,7 @@ pub fn process_credential_management( | ( CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, Ok(StatefulCommand::EnumerateCredentials(_)), - ) => stateful_command_permission.check_command_permission(now)?, + ) => (), (_, _) => { stateful_command_permission.clear(); } @@ -311,6 +317,7 @@ pub fn process_credential_management( Some(process_enumerate_rps_begin( env, stateful_command_permission, + channel, now, )?) } @@ -323,6 +330,7 @@ pub fn process_credential_management( stateful_command_permission, client_pin, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + channel, now, )?) } @@ -359,6 +367,8 @@ mod test { use crate::env::Env; use crypto::rng256::Rng256; + const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]); + fn create_credential_source(rng: &mut impl Rng256) -> PublicKeyCredentialSource { let private_key = crypto::ecdsa::SecKey::gensk(rng); PublicKeyCredentialSource { @@ -407,6 +417,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); let initial_capacity = match cred_management_response.unwrap() { @@ -432,6 +443,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); match cred_management_response.unwrap() { @@ -490,6 +502,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); let first_rp_id = match cred_management_response.unwrap() { @@ -514,6 +527,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); let second_rp_id = match cred_management_response.unwrap() { @@ -539,6 +553,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); assert_eq!( @@ -588,6 +603,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); match cred_management_response.unwrap() { @@ -618,6 +634,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); assert_eq!( @@ -670,6 +687,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); let first_credential_id = match cred_management_response.unwrap() { @@ -693,6 +711,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); let second_credential_id = match cred_management_response.unwrap() { @@ -717,6 +736,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); assert_eq!( @@ -767,6 +787,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); assert_eq!( @@ -785,6 +806,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); assert_eq!( @@ -841,6 +863,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); assert_eq!( @@ -879,6 +902,7 @@ mod test { &mut ctap_state.stateful_command_permission, &mut ctap_state.client_pin, cred_management_params, + DUMMY_CHANNEL, CtapInstant::new(0), ); assert_eq!( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 66afb90..b93b39d 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -266,9 +266,13 @@ pub enum StatefulCommand { /// different commands. Therefore, Reset behaves just like all other stateful /// commands and is included here. Please note that the allowed time for Reset /// differs from all other stateful commands. +/// +/// Additionally, state that is held over multiple commands is assigned to a channel. We discard +/// all state when we receive data on a different channel. pub struct StatefulPermission { permission: TimedPermission, command_type: Option, + channel: Option, } impl StatefulPermission { @@ -280,6 +284,7 @@ impl StatefulPermission { StatefulPermission { permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), command_type: Some(StatefulCommand::Reset), + channel: None, } } @@ -287,15 +292,32 @@ impl StatefulPermission { pub fn clear(&mut self) { self.permission = TimedPermission::waiting(); self.command_type = None; + self.channel = None; } - /// Checks the permission timeout. - pub fn check_command_permission(&mut self, now: CtapInstant) -> Result<(), Ctap2StatusCode> { - if self.permission.is_granted(now) { - Ok(()) - } else { + /// Clears all state if communication is coming from a different channel. + pub fn clear_old_channels(&mut self, channel: &Channel) { + // There are different possible choices for incoming traffic on a different channel: + // A) Always reset state (our choice). + // B) Only reset state if the new command is stateful. + // C) Keep state on all channels until timeout. + // + // If we wanted to switch to (B) or (C), we'd have to be very careful that the state does + // not go stale. For example, we keep credential keys that we expect to still exist. + // However, interleaving (stateless) commands could delete credentials or change the PIN, + // which chould make invalidate our access. Some read-only commands should be okay to run, + // but (A) is the safest and easiest solution. + if let Some(c) = self.channel.as_ref() { + if c != channel { + self.clear(); + } + } + } + + /// Clears all state if the permission timed out. + pub fn clear_timer(&mut self, now: CtapInstant) { + if !self.permission.is_granted(now) { self.clear(); - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) } } @@ -307,13 +329,19 @@ impl StatefulPermission { } /// Sets a new command state, and starts a new clock for timeouts. - pub fn set_command(&mut self, now: CtapInstant, new_command_type: StatefulCommand) { + pub fn set_command( + &mut self, + now: CtapInstant, + new_command_type: StatefulCommand, + channel: Channel, + ) { match &new_command_type { // Reset is only allowed after a power cycle. StatefulCommand::Reset => unreachable!(), _ => { self.permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); self.command_type = Some(new_command_type); + self.channel = Some(channel); } } } @@ -385,10 +413,7 @@ impl CtapState { } pub fn update_timeouts(&mut self, now: CtapInstant) { - // Ignore the result, just update. - let _ = self - .stateful_command_permission - .check_command_permission(now); + self.stateful_command_permission.clear_timer(now); self.client_pin.update_timeouts(now); } @@ -486,94 +511,126 @@ impl CtapState { ) -> Vec { let cmd = Command::deserialize(command_cbor); debug_ctap!(env, "Received command: {:#?}", cmd); - match cmd { - Ok(command) => { - // Correct behavior between CTAP1 and CTAP2 isn't defined yet. Just a guess. - #[cfg(feature = "with_ctap1")] - { - self.u2f_up_state = - U2fUserPresenceState::new(U2F_UP_PROMPT_TIMEOUT, TOUCH_TIMEOUT); - } - match (&command, self.stateful_command_permission.get_command()) { - (Command::AuthenticatorGetNextAssertion, Ok(StatefulCommand::GetAssertion(_))) - | (Command::AuthenticatorReset, Ok(StatefulCommand::Reset)) - // AuthenticatorGetInfo still allows Reset. - | (Command::AuthenticatorGetInfo, Ok(StatefulCommand::Reset)) - // AuthenticatorSelection still allows Reset. - | (Command::AuthenticatorSelection, Ok(StatefulCommand::Reset)) - // AuthenticatorCredentialManagement handles its subcommands later. - | ( - Command::AuthenticatorCredentialManagement(_), - Ok(StatefulCommand::EnumerateRps(_)), - ) - | ( - Command::AuthenticatorCredentialManagement(_), - Ok(StatefulCommand::EnumerateCredentials(_)), - ) => (), - (_, _) => self.stateful_command_permission.clear(), - } - let response = match command { - Command::AuthenticatorMakeCredential(params) => { - self.process_make_credential(env, params, channel) - } - Command::AuthenticatorGetAssertion(params) => { - self.process_get_assertion(env, params, channel, now) - } - Command::AuthenticatorGetNextAssertion => { - self.process_get_next_assertion(env, now) - } - Command::AuthenticatorGetInfo => self.process_get_info(env), - Command::AuthenticatorClientPin(params) => { - self.client_pin.process_command(env, params, now) - } - Command::AuthenticatorReset => self.process_reset(env, channel, now), - Command::AuthenticatorCredentialManagement(params) => { - process_credential_management( - env, - &mut self.stateful_command_permission, - &mut self.client_pin, - params, - now, - ) - } - Command::AuthenticatorSelection => self.process_selection(env, channel), - Command::AuthenticatorLargeBlobs(params) => { - self.large_blobs - .process_command(env, &mut self.client_pin, params) - } - Command::AuthenticatorConfig(params) => { - process_config(env, &mut self.client_pin, params) - } - // Vendor specific commands - Command::AuthenticatorVendorConfigure(params) => { - self.process_vendor_configure(env, params, channel) - } - Command::AuthenticatorVendorUpgrade(params) => { - self.process_vendor_upgrade(env, params) - } - Command::AuthenticatorVendorUpgradeInfo => { - self.process_vendor_upgrade_info(env) - } - }; - debug_ctap!(env, "Sending response: {:#?}", response); - match response { - Ok(response_data) => { - let mut response_vec = vec![0x00]; - if let Some(value) = response_data.into() { - if cbor_write(value, &mut response_vec).is_err() { - response_vec = - vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8]; - } - } - response_vec - } - Err(error_code) => vec![error_code as u8], + let response = + cmd.and_then(|command| self.process_parsed_command(env, command, channel, now)); + debug_ctap!(env, "Sending response: {:#?}", response); + match response { + Ok(response_data) => { + let mut response_vec = vec![Ctap2StatusCode::CTAP2_OK as u8]; + if let Some(value) = response_data.into() { + if cbor_write(value, &mut response_vec).is_err() { + response_vec = vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8]; + } } + response_vec } Err(error_code) => vec![error_code as u8], } } + /// Processed a command after parsing from CBOR, returning its structured output. + /// + /// This function contains the logic of `parse_command`, minus all CBOR encoding and decoding. + /// It should make command parsing easier to test. + fn process_parsed_command( + &mut self, + env: &mut impl Env, + command: Command, + channel: Channel, + now: CtapInstant, + ) -> Result { + // Correct behavior between CTAP1 and CTAP2 isn't defined yet. Just a guess. + #[cfg(feature = "with_ctap1")] + { + self.u2f_up_state = U2fUserPresenceState::new(U2F_UP_PROMPT_TIMEOUT, TOUCH_TIMEOUT); + } + self.stateful_command_permission + .clear_old_channels(&channel); + self.stateful_command_permission.clear_timer(now); + match (&command, self.stateful_command_permission.get_command()) { + (Command::AuthenticatorGetNextAssertion, Ok(StatefulCommand::GetAssertion(_))) + | (Command::AuthenticatorReset, Ok(StatefulCommand::Reset)) + // AuthenticatorGetInfo still allows Reset. + | (Command::AuthenticatorGetInfo, Ok(StatefulCommand::Reset)) + // AuthenticatorSelection still allows Reset. + | (Command::AuthenticatorSelection, Ok(StatefulCommand::Reset)) + // AuthenticatorCredentialManagement handles its subcommands later. + | ( + Command::AuthenticatorCredentialManagement(_), + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( + Command::AuthenticatorCredentialManagement(_), + Ok(StatefulCommand::EnumerateCredentials(_)), + ) => (), + (_, _) => self.stateful_command_permission.clear(), + } + match &channel { + Channel::MainHid(_) => self.process_fido_command(env, command, channel, now), + #[cfg(feature = "vendor_hid")] + Channel::VendorHid(_) => self.process_vendor_command(env, command, channel), + } + } + + fn process_fido_command( + &mut self, + env: &mut impl Env, + command: Command, + channel: Channel, + now: CtapInstant, + ) -> Result { + match command { + Command::AuthenticatorMakeCredential(params) => { + self.process_make_credential(env, params, channel) + } + Command::AuthenticatorGetAssertion(params) => { + self.process_get_assertion(env, params, channel, now) + } + Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(env), + Command::AuthenticatorGetInfo => self.process_get_info(env), + Command::AuthenticatorClientPin(params) => { + self.client_pin.process_command(env, params, now) + } + Command::AuthenticatorReset => self.process_reset(env, channel), + Command::AuthenticatorCredentialManagement(params) => process_credential_management( + env, + &mut self.stateful_command_permission, + &mut self.client_pin, + params, + channel, + now, + ), + Command::AuthenticatorSelection => self.process_selection(env, channel), + Command::AuthenticatorLargeBlobs(params) => { + self.large_blobs + .process_command(env, &mut self.client_pin, params) + } + Command::AuthenticatorConfig(params) => { + process_config(env, &mut self.client_pin, params) + } + #[cfg(feature = "vendor_hid")] + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND), + #[cfg(not(feature = "vendor_hid"))] + _ => self.process_vendor_command(env, command, channel), + } + } + + fn process_vendor_command( + &mut self, + env: &mut impl Env, + command: Command, + channel: Channel, + ) -> Result { + match command { + Command::AuthenticatorVendorConfigure(params) => { + self.process_vendor_configure(env, params, channel) + } + Command::AuthenticatorVendorUpgrade(params) => self.process_vendor_upgrade(env, params), + Command::AuthenticatorVendorUpgradeInfo => self.process_vendor_upgrade_info(env), + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND), + } + } + fn pin_uv_auth_precheck( &mut self, env: &mut impl Env, @@ -851,6 +908,7 @@ impl CtapState { mut credential: PublicKeyCredentialSource, assertion_input: AssertionInput, number_of_credentials: Option, + is_next: bool, ) -> Result { let AssertionInput { client_data_hash, @@ -916,16 +974,20 @@ impl CtapState { } else { None }; - Ok(ResponseData::AuthenticatorGetAssertion( - AuthenticatorGetAssertionResponse { - credential: Some(cred_desc), - auth_data, - signature: signature.to_asn1_der(), - user, - number_of_credentials: number_of_credentials.map(|n| n as u64), - large_blob_key, - }, - )) + let response_data = AuthenticatorGetAssertionResponse { + credential: Some(cred_desc), + auth_data, + signature: signature.to_asn1_der(), + user, + number_of_credentials: number_of_credentials.map(|n| n as u64), + large_blob_key, + }; + // Only returned for the first GetAssertion, not for Next calls. + if is_next { + Ok(ResponseData::AuthenticatorGetNextAssertion(response_data)) + } else { + Ok(ResponseData::AuthenticatorGetAssertion(response_data)) + } } // Returns the first applicable credential from the allow list. @@ -1078,24 +1140,27 @@ impl CtapState { next_credential_keys, })); self.stateful_command_permission - .set_command(now, assertion_state); + .set_command(now, assertion_state, channel); number_of_credentials }; - self.assertion_response(env, credential, assertion_input, number_of_credentials) + self.assertion_response( + env, + credential, + assertion_input, + number_of_credentials, + false, + ) } fn process_get_next_assertion( &mut self, env: &mut impl Env, - now: CtapInstant, ) -> Result { - self.stateful_command_permission - .check_command_permission(now)?; let (assertion_input, credential_key) = self .stateful_command_permission .next_assertion_credential()?; let credential = storage::get_credential(env, credential_key)?; - self.assertion_response(env, credential, assertion_input, None) + self.assertion_response(env, credential, assertion_input, None, true) } fn process_get_info(&self, env: &mut impl Env) -> Result { @@ -1168,10 +1233,7 @@ impl CtapState { &mut self, env: &mut impl Env, channel: Channel, - now: CtapInstant, ) -> Result { - self.stateful_command_permission - .check_command_permission(now)?; match self.stateful_command_permission.get_command()? { StatefulCommand::Reset => (), _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), @@ -1354,10 +1416,13 @@ impl CtapState { #[cfg(test)] mod test { use super::client_pin::PIN_TOKEN_LENGTH; - use super::command::{AuthenticatorAttestationMaterial, AuthenticatorClientPinParameters}; + use super::command::{ + AuthenticatorAttestationMaterial, AuthenticatorClientPinParameters, + AuthenticatorCredentialManagementParameters, + }; use super::data_formats::{ - ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PinUvAuthProtocol, + ClientPinSubCommand, CoseKey, CredentialManagementSubCommand, GetAssertionHmacSecretInput, + GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, PinUvAuthProtocol, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::pin_protocol::{authenticate_pin_uv_auth_token, PinProtocol}; @@ -1370,6 +1435,8 @@ mod test { // In tests where we define a dummy user-presence check that immediately returns, the channel // ID is irrelevant, so we pass this (dummy but valid) value. const DUMMY_CHANNEL: Channel = Channel::MainHid([0x12, 0x34, 0x56, 0x78]); + #[cfg(feature = "vendor_hid")] + const VENDOR_CHANNEL: Channel = Channel::VendorHid([0x12, 0x34, 0x56, 0x78]); fn check_make_response( make_credential_response: Result, @@ -1998,31 +2065,31 @@ mod test { expected_number_of_credentials: Option, expected_extension_cbor: &[u8], ) { - match response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, 0x00, - ]; - let signature_counter_position = expected_auth_data.len() - 4; - BigEndian::write_u32( - &mut expected_auth_data[signature_counter_position..], - signature_counter, - ); - expected_auth_data.extend(expected_extension_cbor); - assert_eq!(auth_data, expected_auth_data); - assert_eq!(user, Some(expected_user)); - assert_eq!(number_of_credentials, expected_number_of_credentials); - } + let assertion_response = match response.unwrap() { + ResponseData::AuthenticatorGetAssertion(r) => r, + ResponseData::AuthenticatorGetNextAssertion(r) => r, _ => panic!("Invalid response type"), - } + }; + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = assertion_response; + let mut expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, + 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, 0x12, 0x55, + 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, 0x00, + ]; + let signature_counter_position = expected_auth_data.len() - 4; + BigEndian::write_u32( + &mut expected_auth_data[signature_counter_position..], + signature_counter, + ); + expected_auth_data.extend(expected_extension_cbor); + assert_eq!(auth_data, expected_auth_data); + assert_eq!(user, Some(expected_user)); + assert_eq!(number_of_credentials, expected_number_of_credentials); } fn check_assertion_response_with_extension( @@ -2574,8 +2641,7 @@ mod test { &[], ); - let get_assertion_response = - ctap_state.process_get_next_assertion(&mut env, CtapInstant::new(0)); + let get_assertion_response = ctap_state.process_get_next_assertion(&mut env); check_assertion_response_with_user( get_assertion_response, user1, @@ -2585,8 +2651,7 @@ mod test { &[], ); - let get_assertion_response = - ctap_state.process_get_next_assertion(&mut env, CtapInstant::new(0)); + let get_assertion_response = ctap_state.process_get_next_assertion(&mut env); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -2659,16 +2724,13 @@ mod test { Some(3), ); - let get_assertion_response = - ctap_state.process_get_next_assertion(&mut env, CtapInstant::new(0)); + let get_assertion_response = ctap_state.process_get_next_assertion(&mut env); check_assertion_response(get_assertion_response, vec![0x02], signature_counter, None); - let get_assertion_response = - ctap_state.process_get_next_assertion(&mut env, CtapInstant::new(0)); + let get_assertion_response = ctap_state.process_get_next_assertion(&mut env); check_assertion_response(get_assertion_response, vec![0x01], signature_counter, None); - let get_assertion_response = - ctap_state.process_get_next_assertion(&mut env, CtapInstant::new(0)); + let get_assertion_response = ctap_state.process_get_next_assertion(&mut env); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -2680,8 +2742,7 @@ mod test { let mut env = TestEnv::new(); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); - let get_assertion_response = - ctap_state.process_get_next_assertion(&mut env, CtapInstant::new(0)); + let get_assertion_response = ctap_state.process_get_next_assertion(&mut env); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -2733,8 +2794,7 @@ mod test { assert!(cbor_write(cbor_value, &mut command_cbor).is_ok()); ctap_state.process_command(&mut env, &command_cbor, DUMMY_CHANNEL, CtapInstant::new(0)); - let get_assertion_response = - ctap_state.process_get_next_assertion(&mut env, CtapInstant::new(0)); + let get_assertion_response = ctap_state.process_get_next_assertion(&mut env); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -2779,7 +2839,7 @@ mod test { .set(|_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL)); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); - let reset_reponse = ctap_state.process_reset(&mut env, DUMMY_CHANNEL, CtapInstant::new(0)); + let reset_reponse = ctap_state.process_reset(&mut env, DUMMY_CHANNEL); assert_eq!( reset_reponse, @@ -2795,7 +2855,7 @@ mod test { // This is a GetNextAssertion command. ctap_state.process_command(&mut env, &[0x08], DUMMY_CHANNEL, CtapInstant::new(0)); - let reset_reponse = ctap_state.process_reset(&mut env, DUMMY_CHANNEL, CtapInstant::new(0)); + let reset_reponse = ctap_state.process_reset(&mut env, DUMMY_CHANNEL); assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); } @@ -3216,4 +3276,277 @@ mod test { )) ); } + + #[test] + fn test_permission_timeout() { + let mut env = TestEnv::new(); + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + + // Write 2 credentials for later assertions. + for i in 0..3 { + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![i as u8]; + assert!(ctap_state + .process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL) + .is_ok()); + } + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetAssertion(get_assertion_params), + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert!(get_assertion_response.is_ok()); + let get_next_assertion_response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetNextAssertion, + DUMMY_CHANNEL, + CtapInstant::new(0) + STATEFUL_COMMAND_TIMEOUT_DURATION - Milliseconds(1 as ClockInt), + ); + assert!(get_next_assertion_response.is_ok()); + let get_next_assertion_response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetNextAssertion, + DUMMY_CHANNEL, + CtapInstant::new(0) + STATEFUL_COMMAND_TIMEOUT_DURATION + Milliseconds(1 as ClockInt), + ); + assert_eq!( + get_next_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_reset_timeout() { + let mut env = TestEnv::new(); + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + + let response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorReset, + DUMMY_CHANNEL, + CtapInstant::new(0) + RESET_TIMEOUT_DURATION + Milliseconds(1 as ClockInt), + ); + assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); + } + + #[test] + fn test_credential_management_timeout() { + let mut env = TestEnv::new(); + let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); + let pin_uv_auth_token = [0x55; 32]; + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + + let credential_source = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: env.rng().gen_uniform_u8x32().to_vec(), + private_key: crypto::ecdsa::SecKey::gensk(env.rng()), + rp_id: String::from("example.com"), + user_handle: vec![0x01], + user_display_name: Some("display_name".to_string()), + cred_protect_policy: None, + creation_order: 0, + user_name: Some("name".to_string()), + user_icon: Some("icon".to_string()), + cred_blob: None, + large_blob_key: None, + }; + + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + ctap_state.client_pin = client_pin; + + for i in 0..3 { + let mut credential = credential_source.clone(); + credential.rp_id = i.to_string(); + storage::store_credential(&mut env, credential).unwrap(); + } + + storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + let pin_uv_auth_param = Some(vec![ + 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, + 0xD0, 0xD1, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, + sub_command_params: None, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, + }; + let response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorCredentialManagement(cred_management_params), + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert!(matches!( + response, + Ok(ResponseData::AuthenticatorCredentialManagement(_)) + )); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + let response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorCredentialManagement(cred_management_params), + DUMMY_CHANNEL, + CtapInstant::new(0) + STATEFUL_COMMAND_TIMEOUT_DURATION - Milliseconds(1 as ClockInt), + ); + assert!(matches!( + response, + Ok(ResponseData::AuthenticatorCredentialManagement(_)) + )); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + let response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorCredentialManagement(cred_management_params), + DUMMY_CHANNEL, + CtapInstant::new(0) + STATEFUL_COMMAND_TIMEOUT_DURATION + Milliseconds(1 as ClockInt), + ); + assert_eq!(response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); + } + + #[test] + fn test_channel_interleaving() { + let mut env = TestEnv::new(); + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + const NEW_CHANNEL: Channel = Channel::MainHid([0xAA, 0xAA, 0xAA, 0xAA]); + + // Write 3 credentials for later assertions. + for i in 0..3 { + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![i as u8]; + assert!(ctap_state + .process_make_credential(&mut env, make_credential_params, DUMMY_CHANNEL) + .is_ok()); + } + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: GetAssertionExtensions::default(), + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetAssertion(get_assertion_params), + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert!(matches!( + get_assertion_response, + Ok(ResponseData::AuthenticatorGetAssertion(_)) + )); + let get_next_assertion_response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetNextAssertion, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert!(get_next_assertion_response.is_ok()); + assert!(matches!( + get_next_assertion_response, + Ok(ResponseData::AuthenticatorGetNextAssertion(_)) + )); + let get_next_assertion_response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetNextAssertion, + NEW_CHANNEL, + CtapInstant::new(0), + ); + assert_eq!( + get_next_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + let get_next_assertion_response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetNextAssertion, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert_eq!( + get_next_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + #[cfg(feature = "vendor_hid")] + fn test_main_hid() { + let mut env = TestEnv::new(); + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + + let response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetInfo, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert!(matches!( + response, + Ok(ResponseData::AuthenticatorGetInfo(_)) + )); + let response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorGetInfo, + VENDOR_CHANNEL, + CtapInstant::new(0), + ); + assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)); + } + + #[test] + #[cfg(feature = "vendor_hid")] + fn test_vendor_hid() { + let mut env = TestEnv::new(); + let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); + + let response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorVendorUpgradeInfo, + VENDOR_CHANNEL, + CtapInstant::new(0), + ); + assert!(matches!( + response, + Ok(ResponseData::AuthenticatorVendorUpgradeInfo(_)) + )); + let response = ctap_state.process_parsed_command( + &mut env, + Command::AuthenticatorVendorUpgradeInfo, + DUMMY_CHANNEL, + CtapInstant::new(0), + ); + assert_eq!(response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND)); + } } diff --git a/src/lib.rs b/src/lib.rs index 4e23b2e..38e3387 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,10 +31,10 @@ use clock::CtapInstant; // adding either the defmt or log feature and crate dependency. #[cfg(feature = "debug_ctap")] macro_rules! debug_ctap { - ($env: expr, $($rest:tt)*) => { + ($env: expr, $($rest:tt)*) => {{ use core::fmt::Write; writeln!($env.write(), $($rest)*).unwrap(); - }; + }}; } #[cfg(not(feature = "debug_ctap"))] macro_rules! debug_ctap {