From c6726660ac09ea05766d09960f93761fe3651d6c Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 11:22:41 +0100 Subject: [PATCH 1/7] adds the command logic for credential management --- src/ctap/command.rs | 91 ++- src/ctap/credential_management.rs | 912 ++++++++++++++++++++++++++++++ src/ctap/data_formats.rs | 148 ++++- src/ctap/mod.rs | 86 ++- src/ctap/pin_protocol_v1.rs | 20 + src/ctap/response.rs | 141 ++++- src/ctap/storage.rs | 10 +- 7 files changed, 1366 insertions(+), 42 deletions(-) create mode 100644 src/ctap/credential_management.rs diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 6f0aa72..57056a7 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -14,10 +14,10 @@ use super::data_formats::{ extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, - extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, - GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, CredentialManagementSubCommand, + CredentialManagementSubCommandParameters, GetAssertionExtensions, GetAssertionOptions, + MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::key_material; use super::status_code::Ctap2StatusCode; @@ -41,6 +41,7 @@ pub enum Command { AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorReset, AuthenticatorGetNextAssertion, + AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters), AuthenticatorSelection, // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands @@ -110,6 +111,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorGetNextAssertion) } + Command::AUTHENTICATOR_CREDENTIAL_MANAGEMENT => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorCredentialManagement( + AuthenticatorCredentialManagementParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_SELECTION => { // Parameters are ignored. Ok(Command::AuthenticatorSelection) @@ -388,6 +395,43 @@ impl TryFrom for AuthenticatorAttestationMaterial { } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorCredentialManagementParameters { + pub sub_command: CredentialManagementSubCommand, + pub sub_command_params: Option, + pub pin_protocol: Option, + pub pin_auth: Option>, +} + +impl TryFrom for AuthenticatorCredentialManagementParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => sub_command, + 0x02 => sub_command_params, + 0x03 => pin_protocol, + 0x04 => pin_auth, + } = extract_map(cbor_value)?; + } + + let sub_command = CredentialManagementSubCommand::try_from(ok_or_missing(sub_command)?)?; + let sub_command_params = sub_command_params + .map(CredentialManagementSubCommandParameters::try_from) + .transpose()?; + let pin_protocol = pin_protocol.map(extract_unsigned).transpose()?; + let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + + Ok(AuthenticatorCredentialManagementParameters { + sub_command, + sub_command_params, + pin_protocol, + pin_auth, + }) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct AuthenticatorVendorConfigureParameters { pub lockdown: bool, @@ -551,10 +595,10 @@ mod test { 9 => 0x03, 10 => "example.com", }; - let returned_pin_protocol_parameters = + let returned_client_pin_parameters = AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); - let expected_pin_protocol_parameters = AuthenticatorClientPinParameters { + let expected_client_pin_parameters = AuthenticatorClientPinParameters { pin_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: Some(cose_key), @@ -568,8 +612,8 @@ mod test { }; assert_eq!( - returned_pin_protocol_parameters, - expected_pin_protocol_parameters + returned_client_pin_parameters, + expected_client_pin_parameters ); } @@ -595,6 +639,37 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); } + #[test] + fn test_from_cbor_cred_management_parameters() { + let cbor_value = cbor_map! { + 1 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, + 2 => cbor_map!{ + 0x01 => vec![0x1D; 32], + }, + 3 => 1, + 4 => vec! [0x9A; 16], + }; + let returned_cred_management_parameters = + AuthenticatorCredentialManagementParameters::try_from(cbor_value).unwrap(); + + let params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(vec![0x1D; 32]), + credential_id: None, + user: None, + }; + let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, + sub_command_params: Some(params), + pin_protocol: Some(1), + pin_auth: Some(vec![0x9A; 16]), + }; + + assert_eq!( + returned_cred_management_parameters, + expected_cred_management_parameters + ); + } + #[test] fn test_deserialize_selection() { let cbor_bytes = [Command::AUTHENTICATOR_SELECTION]; diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs new file mode 100644 index 0000000..4a0d29c --- /dev/null +++ b/src/ctap/credential_management.rs @@ -0,0 +1,912 @@ +// Copyright 2020-2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::command::AuthenticatorCredentialManagementParameters; +use super::data_formats::{ + CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, + PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource, + PublicKeyCredentialUserEntity, +}; +use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; +use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use super::timed_permission::TimedPermission; +use super::{check_command_permission, StatefulCommand, STATEFUL_COMMAND_TIMEOUT_DURATION}; +use alloc::collections::BTreeSet; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::iter::FromIterator; +use crypto::sha256::Sha256; +use crypto::Hash256; +use libtock_drivers::timer::ClockValue; + +/// Generates the response for subcommands enumerating RPs. +fn enumerate_rps_response( + rp_id: Option, + total_rps: Option, +) -> Result { + let rp = rp_id.clone().map(|rp_id| PublicKeyCredentialRpEntity { + rp_id, + rp_name: None, + rp_icon: None, + }); + let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec()); + + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp, + rp_id_hash, + total_rps, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }) +} + +/// Generates the response for subcommands enumerating credentials. +fn enumerate_credentials_response( + credential: PublicKeyCredentialSource, + total_credentials: Option, +) -> Result { + let PublicKeyCredentialSource { + key_type, + credential_id, + private_key, + rp_id: _, + user_handle, + user_display_name, + cred_protect_policy, + creation_order: _, + user_name, + user_icon, + } = credential; + let user = PublicKeyCredentialUserEntity { + user_id: user_handle, + user_name, + user_display_name, + user_icon, + }; + let credential_id = PublicKeyCredentialDescriptor { + key_type, + key_id: credential_id, + transports: None, // You can set USB as a hint here. + }; + let public_key = CoseKey::from(private_key.genpk()); + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: Some(user), + credential_id: Some(credential_id), + public_key: Some(public_key), + total_credentials, + cred_protect: cred_protect_policy, + // TODO(kaczmarczyck) add when largeBlobKey is implemented + large_blob_key: None, + }) +} + +/// Processes the subcommand getCredsMetadata for CredentialManagement. +fn process_get_creds_metadata( + persistent_store: &PersistentStore, +) -> Result { + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: Some(persistent_store.count_credentials()? as u64), + max_possible_remaining_resident_credentials_count: Some( + persistent_store.remaining_credentials()? as u64, + ), + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }) +} + +/// Processes the subcommand enumerateRPsBegin for CredentialManagement. +fn process_enumerate_rps_begin( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + let mut rp_set = BTreeSet::new(); + let mut iter_result = Ok(()); + for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { + rp_set.insert(credential.rp_id); + } + iter_result?; + let mut rp_ids = Vec::from_iter(rp_set); + let total_rps = rp_ids.len(); + + // TODO(kaczmarczyck) behaviour with empty list? + let rp_id = rp_ids.pop(); + if total_rps > 1 { + *stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + *stateful_command_type = Some(StatefulCommand::EnumerateRps(rp_ids)); + } + enumerate_rps_response(rp_id, Some(total_rps as u64)) +} + +/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. +fn process_enumerate_rps_get_next_rp( + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + check_command_permission(stateful_command_permission, now)?; + if let Some(StatefulCommand::EnumerateRps(rp_ids)) = stateful_command_type { + let rp_id = rp_ids.pop().ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + enumerate_rps_response(Some(rp_id), None) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } +} + +/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. +fn process_enumerate_credentials_begin( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + sub_command_params: CredentialManagementSubCommandParameters, + now: ClockValue, +) -> Result { + let rp_id_hash = sub_command_params + .rp_id_hash + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + let mut iter_result = Ok(()); + let iter = persistent_store.iter_credentials(&mut iter_result)?; + let mut rp_credentials: Vec = iter + .filter_map(|(key, credential)| { + let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes()); + if cred_rp_id_hash == rp_id_hash.as_slice() { + Some(key) + } else { + None + } + }) + .collect(); + iter_result?; + let total_credentials = rp_credentials.len(); + let current_key = rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + let credential = persistent_store.get_credential(current_key)?; + if total_credentials > 1 { + *stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + *stateful_command_type = Some(StatefulCommand::EnumerateCredentials(rp_credentials)); + } + enumerate_credentials_response(credential, Some(total_credentials as u64)) +} + +/// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement. +fn process_enumerate_credentials_get_next_credential( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + mut stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + check_command_permission(stateful_command_permission, now)?; + if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut stateful_command_type + { + let current_key = rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + let credential = persistent_store.get_credential(current_key)?; + enumerate_credentials_response(credential, None) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } +} + +/// Processes the subcommand deleteCredential for CredentialManagement. +fn process_delete_credential( + persistent_store: &mut PersistentStore, + sub_command_params: CredentialManagementSubCommandParameters, +) -> Result<(), Ctap2StatusCode> { + let credential_id = sub_command_params + .credential_id + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? + .key_id; + persistent_store.delete_credential(&credential_id) +} + +/// Processes the subcommand updateUserInformation for CredentialManagement. +fn process_update_user_information( + persistent_store: &mut PersistentStore, + sub_command_params: CredentialManagementSubCommandParameters, +) -> Result<(), Ctap2StatusCode> { + let credential_id = sub_command_params + .credential_id + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? + .key_id; + let user = sub_command_params + .user + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + persistent_store.update_credential(&credential_id, user) +} + +/// Checks the PIN protocol. +/// +/// TODO(#246) refactor after #246 is merged +fn pin_uv_auth_protocol_check(pin_uv_auth_protocol: Option) -> Result<(), Ctap2StatusCode> { + match pin_uv_auth_protocol { + Some(1) => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } +} + +/// Processes the CredentialManagement command and all its subcommands. +pub fn process_credential_management( + persistent_store: &mut PersistentStore, + stateful_command_permission: &mut TimedPermission, + mut stateful_command_type: &mut Option, + pin_protocol_v1: &mut PinProtocolV1, + cred_management_params: AuthenticatorCredentialManagementParameters, + now: ClockValue, +) -> Result { + let AuthenticatorCredentialManagementParameters { + sub_command, + sub_command_params, + pin_protocol, + pin_auth, + } = cred_management_params; + + match (sub_command, &mut stateful_command_type) { + ( + CredentialManagementSubCommand::EnumerateRpsGetNextRp, + Some(StatefulCommand::EnumerateRps(_)), + ) => (), + ( + CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + Some(StatefulCommand::EnumerateCredentials(_)), + ) => (), + (_, _) => { + *stateful_command_type = None; + } + } + + match sub_command { + CredentialManagementSubCommand::GetCredsMetadata + | CredentialManagementSubCommand::EnumerateRpsBegin + | CredentialManagementSubCommand::DeleteCredential + | CredentialManagementSubCommand::EnumerateCredentialsBegin + | CredentialManagementSubCommand::UpdateUserInformation => { + pin_uv_auth_protocol_check(pin_protocol)?; + persistent_store + .pin_hash()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let mut management_data = vec![sub_command as u8]; + if let Some(sub_command_params) = sub_command_params.clone() { + if !cbor::write(sub_command_params.into(), &mut management_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + } + if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + pin_protocol_v1.has_permission(PinPermission::CredentialManagement)?; + pin_protocol_v1.has_no_permission_rp_id()?; + // TODO(kaczmarczyck) sometimes allow a RP ID + } + CredentialManagementSubCommand::EnumerateRpsGetNextRp + | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} + } + + let response = match sub_command { + CredentialManagementSubCommand::GetCredsMetadata => { + Some(process_get_creds_metadata(persistent_store)?) + } + CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( + persistent_store, + stateful_command_permission, + stateful_command_type, + now, + )?), + CredentialManagementSubCommand::EnumerateRpsGetNextRp => { + Some(process_enumerate_rps_get_next_rp( + stateful_command_permission, + stateful_command_type, + now, + )?) + } + CredentialManagementSubCommand::EnumerateCredentialsBegin => { + Some(process_enumerate_credentials_begin( + persistent_store, + stateful_command_permission, + stateful_command_type, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + now, + )?) + } + CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => { + Some(process_enumerate_credentials_get_next_credential( + persistent_store, + stateful_command_permission, + stateful_command_type, + now, + )?) + } + CredentialManagementSubCommand::DeleteCredential => { + process_delete_credential( + persistent_store, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + CredentialManagementSubCommand::UpdateUserInformation => { + process_update_user_information( + persistent_store, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + }; + Ok(ResponseData::AuthenticatorCredentialManagement(response)) +} + +#[cfg(test)] +mod test { + use super::super::data_formats::PublicKeyCredentialType; + use super::super::CtapState; + use super::*; + use crypto::rng256::{Rng256, ThreadRng256}; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + + fn create_credential_source(rng: &mut impl Rng256) -> PublicKeyCredentialSource { + let private_key = crypto::ecdsa::SecKey::gensk(rng); + PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: rng.gen_uniform_u8x32().to_vec(), + private_key, + 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()), + } + } + + #[test] + fn test_process_get_creds_metadata() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source = create_credential_source(&mut rng); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, + 0xFB, 0x00, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth: pin_auth.clone(), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let initial_capacity = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.existing_resident_credentials_count, Some(0)); + response + .max_possible_remaining_resident_credentials_count + .unwrap() + } + _ => panic!("Invalid response type"), + }; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.existing_resident_credentials_count, Some(1)); + assert_eq!( + response.max_possible_remaining_resident_credentials_count, + Some(initial_capacity - 1) + ); + } + _ => panic!("Invalid response type"), + }; + } + + #[test] + fn test_process_enumerate_rps_with_uv() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source1 = create_credential_source(&mut rng); + let mut credential_source2 = create_credential_source(&mut rng); + credential_source2.rp_id = "another.example.com".to_string(); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source1) + .unwrap(); + ctap_state + .persistent_store + .store_credential(credential_source2) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = 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_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let first_rp_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.total_rps, Some(2)); + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + rp_id + } + _ => panic!("Invalid response type"), + }; + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let second_rp_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.total_rps, None); + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + rp_id + } + _ => panic!("Invalid response type"), + }; + + assert!(first_rp_id != second_rp_id); + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_enumerate_credentials_with_uv() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source1 = create_credential_source(&mut rng); + let mut credential_source2 = create_credential_source(&mut rng); + credential_source2.user_handle = vec![0x02]; + credential_source2.user_name = Some("user2".to_string()); + credential_source2.user_display_name = Some("User Two".to_string()); + credential_source2.user_icon = Some("icon2".to_string()); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source1) + .unwrap(); + ctap_state + .persistent_store + .store_credential(credential_source2) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28, + 0x2B, 0x68, + ]); + + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()), + credential_id: None, + user: None, + }; + // RP ID hash: + // A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947 + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let first_credential_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert!(response.user.is_some()); + assert!(response.public_key.is_some()); + assert_eq!(response.total_credentials, Some(2)); + response.credential_id.unwrap().key_id + } + _ => panic!("Invalid response type"), + }; + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let second_credential_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert!(response.user.is_some()); + assert!(response.public_key.is_some()); + assert_eq!(response.total_credentials, None); + response.credential_id.unwrap().key_id + } + _ => panic!("Invalid response type"), + }; + + assert!(first_credential_id != second_credential_id); + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_delete_credential() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut credential_source = create_credential_source(&mut rng); + credential_source.credential_id = vec![0x1D; 32]; + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89, + 0x9C, 0x64, + ]); + + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, // You can set USB as a hint here. + }; + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: None, + credential_id: Some(credential_id), + user: None, + }; + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::DeleteCredential, + sub_command_params: Some(sub_command_params.clone()), + pin_protocol: Some(1), + pin_auth: pin_auth.clone(), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Ok(ResponseData::AuthenticatorCredentialManagement(None)) + ); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::DeleteCredential, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + ); + } + + #[test] + fn test_process_update_user_information() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut credential_source = create_credential_source(&mut rng); + credential_source.credential_id = vec![0x1D; 32]; + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD, + 0x9D, 0xB7, + ]); + + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, // You can set USB as a hint here. + }; + let new_user = PublicKeyCredentialUserEntity { + user_id: vec![0xFF], + user_name: Some("new_name".to_string()), + user_display_name: Some("new_display_name".to_string()), + user_icon: Some("new_icon".to_string()), + }; + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: None, + credential_id: Some(credential_id), + user: Some(new_user), + }; + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::UpdateUserInformation, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Ok(ResponseData::AuthenticatorCredentialManagement(None)) + ); + + let updated_credential = ctap_state + .persistent_store + .find_credential("example.com", &[0x1D; 32], false) + .unwrap() + .unwrap(); + assert_eq!(updated_credential.user_handle, vec![0x01]); + assert_eq!(&updated_credential.user_name.unwrap(), "new_name"); + assert_eq!( + &updated_credential.user_display_name.unwrap(), + "new_display_name" + ); + assert_eq!(&updated_credential.user_icon.unwrap(), "new_icon"); + } + + #[test] + fn test_process_credential_management_invalid_pin_protocol() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, + 0xFB, 0x00, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(123456), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_process_credential_management_invalid_pin_auth() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth: Some(vec![0u8; 16]), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } +} diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index dfdf4ed..bb330ef 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -27,6 +27,7 @@ use enum_iterator::IntoEnumIterator; const ES256_ALGORITHM: i64 = -7; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity +#[derive(Clone)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialRpEntity { pub rp_id: String, @@ -58,8 +59,19 @@ impl TryFrom for PublicKeyCredentialRpEntity { } } +impl From for cbor::Value { + fn from(entity: PublicKeyCredentialRpEntity) -> Self { + cbor_map_options! { + "id" => entity.rp_id, + "name" => entity.rp_name, + "icon" => entity.rp_icon, + } + } +} + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -173,7 +185,8 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum AuthenticatorTransport { Usb, @@ -210,7 +223,8 @@ impl TryFrom for AuthenticatorTransport { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialDescriptor { pub key_type: PublicKeyCredentialType, pub key_id: Vec, @@ -788,6 +802,88 @@ impl TryFrom for ClientPinSubCommand { } } +#[derive(Clone, Copy)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum CredentialManagementSubCommand { + GetCredsMetadata = 0x01, + EnumerateRpsBegin = 0x02, + EnumerateRpsGetNextRp = 0x03, + EnumerateCredentialsBegin = 0x04, + EnumerateCredentialsGetNextCredential = 0x05, + DeleteCredential = 0x06, + UpdateUserInformation = 0x07, +} + +impl From for cbor::Value { + fn from(subcommand: CredentialManagementSubCommand) -> Self { + (subcommand as u64).into() + } +} + +impl TryFrom for CredentialManagementSubCommand { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + let subcommand_int = extract_unsigned(cbor_value)?; + match subcommand_int { + 0x01 => Ok(CredentialManagementSubCommand::GetCredsMetadata), + 0x02 => Ok(CredentialManagementSubCommand::EnumerateRpsBegin), + 0x03 => Ok(CredentialManagementSubCommand::EnumerateRpsGetNextRp), + 0x04 => Ok(CredentialManagementSubCommand::EnumerateCredentialsBegin), + 0x05 => Ok(CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential), + 0x06 => Ok(CredentialManagementSubCommand::DeleteCredential), + 0x07 => Ok(CredentialManagementSubCommand::UpdateUserInformation), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), + } + } +} + +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct CredentialManagementSubCommandParameters { + pub rp_id_hash: Option>, + pub credential_id: Option, + pub user: Option, +} + +impl TryFrom for CredentialManagementSubCommandParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => rp_id_hash, + 0x02 => credential_id, + 0x03 => user, + } = extract_map(cbor_value)?; + } + + let rp_id_hash = rp_id_hash.map(extract_byte_string).transpose()?; + let credential_id = credential_id + .map(PublicKeyCredentialDescriptor::try_from) + .transpose()?; + let user = user + .map(PublicKeyCredentialUserEntity::try_from) + .transpose()?; + Ok(Self { + rp_id_hash, + credential_id, + user, + }) + } +} + +impl From for cbor::Value { + fn from(sub_command_params: CredentialManagementSubCommandParameters) -> Self { + cbor_map_options! { + 0x01 => sub_command_params.rp_id_hash, + 0x02 => sub_command_params.credential_id, + 0x03 => sub_command_params.user, + } + } +} + pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { match cbor_value { cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), @@ -1504,6 +1600,52 @@ mod test { } } + #[test] + fn test_from_into_cred_management_sub_command() { + let cbor_sub_command: cbor::Value = cbor_int!(0x01); + let sub_command = CredentialManagementSubCommand::try_from(cbor_sub_command.clone()); + let expected_sub_command = CredentialManagementSubCommand::GetCredsMetadata; + assert_eq!(sub_command, Ok(expected_sub_command)); + let created_cbor: cbor::Value = sub_command.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command); + + for command in CredentialManagementSubCommand::into_enum_iter() { + let created_cbor: cbor::Value = command.clone().into(); + let reconstructed = CredentialManagementSubCommand::try_from(created_cbor).unwrap(); + assert_eq!(command, reconstructed); + } + } + + #[test] + fn test_from_into_cred_management_sub_command_params() { + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x2D, 0x2D, 0x2D, 0x2D], + transports: Some(vec![AuthenticatorTransport::Usb]), + }; + let user_entity = PublicKeyCredentialUserEntity { + user_id: vec![0x1D, 0x1D, 0x1D, 0x1D], + user_name: Some("foo".to_string()), + user_display_name: Some("bar".to_string()), + user_icon: Some("example.com/foo/icon.png".to_string()), + }; + let cbor_sub_command_params = cbor_map! { + 0x01 => vec![0x1D; 32], + 0x02 => credential_id.clone(), + 0x03 => user_entity.clone(), + }; + let sub_command_params = + CredentialManagementSubCommandParameters::try_from(cbor_sub_command_params.clone()); + let expected_sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(vec![0x1D; 32]), + credential_id: Some(credential_id), + user: Some(user_entity), + }; + assert_eq!(sub_command_params, Ok(expected_sub_command_params)); + let created_cbor: cbor::Value = sub_command_params.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command_params); + } + #[test] fn test_credential_source_cbor_round_trip() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8895605..d4b73f4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -14,6 +14,7 @@ pub mod apdu; pub mod command; +mod credential_management; #[cfg(feature = "with_ctap1")] mod ctap1; pub mod data_formats; @@ -30,6 +31,7 @@ use self::command::{ AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, MAX_CREDENTIAL_COUNT_IN_LIST, }; +use self::credential_management::process_credential_management; use self::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, @@ -98,7 +100,7 @@ pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); -const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); +pub const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -139,15 +141,29 @@ struct AssertionInput { has_uv: bool, } -struct AssertionState { +pub struct AssertionState { assertion_input: AssertionInput, // Sorted by ascending order of creation, so the last element is the most recent one. next_credential_keys: Vec, } -enum StatefulCommand { +pub enum StatefulCommand { Reset, GetAssertion(AssertionState), + EnumerateRps(Vec), + EnumerateCredentials(Vec), +} + +pub fn check_command_permission( + stateful_command_permission: &mut TimedPermission, + now: ClockValue, +) -> Result<(), Ctap2StatusCode> { + *stateful_command_permission = stateful_command_permission.check_expiration(now); + if stateful_command_permission.is_granted(now) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } } // This struct currently holds all state, not only the persistent memory. The persistent members are @@ -200,15 +216,6 @@ where self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); } - fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { - self.update_command_permission(now); - if self.stateful_command_permission.is_granted(now) { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } - } - pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { if USE_SIGNATURE_COUNTER { let increment = self.rng.gen_uniform_u32x8()[0] % 8 + 1; @@ -330,6 +337,14 @@ where Command::AuthenticatorGetNextAssertion, Some(StatefulCommand::GetAssertion(_)), ) => (), + ( + Command::AuthenticatorCredentialManagement(_), + Some(StatefulCommand::EnumerateRps(_)), + ) => (), + ( + Command::AuthenticatorCredentialManagement(_), + Some(StatefulCommand::EnumerateCredentials(_)), + ) => (), (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), // GetInfo does not reset stateful commands. (Command::AuthenticatorGetInfo, _) => (), @@ -350,6 +365,16 @@ where Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid, now), + Command::AuthenticatorCredentialManagement(params) => { + process_credential_management( + &mut self.persistent_store, + &mut self.stateful_command_permission, + &mut self.stateful_command_type, + &mut self.pin_protocol_v1, + params, + now, + ) + } Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands @@ -818,7 +843,7 @@ where &mut self, now: ClockValue, ) -> Result { - self.check_command_permission(now)?; + check_command_permission(&mut self.stateful_command_permission, now)?; let (assertion_input, credential) = if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.stateful_command_type @@ -844,6 +869,7 @@ where String::from("clientPin"), self.persistent_store.pin_hash()?.is_some(), ); + options_map.insert(String::from("credMgmt"), true); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -895,7 +921,7 @@ where ) -> Result { // Resets are only possible in the first 10 seconds after booting. // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? - self.check_command_permission(now)?; + check_command_permission(&mut self.stateful_command_permission, now)?; match &self.stateful_command_type { Some(StatefulCommand::Reset) => (), _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), @@ -1053,11 +1079,12 @@ mod test { expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend( [ - 0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, - 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, - 0x08, 0x18, 0x70, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, - 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, - 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, + 0x04, 0xA4, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, + 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, + 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 0x09, + 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, + 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, 0x65, + 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, ] .iter(), ); @@ -2034,6 +2061,22 @@ mod test { assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); } + #[test] + fn test_process_credential_management_unknown_subcommand() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // The subcommand 0xEE does not exist. + let reponse = ctap_state.process_command( + &[0x0A, 0xA1, 0x01, 0x18, 0xEE], + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let expected_response = vec![Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND as u8]; + assert_eq!(reponse, expected_response); + } + #[test] fn test_process_unknown_command() { let mut rng = ThreadRng256 {}; @@ -2041,10 +2084,9 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // This command does not exist. - let reset_reponse = - ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + let reponse = ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8]; - assert_eq!(reset_reponse, expected_response); + assert_eq!(reponse, expected_response); } #[test] diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index b8aeb21..14c3d56 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -563,6 +563,13 @@ impl PinProtocolV1 { } } + pub fn has_no_permission_rp_id(&self) -> Result<(), Ctap2StatusCode> { + if self.permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + Ok(()) + } + pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { if let Some(permissions_rp_id) = &self.permissions_rp_id { if rp_id != permissions_rp_id { @@ -1187,6 +1194,19 @@ mod test { } } + #[test] + fn test_has_no_permission_rp_id() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + assert_eq!(pin_protocol_v1.has_no_permission_rp_id(), Ok(())); + assert_eq!(pin_protocol_v1.permissions_rp_id, None,); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_permission_rp_id(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_has_permission_for_rp_id() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/response.rs b/src/ctap/response.rs index f40dc21..8d5386b 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -14,7 +14,8 @@ use super::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialUserEntity, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; use alloc::collections::BTreeMap; use alloc::string::String; @@ -30,6 +31,7 @@ pub enum ResponseData { AuthenticatorGetInfo(AuthenticatorGetInfoResponse), AuthenticatorClientPin(Option), AuthenticatorReset, + AuthenticatorCredentialManagement(Option), AuthenticatorSelection, AuthenticatorVendor(AuthenticatorVendorResponse), } @@ -41,9 +43,9 @@ impl From for Option { ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()), ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()), ResponseData::AuthenticatorGetInfo(data) => Some(data.into()), - ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()), - ResponseData::AuthenticatorClientPin(None) => None, + ResponseData::AuthenticatorClientPin(data) => data.map(|d| d.into()), ResponseData::AuthenticatorReset => None, + ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()), ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } @@ -199,6 +201,54 @@ impl From for cbor::Value { } } +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub struct AuthenticatorCredentialManagementResponse { + pub existing_resident_credentials_count: Option, + pub max_possible_remaining_resident_credentials_count: Option, + pub rp: Option, + pub rp_id_hash: Option>, + pub total_rps: Option, + pub user: Option, + pub credential_id: Option, + pub public_key: Option, + pub total_credentials: Option, + pub cred_protect: Option, + pub large_blob_key: Option>, +} + +impl From for cbor::Value { + fn from(cred_management_response: AuthenticatorCredentialManagementResponse) -> Self { + let AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count, + max_possible_remaining_resident_credentials_count, + rp, + rp_id_hash, + total_rps, + user, + credential_id, + public_key, + total_credentials, + cred_protect, + large_blob_key, + } = cred_management_response; + + cbor_map_options! { + 0x01 => existing_resident_credentials_count, + 0x02 => max_possible_remaining_resident_credentials_count, + 0x03 => rp, + 0x04 => rp_id_hash, + 0x05 => total_rps, + 0x06 => user, + 0x07 => credential_id, + 0x08 => public_key.map(cbor::Value::from), + 0x09 => total_credentials, + 0x0A => cred_protect, + 0x0B => large_blob_key, + } + } +} + #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub struct AuthenticatorVendorResponse { @@ -222,10 +272,11 @@ impl From for cbor::Value { #[cfg(test)] mod test { - use super::super::data_formats::PackedAttestationStatement; + use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType}; use super::super::ES256_CRED_PARAM; use super::*; use cbor::{cbor_bytes, cbor_map}; + use crypto::rng256::ThreadRng256; #[test] fn test_make_credential_into_cbor() { @@ -379,6 +430,88 @@ mod test { assert_eq!(response_cbor, None); } + #[test] + fn test_used_credential_management_into_cbor() { + let cred_management_response = AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }; + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); + let expected_cbor = cbor_map_options! {}; + assert_eq!(response_cbor, Some(expected_cbor)); + } + + #[test] + fn test_used_credential_management_optionals_into_cbor() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let rp = PublicKeyCredentialRpEntity { + rp_id: String::from("example.com"), + rp_name: None, + rp_icon: None, + }; + let user = PublicKeyCredentialUserEntity { + user_id: vec![0xFA, 0xB1, 0xA2], + user_name: None, + user_display_name: None, + user_icon: None, + }; + let cred_descriptor = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, + }; + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + + let cred_management_response = AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: Some(100), + max_possible_remaining_resident_credentials_count: Some(96), + rp: Some(rp.clone()), + rp_id_hash: Some(vec![0x1D; 32]), + total_rps: Some(3), + user: Some(user.clone()), + credential_id: Some(cred_descriptor.clone()), + public_key: Some(cose_key.clone()), + total_credentials: Some(2), + cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional), + large_blob_key: Some(vec![0xBB; 64]), + }; + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); + let expected_cbor = cbor_map_options! { + 0x01 => 100, + 0x02 => 96, + 0x03 => rp, + 0x04 => vec![0x1D; 32], + 0x05 => 3, + 0x06 => user, + 0x07 => cred_descriptor, + 0x08 => cbor::Value::from(cose_key), + 0x09 => 2, + 0x0A => 0x01, + 0x0B => vec![0xBB; 64], + }; + assert_eq!(response_cbor, Some(expected_cbor)); + } + + #[test] + fn test_empty_credential_management_into_cbor() { + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(None).into(); + assert_eq!(response_cbor, None); + } + #[test] fn test_selection_into_cbor() { let response_cbor: Option = ResponseData::AuthenticatorSelection.into(); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 8df987d..7902f34 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -238,7 +238,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - pub fn _delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { + pub fn delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { let (key, _) = self.find_credential_item(credential_id)?; Ok(self.store.remove(key)?) } @@ -248,7 +248,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - pub fn _update_credential( + pub fn update_credential( &mut self, credential_id: &[u8], user: PublicKeyCredentialUserEntity, @@ -692,7 +692,7 @@ mod test { } let mut count = persistent_store.count_credentials().unwrap(); for credential_id in credential_ids { - assert!(persistent_store._delete_credential(&credential_id).is_ok()); + assert!(persistent_store.delete_credential(&credential_id).is_ok()); count -= 1; assert_eq!(persistent_store.count_credentials().unwrap(), count); } @@ -710,7 +710,7 @@ mod test { user_icon: Some("icon".to_string()), }; assert_eq!( - persistent_store._update_credential(&[0x1D], user.clone()), + persistent_store.update_credential(&[0x1D], user.clone()), Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) ); @@ -725,7 +725,7 @@ mod test { assert_eq!(stored_credential.user_display_name, None); assert_eq!(stored_credential.user_icon, None); assert!(persistent_store - ._update_credential(&credential_id, user.clone()) + .update_credential(&credential_id, user.clone()) .is_ok()); let stored_credential = persistent_store .find_credential("example.com", &credential_id, false) From 0bb6ee32fc96d32faa45a64127c7ae806a159019 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 14 Jan 2021 16:45:38 +0100 Subject: [PATCH 2/7] removes unused duplicate PIN protocol check helper --- src/ctap/credential_management.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index dfc004e..c83c955 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -23,7 +23,10 @@ use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; use super::timed_permission::TimedPermission; -use super::{check_command_permission, StatefulCommand, STATEFUL_COMMAND_TIMEOUT_DURATION}; +use super::{ + check_command_permission, check_pin_uv_auth_protocol, StatefulCommand, + STATEFUL_COMMAND_TIMEOUT_DURATION, +}; use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; @@ -251,16 +254,6 @@ fn process_update_user_information( persistent_store.update_credential(&credential_id, user) } -/// Checks the PIN protocol. -/// -/// TODO(#246) refactor after #246 is merged -fn pin_uv_auth_protocol_check(pin_uv_auth_protocol: Option) -> Result<(), Ctap2StatusCode> { - match pin_uv_auth_protocol { - Some(1) => Ok(()), - _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - } -} - /// Processes the CredentialManagement command and all its subcommands. pub fn process_credential_management( persistent_store: &mut PersistentStore, @@ -297,7 +290,7 @@ pub fn process_credential_management( | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::EnumerateCredentialsBegin | CredentialManagementSubCommand::UpdateUserInformation => { - pin_uv_auth_protocol_check(pin_protocol)?; + check_pin_uv_auth_protocol(pin_protocol)?; persistent_store .pin_hash()? .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; From 3702b61ce7c19dde35a5d9b74e2f9a61b3b0c972 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 15 Jan 2021 17:41:16 +0100 Subject: [PATCH 3/7] implements Default for Response type --- src/ctap/credential_management.rs | 25 +++---------------------- src/ctap/response.rs | 15 ++------------- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index c83c955..71c1e39 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -49,17 +49,10 @@ fn enumerate_rps_response( let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec()); Ok(AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, rp, rp_id_hash, total_rps, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, + ..Default::default() }) } @@ -93,11 +86,6 @@ fn enumerate_credentials_response( }; let public_key = CoseKey::from(private_key.genpk()); Ok(AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, - rp: None, - rp_id_hash: None, - total_rps: None, user: Some(user), credential_id: Some(credential_id), public_key: Some(public_key), @@ -105,6 +93,7 @@ fn enumerate_credentials_response( cred_protect: cred_protect_policy, // TODO(kaczmarczyck) add when largeBlobKey is implemented large_blob_key: None, + ..Default::default() }) } @@ -117,15 +106,7 @@ fn process_get_creds_metadata( max_possible_remaining_resident_credentials_count: Some( persistent_store.remaining_credentials()? as u64, ), - rp: None, - rp_id_hash: None, - total_rps: None, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, + ..Default::default() }) } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 03bc70d..e4cda5e 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -204,6 +204,7 @@ impl From for cbor::Value { } } +#[derive(Default)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub struct AuthenticatorCredentialManagementResponse { @@ -435,19 +436,7 @@ mod test { #[test] fn test_used_credential_management_into_cbor() { - let cred_management_response = AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, - rp: None, - rp_id_hash: None, - total_rps: None, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, - }; + let cred_management_response = AuthenticatorCredentialManagementResponse::default(); let response_cbor: Option = ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); let expected_cbor = cbor_map_options! {}; From e3353cb232e2892de0df43e6a04a8d4a2d376b6c Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 19 Jan 2021 12:42:41 +0100 Subject: [PATCH 4/7] only stores the RP ID index as state --- src/ctap/credential_management.rs | 128 ++++++++++++++++++++++++++---- src/ctap/mod.rs | 2 +- 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 71c1e39..dba7d36 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -31,11 +31,23 @@ use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use core::iter::FromIterator; use crypto::sha256::Sha256; use crypto::Hash256; use libtock_drivers::timer::ClockValue; +/// Generates a set with all existing RP IDs. +fn get_stored_rp_ids( + persistent_store: &PersistentStore, +) -> Result, Ctap2StatusCode> { + let mut rp_set = BTreeSet::new(); + let mut iter_result = Ok(()); + for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { + rp_set.insert(credential.rp_id); + } + iter_result?; + Ok(rp_set) +} + /// Generates the response for subcommands enumerating RPs. fn enumerate_rps_response( rp_id: Option, @@ -117,34 +129,35 @@ fn process_enumerate_rps_begin( stateful_command_type: &mut Option, now: ClockValue, ) -> Result { - let mut rp_set = BTreeSet::new(); - let mut iter_result = Ok(()); - for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { - rp_set.insert(credential.rp_id); - } - iter_result?; - let mut rp_ids = Vec::from_iter(rp_set); - let total_rps = rp_ids.len(); + let rp_set = get_stored_rp_ids(persistent_store)?; + let total_rps = rp_set.len(); - // TODO(kaczmarczyck) behaviour with empty list? - let rp_id = rp_ids.pop(); + // TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty? if total_rps > 1 { *stateful_command_permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateRps(rp_ids)); + *stateful_command_type = Some(StatefulCommand::EnumerateRps(1)); } - enumerate_rps_response(rp_id, Some(total_rps as u64)) + // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() + enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64)) } /// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. fn process_enumerate_rps_get_next_rp( + persistent_store: &PersistentStore, stateful_command_permission: &mut TimedPermission, stateful_command_type: &mut Option, now: ClockValue, ) -> Result { check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateRps(rp_ids)) = stateful_command_type { - let rp_id = rp_ids.pop().ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = stateful_command_type { + let rp_set = get_stored_rp_ids(persistent_store)?; + // A BTreeSet is already sorted. + let rp_id = rp_set + .into_iter() + .nth(*rp_id_index) + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + *stateful_command_type = Some(StatefulCommand::EnumerateRps(*rp_id_index + 1)); enumerate_rps_response(Some(rp_id), None) } else { Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -305,6 +318,7 @@ pub fn process_credential_management( )?), CredentialManagementSubCommand::EnumerateRpsGetNextRp => { Some(process_enumerate_rps_get_next_rp( + persistent_store, stateful_command_permission, stateful_command_type, now, @@ -544,6 +558,90 @@ mod test { ); } + #[test] + fn test_process_enumerate_rps_completeness() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source = create_credential_source(&mut rng); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + const NUM_CREDENTIALS: usize = 20; + for i in 0..NUM_CREDENTIALS { + let mut credential = credential_source.clone(); + credential.rp_id = i.to_string(); + ctap_state + .persistent_store + .store_credential(credential) + .unwrap(); + } + + ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); + let pin_auth = Some(vec![ + 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, + 0xD0, 0xD1, + ]); + + let mut rp_set = BTreeSet::new(); + // This mut is just to make the test code shorter. + // The command is different on the first loop iteration. + let mut cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth, + }; + + for _ in 0..NUM_CREDENTIALS { + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + if rp_set.is_empty() { + assert_eq!(response.total_rps, Some(NUM_CREDENTIALS as u64)); + } else { + assert_eq!(response.total_rps, None); + } + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + assert!(!rp_set.contains(&rp_id)); + rp_set.insert(rp_id); + } + _ => panic!("Invalid response type"), + }; + cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + } + + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + #[test] fn test_process_enumerate_credentials_with_uv() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 96dbd41..49d14f4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -165,7 +165,7 @@ pub struct AssertionState { pub enum StatefulCommand { Reset, GetAssertion(AssertionState), - EnumerateRps(Vec), + EnumerateRps(usize), EnumerateCredentials(Vec), } From 134c880212cb91f400052c33c28be38eac4e3ab8 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 19 Jan 2021 15:07:15 +0100 Subject: [PATCH 5/7] reworks command state to its own struct --- src/ctap/credential_management.rs | 85 +++++------------- src/ctap/mod.rs | 145 ++++++++++++++++++------------ 2 files changed, 111 insertions(+), 119 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index dba7d36..2dbe961 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -22,11 +22,7 @@ use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::timed_permission::TimedPermission; -use super::{ - check_command_permission, check_pin_uv_auth_protocol, StatefulCommand, - STATEFUL_COMMAND_TIMEOUT_DURATION, -}; +use super::{check_pin_uv_auth_protocol, StatefulCommand, StatefulPermission}; use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; @@ -125,8 +121,7 @@ fn process_get_creds_metadata( /// Processes the subcommand enumerateRPsBegin for CredentialManagement. fn process_enumerate_rps_begin( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, now: ClockValue, ) -> Result { let rp_set = get_stored_rp_ids(persistent_store)?; @@ -134,9 +129,7 @@ fn process_enumerate_rps_begin( // TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty? if total_rps > 1 { - *stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateRps(1)); + stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1)); } // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64)) @@ -145,19 +138,16 @@ fn process_enumerate_rps_begin( /// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. fn process_enumerate_rps_get_next_rp( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, - now: ClockValue, + stateful_command_permission: &mut StatefulPermission, ) -> Result { - check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = stateful_command_type { + if let StatefulCommand::EnumerateRps(rp_id_index) = stateful_command_permission.get_command()? { let rp_set = get_stored_rp_ids(persistent_store)?; // A BTreeSet is already sorted. let rp_id = rp_set .into_iter() .nth(*rp_id_index) .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - *stateful_command_type = Some(StatefulCommand::EnumerateRps(*rp_id_index + 1)); + *rp_id_index += 1; enumerate_rps_response(Some(rp_id), None) } else { Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -167,8 +157,7 @@ fn process_enumerate_rps_get_next_rp( /// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. fn process_enumerate_credentials_begin( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, sub_command_params: CredentialManagementSubCommandParameters, now: ClockValue, ) -> Result { @@ -194,9 +183,8 @@ fn process_enumerate_credentials_begin( .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; let credential = persistent_store.get_credential(current_key)?; if total_credentials > 1 { - *stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateCredentials(rp_credentials)); + stateful_command_permission + .set_command(now, StatefulCommand::EnumerateCredentials(rp_credentials)); } enumerate_credentials_response(credential, Some(total_credentials as u64)) } @@ -204,12 +192,10 @@ fn process_enumerate_credentials_begin( /// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement. fn process_enumerate_credentials_get_next_credential( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - mut stateful_command_type: &mut Option, - now: ClockValue, + stateful_command_permission: &mut StatefulPermission, ) -> Result { - check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut stateful_command_type + if let StatefulCommand::EnumerateCredentials(rp_credentials) = + stateful_command_permission.get_command()? { let current_key = rp_credentials .pop() @@ -251,8 +237,7 @@ fn process_update_user_information( /// Processes the CredentialManagement command and all its subcommands. pub fn process_credential_management( persistent_store: &mut PersistentStore, - stateful_command_permission: &mut TimedPermission, - mut stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, pin_protocol_v1: &mut PinProtocolV1, cred_management_params: AuthenticatorCredentialManagementParameters, now: ClockValue, @@ -264,17 +249,17 @@ pub fn process_credential_management( pin_auth, } = cred_management_params; - match (sub_command, &mut stateful_command_type) { + match (sub_command, stateful_command_permission.get_command()) { ( CredentialManagementSubCommand::EnumerateRpsGetNextRp, - Some(StatefulCommand::EnumerateRps(_)), - ) => (), - ( + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, - Some(StatefulCommand::EnumerateCredentials(_)), - ) => (), + Ok(StatefulCommand::EnumerateCredentials(_)), + ) => stateful_command_permission.check_command_permission(now)?, (_, _) => { - *stateful_command_type = None; + stateful_command_permission.clear(); } } @@ -313,22 +298,15 @@ pub fn process_credential_management( CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( persistent_store, stateful_command_permission, - stateful_command_type, now, )?), - CredentialManagementSubCommand::EnumerateRpsGetNextRp => { - Some(process_enumerate_rps_get_next_rp( - persistent_store, - stateful_command_permission, - stateful_command_type, - now, - )?) - } + CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some( + process_enumerate_rps_get_next_rp(persistent_store, stateful_command_permission)?, + ), CredentialManagementSubCommand::EnumerateCredentialsBegin => { Some(process_enumerate_credentials_begin( persistent_store, stateful_command_permission, - stateful_command_type, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, now, )?) @@ -337,8 +315,6 @@ pub fn process_credential_management( Some(process_enumerate_credentials_get_next_credential( persistent_store, stateful_command_permission, - stateful_command_type, - now, )?) } CredentialManagementSubCommand::DeleteCredential => { @@ -412,7 +388,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -441,7 +416,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -496,7 +470,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -521,7 +494,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -547,7 +519,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -600,7 +571,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -631,7 +601,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -690,7 +659,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -714,7 +682,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -739,7 +706,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -793,7 +759,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -812,7 +777,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -872,7 +836,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -922,7 +885,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -950,7 +912,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 49d14f4..46281fa 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -101,8 +101,9 @@ const ED_FLAG: u8 = 0x80; pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); +// TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); -pub const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); +const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -169,15 +170,50 @@ pub enum StatefulCommand { EnumerateCredentials(Vec), } -pub fn check_command_permission( - stateful_command_permission: &mut TimedPermission, - now: ClockValue, -) -> Result<(), Ctap2StatusCode> { - *stateful_command_permission = stateful_command_permission.check_expiration(now); - if stateful_command_permission.is_granted(now) { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) +pub struct StatefulPermission { + permission: TimedPermission, + command_type: Option, +} + +impl StatefulPermission { + // Resets are only possible in the first 10 seconds after booting. + // Therefore, initialization includes allowing Reset. + pub fn new_reset(now: ClockValue) -> StatefulPermission { + StatefulPermission { + permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), + command_type: Some(StatefulCommand::Reset), + } + } + + pub fn clear(&mut self) { + self.permission = TimedPermission::waiting(); + self.command_type = None; + } + + pub fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { + if self.permission.is_granted(now) { + Ok(()) + } else { + self.clear(); + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + pub fn get_command(&mut self) -> Result<&mut StatefulCommand, Ctap2StatusCode> { + self.command_type + .as_mut() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + + pub fn set_command(&mut self, now: ClockValue, new_command_type: StatefulCommand) { + 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); + } + } } } @@ -194,8 +230,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, // The state initializes to Reset and its timeout, and never goes back to Reset. - stateful_command_permission: TimedPermission, - stateful_command_type: Option, + stateful_command_permission: StatefulPermission, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -220,13 +255,15 @@ where U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), - stateful_command_permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), - stateful_command_type: Some(StatefulCommand::Reset), + stateful_command_permission: StatefulPermission::new_reset(now), } } pub fn update_command_permission(&mut self, now: ClockValue) { - self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + // Ignore the result, just update. + let _ = self + .stateful_command_permission + .check_command_permission(now); } pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { @@ -345,27 +382,23 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ); } - match (&command, &self.stateful_command_type) { - ( - Command::AuthenticatorGetNextAssertion, - Some(StatefulCommand::GetAssertion(_)), - ) => (), - ( + 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(_), - Some(StatefulCommand::EnumerateRps(_)), - ) => (), - ( + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( Command::AuthenticatorCredentialManagement(_), - Some(StatefulCommand::EnumerateCredentials(_)), + Ok(StatefulCommand::EnumerateCredentials(_)), ) => (), - (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), - // GetInfo does not reset stateful commands. - (Command::AuthenticatorGetInfo, _) => (), - // AuthenticatorSelection does not reset stateful commands. - (Command::AuthenticatorSelection, _) => (), - (_, _) => { - self.stateful_command_type = None; - } + (_, _) => self.stateful_command_permission.clear(), } let response = match command { Command::AuthenticatorMakeCredential(params) => { @@ -382,7 +415,6 @@ where process_credential_management( &mut self.persistent_store, &mut self.stateful_command_permission, - &mut self.stateful_command_type, &mut self.pin_protocol_v1, params, now, @@ -855,12 +887,12 @@ where None } else { let number_of_credentials = Some(next_credential_keys.len() + 1); - self.stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { + let assertion_state = StatefulCommand::GetAssertion(AssertionState { assertion_input: assertion_input.clone(), next_credential_keys, - })); + }); + self.stateful_command_permission + .set_command(now, assertion_state); number_of_credentials }; self.assertion_response(credential, assertion_input, number_of_credentials) @@ -870,20 +902,20 @@ where &mut self, now: ClockValue, ) -> Result { - check_command_permission(&mut self.stateful_command_permission, now)?; - let (assertion_input, credential) = - if let Some(StatefulCommand::GetAssertion(assertion_state)) = - &mut self.stateful_command_type - { - let credential_key = assertion_state - .next_credential_keys - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self.persistent_store.get_credential(credential_key)?; - (assertion_state.assertion_input.clone(), credential) - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); - }; + self.stateful_command_permission + .check_command_permission(now)?; + let (assertion_input, credential) = if let StatefulCommand::GetAssertion(assertion_state) = + self.stateful_command_permission.get_command()? + { + let credential_key = assertion_state + .next_credential_keys + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + let credential = self.persistent_store.get_credential(credential_key)?; + (assertion_state.assertion_input.clone(), credential) + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + }; self.assertion_response(credential, assertion_input, None) } @@ -949,11 +981,10 @@ where cid: ChannelID, now: ClockValue, ) -> Result { - // Resets are only possible in the first 10 seconds after booting. - // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? - check_command_permission(&mut self.stateful_command_permission, now)?; - match &self.stateful_command_type { - Some(StatefulCommand::Reset) => (), + self.stateful_command_permission + .check_command_permission(now)?; + match self.stateful_command_permission.get_command()? { + StatefulCommand::Reset => (), _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), } (self.check_user_presence)(cid)?; From 9296f51e19daa839fe7bf3167ea0b2b6b7aafcd1 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 12:08:07 +0100 Subject: [PATCH 6/7] stricter API for StatefulCommandPermission --- src/ctap/credential_management.rs | 34 ++++--------- src/ctap/mod.rs | 85 ++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 2dbe961..25fe3b9 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -140,18 +140,14 @@ fn process_enumerate_rps_get_next_rp( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, ) -> Result { - if let StatefulCommand::EnumerateRps(rp_id_index) = stateful_command_permission.get_command()? { - let rp_set = get_stored_rp_ids(persistent_store)?; - // A BTreeSet is already sorted. - let rp_id = rp_set - .into_iter() - .nth(*rp_id_index) - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - *rp_id_index += 1; - enumerate_rps_response(Some(rp_id), None) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + let rp_id_index = stateful_command_permission.next_enumerate_rp()?; + let rp_set = get_stored_rp_ids(persistent_store)?; + // A BTreeSet is already sorted. + let rp_id = rp_set + .into_iter() + .nth(rp_id_index) + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + enumerate_rps_response(Some(rp_id), None) } /// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. @@ -194,17 +190,9 @@ fn process_enumerate_credentials_get_next_credential( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, ) -> Result { - if let StatefulCommand::EnumerateCredentials(rp_credentials) = - stateful_command_permission.get_command()? - { - let current_key = rp_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = persistent_store.get_credential(current_key)?; - enumerate_credentials_response(credential, None) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + let credential_key = stateful_command_permission.next_enumerate_credential()?; + let credential = persistent_store.get_credential(credential_key)?; + enumerate_credentials_response(credential, None) } /// Processes the subcommand deleteCredential for CredentialManagement. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 46281fa..96d7b25 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -149,20 +149,23 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { } } +/// Holds data necessary to sign an assertion for a credential. #[derive(Clone)] -struct AssertionInput { +pub struct AssertionInput { client_data_hash: Vec, auth_data: Vec, hmac_secret_input: Option, has_uv: bool, } +/// Contains the state we need to store for GetNextAssertion. pub struct AssertionState { assertion_input: AssertionInput, // Sorted by ascending order of creation, so the last element is the most recent one. next_credential_keys: Vec, } +/// Stores which command currently holds state for subsequent calls. pub enum StatefulCommand { Reset, GetAssertion(AssertionState), @@ -170,14 +173,25 @@ pub enum StatefulCommand { EnumerateCredentials(Vec), } +/// Stores the current CTAP command state and when it times out. +/// +/// Some commands are executed in a series of calls to the authenticator. +/// Interleaving calls to other commands interrupt the current command and +/// remove all state and permissions. Power cycling allows the Reset command, +/// and to prevent misuse or accidents, we disallow Reset after receiving +/// different commands. Therefore, Reset behaves just like all other stateful +/// commands and is included here. Please not that the allowed time for Reset +/// differs from all other stateful commands. pub struct StatefulPermission { permission: TimedPermission, command_type: Option, } impl StatefulPermission { - // Resets are only possible in the first 10 seconds after booting. - // Therefore, initialization includes allowing Reset. + /// 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. pub fn new_reset(now: ClockValue) -> StatefulPermission { StatefulPermission { permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), @@ -185,11 +199,13 @@ impl StatefulPermission { } } + /// Clears all permissions and state. pub fn clear(&mut self) { self.permission = TimedPermission::waiting(); self.command_type = None; } + /// Checks the permission timeout. pub fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { if self.permission.is_granted(now) { Ok(()) @@ -199,12 +215,14 @@ impl StatefulPermission { } } - pub fn get_command(&mut self) -> Result<&mut StatefulCommand, Ctap2StatusCode> { + /// Gets a reference to the current command state, if any exists. + pub fn get_command(&self) -> Result<&StatefulCommand, Ctap2StatusCode> { self.command_type - .as_mut() + .as_ref() .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) } + /// Sets a new command state, and starts a new clock for timeouts. pub fn set_command(&mut self, now: ClockValue, new_command_type: StatefulCommand) { match &new_command_type { // Reset is only allowed after a power cycle. @@ -215,6 +233,47 @@ impl StatefulPermission { } } } + + /// Returns the state for the next assertion and advances it. + /// + /// The state includes all information from GetAssertion and the storage key + /// to the next credential that needs to be processed. + pub fn next_assertion_credential( + &mut self, + ) -> Result<(AssertionInput, usize), Ctap2StatusCode> { + if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.command_type { + let credential_key = assertion_state + .next_credential_keys + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + Ok((assertion_state.assertion_input.clone(), credential_key)) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Returns the index to the next RP ID for enumeration and advances it. + pub fn next_enumerate_rp(&mut self) -> Result { + if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = &mut self.command_type { + let current_index = *rp_id_index; + *rp_id_index += 1; + Ok(current_index) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Returns the next storage credential key for enumeration and advances it. + pub fn next_enumerate_credential(&mut self) -> Result { + if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut self.command_type + { + rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } } // This struct currently holds all state, not only the persistent memory. The persistent members are @@ -904,18 +963,10 @@ where ) -> Result { self.stateful_command_permission .check_command_permission(now)?; - let (assertion_input, credential) = if let StatefulCommand::GetAssertion(assertion_state) = - self.stateful_command_permission.get_command()? - { - let credential_key = assertion_state - .next_credential_keys - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self.persistent_store.get_credential(credential_key)?; - (assertion_state.assertion_input.clone(), credential) - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); - }; + let (assertion_input, credential_key) = self + .stateful_command_permission + .next_assertion_credential()?; + let credential = self.persistent_store.get_credential(credential_key)?; self.assertion_response(credential, assertion_input, None) } From 6bf4a7edec3370f1bb3b452888f04f5f8c6caec1 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 13:22:24 +0100 Subject: [PATCH 7/7] fix typo --- src/ctap/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 96d7b25..8ac324c 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -180,7 +180,7 @@ pub enum StatefulCommand { /// remove all state and permissions. Power cycling allows the Reset command, /// and to prevent misuse or accidents, we disallow Reset after receiving /// different commands. Therefore, Reset behaves just like all other stateful -/// commands and is included here. Please not that the allowed time for Reset +/// commands and is included here. Please note that the allowed time for Reset /// differs from all other stateful commands. pub struct StatefulPermission { permission: TimedPermission,