From a4becf9acafbda05b2d99c2545221ae7bdd96328 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 18 Jun 2020 15:58:12 +0200 Subject: [PATCH 01/20] new error codes and enum cleanups --- src/ctap/data_formats.rs | 69 +++++++++++++++++++--------------------- src/ctap/status_code.rs | 8 +++++ 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index b5799b48..ed543893 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -176,11 +176,12 @@ pub enum AuthenticatorTransport { impl From for cbor::Value { fn from(transport: AuthenticatorTransport) -> Self { + use AuthenticatorTransport::*; match transport { - AuthenticatorTransport::Usb => "usb", - AuthenticatorTransport::Nfc => "nfc", - AuthenticatorTransport::Ble => "ble", - AuthenticatorTransport::Internal => "internal", + Usb => "usb", + Nfc => "nfc", + Ble => "ble", + Internal => "internal", } .into() } @@ -190,12 +191,13 @@ impl TryFrom for AuthenticatorTransport { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { + use AuthenticatorTransport::*; let transport_string = extract_text_string(cbor_value)?; match &transport_string[..] { - "usb" => Ok(AuthenticatorTransport::Usb), - "nfc" => Ok(AuthenticatorTransport::Nfc), - "ble" => Ok(AuthenticatorTransport::Ble), - "internal" => Ok(AuthenticatorTransport::Internal), + "usb" => Ok(Usb), + "nfc" => Ok(Nfc), + "ble" => Ok(Ble), + "internal" => Ok(Internal), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } @@ -469,10 +471,11 @@ impl TryFrom for CredentialProtectionPolicy { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { + use CredentialProtectionPolicy::*; match extract_integer(cbor_value)? { - 0x01 => Ok(CredentialProtectionPolicy::UserVerificationOptional), - 0x02 => Ok(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList), - 0x03 => Ok(CredentialProtectionPolicy::UserVerificationRequired), + 0x01 => Ok(UserVerificationOptional), + 0x02 => Ok(UserVerificationOptionalWithCredentialIdList), + 0x03 => Ok(UserVerificationRequired), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } @@ -683,27 +686,18 @@ impl TryFrom for ecdh::PubKey { #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub enum ClientPinSubCommand { - GetPinRetries, - GetKeyAgreement, - SetPin, - ChangePin, - GetPinUvAuthTokenUsingPin, - GetPinUvAuthTokenUsingUv, - GetUvRetries, + GetPinRetries = 0x01, + GetKeyAgreement = 0x02, + SetPin = 0x03, + ChangePin = 0x04, + GetPinUvAuthTokenUsingPin = 0x05, + GetPinUvAuthTokenUsingUv = 0x06, + GetUvRetries = 0x07, } impl From for cbor::Value { fn from(subcommand: ClientPinSubCommand) -> Self { - match subcommand { - ClientPinSubCommand::GetPinRetries => 0x01, - ClientPinSubCommand::GetKeyAgreement => 0x02, - ClientPinSubCommand::SetPin => 0x03, - ClientPinSubCommand::ChangePin => 0x04, - ClientPinSubCommand::GetPinUvAuthTokenUsingPin => 0x05, - ClientPinSubCommand::GetPinUvAuthTokenUsingUv => 0x06, - ClientPinSubCommand::GetUvRetries => 0x07, - } - .into() + (subcommand as u64).into() } } @@ -711,16 +705,19 @@ impl TryFrom for ClientPinSubCommand { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { + use ClientPinSubCommand::*; let subcommand_int = extract_unsigned(cbor_value)?; match subcommand_int { - 0x01 => Ok(ClientPinSubCommand::GetPinRetries), - 0x02 => Ok(ClientPinSubCommand::GetKeyAgreement), - 0x03 => Ok(ClientPinSubCommand::SetPin), - 0x04 => Ok(ClientPinSubCommand::ChangePin), - 0x05 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPin), - 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUv), - 0x07 => Ok(ClientPinSubCommand::GetUvRetries), - // TODO(kaczmarczyck) what is the correct status code for this error? + 0x01 => Ok(GetPinRetries), + 0x02 => Ok(GetKeyAgreement), + 0x03 => Ok(SetPin), + 0x04 => Ok(ChangePin), + 0x05 => Ok(GetPinUvAuthTokenUsingPin), + 0x06 => Ok(GetPinUvAuthTokenUsingUv), + 0x07 => Ok(GetUvRetries), + #[cfg(feature = "with_ctap2_1")] + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), + #[cfg(not(feature = "with_ctap2_1"))] _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), } } diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index b58b8d03..adb84fd5 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -32,6 +32,10 @@ pub enum Ctap2StatusCode { CTAP2_ERR_MISSING_PARAMETER = 0x14, CTAP2_ERR_LIMIT_EXCEEDED = 0x15, CTAP2_ERR_UNSUPPORTED_EXTENSION = 0x16, + #[cfg(feature = "with_ctap2_1")] + CTAP2_ERR_FP_DATABASE_FULL = 0x17, + #[cfg(feature = "with_ctap2_1")] + CTAP2_ERR_PC_STORAGE_FULL = 0x18, CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19, CTAP2_ERR_PROCESSING = 0x21, CTAP2_ERR_INVALID_CREDENTIAL = 0x22, @@ -60,6 +64,10 @@ pub enum Ctap2StatusCode { CTAP2_ERR_ACTION_TIMEOUT = 0x3A, CTAP2_ERR_UP_REQUIRED = 0x3B, CTAP2_ERR_UV_BLOCKED = 0x3C, + #[cfg(feature = "with_ctap2_1")] + CTAP2_ERR_INTEGRITY_FAILURE = 0x3D, + #[cfg(feature = "with_ctap2_1")] + CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E, CTAP1_ERR_OTHER = 0x7F, CTAP2_ERR_SPEC_LAST = 0xDF, CTAP2_ERR_EXTENSION_FIRST = 0xE0, From 63aef3bd7645cd4b7eea9a6e2f6b342f20367c2c Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 18 Jun 2020 16:10:08 +0200 Subject: [PATCH 02/20] new client pin subcommands --- src/ctap/command.rs | 73 ++++++++++++++++++++++++++++++++++++ src/ctap/data_formats.rs | 20 ++++++++-- src/ctap/mod.rs | 81 +++++++++++++++++++++++++++++++++++----- 3 files changed, 160 insertions(+), 14 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index ef1e6edd..9b966fe4 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -278,6 +278,14 @@ pub struct AuthenticatorClientPinParameters { pub pin_auth: Option>, pub new_pin_enc: Option>, pub pin_hash_enc: Option>, + #[cfg(feature = "with_ctap2_1")] + pub min_pin_length: Option, + #[cfg(feature = "with_ctap2_1")] + pub min_pin_length_rp_ids: Option>, + #[cfg(feature = "with_ctap2_1")] + pub permissions: Option, + #[cfg(feature = "with_ctap2_1")] + pub permissions_rp_id: Option, } impl TryFrom for AuthenticatorClientPinParameters { @@ -305,6 +313,39 @@ impl TryFrom for AuthenticatorClientPinParameters { 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()?; + + #[cfg(feature = "with_ctap2_1")] + let min_pin_length_rp_ids = match param_map.remove(&cbor_unsigned!(8)) { + Some(entry) => Some( + extract_array(entry)? + .into_iter() + .map(extract_text_string) + .collect::, Ctap2StatusCode>>()?, + ), + 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)) + .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()?; + Ok(AuthenticatorClientPinParameters { pin_protocol, sub_command, @@ -312,6 +353,14 @@ impl TryFrom for AuthenticatorClientPinParameters { 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, }) } } @@ -434,6 +483,9 @@ mod test { #[test] fn test_from_cbor_client_pin_parameters() { + // TODO(kaczmarczyck) inline the #cfg when the ICE is resolved: + // https://github.com/rust-lang/rust/issues/73663 + #[cfg(not(feature = "with_ctap2_1"))] let cbor_value = cbor_map! { 1 => 1, 2 => ClientPinSubCommand::GetPinRetries, @@ -442,6 +494,19 @@ mod test { 5 => vec! [0xCC], 6 => vec! [0xDD], }; + #[cfg(feature = "with_ctap2_1")] + let cbor_value = cbor_map! { + 1 => 1, + 2 => ClientPinSubCommand::GetPinRetries, + 3 => cbor_map!{}, + 4 => vec! [0xBB], + 5 => vec! [0xCC], + 6 => vec! [0xDD], + 7 => 4, + 8 => cbor_array!["example.com"], + 9 => 0x03, + 10 => "example.com", + }; let returned_pin_protocol_parameters = AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); @@ -452,6 +517,14 @@ mod test { pin_auth: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), + #[cfg(feature = "with_ctap2_1")] + min_pin_length: Some(4), + #[cfg(feature = "with_ctap2_1")] + min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), + #[cfg(feature = "with_ctap2_1")] + permissions: Some(0x03), + #[cfg(feature = "with_ctap2_1")] + permissions_rp_id: Some("example.com".to_string()), }; assert_eq!( diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index ed543893..fdff6e13 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -690,9 +690,15 @@ pub enum ClientPinSubCommand { GetKeyAgreement = 0x02, SetPin = 0x03, ChangePin = 0x04, - GetPinUvAuthTokenUsingPin = 0x05, - GetPinUvAuthTokenUsingUv = 0x06, + GetPinToken = 0x05, + #[cfg(feature = "with_ctap2_1")] + GetPinUvAuthTokenUsingUvWithPermissions = 0x06, + #[cfg(feature = "with_ctap2_1")] GetUvRetries = 0x07, + #[cfg(feature = "with_ctap2_1")] + SetMinPinLength = 0x08, + #[cfg(feature = "with_ctap2_1")] + GetPinUvAuthTokenUsingPinWithPermissions = 0x09, } impl From for cbor::Value { @@ -712,10 +718,16 @@ impl TryFrom for ClientPinSubCommand { 0x02 => Ok(GetKeyAgreement), 0x03 => Ok(SetPin), 0x04 => Ok(ChangePin), - 0x05 => Ok(GetPinUvAuthTokenUsingPin), - 0x06 => Ok(GetPinUvAuthTokenUsingUv), + 0x05 => Ok(GetPinToken), + #[cfg(feature = "with_ctap2_1")] + 0x06 => Ok(GetPinUvAuthTokenUsingUvWithPermissions), + #[cfg(feature = "with_ctap2_1")] 0x07 => Ok(GetUvRetries), #[cfg(feature = "with_ctap2_1")] + 0x08 => Ok(SetMinPinLength), + #[cfg(feature = "with_ctap2_1")] + 0x09 => Ok(GetPinUvAuthTokenUsingPinWithPermissions), + #[cfg(feature = "with_ctap2_1")] _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), #[cfg(not(feature = "with_ctap2_1"))] _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 027e91fa..e5bb6f25 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -849,6 +849,7 @@ where } } 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]; @@ -973,7 +974,7 @@ where Ok(()) } - fn process_get_pin_uv_auth_token_using_pin( + fn process_get_pin_token( &mut self, key_agreement: CoseKey, pin_hash_enc: Vec, @@ -1007,7 +1008,8 @@ where }) } - fn process_get_pin_uv_auth_token_using_uv( + #[cfg(feature = "with_ctap2_1")] + fn process_get_pin_uv_auth_token_using_uv_with_permissions( &self, _: CoseKey, ) -> Result { @@ -1019,6 +1021,7 @@ where }) } + #[cfg(feature = "with_ctap2_1")] fn process_get_uv_retries(&self) -> Result { // User verifications is only supported through PIN currently. Ok(AuthenticatorClientPinResponse { @@ -1028,6 +1031,37 @@ where }) } + #[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, @@ -1039,6 +1073,14 @@ where 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 { @@ -1065,18 +1107,37 @@ where )?; None } - ClientPinSubCommand::GetPinUvAuthTokenUsingPin => { - Some(self.process_get_pin_uv_auth_token_using_pin( + 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)?, - pin_hash_enc.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 } - ClientPinSubCommand::GetPinUvAuthTokenUsingUv => { - Some(self.process_get_pin_uv_auth_token_using_uv( + #[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 } - ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), }; Ok(ResponseData::AuthenticatorClientPin(response)) } From 9ff988d3a79ffefbc63b5a7acee3be25212b157d Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 24 Jun 2020 18:28:37 +0200 Subject: [PATCH 03/20] refactors the client PIN implementation into a new module --- src/ctap/command.rs | 36 +-- src/ctap/mod.rs | 484 ++------------------------------- src/ctap/pin_protocol_v1.rs | 526 ++++++++++++++++++++++++++++++++++++ src/ctap/storage.rs | 4 +- 4 files changed, 576 insertions(+), 474 deletions(-) create mode 100644 src/ctap/pin_protocol_v1.rs diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 9b966fe4..f0b08e41 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 e5bb6f25..6ea06721 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 00000000..d936774b --- /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 60dd99d4..a730cd61 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); From 033f544c47c08f5c02d8dbf5b865595f47771093 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 26 Jun 2020 11:34:29 +0200 Subject: [PATCH 04/20] adding tests to pin_protocol_v1 --- src/ctap/pin_protocol_v1.rs | 443 ++++++++++++++++++++++++++++++++---- 1 file changed, 402 insertions(+), 41 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index d936774b..442a855e 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -32,6 +32,7 @@ pub const PIN_AUTH_LENGTH: usize = 16; const PIN_PADDED_LENGTH: usize = 64; const PIN_TOKEN_LENGTH: usize = 32; +/// Checks the given pin_auth against the truncated output of HMAC-SHA256. fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { if pin_auth.len() != PIN_AUTH_LENGTH { return false; @@ -43,9 +44,9 @@ fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> boo ) } -// 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. +/// 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], @@ -100,6 +101,43 @@ fn encrypt_hmac_secret_output( Ok(encrypted_output) } +/// Checks if the decrypted PIN satisfies the PIN policy and stores it persistently. +fn check_and_store_new_pin( + 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 + // TODO(kaczmarczyck) check last byte == 0x00 + 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 +} + pub struct PinProtocolV1 { key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], @@ -117,42 +155,6 @@ impl PinProtocolV1 { } } - 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, @@ -238,7 +240,7 @@ impl PinProtocolV1 { 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) { + if !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(); @@ -270,7 +272,7 @@ impl PinProtocolV1 { 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) { + if !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(); @@ -391,7 +393,10 @@ impl PinProtocolV1 { } = client_pin_params; if pin_protocol != 1 { + #[cfg(not(feature = "with_ctap2_1"))] return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + #[cfg(feature = "with_ctap2_1")] + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let response = match sub_command { @@ -495,6 +500,362 @@ impl PinProtocolV1 { #[cfg(test)] mod test { use super::*; + use crypto::rng256::ThreadRng256; + + fn set_standard_pin(persistent_store: &mut PersistentStore) { + let mut pin = [0x00; 64]; + pin[0] = 0x31; + pin[1] = 0x32; + pin[2] = 0x33; + pin[3] = 0x34; + let mut pin_hash = [0; 16]; + pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); + persistent_store.set_pin_hash(&pin_hash); + } + + fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let mut blocks = [[0u8; 16]; 4]; + blocks[0][0] = 0x31; + blocks[0][1] = 0x32; + blocks[0][2] = 0x33; + blocks[0][3] = 0x34; + let iv = [0; 16]; + cbc_encrypt(&aes_enc_key, iv, &mut blocks); + + let mut encrypted_pin = Vec::with_capacity(64); + for b in &blocks { + encrypted_pin.extend(b); + } + encrypted_pin + } + + fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let mut pin = [0x00; 64]; + pin[0] = 0x31; + pin[1] = 0x32; + pin[2] = 0x33; + pin[3] = 0x34; + let pin_hash = Sha256::hash(&pin); + + let mut blocks = [[0u8; 16]; 1]; + blocks[0].copy_from_slice(&pin_hash[..16]); + let iv = [0; 16]; + cbc_encrypt(&aes_enc_key, iv, &mut blocks); + + let mut encrypted_pin_hash = Vec::with_capacity(16); + encrypted_pin_hash.extend(&blocks[0]); + encrypted_pin_hash + } + + #[test] + fn test_check_pin_hash_enc() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + // The PIN is "1234". + let pin_hash = [ + 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, + 0xC4, 0x12, + ]; + persistent_store.set_pin_hash(&pin_hash); + let shared_secret = [0x88; 32]; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pin_hash_enc = vec![ + 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, + 0x99, 0x66, + ]; + assert_eq!( + pin_protocol_v1.check_pin_hash_enc( + &mut rng, + &mut persistent_store, + &aes_dec_key, + pin_hash_enc + ), + Ok(()) + ); + + let pin_hash_enc = vec![0xEE; 16]; + assert_eq!( + pin_protocol_v1.check_pin_hash_enc( + &mut rng, + &mut persistent_store, + &aes_dec_key, + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + + let pin_hash_enc = vec![ + 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, + 0x99, 0x66, + ]; + pin_protocol_v1.consecutive_pin_mismatches = 3; + assert_eq!( + pin_protocol_v1.check_pin_hash_enc( + &mut rng, + &mut persistent_store, + &aes_dec_key, + pin_hash_enc.clone() + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) + ); + } + + #[test] + fn test_process_get_pin_retries() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let expected_response = Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_token: None, + retries: Some(persistent_store.pin_retries() as u64), + }); + assert_eq!( + pin_protocol_v1.process_get_pin_retries(&mut persistent_store), + expected_response + ); + } + + #[test] + fn test_process_get_key_agreement() { + let mut rng = ThreadRng256 {}; + let pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let expected_response = Ok(AuthenticatorClientPinResponse { + key_agreement: Some(CoseKey::from(pk)), + pin_token: None, + retries: None, + }); + assert_eq!( + pin_protocol_v1.process_get_key_agreement(), + expected_response + ); + } + + #[test] + fn test_process_set_pin() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement = CoseKey::from(pk); + let new_pin_enc = encrypt_standard_pin(&shared_secret); + let pin_auth = hmac_256::(&shared_secret, &new_pin_enc[..])[..16].to_vec(); + assert_eq!( + pin_protocol_v1.process_set_pin( + &mut persistent_store, + key_agreement, + pin_auth, + new_pin_enc + ), + Ok(()) + ); + } + + #[test] + fn test_process_change_pin() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement = CoseKey::from(pk); + let new_pin_enc = encrypt_standard_pin(&shared_secret); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + let mut auth_param_data = new_pin_enc.clone(); + auth_param_data.extend(&pin_hash_enc); + let pin_auth = hmac_256::(&shared_secret, &auth_param_data[..])[..16].to_vec(); + assert_eq!( + pin_protocol_v1.process_change_pin( + &mut rng, + &mut persistent_store, + key_agreement.clone(), + pin_auth.clone(), + new_pin_enc.clone(), + pin_hash_enc.clone() + ), + Ok(()) + ); + + let bad_pin_hash_enc = vec![0xEE; 16]; + assert_eq!( + pin_protocol_v1.process_change_pin( + &mut rng, + &mut persistent_store, + key_agreement.clone(), + pin_auth.clone(), + new_pin_enc.clone(), + bad_pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + + while persistent_store.pin_retries() > 0 { + persistent_store.decr_pin_retries(); + } + assert_eq!( + pin_protocol_v1.process_change_pin( + &mut rng, + &mut persistent_store, + key_agreement, + pin_auth, + new_pin_enc, + pin_hash_enc, + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) + ); + } + + #[test] + fn test_process_get_pin_token() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + assert!(pin_protocol_v1 + .process_get_pin_token( + &mut rng, + &mut persistent_store, + key_agreement.clone(), + pin_hash_enc + ) + .is_ok()); + + let pin_hash_enc = vec![0xEE; 16]; + assert_eq!( + pin_protocol_v1.process_get_pin_token( + &mut rng, + &mut persistent_store, + key_agreement, + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + fn test_process() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let client_pin_params = AuthenticatorClientPinParameters { + pin_protocol: 1, + sub_command: ClientPinSubCommand::GetPinRetries, + key_agreement: None, + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: None, + #[cfg(feature = "with_ctap2_1")] + min_pin_length: None, + #[cfg(feature = "with_ctap2_1")] + min_pin_length_rp_ids: None, + #[cfg(feature = "with_ctap2_1")] + permissions: None, + #[cfg(feature = "with_ctap2_1")] + permissions_rp_id: None, + }; + assert!(pin_protocol_v1 + .process(&mut rng, &mut persistent_store, client_pin_params) + .is_ok()); + + let client_pin_params = AuthenticatorClientPinParameters { + pin_protocol: 2, + sub_command: ClientPinSubCommand::GetPinRetries, + key_agreement: None, + pin_auth: None, + new_pin_enc: None, + pin_hash_enc: None, + #[cfg(feature = "with_ctap2_1")] + min_pin_length: None, + #[cfg(feature = "with_ctap2_1")] + min_pin_length_rp_ids: None, + #[cfg(feature = "with_ctap2_1")] + permissions: None, + #[cfg(feature = "with_ctap2_1")] + permissions_rp_id: None, + }; + #[cfg(not(feature = "with_ctap2_1"))] + let error_code = Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID; + #[cfg(feature = "with_ctap2_1")] + let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; + assert_eq!( + pin_protocol_v1.process(&mut rng, &mut persistent_store, client_pin_params), + Err(error_code) + ); + } + + #[test] + fn test_check_and_store_new_pin() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let shared_secret = [0x88; 32]; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + + // The PIN "1234" should be accepted. + let new_pin_enc = vec![ + 0xC0, 0xCF, 0xAE, 0x4C, 0x79, 0x56, 0x87, 0x99, 0xE5, 0x83, 0x4F, 0xE6, 0x4D, 0xFE, + 0x53, 0x32, 0x36, 0x0D, 0xF9, 0x1E, 0x47, 0x66, 0x10, 0x5C, 0x63, 0x30, 0x1D, 0xCC, + 0x00, 0x09, 0x91, 0xA4, 0x20, 0x6B, 0x78, 0x10, 0xFE, 0xC6, 0x2E, 0x7E, 0x75, 0x14, + 0xEE, 0x01, 0x99, 0x6C, 0xD7, 0xE5, 0x2B, 0xA5, 0x7A, 0x5A, 0xE1, 0xEC, 0x69, 0x31, + 0x18, 0x35, 0x06, 0x66, 0x97, 0x84, 0x68, 0xC2, + ]; + assert!(check_and_store_new_pin( + &mut persistent_store, + &aes_dec_key, + new_pin_enc + )); + + // The PIN "123" has only 3 characters. + let bad_pin_enc = vec![ + 0xF3, 0x54, 0x29, 0x17, 0xD4, 0xF8, 0xCD, 0x23, 0x1D, 0x59, 0xED, 0xE5, 0x33, 0x42, + 0x13, 0x39, 0x22, 0xBB, 0x91, 0x28, 0x87, 0x6A, 0xF9, 0xB1, 0x80, 0x9C, 0x9D, 0x76, + 0xFF, 0xDD, 0xB8, 0xD6, 0x8D, 0x66, 0x99, 0xA2, 0x42, 0x67, 0xB0, 0x5C, 0x82, 0x3F, + 0x08, 0x55, 0x8C, 0x04, 0xC5, 0x91, 0xF0, 0xF9, 0x58, 0x44, 0x00, 0x1B, 0x99, 0xA6, + 0x7C, 0xC7, 0x2D, 0x43, 0x74, 0x4C, 0x1D, 0x7E, + ]; + assert!(!check_and_store_new_pin( + &mut persistent_store, + &aes_dec_key, + bad_pin_enc + )); + + // The last byte of the decrypted PIN is not padding, which is 0x00. + let bad_pin_enc = vec![ + 0x53, 0x3D, 0xAD, 0x69, 0xB6, 0x1B, 0x5F, 0xAF, 0x0F, 0x26, 0xF1, 0x33, 0xB3, 0xCC, + 0x94, 0x26, 0x68, 0xD0, 0xC4, 0x58, 0xD4, 0x2D, 0x3D, 0x8B, 0x6F, 0x1A, 0xA2, 0x0A, + 0x44, 0x47, 0xE8, 0x94, 0xF2, 0x2D, 0x99, 0xEB, 0xA1, 0xA6, 0xBE, 0x32, 0x7C, 0x99, + 0x2B, 0xB8, 0x9A, 0x15, 0x9C, 0xEA, 0x86, 0x47, 0x4B, 0x5E, 0x6C, 0xA2, 0xE2, 0xB9, + 0x0D, 0x85, 0x25, 0xD3, 0x8A, 0x46, 0x39, 0xAD, + ]; + assert!(!check_and_store_new_pin( + &mut persistent_store, + &aes_dec_key, + bad_pin_enc + )); + } + + #[test] + fn test_check_pin_auth() { + let hmac_key = [0x88; 16]; + let pin_auth = [ + 0x88, 0x09, 0x41, 0x13, 0xF7, 0x97, 0x32, 0x0B, 0x3E, 0xD9, 0xBC, 0x76, 0x4F, 0x18, + 0x56, 0x5D, + ]; + assert!(check_pin_auth(&hmac_key, &[], &pin_auth)); + assert!(!check_pin_auth(&hmac_key, &[0x00], &pin_auth)); + } #[test] fn test_encrypt_hmac_secret_output() { From 26595db8102cb35819907a235a5690ddc3df9a85 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 26 Jun 2020 14:30:27 +0200 Subject: [PATCH 05/20] adds new client Pin subcommand minPinLength implementation --- README.md | 3 + src/ctap/command.rs | 7 +- src/ctap/mod.rs | 6 +- src/ctap/pin_protocol_v1.rs | 128 +++++++++++++++---- src/ctap/response.rs | 15 +++ src/ctap/storage.rs | 248 ++++++++++++++++++++++++++++++------ 6 files changed, 337 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 6e68ba1d..8e317968 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,9 @@ a few things you can personalize: When changing the default, resident credentials become undiscoverable without user verification. This helps privacy, but can make usage less comfortable for credentials that need less protection. +6. Increase the default minimum length for PINs in `ctap/storage.rs`. + The current minimum is 4. Values from 4 to 63 are allowed. + You can add relying parties to the list of readers of the minimum PIN length. ### 3D printed enclosure diff --git a/src/ctap/command.rs b/src/ctap/command.rs index f0b08e41..c541f1f8 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -279,7 +279,7 @@ pub struct AuthenticatorClientPinParameters { pub new_pin_enc: Option>, pub pin_hash_enc: Option>, #[cfg(feature = "with_ctap2_1")] - pub min_pin_length: Option, + pub min_pin_length: Option, #[cfg(feature = "with_ctap2_1")] pub min_pin_length_rp_ids: Option>, #[cfg(feature = "with_ctap2_1")] @@ -329,7 +329,10 @@ impl TryFrom for AuthenticatorClientPinParameters { let new_pin_enc = new_pin_enc.map(extract_byte_string).transpose()?; let pin_hash_enc = pin_hash_enc.map(extract_byte_string).transpose()?; #[cfg(feature = "with_ctap2_1")] - let min_pin_length = min_pin_length.map(extract_unsigned).transpose()?; + let min_pin_length = min_pin_length + .map(extract_unsigned) + .transpose()? + .map(|m| m as u8); #[cfg(feature = "with_ctap2_1")] let min_pin_length_rp_ids = match min_pin_length_rp_ids { Some(entry) => Some( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6ea06721..2f072eec 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -730,6 +730,8 @@ where algorithms: Some(vec![ES256_CRED_PARAM]), default_cred_protect: DEFAULT_CRED_PROTECT, #[cfg(feature = "with_ctap2_1")] + min_pin_length: self.persistent_store.min_pin_length(), + #[cfg(feature = "with_ctap2_1")] firmware_version: None, }, )) @@ -812,7 +814,7 @@ mod test { let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID); #[cfg(feature = "with_ctap2_1")] - let mut expected_response = vec![0x00, 0xA8, 0x01]; + let mut expected_response = vec![0x00, 0xA9, 0x01]; #[cfg(not(feature = "with_ctap2_1"))] let mut expected_response = vec![0x00, 0xA6, 0x01]; // The difference here is a longer array of supported versions. @@ -837,7 +839,7 @@ mod test { [ 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, - 0x65, 0x79, + 0x65, 0x79, 0x0D, 0x04, ] .iter(), ); diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 442a855e..560e6d1d 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -17,6 +17,7 @@ use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretIn use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +#[cfg(feature = "with_ctap2_1")] use alloc::string::String; use alloc::vec::Vec; use core::convert::TryInto; @@ -101,7 +102,6 @@ fn encrypt_hmac_secret_output( Ok(encrypted_output) } -/// Checks if the decrypted PIN satisfies the PIN policy and stores it persistently. fn check_and_store_new_pin( persistent_store: &mut PersistentStore, aes_dec_key: &crypto::aes256::DecryptionKey, @@ -127,7 +127,11 @@ fn check_and_store_new_pin( } } } - if pin.len() < 4 || pin.len() == PIN_PADDED_LENGTH { + #[cfg(feature = "with_ctap2_1")] + let min_pin_length = persistent_store.min_pin_length() as usize; + #[cfg(not(feature = "with_ctap2_1"))] + let min_pin_length = 4; + if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH { // TODO(kaczmarczyck) check 4 code point minimum instead // TODO(kaczmarczyck) check last byte == 0x00 return false; @@ -141,7 +145,7 @@ fn check_and_store_new_pin( pub struct PinProtocolV1 { key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], - consecutive_pin_mismatches: u64, + consecutive_pin_mismatches: u8, } impl PinProtocolV1 { @@ -341,31 +345,69 @@ impl PinProtocolV1 { #[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), - }) + persistent_store: &mut PersistentStore, + min_pin_length: u8, + min_pin_length_rp_ids: Option>, + pin_auth: Option>, + ) -> Result<(), Ctap2StatusCode> { + if min_pin_length_rp_ids.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + } + if persistent_store.pin_hash().is_some() { + match pin_auth { + Some(pin_auth) => { + // TODO(kaczmarczyck) not mentioned, but maybe useful? + // if persistent_store.pin_retries() == 0 { + // return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); + // } + if self.consecutive_pin_mismatches >= 3 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); + } + let mut message = vec![0xFF; 32]; + message.extend(&[0x06, 0x08]); + message.extend(&[min_pin_length as u8, 0x00, 0x00, 0x00]); + // TODO(kaczmarczyck) commented code is useful for the extension + // if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) { + // return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + // } + if !check_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + } + None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + }; + } + if min_pin_length < persistent_store.min_pin_length() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); + } + persistent_store.set_min_pin_length(min_pin_length); + // if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids { + // persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?; + // } + Ok(()) } #[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, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + key_agreement: CoseKey, + pin_hash_enc: Vec, + permissions: u8, + _permissions_rp_id: Option, ) -> Result { + if permissions == 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + + // TODO(kaczmarczyck) split the implementation to omit the unnecessary token generation + self.process_get_pin_token(rng, persistent_store, key_agreement, pin_hash_enc)?; // TODO Ok(AuthenticatorClientPinResponse { key_agreement: None, pin_token: None, - retries: Some(0), + retries: None, }) } @@ -441,22 +483,24 @@ impl PinProtocolV1 { #[cfg(feature = "with_ctap2_1")] ClientPinSubCommand::SetMinPinLength => { self.process_set_min_pin_length( + persistent_store, 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)?, + min_pin_length_rp_ids, + pin_auth, )?; None } #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => { + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( self.process_get_pin_uv_auth_token_using_pin_with_permissions( + rng, + persistent_store, 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 - } + permissions_rp_id, + )?, + ), }; Ok(ResponseData::AuthenticatorClientPin(response)) } @@ -744,6 +788,40 @@ mod test { ); } + #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_process_set_min_pin_length() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let min_pin_length = 8; + pin_protocol_v1.pin_uv_auth_token = [0x55; PIN_TOKEN_LENGTH]; + let pin_auth = vec![ + 0x94, 0x86, 0xEF, 0x4C, 0xB3, 0x84, 0x2C, 0x85, 0x72, 0x02, 0xBF, 0xE4, 0x36, 0x22, + 0xFE, 0xC9, + ]; + // TODO(kaczmarczyck) implement test for the min PIN length extension + let response = pin_protocol_v1.process_set_min_pin_length( + &mut persistent_store, + min_pin_length, + None, + Some(pin_auth.clone()), + ); + assert_eq!(response, Ok(())); + assert_eq!(persistent_store.min_pin_length(), min_pin_length); + let response = pin_protocol_v1.process_set_min_pin_length( + &mut persistent_store, + 7, + None, + Some(pin_auth), + ); + assert_eq!( + response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION) + ); + assert_eq!(persistent_store.min_pin_length(), min_pin_length); + } + #[test] fn test_process() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 0a4548d6..bbd591d6 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -125,6 +125,8 @@ pub struct AuthenticatorGetInfoResponse { pub algorithms: Option>, pub default_cred_protect: Option, #[cfg(feature = "with_ctap2_1")] + pub min_pin_length: u8, + #[cfg(feature = "with_ctap2_1")] pub firmware_version: Option, } @@ -143,6 +145,7 @@ impl From for cbor::Value { transports, algorithms, default_cred_protect, + min_pin_length, firmware_version, } = get_info_response; @@ -166,6 +169,7 @@ impl From for cbor::Value { 0x09 => transports.map(|vec| cbor_array_vec!(vec)), 0x0A => algorithms.map(|vec| cbor_array_vec!(vec)), 0x0C => default_cred_protect.map(|p| p as u64), + 0x0D => min_pin_length as u64, 0x0E => firmware_version, } } @@ -301,13 +305,22 @@ mod test { algorithms: None, default_cred_protect: None, #[cfg(feature = "with_ctap2_1")] + min_pin_length: 4, + #[cfg(feature = "with_ctap2_1")] firmware_version: None, }; let response_cbor: Option = ResponseData::AuthenticatorGetInfo(get_info_response).into(); + #[cfg(not(feature = "with_ctap2_1"))] + let expected_cbor = cbor_map_options! { + 0x01 => cbor_array_vec![vec!["FIDO_2_0"]], + 0x03 => vec![0x00; 16], + }; + #[cfg(feature = "with_ctap2_1")] let expected_cbor = cbor_map_options! { 0x01 => cbor_array_vec![vec!["FIDO_2_0"]], 0x03 => vec![0x00; 16], + 0x0D => 4, }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -329,6 +342,7 @@ mod test { transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), + min_pin_length: 4, firmware_version: Some(0), }; let response_cbor: Option = @@ -345,6 +359,7 @@ mod test { 0x09 => cbor_array_vec![vec!["usb"]], 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, + 0x0D => 4, 0x0E => 0, }; assert_eq!(response_cbor, Some(expected_cbor)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a730cd61..b4265a87 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -13,6 +13,8 @@ // limitations under the License. use crate::crypto::rng256::Rng256; +#[cfg(feature = "with_ctap2_1")] +use crate::ctap::data_formats::{extract_array, extract_text_string}; use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource}; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; @@ -20,7 +22,7 @@ use crate::ctap::{key_material, USE_BATCH_ATTESTATION}; use alloc::string::String; use alloc::vec::Vec; use core::convert::TryInto; -use ctap2::embedded_flash::{self, StoreConfig, StoreEntry, StoreError, StoreIndex}; +use ctap2::embedded_flash::{self, StoreConfig, StoreEntry, StoreError}; #[cfg(any(test, feature = "ram_storage"))] type Storage = embedded_flash::BufferStorage; @@ -60,11 +62,25 @@ const PIN_RETRIES: usize = 4; const ATTESTATION_PRIVATE_KEY: usize = 5; const ATTESTATION_CERTIFICATE: usize = 6; const AAGUID: usize = 7; +#[cfg(not(feature = "with_ctap2_1"))] const NUM_TAGS: usize = 8; +#[cfg(feature = "with_ctap2_1")] +const MIN_PIN_LENGTH: usize = 8; +#[cfg(feature = "with_ctap2_1")] +const MIN_PIN_LENGTH_RP_IDS: usize = 9; +#[cfg(feature = "with_ctap2_1")] +const NUM_TAGS: usize = 10; const MAX_PIN_RETRIES: u8 = 6; const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; const AAGUID_LENGTH: usize = 16; +#[cfg(feature = "with_ctap2_1")] +const DEFAULT_MIN_PIN_LENGTH: u8 = 4; +#[cfg(feature = "with_ctap2_1")] +const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); +// TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly. +#[cfg(feature = "with_ctap2_1")] +const _MAX_RP_IDS_LENGTH: usize = 8; #[derive(PartialEq, Eq, PartialOrd, Ord)] enum Key { @@ -82,6 +98,10 @@ enum Key { AttestationPrivateKey, AttestationCertificate, Aaguid, + #[cfg(feature = "with_ctap2_1")] + MinPinLength, + #[cfg(feature = "with_ctap2_1")] + MinPinLengthRpIds, } pub struct MasterKeys<'a> { @@ -136,6 +156,10 @@ impl StoreConfig for Config { ATTESTATION_PRIVATE_KEY => add(Key::AttestationPrivateKey), ATTESTATION_CERTIFICATE => add(Key::AttestationCertificate), AAGUID => add(Key::Aaguid), + #[cfg(feature = "with_ctap2_1")] + MIN_PIN_LENGTH => add(Key::MinPinLength), + #[cfg(feature = "with_ctap2_1")] + MIN_PIN_LENGTH_RP_IDS => add(Key::MinPinLengthRpIds), _ => debug_assert!(false), } } @@ -200,15 +224,6 @@ impl PersistentStore { }) .unwrap(); } - if self.store.find_one(&Key::PinRetries).is_none() { - self.store - .insert(StoreEntry { - tag: PIN_RETRIES, - data: &[MAX_PIN_RETRIES], - sensitive: false, - }) - .unwrap(); - } // The following 3 entries are meant to be written by vendor-specific commands. if USE_BATCH_ATTESTATION { if self.store.find_one(&Key::AttestationPrivateKey).is_none() { @@ -381,44 +396,110 @@ impl PersistentStore { } } - fn pin_retries_entry(&self) -> (StoreIndex, u8) { - let (index, entry) = self.store.find_one(&Key::PinRetries).unwrap(); - let data = entry.data; - debug_assert_eq!(data.len(), 1); - (index, data[0]) - } - pub fn pin_retries(&self) -> u8 { - self.pin_retries_entry().1 + self.store + .find_one(&Key::PinRetries) + .map_or(MAX_PIN_RETRIES, |(_, entry)| entry.data[0]) } pub fn decr_pin_retries(&mut self) { - let (index, old_value) = self.pin_retries_entry(); - let new_value = old_value.saturating_sub(1); - self.store - .replace( - index, - StoreEntry { - tag: PIN_RETRIES, - data: &[new_value], - sensitive: false, - }, - ) - .unwrap(); + match self.store.find_one(&Key::PinRetries) { + None => { + self.store + .insert(StoreEntry { + tag: PIN_RETRIES, + data: &[MAX_PIN_RETRIES.saturating_sub(1)], + sensitive: false, + }) + .unwrap(); + } + Some((index, entry)) => { + debug_assert_eq!(entry.data.len(), 1); + let new_value = entry.data[0].saturating_sub(1); + self.store + .replace( + index, + StoreEntry { + tag: PIN_RETRIES, + data: &[new_value], + sensitive: false, + }, + ) + .unwrap(); + } + } } pub fn reset_pin_retries(&mut self) { - let (index, _) = self.pin_retries_entry(); + if let Some((index, _)) = self.store.find_one(&Key::PinRetries) { + self.store.delete(index).unwrap(); + } + } + + #[cfg(feature = "with_ctap2_1")] + pub fn min_pin_length(&self) -> u8 { self.store - .replace( - index, - StoreEntry { - tag: PIN_RETRIES, - data: &[MAX_PIN_RETRIES], - sensitive: false, - }, - ) - .unwrap(); + .find_one(&Key::MinPinLength) + .map_or(DEFAULT_MIN_PIN_LENGTH, |(_, entry)| entry.data[0]) + } + + #[cfg(feature = "with_ctap2_1")] + pub fn set_min_pin_length(&mut self, min_pin_length: u8) { + let entry = StoreEntry { + tag: MIN_PIN_LENGTH, + data: &[min_pin_length], + sensitive: false, + }; + match self.store.find_one(&Key::MinPinLength) { + None => { + self.store.insert(entry).unwrap(); + } + Some((index, _)) => { + self.store.replace(index, entry).unwrap(); + } + } + } + + #[cfg(feature = "with_ctap2_1")] + pub fn _min_pin_length_rp_ids(&self) -> Vec { + let rp_ids = self + .store + .find_one(&Key::MinPinLengthRpIds) + .map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |(_, entry)| { + _deserialize_min_pin_length_rp_ids(entry.data) + }); + debug_assert!(rp_ids.is_some()); + rp_ids.unwrap_or(vec![]) + } + + #[cfg(feature = "with_ctap2_1")] + pub fn _set_min_pin_length_rp_ids( + &mut self, + min_pin_length_rp_ids: Vec, + ) -> Result<(), Ctap2StatusCode> { + let mut min_pin_length_rp_ids = min_pin_length_rp_ids; + for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { + if !min_pin_length_rp_ids.contains(&rp_id) { + min_pin_length_rp_ids.push(rp_id); + } + } + if min_pin_length_rp_ids.len() > _MAX_RP_IDS_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); + } + let entry = StoreEntry { + tag: MIN_PIN_LENGTH_RP_IDS, + data: &_serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, + sensitive: false, + }; + match self.store.find_one(&Key::MinPinLengthRpIds) { + None => { + self.store.insert(entry).unwrap(); + } + Some((index, _)) => { + self.store.replace(index, entry).unwrap(); + } + } + Ok(()) } pub fn attestation_private_key( @@ -541,7 +622,28 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result if cbor::write(credential.into(), &mut data) { Ok(data) } else { - Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CREDENTIAL) + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) + } +} + +#[cfg(feature = "with_ctap2_1")] +fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { + let cbor = cbor::read(data).ok()?; + extract_array(cbor) + .ok()? + .into_iter() + .map(extract_text_string) + .collect::, Ctap2StatusCode>>() + .ok() +} + +#[cfg(feature = "with_ctap2_1")] +fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { + let mut data = Vec::new(); + if cbor::write(cbor_array_vec!(rp_ids), &mut data) { + Ok(data) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) } } @@ -892,4 +994,68 @@ mod test { ); assert_eq!(persistent_store.aaguid().unwrap(), key_material::AAGUID); } + + #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_min_pin_length() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + // The minimum PIN lenght is initially at the default. + assert_eq!(persistent_store.min_pin_length(), DEFAULT_MIN_PIN_LENGTH); + + // Changes by the setter are reflected by the getter.. + let new_min_pin_length = 8; + persistent_store.set_min_pin_length(new_min_pin_length); + assert_eq!(persistent_store.min_pin_length(), new_min_pin_length); + } + + #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_min_pin_length_rp_ids() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + // The minimum PIN lenght is initially at the default. + assert_eq!( + persistent_store._min_pin_length_rp_ids(), + _DEFAULT_MIN_PIN_LENGTH_RP_IDS + ); + + // Changes by the setter are reflected by the getter.. + let rp_ids = vec![String::from("example.com")]; + assert_eq!( + persistent_store._set_min_pin_length_rp_ids(rp_ids.clone()), + Ok(()) + ); + assert_eq!(persistent_store._min_pin_length_rp_ids(), rp_ids); + } + + #[test] + fn test_serialize_deserialize_credential() { + let mut rng = ThreadRng256 {}; + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id: rng.gen_uniform_u8x32().to_vec(), + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x00], + other_ui: None, + cred_random: None, + cred_protect_policy: None, + }; + let serialized = serialize_credential(credential.clone()).unwrap(); + let reconstructed = deserialize_credential(&serialized).unwrap(); + assert_eq!(credential, reconstructed); + } + + #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_serialize_deserialize_min_pin_length_rp_ids() { + let rp_ids = vec![String::from("example.com")]; + let serialized = _serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap(); + let reconstructed = _deserialize_min_pin_length_rp_ids(&serialized).unwrap(); + assert_eq!(rp_ids, reconstructed); + } } From 216a6a0f6eb501680cee1582c3e99efb8804f546 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 2 Jul 2020 19:11:49 +0200 Subject: [PATCH 06/20] adds permissions and adapts clientPin 2.1 subcommands --- src/ctap/mod.rs | 5 + src/ctap/pin_protocol_v1.rs | 178 ++++++++++++++++++++++++++++++++---- 2 files changed, 163 insertions(+), 20 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 2f072eec..6fe9b7a9 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -413,6 +413,9 @@ where { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } + #[cfg(feature = "with_ctap2_1")] + self.pin_protocol_v1 + .has_make_credential_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -591,6 +594,8 @@ where { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } + #[cfg(feature = "with_ctap2_1")] + self.pin_protocol_v1.has_get_assertion_permission(&rp_id)?; UV_FLAG } None => { diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 560e6d1d..754d4521 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -146,6 +146,10 @@ pub struct PinProtocolV1 { key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], consecutive_pin_mismatches: u8, + #[cfg(feature = "with_ctap2_1")] + permissions: u8, + #[cfg(feature = "with_ctap2_1")] + permissions_rp_id: Option, } impl PinProtocolV1 { @@ -156,6 +160,10 @@ impl PinProtocolV1 { key_agreement_key, pin_uv_auth_token, consecutive_pin_mismatches: 0, + #[cfg(feature = "with_ctap2_1")] + permissions: 0, + #[cfg(feature = "with_ctap2_1")] + permissions_rp_id: None, } } @@ -312,6 +320,12 @@ impl PinProtocolV1 { pin_token.extend(item); } + #[cfg(feature = "with_ctap2_1")] + { + self.permissions = 0x03; + self.permissions_rp_id = None; + } + Ok(AuthenticatorClientPinResponse { key_agreement: None, pin_token: Some(pin_token), @@ -324,22 +338,28 @@ impl PinProtocolV1 { &self, _: CoseKey, ) -> Result { - Ok(AuthenticatorClientPinResponse { - // User verifications is only supported through PIN currently. - key_agreement: None, - pin_token: Some(vec![]), - retries: None, - }) + // User verifications is only supported through PIN currently. + #[cfg(not(feature = "with_ctap2_1"))] + { + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) + } + #[cfg(feature = "with_ctap2_1")] + { + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) + } } #[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(not(feature = "with_ctap2_1"))] + { + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) + } + #[cfg(feature = "with_ctap2_1")] + { + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) + } } #[cfg(feature = "with_ctap2_1")] @@ -395,20 +415,23 @@ impl PinProtocolV1 { key_agreement: CoseKey, pin_hash_enc: Vec, permissions: u8, - _permissions_rp_id: Option, + permissions_rp_id: Option, ) -> Result { 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); + } - // TODO(kaczmarczyck) split the implementation to omit the unnecessary token generation - self.process_get_pin_token(rng, persistent_store, key_agreement, pin_hash_enc)?; - // TODO - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: None, - retries: None, - }) + let response = + self.process_get_pin_token(rng, persistent_store, key_agreement, pin_hash_enc)?; + + self.permissions = permissions; + self.permissions_rp_id = permissions_rp_id; + + Ok(response) } pub fn process( @@ -539,6 +562,34 @@ impl PinProtocolV1 { None => Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION), } } + + #[cfg(feature = "with_ctap2_1")] + pub fn has_make_credential_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + self.has_permission(0x01, rp_id) + } + + #[cfg(feature = "with_ctap2_1")] + pub fn has_get_assertion_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + self.has_permission(0x02, rp_id) + } + + // TODO(kaczmarczyck) use permissons for new commands + + #[cfg(feature = "with_ctap2_1")] + fn has_permission(&mut self, bitmask: u8, rp_id: &str) -> Result<(), Ctap2StatusCode> { + if let Some(permissions_rp_id) = &self.permissions_rp_id { + if rp_id != permissions_rp_id { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + } else { + self.permissions_rp_id = Some(String::from(rp_id)); + } + if self.permissions & bitmask != 0 { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } + } } #[cfg(test)] @@ -788,6 +839,71 @@ mod test { ); } + #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let pk = pin_protocol_v1.key_agreement_key.genpk(); + let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + assert!(pin_protocol_v1 + .process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut rng, + &mut persistent_store, + key_agreement.clone(), + pin_hash_enc.clone(), + 0x03, + Some(String::from("example.com")), + ) + .is_ok()); + assert_eq!(pin_protocol_v1.permissions, 0x03); + assert_eq!( + pin_protocol_v1.permissions_rp_id, + Some(String::from("example.com")) + ); + + assert_eq!( + pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut rng, + &mut persistent_store, + key_agreement.clone(), + pin_hash_enc.clone(), + 0x00, + Some(String::from("example.com")), + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + assert_eq!( + pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut rng, + &mut persistent_store, + key_agreement.clone(), + pin_hash_enc.clone(), + 0x03, + None, + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + let pin_hash_enc = vec![0xEE; 16]; + assert_eq!( + pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut rng, + &mut persistent_store, + key_agreement, + pin_hash_enc, + 0x03, + Some(String::from("example.com")), + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + #[cfg(feature = "with_ctap2_1")] #[test] fn test_process_set_min_pin_length() { @@ -962,4 +1078,26 @@ mod test { Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) ); } + + #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_has_permission() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + pin_protocol_v1.permissions = 0x03; + assert_eq!(pin_protocol_v1.has_permission(0x01, "example.com"), Ok(())); + assert_eq!( + pin_protocol_v1.permissions_rp_id, + Some(String::from("example.com")) + ); + assert_eq!(pin_protocol_v1.has_permission(0x01, "example.com"), Ok(())); + assert_eq!( + pin_protocol_v1.has_permission(0x01, "counter-example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + pin_protocol_v1.has_permission(0x04, "example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } } From 3b6615520f3c11a662de3304f8025a202e6ce9c9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 6 Jul 2020 10:22:22 +0200 Subject: [PATCH 07/20] adds clarifications, improvements and tests --- src/ctap/data_formats.rs | 27 +++++++++++++++++++++ src/ctap/pin_protocol_v1.rs | 5 ++++ src/ctap/storage.rs | 47 ++++++++++++++++++++++--------------- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index fdff6e13..b07cdf06 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -1166,6 +1166,13 @@ mod test { let policy_error = CredentialProtectionPolicy::try_from(cbor_policy_error); let expected_error = Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE); assert_eq!(policy_error, expected_error); + + for policy_literal in 1..=3 { + let cbor_policy: cbor::Value = cbor_int!(policy_literal); + let policy = CredentialProtectionPolicy::try_from(cbor_policy.clone()); + let created_cbor: cbor::Value = policy.unwrap().into(); + assert_eq!(created_cbor, cbor_policy); + } } #[test] @@ -1180,6 +1187,15 @@ mod test { ); let created_cbor: cbor::Value = authenticator_transport.unwrap().into(); assert_eq!(created_cbor, cbor_authenticator_transport); + + let transports = ["usb", "nfc", "ble", "internal"]; + for transport in transports.iter() { + let cbor_authenticator_transport: cbor::Value = cbor_text!(*transport); + let authenticator_transport = + AuthenticatorTransport::try_from(cbor_authenticator_transport.clone()); + let created_cbor: cbor::Value = authenticator_transport.unwrap().into(); + assert_eq!(created_cbor, cbor_authenticator_transport); + } } #[test] @@ -1322,6 +1338,17 @@ mod test { assert_eq!(sub_command, Ok(expected_sub_command)); let created_cbor: cbor::Value = sub_command.unwrap().into(); assert_eq!(created_cbor, cbor_sub_command); + + #[cfg(not(feature = "with_ctap2_1"))] + let last_literal = 0x05; + #[cfg(feature = "with_ctap2_1")] + let last_literal = 0x09; + for command_literal in 1..=last_literal { + let cbor_sub_command: cbor::Value = cbor_int!(command_literal); + let sub_command = ClientPinSubCommand::try_from(cbor_sub_command.clone()); + let created_cbor: cbor::Value = sub_command.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command); + } } #[test] diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 754d4521..3f949802 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -536,6 +536,11 @@ impl PinProtocolV1 { self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); self.pin_uv_auth_token = rng.gen_uniform_u8x32(); self.consecutive_pin_mismatches = 0; + #[cfg(feature = "with_ctap2_1")] + { + self.permissions = 0; + self.permissions_rp_id = None; + } } pub fn process_hmac_secret( diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b4265a87..fffd80fe 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -62,13 +62,11 @@ const PIN_RETRIES: usize = 4; const ATTESTATION_PRIVATE_KEY: usize = 5; const ATTESTATION_CERTIFICATE: usize = 6; const AAGUID: usize = 7; -#[cfg(not(feature = "with_ctap2_1"))] -const NUM_TAGS: usize = 8; #[cfg(feature = "with_ctap2_1")] const MIN_PIN_LENGTH: usize = 8; #[cfg(feature = "with_ctap2_1")] const MIN_PIN_LENGTH_RP_IDS: usize = 9; -#[cfg(feature = "with_ctap2_1")] +// Different NUM_TAGS make the storage incompatible, so we use max(8,10). const NUM_TAGS: usize = 10; const MAX_PIN_RETRIES: u8 = 6; @@ -397,9 +395,13 @@ impl PersistentStore { } pub fn pin_retries(&self) -> u8 { - self.store - .find_one(&Key::PinRetries) - .map_or(MAX_PIN_RETRIES, |(_, entry)| entry.data[0]) + match self.store.find_one(&Key::PinRetries) { + None => MAX_PIN_RETRIES, + Some((_, entry)) => { + debug_assert_eq!(entry.data.len(), 1); + entry.data[0] + } + } } pub fn decr_pin_retries(&mut self) { @@ -415,17 +417,19 @@ impl PersistentStore { } Some((index, entry)) => { debug_assert_eq!(entry.data.len(), 1); - let new_value = entry.data[0].saturating_sub(1); - self.store - .replace( - index, - StoreEntry { - tag: PIN_RETRIES, - data: &[new_value], - sensitive: false, - }, - ) - .unwrap(); + if entry.data[0] > 0 { + let new_value = entry.data[0].saturating_sub(1); + self.store + .replace( + index, + StoreEntry { + tag: PIN_RETRIES, + data: &[new_value], + sensitive: false, + }, + ) + .unwrap(); + } } } } @@ -1022,12 +1026,17 @@ mod test { _DEFAULT_MIN_PIN_LENGTH_RP_IDS ); - // Changes by the setter are reflected by the getter.. - let rp_ids = vec![String::from("example.com")]; + // Changes by the setter are reflected by the getter. + let mut rp_ids = vec![String::from("example.com")]; assert_eq!( persistent_store._set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); + for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { + if !rp_ids.contains(&rp_id) { + rp_ids.push(rp_id); + } + } assert_eq!(persistent_store._min_pin_length_rp_ids(), rp_ids); } From 04278d91d84fa4cedc0126ac2861fa3497f9f0d6 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 8 Jul 2020 16:17:15 +0200 Subject: [PATCH 08/20] adds code style improvements, including a new enum for permissions --- src/ctap/command.rs | 4 +- src/ctap/mod.rs | 15 +++- src/ctap/pin_protocol_v1.rs | 142 +++++++++++++++++++++++++++--------- src/ctap/response.rs | 7 +- src/ctap/storage.rs | 28 +++---- 5 files changed, 140 insertions(+), 56 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index c541f1f8..c3627f78 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -332,7 +332,9 @@ impl TryFrom for AuthenticatorClientPinParameters { let min_pin_length = min_pin_length .map(extract_unsigned) .transpose()? - .map(|m| m as u8); + .map(u8::try_from) + .transpose() + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; #[cfg(feature = "with_ctap2_1")] let min_pin_length_rp_ids = match min_pin_length_rp_ids { Some(entry) => Some( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6fe9b7a9..6df8ad28 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -38,6 +38,8 @@ use self::data_formats::{ PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; +#[cfg(feature = "with_ctap2_1")] +use self::pin_protocol_v1::PinPermission; use self::pin_protocol_v1::PinProtocolV1; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, @@ -414,8 +416,11 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } #[cfg(feature = "with_ctap2_1")] - self.pin_protocol_v1 - .has_make_credential_permission(&rp_id)?; + { + self.pin_protocol_v1 + .has_permission(PinPermission::MakeCredential)?; + self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; + } UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -595,7 +600,11 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } #[cfg(feature = "with_ctap2_1")] - self.pin_protocol_v1.has_get_assertion_permission(&rp_id)?; + { + self.pin_protocol_v1 + .has_permission(PinPermission::GetAssertion)?; + self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; + } UV_FLAG } None => { diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 3f949802..32ca1804 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -63,9 +63,9 @@ fn encrypt_hmac_secret_output( 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 iv = [0u8; 16]; - let mut cred_random_secret = [0; 32]; + let mut cred_random_secret = [0u8; 32]; cred_random_secret.clone_from_slice(cred_random); // Initialization of 4 blocks in any case makes this function more readable. @@ -76,7 +76,7 @@ fn encrypt_hmac_secret_output( } cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]); - let mut decrypted_salt1 = [0; 32]; + let mut decrypted_salt1 = [0u8; 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]); @@ -85,7 +85,7 @@ fn encrypt_hmac_secret_output( } if block_len == 4 { - let mut decrypted_salt2 = [0; 32]; + let mut decrypted_salt2 = [0u8; 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[..]); @@ -110,7 +110,7 @@ fn check_and_store_new_pin( if new_pin_enc.len() != PIN_PADDED_LENGTH { return false; } - let iv = [0; 16]; + let iv = [0u8; 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 { @@ -136,12 +136,31 @@ fn check_and_store_new_pin( // TODO(kaczmarczyck) check last byte == 0x00 return false; } - let mut pin_hash = [0; 16]; + let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); persistent_store.set_pin_hash(&pin_hash); true } +#[cfg(feature = "with_ctap2_1")] +// TODO remove when all variants are used +#[allow(dead_code)] +pub enum PinPermission { + MakeCredential = 0x01, + GetAssertion = 0x02, + CredentialManagement = 0x04, + BioEnrollment = 0x08, + PlatformConfiguration = 0x10, + AuthenticatorConfiguration = 0x20, +} + +#[cfg(feature = "with_ctap2_1")] +impl PinPermission { + pub fn check(self, stored_bits: u8) -> bool { + self as u8 & stored_bits != 0 + } +} + pub struct PinProtocolV1 { key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], @@ -187,7 +206,7 @@ impl PinProtocolV1 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - let iv = [0; 16]; + let iv = [0u8; 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); @@ -309,7 +328,7 @@ impl PinProtocolV1 { 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 iv = [0u8; 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]); @@ -569,19 +588,16 @@ impl PinProtocolV1 { } #[cfg(feature = "with_ctap2_1")] - pub fn has_make_credential_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - self.has_permission(0x01, rp_id) - } - - #[cfg(feature = "with_ctap2_1")] - pub fn has_get_assertion_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { - self.has_permission(0x02, rp_id) + pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { + if permission.check(self.permissions) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } } - // TODO(kaczmarczyck) use permissons for new commands - #[cfg(feature = "with_ctap2_1")] - fn has_permission(&mut self, bitmask: u8, rp_id: &str) -> Result<(), Ctap2StatusCode> { + pub fn has_permission_for_rp_id(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { if let Some(permissions_rp_id) = &self.permissions_rp_id { if rp_id != permissions_rp_id { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); @@ -589,11 +605,7 @@ impl PinProtocolV1 { } else { self.permissions_rp_id = Some(String::from(rp_id)); } - if self.permissions & bitmask != 0 { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - } + Ok(()) } } @@ -603,12 +615,12 @@ mod test { use crypto::rng256::ThreadRng256; fn set_standard_pin(persistent_store: &mut PersistentStore) { - let mut pin = [0x00; 64]; + let mut pin = [0u8; 64]; pin[0] = 0x31; pin[1] = 0x32; pin[2] = 0x33; pin[3] = 0x34; - let mut pin_hash = [0; 16]; + let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); persistent_store.set_pin_hash(&pin_hash); } @@ -620,7 +632,7 @@ mod test { blocks[0][1] = 0x32; blocks[0][2] = 0x33; blocks[0][3] = 0x34; - let iv = [0; 16]; + let iv = [0u8; 16]; cbc_encrypt(&aes_enc_key, iv, &mut blocks); let mut encrypted_pin = Vec::with_capacity(64); @@ -632,7 +644,7 @@ mod test { fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let mut pin = [0x00; 64]; + let mut pin = [0u8; 64]; pin[0] = 0x31; pin[1] = 0x32; pin[2] = 0x33; @@ -641,7 +653,7 @@ mod test { let mut blocks = [[0u8; 16]; 1]; blocks[0].copy_from_slice(&pin_hash[..16]); - let iv = [0; 16]; + let iv = [0u8; 16]; cbc_encrypt(&aes_enc_key, iv, &mut blocks); let mut encrypted_pin_hash = Vec::with_capacity(16); @@ -699,7 +711,7 @@ mod test { &mut rng, &mut persistent_store, &aes_dec_key, - pin_hash_enc.clone() + pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) ); @@ -1089,19 +1101,77 @@ mod test { fn test_has_permission() { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - pin_protocol_v1.permissions = 0x03; - assert_eq!(pin_protocol_v1.has_permission(0x01, "example.com"), Ok(())); + pin_protocol_v1.permissions = 0x7F; + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::CredentialManagement), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::BioEnrollment), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::PlatformConfiguration), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::AuthenticatorConfiguration), + Ok(()) + ); + pin_protocol_v1.permissions = 0x00; + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::MakeCredential), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::GetAssertion), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::CredentialManagement), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::BioEnrollment), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::PlatformConfiguration), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + pin_protocol_v1.has_permission(PinPermission::AuthenticatorConfiguration), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_has_permission_for_rp_id() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + assert_eq!( + pin_protocol_v1.has_permission_for_rp_id("example.com"), + Ok(()) + ); assert_eq!( pin_protocol_v1.permissions_rp_id, Some(String::from("example.com")) ); - assert_eq!(pin_protocol_v1.has_permission(0x01, "example.com"), Ok(())); assert_eq!( - pin_protocol_v1.has_permission(0x01, "counter-example.com"), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + pin_protocol_v1.has_permission_for_rp_id("example.com"), + Ok(()) ); assert_eq!( - pin_protocol_v1.has_permission(0x04, "example.com"), + pin_protocol_v1.has_permission_for_rp_id("counter-example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index bbd591d6..3f16a751 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -288,8 +288,9 @@ mod test { #[test] fn test_get_info_into_cbor() { + let versions = vec!["FIDO_2_0".to_string()]; let get_info_response = AuthenticatorGetInfoResponse { - versions: vec!["FIDO_2_0".to_string()], + versions: versions.clone(), extensions: None, aaguid: [0x00; 16], options: None, @@ -313,12 +314,12 @@ mod test { ResponseData::AuthenticatorGetInfo(get_info_response).into(); #[cfg(not(feature = "with_ctap2_1"))] let expected_cbor = cbor_map_options! { - 0x01 => cbor_array_vec![vec!["FIDO_2_0"]], + 0x01 => cbor_array_vec![versions], 0x03 => vec![0x00; 16], }; #[cfg(feature = "with_ctap2_1")] let expected_cbor = cbor_map_options! { - 0x01 => cbor_array_vec![vec!["FIDO_2_0"]], + 0x01 => cbor_array_vec![versions], 0x03 => vec![0x00; 16], 0x0D => 4, }; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index fffd80fe..415defde 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -66,7 +66,8 @@ const AAGUID: usize = 7; const MIN_PIN_LENGTH: usize = 8; #[cfg(feature = "with_ctap2_1")] const MIN_PIN_LENGTH_RP_IDS: usize = 9; -// Different NUM_TAGS make the storage incompatible, so we use max(8,10). +// Different NUM_TAGS depending on the CTAP version make the storage incompatible, +// so we use the maximum. const NUM_TAGS: usize = 10; const MAX_PIN_RETRIES: u8 = 6; @@ -417,19 +418,20 @@ impl PersistentStore { } Some((index, entry)) => { debug_assert_eq!(entry.data.len(), 1); - if entry.data[0] > 0 { - let new_value = entry.data[0].saturating_sub(1); - self.store - .replace( - index, - StoreEntry { - tag: PIN_RETRIES, - data: &[new_value], - sensitive: false, - }, - ) - .unwrap(); + if entry.data[0] == 0 { + return; } + let new_value = entry.data[0].saturating_sub(1); + self.store + .replace( + index, + StoreEntry { + tag: PIN_RETRIES, + data: &[new_value], + sensitive: false, + }, + ) + .unwrap(); } } } From 131f876cdd25f41f6d885f53b0d545b2c7418d85 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 8 Jul 2020 17:59:20 +0200 Subject: [PATCH 09/20] use the enum-iterator crate for better testing of enums --- Cargo.toml | 1 + src/ctap/data_formats.rs | 40 +++++++++++-------------- src/ctap/pin_protocol_v1.rs | 60 ++++++++----------------------------- 3 files changed, 31 insertions(+), 70 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4c2d16cb..295abe4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ crypto = { path = "libraries/crypto" } byteorder = { version = "1", default-features = false } arrayref = "0.3.6" subtle = { version = "2.2", default-features = false, features = ["nightly"] } +enum-iterator = "0.6.0" [features] debug_allocations = ["libtock/debug_allocations"] diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index b07cdf06..fc3d183f 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -18,6 +18,8 @@ use alloc::string::String; use alloc::vec::Vec; use core::convert::TryFrom; use crypto::{ecdh, ecdsa}; +#[cfg(test)] +use enum_iterator::IntoEnumIterator; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] @@ -167,6 +169,7 @@ impl From for cbor::Value { // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[cfg_attr(test, derive(IntoEnumIterator))] pub enum AuthenticatorTransport { Usb, Nfc, @@ -455,6 +458,7 @@ impl TryFrom for SignatureAlgorithm { #[derive(Clone, Copy, PartialEq, PartialOrd)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialProtectionPolicy { UserVerificationOptional = 0x01, UserVerificationOptionalWithCredentialIdList = 0x02, @@ -684,7 +688,8 @@ impl TryFrom for ecdh::PubKey { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[cfg_attr(test, derive(IntoEnumIterator))] pub enum ClientPinSubCommand { GetPinRetries = 0x01, GetKeyAgreement = 0x02, @@ -1167,11 +1172,10 @@ mod test { let expected_error = Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE); assert_eq!(policy_error, expected_error); - for policy_literal in 1..=3 { - let cbor_policy: cbor::Value = cbor_int!(policy_literal); - let policy = CredentialProtectionPolicy::try_from(cbor_policy.clone()); - let created_cbor: cbor::Value = policy.unwrap().into(); - assert_eq!(created_cbor, cbor_policy); + for policy in CredentialProtectionPolicy::into_enum_iter() { + let created_cbor: cbor::Value = policy.into(); + let reconstructed = CredentialProtectionPolicy::try_from(created_cbor).unwrap(); + assert_eq!(policy, reconstructed); } } @@ -1188,13 +1192,10 @@ mod test { let created_cbor: cbor::Value = authenticator_transport.unwrap().into(); assert_eq!(created_cbor, cbor_authenticator_transport); - let transports = ["usb", "nfc", "ble", "internal"]; - for transport in transports.iter() { - let cbor_authenticator_transport: cbor::Value = cbor_text!(*transport); - let authenticator_transport = - AuthenticatorTransport::try_from(cbor_authenticator_transport.clone()); - let created_cbor: cbor::Value = authenticator_transport.unwrap().into(); - assert_eq!(created_cbor, cbor_authenticator_transport); + for transport in AuthenticatorTransport::into_enum_iter() { + let created_cbor: cbor::Value = transport.clone().into(); + let reconstructed = AuthenticatorTransport::try_from(created_cbor).unwrap(); + assert_eq!(transport, reconstructed); } } @@ -1339,15 +1340,10 @@ mod test { let created_cbor: cbor::Value = sub_command.unwrap().into(); assert_eq!(created_cbor, cbor_sub_command); - #[cfg(not(feature = "with_ctap2_1"))] - let last_literal = 0x05; - #[cfg(feature = "with_ctap2_1")] - let last_literal = 0x09; - for command_literal in 1..=last_literal { - let cbor_sub_command: cbor::Value = cbor_int!(command_literal); - let sub_command = ClientPinSubCommand::try_from(cbor_sub_command.clone()); - let created_cbor: cbor::Value = sub_command.unwrap().into(); - assert_eq!(created_cbor, cbor_sub_command); + for command in ClientPinSubCommand::into_enum_iter() { + let created_cbor: cbor::Value = command.clone().into(); + let reconstructed = ClientPinSubCommand::try_from(created_cbor).unwrap(); + assert_eq!(command, reconstructed); } } diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 32ca1804..8523daf8 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -26,6 +26,8 @@ use crypto::hmac::{hmac_256, verify_hmac_256_first_128bits}; use crypto::rng256::Rng256; use crypto::sha256::Sha256; use crypto::Hash256; +#[cfg(all(test, feature = "with_ctap2_1"))] +use enum_iterator::IntoEnumIterator; use subtle::ConstantTimeEq; // Those constants have to be multiples of 16, the AES block size. @@ -143,6 +145,7 @@ fn check_and_store_new_pin( } #[cfg(feature = "with_ctap2_1")] +#[cfg_attr(test, derive(IntoEnumIterator))] // TODO remove when all variants are used #[allow(dead_code)] pub enum PinPermission { @@ -1102,55 +1105,16 @@ mod test { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); pin_protocol_v1.permissions = 0x7F; - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::MakeCredential), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::GetAssertion), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::CredentialManagement), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::BioEnrollment), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::PlatformConfiguration), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::AuthenticatorConfiguration), - Ok(()) - ); + for permission in PinPermission::into_enum_iter() { + assert_eq!(pin_protocol_v1.has_permission(permission), Ok(())); + } pin_protocol_v1.permissions = 0x00; - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::MakeCredential), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::GetAssertion), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::CredentialManagement), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::BioEnrollment), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::PlatformConfiguration), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - assert_eq!( - pin_protocol_v1.has_permission(PinPermission::AuthenticatorConfiguration), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + pin_protocol_v1.has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } } #[cfg(feature = "with_ctap2_1")] From 25b6756d4f7a64f4dd211fa4491407f9cd6e190e Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 8 Jul 2020 18:09:04 +0200 Subject: [PATCH 10/20] improved documentation for the PinPermission enum --- src/ctap/pin_protocol_v1.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 8523daf8..8b186c06 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -149,6 +149,7 @@ fn check_and_store_new_pin( // TODO remove when all variants are used #[allow(dead_code)] pub enum PinPermission { + // All variants should use integers with a single bit set. MakeCredential = 0x01, GetAssertion = 0x02, CredentialManagement = 0x04, @@ -157,13 +158,6 @@ pub enum PinPermission { AuthenticatorConfiguration = 0x20, } -#[cfg(feature = "with_ctap2_1")] -impl PinPermission { - pub fn check(self, stored_bits: u8) -> bool { - self as u8 & stored_bits != 0 - } -} - pub struct PinProtocolV1 { key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], @@ -592,7 +586,8 @@ impl PinProtocolV1 { #[cfg(feature = "with_ctap2_1")] pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { - if permission.check(self.permissions) { + // Relies on the fact that all permissions are represented by powers of two. + if permission as u8 & self.permissions != 0 { Ok(()) } else { Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) From 950d90ff633146e462ca44f466752ce3760c56d7 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 9 Jul 2020 11:26:42 +0200 Subject: [PATCH 11/20] moves enum-iterator dependency to dev and updates binary reference values --- Cargo.toml | 2 +- .../reference_binaries_macos-10.15.sha256sum | 10 +++++----- .../reference_binaries_ubuntu-18.04.sha256sum | 10 +++++----- reproducible/reference_elf2tab_macos-10.15.txt | 16 ++++++++-------- reproducible/reference_elf2tab_ubuntu-18.04.txt | 16 ++++++++-------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 295abe4e..6a065a9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ crypto = { path = "libraries/crypto" } byteorder = { version = "1", default-features = false } arrayref = "0.3.6" subtle = { version = "2.2", default-features = false, features = ["nightly"] } -enum-iterator = "0.6.0" [features] debug_allocations = ["libtock/debug_allocations"] @@ -30,6 +29,7 @@ with_ctap2_1 = [] [dev-dependencies] elf2tab = "0.4.0" +enum-iterator = "0.6.0" [build-dependencies] openssl = "0.10" diff --git a/reproducible/reference_binaries_macos-10.15.sha256sum b/reproducible/reference_binaries_macos-10.15.sha256sum index ee54c1f2..a7b4da3b 100644 --- a/reproducible/reference_binaries_macos-10.15.sha256sum +++ b/reproducible/reference_binaries_macos-10.15.sha256sum @@ -1,9 +1,9 @@ 0b54df6d548849e24d67b9b022ca09cb33c51f078ce85d0c9c4635ffc69902e1 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -136480e054c13cb3502a78f47b6496f0488adc001a568508f6fbb0bb92715317 target/nrf52840dk_merged.hex +ff9ac2aaab5972d10e0156c18ccf319fc8bd3a9f4ac428c406dfb2a90b63af49 target/nrf52840dk_merged.hex 052eec0ae526038352b9f7573468d0cf7fb5ec331d4dc1a2df75fdbd514ea5ca third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -21602fe8f25e329e80989e97c637ec8a2f1b02d2a88de636a06632465012a9c9 target/nrf52840_dongle_merged.hex +50dfc3b2e78c3ddd1bbf814368908434b5abdc072488a9022dd100be164fa9c1 target/nrf52840_dongle_merged.hex 908d7f4f40936d968b91ab6e19b2406612fe8c2c273d9c0b71ef1f55116780e0 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -40d176b43006dc3b2aaabfffab6210153d99641d9835e953cb48769f58d4cc48 target/nrf52840_dongle_dfu_merged.hex +cf38f8c47556934a7d12753e1a2e9ba5db649a422af99674c6fe9067eb6748b3 target/nrf52840_dongle_dfu_merged.hex 34ecbecaebf1188277f2310fe769c8c60310d8576493242712854deb4ba1036e third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -a3cf456e2f8e8216be891d7d4153c72535029c538612395a843bee65e573f8ba target/nrf52840_mdk_dfu_merged.hex -9fcc808b6df7d773cbe8740fbe33fa6522011991a9ed777656ba0ae67d6e8767 target/tab/ctap2.tab +14d03222b622adabeaf2adff9a2588709b8602726672f0730ba16792a260dc67 target/nrf52840_mdk_dfu_merged.hex +84e78ef82f3f77a8d22714de4f0257e5b003473b04f52589bc62c1aa94d85724 target/tab/ctap2.tab diff --git a/reproducible/reference_binaries_ubuntu-18.04.sha256sum b/reproducible/reference_binaries_ubuntu-18.04.sha256sum index bed5f19d..0a525123 100644 --- a/reproducible/reference_binaries_ubuntu-18.04.sha256sum +++ b/reproducible/reference_binaries_ubuntu-18.04.sha256sum @@ -1,9 +1,9 @@ 29382e72d0f3c6a72ce9517211952ff29ea270193d7f0ddc48ca69009ee29925 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -0c2732e9051d2a920a1b0c8ef1094d920145e34a169af24b25109685b913ba6a target/nrf52840dk_merged.hex +a80bc933736a15f918ec984bbec301e196b445a67656bb937d96a22f24b85990 target/nrf52840dk_merged.hex 30f239390ae9bef0825731e4c82d40470fc5e9bded2bf0d942e92dbb5d4faba1 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -d99d216f5c231b87e50b193dcb2cbdd8e09c5cee605abd72456da905832fece3 target/nrf52840_dongle_merged.hex +0d999587f229b62cb0d30b23e444a26ff73daa8b5266b5c09e4cd650f42b8302 target/nrf52840_dongle_merged.hex e3acf15d5ae3a22aecff6cc58db5fc311f538f47328d348b7ad7db7f9ab5e72c third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -56845828e90e595388a0a219b5a9a5eda6f768d0e5cc7e2b1712ce5b4b2f848d target/nrf52840_dongle_dfu_merged.hex +44447e028aed01a79c024fd77214b2d59d8a4ab0e424a42bb5d05379805ab302 target/nrf52840_dongle_dfu_merged.hex cae312a26a513ada6c198fdc59b2bba3860c51726b817a9fd17a4331ee12c882 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -7028d2253d30e5a809f4d3c2deb4cb46953a9de31978834f01a3b1392da38291 target/nrf52840_mdk_dfu_merged.hex -f96c01efac9382c001d0e277247ff20f9b17b0bb34b98a38f0e4e9cc5c3c0b90 target/tab/ctap2.tab +a9e46744083ed71735ebf5d2bfff76eb37249cf7b9bbe2fdd53682454c9e4ded target/nrf52840_mdk_dfu_merged.hex +121b4c9da41012e5037d61435100a45a69f1bd8fc8e3bb9901d4ec5d5185adee target/tab/ctap2.tab diff --git a/reproducible/reference_elf2tab_macos-10.15.txt b/reproducible/reference_elf2tab_macos-10.15.txt index 19722234..428df84e 100644 --- a/reproducible/reference_elf2tab_macos-10.15.txt +++ b/reproducible/reference_elf2tab_macos-10.15.txt @@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 179172 (0x2bbe4) bytes. - Adding .stack section. Offset: 179300 (0x2bc64). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176676 (0x2b224) bytes. + Adding .stack section. Offset: 176804 (0x2b2a4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 179172 (0x2bbe4) bytes. - Adding .stack section. Offset: 179300 (0x2bc64). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176676 (0x2b224) bytes. + Adding .stack section. Offset: 176804 (0x2b2a4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 179172 (0x2bbe4) bytes. - Adding .stack section. Offset: 179300 (0x2bc64). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176676 (0x2b224) bytes. + Adding .stack section. Offset: 176804 (0x2b2a4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 179172 (0x2bbe4) bytes. - Adding .stack section. Offset: 179300 (0x2bc64). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176676 (0x2b224) bytes. + Adding .stack section. Offset: 176804 (0x2b2a4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 diff --git a/reproducible/reference_elf2tab_ubuntu-18.04.txt b/reproducible/reference_elf2tab_ubuntu-18.04.txt index 2571f3bd..c946cec5 100644 --- a/reproducible/reference_elf2tab_ubuntu-18.04.txt +++ b/reproducible/reference_elf2tab_ubuntu-18.04.txt @@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 178740 (0x2ba34) bytes. - Adding .stack section. Offset: 178868 (0x2bab4). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176036 (0x2afa4) bytes. + Adding .stack section. Offset: 176164 (0x2b024). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 178740 (0x2ba34) bytes. - Adding .stack section. Offset: 178868 (0x2bab4). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176036 (0x2afa4) bytes. + Adding .stack section. Offset: 176164 (0x2b024). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 178740 (0x2ba34) bytes. - Adding .stack section. Offset: 178868 (0x2bab4). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176036 (0x2afa4) bytes. + Adding .stack section. Offset: 176164 (0x2b024). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 178740 (0x2ba34) bytes. - Adding .stack section. Offset: 178868 (0x2bab4). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176036 (0x2afa4) bytes. + Adding .stack section. Offset: 176164 (0x2b024). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 From cc0e2bb1c3a9fe0ff04662e604a40ee2b35e73f4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 9 Jul 2020 13:58:21 +0200 Subject: [PATCH 12/20] updates reproducible binary hashes and sizes --- .../reference_binaries_macos-10.15.sha256sum | 10 +++++----- .../reference_binaries_ubuntu-18.04.sha256sum | 10 +++++----- reproducible/reference_elf2tab_macos-10.15.txt | 16 ++++++++-------- reproducible/reference_elf2tab_ubuntu-18.04.txt | 16 ++++++++-------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/reproducible/reference_binaries_macos-10.15.sha256sum b/reproducible/reference_binaries_macos-10.15.sha256sum index a7b4da3b..1a1d1b09 100644 --- a/reproducible/reference_binaries_macos-10.15.sha256sum +++ b/reproducible/reference_binaries_macos-10.15.sha256sum @@ -1,9 +1,9 @@ 0b54df6d548849e24d67b9b022ca09cb33c51f078ce85d0c9c4635ffc69902e1 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -ff9ac2aaab5972d10e0156c18ccf319fc8bd3a9f4ac428c406dfb2a90b63af49 target/nrf52840dk_merged.hex +949d005f9a356a031dcb0a92a703c87377489ca66869dda490d9a687c77dd723 target/nrf52840dk_merged.hex 052eec0ae526038352b9f7573468d0cf7fb5ec331d4dc1a2df75fdbd514ea5ca third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -50dfc3b2e78c3ddd1bbf814368908434b5abdc072488a9022dd100be164fa9c1 target/nrf52840_dongle_merged.hex +fbf5e36aa4c71a77af5d04392faf29fd6491f0597f0fa35e6c0edeb5d3c8ad26 target/nrf52840_dongle_merged.hex 908d7f4f40936d968b91ab6e19b2406612fe8c2c273d9c0b71ef1f55116780e0 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -cf38f8c47556934a7d12753e1a2e9ba5db649a422af99674c6fe9067eb6748b3 target/nrf52840_dongle_dfu_merged.hex +1ad0c691e8c0b4df8051f0738502e34cb9dd57d449d0c01f050dae577746c3ac target/nrf52840_dongle_dfu_merged.hex 34ecbecaebf1188277f2310fe769c8c60310d8576493242712854deb4ba1036e third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -14d03222b622adabeaf2adff9a2588709b8602726672f0730ba16792a260dc67 target/nrf52840_mdk_dfu_merged.hex -84e78ef82f3f77a8d22714de4f0257e5b003473b04f52589bc62c1aa94d85724 target/tab/ctap2.tab +2e0bdaf152933e7bca99c2c13e13bb14da26e71e0845e09bf97d611df34768c3 target/nrf52840_mdk_dfu_merged.hex +6f72b3e5c35c3d73c7274b0736c4969e2bd566c77815a8e7cdd407d9edb67180 target/tab/ctap2.tab diff --git a/reproducible/reference_binaries_ubuntu-18.04.sha256sum b/reproducible/reference_binaries_ubuntu-18.04.sha256sum index 0a525123..5cf1e040 100644 --- a/reproducible/reference_binaries_ubuntu-18.04.sha256sum +++ b/reproducible/reference_binaries_ubuntu-18.04.sha256sum @@ -1,9 +1,9 @@ 29382e72d0f3c6a72ce9517211952ff29ea270193d7f0ddc48ca69009ee29925 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -a80bc933736a15f918ec984bbec301e196b445a67656bb937d96a22f24b85990 target/nrf52840dk_merged.hex +2c0bdb663edc88ae168ecd12b71730ab26bdc6d23b9fa832acc63cc4c91461ac target/nrf52840dk_merged.hex 30f239390ae9bef0825731e4c82d40470fc5e9bded2bf0d942e92dbb5d4faba1 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -0d999587f229b62cb0d30b23e444a26ff73daa8b5266b5c09e4cd650f42b8302 target/nrf52840_dongle_merged.hex +81564eab5c20f186c0583e0a31f26fcc50ec0ebd997bff6109e663cbb7d59966 target/nrf52840_dongle_merged.hex e3acf15d5ae3a22aecff6cc58db5fc311f538f47328d348b7ad7db7f9ab5e72c third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -44447e028aed01a79c024fd77214b2d59d8a4ab0e424a42bb5d05379805ab302 target/nrf52840_dongle_dfu_merged.hex +25b10d9d80d4961ea0d8373d64e883279baeff6997eb8e541d36930ec423b88b target/nrf52840_dongle_dfu_merged.hex cae312a26a513ada6c198fdc59b2bba3860c51726b817a9fd17a4331ee12c882 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -a9e46744083ed71735ebf5d2bfff76eb37249cf7b9bbe2fdd53682454c9e4ded target/nrf52840_mdk_dfu_merged.hex -121b4c9da41012e5037d61435100a45a69f1bd8fc8e3bb9901d4ec5d5185adee target/tab/ctap2.tab +1077f1acf2c0d65eeda2056d907ee449bfbf7f783c253c1f92838f7ac5e11d99 target/nrf52840_mdk_dfu_merged.hex +3c8dc97b68c5ce5030f0af3879c5f9531d69ea1404899e9dfe306d02d638e0cc target/tab/ctap2.tab diff --git a/reproducible/reference_elf2tab_macos-10.15.txt b/reproducible/reference_elf2tab_macos-10.15.txt index 428df84e..2593f2be 100644 --- a/reproducible/reference_elf2tab_macos-10.15.txt +++ b/reproducible/reference_elf2tab_macos-10.15.txt @@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 176676 (0x2b224) bytes. - Adding .stack section. Offset: 176804 (0x2b2a4). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 175972 (0x2af64) bytes. + Adding .stack section. Offset: 176100 (0x2afe4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 176676 (0x2b224) bytes. - Adding .stack section. Offset: 176804 (0x2b2a4). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 175972 (0x2af64) bytes. + Adding .stack section. Offset: 176100 (0x2afe4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 176676 (0x2b224) bytes. - Adding .stack section. Offset: 176804 (0x2b2a4). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 175972 (0x2af64) bytes. + Adding .stack section. Offset: 176100 (0x2afe4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 176676 (0x2b224) bytes. - Adding .stack section. Offset: 176804 (0x2b2a4). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 175972 (0x2af64) bytes. + Adding .stack section. Offset: 176100 (0x2afe4). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 diff --git a/reproducible/reference_elf2tab_ubuntu-18.04.txt b/reproducible/reference_elf2tab_ubuntu-18.04.txt index c946cec5..e8221412 100644 --- a/reproducible/reference_elf2tab_ubuntu-18.04.txt +++ b/reproducible/reference_elf2tab_ubuntu-18.04.txt @@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 176036 (0x2afa4) bytes. - Adding .stack section. Offset: 176164 (0x2b024). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176516 (0x2b184) bytes. + Adding .stack section. Offset: 176644 (0x2b204). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 176036 (0x2afa4) bytes. - Adding .stack section. Offset: 176164 (0x2b024). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176516 (0x2b184) bytes. + Adding .stack section. Offset: 176644 (0x2b204). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 176036 (0x2afa4) bytes. - Adding .stack section. Offset: 176164 (0x2b024). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176516 (0x2b184) bytes. + Adding .stack section. Offset: 176644 (0x2b204). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 16 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 176036 (0x2afa4) bytes. - Adding .stack section. Offset: 176164 (0x2b024). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 176516 (0x2b184) bytes. + Adding .stack section. Offset: 176644 (0x2b204). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 From 9c673844d5251d888d711fca24a057a784922028 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 9 Jul 2020 19:06:42 +0200 Subject: [PATCH 13/20] improved documentation, especially with regards to the extension --- README.md | 6 +++++- src/ctap/command.rs | 4 ++-- src/ctap/pin_protocol_v1.rs | 8 ++++---- src/ctap/storage.rs | 20 ++++++++++++-------- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8e317968..c5e4bfc5 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,11 @@ a few things you can personalize: user verification. This helps privacy, but can make usage less comfortable for credentials that need less protection. 6. Increase the default minimum length for PINs in `ctap/storage.rs`. - The current minimum is 4. Values from 4 to 63 are allowed. + The current minimum is 4. Values from 4 to 63 are allowed. Requiring longer + PINs can help establish trust between users and relying parties. It makes + user verification harder to break, but less convenient. + NIST recommends 6 at least digit PINs in section 5.1.9.1: + https://pages.nist.gov/800-63-3/sp800-63b.html You can add relying parties to the list of readers of the minimum PIN length. ### 3D printed enclosure diff --git a/src/ctap/command.rs b/src/ctap/command.rs index c3627f78..84ca8df3 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -492,8 +492,8 @@ mod test { #[test] fn test_from_cbor_client_pin_parameters() { - // TODO(kaczmarczyck) inline the #cfg when the ICE is resolved: - // https://github.com/rust-lang/rust/issues/73663 + // TODO(kaczmarczyck) inline the #cfg when #128 is resolved: + // https://github.com/google/OpenSK/issues/128 #[cfg(not(feature = "with_ctap2_1"))] let cbor_value = cbor_map! { 1 => 1, diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 8b186c06..db062655 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -392,10 +392,6 @@ impl PinProtocolV1 { if persistent_store.pin_hash().is_some() { match pin_auth { Some(pin_auth) => { - // TODO(kaczmarczyck) not mentioned, but maybe useful? - // if persistent_store.pin_retries() == 0 { - // return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); - // } if self.consecutive_pin_mismatches >= 3 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); } @@ -403,6 +399,7 @@ impl PinProtocolV1 { message.extend(&[0x06, 0x08]); message.extend(&[min_pin_length as u8, 0x00, 0x00, 0x00]); // TODO(kaczmarczyck) commented code is useful for the extension + // https://github.com/google/OpenSK/issues/129 // if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) { // return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); // } @@ -417,6 +414,8 @@ impl PinProtocolV1 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); } persistent_store.set_min_pin_length(min_pin_length); + // TODO(kaczmarczyck) commented code is useful for the extension + // https://github.com/google/OpenSK/issues/129 // if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids { // persistent_store.set_min_pin_length_rp_ids(min_pin_length_rp_ids)?; // } @@ -932,6 +931,7 @@ mod test { 0xFE, 0xC9, ]; // TODO(kaczmarczyck) implement test for the min PIN length extension + // https://github.com/google/OpenSK/issues/129 let response = pin_protocol_v1.process_set_min_pin_length( &mut persistent_store, min_pin_length, diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 415defde..24456c81 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -75,6 +75,8 @@ const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; const AAGUID_LENGTH: usize = 16; #[cfg(feature = "with_ctap2_1")] const DEFAULT_MIN_PIN_LENGTH: u8 = 4; +// TODO(kaczmarczyck) use this for the minPinLength extension +// https://github.com/google/OpenSK/issues/129 #[cfg(feature = "with_ctap2_1")] const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly. @@ -396,13 +398,12 @@ impl PersistentStore { } pub fn pin_retries(&self) -> u8 { - match self.store.find_one(&Key::PinRetries) { - None => MAX_PIN_RETRIES, - Some((_, entry)) => { + self.store + .find_one(&Key::PinRetries) + .map_or(MAX_PIN_RETRIES, |(_, entry)| { debug_assert_eq!(entry.data.len(), 1); entry.data[0] - } - } + }) } pub fn decr_pin_retries(&mut self) { @@ -446,7 +447,10 @@ impl PersistentStore { pub fn min_pin_length(&self) -> u8 { self.store .find_one(&Key::MinPinLength) - .map_or(DEFAULT_MIN_PIN_LENGTH, |(_, entry)| entry.data[0]) + .map_or(DEFAULT_MIN_PIN_LENGTH, |(_, entry)| { + debug_assert_eq!(entry.data.len(), 1); + entry.data[0] + }) } #[cfg(feature = "with_ctap2_1")] @@ -1007,7 +1011,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - // The minimum PIN lenght is initially at the default. + // The minimum PIN length is initially at the default. assert_eq!(persistent_store.min_pin_length(), DEFAULT_MIN_PIN_LENGTH); // Changes by the setter are reflected by the getter.. @@ -1022,7 +1026,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - // The minimum PIN lenght is initially at the default. + // The minimum PIN length RP IDs are initially at the default. assert_eq!( persistent_store._min_pin_length_rp_ids(), _DEFAULT_MIN_PIN_LENGTH_RP_IDS From a398c404dcf1c44f4e783edf30ead68cb9df793b Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 27 Jul 2020 22:18:51 +0200 Subject: [PATCH 14/20] improves documentation to address comments --- README.md | 2 +- src/ctap/mod.rs | 7 +++++-- src/ctap/pin_protocol_v1.rs | 9 ++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c5e4bfc5..20a10ca9 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ a few things you can personalize: The current minimum is 4. Values from 4 to 63 are allowed. Requiring longer PINs can help establish trust between users and relying parties. It makes user verification harder to break, but less convenient. - NIST recommends 6 at least digit PINs in section 5.1.9.1: + NIST recommends at least 6-digit PINs in section 5.1.9.1: https://pages.nist.gov/800-63-3/sp800-63b.html You can add relying parties to the list of readers of the minimum PIN length. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6df8ad28..692a15ab 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -755,8 +755,11 @@ where &mut self, client_pin_params: AuthenticatorClientPinParameters, ) -> Result { - self.pin_protocol_v1 - .process(self.rng, &mut self.persistent_store, client_pin_params) + self.pin_protocol_v1.process_subcommand( + self.rng, + &mut self.persistent_store, + client_pin_params, + ) } fn process_reset(&mut self, cid: ChannelID) -> Result { diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index db062655..876f1087 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -72,6 +72,7 @@ fn encrypt_hmac_secret_output( // Initialization of 4 blocks in any case makes this function more readable. let mut blocks = [[0u8; 16]; 4]; + // With the if clause restriction above, block_len can only be 2 or 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)]); @@ -395,6 +396,8 @@ impl PinProtocolV1 { if self.consecutive_pin_mismatches >= 3 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); } + // TODO(kaczmarczyck) Values are taken from the (not yet public) new revision + // of CTAP 2.1. The code should link the specification when published. let mut message = vec![0xFF; 32]; message.extend(&[0x06, 0x08]); message.extend(&[min_pin_length as u8, 0x00, 0x00, 0x00]); @@ -449,7 +452,7 @@ impl PinProtocolV1 { Ok(response) } - pub fn process( + pub fn process_subcommand( &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, @@ -975,7 +978,7 @@ mod test { permissions_rp_id: None, }; assert!(pin_protocol_v1 - .process(&mut rng, &mut persistent_store, client_pin_params) + .process_subcommand(&mut rng, &mut persistent_store, client_pin_params) .is_ok()); let client_pin_params = AuthenticatorClientPinParameters { @@ -999,7 +1002,7 @@ mod test { #[cfg(feature = "with_ctap2_1")] let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; assert_eq!( - pin_protocol_v1.process(&mut rng, &mut persistent_store, client_pin_params), + pin_protocol_v1.process_subcommand(&mut rng, &mut persistent_store, client_pin_params), Err(error_code) ); } From d5fefa2f1260e124102b0bfb0f39a493d9afb6eb Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 4 Aug 2020 18:56:54 +0200 Subject: [PATCH 15/20] improved code consistency and documentation --- src/ctap/mod.rs | 4 +- src/ctap/pin_protocol_v1.rs | 157 ++++++++++++++++++++++-------------- 2 files changed, 97 insertions(+), 64 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 692a15ab..35edc774 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -411,7 +411,7 @@ where } if !self .pin_protocol_v1 - .check_pin_auth_token(&client_data_hash, &pin_auth) + .verify_pin_auth_token(&client_data_hash, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } @@ -595,7 +595,7 @@ where } if !self .pin_protocol_v1 - .check_pin_auth_token(&client_data_hash, &pin_auth) + .verify_pin_auth_token(&client_data_hash, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 876f1087..6ee4918b 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -36,7 +36,8 @@ const PIN_PADDED_LENGTH: usize = 64; const PIN_TOKEN_LENGTH: usize = 32; /// Checks the given pin_auth against the truncated output of HMAC-SHA256. -fn check_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { +/// Returns LEFT(HMAC(hmac_key, hmac_contents), 16) == pin_auth). +fn verify_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { if pin_auth.len() != PIN_AUTH_LENGTH { return false; } @@ -68,7 +69,7 @@ fn encrypt_hmac_secret_output( let iv = [0u8; 16]; let mut cred_random_secret = [0u8; 32]; - cred_random_secret.clone_from_slice(cred_random); + cred_random_secret.copy_from_slice(cred_random); // Initialization of 4 blocks in any case makes this function more readable. let mut blocks = [[0u8; 16]; 4]; @@ -80,17 +81,17 @@ fn encrypt_hmac_secret_output( cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]); let mut decrypted_salt1 = [0u8; 32]; - decrypted_salt1[..16].clone_from_slice(&blocks[0]); + decrypted_salt1[..16].copy_from_slice(&blocks[0]); let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); - decrypted_salt1[16..].clone_from_slice(&blocks[1]); + decrypted_salt1[16..].copy_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 = [0u8; 32]; - decrypted_salt2[..16].clone_from_slice(&blocks[2]); - decrypted_salt2[16..].clone_from_slice(&blocks[3]); + decrypted_salt2[..16].copy_from_slice(&blocks[2]); + decrypted_salt2[16..].copy_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)]); @@ -105,6 +106,10 @@ fn encrypt_hmac_secret_output( Ok(encrypted_output) } +/// Stores the encrypted new PIN in the persistent storage, if it satisfies the +/// PIN policy. The PIN is decrypted and stripped from its padding. Next, the +/// length of the PIN is checked to fulfill policy requirements. Last, the PIN +/// is hashed, truncated to 16 bytes and persistently stored. fn check_and_store_new_pin( persistent_store: &mut PersistentStore, aes_dec_key: &crypto::aes256::DecryptionKey, @@ -115,28 +120,28 @@ fn check_and_store_new_pin( } let iv = [0u8; 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 { + const BLOCK_COUNT: usize = PIN_PADDED_LENGTH / 16; + let mut blocks = [[0u8; 16]; BLOCK_COUNT]; + for i in 0..BLOCK_COUNT { 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; - } - } - } + // 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. + let pin: Vec = blocks + .iter() + .flatten() + .cloned() + .take_while(|&c| c != 0) + .collect(); + #[cfg(feature = "with_ctap2_1")] let min_pin_length = persistent_store.min_pin_length() as usize; #[cfg(not(feature = "with_ctap2_1"))] let min_pin_length = 4; if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH { // TODO(kaczmarczyck) check 4 code point minimum instead - // TODO(kaczmarczyck) check last byte == 0x00 return false; } let mut pin_hash = [0u8; 16]; @@ -184,7 +189,9 @@ impl PinProtocolV1 { } } - fn check_pin_hash_enc( + /// Decrypts the encrypted pin_hash and compares it to the stored pin_hash. + /// Resets or decreases the PIN retries, depending on success or failure. + fn verify_pin_hash_enc( &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, @@ -196,9 +203,10 @@ impl PinProtocolV1 { 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(); + // We need to copy the pin hash, because decrementing the pin retries below mutably + // borrows from the same persistent_store reference, and therefore invalidates the + // pin_hash reference. + let pin_hash = pin_hash.clone(); persistent_store.decr_pin_retries(); if pin_hash_enc.len() != PIN_AUTH_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); @@ -209,8 +217,7 @@ impl PinProtocolV1 { 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) { + if !bool::from(pin_hash.ct_eq(&blocks[0])) { self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); if persistent_store.pin_retries() == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); @@ -230,6 +237,26 @@ impl PinProtocolV1 { Ok(()) } + /// Uses the self-owned and passed halves of the key agreement to generate the + /// shared secret for checking pin_auth and generating a decryption key. + fn create_decryption_key_verified( + &self, + key_agreement: CoseKey, + pin_auth: &[u8], + message: &[u8], + ) -> Result { + let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; + let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); + + if !verify_pin_auth(&shared_secret, message, 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); + Ok(aes_dec_key) + } + fn process_get_pin_retries( &self, persistent_store: &PersistentStore, @@ -260,16 +287,9 @@ impl PinProtocolV1 { 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 !check_and_store_new_pin(persistent_store, &aes_dec_key, new_pin_enc) { + let pin_decryption_key = + self.create_decryption_key_verified(key_agreement, &pin_auth, &new_pin_enc)?; + if !check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); } persistent_store.reset_pin_retries(); @@ -288,20 +308,13 @@ impl PinProtocolV1 { 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 pin_decryption_key = + self.create_decryption_key_verified(key_agreement, &pin_auth, &auth_param_data)?; + self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; - 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 !check_and_store_new_pin(persistent_store, &aes_dec_key, new_pin_enc) { + if !check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); } self.pin_uv_auth_token = rng.gen_uniform_u8x32(); @@ -321,9 +334,9 @@ impl PinProtocolV1 { 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)?; + let token_encryption_key = crypto::aes256::EncryptionKey::new(&shared_secret); + let pin_decryption_key = crypto::aes256::DecryptionKey::new(&token_encryption_key); + self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; // Assuming PIN_TOKEN_LENGTH % block_size == 0 here. let iv = [0u8; 16]; @@ -331,7 +344,7 @@ impl PinProtocolV1 { 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); + cbc_encrypt(&token_encryption_key, iv, &mut blocks); let mut pin_token = vec![]; for item in blocks.iter().take(PIN_TOKEN_LENGTH / 16) { pin_token.extend(item); @@ -353,7 +366,11 @@ impl PinProtocolV1 { #[cfg(feature = "with_ctap2_1")] fn process_get_pin_uv_auth_token_using_uv_with_permissions( &self, - _: CoseKey, + // If you want to support local user verification, implement this function. + // Lacking a fingerprint reader, this subcommand is currently unsupported. + _key_agreement: CoseKey, + _permissions: u8, + _permissions_rp_id: Option, ) -> Result { // User verifications is only supported through PIN currently. #[cfg(not(feature = "with_ctap2_1"))] @@ -406,7 +423,7 @@ impl PinProtocolV1 { // if !cbor::write(cbor_array_vec!(min_pin_length_rp_ids), &mut message) { // return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); // } - if !check_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) { + if !verify_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } } @@ -517,6 +534,8 @@ impl PinProtocolV1 { ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( self.process_get_pin_uv_auth_token_using_uv_with_permissions( key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + permissions_rp_id, )?, ), #[cfg(feature = "with_ctap2_1")] @@ -546,8 +565,8 @@ impl PinProtocolV1 { 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 verify_pin_auth_token(&self, hmac_contents: &[u8], pin_auth: &[u8]) -> bool { + verify_pin_auth(&self.pin_uv_auth_token, &hmac_contents, &pin_auth) } pub fn reset(&mut self, rng: &mut impl Rng256) { @@ -574,7 +593,7 @@ impl PinProtocolV1 { 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) { + if !verify_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); } @@ -662,7 +681,7 @@ mod test { } #[test] - fn test_check_pin_hash_enc() { + fn test_verify_pin_hash_enc() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); // The PIN is "1234". @@ -681,7 +700,7 @@ mod test { 0x99, 0x66, ]; assert_eq!( - pin_protocol_v1.check_pin_hash_enc( + pin_protocol_v1.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -692,7 +711,7 @@ mod test { let pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.check_pin_hash_enc( + pin_protocol_v1.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -707,7 +726,7 @@ mod test { ]; pin_protocol_v1.consecutive_pin_mismatches = 3; assert_eq!( - pin_protocol_v1.check_pin_hash_enc( + pin_protocol_v1.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -1043,6 +1062,20 @@ mod test { bad_pin_enc )); + // The PIN "12'\0'4" (a zero byte where the 3 was) should be rejected. + let bad_pin_enc = vec![ + 0x32, 0x78, 0x24, 0xCD, 0x25, 0xB7, 0x2F, 0x42, 0x18, 0x08, 0x27, 0x80, 0x21, 0xEA, + 0xE9, 0x2C, 0x20, 0x06, 0x7E, 0x17, 0xBA, 0x87, 0xCB, 0x35, 0xB0, 0xB9, 0x57, 0x9B, + 0xCB, 0x73, 0x7D, 0xBB, 0x61, 0x0D, 0xBB, 0x70, 0x5F, 0xC3, 0x94, 0x03, 0x64, 0x9A, + 0xC6, 0xDF, 0x9C, 0x74, 0x36, 0x97, 0xD4, 0x1B, 0x31, 0xD9, 0x7D, 0x1E, 0x42, 0xAC, + 0x18, 0xBA, 0xE0, 0x6C, 0x16, 0x0B, 0x25, 0xCC, + ]; + assert!(!check_and_store_new_pin( + &mut persistent_store, + &aes_dec_key, + bad_pin_enc + )); + // The last byte of the decrypted PIN is not padding, which is 0x00. let bad_pin_enc = vec![ 0x53, 0x3D, 0xAD, 0x69, 0xB6, 0x1B, 0x5F, 0xAF, 0x0F, 0x26, 0xF1, 0x33, 0xB3, 0xCC, @@ -1059,14 +1092,14 @@ mod test { } #[test] - fn test_check_pin_auth() { + fn test_verify_pin_auth() { let hmac_key = [0x88; 16]; let pin_auth = [ 0x88, 0x09, 0x41, 0x13, 0xF7, 0x97, 0x32, 0x0B, 0x3E, 0xD9, 0xBC, 0x76, 0x4F, 0x18, 0x56, 0x5D, ]; - assert!(check_pin_auth(&hmac_key, &[], &pin_auth)); - assert!(!check_pin_auth(&hmac_key, &[0x00], &pin_auth)); + assert!(verify_pin_auth(&hmac_key, &[], &pin_auth)); + assert!(!verify_pin_auth(&hmac_key, &[0x00], &pin_auth)); } #[test] From 0aabf8210a4300277896780cfcadd15d4233a9f5 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 13 Aug 2020 05:37:20 +0200 Subject: [PATCH 16/20] improved testing in pin_protocol_v1.rs --- src/ctap/pin_protocol_v1.rs | 222 ++++++++++++++++++++++-------------- 1 file changed, 137 insertions(+), 85 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 6ee4918b..2a7fd65b 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -106,17 +106,13 @@ fn encrypt_hmac_secret_output( Ok(encrypted_output) } -/// Stores the encrypted new PIN in the persistent storage, if it satisfies the -/// PIN policy. The PIN is decrypted and stripped from its padding. Next, the -/// length of the PIN is checked to fulfill policy requirements. Last, the PIN -/// is hashed, truncated to 16 bytes and persistently stored. -fn check_and_store_new_pin( - persistent_store: &mut PersistentStore, +/// Decrypts the new_pin_enc and outputs the found PIN. +fn decrypt_pin( aes_dec_key: &crypto::aes256::DecryptionKey, new_pin_enc: Vec, -) -> bool { +) -> Option> { if new_pin_enc.len() != PIN_PADDED_LENGTH { - return false; + return None; } let iv = [0u8; 16]; // Assuming PIN_PADDED_LENGTH % block_size == 0 here. @@ -129,12 +125,27 @@ fn check_and_store_new_pin( // 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. - let pin: Vec = blocks - .iter() - .flatten() - .cloned() - .take_while(|&c| c != 0) - .collect(); + Some( + blocks + .iter() + .flatten() + .cloned() + .take_while(|&c| c != 0) + .collect::>(), + ) +} + +/// Stores the encrypted new PIN in the persistent storage, if it satisfies the +/// PIN policy. The PIN is decrypted and stripped from its padding. Next, the +/// length of the PIN is checked to fulfill policy requirements. Last, the PIN +/// is hashed, truncated to 16 bytes and persistently stored. +fn check_and_store_new_pin( + persistent_store: &mut PersistentStore, + aes_dec_key: &crypto::aes256::DecryptionKey, + new_pin_enc: Vec, +) -> Result<(), Ctap2StatusCode> { + let pin = decrypt_pin(aes_dec_key, new_pin_enc) + .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; #[cfg(feature = "with_ctap2_1")] let min_pin_length = persistent_store.min_pin_length() as usize; @@ -142,12 +153,12 @@ fn check_and_store_new_pin( let min_pin_length = 4; if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH { // TODO(kaczmarczyck) check 4 code point minimum instead - return false; + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); } let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); persistent_store.set_pin_hash(&pin_hash); - true + Ok(()) } #[cfg(feature = "with_ctap2_1")] @@ -214,7 +225,7 @@ impl PinProtocolV1 { let iv = [0u8; 16]; let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash_enc[0..PIN_AUTH_LENGTH]); + blocks[0].copy_from_slice(&pin_hash_enc); cbc_decrypt(aes_dec_key, iv, &mut blocks); if !bool::from(pin_hash.ct_eq(&blocks[0])) { @@ -239,16 +250,16 @@ impl PinProtocolV1 { /// Uses the self-owned and passed halves of the key agreement to generate the /// shared secret for checking pin_auth and generating a decryption key. - fn create_decryption_key_verified( + fn exchange_decryption_key( &self, key_agreement: CoseKey, pin_auth: &[u8], - message: &[u8], + authenticated_message: &[u8], ) -> Result { let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - if !verify_pin_auth(&shared_secret, message, pin_auth) { + if !verify_pin_auth(&shared_secret, authenticated_message, pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } @@ -288,10 +299,8 @@ impl PinProtocolV1 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } let pin_decryption_key = - self.create_decryption_key_verified(key_agreement, &pin_auth, &new_pin_enc)?; - if !check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); - } + self.exchange_decryption_key(key_agreement, &pin_auth, &new_pin_enc)?; + check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; persistent_store.reset_pin_retries(); Ok(()) } @@ -311,12 +320,10 @@ impl PinProtocolV1 { let mut auth_param_data = new_pin_enc.clone(); auth_param_data.extend(&pin_hash_enc); let pin_decryption_key = - self.create_decryption_key_verified(key_agreement, &pin_auth, &auth_param_data)?; + self.exchange_decryption_key(key_agreement, &pin_auth, &auth_param_data)?; self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; - if !check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); - } + check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; self.pin_uv_auth_token = rng.gen_uniform_u8x32(); Ok(()) } @@ -345,10 +352,7 @@ impl PinProtocolV1 { item.copy_from_slice(&self.pin_uv_auth_token[i * 16..(i + 1) * 16]); } cbc_encrypt(&token_encryption_key, iv, &mut blocks); - let mut pin_token = vec![]; - for item in blocks.iter().take(PIN_TOKEN_LENGTH / 16) { - pin_token.extend(item); - } + let pin_token: Vec = blocks.iter().flatten().cloned().collect(); #[cfg(feature = "with_ctap2_1")] { @@ -415,6 +419,9 @@ impl PinProtocolV1 { } // TODO(kaczmarczyck) Values are taken from the (not yet public) new revision // of CTAP 2.1. The code should link the specification when published. + // From CTAP2.1: "If request contains pinUvAuthParam, the Authenticator calls + // verify(pinUvAuthToken, 32×0xff || 0x0608 || uint32LittleEndian(minPINLength) + // || minPinLengthRPIDs, pinUvAuthParam)" let mut message = vec![0xFF; 32]; message.extend(&[0x06, 0x08]); message.extend(&[min_pin_length as u8, 0x00, 0x00, 0x00]); @@ -644,21 +651,25 @@ mod test { persistent_store.set_pin_hash(&pin_hash); } - fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { + // Fails on PINs bigger than 64 byte.. + fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec) -> Vec { + assert!(pin.len() <= 64); + let mut padded_pin = [0u8; 64]; + padded_pin[..pin.len()].copy_from_slice(&pin[..]); let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); let mut blocks = [[0u8; 16]; 4]; - blocks[0][0] = 0x31; - blocks[0][1] = 0x32; - blocks[0][2] = 0x33; - blocks[0][3] = 0x34; + let (b0, b1, b2, b3) = array_refs!(&padded_pin, 16, 16, 16, 16); + blocks[0][..].copy_from_slice(b0); + blocks[1][..].copy_from_slice(b1); + blocks[2][..].copy_from_slice(b2); + blocks[3][..].copy_from_slice(b3); let iv = [0u8; 16]; cbc_encrypt(&aes_enc_key, iv, &mut blocks); + blocks.iter().flatten().cloned().collect::>() + } - let mut encrypted_pin = Vec::with_capacity(64); - for b in &blocks { - encrypted_pin.extend(b); - } - encrypted_pin + fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { + encrypt_pin(shared_secret, vec![0x31, 0x32, 0x33, 0x34]) } fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { @@ -734,6 +745,29 @@ mod test { ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) ); + pin_protocol_v1.consecutive_pin_mismatches = 0; + + let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; + assert_eq!( + pin_protocol_v1.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + &aes_dec_key, + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + + let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1]; + assert_eq!( + pin_protocol_v1.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + &aes_dec_key, + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); } #[test] @@ -1027,14 +1061,12 @@ mod test { } #[test] - fn test_check_and_store_new_pin() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); + fn test_decrypt_pin() { let shared_secret = [0x88; 32]; let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - // The PIN "1234" should be accepted. + // "1234" let new_pin_enc = vec![ 0xC0, 0xCF, 0xAE, 0x4C, 0x79, 0x56, 0x87, 0x99, 0xE5, 0x83, 0x4F, 0xE6, 0x4D, 0xFE, 0x53, 0x32, 0x36, 0x0D, 0xF9, 0x1E, 0x47, 0x66, 0x10, 0x5C, 0x63, 0x30, 0x1D, 0xCC, @@ -1042,53 +1074,73 @@ mod test { 0xEE, 0x01, 0x99, 0x6C, 0xD7, 0xE5, 0x2B, 0xA5, 0x7A, 0x5A, 0xE1, 0xEC, 0x69, 0x31, 0x18, 0x35, 0x06, 0x66, 0x97, 0x84, 0x68, 0xC2, ]; - assert!(check_and_store_new_pin( - &mut persistent_store, - &aes_dec_key, - new_pin_enc - )); + assert_eq!( + decrypt_pin(&aes_dec_key, new_pin_enc), + Some(vec![0x31, 0x32, 0x33, 0x34]), + ); - // The PIN "123" has only 3 characters. - let bad_pin_enc = vec![ + // "123" + let new_pin_enc = vec![ 0xF3, 0x54, 0x29, 0x17, 0xD4, 0xF8, 0xCD, 0x23, 0x1D, 0x59, 0xED, 0xE5, 0x33, 0x42, 0x13, 0x39, 0x22, 0xBB, 0x91, 0x28, 0x87, 0x6A, 0xF9, 0xB1, 0x80, 0x9C, 0x9D, 0x76, 0xFF, 0xDD, 0xB8, 0xD6, 0x8D, 0x66, 0x99, 0xA2, 0x42, 0x67, 0xB0, 0x5C, 0x82, 0x3F, 0x08, 0x55, 0x8C, 0x04, 0xC5, 0x91, 0xF0, 0xF9, 0x58, 0x44, 0x00, 0x1B, 0x99, 0xA6, 0x7C, 0xC7, 0x2D, 0x43, 0x74, 0x4C, 0x1D, 0x7E, ]; - assert!(!check_and_store_new_pin( - &mut persistent_store, - &aes_dec_key, - bad_pin_enc - )); - - // The PIN "12'\0'4" (a zero byte where the 3 was) should be rejected. - let bad_pin_enc = vec![ - 0x32, 0x78, 0x24, 0xCD, 0x25, 0xB7, 0x2F, 0x42, 0x18, 0x08, 0x27, 0x80, 0x21, 0xEA, - 0xE9, 0x2C, 0x20, 0x06, 0x7E, 0x17, 0xBA, 0x87, 0xCB, 0x35, 0xB0, 0xB9, 0x57, 0x9B, - 0xCB, 0x73, 0x7D, 0xBB, 0x61, 0x0D, 0xBB, 0x70, 0x5F, 0xC3, 0x94, 0x03, 0x64, 0x9A, - 0xC6, 0xDF, 0x9C, 0x74, 0x36, 0x97, 0xD4, 0x1B, 0x31, 0xD9, 0x7D, 0x1E, 0x42, 0xAC, - 0x18, 0xBA, 0xE0, 0x6C, 0x16, 0x0B, 0x25, 0xCC, - ]; - assert!(!check_and_store_new_pin( - &mut persistent_store, - &aes_dec_key, - bad_pin_enc - )); - - // The last byte of the decrypted PIN is not padding, which is 0x00. - let bad_pin_enc = vec![ - 0x53, 0x3D, 0xAD, 0x69, 0xB6, 0x1B, 0x5F, 0xAF, 0x0F, 0x26, 0xF1, 0x33, 0xB3, 0xCC, - 0x94, 0x26, 0x68, 0xD0, 0xC4, 0x58, 0xD4, 0x2D, 0x3D, 0x8B, 0x6F, 0x1A, 0xA2, 0x0A, - 0x44, 0x47, 0xE8, 0x94, 0xF2, 0x2D, 0x99, 0xEB, 0xA1, 0xA6, 0xBE, 0x32, 0x7C, 0x99, - 0x2B, 0xB8, 0x9A, 0x15, 0x9C, 0xEA, 0x86, 0x47, 0x4B, 0x5E, 0x6C, 0xA2, 0xE2, 0xB9, - 0x0D, 0x85, 0x25, 0xD3, 0x8A, 0x46, 0x39, 0xAD, + assert_eq!( + decrypt_pin(&aes_dec_key, new_pin_enc), + Some(vec![0x31, 0x32, 0x33]), + ); + + // Encrypted PIN is too short. + let new_pin_enc = vec![0x44; 63]; + assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); + + // Encrypted PIN is too long. + let new_pin_enc = vec![0x44; 65]; + assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); + } + + #[test] + fn test_check_and_store_new_pin() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let shared_secret = [0x88; 32]; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + + let test_cases = vec![ + // Accept PIN "1234". + (vec![0x31, 0x32, 0x33, 0x34], Ok(())), + // Reject PIN "123" since it is too short. + ( + vec![0x31, 0x32, 0x33], + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), + // Reject PIN "12'\0'4" (a zero byte at index 2). + ( + vec![0x31, 0x32, 0x00, 0x34], + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), + // Reject PINs not ending in 0u8 padding. + ( + vec![0x30; 64], + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), ]; - assert!(!check_and_store_new_pin( - &mut persistent_store, - &aes_dec_key, - bad_pin_enc - )); + for (pin, result) in test_cases { + let old_pin_hash = persistent_store.pin_hash().cloned(); + let new_pin_enc = encrypt_pin(&shared_secret, pin); + assert_eq!( + check_and_store_new_pin(&mut persistent_store, &aes_dec_key, new_pin_enc), + result + ); + if result.is_ok() { + assert_ne!(old_pin_hash.as_ref(), persistent_store.pin_hash()); + } else { + assert_eq!(old_pin_hash.as_ref(), persistent_store.pin_hash()); + } + } } #[test] From bbcff488d5dec1db0cd2cf2aa006f06222a651f2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 17 Aug 2020 17:36:17 +0200 Subject: [PATCH 17/20] unifying the use instructions to another standard --- src/ctap/data_formats.rs | 76 +++++++++++++++++-------------------- src/ctap/pin_protocol_v1.rs | 5 ++- 2 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index fc3d183f..fad58200 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -179,12 +179,11 @@ pub enum AuthenticatorTransport { impl From for cbor::Value { fn from(transport: AuthenticatorTransport) -> Self { - use AuthenticatorTransport::*; match transport { - Usb => "usb", - Nfc => "nfc", - Ble => "ble", - Internal => "internal", + AuthenticatorTransport::Usb => "usb", + AuthenticatorTransport::Nfc => "nfc", + AuthenticatorTransport::Ble => "ble", + AuthenticatorTransport::Internal => "internal", } .into() } @@ -194,13 +193,12 @@ impl TryFrom for AuthenticatorTransport { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - use AuthenticatorTransport::*; let transport_string = extract_text_string(cbor_value)?; match &transport_string[..] { - "usb" => Ok(Usb), - "nfc" => Ok(Nfc), - "ble" => Ok(Ble), - "internal" => Ok(Internal), + "usb" => Ok(AuthenticatorTransport::Usb), + "nfc" => Ok(AuthenticatorTransport::Nfc), + "ble" => Ok(AuthenticatorTransport::Ble), + "internal" => Ok(AuthenticatorTransport::Internal), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } @@ -475,11 +473,10 @@ impl TryFrom for CredentialProtectionPolicy { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - use CredentialProtectionPolicy::*; match extract_integer(cbor_value)? { - 0x01 => Ok(UserVerificationOptional), - 0x02 => Ok(UserVerificationOptionalWithCredentialIdList), - 0x03 => Ok(UserVerificationRequired), + 0x01 => Ok(CredentialProtectionPolicy::UserVerificationOptional), + 0x02 => Ok(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList), + 0x03 => Ok(CredentialProtectionPolicy::UserVerificationRequired), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } @@ -527,17 +524,16 @@ impl From for cbor::KeyType { impl From for cbor::Value { fn from(credential: PublicKeyCredentialSource) -> cbor::Value { - use PublicKeyCredentialSourceField::*; let mut private_key = [0u8; 32]; credential.private_key.to_bytes(&mut private_key); cbor_map_options! { - CredentialId => Some(credential.credential_id), - PrivateKey => Some(private_key.to_vec()), - RpId => Some(credential.rp_id), - UserHandle => Some(credential.user_handle), - OtherUi => credential.other_ui, - CredRandom => credential.cred_random, - CredProtectPolicy => credential.cred_protect_policy, + PublicKeyCredentialSourceField::CredentialId => Some(credential.credential_id), + PublicKeyCredentialSourceField::PrivateKey => Some(private_key.to_vec()), + PublicKeyCredentialSourceField::RpId => Some(credential.rp_id), + PublicKeyCredentialSourceField::UserHandle => Some(credential.user_handle), + PublicKeyCredentialSourceField::OtherUi => credential.other_ui, + PublicKeyCredentialSourceField::CredRandom => credential.cred_random, + PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, } } } @@ -546,18 +542,15 @@ impl TryFrom for PublicKeyCredentialSource { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - use PublicKeyCredentialSourceField::{ - CredProtectPolicy, CredRandom, CredentialId, OtherUi, PrivateKey, RpId, UserHandle, - }; destructure_cbor_map! { let { - CredentialId => credential_id, - PrivateKey => private_key, - RpId => rp_id, - UserHandle => user_handle, - OtherUi => other_ui, - CredRandom => cred_random, - CredProtectPolicy => cred_protect_policy, + PublicKeyCredentialSourceField::CredentialId => credential_id, + PublicKeyCredentialSourceField::PrivateKey => private_key, + PublicKeyCredentialSourceField::RpId => rp_id, + PublicKeyCredentialSourceField::UserHandle => user_handle, + PublicKeyCredentialSourceField::OtherUi => other_ui, + PublicKeyCredentialSourceField::CredRandom => cred_random, + PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, } = extract_map(cbor_value)?; } @@ -716,22 +709,21 @@ impl TryFrom for ClientPinSubCommand { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - use ClientPinSubCommand::*; let subcommand_int = extract_unsigned(cbor_value)?; match subcommand_int { - 0x01 => Ok(GetPinRetries), - 0x02 => Ok(GetKeyAgreement), - 0x03 => Ok(SetPin), - 0x04 => Ok(ChangePin), - 0x05 => Ok(GetPinToken), + 0x01 => Ok(ClientPinSubCommand::GetPinRetries), + 0x02 => Ok(ClientPinSubCommand::GetKeyAgreement), + 0x03 => Ok(ClientPinSubCommand::SetPin), + 0x04 => Ok(ClientPinSubCommand::ChangePin), + 0x05 => Ok(ClientPinSubCommand::GetPinToken), #[cfg(feature = "with_ctap2_1")] - 0x06 => Ok(GetPinUvAuthTokenUsingUvWithPermissions), + 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions), #[cfg(feature = "with_ctap2_1")] - 0x07 => Ok(GetUvRetries), + 0x07 => Ok(ClientPinSubCommand::GetUvRetries), #[cfg(feature = "with_ctap2_1")] - 0x08 => Ok(SetMinPinLength), + 0x08 => Ok(ClientPinSubCommand::SetMinPinLength), #[cfg(feature = "with_ctap2_1")] - 0x09 => Ok(GetPinUvAuthTokenUsingPinWithPermissions), + 0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions), #[cfg(feature = "with_ctap2_1")] _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), #[cfg(not(feature = "with_ctap2_1"))] diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 2a7fd65b..1451f54a 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -640,6 +640,7 @@ mod test { use super::*; use crypto::rng256::ThreadRng256; + // Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(persistent_store: &mut PersistentStore) { let mut pin = [0u8; 64]; pin[0] = 0x31; @@ -651,7 +652,7 @@ mod test { persistent_store.set_pin_hash(&pin_hash); } - // Fails on PINs bigger than 64 byte.. + // Fails on PINs bigger than 64 bytes. fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec) -> Vec { assert!(pin.len() <= 64); let mut padded_pin = [0u8; 64]; @@ -668,10 +669,12 @@ mod test { blocks.iter().flatten().cloned().collect::>() } + // Encrypts the dummy PIN "1234". fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { encrypt_pin(shared_secret, vec![0x31, 0x32, 0x33, 0x34]) } + // Encrypts the PIN hash corresponding to the dummy PIN "1234". fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); let mut pin = [0u8; 64]; From 77b21e9ecfc708a3859162647422b98d5b38948e Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 19 Aug 2020 19:20:41 +0200 Subject: [PATCH 18/20] improved documentation --- src/ctap/pin_protocol_v1.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 1451f54a..045a4515 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -48,9 +48,9 @@ fn verify_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bo ) } -/// Decrypts the HMAC secret salt(s) that were encrypted with the shared secret. +/// Encrypts the HMAC-secret outputs. To compute them, we first have to +/// decrypt 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], @@ -202,6 +202,7 @@ impl PinProtocolV1 { /// 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, rng: &mut impl Rng256, @@ -1079,7 +1080,7 @@ mod test { ]; assert_eq!( decrypt_pin(&aes_dec_key, new_pin_enc), - Some(vec![0x31, 0x32, 0x33, 0x34]), + Some(b"1234".to_vec()), ); // "123" @@ -1092,7 +1093,7 @@ mod test { ]; assert_eq!( decrypt_pin(&aes_dec_key, new_pin_enc), - Some(vec![0x31, 0x32, 0x33]), + Some(b"123".to_vec()), ); // Encrypted PIN is too short. From 9259102a12242689a67fb9d8f8cca3a3428be944 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 20 Aug 2020 17:22:35 +0200 Subject: [PATCH 19/20] makes tests more readable --- src/ctap/pin_protocol_v1.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 045a4515..b2c49c36 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -672,7 +672,7 @@ mod test { // Encrypts the dummy PIN "1234". fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { - encrypt_pin(shared_secret, vec![0x31, 0x32, 0x33, 0x34]) + encrypt_pin(shared_secret, b"1234".to_vec()) } // Encrypts the PIN hash corresponding to the dummy PIN "1234". @@ -1115,18 +1115,18 @@ mod test { let test_cases = vec![ // Accept PIN "1234". - (vec![0x31, 0x32, 0x33, 0x34], Ok(())), + (b"1234".to_vec(), Ok(())), // Reject PIN "123" since it is too short. ( - vec![0x31, 0x32, 0x33], + b"123".to_vec(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), ), // Reject PIN "12'\0'4" (a zero byte at index 2). ( - vec![0x31, 0x32, 0x00, 0x34], + b"12\04".to_vec(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), ), - // Reject PINs not ending in 0u8 padding. + // 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), From 690211509930a06bd20b1a8439256142af883a03 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 20 Aug 2020 18:01:52 +0200 Subject: [PATCH 20/20] updates reproducible references --- .../reference_binaries_macos-10.15.sha256sum | 10 +++++----- .../reference_binaries_ubuntu-18.04.sha256sum | 10 +++++----- reproducible/reference_elf2tab_macos-10.15.txt | 16 ++++++++-------- reproducible/reference_elf2tab_ubuntu-18.04.txt | 16 ++++++++-------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/reproducible/reference_binaries_macos-10.15.sha256sum b/reproducible/reference_binaries_macos-10.15.sha256sum index 96d67021..15e210dd 100644 --- a/reproducible/reference_binaries_macos-10.15.sha256sum +++ b/reproducible/reference_binaries_macos-10.15.sha256sum @@ -1,9 +1,9 @@ 91a98f475cb3042dd5184598a8292edb2a414df8d967a35c8f2295826b5a161b third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -33164f39a0b5354cdf61236c301242476284c6b96d55275aa603734054ca7928 target/nrf52840dk_merged.hex +7cc5c802e22e73c1edfd5b890642c5f6c4a1f888b61f0cd6d638a770eb0af739 target/nrf52840dk_merged.hex a5943c5311158b0f99370246d37782eb9b12fc36c56387eadb6587a3a4fe8fd5 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -1232b44947f302900291692690f2e94cdfb165e00e74c682433100882754a516 target/nrf52840_dongle_merged.hex +9ff31263bd33e92b5f1f59d83f557046cb4d022919a5082931a197a2f6ec4398 target/nrf52840_dongle_merged.hex 663297e3e29b9e2a972b68cea1592aaf965d797242579bb5bca09cd73cdfb637 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -b95ce848465523e98cf0c30f94f6430e99dc8ac4b33da5bc0d0f643523ff4b50 target/nrf52840_dongle_dfu_merged.hex +4b1f17b3dda2460fe83adc157f8ae1fb2559fb151b8897806e7b0aa25c898ec1 target/nrf52840_dongle_dfu_merged.hex 162a05d056aafc16d4868d5c3aa10518e41299dddd60608f96954dc9cf964cd3 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -1085e1789c4429430c47d28b23a975223717eddd7c8aa23114acbc3ec2ec7080 target/nrf52840_mdk_dfu_merged.hex -5bd063ce44e9ddcad8c4d17165a247387e4f1a9c6db81060fbb97244be1929b8 target/tab/ctap2.tab +90369c2f5c1b3b3a443114be069fd2da0806444865830a7e992ed52e036c5a39 target/nrf52840_mdk_dfu_merged.hex +299201ff87cd84bd767516143b4e6a54759e01fcd864c0e579c62217b21d4fa4 target/tab/ctap2.tab diff --git a/reproducible/reference_binaries_ubuntu-18.04.sha256sum b/reproducible/reference_binaries_ubuntu-18.04.sha256sum index 750aa649..a752d4bf 100644 --- a/reproducible/reference_binaries_ubuntu-18.04.sha256sum +++ b/reproducible/reference_binaries_ubuntu-18.04.sha256sum @@ -1,9 +1,9 @@ 3feb5d29a3d669107b460a00391440be4ebc5e50461f9ef3248714f4f99c070e third_party/tock/target/thumbv7em-none-eabi/release/nrf52840dk.bin -a02f078e165373113adbaf7fa5d272e7e01134061e8212331d54f0b0a8809aaa target/nrf52840dk_merged.hex +875fdc2bbd473a5c77c119ba860e54a43f8097c20931cc5ae83a26e9311ce124 target/nrf52840dk_merged.hex 8eebe1c1dfe22003466c2570b3735c54c58ae91b8168582ad363ab79c9230a15 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle.bin -973bf7d0b6ddb37bb9698cf8f2ef3c2a3dd27cd482b7a4c02e452902394ffa37 target/nrf52840_dongle_merged.hex +70c1249370144c6ca55ad490dc5e418f9c2994c2649941dec41d769963a0e0ad target/nrf52840_dongle_merged.hex 779d77071d1e629f92210ac313e230bcaea6d77c710210c1ac4b40f8085cdad7 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_dongle_dfu.bin -d0e7ecc1d2a45ef4c77b39720b95b3e349a0d48d7b9ca99fa591019a9f2cafee target/nrf52840_dongle_dfu_merged.hex +6c12edd4ec4d952619e976e635df39235d821eec9902e8882563fc43a1690ddb target/nrf52840_dongle_dfu_merged.hex f466490d6498f6c06c7c4a717eb437ba2fb06d1985532c23f145d38b9daa8259 third_party/tock/target/thumbv7em-none-eabi/release/nrf52840_mdk_dfu.bin -d3d4a9d3442bb8cf924f553f8df7085e3d6331f1b6d9557115d485e584285d68 target/nrf52840_mdk_dfu_merged.hex -6cda1346503867ef18d3fe7a3d32de6e22585c6134ef3347877894c5469390f5 target/tab/ctap2.tab +7b67e726071ac5161344212821b9869c8f289559c8b91a5f2a0f17624855ce8a target/nrf52840_mdk_dfu_merged.hex +4dd8753dba382bdbadd0c9761949f7bdacbd77408cfc8dc466107a81ff664b15 target/tab/ctap2.tab diff --git a/reproducible/reference_elf2tab_macos-10.15.txt b/reproducible/reference_elf2tab_macos-10.15.txt index 0b209505..25d95739 100644 --- a/reproducible/reference_elf2tab_macos-10.15.txt +++ b/reproducible/reference_elf2tab_macos-10.15.txt @@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 20 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 187792 (0x2dd90) bytes. - Adding .stack section. Offset: 187920 (0x2de10). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 178688 (0x2ba00) bytes. + Adding .stack section. Offset: 178816 (0x2ba80). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 20 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 187792 (0x2dd90) bytes. - Adding .stack section. Offset: 187920 (0x2de10). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 178688 (0x2ba00) bytes. + Adding .stack section. Offset: 178816 (0x2ba80). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 20 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 187792 (0x2dd90) bytes. - Adding .stack section. Offset: 187920 (0x2de10). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 178688 (0x2ba00) bytes. + Adding .stack section. Offset: 178816 (0x2ba80). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 20 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 187792 (0x2dd90) bytes. - Adding .stack section. Offset: 187920 (0x2de10). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 178688 (0x2ba00) bytes. + Adding .stack section. Offset: 178816 (0x2ba80). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 diff --git a/reproducible/reference_elf2tab_ubuntu-18.04.txt b/reproducible/reference_elf2tab_ubuntu-18.04.txt index efcb80ee..b652638c 100644 --- a/reproducible/reference_elf2tab_ubuntu-18.04.txt +++ b/reproducible/reference_elf2tab_ubuntu-18.04.txt @@ -5,8 +5,8 @@ Min RAM size from sections in ELF: 20 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 187736 (0x2dd58) bytes. - Adding .stack section. Offset: 187864 (0x2ddd8). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 178536 (0x2b968) bytes. + Adding .stack section. Offset: 178664 (0x2b9e8). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -24,8 +24,8 @@ Min RAM size from sections in ELF: 20 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 187736 (0x2dd58) bytes. - Adding .stack section. Offset: 187864 (0x2ddd8). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 178536 (0x2b968) bytes. + Adding .stack section. Offset: 178664 (0x2b9e8). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -43,8 +43,8 @@ Min RAM size from sections in ELF: 20 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 187736 (0x2dd58) bytes. - Adding .stack section. Offset: 187864 (0x2ddd8). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 178536 (0x2b968) bytes. + Adding .stack section. Offset: 178664 (0x2b9e8). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2 @@ -62,8 +62,8 @@ Min RAM size from sections in ELF: 20 bytes Number of writeable flash regions: 0 Adding .crt0_header section. Offset: 64 (0x40). Length: 64 (0x40) bytes. Entry point is in .text section - Adding .text section. Offset: 128 (0x80). Length: 187736 (0x2dd58) bytes. - Adding .stack section. Offset: 187864 (0x2ddd8). Length: 16384 (0x4000) bytes. + Adding .text section. Offset: 128 (0x80). Length: 178536 (0x2b968) bytes. + Adding .stack section. Offset: 178664 (0x2b9e8). Length: 16384 (0x4000) bytes. Searching for .rel.X sections to add. TBF Header: version: 2 0x2