diff --git a/src/api/customization.rs b/src/api/customization.rs index 730b884..4747212 100644 --- a/src/api/customization.rs +++ b/src/api/customization.rs @@ -68,6 +68,16 @@ pub trait Customization { /// the minimum PIN length with the minPinLength extension. fn default_min_pin_length_rp_ids(&self) -> &[&str]; + /// Enforces the alwaysUv option. + /// + /// When setting to true, commands require a PIN. + /// Also, alwaysUv can not be disabled by commands. + /// + /// A certification (additional to FIDO Alliance's) might require enforcing + /// alwaysUv. Otherwise, users should have the choice to configure alwaysUv. + /// Calling toggleAlwaysUv is preferred over enforcing alwaysUv here. + fn enforce_always_uv(&self) -> bool; + /// Maximum message size send for CTAP commands. /// /// The maximum value is 7609, as HID packets can not encode longer messages. @@ -77,6 +87,28 @@ pub trait Customization { /// this value. fn max_msg_size(&self) -> usize; + /// Sets the number of consecutive failed PINs before blocking interaction. + /// + /// # Invariant + /// + /// - CTAP2.0: Maximum PIN retries must be 8. + /// - CTAP2.1: Maximum PIN retries must be 8 at most. + /// + /// The fail retry counter is reset after entering the correct PIN. + fn max_pin_retries(&self) -> u8; + + /// Enables or disables signature counters. + /// + /// The signature counter is currently implemented as a global counter. + /// The specification strongly suggests to have per-credential counters. + /// Implementing those means you can't have an infinite amount of server-side + /// credentials anymore. Also, since counters need frequent writes on the + /// persistent storage, we might need a flash friendly implementation. This + /// solution is a compromise to be compatible with U2F and not wasting storage. + /// + /// https://www.w3.org/TR/webauthn/#signature-counter + fn use_signature_counter(&self) -> bool; + // ########################################################################### // Constants for performance optimization or adapting to different hardware. // @@ -102,18 +134,24 @@ pub trait Customization { #[derive(Clone)] pub struct CustomizationImpl { + pub default_cred_protect: Option, pub default_min_pin_length: u8, pub default_min_pin_length_rp_ids: &'static [&'static str], - pub default_cred_protect: Option, + pub enforce_always_uv: bool, pub max_msg_size: usize, + pub max_pin_retries: u8, + pub use_signature_counter: bool, pub max_rp_ids_length: usize, } pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl { default_min_pin_length: 4, default_min_pin_length_rp_ids: &[], + enforce_always_uv: false, default_cred_protect: None, max_msg_size: 7609, + max_pin_retries: 8, + use_signature_counter: true, max_rp_ids_length: 8, }; @@ -130,10 +168,22 @@ impl Customization for CustomizationImpl { self.default_min_pin_length_rp_ids } + fn enforce_always_uv(&self) -> bool { + self.enforce_always_uv + } + fn max_msg_size(&self) -> usize { self.max_msg_size } + fn max_pin_retries(&self) -> u8 { + self.max_pin_retries + } + + fn use_signature_counter(&self) -> bool { + self.use_signature_counter + } + fn max_rp_ids_length(&self) -> usize { self.max_rp_ids_length } @@ -151,6 +201,11 @@ pub fn is_valid(customization: &impl Customization) -> bool { return false; } + // Max pin retries must be less or equal than 8. + if customization.max_pin_retries() > 8 { + return false; + } + // Default min pin length rp ids must be non-empty if max rp ids length is 0. if customization.max_rp_ids_length() == 0 && customization.default_min_pin_length_rp_ids().is_empty() diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 6cb1948..c895faa 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -123,7 +123,7 @@ pub fn process_config( #[cfg(test)] mod test { use super::*; - use crate::ctap::customization::ENFORCE_ALWAYS_UV; + use crate::api::customization::Customization; use crate::ctap::data_formats::PinUvAuthProtocol; use crate::ctap::pin_protocol::authenticate_pin_uv_auth_token; use crate::env::test::TestEnv; @@ -180,7 +180,7 @@ mod test { pin_uv_auth_protocol: None, }; let config_response = process_config(&mut env, &mut client_pin, config_params); - if ENFORCE_ALWAYS_UV { + if env.customization().enforce_always_uv() { assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) @@ -210,7 +210,7 @@ mod test { pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let config_response = process_config(&mut env, &mut client_pin, config_params); - if ENFORCE_ALWAYS_UV { + if env.customization().enforce_always_uv() { assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index c1e08d6..42c0e23 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -344,8 +344,9 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::{key_material, CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER}; + use super::super::{key_material, CREDENTIAL_ID_SIZE}; use super::*; + use crate::api::customization::Customization; use crate::clock::TEST_CLOCK_FREQUENCY_HZ; use crate::env::test::TestEnv; use crypto::Hash256; @@ -643,8 +644,8 @@ mod test { assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA)); } - fn check_signature_counter(response: &[u8; 4], signature_counter: u32) { - if USE_SIGNATURE_COUNTER { + fn check_signature_counter(env: &mut impl Env, response: &[u8; 4], signature_counter: u32) { + if env.customization().use_signature_counter() { assert_eq!(u32::from_be_bytes(*response), signature_counter); } else { assert_eq!(response, &[0x00, 0x00, 0x00, 0x00]); @@ -673,9 +674,11 @@ mod test { Ctap1Command::process_command(&mut env, &message, &mut ctap_state, CtapInstant::new(0)) .unwrap(); assert_eq!(response[0], 0x01); + let global_signature_counter = storage::global_signature_counter(&mut env).unwrap(); check_signature_counter( + &mut env, array_ref!(response, 1, 4), - storage::global_signature_counter(&mut env).unwrap(), + global_signature_counter, ); } @@ -706,9 +709,11 @@ mod test { ) .unwrap(); assert_eq!(response[0], 0x01); + let global_signature_counter = storage::global_signature_counter(&mut env).unwrap(); check_signature_counter( + &mut env, array_ref!(response, 1, 4), - storage::global_signature_counter(&mut env).unwrap(), + global_signature_counter, ); } diff --git a/src/ctap/customization.rs b/src/ctap/customization.rs index 60ef61e..d5976c9 100644 --- a/src/ctap/customization.rs +++ b/src/ctap/customization.rs @@ -19,16 +19,6 @@ use crate::ctap::data_formats::EnterpriseAttestationMode; -/// Enforces the alwaysUv option. -/// -/// When setting to true, commands require a PIN. -/// Also, alwaysUv can not be disabled by commands. -/// -/// A certification (additional to FIDO Alliance's) might require enforcing -/// alwaysUv. Otherwise, users should have the choice to configure alwaysUv. -/// Calling toggleAlwaysUv is preferred over enforcing alwaysUv here. -pub const ENFORCE_ALWAYS_UV: bool = false; - /// Allows usage of enterprise attestation. /// /// # Invariant @@ -71,16 +61,6 @@ pub const ENTERPRISE_ATTESTATION_MODE: Option = None; /// VendorFacilitated. pub const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; -/// Sets the number of consecutive failed PINs before blocking interaction. -/// -/// # Invariant -/// -/// - CTAP2.0: Maximum PIN retries must be 8. -/// - CTAP2.1: Maximum PIN retries must be 8 at most. -/// -/// The fail retry counter is reset after entering the correct PIN. -pub const MAX_PIN_RETRIES: u8 = 8; - /// Enables or disables basic attestation for FIDO2. /// /// # Invariant @@ -97,18 +77,6 @@ pub const MAX_PIN_RETRIES: u8 = 8; /// https://www.w3.org/TR/webauthn/#attestation pub const USE_BATCH_ATTESTATION: bool = false; -/// Enables or disables signature counters. -/// -/// The signature counter is currently implemented as a global counter. -/// The specification strongly suggests to have per-credential counters. -/// Implementing those means you can't have an infinite amount of server-side -/// credentials anymore. Also, since counters need frequent writes on the -/// persistent storage, we might need a flash friendly implementation. This -/// solution is a compromise to be compatible with U2F and not wasting storage. -/// -/// https://www.w3.org/TR/webauthn/#signature-counter -pub const USE_SIGNATURE_COUNTER: bool = true; - // ########################################################################### // Constants for performance optimization or adapting to different hardware. // @@ -178,7 +146,6 @@ mod test { } else { assert!(ENTERPRISE_RP_ID_LIST.is_empty()); } - assert!(MAX_PIN_RETRIES <= 8); assert!(MAX_CRED_BLOB_LENGTH >= 32); if let Some(count) = MAX_CREDENTIAL_COUNT_IN_LIST { assert!(count >= 1); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index a3dc945..5a4fd87 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -45,7 +45,7 @@ use self::credential_management::process_credential_management; use self::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt}; use self::customization::{ ENTERPRISE_ATTESTATION_MODE, ENTERPRISE_RP_ID_LIST, MAX_CREDENTIAL_COUNT_IN_LIST, - MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, USE_BATCH_ATTESTATION, USE_SIGNATURE_COUNTER, + MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, USE_BATCH_ATTESTATION, }; use self::data_formats::{ AuthenticatorTransport, CoseKey, CoseSignature, CredentialProtectionPolicy, @@ -421,7 +421,7 @@ impl CtapState { &mut self, env: &mut impl Env, ) -> Result<(), Ctap2StatusCode> { - if USE_SIGNATURE_COUNTER { + if env.customization().use_signature_counter() { let increment = env.rng().gen_uniform_u32x8()[0] % 8 + 1; storage::incr_global_signature_counter(env, increment)?; } @@ -1398,7 +1398,7 @@ impl CtapState { let mut auth_data = vec![]; auth_data.extend(rp_id_hash); auth_data.push(flag_byte); - // The global counter is only increased if USE_SIGNATURE_COUNTER is true. + // The global counter is only increased if use_signature_counter() is true. // It uses a big-endian representation. let mut signature_counter = [0u8; 4]; BigEndian::write_u32( diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b2d5440..19782d4 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -16,9 +16,7 @@ mod key; use crate::api::customization::Customization; use crate::ctap::client_pin::PIN_AUTH_LENGTH; -use crate::ctap::customization::{ - ENFORCE_ALWAYS_UV, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_PIN_RETRIES, MAX_SUPPORTED_RESIDENT_KEYS, -}; +use crate::ctap::customization::{MAX_LARGE_BLOB_ARRAY_SIZE, MAX_SUPPORTED_RESIDENT_KEYS}; use crate::ctap::data_formats::{ extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialUserEntity, @@ -360,7 +358,7 @@ pub fn set_pin( /// Returns the number of remaining PIN retries. pub fn pin_retries(env: &mut impl Env) -> Result { match env.store().find(key::PIN_RETRIES)? { - None => Ok(MAX_PIN_RETRIES), + None => Ok(env.customization().max_pin_retries()), Some(value) if value.len() == 1 => Ok(value[0]), _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } @@ -596,7 +594,7 @@ pub fn enable_enterprise_attestation(env: &mut impl Env) -> Result<(), Ctap2Stat /// Returns whether alwaysUv is enabled. pub fn has_always_uv(env: &mut impl Env) -> Result { - if ENFORCE_ALWAYS_UV { + if env.customization().enforce_always_uv() { return Ok(true); } match env.store().find(key::ALWAYS_UV)? { @@ -608,7 +606,7 @@ pub fn has_always_uv(env: &mut impl Env) -> Result { /// Enables alwaysUv, when disabled, and vice versa. pub fn toggle_always_uv(env: &mut impl Env) -> Result<(), Ctap2StatusCode> { - if ENFORCE_ALWAYS_UV { + if env.customization().enforce_always_uv() { return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); } if has_always_uv(env)? { @@ -1061,10 +1059,13 @@ mod test { let mut env = TestEnv::new(); // The pin retries is initially at the maximum. - assert_eq!(pin_retries(&mut env), Ok(MAX_PIN_RETRIES)); + assert_eq!( + pin_retries(&mut env), + Ok(env.customization().max_pin_retries()) + ); // Decrementing the pin retries decrements the pin retries. - for retries in (0..MAX_PIN_RETRIES).rev() { + for retries in (0..env.customization().max_pin_retries()).rev() { decr_pin_retries(&mut env).unwrap(); assert_eq!(pin_retries(&mut env), Ok(retries)); } @@ -1075,7 +1076,10 @@ mod test { // Resetting the pin retries resets the pin retries. reset_pin_retries(&mut env).unwrap(); - assert_eq!(pin_retries(&mut env), Ok(MAX_PIN_RETRIES)); + assert_eq!( + pin_retries(&mut env), + Ok(env.customization().max_pin_retries()) + ); } #[test] @@ -1250,7 +1254,7 @@ mod test { fn test_always_uv() { let mut env = TestEnv::new(); - if ENFORCE_ALWAYS_UV { + if env.customization().enforce_always_uv() { assert!(has_always_uv(&mut env).unwrap()); assert_eq!( toggle_always_uv(&mut env), diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 926c19d..7ee0b53 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -115,7 +115,7 @@ make_partition! { /// The number of PIN retries. /// - /// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`. + /// If the entry is absent, the number of PIN retries is `Customization::max_pin_retries()`. PIN_RETRIES = 2044; /// The PIN hash and length. diff --git a/src/env/test/customization.rs b/src/env/test/customization.rs index 8180e68..79bea19 100644 --- a/src/env/test/customization.rs +++ b/src/env/test/customization.rs @@ -4,11 +4,14 @@ use crate::api::customization::{Customization, CustomizationImpl}; use crate::ctap::data_formats::CredentialProtectionPolicy; pub struct TestCustomization { + pub default_cred_protect: Option, pub default_min_pin_length: u8, default_min_pin_length_rp_ids_backing_store: Vec, default_min_pin_length_rp_ids: Vec<*const str>, - pub default_cred_protect: Option, + pub enforce_always_uv: bool, pub max_msg_size: usize, + pub max_pin_retries: u8, + pub use_signature_counter: bool, pub max_rp_ids_length: usize, } @@ -38,10 +41,22 @@ impl Customization for TestCustomization { unsafe { from_raw_parts(rp_ids, length) } } + fn enforce_always_uv(&self) -> bool { + self.enforce_always_uv + } + fn max_msg_size(&self) -> usize { self.max_msg_size } + fn max_pin_retries(&self) -> u8 { + self.max_pin_retries + } + + fn use_signature_counter(&self) -> bool { + self.use_signature_counter + } + fn max_rp_ids_length(&self) -> usize { self.max_rp_ids_length } @@ -50,10 +65,13 @@ impl Customization for TestCustomization { impl From for TestCustomization { fn from(c: CustomizationImpl) -> Self { let CustomizationImpl { + default_cred_protect, default_min_pin_length, default_min_pin_length_rp_ids, - default_cred_protect, + enforce_always_uv, max_msg_size, + max_pin_retries, + use_signature_counter, max_rp_ids_length, } = c; @@ -63,11 +81,14 @@ impl From for TestCustomization { .collect::>(); let mut ret = Self { + default_cred_protect, default_min_pin_length, default_min_pin_length_rp_ids_backing_store, default_min_pin_length_rp_ids: vec![], - default_cred_protect, + enforce_always_uv, max_msg_size, + max_pin_retries, + use_signature_counter, max_rp_ids_length, };