// 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. #[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::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; use crate::embedded_flash::{self, StoreConfig, StoreEntry, StoreError}; use alloc::string::String; #[cfg(any(test, feature = "ram_storage", feature = "with_ctap2_1"))] use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; #[cfg(feature = "with_ctap2_1")] use cbor::cbor_array_vec; use core::convert::TryInto; use crypto::rng256::Rng256; #[cfg(any(test, feature = "ram_storage"))] type Storage = embedded_flash::BufferStorage; #[cfg(not(any(test, feature = "ram_storage")))] type Storage = embedded_flash::SyscallStorage; // Those constants may be modified before compilation to tune the behavior of the key. // // The number of pages should be at least 2 and at most what the flash can hold. There should be no // reason to put a small number here, except that the latency of flash operations depends on the // number of pages. This will improve in the future. Currently, using 20 pages gives 65ms per // operation. The rule of thumb is 3.5ms per additional page. // // Limiting the number of residential keys permits to ensure a minimum number of counter increments. // Let: // - P the number of pages (NUM_PAGES) // - K the maximum number of residential keys (MAX_SUPPORTED_RESIDENTIAL_KEYS) // - S the maximum size of a residential key (about 500) // - C the number of erase cycles (10000) // - I the minimum number of counter increments // // We have: I = ((P - 1) * 4092 - K * S) / 12 * C // // With P=20 and K=150, we have I > 2M which is enough for 500 increments per day for 10 years. #[cfg(feature = "ram_storage")] const NUM_PAGES: usize = 2; #[cfg(not(feature = "ram_storage"))] const NUM_PAGES: usize = 20; const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; // List of tags. They should all be unique. And there should be less than NUM_TAGS. const TAG_CREDENTIAL: usize = 0; const GLOBAL_SIGNATURE_COUNTER: usize = 1; const MASTER_KEYS: usize = 2; const PIN_HASH: usize = 3; const PIN_RETRIES: usize = 4; const ATTESTATION_PRIVATE_KEY: usize = 5; const ATTESTATION_CERTIFICATE: usize = 6; const AAGUID: usize = 7; #[cfg(feature = "with_ctap2_1")] const MIN_PIN_LENGTH: usize = 8; #[cfg(feature = "with_ctap2_1")] const MIN_PIN_LENGTH_RP_IDS: usize = 9; // 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 = 8; #[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. #[cfg(feature = "with_ctap2_1")] const _MAX_RP_IDS_LENGTH: usize = 8; #[allow(clippy::enum_variant_names)] #[derive(PartialEq, Eq, PartialOrd, Ord)] enum Key { // TODO(cretin): Test whether this doesn't consume too much memory. Otherwise, we can use less // keys. Either only a simple enum value for all credentials, or group by rp_id. Credential { rp_id: Option, credential_id: Option>, user_handle: Option>, }, GlobalSignatureCounter, MasterKeys, PinHash, PinRetries, AttestationPrivateKey, AttestationCertificate, Aaguid, #[cfg(feature = "with_ctap2_1")] MinPinLength, #[cfg(feature = "with_ctap2_1")] MinPinLengthRpIds, } pub struct MasterKeys { pub encryption: [u8; 32], pub hmac: [u8; 32], } struct Config; impl StoreConfig for Config { type Key = Key; fn num_tags(&self) -> usize { NUM_TAGS } fn keys(&self, entry: StoreEntry, mut add: impl FnMut(Key)) { match entry.tag { TAG_CREDENTIAL => { let credential = match deserialize_credential(entry.data) { None => { debug_assert!(false); return; } Some(credential) => credential, }; add(Key::Credential { rp_id: Some(credential.rp_id.clone()), credential_id: Some(credential.credential_id), user_handle: None, }); add(Key::Credential { rp_id: Some(credential.rp_id.clone()), credential_id: None, user_handle: None, }); add(Key::Credential { rp_id: Some(credential.rp_id), credential_id: None, user_handle: Some(credential.user_handle), }); add(Key::Credential { rp_id: None, credential_id: None, user_handle: None, }); } GLOBAL_SIGNATURE_COUNTER => add(Key::GlobalSignatureCounter), MASTER_KEYS => add(Key::MasterKeys), PIN_HASH => add(Key::PinHash), PIN_RETRIES => add(Key::PinRetries), 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), } } } pub struct PersistentStore { store: embedded_flash::Store, } impl PersistentStore { /// Gives access to the persistent store. /// /// # Safety /// /// This should be at most one instance of persistent store per program lifetime. pub fn new(rng: &mut impl Rng256) -> PersistentStore { #[cfg(not(any(test, feature = "ram_storage")))] let storage = PersistentStore::new_prod_storage(); #[cfg(any(test, feature = "ram_storage"))] let storage = PersistentStore::new_test_storage(); let mut store = PersistentStore { store: embedded_flash::Store::new(storage, Config).unwrap(), }; store.init(rng); store } #[cfg(not(any(test, feature = "ram_storage")))] fn new_prod_storage() -> Storage { Storage::new(NUM_PAGES).unwrap() } #[cfg(any(test, feature = "ram_storage"))] fn new_test_storage() -> Storage { #[cfg(not(test))] const PAGE_SIZE: usize = 0x100; #[cfg(test)] const PAGE_SIZE: usize = 0x1000; let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); let options = embedded_flash::BufferOptions { word_size: 4, page_size: PAGE_SIZE, max_word_writes: 2, max_page_erases: 10000, strict_write: true, }; Storage::new(store, options) } fn init(&mut self, rng: &mut impl Rng256) { if self.store.find_one(&Key::MasterKeys).is_none() { let master_encryption_key = rng.gen_uniform_u8x32(); let master_hmac_key = rng.gen_uniform_u8x32(); let mut master_keys = Vec::with_capacity(64); master_keys.extend_from_slice(&master_encryption_key); master_keys.extend_from_slice(&master_hmac_key); self.store .insert(StoreEntry { tag: MASTER_KEYS, data: &master_keys, sensitive: true, }) .unwrap(); } // TODO(jmichel): remove this when vendor command is in place #[cfg(not(test))] self.load_attestation_data_from_firmware(); if self.store.find_one(&Key::Aaguid).is_none() { self.set_aaguid(key_material::AAGUID).unwrap(); } } // TODO(jmichel): remove this function when vendor command is in place. #[cfg(not(test))] fn load_attestation_data_from_firmware(&mut self) { // The following 2 entries are meant to be written by vendor-specific commands. if self.store.find_one(&Key::AttestationPrivateKey).is_none() { self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) .unwrap(); } if self.store.find_one(&Key::AttestationCertificate).is_none() { self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) .unwrap(); } } pub fn find_credential( &self, rp_id: &str, credential_id: &[u8], check_cred_protect: bool, ) -> Result, Ctap2StatusCode> { let key = Key::Credential { rp_id: Some(rp_id.into()), credential_id: Some(credential_id.into()), user_handle: None, }; let entry = match self.store.find_one(&key) { None => return Ok(None), Some((_, entry)) => entry, }; debug_assert_eq!(entry.tag, TAG_CREDENTIAL); let result = deserialize_credential(entry.data); debug_assert!(result.is_some()); let user_verification_required = result.as_ref().map_or(false, |cred| { cred.cred_protect_policy == Some(CredentialProtectionPolicy::UserVerificationRequired) }); if check_cred_protect && user_verification_required { Ok(None) } else { Ok(result) } } pub fn store_credential( &mut self, credential: PublicKeyCredentialSource, ) -> Result<(), Ctap2StatusCode> { let key = Key::Credential { rp_id: Some(credential.rp_id.clone()), credential_id: None, user_handle: Some(credential.user_handle.clone()), }; let old_entry = self.store.find_one(&key); if old_entry.is_none() && self.count_credentials()? >= MAX_SUPPORTED_RESIDENTIAL_KEYS { return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); } let credential = serialize_credential(credential)?; let new_entry = StoreEntry { tag: TAG_CREDENTIAL, data: &credential, sensitive: true, }; match old_entry { None => self.store.insert(new_entry)?, Some((index, old_entry)) => { debug_assert_eq!(old_entry.tag, TAG_CREDENTIAL); self.store.replace(index, new_entry)? } }; Ok(()) } pub fn filter_credential( &self, rp_id: &str, check_cred_protect: bool, ) -> Result, Ctap2StatusCode> { Ok(self .store .find_all(&Key::Credential { rp_id: Some(rp_id.into()), credential_id: None, user_handle: None, }) .filter_map(|(_, entry)| { debug_assert_eq!(entry.tag, TAG_CREDENTIAL); let credential = deserialize_credential(entry.data); debug_assert!(credential.is_some()); credential }) .filter(|cred| !check_cred_protect || cred.is_discoverable()) .collect()) } pub fn count_credentials(&self) -> Result { Ok(self .store .find_all(&Key::Credential { rp_id: None, credential_id: None, user_handle: None, }) .count()) } pub fn new_creation_order(&self) -> Result { Ok(self .store .find_all(&Key::Credential { rp_id: None, credential_id: None, user_handle: None, }) .filter_map(|(_, entry)| { debug_assert_eq!(entry.tag, TAG_CREDENTIAL); let credential = deserialize_credential(entry.data); debug_assert!(credential.is_some()); credential }) .map(|c| c.creation_order) .max() .unwrap_or(0) .wrapping_add(1)) } pub fn global_signature_counter(&self) -> Result { Ok(self .store .find_one(&Key::GlobalSignatureCounter) .map_or(0, |(_, entry)| { u32::from_ne_bytes(*array_ref!(entry.data, 0, 4)) })) } pub fn incr_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { let mut buffer = [0; core::mem::size_of::()]; match self.store.find_one(&Key::GlobalSignatureCounter) { None => { buffer.copy_from_slice(&1u32.to_ne_bytes()); self.store.insert(StoreEntry { tag: GLOBAL_SIGNATURE_COUNTER, data: &buffer, sensitive: false, })?; } Some((index, entry)) => { let value = u32::from_ne_bytes(*array_ref!(entry.data, 0, 4)); // In hopes that servers handle the wrapping gracefully. buffer.copy_from_slice(&value.wrapping_add(1).to_ne_bytes()); self.store.replace( index, StoreEntry { tag: GLOBAL_SIGNATURE_COUNTER, data: &buffer, sensitive: false, }, )?; } } Ok(()) } pub fn master_keys(&self) -> Result { let (_, entry) = self.store.find_one(&Key::MasterKeys).unwrap(); if entry.data.len() != 64 { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } Ok(MasterKeys { encryption: *array_ref![entry.data, 0, 32], hmac: *array_ref![entry.data, 32, 32], }) } pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { let data = match self.store.find_one(&Key::PinHash) { None => return Ok(None), Some((_, entry)) => entry.data, }; if data.len() != PIN_AUTH_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } Ok(Some(*array_ref![data, 0, PIN_AUTH_LENGTH])) } pub fn set_pin_hash( &mut self, pin_hash: &[u8; PIN_AUTH_LENGTH], ) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: PIN_HASH, data: pin_hash, sensitive: true, }; match self.store.find_one(&Key::PinHash) { None => self.store.insert(entry)?, Some((index, _)) => self.store.replace(index, entry)?, } Ok(()) } pub fn pin_retries(&self) -> Result { Ok(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) -> Result<(), Ctap2StatusCode> { match self.store.find_one(&Key::PinRetries) { None => { self.store.insert(StoreEntry { tag: PIN_RETRIES, data: &[MAX_PIN_RETRIES.saturating_sub(1)], sensitive: false, })?; } Some((index, entry)) => { debug_assert_eq!(entry.data.len(), 1); if entry.data[0] == 0 { return Ok(()); } let new_value = entry.data[0].saturating_sub(1); self.store.replace( index, StoreEntry { tag: PIN_RETRIES, data: &[new_value], sensitive: false, }, )?; } } Ok(()) } pub fn reset_pin_retries(&mut self) -> Result<(), Ctap2StatusCode> { if let Some((index, _)) = self.store.find_one(&Key::PinRetries) { self.store.delete(index)?; } Ok(()) } #[cfg(feature = "with_ctap2_1")] pub fn min_pin_length(&self) -> Result { Ok(self .store .find_one(&Key::MinPinLength) .map_or(DEFAULT_MIN_PIN_LENGTH, |(_, entry)| { debug_assert_eq!(entry.data.len(), 1); entry.data[0] })) } #[cfg(feature = "with_ctap2_1")] pub fn set_min_pin_length(&mut self, min_pin_length: u8) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: MIN_PIN_LENGTH, data: &[min_pin_length], sensitive: false, }; Ok(match self.store.find_one(&Key::MinPinLength) { None => self.store.insert(entry)?, Some((index, _)) => self.store.replace(index, entry)?, }) } #[cfg(feature = "with_ctap2_1")] pub fn _min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { 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()); Ok(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( &self, ) -> Result, Ctap2StatusCode> { let data = match self.store.find_one(&Key::AttestationPrivateKey) { None => return Ok(None), Some((_, entry)) => entry.data, }; if data.len() != key_material::ATTESTATION_PRIVATE_KEY_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } Ok(Some(array_ref!( data, 0, key_material::ATTESTATION_PRIVATE_KEY_LENGTH ))) } pub fn set_attestation_private_key( &mut self, attestation_private_key: &[u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], ) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: ATTESTATION_PRIVATE_KEY, data: attestation_private_key, sensitive: true, }; match self.store.find_one(&Key::AttestationPrivateKey) { None => self.store.insert(entry)?, _ => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } Ok(()) } pub fn attestation_certificate(&self) -> Result>, Ctap2StatusCode> { let data = match self.store.find_one(&Key::AttestationCertificate) { None => return Ok(None), Some((_, entry)) => entry.data, }; Ok(Some(data.to_vec())) } pub fn set_attestation_certificate( &mut self, attestation_certificate: &[u8], ) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: ATTESTATION_CERTIFICATE, data: attestation_certificate, sensitive: false, }; match self.store.find_one(&Key::AttestationCertificate) { None => self.store.insert(entry)?, _ => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } Ok(()) } pub fn aaguid(&self) -> Result<[u8; key_material::AAGUID_LENGTH], Ctap2StatusCode> { let (_, entry) = self .store .find_one(&Key::Aaguid) .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; let data = entry.data; if data.len() != key_material::AAGUID_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } Ok(*array_ref![data, 0, key_material::AAGUID_LENGTH]) } pub fn set_aaguid( &mut self, aaguid: &[u8; key_material::AAGUID_LENGTH], ) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: AAGUID, data: aaguid, sensitive: false, }; match self.store.find_one(&Key::Aaguid) { None => self.store.insert(entry)?, Some((index, _)) => self.store.replace(index, entry)?, } Ok(()) } pub fn reset(&mut self, rng: &mut impl Rng256) -> Result<(), Ctap2StatusCode> { loop { let index = { let mut iter = self.store.iter().filter(|(_, entry)| should_reset(entry)); match iter.next() { None => break, Some((index, _)) => index, } }; self.store.delete(index)?; } self.init(rng); Ok(()) } } impl From for Ctap2StatusCode { fn from(error: StoreError) -> Ctap2StatusCode { match error { StoreError::StoreFull => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, StoreError::InvalidTag => unreachable!(), StoreError::InvalidPrecondition => unreachable!(), } } } fn should_reset(entry: &StoreEntry<'_>) -> bool { match entry.tag { ATTESTATION_PRIVATE_KEY | ATTESTATION_CERTIFICATE | AAGUID => false, _ => true, } } fn deserialize_credential(data: &[u8]) -> Option { let cbor = cbor::read(data).ok()?; cbor.try_into().ok() } fn serialize_credential(credential: PublicKeyCredentialSource) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); if cbor::write(credential.into(), &mut data) { Ok(data) } else { 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) } } #[cfg(test)] mod test { use super::*; use crate::ctap::data_formats::{PublicKeyCredentialSource, PublicKeyCredentialType}; use crypto::rng256::{Rng256, ThreadRng256}; fn create_credential_source( rng: &mut ThreadRng256, rp_id: &str, user_handle: Vec, ) -> PublicKeyCredentialSource { let private_key = crypto::ecdsa::SecKey::gensk(rng); PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id: rng.gen_uniform_u8x32().to_vec(), private_key, rp_id: String::from(rp_id), user_handle, user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, user_icon: None, } } #[test] fn format_overhead() { // nRF52840 NVMC const WORD_SIZE: usize = 4; const PAGE_SIZE: usize = 0x1000; const NUM_PAGES: usize = 100; let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); let options = embedded_flash::BufferOptions { word_size: WORD_SIZE, page_size: PAGE_SIZE, max_word_writes: 2, max_page_erases: 10000, strict_write: true, }; let storage = Storage::new(store, options); let store = embedded_flash::Store::new(storage, Config).unwrap(); // We can replace 3 bytes with minimal overhead. assert_eq!(store.replace_len(false, 0), 2 * WORD_SIZE); assert_eq!(store.replace_len(false, 3), 3 * WORD_SIZE); assert_eq!(store.replace_len(false, 4), 3 * WORD_SIZE); } #[test] fn test_store() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); let credential_source = create_credential_source(&mut rng, "example.com", vec![]); assert!(persistent_store.store_credential(credential_source).is_ok()); assert!(persistent_store.count_credentials().unwrap() > 0); } #[test] fn test_credential_order() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); let credential_source = create_credential_source(&mut rng, "example.com", vec![]); let current_latest_creation = credential_source.creation_order; assert!(persistent_store.store_credential(credential_source).is_ok()); let mut credential_source = create_credential_source(&mut rng, "example.com", vec![]); credential_source.creation_order = persistent_store.new_creation_order().unwrap(); assert!(credential_source.creation_order > current_latest_creation); let current_latest_creation = credential_source.creation_order; assert!(persistent_store.store_credential(credential_source).is_ok()); assert!(persistent_store.new_creation_order().unwrap() > current_latest_creation); } #[test] #[allow(clippy::assertions_on_constants)] fn test_fill_store() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); // To make this test work for bigger storages, implement better int -> Vec conversion. assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { let credential_source = create_credential_source(&mut rng, "example.com", vec![i as u8]); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); } let credential_source = create_credential_source( &mut rng, "example.com", vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], ); assert_eq!( persistent_store.store_credential(credential_source), Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL) ); assert_eq!( persistent_store.count_credentials().unwrap(), MAX_SUPPORTED_RESIDENTIAL_KEYS ); } #[test] #[allow(clippy::assertions_on_constants)] fn test_overwrite() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); // These should have different IDs. let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x00]); let expected_credential = credential_source1.clone(); assert!(persistent_store .store_credential(credential_source0) .is_ok()); assert!(persistent_store .store_credential(credential_source1) .is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), 1); assert_eq!( &persistent_store .filter_credential("example.com", false) .unwrap(), &[expected_credential] ); // To make this test work for bigger storages, implement better int -> Vec conversion. assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { let credential_source = create_credential_source(&mut rng, "example.com", vec![i as u8]); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); } let credential_source = create_credential_source( &mut rng, "example.com", vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], ); assert_eq!( persistent_store.store_credential(credential_source), Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL) ); assert_eq!( persistent_store.count_credentials().unwrap(), MAX_SUPPORTED_RESIDENTIAL_KEYS ); } #[test] fn test_filter() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x01]); let credential_source2 = create_credential_source(&mut rng, "another.example.com", vec![0x02]); let id0 = credential_source0.credential_id.clone(); let id1 = credential_source1.credential_id.clone(); assert!(persistent_store .store_credential(credential_source0) .is_ok()); assert!(persistent_store .store_credential(credential_source1) .is_ok()); assert!(persistent_store .store_credential(credential_source2) .is_ok()); let filtered_credentials = persistent_store .filter_credential("example.com", false) .unwrap(); assert_eq!(filtered_credentials.len(), 2); assert!( (filtered_credentials[0].credential_id == id0 && filtered_credentials[1].credential_id == id1) || (filtered_credentials[1].credential_id == id0 && filtered_credentials[0].credential_id == id1) ); } #[test] fn test_filter_with_cred_protect() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); 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], user_display_name: None, cred_random: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), creation_order: 0, user_name: None, user_icon: None, }; assert!(persistent_store.store_credential(credential).is_ok()); let no_credential = persistent_store .filter_credential("example.com", true) .unwrap(); assert_eq!(no_credential, vec![]); } #[test] fn test_find() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); let credential_source0 = create_credential_source(&mut rng, "example.com", vec![0x00]); let credential_source1 = create_credential_source(&mut rng, "example.com", vec![0x01]); let id0 = credential_source0.credential_id.clone(); let key0 = credential_source0.private_key.clone(); assert!(persistent_store .store_credential(credential_source0) .is_ok()); assert!(persistent_store .store_credential(credential_source1) .is_ok()); let no_credential = persistent_store .find_credential("another.example.com", &id0, false) .unwrap(); assert_eq!(no_credential, None); let found_credential = persistent_store .find_credential("example.com", &id0, false) .unwrap(); let expected_credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id: id0, private_key: key0, rp_id: String::from("example.com"), user_handle: vec![0x00], user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, user_icon: None, }; assert_eq!(found_credential, Some(expected_credential)); } #[test] fn test_find_with_cred_protect() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); 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], user_display_name: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), creation_order: 0, user_name: None, user_icon: None, }; assert!(persistent_store.store_credential(credential).is_ok()); let no_credential = persistent_store .find_credential("example.com", &[0x00], true) .unwrap(); assert_eq!(no_credential, None); } #[test] fn test_master_keys() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); // Master keys stay the same between resets. let master_keys_1 = persistent_store.master_keys().unwrap(); let master_keys_2 = persistent_store.master_keys().unwrap(); assert_eq!(master_keys_2.encryption, master_keys_1.encryption); assert_eq!(master_keys_2.hmac, master_keys_1.hmac); // Master keys change after reset. This test may fail if the random generator produces the // same keys. let master_encryption_key = master_keys_1.encryption.to_vec(); let master_hmac_key = master_keys_1.hmac.to_vec(); persistent_store.reset(&mut rng).unwrap(); let master_keys_3 = persistent_store.master_keys().unwrap(); assert!(master_keys_3.encryption != master_encryption_key.as_slice()); assert!(master_keys_3.hmac != master_hmac_key.as_slice()); } #[test] fn test_pin_hash() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); // Pin hash is initially not set. assert!(persistent_store.pin_hash().unwrap().is_none()); // Setting the pin hash sets the pin hash. let random_data = rng.gen_uniform_u8x32(); assert_eq!(random_data.len(), 2 * PIN_AUTH_LENGTH); let pin_hash_1 = *array_ref!(random_data, 0, PIN_AUTH_LENGTH); let pin_hash_2 = *array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH); persistent_store.set_pin_hash(&pin_hash_1).unwrap(); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_1)); persistent_store.set_pin_hash(&pin_hash_2).unwrap(); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); // Resetting the storage resets the pin hash. persistent_store.reset(&mut rng).unwrap(); assert!(persistent_store.pin_hash().unwrap().is_none()); } #[test] fn test_pin_retries() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); // The pin retries is initially at the maximum. assert_eq!(persistent_store.pin_retries().unwrap(), MAX_PIN_RETRIES); // Decrementing the pin retries decrements the pin retries. for pin_retries in (0..MAX_PIN_RETRIES).rev() { persistent_store.decr_pin_retries().unwrap(); assert_eq!(persistent_store.pin_retries().unwrap(), pin_retries); } // Decrementing the pin retries after zero does not modify the pin retries. persistent_store.decr_pin_retries().unwrap(); assert_eq!(persistent_store.pin_retries().unwrap(), 0); // Resetting the pin retries resets the pin retries. persistent_store.reset_pin_retries().unwrap(); assert_eq!(persistent_store.pin_retries().unwrap(), MAX_PIN_RETRIES); } #[test] fn test_persistent_keys() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); // Make sure the attestation are absent. There is no batch attestation in tests. assert!(persistent_store .attestation_private_key() .unwrap() .is_none()); assert!(persistent_store .attestation_certificate() .unwrap() .is_none()); // Make sure the persistent keys are initialized. persistent_store .set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) .unwrap(); persistent_store .set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) .unwrap(); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); // The persistent keys stay initialized and preserve their value after a reset. persistent_store.reset(&mut rng).unwrap(); assert_eq!( persistent_store.attestation_private_key().unwrap().unwrap(), key_material::ATTESTATION_PRIVATE_KEY ); assert_eq!( persistent_store.attestation_certificate().unwrap().unwrap(), key_material::ATTESTATION_CERTIFICATE ); 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 length is initially at the default. assert_eq!( persistent_store.min_pin_length().unwrap(), 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) .unwrap(); assert_eq!( persistent_store.min_pin_length().unwrap(), 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 length RP IDs are initially at the default. assert_eq!( persistent_store._min_pin_length_rp_ids().unwrap(), _DEFAULT_MIN_PIN_LENGTH_RP_IDS ); // 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().unwrap(), 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], user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, user_icon: 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); } }