diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 9b966fe..f0b08e4 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -292,6 +292,7 @@ impl TryFrom for AuthenticatorClientPinParameters { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { + #[cfg(not(feature = "with_ctap2_1"))] destructure_cbor_map! { let { 1 => pin_protocol, @@ -302,6 +303,21 @@ impl TryFrom for AuthenticatorClientPinParameters { 6 => pin_hash_enc, } = extract_map(cbor_value)?; } + #[cfg(feature = "with_ctap2_1")] + destructure_cbor_map! { + let { + 1 => pin_protocol, + 2 => sub_command, + 3 => key_agreement, + 4 => pin_auth, + 5 => new_pin_enc, + 6 => pin_hash_enc, + 7 => min_pin_length, + 8 => min_pin_length_rp_ids, + 9 => permissions, + 10 => permissions_rp_id, + } = extract_map(cbor_value)?; + } let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?; let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; @@ -312,16 +328,10 @@ impl TryFrom for AuthenticatorClientPinParameters { let pin_auth = pin_auth.map(extract_byte_string).transpose()?; let new_pin_enc = new_pin_enc.map(extract_byte_string).transpose()?; let pin_hash_enc = pin_hash_enc.map(extract_byte_string).transpose()?; - - // TODO(kaczmarczyck) merge with new map destructuring (and use hex!) #[cfg(feature = "with_ctap2_1")] - let min_pin_length = param_map - .remove(&cbor_unsigned!(7)) - .map(extract_unsigned) - .transpose()?; - + let min_pin_length = min_pin_length.map(extract_unsigned).transpose()?; #[cfg(feature = "with_ctap2_1")] - let min_pin_length_rp_ids = match param_map.remove(&cbor_unsigned!(8)) { + let min_pin_length_rp_ids = match min_pin_length_rp_ids { Some(entry) => Some( extract_array(entry)? .into_iter() @@ -330,21 +340,15 @@ impl TryFrom for AuthenticatorClientPinParameters { ), None => None, }; - #[cfg(feature = "with_ctap2_1")] // We expect a bit field of 8 bits, and drop everything else. // This means we ignore extensions in future versions. - let permissions = param_map - .remove(&cbor_unsigned!(9)) + let permissions = permissions .map(extract_unsigned) .transpose()? .map(|p| p as u8); - #[cfg(feature = "with_ctap2_1")] - let permissions_rp_id = param_map - .remove(&cbor_unsigned!(10)) - .map(extract_text_string) - .transpose()?; + let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?; Ok(AuthenticatorClientPinParameters { pin_protocol, diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index e5bb6f2..6ea0672 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -18,6 +18,7 @@ mod ctap1; pub mod data_formats; pub mod hid; mod key_material; +mod pin_protocol_v1; pub mod response; pub mod status_code; mod storage; @@ -32,15 +33,15 @@ use self::command::{ #[cfg(feature = "with_ctap2_1")] use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - ClientPinSubCommand, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, - PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, - PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, - SignatureAlgorithm, + CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, + PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; +use self::pin_protocol_v1::PinProtocolV1; use self::response::{ - AuthenticatorClientPinResponse, AuthenticatorGetAssertionResponse, - AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, ResponseData, + AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, + AuthenticatorMakeCredentialResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; use self::storage::PersistentStore; @@ -50,18 +51,16 @@ use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec::Vec; use byteorder::{BigEndian, ByteOrder}; -use core::convert::TryInto; #[cfg(feature = "debug_ctap")] use core::fmt::Write; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; -use crypto::hmac::{hmac_256, verify_hmac_256, verify_hmac_256_first_128bits}; +use crypto::hmac::{hmac_256, verify_hmac_256}; use crypto::rng256::Rng256; use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(feature = "debug_ctap")] use libtock::console::Console; use libtock::timer::{Duration, Timestamp}; -use subtle::ConstantTimeEq; // 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 @@ -75,10 +74,6 @@ const USE_BATCH_ATTESTATION: bool = false; // need a flash storage friendly way to implement this feature. The implemented // solution is a compromise to be compatible with U2F and not wasting storage. const USE_SIGNATURE_COUNTER: bool = true; -// Those constants have to be multiples of 16, the AES block size. -const PIN_AUTH_LENGTH: usize = 16; -const PIN_TOKEN_LENGTH: usize = 32; -const PIN_PADDED_LENGTH: usize = 64; // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -114,74 +109,6 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa // - Some(CredentialProtectionPolicy::UserVerificationRequired) const DEFAULT_CRED_PROTECT: Option = None; -fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { - if pin_auth.len() != PIN_AUTH_LENGTH { - return false; - } - verify_hmac_256_first_128bits::( - hmac_key, - hmac_contents, - array_ref![pin_auth, 0, PIN_AUTH_LENGTH], - ) -} - -// Decrypts the HMAC secret salt(s) that were encrypted with the shared secret. -// The credRandom is used as a secret to HMAC those salts. -// The last step is to re-encrypt the outputs. -pub fn encrypt_hmac_secret_output( - shared_secret: &[u8; 32], - salt_enc: &[u8], - cred_random: &[u8], -) -> Result, Ctap2StatusCode> { - if salt_enc.len() != 32 && salt_enc.len() != 64 { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - if cred_random.len() != 32 { - // We are strict here. We need at least 32 byte, but expect exactly 32. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - // The specification specifically asks for a zero IV. - let iv = [0; 16]; - - let mut cred_random_secret = [0; 32]; - cred_random_secret.clone_from_slice(cred_random); - - // Initialization of 4 blocks in any case makes this function more readable. - let mut blocks = [[0u8; 16]; 4]; - let block_len = salt_enc.len() / 16; - for i in 0..block_len { - blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]); - } - cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]); - - let mut decrypted_salt1 = [0; 32]; - decrypted_salt1[..16].clone_from_slice(&blocks[0]); - let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); - decrypted_salt1[16..].clone_from_slice(&blocks[1]); - for i in 0..2 { - blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); - } - - if block_len == 4 { - let mut decrypted_salt2 = [0; 32]; - decrypted_salt2[..16].clone_from_slice(&blocks[2]); - decrypted_salt2[16..].clone_from_slice(&blocks[3]); - let output2 = hmac_256::(&cred_random_secret, &decrypted_salt2[..]); - for i in 0..2 { - blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]); - } - } - - cbc_encrypt(&aes_enc_key, iv, &mut blocks[..block_len]); - let mut encrypted_output = Vec::with_capacity(salt_enc.len()); - for b in &blocks[..block_len] { - encrypted_output.extend(b); - } - Ok(encrypted_output) -} - // This function is adapted from https://doc.rust-lang.org/nightly/src/core/str/mod.rs.html#2110 // (as of 2020-01-20) and truncates to "max" bytes, not breaking the encoding. // We change the return value, since we don't need the bool. @@ -205,9 +132,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( // false otherwise. check_user_presence: CheckUserPresence, persistent_store: PersistentStore, - key_agreement_key: crypto::ecdh::SecKey, - pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], - consecutive_pin_mismatches: u64, + 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")] @@ -225,16 +150,13 @@ where rng: &'a mut R, check_user_presence: CheckUserPresence, ) -> CtapState<'a, R, CheckUserPresence> { - let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - let pin_uv_auth_token = rng.gen_uniform_u8x32(); let persistent_store = PersistentStore::new(rng); + let pin_protocol_v1 = PinProtocolV1::new(rng); CtapState { rng, check_user_presence, persistent_store, - key_agreement_key, - pin_uv_auth_token, - consecutive_pin_mismatches: 0, + pin_protocol_v1, accepts_reset: true, #[cfg(feature = "with_ctap1")] u2f_up_state: U2fUserPresenceState::new( @@ -485,7 +407,10 @@ where // Specification is unclear, could be CTAP2_ERR_INVALID_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - if !check_pin_auth(&self.pin_uv_auth_token, &client_data_hash, &pin_auth) { + if !self + .pin_protocol_v1 + .check_pin_auth_token(&client_data_hash, &pin_auth) + { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } UP_FLAG | UV_FLAG | AT_FLAG | ed_flag @@ -660,7 +585,10 @@ where // Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - if !check_pin_auth(&self.pin_uv_auth_token, &client_data_hash, &pin_auth) { + if !self + .pin_protocol_v1 + .check_pin_auth_token(&client_data_hash, &pin_auth) + { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } UV_FLAG @@ -724,25 +652,9 @@ where let mut auth_data = self.generate_auth_data(&rp_id_hash, flags); // Process extensions. if let Some(hmac_secret_input) = hmac_secret_input { - let GetAssertionHmacSecretInput { - key_agreement, - salt_enc, - salt_auth, - } = hmac_secret_input; - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - // HMAC-secret does the same 16 byte truncated check. - if !check_pin_auth(&shared_secret, &salt_enc, &salt_auth) { - // Again, hard to tell what the correct error code here is. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - - let encrypted_output = match &credential.cred_random { - Some(cr) => encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cr)?, - // This is the case if the credential was not created with HMAC-secret. - None => return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION), - }; - + 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, }; @@ -823,323 +735,12 @@ where )) } - fn check_and_store_new_pin( - &mut self, - aes_dec_key: &crypto::aes256::DecryptionKey, - new_pin_enc: Vec, - ) -> bool { - if new_pin_enc.len() != PIN_PADDED_LENGTH { - return false; - } - let iv = [0; 16]; - // Assuming PIN_PADDED_LENGTH % block_size == 0 here. - let mut blocks = [[0u8; 16]; PIN_PADDED_LENGTH / 16]; - for i in 0..PIN_PADDED_LENGTH / 16 { - blocks[i].copy_from_slice(&new_pin_enc[i * 16..(i + 1) * 16]); - } - cbc_decrypt(aes_dec_key, iv, &mut blocks); - let mut pin = vec![]; - 'pin_block_loop: for block in blocks.iter().take(PIN_PADDED_LENGTH / 16) { - for cur_char in block.iter() { - if *cur_char != 0 { - pin.push(*cur_char); - } else { - break 'pin_block_loop; - } - } - } - if pin.len() < 4 || pin.len() == PIN_PADDED_LENGTH { - // TODO(kaczmarczyck) check 4 code point minimum instead - return false; - } - let mut pin_hash = [0; 16]; - pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); - self.persistent_store.set_pin_hash(&pin_hash); - true - } - - fn check_pin_hash_enc( - &mut self, - aes_dec_key: &crypto::aes256::DecryptionKey, - pin_hash_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - match self.persistent_store.pin_hash() { - Some(pin_hash) => { - if self.consecutive_pin_mismatches >= 3 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); - } - // We need to copy the pin hash, because decrementing the pin retries below may - // invalidate the reference (if the page containing the pin hash is compacted). - let pin_hash = pin_hash.to_vec(); - self.persistent_store.decr_pin_retries(); - if pin_hash_enc.len() != PIN_AUTH_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - - let iv = [0; 16]; - let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash_enc[0..PIN_AUTH_LENGTH]); - cbc_decrypt(aes_dec_key, iv, &mut blocks); - - let pin_comparison = array_ref![pin_hash, 0, PIN_AUTH_LENGTH].ct_eq(&blocks[0]); - if !bool::from(pin_comparison) { - self.key_agreement_key = crypto::ecdh::SecKey::gensk(self.rng); - if self.persistent_store.pin_retries() == 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_PIN_REQUIRED), - } - self.persistent_store.reset_pin_retries(); - self.consecutive_pin_mismatches = 0; - Ok(()) - } - - fn process_get_pin_retries(&self) -> Result { - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: None, - retries: Some(self.persistent_store.pin_retries() as u64), - }) - } - - fn process_get_key_agreement(&self) -> Result { - let pk = self.key_agreement_key.genpk(); - Ok(AuthenticatorClientPinResponse { - key_agreement: Some(CoseKey::from(pk)), - pin_token: None, - retries: None, - }) - } - - fn process_set_pin( - &mut self, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - if self.persistent_store.pin_hash().is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - if !check_pin_auth(&shared_secret, &new_pin_enc, &pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - if !self.check_and_store_new_pin(&aes_dec_key, new_pin_enc) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); - } - self.persistent_store.reset_pin_retries(); - Ok(()) - } - - fn process_change_pin( - &mut self, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, - pin_hash_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - if self.persistent_store.pin_retries() == 0 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); - } - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - let mut auth_param_data = new_pin_enc.clone(); - auth_param_data.extend(&pin_hash_enc); - if !check_pin_auth(&shared_secret, &auth_param_data, &pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - self.check_pin_hash_enc(&aes_dec_key, pin_hash_enc)?; - - if !self.check_and_store_new_pin(&aes_dec_key, new_pin_enc) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); - } - self.pin_uv_auth_token = self.rng.gen_uniform_u8x32(); - Ok(()) - } - - fn process_get_pin_token( - &mut self, - key_agreement: CoseKey, - pin_hash_enc: Vec, - ) -> Result { - if self.persistent_store.pin_retries() == 0 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); - } - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - self.check_pin_hash_enc(&aes_dec_key, pin_hash_enc)?; - - // Assuming PIN_TOKEN_LENGTH % block_size == 0 here. - let iv = [0; 16]; - let mut blocks = [[0u8; 16]; PIN_TOKEN_LENGTH / 16]; - for (i, item) in blocks.iter_mut().take(PIN_TOKEN_LENGTH / 16).enumerate() { - item.copy_from_slice(&self.pin_uv_auth_token[i * 16..(i + 1) * 16]); - } - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - let mut pin_token = vec![]; - for item in blocks.iter().take(PIN_TOKEN_LENGTH / 16) { - pin_token.extend(item); - } - - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: Some(pin_token), - retries: None, - }) - } - - #[cfg(feature = "with_ctap2_1")] - fn process_get_pin_uv_auth_token_using_uv_with_permissions( - &self, - _: CoseKey, - ) -> Result { - Ok(AuthenticatorClientPinResponse { - // User verifications is only supported through PIN currently. - key_agreement: None, - pin_token: Some(vec![]), - retries: None, - }) - } - - #[cfg(feature = "with_ctap2_1")] - fn process_get_uv_retries(&self) -> Result { - // User verifications is only supported through PIN currently. - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: None, - retries: Some(0), - }) - } - - #[cfg(feature = "with_ctap2_1")] - fn process_set_min_pin_length( - &mut self, - _min_pin_length: u64, - _min_pin_length_rp_ids: Vec, - _pin_auth: Vec, - ) -> Result { - // TODO - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: None, - retries: Some(0), - }) - } - - #[cfg(feature = "with_ctap2_1")] - fn process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut self, - _key_agreement: CoseKey, - _pin_hash_enc: Vec, - _permissions: u8, - _permissions_rp_id: String, - ) -> Result { - // TODO - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: None, - retries: Some(0), - }) - } - fn process_client_pin( &mut self, client_pin_params: AuthenticatorClientPinParameters, ) -> Result { - let AuthenticatorClientPinParameters { - pin_protocol, - sub_command, - key_agreement, - pin_auth, - new_pin_enc, - pin_hash_enc, - #[cfg(feature = "with_ctap2_1")] - min_pin_length, - #[cfg(feature = "with_ctap2_1")] - min_pin_length_rp_ids, - #[cfg(feature = "with_ctap2_1")] - permissions, - #[cfg(feature = "with_ctap2_1")] - permissions_rp_id, - } = client_pin_params; - - if pin_protocol != 1 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - - let response = match sub_command { - ClientPinSubCommand::GetPinRetries => Some(self.process_get_pin_retries()?), - ClientPinSubCommand::GetKeyAgreement => Some(self.process_get_key_agreement()?), - ClientPinSubCommand::SetPin => { - self.process_set_pin( - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; - None - } - ClientPinSubCommand::ChangePin => { - self.process_change_pin( - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; - None - } - ClientPinSubCommand::GetPinToken => Some(self.process_get_pin_token( - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?), - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( - self.process_get_pin_uv_auth_token_using_uv_with_permissions( - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?, - ), - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::SetMinPinLength => { - self.process_set_min_pin_length( - min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - min_pin_length_rp_ids.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; - None - } - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => { - self.process_get_pin_uv_auth_token_using_pin_with_permissions( - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions_rp_id.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; - None - } - }; - Ok(ResponseData::AuthenticatorClientPin(response)) + self.pin_protocol_v1 + .process(self.rng, &mut self.persistent_store, client_pin_params) } fn process_reset(&mut self, cid: ChannelID) -> Result { @@ -1150,9 +751,7 @@ where (self.check_user_presence)(cid)?; self.persistent_store.reset(self.rng); - self.key_agreement_key = crypto::ecdh::SecKey::gensk(self.rng); - self.pin_uv_auth_token = self.rng.gen_uniform_u8x32(); - self.consecutive_pin_mismatches = 0; + self.pin_protocol_v1.reset(self.rng); #[cfg(feature = "with_ctap1")] { self.u2f_up_state = U2fUserPresenceState::new( @@ -1192,8 +791,9 @@ where #[cfg(test)] mod test { use super::data_formats::{ - GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, - MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + CoseKey, GetAssertionExtensions, GetAssertionHmacSecretInput, GetAssertionOptions, + MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; use super::*; use crypto::rng256::ThreadRng256; @@ -1829,32 +1429,4 @@ mod test { .is_none()); } } - - #[test] - fn test_encrypt_hmac_secret_output() { - let shared_secret = [0x55; 32]; - let salt_enc = [0x5E; 32]; - let cred_random = [0xC9; 32]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 32); - - let salt_enc = [0x5E; 48]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!( - output, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) - ); - - let salt_enc = [0x5E; 64]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 64); - - let salt_enc = [0x5E; 32]; - let cred_random = [0xC9; 33]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!( - output, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) - ); - } } diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs new file mode 100644 index 0000000..d936774 --- /dev/null +++ b/src/ctap/pin_protocol_v1.rs @@ -0,0 +1,526 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::command::AuthenticatorClientPinParameters; +use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput}; +use super::response::{AuthenticatorClientPinResponse, ResponseData}; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use alloc::string::String; +use alloc::vec::Vec; +use core::convert::TryInto; +use crypto::cbc::{cbc_decrypt, cbc_encrypt}; +use crypto::hmac::{hmac_256, verify_hmac_256_first_128bits}; +use crypto::rng256::Rng256; +use crypto::sha256::Sha256; +use crypto::Hash256; +use subtle::ConstantTimeEq; + +// Those constants have to be multiples of 16, the AES block size. +pub const PIN_AUTH_LENGTH: usize = 16; +const PIN_PADDED_LENGTH: usize = 64; +const PIN_TOKEN_LENGTH: usize = 32; + +fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { + if pin_auth.len() != PIN_AUTH_LENGTH { + return false; + } + verify_hmac_256_first_128bits::( + hmac_key, + hmac_contents, + array_ref![pin_auth, 0, PIN_AUTH_LENGTH], + ) +} + +// Decrypts the HMAC secret salt(s) that were encrypted with the shared secret. +// The credRandom is used as a secret to HMAC those salts. +// The last step is to re-encrypt the outputs. +fn encrypt_hmac_secret_output( + shared_secret: &[u8; 32], + salt_enc: &[u8], + cred_random: &[u8], +) -> Result, Ctap2StatusCode> { + if salt_enc.len() != 32 && salt_enc.len() != 64 { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + } + if cred_random.len() != 32 { + // We are strict here. We need at least 32 byte, but expect exactly 32. + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + } + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + // The specification specifically asks for a zero IV. + let iv = [0; 16]; + + let mut cred_random_secret = [0; 32]; + cred_random_secret.clone_from_slice(cred_random); + + // Initialization of 4 blocks in any case makes this function more readable. + let mut blocks = [[0u8; 16]; 4]; + let block_len = salt_enc.len() / 16; + for i in 0..block_len { + blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]); + } + cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]); + + let mut decrypted_salt1 = [0; 32]; + decrypted_salt1[..16].clone_from_slice(&blocks[0]); + let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); + decrypted_salt1[16..].clone_from_slice(&blocks[1]); + for i in 0..2 { + blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); + } + + if block_len == 4 { + let mut decrypted_salt2 = [0; 32]; + decrypted_salt2[..16].clone_from_slice(&blocks[2]); + decrypted_salt2[16..].clone_from_slice(&blocks[3]); + let output2 = hmac_256::(&cred_random_secret, &decrypted_salt2[..]); + for i in 0..2 { + blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]); + } + } + + cbc_encrypt(&aes_enc_key, iv, &mut blocks[..block_len]); + let mut encrypted_output = Vec::with_capacity(salt_enc.len()); + for b in &blocks[..block_len] { + encrypted_output.extend(b); + } + Ok(encrypted_output) +} + +pub struct PinProtocolV1 { + key_agreement_key: crypto::ecdh::SecKey, + pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], + consecutive_pin_mismatches: u64, +} + +impl PinProtocolV1 { + pub fn new(rng: &mut impl Rng256) -> PinProtocolV1 { + let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + let pin_uv_auth_token = rng.gen_uniform_u8x32(); + PinProtocolV1 { + key_agreement_key, + pin_uv_auth_token, + consecutive_pin_mismatches: 0, + } + } + + fn check_and_store_new_pin( + &mut self, + persistent_store: &mut PersistentStore, + aes_dec_key: &crypto::aes256::DecryptionKey, + new_pin_enc: Vec, + ) -> bool { + if new_pin_enc.len() != PIN_PADDED_LENGTH { + return false; + } + let iv = [0; 16]; + // Assuming PIN_PADDED_LENGTH % block_size == 0 here. + let mut blocks = [[0u8; 16]; PIN_PADDED_LENGTH / 16]; + for i in 0..PIN_PADDED_LENGTH / 16 { + blocks[i].copy_from_slice(&new_pin_enc[i * 16..(i + 1) * 16]); + } + cbc_decrypt(aes_dec_key, iv, &mut blocks); + let mut pin = vec![]; + 'pin_block_loop: for block in blocks.iter().take(PIN_PADDED_LENGTH / 16) { + for cur_char in block.iter() { + if *cur_char != 0 { + pin.push(*cur_char); + } else { + break 'pin_block_loop; + } + } + } + if pin.len() < 4 || pin.len() == PIN_PADDED_LENGTH { + // TODO(kaczmarczyck) check 4 code point minimum instead + return false; + } + let mut pin_hash = [0; 16]; + pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); + persistent_store.set_pin_hash(&pin_hash); + true + } + + fn check_pin_hash_enc( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + aes_dec_key: &crypto::aes256::DecryptionKey, + pin_hash_enc: Vec, + ) -> Result<(), Ctap2StatusCode> { + match persistent_store.pin_hash() { + Some(pin_hash) => { + if self.consecutive_pin_mismatches >= 3 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); + } + // We need to copy the pin hash, because decrementing the pin retries below may + // invalidate the reference (if the page containing the pin hash is compacted). + let pin_hash = pin_hash.to_vec(); + persistent_store.decr_pin_retries(); + if pin_hash_enc.len() != PIN_AUTH_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } + + let iv = [0; 16]; + let mut blocks = [[0u8; 16]; 1]; + blocks[0].copy_from_slice(&pin_hash_enc[0..PIN_AUTH_LENGTH]); + cbc_decrypt(aes_dec_key, iv, &mut blocks); + + let pin_comparison = array_ref![pin_hash, 0, PIN_AUTH_LENGTH].ct_eq(&blocks[0]); + if !bool::from(pin_comparison) { + self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + if persistent_store.pin_retries() == 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_PIN_REQUIRED), + } + persistent_store.reset_pin_retries(); + self.consecutive_pin_mismatches = 0; + Ok(()) + } + + fn process_get_pin_retries( + &self, + persistent_store: &PersistentStore, + ) -> Result { + Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_token: None, + retries: Some(persistent_store.pin_retries() as u64), + }) + } + + fn process_get_key_agreement(&self) -> Result { + let pk = self.key_agreement_key.genpk(); + Ok(AuthenticatorClientPinResponse { + key_agreement: Some(CoseKey::from(pk)), + pin_token: None, + retries: None, + }) + } + + fn process_set_pin( + &mut self, + persistent_store: &mut PersistentStore, + key_agreement: CoseKey, + pin_auth: Vec, + new_pin_enc: Vec, + ) -> Result<(), Ctap2StatusCode> { + if persistent_store.pin_hash().is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; + let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); + + if !check_pin_auth(&shared_secret, &new_pin_enc, &pin_auth) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + + let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + if !self.check_and_store_new_pin(persistent_store, &aes_dec_key, new_pin_enc) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); + } + persistent_store.reset_pin_retries(); + Ok(()) + } + + fn process_change_pin( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + key_agreement: CoseKey, + pin_auth: Vec, + new_pin_enc: Vec, + pin_hash_enc: Vec, + ) -> Result<(), Ctap2StatusCode> { + if persistent_store.pin_retries() == 0 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); + } + let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; + let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); + + let mut auth_param_data = new_pin_enc.clone(); + auth_param_data.extend(&pin_hash_enc); + if !check_pin_auth(&shared_secret, &auth_param_data, &pin_auth) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + + let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + self.check_pin_hash_enc(rng, persistent_store, &aes_dec_key, pin_hash_enc)?; + + if !self.check_and_store_new_pin(persistent_store, &aes_dec_key, new_pin_enc) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); + } + self.pin_uv_auth_token = rng.gen_uniform_u8x32(); + Ok(()) + } + + fn process_get_pin_token( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + key_agreement: CoseKey, + pin_hash_enc: Vec, + ) -> Result { + if persistent_store.pin_retries() == 0 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); + } + let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; + let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); + + let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + self.check_pin_hash_enc(rng, persistent_store, &aes_dec_key, pin_hash_enc)?; + + // Assuming PIN_TOKEN_LENGTH % block_size == 0 here. + let iv = [0; 16]; + let mut blocks = [[0u8; 16]; PIN_TOKEN_LENGTH / 16]; + for (i, item) in blocks.iter_mut().take(PIN_TOKEN_LENGTH / 16).enumerate() { + item.copy_from_slice(&self.pin_uv_auth_token[i * 16..(i + 1) * 16]); + } + cbc_encrypt(&aes_enc_key, iv, &mut blocks); + let mut pin_token = vec![]; + for item in blocks.iter().take(PIN_TOKEN_LENGTH / 16) { + pin_token.extend(item); + } + + Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_token: Some(pin_token), + retries: None, + }) + } + + #[cfg(feature = "with_ctap2_1")] + fn process_get_pin_uv_auth_token_using_uv_with_permissions( + &self, + _: CoseKey, + ) -> Result { + Ok(AuthenticatorClientPinResponse { + // User verifications is only supported through PIN currently. + key_agreement: None, + pin_token: Some(vec![]), + retries: None, + }) + } + + #[cfg(feature = "with_ctap2_1")] + fn process_get_uv_retries(&self) -> Result { + // User verifications is only supported through PIN currently. + Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_token: None, + retries: Some(0), + }) + } + + #[cfg(feature = "with_ctap2_1")] + fn process_set_min_pin_length( + &mut self, + _min_pin_length: u64, + _min_pin_length_rp_ids: Vec, + _pin_auth: Vec, + ) -> Result { + // TODO + Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_token: None, + retries: Some(0), + }) + } + + #[cfg(feature = "with_ctap2_1")] + fn process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut self, + _key_agreement: CoseKey, + _pin_hash_enc: Vec, + _permissions: u8, + _permissions_rp_id: String, + ) -> Result { + // TODO + Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_token: None, + retries: Some(0), + }) + } + + pub fn process( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + client_pin_params: AuthenticatorClientPinParameters, + ) -> Result { + let AuthenticatorClientPinParameters { + pin_protocol, + sub_command, + key_agreement, + pin_auth, + new_pin_enc, + pin_hash_enc, + #[cfg(feature = "with_ctap2_1")] + min_pin_length, + #[cfg(feature = "with_ctap2_1")] + min_pin_length_rp_ids, + #[cfg(feature = "with_ctap2_1")] + permissions, + #[cfg(feature = "with_ctap2_1")] + permissions_rp_id, + } = client_pin_params; + + if pin_protocol != 1 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + + let response = match sub_command { + ClientPinSubCommand::GetPinRetries => { + Some(self.process_get_pin_retries(persistent_store)?) + } + ClientPinSubCommand::GetKeyAgreement => Some(self.process_get_key_agreement()?), + ClientPinSubCommand::SetPin => { + self.process_set_pin( + persistent_store, + key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + ClientPinSubCommand::ChangePin => { + self.process_change_pin( + rng, + persistent_store, + key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + ClientPinSubCommand::GetPinToken => Some(self.process_get_pin_token( + rng, + persistent_store, + key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?), + #[cfg(feature = "with_ctap2_1")] + ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( + self.process_get_pin_uv_auth_token_using_uv_with_permissions( + key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?, + ), + #[cfg(feature = "with_ctap2_1")] + ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), + #[cfg(feature = "with_ctap2_1")] + ClientPinSubCommand::SetMinPinLength => { + self.process_set_min_pin_length( + min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + min_pin_length_rp_ids.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + #[cfg(feature = "with_ctap2_1")] + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => { + self.process_get_pin_uv_auth_token_using_pin_with_permissions( + key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + permissions_rp_id.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + }; + Ok(ResponseData::AuthenticatorClientPin(response)) + } + + pub fn check_pin_auth_token(&self, hmac_contents: &[u8], pin_auth: &[u8]) -> bool { + check_pin_auth(&self.pin_uv_auth_token, &hmac_contents, &pin_auth) + } + + pub fn reset(&mut self, rng: &mut impl Rng256) { + self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + self.pin_uv_auth_token = rng.gen_uniform_u8x32(); + self.consecutive_pin_mismatches = 0; + } + + pub fn process_hmac_secret( + &self, + hmac_secret_input: GetAssertionHmacSecretInput, + cred_random: &Option>, + ) -> Result, Ctap2StatusCode> { + let GetAssertionHmacSecretInput { + key_agreement, + salt_enc, + salt_auth, + } = hmac_secret_input; + let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; + let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); + // HMAC-secret does the same 16 byte truncated check. + if !check_pin_auth(&shared_secret, &salt_enc, &salt_auth) { + // Hard to tell what the correct error code here is. + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + } + + match cred_random { + Some(cr) => encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cr), + // This is the case if the credential was not created with HMAC-secret. + None => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_encrypt_hmac_secret_output() { + let shared_secret = [0x55; 32]; + let salt_enc = [0x5E; 32]; + let cred_random = [0xC9; 32]; + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + assert_eq!(output.unwrap().len(), 32); + + let salt_enc = [0x5E; 48]; + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + assert_eq!( + output, + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + ); + + let salt_enc = [0x5E; 64]; + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + assert_eq!(output.unwrap().len(), 64); + + let salt_enc = [0x5E; 32]; + let cred_random = [0xC9; 33]; + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + assert_eq!( + output, + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + ); + } +} diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 60dd99d..a730cd6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -14,8 +14,9 @@ use crate::crypto::rng256::Rng256; use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource}; +use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; -use crate::ctap::{key_material, PIN_AUTH_LENGTH, USE_BATCH_ATTESTATION}; +use crate::ctap::{key_material, USE_BATCH_ATTESTATION}; use alloc::string::String; use alloc::vec::Vec; use core::convert::TryInto; @@ -809,7 +810,6 @@ mod test { #[test] fn test_pin_hash() { - use crate::ctap::PIN_AUTH_LENGTH; let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng);