// 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::super::clock::CtapInstant; use super::command::AuthenticatorClientPinParameters; use super::data_formats::{ ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PinUvAuthProtocol, }; use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::token_state::PinUvAuthTokenState; use crate::api::customization::Customization; use crate::ctap::storage; use crate::env::Env; use alloc::boxed::Box; use alloc::str; use alloc::string::String; use alloc::vec::Vec; use crypto::hmac::hmac_256; use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(test)] use enum_iterator::IntoEnumIterator; use rng256::Rng256; use subtle::ConstantTimeEq; /// The prefix length of the PIN hash that is stored and compared. /// /// The code assumes that this value is a multiple of the AES block length, fits /// an u8 and is at most as long as a SHA256. The value is fixed for all PIN /// protocols. pub const PIN_AUTH_LENGTH: usize = 16; /// The length of the pinUvAuthToken used throughout PIN protocols. /// /// The code assumes that this value is a multiple of the AES block length. It /// is fixed since CTAP2.1, and the specification suggests that it coincides /// with the HMAC key length. Therefore a change would require a more general /// HMAC implementation. pub const PIN_TOKEN_LENGTH: usize = 32; /// The length of the encrypted PINs when received by SetPin or ChangePin. /// /// The code assumes that this value is a multiple of the AES block length. It /// is fixed since CTAP2.1. const PIN_PADDED_LENGTH: usize = 64; /// Decrypts the new_pin_enc and outputs the found PIN. fn decrypt_pin( shared_secret: &dyn SharedSecret, new_pin_enc: Vec, ) -> Result, Ctap2StatusCode> { let decrypted_pin = shared_secret.decrypt(&new_pin_enc)?; if decrypted_pin.len() != PIN_PADDED_LENGTH { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } // In CTAP 2.1, the specification changed. The new wording might lead to // different behavior when there are non-zero bytes after zero bytes. // This implementation consistently ignores those degenerate cases. Ok(decrypted_pin.into_iter().take_while(|&c| c != 0).collect()) } /// Stores a hash prefix of the new PIN in the persistent storage, if correct. /// /// The new PIN is passed encrypted, so it is first decrypted and stripped from /// padding. Next, it is checked against the PIN policy. Last, it is hashed and /// truncated for persistent storage. fn check_and_store_new_pin( env: &mut impl Env, shared_secret: &dyn SharedSecret, new_pin_enc: Vec, ) -> Result<(), Ctap2StatusCode> { let pin = decrypt_pin(shared_secret, new_pin_enc)?; let min_pin_length = storage::min_pin_length(env)? as usize; let pin_length = str::from_utf8(&pin).unwrap_or("").chars().count(); if pin_length < min_pin_length || pin.len() == PIN_PADDED_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); } let mut pin_hash = [0u8; PIN_AUTH_LENGTH]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..PIN_AUTH_LENGTH]); // The PIN length is always < PIN_PADDED_LENGTH < 256. storage::set_pin(env, &pin_hash, pin_length as u8)?; Ok(()) } #[cfg_attr(test, derive(IntoEnumIterator))] pub enum PinPermission { // All variants should use integers with a single bit set. MakeCredential = 0x01, GetAssertion = 0x02, CredentialManagement = 0x04, _BioEnrollment = 0x08, LargeBlobWrite = 0x10, AuthenticatorConfiguration = 0x20, } pub struct ClientPin { pin_protocol_v1: PinProtocol, pin_protocol_v2: PinProtocol, consecutive_pin_mismatches: u8, pin_uv_auth_token_state: PinUvAuthTokenState, } impl ClientPin { pub fn new(rng: &mut impl Rng256) -> ClientPin { ClientPin { pin_protocol_v1: PinProtocol::new(rng), pin_protocol_v2: PinProtocol::new(rng), consecutive_pin_mismatches: 0, pin_uv_auth_token_state: PinUvAuthTokenState::new(), } } /// Gets a reference to the PIN protocol of the given version. fn get_pin_protocol(&self, pin_uv_auth_protocol: PinUvAuthProtocol) -> &PinProtocol { match pin_uv_auth_protocol { PinUvAuthProtocol::V1 => &self.pin_protocol_v1, PinUvAuthProtocol::V2 => &self.pin_protocol_v2, } } /// Gets a mutable reference to the PIN protocol of the given version. fn get_mut_pin_protocol( &mut self, pin_uv_auth_protocol: PinUvAuthProtocol, ) -> &mut PinProtocol { match pin_uv_auth_protocol { PinUvAuthProtocol::V1 => &mut self.pin_protocol_v1, PinUvAuthProtocol::V2 => &mut self.pin_protocol_v2, } } /// Computes the shared secret for the given version. fn get_shared_secret( &self, pin_uv_auth_protocol: PinUvAuthProtocol, key_agreement: CoseKey, ) -> Result, Ctap2StatusCode> { self.get_pin_protocol(pin_uv_auth_protocol) .decapsulate(key_agreement, pin_uv_auth_protocol) } /// Checks the given encrypted PIN hash against the stored PIN hash. /// /// Decrypts the encrypted pin_hash and compares it to the stored pin_hash. /// Resets or decreases the PIN retries, depending on success or failure. /// Also, in case of failure, the key agreement key is randomly reset. fn verify_pin_hash_enc( &mut self, env: &mut impl Env, pin_uv_auth_protocol: PinUvAuthProtocol, shared_secret: &dyn SharedSecret, pin_hash_enc: Vec, ) -> Result<(), Ctap2StatusCode> { match storage::pin_hash(env)? { Some(pin_hash) => { if self.consecutive_pin_mismatches >= 3 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); } storage::decr_pin_retries(env)?; let pin_hash_dec = shared_secret .decrypt(&pin_hash_enc) .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)?; if !bool::from(pin_hash.ct_eq(&pin_hash_dec)) { self.get_mut_pin_protocol(pin_uv_auth_protocol) .regenerate(env.rng()); if storage::pin_retries(env)? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } self.consecutive_pin_mismatches += 1; if self.consecutive_pin_mismatches >= 3 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); } return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } } // This status code is not explicitly mentioned in the specification. None => return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED), } storage::reset_pin_retries(env)?; self.consecutive_pin_mismatches = 0; Ok(()) } fn process_get_pin_retries( &self, env: &mut impl Env, ) -> Result { Ok(AuthenticatorClientPinResponse { key_agreement: None, pin_uv_auth_token: None, retries: Some(storage::pin_retries(env)? as u64), power_cycle_state: Some(self.consecutive_pin_mismatches >= 3), }) } fn process_get_key_agreement( &self, client_pin_params: AuthenticatorClientPinParameters, ) -> Result { let key_agreement = Some( self.get_pin_protocol(client_pin_params.pin_uv_auth_protocol) .get_public_key(), ); Ok(AuthenticatorClientPinResponse { key_agreement, pin_uv_auth_token: None, retries: None, power_cycle_state: None, }) } fn process_set_pin( &mut self, env: &mut impl Env, client_pin_params: AuthenticatorClientPinParameters, ) -> Result<(), Ctap2StatusCode> { let AuthenticatorClientPinParameters { pin_uv_auth_protocol, key_agreement, pin_uv_auth_param, new_pin_enc, .. } = client_pin_params; let key_agreement = ok_or_missing(key_agreement)?; let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?; let new_pin_enc = ok_or_missing(new_pin_enc)?; if storage::pin_hash(env)?.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; shared_secret.verify(&new_pin_enc, &pin_uv_auth_param)?; check_and_store_new_pin(env, shared_secret.as_ref(), new_pin_enc)?; storage::reset_pin_retries(env)?; Ok(()) } fn process_change_pin( &mut self, env: &mut impl Env, client_pin_params: AuthenticatorClientPinParameters, ) -> Result<(), Ctap2StatusCode> { let AuthenticatorClientPinParameters { pin_uv_auth_protocol, key_agreement, pin_uv_auth_param, new_pin_enc, pin_hash_enc, .. } = client_pin_params; let key_agreement = ok_or_missing(key_agreement)?; let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?; let new_pin_enc = ok_or_missing(new_pin_enc)?; let pin_hash_enc = ok_or_missing(pin_hash_enc)?; if storage::pin_retries(env)? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; let mut auth_param_data = new_pin_enc.clone(); auth_param_data.extend(&pin_hash_enc); shared_secret.verify(&auth_param_data, &pin_uv_auth_param)?; self.verify_pin_hash_enc( env, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc, )?; check_and_store_new_pin(env, shared_secret.as_ref(), new_pin_enc)?; self.pin_protocol_v1.reset_pin_uv_auth_token(env.rng()); self.pin_protocol_v2.reset_pin_uv_auth_token(env.rng()); Ok(()) } fn process_get_pin_token( &mut self, env: &mut impl Env, client_pin_params: AuthenticatorClientPinParameters, now: CtapInstant, ) -> Result { let AuthenticatorClientPinParameters { pin_uv_auth_protocol, key_agreement, pin_hash_enc, permissions, permissions_rp_id, .. } = client_pin_params; let key_agreement = ok_or_missing(key_agreement)?; let pin_hash_enc = ok_or_missing(pin_hash_enc)?; if permissions.is_some() || permissions_rp_id.is_some() { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } if storage::pin_retries(env)? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; self.verify_pin_hash_enc( env, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc, )?; if storage::has_force_pin_change(env)? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } self.pin_protocol_v1.reset_pin_uv_auth_token(env.rng()); self.pin_protocol_v2.reset_pin_uv_auth_token(env.rng()); self.pin_uv_auth_token_state .begin_using_pin_uv_auth_token(now); self.pin_uv_auth_token_state.set_default_permissions(); let pin_uv_auth_token = shared_secret.encrypt( env.rng(), self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), )?; Ok(AuthenticatorClientPinResponse { key_agreement: None, pin_uv_auth_token: Some(pin_uv_auth_token), retries: None, power_cycle_state: None, }) } fn process_get_pin_uv_auth_token_using_uv_with_permissions( &self, // If you want to support local user verification, implement this function. // Lacking a fingerprint reader, this subcommand is currently unsupported. _client_pin_params: AuthenticatorClientPinParameters, ) -> Result { // User verification is only supported through PIN currently. Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } fn process_get_uv_retries(&self) -> Result { // User verification is only supported through PIN currently. Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } fn process_get_pin_uv_auth_token_using_pin_with_permissions( &mut self, env: &mut impl Env, mut client_pin_params: AuthenticatorClientPinParameters, now: CtapInstant, ) -> Result { // Mutating client_pin_params is just an optimization to move it into // process_get_pin_token, without cloning permissions_rp_id here. // getPinToken requires permissions* to be None. let permissions = ok_or_missing(client_pin_params.permissions.take())?; let permissions_rp_id = client_pin_params.permissions_rp_id.take(); if permissions == 0 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } // This check is not mentioned protocol steps, but mentioned in a side note. if permissions & 0x03 != 0 && permissions_rp_id.is_none() { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let response = self.process_get_pin_token(env, client_pin_params, now)?; self.pin_uv_auth_token_state.set_permissions(permissions); self.pin_uv_auth_token_state .set_permissions_rp_id(permissions_rp_id); Ok(response) } /// Processes the authenticatorClientPin command. pub fn process_command( &mut self, env: &mut impl Env, client_pin_params: AuthenticatorClientPinParameters, now: CtapInstant, ) -> Result { if !env.customization().allows_pin_protocol_v1() && client_pin_params.pin_uv_auth_protocol == PinUvAuthProtocol::V1 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let response = match client_pin_params.sub_command { ClientPinSubCommand::GetPinRetries => Some(self.process_get_pin_retries(env)?), ClientPinSubCommand::GetKeyAgreement => { Some(self.process_get_key_agreement(client_pin_params)?) } ClientPinSubCommand::SetPin => { self.process_set_pin(env, client_pin_params)?; None } ClientPinSubCommand::ChangePin => { self.process_change_pin(env, client_pin_params)?; None } ClientPinSubCommand::GetPinToken => { Some(self.process_get_pin_token(env, client_pin_params, now)?) } ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( self.process_get_pin_uv_auth_token_using_uv_with_permissions(client_pin_params)?, ), ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( self.process_get_pin_uv_auth_token_using_pin_with_permissions( env, client_pin_params, now, )?, ), }; Ok(ResponseData::AuthenticatorClientPin(response)) } /// Verifies the HMAC for the pinUvAuthToken of the given version. pub fn verify_pin_uv_auth_token( &self, hmac_contents: &[u8], pin_uv_auth_param: &[u8], pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result<(), Ctap2StatusCode> { if !self.pin_uv_auth_token_state.is_in_use() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } verify_pin_uv_auth_token( self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), hmac_contents, pin_uv_auth_param, pin_uv_auth_protocol, ) } /// Resets all held state. pub fn reset(&mut self, rng: &mut impl Rng256) { self.pin_protocol_v1.regenerate(rng); self.pin_protocol_v1.reset_pin_uv_auth_token(rng); self.pin_protocol_v2.regenerate(rng); self.pin_protocol_v2.reset_pin_uv_auth_token(rng); self.consecutive_pin_mismatches = 0; self.pin_uv_auth_token_state.stop_using_pin_uv_auth_token(); } /// Verifies, computes and encrypts the HMAC-secret outputs. /// /// The salt_enc is /// - verified with the shared secret and salt_auth, /// - decrypted with the shared secret, /// - HMAC'ed with cred_random. /// The length of the output matches salt_enc and has to be 1 or 2 blocks of /// 32 byte. pub fn process_hmac_secret( &self, rng: &mut impl Rng256, hmac_secret_input: GetAssertionHmacSecretInput, cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { let GetAssertionHmacSecretInput { key_agreement, salt_enc, salt_auth, pin_uv_auth_protocol, } = hmac_secret_input; let shared_secret = self .get_pin_protocol(pin_uv_auth_protocol) .decapsulate(key_agreement, pin_uv_auth_protocol)?; shared_secret.verify(&salt_enc, &salt_auth)?; let decrypted_salts = shared_secret.decrypt(&salt_enc)?; if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let mut output = hmac_256::(cred_random, &decrypted_salts[..32]).to_vec(); if decrypted_salts.len() == 64 { let mut output2 = hmac_256::(cred_random, &decrypted_salts[32..]).to_vec(); output.append(&mut output2); } shared_secret.encrypt(rng, &output) } /// Consumes flags and permissions related to the pinUvAuthToken. pub fn clear_token_flags(&mut self) { self.pin_uv_auth_token_state.clear_user_verified_flag(); self.pin_uv_auth_token_state .clear_pin_uv_auth_token_permissions_except_lbw(); } /// Updates the running timers, triggers timeout events. pub fn update_timeouts(&mut self, now: CtapInstant) { self.pin_uv_auth_token_state .pin_uv_auth_token_usage_timer_observer(now); } /// Checks if user verification is cached for use of the pinUvAuthToken. pub fn check_user_verified_flag(&mut self) -> Result<(), Ctap2StatusCode> { if self.pin_uv_auth_token_state.get_user_verified_flag_value() { Ok(()) } else { Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) } } /// Check if the required command's token permission is granted. pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { self.pin_uv_auth_token_state.has_permission(permission) } /// Check if no RP ID is associated with the token permission. pub fn has_no_rp_id_permission(&self) -> Result<(), Ctap2StatusCode> { self.pin_uv_auth_token_state.has_no_permissions_rp_id() } /// Check if no or the passed RP ID is associated with the token permission. pub fn has_no_or_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { self.pin_uv_auth_token_state .has_no_permissions_rp_id() .or_else(|_| self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id)) } /// Check if no RP ID is associated with the token permission, or it matches the hash. pub fn has_no_or_rp_id_hash_permission( &self, rp_id_hash: &[u8], ) -> Result<(), Ctap2StatusCode> { self.pin_uv_auth_token_state .has_no_permissions_rp_id() .or_else(|_| { self.pin_uv_auth_token_state .has_permissions_rp_id_hash(rp_id_hash) }) } /// Check if the passed RP ID is associated with the token permission. /// /// If no RP ID is associated, associate the passed RP ID as a side effect. pub fn ensure_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { if self .pin_uv_auth_token_state .has_no_permissions_rp_id() .is_ok() { self.pin_uv_auth_token_state .set_permissions_rp_id(Some(String::from(rp_id))); return Ok(()); } self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id) } #[cfg(test)] pub fn new_test( key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], pin_uv_auth_protocol: PinUvAuthProtocol, ) -> ClientPin { let mut env = crate::env::test::TestEnv::new(); let (key_agreement_key_v1, key_agreement_key_v2) = match pin_uv_auth_protocol { PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(env.rng())), PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(env.rng()), key_agreement_key), }; let mut pin_uv_auth_token_state = PinUvAuthTokenState::new(); pin_uv_auth_token_state.set_permissions(0xFF); pin_uv_auth_token_state.begin_using_pin_uv_auth_token(CtapInstant::new(0)); ClientPin { pin_protocol_v1: PinProtocol::new_test(key_agreement_key_v1, pin_uv_auth_token), pin_protocol_v2: PinProtocol::new_test(key_agreement_key_v2, pin_uv_auth_token), consecutive_pin_mismatches: 0, pin_uv_auth_token_state, } } } #[cfg(test)] mod test { use super::super::pin_protocol::authenticate_pin_uv_auth_token; use super::*; use crate::env::test::TestEnv; use alloc::vec; use embedded_time::duration::Milliseconds; /// Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(env: &mut TestEnv) { let mut pin = [0u8; 64]; pin[..4].copy_from_slice(b"1234"); let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); storage::set_pin(env, &pin_hash, 4).unwrap(); } /// Fails on PINs bigger than 64 bytes. fn encrypt_pin(shared_secret: &dyn SharedSecret, pin: Vec) -> Vec { assert!(pin.len() <= 64); let mut env = TestEnv::new(); let mut padded_pin = [0u8; 64]; padded_pin[..pin.len()].copy_from_slice(&pin[..]); shared_secret.encrypt(env.rng(), &padded_pin).unwrap() } /// Generates a ClientPin instance and a shared secret for testing. /// /// The shared secret for the desired PIN protocol is generated in a /// handshake with itself. The other protocol has a random private key, so /// tests using the wrong combination of PIN protocol and shared secret /// should fail. fn create_client_pin_and_shared_secret( pin_uv_auth_protocol: PinUvAuthProtocol, ) -> (ClientPin, Box) { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pk = key_agreement_key.genpk(); let key_agreement = CoseKey::from(pk); let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); let shared_secret = client_pin .get_pin_protocol(pin_uv_auth_protocol) .decapsulate(key_agreement, pin_uv_auth_protocol) .unwrap(); (client_pin, shared_secret) } /// Generates standard input parameters to the ClientPin command. /// /// All fields are populated for simplicity, even though most are unused. fn create_client_pin_and_parameters( pin_uv_auth_protocol: PinUvAuthProtocol, sub_command: ClientPinSubCommand, ) -> (ClientPin, AuthenticatorClientPinParameters) { let mut env = TestEnv::new(); let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); let pin = b"1234"; let mut padded_pin = [0u8; 64]; padded_pin[..pin.len()].copy_from_slice(&pin[..]); let pin_hash = Sha256::hash(&padded_pin); let new_pin_enc = shared_secret .as_ref() .encrypt(env.rng(), &padded_pin) .unwrap(); let pin_uv_auth_param = shared_secret.as_ref().authenticate(&new_pin_enc); let pin_hash_enc = shared_secret .as_ref() .encrypt(env.rng(), &pin_hash[..16]) .unwrap(); let (permissions, permissions_rp_id) = match sub_command { ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions | ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => { (Some(0x03), Some("example.com".to_string())) } _ => (None, None), }; let params = AuthenticatorClientPinParameters { pin_uv_auth_protocol, sub_command, key_agreement: Some( client_pin .get_pin_protocol(pin_uv_auth_protocol) .get_public_key(), ), pin_uv_auth_param: Some(pin_uv_auth_param), new_pin_enc: Some(new_pin_enc), pin_hash_enc: Some(pin_hash_enc), permissions, permissions_rp_id, }; (client_pin, params) } #[test] fn test_mix_pin_protocols() { let mut env = TestEnv::new(); let client_pin = ClientPin::new(env.rng()); let pin_protocol_v1 = client_pin.get_pin_protocol(PinUvAuthProtocol::V1); let pin_protocol_v2 = client_pin.get_pin_protocol(PinUvAuthProtocol::V2); let message = vec![0xAA; 16]; let shared_secret_v1 = pin_protocol_v1 .decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V1) .unwrap(); let shared_secret_v2 = pin_protocol_v2 .decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V2) .unwrap(); let ciphertext = shared_secret_v1.encrypt(env.rng(), &message).unwrap(); let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap(); assert_ne!(&message, &plaintext); let ciphertext = shared_secret_v2.encrypt(env.rng(), &message).unwrap(); let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap(); assert_ne!(&message, &plaintext); let fake_secret_v1 = pin_protocol_v1 .decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V1) .unwrap(); let ciphertext = shared_secret_v1.encrypt(env.rng(), &message).unwrap(); let plaintext = fake_secret_v1.decrypt(&ciphertext).unwrap(); assert_ne!(&message, &plaintext); let ciphertext = fake_secret_v1.encrypt(env.rng(), &message).unwrap(); let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap(); assert_ne!(&message, &plaintext); let fake_secret_v2 = pin_protocol_v2 .decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V2) .unwrap(); let ciphertext = shared_secret_v2.encrypt(env.rng(), &message).unwrap(); let plaintext = fake_secret_v2.decrypt(&ciphertext).unwrap(); assert_ne!(&message, &plaintext); let ciphertext = fake_secret_v2.encrypt(env.rng(), &message).unwrap(); let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap(); assert_ne!(&message, &plaintext); } fn test_helper_verify_pin_hash_enc(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut env = TestEnv::new(); let mut client_pin = ClientPin::new(env.rng()); let pin_protocol = client_pin.get_pin_protocol(pin_uv_auth_protocol); let shared_secret = pin_protocol .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) .unwrap(); // The PIN is "1234". let pin_hash = [ 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, 0xC4, 0x12, ]; storage::set_pin(&mut env, &pin_hash, 4).unwrap(); let pin_hash_enc = shared_secret .as_ref() .encrypt(env.rng(), &pin_hash) .unwrap(); assert_eq!( client_pin.verify_pin_hash_enc( &mut env, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc ), Ok(()) ); let pin_hash_enc = vec![0xEE; 16]; assert_eq!( client_pin.verify_pin_hash_enc( &mut env, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); let pin_hash_enc = shared_secret .as_ref() .encrypt(env.rng(), &pin_hash) .unwrap(); client_pin.consecutive_pin_mismatches = 3; assert_eq!( client_pin.verify_pin_hash_enc( &mut env, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) ); client_pin.consecutive_pin_mismatches = 0; let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; assert_eq!( client_pin.verify_pin_hash_enc( &mut env, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1]; assert_eq!( client_pin.verify_pin_hash_enc( &mut env, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] fn test_verify_pin_hash_enc_v1() { test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V1); } #[test] fn test_verify_pin_hash_enc_v2() { test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V2); } fn test_helper_process_get_pin_retries(pin_uv_auth_protocol: PinUvAuthProtocol) { let (mut client_pin, params) = create_client_pin_and_parameters( pin_uv_auth_protocol, ClientPinSubCommand::GetPinRetries, ); let mut env = TestEnv::new(); let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, pin_uv_auth_token: None, retries: Some(storage::pin_retries(&mut env).unwrap() as u64), power_cycle_state: Some(false), }); assert_eq!( client_pin.process_command(&mut env, params.clone(), CtapInstant::new(0)), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); client_pin.consecutive_pin_mismatches = 3; let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, pin_uv_auth_token: None, retries: Some(storage::pin_retries(&mut env).unwrap() as u64), power_cycle_state: Some(true), }); assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } #[test] fn test_process_get_pin_retries_v1() { test_helper_process_get_pin_retries(PinUvAuthProtocol::V1); } #[test] fn test_process_get_pin_retries_v2() { test_helper_process_get_pin_retries(PinUvAuthProtocol::V2); } fn test_helper_process_get_key_agreement(pin_uv_auth_protocol: PinUvAuthProtocol) { let (mut client_pin, params) = create_client_pin_and_parameters( pin_uv_auth_protocol, ClientPinSubCommand::GetKeyAgreement, ); let mut env = TestEnv::new(); let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: params.key_agreement.clone(), pin_uv_auth_token: None, retries: None, power_cycle_state: None, }); assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } #[test] fn test_process_get_key_agreement_v1() { test_helper_process_get_key_agreement(PinUvAuthProtocol::V1); } #[test] fn test_process_get_key_agreement_v2() { test_helper_process_get_key_agreement(PinUvAuthProtocol::V2); } #[test] fn test_process_get_key_agreement_v1_not_allowed() { let (mut client_pin, params) = create_client_pin_and_parameters( PinUvAuthProtocol::V1, ClientPinSubCommand::GetKeyAgreement, ); let mut env = TestEnv::new(); env.customization_mut().set_allows_pin_protocol_v1(false); assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); } fn test_helper_process_set_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let (mut client_pin, params) = create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::SetPin); let mut env = TestEnv::new(); assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Ok(ResponseData::AuthenticatorClientPin(None)) ); } #[test] fn test_process_set_pin_v1() { test_helper_process_set_pin(PinUvAuthProtocol::V1); } #[test] fn test_process_set_pin_v2() { test_helper_process_set_pin(PinUvAuthProtocol::V2); } fn test_helper_process_change_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let (mut client_pin, mut params) = create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::ChangePin); let shared_secret = client_pin .get_pin_protocol(pin_uv_auth_protocol) .decapsulate( params.key_agreement.clone().unwrap(), params.pin_uv_auth_protocol, ) .unwrap(); let mut env = TestEnv::new(); set_standard_pin(&mut env); let mut auth_param_data = params.new_pin_enc.clone().unwrap(); auth_param_data.extend(params.pin_hash_enc.as_ref().unwrap()); let pin_uv_auth_param = shared_secret.authenticate(&auth_param_data); params.pin_uv_auth_param = Some(pin_uv_auth_param); assert_eq!( client_pin.process_command(&mut env, params.clone(), CtapInstant::new(0)), Ok(ResponseData::AuthenticatorClientPin(None)) ); let mut bad_params = params.clone(); bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); while storage::pin_retries(&mut env).unwrap() > 0 { storage::decr_pin_retries(&mut env).unwrap(); } assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) ); } #[test] fn test_process_change_pin_v1() { test_helper_process_change_pin(PinUvAuthProtocol::V1); } #[test] fn test_process_change_pin_v2() { test_helper_process_change_pin(PinUvAuthProtocol::V2); } fn test_helper_process_get_pin_token(pin_uv_auth_protocol: PinUvAuthProtocol) { let (mut client_pin, params) = create_client_pin_and_parameters( pin_uv_auth_protocol, ClientPinSubCommand::GetPinToken, ); let shared_secret = client_pin .get_pin_protocol(pin_uv_auth_protocol) .decapsulate( params.key_agreement.clone().unwrap(), params.pin_uv_auth_protocol, ) .unwrap(); let mut env = TestEnv::new(); set_standard_pin(&mut env); let response = client_pin .process_command(&mut env, params.clone(), CtapInstant::new(0)) .unwrap(); let encrypted_token = match response { ResponseData::AuthenticatorClientPin(Some(response)) => { response.pin_uv_auth_token.unwrap() } _ => panic!("Invalid response type"), }; assert_eq!( &shared_secret.decrypt(&encrypted_token).unwrap(), client_pin .get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token() ); assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(PinPermission::MakeCredential), Ok(()) ); assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(PinPermission::GetAssertion), Ok(()) ); assert_eq!( client_pin .pin_uv_auth_token_state .has_no_permissions_rp_id(), Ok(()) ); let mut bad_params = params; bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] fn test_process_get_pin_token_v1() { test_helper_process_get_pin_token(PinUvAuthProtocol::V1); } #[test] fn test_process_get_pin_token_v2() { test_helper_process_get_pin_token(PinUvAuthProtocol::V2); } fn test_helper_process_get_pin_token_force_pin_change(pin_uv_auth_protocol: PinUvAuthProtocol) { let (mut client_pin, params) = create_client_pin_and_parameters( pin_uv_auth_protocol, ClientPinSubCommand::GetPinToken, ); let mut env = TestEnv::new(); set_standard_pin(&mut env); assert_eq!(storage::force_pin_change(&mut env), Ok(())); assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), ); } #[test] fn test_process_get_pin_token_force_pin_change_v1() { test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V1); } #[test] fn test_process_get_pin_token_force_pin_change_v2() { test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V2); } fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions( pin_uv_auth_protocol: PinUvAuthProtocol, ) { let (mut client_pin, params) = create_client_pin_and_parameters( pin_uv_auth_protocol, ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, ); let shared_secret = client_pin .get_pin_protocol(pin_uv_auth_protocol) .decapsulate( params.key_agreement.clone().unwrap(), params.pin_uv_auth_protocol, ) .unwrap(); let mut env = TestEnv::new(); set_standard_pin(&mut env); let response = client_pin .process_command(&mut env, params.clone(), CtapInstant::new(0)) .unwrap(); let encrypted_token = match response { ResponseData::AuthenticatorClientPin(Some(response)) => { response.pin_uv_auth_token.unwrap() } _ => panic!("Invalid response type"), }; assert_eq!( &shared_secret.decrypt(&encrypted_token).unwrap(), client_pin .get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token() ); assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(PinPermission::MakeCredential), Ok(()) ); assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(PinPermission::GetAssertion), Ok(()) ); assert_eq!( client_pin .pin_uv_auth_token_state .has_permissions_rp_id("example.com"), Ok(()) ); let mut bad_params = params.clone(); bad_params.permissions = Some(0x00); assert_eq!( client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); let mut bad_params = params.clone(); bad_params.permissions_rp_id = None; assert_eq!( client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); let mut bad_params = params; bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( client_pin.process_command(&mut env, bad_params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v1() { test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V1); } #[test] fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v2() { test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V2); } fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( pin_uv_auth_protocol: PinUvAuthProtocol, ) { let (mut client_pin, params) = create_client_pin_and_parameters( pin_uv_auth_protocol, ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, ); let mut env = TestEnv::new(); set_standard_pin(&mut env); assert_eq!(storage::force_pin_change(&mut env), Ok(())); assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v1() { test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( PinUvAuthProtocol::V1, ); } #[test] fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v2() { test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( PinUvAuthProtocol::V2, ); } fn test_helper_decrypt_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut env = TestEnv::new(); let pin_protocol = PinProtocol::new(env.rng()); let shared_secret = pin_protocol .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) .unwrap(); let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"1234".to_vec()); assert_eq!( decrypt_pin(shared_secret.as_ref(), new_pin_enc), Ok(b"1234".to_vec()), ); let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"123".to_vec()); assert_eq!( decrypt_pin(shared_secret.as_ref(), new_pin_enc), Ok(b"123".to_vec()), ); // Encrypted PIN is too short. let new_pin_enc = vec![0x44; 63]; assert_eq!( decrypt_pin(shared_secret.as_ref(), new_pin_enc), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); // Encrypted PIN is too long. let new_pin_enc = vec![0x44; 65]; assert_eq!( decrypt_pin(shared_secret.as_ref(), new_pin_enc), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); } #[test] fn test_decrypt_pin_v1() { test_helper_decrypt_pin(PinUvAuthProtocol::V1); } #[test] fn test_decrypt_pin_v2() { test_helper_decrypt_pin(PinUvAuthProtocol::V2); } fn test_helper_check_and_store_new_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut env = TestEnv::new(); let pin_protocol = PinProtocol::new(env.rng()); let shared_secret = pin_protocol .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) .unwrap(); let test_cases = vec![ // Accept PIN "1234". (b"1234".to_vec(), Ok(())), // Reject PIN "123" since it is too short. ( b"123".to_vec(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), ), // Reject PIN "12'\0'4" (a zero byte at index 2). ( b"12\04".to_vec(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), ), // PINs must be at most 63 bytes long, to allow for a trailing 0u8 padding. ( vec![0x30; 64], Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), ), ]; for (pin, result) in test_cases { let old_pin_hash = storage::pin_hash(&mut env).unwrap(); let new_pin_enc = encrypt_pin(shared_secret.as_ref(), pin); assert_eq!( check_and_store_new_pin(&mut env, shared_secret.as_ref(), new_pin_enc), result ); if result.is_ok() { assert_ne!(old_pin_hash, storage::pin_hash(&mut env).unwrap()); } else { assert_eq!(old_pin_hash, storage::pin_hash(&mut env).unwrap()); } } } #[test] fn test_check_and_store_new_pin_v1() { test_helper_check_and_store_new_pin(PinUvAuthProtocol::V1); } #[test] fn test_check_and_store_new_pin_v2() { test_helper_check_and_store_new_pin(PinUvAuthProtocol::V2); } /// Generates valid inputs for process_hmac_secret and returns the output. fn get_process_hmac_secret_decrypted_output( pin_uv_auth_protocol: PinUvAuthProtocol, cred_random: &[u8; 32], salt: Vec, ) -> Result, Ctap2StatusCode> { let mut env = TestEnv::new(); let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); let salt_enc = shared_secret.as_ref().encrypt(env.rng(), &salt).unwrap(); let salt_auth = shared_secret.authenticate(&salt_enc); let hmac_secret_input = GetAssertionHmacSecretInput { key_agreement: client_pin .get_pin_protocol(pin_uv_auth_protocol) .get_public_key(), salt_enc, salt_auth, pin_uv_auth_protocol, }; let output = client_pin.process_hmac_secret(env.rng(), hmac_secret_input, cred_random); output.map(|v| shared_secret.as_ref().decrypt(&v).unwrap()) } fn test_helper_process_hmac_secret_bad_salt_auth(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut env = TestEnv::new(); let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); let cred_random = [0xC9; 32]; let salt_enc = vec![0x01; 32]; let mut salt_auth = shared_secret.authenticate(&salt_enc); salt_auth[0] ^= 0x01; let hmac_secret_input = GetAssertionHmacSecretInput { key_agreement: client_pin .get_pin_protocol(pin_uv_auth_protocol) .get_public_key(), salt_enc, salt_auth, pin_uv_auth_protocol, }; let output = client_pin.process_hmac_secret(env.rng(), hmac_secret_input, &cred_random); assert_eq!(output, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)); } #[test] fn test_process_hmac_secret_bad_salt_auth_v1() { test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V1); } #[test] fn test_process_hmac_secret_bad_salt_auth_v2() { test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V2); } fn test_helper_process_hmac_secret_one_salt(pin_uv_auth_protocol: PinUvAuthProtocol) { let cred_random = [0xC9; 32]; let salt = vec![0x01; 32]; let expected_output = hmac_256::(&cred_random, &salt); let output = get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt) .unwrap(); assert_eq!(&output, &expected_output); } #[test] fn test_process_hmac_secret_one_salt_v1() { test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V1); } #[test] fn test_process_hmac_secret_one_salt_v2() { test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V2); } fn test_helper_process_hmac_secret_two_salts(pin_uv_auth_protocol: PinUvAuthProtocol) { let cred_random = [0xC9; 32]; let salt1 = [0x01; 32]; let salt2 = [0x02; 32]; let expected_output1 = hmac_256::(&cred_random, &salt1); let expected_output2 = hmac_256::(&cred_random, &salt2); let mut salt12 = vec![0x00; 64]; salt12[..32].copy_from_slice(&salt1); salt12[32..].copy_from_slice(&salt2); let output = get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt12) .unwrap(); assert_eq!(&output[..32], &expected_output1); assert_eq!(&output[32..], &expected_output2); let mut salt02 = vec![0x00; 64]; salt02[32..].copy_from_slice(&salt2); let output = get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt02) .unwrap(); assert_eq!(&output[32..], &expected_output2); let mut salt10 = vec![0x00; 64]; salt10[..32].copy_from_slice(&salt1); let output = get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt10) .unwrap(); assert_eq!(&output[..32], &expected_output1); } #[test] fn test_process_hmac_secret_two_salts_v1() { test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V1); } #[test] fn test_process_hmac_secret_two_salts_v2() { test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V2); } fn test_helper_process_hmac_secret_wrong_length(pin_uv_auth_protocol: PinUvAuthProtocol) { let cred_random = [0xC9; 32]; let output = get_process_hmac_secret_decrypted_output( pin_uv_auth_protocol, &cred_random, vec![0x5E; 48], ); assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); } #[test] fn test_process_hmac_secret_wrong_length_v1() { test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V1); } #[test] fn test_process_hmac_secret_wrong_length_v2() { test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V2); } #[test] fn test_has_permission() { let mut env = TestEnv::new(); let mut client_pin = ClientPin::new(env.rng()); client_pin.pin_uv_auth_token_state.set_permissions(0x7F); for permission in PinPermission::into_enum_iter() { assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(permission), Ok(()) ); } client_pin.pin_uv_auth_token_state.set_permissions(0x00); for permission in PinPermission::into_enum_iter() { assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(permission), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } } #[test] fn test_has_no_rp_id_permission() { let mut env = TestEnv::new(); let mut client_pin = ClientPin::new(env.rng()); assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); client_pin .pin_uv_auth_token_state .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!( client_pin.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] fn test_has_no_or_rp_id_permission() { let mut env = TestEnv::new(); let mut client_pin = ClientPin::new(env.rng()); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); client_pin .pin_uv_auth_token_state .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); assert_eq!( client_pin.has_no_or_rp_id_permission("another.example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] fn test_has_no_or_rp_id_hash_permission() { let mut env = TestEnv::new(); let mut client_pin = ClientPin::new(env.rng()); let rp_id_hash = Sha256::hash(b"example.com"); assert_eq!( client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) ); client_pin .pin_uv_auth_token_state .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!( client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) ); assert_eq!( client_pin.has_no_or_rp_id_hash_permission(&[0x4A; 32]), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] fn test_ensure_rp_id_permission() { let mut env = TestEnv::new(); let mut client_pin = ClientPin::new(env.rng()); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( client_pin .pin_uv_auth_token_state .has_permissions_rp_id("example.com"), Ok(()) ); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( client_pin.ensure_rp_id_permission("another.example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] fn test_verify_pin_uv_auth_token() { let mut env = TestEnv::new(); let mut client_pin = ClientPin::new(env.rng()); let message = [0xAA]; client_pin .pin_uv_auth_token_state .begin_using_pin_uv_auth_token(CtapInstant::new(0)); let pin_uv_auth_token_v1 = client_pin .get_pin_protocol(PinUvAuthProtocol::V1) .get_pin_uv_auth_token(); let pin_uv_auth_param_v1 = authenticate_pin_uv_auth_token(pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); let pin_uv_auth_token_v2 = client_pin .get_pin_protocol(PinUvAuthProtocol::V2) .get_pin_uv_auth_token(); let pin_uv_auth_param_v2 = authenticate_pin_uv_auth_token(pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V2); let pin_uv_auth_param_v1_from_v2_token = authenticate_pin_uv_auth_token(pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V1); let pin_uv_auth_param_v2_from_v1_token = authenticate_pin_uv_auth_token(pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V2); assert_eq!( client_pin.verify_pin_uv_auth_token( &message, &pin_uv_auth_param_v1, PinUvAuthProtocol::V1 ), Ok(()) ); assert_eq!( client_pin.verify_pin_uv_auth_token( &message, &pin_uv_auth_param_v2, PinUvAuthProtocol::V2 ), Ok(()) ); assert_eq!( client_pin.verify_pin_uv_auth_token( &message, &pin_uv_auth_param_v1, PinUvAuthProtocol::V2 ), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); assert_eq!( client_pin.verify_pin_uv_auth_token( &message, &pin_uv_auth_param_v2, PinUvAuthProtocol::V1 ), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); assert_eq!( client_pin.verify_pin_uv_auth_token( &message, &pin_uv_auth_param_v1_from_v2_token, PinUvAuthProtocol::V1 ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( client_pin.verify_pin_uv_auth_token( &message, &pin_uv_auth_param_v2_from_v1_token, PinUvAuthProtocol::V2 ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] fn test_verify_pin_uv_auth_token_not_in_use() { let mut env = TestEnv::new(); let client_pin = ClientPin::new(env.rng()); let message = [0xAA]; let pin_uv_auth_token_v1 = client_pin .get_pin_protocol(PinUvAuthProtocol::V1) .get_pin_uv_auth_token(); let pin_uv_auth_param_v1 = authenticate_pin_uv_auth_token(pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); assert_eq!( client_pin.verify_pin_uv_auth_token( &message, &pin_uv_auth_param_v1, PinUvAuthProtocol::V1 ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] fn test_reset() { let mut env = TestEnv::new(); let mut client_pin = ClientPin::new(env.rng()); let public_key_v1 = client_pin.pin_protocol_v1.get_public_key(); let public_key_v2 = client_pin.pin_protocol_v2.get_public_key(); let token_v1 = *client_pin.pin_protocol_v1.get_pin_uv_auth_token(); let token_v2 = *client_pin.pin_protocol_v2.get_pin_uv_auth_token(); client_pin.pin_uv_auth_token_state.set_permissions(0xFF); client_pin .pin_uv_auth_token_state .set_permissions_rp_id(Some(String::from("example.com"))); client_pin.reset(env.rng()); assert_ne!(public_key_v1, client_pin.pin_protocol_v1.get_public_key()); assert_ne!(public_key_v2, client_pin.pin_protocol_v2.get_public_key()); assert_ne!( &token_v1, client_pin.pin_protocol_v1.get_pin_uv_auth_token() ); assert_ne!( &token_v2, client_pin.pin_protocol_v2.get_pin_uv_auth_token() ); for permission in PinPermission::into_enum_iter() { assert_eq!( client_pin.has_permission(permission), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); } #[test] fn test_update_timeouts() { let (mut client_pin, mut params) = create_client_pin_and_parameters( PinUvAuthProtocol::V2, ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, ); let mut env = TestEnv::new(); set_standard_pin(&mut env); params.permissions = Some(0xFF); assert!(client_pin .process_command(&mut env, params, CtapInstant::new(0)) .is_ok()); for permission in PinPermission::into_enum_iter() { assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(permission), Ok(()) ); } assert_eq!( client_pin .pin_uv_auth_token_state .has_permissions_rp_id("example.com"), Ok(()) ); let timeout = CtapInstant::new(0) + Milliseconds::new(30001_u32); client_pin.update_timeouts(timeout); for permission in PinPermission::into_enum_iter() { assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(permission), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } assert_eq!( client_pin .pin_uv_auth_token_state .has_permissions_rp_id("example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] fn test_clear_token_flags() { let (mut client_pin, mut params) = create_client_pin_and_parameters( PinUvAuthProtocol::V2, ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, ); let mut env = TestEnv::new(); set_standard_pin(&mut env); params.permissions = Some(0xFF); assert!(client_pin .process_command(&mut env, params, CtapInstant::new(0)) .is_ok()); for permission in PinPermission::into_enum_iter() { assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(permission), Ok(()) ); } assert_eq!(client_pin.check_user_verified_flag(), Ok(())); client_pin.clear_token_flags(); assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(PinPermission::CredentialManagement), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( client_pin .pin_uv_auth_token_state .has_permission(PinPermission::LargeBlobWrite), Ok(()) ); assert_eq!( client_pin.check_user_verified_flag(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } }