From 3ae59ce1ecfdfaa7a2e84f86d0c8050d2b1e08e5 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 17 Sep 2020 09:23:17 +0200 Subject: [PATCH 1/7] GetNextAssertion command minimal implementation This still lacks order of credentials and timeouts. --- src/ctap/command.rs | 2 +- src/ctap/data_formats.rs | 6 +- src/ctap/mod.rs | 168 +++++++++++++++++++++++++-------------- 3 files changed, 112 insertions(+), 64 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 5c58234..1b4b96a 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -57,8 +57,8 @@ impl Command { const AUTHENTICATOR_GET_INFO: u8 = 0x04; const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06; const AUTHENTICATOR_RESET: u8 = 0x07; - // TODO(kaczmarczyck) use or remove those constants const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; + // TODO(kaczmarczyck) use or remove those constants const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; const AUTHENTICATOR_SELECTION: u8 = 0xB0; diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 45ebf9f..35d5bcf 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -308,7 +308,8 @@ impl TryFrom for GetAssertionExtensions { } } -#[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 GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, @@ -603,7 +604,8 @@ impl PublicKeyCredentialSource { // TODO(kaczmarczyck) we could decide to split this data type up // It depends on the algorithm though, I think. // So before creating a mess, this is my workaround. -#[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 CoseKey(pub BTreeMap); // This is the algorithm specifier that is supposed to be used in a COSE key diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 13fc951..f037655 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -33,9 +33,9 @@ use self::command::{ #[cfg(feature = "with_ctap2_1")] use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, SignatureAlgorithm, + CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, + PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; #[cfg(feature = "with_ctap2_1")] @@ -133,6 +133,14 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { } } +#[derive(Clone)] +struct AssertionInput { + client_data_hash: Vec, + auth_data: Vec, + uv: bool, + hmac_secret_input: Option, +} + // This struct currently holds all state, not only the persistent memory. The persistent members are // in the persistent store field. pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>> @@ -147,6 +155,8 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( accepts_reset: bool, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, + next_credentials: Vec, + next_assertion_input: Option, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -173,6 +183,8 @@ where U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), + next_credentials: vec![], + next_assertion_input: None, } } @@ -314,13 +326,13 @@ where Command::AuthenticatorGetAssertion(params) => { self.process_get_assertion(params, cid) } + Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(), Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid), #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), - // TODO(kaczmarczyck) implement GetNextAssertion and FIDO 2.1 commands - _ => self.process_unknown_command(), + // TODO(kaczmarczyck) implement FIDO 2.1 commands }; #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Sending response: {:#?}", response).unwrap(); @@ -557,6 +569,63 @@ where )) } + fn assertion_response( + &self, + credential: &PublicKeyCredentialSource, + assertion_input: AssertionInput, + ) -> Result { + let AssertionInput { + client_data_hash, + mut auth_data, + uv, + hmac_secret_input, + } = assertion_input; + + // Process extensions. + if let Some(hmac_secret_input) = hmac_secret_input { + let encrypted_output = self + .pin_protocol_v1 + .process_hmac_secret(hmac_secret_input, &credential.cred_random)?; + let extensions_output = cbor_map! { + "hmac-secret" => encrypted_output, + }; + if !cbor::write(extensions_output, &mut auth_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + } + } + + let mut signature_data = auth_data.clone(); + signature_data.extend(client_data_hash); + let signature = credential + .private_key + .sign_rfc6979::(&signature_data); + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential.credential_id.clone(), + transports: None, // You can set USB as a hint here. + }; + let user = if uv && !credential.user_handle.is_empty() { + Some(PublicKeyCredentialUserEntity { + user_id: credential.user_handle.clone(), + user_name: None, + user_display_name: credential.other_ui.clone(), + user_icon: None, + }) + } else { + None + }; + Ok(ResponseData::AuthenticatorGetAssertion( + AuthenticatorGetAssertionResponse { + credential: Some(cred_desc), + auth_data, + signature: signature.to_asn1_der(), + user, + number_of_credentials: None, + }, + )) + } + fn process_get_assertion( &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, @@ -643,17 +712,19 @@ where } found_credentials } else { - // TODO(kaczmarczyck) use GetNextAssertion self.persistent_store.filter_credential(&rp_id, !has_uv)? }; - let credential = if let Some(credential) = credentials.first() { - credential - } else { - decrypted_credential - .as_ref() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? - }; + let credential = + if let Some((credential, remaining_credentials)) = credentials.split_first() { + // TODO(kaczmarczyck) correct credential order + self.next_credentials = remaining_credentials.to_vec(); + credential + } else { + decrypted_credential + .as_ref() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? + }; if options.up { (self.check_user_presence)(cid)?; @@ -661,50 +732,30 @@ where self.increment_global_signature_counter()?; - let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; - // Process extensions. - if let Some(hmac_secret_input) = hmac_secret_input { - let encrypted_output = self - .pin_protocol_v1 - .process_hmac_secret(hmac_secret_input, &credential.cred_random)?; - let extensions_output = cbor_map! { - "hmac-secret" => encrypted_output, - }; - if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); - } + let assertion_input = AssertionInput { + client_data_hash, + auth_data: self.generate_auth_data(&rp_id_hash, flags)?, + uv: flags & UV_FLAG != 0, + hmac_secret_input, + }; + if !self.next_credentials.is_empty() { + self.next_assertion_input = Some(assertion_input.clone()); } + self.assertion_response(credential, assertion_input) + } - let mut signature_data = auth_data.clone(); - signature_data.extend(client_data_hash); - let signature = credential - .private_key - .sign_rfc6979::(&signature_data); - - let cred_desc = PublicKeyCredentialDescriptor { - key_type: PublicKeyCredentialType::PublicKey, - key_id: credential.credential_id.clone(), - transports: None, // You can set USB as a hint here. - }; - let user = if (flags & UV_FLAG != 0) && !credential.user_handle.is_empty() { - Some(PublicKeyCredentialUserEntity { - user_id: credential.user_handle.clone(), - user_name: None, - user_display_name: credential.other_ui.clone(), - user_icon: None, - }) + fn process_get_next_assertion(&mut self) -> Result { + // TODO(kaczmarczyck) introduce timer + let assertion_input = self + .next_assertion_input + .clone() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if let Some(credential) = self.next_credentials.pop() { + self.assertion_response(&credential, assertion_input) } else { - None - }; - Ok(ResponseData::AuthenticatorGetAssertion( - AuthenticatorGetAssertionResponse { - credential: Some(cred_desc), - auth_data, - signature: signature.to_asn1_der(), - user, - number_of_credentials: None, - }, - )) + self.next_assertion_input = None; + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } } fn process_get_info(&self) -> Result { @@ -786,10 +837,6 @@ where Ok(ResponseData::AuthenticatorSelection) } - fn process_unknown_command(&self) -> Result { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) - } - pub fn generate_auth_data( &self, rp_id_hash: &[u8], @@ -813,9 +860,8 @@ where #[cfg(test)] mod test { use super::data_formats::{ - CoseKey, GetAssertionExtensions, GetAssertionHmacSecretInput, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; use crypto::rng256::ThreadRng256; From af4eef808519b10055a92cce3aa8a97960df3d2a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 17 Nov 2020 16:57:03 +0100 Subject: [PATCH 2/7] adds credential ordering --- src/ctap/data_formats.rs | 7 +++++++ src/ctap/mod.rs | 11 +++++++++-- src/ctap/storage.rs | 25 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 35d5bcf..fa747bc 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -500,6 +500,7 @@ pub struct PublicKeyCredentialSource { pub other_ui: Option, pub cred_random: Option>, pub cred_protect_policy: Option, + pub creation_order: u64, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -512,6 +513,7 @@ enum PublicKeyCredentialSourceField { OtherUi = 4, CredRandom = 5, CredProtectPolicy = 6, + CreationOrder = 7, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: none. @@ -535,6 +537,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::OtherUi => credential.other_ui, PublicKeyCredentialSourceField::CredRandom => credential.cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, + PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, } } } @@ -552,6 +555,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::OtherUi => other_ui, PublicKeyCredentialSourceField::CredRandom => cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, + PublicKeyCredentialSourceField::CreationOrder => creation_order, } = extract_map(cbor_value)?; } @@ -569,6 +573,7 @@ impl TryFrom for PublicKeyCredentialSource { let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; + let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -588,6 +593,7 @@ impl TryFrom for PublicKeyCredentialSource { other_ui, cred_random, cred_protect_policy, + creation_order, }) } } @@ -1357,6 +1363,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert_eq!( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index f037655..331ccc1 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -155,6 +155,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( accepts_reset: bool, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, + // Sorted by ascending order of creation, so the last element is the most recent one. next_credentials: Vec, next_assertion_input: Option, } @@ -302,6 +303,7 @@ where other_ui: None, cred_random, cred_protect_policy: None, + creation_order: 0, })) } @@ -424,7 +426,6 @@ where } else { None }; - // TODO(kaczmarczyck) unsolicited output for default credProtect level let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); let rp_id = rp.rp_id; @@ -501,6 +502,7 @@ where .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, + creation_order: self.persistent_store.new_creation_order()?, }; self.persistent_store.store_credential(credential_source)?; random_id @@ -717,8 +719,9 @@ where let credential = if let Some((credential, remaining_credentials)) = credentials.split_first() { - // TODO(kaczmarczyck) correct credential order self.next_credentials = remaining_credentials.to_vec(); + self.next_credentials + .sort_unstable_by_key(|c| c.creation_order); credential } else { decrypted_credential @@ -1091,6 +1094,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1457,6 +1461,7 @@ mod test { cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1507,6 +1512,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1550,6 +1556,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 127dc23..6e4506a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -337,6 +337,26 @@ impl PersistentStore { .count()) } + pub fn new_creation_order(&self) -> Result { + Ok(self + .store + .find_all(&Key::Credential { + rp_id: None, + credential_id: None, + user_handle: None, + }) + .filter_map(|(_, entry)| { + debug_assert_eq!(entry.tag, TAG_CREDENTIAL); + let credential = deserialize_credential(entry.data); + debug_assert!(credential.is_some()); + credential + }) + .map(|c| c.creation_order) + .max() + .unwrap_or(0) + + 1) + } + pub fn global_signature_counter(&self) -> Result { Ok(self .store @@ -690,6 +710,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, } } @@ -853,6 +874,7 @@ mod test { cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), + creation_order: 0, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -894,6 +916,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -913,6 +936,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + creation_order: 0, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1090,6 +1114,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From 5ff381678251473c0417b77f4b9a657d95d870a4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 23 Nov 2020 17:33:20 +0100 Subject: [PATCH 3/7] sets the correct user and number of credentials --- src/ctap/mod.rs | 209 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 162 insertions(+), 47 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 331ccc1..1f91f4f 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -137,7 +137,6 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { struct AssertionInput { client_data_hash: Vec, auth_data: Vec, - uv: bool, hmac_secret_input: Option, } @@ -571,15 +570,17 @@ where )) } + // Processes the input of a get_assertion operation for a given credential + // and returns the correct Get(Next)Assertion response. fn assertion_response( &self, credential: &PublicKeyCredentialSource, assertion_input: AssertionInput, + number_of_credentials: Option, ) -> Result { let AssertionInput { client_data_hash, mut auth_data, - uv, hmac_secret_input, } = assertion_input; @@ -607,7 +608,7 @@ where key_id: credential.credential_id.clone(), transports: None, // You can set USB as a hint here. }; - let user = if uv && !credential.user_handle.is_empty() { + let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { user_id: credential.user_handle.clone(), user_name: None, @@ -623,11 +624,37 @@ where auth_data, signature: signature.to_asn1_der(), user, - number_of_credentials: None, + number_of_credentials: number_of_credentials.map(|n| n as u64), }, )) } + // Returns the first applicable credential from the allow list. + fn get_any_credential_from_allow_list( + &mut self, + allow_list: Vec, + rp_id: &str, + rp_id_hash: &[u8], + has_uv: bool, + ) -> Result, Ctap2StatusCode> { + for allowed_credential in allow_list { + let credential = self.persistent_store.find_credential( + rp_id, + &allowed_credential.key_id, + !has_uv, + )?; + if credential.is_some() { + return Ok(credential); + } + let credential = + self.decrypt_credential_source(allowed_credential.key_id, &rp_id_hash)?; + if credential.is_some() { + return Ok(credential); + } + } + Ok(None) + } + fn process_get_assertion( &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, @@ -690,44 +717,29 @@ where } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut decrypted_credential = None; - let credentials = if let Some(allow_list) = allow_list { - let mut found_credentials = vec![]; - for allowed_credential in allow_list { - match self.persistent_store.find_credential( - &rp_id, - &allowed_credential.key_id, - !has_uv, - )? { - Some(credential) => { - found_credentials.push(credential); - } - None => { - if decrypted_credential.is_none() { - decrypted_credential = self.decrypt_credential_source( - allowed_credential.key_id, - &rp_id_hash, - )?; - } - } - } + let mut credentials = if let Some(allow_list) = allow_list { + if let Some(credential) = + self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)? + { + vec![credential] + } else { + vec![] } - found_credentials } else { self.persistent_store.filter_credential(&rp_id, !has_uv)? }; + // Remove user identifiable information without uv. + if !has_uv { + for credential in &mut credentials { + credential.other_ui = None; + } + } + credentials.sort_unstable_by_key(|c| c.creation_order); - let credential = - if let Some((credential, remaining_credentials)) = credentials.split_first() { - self.next_credentials = remaining_credentials.to_vec(); - self.next_credentials - .sort_unstable_by_key(|c| c.creation_order); - credential - } else { - decrypted_credential - .as_ref() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? - }; + let (credential, remaining_credentials) = credentials + .split_last() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + self.next_credentials = remaining_credentials.to_vec(); if options.up { (self.check_user_presence)(cid)?; @@ -738,13 +750,16 @@ where let assertion_input = AssertionInput { client_data_hash, auth_data: self.generate_auth_data(&rp_id_hash, flags)?, - uv: flags & UV_FLAG != 0, hmac_secret_input, }; - if !self.next_credentials.is_empty() { + let number_of_credentials = if self.next_credentials.is_empty() { + self.next_assertion_input = None; + None + } else { self.next_assertion_input = Some(assertion_input.clone()); - } - self.assertion_response(credential, assertion_input) + Some(self.next_credentials.len() + 1) + }; + self.assertion_response(credential, assertion_input, number_of_credentials) } fn process_get_next_assertion(&mut self) -> Result { @@ -753,12 +768,14 @@ where .next_assertion_input .clone() .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - if let Some(credential) = self.next_credentials.pop() { - self.assertion_response(&credential, assertion_input) - } else { + let credential = self + .next_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if self.next_credentials.is_empty() { self.next_assertion_input = None; - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + }; + self.assertion_response(&credential, assertion_input, None) } fn process_get_info(&self) -> Result { @@ -1318,7 +1335,13 @@ mod test { 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, ]; assert_eq!(auth_data, expected_auth_data); - assert!(user.is_none()); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0xFA, 0xB1, 0xA2], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); assert!(number_of_credentials.is_none()); } _ => panic!("Invalid response type"), @@ -1539,6 +1562,98 @@ mod test { ); } + #[test] + fn test_process_get_next_assertion() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = + ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + + match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0x02], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert_eq!(number_of_credentials, Some(2)); + } + _ => panic!("Invalid response type"), + } + + let get_assertion_response = ctap_state.process_get_next_assertion(); + match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0x01], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert!(number_of_credentials.is_none()); + } + _ => panic!("Invalid response type"), + } + + let get_assertion_response = ctap_state.process_get_next_assertion(); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + #[test] fn test_process_reset() { let mut rng = ThreadRng256 {}; From ffe19e152b4bc334a188f7e35fcaf1bf51853ca2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 24 Nov 2020 17:17:18 +0100 Subject: [PATCH 4/7] moves UP check in GetAssertion before NO_CREDENTIALS --- src/ctap/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 1f91f4f..98058ce 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -736,15 +736,17 @@ where } credentials.sort_unstable_by_key(|c| c.creation_order); + // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. + // For CTAP 2.1, it was moved to a later protocol step. + if options.up { + (self.check_user_presence)(cid)?; + } + let (credential, remaining_credentials) = credentials .split_last() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; self.next_credentials = remaining_credentials.to_vec(); - if options.up { - (self.check_user_presence)(cid)?; - } - self.increment_global_signature_counter()?; let assertion_input = AssertionInput { From ed59ebac0dd3dc5f44057e11c3b8a106d20a46e7 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 14:40:02 +0100 Subject: [PATCH 5/7] command timeout for GetNextAssertion --- fuzz/fuzz_helper/src/lib.rs | 4 +- src/ctap/ctap1.rs | 26 +- src/ctap/hid/mod.rs | 11 +- src/ctap/mod.rs | 490 ++++++++++++++++++++++++------------ src/ctap/storage.rs | 15 ++ src/main.rs | 11 +- 6 files changed, 373 insertions(+), 184 deletions(-) diff --git a/fuzz/fuzz_helper/src/lib.rs b/fuzz/fuzz_helper/src/lib.rs index a6dc1a7..1553f65 100644 --- a/fuzz/fuzz_helper/src/lib.rs +++ b/fuzz/fuzz_helper/src/lib.rs @@ -147,7 +147,7 @@ fn process_message( pub fn process_ctap_any_type(data: &[u8]) { // Initialize ctap state and hid and get the allocated cid. let mut rng = ThreadRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = initialize(&mut ctap_state, &mut ctap_hid); // Wrap input as message with the allocated cid. @@ -165,7 +165,7 @@ pub fn process_ctap_specific_type(data: &[u8], input_type: InputType) { } // Initialize ctap state and hid and get the allocated cid. let mut rng = ThreadRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = initialize(&mut ctap_state, &mut ctap_hid); // Wrap input as message with allocated cid and command type. diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index a5a7921..f299926 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -427,7 +427,7 @@ mod test { fn test_process_register() { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let application = [0x0A; 32]; let message = create_register_message(&application); @@ -456,7 +456,7 @@ mod test { fn test_process_register_bad_message() { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let application = [0x0A; 32]; let message = create_register_message(&application); @@ -476,7 +476,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); @@ -490,7 +490,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -508,7 +508,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -527,7 +527,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -553,7 +553,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -573,7 +573,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -593,7 +593,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -613,7 +613,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -640,7 +640,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -672,7 +672,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); @@ -689,7 +689,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index a6a18f7..3874792 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -200,7 +200,8 @@ impl CtapHid { // Each transaction is atomic, so we process the command directly here and // don't handle any other packet in the meantime. // TODO: Send keep-alive packets in the meantime. - let response = ctap_state.process_command(&message.payload, cid); + let response = + ctap_state.process_command(&message.payload, cid, clock_value); if let Some(iterator) = CtapHid::split_message(Message { cid, cmd: CtapHid::COMMAND_CBOR, @@ -520,7 +521,7 @@ mod test { fn test_spurious_continuation_packet() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let mut packet = [0x00; 64]; @@ -541,7 +542,7 @@ mod test { fn test_command_init() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let reply = process_messages( @@ -586,7 +587,7 @@ mod test { fn test_command_init_for_sync() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = cid_from_init(&mut ctap_hid, &mut ctap_state); @@ -646,7 +647,7 @@ mod test { fn test_command_ping() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = cid_from_init(&mut ctap_hid, &mut ctap_state); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 98058ce..4675350 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -47,6 +47,7 @@ use self::response::{ }; use self::status_code::Ctap2StatusCode; use self::storage::PersistentStore; +use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; use alloc::collections::BTreeMap; @@ -65,7 +66,7 @@ use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(feature = "debug_ctap")] use libtock_drivers::console::Console; -use libtock_drivers::timer::{Duration, Timestamp}; +use libtock_drivers::timer::{ClockValue, Duration}; // This flag enables or disables basic attestation for FIDO2. U2F is unaffected by // this setting. The basic attestation uses the signing key from key_material.rs @@ -99,7 +100,8 @@ 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); -const RESET_TIMEOUT_MS: isize = 10000; +const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); +const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -140,6 +142,17 @@ struct AssertionInput { hmac_secret_input: Option, } +struct AssertionState { + assertion_input: AssertionInput, + // Sorted by ascending order of creation, so the last element is the most recent one. + next_credentials: Vec, +} + +enum StatefulCommand { + Reset, + GetAssertion(AssertionState), +} + // This struct currently holds all state, not only the persistent memory. The persistent members are // in the persistent store field. pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>> @@ -150,13 +163,11 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( check_user_presence: CheckUserPresence, persistent_store: PersistentStore, pin_protocol_v1: PinProtocolV1, - // This variable will be irreversibly set to false RESET_TIMEOUT_MS milliseconds after boot. - accepts_reset: bool, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, - // Sorted by ascending order of creation, so the last element is the most recent one. - next_credentials: Vec, - next_assertion_input: Option, + // The state initializes to Reset and its timeout, and never goes back to Reset. + stateful_command_permission: TimedPermission, + stateful_command_type: Option, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -169,6 +180,7 @@ where pub fn new( rng: &'a mut R, check_user_presence: CheckUserPresence, + now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { let persistent_store = PersistentStore::new(rng); let pin_protocol_v1 = PinProtocolV1::new(rng); @@ -177,20 +189,26 @@ where check_user_presence, persistent_store, pin_protocol_v1, - accepts_reset: true, #[cfg(feature = "with_ctap1")] u2f_up_state: U2fUserPresenceState::new( U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), - next_credentials: vec![], - next_assertion_input: None, + stateful_command_permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), + stateful_command_type: Some(StatefulCommand::Reset), } } - pub fn check_disable_reset(&mut self, timestamp: Timestamp) { - if timestamp - Timestamp::::from_ms(0) > Duration::from_ms(RESET_TIMEOUT_MS) { - self.accepts_reset = false; + pub fn update_command_permission(&mut self, now: ClockValue) { + self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + } + + fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { + self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + if self.stateful_command_permission.is_granted(now) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) } } @@ -306,7 +324,12 @@ where })) } - pub fn process_command(&mut self, command_cbor: &[u8], cid: ChannelID) -> Vec { + pub fn process_command( + &mut self, + command_cbor: &[u8], + cid: ChannelID, + now: ClockValue, + ) -> Vec { let cmd = Command::deserialize(command_cbor); #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Received command: {:#?}", cmd).unwrap(); @@ -320,17 +343,32 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ); } + match (&command, &self.stateful_command_type) { + ( + Command::AuthenticatorGetNextAssertion, + Some(StatefulCommand::GetAssertion(_)), + ) => (), + (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), + // GetInfo does not reset stateful commands. + (Command::AuthenticatorGetInfo, _) => (), + // AuthenticatorSelection does not reset stateful commands. + #[cfg(feature = "with_ctap2_1")] + (Command::AuthenticatorSelection, _) => (), + (_, _) => { + self.stateful_command_type = None; + } + } let response = match command { Command::AuthenticatorMakeCredential(params) => { self.process_make_credential(params, cid) } Command::AuthenticatorGetAssertion(params) => { - self.process_get_assertion(params, cid) + self.process_get_assertion(params, cid, now) } - Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(), + Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(now), Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), - Command::AuthenticatorReset => self.process_reset(cid), + Command::AuthenticatorReset => self.process_reset(cid, now), #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands @@ -659,6 +697,7 @@ where &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, cid: ChannelID, + now: ClockValue, ) -> Result { let AuthenticatorGetAssertionParameters { rp_id, @@ -745,7 +784,6 @@ where let (credential, remaining_credentials) = credentials .split_last() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; - self.next_credentials = remaining_credentials.to_vec(); self.increment_global_signature_counter()?; @@ -754,29 +792,37 @@ where auth_data: self.generate_auth_data(&rp_id_hash, flags)?, hmac_secret_input, }; - let number_of_credentials = if self.next_credentials.is_empty() { - self.next_assertion_input = None; + let number_of_credentials = if remaining_credentials.is_empty() { None } else { - self.next_assertion_input = Some(assertion_input.clone()); - Some(self.next_credentials.len() + 1) + self.stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { + assertion_input: assertion_input.clone(), + next_credentials: remaining_credentials.to_vec(), + })); + Some(remaining_credentials.len() + 1) }; self.assertion_response(credential, assertion_input, number_of_credentials) } - fn process_get_next_assertion(&mut self) -> Result { - // TODO(kaczmarczyck) introduce timer - let assertion_input = self - .next_assertion_input - .clone() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self - .next_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - if self.next_credentials.is_empty() { - self.next_assertion_input = None; - }; + fn process_get_next_assertion( + &mut self, + now: ClockValue, + ) -> Result { + self.check_command_permission(now)?; + let (assertion_input, credential) = + if let Some(StatefulCommand::GetAssertion(assertion_state)) = + &mut self.stateful_command_type + { + let credential = assertion_state + .next_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + (assertion_state.assertion_input.clone(), credential) + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + }; self.assertion_response(&credential, assertion_input, None) } @@ -834,10 +880,17 @@ where ) } - fn process_reset(&mut self, cid: ChannelID) -> Result { + fn process_reset( + &mut self, + cid: ChannelID, + now: ClockValue, + ) -> Result { // Resets are only possible in the first 10 seconds after booting. - if !self.accepts_reset { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? + self.check_command_permission(now)?; + match &self.stateful_command_type { + Some(StatefulCommand::Reset) => (), + _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), } (self.check_user_presence)(cid)?; @@ -886,8 +939,11 @@ mod test { MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; + use cbor::cbor_array; use crypto::rng256::ThreadRng256; + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); // The keep-alive logic in the processing of some commands needs a channel ID to send // keep-alive packets to. // In tests where we define a dummy user-presence check that immediately returns, the channel @@ -898,8 +954,8 @@ mod test { fn test_get_info() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); - let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); #[cfg(feature = "with_ctap2_1")] let mut expected_response = vec![0x00, 0xAA, 0x01]; @@ -955,7 +1011,7 @@ mod test { rp_icon: None, }; let user = PublicKeyCredentialUserEntity { - user_id: vec![0xFA, 0xB1, 0xA2], + user_id: vec![0x1D], user_name: None, user_display_name: None, user_icon: None, @@ -1008,7 +1064,7 @@ mod test { fn test_residential_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = @@ -1044,7 +1100,7 @@ mod test { fn test_non_residential_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1081,7 +1137,7 @@ mod test { fn test_process_make_credential_unsupported_algorithm() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.pub_key_cred_params = vec![]; @@ -1099,7 +1155,7 @@ mod test { let mut rng = ThreadRng256 {}; let excluded_private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; let make_credential_params = @@ -1132,7 +1188,7 @@ mod test { fn test_process_make_credential_credential_with_cred_protect() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let test_policy = CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; let make_credential_params = @@ -1186,7 +1242,7 @@ mod test { fn test_process_make_credential_hmac_secret() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1236,7 +1292,7 @@ mod test { fn test_process_make_credential_hmac_secret_resident_key() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1285,7 +1341,8 @@ mod test { fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; let user_presence_always_cancel = |_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL); - let mut ctap_state = CtapState::new(&mut rng, user_presence_always_cancel); + let mut ctap_state = + CtapState::new(&mut rng, user_presence_always_cancel, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = @@ -1297,11 +1354,43 @@ mod test { ); } + fn check_assertion_response( + response: Result, + expected_user_id: Vec, + expected_number_of_credentials: Option, + ) { + match response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert_eq!(number_of_credentials, expected_number_of_credentials); + } + _ => panic!("Invalid response type"), + } + } + #[test] fn test_residential_process_get_assertion() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); assert!(ctap_state @@ -1320,34 +1409,12 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); - - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0xFA, 0xB1, 0xA2], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert!(number_of_credentials.is_none()); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x1D], None); } #[test] @@ -1355,7 +1422,7 @@ mod test { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1405,8 +1472,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, @@ -1419,7 +1489,7 @@ mod test { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1453,8 +1523,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, @@ -1468,7 +1541,7 @@ mod test { let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let credential_id = rng.gen_uniform_u8x32().to_vec(); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, @@ -1480,7 +1553,7 @@ mod test { credential_id: credential_id.clone(), private_key: private_key.clone(), rp_id: String::from("example.com"), - user_handle: vec![0x00], + user_handle: vec![0x1D], other_ui: None, cred_random: None, cred_protect_policy: Some( @@ -1505,8 +1578,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), @@ -1524,16 +1600,19 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); - assert!(get_assertion_response.is_ok()); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x1D], None); let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id, private_key, rp_id: String::from("example.com"), - user_handle: vec![0x00], + user_handle: vec![0x1D], other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), @@ -1556,8 +1635,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), @@ -1565,10 +1647,10 @@ mod test { } #[test] - fn test_process_get_next_assertion() { + fn test_process_get_next_assertion_two_credentials() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x01]; @@ -1593,63 +1675,135 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x02], Some(2)); - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0x02], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert_eq!(number_of_credentials, Some(2)); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x01], None); - let get_assertion_response = ctap_state.process_get_next_assertion(); - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0x01], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert!(number_of_credentials.is_none()); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } - let get_assertion_response = ctap_state.process_get_next_assertion(); + #[test] + fn test_process_get_next_assertion_three_credentials() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x03]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x03], Some(3)); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x02], None); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x01], None); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_get_next_assertion_not_allowed() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + assert!(get_assertion_response.is_ok()); + + // This is a MakeCredential command. + let mut command_cbor = vec![0x01]; + let cbor_value = cbor_map! { + 1 => vec![0xCD; 16], + 2 => cbor_map! { + "id" => "example.com", + }, + 3 => cbor_map! { + "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + }, + 4 => cbor_array![ES256_CRED_PARAM], + }; + assert!(cbor::write(cbor_value, &mut command_cbor)); + ctap_state.process_command(&command_cbor, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -1661,7 +1815,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let credential_id = vec![0x01, 0x23, 0x45, 0x67]; let credential_source = PublicKeyCredentialSource { @@ -1681,7 +1835,8 @@ mod test { .is_ok()); assert!(ctap_state.persistent_store.count_credentials().unwrap() > 0); - let reset_reponse = ctap_state.process_command(&[0x07], DUMMY_CHANNEL_ID); + let reset_reponse = + ctap_state.process_command(&[0x07], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![0x00]; assert_eq!(reset_reponse, expected_response); assert!(ctap_state.persistent_store.count_credentials().unwrap() == 0); @@ -1691,9 +1846,10 @@ mod test { fn test_process_reset_cancelled() { let mut rng = ThreadRng256 {}; let user_presence_always_cancel = |_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL); - let mut ctap_state = CtapState::new(&mut rng, user_presence_always_cancel); + let mut ctap_state = + CtapState::new(&mut rng, user_presence_always_cancel, DUMMY_CLOCK_VALUE); - let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID); + let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); assert_eq!( reset_reponse, @@ -1701,14 +1857,28 @@ mod test { ); } + #[test] + fn test_process_reset_not_first() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // This is a GetNextAssertion command. + ctap_state.process_command(&[0x08], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + + let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); + } + #[test] fn test_process_unknown_command() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + 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); + let reset_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); } @@ -1718,7 +1888,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Usually, the relying party ID or its hash is provided by the client. // We are not testing the correctness of our SHA256 here, only if it is checked. @@ -1739,7 +1909,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Usually, the relying party ID or its hash is provided by the client. // We are not testing the correctness of our SHA256 here, only if it is checked. @@ -1762,7 +1932,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Same as above. let rp_id_hash = [0x55; 32]; @@ -1784,7 +1954,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Same as above. let rp_id_hash = [0x55; 32]; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 6e4506a..aae6add 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -746,6 +746,21 @@ mod test { assert!(persistent_store.count_credentials().unwrap() > 0); } + #[test] + fn test_credential_order() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let credential_source = create_credential_source(&mut rng, "example.com", vec![]); + let current_latest_creation = credential_source.creation_order; + assert!(persistent_store.store_credential(credential_source).is_ok()); + let mut credential_source = create_credential_source(&mut rng, "example.com", vec![]); + credential_source.creation_order = persistent_store.new_creation_order().unwrap(); + assert!(credential_source.creation_order > current_latest_creation); + let current_latest_creation = credential_source.creation_order; + assert!(persistent_store.store_credential(credential_source).is_ok()); + assert!(persistent_store.new_creation_order().unwrap() > current_latest_creation); + } + #[test] #[allow(clippy::assertions_on_constants)] fn test_fill_store() { diff --git a/src/main.rs b/src/main.rs index 855325e..51c8305 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,9 +37,11 @@ use libtock_drivers::console::Console; use libtock_drivers::led; use libtock_drivers::result::{FlexUnwrap, TockError}; use libtock_drivers::timer; +use libtock_drivers::timer::Duration; #[cfg(feature = "debug_ctap")] use libtock_drivers::timer::Timer; -use libtock_drivers::timer::{Duration, Timestamp}; +#[cfg(feature = "debug_ctap")] +use libtock_drivers::timer::Timestamp; use libtock_drivers::usb_ctap_hid; const KEEPALIVE_DELAY_MS: isize = 100; @@ -57,12 +59,13 @@ fn main() { panic!("Cannot setup USB driver"); } + let boot_time = timer.get_current_clock().flex_unwrap(); let mut rng = TockRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, check_user_presence); + let mut ctap_state = CtapState::new(&mut rng, check_user_presence, boot_time); let mut ctap_hid = CtapHid::new(); let mut led_counter = 0; - let mut last_led_increment = timer.get_current_clock().flex_unwrap(); + let mut last_led_increment = boot_time; // Main loop. If CTAP1 is used, we register button presses for U2F while receiving and waiting. // The way TockOS and apps currently interact, callbacks need a yield syscall to execute, @@ -115,7 +118,7 @@ fn main() { // These calls are making sure that even for long inactivity, wrapping clock values // never randomly wink or grant user presence for U2F. - ctap_state.check_disable_reset(Timestamp::::from_clock_value(now)); + ctap_state.update_command_permission(now); ctap_hid.wink_permission = ctap_hid.wink_permission.check_expiration(now); if has_packet { From 3aef7e8b19d6a522af175f73de1b239bae3bd682 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 15:56:59 +0100 Subject: [PATCH 6/7] reuse update_command_permission --- 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 4675350..f2043d0 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -204,7 +204,7 @@ where } fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { - self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + self.update_command_permission(now); if self.stateful_command_permission.is_granted(now) { Ok(()) } else { From 1571f58cd3e51849626cb3681bb15ccef10155b8 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 19:21:41 +0100 Subject: [PATCH 7/7] wrapping_add in storage and more moving --- src/ctap/mod.rs | 27 ++++++++++++++------------- src/ctap/storage.rs | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 9626da4..5fdfa9e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -614,7 +614,7 @@ where // and returns the correct Get(Next)Assertion response. fn assertion_response( &self, - credential: &PublicKeyCredentialSource, + credential: PublicKeyCredentialSource, assertion_input: AssertionInput, number_of_credentials: Option, ) -> Result { @@ -645,14 +645,14 @@ where let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, - key_id: credential.credential_id.clone(), + key_id: credential.credential_id, transports: None, // You can set USB as a hint here. }; let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { - user_id: credential.user_handle.clone(), + user_id: credential.user_handle, user_name: None, - user_display_name: credential.other_ui.clone(), + user_display_name: credential.other_ui, user_icon: None, }) } else { @@ -758,7 +758,7 @@ where } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut credentials = if let Some(allow_list) = allow_list { + let mut applicable_credentials = if let Some(allow_list) = allow_list { if let Some(credential) = self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)? { @@ -771,11 +771,11 @@ where }; // Remove user identifiable information without uv. if !has_uv { - for credential in &mut credentials { + for credential in &mut applicable_credentials { credential.other_ui = None; } } - credentials.sort_unstable_by_key(|c| c.creation_order); + applicable_credentials.sort_unstable_by_key(|c| c.creation_order); // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. // For CTAP 2.1, it was moved to a later protocol step. @@ -783,8 +783,8 @@ where (self.check_user_presence)(cid)?; } - let (credential, remaining_credentials) = credentials - .split_last() + let credential = applicable_credentials + .pop() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; self.increment_global_signature_counter()?; @@ -794,16 +794,17 @@ where auth_data: self.generate_auth_data(&rp_id_hash, flags)?, hmac_secret_input, }; - let number_of_credentials = if remaining_credentials.is_empty() { + let number_of_credentials = if applicable_credentials.is_empty() { None } else { + let number_of_credentials = Some(applicable_credentials.len() + 1); self.stateful_command_permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { assertion_input: assertion_input.clone(), - next_credentials: remaining_credentials.to_vec(), + next_credentials: applicable_credentials, })); - Some(remaining_credentials.len() + 1) + number_of_credentials }; self.assertion_response(credential, assertion_input, number_of_credentials) } @@ -825,7 +826,7 @@ where } else { return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); }; - self.assertion_response(&credential, assertion_input, None) + self.assertion_response(credential, assertion_input, None) } fn process_get_info(&self) -> Result { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index fd91cce..7952d75 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -359,7 +359,7 @@ impl PersistentStore { .map(|c| c.creation_order) .max() .unwrap_or(0) - + 1) + .wrapping_add(1)) } pub fn global_signature_counter(&self) -> Result {