From 26595db8102cb35819907a235a5690ddc3df9a85 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 26 Jun 2020 14:30:27 +0200 Subject: [PATCH] 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 6e68ba1..8e31796 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 f0b08e4..c541f1f 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 6ea0672..2f072ee 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 442a855..560e6d1 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 0a4548d..bbd591d 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,14 +305,23 @@ 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 a730cd6..b4265a8 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); + } }