From 77d1b63284b8518b3e6b0685e3abe9acbec17f74 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 6 Nov 2020 17:30:59 +0100 Subject: [PATCH 001/192] adds a UP check where 2.1 is asking for it --- src/ctap/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 442d942..3ec7c6a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -342,6 +342,7 @@ where if let Some(auth_param) = &pin_uv_auth_param { // This case was added in FIDO 2.1. if auth_param.is_empty() { + let _ = (self.check_user_presence)(cid); if self.persistent_store.pin_hash()?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { @@ -545,6 +546,7 @@ where if let Some(auth_param) = &pin_uv_auth_param { // This case was added in FIDO 2.1. if auth_param.is_empty() { + let _ = (self.check_user_presence)(cid); if self.persistent_store.pin_hash()?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { From 5673b9148f185931a3a35c576fa7435b3a2b40fa Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 11 Nov 2020 12:55:37 +0100 Subject: [PATCH 002/192] Use new persistent store library (and delete old) --- Cargo.toml | 3 +- src/ctap/mod.rs | 2 +- src/ctap/status_code.rs | 12 + src/ctap/storage.rs | 744 +++++++--------- src/ctap/storage/key.rs | 135 +++ src/embedded_flash/buffer.rs | 457 ---------- src/embedded_flash/mod.rs | 10 +- src/embedded_flash/storage.rs | 107 --- src/embedded_flash/store/bitfield.rs | 177 ---- src/embedded_flash/store/format.rs | 565 ------------ src/embedded_flash/store/mod.rs | 1182 -------------------------- src/embedded_flash/syscall.rs | 36 +- 12 files changed, 493 insertions(+), 2937 deletions(-) create mode 100644 src/ctap/storage/key.rs delete mode 100644 src/embedded_flash/buffer.rs delete mode 100644 src/embedded_flash/storage.rs delete mode 100644 src/embedded_flash/store/bitfield.rs delete mode 100644 src/embedded_flash/store/format.rs delete mode 100644 src/embedded_flash/store/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 8faf8dd..5b01795 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ libtock_drivers = { path = "third_party/libtock-drivers" } lang_items = { path = "third_party/lang-items" } cbor = { path = "libraries/cbor" } crypto = { path = "libraries/crypto" } +persistent_store = { path = "libraries/persistent_store" } byteorder = { version = "1", default-features = false } arrayref = "0.3.6" subtle = { version = "2.2", default-features = false, features = ["nightly"] } @@ -23,7 +24,7 @@ subtle = { version = "2.2", default-features = false, features = ["nightly"] } debug_allocations = ["lang_items/debug_allocations"] debug_ctap = ["crypto/derive_debug", "libtock_drivers/debug_ctap"] panic_console = ["lang_items/panic_console"] -std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std"] +std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std", "persistent_store/std"] ram_storage = [] verbose = ["debug_ctap", "libtock_drivers/verbose_usb"] with_ctap1 = ["crypto/with_ctap1"] diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 1a98ce5..8f46f77 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -500,7 +500,7 @@ where let (signature, x5c) = match self.persistent_store.attestation_private_key()? { Some(attestation_private_key) => { let attestation_key = - crypto::ecdsa::SecKey::from_bytes(attestation_private_key).unwrap(); + crypto::ecdsa::SecKey::from_bytes(&attestation_private_key).unwrap(); let attestation_certificate = self .persistent_store .attestation_certificate()? diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index adb84fd..b4f78fd 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -81,5 +81,17 @@ pub enum Ctap2StatusCode { /// This type of error is unexpected and the current state is undefined. CTAP2_ERR_VENDOR_INTERNAL_ERROR = 0xF2, + /// The persistent storage invariant is broken. + /// + /// There can be multiple reasons: + /// - The persistent storage has not been erased before its first usage. + /// - The persistent storage has been tempered by a third party. + /// - The flash is malfunctioning (including the Tock driver). + /// + /// In the first 2 cases the persistent storage should be completely erased. If the error + /// reproduces, it may indicate a software bug or a hardware deficiency. In both cases, the + /// error should be reported. + CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE = 0xF3, + CTAP2_ERR_VENDOR_LAST = 0xFF, } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 127dc23..d99d62c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod key; + #[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; use crate::ctap::{key_material, USE_BATCH_ATTESTATION}; -use crate::embedded_flash::{self, StoreConfig, StoreEntry, StoreError}; +#[cfg(feature = "with_ctap2_1")] use alloc::string::String; #[cfg(any(test, feature = "ram_storage", feature = "with_ctap2_1"))] use alloc::vec; @@ -30,16 +32,16 @@ use core::convert::TryInto; use crypto::rng256::Rng256; #[cfg(any(test, feature = "ram_storage"))] -type Storage = embedded_flash::BufferStorage; +type Storage = persistent_store::BufferStorage; #[cfg(not(any(test, feature = "ram_storage")))] -type Storage = embedded_flash::SyscallStorage; +type Storage = crate::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. +// The number of pages should be at least 3 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 is linear in the +// number of pages. This may improve in the future. Currently, using 20 pages gives between 20ms and +// 240ms per operation. The rule of thumb is between 1ms and 12ms per additional page. // // Limiting the number of residential keys permits to ensure a minimum number of counter increments. // Let: @@ -49,32 +51,15 @@ type Storage = embedded_flash::SyscallStorage; // - C the number of erase cycles (10000) // - I the minimum number of counter increments // -// We have: I = ((P - 1) * 4092 - K * S) / 12 * C +// We have: I = (P * 4084 - 5107 - K * S) / 8 * C // -// With P=20 and K=150, we have I > 2M which is enough for 500 increments per day for 10 years. +// 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; +const NUM_PAGES: usize = 3; #[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; const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; const AAGUID_LENGTH: usize = 16; @@ -88,92 +73,18 @@ const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); #[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, -} - +/// Wrapper for master keys. pub struct MasterKeys { + /// Master encryption key. pub encryption: [u8; 32], + + /// Master hmac key. 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), - } - } -} - +/// CTAP persistent storage. pub struct PersistentStore { - store: embedded_flash::Store, + store: persistent_store::Store, } impl PersistentStore { @@ -188,17 +99,19 @@ impl PersistentStore { #[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: persistent_store::Store::new(storage).ok().unwrap(), }; - store.init(rng); + store.init(rng).unwrap(); store } + /// Creates a syscall storage in flash. #[cfg(not(any(test, feature = "ram_storage")))] fn new_prod_storage() -> Storage { Storage::new(NUM_PAGES).unwrap() } + /// Creates a buffer storage in RAM. #[cfg(any(test, feature = "ram_storage"))] fn new_test_storage() -> Storage { #[cfg(not(test))] @@ -206,7 +119,7 @@ impl PersistentStore { #[cfg(test)] const PAGE_SIZE: usize = 0x1000; let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); - let options = embedded_flash::BufferOptions { + let options = persistent_store::BufferOptions { word_size: 4, page_size: PAGE_SIZE, max_word_writes: 2, @@ -216,283 +129,265 @@ impl PersistentStore { Storage::new(store, options) } - fn init(&mut self, rng: &mut impl Rng256) { - if self.store.find_one(&Key::MasterKeys).is_none() { + /// Initializes the store by creating missing objects. + fn init(&mut self, rng: &mut impl Rng256) -> Result<(), Ctap2StatusCode> { + // Generate and store the master keys if they are missing. + if self.store.find_handle(key::MASTER_KEYS)?.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(); + self.store.insert(key::MASTER_KEYS, &master_keys)?; } // 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() { - self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) - .unwrap(); + if self + .store + .find_handle(key::ATTESTATION_PRIVATE_KEY)? + .is_none() + { + self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY)?; } - if self.store.find_one(&Key::AttestationCertificate).is_none() { - self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) - .unwrap(); + if self + .store + .find_handle(key::ATTESTATION_CERTIFICATE)? + .is_none() + { + self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE)?; } } - if self.store.find_one(&Key::Aaguid).is_none() { - self.set_aaguid(key_material::AAGUID).unwrap(); + if self.store.find_handle(key::AAGUID)?.is_none() { + self.set_aaguid(key_material::AAGUID)?; } + Ok(()) } + /// Returns the first matching credential. + /// + /// Returns `None` if no credentials are matched or if `check_cred_protect` is set and the first + /// matched credential requires user verification. 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) + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + // TODO(reviewer): Should we return an error if we find more than one matching credential? + // We did not use to in the previous version (panic in debug mode, nothing in release mode) + // but I don't remember why. Let's document it. + let result = iter.map(|(_, credential)| credential).find(|credential| { + credential.rp_id == rp_id && credential.credential_id == credential_id }); - if check_cred_protect && user_verification_required { - Ok(None) - } else { - Ok(result) + iter_result?; + if let Some(cred) = &result { + let user_verification_required = cred.cred_protect_policy + == Some(CredentialProtectionPolicy::UserVerificationRequired); + if check_cred_protect && user_verification_required { + return Ok(None); + } } + Ok(result) } + /// Stores or updates a credential. + /// + /// If a credential with the same RP id and user handle already exists, it is replaced. pub fn store_credential( &mut self, - credential: PublicKeyCredentialSource, + new_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 { + // Holds the key of the existing credential if this is an update. + let mut old_key = None; + // Holds the unordered list of used keys. + let mut keys = Vec::new(); + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + for (key, credential) in iter { + keys.push(key); + if credential.rp_id == new_credential.rp_id + && credential.user_handle == new_credential.user_handle + { + if old_key.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + } + old_key = Some(key); + } + } + iter_result?; + if old_key.is_none() && keys.len() >= 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)? - } + let key = match old_key { + // This is a new credential being added, we need to allocate a free key. We choose the + // first available key. This is quadratic in the number of existing keys. + None => key::CREDENTIALS + .take(MAX_SUPPORTED_RESIDENTIAL_KEYS) + .find(|key| !keys.contains(key)) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE)?, + // This is an existing credential being updated, we reuse its key. + Some(x) => x, }; + let value = serialize_credential(new_credential)?; + self.store.insert(key, &value)?; Ok(()) } + /// Returns the list of matching credentials. + /// + /// Does not return credentials that are not discoverable if `check_cred_protect` is set. 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 + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + let result = iter + .filter_map(|(_, credential)| { + if credential.rp_id == rp_id { + Some(credential) + } else { + None + } }) .filter(|cred| !check_cred_protect || cred.is_discoverable()) - .collect()) + .collect(); + iter_result?; + Ok(result) } + /// Returns the number of credentials. + #[cfg(test)] pub fn count_credentials(&self) -> Result { - Ok(self - .store - .find_all(&Key::Credential { - rp_id: None, - credential_id: None, - user_handle: None, - }) - .count()) + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + let result = iter.count(); + iter_result?; + Ok(result) } + /// Iterates through the credentials. + /// + /// If an error is encountered during iteration, it is written to `result`. + fn iter_credentials<'a>( + &'a self, + result: &'a mut Result<(), Ctap2StatusCode>, + ) -> Result, Ctap2StatusCode> { + IterCredentials::new(&self.store, result) + } + + /// Returns the global signature counter. 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], + Ok(match self.store.find(key::GLOBAL_SIGNATURE_COUNTER)? { + None => 0, + Some(value) => u32::from_ne_bytes(*array_ref!(&value, 0, 4)), }) } - 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])) + /// Increments the global signature counter. + pub fn incr_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { + let old_value = self.global_signature_counter()?; + // In hopes that servers handle the wrapping gracefully. + let new_value = old_value.wrapping_add(1); + self.store + .insert(key::GLOBAL_SIGNATURE_COUNTER, &new_value.to_ne_bytes())?; + Ok(()) } + /// Returns the master keys. + pub fn master_keys(&self) -> Result { + let master_keys = self + .store + .find(key::MASTER_KEYS)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE)?; + if master_keys.len() != 64 { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + } + Ok(MasterKeys { + encryption: *array_ref![master_keys, 0, 32], + hmac: *array_ref![master_keys, 32, 32], + }) + } + + /// Returns the PIN hash if defined. + pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { + let pin_hash = match self.store.find(key::PIN_HASH)? { + None => return Ok(None), + Some(pin_hash) => pin_hash, + }; + if pin_hash.len() != PIN_AUTH_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + } + Ok(Some(*array_ref![pin_hash, 0, PIN_AUTH_LENGTH])) + } + + /// Sets the PIN hash. + /// + /// If it was already defined, it is updated. 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(()) + Ok(self.store.insert(key::PIN_HASH, pin_hash)?) } + /// Returns the number of remaining PIN retries. 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] - })) + match self.store.find(key::PIN_RETRIES)? { + None => Ok(MAX_PIN_RETRIES), + Some(value) if value.len() == 1 => Ok(value[0]), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE), + } } + /// Decrements the number of remaining PIN retries. 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, - }, - )?; - } + let old_value = self.pin_retries()?; + let new_value = old_value.saturating_sub(1); + if new_value != old_value { + self.store.insert(key::PIN_RETRIES, &[new_value])?; } Ok(()) } + /// Resets the number of remaining PIN retries. pub fn reset_pin_retries(&mut self) -> Result<(), Ctap2StatusCode> { - if let Some((index, _)) = self.store.find_one(&Key::PinRetries) { - self.store.delete(index)?; - } - Ok(()) + Ok(self.store.remove(key::PIN_RETRIES)?) } + /// Returns the minimum PIN length. #[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] - })) + match self.store.find(key::MIN_PIN_LENGTH)? { + None => Ok(DEFAULT_MIN_PIN_LENGTH), + Some(value) if value.len() == 1 => Ok(value[0]), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE), + } } + /// Sets the minimum PIN length. #[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)?, - }) + Ok(self.store.insert(key::MIN_PIN_LENGTH, &[min_pin_length])?) } + /// TODO: Help from reviewer needed for documentation. #[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) + .find(key::_MIN_PIN_LENGTH_RP_IDS)? + .map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { + _deserialize_min_pin_length_rp_ids(&value) }); debug_assert!(rp_ids.is_some()); Ok(rp_ids.unwrap_or(vec![])) } + /// TODO: Help from reviewer needed for documentation. #[cfg(feature = "with_ctap2_1")] pub fn _set_min_pin_length_rp_ids( &mut self, @@ -507,138 +402,173 @@ impl PersistentStore { 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(()) + Ok(self.store.insert( + key::_MIN_PIN_LENGTH_RP_IDS, + &_serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, + )?) } + /// Returns the attestation private key if defined. 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() != ATTESTATION_PRIVATE_KEY_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + ) -> Result, Ctap2StatusCode> { + match self.store.find(key::ATTESTATION_PRIVATE_KEY)? { + None => Ok(None), + Some(key) if key.len() != ATTESTATION_PRIVATE_KEY_LENGTH => { + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE) + } + Some(key) => Ok(Some(*array_ref![key, 0, ATTESTATION_PRIVATE_KEY_LENGTH])), } - Ok(Some(array_ref!(data, 0, ATTESTATION_PRIVATE_KEY_LENGTH))) } + /// Sets the attestation private key. + /// + /// If it is already defined, it is overwritten. pub fn set_attestation_private_key( &mut self, attestation_private_key: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH], ) -> Result<(), Ctap2StatusCode> { - let entry = StoreEntry { - tag: ATTESTATION_PRIVATE_KEY, - data: attestation_private_key, - sensitive: false, - }; - match self.store.find_one(&Key::AttestationPrivateKey) { - None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, - } - Ok(()) + Ok(self + .store + .insert(key::ATTESTATION_PRIVATE_KEY, attestation_private_key)?) } + /// Returns the attestation certificate if defined. 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())) + Ok(self.store.find(key::ATTESTATION_CERTIFICATE)?) } + /// Sets the attestation certificate. + /// + /// If it is already defined, it is overwritten. 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)?, - Some((index, _)) => self.store.replace(index, entry)?, - } - Ok(()) - } - - pub fn aaguid(&self) -> Result<[u8; AAGUID_LENGTH], Ctap2StatusCode> { - let (_, entry) = self + Ok(self .store - .find_one(&Key::Aaguid) - .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; - let data = entry.data; - if data.len() != AAGUID_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - Ok(*array_ref![data, 0, AAGUID_LENGTH]) + .insert(key::ATTESTATION_CERTIFICATE, attestation_certificate)?) } + /// Returns the AAGUID. + pub fn aaguid(&self) -> Result<[u8; AAGUID_LENGTH], Ctap2StatusCode> { + let aaguid = self + .store + .find(key::AAGUID)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE)?; + if aaguid.len() != AAGUID_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + } + Ok(*array_ref![aaguid, 0, AAGUID_LENGTH]) + } + + /// Sets the AAGUID. + /// + /// If it is already defined, it is overwritten. pub fn set_aaguid(&mut self, aaguid: &[u8; 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(()) + Ok(self.store.insert(key::AAGUID, aaguid)?) } + /// Resets the store as for a CTAP reset. + /// + /// In particular persistent entries are not reset. 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); + self.store.clear(key::NUM_PERSISTENT_KEYS)?; + self.init(rng)?; Ok(()) } } -impl From for Ctap2StatusCode { - fn from(error: StoreError) -> Ctap2StatusCode { +impl From for Ctap2StatusCode { + fn from(error: persistent_store::StoreError) -> Ctap2StatusCode { + use persistent_store::StoreError::*; match error { - StoreError::StoreFull => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, - StoreError::InvalidTag => unreachable!(), - StoreError::InvalidPrecondition => unreachable!(), + // This error is expected. The store is full. + NoCapacity => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, + // This error is expected. The flash is out of life. + NoLifetime => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, + // This error is expected if we don't satisfy the store preconditions. For example we + // try to store a credential which is too long. + InvalidArgument => Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR, + // This error is not expected. The storage has been tempered with. We could erase the + // storage. + InvalidStorage => Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE, + // This error is not expected. The kernel is failing our syscalls. + StorageError => Ctap2StatusCode::CTAP1_ERR_OTHER, } } } -fn should_reset(entry: &StoreEntry<'_>) -> bool { - match entry.tag { - ATTESTATION_PRIVATE_KEY | ATTESTATION_CERTIFICATE | AAGUID => false, - _ => true, +/// Iterator for credentials. +struct IterCredentials<'a> { + /// The store being iterated. + store: &'a persistent_store::Store, + + /// The store iterator. + iter: persistent_store::StoreIter<'a, Storage>, + + /// The iteration result. + /// + /// It starts as success and gets written at most once with an error if something fails. The + /// iteration stops as soon as an error is encountered. + result: &'a mut Result<(), Ctap2StatusCode>, +} + +impl<'a> IterCredentials<'a> { + /// Creates a credential iterator. + fn new( + store: &'a persistent_store::Store, + result: &'a mut Result<(), Ctap2StatusCode>, + ) -> Result, Ctap2StatusCode> { + let iter = store.iter()?; + Ok(IterCredentials { + store, + iter, + result, + }) + } + + /// Marks the iteration as failed if the content is absent. + /// + /// For convenience, the function takes and returns ownership instead of taking a shared + /// reference and returning nothing. This permits to use it in both expressions and statements + /// instead of statements only. + fn unwrap(&mut self, x: Option) -> Option { + if x.is_none() { + *self.result = Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + } + x } } +impl<'a> Iterator for IterCredentials<'a> { + type Item = (usize, PublicKeyCredentialSource); + + fn next(&mut self) -> Option<(usize, PublicKeyCredentialSource)> { + if self.result.is_err() { + return None; + } + while let Some(next) = self.iter.next() { + let handle = self.unwrap(next.ok())?; + let key = handle.get_key(); + if !key::CREDENTIALS.contains(&key) { + continue; + } + let value = self.unwrap(handle.get_value(&self.store).ok())?; + let credential = self.unwrap(deserialize_credential(&value))?; + return Some((key, credential)); + } + None + } +} + +/// Deserializes a credential from storage representation. fn deserialize_credential(data: &[u8]) -> Option { let cbor = cbor::read(data).ok()?; cbor.try_into().ok() } +/// Serializes a credential to storage representation. fn serialize_credential(credential: PublicKeyCredentialSource) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); if cbor::write(credential.into(), &mut data) { @@ -648,6 +578,7 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result } } +/// TODO: Help from reviewer needed for documentation. #[cfg(feature = "with_ctap2_1")] fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { let cbor = cbor::read(data).ok()?; @@ -659,6 +590,7 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { .ok() } +/// TODO: Help from reviewer needed for documentation. #[cfg(feature = "with_ctap2_1")] fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); @@ -693,28 +625,6 @@ mod test { } } - #[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 {}; @@ -974,21 +884,21 @@ mod test { 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); + assert_eq!(persistent_store.pin_retries(), Ok(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); + assert_eq!(persistent_store.pin_retries(), Ok(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); + assert_eq!(persistent_store.pin_retries(), Ok(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); + assert_eq!(persistent_store.pin_retries(), Ok(MAX_PIN_RETRIES)); } #[test] @@ -1018,7 +928,7 @@ mod test { // 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(), + &persistent_store.attestation_private_key().unwrap().unwrap(), key_material::ATTESTATION_PRIVATE_KEY ); assert_eq!( diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs new file mode 100644 index 0000000..9796d5a --- /dev/null +++ b/src/ctap/storage/key.rs @@ -0,0 +1,135 @@ +// Copyright 2019-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. +// 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. + +/// Number of keys that persist the CTAP reset command. +pub const NUM_PERSISTENT_KEYS: usize = 20; + +/// Defines a key given its name and value or range of values. +macro_rules! make_key { + ($(#[$doc: meta])* $name: ident = $key: literal..$end: literal) => { + $(#[$doc])* pub const $name: core::ops::Range = $key..$end; + }; + ($(#[$doc: meta])* $name: ident = $key: literal) => { + $(#[$doc])* pub const $name: usize = $key; + }; +} + +/// Returns the range of values of a key given its value description. +#[cfg(test)] +macro_rules! make_range { + ($key: literal..$end: literal) => { + $key..$end + }; + ($key: literal) => { + $key..$key + 1 + }; +} + +/// Helper to define keys as a partial partition of a range. +macro_rules! make_partition { + ($range: expr, + $( + $(#[$doc: meta])* + $name: ident = $key: literal $(.. $end: literal)?; + )*) => { + $( + make_key!($(#[$doc])* $name = $key $(.. $end)?); + )* + #[cfg(test)] + const KEY_RANGE: core::ops::Range = $range; + #[cfg(test)] + const ALL_KEYS: &[core::ops::Range] = &[$(make_range!($key $(.. $end)?)),*]; + }; + } + +make_partition! { + // We reserve 0 and 2048+ for possible migration purposes. We add persistent entries starting + // from 1 and going up. We add non-persistent entries starting from 2047 and going down. This + // way, we don't commit to a fixed number of persistent keys. + 1..2048, + + // WARNING: Keys should not be deleted but prefixed with `_` to avoid accidentally reusing them. + + /// The attestation private key. + ATTESTATION_PRIVATE_KEY = 1; + + /// The attestation certificate. + ATTESTATION_CERTIFICATE = 2; + + /// The aaguid. + AAGUID = 3; + + // This is the persistent key limit: + // - When adding a (persistent) key above this message, make sure its value is smaller than + // NUM_PERSISTENT_KEYS. + // - When adding a (non-persistent) key below this message, make sure its value is bigger or + // equal than NUM_PERSISTENT_KEYS. + + /// The credentials. + /// + /// Depending on `MAX_SUPPORTED_RESIDENTIAL_KEYS`, only a prefix of those keys is used. Each + /// board may configure `MAX_SUPPORTED_RESIDENTIAL_KEYS` depending on the storage size. + CREDENTIALS = 1700..2000; + + /// TODO: Help from reviewer needed for documentation. + _MIN_PIN_LENGTH_RP_IDS = 2042; + + /// The minimum PIN length. + #[cfg(feature = "with_ctap2_1")] + MIN_PIN_LENGTH = 2043; + + /// The number of PIN retries. + /// + /// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`. + PIN_RETRIES = 2044; + + /// The PIN hash. + /// + /// If the entry is absent, there is no PIN set. + PIN_HASH = 2045; + + /// The encryption and hmac keys. + /// + /// This entry is always present. It is generated at startup if absent. This is not a persistent + /// key because its value should change after a CTAP reset. + MASTER_KEYS = 2046; + + /// The global signature counter. + /// + /// If the entry is absent, the counter is 0. + GLOBAL_SIGNATURE_COUNTER = 2047; +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn enough_credentials() { + use super::super::MAX_SUPPORTED_RESIDENTIAL_KEYS; + assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS <= CREDENTIALS.end - CREDENTIALS.start); + } + + #[test] + fn keys_are_disjoint() { + // Check that keys are in the range. + for keys in ALL_KEYS { + assert!(KEY_RANGE.start <= keys.start && keys.end <= KEY_RANGE.end); + } + // Check that keys are assigned at most once, essentially partitioning the range. + for key in KEY_RANGE { + assert!(ALL_KEYS.iter().filter(|keys| keys.contains(&key)).count() <= 1); + } + } +} diff --git a/src/embedded_flash/buffer.rs b/src/embedded_flash/buffer.rs deleted file mode 100644 index 0e7171a..0000000 --- a/src/embedded_flash/buffer.rs +++ /dev/null @@ -1,457 +0,0 @@ -// 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::{Index, Storage, StorageError, StorageResult}; -use alloc::boxed::Box; -use alloc::vec; - -pub struct BufferStorage { - storage: Box<[u8]>, - options: BufferOptions, - word_writes: Box<[usize]>, - page_erases: Box<[usize]>, - snapshot: Snapshot, -} - -#[derive(Copy, Clone, Debug)] -pub struct BufferOptions { - /// Size of a word in bytes. - pub word_size: usize, - - /// Size of a page in bytes. - pub page_size: usize, - - /// How many times a word can be written between page erasures - pub max_word_writes: usize, - - /// How many times a page can be erased. - pub max_page_erases: usize, - - /// Bits cannot be written from 0 to 1. - pub strict_write: bool, -} - -impl BufferStorage { - /// Creates a fake embedded flash using a buffer. - /// - /// This implementation checks that no words are written more than `max_word_writes` between - /// page erasures and than no pages are erased more than `max_page_erases`. If `strict_write` is - /// true, it also checks that no bits are written from 0 to 1. It also permits to take snapshots - /// of the storage during write and erase operations (although words would still be written or - /// erased completely). - /// - /// # Panics - /// - /// The following preconditions must hold: - /// - `options.word_size` must be a power of two. - /// - `options.page_size` must be a power of two. - /// - `options.page_size` must be word-aligned. - /// - `storage.len()` must be page-aligned. - pub fn new(storage: Box<[u8]>, options: BufferOptions) -> BufferStorage { - assert!(options.word_size.is_power_of_two()); - assert!(options.page_size.is_power_of_two()); - let num_words = storage.len() / options.word_size; - let num_pages = storage.len() / options.page_size; - let buffer = BufferStorage { - storage, - options, - word_writes: vec![0; num_words].into_boxed_slice(), - page_erases: vec![0; num_pages].into_boxed_slice(), - snapshot: Snapshot::Ready, - }; - assert!(buffer.is_word_aligned(buffer.options.page_size)); - assert!(buffer.is_page_aligned(buffer.storage.len())); - buffer - } - - /// Takes a snapshot of the storage after a given amount of word operations. - /// - /// Each time a word is written or erased, the delay is decremented if positive. Otherwise, a - /// snapshot is taken before the operation is executed. - /// - /// # Panics - /// - /// Panics if a snapshot has been armed and not examined. - pub fn arm_snapshot(&mut self, delay: usize) { - self.snapshot.arm(delay); - } - - /// Unarms and returns the snapshot or the delay remaining. - /// - /// # Panics - /// - /// Panics if a snapshot was not armed. - pub fn get_snapshot(&mut self) -> Result, usize> { - self.snapshot.get() - } - - /// Takes a snapshot of the storage. - pub fn take_snapshot(&self) -> Box<[u8]> { - self.storage.clone() - } - - /// Returns the storage. - pub fn get_storage(self) -> Box<[u8]> { - self.storage - } - - fn is_word_aligned(&self, x: usize) -> bool { - x & (self.options.word_size - 1) == 0 - } - - fn is_page_aligned(&self, x: usize) -> bool { - x & (self.options.page_size - 1) == 0 - } - - /// Writes a slice to the storage. - /// - /// The slice `value` is written to `index`. The `erase` boolean specifies whether this is an - /// erase operation or a write operation which matters for the checks and updating the shadow - /// storage. This also takes a snapshot of the storage if a snapshot was armed and the delay has - /// elapsed. - /// - /// The following preconditions should hold: - /// - `index` is word-aligned. - /// - `value.len()` is word-aligned. - /// - /// The following checks are performed: - /// - The region of length `value.len()` starting at `index` fits in a storage page. - /// - A word is not written more than `max_word_writes`. - /// - A page is not erased more than `max_page_erases`. - /// - The new word only switches 1s to 0s (only if `strict_write` is set). - fn update_storage(&mut self, index: Index, value: &[u8], erase: bool) -> StorageResult<()> { - debug_assert!(self.is_word_aligned(index.byte) && self.is_word_aligned(value.len())); - let dst = index.range(value.len(), self)?.step_by(self.word_size()); - let src = value.chunks(self.word_size()); - // Check and update page shadow. - if erase { - let page = index.page; - assert!(self.page_erases[page] < self.max_page_erases()); - self.page_erases[page] += 1; - } - for (byte, val) in dst.zip(src) { - let range = byte..byte + self.word_size(); - // The driver doesn't write identical words. - if &self.storage[range.clone()] == val { - continue; - } - // Check and update word shadow. - let word = byte / self.word_size(); - if erase { - self.word_writes[word] = 0; - } else { - assert!(self.word_writes[word] < self.max_word_writes()); - self.word_writes[word] += 1; - } - // Check strict write. - if !erase && self.options.strict_write { - for (byte, &val) in range.clone().zip(val) { - assert_eq!(self.storage[byte] & val, val); - } - } - // Take snapshot if armed and delay expired. - self.snapshot.take(&self.storage); - // Write storage - self.storage[range].copy_from_slice(val); - } - Ok(()) - } -} - -impl Storage for BufferStorage { - fn word_size(&self) -> usize { - self.options.word_size - } - - fn page_size(&self) -> usize { - self.options.page_size - } - - fn num_pages(&self) -> usize { - self.storage.len() / self.options.page_size - } - - fn max_word_writes(&self) -> usize { - self.options.max_word_writes - } - - fn max_page_erases(&self) -> usize { - self.options.max_page_erases - } - - fn read_slice(&self, index: Index, length: usize) -> StorageResult<&[u8]> { - Ok(&self.storage[index.range(length, self)?]) - } - - fn write_slice(&mut self, index: Index, value: &[u8]) -> StorageResult<()> { - if !self.is_word_aligned(index.byte) || !self.is_word_aligned(value.len()) { - return Err(StorageError::NotAligned); - } - self.update_storage(index, value, false) - } - - fn erase_page(&mut self, page: usize) -> StorageResult<()> { - let index = Index { page, byte: 0 }; - let value = vec![0xff; self.page_size()]; - self.update_storage(index, &value, true) - } -} - -// Controls when a snapshot of the storage is taken. -// -// This can be used to simulate power-offs while the device is writing to the storage or erasing a -// page in the storage. -enum Snapshot { - // Mutable word operations have normal behavior. - Ready, - // If the delay is positive, mutable word operations decrement it. If the count is zero, mutable - // word operations take a snapshot of the storage. - Armed { delay: usize }, - // Mutable word operations have normal behavior. - Taken { storage: Box<[u8]> }, -} - -impl Snapshot { - fn arm(&mut self, delay: usize) { - match self { - Snapshot::Ready => *self = Snapshot::Armed { delay }, - _ => panic!(), - } - } - - fn get(&mut self) -> Result, usize> { - let mut snapshot = Snapshot::Ready; - core::mem::swap(self, &mut snapshot); - match snapshot { - Snapshot::Armed { delay } => Err(delay), - Snapshot::Taken { storage } => Ok(storage), - _ => panic!(), - } - } - - fn take(&mut self, storage: &[u8]) { - if let Snapshot::Armed { delay } = self { - if *delay == 0 { - let storage = storage.to_vec().into_boxed_slice(); - *self = Snapshot::Taken { storage }; - } else { - *delay -= 1; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const NUM_PAGES: usize = 2; - const OPTIONS: BufferOptions = BufferOptions { - word_size: 4, - page_size: 16, - max_word_writes: 2, - max_page_erases: 3, - strict_write: true, - }; - // Those words are decreasing bit patterns. Bits are only changed from 1 to 0 and at last one - // bit is changed. - const BLANK_WORD: &[u8] = &[0xff, 0xff, 0xff, 0xff]; - const FIRST_WORD: &[u8] = &[0xee, 0xdd, 0xbb, 0x77]; - const SECOND_WORD: &[u8] = &[0xca, 0xc9, 0xa9, 0x65]; - const THIRD_WORD: &[u8] = &[0x88, 0x88, 0x88, 0x44]; - - fn new_storage() -> Box<[u8]> { - vec![0xff; NUM_PAGES * OPTIONS.page_size].into_boxed_slice() - } - - #[test] - fn words_are_decreasing() { - fn assert_is_decreasing(prev: &[u8], next: &[u8]) { - for (&prev, &next) in prev.iter().zip(next.iter()) { - assert_eq!(prev & next, next); - assert!(prev != next); - } - } - assert_is_decreasing(BLANK_WORD, FIRST_WORD); - assert_is_decreasing(FIRST_WORD, SECOND_WORD); - assert_is_decreasing(SECOND_WORD, THIRD_WORD); - } - - #[test] - fn options_ok() { - let buffer = BufferStorage::new(new_storage(), OPTIONS); - assert_eq!(buffer.word_size(), OPTIONS.word_size); - assert_eq!(buffer.page_size(), OPTIONS.page_size); - assert_eq!(buffer.num_pages(), NUM_PAGES); - assert_eq!(buffer.max_word_writes(), OPTIONS.max_word_writes); - assert_eq!(buffer.max_page_erases(), OPTIONS.max_page_erases); - } - - #[test] - fn read_write_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 0 }; - let next_index = Index { page: 0, byte: 4 }; - assert_eq!(buffer.read_slice(index, 4).unwrap(), BLANK_WORD); - buffer.write_slice(index, FIRST_WORD).unwrap(); - assert_eq!(buffer.read_slice(index, 4).unwrap(), FIRST_WORD); - assert_eq!(buffer.read_slice(next_index, 4).unwrap(), BLANK_WORD); - } - - #[test] - fn erase_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 0 }; - let other_index = Index { page: 1, byte: 0 }; - buffer.write_slice(index, FIRST_WORD).unwrap(); - buffer.write_slice(other_index, FIRST_WORD).unwrap(); - assert_eq!(buffer.read_slice(index, 4).unwrap(), FIRST_WORD); - assert_eq!(buffer.read_slice(other_index, 4).unwrap(), FIRST_WORD); - buffer.erase_page(0).unwrap(); - assert_eq!(buffer.read_slice(index, 4).unwrap(), BLANK_WORD); - assert_eq!(buffer.read_slice(other_index, 4).unwrap(), FIRST_WORD); - } - - #[test] - fn invalid_range() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 12 }; - let half_index = Index { page: 0, byte: 14 }; - let over_index = Index { page: 0, byte: 16 }; - let bad_page = Index { page: 2, byte: 0 }; - - // Reading a word in the storage is ok. - assert!(buffer.read_slice(index, 4).is_ok()); - // Reading a half-word in the storage is ok. - assert!(buffer.read_slice(half_index, 2).is_ok()); - // Reading even a single byte outside a page is not ok. - assert!(buffer.read_slice(over_index, 1).is_err()); - // But reading an empty slice just after a page is ok. - assert!(buffer.read_slice(over_index, 0).is_ok()); - // Reading even an empty slice outside the storage is not ok. - assert!(buffer.read_slice(bad_page, 0).is_err()); - - // Writing a word in the storage is ok. - assert!(buffer.write_slice(index, FIRST_WORD).is_ok()); - // Writing an unaligned word is not ok. - assert!(buffer.write_slice(half_index, FIRST_WORD).is_err()); - // Writing a word outside a page is not ok. - assert!(buffer.write_slice(over_index, FIRST_WORD).is_err()); - // But writing an empty slice just after a page is ok. - assert!(buffer.write_slice(over_index, &[]).is_ok()); - // Writing even an empty slice outside the storage is not ok. - assert!(buffer.write_slice(bad_page, &[]).is_err()); - - // Only pages in the storage can be erased. - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.erase_page(2).is_err()); - } - - #[test] - fn write_twice_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 4 }; - assert!(buffer.write_slice(index, FIRST_WORD).is_ok()); - assert!(buffer.write_slice(index, SECOND_WORD).is_ok()); - } - - #[test] - fn write_twice_and_once_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 0 }; - let next_index = Index { page: 0, byte: 4 }; - assert!(buffer.write_slice(index, FIRST_WORD).is_ok()); - assert!(buffer.write_slice(index, SECOND_WORD).is_ok()); - assert!(buffer.write_slice(next_index, THIRD_WORD).is_ok()); - } - - #[test] - #[should_panic] - fn write_three_times_panics() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 4 }; - assert!(buffer.write_slice(index, FIRST_WORD).is_ok()); - assert!(buffer.write_slice(index, SECOND_WORD).is_ok()); - let _ = buffer.write_slice(index, THIRD_WORD); - } - - #[test] - fn write_twice_then_once_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 0 }; - assert!(buffer.write_slice(index, FIRST_WORD).is_ok()); - assert!(buffer.write_slice(index, SECOND_WORD).is_ok()); - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.write_slice(index, FIRST_WORD).is_ok()); - } - - #[test] - fn erase_three_times_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.erase_page(0).is_ok()); - } - - #[test] - fn erase_three_times_and_once_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.erase_page(1).is_ok()); - } - - #[test] - #[should_panic] - fn erase_four_times_panics() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.erase_page(0).is_ok()); - assert!(buffer.erase_page(0).is_ok()); - let _ = buffer.erase_page(0).is_ok(); - } - - #[test] - #[should_panic] - fn switch_zero_to_one_panics() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 0 }; - assert!(buffer.write_slice(index, SECOND_WORD).is_ok()); - let _ = buffer.write_slice(index, FIRST_WORD); - } - - #[test] - fn get_storage_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 4 }; - buffer.write_slice(index, FIRST_WORD).unwrap(); - let storage = buffer.get_storage(); - assert_eq!(&storage[..4], BLANK_WORD); - assert_eq!(&storage[4..8], FIRST_WORD); - } - - #[test] - fn snapshot_ok() { - let mut buffer = BufferStorage::new(new_storage(), OPTIONS); - let index = Index { page: 0, byte: 0 }; - let value = [FIRST_WORD, SECOND_WORD].concat(); - buffer.arm_snapshot(1); - buffer.write_slice(index, &value).unwrap(); - let storage = buffer.get_snapshot().unwrap(); - assert_eq!(&storage[..8], &[FIRST_WORD, BLANK_WORD].concat()[..]); - let storage = buffer.take_snapshot(); - assert_eq!(&storage[..8], &value[..]); - } -} diff --git a/src/embedded_flash/mod.rs b/src/embedded_flash/mod.rs index 05407c0..b16504b 100644 --- a/src/embedded_flash/mod.rs +++ b/src/embedded_flash/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-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. @@ -12,16 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(any(test, feature = "ram_storage"))] -mod buffer; -mod storage; -mod store; #[cfg(not(any(test, feature = "ram_storage")))] mod syscall; -#[cfg(any(test, feature = "ram_storage"))] -pub use self::buffer::{BufferOptions, BufferStorage}; -pub use self::storage::{Index, Storage, StorageError, StorageResult}; -pub use self::store::{Store, StoreConfig, StoreEntry, StoreError, StoreIndex}; #[cfg(not(any(test, feature = "ram_storage")))] pub use self::syscall::SyscallStorage; diff --git a/src/embedded_flash/storage.rs b/src/embedded_flash/storage.rs deleted file mode 100644 index fe87ac4..0000000 --- a/src/embedded_flash/storage.rs +++ /dev/null @@ -1,107 +0,0 @@ -// 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. - -#[derive(Copy, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct Index { - pub page: usize, - pub byte: usize, -} - -#[derive(Debug)] -pub enum StorageError { - BadFlash, - NotAligned, - OutOfBounds, - KernelError { code: isize }, -} - -pub type StorageResult = Result; - -/// Abstraction for embedded flash storage. -pub trait Storage { - /// Returns the size of a word in bytes. - fn word_size(&self) -> usize; - - /// Returns the size of a page in bytes. - fn page_size(&self) -> usize; - - /// Returns the number of pages in the storage. - fn num_pages(&self) -> usize; - - /// Returns how many times a word can be written between page erasures. - fn max_word_writes(&self) -> usize; - - /// Returns how many times a page can be erased in the lifetime of the flash. - fn max_page_erases(&self) -> usize; - - /// Reads a slice from the storage. - /// - /// The slice does not need to be word-aligned. - /// - /// # Errors - /// - /// The `index` must designate `length` bytes in the storage. - fn read_slice(&self, index: Index, length: usize) -> StorageResult<&[u8]>; - - /// Writes a word-aligned slice to the storage. - /// - /// The written words should not have been written too many times since last page erasure. - /// - /// # Errors - /// - /// The following preconditions must hold: - /// - `index` must be word-aligned. - /// - `value.len()` must be a multiple of the word size. - /// - `index` must designate `value.len()` bytes in the storage. - /// - `value` must be in memory until [read-only allow][tock_1274] is resolved. - /// - /// [tock_1274]: https://github.com/tock/tock/issues/1274. - fn write_slice(&mut self, index: Index, value: &[u8]) -> StorageResult<()>; - - /// Erases a page of the storage. - /// - /// # Errors - /// - /// The `page` must be in the storage. - fn erase_page(&mut self, page: usize) -> StorageResult<()>; -} - -impl Index { - /// Returns whether a slice fits in a storage page. - fn is_valid(self, length: usize, storage: &impl Storage) -> bool { - self.page < storage.num_pages() - && storage - .page_size() - .checked_sub(length) - .map(|limit| self.byte <= limit) - .unwrap_or(false) - } - - /// Returns the range of a valid slice. - /// - /// The range starts at `self` with `length` bytes. - pub fn range( - self, - length: usize, - storage: &impl Storage, - ) -> StorageResult> { - if self.is_valid(length, storage) { - let start = self.page * storage.page_size() + self.byte; - Ok(start..start + length) - } else { - Err(StorageError::OutOfBounds) - } - } -} diff --git a/src/embedded_flash/store/bitfield.rs b/src/embedded_flash/store/bitfield.rs deleted file mode 100644 index 797c78b..0000000 --- a/src/embedded_flash/store/bitfield.rs +++ /dev/null @@ -1,177 +0,0 @@ -// 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. - -/// Defines a consecutive sequence of bits. -#[derive(Copy, Clone)] -pub struct BitRange { - /// The first bit of the sequence. - pub start: usize, - - /// The length in bits of the sequence. - pub length: usize, -} - -impl BitRange { - /// Returns the first bit following a bit range. - pub fn end(self) -> usize { - self.start + self.length - } -} - -/// Defines a consecutive sequence of bytes. -/// -/// The bits in those bytes are ignored which essentially creates a gap in a sequence of bits. The -/// gap is necessarily at byte boundaries. This is used to ignore the user data in an entry -/// essentially providing a view of the entry information (header and footer). -#[derive(Copy, Clone)] -pub struct ByteGap { - pub start: usize, - pub length: usize, -} - -/// Empty gap. All bits count. -pub const NO_GAP: ByteGap = ByteGap { - start: 0, - length: 0, -}; - -impl ByteGap { - /// Translates a bit to skip the gap. - fn shift(self, bit: usize) -> usize { - if bit < 8 * self.start { - bit - } else { - bit + 8 * self.length - } - } - - /// Returns the slice of `data` corresponding to the gap. - pub fn slice(self, data: &[u8]) -> &[u8] { - &data[self.start..self.start + self.length] - } -} - -/// Returns whether a bit is set in a sequence of bits. -/// -/// The sequence of bits is little-endian (both for bytes and bits) and defined by the bits that -/// are in `data` but not in `gap`. -pub fn is_zero(bit: usize, data: &[u8], gap: ByteGap) -> bool { - let bit = gap.shift(bit); - debug_assert!(bit < 8 * data.len()); - data[bit / 8] & (1 << (bit % 8)) == 0 -} - -/// Sets a bit to zero in a sequence of bits. -/// -/// The sequence of bits is little-endian (both for bytes and bits) and defined by the bits that -/// are in `data` but not in `gap`. -pub fn set_zero(bit: usize, data: &mut [u8], gap: ByteGap) { - let bit = gap.shift(bit); - debug_assert!(bit < 8 * data.len()); - data[bit / 8] &= !(1 << (bit % 8)); -} - -/// Returns a little-endian value in a sequence of bits. -/// -/// The sequence of bits is little-endian (both for bytes and bits) and defined by the bits that -/// are in `data` but not in `gap`. The range of bits where the value is stored in defined by -/// `range`. The value must fit in a `usize`. -pub fn get_range(range: BitRange, data: &[u8], gap: ByteGap) -> usize { - debug_assert!(range.length <= 8 * core::mem::size_of::()); - let mut result = 0; - for i in 0..range.length { - if !is_zero(range.start + i, data, gap) { - result |= 1 << i; - } - } - result -} - -/// Sets a little-endian value in a sequence of bits. -/// -/// The sequence of bits is little-endian (both for bytes and bits) and defined by the bits that -/// are in `data` but not in `gap`. The range of bits where the value is stored in defined by -/// `range`. The bits set to 1 in `value` must also be set to `1` in the sequence of bits. -pub fn set_range(range: BitRange, data: &mut [u8], gap: ByteGap, value: usize) { - debug_assert!(range.length == 8 * core::mem::size_of::() || value < 1 << range.length); - for i in 0..range.length { - if value & 1 << i == 0 { - set_zero(range.start + i, data, gap); - } - } -} - -/// Tests the `is_zero` and `set_zero` pair of functions. -#[test] -fn zero_ok() { - const GAP: ByteGap = ByteGap { - start: 2, - length: 1, - }; - for i in 0..24 { - assert!(!is_zero(i, &[0xffu8, 0xff, 0x00, 0xff] as &[u8], GAP)); - } - // Tests reading and setting a bit. The result should have all bits set to 1 except for the bit - // to test and the gap. - fn test(bit: usize, result: &[u8]) { - assert!(is_zero(bit, result, GAP)); - let mut data = vec![0xff; result.len()]; - // Set the gap bits to 0. - for i in 0..GAP.length { - data[GAP.start + i] = 0x00; - } - set_zero(bit, &mut data, GAP); - assert_eq!(data, result); - } - test(0, &[0xfe, 0xff, 0x00, 0xff]); - test(1, &[0xfd, 0xff, 0x00, 0xff]); - test(2, &[0xfb, 0xff, 0x00, 0xff]); - test(7, &[0x7f, 0xff, 0x00, 0xff]); - test(8, &[0xff, 0xfe, 0x00, 0xff]); - test(15, &[0xff, 0x7f, 0x00, 0xff]); - test(16, &[0xff, 0xff, 0x00, 0xfe]); - test(17, &[0xff, 0xff, 0x00, 0xfd]); - test(23, &[0xff, 0xff, 0x00, 0x7f]); -} - -/// Tests the `get_range` and `set_range` pair of functions. -#[test] -fn range_ok() { - // Tests reading and setting a range. The result should have all bits set to 1 except for the - // range to test and the gap. - fn test(start: usize, length: usize, value: usize, result: &[u8], gap: ByteGap) { - let range = BitRange { start, length }; - assert_eq!(get_range(range, result, gap), value); - let mut data = vec![0xff; result.len()]; - for i in 0..gap.length { - data[gap.start + i] = 0x00; - } - set_range(range, &mut data, gap, value); - assert_eq!(data, result); - } - test(0, 8, 42, &[42], NO_GAP); - test(3, 12, 0b11_0101, &[0b1010_1111, 0b1000_0001], NO_GAP); - test(0, 16, 0x1234, &[0x34, 0x12], NO_GAP); - test(4, 16, 0x1234, &[0x4f, 0x23, 0xf1], NO_GAP); - let mut gap = ByteGap { - start: 1, - length: 1, - }; - test(3, 12, 0b11_0101, &[0b1010_1111, 0x00, 0b1000_0001], gap); - gap.length = 2; - test(0, 16, 0x1234, &[0x34, 0x00, 0x00, 0x12], gap); - gap.start = 2; - gap.length = 1; - test(4, 16, 0x1234, &[0x4f, 0x23, 0x00, 0xf1], gap); -} diff --git a/src/embedded_flash/store/format.rs b/src/embedded_flash/store/format.rs deleted file mode 100644 index 03787cf..0000000 --- a/src/embedded_flash/store/format.rs +++ /dev/null @@ -1,565 +0,0 @@ -// 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::super::{Index, Storage}; -use super::{bitfield, StoreConfig, StoreEntry, StoreError}; -use alloc::vec; -use alloc::vec::Vec; - -/// Whether a user entry is a replace entry. -pub enum IsReplace { - /// This is a replace entry. - Replace, - - /// This is an insert entry. - Insert, -} - -/// Helpers to parse the store format. -/// -/// See the store module-level documentation for information about the format. -pub struct Format { - pub word_size: usize, - pub page_size: usize, - pub num_pages: usize, - pub max_page_erases: usize, - pub num_tags: usize, - - /// Whether an entry is present. - /// - /// - 0 for entries (user entries or internal entries). - /// - 1 for free space until the end of the page. - present_bit: usize, - - /// Whether an entry is deleted. - /// - /// - 0 for deleted entries. - /// - 1 for alive entries. - deleted_bit: usize, - - /// Whether an entry is internal. - /// - /// - 0 for internal entries. - /// - 1 for user entries. - internal_bit: usize, - - /// Whether a user entry is a replace entry. - /// - /// - 0 for replace entries. - /// - 1 for insert entries. - replace_bit: usize, - - /// Whether a user entry has sensitive data. - /// - /// - 0 for sensitive data. - /// - 1 for non-sensitive data. - /// - /// When a user entry with sensitive data is deleted, the data is overwritten with zeroes. This - /// feature is subject to the same guarantees as all other features of the store, in particular - /// deleting a sensitive entry is atomic. See the store module-level documentation for more - /// information. - sensitive_bit: usize, - - /// The data length of a user entry. - length_range: bitfield::BitRange, - - /// The tag of a user entry. - tag_range: bitfield::BitRange, - - /// The page index of a replace entry. - replace_page_range: bitfield::BitRange, - - /// The byte index of a replace entry. - replace_byte_range: bitfield::BitRange, - - /// The index of the page to erase. - /// - /// This is only present for internal entries. - old_page_range: bitfield::BitRange, - - /// The current erase count of the page to erase. - /// - /// This is only present for internal entries. - saved_erase_count_range: bitfield::BitRange, - - /// Whether a page is initialized. - /// - /// - 0 for initialized pages. - /// - 1 for uninitialized pages. - initialized_bit: usize, - - /// The erase count of a page. - erase_count_range: bitfield::BitRange, - - /// Whether a page is being compacted. - /// - /// - 0 for pages being compacted. - /// - 1 otherwise. - compacting_bit: usize, - - /// The page index to which a page is being compacted. - new_page_range: bitfield::BitRange, -} - -impl Format { - /// Returns a helper to parse the store format for a given storage and config. - /// - /// # Errors - /// - /// Returns `None` if any of the following conditions does not hold: - /// - The word size must be a power of two. - /// - The page size must be a power of two. - /// - There should be at least 2 pages in the storage. - /// - It should be possible to write a word at least twice. - /// - It should be possible to erase a page at least once. - /// - There should be at least 1 tag. - pub fn new(storage: &S, config: &C) -> Option { - let word_size = storage.word_size(); - let page_size = storage.page_size(); - let num_pages = storage.num_pages(); - let max_word_writes = storage.max_word_writes(); - let max_page_erases = storage.max_page_erases(); - let num_tags = config.num_tags(); - if !(word_size.is_power_of_two() - && page_size.is_power_of_two() - && num_pages > 1 - && max_word_writes >= 2 - && max_page_erases > 0 - && num_tags > 0) - { - return None; - } - // Compute how many bits we need to store the fields. - let page_bits = num_bits(num_pages); - let byte_bits = num_bits(page_size); - let tag_bits = num_bits(num_tags); - let erase_bits = num_bits(max_page_erases + 1); - // Compute the bit position of the fields. - let present_bit = 0; - let deleted_bit = present_bit + 1; - let internal_bit = deleted_bit + 1; - let replace_bit = internal_bit + 1; - let sensitive_bit = replace_bit + 1; - let length_range = bitfield::BitRange { - start: sensitive_bit + 1, - length: byte_bits, - }; - let tag_range = bitfield::BitRange { - start: length_range.end(), - length: tag_bits, - }; - let replace_page_range = bitfield::BitRange { - start: tag_range.end(), - length: page_bits, - }; - let replace_byte_range = bitfield::BitRange { - start: replace_page_range.end(), - length: byte_bits, - }; - let old_page_range = bitfield::BitRange { - start: internal_bit + 1, - length: page_bits, - }; - let saved_erase_count_range = bitfield::BitRange { - start: old_page_range.end(), - length: erase_bits, - }; - let initialized_bit = 0; - let erase_count_range = bitfield::BitRange { - start: initialized_bit + 1, - length: erase_bits, - }; - let compacting_bit = erase_count_range.end(); - let new_page_range = bitfield::BitRange { - start: compacting_bit + 1, - length: page_bits, - }; - let format = Format { - word_size, - page_size, - num_pages, - max_page_erases, - num_tags, - present_bit, - deleted_bit, - internal_bit, - replace_bit, - sensitive_bit, - length_range, - tag_range, - replace_page_range, - replace_byte_range, - old_page_range, - saved_erase_count_range, - initialized_bit, - erase_count_range, - compacting_bit, - new_page_range, - }; - // Make sure all the following conditions hold: - // - The page header is one word. - // - The internal entry is one word. - // - The entry header fits in one word (which is equivalent to the entry header size being - // exactly one word for sensitive entries). - if format.page_header_size() != word_size - || format.internal_entry_size() != word_size - || format.header_size(true) != word_size - { - return None; - } - Some(format) - } - - /// Ensures a user entry is valid. - pub fn validate_entry(&self, entry: StoreEntry) -> Result<(), StoreError> { - if entry.tag >= self.num_tags { - return Err(StoreError::InvalidTag); - } - if entry.data.len() >= self.page_size { - return Err(StoreError::StoreFull); - } - Ok(()) - } - - /// Returns the entry header length in bytes. - /// - /// This is the smallest number of bytes necessary to store all fields of the entry info up to - /// and including `length`. For sensitive entries, the result is word-aligned. - pub fn header_size(&self, sensitive: bool) -> usize { - let mut size = self.bits_to_bytes(self.length_range.end()); - if sensitive { - // We need to align to the next word boundary so that wiping the user data will not - // count as a write to the header. - size = self.align_word(size); - } - size - } - - /// Returns the entry header length in bytes. - /// - /// This is a convenience function for `header_size` above. - fn header_offset(&self, entry: &[u8]) -> usize { - self.header_size(self.is_sensitive(entry)) - } - - /// Returns the entry info length in bytes. - /// - /// This is the number of bytes necessary to store all fields of the entry info. This also - /// includes the internal padding to protect the `committed` bit from the `deleted` bit and to - /// protect the entry info from the user data for sensitive entries. - fn info_size(&self, is_replace: IsReplace, sensitive: bool) -> usize { - let suffix_bits = 2; // committed + complete - let info_bits = match is_replace { - IsReplace::Replace => self.replace_byte_range.end() + suffix_bits, - IsReplace::Insert => self.tag_range.end() + suffix_bits, - }; - let mut info_size = self.bits_to_bytes(info_bits); - // If the suffix bits would end up in the header, we need to add one byte for them. - let header_size = self.header_size(sensitive); - if info_size <= header_size { - info_size = header_size + 1; - } - // If the entry is sensitive, we need to align to the next word boundary. - if sensitive { - info_size = self.align_word(info_size); - } - info_size - } - - /// Returns the length in bytes of an entry. - /// - /// This depends on the length of the user data and whether the entry replaces an old entry or - /// is an insertion. This also includes the internal padding to protect the `committed` bit from - /// the `deleted` bit. - pub fn entry_size(&self, is_replace: IsReplace, sensitive: bool, length: usize) -> usize { - let mut entry_size = length + self.info_size(is_replace, sensitive); - let word_size = self.word_size; - entry_size = self.align_word(entry_size); - // The entry must be at least 2 words such that the `committed` and `deleted` bits are on - // different words. - if entry_size == word_size { - entry_size += word_size; - } - entry_size - } - - /// Returns the length in bytes of an internal entry. - pub fn internal_entry_size(&self) -> usize { - let length = self.bits_to_bytes(self.saved_erase_count_range.end()); - self.align_word(length) - } - - pub fn is_present(&self, header: &[u8]) -> bool { - bitfield::is_zero(self.present_bit, header, bitfield::NO_GAP) - } - - pub fn set_present(&self, header: &mut [u8]) { - bitfield::set_zero(self.present_bit, header, bitfield::NO_GAP) - } - - pub fn is_deleted(&self, header: &[u8]) -> bool { - bitfield::is_zero(self.deleted_bit, header, bitfield::NO_GAP) - } - - /// Returns whether an entry is present and not deleted. - pub fn is_alive(&self, header: &[u8]) -> bool { - self.is_present(header) && !self.is_deleted(header) - } - - pub fn set_deleted(&self, header: &mut [u8]) { - bitfield::set_zero(self.deleted_bit, header, bitfield::NO_GAP) - } - - pub fn is_internal(&self, header: &[u8]) -> bool { - bitfield::is_zero(self.internal_bit, header, bitfield::NO_GAP) - } - - pub fn set_internal(&self, header: &mut [u8]) { - bitfield::set_zero(self.internal_bit, header, bitfield::NO_GAP) - } - - pub fn is_replace(&self, header: &[u8]) -> IsReplace { - if bitfield::is_zero(self.replace_bit, header, bitfield::NO_GAP) { - IsReplace::Replace - } else { - IsReplace::Insert - } - } - - fn set_replace(&self, header: &mut [u8]) { - bitfield::set_zero(self.replace_bit, header, bitfield::NO_GAP) - } - - pub fn is_sensitive(&self, header: &[u8]) -> bool { - bitfield::is_zero(self.sensitive_bit, header, bitfield::NO_GAP) - } - - pub fn set_sensitive(&self, header: &mut [u8]) { - bitfield::set_zero(self.sensitive_bit, header, bitfield::NO_GAP) - } - - pub fn get_length(&self, header: &[u8]) -> usize { - bitfield::get_range(self.length_range, header, bitfield::NO_GAP) - } - - fn set_length(&self, header: &mut [u8], length: usize) { - bitfield::set_range(self.length_range, header, bitfield::NO_GAP, length) - } - - pub fn get_data<'a>(&self, entry: &'a [u8]) -> &'a [u8] { - &entry[self.header_offset(entry)..][..self.get_length(entry)] - } - - /// Returns the span of user data in an entry. - /// - /// The complement of this gap in the entry is exactly the entry info. The header is before the - /// gap and the footer is after the gap. - pub fn entry_gap(&self, entry: &[u8]) -> bitfield::ByteGap { - let start = self.header_offset(entry); - let mut length = self.get_length(entry); - if self.is_sensitive(entry) { - length = self.align_word(length); - } - bitfield::ByteGap { start, length } - } - - pub fn get_tag(&self, entry: &[u8]) -> usize { - bitfield::get_range(self.tag_range, entry, self.entry_gap(entry)) - } - - fn set_tag(&self, entry: &mut [u8], tag: usize) { - bitfield::set_range(self.tag_range, entry, self.entry_gap(entry), tag) - } - - pub fn get_replace_index(&self, entry: &[u8]) -> Index { - let gap = self.entry_gap(entry); - let page = bitfield::get_range(self.replace_page_range, entry, gap); - let byte = bitfield::get_range(self.replace_byte_range, entry, gap); - Index { page, byte } - } - - fn set_replace_page(&self, entry: &mut [u8], page: usize) { - bitfield::set_range(self.replace_page_range, entry, self.entry_gap(entry), page) - } - - fn set_replace_byte(&self, entry: &mut [u8], byte: usize) { - bitfield::set_range(self.replace_byte_range, entry, self.entry_gap(entry), byte) - } - - /// Returns the bit position of the `committed` bit. - /// - /// This cannot be precomputed like other fields since it depends on the length of the entry. - fn committed_bit(&self, entry: &[u8]) -> usize { - 8 * entry.len() - 2 - } - - /// Returns the bit position of the `complete` bit. - /// - /// This cannot be precomputed like other fields since it depends on the length of the entry. - fn complete_bit(&self, entry: &[u8]) -> usize { - 8 * entry.len() - 1 - } - - pub fn is_committed(&self, entry: &[u8]) -> bool { - bitfield::is_zero(self.committed_bit(entry), entry, bitfield::NO_GAP) - } - - pub fn set_committed(&self, entry: &mut [u8]) { - bitfield::set_zero(self.committed_bit(entry), entry, bitfield::NO_GAP) - } - - pub fn is_complete(&self, entry: &[u8]) -> bool { - bitfield::is_zero(self.complete_bit(entry), entry, bitfield::NO_GAP) - } - - fn set_complete(&self, entry: &mut [u8]) { - bitfield::set_zero(self.complete_bit(entry), entry, bitfield::NO_GAP) - } - - pub fn get_old_page(&self, header: &[u8]) -> usize { - bitfield::get_range(self.old_page_range, header, bitfield::NO_GAP) - } - - pub fn set_old_page(&self, header: &mut [u8], old_page: usize) { - bitfield::set_range(self.old_page_range, header, bitfield::NO_GAP, old_page) - } - - pub fn get_saved_erase_count(&self, header: &[u8]) -> usize { - bitfield::get_range(self.saved_erase_count_range, header, bitfield::NO_GAP) - } - - pub fn set_saved_erase_count(&self, header: &mut [u8], erase_count: usize) { - bitfield::set_range( - self.saved_erase_count_range, - header, - bitfield::NO_GAP, - erase_count, - ) - } - - /// Builds an entry for replace or insert operations. - pub fn build_entry(&self, replace: Option, user_entry: StoreEntry) -> Vec { - let StoreEntry { - tag, - data, - sensitive, - } = user_entry; - let is_replace = match replace { - None => IsReplace::Insert, - Some(_) => IsReplace::Replace, - }; - let entry_len = self.entry_size(is_replace, sensitive, data.len()); - let mut entry = Vec::with_capacity(entry_len); - // Build the header. - entry.resize(self.header_size(sensitive), 0xff); - self.set_present(&mut entry[..]); - if sensitive { - self.set_sensitive(&mut entry[..]); - } - self.set_length(&mut entry[..], data.len()); - // Add the data. - entry.extend_from_slice(data); - // Build the footer. - entry.resize(entry_len, 0xff); - self.set_tag(&mut entry[..], tag); - self.set_complete(&mut entry[..]); - match replace { - None => self.set_committed(&mut entry[..]), - Some(Index { page, byte }) => { - self.set_replace(&mut entry[..]); - self.set_replace_page(&mut entry[..], page); - self.set_replace_byte(&mut entry[..], byte); - } - } - entry - } - - /// Builds an entry for replace or insert operations. - pub fn build_erase_entry(&self, old_page: usize, saved_erase_count: usize) -> Vec { - let mut entry = vec![0xff; self.internal_entry_size()]; - self.set_present(&mut entry[..]); - self.set_internal(&mut entry[..]); - self.set_old_page(&mut entry[..], old_page); - self.set_saved_erase_count(&mut entry[..], saved_erase_count); - entry - } - - /// Returns the length in bytes of a page header entry. - /// - /// This includes the word padding. - pub fn page_header_size(&self) -> usize { - self.align_word(self.bits_to_bytes(self.erase_count_range.end())) - } - - pub fn is_initialized(&self, header: &[u8]) -> bool { - bitfield::is_zero(self.initialized_bit, header, bitfield::NO_GAP) - } - - pub fn set_initialized(&self, header: &mut [u8]) { - bitfield::set_zero(self.initialized_bit, header, bitfield::NO_GAP) - } - - pub fn get_erase_count(&self, header: &[u8]) -> usize { - bitfield::get_range(self.erase_count_range, header, bitfield::NO_GAP) - } - - pub fn set_erase_count(&self, header: &mut [u8], count: usize) { - bitfield::set_range(self.erase_count_range, header, bitfield::NO_GAP, count) - } - - pub fn is_compacting(&self, header: &[u8]) -> bool { - bitfield::is_zero(self.compacting_bit, header, bitfield::NO_GAP) - } - - pub fn set_compacting(&self, header: &mut [u8]) { - bitfield::set_zero(self.compacting_bit, header, bitfield::NO_GAP) - } - - pub fn get_new_page(&self, header: &[u8]) -> usize { - bitfield::get_range(self.new_page_range, header, bitfield::NO_GAP) - } - - pub fn set_new_page(&self, header: &mut [u8], new_page: usize) { - bitfield::set_range(self.new_page_range, header, bitfield::NO_GAP, new_page) - } - - /// Returns the smallest word boundary greater or equal to a value. - fn align_word(&self, value: usize) -> usize { - let word_size = self.word_size; - (value + word_size - 1) / word_size * word_size - } - - /// Returns the minimum number of bytes to represent a given number of bits. - fn bits_to_bytes(&self, bits: usize) -> usize { - (bits + 7) / 8 - } -} - -/// Returns the number of bits necessary to write numbers smaller than `x`. -fn num_bits(x: usize) -> usize { - x.next_power_of_two().trailing_zeros() as usize -} - -#[test] -fn num_bits_ok() { - assert_eq!(num_bits(0), 0); - assert_eq!(num_bits(1), 0); - assert_eq!(num_bits(2), 1); - assert_eq!(num_bits(3), 2); - assert_eq!(num_bits(4), 2); - assert_eq!(num_bits(5), 3); - assert_eq!(num_bits(8), 3); - assert_eq!(num_bits(9), 4); - assert_eq!(num_bits(16), 4); -} diff --git a/src/embedded_flash/store/mod.rs b/src/embedded_flash/store/mod.rs deleted file mode 100644 index 170fd87..0000000 --- a/src/embedded_flash/store/mod.rs +++ /dev/null @@ -1,1182 +0,0 @@ -// 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. - -//! Provides a multi-purpose data-structure. -//! -//! # Description -//! -//! The `Store` data-structure permits to iterate, find, insert, delete, and replace entries in a -//! multi-set. The mutable operations (insert, delete, and replace) are atomic, in the sense that if -//! power is lost during the operation, then the operation might either succeed or fail but the -//! store remains in a coherent state. The data-structure is flash-efficient, in the sense that it -//! tries to minimize the number of times a page is erased. -//! -//! An _entry_ is made of a _tag_, which is a number, and a _data_, which is a slice of bytes. The -//! tag is stored efficiently by using unassigned bits of the entry header and footer. For example, -//! it can be used to decide how to deserialize the data. It is not necessary to use tags since a -//! prefix of the data could be used to decide how to deserialize the rest. -//! -//! Entries can also be associated to a set of _keys_. The find operation permits to retrieve all -//! entries associated to a given key. The same key can be associated to multiple entries and the -//! same entry can be associated to multiple keys. -//! -//! # Storage -//! -//! The data-structure is parametric over its storage which must implement the `Storage` trait. -//! There are currently 2 implementations of this trait: -//! - `SyscallStorage` using the `embedded_flash` syscall API for production builds. -//! - `BufferStorage` using a heap-allocated buffer for testing. -//! -//! # Configuration -//! -//! The data-structure can be configured with the `StoreConfig` trait. By implementing this trait, -//! the number of possible tags and the association between keys and entries are defined. -//! -//! # Properties -//! -//! The data-structure provides the following properties: -//! - When an operation returns success, then the represented multi-set is updated accordingly. For -//! example, an inserted entry can be found without alteration until replaced or deleted. -//! - When an operation returns an error, the resulting multi-set state is described in the error -//! documentation. -//! - When power is lost before an operation returns, the operation will either succeed or be -//! rolled-back on the next initialization. So the multi-set would be either left unchanged or -//! updated accordingly. -//! -//! Those properties rely on the following assumptions: -//! - Writing a word to flash is atomic. When power is lost, the word is either fully written or not -//! written at all. -//! - Reading a word from flash is deterministic. When power is lost while writing or erasing a word -//! (erasing a page containing that word), reading that word repeatedly returns the same result -//! (until it is written or its page is erased). -//! - To decide whether a page has been erased, it is enough to test if all its bits are equal to 1. -//! -//! The properties may still hold outside those assumptions but with weaker probabilities as the -//! usage diverges from the assumptions. -//! -//! # Implementation -//! -//! The store is a page-aligned sequence of bits. It matches the following grammar: -//! -//! ```text -//! Store := Page* -//! Page := PageHeader (Entry | InternalEntry)* Padding(page) -//! PageHeader := // must fit in one word -//! initialized:1 -//! erase_count:erase_bits -//! compacting:1 -//! new_page:page_bits -//! Padding(word) -//! Entry := Header Data Footer -//! // Let X be the byte (word-aligned for sensitive queries) following `length` in `Info`. -//! Header := Info[..X] // must fit in one word -//! Footer := Info[X..] // must fit in one word -//! Info := -//! present=0 -//! deleted:1 -//! internal=1 -//! replace:1 -//! sensitive:1 -//! length:byte_bits -//! tag:tag_bits -//! [ // present if `replace` is 0 -//! replace_page:page_bits -//! replace_byte:byte_bits -//! ] -//! [Padding(bit)] // until `complete` is the last bit of a different word than `present` -//! committed:1 -//! complete=0 -//! InternalEntry := -//! present=0 -//! deleted:1 -//! internal=0 -//! old_page:page_bits -//! saved_erase_count:erase_bits -//! Padding(word) -//! Padding(X) := 1* until X-aligned -//! ``` -//! -//! For bit flags, a value of 0 means true and a value of 1 means false. So when erased, bits are -//! false. They can be set to true by writing 0. -//! -//! The `Entry` rule is for user entries and the `InternalEntry` rule is for internal entries of the -//! store. Currently, there is only one kind of internal entry: an entry to erase the page being -//! compacted. -//! -//! The `Header` and `Footer` rules are computed from the `Info` rule. An entry could simply be the -//! concatenation of internal metadata and the user data. However, to optimize the size in flash, we -//! splice the user data in the middle of the metadata. The reason is that we can only write twice -//! the same word and for replace entries we need to write the deleted bit and the committed bit -//! independently. Also, this is important for the complete bit to be the last written bit (since -//! slices are written to flash from low to high addresses). Here is the representation of a -//! specific replace entry for a specific configuration: -//! -//! ```text -//! page_bits=6 -//! byte_bits=9 -//! tag_bits=5 -//! -//! byte.bit name -//! 0.0 present -//! 0.1 deleted -//! 0.2 internal -//! 0.3 replace -//! 0.4 sensitive -//! 0.5 length (9 bits) -//! 1.6 tag (least significant 2 bits out of 5) -//! (the header ends at the first byte boundary after `length`) -//! 2.0 (2 bytes in this example) -//! (the footer starts immediately after the user data) -//! 4.0 tag (most significant 3 bits out of 5) -//! 4.3 replace_page (6 bits) -//! 5.1 replace_byte (9 bits) -//! 6.2 padding (make sure the 2 properties below hold) -//! 7.6 committed -//! 7.7 complete (on a different word than `present`) -//! 8.0 (word-aligned) -//! ``` -//! -//! The store should always contain at least one blank page, so that it is always possible to -//! compact. - -// TODO(cretin): We don't need inner padding for insert entries. The store format can be: -// InsertEntry | ReplaceEntry | InternalEntry (maybe rename to EraseEntry) -// InsertEntry padding is until `complete` is the last bit of a word. -// ReplaceEntry padding is until `complete` is the last bit of a different word than `present`. -// TODO(cretin): Add checksum (may play the same role as the completed bit) and recovery strategy? -// TODO(cretin): Add corruption (deterministic but undetermined reads) to fuzzing. -// TODO(cretin): Add more complex transactions? (this does not seem necessary yet) -// TODO(cretin): Add possibility to shred an entry (force compact page after delete)? - -mod bitfield; -mod format; - -use self::format::{Format, IsReplace}; -use super::{Index, Storage}; -#[cfg(any(test, feature = "ram_storage"))] -use crate::embedded_flash::BufferStorage; -#[cfg(any(test, feature = "ram_storage"))] -use alloc::boxed::Box; -use alloc::collections::BTreeMap; -use alloc::vec; -use alloc::vec::Vec; - -/// Configures a store. -pub trait StoreConfig { - /// How entries are keyed. - /// - /// To disable keys, this may be defined to `()` or even better a custom empty enum. - type Key: Ord; - - /// Number of entry tags. - /// - /// All tags must be smaller than this value. - /// - /// To disable tags, this function should return `1`. The only valid tag would then be `0`. - fn num_tags(&self) -> usize; - - /// Specifies the set of keys of an entry. - /// - /// If keys are not used, this function can immediately return. Otherwise, it should call - /// `associate_key` for each key that should be associated to `entry`. - fn keys(&self, entry: StoreEntry, associate_key: impl FnMut(Self::Key)); -} - -/// Errors returned by store operations. -#[derive(Debug, PartialEq, Eq)] -pub enum StoreError { - /// The operation could not proceed because the store is full. - StoreFull, - - /// The operation could not proceed because the provided tag is invalid. - InvalidTag, - - /// The operation could not proceed because the preconditions do not hold. - InvalidPrecondition, -} - -/// The position of an entry in the store. -#[cfg_attr(feature = "std", derive(Debug))] -#[derive(Copy, Clone)] -pub struct StoreIndex { - /// The index of this entry in the storage. - index: Index, - - /// The generation at which this index is valid. - /// - /// See the documentation of the field with the same name in the `Store` struct. - generation: usize, -} - -/// A user entry. -#[cfg_attr(feature = "std", derive(Debug, PartialEq, Eq))] -#[derive(Copy, Clone)] -pub struct StoreEntry<'a> { - /// The tag of the entry. - /// - /// Must be smaller than the configured number of tags. - pub tag: usize, - - /// The data of the entry. - pub data: &'a [u8], - - /// Whether the data is sensitive. - /// - /// Sensitive data is overwritten with zeroes when the entry is deleted. - pub sensitive: bool, -} - -/// Implements a configurable multi-set on top of any storage. -pub struct Store { - storage: S, - config: C, - format: Format, - - /// The index of the blank page reserved for compaction. - blank_page: usize, - - /// Counts the number of compactions since the store creation. - /// - /// A `StoreIndex` is valid only if they originate from the same generation. This is checked by - /// operations that take a `StoreIndex` as argument. - generation: usize, -} - -impl Store { - /// Creates a new store. - /// - /// Initializes the storage if it is fresh (filled with `0xff`). Rolls-back or completes an - /// operation if the store was powered off in the middle of that operation. In other words, - /// operations are atomic. - /// - /// # Errors - /// - /// Returns `None` if `storage` and/or `config` are not supported. - pub fn new(storage: S, config: C) -> Option> { - let format = Format::new(&storage, &config)?; - let blank_page = format.num_pages; - let mut store = Store { - storage, - config, - format, - blank_page, - generation: 0, - }; - // Finish any ongoing page compaction. - store.recover_compact_page(); - // Finish or roll-back any other entry-level operations. - store.recover_entry_operations(); - // Initialize uninitialized pages. - store.initialize_storage(); - Some(store) - } - - /// Iterates over all entries in the store. - pub fn iter(&self) -> impl Iterator { - Iter::new(self).filter_map(move |(index, entry)| { - if self.format.is_alive(entry) { - Some(( - StoreIndex { - index, - generation: self.generation, - }, - StoreEntry { - tag: self.format.get_tag(entry), - data: self.format.get_data(entry), - sensitive: self.format.is_sensitive(entry), - }, - )) - } else { - None - } - }) - } - - /// Iterates over all entries matching a key in the store. - pub fn find_all<'a>( - &'a self, - key: &'a C::Key, - ) -> impl Iterator + 'a { - self.iter().filter(move |&(_, entry)| { - let mut has_match = false; - self.config.keys(entry, |k| has_match |= key == &k); - has_match - }) - } - - /// Returns the first entry matching a key in the store. - /// - /// This is a convenience function for when at most one entry should match the key. - /// - /// # Panics - /// - /// In debug mode, panics if more than one entry matches the key. - pub fn find_one<'a>(&'a self, key: &'a C::Key) -> Option<(StoreIndex, StoreEntry<'a>)> { - let mut iter = self.find_all(key); - let first = iter.next()?; - let has_only_one_element = iter.next().is_none(); - debug_assert!(has_only_one_element); - Some(first) - } - - /// Deletes an entry from the store. - pub fn delete(&mut self, index: StoreIndex) -> Result<(), StoreError> { - if self.generation != index.generation { - return Err(StoreError::InvalidPrecondition); - } - self.delete_index(index.index); - Ok(()) - } - - /// Replaces an entry with another with the same tag in the store. - /// - /// This operation (like others) is atomic. If it returns successfully, then the old entry is - /// deleted and the new is inserted. If it fails, the old entry is not deleted and the new entry - /// is not inserted. If power is lost during the operation, during next startup, the operation - /// is either rolled-back (like in case of failure) or completed (like in case of success). - /// - /// # Errors - /// - /// Returns: - /// - `StoreFull` if the new entry does not fit in the store. - /// - `InvalidTag` if the tag of the new entry is not smaller than the configured number of - /// tags. - pub fn replace(&mut self, old: StoreIndex, new: StoreEntry) -> Result<(), StoreError> { - if self.generation != old.generation { - return Err(StoreError::InvalidPrecondition); - } - self.format.validate_entry(new)?; - let mut old_index = old.index; - // Find a slot. - let entry_len = self.replace_len(new.sensitive, new.data.len()); - let index = self.find_slot_for_write(entry_len, Some(&mut old_index))?; - // Build a new entry replacing the old one. - let entry = self.format.build_entry(Some(old_index), new); - debug_assert_eq!(entry.len(), entry_len); - // Write the new entry. - self.write_entry(index, &entry); - // Commit the new entry, which both deletes the old entry and commits the new one. - self.commit_index(index); - Ok(()) - } - - /// Inserts an entry in the store. - /// - /// # Errors - /// - /// Returns: - /// - `StoreFull` if the new entry does not fit in the store. - /// - `InvalidTag` if the tag of the new entry is not smaller than the configured number of - /// tags. - pub fn insert(&mut self, entry: StoreEntry) -> Result<(), StoreError> { - self.format.validate_entry(entry)?; - // Build entry. - let entry = self.format.build_entry(None, entry); - // Find a slot. - let index = self.find_slot_for_write(entry.len(), None)?; - // Write entry. - self.write_entry(index, &entry); - Ok(()) - } - - /// Returns the byte cost of a replace operation. - /// - /// Computes the length in bytes that would be used in the storage if a replace operation is - /// executed provided the data of the new entry has `length` bytes and whether this data is - /// sensitive. - pub fn replace_len(&self, sensitive: bool, length: usize) -> usize { - self.format - .entry_size(IsReplace::Replace, sensitive, length) - } - - /// Returns the byte cost of an insert operation. - /// - /// Computes the length in bytes that would be used in the storage if an insert operation is - /// executed provided the data of the inserted entry has `length` bytes and whether this data is - /// sensitive. - #[allow(dead_code)] - pub fn insert_len(&self, sensitive: bool, length: usize) -> usize { - self.format.entry_size(IsReplace::Insert, sensitive, length) - } - - /// Returns the erase count of all pages. - /// - /// The value at index `page` of the result is the number of times page `page` was erased. This - /// number is an underestimate in case power was lost when this page was erased. - #[allow(dead_code)] - pub fn compaction_info(&self) -> Vec { - let mut info = Vec::with_capacity(self.format.num_pages); - for page in 0..self.format.num_pages { - let (page_header, _) = self.read_page_header(page); - let erase_count = self.format.get_erase_count(page_header); - info.push(erase_count); - } - info - } - - /// Completes any ongoing page compaction. - fn recover_compact_page(&mut self) { - for page in 0..self.format.num_pages { - let (page_header, _) = self.read_page_header(page); - if self.format.is_compacting(page_header) { - let new_page = self.format.get_new_page(page_header); - self.compact_page(page, new_page); - } - } - } - - /// Rolls-back or completes any ongoing operation. - fn recover_entry_operations(&mut self) { - for page in 0..self.format.num_pages { - let (page_header, mut index) = self.read_page_header(page); - if !self.format.is_initialized(page_header) { - // Skip uninitialized pages. - continue; - } - while index.byte < self.format.page_size { - let entry_index = index; - let entry = self.read_entry(index); - index.byte += entry.len(); - if !self.format.is_present(entry) { - // Reached the end of the page. - } else if self.format.is_deleted(entry) { - // Wipe sensitive data if needed. - self.wipe_sensitive_data(entry_index); - } else if self.format.is_internal(entry) { - // Finish page compaction. - self.erase_page(entry_index); - } else if !self.format.is_complete(entry) { - // Roll-back incomplete operations. - self.delete_index(entry_index); - } else if !self.format.is_committed(entry) { - // Finish complete but uncommitted operations. - self.commit_index(entry_index) - } - } - } - } - - /// Initializes uninitialized pages. - fn initialize_storage(&mut self) { - for page in 0..self.format.num_pages { - let (header, index) = self.read_page_header(page); - if self.format.is_initialized(header) { - // Update blank page. - let first_entry = self.read_entry(index); - if !self.format.is_present(first_entry) { - self.blank_page = page; - } - } else { - // We set the erase count to zero the very first time we initialize a page. - self.initialize_page(page, 0); - } - } - debug_assert!(self.blank_page != self.format.num_pages); - } - - /// Marks an entry as deleted. - /// - /// The provided index must point to the beginning of an entry. - fn delete_index(&mut self, index: Index) { - self.update_word(index, |format, word| format.set_deleted(word)); - self.wipe_sensitive_data(index); - } - - /// Wipes the data of a sensitive entry. - /// - /// If the entry at the provided index is sensitive, overwrites the data with zeroes. Otherwise, - /// does nothing. - fn wipe_sensitive_data(&mut self, mut index: Index) { - let entry = self.read_entry(index); - debug_assert!(self.format.is_present(entry)); - debug_assert!(self.format.is_deleted(entry)); - if self.format.is_internal(entry) || !self.format.is_sensitive(entry) { - // No need to wipe the data. - return; - } - let gap = self.format.entry_gap(entry); - let data = gap.slice(entry); - if data.iter().all(|&byte| byte == 0x00) { - // The data is already wiped. - return; - } - index.byte += gap.start; - self.storage - .write_slice(index, &vec![0; gap.length]) - .unwrap(); - } - - /// Finds a page with enough free space. - /// - /// Returns an index to the free space of a page which can hold an entry of `length` bytes. If - /// necessary, pages may be compacted to free space. In that case, if provided, the `old_index` - /// is updated according to compaction. - fn find_slot_for_write( - &mut self, - length: usize, - mut old_index: Option<&mut Index>, - ) -> Result { - loop { - if let Some(index) = self.choose_slot_for_write(length) { - return Ok(index); - } - match self.choose_page_for_compact() { - None => return Err(StoreError::StoreFull), - Some(page) => { - let blank_page = self.blank_page; - // Compact the chosen page and update the old index to point to the entry in the - // new page if it happened to be in the old page. This is essentially a way to - // avoid index invalidation due to compaction. - let map = self.compact_page(page, blank_page); - if let Some(old_index) = &mut old_index { - map_index(page, blank_page, &map, old_index); - } - } - } - } - } - - /// Returns whether a page has enough free space. - /// - /// Returns an index to the free space of a page with smallest free space that may hold `length` - /// bytes. - fn choose_slot_for_write(&self, length: usize) -> Option { - Iter::new(self) - .filter(|(index, entry)| { - index.page != self.blank_page - && !self.format.is_present(entry) - && length <= entry.len() - }) - .min_by_key(|(_, entry)| entry.len()) - .map(|(index, _)| index) - } - - /// Returns the page that should be compacted. - fn choose_page_for_compact(&self) -> Option { - // TODO(cretin): This could be optimized by using some cost function depending on: - // - the erase count - // - the length of the free space - // - the length of the alive entries - // We want to minimize this cost. We could also take into account the length of the entry we - // want to write to bound the number of compaction before failing with StoreFull. - // - // We should also make sure that all pages (including if they have no deleted entries and no - // free space) are eventually compacted (ideally to a heavily used page) to benefit from the - // low erase count of those pages. - (0..self.format.num_pages) - .map(|page| (page, self.page_info(page))) - .filter(|&(page, ref info)| { - page != self.blank_page - && info.erase_count < self.format.max_page_erases - && info.deleted_length > self.format.internal_entry_size() - }) - .min_by(|(_, lhs_info), (_, rhs_info)| lhs_info.compare_for_compaction(rhs_info)) - .map(|(page, _)| page) - } - - fn page_info(&self, page: usize) -> PageInfo { - let (page_header, mut index) = self.read_page_header(page); - let mut info = PageInfo { - erase_count: self.format.get_erase_count(page_header), - deleted_length: 0, - free_length: 0, - }; - while index.byte < self.format.page_size { - let entry = self.read_entry(index); - index.byte += entry.len(); - if !self.format.is_present(entry) { - debug_assert_eq!(info.free_length, 0); - info.free_length = entry.len(); - } else if self.format.is_deleted(entry) { - info.deleted_length += entry.len(); - } - } - debug_assert_eq!(index.page, page); - info - } - - fn read_slice(&self, index: Index, length: usize) -> &[u8] { - self.storage.read_slice(index, length).unwrap() - } - - /// Reads an entry (with header and footer) at a given index. - /// - /// If no entry is present, returns the free space up to the end of the page. - fn read_entry(&self, index: Index) -> &[u8] { - let first_byte = self.read_slice(index, 1); - let max_length = self.format.page_size - index.byte; - let mut length = if !self.format.is_present(first_byte) { - max_length - } else if self.format.is_internal(first_byte) { - self.format.internal_entry_size() - } else { - // We don't know if the entry is sensitive or not, but it doesn't matter here. We just - // need to read the replace, sensitive, and length fields. - let header = self.read_slice(index, self.format.header_size(false)); - let replace = self.format.is_replace(header); - let sensitive = self.format.is_sensitive(header); - let length = self.format.get_length(header); - self.format.entry_size(replace, sensitive, length) - }; - // Truncate the length to fit the page. This can only happen in case of corruption or - // partial writes. - length = core::cmp::min(length, max_length); - self.read_slice(index, length) - } - - /// Reads a page header. - /// - /// Also returns the index after the page header. - fn read_page_header(&self, page: usize) -> (&[u8], Index) { - let mut index = Index { page, byte: 0 }; - let page_header = self.read_slice(index, self.format.page_header_size()); - index.byte += page_header.len(); - (page_header, index) - } - - /// Updates a word at a given index. - /// - /// The `update` function is called with the word at `index`. The input value is the current - /// value of the word. The output value is the value that will be written. It should only change - /// bits from 1 to 0. - fn update_word(&mut self, index: Index, update: impl FnOnce(&Format, &mut [u8])) { - let word_size = self.format.word_size; - let mut word = self.read_slice(index, word_size).to_vec(); - update(&self.format, &mut word); - self.storage.write_slice(index, &word).unwrap(); - } - - fn write_entry(&mut self, index: Index, entry: &[u8]) { - self.storage.write_slice(index, entry).unwrap(); - } - - /// Initializes a page by writing the page header. - /// - /// If the page is not erased, it is first erased. - fn initialize_page(&mut self, page: usize, erase_count: usize) { - let index = Index { page, byte: 0 }; - let page = self.read_slice(index, self.format.page_size); - if !page.iter().all(|&byte| byte == 0xff) { - self.storage.erase_page(index.page).unwrap(); - } - self.update_word(index, |format, header| { - format.set_initialized(header); - format.set_erase_count(header, erase_count); - }); - self.blank_page = index.page; - } - - /// Commits a replace entry. - /// - /// Deletes the old entry and commits the new entry. - fn commit_index(&mut self, mut index: Index) { - let entry = self.read_entry(index); - index.byte += entry.len(); - let word_size = self.format.word_size; - debug_assert!(entry.len() >= 2 * word_size); - match self.format.is_replace(entry) { - IsReplace::Replace => { - let delete_index = self.format.get_replace_index(entry); - self.delete_index(delete_index); - } - IsReplace::Insert => debug_assert!(false), - }; - index.byte -= word_size; - self.update_word(index, |format, word| format.set_committed(word)); - } - - /// Compacts a page to an other. - /// - /// Returns the mapping from the alive entries in the old page to their index in the new page. - fn compact_page(&mut self, old_page: usize, new_page: usize) -> BTreeMap { - // Write the old page as being compacted to the new page. - let mut erase_count = 0; - self.update_word( - Index { - page: old_page, - byte: 0, - }, - |format, header| { - erase_count = format.get_erase_count(header); - format.set_compacting(header); - format.set_new_page(header, new_page); - }, - ); - // Copy alive entries from the old page to the new page. - let page_header_size = self.format.page_header_size(); - let mut old_index = Index { - page: old_page, - byte: page_header_size, - }; - let mut new_index = Index { - page: new_page, - byte: page_header_size, - }; - let mut map = BTreeMap::new(); - while old_index.byte < self.format.page_size { - let old_entry = self.read_entry(old_index); - let old_entry_index = old_index.byte; - old_index.byte += old_entry.len(); - if !self.format.is_alive(old_entry) { - continue; - } - let previous_mapping = map.insert(old_entry_index, new_index.byte); - debug_assert!(previous_mapping.is_none()); - // We need to copy the old entry because it is in the storage and we are going to write - // to the storage. Rust cannot tell that both entries don't overlap. - let old_entry = old_entry.to_vec(); - self.write_entry(new_index, &old_entry); - new_index.byte += old_entry.len(); - } - // Save the old page index and erase count to the new page. - let erase_index = new_index; - let erase_entry = self.format.build_erase_entry(old_page, erase_count); - self.write_entry(new_index, &erase_entry); - // Erase the page. - self.erase_page(erase_index); - // Increase generation. - self.generation += 1; - map - } - - /// Commits an internal entry. - /// - /// The only kind of internal entry is to erase a page, which first erases the page, then - /// initializes it with the saved erase count, and finally deletes the internal entry. - fn erase_page(&mut self, erase_index: Index) { - let erase_entry = self.read_entry(erase_index); - debug_assert!(self.format.is_present(erase_entry)); - debug_assert!(!self.format.is_deleted(erase_entry)); - debug_assert!(self.format.is_internal(erase_entry)); - let old_page = self.format.get_old_page(erase_entry); - let erase_count = self.format.get_saved_erase_count(erase_entry) + 1; - // Erase the page. - self.storage.erase_page(old_page).unwrap(); - // Initialize the page. - self.initialize_page(old_page, erase_count); - // Delete the internal entry. - self.delete_index(erase_index); - } -} - -// Those functions are not meant for production. -#[cfg(any(test, feature = "ram_storage"))] -impl Store { - /// Takes a snapshot of the storage after a given amount of word operations. - pub fn arm_snapshot(&mut self, delay: usize) { - self.storage.arm_snapshot(delay); - } - - /// Unarms and returns the snapshot or the delay remaining. - pub fn get_snapshot(&mut self) -> Result, usize> { - self.storage.get_snapshot() - } - - /// Takes a snapshot of the storage. - pub fn take_snapshot(&self) -> Box<[u8]> { - self.storage.take_snapshot() - } - - /// Returns the storage. - pub fn get_storage(self) -> Box<[u8]> { - self.storage.get_storage() - } - - /// Erases and initializes a page with a given erase count. - pub fn set_erase_count(&mut self, page: usize, erase_count: usize) { - self.initialize_page(page, erase_count); - } - - /// Returns whether all deleted sensitive entries have been wiped. - pub fn deleted_entries_are_wiped(&self) -> bool { - for (_, entry) in Iter::new(self) { - if !self.format.is_present(entry) - || !self.format.is_deleted(entry) - || self.format.is_internal(entry) - || !self.format.is_sensitive(entry) - { - continue; - } - let gap = self.format.entry_gap(entry); - let data = gap.slice(entry); - if !data.iter().all(|&byte| byte == 0x00) { - return false; - } - } - true - } -} - -/// Maps an index from an old page to a new page if needed. -fn map_index(old_page: usize, new_page: usize, map: &BTreeMap, index: &mut Index) { - if index.page == old_page { - index.page = new_page; - index.byte = *map.get(&index.byte).unwrap(); - } -} - -/// Page information for compaction. -struct PageInfo { - /// How many times the page was erased. - erase_count: usize, - - /// Cumulative length of deleted entries (including header and footer). - deleted_length: usize, - - /// Length of the free space. - free_length: usize, -} - -impl PageInfo { - /// Returns whether a page should be compacted before another. - fn compare_for_compaction(&self, rhs: &PageInfo) -> core::cmp::Ordering { - self.erase_count - .cmp(&rhs.erase_count) - .then(rhs.deleted_length.cmp(&self.deleted_length)) - .then(self.free_length.cmp(&rhs.free_length)) - } -} - -/// Iterates over all entries (including free space) of a store. -struct Iter<'a, S: Storage, C: StoreConfig> { - store: &'a Store, - index: Index, -} - -impl<'a, S: Storage, C: StoreConfig> Iter<'a, S, C> { - fn new(store: &'a Store) -> Iter<'a, S, C> { - let index = Index { - page: 0, - byte: store.format.page_header_size(), - }; - Iter { store, index } - } -} - -impl<'a, S: Storage, C: StoreConfig> Iterator for Iter<'a, S, C> { - type Item = (Index, &'a [u8]); - - fn next(&mut self) -> Option<(Index, &'a [u8])> { - if self.index.byte == self.store.format.page_size { - self.index.page += 1; - self.index.byte = self.store.format.page_header_size(); - } - if self.index.page == self.store.format.num_pages { - return None; - } - let index = self.index; - let entry = self.store.read_entry(self.index); - self.index.byte += entry.len(); - Some((index, entry)) - } -} - -#[cfg(test)] -mod tests { - use super::super::{BufferOptions, BufferStorage}; - use super::*; - - struct Config; - - const WORD_SIZE: usize = 4; - const PAGE_SIZE: usize = 8 * WORD_SIZE; - const NUM_PAGES: usize = 3; - - impl StoreConfig for Config { - type Key = u8; - - fn num_tags(&self) -> usize { - 1 - } - - fn keys(&self, entry: StoreEntry, mut add: impl FnMut(u8)) { - assert_eq!(entry.tag, 0); - if !entry.data.is_empty() { - add(entry.data[0]); - } - } - } - - fn new_buffer(storage: Box<[u8]>) -> BufferStorage { - let options = BufferOptions { - word_size: WORD_SIZE, - page_size: PAGE_SIZE, - max_word_writes: 2, - max_page_erases: 2, - strict_write: true, - }; - BufferStorage::new(storage, options) - } - - fn new_store() -> Store { - let storage = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); - Store::new(new_buffer(storage), Config).unwrap() - } - - #[test] - fn insert_ok() { - let mut store = new_store(); - assert_eq!(store.iter().count(), 0); - let tag = 0; - let key = 1; - let data = &[key, 2]; - let entry = StoreEntry { - tag, - data, - sensitive: false, - }; - store.insert(entry).unwrap(); - assert_eq!(store.iter().count(), 1); - assert_eq!(store.find_one(&key).unwrap().1, entry); - } - - #[test] - fn insert_sensitive_ok() { - let mut store = new_store(); - let tag = 0; - let key = 1; - let data = &[key, 4]; - let entry = StoreEntry { - tag, - data, - sensitive: true, - }; - store.insert(entry).unwrap(); - assert_eq!(store.iter().count(), 1); - assert_eq!(store.find_one(&key).unwrap().1, entry); - } - - #[test] - fn delete_ok() { - let mut store = new_store(); - let tag = 0; - let key = 1; - let entry = StoreEntry { - tag, - data: &[key, 2], - sensitive: false, - }; - store.insert(entry).unwrap(); - assert_eq!(store.find_all(&key).count(), 1); - let (index, _) = store.find_one(&key).unwrap(); - store.delete(index).unwrap(); - assert_eq!(store.find_all(&key).count(), 0); - assert_eq!(store.iter().count(), 0); - } - - #[test] - fn delete_sensitive_ok() { - let mut store = new_store(); - let tag = 0; - let key = 1; - let entry = StoreEntry { - tag, - data: &[key, 2], - sensitive: true, - }; - store.insert(entry).unwrap(); - assert_eq!(store.find_all(&key).count(), 1); - let (index, _) = store.find_one(&key).unwrap(); - store.delete(index).unwrap(); - assert_eq!(store.find_all(&key).count(), 0); - assert_eq!(store.iter().count(), 0); - assert!(store.deleted_entries_are_wiped()); - } - - #[test] - fn insert_until_full() { - let mut store = new_store(); - let tag = 0; - let mut key = 0; - while store - .insert(StoreEntry { - tag, - data: &[key, 0], - sensitive: false, - }) - .is_ok() - { - key += 1; - } - assert!(key > 0); - } - - #[test] - fn compact_ok() { - let mut store = new_store(); - let tag = 0; - let mut key = 0; - while store - .insert(StoreEntry { - tag, - data: &[key, 0], - sensitive: false, - }) - .is_ok() - { - key += 1; - } - let (index, _) = store.find_one(&0).unwrap(); - store.delete(index).unwrap(); - store - .insert(StoreEntry { - tag: 0, - data: &[key, 0], - sensitive: false, - }) - .unwrap(); - for k in 1..=key { - assert_eq!(store.find_all(&k).count(), 1); - } - } - - #[test] - fn reboot_ok() { - let mut store = new_store(); - let tag = 0; - let key = 1; - let data = &[key, 2]; - let entry = StoreEntry { - tag, - data, - sensitive: false, - }; - store.insert(entry).unwrap(); - - // Reboot the store. - let store = store.get_storage(); - let store = Store::new(new_buffer(store), Config).unwrap(); - - assert_eq!(store.iter().count(), 1); - assert_eq!(store.find_one(&key).unwrap().1, entry); - } - - #[test] - fn replace_atomic() { - let tag = 0; - let key = 1; - let old_entry = StoreEntry { - tag, - data: &[key, 2, 3, 4, 5, 6], - sensitive: false, - }; - let new_entry = StoreEntry { - tag, - data: &[key, 7, 8, 9], - sensitive: false, - }; - let mut delay = 0; - loop { - let mut store = new_store(); - store.insert(old_entry).unwrap(); - store.arm_snapshot(delay); - let (index, _) = store.find_one(&key).unwrap(); - store.replace(index, new_entry).unwrap(); - let (complete, store) = match store.get_snapshot() { - Err(_) => (true, store.get_storage()), - Ok(store) => (false, store), - }; - let store = Store::new(new_buffer(store), Config).unwrap(); - assert_eq!(store.iter().count(), 1); - assert_eq!(store.find_all(&key).count(), 1); - let (_, cur_entry) = store.find_one(&key).unwrap(); - assert!((cur_entry == old_entry && !complete) || cur_entry == new_entry); - if complete { - break; - } - delay += 1; - } - } - - #[test] - fn compact_atomic() { - let tag = 0; - let mut delay = 0; - loop { - let mut store = new_store(); - let mut key = 0; - while store - .insert(StoreEntry { - tag, - data: &[key, 0], - sensitive: false, - }) - .is_ok() - { - key += 1; - } - let (index, _) = store.find_one(&0).unwrap(); - store.delete(index).unwrap(); - let (index, _) = store.find_one(&1).unwrap(); - store.arm_snapshot(delay); - store - .replace( - index, - StoreEntry { - tag, - data: &[1, 1], - sensitive: false, - }, - ) - .unwrap(); - let (complete, store) = match store.get_snapshot() { - Err(_) => (true, store.get_storage()), - Ok(store) => (false, store), - }; - let store = Store::new(new_buffer(store), Config).unwrap(); - assert_eq!(store.iter().count(), key as usize - 1); - for k in 2..key { - assert_eq!(store.find_all(&k).count(), 1); - assert_eq!( - store.find_one(&k).unwrap().1, - StoreEntry { - tag, - data: &[k, 0], - sensitive: false, - } - ); - } - assert_eq!(store.find_all(&1).count(), 1); - let (_, entry) = store.find_one(&1).unwrap(); - assert_eq!(entry.tag, tag); - assert!((entry.data == [1, 0] && !complete) || entry.data == [1, 1]); - if complete { - break; - } - delay += 1; - } - } - - #[test] - fn invalid_tag() { - let mut store = new_store(); - let entry = StoreEntry { - tag: 1, - data: &[], - sensitive: false, - }; - assert_eq!(store.insert(entry), Err(StoreError::InvalidTag)); - } - - #[test] - fn invalid_length() { - let mut store = new_store(); - let entry = StoreEntry { - tag: 0, - data: &[0; PAGE_SIZE], - sensitive: false, - }; - assert_eq!(store.insert(entry), Err(StoreError::StoreFull)); - } -} diff --git a/src/embedded_flash/syscall.rs b/src/embedded_flash/syscall.rs index 5eae561..e043772 100644 --- a/src/embedded_flash/syscall.rs +++ b/src/embedded_flash/syscall.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-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. @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::{Index, Storage, StorageError, StorageResult}; use alloc::vec::Vec; use libtock_core::syscalls; +use persistent_store::{Storage, StorageError, StorageIndex, StorageResult}; const DRIVER_NUMBER: usize = 0x50003; @@ -42,15 +42,13 @@ mod memop_nr { fn get_info(nr: usize, arg: usize) -> StorageResult { let code = syscalls::command(DRIVER_NUMBER, command_nr::GET_INFO, nr, arg); - code.map_err(|e| StorageError::KernelError { - code: e.return_code, - }) + code.map_err(|_| StorageError::CustomError) } fn memop(nr: u32, arg: usize) -> StorageResult { let code = unsafe { syscalls::raw::memop(nr, arg) }; if code < 0 { - Err(StorageError::KernelError { code }) + Err(StorageError::CustomError) } else { Ok(code as usize) } @@ -70,7 +68,7 @@ impl SyscallStorage { /// /// # Errors /// - /// Returns `BadFlash` if any of the following conditions do not hold: + /// Returns `CustomError` if any of the following conditions do not hold: /// - The word size is a power of two. /// - The page size is a power of two. /// - The page size is a multiple of the word size. @@ -90,13 +88,13 @@ impl SyscallStorage { || !syscall.page_size.is_power_of_two() || !syscall.is_word_aligned(syscall.page_size) { - return Err(StorageError::BadFlash); + return Err(StorageError::CustomError); } for i in 0..memop(memop_nr::STORAGE_CNT, 0)? { let storage_ptr = memop(memop_nr::STORAGE_PTR, i)?; let max_storage_len = memop(memop_nr::STORAGE_LEN, i)?; if !syscall.is_page_aligned(storage_ptr) || !syscall.is_page_aligned(max_storage_len) { - return Err(StorageError::BadFlash); + return Err(StorageError::CustomError); } let storage_len = core::cmp::min(num_pages * syscall.page_size, max_storage_len); num_pages -= storage_len / syscall.page_size; @@ -141,12 +139,12 @@ impl Storage for SyscallStorage { self.max_page_erases } - fn read_slice(&self, index: Index, length: usize) -> StorageResult<&[u8]> { + fn read_slice(&self, index: StorageIndex, length: usize) -> StorageResult<&[u8]> { let start = index.range(length, self)?.start; find_slice(&self.storage_locations, start, length) } - fn write_slice(&mut self, index: Index, value: &[u8]) -> StorageResult<()> { + fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> StorageResult<()> { if !self.is_word_aligned(index.byte) || !self.is_word_aligned(value.len()) { return Err(StorageError::NotAligned); } @@ -163,28 +161,24 @@ impl Storage for SyscallStorage { ) }; if code < 0 { - return Err(StorageError::KernelError { code }); + return Err(StorageError::CustomError); } let code = syscalls::command(DRIVER_NUMBER, command_nr::WRITE_SLICE, ptr, value.len()); - if let Err(e) = code { - return Err(StorageError::KernelError { - code: e.return_code, - }); + if code.is_err() { + return Err(StorageError::CustomError); } Ok(()) } fn erase_page(&mut self, page: usize) -> StorageResult<()> { - let index = Index { page, byte: 0 }; + let index = StorageIndex { page, byte: 0 }; let length = self.page_size(); let ptr = self.read_slice(index, length)?.as_ptr() as usize; let code = syscalls::command(DRIVER_NUMBER, command_nr::ERASE_PAGE, ptr, length); - if let Err(e) = code { - return Err(StorageError::KernelError { - code: e.return_code, - }); + if code.is_err() { + return Err(StorageError::CustomError); } Ok(()) } From 51681e49100887dc4b6d46ac86db8426d0df73ef Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 13 Nov 2020 06:51:53 +0100 Subject: [PATCH 003/192] changes operation touch behaviour --- src/ctap/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index b3bbd64..06e3a57 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -345,7 +345,7 @@ where if let Some(auth_param) = &pin_uv_auth_param { // This case was added in FIDO 2.1. if auth_param.is_empty() { - let _ = (self.check_user_presence)(cid); + (self.check_user_presence)(cid)?; if self.persistent_store.pin_hash()?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { @@ -549,7 +549,7 @@ where if let Some(auth_param) = &pin_uv_auth_param { // This case was added in FIDO 2.1. if auth_param.is_empty() { - let _ = (self.check_user_presence)(cid); + (self.check_user_presence)(cid)?; if self.persistent_store.pin_hash()?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { From 58b5e4d8fab3d72443a58ea2aaabd624bc5df46a Mon Sep 17 00:00:00 2001 From: Mirna Date: Fri, 13 Nov 2020 09:32:59 +0200 Subject: [PATCH 004/192] Add short APDUs parser --- src/ctap/apdu.rs | 303 +++++++++++++++++++++++++++++++++++++++++++++++ src/ctap/mod.rs | 1 + 2 files changed, 304 insertions(+) create mode 100644 src/ctap/apdu.rs diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs new file mode 100644 index 0000000..f324a03 --- /dev/null +++ b/src/ctap/apdu.rs @@ -0,0 +1,303 @@ +use alloc::vec::Vec; +use core::convert::TryFrom; + +type ByteArray = &'static [u8]; + +#[cfg_attr(test, derive(Clone, Debug))] +#[allow(non_camel_case_types)] +#[derive(PartialEq)] +pub enum ApduStatusCode { + SW_SUCCESS, + /// Command successfully executed; 'XX' bytes of data are + /// available and can be requested using GET RESPONSE. + SW_GET_RESPONSE, + SW_WRONG_DATA, + SW_WRONG_LENGTH, + SW_COND_USE_NOT_SATISFIED, + SW_FILE_NOT_FOUND, + SW_INCORRECT_P1P2, + /// Instruction code not supported or invalid + SW_INS_INVALID, + SW_CLA_INVALID, + SW_INTERNAL_EXCEPTION, +} + +impl From for ByteArray { + fn from(status_code: ApduStatusCode) -> ByteArray { + match status_code { + ApduStatusCode::SW_SUCCESS => b"\x90\x00", + ApduStatusCode::SW_GET_RESPONSE => b"\x61\x00", + ApduStatusCode::SW_WRONG_DATA => b"\x6A\x80", + ApduStatusCode::SW_WRONG_LENGTH => b"\x67\x00", + ApduStatusCode::SW_COND_USE_NOT_SATISFIED => b"\x69\x85", + ApduStatusCode::SW_FILE_NOT_FOUND => b"\x6a\x82", + ApduStatusCode::SW_INCORRECT_P1P2 => b"\x6a\x86", + ApduStatusCode::SW_INS_INVALID => b"\x6d\x00", + ApduStatusCode::SW_CLA_INVALID => b"\x6e\x00", + ApduStatusCode::SW_INTERNAL_EXCEPTION => b"\x6f\x00", + } + } +} + +#[allow(non_camel_case_types, dead_code)] +pub enum ApduIns { + SELECT = 0xA4, + READ_BINARY = 0xB0, + GET_RESPONSE = 0xC0, +} + +#[cfg_attr(test, derive(Clone, Debug))] +#[allow(dead_code)] +#[derive(Default, PartialEq)] +pub struct ApduHeader { + cla: u8, + ins: u8, + p1: u8, + p2: u8, +} + +impl From<&[u8]> for ApduHeader { + fn from(header: &[u8]) -> Self { + ApduHeader { + cla: header[0], + ins: header[1], + p1: header[2], + p2: header[3], + } + } +} + +#[cfg_attr(test, derive(Clone, Debug))] +#[allow(dead_code)] +#[derive(Default, PartialEq)] +pub struct APDU { + header: ApduHeader, + lc: u16, + data: Vec, + le: u32, + case_type: u8, +} + +const APDU_HEADER_LEN: u8 = 4; + +impl TryFrom<&[u8]> for APDU { + type Error = ApduStatusCode; + + fn try_from(frame: &[u8]) -> Result { + if frame.len() < APDU_HEADER_LEN as usize { + return Err(ApduStatusCode::SW_WRONG_DATA); + } + // +-----+-----+----+----+ + // header | CLA | INS | P1 | P2 | + // +-----+-----+----+----+ + let (header, payload) = frame.split_at(APDU_HEADER_LEN as usize); + + let mut apdu = APDU { + header: header.into(), + lc: 0x00, + data: Vec::new(), + le: 0x00, + case_type: 0x00, + }; + + // case 1 + if payload.is_empty() { + apdu.case_type = 0x01; + } else { + let byte_0 = payload[0]; + // case 2S (Le) + if payload.len() == 1 { + apdu.case_type = 0x02; + apdu.le = if byte_0 == 0x00 { + // Ne = 256 + 0x100 + } else { + byte_0.into() + } + } + // case 3S (Lc + data) + if payload.len() == (1 + byte_0) as usize && byte_0 != 0 { + apdu.case_type = 0x03; + apdu.lc = byte_0.into(); + apdu.data = payload[1..].to_vec(); + } + // case 4S (Lc + data + Le) + if payload.len() == (1 + byte_0 + 1) as usize && byte_0 != 0 { + apdu.case_type = 0x04; + apdu.lc = byte_0.into(); + apdu.data = payload[1..(payload.len() - 1)].to_vec(); + apdu.le = (*payload.last().unwrap()).into(); + if apdu.le == 0x00 { + apdu.le = 0x100; + } + } + } + // TODO: Add extended length cases + if apdu.case_type == 0x00 { + return Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED); + } + Ok(apdu) + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn pass_frame(frame: &[u8]) -> Result { + APDU::try_from(frame) + } + + #[test] + fn test_case_type_1() { + let frame: [u8; 4] = [0x00, 0x12, 0x00, 0x80]; + let response = pass_frame(&frame); + assert!(response.is_ok()); + let expected = APDU { + header: ApduHeader { + cla: 0x00, + ins: 0x12, + p1: 0x00, + p2: 0x80, + }, + lc: 0x00, + data: Vec::new(), + le: 0x00, + case_type: 0x01, + }; + assert_eq!(expected, response.unwrap()); + } + + #[test] + fn test_case_type_2_short() { + let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x0f]; + let response = pass_frame(&frame); + assert!(response.is_ok()); + let expected = APDU { + header: ApduHeader { + cla: 0x00, + ins: 0xb0, + p1: 0x00, + p2: 0x00, + }, + lc: 0x00, + data: Vec::new(), + le: 0x0f, + case_type: 0x02, + }; + assert_eq!(expected, response.unwrap()); + } + + #[test] + fn test_case_type_2_short_le() { + let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x00]; + let response = pass_frame(&frame); + assert!(response.is_ok()); + let expected = APDU { + header: ApduHeader { + cla: 0x00, + ins: 0xb0, + p1: 0x00, + p2: 0x00, + }, + lc: 0x00, + data: Vec::new(), + le: 0x100, + case_type: 0x02, + }; + assert_eq!(expected, response.unwrap()); + } + + #[test] + fn test_case_type_3_short() { + let frame: [u8; 7] = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04]; + let payload = [0xe1, 0x04]; + let response = pass_frame(&frame); + assert!(response.is_ok()); + let expected = APDU { + header: ApduHeader { + cla: 0x00, + ins: 0xa4, + p1: 0x00, + p2: 0x0c, + }, + lc: 0x02, + data: payload.to_vec(), + le: 0x00, + case_type: 0x03, + }; + assert_eq!(expected, response.unwrap()); + } + + #[test] + fn test_case_type_4_short() { + let frame: [u8; 13] = [ + 0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0xff, + ]; + let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01]; + let response = pass_frame(&frame); + assert!(response.is_ok()); + let expected = APDU { + header: ApduHeader { + cla: 0x00, + ins: 0xa4, + p1: 0x04, + p2: 0x00, + }, + lc: 0x07, + data: payload.to_vec(), + le: 0xff, + case_type: 0x04, + }; + assert_eq!(expected, response.unwrap()); + } + + #[test] + fn test_case_type_4_short_le() { + let frame: [u8; 13] = [ + 0x00, 0xa4, 0x04, 0x00, 0x07, 0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01, 0x00, + ]; + let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01]; + let response = pass_frame(&frame); + assert!(response.is_ok()); + let expected = APDU { + header: ApduHeader { + cla: 0x00, + ins: 0xa4, + p1: 0x04, + p2: 0x00, + }, + lc: 0x07, + data: payload.to_vec(), + le: 0x100, + case_type: 0x04, + }; + assert_eq!(expected, response.unwrap()); + } + + #[test] + fn test_invalid_apdu_header_length() { + let frame: [u8; 3] = [0x00, 0x12, 0x00]; + let response = pass_frame(&frame); + assert!(response.is_err()); + assert_eq!(Some(ApduStatusCode::SW_WRONG_DATA), response.err()); + } + + #[test] + fn test_unsupported_case_type() { + let frame: [u8; 73] = [ + 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x40, 0xe3, 0x8f, 0xde, 0x51, 0x3d, 0xac, 0x9d, + 0x1c, 0x6e, 0x86, 0x76, 0x31, 0x40, 0x25, 0x96, 0x86, 0x4d, 0x29, 0xe8, 0x07, 0xb3, + 0x56, 0x19, 0xdf, 0x4a, 0x00, 0x02, 0xae, 0x2a, 0x8c, 0x9d, 0x5a, 0xab, 0xc3, 0x4b, + 0x4e, 0xb9, 0x78, 0xb9, 0x11, 0xe5, 0x52, 0x40, 0xf3, 0x45, 0x64, 0x9c, 0xd3, 0xd7, + 0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d, + 0xcb, 0x00, 0x00, + ]; + let response = pass_frame(&frame); + assert!(response.is_err()); + assert_eq!( + Some(ApduStatusCode::SW_COND_USE_NOT_SATISFIED), + response.err() + ); + } +} diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 1a98ce5..2551e7b 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod apdu; pub mod command; #[cfg(feature = "with_ctap1")] mod ctap1; From fce91744c68abe1f41761500b563b4e10c612e80 Mon Sep 17 00:00:00 2001 From: Mirna Date: Fri, 13 Nov 2020 22:06:27 +0200 Subject: [PATCH 005/192] Addressing some of the requested changes --- src/ctap/apdu.rs | 98 +++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index f324a03..431b18e 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -3,6 +3,8 @@ use core::convert::TryFrom; type ByteArray = &'static [u8]; +const APDU_HEADER_LEN: usize = 4; + #[cfg_attr(test, derive(Clone, Debug))] #[allow(non_camel_case_types)] #[derive(PartialEq)] @@ -39,11 +41,11 @@ impl From for ByteArray { } } -#[allow(non_camel_case_types, dead_code)] -pub enum ApduIns { - SELECT = 0xA4, - READ_BINARY = 0xB0, - GET_RESPONSE = 0xC0, +#[allow(dead_code)] +pub enum ApduInstructions { + Select = 0xA4, + ReadBinary = 0xB0, + GetResponse = 0xC0, } #[cfg_attr(test, derive(Clone, Debug))] @@ -67,6 +69,33 @@ impl From<&[u8]> for ApduHeader { } } +#[cfg_attr(test, derive(Clone, Debug))] +#[derive(PartialEq)] +/// The APDU cases +pub enum Case { + Le, + LcData, + LcDataLe, + // TODO: More cases to add as extended length APDUs + // Le could be 2 or 3 Bytes +} + +#[cfg_attr(test, derive(Clone, Debug))] +#[allow(dead_code)] +#[derive(PartialEq)] +pub enum ApduType { + Instruction, + Short(Case), + Extended(Case), + Unknown, +} + +impl Default for ApduType { + fn default() -> ApduType { + ApduType::Unknown + } +} + #[cfg_attr(test, derive(Clone, Debug))] #[allow(dead_code)] #[derive(Default, PartialEq)] @@ -75,11 +104,9 @@ pub struct APDU { lc: u16, data: Vec, le: u32, - case_type: u8, + case_type: ApduType, } -const APDU_HEADER_LEN: u8 = 4; - impl TryFrom<&[u8]> for APDU { type Error = ApduStatusCode; @@ -90,24 +117,23 @@ impl TryFrom<&[u8]> for APDU { // +-----+-----+----+----+ // header | CLA | INS | P1 | P2 | // +-----+-----+----+----+ - let (header, payload) = frame.split_at(APDU_HEADER_LEN as usize); + let (header, payload) = frame.split_at(APDU_HEADER_LEN); let mut apdu = APDU { header: header.into(), lc: 0x00, data: Vec::new(), le: 0x00, - case_type: 0x00, + case_type: ApduType::default(), }; // case 1 if payload.is_empty() { - apdu.case_type = 0x01; + apdu.case_type = ApduType::Instruction; } else { let byte_0 = payload[0]; - // case 2S (Le) if payload.len() == 1 { - apdu.case_type = 0x02; + apdu.case_type = ApduType::Short(Case::Le); apdu.le = if byte_0 == 0x00 { // Ne = 256 0x100 @@ -115,15 +141,13 @@ impl TryFrom<&[u8]> for APDU { byte_0.into() } } - // case 3S (Lc + data) if payload.len() == (1 + byte_0) as usize && byte_0 != 0 { - apdu.case_type = 0x03; + apdu.case_type = ApduType::Short(Case::LcData); apdu.lc = byte_0.into(); apdu.data = payload[1..].to_vec(); } - // case 4S (Lc + data + Le) if payload.len() == (1 + byte_0 + 1) as usize && byte_0 != 0 { - apdu.case_type = 0x04; + apdu.case_type = ApduType::Short(Case::LcDataLe); apdu.lc = byte_0.into(); apdu.data = payload[1..(payload.len() - 1)].to_vec(); apdu.le = (*payload.last().unwrap()).into(); @@ -133,7 +157,7 @@ impl TryFrom<&[u8]> for APDU { } } // TODO: Add extended length cases - if apdu.case_type == 0x00 { + if apdu.case_type == ApduType::default() { return Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED); } Ok(apdu) @@ -163,16 +187,15 @@ mod test { lc: 0x00, data: Vec::new(), le: 0x00, - case_type: 0x01, + case_type: ApduType::Instruction, }; - assert_eq!(expected, response.unwrap()); + assert_eq!(Ok(expected), response); } #[test] fn test_case_type_2_short() { let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x0f]; let response = pass_frame(&frame); - assert!(response.is_ok()); let expected = APDU { header: ApduHeader { cla: 0x00, @@ -183,16 +206,15 @@ mod test { lc: 0x00, data: Vec::new(), le: 0x0f, - case_type: 0x02, + case_type: ApduType::Short(Case::Le), }; - assert_eq!(expected, response.unwrap()); + assert_eq!(Ok(expected), response); } #[test] fn test_case_type_2_short_le() { let frame: [u8; 5] = [0x00, 0xb0, 0x00, 0x00, 0x00]; let response = pass_frame(&frame); - assert!(response.is_ok()); let expected = APDU { header: ApduHeader { cla: 0x00, @@ -203,9 +225,9 @@ mod test { lc: 0x00, data: Vec::new(), le: 0x100, - case_type: 0x02, + case_type: ApduType::Short(Case::Le), }; - assert_eq!(expected, response.unwrap()); + assert_eq!(Ok(expected), response); } #[test] @@ -213,7 +235,6 @@ mod test { let frame: [u8; 7] = [0x00, 0xa4, 0x00, 0x0c, 0x02, 0xe1, 0x04]; let payload = [0xe1, 0x04]; let response = pass_frame(&frame); - assert!(response.is_ok()); let expected = APDU { header: ApduHeader { cla: 0x00, @@ -224,9 +245,9 @@ mod test { lc: 0x02, data: payload.to_vec(), le: 0x00, - case_type: 0x03, + case_type: ApduType::Short(Case::LcData), }; - assert_eq!(expected, response.unwrap()); + assert_eq!(Ok(expected), response); } #[test] @@ -236,7 +257,6 @@ mod test { ]; let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01]; let response = pass_frame(&frame); - assert!(response.is_ok()); let expected = APDU { header: ApduHeader { cla: 0x00, @@ -247,9 +267,9 @@ mod test { lc: 0x07, data: payload.to_vec(), le: 0xff, - case_type: 0x04, + case_type: ApduType::Short(Case::LcDataLe), }; - assert_eq!(expected, response.unwrap()); + assert_eq!(Ok(expected), response); } #[test] @@ -259,7 +279,6 @@ mod test { ]; let payload = [0xd2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01]; let response = pass_frame(&frame); - assert!(response.is_ok()); let expected = APDU { header: ApduHeader { cla: 0x00, @@ -270,17 +289,16 @@ mod test { lc: 0x07, data: payload.to_vec(), le: 0x100, - case_type: 0x04, + case_type: ApduType::Short(Case::LcDataLe), }; - assert_eq!(expected, response.unwrap()); + assert_eq!(Ok(expected), response); } #[test] fn test_invalid_apdu_header_length() { let frame: [u8; 3] = [0x00, 0x12, 0x00]; let response = pass_frame(&frame); - assert!(response.is_err()); - assert_eq!(Some(ApduStatusCode::SW_WRONG_DATA), response.err()); + assert_eq!(Err(ApduStatusCode::SW_WRONG_DATA), response); } #[test] @@ -294,10 +312,6 @@ mod test { 0xcb, 0x00, 0x00, ]; let response = pass_frame(&frame); - assert!(response.is_err()); - assert_eq!( - Some(ApduStatusCode::SW_COND_USE_NOT_SATISFIED), - response.err() - ); + assert_eq!(Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED), response); } } From e842da0de7abe8c4304986e6dfa0dbbf68983a15 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 19 Nov 2020 11:27:50 +0100 Subject: [PATCH 006/192] Add store fuzzing --- libraries/persistent_store/fuzz/Cargo.toml | 2 + .../fuzz/fuzz_targets/store.rs | 2 +- libraries/persistent_store/fuzz/src/lib.rs | 5 +- libraries/persistent_store/fuzz/src/store.rs | 533 ++++++++++++++++++ 4 files changed, 538 insertions(+), 4 deletions(-) create mode 100644 libraries/persistent_store/fuzz/src/store.rs diff --git a/libraries/persistent_store/fuzz/Cargo.toml b/libraries/persistent_store/fuzz/Cargo.toml index fdc4f89..f0b01f1 100644 --- a/libraries/persistent_store/fuzz/Cargo.toml +++ b/libraries/persistent_store/fuzz/Cargo.toml @@ -11,6 +11,8 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.3" persistent_store = { path = "..", features = ["std"] } +rand_core = "0.5" +rand_pcg = "0.2" strum = { version = "0.19", features = ["derive"] } # Prevent this from interfering with workspaces diff --git a/libraries/persistent_store/fuzz/fuzz_targets/store.rs b/libraries/persistent_store/fuzz/fuzz_targets/store.rs index 1cff2a4..d96874d 100644 --- a/libraries/persistent_store/fuzz/fuzz_targets/store.rs +++ b/libraries/persistent_store/fuzz/fuzz_targets/store.rs @@ -17,5 +17,5 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { - // TODO(ia0): Call fuzzing when implemented. + fuzz_store::fuzz(data, false, None); }); diff --git a/libraries/persistent_store/fuzz/src/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs index 11645f3..3d32624 100644 --- a/libraries/persistent_store/fuzz/src/lib.rs +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -25,13 +25,12 @@ //! situation where coverage takes precedence over surjectivity is for the value of insert updates //! where a pseudo-random generator is used to avoid wasting entropy. -// TODO(ia0): Remove when used. -#![allow(dead_code)] - mod histogram; mod stats; +mod store; pub use stats::{StatKey, Stats}; +pub use store::fuzz; /// Bit-level entropy source based on a byte slice shared reference. /// diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs new file mode 100644 index 0000000..e51ed71 --- /dev/null +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -0,0 +1,533 @@ +// Copyright 2019-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. +// 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 crate::stats::{StatKey, Stats}; +use crate::Entropy; +use persistent_store::{ + BufferOptions, BufferStorage, Store, StoreDriver, StoreDriverOff, StoreDriverOn, + StoreInterruption, StoreInvariant, StoreOperation, StoreUpdate, +}; +use rand_core::{RngCore, SeedableRng}; +use rand_pcg::Pcg32; +use std::collections::HashMap; +use std::convert::TryInto; + +// NOTE: We should be able to improve coverage by only checking the last operation. Because +// operations before the last could be checked with a shorter entropy. + +/// Checks the store against a sequence of manipulations. +/// +/// The entropy to generate the sequence of manipulation should be provided in `data`. Debugging +/// information is printed if `debug` is set. Statistics are gathered if `stats` is set. +pub fn fuzz(data: &[u8], debug: bool, stats: Option<&mut Stats>) { + let mut fuzzer = Fuzzer::new(data, debug, stats); + fuzzer.init_counters(); + fuzzer.record(StatKey::Entropy, data.len()); + let mut driver = fuzzer.init(); + let store = loop { + if fuzzer.debug { + print!("{}", driver.storage()); + } + if let StoreDriver::On(driver) = &driver { + if !fuzzer.init.is_dirty() { + driver.check().unwrap(); + } + if fuzzer.debug { + println!("----------------------------------------------------------------------"); + } + } + if fuzzer.entropy.is_empty() { + if fuzzer.debug { + println!("No more entropy."); + } + if fuzzer.init.is_dirty() { + return; + } + fuzzer.record(StatKey::FinishedLifetime, 0); + break driver.power_on().unwrap().extract_store(); + } + driver = match driver { + StoreDriver::On(driver) => match fuzzer.apply(driver) { + Ok(x) => x, + Err(store) => { + if fuzzer.debug { + println!("No more lifetime."); + } + if fuzzer.init.is_dirty() { + return; + } + fuzzer.record(StatKey::FinishedLifetime, 1); + break store; + } + }, + StoreDriver::Off(driver) => fuzzer.power_on(driver), + } + }; + let virt_window = (store.format().num_pages() * store.format().virt_page_size()) as usize; + let init_lifetime = fuzzer.init.used_cycles() * virt_window; + let lifetime = store.lifetime().unwrap().used() - init_lifetime; + fuzzer.record(StatKey::UsedLifetime, lifetime); + fuzzer.record(StatKey::NumCompactions, lifetime / virt_window); + fuzzer.record_counters(); +} + +/// Fuzzing state. +struct Fuzzer<'a> { + /// Remaining fuzzing entropy. + entropy: Entropy<'a>, + + /// Unlimited pseudo entropy. + /// + /// This source is only used to generate the values of entries. This is a compromise to avoid + /// consuming fuzzing entropy for low additional coverage. + values: Pcg32, + + /// The fuzzing mode. + init: Init, + + /// Whether debugging is enabled. + debug: bool, + + /// Whether statistics should be gathered. + stats: Option<&'a mut Stats>, + + /// Statistics counters (only used when gathering statistics). + /// + /// The counters are written to the statistics at the end of the fuzzing run, when their value + /// is final. + counters: HashMap, +} + +impl<'a> Fuzzer<'a> { + /// Creates an initial fuzzing state. + fn new(data: &'a [u8], debug: bool, stats: Option<&'a mut Stats>) -> Fuzzer<'a> { + let mut entropy = Entropy::new(data); + let seed = entropy.read_slice(16); + let values = Pcg32::from_seed(seed[..].try_into().unwrap()); + Fuzzer { + entropy, + values, + init: Init::Clean, + debug, + stats, + counters: HashMap::new(), + } + } + + /// Initializes the fuzzing state and returns the store driver. + fn init(&mut self) -> StoreDriver { + let mut options = BufferOptions { + word_size: 4, + page_size: 1 << self.entropy.read_range(5, 12), + max_word_writes: 2, + max_page_erases: self.entropy.read_range(0, 50000), + strict_write: true, + }; + let num_pages = self.entropy.read_range(3, 64); + self.record(StatKey::PageSize, options.page_size); + self.record(StatKey::MaxPageErases, options.max_page_erases); + self.record(StatKey::NumPages, num_pages); + if self.debug { + println!("page_size: {}", options.page_size); + println!("num_pages: {}", num_pages); + println!("max_cycle: {}", options.max_page_erases); + } + let storage_size = num_pages * options.page_size; + if self.entropy.read_bit() { + self.init = Init::Dirty; + let mut storage = vec![0xff; storage_size].into_boxed_slice(); + let length = self.entropy.read_range(0, storage_size); + self.record(StatKey::DirtyLength, length); + for byte in &mut storage[0..length] { + *byte = self.entropy.read_byte(); + } + if self.debug { + println!("Start with dirty storage."); + } + options.strict_write = false; + let storage = BufferStorage::new(storage, options); + StoreDriver::Off(StoreDriverOff::new_dirty(storage)) + } else if self.entropy.read_bit() { + let cycle = self.entropy.read_range(0, options.max_page_erases); + self.init = Init::Used { cycle }; + if self.debug { + println!("Start with {} consumed erase cycles.", cycle); + } + self.record(StatKey::InitCycles, cycle); + let storage = vec![0xff; storage_size].into_boxed_slice(); + let mut storage = BufferStorage::new(storage, options); + Store::init_with_cycle(&mut storage, cycle); + StoreDriver::Off(StoreDriverOff::new_dirty(storage)) + } else { + StoreDriver::Off(StoreDriverOff::new(options, num_pages)) + } + } + + /// Powers a driver with possible interruption. + fn power_on(&mut self, driver: StoreDriverOff) -> StoreDriver { + if self.debug { + println!("Power on the store."); + } + self.increment(StatKey::PowerOnCount); + let interruption = self.interruption(driver.delay_map()); + match driver.partial_power_on(interruption) { + Err((storage, _)) if self.init.is_dirty() => { + self.entropy.consume_all(); + StoreDriver::Off(StoreDriverOff::new_dirty(storage)) + } + Err(error) => self.crash(error), + Ok(driver) => driver, + } + } + + /// Generates and applies an operation with possible interruption. + fn apply(&mut self, driver: StoreDriverOn) -> Result> { + let operation = self.operation(&driver); + if self.debug { + println!("{:?}", operation); + } + let interruption = self.interruption(driver.delay_map(&operation)); + match driver.partial_apply(operation, interruption) { + Err((store, _)) if self.init.is_dirty() => { + self.entropy.consume_all(); + Err(store) + } + Err((store, StoreInvariant::NoLifetime)) => Err(store), + Err((store, error)) => self.crash((store.extract_storage(), error)), + Ok((error, driver)) => { + if self.debug { + if let Some(error) = error { + println!("{:?}", error); + } + } + Ok(driver) + } + } + } + + /// Reports a broken invariant and terminates fuzzing. + fn crash(&self, error: (BufferStorage, StoreInvariant)) -> ! { + let (storage, invariant) = error; + if self.debug { + print!("{}", storage); + } + panic!("{:?}", invariant); + } + + /// Records a statistics if enabled. + fn record(&mut self, key: StatKey, value: usize) { + if let Some(stats) = &mut self.stats { + stats.add(key, value); + } + } + + /// Increments a counter if statistics are enabled. + fn increment(&mut self, key: StatKey) { + if self.stats.is_some() { + *self.counters.get_mut(&key).unwrap() += 1; + } + } + + /// Initializes all counters if statistics are enabled. + fn init_counters(&mut self) { + if self.stats.is_some() { + use StatKey::*; + self.counters.insert(PowerOnCount, 0); + self.counters.insert(TransactionCount, 0); + self.counters.insert(ClearCount, 0); + self.counters.insert(PrepareCount, 0); + self.counters.insert(InsertCount, 0); + self.counters.insert(RemoveCount, 0); + self.counters.insert(InterruptionCount, 0); + } + } + + /// Records all counters if statistics are enabled. + fn record_counters(&mut self) { + if let Some(stats) = &mut self.stats { + for (&key, &value) in self.counters.iter() { + stats.add(key, value); + } + } + } + + /// Generates a possibly invalid operation. + fn operation(&mut self, driver: &StoreDriverOn) -> StoreOperation { + let format = driver.model().format(); + match self.entropy.read_range(0, 2) { + 0 => { + // We also generate an invalid count (one past the maximum value) to test the error + // scenario. Since the test for the error scenario is monotonic, this is a good + // compromise to keep entropy bounded. + let count = self + .entropy + .read_range(0, format.max_updates() as usize + 1); + let mut updates = Vec::with_capacity(count); + for _ in 0..count { + updates.push(self.update()); + } + self.increment(StatKey::TransactionCount); + StoreOperation::Transaction { updates } + } + 1 => { + let min_key = self.key(); + self.increment(StatKey::ClearCount); + StoreOperation::Clear { min_key } + } + 2 => { + // We also generate an invalid length (one past the total capacity) to test the + // error scenario. See the explanation for transactions above for why it's enough. + let length = self + .entropy + .read_range(0, format.total_capacity() as usize + 1); + self.increment(StatKey::PrepareCount); + StoreOperation::Prepare { length } + } + _ => unreachable!(), + } + } + + /// Generates a possibly invalid update. + fn update(&mut self) -> StoreUpdate { + match self.entropy.read_range(0, 1) { + 0 => { + let key = self.key(); + let value = self.value(); + self.increment(StatKey::InsertCount); + StoreUpdate::Insert { key, value } + } + 1 => { + let key = self.key(); + self.increment(StatKey::RemoveCount); + StoreUpdate::Remove { key } + } + _ => unreachable!(), + } + } + + /// Generates a possibly invalid key. + fn key(&mut self) -> usize { + // Use 4096 as the canonical invalid key. + self.entropy.read_range(0, 4096) + } + + /// Generates a possibly invalid value. + fn value(&mut self) -> Vec { + // Use 1024 as the canonical invalid length. + let length = self.entropy.read_range(0, 1024); + let mut value = vec![0; length]; + self.values.fill_bytes(&mut value); + value + } + + /// Generates an interruption. + /// + /// The `delay_map` describes the number of modified bits by the upcoming sequence of store + /// operations. + // TODO(ia0): We use too much CPU to compute the delay map. We should be able to just count the + // number of storage operations by checking the remaining delay. We can then use the entropy + // directly from the corruption function because it's called at most once. + fn interruption( + &mut self, + delay_map: Result, (usize, BufferStorage)>, + ) -> StoreInterruption { + if self.init.is_dirty() { + // We only test that the store can power on without crashing. If it would get + // interrupted then it's like powering up with a different initial state, which would be + // tested with another fuzzing input. + return StoreInterruption::none(); + } + let delay_map = match delay_map { + Ok(x) => x, + Err((delay, storage)) => { + print!("{}", storage); + panic!("delay={}", delay); + } + }; + let delay = self.entropy.read_range(0, delay_map.len() - 1); + let mut complete_bits = BitStack::default(); + for _ in 0..delay_map[delay] { + complete_bits.push(self.entropy.read_bit()); + } + if self.debug { + if delay == delay_map.len() - 1 { + assert!(complete_bits.is_empty()); + println!("Do not interrupt."); + } else { + println!( + "Interrupt after {} operations with complete mask {}.", + delay, complete_bits + ); + } + } + if delay < delay_map.len() - 1 { + self.increment(StatKey::InterruptionCount); + } + let corrupt = Box::new(move |old: &mut [u8], new: &[u8]| { + for (old, new) in old.iter_mut().zip(new.iter()) { + for bit in 0..8 { + let mask = 1 << bit; + if *old & mask == *new & mask { + continue; + } + if complete_bits.pop().unwrap() { + *old ^= mask; + } + } + } + }); + StoreInterruption { delay, corrupt } + } +} + +/// The initial fuzzing mode. +enum Init { + /// Fuzzing starts from a clean storage. + /// + /// All invariant are checked. + Clean, + + /// Fuzzing starts from a dirty storage. + /// + /// Only crashing is checked. + Dirty, + + /// Fuzzing starts from a simulated old storage. + /// + /// All invariant are checked. + Used { + /// Number of simulated used cycles. + cycle: usize, + }, +} + +impl Init { + /// Returns whether fuzzing is in dirty mode. + fn is_dirty(&self) -> bool { + match self { + Init::Dirty => true, + _ => false, + } + } + + /// Returns the number of used cycles. + /// + /// This is zero if the storage was not artificially aged. + fn used_cycles(&self) -> usize { + match self { + Init::Used { cycle } => *cycle, + _ => 0, + } + } +} + +/// Compact stack of bits. +// NOTE: This would probably go away once the delay map is simplified. +#[derive(Default, Clone, Debug)] +struct BitStack { + /// Bits stored in little-endian (for bytes and bits). + data: Vec, + + /// Number of bits stored. + len: usize, +} + +impl BitStack { + /// Returns whether the stack is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the length of the stack. + fn len(&self) -> usize { + if self.len == 0 { + 8 * self.data.len() + } else { + 8 * (self.data.len() - 1) + self.len + } + } + + /// Pushes a bit to the stack. + fn push(&mut self, value: bool) { + if self.len == 0 { + self.data.push(0); + } + if value { + *self.data.last_mut().unwrap() |= 1 << self.len; + } + self.len += 1; + if self.len == 8 { + self.len = 0; + } + } + + /// Pops a bit from the stack. + fn pop(&mut self) -> Option { + if self.len == 0 { + if self.data.is_empty() { + return None; + } + self.len = 8; + } + self.len -= 1; + let result = self.data.last().unwrap() & 1 << self.len; + if self.len == 0 { + self.data.pop().unwrap(); + } + Some(result != 0) + } +} + +impl std::fmt::Display for BitStack { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + let mut bits = self.clone(); + while let Some(bit) = bits.pop() { + write!(f, "{}", bit as usize)?; + } + write!(f, " ({} bits)", self.len())?; + Ok(()) + } +} + +#[test] +fn bit_stack_ok() { + let mut bits = BitStack::default(); + + assert_eq!(bits.pop(), None); + + bits.push(true); + assert_eq!(bits.pop(), Some(true)); + assert_eq!(bits.pop(), None); + + bits.push(false); + assert_eq!(bits.pop(), Some(false)); + assert_eq!(bits.pop(), None); + + bits.push(true); + bits.push(false); + assert_eq!(bits.pop(), Some(false)); + assert_eq!(bits.pop(), Some(true)); + assert_eq!(bits.pop(), None); + + bits.push(false); + bits.push(true); + assert_eq!(bits.pop(), Some(true)); + assert_eq!(bits.pop(), Some(false)); + assert_eq!(bits.pop(), None); + + for i in 0..27 { + assert_eq!(bits.len(), i); + bits.push(true); + } +} From 5bf73cb8fdcd4bcb33381992c8608fd77df7dce6 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 20 Nov 2020 03:26:26 +0100 Subject: [PATCH 007/192] fail on UP=true in make --- src/ctap/data_formats.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index dacafe5..45ebf9f 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -361,10 +361,8 @@ impl TryFrom for MakeCredentialOptions { Some(options_entry) => extract_bool(options_entry)?, None => false, }; - if let Some(options_entry) = up { - if !extract_bool(options_entry)? { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); - } + if up.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } let uv = match uv { Some(options_entry) => extract_bool(options_entry)?, From 9bb1aad45d526b29bb5f6477c56b5136a880cff2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 23 Nov 2020 12:02:51 +0100 Subject: [PATCH 008/192] wraps HMAC secret into credentials --- src/ctap/ctap1.rs | 50 ++++++---- src/ctap/mod.rs | 229 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 236 insertions(+), 43 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 84c6fb0..a5a7921 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -288,7 +288,7 @@ impl Ctap1Command { let sk = crypto::ecdsa::SecKey::gensk(ctap_state.rng); let pk = sk.genpk(); let key_handle = ctap_state - .encrypt_key_handle(sk, &application) + .encrypt_key_handle(sk, &application, None) .map_err(|_| Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. @@ -373,7 +373,7 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::{ENCRYPTED_CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER}; + use super::super::{CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; use super::*; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -413,12 +413,12 @@ mod test { 0x00, 0x00, 0x00, - 65 + ENCRYPTED_CREDENTIAL_ID_SIZE as u8, + 65 + CREDENTIAL_ID_BASE_SIZE as u8, ]; let challenge = [0x0C; 32]; message.extend(&challenge); message.extend(application); - message.push(ENCRYPTED_CREDENTIAL_ID_SIZE as u8); + message.push(CREDENTIAL_ID_BASE_SIZE as u8); message.extend(key_handle); message } @@ -437,15 +437,15 @@ mod test { Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); assert_eq!(response[0], Ctap1Command::LEGACY_BYTE); - assert_eq!(response[66], ENCRYPTED_CREDENTIAL_ID_SIZE as u8); + assert_eq!(response[66], CREDENTIAL_ID_BASE_SIZE as u8); assert!(ctap_state .decrypt_credential_source( - response[67..67 + ENCRYPTED_CREDENTIAL_ID_SIZE].to_vec(), + response[67..67 + CREDENTIAL_ID_BASE_SIZE].to_vec(), &application ) .unwrap() .is_some()); - const CERT_START: usize = 67 + ENCRYPTED_CREDENTIAL_ID_SIZE; + const CERT_START: usize = 67 + CREDENTIAL_ID_BASE_SIZE; assert_eq!( &response[CERT_START..CERT_START + ATTESTATION_CERTIFICATE.len()], &ATTESTATION_CERTIFICATE[..] @@ -494,7 +494,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); @@ -510,7 +512,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let application = [0x55; 32]; let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -527,7 +531,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -551,7 +557,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[0] = 0xEE; @@ -569,7 +577,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[1] = 0xEE; @@ -587,7 +597,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[2] = 0xEE; @@ -605,7 +617,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -630,7 +644,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, @@ -650,7 +666,7 @@ mod test { #[test] fn test_process_authenticate_bad_key_handle() { let application = [0x0A; 32]; - let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -667,7 +683,7 @@ mod test { #[test] fn test_process_authenticate_without_up() { let application = [0x0A; 32]; - let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 66ef234..2e095ed 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -83,8 +83,9 @@ const USE_SIGNATURE_COUNTER: bool = true; // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, // - 32 byte relying party ID hashed with SHA256, +// - (optional) 32 byte for HMAC-secret, // - 32 byte HMAC-SHA256 over everything else. -pub const ENCRYPTED_CREDENTIAL_ID_SIZE: usize = 112; +pub const CREDENTIAL_ID_BASE_SIZE: usize = 112; // Set this bit when checking user presence. const UP_FLAG: u8 = 0x01; // Set this bit when checking user verification. @@ -195,6 +196,7 @@ where &mut self, private_key: crypto::ecdsa::SecKey, application: &[u8; 32], + cred_random: Option<&[u8; 32]>, ) -> Result, Ctap2StatusCode> { let master_keys = self.persistent_store.master_keys()?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); @@ -203,14 +205,19 @@ where let mut iv = [0; 16]; iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); - let mut blocks = [[0u8; 16]; 4]; + let block_len = if cred_random.is_some() { 6 } else { 4 }; + let mut blocks = vec![[0u8; 16]; block_len]; blocks[0].copy_from_slice(&sk_bytes[..16]); blocks[1].copy_from_slice(&sk_bytes[16..]); blocks[2].copy_from_slice(&application[..16]); blocks[3].copy_from_slice(&application[16..]); + if let Some(cred_random) = cred_random { + blocks[4].copy_from_slice(&cred_random[..16]); + blocks[5].copy_from_slice(&cred_random[16..]); + } cbc_encrypt(&aes_enc_key, iv, &mut blocks); - let mut encrypted_id = Vec::with_capacity(ENCRYPTED_CREDENTIAL_ID_SIZE); + let mut encrypted_id = Vec::with_capacity(16 * (block_len + 3)); encrypted_id.extend(&iv); for b in &blocks { encrypted_id.extend(b); @@ -228,11 +235,15 @@ where credential_id: Vec, rp_id_hash: &[u8], ) -> Result, Ctap2StatusCode> { - if credential_id.len() != ENCRYPTED_CREDENTIAL_ID_SIZE { + let has_cred_random = if credential_id.len() == CREDENTIAL_ID_BASE_SIZE { + false + } else if credential_id.len() == CREDENTIAL_ID_BASE_SIZE + 32 { + true + } else { return Ok(None); - } + }; let master_keys = self.persistent_store.master_keys()?; - let payload_size = ENCRYPTED_CREDENTIAL_ID_SIZE - 32; + let payload_size = credential_id.len() - 32; if !verify_hmac_256::( &master_keys.hmac, &credential_id[..payload_size], @@ -244,8 +255,9 @@ where let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let mut iv = [0; 16]; iv.copy_from_slice(&credential_id[..16]); - let mut blocks = [[0u8; 16]; 4]; - for i in 0..4 { + let block_len = if has_cred_random { 6 } else { 4 }; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); } @@ -256,6 +268,14 @@ where decrypted_sk[16..].clone_from_slice(&blocks[1]); decrypted_rp_id_hash[..16].clone_from_slice(&blocks[2]); decrypted_rp_id_hash[16..].clone_from_slice(&blocks[3]); + let cred_random = if has_cred_random { + let mut decrypted_cred_random = [0; 32]; + decrypted_cred_random[..16].clone_from_slice(&blocks[4]); + decrypted_cred_random[16..].clone_from_slice(&blocks[5]); + Some(decrypted_cred_random.to_vec()) + } else { + None + }; if rp_id_hash != decrypted_rp_id_hash { return Ok(None); @@ -269,7 +289,7 @@ where rp_id: String::from(""), user_handle: vec![], other_ui: None, - cred_random: None, + cred_random, cred_protect_policy: None, })) } @@ -380,11 +400,7 @@ where }; let cred_random = if use_hmac_extension { - if !options.rk { - // The extension is actually supported, but we need resident keys. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - Some(self.rng.gen_uniform_u8x32().to_vec()) + Some(self.rng.gen_uniform_u8x32()) } else { None }; @@ -463,13 +479,13 @@ where other_ui: user .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), - cred_random, + cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, }; self.persistent_store.store_credential(credential_source)?; random_id } else { - self.encrypt_key_handle(sk.clone(), &rp_id_hash)? + self.encrypt_key_handle(sk.clone(), &rp_id_hash, cred_random.as_ref())? }; let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; @@ -728,10 +744,9 @@ where ]), #[cfg(feature = "with_ctap2_1")] max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // You can use ENCRYPTED_CREDENTIAL_ID_SIZE here, but if your - // browser passes that value, it might be used to fingerprint. + // #TODO(106) update with version 2.1 of HMAC-secret #[cfg(feature = "with_ctap2_1")] - max_credential_id_length: None, + max_credential_id_length: Some(CREDENTIAL_ID_BASE_SIZE as u64 + 32), #[cfg(feature = "with_ctap2_1")] transports: Some(vec![AuthenticatorTransport::Usb]), #[cfg(feature = "with_ctap2_1")] @@ -829,7 +844,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, 0xA9, 0x01]; + let mut expected_response = vec![0x00, 0xAA, 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. @@ -864,9 +879,9 @@ mod test { #[cfg(feature = "with_ctap2_1")] expected_response.extend( [ - 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, 0x0D, 0x04, + 0x08, 0x18, 0x90, 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, 0x0D, 0x04, ] .iter(), ); @@ -993,7 +1008,7 @@ mod test { 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, ENCRYPTED_CREDENTIAL_ID_SIZE as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_BASE_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1114,6 +1129,57 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let extensions = Some(MakeCredentialExtensions { + hmac_secret: true, + cred_protect: None, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let AuthenticatorMakeCredentialResponse { + fmt, + auth_data, + att_stmt, + } = make_credential_response; + // The expected response is split to only assert the non-random parts. + assert_eq!(fmt, "packed"); + let mut expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, + ]; + expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); + let credential_size = CREDENTIAL_ID_BASE_SIZE + 32; + expected_auth_data.extend(&[0x00, credential_size as u8]); + assert_eq!( + auth_data[0..expected_auth_data.len()], + expected_auth_data[..] + ); + let expected_extension_cbor = vec![ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, + 0xF5, + ]; + assert_eq!( + auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], + expected_extension_cbor[..] + ); + assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + } + _ => panic!("Invalid response type"), + } + } + + #[test] + fn test_process_make_credential_hmac_secret_resident_key() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, @@ -1220,6 +1286,71 @@ mod test { } } + #[test] + fn test_process_get_assertion_hmac_secret() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + let make_extensions = Some(MakeCredentialExtensions { + hmac_secret: true, + cred_protect: None, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + make_credential_params.extensions = make_extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert!(make_credential_response.is_ok()); + let credential_id = match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let auth_data = make_credential_response.auth_data; + let offset = 37 + ctap_state.persistent_store.aaguid().unwrap().len(); + let credential_size = CREDENTIAL_ID_BASE_SIZE + 32; + assert_eq!(auth_data[offset], 0x00); + assert_eq!(auth_data[offset + 1] as usize, credential_size); + auth_data[offset + 2..offset + 2 + credential_size].to_vec() + } + _ => panic!("Invalid response type"), + }; + + let pk = sk.genpk(); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: CoseKey::from(pk), + salt_enc: vec![0x02; 32], + salt_auth: vec![0x03; 16], + }; + let get_extensions = Some(GetAssertionExtensions { + hmac_secret: Some(hmac_secret_input), + }); + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential_id, + transports: None, + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: Some(vec![cred_desc]), + extensions: get_extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = + ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + ); + } + #[test] fn test_residential_process_get_assertion_hmac_secret() { let mut rng = ThreadRng256 {}; @@ -1435,7 +1566,7 @@ mod test { // We are not testing the correctness of our SHA256 here, only if it is checked. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key.clone(), &rp_id_hash) + .encrypt_key_handle(private_key.clone(), &rp_id_hash, None) .unwrap(); let decrypted_source = ctap_state .decrypt_credential_source(encrypted_id, &rp_id_hash) @@ -1445,6 +1576,29 @@ mod test { assert_eq!(private_key, decrypted_source.private_key); } + #[test] + fn test_encrypt_decrypt_credential_with_cred_random() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + // Usually, the relying party ID or its hash is provided by the client. + // We are not testing the correctness of our SHA256 here, only if it is checked. + let rp_id_hash = [0x55; 32]; + let cred_random = [0xC9; 32]; + let encrypted_id = ctap_state + .encrypt_key_handle(private_key.clone(), &rp_id_hash, Some(&cred_random)) + .unwrap(); + let decrypted_source = ctap_state + .decrypt_credential_source(encrypted_id, &rp_id_hash) + .unwrap() + .unwrap(); + + assert_eq!(private_key, decrypted_source.private_key); + assert_eq!(Some(cred_random.to_vec()), decrypted_source.cred_random); + } + #[test] fn test_encrypt_decrypt_bad_hmac() { let mut rng = ThreadRng256 {}; @@ -1455,7 +1609,30 @@ mod test { // Same as above. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key, &rp_id_hash) + .encrypt_key_handle(private_key, &rp_id_hash, None) + .unwrap(); + for i in 0..encrypted_id.len() { + let mut modified_id = encrypted_id.clone(); + modified_id[i] ^= 0x01; + assert!(ctap_state + .decrypt_credential_source(modified_id, &rp_id_hash) + .unwrap() + .is_none()); + } + } + + #[test] + fn test_encrypt_decrypt_bad_hmac_with_cred_random() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + // Same as above. + let rp_id_hash = [0x55; 32]; + let cred_random = [0xC9; 32]; + let encrypted_id = ctap_state + .encrypt_key_handle(private_key, &rp_id_hash, Some(&cred_random)) .unwrap(); for i in 0..encrypted_id.len() { let mut modified_id = encrypted_id.clone(); From a099ddbabde0c208b821c5417d5ed16e86560bd9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 23 Nov 2020 14:34:38 +0100 Subject: [PATCH 009/192] introduce max credential size for readability --- src/ctap/mod.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 2e095ed..dd9cabe 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -86,6 +86,7 @@ const USE_SIGNATURE_COUNTER: bool = true; // - (optional) 32 byte for HMAC-secret, // - 32 byte HMAC-SHA256 over everything else. pub const CREDENTIAL_ID_BASE_SIZE: usize = 112; +pub const CREDENTIAL_ID_MAX_SIZE: usize = CREDENTIAL_ID_BASE_SIZE + 32; // Set this bit when checking user presence. const UP_FLAG: u8 = 0x01; // Set this bit when checking user verification. @@ -235,12 +236,10 @@ where credential_id: Vec, rp_id_hash: &[u8], ) -> Result, Ctap2StatusCode> { - let has_cred_random = if credential_id.len() == CREDENTIAL_ID_BASE_SIZE { - false - } else if credential_id.len() == CREDENTIAL_ID_BASE_SIZE + 32 { - true - } else { - return Ok(None); + let has_cred_random = match credential_id.len() { + CREDENTIAL_ID_BASE_SIZE => false, + CREDENTIAL_ID_MAX_SIZE => true, + _ => return Ok(None), }; let master_keys = self.persistent_store.master_keys()?; let payload_size = credential_id.len() - 32; @@ -1154,8 +1153,7 @@ mod test { 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - let credential_size = CREDENTIAL_ID_BASE_SIZE + 32; - expected_auth_data.extend(&[0x00, credential_size as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_MAX_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1307,10 +1305,9 @@ mod test { ResponseData::AuthenticatorMakeCredential(make_credential_response) => { let auth_data = make_credential_response.auth_data; let offset = 37 + ctap_state.persistent_store.aaguid().unwrap().len(); - let credential_size = CREDENTIAL_ID_BASE_SIZE + 32; assert_eq!(auth_data[offset], 0x00); - assert_eq!(auth_data[offset + 1] as usize, credential_size); - auth_data[offset + 2..offset + 2 + credential_size].to_vec() + assert_eq!(auth_data[offset + 1] as usize, CREDENTIAL_ID_MAX_SIZE); + auth_data[offset + 2..offset + 2 + CREDENTIAL_ID_MAX_SIZE].to_vec() } _ => panic!("Invalid response type"), }; From 174c292f2f700cbfc85dec8da6c0412180e50fff Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Mon, 23 Nov 2020 19:16:48 +0100 Subject: [PATCH 010/192] Adding metadata file used for certification. --- metadata/metadata.json | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 metadata/metadata.json diff --git a/metadata/metadata.json b/metadata/metadata.json new file mode 100644 index 0000000..bb81d0c --- /dev/null +++ b/metadata/metadata.json @@ -0,0 +1,47 @@ +{ + "assertionScheme" : "FIDOV2", + "keyProtection" : 1, + "attestationRootCertificates" : [ + ], + "aaguid" : "664d9f67-84a2-412a-9ff7-b4f7d8ee6d05", + "publicKeyAlgAndEncoding" : 260, + "protocolFamily" : "fido2", + "upv" : [ + { + "major" : 1, + "minor" : 0 + } + ], + "icon" : "data:image/png;base64,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", + "matcherProtection" : 1, + "supportedExtensions" : [ + { + "id" : "hmac-secret", + "fail_if_unknown" : false + }, + { + "id" : "credProtect", + "fail_if_unknown" : false + } + ], + "cryptoStrength" : 128, + "description" : "OpenSK authenticator", + "authenticatorVersion" : 1, + "isSecondFactorOnly" : false, + "userVerificationDetails" : [ + [ + { + "userVerification" : 1 + }, + { + "userVerification" : 4 + } + ] + ], + "attachmentHint" : 6, + "attestationTypes" : [ + 15880 + ], + "authenticationAlgorithm" : 1, + "tcDisplay" : 0 +} From 90f2d4a24955544fe426eb79a201f18b20253aa3 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Mon, 23 Nov 2020 20:33:01 +0100 Subject: [PATCH 011/192] Fix indentation --- metadata/metadata.json | 55 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/metadata/metadata.json b/metadata/metadata.json index bb81d0c..eedeed9 100644 --- a/metadata/metadata.json +++ b/metadata/metadata.json @@ -1,47 +1,46 @@ { - "assertionScheme" : "FIDOV2", - "keyProtection" : 1, - "attestationRootCertificates" : [ - ], - "aaguid" : "664d9f67-84a2-412a-9ff7-b4f7d8ee6d05", - "publicKeyAlgAndEncoding" : 260, - "protocolFamily" : "fido2", - "upv" : [ + "assertionScheme": "FIDOV2", + "keyProtection": 1, + "attestationRootCertificates": [], + "aaguid": "664d9f67-84a2-412a-9ff7-b4f7d8ee6d05", + "publicKeyAlgAndEncoding": 260, + "protocolFamily": "fido2", + "upv": [ { - "major" : 1, - "minor" : 0 + "major": 1, + "minor": 0 } ], - "icon" : "data:image/png;base64,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", - "matcherProtection" : 1, - "supportedExtensions" : [ + "icon": "data:image/png;base64,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", + "matcherProtection": 1, + "supportedExtensions": [ { - "id" : "hmac-secret", - "fail_if_unknown" : false + "id": "hmac-secret", + "fail_if_unknown": false }, { - "id" : "credProtect", - "fail_if_unknown" : false + "id": "credProtect", + "fail_if_unknown": false } ], - "cryptoStrength" : 128, - "description" : "OpenSK authenticator", - "authenticatorVersion" : 1, - "isSecondFactorOnly" : false, - "userVerificationDetails" : [ + "cryptoStrength": 128, + "description": "OpenSK authenticator", + "authenticatorVersion": 1, + "isSecondFactorOnly": false, + "userVerificationDetails": [ [ { - "userVerification" : 1 + "userVerification": 1 }, { - "userVerification" : 4 + "userVerification": 4 } ] ], - "attachmentHint" : 6, - "attestationTypes" : [ + "attachmentHint": 6, + "attestationTypes": [ 15880 ], - "authenticationAlgorithm" : 1, - "tcDisplay" : 0 + "authenticationAlgorithm": 1, + "tcDisplay": 0 } From 29ee45de6cf809e1a78250476d914a5e34308163 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 24 Nov 2020 11:29:14 +0100 Subject: [PATCH 012/192] Do not crash in the driver for store errors We prefer to return those errors to the fuzzer which can then decide whether they are expected or not (e.g. when starting from a dirty storage, the store is expected to have errors). --- libraries/persistent_store/src/driver.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/libraries/persistent_store/src/driver.rs b/libraries/persistent_store/src/driver.rs index 15001cb..e529274 100644 --- a/libraries/persistent_store/src/driver.rs +++ b/libraries/persistent_store/src/driver.rs @@ -181,6 +181,12 @@ pub enum StoreInvariant { }, } +impl From for StoreInvariant { + fn from(error: StoreError) -> StoreInvariant { + StoreInvariant::StoreError(error) + } +} + impl StoreDriver { /// Provides read-only access to the storage. pub fn storage(&self) -> &BufferStorage { @@ -249,6 +255,10 @@ impl StoreDriverOff { } /// Powers on the store without interruption. + /// + /// # Panics + /// + /// Panics if the store cannot be powered on. pub fn power_on(self) -> Result { Ok(self .partial_power_on(StoreInterruption::none()) @@ -506,8 +516,8 @@ impl StoreDriverOn { /// Checks that the store and model are in sync. fn check_model(&self) -> Result<(), StoreInvariant> { let mut model_content = self.model.content().clone(); - for handle in self.store.iter().unwrap() { - let handle = handle.unwrap(); + for handle in self.store.iter()? { + let handle = handle?; let model_value = match model_content.remove(&handle.get_key()) { None => { return Err(StoreInvariant::OnlyInStore { @@ -516,7 +526,7 @@ impl StoreDriverOn { } Some(x) => x, }; - let store_value = handle.get_value(&self.store).unwrap().into_boxed_slice(); + let store_value = handle.get_value(&self.store)?.into_boxed_slice(); if store_value != model_value { return Err(StoreInvariant::DifferentValue { key: handle.get_key(), @@ -528,7 +538,7 @@ impl StoreDriverOn { if let Some(&key) = model_content.keys().next() { return Err(StoreInvariant::OnlyInModel { key }); } - let store_capacity = self.store.capacity().unwrap().remaining(); + let store_capacity = self.store.capacity()?.remaining(); let model_capacity = self.model.capacity().remaining(); if store_capacity != model_capacity { return Err(StoreInvariant::DifferentCapacity { @@ -544,8 +554,8 @@ impl StoreDriverOn { let format = self.model.format(); let storage = self.store.storage(); let num_words = format.page_size() / format.word_size(); - let head = self.store.head().unwrap(); - let tail = self.store.tail().unwrap(); + let head = self.store.head()?; + let tail = self.store.tail()?; for page in 0..format.num_pages() { // Check the erase cycle of the page. let store_erase = head.cycle(format) + (page < head.page(format)) as Nat; From 0b2ea7d98be2dabe7925ed8c98504631d261b09e Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 24 Nov 2020 15:14:03 +0100 Subject: [PATCH 013/192] makes HMAC secret output reproducible --- src/ctap/pin_protocol_v1.rs | 118 ++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 33 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index c03b81b..564ca6d 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -19,7 +19,6 @@ use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; #[cfg(feature = "with_ctap2_1")] use alloc::string::String; -#[cfg(feature = "with_ctap2_1")] use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; @@ -74,10 +73,9 @@ fn encrypt_hmac_secret_output( let mut cred_random_secret = [0u8; 32]; 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]; // With the if clause restriction above, block_len can only be 2 or 4. let block_len = salt_enc.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; for i in 0..block_len { blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]); } @@ -85,8 +83,8 @@ fn encrypt_hmac_secret_output( let mut decrypted_salt1 = [0u8; 32]; decrypted_salt1[..16].copy_from_slice(&blocks[0]); - let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); decrypted_salt1[16..].copy_from_slice(&blocks[1]); + let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); for i in 0..2 { blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); } @@ -638,36 +636,52 @@ impl PinProtocolV1 { #[cfg(test)] mod test { use super::*; - use arrayref::array_refs; 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; - pin[1] = 0x32; - pin[2] = 0x33; - pin[3] = 0x34; + pin[..4].copy_from_slice(b"1234"); let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); persistent_store.set_pin_hash(&pin_hash).unwrap(); } + // Encrypts the message with a zero IV and key derived from shared_secret. + fn encrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { + assert!(message.len() % 16 == 0); + let block_len = message.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { + blocks[i][..].copy_from_slice(&message[i * 16..(i + 1) * 16]); + } + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let iv = [0u8; 16]; + cbc_encrypt(&aes_enc_key, iv, &mut blocks); + blocks.iter().flatten().cloned().collect::>() + } + + // Decrypts the message with a zero IV and key derived from shared_secret. + fn decrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { + assert!(message.len() % 16 == 0); + let block_len = message.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { + blocks[i][..].copy_from_slice(&message[i * 16..(i + 1) * 16]); + } + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + let iv = [0u8; 16]; + cbc_decrypt(&aes_dec_key, iv, &mut blocks); + blocks.iter().flatten().cloned().collect::>() + } + // 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]; padded_pin[..pin.len()].copy_from_slice(&pin[..]); - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let mut blocks = [[0u8; 16]; 4]; - 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::>() + encrypt_message(shared_secret, &padded_pin) } // Encrypts the dummy PIN "1234". @@ -677,22 +691,10 @@ mod test { // 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]; - pin[0] = 0x31; - pin[1] = 0x32; - pin[2] = 0x33; - pin[3] = 0x34; + pin[..4].copy_from_slice(b"1234"); let pin_hash = Sha256::hash(&pin); - - let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash[..16]); - let iv = [0u8; 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 + encrypt_message(shared_secret, &pin_hash[..16]) } #[test] @@ -1184,6 +1186,56 @@ mod test { output, Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) ); + + let mut salt_enc = [0x00; 32]; + let cred_random = [0xC9; 32]; + + // Test values to check for reproducibility. + let salt1 = [0x01; 32]; + let salt2 = [0x02; 32]; + let expected_output1 = hmac_256::(&cred_random, &salt1); + let expected_output2 = hmac_256::(&cred_random, &salt2); + + let salt_enc1 = encrypt_message(&shared_secret, &salt1); + salt_enc.copy_from_slice(salt_enc1.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec, &expected_output1); + + let salt_enc2 = &encrypt_message(&shared_secret, &salt2); + salt_enc.copy_from_slice(salt_enc2.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec, &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt12 = [0x00; 64]; + salt12[..32].copy_from_slice(&salt1); + salt12[32..].copy_from_slice(&salt2); + let salt_enc12 = encrypt_message(&shared_secret, &salt12); + salt_enc.copy_from_slice(salt_enc12.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[..32], &expected_output1); + assert_eq!(&output_dec[32..], &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt02 = [0x00; 64]; + salt02[32..].copy_from_slice(&salt2); + let salt_enc02 = encrypt_message(&shared_secret, &salt02); + salt_enc.copy_from_slice(salt_enc02.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[32..], &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt10 = [0x00; 64]; + salt10[..32].copy_from_slice(&salt1); + let salt_enc10 = encrypt_message(&shared_secret, &salt10); + salt_enc.copy_from_slice(salt_enc10.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[..32], &expected_output1); } #[cfg(feature = "with_ctap2_1")] From 65f4f2de25b813ccf07871c0e57d8c84db8fd3f9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 24 Nov 2020 18:11:18 +0100 Subject: [PATCH 014/192] moves shared precheck into helper function --- src/ctap/mod.rs | 69 +++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index caea0a0..13fc951 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -344,6 +344,33 @@ where } } + fn pin_uv_auth_precheck( + &mut self, + pin_uv_auth_param: &Option>, + pin_uv_auth_protocol: Option, + cid: ChannelID, + ) -> Result<(), Ctap2StatusCode> { + if let Some(auth_param) = &pin_uv_auth_param { + // This case was added in FIDO 2.1. + if auth_param.is_empty() { + (self.check_user_presence)(cid)?; + if self.persistent_store.pin_hash()?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } + } + + match pin_uv_auth_protocol { + Some(CtapState::::PIN_PROTOCOL_VERSION) => Ok(()), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + None => Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), + } + } else { + Ok(()) + } + } + fn process_make_credential( &mut self, make_credential_params: AuthenticatorMakeCredentialParameters, @@ -361,26 +388,7 @@ where pin_uv_auth_protocol, } = make_credential_params; - if let Some(auth_param) = &pin_uv_auth_param { - // This case was added in FIDO 2.1. - if auth_param.is_empty() { - (self.check_user_presence)(cid)?; - if self.persistent_store.pin_hash()?.is_none() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - } - - match pin_uv_auth_protocol { - Some(protocol) => { - if protocol != CtapState::::PIN_PROTOCOL_VERSION { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } - None => return Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), - } - } + self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; if !pub_key_cred_params.contains(&ES256_CRED_PARAM) { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); @@ -564,26 +572,7 @@ where pin_uv_auth_protocol, } = get_assertion_params; - if let Some(auth_param) = &pin_uv_auth_param { - // This case was added in FIDO 2.1. - if auth_param.is_empty() { - (self.check_user_presence)(cid)?; - if self.persistent_store.pin_hash()?.is_none() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - } - - match pin_uv_auth_protocol { - Some(protocol) => { - if protocol != CtapState::::PIN_PROTOCOL_VERSION { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } - None => return Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), - } - } + self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); if hmac_secret_input.is_some() && !options.up { From 3dbfae972fa5721e12c48deea2c538136d8e5824 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:12:03 +0100 Subject: [PATCH 015/192] Always insert attestation material in the store --- src/ctap/key_material.rs | 7 +++-- src/ctap/storage.rs | 63 ++++++++++++++++------------------------ 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index 1958040..f8a833f 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub const AAGUID: &[u8; 16] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); +pub const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; +pub const AAGUID_LENGTH: usize = 16; + +pub const AAGUID: &[u8; AAGUID_LENGTH] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); pub const ATTESTATION_CERTIFICATE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_cert.bin")); -pub const ATTESTATION_PRIVATE_KEY: &[u8; 32] = +pub const ATTESTATION_PRIVATE_KEY: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_pkey.bin")); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 127dc23..5793e6c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -15,9 +15,9 @@ #[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::ctap::{key_material, USE_BATCH_ATTESTATION}; use crate::embedded_flash::{self, StoreConfig, StoreEntry, StoreError}; use alloc::string::String; #[cfg(any(test, feature = "ram_storage", feature = "with_ctap2_1"))] @@ -76,8 +76,6 @@ const MIN_PIN_LENGTH_RP_IDS: usize = 9; const NUM_TAGS: usize = 10; const MAX_PIN_RETRIES: u8 = 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 @@ -231,17 +229,16 @@ impl PersistentStore { }) .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() { - 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(); - } + // 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(); + } + if self.store.find_one(&Key::Aaguid).is_none() { self.set_aaguid(key_material::AAGUID).unwrap(); } @@ -525,20 +522,24 @@ impl PersistentStore { pub fn attestation_private_key( &self, - ) -> Result, Ctap2StatusCode> { + ) -> Result, Ctap2StatusCode> { let data = match self.store.find_one(&Key::AttestationPrivateKey) { None => return Ok(None), Some((_, entry)) => entry.data, }; - if data.len() != ATTESTATION_PRIVATE_KEY_LENGTH { + if data.len() != key_material::ATTESTATION_PRIVATE_KEY_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(Some(array_ref!(data, 0, ATTESTATION_PRIVATE_KEY_LENGTH))) + Ok(Some(array_ref!( + data, + 0, + key_material::ATTESTATION_PRIVATE_KEY_LENGTH + ))) } pub fn set_attestation_private_key( &mut self, - attestation_private_key: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH], + attestation_private_key: &[u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], ) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: ATTESTATION_PRIVATE_KEY, @@ -576,19 +577,22 @@ impl PersistentStore { Ok(()) } - pub fn aaguid(&self) -> Result<[u8; AAGUID_LENGTH], Ctap2StatusCode> { + 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() != AAGUID_LENGTH { + if data.len() != key_material::AAGUID_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(*array_ref![data, 0, AAGUID_LENGTH]) + Ok(*array_ref![data, 0, key_material::AAGUID_LENGTH]) } - pub fn set_aaguid(&mut self, aaguid: &[u8; AAGUID_LENGTH]) -> Result<(), Ctap2StatusCode> { + pub fn set_aaguid( + &mut self, + aaguid: &[u8; key_material::AAGUID_LENGTH], + ) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: AAGUID, data: aaguid, @@ -996,23 +1000,6 @@ mod test { 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. From 026b4a66ac1d983c09d1bf9385adf52ad94411ba Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:26:08 +0100 Subject: [PATCH 016/192] Fix CTAP2 batch attestation --- src/ctap/mod.rs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 13fc951..e92afb7 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -522,25 +522,27 @@ where let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); - // We currently use the presence of the attestation private key in the persistent storage to - // decide whether batch attestation is needed. - let (signature, x5c) = match self.persistent_store.attestation_private_key()? { - Some(attestation_private_key) => { - let attestation_key = - crypto::ecdsa::SecKey::from_bytes(attestation_private_key).unwrap(); - let attestation_certificate = self - .persistent_store - .attestation_certificate()? - .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; - ( - attestation_key.sign_rfc6979::(&signature_data), - Some(vec![attestation_certificate]), - ) - } - None => ( + + let (signature, x5c) = if USE_BATCH_ATTESTATION { + let attestation_private_key = self + .persistent_store + .attestation_private_key()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + let attestation_key = + crypto::ecdsa::SecKey::from_bytes(attestation_private_key).unwrap(); + let attestation_certificate = self + .persistent_store + .attestation_certificate()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + ( + attestation_key.sign_rfc6979::(&signature_data), + Some(vec![attestation_certificate]), + ) + } else { + ( sk.sign_rfc6979::(&signature_data), None, - ), + ) }; let attestation_statement = PackedAttestationStatement { alg: SignatureAlgorithm::ES256 as i64, From 41f7cc7b141db809d146d522be142635447b8d52 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:31:05 +0100 Subject: [PATCH 017/192] CTAP1/U2F accesses attestation material through the store. --- src/ctap/ctap1.rs | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index a5a7921..9eb1186 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -13,7 +13,6 @@ // limitations under the License. use super::hid::ChannelID; -use super::key_material::{ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY}; use super::status_code::Ctap2StatusCode; use super::CtapState; use alloc::vec::Vec; @@ -295,14 +294,22 @@ impl Ctap1Command { return Err(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG); } - let mut response = - Vec::with_capacity(105 + key_handle.len() + ATTESTATION_CERTIFICATE.len()); + let certificate = match ctap_state.persistent_store.attestation_certificate() { + Ok(Some(value)) => value, + _ => return Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), + }; + let private_key = match ctap_state.persistent_store.attestation_private_key() { + Ok(Some(value)) => value, + _ => return Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), + }; + + let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len()); response.push(Ctap1Command::LEGACY_BYTE); let user_pk = pk.to_uncompressed(); response.extend_from_slice(&user_pk); response.push(key_handle.len() as u8); response.extend(key_handle.clone()); - response.extend_from_slice(&ATTESTATION_CERTIFICATE); + response.extend_from_slice(&certificate); // The first byte is reserved. let mut signature_data = Vec::with_capacity(66 + key_handle.len()); @@ -312,7 +319,7 @@ impl Ctap1Command { signature_data.extend(key_handle); signature_data.extend_from_slice(&user_pk); - let attestation_key = crypto::ecdsa::SecKey::from_bytes(ATTESTATION_PRIVATE_KEY).unwrap(); + let attestation_key = crypto::ecdsa::SecKey::from_bytes(private_key).unwrap(); let signature = attestation_key.sign_rfc6979::(&signature_data); response.extend(signature.to_asn1_der()); @@ -373,7 +380,7 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::{CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; + use super::super::{key_material, CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; use super::*; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -434,8 +441,25 @@ mod test { ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = - Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); + Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + // Certificate and private key are missing + assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + assert!(ctap_state.persistent_store.set_attestation_private_key(&fake_key).is_ok()); + ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); + ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); + let response = + Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + // Certificate is still missing + assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + + let fake_cert = [0x99u8; 100]; // Arbitrary length + assert!(ctap_state.persistent_store.set_attestation_certificate(&fake_cert[..]).is_ok()); + ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); + ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); + let response = + Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); assert_eq!(response[0], Ctap1Command::LEGACY_BYTE); assert_eq!(response[66], CREDENTIAL_ID_BASE_SIZE as u8); assert!(ctap_state @@ -447,8 +471,8 @@ mod test { .is_some()); const CERT_START: usize = 67 + CREDENTIAL_ID_BASE_SIZE; assert_eq!( - &response[CERT_START..CERT_START + ATTESTATION_CERTIFICATE.len()], - &ATTESTATION_CERTIFICATE[..] + &response[CERT_START..CERT_START + fake_cert.len()], + &fake_cert[..] ); } From f47e1e2a8663e60a3c9b61db246cebfeb193d8f5 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:44:19 +0100 Subject: [PATCH 018/192] Ensure store behaves as expected in prod --- src/ctap/storage.rs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 5793e6c..da8828c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -229,6 +229,18 @@ impl PersistentStore { }) .unwrap(); } + // TODO(jmichel): remove this when vendor command is in place + #[cfg(not(any(test, feature = "ram_storage")))] + self.load_attestation_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(any(test, feature = "ram_storage")))] + fn load_attestation_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) @@ -238,10 +250,6 @@ impl PersistentStore { self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) .unwrap(); } - - if self.store.find_one(&Key::Aaguid).is_none() { - self.set_aaguid(key_material::AAGUID).unwrap(); - } } pub fn find_credential( @@ -1000,6 +1008,23 @@ mod test { 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. From f2b3ca4029227e532eb88972c766fd8dc99aba5d Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:44:52 +0100 Subject: [PATCH 019/192] Make private key sensitive and ensure attestation is OTP --- src/ctap/storage.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index da8828c..8e396b6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -552,11 +552,11 @@ impl PersistentStore { let entry = StoreEntry { tag: ATTESTATION_PRIVATE_KEY, data: attestation_private_key, - sensitive: false, + sensitive: true, }; match self.store.find_one(&Key::AttestationPrivateKey) { None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, + _ => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } Ok(()) } @@ -580,7 +580,7 @@ impl PersistentStore { }; match self.store.find_one(&Key::AttestationCertificate) { None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, + _ => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } Ok(()) } From d491492554e5e86a40d1d71b524fec569144217f Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:48:47 +0100 Subject: [PATCH 020/192] Format --- src/ctap/ctap1.rs | 16 ++++++++++------ src/ctap/key_material.rs | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 9eb1186..0fa4745 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -440,22 +440,26 @@ mod test { let message = create_register_message(&application); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); - let response = - Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate and private key are missing assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; - assert!(ctap_state.persistent_store.set_attestation_private_key(&fake_key).is_ok()); + assert!(ctap_state + .persistent_store + .set_attestation_private_key(&fake_key) + .is_ok()); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); - let response = - Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate is still missing assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); let fake_cert = [0x99u8; 100]; // Arbitrary length - assert!(ctap_state.persistent_store.set_attestation_certificate(&fake_cert[..]).is_ok()); + assert!(ctap_state + .persistent_store + .set_attestation_certificate(&fake_cert[..]) + .is_ok()); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index f8a833f..2563798 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -15,7 +15,8 @@ pub const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; pub const AAGUID_LENGTH: usize = 16; -pub const AAGUID: &[u8; AAGUID_LENGTH] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); +pub const AAGUID: &[u8; AAGUID_LENGTH] = + include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); pub const ATTESTATION_CERTIFICATE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_cert.bin")); From 3ae59ce1ecfdfaa7a2e84f86d0c8050d2b1e08e5 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 17 Sep 2020 09:23:17 +0200 Subject: [PATCH 021/192] GetNextAssertion command minimal implementation This still lacks order of credentials and timeouts. --- src/ctap/command.rs | 2 +- src/ctap/data_formats.rs | 6 +- src/ctap/mod.rs | 168 +++++++++++++++++++++++++-------------- 3 files changed, 112 insertions(+), 64 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 5c58234..1b4b96a 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -57,8 +57,8 @@ impl Command { const AUTHENTICATOR_GET_INFO: u8 = 0x04; const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06; const AUTHENTICATOR_RESET: u8 = 0x07; - // TODO(kaczmarczyck) use or remove those constants const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; + // TODO(kaczmarczyck) use or remove those constants const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; const AUTHENTICATOR_SELECTION: u8 = 0xB0; diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 45ebf9f..35d5bcf 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -308,7 +308,8 @@ impl TryFrom for GetAssertionExtensions { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, @@ -603,7 +604,8 @@ impl PublicKeyCredentialSource { // TODO(kaczmarczyck) we could decide to split this data type up // It depends on the algorithm though, I think. // So before creating a mess, this is my workaround. -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct CoseKey(pub BTreeMap); // This is the algorithm specifier that is supposed to be used in a COSE key diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 13fc951..f037655 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -33,9 +33,9 @@ use self::command::{ #[cfg(feature = "with_ctap2_1")] use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, SignatureAlgorithm, + CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, + PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; #[cfg(feature = "with_ctap2_1")] @@ -133,6 +133,14 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { } } +#[derive(Clone)] +struct AssertionInput { + client_data_hash: Vec, + auth_data: Vec, + uv: bool, + hmac_secret_input: Option, +} + // This struct currently holds all state, not only the persistent memory. The persistent members are // in the persistent store field. pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>> @@ -147,6 +155,8 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( accepts_reset: bool, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, + next_credentials: Vec, + next_assertion_input: Option, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -173,6 +183,8 @@ where U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), + next_credentials: vec![], + next_assertion_input: None, } } @@ -314,13 +326,13 @@ where Command::AuthenticatorGetAssertion(params) => { self.process_get_assertion(params, cid) } + Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(), Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid), #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), - // TODO(kaczmarczyck) implement GetNextAssertion and FIDO 2.1 commands - _ => self.process_unknown_command(), + // TODO(kaczmarczyck) implement FIDO 2.1 commands }; #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Sending response: {:#?}", response).unwrap(); @@ -557,6 +569,63 @@ where )) } + fn assertion_response( + &self, + credential: &PublicKeyCredentialSource, + assertion_input: AssertionInput, + ) -> Result { + let AssertionInput { + client_data_hash, + mut auth_data, + uv, + hmac_secret_input, + } = assertion_input; + + // Process extensions. + if let Some(hmac_secret_input) = hmac_secret_input { + 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, + }; + if !cbor::write(extensions_output, &mut auth_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + } + } + + let mut signature_data = auth_data.clone(); + signature_data.extend(client_data_hash); + let signature = credential + .private_key + .sign_rfc6979::(&signature_data); + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential.credential_id.clone(), + transports: None, // You can set USB as a hint here. + }; + let user = if uv && !credential.user_handle.is_empty() { + Some(PublicKeyCredentialUserEntity { + user_id: credential.user_handle.clone(), + user_name: None, + user_display_name: credential.other_ui.clone(), + user_icon: None, + }) + } else { + None + }; + Ok(ResponseData::AuthenticatorGetAssertion( + AuthenticatorGetAssertionResponse { + credential: Some(cred_desc), + auth_data, + signature: signature.to_asn1_der(), + user, + number_of_credentials: None, + }, + )) + } + fn process_get_assertion( &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, @@ -643,17 +712,19 @@ where } found_credentials } else { - // TODO(kaczmarczyck) use GetNextAssertion self.persistent_store.filter_credential(&rp_id, !has_uv)? }; - let credential = if let Some(credential) = credentials.first() { - credential - } else { - decrypted_credential - .as_ref() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? - }; + let credential = + if let Some((credential, remaining_credentials)) = credentials.split_first() { + // TODO(kaczmarczyck) correct credential order + self.next_credentials = remaining_credentials.to_vec(); + credential + } else { + decrypted_credential + .as_ref() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? + }; if options.up { (self.check_user_presence)(cid)?; @@ -661,50 +732,30 @@ where self.increment_global_signature_counter()?; - let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; - // Process extensions. - if let Some(hmac_secret_input) = hmac_secret_input { - 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, - }; - if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); - } + let assertion_input = AssertionInput { + client_data_hash, + auth_data: self.generate_auth_data(&rp_id_hash, flags)?, + uv: flags & UV_FLAG != 0, + hmac_secret_input, + }; + if !self.next_credentials.is_empty() { + self.next_assertion_input = Some(assertion_input.clone()); } + self.assertion_response(credential, assertion_input) + } - let mut signature_data = auth_data.clone(); - signature_data.extend(client_data_hash); - let signature = credential - .private_key - .sign_rfc6979::(&signature_data); - - let cred_desc = PublicKeyCredentialDescriptor { - key_type: PublicKeyCredentialType::PublicKey, - key_id: credential.credential_id.clone(), - transports: None, // You can set USB as a hint here. - }; - let user = if (flags & UV_FLAG != 0) && !credential.user_handle.is_empty() { - Some(PublicKeyCredentialUserEntity { - user_id: credential.user_handle.clone(), - user_name: None, - user_display_name: credential.other_ui.clone(), - user_icon: None, - }) + fn process_get_next_assertion(&mut self) -> Result { + // TODO(kaczmarczyck) introduce timer + let assertion_input = self + .next_assertion_input + .clone() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if let Some(credential) = self.next_credentials.pop() { + self.assertion_response(&credential, assertion_input) } else { - None - }; - Ok(ResponseData::AuthenticatorGetAssertion( - AuthenticatorGetAssertionResponse { - credential: Some(cred_desc), - auth_data, - signature: signature.to_asn1_der(), - user, - number_of_credentials: None, - }, - )) + self.next_assertion_input = None; + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } } fn process_get_info(&self) -> Result { @@ -786,10 +837,6 @@ where Ok(ResponseData::AuthenticatorSelection) } - fn process_unknown_command(&self) -> Result { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) - } - pub fn generate_auth_data( &self, rp_id_hash: &[u8], @@ -813,9 +860,8 @@ where #[cfg(test)] mod test { use super::data_formats::{ - CoseKey, GetAssertionExtensions, GetAssertionHmacSecretInput, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; use crypto::rng256::ThreadRng256; From af4eef808519b10055a92cce3aa8a97960df3d2a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 17 Nov 2020 16:57:03 +0100 Subject: [PATCH 022/192] adds credential ordering --- src/ctap/data_formats.rs | 7 +++++++ src/ctap/mod.rs | 11 +++++++++-- src/ctap/storage.rs | 25 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 35d5bcf..fa747bc 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -500,6 +500,7 @@ pub struct PublicKeyCredentialSource { pub other_ui: Option, pub cred_random: Option>, pub cred_protect_policy: Option, + pub creation_order: u64, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -512,6 +513,7 @@ enum PublicKeyCredentialSourceField { OtherUi = 4, CredRandom = 5, CredProtectPolicy = 6, + CreationOrder = 7, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: none. @@ -535,6 +537,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::OtherUi => credential.other_ui, PublicKeyCredentialSourceField::CredRandom => credential.cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, + PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, } } } @@ -552,6 +555,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::OtherUi => other_ui, PublicKeyCredentialSourceField::CredRandom => cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, + PublicKeyCredentialSourceField::CreationOrder => creation_order, } = extract_map(cbor_value)?; } @@ -569,6 +573,7 @@ impl TryFrom for PublicKeyCredentialSource { let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; + let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -588,6 +593,7 @@ impl TryFrom for PublicKeyCredentialSource { other_ui, cred_random, cred_protect_policy, + creation_order, }) } } @@ -1357,6 +1363,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert_eq!( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index f037655..331ccc1 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -155,6 +155,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( accepts_reset: bool, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, + // Sorted by ascending order of creation, so the last element is the most recent one. next_credentials: Vec, next_assertion_input: Option, } @@ -302,6 +303,7 @@ where other_ui: None, cred_random, cred_protect_policy: None, + creation_order: 0, })) } @@ -424,7 +426,6 @@ where } else { None }; - // TODO(kaczmarczyck) unsolicited output for default credProtect level let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); let rp_id = rp.rp_id; @@ -501,6 +502,7 @@ where .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, + creation_order: self.persistent_store.new_creation_order()?, }; self.persistent_store.store_credential(credential_source)?; random_id @@ -717,8 +719,9 @@ where let credential = if let Some((credential, remaining_credentials)) = credentials.split_first() { - // TODO(kaczmarczyck) correct credential order self.next_credentials = remaining_credentials.to_vec(); + self.next_credentials + .sort_unstable_by_key(|c| c.creation_order); credential } else { decrypted_credential @@ -1091,6 +1094,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1457,6 +1461,7 @@ mod test { cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1507,6 +1512,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1550,6 +1556,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 127dc23..6e4506a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -337,6 +337,26 @@ impl PersistentStore { .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) + + 1) + } + pub fn global_signature_counter(&self) -> Result { Ok(self .store @@ -690,6 +710,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, } } @@ -853,6 +874,7 @@ mod test { cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), + creation_order: 0, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -894,6 +916,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -913,6 +936,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + creation_order: 0, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1090,6 +1114,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From 5ff381678251473c0417b77f4b9a657d95d870a4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 23 Nov 2020 17:33:20 +0100 Subject: [PATCH 023/192] sets the correct user and number of credentials --- src/ctap/mod.rs | 209 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 162 insertions(+), 47 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 331ccc1..1f91f4f 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -137,7 +137,6 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { struct AssertionInput { client_data_hash: Vec, auth_data: Vec, - uv: bool, hmac_secret_input: Option, } @@ -571,15 +570,17 @@ where )) } + // Processes the input of a get_assertion operation for a given credential + // and returns the correct Get(Next)Assertion response. fn assertion_response( &self, credential: &PublicKeyCredentialSource, assertion_input: AssertionInput, + number_of_credentials: Option, ) -> Result { let AssertionInput { client_data_hash, mut auth_data, - uv, hmac_secret_input, } = assertion_input; @@ -607,7 +608,7 @@ where key_id: credential.credential_id.clone(), transports: None, // You can set USB as a hint here. }; - let user = if uv && !credential.user_handle.is_empty() { + let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { user_id: credential.user_handle.clone(), user_name: None, @@ -623,11 +624,37 @@ where auth_data, signature: signature.to_asn1_der(), user, - number_of_credentials: None, + number_of_credentials: number_of_credentials.map(|n| n as u64), }, )) } + // Returns the first applicable credential from the allow list. + fn get_any_credential_from_allow_list( + &mut self, + allow_list: Vec, + rp_id: &str, + rp_id_hash: &[u8], + has_uv: bool, + ) -> Result, Ctap2StatusCode> { + for allowed_credential in allow_list { + let credential = self.persistent_store.find_credential( + rp_id, + &allowed_credential.key_id, + !has_uv, + )?; + if credential.is_some() { + return Ok(credential); + } + let credential = + self.decrypt_credential_source(allowed_credential.key_id, &rp_id_hash)?; + if credential.is_some() { + return Ok(credential); + } + } + Ok(None) + } + fn process_get_assertion( &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, @@ -690,44 +717,29 @@ where } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut decrypted_credential = None; - let credentials = if let Some(allow_list) = allow_list { - let mut found_credentials = vec![]; - for allowed_credential in allow_list { - match self.persistent_store.find_credential( - &rp_id, - &allowed_credential.key_id, - !has_uv, - )? { - Some(credential) => { - found_credentials.push(credential); - } - None => { - if decrypted_credential.is_none() { - decrypted_credential = self.decrypt_credential_source( - allowed_credential.key_id, - &rp_id_hash, - )?; - } - } - } + let mut credentials = if let Some(allow_list) = allow_list { + if let Some(credential) = + self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)? + { + vec![credential] + } else { + vec![] } - found_credentials } else { self.persistent_store.filter_credential(&rp_id, !has_uv)? }; + // Remove user identifiable information without uv. + if !has_uv { + for credential in &mut credentials { + credential.other_ui = None; + } + } + credentials.sort_unstable_by_key(|c| c.creation_order); - let credential = - if let Some((credential, remaining_credentials)) = credentials.split_first() { - self.next_credentials = remaining_credentials.to_vec(); - self.next_credentials - .sort_unstable_by_key(|c| c.creation_order); - credential - } else { - decrypted_credential - .as_ref() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? - }; + let (credential, remaining_credentials) = credentials + .split_last() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + self.next_credentials = remaining_credentials.to_vec(); if options.up { (self.check_user_presence)(cid)?; @@ -738,13 +750,16 @@ where let assertion_input = AssertionInput { client_data_hash, auth_data: self.generate_auth_data(&rp_id_hash, flags)?, - uv: flags & UV_FLAG != 0, hmac_secret_input, }; - if !self.next_credentials.is_empty() { + let number_of_credentials = if self.next_credentials.is_empty() { + self.next_assertion_input = None; + None + } else { self.next_assertion_input = Some(assertion_input.clone()); - } - self.assertion_response(credential, assertion_input) + Some(self.next_credentials.len() + 1) + }; + self.assertion_response(credential, assertion_input, number_of_credentials) } fn process_get_next_assertion(&mut self) -> Result { @@ -753,12 +768,14 @@ where .next_assertion_input .clone() .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - if let Some(credential) = self.next_credentials.pop() { - self.assertion_response(&credential, assertion_input) - } else { + let credential = self + .next_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if self.next_credentials.is_empty() { self.next_assertion_input = None; - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + }; + self.assertion_response(&credential, assertion_input, None) } fn process_get_info(&self) -> Result { @@ -1318,7 +1335,13 @@ mod test { 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, ]; assert_eq!(auth_data, expected_auth_data); - assert!(user.is_none()); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0xFA, 0xB1, 0xA2], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); assert!(number_of_credentials.is_none()); } _ => panic!("Invalid response type"), @@ -1539,6 +1562,98 @@ mod test { ); } + #[test] + fn test_process_get_next_assertion() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = + ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + + match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0x02], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert_eq!(number_of_credentials, Some(2)); + } + _ => panic!("Invalid response type"), + } + + let get_assertion_response = ctap_state.process_get_next_assertion(); + match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0x01], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert!(number_of_credentials.is_none()); + } + _ => panic!("Invalid response type"), + } + + let get_assertion_response = ctap_state.process_get_next_assertion(); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + #[test] fn test_process_reset() { let mut rng = ThreadRng256 {}; From ffe19e152b4bc334a188f7e35fcaf1bf51853ca2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 24 Nov 2020 17:17:18 +0100 Subject: [PATCH 024/192] moves UP check in GetAssertion before NO_CREDENTIALS --- src/ctap/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 1f91f4f..98058ce 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -736,15 +736,17 @@ where } credentials.sort_unstable_by_key(|c| c.creation_order); + // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. + // For CTAP 2.1, it was moved to a later protocol step. + if options.up { + (self.check_user_presence)(cid)?; + } + let (credential, remaining_credentials) = credentials .split_last() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; self.next_credentials = remaining_credentials.to_vec(); - if options.up { - (self.check_user_presence)(cid)?; - } - self.increment_global_signature_counter()?; let assertion_input = AssertionInput { From ed59ebac0dd3dc5f44057e11c3b8a106d20a46e7 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 14:40:02 +0100 Subject: [PATCH 025/192] command timeout for GetNextAssertion --- fuzz/fuzz_helper/src/lib.rs | 4 +- src/ctap/ctap1.rs | 26 +- src/ctap/hid/mod.rs | 11 +- src/ctap/mod.rs | 490 ++++++++++++++++++++++++------------ src/ctap/storage.rs | 15 ++ src/main.rs | 11 +- 6 files changed, 373 insertions(+), 184 deletions(-) diff --git a/fuzz/fuzz_helper/src/lib.rs b/fuzz/fuzz_helper/src/lib.rs index a6dc1a7..1553f65 100644 --- a/fuzz/fuzz_helper/src/lib.rs +++ b/fuzz/fuzz_helper/src/lib.rs @@ -147,7 +147,7 @@ fn process_message( pub fn process_ctap_any_type(data: &[u8]) { // Initialize ctap state and hid and get the allocated cid. let mut rng = ThreadRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = initialize(&mut ctap_state, &mut ctap_hid); // Wrap input as message with the allocated cid. @@ -165,7 +165,7 @@ pub fn process_ctap_specific_type(data: &[u8], input_type: InputType) { } // Initialize ctap state and hid and get the allocated cid. let mut rng = ThreadRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = initialize(&mut ctap_state, &mut ctap_hid); // Wrap input as message with allocated cid and command type. diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index a5a7921..f299926 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -427,7 +427,7 @@ mod test { fn test_process_register() { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let application = [0x0A; 32]; let message = create_register_message(&application); @@ -456,7 +456,7 @@ mod test { fn test_process_register_bad_message() { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let application = [0x0A; 32]; let message = create_register_message(&application); @@ -476,7 +476,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); @@ -490,7 +490,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -508,7 +508,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -527,7 +527,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -553,7 +553,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -573,7 +573,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -593,7 +593,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -613,7 +613,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -640,7 +640,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -672,7 +672,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); @@ -689,7 +689,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index a6a18f7..3874792 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -200,7 +200,8 @@ impl CtapHid { // Each transaction is atomic, so we process the command directly here and // don't handle any other packet in the meantime. // TODO: Send keep-alive packets in the meantime. - let response = ctap_state.process_command(&message.payload, cid); + let response = + ctap_state.process_command(&message.payload, cid, clock_value); if let Some(iterator) = CtapHid::split_message(Message { cid, cmd: CtapHid::COMMAND_CBOR, @@ -520,7 +521,7 @@ mod test { fn test_spurious_continuation_packet() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let mut packet = [0x00; 64]; @@ -541,7 +542,7 @@ mod test { fn test_command_init() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let reply = process_messages( @@ -586,7 +587,7 @@ mod test { fn test_command_init_for_sync() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = cid_from_init(&mut ctap_hid, &mut ctap_state); @@ -646,7 +647,7 @@ mod test { fn test_command_ping() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = cid_from_init(&mut ctap_hid, &mut ctap_state); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 98058ce..4675350 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -47,6 +47,7 @@ use self::response::{ }; use self::status_code::Ctap2StatusCode; use self::storage::PersistentStore; +use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; use alloc::collections::BTreeMap; @@ -65,7 +66,7 @@ use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(feature = "debug_ctap")] use libtock_drivers::console::Console; -use libtock_drivers::timer::{Duration, Timestamp}; +use libtock_drivers::timer::{ClockValue, Duration}; // 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 @@ -99,7 +100,8 @@ const ED_FLAG: u8 = 0x80; pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); -const RESET_TIMEOUT_MS: isize = 10000; +const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); +const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -140,6 +142,17 @@ struct AssertionInput { hmac_secret_input: Option, } +struct AssertionState { + assertion_input: AssertionInput, + // Sorted by ascending order of creation, so the last element is the most recent one. + next_credentials: Vec, +} + +enum StatefulCommand { + Reset, + GetAssertion(AssertionState), +} + // This struct currently holds all state, not only the persistent memory. The persistent members are // in the persistent store field. pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>> @@ -150,13 +163,11 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( check_user_presence: CheckUserPresence, persistent_store: PersistentStore, 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")] pub u2f_up_state: U2fUserPresenceState, - // Sorted by ascending order of creation, so the last element is the most recent one. - next_credentials: Vec, - next_assertion_input: Option, + // The state initializes to Reset and its timeout, and never goes back to Reset. + stateful_command_permission: TimedPermission, + stateful_command_type: Option, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -169,6 +180,7 @@ where pub fn new( rng: &'a mut R, check_user_presence: CheckUserPresence, + now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { let persistent_store = PersistentStore::new(rng); let pin_protocol_v1 = PinProtocolV1::new(rng); @@ -177,20 +189,26 @@ where check_user_presence, persistent_store, pin_protocol_v1, - accepts_reset: true, #[cfg(feature = "with_ctap1")] u2f_up_state: U2fUserPresenceState::new( U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), - next_credentials: vec![], - next_assertion_input: None, + stateful_command_permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), + stateful_command_type: Some(StatefulCommand::Reset), } } - pub fn check_disable_reset(&mut self, timestamp: Timestamp) { - if timestamp - Timestamp::::from_ms(0) > Duration::from_ms(RESET_TIMEOUT_MS) { - self.accepts_reset = false; + pub fn update_command_permission(&mut self, now: ClockValue) { + self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + } + + fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { + self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + if self.stateful_command_permission.is_granted(now) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) } } @@ -306,7 +324,12 @@ where })) } - pub fn process_command(&mut self, command_cbor: &[u8], cid: ChannelID) -> Vec { + pub fn process_command( + &mut self, + command_cbor: &[u8], + cid: ChannelID, + now: ClockValue, + ) -> Vec { let cmd = Command::deserialize(command_cbor); #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Received command: {:#?}", cmd).unwrap(); @@ -320,17 +343,32 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ); } + match (&command, &self.stateful_command_type) { + ( + Command::AuthenticatorGetNextAssertion, + Some(StatefulCommand::GetAssertion(_)), + ) => (), + (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), + // GetInfo does not reset stateful commands. + (Command::AuthenticatorGetInfo, _) => (), + // AuthenticatorSelection does not reset stateful commands. + #[cfg(feature = "with_ctap2_1")] + (Command::AuthenticatorSelection, _) => (), + (_, _) => { + self.stateful_command_type = None; + } + } let response = match command { Command::AuthenticatorMakeCredential(params) => { self.process_make_credential(params, cid) } Command::AuthenticatorGetAssertion(params) => { - self.process_get_assertion(params, cid) + self.process_get_assertion(params, cid, now) } - Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(), + Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(now), Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), - Command::AuthenticatorReset => self.process_reset(cid), + Command::AuthenticatorReset => self.process_reset(cid, now), #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands @@ -659,6 +697,7 @@ where &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, cid: ChannelID, + now: ClockValue, ) -> Result { let AuthenticatorGetAssertionParameters { rp_id, @@ -745,7 +784,6 @@ where let (credential, remaining_credentials) = credentials .split_last() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; - self.next_credentials = remaining_credentials.to_vec(); self.increment_global_signature_counter()?; @@ -754,29 +792,37 @@ where auth_data: self.generate_auth_data(&rp_id_hash, flags)?, hmac_secret_input, }; - let number_of_credentials = if self.next_credentials.is_empty() { - self.next_assertion_input = None; + let number_of_credentials = if remaining_credentials.is_empty() { None } else { - self.next_assertion_input = Some(assertion_input.clone()); - Some(self.next_credentials.len() + 1) + self.stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { + assertion_input: assertion_input.clone(), + next_credentials: remaining_credentials.to_vec(), + })); + Some(remaining_credentials.len() + 1) }; self.assertion_response(credential, assertion_input, number_of_credentials) } - fn process_get_next_assertion(&mut self) -> Result { - // TODO(kaczmarczyck) introduce timer - let assertion_input = self - .next_assertion_input - .clone() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self - .next_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - if self.next_credentials.is_empty() { - self.next_assertion_input = None; - }; + fn process_get_next_assertion( + &mut self, + now: ClockValue, + ) -> Result { + self.check_command_permission(now)?; + let (assertion_input, credential) = + if let Some(StatefulCommand::GetAssertion(assertion_state)) = + &mut self.stateful_command_type + { + let credential = assertion_state + .next_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + (assertion_state.assertion_input.clone(), credential) + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + }; self.assertion_response(&credential, assertion_input, None) } @@ -834,10 +880,17 @@ where ) } - fn process_reset(&mut self, cid: ChannelID) -> Result { + fn process_reset( + &mut self, + cid: ChannelID, + now: ClockValue, + ) -> Result { // Resets are only possible in the first 10 seconds after booting. - if !self.accepts_reset { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? + self.check_command_permission(now)?; + match &self.stateful_command_type { + Some(StatefulCommand::Reset) => (), + _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), } (self.check_user_presence)(cid)?; @@ -886,8 +939,11 @@ mod test { MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; + use cbor::cbor_array; use crypto::rng256::ThreadRng256; + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); // The keep-alive logic in the processing of some commands needs a channel ID to send // keep-alive packets to. // In tests where we define a dummy user-presence check that immediately returns, the channel @@ -898,8 +954,8 @@ mod test { fn test_get_info() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); - let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); #[cfg(feature = "with_ctap2_1")] let mut expected_response = vec![0x00, 0xAA, 0x01]; @@ -955,7 +1011,7 @@ mod test { rp_icon: None, }; let user = PublicKeyCredentialUserEntity { - user_id: vec![0xFA, 0xB1, 0xA2], + user_id: vec![0x1D], user_name: None, user_display_name: None, user_icon: None, @@ -1008,7 +1064,7 @@ mod test { fn test_residential_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = @@ -1044,7 +1100,7 @@ mod test { fn test_non_residential_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1081,7 +1137,7 @@ mod test { fn test_process_make_credential_unsupported_algorithm() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.pub_key_cred_params = vec![]; @@ -1099,7 +1155,7 @@ mod test { let mut rng = ThreadRng256 {}; let excluded_private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; let make_credential_params = @@ -1132,7 +1188,7 @@ mod test { fn test_process_make_credential_credential_with_cred_protect() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let test_policy = CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; let make_credential_params = @@ -1186,7 +1242,7 @@ mod test { fn test_process_make_credential_hmac_secret() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1236,7 +1292,7 @@ mod test { fn test_process_make_credential_hmac_secret_resident_key() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1285,7 +1341,8 @@ mod test { fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; let user_presence_always_cancel = |_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL); - let mut ctap_state = CtapState::new(&mut rng, user_presence_always_cancel); + let mut ctap_state = + CtapState::new(&mut rng, user_presence_always_cancel, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = @@ -1297,11 +1354,43 @@ mod test { ); } + fn check_assertion_response( + response: Result, + expected_user_id: Vec, + expected_number_of_credentials: Option, + ) { + match response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert_eq!(number_of_credentials, expected_number_of_credentials); + } + _ => panic!("Invalid response type"), + } + } + #[test] fn test_residential_process_get_assertion() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); assert!(ctap_state @@ -1320,34 +1409,12 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); - - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0xFA, 0xB1, 0xA2], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert!(number_of_credentials.is_none()); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x1D], None); } #[test] @@ -1355,7 +1422,7 @@ mod test { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1405,8 +1472,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, @@ -1419,7 +1489,7 @@ mod test { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1453,8 +1523,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, @@ -1468,7 +1541,7 @@ mod test { let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let credential_id = rng.gen_uniform_u8x32().to_vec(); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, @@ -1480,7 +1553,7 @@ mod test { credential_id: credential_id.clone(), private_key: private_key.clone(), rp_id: String::from("example.com"), - user_handle: vec![0x00], + user_handle: vec![0x1D], other_ui: None, cred_random: None, cred_protect_policy: Some( @@ -1505,8 +1578,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), @@ -1524,16 +1600,19 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); - assert!(get_assertion_response.is_ok()); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x1D], None); let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id, private_key, rp_id: String::from("example.com"), - user_handle: vec![0x00], + user_handle: vec![0x1D], other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), @@ -1556,8 +1635,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), @@ -1565,10 +1647,10 @@ mod test { } #[test] - fn test_process_get_next_assertion() { + fn test_process_get_next_assertion_two_credentials() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x01]; @@ -1593,63 +1675,135 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x02], Some(2)); - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0x02], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert_eq!(number_of_credentials, Some(2)); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x01], None); - let get_assertion_response = ctap_state.process_get_next_assertion(); - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0x01], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert!(number_of_credentials.is_none()); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } - let get_assertion_response = ctap_state.process_get_next_assertion(); + #[test] + fn test_process_get_next_assertion_three_credentials() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x03]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x03], Some(3)); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x02], None); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x01], None); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_get_next_assertion_not_allowed() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + assert!(get_assertion_response.is_ok()); + + // This is a MakeCredential command. + let mut command_cbor = vec![0x01]; + let cbor_value = cbor_map! { + 1 => vec![0xCD; 16], + 2 => cbor_map! { + "id" => "example.com", + }, + 3 => cbor_map! { + "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + }, + 4 => cbor_array![ES256_CRED_PARAM], + }; + assert!(cbor::write(cbor_value, &mut command_cbor)); + ctap_state.process_command(&command_cbor, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -1661,7 +1815,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let credential_id = vec![0x01, 0x23, 0x45, 0x67]; let credential_source = PublicKeyCredentialSource { @@ -1681,7 +1835,8 @@ mod test { .is_ok()); assert!(ctap_state.persistent_store.count_credentials().unwrap() > 0); - let reset_reponse = ctap_state.process_command(&[0x07], DUMMY_CHANNEL_ID); + let reset_reponse = + ctap_state.process_command(&[0x07], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![0x00]; assert_eq!(reset_reponse, expected_response); assert!(ctap_state.persistent_store.count_credentials().unwrap() == 0); @@ -1691,9 +1846,10 @@ mod test { fn test_process_reset_cancelled() { let mut rng = ThreadRng256 {}; let user_presence_always_cancel = |_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL); - let mut ctap_state = CtapState::new(&mut rng, user_presence_always_cancel); + let mut ctap_state = + CtapState::new(&mut rng, user_presence_always_cancel, DUMMY_CLOCK_VALUE); - let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID); + let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); assert_eq!( reset_reponse, @@ -1701,14 +1857,28 @@ mod test { ); } + #[test] + fn test_process_reset_not_first() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // This is a GetNextAssertion command. + ctap_state.process_command(&[0x08], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + + let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); + } + #[test] fn test_process_unknown_command() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // This command does not exist. - let reset_reponse = ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID); + let reset_reponse = + ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8]; assert_eq!(reset_reponse, expected_response); } @@ -1718,7 +1888,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Usually, the relying party ID or its hash is provided by the client. // We are not testing the correctness of our SHA256 here, only if it is checked. @@ -1739,7 +1909,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Usually, the relying party ID or its hash is provided by the client. // We are not testing the correctness of our SHA256 here, only if it is checked. @@ -1762,7 +1932,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Same as above. let rp_id_hash = [0x55; 32]; @@ -1784,7 +1954,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Same as above. let rp_id_hash = [0x55; 32]; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 6e4506a..aae6add 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -746,6 +746,21 @@ mod test { 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() { diff --git a/src/main.rs b/src/main.rs index 855325e..51c8305 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,9 +37,11 @@ use libtock_drivers::console::Console; use libtock_drivers::led; use libtock_drivers::result::{FlexUnwrap, TockError}; use libtock_drivers::timer; +use libtock_drivers::timer::Duration; #[cfg(feature = "debug_ctap")] use libtock_drivers::timer::Timer; -use libtock_drivers::timer::{Duration, Timestamp}; +#[cfg(feature = "debug_ctap")] +use libtock_drivers::timer::Timestamp; use libtock_drivers::usb_ctap_hid; const KEEPALIVE_DELAY_MS: isize = 100; @@ -57,12 +59,13 @@ fn main() { panic!("Cannot setup USB driver"); } + let boot_time = timer.get_current_clock().flex_unwrap(); let mut rng = TockRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, check_user_presence); + let mut ctap_state = CtapState::new(&mut rng, check_user_presence, boot_time); let mut ctap_hid = CtapHid::new(); let mut led_counter = 0; - let mut last_led_increment = timer.get_current_clock().flex_unwrap(); + let mut last_led_increment = boot_time; // Main loop. If CTAP1 is used, we register button presses for U2F while receiving and waiting. // The way TockOS and apps currently interact, callbacks need a yield syscall to execute, @@ -115,7 +118,7 @@ fn main() { // These calls are making sure that even for long inactivity, wrapping clock values // never randomly wink or grant user presence for U2F. - ctap_state.check_disable_reset(Timestamp::::from_clock_value(now)); + ctap_state.update_command_permission(now); ctap_hid.wink_permission = ctap_hid.wink_permission.check_expiration(now); if has_packet { From 3aef7e8b19d6a522af175f73de1b239bae3bd682 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 15:56:59 +0100 Subject: [PATCH 026/192] reuse update_command_permission --- src/ctap/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4675350..f2043d0 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -204,7 +204,7 @@ where } fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { - self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + self.update_command_permission(now); if self.stateful_command_permission.is_granted(now) { Ok(()) } else { From 3d1d8279847ef0dd8407b0a74e6c963a2eef4ce0 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Thu, 26 Nov 2020 16:29:14 +0100 Subject: [PATCH 027/192] Address PR comments --- src/ctap/ctap1.rs | 28 +++++++++++++++++----------- src/ctap/storage.rs | 8 ++++---- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 0fa4745..41df364 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -35,6 +35,8 @@ pub enum Ctap1StatusCode { SW_WRONG_LENGTH = 0x6700, SW_CLA_NOT_SUPPORTED = 0x6E00, SW_INS_NOT_SUPPORTED = 0x6D00, + SW_MEMERR = 0x6501, + SW_COMMAND_ABORTED = 0x6F00, SW_VENDOR_KEY_HANDLE_TOO_LONG = 0xF000, } @@ -49,6 +51,8 @@ impl TryFrom for Ctap1StatusCode { 0x6700 => Ok(Ctap1StatusCode::SW_WRONG_LENGTH), 0x6E00 => Ok(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED), 0x6D00 => Ok(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), + 0x6501 => Ok(Ctap1StatusCode::SW_MEMERR), + 0x6F00 => Ok(Ctap1StatusCode::SW_COMMAND_ABORTED), 0xF000 => Ok(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG), _ => Err(()), } @@ -288,20 +292,22 @@ impl Ctap1Command { let pk = sk.genpk(); let key_handle = ctap_state .encrypt_key_handle(sk, &application, None) - .map_err(|_| Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG)?; + .map_err(|_| Ctap1StatusCode::SW_COMMAND_ABORTED)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. return Err(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG); } - let certificate = match ctap_state.persistent_store.attestation_certificate() { - Ok(Some(value)) => value, - _ => return Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), - }; - let private_key = match ctap_state.persistent_store.attestation_private_key() { - Ok(Some(value)) => value, - _ => return Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), - }; + let certificate = ctap_state + .persistent_store + .attestation_certificate() + .map_err(|_| Ctap1StatusCode::SW_MEMERR)? + .ok_or(Ctap1StatusCode::SW_COMMAND_ABORTED)?; + let private_key = ctap_state + .persistent_store + .attestation_private_key() + .map_err(|_| Ctap1StatusCode::SW_MEMERR)? + .ok_or(Ctap1StatusCode::SW_COMMAND_ABORTED)?; let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len()); response.push(Ctap1Command::LEGACY_BYTE); @@ -442,7 +448,7 @@ mod test { ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate and private key are missing - assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_ABORTED)); let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; assert!(ctap_state @@ -453,7 +459,7 @@ mod test { ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate is still missing - assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_ABORTED)); let fake_cert = [0x99u8; 100]; // Arbitrary length assert!(ctap_state diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 8e396b6..5ec6511 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -230,8 +230,8 @@ impl PersistentStore { .unwrap(); } // TODO(jmichel): remove this when vendor command is in place - #[cfg(not(any(test, feature = "ram_storage")))] - self.load_attestation_from_firmware(); + #[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(); @@ -239,8 +239,8 @@ impl PersistentStore { } // TODO(jmichel): remove this function when vendor command is in place. - #[cfg(not(any(test, feature = "ram_storage")))] - fn load_attestation_from_firmware(&mut self) { + #[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) From 1571f58cd3e51849626cb3681bb15ccef10155b8 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 19:21:41 +0100 Subject: [PATCH 028/192] wrapping_add in storage and more moving --- src/ctap/mod.rs | 27 ++++++++++++++------------- src/ctap/storage.rs | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 9626da4..5fdfa9e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -614,7 +614,7 @@ where // and returns the correct Get(Next)Assertion response. fn assertion_response( &self, - credential: &PublicKeyCredentialSource, + credential: PublicKeyCredentialSource, assertion_input: AssertionInput, number_of_credentials: Option, ) -> Result { @@ -645,14 +645,14 @@ where let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, - key_id: credential.credential_id.clone(), + key_id: credential.credential_id, transports: None, // You can set USB as a hint here. }; let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { - user_id: credential.user_handle.clone(), + user_id: credential.user_handle, user_name: None, - user_display_name: credential.other_ui.clone(), + user_display_name: credential.other_ui, user_icon: None, }) } else { @@ -758,7 +758,7 @@ where } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut credentials = if let Some(allow_list) = allow_list { + let mut applicable_credentials = if let Some(allow_list) = allow_list { if let Some(credential) = self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)? { @@ -771,11 +771,11 @@ where }; // Remove user identifiable information without uv. if !has_uv { - for credential in &mut credentials { + for credential in &mut applicable_credentials { credential.other_ui = None; } } - credentials.sort_unstable_by_key(|c| c.creation_order); + applicable_credentials.sort_unstable_by_key(|c| c.creation_order); // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. // For CTAP 2.1, it was moved to a later protocol step. @@ -783,8 +783,8 @@ where (self.check_user_presence)(cid)?; } - let (credential, remaining_credentials) = credentials - .split_last() + let credential = applicable_credentials + .pop() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; self.increment_global_signature_counter()?; @@ -794,16 +794,17 @@ where auth_data: self.generate_auth_data(&rp_id_hash, flags)?, hmac_secret_input, }; - let number_of_credentials = if remaining_credentials.is_empty() { + let number_of_credentials = if applicable_credentials.is_empty() { None } else { + let number_of_credentials = Some(applicable_credentials.len() + 1); self.stateful_command_permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { assertion_input: assertion_input.clone(), - next_credentials: remaining_credentials.to_vec(), + next_credentials: applicable_credentials, })); - Some(remaining_credentials.len() + 1) + number_of_credentials }; self.assertion_response(credential, assertion_input, number_of_credentials) } @@ -825,7 +826,7 @@ where } else { return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); }; - self.assertion_response(&credential, assertion_input, None) + self.assertion_response(credential, assertion_input, None) } fn process_get_info(&self) -> Result { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index fd91cce..7952d75 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -359,7 +359,7 @@ impl PersistentStore { .map(|c| c.creation_order) .max() .unwrap_or(0) - + 1) + .wrapping_add(1)) } pub fn global_signature_counter(&self) -> Result { From 2a4677c0b1a03229dd5049c0d32d12ea736e8167 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 27 Nov 2020 15:04:47 +0100 Subject: [PATCH 029/192] adds user data to persistent storage --- src/ctap/data_formats.rs | 52 ++++++++++++--- src/ctap/mod.rs | 123 ++++++++++++++++++++++++++++-------- src/ctap/pin_protocol_v1.rs | 16 +++++ src/ctap/storage.rs | 20 ++++-- 4 files changed, 169 insertions(+), 42 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index fa747bc..e7419fd 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -56,7 +56,7 @@ impl TryFrom for PublicKeyCredentialRpEntity { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -497,10 +497,12 @@ pub struct PublicKeyCredentialSource { pub private_key: ecdsa::SecKey, // TODO(kaczmarczyck) open for other algorithms pub rp_id: String, pub user_handle: Vec, // not optional, but nullable - pub other_ui: Option, + pub user_display_name: Option, pub cred_random: Option>, pub cred_protect_policy: Option, pub creation_order: u64, + pub user_name: Option, + pub user_icon: Option, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -510,10 +512,12 @@ enum PublicKeyCredentialSourceField { PrivateKey = 1, RpId = 2, UserHandle = 3, - OtherUi = 4, + UserDisplayName = 4, CredRandom = 5, CredProtectPolicy = 6, CreationOrder = 7, + UserName = 8, + UserIcon = 9, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: none. @@ -534,10 +538,12 @@ impl From for cbor::Value { 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::UserDisplayName => credential.user_display_name, PublicKeyCredentialSourceField::CredRandom => credential.cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, + PublicKeyCredentialSourceField::UserName => credential.user_name, + PublicKeyCredentialSourceField::UserIcon => credential.user_icon, } } } @@ -552,10 +558,12 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::PrivateKey => private_key, PublicKeyCredentialSourceField::RpId => rp_id, PublicKeyCredentialSourceField::UserHandle => user_handle, - PublicKeyCredentialSourceField::OtherUi => other_ui, + PublicKeyCredentialSourceField::UserDisplayName => user_display_name, PublicKeyCredentialSourceField::CredRandom => cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, PublicKeyCredentialSourceField::CreationOrder => creation_order, + PublicKeyCredentialSourceField::UserName => user_name, + PublicKeyCredentialSourceField::UserIcon => user_icon, } = extract_map(cbor_value)?; } @@ -568,12 +576,14 @@ impl TryFrom for PublicKeyCredentialSource { .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)?; let rp_id = extract_text_string(ok_or_missing(rp_id)?)?; let user_handle = extract_byte_string(ok_or_missing(user_handle)?)?; - let other_ui = other_ui.map(extract_text_string).transpose()?; + let user_display_name = user_display_name.map(extract_text_string).transpose()?; let cred_random = cred_random.map(extract_byte_string).transpose()?; let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; + let user_name = user_name.map(extract_text_string).transpose()?; + let user_icon = user_icon.map(extract_text_string).transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -590,10 +600,12 @@ impl TryFrom for PublicKeyCredentialSource { private_key, rp_id, user_handle, - other_ui, + user_display_name, cred_random, cred_protect_policy, creation_order, + user_name, + user_icon, }) } } @@ -1360,10 +1372,12 @@ mod test { private_key: crypto::ecdsa::SecKey::gensk(&mut rng), rp_id: "example.com".to_string(), user_handle: b"foo".to_vec(), - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, }; assert_eq!( @@ -1372,7 +1386,7 @@ mod test { ); let credential = PublicKeyCredentialSource { - other_ui: Some("other".to_string()), + user_display_name: Some("Display Name".to_string()), ..credential }; @@ -1396,6 +1410,26 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + user_name: Some("name".to_string()), + ..credential + }; + + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + user_icon: Some("icon".to_string()), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 5fdfa9e..8efb92e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -317,10 +317,12 @@ where private_key: sk, rp_id: String::from(""), user_handle: vec![], - other_ui: None, + user_display_name: None, cred_random, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, })) } @@ -534,12 +536,18 @@ where user_handle: user.user_id, // This input is user provided, so we crop it to 64 byte for storage. // The UTF8 encoding is always preserved, so the string might end up shorter. - other_ui: user + user_display_name: user .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, creation_order: self.persistent_store.new_creation_order()?, + user_name: user + .user_name + .map(|s| truncate_to_char_boundary(&s, 64).to_string()), + user_icon: user + .user_icon + .map(|s| truncate_to_char_boundary(&s, 64).to_string()), }; self.persistent_store.store_credential(credential_source)?; random_id @@ -651,9 +659,9 @@ where let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { user_id: credential.user_handle, - user_name: None, - user_display_name: credential.other_ui, - user_icon: None, + user_name: credential.user_name, + user_display_name: credential.user_display_name, + user_icon: credential.user_icon, }) } else { None @@ -772,7 +780,9 @@ where // Remove user identifiable information without uv. if !has_uv { for credential in &mut applicable_credentials { - credential.other_ui = None; + credential.user_name = None; + credential.user_display_name = None; + credential.user_icon = None; } } applicable_credentials.sort_unstable_by_key(|c| c.creation_order); @@ -1169,10 +1179,12 @@ mod test { private_key: excluded_private_key, rp_id: String::from("example.com"), user_handle: vec![], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, }; assert!(ctap_state .persistent_store @@ -1357,9 +1369,10 @@ mod test { ); } - fn check_assertion_response( + fn check_assertion_response_with_user( response: Result, - expected_user_id: Vec, + expected_user: PublicKeyCredentialUserEntity, + flags: u8, expected_number_of_credentials: Option, ) { match response.unwrap() { @@ -1373,15 +1386,9 @@ mod test { let expected_auth_data = vec![ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, 0x01, ]; assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: expected_user_id, - user_name: None, - user_display_name: None, - user_icon: None, - }; assert_eq!(user, Some(expected_user)); assert_eq!(number_of_credentials, expected_number_of_credentials); } @@ -1389,6 +1396,25 @@ mod test { } } + fn check_assertion_response( + response: Result, + expected_user_id: Vec, + expected_number_of_credentials: Option, + ) { + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + check_assertion_response_with_user( + response, + expected_user, + 0x00, + expected_number_of_credentials, + ); + } + #[test] fn test_residential_process_get_assertion() { let mut rng = ThreadRng256 {}; @@ -1557,12 +1583,14 @@ mod test { private_key: private_key.clone(), rp_id: String::from("example.com"), user_handle: vec![0x1D], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), creation_order: 0, + user_name: None, + user_icon: None, }; assert!(ctap_state .persistent_store @@ -1616,10 +1644,12 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x1D], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), creation_order: 0, + user_name: None, + user_icon: None, }; assert!(ctap_state .persistent_store @@ -1650,22 +1680,48 @@ mod test { } #[test] - fn test_process_get_next_assertion_two_credentials() { + fn test_process_get_next_assertion_two_credentials_with_uv() { let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x88; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; let mut make_credential_params = create_minimal_make_credential_parameters(); - make_credential_params.user.user_id = vec![0x01]; + let user1 = PublicKeyCredentialUserEntity { + user_id: vec![0x01], + user_name: Some("user1".to_string()), + user_display_name: Some("User One".to_string()), + user_icon: Some("icon1".to_string()), + }; + make_credential_params.user = user1.clone(); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); let mut make_credential_params = create_minimal_make_credential_parameters(); - make_credential_params.user.user_id = vec![0x02]; + let user2 = PublicKeyCredentialUserEntity { + user_id: vec![0x02], + user_name: Some("user2".to_string()), + user_display_name: Some("User Two".to_string()), + user_icon: Some("icon2".to_string()), + }; + make_credential_params.user = user2.clone(); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_uv_auth_param = Some(vec![ + 0x6F, 0x52, 0x83, 0xBF, 0x1A, 0x91, 0xEE, 0x67, 0xE9, 0xD4, 0x4C, 0x80, 0x08, 0x79, + 0x90, 0x8D, + ]); + let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], @@ -1673,20 +1729,20 @@ mod test { extensions: None, options: GetAssertionOptions { up: false, - uv: false, + uv: true, }, - pin_uv_auth_param: None, - pin_uv_auth_protocol: None, + pin_uv_auth_param, + pin_uv_auth_protocol: Some(1), }; let get_assertion_response = ctap_state.process_get_assertion( get_assertion_params, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); - check_assertion_response(get_assertion_response, vec![0x02], Some(2)); + check_assertion_response_with_user(get_assertion_response, user2, 0x04, Some(2)); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); - check_assertion_response(get_assertion_response, vec![0x01], None); + check_assertion_response_with_user(get_assertion_response, user1, 0x04, None); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); assert_eq!( @@ -1696,23 +1752,32 @@ mod test { } #[test] - fn test_process_get_next_assertion_three_credentials() { + fn test_process_get_next_assertion_three_credentials_no_uv() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x01]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x02]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x03]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); @@ -1827,10 +1892,12 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 564ca6d..44aad71 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -631,6 +631,22 @@ impl PinProtocolV1 { } Ok(()) } + + #[cfg(test)] + pub fn new_test( + key_agreement_key: crypto::ecdh::SecKey, + pin_uv_auth_token: [u8; 32], + ) -> PinProtocolV1 { + PinProtocolV1 { + key_agreement_key, + pin_uv_auth_token, + consecutive_pin_mismatches: 0, + #[cfg(feature = "with_ctap2_1")] + permissions: 0xFF, + #[cfg(feature = "with_ctap2_1")] + permissions_rp_id: None, + } + } } #[cfg(test)] diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 7952d75..0e4941a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -719,10 +719,12 @@ mod test { private_key, rp_id: String::from(rp_id), user_handle, - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, } } @@ -896,12 +898,14 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, + 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()); @@ -940,10 +944,12 @@ mod test { private_key: key0, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, + 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)); } @@ -960,10 +966,12 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, + 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()); @@ -1138,10 +1146,12 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, + 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(); From ed5a9e5b24f6d1d629cd6a575f29e4d0b5c24ddb Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Sat, 28 Nov 2020 19:01:16 +0100 Subject: [PATCH 030/192] Apply review comments --- libraries/persistent_store/fuzz/src/store.rs | 27 ++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs index e51ed71..a008a4e 100644 --- a/libraries/persistent_store/fuzz/src/store.rs +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -32,8 +32,6 @@ use std::convert::TryInto; /// information is printed if `debug` is set. Statistics are gathered if `stats` is set. pub fn fuzz(data: &[u8], debug: bool, stats: Option<&mut Stats>) { let mut fuzzer = Fuzzer::new(data, debug, stats); - fuzzer.init_counters(); - fuzzer.record(StatKey::Entropy, data.len()); let mut driver = fuzzer.init(); let store = loop { if fuzzer.debug { @@ -115,14 +113,17 @@ impl<'a> Fuzzer<'a> { let mut entropy = Entropy::new(data); let seed = entropy.read_slice(16); let values = Pcg32::from_seed(seed[..].try_into().unwrap()); - Fuzzer { + let mut fuzzer = Fuzzer { entropy, values, init: Init::Clean, debug, stats, counters: HashMap::new(), - } + }; + fuzzer.init_counters(); + fuzzer.record(StatKey::Entropy, data.len()); + fuzzer } /// Initializes the fuzzing state and returns the store driver. @@ -395,7 +396,7 @@ impl<'a> Fuzzer<'a> { enum Init { /// Fuzzing starts from a clean storage. /// - /// All invariant are checked. + /// All invariants are checked. Clean, /// Fuzzing starts from a dirty storage. @@ -405,7 +406,7 @@ enum Init { /// Fuzzing starts from a simulated old storage. /// - /// All invariant are checked. + /// All invariants are checked. Used { /// Number of simulated used cycles. cycle: usize, @@ -437,9 +438,13 @@ impl Init { #[derive(Default, Clone, Debug)] struct BitStack { /// Bits stored in little-endian (for bytes and bits). + /// + /// The last byte only contains `len` bits. data: Vec, - /// Number of bits stored. + /// Number of bits stored in the last byte. + /// + /// It is 0 if the last byte is full, not 8. len: usize, } @@ -526,8 +531,14 @@ fn bit_stack_ok() { assert_eq!(bits.pop(), Some(false)); assert_eq!(bits.pop(), None); - for i in 0..27 { + let n = 27; + for i in 0..n { assert_eq!(bits.len(), i); bits.push(true); } + for i in (0..n).rev() { + assert_eq!(bits.pop(), Some(true)); + assert_eq!(bits.len(), i); + } + assert_eq!(bits.pop(), None); } From f548a35f011de034785a5bd68c8423eee236362b Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 30 Nov 2020 10:29:18 +0100 Subject: [PATCH 031/192] Do not crash with dirty init --- libraries/persistent_store/fuzz/src/store.rs | 4 +-- libraries/persistent_store/src/buffer.rs | 31 +++++++++++++------- libraries/persistent_store/src/store.rs | 2 +- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs index a008a4e..c18eb51 100644 --- a/libraries/persistent_store/fuzz/src/store.rs +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -133,7 +133,7 @@ impl<'a> Fuzzer<'a> { page_size: 1 << self.entropy.read_range(5, 12), max_word_writes: 2, max_page_erases: self.entropy.read_range(0, 50000), - strict_write: true, + strict_mode: true, }; let num_pages = self.entropy.read_range(3, 64); self.record(StatKey::PageSize, options.page_size); @@ -156,7 +156,7 @@ impl<'a> Fuzzer<'a> { if self.debug { println!("Start with dirty storage."); } - options.strict_write = false; + options.strict_mode = false; let storage = BufferStorage::new(storage, options); StoreDriver::Off(StoreDriverOff::new_dirty(storage)) } else if self.entropy.read_bit() { diff --git a/libraries/persistent_store/src/buffer.rs b/libraries/persistent_store/src/buffer.rs index 0059826..ae35089 100644 --- a/libraries/persistent_store/src/buffer.rs +++ b/libraries/persistent_store/src/buffer.rs @@ -23,9 +23,9 @@ use alloc::vec; /// for tests and fuzzing, for which it has dedicated functionalities. /// /// This storage tracks how many times words are written between page erase cycles, how many times -/// pages are erased, and whether an operation flips bits in the wrong direction (optional). -/// Operations panic if those conditions are broken. This storage also permits to interrupt -/// operations for inspection or to corrupt the operation. +/// pages are erased, and whether an operation flips bits in the wrong direction. Operations panic +/// if those conditions are broken (optional). This storage also permits to interrupt operations for +/// inspection or to corrupt the operation. #[derive(Clone)] pub struct BufferStorage { /// Content of the storage. @@ -59,8 +59,13 @@ pub struct BufferOptions { /// How many times a page can be erased. pub max_page_erases: usize, - /// Whether bits cannot be written from 0 to 1. - pub strict_write: bool, + /// Whether the storage should check the flash invariant. + /// + /// When set, the following conditions would panic: + /// - A bit is written from 0 to 1. + /// - A word is written more than `max_word_writes`. + /// - A page is erased more than `max_page_erases`. + pub strict_mode: bool, } /// Corrupts a slice given actual and expected value. @@ -214,7 +219,10 @@ impl BufferStorage { /// /// Panics if the maximum number of erase cycles per page is reached. fn incr_page_erases(&mut self, page: usize) { - assert!(self.page_erases[page] < self.max_page_erases()); + // Check that pages are not erased too many times. + if self.options.strict_mode { + assert!(self.page_erases[page] < self.max_page_erases()); + } self.page_erases[page] += 1; let num_words = self.page_size() / self.word_size(); for word in 0..num_words { @@ -252,7 +260,10 @@ impl BufferStorage { continue; } let word = index / word_size + i; - assert!(self.word_writes[word] < self.max_word_writes()); + // Check that words are not written too many times. + if self.options.strict_mode { + assert!(self.word_writes[word] < self.max_word_writes()); + } self.word_writes[word] += 1; } } @@ -306,8 +317,8 @@ impl Storage for BufferStorage { self.interruption.tick(&operation)?; // Check and update counters. self.incr_word_writes(range.start, value, value); - // Check strict write. - if self.options.strict_write { + // Check that bits are correctly flipped. + if self.options.strict_mode { for (byte, &val) in range.clone().zip(value.iter()) { assert_eq!(self.storage[byte] & val, val); } @@ -472,7 +483,7 @@ mod tests { page_size: 16, max_word_writes: 2, max_page_erases: 3, - strict_write: true, + strict_mode: true, }; // Those words are decreasing bit patterns. Bits are only changed from 1 to 0 and at least one // bit is changed. diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index a3e084b..2559485 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -1257,7 +1257,7 @@ mod tests { page_size: self.page_size, max_word_writes: self.max_word_writes, max_page_erases: self.max_page_erases, - strict_write: true, + strict_mode: true, }; StoreDriverOff::new(options, self.num_pages) } From 5f5f72b6d13f31da3a54569f75d16e85f134451d Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 30 Nov 2020 02:04:52 -0800 Subject: [PATCH 032/192] Use arrayref for converting into ApduHeader --- src/ctap/apdu.rs | 6 +++--- src/lib.rs | 4 ++++ src/main.rs | 3 +++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 431b18e..fd03f2a 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -58,8 +58,8 @@ pub struct ApduHeader { p2: u8, } -impl From<&[u8]> for ApduHeader { - fn from(header: &[u8]) -> Self { +impl From<&[u8; 4]> for ApduHeader { + fn from(header: &[u8; 4]) -> Self { ApduHeader { cla: header[0], ins: header[1], @@ -120,7 +120,7 @@ impl TryFrom<&[u8]> for APDU { let (header, payload) = frame.split_at(APDU_HEADER_LEN); let mut apdu = APDU { - header: header.into(), + header: array_ref!(header, 0, 4).into(), lc: 0x00, data: Vec::new(), le: 0x00, diff --git a/src/lib.rs b/src/lib.rs index d6260c7..07fd02e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,3 +18,7 @@ extern crate alloc; pub mod ctap; pub mod embedded_flash; + +#[macro_use] +extern crate arrayref; + diff --git a/src/main.rs b/src/main.rs index 51c8305..ca10c3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,9 @@ extern crate alloc; extern crate core; extern crate lang_items; +#[macro_use] +extern crate arrayref; + mod ctap; pub mod embedded_flash; From a0e3048f827b1e80e673ad959eb63f437a69f38e Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 30 Nov 2020 11:30:49 +0100 Subject: [PATCH 033/192] Add debug helper for fuzzing --- .../persistent_store/fuzz/examples/store.rs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 libraries/persistent_store/fuzz/examples/store.rs diff --git a/libraries/persistent_store/fuzz/examples/store.rs b/libraries/persistent_store/fuzz/examples/store.rs new file mode 100644 index 0000000..be9240b --- /dev/null +++ b/libraries/persistent_store/fuzz/examples/store.rs @@ -0,0 +1,116 @@ +// Copyright 2019-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. +// 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 fuzz_store::{fuzz, StatKey, Stats}; +use std::io::Write; +use std::io::{stdout, Read}; +use std::path::Path; + +fn usage(program: &str) { + println!( + r#"Usage: {} {{ [] | .. }} + +If is not provided, it is read from standard input. + +When .. are provided, only runs matching all predicates are shown. The format of +each is =."#, + program + ); +} + +fn debug(data: &[u8]) { + println!("{:02x?}", data); + fuzz(data, true, None); +} + +/// Bucket predicate. +struct Predicate { + /// Bucket key. + key: StatKey, + + /// Bucket value. + value: usize, +} + +impl std::str::FromStr for Predicate { + type Err = String; + + fn from_str(input: &str) -> Result { + let predicate: Vec<&str> = input.split('=').collect(); + if predicate.len() != 2 { + return Err("Predicate should have exactly one equal sign.".to_string()); + } + let key = predicate[0] + .parse() + .map_err(|_| format!("Predicate key `{}` is not recognized.", predicate[0]))?; + let value: usize = predicate[1] + .parse() + .map_err(|_| format!("Predicate value `{}` is not a number.", predicate[1]))?; + if value != 0 && !value.is_power_of_two() { + return Err(format!( + "Predicate value `{}` is not a bucket.", + predicate[1] + )); + } + Ok(Predicate { key, value }) + } +} + +fn analyze(corpus: &Path, predicates: Vec) { + let mut stats = Stats::default(); + let mut count = 0; + let total = std::fs::read_dir(corpus).unwrap().count(); + for entry in std::fs::read_dir(corpus).unwrap() { + let data = std::fs::read(entry.unwrap().path()).unwrap(); + let mut stat = Stats::default(); + fuzz(&data, false, Some(&mut stat)); + if predicates + .iter() + .all(|p| stat.get_count(p.key, p.value).is_some()) + { + stats.merge(&stat); + } + count += 1; + print!("\u{1b}[K{} / {}\r", count, total); + stdout().flush().unwrap(); + } + // NOTE: To avoid reloading the corpus each time we want to check a different filter, we can + // start an interactive loop here taking filters as input and printing the filtered stats. We + // would keep all individual stats for each run in a vector. + print!("{}", stats); +} + +fn main() { + let args: Vec = std::env::args().collect(); + // No arguments reads from stdin. + if args.len() <= 1 { + let stdin = std::io::stdin(); + let mut data = Vec::new(); + stdin.lock().read_to_end(&mut data).unwrap(); + return debug(&data); + } + let path = Path::new(&args[1]); + // File argument assumes artifact. + if path.is_file() && args.len() == 2 { + return debug(&std::fs::read(path).unwrap()); + } + // Directory argument assumes corpus. + if path.is_dir() { + match args[2..].iter().map(|x| x.parse()).collect() { + Ok(predicates) => return analyze(path, predicates), + Err(error) => eprintln!("Error: {}", error), + } + } + usage(&args[0]); +} From f8a6fb35e2bf178e755106e5b4e00a72ab7d56d0 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 30 Nov 2020 08:46:02 -0800 Subject: [PATCH 034/192] Ignore dirty submodules --- .gitmodules | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitmodules b/.gitmodules index b70a516..273601b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,8 @@ [submodule "third_party/libtock-rs"] path = third_party/libtock-rs url = https://github.com/tock/libtock-rs + ignore = dirty [submodule "third_party/tock"] path = third_party/tock url = https://github.com/tock/tock + ignore = dirty From 94f548d5c53195c08df5ec59ce26867060050861 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 30 Nov 2020 14:35:01 -0800 Subject: [PATCH 035/192] Add extended APDU parser --- src/ctap/apdu.rs | 143 +++++++++++++++++++++++++++++++++++++++++------ src/main.rs | 2 +- 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index fd03f2a..a761564 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -1,4 +1,5 @@ use alloc::vec::Vec; +use byteorder::{BigEndian, ByteOrder}; use core::convert::TryFrom; type ByteArray = &'static [u8]; @@ -73,11 +74,14 @@ impl From<&[u8; 4]> for ApduHeader { #[derive(PartialEq)] /// The APDU cases pub enum Case { - Le, - LcData, - LcDataLe, - // TODO: More cases to add as extended length APDUs - // Le could be 2 or 3 Bytes + Le1, + Lc1Data, + Lc1DataLe1, + Lc3Data, + Lc3DataLe1, + Lc3DataLe2, + Lc3DataLe3, + Unknown, } #[cfg_attr(test, derive(Clone, Debug))] @@ -127,13 +131,16 @@ impl TryFrom<&[u8]> for APDU { case_type: ApduType::default(), }; - // case 1 if payload.is_empty() { + // This branch is for Lc = 0 apdu.case_type = ApduType::Instruction; } else { + // Lc is not equal to zero, let's figure out how long it is let byte_0 = payload[0]; if payload.len() == 1 { - apdu.case_type = ApduType::Short(Case::Le); + // There is only one byte in the payload, that byte cannot be Lc because that would + // entail at *least* one another byte in the payload (for the command data) + apdu.case_type = ApduType::Short(Case::Le1); apdu.le = if byte_0 == 0x00 { // Ne = 256 0x100 @@ -142,12 +149,16 @@ impl TryFrom<&[u8]> for APDU { } } if payload.len() == (1 + byte_0) as usize && byte_0 != 0 { - apdu.case_type = ApduType::Short(Case::LcData); + // Lc is one-byte long and since the size specified by Lc covers the rest of the + // payload there's no Le at the end + apdu.case_type = ApduType::Short(Case::Lc1Data); apdu.lc = byte_0.into(); apdu.data = payload[1..].to_vec(); } if payload.len() == (1 + byte_0 + 1) as usize && byte_0 != 0 { - apdu.case_type = ApduType::Short(Case::LcDataLe); + // Lc is one-byte long and since the size specified by Lc covers the rest of the + // payload with ONE additional byte that byte must be Le + apdu.case_type = ApduType::Short(Case::Lc1DataLe1); apdu.lc = byte_0.into(); apdu.data = payload[1..(payload.len() - 1)].to_vec(); apdu.le = (*payload.last().unwrap()).into(); @@ -155,8 +166,38 @@ impl TryFrom<&[u8]> for APDU { apdu.le = 0x100; } } + if payload.len() > 2 { + // Lc is possibly three-bytes long + let extended_apdu_lc: usize = BigEndian::read_u16(&payload[1..]) as usize; + let extended_apdu_le_len: usize = if payload.len() > extended_apdu_lc { + payload.len() - extended_apdu_lc - 3 + } else { + 0 + }; + if byte_0 == 0 && extended_apdu_le_len <= 3 { + // If first byte is zero AND the next two bytes can be parsed as a big-endian + // length that covers the rest of the block (plus few additional bytes for Le), we + // have an extended-length APDU + apdu.case_type = ApduType::Extended(match extended_apdu_le_len { + 0 => Case::Lc3Data, + 1 => Case::Lc3DataLe1, + 2 => Case::Lc3DataLe2, + 3 => Case::Lc3DataLe3, + _ => Case::Unknown, + }); + apdu.data = payload[3..(payload.len() - extended_apdu_le_len)].to_vec(); + apdu.lc = extended_apdu_lc as u16; + apdu.le = match extended_apdu_le_len { + 0 => 0, + 1 => (*payload.last().unwrap()).into(), + 2 => BigEndian::read_u16(&payload[payload.len() - extended_apdu_le_len..]) + as u32, + 3 => BigEndian::read_u32(&payload[payload.len() - extended_apdu_le_len..]), + _ => 0, + } + } + } } - // TODO: Add extended length cases if apdu.case_type == ApduType::default() { return Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED); } @@ -206,7 +247,7 @@ mod test { lc: 0x00, data: Vec::new(), le: 0x0f, - case_type: ApduType::Short(Case::Le), + case_type: ApduType::Short(Case::Le1), }; assert_eq!(Ok(expected), response); } @@ -225,7 +266,7 @@ mod test { lc: 0x00, data: Vec::new(), le: 0x100, - case_type: ApduType::Short(Case::Le), + case_type: ApduType::Short(Case::Le1), }; assert_eq!(Ok(expected), response); } @@ -245,7 +286,7 @@ mod test { lc: 0x02, data: payload.to_vec(), le: 0x00, - case_type: ApduType::Short(Case::LcData), + case_type: ApduType::Short(Case::Lc1Data), }; assert_eq!(Ok(expected), response); } @@ -267,7 +308,7 @@ mod test { lc: 0x07, data: payload.to_vec(), le: 0xff, - case_type: ApduType::Short(Case::LcDataLe), + case_type: ApduType::Short(Case::Lc1DataLe1), }; assert_eq!(Ok(expected), response); } @@ -289,7 +330,7 @@ mod test { lc: 0x07, data: payload.to_vec(), le: 0x100, - case_type: ApduType::Short(Case::LcDataLe), + case_type: ApduType::Short(Case::Lc1DataLe1), }; assert_eq!(Ok(expected), response); } @@ -302,7 +343,56 @@ mod test { } #[test] - fn test_unsupported_case_type() { + fn test_extended_length_apdu() { + let frame: [u8; 186] = [ + 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0xb1, 0x60, 0xc5, 0xb3, 0x42, 0x58, 0x6b, 0x49, + 0xdb, 0x3e, 0x72, 0xd8, 0x24, 0x4b, 0xa5, 0x6c, 0x8d, 0x79, 0x2b, 0x65, 0x08, 0xe8, + 0xda, 0x9b, 0x0e, 0x2b, 0xc1, 0x63, 0x0d, 0xbc, 0xf3, 0x6d, 0x66, 0xa5, 0x46, 0x72, + 0xb2, 0x22, 0xc4, 0xcf, 0x95, 0xe1, 0x51, 0xed, 0x8d, 0x4d, 0x3c, 0x76, 0x7a, 0x6c, + 0xc3, 0x49, 0x43, 0x59, 0x43, 0x79, 0x4e, 0x88, 0x4f, 0x3d, 0x02, 0x3a, 0x82, 0x29, + 0xfd, 0x70, 0x3f, 0x8b, 0xd4, 0xff, 0xe0, 0xa8, 0x93, 0xdf, 0x1a, 0x58, 0x34, 0x16, + 0xb0, 0x1b, 0x8e, 0xbc, 0xf0, 0x2d, 0xc9, 0x99, 0x8d, 0x6f, 0xe4, 0x8a, 0xb2, 0x70, + 0x9a, 0x70, 0x3a, 0x27, 0x71, 0x88, 0x3c, 0x75, 0x30, 0x16, 0xfb, 0x02, 0x11, 0x4d, + 0x30, 0x54, 0x6c, 0x4e, 0x8c, 0x76, 0xb2, 0xf0, 0xa8, 0x4e, 0xd6, 0x90, 0xe4, 0x40, + 0x25, 0x6a, 0xdd, 0x64, 0x63, 0x3e, 0x83, 0x4f, 0x8b, 0x25, 0xcf, 0x88, 0x68, 0x80, + 0x01, 0x07, 0xdb, 0xc8, 0x64, 0xf7, 0xca, 0x4f, 0xd1, 0xc7, 0x95, 0x7c, 0xe8, 0x45, + 0xbc, 0xda, 0xd4, 0xef, 0x45, 0x63, 0x5a, 0x7a, 0x65, 0x3f, 0xaa, 0x22, 0x67, 0xe7, + 0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb, + 0xb6, 0x2b, 0x00, 0x00, + ]; + let payload = [ + 0x60, 0xc5, 0xb3, 0x42, 0x58, 0x6b, 0x49, 0xdb, 0x3e, 0x72, 0xd8, 0x24, 0x4b, 0xa5, + 0x6c, 0x8d, 0x79, 0x2b, 0x65, 0x08, 0xe8, 0xda, 0x9b, 0x0e, 0x2b, 0xc1, 0x63, 0x0d, + 0xbc, 0xf3, 0x6d, 0x66, 0xa5, 0x46, 0x72, 0xb2, 0x22, 0xc4, 0xcf, 0x95, 0xe1, 0x51, + 0xed, 0x8d, 0x4d, 0x3c, 0x76, 0x7a, 0x6c, 0xc3, 0x49, 0x43, 0x59, 0x43, 0x79, 0x4e, + 0x88, 0x4f, 0x3d, 0x02, 0x3a, 0x82, 0x29, 0xfd, 0x70, 0x3f, 0x8b, 0xd4, 0xff, 0xe0, + 0xa8, 0x93, 0xdf, 0x1a, 0x58, 0x34, 0x16, 0xb0, 0x1b, 0x8e, 0xbc, 0xf0, 0x2d, 0xc9, + 0x99, 0x8d, 0x6f, 0xe4, 0x8a, 0xb2, 0x70, 0x9a, 0x70, 0x3a, 0x27, 0x71, 0x88, 0x3c, + 0x75, 0x30, 0x16, 0xfb, 0x02, 0x11, 0x4d, 0x30, 0x54, 0x6c, 0x4e, 0x8c, 0x76, 0xb2, + 0xf0, 0xa8, 0x4e, 0xd6, 0x90, 0xe4, 0x40, 0x25, 0x6a, 0xdd, 0x64, 0x63, 0x3e, 0x83, + 0x4f, 0x8b, 0x25, 0xcf, 0x88, 0x68, 0x80, 0x01, 0x07, 0xdb, 0xc8, 0x64, 0xf7, 0xca, + 0x4f, 0xd1, 0xc7, 0x95, 0x7c, 0xe8, 0x45, 0xbc, 0xda, 0xd4, 0xef, 0x45, 0x63, 0x5a, + 0x7a, 0x65, 0x3f, 0xaa, 0x22, 0x67, 0xe7, 0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, + 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb, 0xb6, 0x2b, + ]; + let response = pass_frame(&frame); + let expected = APDU { + header: ApduHeader { + cla: 0x00, + ins: 0x02, + p1: 0x03, + p2: 0x00, + }, + lc: 0xb1, + data: payload.to_vec(), + le: 0x00, + case_type: ApduType::Extended(Case::Lc3DataLe2), + }; + assert_eq!(Ok(expected), response); + } + + #[test] + fn test_previously_unsupported_case_type() { let frame: [u8; 73] = [ 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, 0x40, 0xe3, 0x8f, 0xde, 0x51, 0x3d, 0xac, 0x9d, 0x1c, 0x6e, 0x86, 0x76, 0x31, 0x40, 0x25, 0x96, 0x86, 0x4d, 0x29, 0xe8, 0x07, 0xb3, @@ -311,7 +401,26 @@ mod test { 0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d, 0xcb, 0x00, 0x00, ]; + let payload = [ + 0xe3, 0x8f, 0xde, 0x51, 0x3d, 0xac, 0x9d, 0x1c, 0x6e, 0x86, 0x76, 0x31, 0x40, 0x25, + 0x96, 0x86, 0x4d, 0x29, 0xe8, 0x07, 0xb3, 0x56, 0x19, 0xdf, 0x4a, 0x00, 0x02, 0xae, + 0x2a, 0x8c, 0x9d, 0x5a, 0xab, 0xc3, 0x4b, 0x4e, 0xb9, 0x78, 0xb9, 0x11, 0xe5, 0x52, + 0x40, 0xf3, 0x45, 0x64, 0x9c, 0xd3, 0xd7, 0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, + 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d, 0xcb, + ]; let response = pass_frame(&frame); - assert_eq!(Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED), response); + let expected = APDU { + header: ApduHeader { + cla: 0x00, + ins: 0x01, + p1: 0x03, + p2: 0x00, + }, + lc: 0x40, + data: payload.to_vec(), + le: 0x00, + case_type: ApduType::Extended(Case::Lc3DataLe2), + }; + assert_eq!(Ok(expected), response); } } diff --git a/src/main.rs b/src/main.rs index ca10c3a..2ca12a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,9 +18,9 @@ extern crate alloc; #[cfg(feature = "std")] extern crate core; extern crate lang_items; - #[macro_use] extern crate arrayref; +extern crate byteorder; mod ctap; pub mod embedded_flash; From ce46af0b6b9897bfd5287f98033d3efbffc449f3 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 30 Nov 2020 14:43:44 -0800 Subject: [PATCH 036/192] Make cargo fmt happy --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 07fd02e..a5c4cba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,4 +21,3 @@ pub mod embedded_flash; #[macro_use] extern crate arrayref; - From 1db73c699be15102323d82a9cc9f26c3d050b8d2 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 1 Dec 2020 11:29:52 +0100 Subject: [PATCH 037/192] Apply review comments --- src/ctap/status_code.rs | 2 +- src/ctap/storage.rs | 29 +++++++++++++++-------------- src/ctap/storage/key.rs | 5 ++++- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index b4f78fd..638caef 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -85,7 +85,7 @@ pub enum Ctap2StatusCode { /// /// There can be multiple reasons: /// - The persistent storage has not been erased before its first usage. - /// - The persistent storage has been tempered by a third party. + /// - The persistent storage has been tempered with by a third party. /// - The flash is malfunctioning (including the Tock driver). /// /// In the first 2 cases the persistent storage should be completely erased. If the error diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 158bc4f..054d3c8 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -122,7 +122,7 @@ impl PersistentStore { page_size: PAGE_SIZE, max_word_writes: 2, max_page_erases: 10000, - strict_write: true, + strict_mode: true, }; Storage::new(store, options) } @@ -180,9 +180,8 @@ impl PersistentStore { ) -> Result, Ctap2StatusCode> { let mut iter_result = Ok(()); let iter = self.iter_credentials(&mut iter_result)?; - // TODO(reviewer): Should we return an error if we find more than one matching credential? - // We did not use to in the previous version (panic in debug mode, nothing in release mode) - // but I don't remember why. Let's document it. + // We don't check whether there is more than one matching credential to be able to exit + // early. let result = iter.map(|(_, credential)| credential).find(|credential| { credential.rp_id == rp_id && credential.credential_id == credential_id }); @@ -388,7 +387,7 @@ impl PersistentStore { Ok(self.store.insert(key::MIN_PIN_LENGTH, &[min_pin_length])?) } - /// TODO: Help from reviewer needed for documentation. + /// Returns a list of RP IDs that used to check if reading the minimum PIN length is allowed. #[cfg(feature = "with_ctap2_1")] pub fn _min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { let rp_ids = self @@ -401,7 +400,7 @@ impl PersistentStore { Ok(rp_ids.unwrap_or(vec![])) } - /// TODO: Help from reviewer needed for documentation. + /// Set a list of RP IDs that used to check if reading the minimum PIN length is allowed. #[cfg(feature = "with_ctap2_1")] pub fn _set_min_pin_length_rp_ids( &mut self, @@ -508,20 +507,22 @@ impl PersistentStore { impl From for Ctap2StatusCode { fn from(error: persistent_store::StoreError) -> Ctap2StatusCode { - use persistent_store::StoreError::*; + use persistent_store::StoreError; match error { // This error is expected. The store is full. - NoCapacity => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, + StoreError::NoCapacity => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, // This error is expected. The flash is out of life. - NoLifetime => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, + StoreError::NoLifetime => Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL, // This error is expected if we don't satisfy the store preconditions. For example we // try to store a credential which is too long. - InvalidArgument => Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR, + StoreError::InvalidArgument => Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR, // This error is not expected. The storage has been tempered with. We could erase the // storage. - InvalidStorage => Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE, + StoreError::InvalidStorage => { + Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE + } // This error is not expected. The kernel is failing our syscalls. - StorageError => Ctap2StatusCode::CTAP1_ERR_OTHER, + StoreError::StorageError => Ctap2StatusCode::CTAP1_ERR_OTHER, } } } @@ -605,7 +606,7 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result } } -/// TODO: Help from reviewer needed for documentation. +/// Deserializes a list of RP IDs from storage representation. #[cfg(feature = "with_ctap2_1")] fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { let cbor = cbor::read(data).ok()?; @@ -617,7 +618,7 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { .ok() } -/// TODO: Help from reviewer needed for documentation. +/// Serializes a list of RP IDs to storage representation. #[cfg(feature = "with_ctap2_1")] fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 9796d5a..3e86d8b 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -82,10 +82,13 @@ make_partition! { /// board may configure `MAX_SUPPORTED_RESIDENTIAL_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; - /// TODO: Help from reviewer needed for documentation. + /// List of RP IDs allowed to read the minimum PIN length. + #[cfg(feature = "with_ctap2_1")] _MIN_PIN_LENGTH_RP_IDS = 2042; /// The minimum PIN length. + /// + /// If the entry is absent, the minimum PIN length is `DEFAULT_MIN_PIN_LENGTH`. #[cfg(feature = "with_ctap2_1")] MIN_PIN_LENGTH = 2043; From b55d4320439fe1c59dd88d81a5a946738817e3f5 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 1 Dec 2020 15:39:51 +0100 Subject: [PATCH 038/192] Apply review comments --- src/ctap/storage.rs | 5 +++-- src/ctap/storage/key.rs | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 054d3c8..0f1c73f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -387,7 +387,8 @@ impl PersistentStore { Ok(self.store.insert(key::MIN_PIN_LENGTH, &[min_pin_length])?) } - /// Returns a list of RP IDs that used to check if reading the minimum PIN length is allowed. + /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is + /// allowed. #[cfg(feature = "with_ctap2_1")] pub fn _min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { let rp_ids = self @@ -400,7 +401,7 @@ impl PersistentStore { Ok(rp_ids.unwrap_or(vec![])) } - /// Set a list of RP IDs that used to check if reading the minimum PIN length is allowed. + /// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed. #[cfg(feature = "with_ctap2_1")] pub fn _set_min_pin_length_rp_ids( &mut self, diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 3e86d8b..e37e470 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -104,8 +104,7 @@ make_partition! { /// The encryption and hmac keys. /// - /// This entry is always present. It is generated at startup if absent. This is not a persistent - /// key because its value should change after a CTAP reset. + /// This entry is always present. It is generated at startup if absent. MASTER_KEYS = 2046; /// The global signature counter. From 042108e3d9d753fb0d0820733021c5c493f9eb4c Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 1 Dec 2020 17:46:28 +0100 Subject: [PATCH 039/192] Reserve 700 additional keys for credential-related stuff --- src/ctap/storage/key.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index e37e470..6dab699 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -76,6 +76,12 @@ make_partition! { // - When adding a (non-persistent) key below this message, make sure its value is bigger or // equal than NUM_PERSISTENT_KEYS. + /// Reserved for future credential-related objects. + /// + /// In particular, additional credentials could be added there by reducing the lower bound of + /// the credential range below as well as the upper bound of this range in a similar manner. + _RESERVED_CREDENTIALS = 1000..1700; + /// The credentials. /// /// Depending on `MAX_SUPPORTED_RESIDENTIAL_KEYS`, only a prefix of those keys is used. Each From dc95310fc0e58418b546c2f9def1f70a06e515fa Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Tue, 1 Dec 2020 10:13:25 -0800 Subject: [PATCH 040/192] Clarify comments --- src/ctap/apdu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index a761564..dd95caa 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -132,10 +132,10 @@ impl TryFrom<&[u8]> for APDU { }; if payload.is_empty() { - // This branch is for Lc = 0 + // Lc is zero-bytes in length apdu.case_type = ApduType::Instruction; } else { - // Lc is not equal to zero, let's figure out how long it is + // Lc is not zero-bytes in length, let's figure out how long it is let byte_0 = payload[0]; if payload.len() == 1 { // There is only one byte in the payload, that byte cannot be Lc because that would From b9ffe7e4ce0664a0ebe4e5088efbbaa16a75e1a0 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 2 Dec 2020 23:02:07 -0800 Subject: [PATCH 041/192] Use constant instead of hardcoded integer --- src/ctap/apdu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index dd95caa..5783cd7 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -59,8 +59,8 @@ pub struct ApduHeader { p2: u8, } -impl From<&[u8; 4]> for ApduHeader { - fn from(header: &[u8; 4]) -> Self { +impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader { + fn from(header: &[u8; APDU_HEADER_LEN]) -> Self { ApduHeader { cla: header[0], ins: header[1], From 2c49718fee700d349e0f5df9d1079823eca5259b Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 2 Dec 2020 23:03:35 -0800 Subject: [PATCH 042/192] Lc3DataLe3 is not a valid case --- src/ctap/apdu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 5783cd7..6c2a03a 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -80,7 +80,7 @@ pub enum Case { Lc3Data, Lc3DataLe1, Lc3DataLe2, - Lc3DataLe3, + Le3, Unknown, } From 0420ad8de6ebb9157202dd5333d1b49e012ed79a Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 2 Dec 2020 23:06:24 -0800 Subject: [PATCH 043/192] Use constant for consistency --- src/ctap/apdu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 6c2a03a..d5210b1 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -124,7 +124,7 @@ impl TryFrom<&[u8]> for APDU { let (header, payload) = frame.split_at(APDU_HEADER_LEN); let mut apdu = APDU { - header: array_ref!(header, 0, 4).into(), + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), lc: 0x00, data: Vec::new(), le: 0x00, From 1d8c103d9b8180b93994e35ea9365c780e15803b Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 2 Dec 2020 23:29:11 -0800 Subject: [PATCH 044/192] Construct and return immutable instances of APDU instead of mutating one --- src/ctap/apdu.rs | 110 +++++++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 46 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index d5210b1..80be816 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -123,48 +123,56 @@ impl TryFrom<&[u8]> for APDU { // +-----+-----+----+----+ let (header, payload) = frame.split_at(APDU_HEADER_LEN); - let mut apdu = APDU { - header: array_ref!(header, 0, APDU_HEADER_LEN).into(), - lc: 0x00, - data: Vec::new(), - le: 0x00, - case_type: ApduType::default(), - }; - if payload.is_empty() { // Lc is zero-bytes in length - apdu.case_type = ApduType::Instruction; + return Ok(APDU { + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), + lc: 0x00, + data: Vec::new(), + le: 0x00, + case_type: ApduType::Instruction, + }); } else { // Lc is not zero-bytes in length, let's figure out how long it is let byte_0 = payload[0]; if payload.len() == 1 { // There is only one byte in the payload, that byte cannot be Lc because that would // entail at *least* one another byte in the payload (for the command data) - apdu.case_type = ApduType::Short(Case::Le1); - apdu.le = if byte_0 == 0x00 { - // Ne = 256 - 0x100 - } else { - byte_0.into() - } + return Ok(APDU { + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), + lc: 0x00, + data: Vec::new(), + le: if byte_0 == 0x00 { + // Ne = 256 + 0x100 + } else { + byte_0.into() + }, + case_type: ApduType::Short(Case::Le1), + }); } if payload.len() == (1 + byte_0) as usize && byte_0 != 0 { // Lc is one-byte long and since the size specified by Lc covers the rest of the // payload there's no Le at the end - apdu.case_type = ApduType::Short(Case::Lc1Data); - apdu.lc = byte_0.into(); - apdu.data = payload[1..].to_vec(); + return Ok(APDU { + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), + lc: byte_0.into(), + data: payload[1..].to_vec(), + case_type: ApduType::Short(Case::Lc1Data), + le: 0, + }); } if payload.len() == (1 + byte_0 + 1) as usize && byte_0 != 0 { // Lc is one-byte long and since the size specified by Lc covers the rest of the // payload with ONE additional byte that byte must be Le - apdu.case_type = ApduType::Short(Case::Lc1DataLe1); - apdu.lc = byte_0.into(); - apdu.data = payload[1..(payload.len() - 1)].to_vec(); - apdu.le = (*payload.last().unwrap()).into(); - if apdu.le == 0x00 { - apdu.le = 0x100; - } + let last_byte: u32 = (*payload.last().unwrap()).into(); + return Ok(APDU { + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), + lc: byte_0.into(), + data: payload[1..(payload.len() - 1)].to_vec(), + le: if last_byte == 0x00 { 0x100 } else { last_byte }, + case_type: ApduType::Short(Case::Lc1DataLe1), + }); } if payload.len() > 2 { // Lc is possibly three-bytes long @@ -178,30 +186,40 @@ impl TryFrom<&[u8]> for APDU { // If first byte is zero AND the next two bytes can be parsed as a big-endian // length that covers the rest of the block (plus few additional bytes for Le), we // have an extended-length APDU - apdu.case_type = ApduType::Extended(match extended_apdu_le_len { - 0 => Case::Lc3Data, - 1 => Case::Lc3DataLe1, - 2 => Case::Lc3DataLe2, - 3 => Case::Lc3DataLe3, - _ => Case::Unknown, + let last_byte: u32 = (*payload.last().unwrap()).into(); + return Ok(APDU { + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), + lc: extended_apdu_lc as u16, + data: payload[3..(payload.len() - extended_apdu_le_len)].to_vec(), + le: match extended_apdu_le_len { + 0 => 0, + 1 => { + if last_byte == 0x00 { + 0x100 + } else { + last_byte + } + } + 2 => BigEndian::read_u16( + &payload[payload.len() - extended_apdu_le_len..], + ) as u32, + 3 => BigEndian::read_u32( + &payload[payload.len() - extended_apdu_le_len..], + ), + _ => 0, + }, + case_type: ApduType::Extended(match extended_apdu_le_len { + 0 => Case::Lc3Data, + 1 => Case::Lc3DataLe1, + 2 => Case::Lc3DataLe2, + 3 => Case::Le3, + _ => Case::Unknown, + }), }); - apdu.data = payload[3..(payload.len() - extended_apdu_le_len)].to_vec(); - apdu.lc = extended_apdu_lc as u16; - apdu.le = match extended_apdu_le_len { - 0 => 0, - 1 => (*payload.last().unwrap()).into(), - 2 => BigEndian::read_u16(&payload[payload.len() - extended_apdu_le_len..]) - as u32, - 3 => BigEndian::read_u32(&payload[payload.len() - extended_apdu_le_len..]), - _ => 0, - } } } } - if apdu.case_type == ApduType::default() { - return Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED); - } - Ok(apdu) + return Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED); } } From 524ebe3fce0877dc019fb504d6c98aab31d751d3 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 2 Dec 2020 23:32:25 -0800 Subject: [PATCH 045/192] Prevent int overflow by casting before addition --- src/ctap/apdu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 80be816..8e53276 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -151,7 +151,7 @@ impl TryFrom<&[u8]> for APDU { case_type: ApduType::Short(Case::Le1), }); } - if payload.len() == (1 + byte_0) as usize && byte_0 != 0 { + if payload.len() == 1 + (byte_0 as usize) && byte_0 != 0 { // Lc is one-byte long and since the size specified by Lc covers the rest of the // payload there's no Le at the end return Ok(APDU { @@ -162,7 +162,7 @@ impl TryFrom<&[u8]> for APDU { le: 0, }); } - if payload.len() == (1 + byte_0 + 1) as usize && byte_0 != 0 { + if payload.len() == 2 + (byte_0 as usize) && byte_0 != 0 { // Lc is one-byte long and since the size specified by Lc covers the rest of the // payload with ONE additional byte that byte must be Le let last_byte: u32 = (*payload.last().unwrap()).into(); From 9fc1ac114d99bc7cc2b57c72103e042765b09fbe Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 2 Dec 2020 23:39:48 -0800 Subject: [PATCH 046/192] Reuse frame bytes for payload --- src/ctap/apdu.rs | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 8e53276..6d06dc5 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -378,21 +378,7 @@ mod test { 0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb, 0xb6, 0x2b, 0x00, 0x00, ]; - let payload = [ - 0x60, 0xc5, 0xb3, 0x42, 0x58, 0x6b, 0x49, 0xdb, 0x3e, 0x72, 0xd8, 0x24, 0x4b, 0xa5, - 0x6c, 0x8d, 0x79, 0x2b, 0x65, 0x08, 0xe8, 0xda, 0x9b, 0x0e, 0x2b, 0xc1, 0x63, 0x0d, - 0xbc, 0xf3, 0x6d, 0x66, 0xa5, 0x46, 0x72, 0xb2, 0x22, 0xc4, 0xcf, 0x95, 0xe1, 0x51, - 0xed, 0x8d, 0x4d, 0x3c, 0x76, 0x7a, 0x6c, 0xc3, 0x49, 0x43, 0x59, 0x43, 0x79, 0x4e, - 0x88, 0x4f, 0x3d, 0x02, 0x3a, 0x82, 0x29, 0xfd, 0x70, 0x3f, 0x8b, 0xd4, 0xff, 0xe0, - 0xa8, 0x93, 0xdf, 0x1a, 0x58, 0x34, 0x16, 0xb0, 0x1b, 0x8e, 0xbc, 0xf0, 0x2d, 0xc9, - 0x99, 0x8d, 0x6f, 0xe4, 0x8a, 0xb2, 0x70, 0x9a, 0x70, 0x3a, 0x27, 0x71, 0x88, 0x3c, - 0x75, 0x30, 0x16, 0xfb, 0x02, 0x11, 0x4d, 0x30, 0x54, 0x6c, 0x4e, 0x8c, 0x76, 0xb2, - 0xf0, 0xa8, 0x4e, 0xd6, 0x90, 0xe4, 0x40, 0x25, 0x6a, 0xdd, 0x64, 0x63, 0x3e, 0x83, - 0x4f, 0x8b, 0x25, 0xcf, 0x88, 0x68, 0x80, 0x01, 0x07, 0xdb, 0xc8, 0x64, 0xf7, 0xca, - 0x4f, 0xd1, 0xc7, 0x95, 0x7c, 0xe8, 0x45, 0xbc, 0xda, 0xd4, 0xef, 0x45, 0x63, 0x5a, - 0x7a, 0x65, 0x3f, 0xaa, 0x22, 0x67, 0xe7, 0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, - 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb, 0xb6, 0x2b, - ]; + let payload = array_ref!(frame, 7, 186 - 7 - 2); let response = pass_frame(&frame); let expected = APDU { header: ApduHeader { @@ -419,13 +405,7 @@ mod test { 0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d, 0xcb, 0x00, 0x00, ]; - let payload = [ - 0xe3, 0x8f, 0xde, 0x51, 0x3d, 0xac, 0x9d, 0x1c, 0x6e, 0x86, 0x76, 0x31, 0x40, 0x25, - 0x96, 0x86, 0x4d, 0x29, 0xe8, 0x07, 0xb3, 0x56, 0x19, 0xdf, 0x4a, 0x00, 0x02, 0xae, - 0x2a, 0x8c, 0x9d, 0x5a, 0xab, 0xc3, 0x4b, 0x4e, 0xb9, 0x78, 0xb9, 0x11, 0xe5, 0x52, - 0x40, 0xf3, 0x45, 0x64, 0x9c, 0xd3, 0xd7, 0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, - 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d, 0xcb, - ]; + let payload = array_ref!(frame, 7, 73 - 7 - 2); let response = pass_frame(&frame); let expected = APDU { header: ApduHeader { From 943d7af5039656acb551df7af0172344e1791802 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 2 Dec 2020 23:43:35 -0800 Subject: [PATCH 047/192] Payload does not need to be an array --- src/ctap/apdu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 6d06dc5..8cff770 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -378,7 +378,7 @@ mod test { 0x8a, 0xf2, 0x5f, 0xe8, 0x59, 0x2e, 0x0b, 0xc6, 0x85, 0xc6, 0xf7, 0x0e, 0x9e, 0xdb, 0xb6, 0x2b, 0x00, 0x00, ]; - let payload = array_ref!(frame, 7, 186 - 7 - 2); + let payload: &[u8] = &frame[7..frame.len() - 2]; let response = pass_frame(&frame); let expected = APDU { header: ApduHeader { @@ -405,7 +405,7 @@ mod test { 0xe8, 0xb5, 0x83, 0xfb, 0xe0, 0x66, 0x98, 0x4d, 0x98, 0x81, 0xf7, 0xb5, 0x49, 0x4d, 0xcb, 0x00, 0x00, ]; - let payload = array_ref!(frame, 7, 73 - 7 - 2); + let payload: &[u8] = &frame[7..frame.len() - 2]; let response = pass_frame(&frame); let expected = APDU { header: ApduHeader { From 71ec2cf937260c075222960405f478108cbe0106 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 3 Dec 2020 07:50:05 -0800 Subject: [PATCH 048/192] Return an error when the case isn't determined --- src/ctap/apdu.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 8cff770..07cf136 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -81,7 +81,6 @@ pub enum Case { Lc3DataLe1, Lc3DataLe2, Le3, - Unknown, } #[cfg_attr(test, derive(Clone, Debug))] @@ -213,7 +212,7 @@ impl TryFrom<&[u8]> for APDU { 1 => Case::Lc3DataLe1, 2 => Case::Lc3DataLe2, 3 => Case::Le3, - _ => Case::Unknown, + _ => return Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED), }), }); } From 69cdd4a0dc3c1bb6967afc89c752c966ce6b8d99 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 3 Dec 2020 07:53:22 -0800 Subject: [PATCH 049/192] Use (relatively more) appropriate error code) --- src/ctap/apdu.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 07cf136..db4653f 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -205,20 +205,20 @@ impl TryFrom<&[u8]> for APDU { 3 => BigEndian::read_u32( &payload[payload.len() - extended_apdu_le_len..], ), - _ => 0, + _ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION), }, case_type: ApduType::Extended(match extended_apdu_le_len { 0 => Case::Lc3Data, 1 => Case::Lc3DataLe1, 2 => Case::Lc3DataLe2, 3 => Case::Le3, - _ => return Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED), + _ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION), }), }); } } } - return Err(ApduStatusCode::SW_COND_USE_NOT_SATISFIED); + return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION); } } From cc8bdb982d327275abc73a5c80dcfa84b22220f8 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 3 Dec 2020 07:55:34 -0800 Subject: [PATCH 050/192] Remove unknown apdu type --- src/ctap/apdu.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index db4653f..0afe6b7 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -90,18 +90,11 @@ pub enum ApduType { Instruction, Short(Case), Extended(Case), - Unknown, -} - -impl Default for ApduType { - fn default() -> ApduType { - ApduType::Unknown - } } #[cfg_attr(test, derive(Clone, Debug))] #[allow(dead_code)] -#[derive(Default, PartialEq)] +#[derive(PartialEq)] pub struct APDU { header: ApduHeader, lc: u16, From bec94f02be79c4ae520ef97acc7b5899d352cf14 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 3 Dec 2020 08:10:44 -0800 Subject: [PATCH 051/192] Tweak Le appropriately depending on its swize --- src/ctap/apdu.rs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 0afe6b7..17677c9 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -192,12 +192,27 @@ impl TryFrom<&[u8]> for APDU { last_byte } } - 2 => BigEndian::read_u16( - &payload[payload.len() - extended_apdu_le_len..], - ) as u32, - 3 => BigEndian::read_u32( - &payload[payload.len() - extended_apdu_le_len..], - ), + 2 => { + let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]); + if le_parsed == 0x00 { + 0x100 + } else { + le_parsed as u32 + } + } + 3 => { + let le_first_byte: u32 = + (*payload.get(payload.len() - 3).unwrap()).into(); + if le_first_byte != 0x00 { + return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION); + } + let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]); + if le_parsed == 0x00 { + 0x10000 + } else { + le_parsed as u32 + } + } _ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION), }, case_type: ApduType::Extended(match extended_apdu_le_len { @@ -381,7 +396,7 @@ mod test { }, lc: 0xb1, data: payload.to_vec(), - le: 0x00, + le: 0x100, case_type: ApduType::Extended(Case::Lc3DataLe2), }; assert_eq!(Ok(expected), response); @@ -408,7 +423,7 @@ mod test { }, lc: 0x40, data: payload.to_vec(), - le: 0x00, + le: 0x100, case_type: ApduType::Extended(Case::Lc3DataLe2), }; assert_eq!(Ok(expected), response); From 4bfce88e9b056d3ba2e79c081607dbf9fde00ca6 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 3 Dec 2020 08:14:07 -0800 Subject: [PATCH 052/192] Remove indention level made redundant by early-return --- src/ctap/apdu.rs | 192 +++++++++++++++++++++++------------------------ 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 17677c9..6a94790 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -124,108 +124,108 @@ impl TryFrom<&[u8]> for APDU { le: 0x00, case_type: ApduType::Instruction, }); - } else { - // Lc is not zero-bytes in length, let's figure out how long it is - let byte_0 = payload[0]; - if payload.len() == 1 { - // There is only one byte in the payload, that byte cannot be Lc because that would - // entail at *least* one another byte in the payload (for the command data) - return Ok(APDU { - header: array_ref!(header, 0, APDU_HEADER_LEN).into(), - lc: 0x00, - data: Vec::new(), - le: if byte_0 == 0x00 { - // Ne = 256 - 0x100 - } else { - byte_0.into() - }, - case_type: ApduType::Short(Case::Le1), - }); - } - if payload.len() == 1 + (byte_0 as usize) && byte_0 != 0 { - // Lc is one-byte long and since the size specified by Lc covers the rest of the - // payload there's no Le at the end - return Ok(APDU { - header: array_ref!(header, 0, APDU_HEADER_LEN).into(), - lc: byte_0.into(), - data: payload[1..].to_vec(), - case_type: ApduType::Short(Case::Lc1Data), - le: 0, - }); - } - if payload.len() == 2 + (byte_0 as usize) && byte_0 != 0 { - // Lc is one-byte long and since the size specified by Lc covers the rest of the - // payload with ONE additional byte that byte must be Le + } + // Lc is not zero-bytes in length, let's figure out how long it is + let byte_0 = payload[0]; + if payload.len() == 1 { + // There is only one byte in the payload, that byte cannot be Lc because that would + // entail at *least* one another byte in the payload (for the command data) + return Ok(APDU { + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), + lc: 0x00, + data: Vec::new(), + le: if byte_0 == 0x00 { + // Ne = 256 + 0x100 + } else { + byte_0.into() + }, + case_type: ApduType::Short(Case::Le1), + }); + } + if payload.len() == 1 + (byte_0 as usize) && byte_0 != 0 { + // Lc is one-byte long and since the size specified by Lc covers the rest of the + // payload there's no Le at the end + return Ok(APDU { + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), + lc: byte_0.into(), + data: payload[1..].to_vec(), + case_type: ApduType::Short(Case::Lc1Data), + le: 0, + }); + } + if payload.len() == 2 + (byte_0 as usize) && byte_0 != 0 { + // Lc is one-byte long and since the size specified by Lc covers the rest of the + // payload with ONE additional byte that byte must be Le + let last_byte: u32 = (*payload.last().unwrap()).into(); + return Ok(APDU { + header: array_ref!(header, 0, APDU_HEADER_LEN).into(), + lc: byte_0.into(), + data: payload[1..(payload.len() - 1)].to_vec(), + le: if last_byte == 0x00 { 0x100 } else { last_byte }, + case_type: ApduType::Short(Case::Lc1DataLe1), + }); + } + if payload.len() > 2 { + // Lc is possibly three-bytes long + let extended_apdu_lc: usize = BigEndian::read_u16(&payload[1..]) as usize; + let extended_apdu_le_len: usize = if payload.len() > extended_apdu_lc { + payload.len() - extended_apdu_lc - 3 + } else { + 0 + }; + if byte_0 == 0 && extended_apdu_le_len <= 3 { + // If first byte is zero AND the next two bytes can be parsed as a big-endian + // length that covers the rest of the block (plus few additional bytes for Le), we + // have an extended-length APDU let last_byte: u32 = (*payload.last().unwrap()).into(); return Ok(APDU { header: array_ref!(header, 0, APDU_HEADER_LEN).into(), - lc: byte_0.into(), - data: payload[1..(payload.len() - 1)].to_vec(), - le: if last_byte == 0x00 { 0x100 } else { last_byte }, - case_type: ApduType::Short(Case::Lc1DataLe1), + lc: extended_apdu_lc as u16, + data: payload[3..(payload.len() - extended_apdu_le_len)].to_vec(), + le: match extended_apdu_le_len { + 0 => 0, + 1 => { + if last_byte == 0x00 { + 0x100 + } else { + last_byte + } + } + 2 => { + let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]); + if le_parsed == 0x00 { + 0x100 + } else { + le_parsed as u32 + } + } + 3 => { + let le_first_byte: u32 = + (*payload.get(payload.len() - 3).unwrap()).into(); + if le_first_byte != 0x00 { + return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION); + } + let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]); + if le_parsed == 0x00 { + 0x10000 + } else { + le_parsed as u32 + } + } + _ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION), + }, + case_type: ApduType::Extended(match extended_apdu_le_len { + 0 => Case::Lc3Data, + 1 => Case::Lc3DataLe1, + 2 => Case::Lc3DataLe2, + 3 => Case::Le3, + _ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION), + }), }); } - if payload.len() > 2 { - // Lc is possibly three-bytes long - let extended_apdu_lc: usize = BigEndian::read_u16(&payload[1..]) as usize; - let extended_apdu_le_len: usize = if payload.len() > extended_apdu_lc { - payload.len() - extended_apdu_lc - 3 - } else { - 0 - }; - if byte_0 == 0 && extended_apdu_le_len <= 3 { - // If first byte is zero AND the next two bytes can be parsed as a big-endian - // length that covers the rest of the block (plus few additional bytes for Le), we - // have an extended-length APDU - let last_byte: u32 = (*payload.last().unwrap()).into(); - return Ok(APDU { - header: array_ref!(header, 0, APDU_HEADER_LEN).into(), - lc: extended_apdu_lc as u16, - data: payload[3..(payload.len() - extended_apdu_le_len)].to_vec(), - le: match extended_apdu_le_len { - 0 => 0, - 1 => { - if last_byte == 0x00 { - 0x100 - } else { - last_byte - } - } - 2 => { - let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]); - if le_parsed == 0x00 { - 0x100 - } else { - le_parsed as u32 - } - } - 3 => { - let le_first_byte: u32 = - (*payload.get(payload.len() - 3).unwrap()).into(); - if le_first_byte != 0x00 { - return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION); - } - let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]); - if le_parsed == 0x00 { - 0x10000 - } else { - le_parsed as u32 - } - } - _ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION), - }, - case_type: ApduType::Extended(match extended_apdu_le_len { - 0 => Case::Lc3Data, - 1 => Case::Lc3DataLe1, - 2 => Case::Lc3DataLe2, - 3 => Case::Le3, - _ => return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION), - }), - }); - } - } } + return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION); } } From 1eaff57c885836e80e4bb76ee28649dabf0ea93c Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 3 Dec 2020 08:25:34 -0800 Subject: [PATCH 053/192] Le should be interpreted as 0x10000 even in the 2-byte case --- src/ctap/apdu.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 6a94790..b39d7fb 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -195,7 +195,7 @@ impl TryFrom<&[u8]> for APDU { 2 => { let le_parsed = BigEndian::read_u16(&payload[payload.len() - 2..]); if le_parsed == 0x00 { - 0x100 + 0x10000 } else { le_parsed as u32 } @@ -396,7 +396,7 @@ mod test { }, lc: 0xb1, data: payload.to_vec(), - le: 0x100, + le: 0x10000, case_type: ApduType::Extended(Case::Lc3DataLe2), }; assert_eq!(Ok(expected), response); @@ -423,7 +423,7 @@ mod test { }, lc: 0x40, data: payload.to_vec(), - le: 0x100, + le: 0x10000, case_type: ApduType::Extended(Case::Lc3DataLe2), }; assert_eq!(Ok(expected), response); From b032a15654711bafcc797c148ed389056f8bc92d Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 4 Dec 2020 13:17:33 +0100 Subject: [PATCH 054/192] makes the global signature counter more privacy friendly --- src/ctap/ctap1.rs | 35 +++++-- src/ctap/mod.rs | 246 ++++++++++++++++++++++++++++++++++++++++---- src/ctap/storage.rs | 31 +++++- 3 files changed, 276 insertions(+), 36 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 5bc759c..5919431 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -388,6 +388,7 @@ impl Ctap1Command { mod test { use super::super::{key_material, CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; use super::*; + use byteorder::{BigEndian, ByteOrder}; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -642,6 +643,16 @@ mod test { assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_DATA)); } + fn check_signature_counter(response: &[u8; 4], signature_counter: u32) { + if USE_SIGNATURE_COUNTER { + let mut signature_counter_bytes = [0u8; 4]; + BigEndian::write_u32(&mut signature_counter_bytes, signature_counter); + assert_eq!(response, &signature_counter_bytes); + } else { + assert_eq!(response, &[0x00, 0x00, 0x00, 0x00]); + } + } + #[test] fn test_process_authenticate_enforce() { let mut rng = ThreadRng256 {}; @@ -662,11 +673,13 @@ mod test { let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); assert_eq!(response[0], 0x01); - if USE_SIGNATURE_COUNTER { - assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x01]); - } else { - assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x00]); - } + check_signature_counter( + array_ref!(response, 1, 4), + ctap_state + .persistent_store + .global_signature_counter() + .unwrap(), + ); } #[test] @@ -690,11 +703,13 @@ mod test { let response = Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE).unwrap(); assert_eq!(response[0], 0x01); - if USE_SIGNATURE_COUNTER { - assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x01]); - } else { - assert_eq!(response[1..5], [0x00, 0x00, 0x00, 0x00]); - } + check_signature_counter( + array_ref!(response, 1, 4), + ctap_state + .persistent_store + .global_signature_counter() + .unwrap(), + ); } #[test] diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d67796b..56cca9a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -81,6 +81,7 @@ 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; +pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -215,7 +216,9 @@ where pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { if USE_SIGNATURE_COUNTER { - self.persistent_store.incr_global_signature_counter()?; + let increment = self.rng.gen_uniform_u32x8()[0] % 8 + 1; + self.persistent_store + .incr_global_signature_counter(increment)?; } Ok(()) } @@ -1094,9 +1097,43 @@ mod test { // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, + 0xA3, + 0x79, + 0xA6, + 0xF6, + 0xEE, + 0xAF, + 0xB9, + 0xA5, + 0x5E, + 0x37, + 0x8C, + 0x11, + 0x80, + 0x34, + 0xE2, + 0x75, + 0x1E, + 0x68, + 0x2F, + 0xAB, + 0x9F, + 0x2D, + 0x30, + 0xAB, + 0x13, + 0xD2, + 0x12, + 0x55, + 0x86, + 0xCE, + 0x19, + 0x47, + 0x41, + 0x00, + 0x00, + 0x00, + INITIAL_SIGNATURE_COUNTER as u8, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, 0x20]); @@ -1131,9 +1168,43 @@ mod test { // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, + 0xA3, + 0x79, + 0xA6, + 0xF6, + 0xEE, + 0xAF, + 0xB9, + 0xA5, + 0x5E, + 0x37, + 0x8C, + 0x11, + 0x80, + 0x34, + 0xE2, + 0x75, + 0x1E, + 0x68, + 0x2F, + 0xAB, + 0x9F, + 0x2D, + 0x30, + 0xAB, + 0x13, + 0xD2, + 0x12, + 0x55, + 0x86, + 0xCE, + 0x19, + 0x47, + 0x41, + 0x00, + 0x00, + 0x00, + INITIAL_SIGNATURE_COUNTER as u8, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, CREDENTIAL_ID_BASE_SIZE as u8]); @@ -1280,9 +1351,43 @@ mod test { // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, + 0xA3, + 0x79, + 0xA6, + 0xF6, + 0xEE, + 0xAF, + 0xB9, + 0xA5, + 0x5E, + 0x37, + 0x8C, + 0x11, + 0x80, + 0x34, + 0xE2, + 0x75, + 0x1E, + 0x68, + 0x2F, + 0xAB, + 0x9F, + 0x2D, + 0x30, + 0xAB, + 0x13, + 0xD2, + 0x12, + 0x55, + 0x86, + 0xCE, + 0x19, + 0x47, + 0xC1, + 0x00, + 0x00, + 0x00, + INITIAL_SIGNATURE_COUNTER as u8, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, CREDENTIAL_ID_MAX_SIZE as u8]); @@ -1329,9 +1434,43 @@ mod test { // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, + 0xA3, + 0x79, + 0xA6, + 0xF6, + 0xEE, + 0xAF, + 0xB9, + 0xA5, + 0x5E, + 0x37, + 0x8C, + 0x11, + 0x80, + 0x34, + 0xE2, + 0x75, + 0x1E, + 0x68, + 0x2F, + 0xAB, + 0x9F, + 0x2D, + 0x30, + 0xAB, + 0x13, + 0xD2, + 0x12, + 0x55, + 0x86, + 0xCE, + 0x19, + 0x47, + 0xC1, + 0x00, + 0x00, + 0x00, + INITIAL_SIGNATURE_COUNTER as u8, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, 0x20]); @@ -1374,6 +1513,7 @@ mod test { response: Result, expected_user: PublicKeyCredentialUserEntity, flags: u8, + signature_counter: u32, expected_number_of_credentials: Option, ) { match response.unwrap() { @@ -1384,11 +1524,16 @@ mod test { number_of_credentials, .. } = get_assertion_response; - let expected_auth_data = vec![ + let mut expected_auth_data = vec![ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, 0x01, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, 0x00, ]; + let signature_counter_position = expected_auth_data.len() - 4; + BigEndian::write_u32( + &mut expected_auth_data[signature_counter_position..], + signature_counter, + ); assert_eq!(auth_data, expected_auth_data); assert_eq!(user, Some(expected_user)); assert_eq!(number_of_credentials, expected_number_of_credentials); @@ -1400,6 +1545,7 @@ mod test { fn check_assertion_response( response: Result, expected_user_id: Vec, + signature_counter: u32, expected_number_of_credentials: Option, ) { let expected_user = PublicKeyCredentialUserEntity { @@ -1412,6 +1558,7 @@ mod test { response, expected_user, 0x00, + signature_counter, expected_number_of_credentials, ); } @@ -1444,7 +1591,11 @@ mod test { DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); - check_assertion_response(get_assertion_response, vec![0x1D], None); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + check_assertion_response(get_assertion_response, vec![0x1D], signature_counter, None); } #[test] @@ -1637,7 +1788,11 @@ mod test { DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); - check_assertion_response(get_assertion_response, vec![0x1D], None); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + check_assertion_response(get_assertion_response, vec![0x1D], signature_counter, None); let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, @@ -1740,10 +1895,26 @@ mod test { DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); - check_assertion_response_with_user(get_assertion_response, user2, 0x04, Some(2)); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + check_assertion_response_with_user( + get_assertion_response, + user2, + 0x04, + signature_counter, + Some(2), + ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); - check_assertion_response_with_user(get_assertion_response, user1, 0x04, None); + check_assertion_response_with_user( + get_assertion_response, + user1, + 0x04, + signature_counter, + None, + ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); assert_eq!( @@ -1800,13 +1971,22 @@ mod test { DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); - check_assertion_response(get_assertion_response, vec![0x03], Some(3)); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + check_assertion_response( + get_assertion_response, + vec![0x03], + signature_counter, + Some(3), + ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); - check_assertion_response(get_assertion_response, vec![0x02], None); + check_assertion_response(get_assertion_response, vec![0x02], signature_counter, None); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); - check_assertion_response(get_assertion_response, vec![0x01], None); + check_assertion_response(get_assertion_response, vec![0x01], signature_counter, None); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); assert_eq!( @@ -2042,4 +2222,26 @@ mod test { .is_none()); } } + + #[test] + fn test_signature_counter() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let mut last_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + assert!(last_counter > 0); + for _ in 0..100 { + assert!(ctap_state.increment_global_signature_counter().is_ok()); + let next_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + assert!(next_counter > last_counter); + last_counter = next_counter; + } + } } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0e4941a..911bdd7 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -18,6 +18,7 @@ use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialS use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; +use crate::ctap::INITIAL_SIGNATURE_COUNTER; use crate::embedded_flash::{self, StoreConfig, StoreEntry, StoreError}; use alloc::string::String; #[cfg(any(test, feature = "ram_storage", feature = "with_ctap2_1"))] @@ -366,16 +367,16 @@ impl PersistentStore { Ok(self .store .find_one(&Key::GlobalSignatureCounter) - .map_or(0, |(_, entry)| { + .map_or(INITIAL_SIGNATURE_COUNTER, |(_, entry)| { u32::from_ne_bytes(*array_ref!(entry.data, 0, 4)) })) } - pub fn incr_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { + pub fn incr_global_signature_counter(&mut self, increment: u32) -> 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()); + buffer.copy_from_slice(&(increment + INITIAL_SIGNATURE_COUNTER).to_ne_bytes()); self.store.insert(StoreEntry { tag: GLOBAL_SIGNATURE_COUNTER, data: &buffer, @@ -385,7 +386,7 @@ impl PersistentStore { 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()); + buffer.copy_from_slice(&value.wrapping_add(increment).to_ne_bytes()); self.store.replace( index, StoreEntry { @@ -1136,6 +1137,28 @@ mod test { assert_eq!(persistent_store._min_pin_length_rp_ids().unwrap(), rp_ids); } + #[test] + fn test_global_signature_counter() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let mut counter_value = 1; + assert_eq!( + persistent_store.global_signature_counter().unwrap(), + counter_value + ); + for increment in 1..10 { + assert!(persistent_store + .incr_global_signature_counter(increment) + .is_ok()); + counter_value += increment; + assert_eq!( + persistent_store.global_signature_counter().unwrap(), + counter_value + ); + } + } + #[test] fn test_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; From 21b8ad18cecb47fb465b2870fa45570f767d238f Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 4 Dec 2020 13:41:56 +0100 Subject: [PATCH 055/192] fix clippy warning in apdu --- src/ctap/apdu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index b39d7fb..949151e 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -226,7 +226,7 @@ impl TryFrom<&[u8]> for APDU { } } - return Err(ApduStatusCode::SW_INTERNAL_EXCEPTION); + Err(ApduStatusCode::SW_INTERNAL_EXCEPTION) } } From 16c0196b1d3a97abd00e6b52615a78b7ac0ad11c Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Fri, 4 Dec 2020 14:42:16 +0100 Subject: [PATCH 056/192] Check global counter length --- src/ctap/storage.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0f1c73f..a10f2eb 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -294,10 +294,11 @@ impl PersistentStore { /// Returns the global signature counter. pub fn global_signature_counter(&self) -> Result { - Ok(match self.store.find(key::GLOBAL_SIGNATURE_COUNTER)? { - None => 0, - Some(value) => u32::from_ne_bytes(*array_ref!(&value, 0, 4)), - }) + match self.store.find(key::GLOBAL_SIGNATURE_COUNTER)? { + None => Ok(0), + Some(value) if value.len() == 4 => Ok(u32::from_ne_bytes(*array_ref!(&value, 0, 4))), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE), + } } /// Increments the global signature counter. From 0b55ff3c3ad995b34113d71808bf33fa7828e1bf Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 4 Dec 2020 14:57:11 +0100 Subject: [PATCH 057/192] fixes formatting --- src/ctap/ctap1.rs | 5 +- src/ctap/mod.rs | 164 +++++----------------------------------------- 2 files changed, 17 insertions(+), 152 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 5919431..e434c36 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -388,7 +388,6 @@ impl Ctap1Command { mod test { use super::super::{key_material, CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; use super::*; - use byteorder::{BigEndian, ByteOrder}; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -645,9 +644,7 @@ mod test { fn check_signature_counter(response: &[u8; 4], signature_counter: u32) { if USE_SIGNATURE_COUNTER { - let mut signature_counter_bytes = [0u8; 4]; - BigEndian::write_u32(&mut signature_counter_bytes, signature_counter); - assert_eq!(response, &signature_counter_bytes); + assert_eq!(u32::from_be_bytes(*response), signature_counter); } else { assert_eq!(response, &[0x00, 0x00, 0x00, 0x00]); } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 56cca9a..ea42caf 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1097,44 +1097,11 @@ mod test { // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); let mut expected_auth_data = vec![ - 0xA3, - 0x79, - 0xA6, - 0xF6, - 0xEE, - 0xAF, - 0xB9, - 0xA5, - 0x5E, - 0x37, - 0x8C, - 0x11, - 0x80, - 0x34, - 0xE2, - 0x75, - 0x1E, - 0x68, - 0x2F, - 0xAB, - 0x9F, - 0x2D, - 0x30, - 0xAB, - 0x13, - 0xD2, - 0x12, - 0x55, - 0x86, - 0xCE, - 0x19, - 0x47, - 0x41, - 0x00, - 0x00, - 0x00, - INITIAL_SIGNATURE_COUNTER as u8, + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, ]; + expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, 0x20]); assert_eq!( @@ -1168,44 +1135,11 @@ mod test { // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); let mut expected_auth_data = vec![ - 0xA3, - 0x79, - 0xA6, - 0xF6, - 0xEE, - 0xAF, - 0xB9, - 0xA5, - 0x5E, - 0x37, - 0x8C, - 0x11, - 0x80, - 0x34, - 0xE2, - 0x75, - 0x1E, - 0x68, - 0x2F, - 0xAB, - 0x9F, - 0x2D, - 0x30, - 0xAB, - 0x13, - 0xD2, - 0x12, - 0x55, - 0x86, - 0xCE, - 0x19, - 0x47, - 0x41, - 0x00, - 0x00, - 0x00, - INITIAL_SIGNATURE_COUNTER as u8, + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, ]; + expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, CREDENTIAL_ID_BASE_SIZE as u8]); assert_eq!( @@ -1351,44 +1285,11 @@ mod test { // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); let mut expected_auth_data = vec![ - 0xA3, - 0x79, - 0xA6, - 0xF6, - 0xEE, - 0xAF, - 0xB9, - 0xA5, - 0x5E, - 0x37, - 0x8C, - 0x11, - 0x80, - 0x34, - 0xE2, - 0x75, - 0x1E, - 0x68, - 0x2F, - 0xAB, - 0x9F, - 0x2D, - 0x30, - 0xAB, - 0x13, - 0xD2, - 0x12, - 0x55, - 0x86, - 0xCE, - 0x19, - 0x47, - 0xC1, - 0x00, - 0x00, - 0x00, - INITIAL_SIGNATURE_COUNTER as u8, + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, ]; + expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, CREDENTIAL_ID_MAX_SIZE as u8]); assert_eq!( @@ -1434,44 +1335,11 @@ mod test { // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); let mut expected_auth_data = vec![ - 0xA3, - 0x79, - 0xA6, - 0xF6, - 0xEE, - 0xAF, - 0xB9, - 0xA5, - 0x5E, - 0x37, - 0x8C, - 0x11, - 0x80, - 0x34, - 0xE2, - 0x75, - 0x1E, - 0x68, - 0x2F, - 0xAB, - 0x9F, - 0x2D, - 0x30, - 0xAB, - 0x13, - 0xD2, - 0x12, - 0x55, - 0x86, - 0xCE, - 0x19, - 0x47, - 0xC1, - 0x00, - 0x00, - 0x00, - INITIAL_SIGNATURE_COUNTER as u8, + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, ]; + expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_auth_data.extend(&[0x00, 0x20]); assert_eq!( From 4c84e940391149b1fb3526e148d290376787a71c Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 7 Dec 2020 21:23:55 -0800 Subject: [PATCH 058/192] Use new APDU parser in CTAP1 code --- src/ctap/apdu.rs | 33 +++++++++++++++----------- src/ctap/ctap1.rs | 59 ++++++++++++++++++++++------------------------- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 949151e..530a85b 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -53,10 +53,10 @@ pub enum ApduInstructions { #[allow(dead_code)] #[derive(Default, PartialEq)] pub struct ApduHeader { - cla: u8, - ins: u8, - p1: u8, - p2: u8, + pub cla: u8, + pub ins: u8, + pub p1: u8, + pub p2: u8, } impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader { @@ -96,11 +96,11 @@ pub enum ApduType { #[allow(dead_code)] #[derive(PartialEq)] pub struct APDU { - header: ApduHeader, - lc: u16, - data: Vec, - le: u32, - case_type: ApduType, + pub header: ApduHeader, + pub lc: u16, + pub data: Vec, + pub le: u32, + pub case_type: ApduType, } impl TryFrom<&[u8]> for APDU { @@ -168,12 +168,17 @@ impl TryFrom<&[u8]> for APDU { } if payload.len() > 2 { // Lc is possibly three-bytes long - let extended_apdu_lc: usize = BigEndian::read_u16(&payload[1..]) as usize; - let extended_apdu_le_len: usize = if payload.len() > extended_apdu_lc { - payload.len() - extended_apdu_lc - 3 - } else { - 0 + let extended_apdu_lc: usize = BigEndian::read_u16(&payload[1..3]) as usize; + if payload.len() < extended_apdu_lc + 3 { + return Err(ApduStatusCode::SW_WRONG_LENGTH); + } + let extended_apdu_le_len: usize = match payload.len() - extended_apdu_lc { + // There's some possible Le bytes at the end + 2..=5 => payload.len() - extended_apdu_lc - 3, + // There are more bytes than even Le3 can consume, return an error + _ => return Err(ApduStatusCode::SW_WRONG_LENGTH), }; + if byte_0 == 0 && extended_apdu_le_len <= 3 { // If first byte is zero AND the next two bytes can be parsed as a big-endian // length that covers the rest of the block (plus few additional bytes for Le), we diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index e434c36..ea5f894 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::apdu::{ApduStatusCode, APDU}; use super::hid::ChannelID; use super::status_code::Ctap2StatusCode; use super::CtapState; @@ -118,11 +119,16 @@ impl TryFrom<&[u8]> for U2fCommand { type Error = Ctap1StatusCode; fn try_from(message: &[u8]) -> Result { - if message.len() < Ctap1Command::APDU_HEADER_LEN as usize { - return Err(Ctap1StatusCode::SW_WRONG_DATA); - } - - let (apdu, payload) = message.split_at(Ctap1Command::APDU_HEADER_LEN as usize); + let apdu: APDU = match APDU::try_from(message) { + Ok(apdu) => apdu, + // Todo: Better conversion between ApduStatusCode and Ctap1StatusCode + // Maybe use TryFrom? + Err(apdu_status_code) => match apdu_status_code { + ApduStatusCode::SW_WRONG_LENGTH => return Err(Ctap1StatusCode::SW_WRONG_LENGTH), + ApduStatusCode::SW_WRONG_DATA => return Err(Ctap1StatusCode::SW_WRONG_DATA), + _ => return Err(Ctap1StatusCode::SW_COMMAND_ABORTED), + }, + }; // ISO7816 APDU Header format. Each cell is 1 byte. Note that the CTAP flavor always // encodes the length on 3 bytes and doesn't use the field "Le" (Length Expected). @@ -131,30 +137,29 @@ impl TryFrom<&[u8]> for U2fCommand { // +-----+-----+----+----+-----+-----+-----+ // | CLA | INS | P1 | P2 | Lc1 | Lc2 | Lc3 | // +-----+-----+----+----+-----+-----+-----+ - if apdu[0] != Ctap1Command::CTAP1_CLA { + if apdu.header.cla != Ctap1Command::CTAP1_CLA { return Err(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED); } - let lc = (((apdu[4] as u32) << 16) | ((apdu[5] as u32) << 8) | (apdu[6] as u32)) as usize; - // Since there is always request data, the expected length is either omitted or // encoded in 2 bytes. - if lc != payload.len() && lc + 2 != payload.len() { + // Todo: support extended APDUs now that the new parser can work with those + if apdu.lc as usize != apdu.data.len() && (apdu.lc as usize) + 2 != apdu.data.len() { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } - match apdu[1] { + match apdu.header.ins { // U2F raw message format specification, Section 4.1 // +-----------------+-------------------+ // + Challenge (32B) | Application (32B) | // +-----------------+-------------------+ Ctap1Command::U2F_REGISTER => { - if lc != 64 { + if apdu.lc != 64 { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } Ok(Self::Register { - challenge: *array_ref!(payload, 0, 32), - application: *array_ref!(payload, 32, 32), + challenge: *array_ref!(apdu.data, 0, 32), + application: *array_ref!(apdu.data, 32, 32), }) } @@ -163,25 +168,25 @@ impl TryFrom<&[u8]> for U2fCommand { // + Challenge (32B) | Application (32B) | key handle len (1B) | key handle | // +-----------------+-------------------+---------------------+------------+ Ctap1Command::U2F_AUTHENTICATE => { - if lc < 65 { + if apdu.lc < 65 { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } - let handle_length = payload[64] as usize; - if lc != 65 + handle_length { + let handle_length = apdu.data[64] as usize; + if apdu.lc as usize != 65 + handle_length { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } - let flag = Ctap1Flags::try_from(apdu[2])?; + let flag = Ctap1Flags::try_from(apdu.header.p1)?; Ok(Self::Authenticate { - challenge: *array_ref!(payload, 0, 32), - application: *array_ref!(payload, 32, 32), - key_handle: payload[65..lc].to_vec(), + challenge: *array_ref!(apdu.data, 0, 32), + application: *array_ref!(apdu.data, 32, 32), + key_handle: apdu.data[65..].to_vec(), flags: flag, }) } // U2F raw message format specification, Section 6.1 Ctap1Command::U2F_VERSION => { - if lc != 0 { + if apdu.lc != 0 { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } Ok(Self::Version) @@ -190,7 +195,7 @@ impl TryFrom<&[u8]> for U2fCommand { // For Vendor specific command. Ctap1Command::VENDOR_SPECIFIC_FIRST..=Ctap1Command::VENDOR_SPECIFIC_LAST => { Ok(Self::VendorSpecific { - payload: payload.to_vec(), + payload: apdu.data.to_vec(), }) } @@ -202,8 +207,6 @@ impl TryFrom<&[u8]> for U2fCommand { pub struct Ctap1Command {} impl Ctap1Command { - const APDU_HEADER_LEN: u32 = 7; // CLA + INS + P1 + P2 + LC1-3 - const CTAP1_CLA: u8 = 0; // This byte is used in Register, but only serves backwards compatibility. const LEGACY_BYTE: u8 = 0x05; @@ -571,13 +574,7 @@ mod test { let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); - message.push(0x00); - let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); - assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH)); - - // Two extra zeros are okay, they could encode the expected response length. - message.push(0x00); - message.push(0x00); + message.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH)); } From e4d160aaeeb4c209908a1c9a1eb073273d2792df Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 7 Dec 2020 23:32:04 -0800 Subject: [PATCH 059/192] Use TryFrom to convert between APDU and CTAP status codes --- src/ctap/ctap1.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index ea5f894..b0ed506 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -60,6 +60,24 @@ impl TryFrom for Ctap1StatusCode { } } +impl TryFrom for Ctap1StatusCode { + type Error = (); + + fn try_from(apdu_status_code: ApduStatusCode) -> Result { + match apdu_status_code { + ApduStatusCode::SW_WRONG_LENGTH => Ok(Ctap1StatusCode::SW_WRONG_LENGTH), + ApduStatusCode::SW_WRONG_DATA => Ok(Ctap1StatusCode::SW_WRONG_DATA), + ApduStatusCode::SW_CLA_INVALID => Ok(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED), + ApduStatusCode::SW_INS_INVALID => Ok(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), + ApduStatusCode::SW_COND_USE_NOT_SATISFIED => { + Ok(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED) + } + ApduStatusCode::SW_SUCCESS => Ok(Ctap1StatusCode::SW_NO_ERROR), + _ => Ok(Ctap1StatusCode::SW_COMMAND_ABORTED), + } + } +} + impl Into for Ctap1StatusCode { fn into(self) -> u16 { self as u16 @@ -121,13 +139,9 @@ impl TryFrom<&[u8]> for U2fCommand { fn try_from(message: &[u8]) -> Result { let apdu: APDU = match APDU::try_from(message) { Ok(apdu) => apdu, - // Todo: Better conversion between ApduStatusCode and Ctap1StatusCode - // Maybe use TryFrom? - Err(apdu_status_code) => match apdu_status_code { - ApduStatusCode::SW_WRONG_LENGTH => return Err(Ctap1StatusCode::SW_WRONG_LENGTH), - ApduStatusCode::SW_WRONG_DATA => return Err(Ctap1StatusCode::SW_WRONG_DATA), - _ => return Err(Ctap1StatusCode::SW_COMMAND_ABORTED), - }, + Err(apdu_status_code) => { + return Err(Ctap1StatusCode::try_from(apdu_status_code).unwrap()) + } }; // ISO7816 APDU Header format. Each cell is 1 byte. Note that the CTAP flavor always From 373464b72d0d851d10481d3371f1ea09b4958a98 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 7 Dec 2020 23:35:47 -0800 Subject: [PATCH 060/192] Remove redundant type declaration --- src/ctap/apdu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 530a85b..e3dfe23 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -168,7 +168,7 @@ impl TryFrom<&[u8]> for APDU { } if payload.len() > 2 { // Lc is possibly three-bytes long - let extended_apdu_lc: usize = BigEndian::read_u16(&payload[1..3]) as usize; + let extended_apdu_lc = BigEndian::read_u16(&payload[1..3]) as usize; if payload.len() < extended_apdu_lc + 3 { return Err(ApduStatusCode::SW_WRONG_LENGTH); } From 2d17bb2afafe1d94abd55978c6863d2015ff219b Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 7 Dec 2020 23:38:21 -0800 Subject: [PATCH 061/192] Readability improvements --- src/ctap/apdu.rs | 4 ++-- src/ctap/ctap1.rs | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index e3dfe23..70b17eb 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -172,9 +172,9 @@ impl TryFrom<&[u8]> for APDU { if payload.len() < extended_apdu_lc + 3 { return Err(ApduStatusCode::SW_WRONG_LENGTH); } - let extended_apdu_le_len: usize = match payload.len() - extended_apdu_lc { + let extended_apdu_le_len: usize = match payload.len() - extended_apdu_lc - 3 { // There's some possible Le bytes at the end - 2..=5 => payload.len() - extended_apdu_lc - 3, + 0..=3 => payload.len() - extended_apdu_lc - 3, // There are more bytes than even Le3 can consume, return an error _ => return Err(ApduStatusCode::SW_WRONG_LENGTH), }; diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index b0ed506..2677201 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -144,6 +144,8 @@ impl TryFrom<&[u8]> for U2fCommand { } }; + let lc = apdu.lc as usize; + // ISO7816 APDU Header format. Each cell is 1 byte. Note that the CTAP flavor always // encodes the length on 3 bytes and doesn't use the field "Le" (Length Expected). // We keep the 2 byte of "Le" for the packet length in mind, but always ignore its value. @@ -157,8 +159,7 @@ impl TryFrom<&[u8]> for U2fCommand { // Since there is always request data, the expected length is either omitted or // encoded in 2 bytes. - // Todo: support extended APDUs now that the new parser can work with those - if apdu.lc as usize != apdu.data.len() && (apdu.lc as usize) + 2 != apdu.data.len() { + if lc != apdu.data.len() && (lc as usize) + 2 != apdu.data.len() { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } @@ -168,7 +169,7 @@ impl TryFrom<&[u8]> for U2fCommand { // + Challenge (32B) | Application (32B) | // +-----------------+-------------------+ Ctap1Command::U2F_REGISTER => { - if apdu.lc != 64 { + if lc != 64 { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } Ok(Self::Register { @@ -182,11 +183,11 @@ impl TryFrom<&[u8]> for U2fCommand { // + Challenge (32B) | Application (32B) | key handle len (1B) | key handle | // +-----------------+-------------------+---------------------+------------+ Ctap1Command::U2F_AUTHENTICATE => { - if apdu.lc < 65 { + if lc < 65 { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } let handle_length = apdu.data[64] as usize; - if apdu.lc as usize != 65 + handle_length { + if lc as usize != 65 + handle_length { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } let flag = Ctap1Flags::try_from(apdu.header.p1)?; @@ -200,7 +201,7 @@ impl TryFrom<&[u8]> for U2fCommand { // U2F raw message format specification, Section 6.1 Ctap1Command::U2F_VERSION => { - if apdu.lc != 0 { + if lc != 0 { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } Ok(Self::Version) From 56bc86c5d01146e4c7c52f58a17e7386e87f147c Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 7 Dec 2020 23:40:06 -0800 Subject: [PATCH 062/192] No need to cast again --- src/ctap/ctap1.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 2677201..4d18846 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -159,7 +159,7 @@ impl TryFrom<&[u8]> for U2fCommand { // Since there is always request data, the expected length is either omitted or // encoded in 2 bytes. - if lc != apdu.data.len() && (lc as usize) + 2 != apdu.data.len() { + if lc != apdu.data.len() && lc + 2 != apdu.data.len() { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } @@ -187,7 +187,7 @@ impl TryFrom<&[u8]> for U2fCommand { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } let handle_length = apdu.data[64] as usize; - if lc as usize != 65 + handle_length { + if lc != 65 + handle_length { return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } let flag = Ctap1Flags::try_from(apdu.header.p1)?; From 90def7dfd3c913d04fe107373b926d1d012655ce Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 8 Dec 2020 18:12:48 +0100 Subject: [PATCH 063/192] implicitly generate HMAC-secret --- src/ctap/ctap1.rs | 53 +++++---------- src/ctap/data_formats.rs | 20 +----- src/ctap/mod.rs | 128 ++++++++++-------------------------- src/ctap/pin_protocol_v1.rs | 30 ++------- src/ctap/storage.rs | 59 +++++++++++++++-- 5 files changed, 112 insertions(+), 178 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index e434c36..47d2120 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -291,7 +291,7 @@ impl Ctap1Command { let sk = crypto::ecdsa::SecKey::gensk(ctap_state.rng); let pk = sk.genpk(); let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) + .encrypt_key_handle(sk, &application) .map_err(|_| Ctap1StatusCode::SW_COMMAND_ABORTED)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. @@ -386,7 +386,7 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::{key_material, CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; + use super::super::{key_material, CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER}; use super::*; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -426,12 +426,12 @@ mod test { 0x00, 0x00, 0x00, - 65 + CREDENTIAL_ID_BASE_SIZE as u8, + 65 + CREDENTIAL_ID_SIZE as u8, ]; let challenge = [0x0C; 32]; message.extend(&challenge); message.extend(application); - message.push(CREDENTIAL_ID_BASE_SIZE as u8); + message.push(CREDENTIAL_ID_SIZE as u8); message.extend(key_handle); message } @@ -471,15 +471,12 @@ mod test { let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); assert_eq!(response[0], Ctap1Command::LEGACY_BYTE); - assert_eq!(response[66], CREDENTIAL_ID_BASE_SIZE as u8); + assert_eq!(response[66], CREDENTIAL_ID_SIZE as u8); assert!(ctap_state - .decrypt_credential_source( - response[67..67 + CREDENTIAL_ID_BASE_SIZE].to_vec(), - &application - ) + .decrypt_credential_source(response[67..67 + CREDENTIAL_ID_SIZE].to_vec(), &application) .unwrap() .is_some()); - const CERT_START: usize = 67 + CREDENTIAL_ID_BASE_SIZE; + const CERT_START: usize = 67 + CREDENTIAL_ID_SIZE; assert_eq!( &response[CERT_START..CERT_START + fake_cert.len()], &fake_cert[..] @@ -528,9 +525,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); @@ -546,9 +541,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let application = [0x55; 32]; let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -565,9 +558,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -591,9 +582,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[0] = 0xEE; @@ -611,9 +600,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[1] = 0xEE; @@ -631,9 +618,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[2] = 0xEE; @@ -659,9 +644,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -688,9 +671,7 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state - .encrypt_key_handle(sk, &application, None) - .unwrap(); + let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); let message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, @@ -712,7 +693,7 @@ mod test { #[test] fn test_process_authenticate_bad_key_handle() { let application = [0x0A; 32]; - let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -729,7 +710,7 @@ mod test { #[test] fn test_process_authenticate_without_up() { let application = [0x0A; 32]; - let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index e7419fd..a2b490d 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -498,7 +498,6 @@ pub struct PublicKeyCredentialSource { pub rp_id: String, pub user_handle: Vec, // not optional, but nullable pub user_display_name: Option, - pub cred_random: Option>, pub cred_protect_policy: Option, pub creation_order: u64, pub user_name: Option, @@ -513,14 +512,14 @@ enum PublicKeyCredentialSourceField { RpId = 2, UserHandle = 3, UserDisplayName = 4, - CredRandom = 5, CredProtectPolicy = 6, CreationOrder = 7, UserName = 8, UserIcon = 9, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. - // Reserved tags: none. + // Reserved tags: + // - CredRandom = 5, } impl From for cbor::KeyType { @@ -539,7 +538,6 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::RpId => Some(credential.rp_id), PublicKeyCredentialSourceField::UserHandle => Some(credential.user_handle), PublicKeyCredentialSourceField::UserDisplayName => credential.user_display_name, - PublicKeyCredentialSourceField::CredRandom => credential.cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, PublicKeyCredentialSourceField::UserName => credential.user_name, @@ -559,7 +557,6 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::RpId => rp_id, PublicKeyCredentialSourceField::UserHandle => user_handle, PublicKeyCredentialSourceField::UserDisplayName => user_display_name, - PublicKeyCredentialSourceField::CredRandom => cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, PublicKeyCredentialSourceField::CreationOrder => creation_order, PublicKeyCredentialSourceField::UserName => user_name, @@ -577,7 +574,6 @@ impl TryFrom for PublicKeyCredentialSource { let rp_id = extract_text_string(ok_or_missing(rp_id)?)?; let user_handle = extract_byte_string(ok_or_missing(user_handle)?)?; let user_display_name = user_display_name.map(extract_text_string).transpose()?; - let cred_random = cred_random.map(extract_byte_string).transpose()?; let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; @@ -601,7 +597,6 @@ impl TryFrom for PublicKeyCredentialSource { rp_id, user_handle, user_display_name, - cred_random, cred_protect_policy, creation_order, user_name, @@ -1373,7 +1368,6 @@ mod test { rp_id: "example.com".to_string(), user_handle: b"foo".to_vec(), user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -1395,16 +1389,6 @@ mod test { Ok(credential.clone()) ); - let credential = PublicKeyCredentialSource { - cred_random: Some(vec![0x00; 32]), - ..credential - }; - - assert_eq!( - PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), - Ok(credential.clone()) - ); - let credential = PublicKeyCredentialSource { cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), ..credential diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index ea42caf..67a0e27 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -86,10 +86,8 @@ pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, // - 32 byte relying party ID hashed with SHA256, -// - (optional) 32 byte for HMAC-secret, // - 32 byte HMAC-SHA256 over everything else. -pub const CREDENTIAL_ID_BASE_SIZE: usize = 112; -pub const CREDENTIAL_ID_MAX_SIZE: usize = CREDENTIAL_ID_BASE_SIZE + 32; +pub const CREDENTIAL_ID_SIZE: usize = 112; // Set this bit when checking user presence. const UP_FLAG: u8 = 0x01; // Set this bit when checking user verification. @@ -142,6 +140,7 @@ struct AssertionInput { client_data_hash: Vec, auth_data: Vec, hmac_secret_input: Option, + has_uv: bool, } struct AssertionState { @@ -231,7 +230,6 @@ where &mut self, private_key: crypto::ecdsa::SecKey, application: &[u8; 32], - cred_random: Option<&[u8; 32]>, ) -> Result, Ctap2StatusCode> { let master_keys = self.persistent_store.master_keys()?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); @@ -240,16 +238,12 @@ where let mut iv = [0; 16]; iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); - let block_len = if cred_random.is_some() { 6 } else { 4 }; + let block_len = 4; let mut blocks = vec![[0u8; 16]; block_len]; blocks[0].copy_from_slice(&sk_bytes[..16]); blocks[1].copy_from_slice(&sk_bytes[16..]); blocks[2].copy_from_slice(&application[..16]); blocks[3].copy_from_slice(&application[16..]); - if let Some(cred_random) = cred_random { - blocks[4].copy_from_slice(&cred_random[..16]); - blocks[5].copy_from_slice(&cred_random[16..]); - } cbc_encrypt(&aes_enc_key, iv, &mut blocks); let mut encrypted_id = Vec::with_capacity(16 * (block_len + 3)); @@ -270,11 +264,9 @@ where credential_id: Vec, rp_id_hash: &[u8], ) -> Result, Ctap2StatusCode> { - let has_cred_random = match credential_id.len() { - CREDENTIAL_ID_BASE_SIZE => false, - CREDENTIAL_ID_MAX_SIZE => true, - _ => return Ok(None), - }; + if credential_id.len() != CREDENTIAL_ID_SIZE { + return Ok(None); + } let master_keys = self.persistent_store.master_keys()?; let payload_size = credential_id.len() - 32; if !verify_hmac_256::( @@ -288,7 +280,7 @@ where let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let mut iv = [0; 16]; iv.copy_from_slice(&credential_id[..16]); - let block_len = if has_cred_random { 6 } else { 4 }; + let block_len = 4; let mut blocks = vec![[0u8; 16]; block_len]; for i in 0..block_len { blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); @@ -301,15 +293,6 @@ where decrypted_sk[16..].clone_from_slice(&blocks[1]); decrypted_rp_id_hash[..16].clone_from_slice(&blocks[2]); decrypted_rp_id_hash[16..].clone_from_slice(&blocks[3]); - let cred_random = if has_cred_random { - let mut decrypted_cred_random = [0; 32]; - decrypted_cred_random[..16].clone_from_slice(&blocks[4]); - decrypted_cred_random[16..].clone_from_slice(&blocks[5]); - Some(decrypted_cred_random.to_vec()) - } else { - None - }; - if rp_id_hash != decrypted_rp_id_hash { return Ok(None); } @@ -322,7 +305,6 @@ where rp_id: String::from(""), user_handle: vec![], user_display_name: None, - cred_random, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -464,11 +446,6 @@ where (false, DEFAULT_CRED_PROTECT) }; - let cred_random = if use_hmac_extension { - Some(self.rng.gen_uniform_u8x32()) - } else { - None - }; let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); let rp_id = rp.rp_id; @@ -543,7 +520,6 @@ where user_display_name: user .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), - cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, creation_order: self.persistent_store.new_creation_order()?, user_name: user @@ -556,7 +532,7 @@ where self.persistent_store.store_credential(credential_source)?; random_id } else { - self.encrypt_key_handle(sk.clone(), &rp_id_hash, cred_random.as_ref())? + self.encrypt_key_handle(sk.clone(), &rp_id_hash)? }; let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; @@ -622,10 +598,25 @@ where )) } + // Generates a different per-credential secret for each UV mode. + // The computation is deterministic, and private_key expected to be unique. + fn generate_cred_random( + &mut self, + private_key: &crypto::ecdsa::SecKey, + has_uv: bool, + ) -> Result<[u8; 32], Ctap2StatusCode> { + let mut private_key_bytes = [0u8; 32]; + private_key.to_bytes(&mut private_key_bytes); + let salt = crypto::sha256::Sha256::hash(&private_key_bytes); + // TODO(kaczmarczyck) KDF? hash salt together with rp_id_hash? + let key = self.persistent_store.cred_random_secret(has_uv)?; + Ok(hmac_256::(&key, &salt[..])) + } + // Processes the input of a get_assertion operation for a given credential // and returns the correct Get(Next)Assertion response. fn assertion_response( - &self, + &mut self, credential: PublicKeyCredentialSource, assertion_input: AssertionInput, number_of_credentials: Option, @@ -634,13 +625,15 @@ where client_data_hash, mut auth_data, hmac_secret_input, + has_uv, } = assertion_input; // Process extensions. if let Some(hmac_secret_input) = hmac_secret_input { + let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; let encrypted_output = self .pin_protocol_v1 - .process_hmac_secret(hmac_secret_input, &credential.cred_random)?; + .process_hmac_secret(hmac_secret_input, &cred_random)?; let extensions_output = cbor_map! { "hmac-secret" => encrypted_output, }; @@ -807,6 +800,7 @@ where client_data_hash, auth_data: self.generate_auth_data(&rp_id_hash, flags)?, hmac_secret_input, + has_uv, }; let number_of_credentials = if applicable_credentials.is_empty() { None @@ -872,7 +866,7 @@ where max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), // #TODO(106) update with version 2.1 of HMAC-secret #[cfg(feature = "with_ctap2_1")] - max_credential_id_length: Some(CREDENTIAL_ID_BASE_SIZE as u64 + 32), + max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), #[cfg(feature = "with_ctap2_1")] transports: Some(vec![AuthenticatorTransport::Usb]), #[cfg(feature = "with_ctap2_1")] @@ -1010,7 +1004,7 @@ mod test { #[cfg(feature = "with_ctap2_1")] expected_response.extend( [ - 0x08, 0x18, 0x90, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, + 0x08, 0x18, 0x70, 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, 0x0D, 0x04, ] @@ -1141,7 +1135,7 @@ mod test { ]; expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, CREDENTIAL_ID_BASE_SIZE as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1186,7 +1180,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![], user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -1291,7 +1284,7 @@ mod test { ]; expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, CREDENTIAL_ID_MAX_SIZE as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1488,8 +1481,8 @@ mod test { let auth_data = make_credential_response.auth_data; let offset = 37 + ctap_state.persistent_store.aaguid().unwrap().len(); assert_eq!(auth_data[offset], 0x00); - assert_eq!(auth_data[offset + 1] as usize, CREDENTIAL_ID_MAX_SIZE); - auth_data[offset + 2..offset + 2 + CREDENTIAL_ID_MAX_SIZE].to_vec() + assert_eq!(auth_data[offset + 1] as usize, CREDENTIAL_ID_SIZE); + auth_data[offset + 2..offset + 2 + CREDENTIAL_ID_SIZE].to_vec() } _ => panic!("Invalid response type"), }; @@ -1604,7 +1597,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x1D], user_display_name: None, - cred_random: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), @@ -1669,7 +1661,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x1D], user_display_name: None, - cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), creation_order: 0, user_name: None, @@ -1942,7 +1933,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![], user_display_name: None, - cred_random: None, cred_protect_policy: None, creation_order: 0, user_name: None, @@ -2013,7 +2003,7 @@ mod test { // We are not testing the correctness of our SHA256 here, only if it is checked. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key.clone(), &rp_id_hash, None) + .encrypt_key_handle(private_key.clone(), &rp_id_hash) .unwrap(); let decrypted_source = ctap_state .decrypt_credential_source(encrypted_id, &rp_id_hash) @@ -2023,29 +2013,6 @@ mod test { assert_eq!(private_key, decrypted_source.private_key); } - #[test] - fn test_encrypt_decrypt_credential_with_cred_random() { - let mut rng = ThreadRng256 {}; - let user_immediately_present = |_| Ok(()); - let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - - // Usually, the relying party ID or its hash is provided by the client. - // We are not testing the correctness of our SHA256 here, only if it is checked. - let rp_id_hash = [0x55; 32]; - let cred_random = [0xC9; 32]; - let encrypted_id = ctap_state - .encrypt_key_handle(private_key.clone(), &rp_id_hash, Some(&cred_random)) - .unwrap(); - let decrypted_source = ctap_state - .decrypt_credential_source(encrypted_id, &rp_id_hash) - .unwrap() - .unwrap(); - - assert_eq!(private_key, decrypted_source.private_key); - assert_eq!(Some(cred_random.to_vec()), decrypted_source.cred_random); - } - #[test] fn test_encrypt_decrypt_bad_hmac() { let mut rng = ThreadRng256 {}; @@ -2056,30 +2023,7 @@ mod test { // Same as above. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key, &rp_id_hash, None) - .unwrap(); - for i in 0..encrypted_id.len() { - let mut modified_id = encrypted_id.clone(); - modified_id[i] ^= 0x01; - assert!(ctap_state - .decrypt_credential_source(modified_id, &rp_id_hash) - .unwrap() - .is_none()); - } - } - - #[test] - fn test_encrypt_decrypt_bad_hmac_with_cred_random() { - let mut rng = ThreadRng256 {}; - let user_immediately_present = |_| Ok(()); - let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - - // Same as above. - let rp_id_hash = [0x55; 32]; - let cred_random = [0xC9; 32]; - let encrypted_id = ctap_state - .encrypt_key_handle(private_key, &rp_id_hash, Some(&cred_random)) + .encrypt_key_handle(private_key, &rp_id_hash) .unwrap(); for i in 0..encrypted_id.len() { let mut modified_id = encrypted_id.clone(); diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 44aad71..410dac7 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -56,23 +56,16 @@ fn verify_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bo fn encrypt_hmac_secret_output( shared_secret: &[u8; 32], salt_enc: &[u8], - cred_random: &[u8], + cred_random: &[u8; 32], ) -> 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 = [0u8; 16]; - let mut cred_random_secret = [0u8; 32]; - cred_random_secret.copy_from_slice(cred_random); - // With the if clause restriction above, block_len can only be 2 or 4. let block_len = salt_enc.len() / 16; let mut blocks = vec![[0u8; 16]; block_len]; @@ -84,7 +77,7 @@ fn encrypt_hmac_secret_output( let mut decrypted_salt1 = [0u8; 32]; decrypted_salt1[..16].copy_from_slice(&blocks[0]); decrypted_salt1[16..].copy_from_slice(&blocks[1]); - let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); + let output1 = hmac_256::(&cred_random[..], &decrypted_salt1[..]); for i in 0..2 { blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); } @@ -93,7 +86,7 @@ fn encrypt_hmac_secret_output( let mut decrypted_salt2 = [0u8; 32]; 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[..]); + let output2 = hmac_256::(&cred_random[..], &decrypted_salt2[..]); for i in 0..2 { blocks[i + 2].copy_from_slice(&output2[16 * i..16 * (i + 1)]); } @@ -588,7 +581,7 @@ impl PinProtocolV1 { pub fn process_hmac_secret( &self, hmac_secret_input: GetAssertionHmacSecretInput, - cred_random: &Option>, + cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { let GetAssertionHmacSecretInput { key_agreement, @@ -602,12 +595,7 @@ impl PinProtocolV1 { // 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), - } + encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } #[cfg(feature = "with_ctap2_1")] @@ -1195,14 +1183,6 @@ mod test { 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) - ); - let mut salt_enc = [0x00; 32]; let cred_random = [0xC9; 32]; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 911bdd7..ae38219 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -72,9 +72,10 @@ const AAGUID: usize = 7; const MIN_PIN_LENGTH: usize = 8; #[cfg(feature = "with_ctap2_1")] const MIN_PIN_LENGTH_RP_IDS: usize = 9; +const CRED_RANDOM_SECRET: usize = 10; // Different NUM_TAGS depending on the CTAP version make the storage incompatible, // so we use the maximum. -const NUM_TAGS: usize = 10; +const NUM_TAGS: usize = 11; const MAX_PIN_RETRIES: u8 = 8; #[cfg(feature = "with_ctap2_1")] @@ -108,6 +109,7 @@ enum Key { MinPinLength, #[cfg(feature = "with_ctap2_1")] MinPinLengthRpIds, + CredRandomSecret, } pub struct MasterKeys { @@ -166,6 +168,7 @@ impl StoreConfig for Config { MIN_PIN_LENGTH => add(Key::MinPinLength), #[cfg(feature = "with_ctap2_1")] MIN_PIN_LENGTH_RP_IDS => add(Key::MinPinLengthRpIds), + CRED_RANDOM_SECRET => add(Key::CredRandomSecret), _ => debug_assert!(false), } } @@ -230,6 +233,22 @@ impl PersistentStore { }) .unwrap(); } + + if self.store.find_one(&Key::CredRandomSecret).is_none() { + let cred_random_with_uv = rng.gen_uniform_u8x32(); + let cred_random_without_uv = rng.gen_uniform_u8x32(); + let mut cred_random = Vec::with_capacity(64); + cred_random.extend_from_slice(&cred_random_without_uv); + cred_random.extend_from_slice(&cred_random_with_uv); + self.store + .insert(StoreEntry { + tag: CRED_RANDOM_SECRET, + data: &cred_random, + sensitive: true, + }) + .unwrap(); + } + // TODO(jmichel): remove this when vendor command is in place #[cfg(not(test))] self.load_attestation_data_from_firmware(); @@ -411,6 +430,15 @@ impl PersistentStore { }) } + pub fn cred_random_secret(&self, has_uv: bool) -> Result<[u8; 32], Ctap2StatusCode> { + let (_, entry) = self.store.find_one(&Key::CredRandomSecret).unwrap(); + if entry.data.len() != 64 { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let offset = if has_uv { 32 } else { 0 }; + Ok(*array_ref![entry.data, offset, 32]) + } + pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { let data = match self.store.find_one(&Key::PinHash) { None => return Ok(None), @@ -721,7 +749,6 @@ mod test { 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, @@ -900,7 +927,6 @@ mod test { rp_id: String::from("example.com"), user_handle: vec![0x00], user_display_name: None, - cred_random: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), @@ -946,7 +972,6 @@ mod test { 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, @@ -968,7 +993,6 @@ mod test { 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, @@ -987,7 +1011,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - // Master keys stay the same between resets. + // Master keys stay the same between calls. 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); @@ -1003,6 +1027,28 @@ mod test { assert!(master_keys_3.hmac != master_hmac_key.as_slice()); } + #[test] + fn test_cred_random_secret() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + // Master keys stay the same between calls. + let cred_random_with_uv_1 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_1 = persistent_store.cred_random_secret(false).unwrap(); + let cred_random_with_uv_2 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_2 = persistent_store.cred_random_secret(false).unwrap(); + assert_eq!(cred_random_with_uv_1, cred_random_with_uv_2); + assert_eq!(cred_random_without_uv_1, cred_random_without_uv_2); + + // Master keys change after reset. This test may fail if the random generator produces the + // same keys. + persistent_store.reset(&mut rng).unwrap(); + let cred_random_with_uv_3 = persistent_store.cred_random_secret(true).unwrap(); + let cred_random_without_uv_3 = persistent_store.cred_random_secret(false).unwrap(); + assert!(cred_random_with_uv_1 != cred_random_with_uv_3); + assert!(cred_random_without_uv_1 != cred_random_without_uv_3); + } + #[test] fn test_pin_hash() { let mut rng = ThreadRng256 {}; @@ -1170,7 +1216,6 @@ mod test { 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, From fcbaf1e973f396eb2911bb9f53f37ff650f1b0ce Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 8 Dec 2020 19:31:56 +0100 Subject: [PATCH 064/192] fixes comments --- src/ctap/storage.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index ae38219..7be33f1 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -1011,7 +1011,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - // Master keys stay the same between calls. + // Master keys stay the same within the same CTAP reset cycle. 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); @@ -1032,7 +1032,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - // Master keys stay the same between calls. + // CredRandom secrets stay the same within the same CTAP reset cycle. let cred_random_with_uv_1 = persistent_store.cred_random_secret(true).unwrap(); let cred_random_without_uv_1 = persistent_store.cred_random_secret(false).unwrap(); let cred_random_with_uv_2 = persistent_store.cred_random_secret(true).unwrap(); @@ -1040,7 +1040,7 @@ mod test { assert_eq!(cred_random_with_uv_1, cred_random_with_uv_2); assert_eq!(cred_random_without_uv_1, cred_random_without_uv_2); - // Master keys change after reset. This test may fail if the random generator produces the + // CredRandom secrets change after reset. This test may fail if the random generator produces the // same keys. persistent_store.reset(&mut rng).unwrap(); let cred_random_with_uv_3 = persistent_store.cred_random_secret(true).unwrap(); From 8965c6c8fb2d3879dcb84ec9efdd2af8899947c0 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 8 Dec 2020 20:45:27 +0100 Subject: [PATCH 065/192] Rename and use HARDWARE_FAILURE error --- src/ctap/status_code.rs | 13 +++---------- src/ctap/storage.rs | 28 +++++++++++++--------------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 638caef..097d7ec 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -81,17 +81,10 @@ pub enum Ctap2StatusCode { /// This type of error is unexpected and the current state is undefined. CTAP2_ERR_VENDOR_INTERNAL_ERROR = 0xF2, - /// The persistent storage invariant is broken. + /// The hardware is malfunctioning. /// - /// There can be multiple reasons: - /// - The persistent storage has not been erased before its first usage. - /// - The persistent storage has been tempered with by a third party. - /// - The flash is malfunctioning (including the Tock driver). - /// - /// In the first 2 cases the persistent storage should be completely erased. If the error - /// reproduces, it may indicate a software bug or a hardware deficiency. In both cases, the - /// error should be reported. - CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE = 0xF3, + /// It may be possible that some of those errors are actually internal errors. + CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3, CTAP2_ERR_VENDOR_LAST = 0xFF, } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 40ec89a..0660b6c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -216,7 +216,7 @@ impl PersistentStore { && credential.user_handle == new_credential.user_handle { if old_key.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } old_key = Some(key); } @@ -231,7 +231,7 @@ impl PersistentStore { None => key::CREDENTIALS .take(MAX_SUPPORTED_RESIDENTIAL_KEYS) .find(|key| !keys.contains(key)) - .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE)?, + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, // This is an existing credential being updated, we reuse its key. Some(x) => x, }; @@ -298,7 +298,7 @@ impl PersistentStore { match self.store.find(key::GLOBAL_SIGNATURE_COUNTER)? { None => Ok(INITIAL_SIGNATURE_COUNTER), Some(value) if value.len() == 4 => Ok(u32::from_ne_bytes(*array_ref!(&value, 0, 4))), - Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } } @@ -317,9 +317,9 @@ impl PersistentStore { let master_keys = self .store .find(key::MASTER_KEYS)? - .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE)?; + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; if master_keys.len() != 64 { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } Ok(MasterKeys { encryption: *array_ref![master_keys, 0, 32], @@ -334,7 +334,7 @@ impl PersistentStore { Some(pin_hash) => pin_hash, }; if pin_hash.len() != PIN_AUTH_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } Ok(Some(*array_ref![pin_hash, 0, PIN_AUTH_LENGTH])) } @@ -354,7 +354,7 @@ impl PersistentStore { match self.store.find(key::PIN_RETRIES)? { None => Ok(MAX_PIN_RETRIES), Some(value) if value.len() == 1 => Ok(value[0]), - _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } } @@ -379,7 +379,7 @@ impl PersistentStore { match self.store.find(key::MIN_PIN_LENGTH)? { None => Ok(DEFAULT_MIN_PIN_LENGTH), Some(value) if value.len() == 1 => Ok(value[0]), - _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } } @@ -437,7 +437,7 @@ impl PersistentStore { key_material::ATTESTATION_PRIVATE_KEY_LENGTH ])) } - Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } } @@ -481,9 +481,9 @@ impl PersistentStore { let aaguid = self .store .find(key::AAGUID)? - .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE)?; + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; if aaguid.len() != key_material::AAGUID_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } Ok(*array_ref![aaguid, 0, key_material::AAGUID_LENGTH]) } @@ -521,9 +521,7 @@ impl From for Ctap2StatusCode { StoreError::InvalidArgument => Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR, // This error is not expected. The storage has been tempered with. We could erase the // storage. - StoreError::InvalidStorage => { - Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE - } + StoreError::InvalidStorage => Ctap2StatusCode::CTAP2_ERR_VENDOR_HARDWARE_FAILURE, // This error is not expected. The kernel is failing our syscalls. StoreError::StorageError => Ctap2StatusCode::CTAP1_ERR_OTHER, } @@ -566,7 +564,7 @@ impl<'a> IterCredentials<'a> { /// instead of statements only. fn unwrap(&mut self, x: Option) -> Option { if x.is_none() { - *self.result = Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INVALID_PERSISTENT_STORAGE); + *self.result = Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } x } From 776093a68b54bf82c249c54777184f5e2990ed35 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 9 Dec 2020 10:52:51 +0100 Subject: [PATCH 066/192] Find the next free key in a linear way --- src/ctap/storage.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0660b6c..f48b70a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -23,7 +23,6 @@ use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::INITIAL_SIGNATURE_COUNTER; #[cfg(feature = "with_ctap2_1")] 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; @@ -206,12 +205,19 @@ impl PersistentStore { ) -> Result<(), Ctap2StatusCode> { // Holds the key of the existing credential if this is an update. let mut old_key = None; - // Holds the unordered list of used keys. - let mut keys = Vec::new(); + let min_key = key::CREDENTIALS.start; + // Holds whether a key is used (indices are shifted by min_key). + let mut keys = vec![false; MAX_SUPPORTED_RESIDENTIAL_KEYS]; let mut iter_result = Ok(()); let iter = self.iter_credentials(&mut iter_result)?; for (key, credential) in iter { - keys.push(key); + if key < min_key + || key - min_key >= MAX_SUPPORTED_RESIDENTIAL_KEYS + || keys[key - min_key] + { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + keys[key - min_key] = true; if credential.rp_id == new_credential.rp_id && credential.user_handle == new_credential.user_handle { @@ -222,15 +228,17 @@ impl PersistentStore { } } iter_result?; - if old_key.is_none() && keys.len() >= MAX_SUPPORTED_RESIDENTIAL_KEYS { + if old_key.is_none() + && keys.iter().filter(|&&x| x).count() >= MAX_SUPPORTED_RESIDENTIAL_KEYS + { return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); } let key = match old_key { // This is a new credential being added, we need to allocate a free key. We choose the - // first available key. This is quadratic in the number of existing keys. + // first available key. None => key::CREDENTIALS .take(MAX_SUPPORTED_RESIDENTIAL_KEYS) - .find(|key| !keys.contains(key)) + .find(|key| !keys[key - min_key]) .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, // This is an existing credential being updated, we reuse its key. Some(x) => x, From 62dd088cd0ba55e0299dffe47fb20521c8874dee Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 9 Dec 2020 18:55:08 +0100 Subject: [PATCH 067/192] Add missing license header. --- src/ctap/apdu.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 949151e..bb94a91 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -1,3 +1,17 @@ +// 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. +// 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 alloc::vec::Vec; use byteorder::{BigEndian, ByteOrder}; use core::convert::TryFrom; From 863bf521deabf4fe02ac2ac665a907181c942c87 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 9 Dec 2020 19:05:03 +0100 Subject: [PATCH 068/192] removes extra sha256 --- src/ctap/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 67a0e27..55494a0 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -607,10 +607,8 @@ where ) -> Result<[u8; 32], Ctap2StatusCode> { let mut private_key_bytes = [0u8; 32]; private_key.to_bytes(&mut private_key_bytes); - let salt = crypto::sha256::Sha256::hash(&private_key_bytes); - // TODO(kaczmarczyck) KDF? hash salt together with rp_id_hash? let key = self.persistent_store.cred_random_secret(has_uv)?; - Ok(hmac_256::(&key, &salt[..])) + Ok(hmac_256::(&key, &private_key_bytes[..])) } // Processes the input of a get_assertion operation for a given credential From d942f0173f5cb88d395d96ca9d83a65835bd4916 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 9 Dec 2020 20:09:49 +0100 Subject: [PATCH 069/192] reverts block_len to a fixed number --- src/ctap/mod.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 55494a0..a00fecf 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -238,15 +238,14 @@ where let mut iv = [0; 16]; iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); - let block_len = 4; - let mut blocks = vec![[0u8; 16]; block_len]; + let mut blocks = [[0u8; 16]; 4]; blocks[0].copy_from_slice(&sk_bytes[..16]); blocks[1].copy_from_slice(&sk_bytes[16..]); blocks[2].copy_from_slice(&application[..16]); blocks[3].copy_from_slice(&application[16..]); cbc_encrypt(&aes_enc_key, iv, &mut blocks); - let mut encrypted_id = Vec::with_capacity(16 * (block_len + 3)); + let mut encrypted_id = Vec::with_capacity(0x70); encrypted_id.extend(&iv); for b in &blocks { encrypted_id.extend(b); @@ -280,9 +279,8 @@ where let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let mut iv = [0; 16]; iv.copy_from_slice(&credential_id[..16]); - let block_len = 4; - let mut blocks = vec![[0u8; 16]; block_len]; - for i in 0..block_len { + let mut blocks = [[0u8; 16]; 4]; + for i in 0..4 { blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); } @@ -608,7 +606,7 @@ where let mut private_key_bytes = [0u8; 32]; private_key.to_bytes(&mut private_key_bytes); let key = self.persistent_store.cred_random_secret(has_uv)?; - Ok(hmac_256::(&key, &private_key_bytes[..])) + Ok(hmac_256::(&key, &private_key_bytes)) } // Processes the input of a get_assertion operation for a given credential From 0da13cd61fc4485496227645c26bee34f8ad39ac Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 9 Dec 2020 20:43:06 -0800 Subject: [PATCH 070/192] De-deuplicate le length calculation --- src/ctap/apdu.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 70b17eb..7136084 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -172,9 +172,12 @@ impl TryFrom<&[u8]> for APDU { if payload.len() < extended_apdu_lc + 3 { return Err(ApduStatusCode::SW_WRONG_LENGTH); } - let extended_apdu_le_len: usize = match payload.len() - extended_apdu_lc - 3 { + + let possible_le_len = payload.len() as i32 - extended_apdu_lc as i32 - 3; + + let extended_apdu_le_len: usize = match possible_le_len { // There's some possible Le bytes at the end - 0..=3 => payload.len() - extended_apdu_lc - 3, + 0..=3 => possible_le_len as usize, // There are more bytes than even Le3 can consume, return an error _ => return Err(ApduStatusCode::SW_WRONG_LENGTH), }; From 6f1c63e9b8b451993e7f2ca3498849b4f2c1aab6 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Wed, 9 Dec 2020 21:06:49 -0800 Subject: [PATCH 071/192] Add test cases to cover different length scenarios --- src/ctap/ctap1.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 4d18846..ff07c0a 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -586,10 +586,25 @@ mod test { let key_handle = ctap_state .encrypt_key_handle(sk, &application, None) .unwrap(); - let mut message = - create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); + let mut message = create_authenticate_message( + &application, + Ctap1Flags::DontEnforceUpAndSign, + &key_handle, + ); - message.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); + message.push(0x00); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + assert!(response.is_ok()); + + message.push(0x00); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + assert!(response.is_ok()); + + message.push(0x00); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + assert!(response.is_ok()); + + message.push(0x00); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH)); } From c2de3e7ed9da29e43a9c6fee8dd085bbc4b3c52c Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Thu, 10 Dec 2020 10:02:48 +0100 Subject: [PATCH 072/192] Use a vscode workspace instead of local settings. --- .vscode/extensions.json | 7 ----- .vscode/settings.json | 27 ------------------- OpenSK.code-workspace | 57 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 34 deletions(-) delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/settings.json create mode 100644 OpenSK.code-workspace diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index cce0ec5..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "recommendations": [ - "davidanson.vscode-markdownlint", - "rust-lang.rust", - "ms-python.python" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 138097a..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "clang-format.fallbackStyle": "google", - "editor.detectIndentation": true, - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "editor.insertSpaces": true, - "editor.tabSize": 4, - "files.insertFinalNewline": true, - "files.trimTrailingWhitespace": true, - "rust-client.channel": "nightly", - // The toolchain is updated from time to time so let's make sure that RLS is updated too - "rust-client.updateOnStartup": true, - "rust.clippy_preference": "on", - // Try to make VSCode formating as close as possible to the Google style. - "python.formatting.provider": "yapf", - "python.formatting.yapfArgs": [ - "--style=yapf" - ], - "python.linting.enabled": true, - "python.linting.lintOnSave": true, - "python.linting.pylintEnabled": true, - "python.linting.pylintPath": "pylint", - "[python]": { - "editor.tabSize": 2 - }, -} diff --git a/OpenSK.code-workspace b/OpenSK.code-workspace new file mode 100644 index 0000000..c367c89 --- /dev/null +++ b/OpenSK.code-workspace @@ -0,0 +1,57 @@ +{ + "folders": [ + { + "name": "OpenSK", + "path": "." + }, + { + "name": "tock", + "path": "third_party/tock" + }, + { + "name": "libtock-rs", + "path": "third_party/libtock-rs" + }, + { + "name": "libtock-drivers", + "path": "third_party/libtock-drivers" + } + ], + "settings": { + "clang-format.fallbackStyle": "google", + "editor.detectIndentation": true, + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.insertSpaces": true, + "editor.tabSize": 4, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + // Ensure we use the toolchain we set in rust-toolchain file + "rust-client.channel": "default", + // The toolchain is updated from time to time so let's make sure that RLS is updated too + "rust-client.updateOnStartup": true, + "rust.clippy_preference": "on", + "rust.target": "thumbv7em-none-eabi", + "rust.all_targets": false, + // Try to make VSCode formating as close as possible to the Google style. + "python.formatting.provider": "yapf", + "python.formatting.yapfArgs": [ + "--style=yapf" + ], + "python.linting.enabled": true, + "python.linting.lintOnSave": true, + "python.linting.pylintEnabled": true, + "python.linting.pylintPath": "pylint", + "[python]": { + "editor.tabSize": 2 + }, + }, + "extensions": { + "recommendations": [ + "davidanson.vscode-markdownlint", + "rust-lang.rust", + "ms-python.python" + ] + } +} From 4253854cf145722839fe9c53331ae8d418e7a171 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 13:06:05 +0100 Subject: [PATCH 073/192] Remove ram_storage feature We don't need to build a production key without persistent storage. Tests and fuzzing continue to use the std feature to use the RAM implementation (that does sanity checks). --- .github/workflows/cargo_check.yml | 6 ------ Cargo.toml | 1 - deploy.py | 8 -------- fuzz/fuzz_helper/Cargo.toml | 2 +- libraries/persistent_store/src/lib.rs | 2 ++ run_desktop_tests.sh | 1 - src/ctap/storage.rs | 18 ++++++------------ src/embedded_flash/mod.rs | 4 ++-- 8 files changed, 11 insertions(+), 31 deletions(-) diff --git a/.github/workflows/cargo_check.yml b/.github/workflows/cargo_check.yml index 6676e16..fd39614 100644 --- a/.github/workflows/cargo_check.yml +++ b/.github/workflows/cargo_check.yml @@ -66,12 +66,6 @@ jobs: command: check args: --target thumbv7em-none-eabi --release --features debug_allocations - - name: Check OpenSK ram_storage - uses: actions-rs/cargo@v1 - with: - command: check - args: --target thumbv7em-none-eabi --release --features ram_storage - - name: Check OpenSK verbose uses: actions-rs/cargo@v1 with: diff --git a/Cargo.toml b/Cargo.toml index 5b01795..ed293cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,6 @@ debug_allocations = ["lang_items/debug_allocations"] debug_ctap = ["crypto/derive_debug", "libtock_drivers/debug_ctap"] panic_console = ["lang_items/panic_console"] std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std", "persistent_store/std"] -ram_storage = [] verbose = ["debug_ctap", "libtock_drivers/verbose_usb"] with_ctap1 = ["crypto/with_ctap1"] with_ctap2_1 = [] diff --git a/deploy.py b/deploy.py index e1ec38f..b28e645 100755 --- a/deploy.py +++ b/deploy.py @@ -863,14 +863,6 @@ if __name__ == "__main__": "This is useful to allow flashing multiple OpenSK authenticators " "in a row without them being considered clones."), ) - main_parser.add_argument( - "--no-persistent-storage", - action="append_const", - const="ram_storage", - dest="features", - help=("Compiles and installs the OpenSK application without persistent " - "storage (i.e. unplugging the key will reset the key)."), - ) main_parser.add_argument( "--elf2tab-output", diff --git a/fuzz/fuzz_helper/Cargo.toml b/fuzz/fuzz_helper/Cargo.toml index 3b70f43..2f2a5c1 100644 --- a/fuzz/fuzz_helper/Cargo.toml +++ b/fuzz/fuzz_helper/Cargo.toml @@ -10,5 +10,5 @@ arrayref = "0.3.6" libtock_drivers = { path = "../../third_party/libtock-drivers" } crypto = { path = "../../libraries/crypto", features = ['std'] } cbor = { path = "../../libraries/cbor", features = ['std'] } -ctap2 = { path = "../..", features = ['std', 'ram_storage'] } +ctap2 = { path = "../..", features = ['std'] } lang_items = { path = "../../third_party/lang-items", features = ['std'] } diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index 9b87bd4..c8be44b 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -348,6 +348,7 @@ #[macro_use] extern crate alloc; +#[cfg(feature = "std")] mod buffer; #[cfg(feature = "std")] mod driver; @@ -357,6 +358,7 @@ mod model; mod storage; mod store; +#[cfg(feature = "std")] pub use self::buffer::{BufferCorruptFunction, BufferOptions, BufferStorage}; #[cfg(feature = "std")] pub use self::driver::{ diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index 7f3b8f3..2e80b3d 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -48,7 +48,6 @@ cargo check --release --target=thumbv7em-none-eabi --features with_ctap2_1 cargo check --release --target=thumbv7em-none-eabi --features debug_ctap cargo check --release --target=thumbv7em-none-eabi --features panic_console cargo check --release --target=thumbv7em-none-eabi --features debug_allocations -cargo check --release --target=thumbv7em-none-eabi --features ram_storage cargo check --release --target=thumbv7em-none-eabi --features verbose cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ctap1 cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ctap1,panic_console,debug_allocations,verbose diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 87c4c6c..5e42ec7 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -31,9 +31,9 @@ use cbor::cbor_array_vec; use core::convert::TryInto; use crypto::rng256::Rng256; -#[cfg(any(test, feature = "ram_storage"))] +#[cfg(feature = "std")] type Storage = persistent_store::BufferStorage; -#[cfg(not(any(test, feature = "ram_storage")))] +#[cfg(not(feature = "std"))] type Storage = crate::embedded_flash::SyscallStorage; // Those constants may be modified before compilation to tune the behavior of the key. @@ -54,9 +54,6 @@ type Storage = crate::embedded_flash::SyscallStorage; // We have: I = (P * 4084 - 5107 - K * S) / 8 * 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 = 3; -#[cfg(not(feature = "ram_storage"))] const NUM_PAGES: usize = 20; const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; @@ -92,9 +89,9 @@ impl PersistentStore { /// /// 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")))] + #[cfg(not(feature = "std"))] let storage = PersistentStore::new_prod_storage(); - #[cfg(any(test, feature = "ram_storage"))] + #[cfg(feature = "std")] let storage = PersistentStore::new_test_storage(); let mut store = PersistentStore { store: persistent_store::Store::new(storage).ok().unwrap(), @@ -104,17 +101,14 @@ impl PersistentStore { } /// Creates a syscall storage in flash. - #[cfg(not(any(test, feature = "ram_storage")))] + #[cfg(not(feature = "std"))] fn new_prod_storage() -> Storage { Storage::new(NUM_PAGES).unwrap() } /// Creates a buffer storage in RAM. - #[cfg(any(test, feature = "ram_storage"))] + #[cfg(feature = "std")] 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 = persistent_store::BufferOptions { diff --git a/src/embedded_flash/mod.rs b/src/embedded_flash/mod.rs index b16504b..862b37e 100644 --- a/src/embedded_flash/mod.rs +++ b/src/embedded_flash/mod.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(not(any(test, feature = "ram_storage")))] +#[cfg(not(feature = "std"))] mod syscall; -#[cfg(not(any(test, feature = "ram_storage")))] +#[cfg(not(feature = "std"))] pub use self::syscall::SyscallStorage; From ae08221cdb887ed4ffc831b833c17c97201743eb Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 13:31:25 +0100 Subject: [PATCH 074/192] Add latency example --- deploy.py | 6 ++ examples/store_latency.rs | 126 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 examples/store_latency.rs diff --git a/deploy.py b/deploy.py index e1ec38f..42f3e44 100755 --- a/deploy.py +++ b/deploy.py @@ -907,6 +907,12 @@ if __name__ == "__main__": const="crypto_bench", help=("Compiles and installs the crypto_bench example that benchmarks " "the performance of the cryptographic algorithms on the board.")) + apps_group.add_argument( + "--store_latency", + dest="application", + action="store_const", + const="store_latency", + help=("Compiles and installs the store_latency example.")) apps_group.add_argument( "--panic_test", dest="application", diff --git a/examples/store_latency.rs b/examples/store_latency.rs new file mode 100644 index 0000000..f85d14d --- /dev/null +++ b/examples/store_latency.rs @@ -0,0 +1,126 @@ +// Copyright 2019-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. +// 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. + +#![no_std] + +extern crate alloc; +extern crate lang_items; + +use alloc::vec; +use core::fmt::Write; +use ctap2::embedded_flash::SyscallStorage; +use libtock_drivers::console::Console; +use libtock_drivers::timer::{self, Duration, Timer, Timestamp}; +use persistent_store::{Storage, Store}; + +fn timestamp(timer: &Timer) -> Timestamp { + Timestamp::::from_clock_value(timer.get_current_clock().ok().unwrap()) +} + +fn measure(timer: &Timer, operation: impl FnOnce() -> T) -> (T, Duration) { + let before = timestamp(timer); + let result = operation(); + let after = timestamp(timer); + (result, after - before) +} + +// Only use one store at a time. +unsafe fn boot_store(num_pages: usize, erase: bool) -> Store { + let mut storage = SyscallStorage::new(num_pages).unwrap(); + if erase { + for page in 0..storage.num_pages() { + storage.erase_page(page).unwrap(); + } + } + Store::new(storage).ok().unwrap() +} + +fn compute_latency(timer: &Timer, num_pages: usize, key_increment: usize, word_length: usize) { + let mut console = Console::new(); + writeln!( + console, + "\nLatency for num_pages={} key_increment={} word_length={}.", + num_pages, key_increment, word_length + ) + .unwrap(); + + let mut store = unsafe { boot_store(num_pages, true) }; + let total_capacity = store.capacity().unwrap().total(); + + // Burn N words to align the end of the user capacity with the virtual capacity. + store.insert(0, &vec![0; 4 * (num_pages - 1)]).unwrap(); + store.remove(0).unwrap(); + + // Insert entries until there is space for one more. + let count = total_capacity / (1 + word_length) - 1; + let ((), time) = measure(timer, || { + for i in 0..count { + let key = 1 + key_increment * i; + // For some reason the kernel sometimes fails. + while store.insert(key, &vec![0; 4 * word_length]).is_err() { + // We never enter this loop in practice, but we still need it for the kernel. + writeln!(console, "Retry insert.").unwrap(); + } + } + }); + writeln!(console, "Setup: {:.1}ms for {} entries.", time.ms(), count).unwrap(); + + // Measure latency of insert. + let key = 1 + key_increment * count; + let ((), time) = measure(&timer, || { + store.insert(key, &vec![0; 4 * word_length]).unwrap() + }); + writeln!(console, "Insert: {:.1}ms.", time.ms()).unwrap(); + + // Measure latency of boot. + let (mut store, time) = measure(&timer, || unsafe { boot_store(num_pages, false) }); + writeln!(console, "Boot: {:.1}ms.", time.ms()).unwrap(); + + // Measure latency of remove. + let ((), time) = measure(&timer, || store.remove(key).unwrap()); + writeln!(console, "Remove: {:.1}ms.", time.ms()).unwrap(); + + // Measure latency of compaction. + let length = total_capacity + num_pages - store.lifetime().unwrap().used(); + if length > 0 { + // Fill the store such that compaction is needed for one word. + store.insert(0, &vec![0; 4 * (length - 1)]).unwrap(); + store.remove(0).unwrap(); + } + let ((), time) = measure(timer, || store.prepare(1).unwrap()); + writeln!(console, "Compaction: {:.1}ms.", time.ms()).unwrap(); +} + +fn main() { + let mut with_callback = timer::with_callback(|_, _| {}); + let timer = with_callback.init().ok().unwrap(); + + writeln!(Console::new(), "\nRunning 4 tests...").unwrap(); + // Those non-overwritten 50 words entries simulate credentials. + compute_latency(&timer, 3, 1, 50); + compute_latency(&timer, 20, 1, 50); + // Those overwritten 1 word entries simulate counters. + compute_latency(&timer, 3, 0, 1); + compute_latency(&timer, 6, 0, 1); + writeln!(Console::new(), "\nDone.").unwrap(); + + // Results on nrf52840dk: + // + // | Pages | Overwrite | Length | Boot | Compaction | Insert | Remove | + // | ----- | --------- | --------- | ------- | ---------- | ------ | ------- | + // | 3 | no | 50 words | 2.0 ms | 132.5 ms | 4.8 ms | 1.2 ms | + // | 20 | no | 50 words | 7.4 ms | 135.5 ms | 10.2 ms | 3.9 ms | + // | 3 | yes | 1 word | 21.9 ms | 94.5 ms | 12.4 ms | 5.9 ms | + // | 6 | yes | 1 word | 55.2 ms | 100.8 ms | 24.8 ms | 12.1 ms | +} From 19ebacec15d808d4fd05e30b0ba93e214ffd526b Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 13:36:33 +0100 Subject: [PATCH 075/192] Do not use delay_map anymore This permits to avoid copies. Before we used to do one copy per storage operation. Now we do one copy per store operation. --- libraries/persistent_store/fuzz/src/store.rs | 162 +++---------------- libraries/persistent_store/src/driver.rs | 81 ++-------- 2 files changed, 38 insertions(+), 205 deletions(-) diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs index c18eb51..3113f77 100644 --- a/libraries/persistent_store/fuzz/src/store.rs +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -26,6 +26,9 @@ use std::convert::TryInto; // NOTE: We should be able to improve coverage by only checking the last operation. Because // operations before the last could be checked with a shorter entropy. +// NOTE: Maybe we should split the fuzz target in smaller parts (like one per init). We should also +// name the fuzz targets with action names. + /// Checks the store against a sequence of manipulations. /// /// The entropy to generate the sequence of manipulation should be provided in `data`. Debugging @@ -181,7 +184,7 @@ impl<'a> Fuzzer<'a> { println!("Power on the store."); } self.increment(StatKey::PowerOnCount); - let interruption = self.interruption(driver.delay_map()); + let interruption = self.interruption(driver.count_operations()); match driver.partial_power_on(interruption) { Err((storage, _)) if self.init.is_dirty() => { self.entropy.consume_all(); @@ -198,7 +201,7 @@ impl<'a> Fuzzer<'a> { if self.debug { println!("{:?}", operation); } - let interruption = self.interruption(driver.delay_map(&operation)); + let interruption = self.interruption(driver.count_operations(&operation)); match driver.partial_apply(operation, interruption) { Err((store, _)) if self.init.is_dirty() => { self.entropy.consume_all(); @@ -334,59 +337,48 @@ impl<'a> Fuzzer<'a> { /// Generates an interruption. /// - /// The `delay_map` describes the number of modified bits by the upcoming sequence of store - /// operations. - // TODO(ia0): We use too much CPU to compute the delay map. We should be able to just count the - // number of storage operations by checking the remaining delay. We can then use the entropy - // directly from the corruption function because it's called at most once. - fn interruption( - &mut self, - delay_map: Result, (usize, BufferStorage)>, - ) -> StoreInterruption { + /// The `max_delay` describes the number of storage operations. + fn interruption(&mut self, max_delay: Option) -> StoreInterruption { if self.init.is_dirty() { // We only test that the store can power on without crashing. If it would get // interrupted then it's like powering up with a different initial state, which would be // tested with another fuzzing input. return StoreInterruption::none(); } - let delay_map = match delay_map { - Ok(x) => x, - Err((delay, storage)) => { - print!("{}", storage); - panic!("delay={}", delay); - } + let max_delay = match max_delay { + Some(x) => x, + None => return StoreInterruption::none(), }; - let delay = self.entropy.read_range(0, delay_map.len() - 1); - let mut complete_bits = BitStack::default(); - for _ in 0..delay_map[delay] { - complete_bits.push(self.entropy.read_bit()); - } + let delay = self.entropy.read_range(0, max_delay); if self.debug { - if delay == delay_map.len() - 1 { - assert!(complete_bits.is_empty()); + if delay == max_delay { println!("Do not interrupt."); } else { - println!( - "Interrupt after {} operations with complete mask {}.", - delay, complete_bits - ); + println!("Interrupt after {} operations.", delay); } } - if delay < delay_map.len() - 1 { + if delay < max_delay { self.increment(StatKey::InterruptionCount); } let corrupt = Box::new(move |old: &mut [u8], new: &[u8]| { + let mut count = 0; + let mut total = 0; for (old, new) in old.iter_mut().zip(new.iter()) { for bit in 0..8 { let mask = 1 << bit; if *old & mask == *new & mask { continue; } - if complete_bits.pop().unwrap() { + total += 1; + if self.entropy.read_bit() { + count += 1; *old ^= mask; } } } + if self.debug { + println!("Flip {} bits out of {}.", count, total); + } }); StoreInterruption { delay, corrupt } } @@ -432,113 +424,3 @@ impl Init { } } } - -/// Compact stack of bits. -// NOTE: This would probably go away once the delay map is simplified. -#[derive(Default, Clone, Debug)] -struct BitStack { - /// Bits stored in little-endian (for bytes and bits). - /// - /// The last byte only contains `len` bits. - data: Vec, - - /// Number of bits stored in the last byte. - /// - /// It is 0 if the last byte is full, not 8. - len: usize, -} - -impl BitStack { - /// Returns whether the stack is empty. - fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns the length of the stack. - fn len(&self) -> usize { - if self.len == 0 { - 8 * self.data.len() - } else { - 8 * (self.data.len() - 1) + self.len - } - } - - /// Pushes a bit to the stack. - fn push(&mut self, value: bool) { - if self.len == 0 { - self.data.push(0); - } - if value { - *self.data.last_mut().unwrap() |= 1 << self.len; - } - self.len += 1; - if self.len == 8 { - self.len = 0; - } - } - - /// Pops a bit from the stack. - fn pop(&mut self) -> Option { - if self.len == 0 { - if self.data.is_empty() { - return None; - } - self.len = 8; - } - self.len -= 1; - let result = self.data.last().unwrap() & 1 << self.len; - if self.len == 0 { - self.data.pop().unwrap(); - } - Some(result != 0) - } -} - -impl std::fmt::Display for BitStack { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - let mut bits = self.clone(); - while let Some(bit) = bits.pop() { - write!(f, "{}", bit as usize)?; - } - write!(f, " ({} bits)", self.len())?; - Ok(()) - } -} - -#[test] -fn bit_stack_ok() { - let mut bits = BitStack::default(); - - assert_eq!(bits.pop(), None); - - bits.push(true); - assert_eq!(bits.pop(), Some(true)); - assert_eq!(bits.pop(), None); - - bits.push(false); - assert_eq!(bits.pop(), Some(false)); - assert_eq!(bits.pop(), None); - - bits.push(true); - bits.push(false); - assert_eq!(bits.pop(), Some(false)); - assert_eq!(bits.pop(), Some(true)); - assert_eq!(bits.pop(), None); - - bits.push(false); - bits.push(true); - assert_eq!(bits.pop(), Some(true)); - assert_eq!(bits.pop(), Some(false)); - assert_eq!(bits.pop(), None); - - let n = 27; - for i in 0..n { - assert_eq!(bits.len(), i); - bits.push(true); - } - for i in (0..n).rev() { - assert_eq!(bits.pop(), Some(true)); - assert_eq!(bits.len(), i); - } - assert_eq!(bits.pop(), None); -} diff --git a/libraries/persistent_store/src/driver.rs b/libraries/persistent_store/src/driver.rs index e529274..f17f576 100644 --- a/libraries/persistent_store/src/driver.rs +++ b/libraries/persistent_store/src/driver.rs @@ -311,31 +311,15 @@ impl StoreDriverOff { }) } - /// Returns a mapping from delay time to number of modified bits. + /// Returns the number of storage operations to power on. /// - /// For example if the `i`-th value is `n`, it means that the `i`-th operation modifies `n` bits - /// in the storage. For convenience, the vector always ends with `0` for one past the last - /// operation. This permits to choose a random index in the vector and then a random set of bit - /// positions among the number of modified bits to simulate any possible corruption (including - /// no corruption with the last index). - pub fn delay_map(&self) -> Result, (usize, BufferStorage)> { - let mut result = Vec::new(); - loop { - let delay = result.len(); - let mut storage = self.storage.clone(); - storage.arm_interruption(delay); - match Store::new(storage) { - Err((StoreError::StorageError, x)) => storage = x, - Err((StoreError::InvalidStorage, mut storage)) => { - storage.reset_interruption(); - return Err((delay, storage)); - } - Ok(_) | Err(_) => break, - } - result.push(count_modified_bits(&mut storage)); - } - result.push(0); - Ok(result) + /// Returns `None` if the store cannot power on successfully. + pub fn count_operations(&self) -> Option { + let initial_delay = usize::MAX; + let mut storage = self.storage.clone(); + storage.arm_interruption(initial_delay); + let mut store = Store::new(storage).ok()?; + Some(initial_delay - store.storage_mut().disarm_interruption()) } } @@ -422,29 +406,15 @@ impl StoreDriverOn { }) } - /// Returns a mapping from delay time to number of modified bits. + /// Returns the number of storage operations to apply a store operation. /// - /// See the documentation of [`StoreDriverOff::delay_map`] for details. - /// - /// [`StoreDriverOff::delay_map`]: struct.StoreDriverOff.html#method.delay_map - pub fn delay_map( - &self, - operation: &StoreOperation, - ) -> Result, (usize, BufferStorage)> { - let mut result = Vec::new(); - loop { - let delay = result.len(); - let mut store = self.store.clone(); - store.storage_mut().arm_interruption(delay); - match store.apply(operation).1 { - Err(StoreError::StorageError) => (), - Err(StoreError::InvalidStorage) => return Err((delay, store.extract_storage())), - Ok(()) | Err(_) => break, - } - result.push(count_modified_bits(store.storage_mut())); - } - result.push(0); - Ok(result) + /// Returns `None` if the store cannot apply the operation successfully. + pub fn count_operations(&self, operation: &StoreOperation) -> Option { + let initial_delay = usize::MAX; + let mut store = self.store.clone(); + store.storage_mut().arm_interruption(initial_delay); + store.apply(operation).1.ok()?; + Some(initial_delay - store.storage_mut().disarm_interruption()) } /// Powers off the store. @@ -629,22 +599,3 @@ impl<'a> StoreInterruption<'a> { } } } - -/// Counts the number of bits modified by an interrupted operation. -/// -/// # Panics -/// -/// Panics if an interruption did not trigger. -fn count_modified_bits(storage: &mut BufferStorage) -> usize { - let mut modified_bits = 0; - storage.corrupt_operation(Box::new(|before, after| { - modified_bits = before - .iter() - .zip(after.iter()) - .map(|(x, y)| (x ^ y).count_ones() as usize) - .sum(); - })); - // We should never write the same slice or erase an erased page. - assert!(modified_bits > 0); - modified_bits -} From d4b20a5acc65d10b5b09b67b9a26b9cf8b6cd552 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 16:14:17 +0100 Subject: [PATCH 076/192] Fix linked_list_allocator version to fix build They released version 0.8.7 today which breaks our assumption that Heap::empty is callable in const context. --- third_party/lang-items/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/lang-items/Cargo.toml b/third_party/lang-items/Cargo.toml index 39ffbf0..d4b3e33 100644 --- a/third_party/lang-items/Cargo.toml +++ b/third_party/lang-items/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" [dependencies] libtock_core = { path = "../../third_party/libtock-rs/core", default-features = false, features = ["alloc_init", "custom_panic_handler", "custom_alloc_error_handler"] } libtock_drivers = { path = "../libtock-drivers" } -linked_list_allocator = { version = "0.8.1", default-features = false } +linked_list_allocator = { version = "=0.8.1", default-features = false } [features] debug_allocations = [] From 7a641d639199c108007149eb01cedd26e4e0e655 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 16:20:26 +0100 Subject: [PATCH 077/192] Use the new const_mut_refs default feature of linked_list_allocator This is necessary for Heap::empty() to be const. --- third_party/lang-items/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third_party/lang-items/Cargo.toml b/third_party/lang-items/Cargo.toml index d4b3e33..5d109f3 100644 --- a/third_party/lang-items/Cargo.toml +++ b/third_party/lang-items/Cargo.toml @@ -11,7 +11,7 @@ edition = "2018" [dependencies] libtock_core = { path = "../../third_party/libtock-rs/core", default-features = false, features = ["alloc_init", "custom_panic_handler", "custom_alloc_error_handler"] } libtock_drivers = { path = "../libtock-drivers" } -linked_list_allocator = { version = "=0.8.1", default-features = false } +linked_list_allocator = { version = "0.8.7", default-features = false, features = ["const_mut_refs"] } [features] debug_allocations = [] From 869e93234952fd09c5e85acd61430c5d079cb1fa Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 16:44:00 +0100 Subject: [PATCH 078/192] Add asserts to make sure we compact --- examples/store_latency.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/store_latency.rs b/examples/store_latency.rs index f85d14d..e664964 100644 --- a/examples/store_latency.rs +++ b/examples/store_latency.rs @@ -57,10 +57,14 @@ fn compute_latency(timer: &Timer, num_pages: usize, key_increment: usize, word_l let mut store = unsafe { boot_store(num_pages, true) }; let total_capacity = store.capacity().unwrap().total(); + assert_eq!(store.capacity().unwrap().used(), 0); + assert_eq!(store.lifetime().unwrap().used(), 0); // Burn N words to align the end of the user capacity with the virtual capacity. store.insert(0, &vec![0; 4 * (num_pages - 1)]).unwrap(); store.remove(0).unwrap(); + assert_eq!(store.capacity().unwrap().used(), 0); + assert_eq!(store.lifetime().unwrap().used(), num_pages); // Insert entries until there is space for one more. let count = total_capacity / (1 + word_length) - 1; @@ -82,6 +86,10 @@ fn compute_latency(timer: &Timer, num_pages: usize, key_increment: usize, word_l store.insert(key, &vec![0; 4 * word_length]).unwrap() }); writeln!(console, "Insert: {:.1}ms.", time.ms()).unwrap(); + assert_eq!( + store.lifetime().unwrap().used(), + num_pages + (1 + count) * (1 + word_length) + ); // Measure latency of boot. let (mut store, time) = measure(&timer, || unsafe { boot_store(num_pages, false) }); @@ -98,8 +106,11 @@ fn compute_latency(timer: &Timer, num_pages: usize, key_increment: usize, word_l store.insert(0, &vec![0; 4 * (length - 1)]).unwrap(); store.remove(0).unwrap(); } + assert!(store.capacity().unwrap().remaining() > 0); + assert_eq!(store.lifetime().unwrap().used(), num_pages + total_capacity); let ((), time) = measure(timer, || store.prepare(1).unwrap()); writeln!(console, "Compaction: {:.1}ms.", time.ms()).unwrap(); + assert!(store.lifetime().unwrap().used() > total_capacity + num_pages); } fn main() { From 371b8af22425689dc5af6ea4a8745a4cc86a1d8c Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 18:04:25 +0100 Subject: [PATCH 079/192] Move choice between prod and test storage to embedded_flash module This way all users of storage can share the logic to choose between flash or RAM storage depending on the "std" feature. This is needed because the store_latency example assumes flash storage but is built when running `cargo test --features=std`. --- examples/store_latency.rs | 11 ++++++----- src/ctap/storage.rs | 32 ++------------------------------ src/embedded_flash/mod.rs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/examples/store_latency.rs b/examples/store_latency.rs index e664964..c3f3e71 100644 --- a/examples/store_latency.rs +++ b/examples/store_latency.rs @@ -19,10 +19,10 @@ extern crate lang_items; use alloc::vec; use core::fmt::Write; -use ctap2::embedded_flash::SyscallStorage; +use ctap2::embedded_flash::{new_storage, Storage}; use libtock_drivers::console::Console; use libtock_drivers::timer::{self, Duration, Timer, Timestamp}; -use persistent_store::{Storage, Store}; +use persistent_store::Store; fn timestamp(timer: &Timer) -> Timestamp { Timestamp::::from_clock_value(timer.get_current_clock().ok().unwrap()) @@ -36,10 +36,11 @@ fn measure(timer: &Timer, operation: impl FnOnce() -> T) -> (T, Duration } // Only use one store at a time. -unsafe fn boot_store(num_pages: usize, erase: bool) -> Store { - let mut storage = SyscallStorage::new(num_pages).unwrap(); +unsafe fn boot_store(num_pages: usize, erase: bool) -> Store { + let mut storage = new_storage(num_pages); if erase { - for page in 0..storage.num_pages() { + for page in 0..num_pages { + use persistent_store::Storage; storage.erase_page(page).unwrap(); } } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 5e42ec7..5f17052 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -21,6 +21,7 @@ use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::INITIAL_SIGNATURE_COUNTER; +use crate::embedded_flash::{new_storage, Storage}; #[cfg(feature = "with_ctap2_1")] use alloc::string::String; use alloc::vec; @@ -31,11 +32,6 @@ use cbor::cbor_array_vec; use core::convert::TryInto; use crypto::rng256::Rng256; -#[cfg(feature = "std")] -type Storage = persistent_store::BufferStorage; -#[cfg(not(feature = "std"))] -type Storage = crate::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 3 and at most what the flash can hold. There should be no @@ -89,10 +85,7 @@ impl PersistentStore { /// /// This should be at most one instance of persistent store per program lifetime. pub fn new(rng: &mut impl Rng256) -> PersistentStore { - #[cfg(not(feature = "std"))] - let storage = PersistentStore::new_prod_storage(); - #[cfg(feature = "std")] - let storage = PersistentStore::new_test_storage(); + let storage = new_storage(NUM_PAGES); let mut store = PersistentStore { store: persistent_store::Store::new(storage).ok().unwrap(), }; @@ -100,27 +93,6 @@ impl PersistentStore { store } - /// Creates a syscall storage in flash. - #[cfg(not(feature = "std"))] - fn new_prod_storage() -> Storage { - Storage::new(NUM_PAGES).unwrap() - } - - /// Creates a buffer storage in RAM. - #[cfg(feature = "std")] - fn new_test_storage() -> Storage { - const PAGE_SIZE: usize = 0x1000; - let store = vec![0xff; NUM_PAGES * PAGE_SIZE].into_boxed_slice(); - let options = persistent_store::BufferOptions { - word_size: 4, - page_size: PAGE_SIZE, - max_word_writes: 2, - max_page_erases: 10000, - strict_mode: true, - }; - Storage::new(store, options) - } - /// Initializes the store by creating missing objects. fn init(&mut self, rng: &mut impl Rng256) -> Result<(), Ctap2StatusCode> { // Generate and store the master keys if they are missing. diff --git a/src/embedded_flash/mod.rs b/src/embedded_flash/mod.rs index 862b37e..e307a55 100644 --- a/src/embedded_flash/mod.rs +++ b/src/embedded_flash/mod.rs @@ -17,3 +17,36 @@ mod syscall; #[cfg(not(feature = "std"))] pub use self::syscall::SyscallStorage; + +/// Storage definition for production. +#[cfg(not(feature = "std"))] +mod prod { + pub type Storage = super::SyscallStorage; + + pub fn new_storage(num_pages: usize) -> Storage { + Storage::new(num_pages).unwrap() + } +} +#[cfg(not(feature = "std"))] +pub use self::prod::{new_storage, Storage}; + +/// Storage definition for testing. +#[cfg(feature = "std")] +mod test { + pub type Storage = persistent_store::BufferStorage; + + pub fn new_storage(num_pages: usize) -> Storage { + const PAGE_SIZE: usize = 0x1000; + let store = vec![0xff; num_pages * PAGE_SIZE].into_boxed_slice(); + let options = persistent_store::BufferOptions { + word_size: 4, + page_size: PAGE_SIZE, + max_word_writes: 2, + max_page_erases: 10000, + strict_mode: true, + }; + Storage::new(store, options) + } +} +#[cfg(feature = "std")] +pub use self::test::{new_storage, Storage}; From edcc206e9d1cfbc4616d4ef6adb33c4f24ebba18 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 18:29:31 +0100 Subject: [PATCH 080/192] Make store operations constant wrt flash operations --- examples/store_latency.rs | 14 +- libraries/persistent_store/src/format.rs | 8 + libraries/persistent_store/src/lib.rs | 1 + libraries/persistent_store/src/store.rs | 260 +++++++++++++---------- src/ctap/storage.rs | 2 +- 5 files changed, 162 insertions(+), 123 deletions(-) diff --git a/examples/store_latency.rs b/examples/store_latency.rs index c3f3e71..fd6f504 100644 --- a/examples/store_latency.rs +++ b/examples/store_latency.rs @@ -124,15 +124,15 @@ fn main() { compute_latency(&timer, 20, 1, 50); // Those overwritten 1 word entries simulate counters. compute_latency(&timer, 3, 0, 1); - compute_latency(&timer, 6, 0, 1); + compute_latency(&timer, 20, 0, 1); writeln!(Console::new(), "\nDone.").unwrap(); // Results on nrf52840dk: // - // | Pages | Overwrite | Length | Boot | Compaction | Insert | Remove | - // | ----- | --------- | --------- | ------- | ---------- | ------ | ------- | - // | 3 | no | 50 words | 2.0 ms | 132.5 ms | 4.8 ms | 1.2 ms | - // | 20 | no | 50 words | 7.4 ms | 135.5 ms | 10.2 ms | 3.9 ms | - // | 3 | yes | 1 word | 21.9 ms | 94.5 ms | 12.4 ms | 5.9 ms | - // | 6 | yes | 1 word | 55.2 ms | 100.8 ms | 24.8 ms | 12.1 ms | + // | Pages | Overwrite | Length | Boot | Compaction | Insert | Remove | + // | ----- | --------- | --------- | ------- | ---------- | ------ | ------ | + // | 3 | no | 50 words | 2.0 ms | 132.8 ms | 4.3 ms | 1.2 ms | + // | 20 | no | 50 words | 7.8 ms | 135.7 ms | 9.9 ms | 4.0 ms | + // | 3 | yes | 1 word | 19.6 ms | 90.8 ms | 4.7 ms | 2.3 ms | + // | 20 | yes | 1 word | 183.3 ms | 90.9 ms | 4.8 ms | 2.3 ms | } diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index 1f87ef3..8de88e4 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -1077,4 +1077,12 @@ mod tests { 0xff800000 ); } + + #[test] + fn position_offsets_fit_in_a_halfword() { + // The store stores the entry positions as their offset from the head. Those offsets are + // represented as u16. The bound below is a large over-approximation of the maximal offset + // but it already fits. + assert_eq!((MAX_PAGE_INDEX + 1) * MAX_VIRT_PAGE_SIZE, 0xff80); + } } diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index c8be44b..06a3a68 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -344,6 +344,7 @@ //! storage, the store is checked not to crash. #![cfg_attr(not(feature = "std"), no_std)] +#![feature(try_trait)] #[macro_use] extern crate alloc; diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index 2559485..ba7ab4b 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -23,8 +23,11 @@ use crate::{usize_to_nat, Nat, Storage, StorageError, StorageIndex}; pub use crate::{ BufferStorage, StoreDriver, StoreDriverOff, StoreDriverOn, StoreInterruption, StoreInvariant, }; +use alloc::boxed::Box; use alloc::vec::Vec; use core::cmp::{max, min, Ordering}; +use core::convert::TryFrom; +use core::option::NoneError; #[cfg(feature = "std")] use std::collections::HashSet; @@ -75,6 +78,14 @@ impl From for StoreError { } } +impl From for StoreError { + fn from(error: NoneError) -> StoreError { + match error { + NoneError => StoreError::InvalidStorage, + } + } +} + /// Result of store operations. pub type StoreResult = Result; @@ -174,6 +185,8 @@ impl StoreUpdate { } } +pub type StoreIter<'a> = Box> + 'a>; + /// Implements a store with a map interface over a storage. #[derive(Clone)] pub struct Store { @@ -182,6 +195,14 @@ pub struct Store { /// The storage configuration. format: Format, + + /// The position of the first word in the store. + head: Option, + + /// The list of the position of the user entries. + /// + /// The position is encoded as the word offset from the [head](Store#structfield.head). + entries: Option>, } impl Store { @@ -199,7 +220,12 @@ impl Store { None => return Err((StoreError::InvalidArgument, storage)), Some(x) => x, }; - let mut store = Store { storage, format }; + let mut store = Store { + storage, + format, + head: None, + entries: None, + }; if let Err(error) = store.recover() { return Err((error, store.storage)); } @@ -207,8 +233,19 @@ impl Store { } /// Iterates over the entries. - pub fn iter<'a>(&'a self) -> StoreResult> { - StoreIter::new(self) + pub fn iter<'a>(&'a self) -> StoreResult> { + let head = self.head?; + Ok(Box::new(self.entries.as_ref()?.iter().map( + move |&offset| { + let pos = head + offset as Nat; + match self.parse_entry(&mut pos.clone())? { + ParsedEntry::User(Header { + key, length: len, .. + }) => Ok(StoreHandle { key, pos, len }), + _ => Err(StoreError::InvalidStorage), + } + }, + ))) } /// Returns the current capacity in words. @@ -217,16 +254,9 @@ impl Store { pub fn capacity(&self) -> StoreResult { let total = self.format.total_capacity(); let mut used = 0; - let mut pos = self.head()?; - let end = pos + self.format.virt_size(); - while pos < end { - let entry_pos = pos; - match self.parse_entry(&mut pos)? { - ParsedEntry::Tail => break, - ParsedEntry::Padding => (), - ParsedEntry::User(_) => used += pos - entry_pos, - _ => return Err(StoreError::InvalidStorage), - } + for handle in self.iter()? { + let handle = handle?; + used += 1 + self.format.bytes_to_words(handle.len); } Ok(StoreRatio { used, total }) } @@ -381,6 +411,7 @@ impl Store { let footer = entry_len / word_size - 1; self.write_slice(tail, &entry[..(footer * word_size) as usize])?; self.write_slice(tail + footer, &entry[(footer * word_size) as usize..])?; + self.push_entry(tail)?; self.insert_init(tail, footer, key) } @@ -398,7 +429,8 @@ impl Store { /// Removes an entry given a handle. pub fn remove_handle(&mut self, handle: &StoreHandle) -> StoreResult<()> { self.check_handle(handle)?; - self.delete_pos(handle.pos, self.format.bytes_to_words(handle.len)) + self.delete_pos(handle.pos, self.format.bytes_to_words(handle.len))?; + self.remove_entry(handle.pos) } /// Returns the maximum length in bytes of a value. @@ -460,7 +492,9 @@ impl Store { /// Recovers a possible compaction interrupted while copying the entries. fn recover_compaction(&mut self) -> StoreResult<()> { - let head_page = self.head()?.page(&self.format); + let head = self.get_extremum_page_head(Ordering::Less)?; + self.head = Some(head); + let head_page = head.page(&self.format); match self.parse_compact(head_page)? { WordState::Erased => Ok(()), WordState::Partial => self.compact(), @@ -470,14 +504,15 @@ impl Store { /// Recover a possible interrupted operation which is not a compaction. fn recover_operation(&mut self) -> StoreResult<()> { - let mut pos = self.head()?; + self.entries = Some(Vec::new()); + let mut pos = self.head?; let mut prev_pos = pos; let end = pos + self.format.virt_size(); while pos < end { let entry_pos = pos; match self.parse_entry(&mut pos)? { ParsedEntry::Tail => break, - ParsedEntry::User(_) => (), + ParsedEntry::User(_) => self.push_entry(entry_pos)?, ParsedEntry::Padding => { self.wipe_span(entry_pos + 1, pos - entry_pos - 1)?; } @@ -610,7 +645,7 @@ impl Store { /// /// In particular, the handle has not been compacted. fn check_handle(&self, handle: &StoreHandle) -> StoreResult<()> { - if handle.pos < self.head()? { + if handle.pos < self.head? { Err(StoreError::InvalidArgument) } else { Ok(()) @@ -640,7 +675,7 @@ impl Store { /// Compacts one page. fn compact(&mut self) -> StoreResult<()> { - let head = self.head()?; + let head = self.head?; if head.cycle(&self.format) >= self.format.max_page_erases() { return Err(StoreError::NoLifetime); } @@ -653,7 +688,7 @@ impl Store { /// Continues a compaction after its compact page info has been written. fn compact_copy(&mut self) -> StoreResult<()> { - let mut head = self.head()?; + let mut head = self.head?; let page = head.page(&self.format); let end = head.next_page(&self.format); let mut tail = match self.parse_compact(page)? { @@ -667,8 +702,12 @@ impl Store { let pos = head; match self.parse_entry(&mut head)? { ParsedEntry::Tail => break, + // This can happen if we copy to the next page. We actually reached the tail but we + // read what we just copied. + ParsedEntry::Partial if head > end => break, ParsedEntry::User(_) => (), - _ => continue, + ParsedEntry::Padding => continue, + _ => return Err(StoreError::InvalidStorage), }; let length = head - pos; // We have to copy the slice for 2 reasons: @@ -676,7 +715,9 @@ impl Store { // 2. We can't pass a flash slice to the kernel. This should get fixed with // https://github.com/tock/tock/issues/1274. let entry = self.read_slice(pos, length * self.format.word_size()); + self.remove_entry(pos)?; self.write_slice(tail, &entry)?; + self.push_entry(tail)?; self.init_page(tail, tail + (length - 1))?; tail += length; } @@ -688,14 +729,31 @@ impl Store { /// Continues a compaction after its erase entry has been written. fn compact_erase(&mut self, erase: Position) -> StoreResult<()> { - let page = match self.parse_entry(&mut erase.clone())? { + // Read the page to erase from the erase entry. + let mut page = match self.parse_entry(&mut erase.clone())? { ParsedEntry::Internal(InternalEntry::Erase { page }) => page, _ => return Err(StoreError::InvalidStorage), }; + // Erase the page. self.storage_erase_page(page)?; - let head = self.head()?; + // Update the head. + page = (page + 1) % self.format.num_pages(); + let init = match self.parse_init(page)? { + WordState::Valid(x) => x, + _ => return Err(StoreError::InvalidStorage), + }; + let head = self.format.page_head(init, page); + if let Some(entries) = &mut self.entries { + let head_offset = u16::try_from(head - self.head?).ok()?; + for entry in entries { + *entry = entry.checked_sub(head_offset)?; + } + } + self.head = Some(head); + // Wipe the overlapping entry from the erased page. let pos = head.page_begin(&self.format); self.wipe_span(pos, head - pos)?; + // Mark the erase entry as done. self.set_padding(erase)?; Ok(()) } @@ -704,13 +762,13 @@ impl Store { fn transaction_apply(&mut self, sorted_keys: &[Nat], marker: Position) -> StoreResult<()> { self.delete_keys(&sorted_keys, marker)?; self.set_padding(marker)?; - let end = self.head()? + self.format.virt_size(); + let end = self.head? + self.format.virt_size(); let mut pos = marker + 1; while pos < end { let entry_pos = pos; match self.parse_entry(&mut pos)? { ParsedEntry::Tail => break, - ParsedEntry::User(_) => (), + ParsedEntry::User(_) => self.push_entry(entry_pos)?, ParsedEntry::Internal(InternalEntry::Remove { .. }) => { self.set_padding(entry_pos)? } @@ -727,37 +785,38 @@ impl Store { ParsedEntry::Internal(InternalEntry::Clear { min_key }) => min_key, _ => return Err(StoreError::InvalidStorage), }; - let mut pos = self.head()?; - let end = pos + self.format.virt_size(); - while pos < end { - let entry_pos = pos; - match self.parse_entry(&mut pos)? { - ParsedEntry::Internal(InternalEntry::Clear { .. }) if entry_pos == clear => break, - ParsedEntry::User(header) if header.key >= min_key => { - self.delete_pos(entry_pos, pos - entry_pos - 1)?; - } - ParsedEntry::Padding | ParsedEntry::User(_) => (), - _ => return Err(StoreError::InvalidStorage), - } - } + self.delete_if(clear, |key| key >= min_key)?; self.set_padding(clear)?; Ok(()) } /// Deletes a set of entries up to a certain position. fn delete_keys(&mut self, sorted_keys: &[Nat], end: Position) -> StoreResult<()> { - let mut pos = self.head()?; - while pos < end { - let entry_pos = pos; - match self.parse_entry(&mut pos)? { - ParsedEntry::Tail => break, - ParsedEntry::User(header) if sorted_keys.binary_search(&header.key).is_ok() => { - self.delete_pos(entry_pos, pos - entry_pos - 1)?; - } - ParsedEntry::Padding | ParsedEntry::User(_) => (), + self.delete_if(end, |key| sorted_keys.binary_search(&key).is_ok()) + } + + /// Deletes entries matching a predicate up to a certain position. + fn delete_if(&mut self, end: Position, delete: impl Fn(Nat) -> bool) -> StoreResult<()> { + let head = self.head?; + let mut entries = self.entries.take()?; + let mut i = 0; + while i < entries.len() { + let pos = head + entries[i] as Nat; + if pos >= end { + break; + } + let header = match self.parse_entry(&mut pos.clone())? { + ParsedEntry::User(x) => x, _ => return Err(StoreError::InvalidStorage), + }; + if delete(header.key) { + self.delete_pos(pos, self.format.bytes_to_words(header.length))?; + entries.swap_remove(i); + } else { + i += 1; } } + self.entries = Some(entries); Ok(()) } @@ -836,19 +895,20 @@ impl Store { } } // There is always at least one initialized page. - best.ok_or(StoreError::InvalidStorage) + Ok(best?) } /// Returns the number of words that can be written without compaction. fn immediate_capacity(&self) -> StoreResult { let tail = self.tail()?; - let end = self.head()? + self.format.virt_size(); + let end = self.head? + self.format.virt_size(); Ok(end.get().saturating_sub(tail.get())) } /// Returns the position of the first word in the store. + #[cfg(feature = "std")] pub(crate) fn head(&self) -> StoreResult { - self.get_extremum_page_head(Ordering::Less) + Ok(self.head?) } /// Returns one past the position of the last word in the store. @@ -863,6 +923,30 @@ impl Store { Ok(pos) } + fn push_entry(&mut self, pos: Position) -> StoreResult<()> { + let entries = match &mut self.entries { + None => return Ok(()), + Some(x) => x, + }; + let head = self.head?; + let offset = u16::try_from(pos - head).ok()?; + debug_assert!(!entries.contains(&offset)); + entries.push(offset); + Ok(()) + } + + fn remove_entry(&mut self, pos: Position) -> StoreResult<()> { + let entries = match &mut self.entries { + None => return Ok(()), + Some(x) => x, + }; + let head = self.head?; + let offset = u16::try_from(pos - head).ok()?; + let i = entries.iter().position(|x| *x == offset)?; + entries.swap_remove(i); + Ok(()) + } + /// Parses the entry at a given position. /// /// The position is updated to point to the next entry. @@ -1061,7 +1145,7 @@ impl Store { /// If the value has been partially compacted, only return the non-compacted part. Returns an /// empty value if it has been fully compacted. pub fn inspect_value(&self, handle: &StoreHandle) -> Vec { - let head = self.head().unwrap(); + let head = self.head.unwrap(); let length = self.format.bytes_to_words(handle.len); if head <= handle.pos { // The value has not been compacted. @@ -1087,20 +1171,21 @@ impl Store { store .iter() .unwrap() - .map(|x| x.unwrap()) - .filter(|x| delete_key(x.key as usize)) - .collect::>() + .filter(|x| x.is_err() || delete_key(x.as_ref().unwrap().key as usize)) + .collect::, _>>() }; match *operation { StoreOperation::Transaction { ref updates } => { let keys: HashSet = updates.iter().map(|x| x.key()).collect(); - let deleted = deleted(self, &|key| keys.contains(&key)); - (deleted, self.transaction(updates)) - } - StoreOperation::Clear { min_key } => { - let deleted = deleted(self, &|key| key >= min_key); - (deleted, self.clear(min_key)) + match deleted(self, &|key| keys.contains(&key)) { + Ok(deleted) => (deleted, self.transaction(updates)), + Err(error) => (Vec::new(), Err(error)), + } } + StoreOperation::Clear { min_key } => match deleted(self, &|key| key >= min_key) { + Ok(deleted) => (deleted, self.clear(min_key)), + Err(error) => (Vec::new(), Err(error)), + }, StoreOperation::Prepare { length } => (Vec::new(), self.prepare(length)), } } @@ -1165,61 +1250,6 @@ enum ParsedEntry { Tail, } -/// Iterates over the entries of a store. -pub struct StoreIter<'a, S: Storage> { - /// The store being iterated. - store: &'a Store, - - /// The position of the next entry. - pos: Position, - - /// Iteration stops when reaching this position. - end: Position, -} - -impl<'a, S: Storage> StoreIter<'a, S> { - /// Creates an iterator over the entries of a store. - fn new(store: &'a Store) -> StoreResult> { - let pos = store.head()?; - let end = pos + store.format.virt_size(); - Ok(StoreIter { store, pos, end }) - } -} - -impl<'a, S: Storage> StoreIter<'a, S> { - /// Returns the next entry and advances the iterator. - fn transposed_next(&mut self) -> StoreResult> { - if self.pos >= self.end { - return Ok(None); - } - while self.pos < self.end { - let entry_pos = self.pos; - match self.store.parse_entry(&mut self.pos)? { - ParsedEntry::Tail => break, - ParsedEntry::Padding => (), - ParsedEntry::User(header) => { - return Ok(Some(StoreHandle { - key: header.key, - pos: entry_pos, - len: header.length, - })) - } - _ => return Err(StoreError::InvalidStorage), - } - } - self.pos = self.end; - Ok(None) - } -} - -impl<'a, S: Storage> Iterator for StoreIter<'a, S> { - type Item = StoreResult; - - fn next(&mut self) -> Option> { - self.transposed_next().transpose() - } -} - /// Returns whether 2 slices are different. /// /// Returns an error if `target` has a bit set to one for which `source` is set to zero. diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 5f17052..6b7ff20 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -532,7 +532,7 @@ struct IterCredentials<'a> { store: &'a persistent_store::Store, /// The store iterator. - iter: persistent_store::StoreIter<'a, Storage>, + iter: persistent_store::StoreIter<'a>, /// The iteration result. /// From fb15032f0b191d55dd586113fd5b528b685c082b Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 10 Dec 2020 18:52:13 +0100 Subject: [PATCH 081/192] Test with nightly --- .github/workflows/persistent_store_test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/persistent_store_test.yml b/.github/workflows/persistent_store_test.yml index 1a1d942..ffe10ff 100644 --- a/.github/workflows/persistent_store_test.yml +++ b/.github/workflows/persistent_store_test.yml @@ -13,6 +13,11 @@ jobs: steps: - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + - name: Unit testing of Persistent store library (release mode) uses: actions-rs/cargo@v1 with: From 162c00a0d12e5c92bd08ff180d365c57568476b3 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 10 Dec 2020 19:54:25 -0800 Subject: [PATCH 082/192] Simplify Le length calculation --- src/ctap/apdu.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 7136084..3bf83da 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -173,14 +173,10 @@ impl TryFrom<&[u8]> for APDU { return Err(ApduStatusCode::SW_WRONG_LENGTH); } - let possible_le_len = payload.len() as i32 - extended_apdu_lc as i32 - 3; - - let extended_apdu_le_len: usize = match possible_le_len { - // There's some possible Le bytes at the end - 0..=3 => possible_le_len as usize, - // There are more bytes than even Le3 can consume, return an error - _ => return Err(ApduStatusCode::SW_WRONG_LENGTH), - }; + let extended_apdu_le_len: usize = payload.len() - extended_apdu_lc - 3; + if extended_apdu_le_len > 3 { + return Err(ApduStatusCode::SW_WRONG_LENGTH); + } if byte_0 == 0 && extended_apdu_le_len <= 3 { // If first byte is zero AND the next two bytes can be parsed as a big-endian From 21bdbd8114236795430326c31fb33b6d7a856d2b Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 10 Dec 2020 20:01:06 -0800 Subject: [PATCH 083/192] Use integers instead of ByteArray for the ApduStatusCode enum --- src/ctap/apdu.rs | 41 +++++++++++------------------------------ 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 3bf83da..b1d662f 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -2,44 +2,25 @@ use alloc::vec::Vec; use byteorder::{BigEndian, ByteOrder}; use core::convert::TryFrom; -type ByteArray = &'static [u8]; - const APDU_HEADER_LEN: usize = 4; #[cfg_attr(test, derive(Clone, Debug))] -#[allow(non_camel_case_types)] +#[allow(non_camel_case_types, dead_code)] #[derive(PartialEq)] pub enum ApduStatusCode { - SW_SUCCESS, + SW_SUCCESS = 0x90_00, /// Command successfully executed; 'XX' bytes of data are /// available and can be requested using GET RESPONSE. - SW_GET_RESPONSE, - SW_WRONG_DATA, - SW_WRONG_LENGTH, - SW_COND_USE_NOT_SATISFIED, - SW_FILE_NOT_FOUND, - SW_INCORRECT_P1P2, + SW_GET_RESPONSE = 0x61_00, + SW_WRONG_DATA = 0x6a_80, + SW_WRONG_LENGTH = 0x67_00, + SW_COND_USE_NOT_SATISFIED = 0x69_85, + SW_FILE_NOT_FOUND = 0x6a_82, + SW_INCORRECT_P1P2 = 0x6a_86, /// Instruction code not supported or invalid - SW_INS_INVALID, - SW_CLA_INVALID, - SW_INTERNAL_EXCEPTION, -} - -impl From for ByteArray { - fn from(status_code: ApduStatusCode) -> ByteArray { - match status_code { - ApduStatusCode::SW_SUCCESS => b"\x90\x00", - ApduStatusCode::SW_GET_RESPONSE => b"\x61\x00", - ApduStatusCode::SW_WRONG_DATA => b"\x6A\x80", - ApduStatusCode::SW_WRONG_LENGTH => b"\x67\x00", - ApduStatusCode::SW_COND_USE_NOT_SATISFIED => b"\x69\x85", - ApduStatusCode::SW_FILE_NOT_FOUND => b"\x6a\x82", - ApduStatusCode::SW_INCORRECT_P1P2 => b"\x6a\x86", - ApduStatusCode::SW_INS_INVALID => b"\x6d\x00", - ApduStatusCode::SW_CLA_INVALID => b"\x6e\x00", - ApduStatusCode::SW_INTERNAL_EXCEPTION => b"\x6f\x00", - } - } + SW_INS_INVALID = 0x6d_00, + SW_CLA_INVALID = 0x6e_00, + SW_INTERNAL_EXCEPTION = 0x6f_00, } #[allow(dead_code)] From 29dbff7a40a1207e19fc6f2e0249eebd6b6d5db8 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 10 Dec 2020 20:15:05 -0800 Subject: [PATCH 084/192] The great ApduStatusCode encroachment --- src/ctap/apdu.rs | 6 +++ src/ctap/ctap1.rs | 101 ++++++++++---------------------------------- src/ctap/hid/mod.rs | 2 +- 3 files changed, 30 insertions(+), 79 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index b1d662f..6338d15 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -23,6 +23,12 @@ pub enum ApduStatusCode { SW_INTERNAL_EXCEPTION = 0x6f_00, } +impl From for u16 { + fn from(_: ApduStatusCode) -> Self { + 0 + } +} + #[allow(dead_code)] pub enum ApduInstructions { Select = 0xA4, diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index ff07c0a..1156c6d 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -23,67 +23,12 @@ use core::convert::TryFrom; use crypto::rng256::Rng256; use libtock_drivers::timer::ClockValue; +// For now, they're the same thing with apdu.rs containing the authoritative definition +pub type Ctap1StatusCode = ApduStatusCode; + // The specification referenced in this file is at: // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf -// status codes specification (version 20170411) section 3.3 -#[allow(non_camel_case_types)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] -pub enum Ctap1StatusCode { - SW_NO_ERROR = 0x9000, - SW_CONDITIONS_NOT_SATISFIED = 0x6985, - SW_WRONG_DATA = 0x6A80, - SW_WRONG_LENGTH = 0x6700, - SW_CLA_NOT_SUPPORTED = 0x6E00, - SW_INS_NOT_SUPPORTED = 0x6D00, - SW_MEMERR = 0x6501, - SW_COMMAND_ABORTED = 0x6F00, - SW_VENDOR_KEY_HANDLE_TOO_LONG = 0xF000, -} - -impl TryFrom for Ctap1StatusCode { - type Error = (); - - fn try_from(value: u16) -> Result { - match value { - 0x9000 => Ok(Ctap1StatusCode::SW_NO_ERROR), - 0x6985 => Ok(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED), - 0x6A80 => Ok(Ctap1StatusCode::SW_WRONG_DATA), - 0x6700 => Ok(Ctap1StatusCode::SW_WRONG_LENGTH), - 0x6E00 => Ok(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED), - 0x6D00 => Ok(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), - 0x6501 => Ok(Ctap1StatusCode::SW_MEMERR), - 0x6F00 => Ok(Ctap1StatusCode::SW_COMMAND_ABORTED), - 0xF000 => Ok(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG), - _ => Err(()), - } - } -} - -impl TryFrom for Ctap1StatusCode { - type Error = (); - - fn try_from(apdu_status_code: ApduStatusCode) -> Result { - match apdu_status_code { - ApduStatusCode::SW_WRONG_LENGTH => Ok(Ctap1StatusCode::SW_WRONG_LENGTH), - ApduStatusCode::SW_WRONG_DATA => Ok(Ctap1StatusCode::SW_WRONG_DATA), - ApduStatusCode::SW_CLA_INVALID => Ok(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED), - ApduStatusCode::SW_INS_INVALID => Ok(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), - ApduStatusCode::SW_COND_USE_NOT_SATISFIED => { - Ok(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED) - } - ApduStatusCode::SW_SUCCESS => Ok(Ctap1StatusCode::SW_NO_ERROR), - _ => Ok(Ctap1StatusCode::SW_COMMAND_ABORTED), - } - } -} - -impl Into for Ctap1StatusCode { - fn into(self) -> u16 { - self as u16 - } -} - #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug))] #[derive(PartialEq)] pub enum Ctap1Flags { @@ -154,7 +99,7 @@ impl TryFrom<&[u8]> for U2fCommand { // | CLA | INS | P1 | P2 | Lc1 | Lc2 | Lc3 | // +-----+-----+----+----+-----+-----+-----+ if apdu.header.cla != Ctap1Command::CTAP1_CLA { - return Err(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED); + return Err(Ctap1StatusCode::SW_CLA_INVALID); } // Since there is always request data, the expected length is either omitted or @@ -214,7 +159,7 @@ impl TryFrom<&[u8]> for U2fCommand { }) } - _ => Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), + _ => Err(Ctap1StatusCode::SW_INS_INVALID), } } } @@ -252,7 +197,7 @@ impl Ctap1Command { application, } => { if !ctap_state.u2f_up_state.consume_up(clock_value) { - return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED); + return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED); } Ctap1Command::process_register(challenge, application, ctap_state) } @@ -267,7 +212,7 @@ impl Ctap1Command { if flags == Ctap1Flags::EnforceUpAndSign && !ctap_state.u2f_up_state.consume_up(clock_value) { - return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED); + return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED); } Ctap1Command::process_authenticate( challenge, @@ -282,7 +227,7 @@ impl Ctap1Command { U2fCommand::Version => Ok(Vec::::from(super::U2F_VERSION_STRING)), // TODO: should we return an error instead such as SW_INS_NOT_SUPPORTED? - U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_NO_ERROR), + U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION), } } @@ -310,22 +255,22 @@ impl Ctap1Command { let pk = sk.genpk(); let key_handle = ctap_state .encrypt_key_handle(sk, &application, None) - .map_err(|_| Ctap1StatusCode::SW_COMMAND_ABORTED)?; + .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. - return Err(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG); + return Err(Ctap1StatusCode::SW_WRONG_LENGTH); } let certificate = ctap_state .persistent_store .attestation_certificate() - .map_err(|_| Ctap1StatusCode::SW_MEMERR)? - .ok_or(Ctap1StatusCode::SW_COMMAND_ABORTED)?; + .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)? + .ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; let private_key = ctap_state .persistent_store .attestation_private_key() - .map_err(|_| Ctap1StatusCode::SW_MEMERR)? - .ok_or(Ctap1StatusCode::SW_COMMAND_ABORTED)?; + .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)? + .ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len()); response.push(Ctap1Command::LEGACY_BYTE); @@ -380,14 +325,14 @@ impl Ctap1Command { .map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?; if let Some(credential_source) = credential_source { if flags == Ctap1Flags::CheckOnly { - return Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED); + return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED); } ctap_state .increment_global_signature_counter() .map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?; let mut signature_data = ctap_state .generate_auth_data(&application, Ctap1Command::USER_PRESENCE_INDICATOR_BYTE) - .map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?; + .map_err(|_| Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED)?; signature_data.extend(&challenge); let signature = credential_source .private_key @@ -466,7 +411,7 @@ mod test { ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate and private key are missing - assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_ABORTED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)); let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; assert!(ctap_state @@ -477,7 +422,7 @@ mod test { ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate is still missing - assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_ABORTED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)); let fake_cert = [0x99u8; 100]; // Arbitrary length assert!(ctap_state @@ -534,7 +479,7 @@ mod test { ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE); - assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED)); } #[test] @@ -552,7 +497,7 @@ mod test { let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); - assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED)); } #[test] @@ -626,7 +571,7 @@ mod test { message[0] = 0xEE; let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); - assert_eq!(response, Err(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_CLA_INVALID)); } #[test] @@ -646,7 +591,7 @@ mod test { message[1] = 0xEE; let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); - assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_INS_INVALID)); } #[test] @@ -768,6 +713,6 @@ mod test { ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, TIMEOUT_CLOCK_VALUE); - assert_eq!(response, Err(Ctap1StatusCode::SW_CONDITIONS_NOT_SATISFIED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED)); } } diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 3874792..7315449 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -417,7 +417,7 @@ impl CtapHid { #[cfg(feature = "with_ctap1")] fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> HidPacketIterator { let mut response = payload.to_vec(); - let code: u16 = ctap1::Ctap1StatusCode::SW_NO_ERROR.into(); + let code: u16 = ctap1::Ctap1StatusCode::SW_INTERNAL_EXCEPTION.into(); response.extend_from_slice(&code.to_be_bytes()); CtapHid::split_message(Message { cid, From a7eb38aac8b6b3400cebba9b09fefca1c04cfbb3 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 10 Dec 2020 21:26:44 -0800 Subject: [PATCH 085/192] Use checked sub --- src/ctap/apdu.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 7a65fd2..66dde63 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -174,7 +174,8 @@ impl TryFrom<&[u8]> for APDU { return Err(ApduStatusCode::SW_WRONG_LENGTH); } - let extended_apdu_le_len: usize = payload.len() - extended_apdu_lc - 3; + let extended_apdu_le_len: usize = + payload.len().checked_sub(extended_apdu_lc + 3).unwrap_or(0); if extended_apdu_le_len > 3 { return Err(ApduStatusCode::SW_WRONG_LENGTH); } From f74d1b9ffd96d54315066058fa3a4656968c17df Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 10 Dec 2020 21:27:52 -0800 Subject: [PATCH 086/192] Return error when Le calculation overflows --- src/ctap/apdu.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 66dde63..d9344ec 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -174,8 +174,10 @@ impl TryFrom<&[u8]> for APDU { return Err(ApduStatusCode::SW_WRONG_LENGTH); } - let extended_apdu_le_len: usize = - payload.len().checked_sub(extended_apdu_lc + 3).unwrap_or(0); + let extended_apdu_le_len: usize = payload + .len() + .checked_sub(extended_apdu_lc + 3) + .unwrap_or(0xff); if extended_apdu_le_len > 3 { return Err(ApduStatusCode::SW_WRONG_LENGTH); } From 5882a6a3cc95348dc0b88bc0f68214e573bf1dd3 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Thu, 10 Dec 2020 23:40:47 -0800 Subject: [PATCH 087/192] Fix ApduStatusCode->u16 implementation --- src/ctap/apdu.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index d9344ec..ea59ac5 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -38,8 +38,8 @@ pub enum ApduStatusCode { } impl From for u16 { - fn from(_: ApduStatusCode) -> Self { - 0 + fn from(code: ApduStatusCode) -> Self { + code as u16 } } From dbbdddd58b83188dfca433c471c867a2458e0cf9 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 14 Dec 2020 03:45:13 -0800 Subject: [PATCH 088/192] Fix error codes --- src/ctap/apdu.rs | 6 ++---- src/ctap/ctap1.rs | 10 +++++----- src/ctap/hid/mod.rs | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index ea59ac5..14926ba 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -26,6 +26,7 @@ pub enum ApduStatusCode { /// Command successfully executed; 'XX' bytes of data are /// available and can be requested using GET RESPONSE. SW_GET_RESPONSE = 0x61_00, + SW_MEMERR = 0x65_01, SW_WRONG_DATA = 0x6a_80, SW_WRONG_LENGTH = 0x67_00, SW_COND_USE_NOT_SATISFIED = 0x69_85, @@ -177,10 +178,7 @@ impl TryFrom<&[u8]> for APDU { let extended_apdu_le_len: usize = payload .len() .checked_sub(extended_apdu_lc + 3) - .unwrap_or(0xff); - if extended_apdu_le_len > 3 { - return Err(ApduStatusCode::SW_WRONG_LENGTH); - } + .ok_or(ApduStatusCode::SW_WRONG_LENGTH)?; if byte_0 == 0 && extended_apdu_le_len <= 3 { // If first byte is zero AND the next two bytes can be parsed as a big-endian diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 13f4819..cc15fa0 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -227,7 +227,7 @@ impl Ctap1Command { U2fCommand::Version => Ok(Vec::::from(super::U2F_VERSION_STRING)), // TODO: should we return an error instead such as SW_INS_NOT_SUPPORTED? - U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION), + U2fCommand::VendorSpecific { .. } => Err(Ctap1StatusCode::SW_SUCCESS), } } @@ -258,13 +258,13 @@ impl Ctap1Command { .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. - return Err(Ctap1StatusCode::SW_WRONG_LENGTH); + return Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION); } let certificate = ctap_state .persistent_store .attestation_certificate() - .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)? + .map_err(|_| Ctap1StatusCode::SW_MEMERR)? .ok_or(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)?; let private_key = ctap_state .persistent_store @@ -332,7 +332,7 @@ impl Ctap1Command { .map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?; let mut signature_data = ctap_state .generate_auth_data(&application, Ctap1Command::USER_PRESENCE_INDICATOR_BYTE) - .map_err(|_| Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED)?; + .map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?; signature_data.extend(&challenge); let signature = credential_source .private_key @@ -542,7 +542,7 @@ mod test { message.push(0x00); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); - assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH)); + assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)); } #[test] diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 7315449..a36063b 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -417,7 +417,7 @@ impl CtapHid { #[cfg(feature = "with_ctap1")] fn ctap1_success_message(cid: ChannelID, payload: &[u8]) -> HidPacketIterator { let mut response = payload.to_vec(); - let code: u16 = ctap1::Ctap1StatusCode::SW_INTERNAL_EXCEPTION.into(); + let code: u16 = ctap1::Ctap1StatusCode::SW_SUCCESS.into(); response.extend_from_slice(&code.to_be_bytes()); CtapHid::split_message(Message { cid, From 35bdfe90ed52a5fac37d84b1ea6702b7d8c78001 Mon Sep 17 00:00:00 2001 From: Kamran Khan Date: Mon, 14 Dec 2020 04:54:25 -0800 Subject: [PATCH 089/192] Re-instate the length check for Le bytes --- src/ctap/apdu.rs | 3 +++ src/ctap/ctap1.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index 14926ba..c99bf84 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -179,6 +179,9 @@ impl TryFrom<&[u8]> for APDU { .len() .checked_sub(extended_apdu_lc + 3) .ok_or(ApduStatusCode::SW_WRONG_LENGTH)?; + if extended_apdu_le_len > 3 { + return Err(ApduStatusCode::SW_WRONG_LENGTH); + } if byte_0 == 0 && extended_apdu_le_len <= 3 { // If first byte is zero AND the next two bytes can be parsed as a big-endian diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index cc15fa0..0932e2c 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -542,7 +542,7 @@ mod test { message.push(0x00); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); - assert_eq!(response, Err(Ctap1StatusCode::SW_INTERNAL_EXCEPTION)); + assert_eq!(response, Err(Ctap1StatusCode::SW_WRONG_LENGTH)); } #[test] From 1d576fdd316027f0cf4d565cb16b2002bc631ae6 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 14 Dec 2020 21:06:12 +0100 Subject: [PATCH 090/192] Add unit-test for Store::entries --- libraries/persistent_store/src/store.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index ba7ab4b..bc4258a 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -1468,4 +1468,22 @@ mod tests { driver = driver.power_off().power_on().unwrap(); driver.check().unwrap(); } + + #[test] + fn entries_ok() { + let mut driver = MINIMAL.new_driver().power_on().unwrap(); + + // The store is initially empty. + assert!(driver.store().entries.as_ref().unwrap().is_empty()); + + // Inserted elements are added. + const LEN: usize = 6; + driver.insert(0, &[0x38; (LEN - 1) * 4]).unwrap(); + driver.insert(1, &[0x5c; 4]).unwrap(); + assert_eq!(driver.store().entries, Some(vec![0, LEN as u16])); + + // Deleted elements are removed. + driver.remove(0).unwrap(); + assert_eq!(driver.store().entries, Some(vec![LEN as u16])); + } } From 6c9fc2565a6e734fafb3658ee820f50e330e0256 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 16 Dec 2020 10:48:01 +0100 Subject: [PATCH 091/192] changes channel ID endianness to big endian --- src/ctap/hid/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index a36063b..1ae8334 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -68,8 +68,8 @@ pub struct CtapHid { // vendor specific. // We allocate them incrementally, that is all `cid` such that 1 <= cid <= allocated_cids are // allocated. - // In packets, the ids are then encoded with the native endianness (with the - // u32::to/from_ne_bytes methods). + // In packets, the ID encoding is Big Endian to match what is used throughout CTAP (with the + // u32::to/from_be_bytes methods). allocated_cids: usize, pub wink_permission: TimedPermission, } @@ -235,7 +235,7 @@ impl CtapHid { let new_cid = if cid == CtapHid::CHANNEL_BROADCAST { // TODO: Prevent allocating 2^32 channels. self.allocated_cids += 1; - (self.allocated_cids as u32).to_ne_bytes() + (self.allocated_cids as u32).to_be_bytes() } else { // Sync the channel and discard the current transaction. cid @@ -342,7 +342,7 @@ impl CtapHid { } fn is_allocated_channel(&self, cid: ChannelID) -> bool { - cid != CtapHid::CHANNEL_RESERVED && u32::from_ne_bytes(cid) as usize <= self.allocated_cids + cid != CtapHid::CHANNEL_RESERVED && u32::from_be_bytes(cid) as usize <= self.allocated_cids } fn error_message(cid: ChannelID, error_code: u8) -> HidPacketIterator { @@ -569,10 +569,10 @@ mod test { 0xBC, 0xDE, 0xF0, - 0x01, // Allocated CID - 0x00, + 0x00, // Allocated CID 0x00, 0x00, + 0x01, 0x02, // Protocol version 0x00, // Device version 0x00, From b002b4669e811219f9e83098aa4bb3f2ee77f9bf Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Fri, 11 Dec 2020 01:50:50 +0100 Subject: [PATCH 092/192] Update UICR registers. --- patches/tock/06-update-uicr.patch | 100 ++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 patches/tock/06-update-uicr.patch diff --git a/patches/tock/06-update-uicr.patch b/patches/tock/06-update-uicr.patch new file mode 100644 index 0000000..53ee945 --- /dev/null +++ b/patches/tock/06-update-uicr.patch @@ -0,0 +1,100 @@ +diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs +index 6bb6c86..3bb8b5a 100644 +--- a/chips/nrf52/src/uicr.rs ++++ b/chips/nrf52/src/uicr.rs +@@ -1,38 +1,45 @@ + //! User information configuration registers +-//! +-//! Minimal implementation to support activation of the reset button on +-//! nRF52-DK. ++ + + use enum_primitive::cast::FromPrimitive; +-use kernel::common::registers::{register_bitfields, ReadWrite}; ++use kernel::common::registers::{register_bitfields, register_structs, ReadWrite}; + use kernel::common::StaticRef; ++use kernel::hil; ++use kernel::ReturnCode; + + use crate::gpio::Pin; + + const UICR_BASE: StaticRef = +- unsafe { StaticRef::new(0x10001200 as *const UicrRegisters) }; +- +-#[repr(C)] +-struct UicrRegisters { +- /// Mapping of the nRESET function (see POWER chapter for details) +- /// - Address: 0x200 - 0x204 +- pselreset0: ReadWrite, +- /// Mapping of the nRESET function (see POWER chapter for details) +- /// - Address: 0x204 - 0x208 +- pselreset1: ReadWrite, +- /// Access Port protection +- /// - Address: 0x208 - 0x20c +- approtect: ReadWrite, +- /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO +- /// - Address: 0x20c - 0x210 +- nfcpins: ReadWrite, +- _reserved1: [u32; 60], +- /// External circuitry to be supplied from VDD pin. +- /// - Address: 0x300 - 0x304 +- extsupply: ReadWrite, +- /// GPIO reference voltage +- /// - Address: 0x304 - 0x308 +- regout0: ReadWrite, ++ unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) }; ++ ++register_structs! { ++ UicrRegisters { ++ (0x000 => _reserved1), ++ /// Reserved for Nordic firmware design ++ (0x014 => nrffw: [ReadWrite; 13]), ++ (0x048 => _reserved2), ++ /// Reserved for Nordic hardware design ++ (0x050 => nrfhw: [ReadWrite; 12]), ++ /// Reserved for customer ++ (0x080 => customer: [ReadWrite; 32]), ++ (0x100 => _reserved3), ++ /// Mapping of the nRESET function (see POWER chapter for details) ++ (0x200 => pselreset0: ReadWrite), ++ /// Mapping of the nRESET function (see POWER chapter for details) ++ (0x204 => pselreset1: ReadWrite), ++ /// Access Port protection ++ (0x208 => approtect: ReadWrite), ++ /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO ++ /// - Address: 0x20c - 0x210 ++ (0x20c => nfcpins: ReadWrite), ++ (0x210 => debugctrl: ReadWrite), ++ (0x214 => _reserved4), ++ /// External circuitry to be supplied from VDD pin. ++ (0x300 => extsupply: ReadWrite), ++ /// GPIO reference voltage ++ (0x304 => regout0: ReadWrite), ++ (0x308 => @END), ++ } + } + + register_bitfields! [u32, +@@ -58,6 +65,21 @@ register_bitfields! [u32, + DISABLED = 0xff + ] + ], ++ /// Processor debug control ++ DebugControl [ ++ CPUNIDEN OFFSET(0) NUMBITS(8) [ ++ /// Enable ++ ENABLED = 0xff, ++ /// Disable ++ DISABLED = 0x00 ++ ], ++ CPUFPBEN OFFSET(8) NUMBITS(8) [ ++ /// Enable ++ ENABLED = 0xff, ++ /// Disable ++ DISABLED = 0x00 ++ ] ++ ], + /// Setting of pins dedicated to NFC functionality: NFC antenna or GPIO + NfcPins [ + /// Setting pins dedicated to NFC functionality + From 6e5a8cdf6d80167f55256ca8d80d302e361f2f1d Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Fri, 11 Dec 2020 01:51:16 +0100 Subject: [PATCH 093/192] Add kernel support for firmware protection --- patches/tock/07-firmware-protect.patch | 350 +++++++++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 patches/tock/07-firmware-protect.patch diff --git a/patches/tock/07-firmware-protect.patch b/patches/tock/07-firmware-protect.patch new file mode 100644 index 0000000..c062648 --- /dev/null +++ b/patches/tock/07-firmware-protect.patch @@ -0,0 +1,350 @@ +diff --git a/boards/components/src/firmware_protection.rs b/boards/components/src/firmware_protection.rs +new file mode 100644 +index 0000000..5eda591 +--- /dev/null ++++ b/boards/components/src/firmware_protection.rs +@@ -0,0 +1,67 @@ ++//! Component for firmware protection syscall interface. ++//! ++//! This provides one Component, `FirmwareProtectionComponent`, which implements a ++//! userspace syscall interface to enable the code readout protection. ++//! ++//! Usage ++//! ----- ++//! ```rust ++//! let crp = components::firmware_protection::FirmwareProtectionComponent::new( ++//! board_kernel, ++//! nrf52840::uicr::Uicr::new() ++//! ) ++//! .finalize( ++//! components::firmware_protection_component_helper!(uicr)); ++//! ``` ++ ++use core::mem::MaybeUninit; ++ ++use capsules::firmware_protection; ++use kernel::capabilities; ++use kernel::component::Component; ++use kernel::create_capability; ++use kernel::hil; ++use kernel::static_init_half; ++ ++// Setup static space for the objects. ++#[macro_export] ++macro_rules! firmware_protection_component_helper { ++ ($C:ty) => {{ ++ use capsules::firmware_protection; ++ use core::mem::MaybeUninit; ++ static mut BUF: MaybeUninit> = MaybeUninit::uninit(); ++ &mut BUF ++ };}; ++} ++ ++pub struct FirmwareProtectionComponent { ++ board_kernel: &'static kernel::Kernel, ++ crp: C, ++} ++ ++impl FirmwareProtectionComponent { ++ pub fn new(board_kernel: &'static kernel::Kernel, crp: C) -> FirmwareProtectionComponent { ++ FirmwareProtectionComponent { ++ board_kernel: board_kernel, ++ crp: crp, ++ } ++ } ++} ++ ++impl Component for FirmwareProtectionComponent { ++ type StaticInput = &'static mut MaybeUninit>; ++ type Output = &'static firmware_protection::FirmwareProtection<'static, C>; ++ ++ unsafe fn finalize(self, static_buffer: Self::StaticInput) -> Self::Output { ++ let grant_cap = create_capability!(capabilities::MemoryAllocationCapability); ++ ++ static_init_half!( ++ static_buffer, ++ firmware_protection::FirmwareProtection<'static, C>, ++ firmware_protection::FirmwareProtection::new( ++ self.crp, ++ self.board_kernel.create_grant(&grant_cap), ++ ) ++ ) ++ } ++} +diff --git a/boards/components/src/lib.rs b/boards/components/src/lib.rs +index 917497a..520408f 100644 +--- a/boards/components/src/lib.rs ++++ b/boards/components/src/lib.rs +@@ -9,6 +9,7 @@ pub mod console; + pub mod crc; + pub mod debug_queue; + pub mod debug_writer; ++pub mod firmware_protection; + pub mod ft6x06; + pub mod gpio; + pub mod hd44780; +diff --git a/boards/nordic/nrf52840_dongle/src/main.rs b/boards/nordic/nrf52840_dongle/src/main.rs +index 118ea6d..f340dd1 100644 +--- a/boards/nordic/nrf52840_dongle/src/main.rs ++++ b/boards/nordic/nrf52840_dongle/src/main.rs +@@ -112,6 +112,10 @@ pub struct Platform { + 'static, + nrf52840::usbd::Usbd<'static>, + >, ++ crp: &'static capsules::firmware_protection::FirmwareProtection< ++ 'static, ++ nrf52840::uicr::Uicr, ++ >, + } + + impl kernel::Platform for Platform { +@@ -132,6 +136,7 @@ impl kernel::Platform for Platform { + capsules::analog_comparator::DRIVER_NUM => f(Some(self.analog_comparator)), + nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)), + capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)), ++ capsules::firmware_protection::DRIVER_NUM => f(Some(self.crp)), + kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)), + _ => f(None), + } +@@ -355,6 +360,12 @@ pub unsafe fn reset_handler() { + ) + .finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd)); + ++ let crp = components::firmware_protection::FirmwareProtectionComponent::new( ++ board_kernel, ++ nrf52840::uicr::Uicr::new(), ++ ) ++ .finalize(components::firmware_protection_component_helper!(nrf52840::uicr::Uicr)); ++ + nrf52_components::NrfClockComponent::new().finalize(()); + + let platform = Platform { +@@ -371,6 +382,7 @@ pub unsafe fn reset_handler() { + analog_comparator, + nvmc, + usb, ++ crp, + ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability), + }; + +diff --git a/boards/nordic/nrf52840dk/src/main.rs b/boards/nordic/nrf52840dk/src/main.rs +index b1d0d3c..a37d180 100644 +--- a/boards/nordic/nrf52840dk/src/main.rs ++++ b/boards/nordic/nrf52840dk/src/main.rs +@@ -180,6 +180,10 @@ pub struct Platform { + 'static, + nrf52840::usbd::Usbd<'static>, + >, ++ crp: &'static capsules::firmware_protection::FirmwareProtection< ++ 'static, ++ nrf52840::uicr::Uicr, ++ >, + } + + impl kernel::Platform for Platform { +@@ -201,6 +205,7 @@ impl kernel::Platform for Platform { + capsules::nonvolatile_storage_driver::DRIVER_NUM => f(Some(self.nonvolatile_storage)), + nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)), + capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)), ++ capsules::firmware_protection::DRIVER_NUM => f(Some(self.crp)), + kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)), + _ => f(None), + } +@@ -480,6 +485,12 @@ pub unsafe fn reset_handler() { + ) + .finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd)); + ++ let crp = components::firmware_protection::FirmwareProtectionComponent::new( ++ board_kernel, ++ nrf52840::uicr::Uicr::new(), ++ ) ++ .finalize(components::firmware_protection_component_helper!(nrf52840::uicr::Uicr)); ++ + nrf52_components::NrfClockComponent::new().finalize(()); + + let platform = Platform { +@@ -497,6 +508,7 @@ pub unsafe fn reset_handler() { + nonvolatile_storage, + nvmc, + usb, ++ crp, + ipc: kernel::ipc::IPC::new(board_kernel, &memory_allocation_capability), + }; + +diff --git a/capsules/src/driver.rs b/capsules/src/driver.rs +index ae458b3..f536dad 100644 +--- a/capsules/src/driver.rs ++++ b/capsules/src/driver.rs +@@ -16,6 +16,7 @@ pub enum NUM { + Adc = 0x00005, + Dac = 0x00006, + AnalogComparator = 0x00007, ++ FirmwareProtection = 0x00008, + + // Kernel + Ipc = 0x10000, +diff --git a/capsules/src/firmware_protection.rs b/capsules/src/firmware_protection.rs +new file mode 100644 +index 0000000..2c61d06 +--- /dev/null ++++ b/capsules/src/firmware_protection.rs +@@ -0,0 +1,87 @@ ++//! Provides userspace control of firmware protection on a board. ++//! ++//! This allows an application to enable firware readout protection, ++//! disabling JTAG interface and other ways to read/tamper the firmware. ++//! Of course, outside of a hardware bug, once set, the only way to enable ++//! programming/debugging is by fully erasing the flash. ++//! ++//! Usage ++//! ----- ++//! ++//! ```rust ++//! # use kernel::static_init; ++//! ++//! let crp = static_init!( ++//! capsules::firware_protection::FirmwareProtection<'static>, ++//! capsules::firware_protection::FirmwareProtection::new( ++//! nrf52840::uicr::Uicr, ++//! board_kernel.create_grant(&grant_cap), ++//! ); ++//! ``` ++//! ++//! Syscall Interface ++//! ----------------- ++//! ++//! - Stability: 2 - Stable ++//! ++//! ### Command ++//! ++//! Enable code readout protection on the current board. ++//! ++//! #### `command_num` ++//! ++//! - `0`: Driver check. ++//! - `1`: Enable firmware readout protection (aka CRP). ++//! ++ ++use core::marker::PhantomData; ++use kernel::hil; ++use kernel::{AppId, Callback, Driver, Grant, ReturnCode}; ++ ++/// Syscall driver number. ++use crate::driver; ++pub const DRIVER_NUM: usize = driver::NUM::FirmwareProtection as usize; ++ ++pub struct FirmwareProtection<'a, C: hil::firmware_protection::FirmwareProtection> { ++ crp_unit: C, ++ apps: Grant>, ++ _phantom: PhantomData<&'a C>, ++} ++ ++impl<'a, C: hil::firmware_protection::FirmwareProtection> FirmwareProtection<'a, C> { ++ pub fn new( ++ crp_unit: C, ++ apps: Grant>, ++ ) -> Self { ++ Self { ++ crp_unit, ++ apps, ++ _phantom: PhantomData, ++ } ++ } ++} ++ ++impl<'a, C: hil::firmware_protection::FirmwareProtection> Driver for FirmwareProtection<'a, C> { ++ /// ++ /// ### Command numbers ++ /// ++ /// * `0`: Returns non-zero to indicate the driver is present. ++ /// * `1`: Enable firmware protection. ++ fn command(&self, command_num: usize, _: usize, _: usize, appid: AppId) -> ReturnCode { ++ match command_num { ++ // return if driver is available ++ 0 => ReturnCode::SUCCESS, ++ ++ // enable firmware protection ++ 1 => { ++ self.apps.enter(appid, |_, _| { ++ self.crp_unit.protect() ++ }) ++ .unwrap_or_else(|err| err.into()) ++ } ++ ++ // default ++ _ => ReturnCode::ENOSUPPORT, ++ } ++ } ++} +diff --git a/capsules/src/lib.rs b/capsules/src/lib.rs +index e4423fe..7538aad 100644 +--- a/capsules/src/lib.rs ++++ b/capsules/src/lib.rs +@@ -22,6 +22,7 @@ pub mod crc; + pub mod dac; + pub mod debug_process_restart; + pub mod driver; ++pub mod firmware_protection; + pub mod fm25cl; + pub mod ft6x06; + pub mod fxos8700cq; +diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs +index 3bb8b5a..b8895d3 100644 +--- a/chips/nrf52/src/uicr.rs ++++ b/chips/nrf52/src/uicr.rs +@@ -8,6 +8,7 @@ use kernel::hil; + use kernel::ReturnCode; + + use crate::gpio::Pin; ++use crate::nvmc::NVMC; + + const UICR_BASE: StaticRef = + unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) }; +@@ -210,3 +211,20 @@ impl Uicr { + self.registers.approtect.write(ApProtect::PALL::ENABLED); + } + } ++ ++impl hil::firmware_protection::FirmwareProtection for Uicr { ++ fn protect(&self) -> ReturnCode { ++ unsafe { NVMC.configure_writeable() }; ++ self.set_ap_protect(); ++ // Prevent CPU debug ++ self.registers.debugctrl.write( ++ DebugControl::CPUNIDEN::DISABLED + DebugControl::CPUFPBEN::DISABLED); ++ // TODO(jmichel): Kill bootloader if present ++ unsafe { NVMC.configure_readonly() }; ++ if self.is_ap_protect_enabled() { ++ ReturnCode::SUCCESS ++ } else { ++ ReturnCode::FAIL ++ } ++ } ++} +diff --git a/kernel/src/hil/firmware_protection.rs b/kernel/src/hil/firmware_protection.rs +new file mode 100644 +index 0000000..e43fdf0 +--- /dev/null ++++ b/kernel/src/hil/firmware_protection.rs +@@ -0,0 +1,8 @@ ++//! Interface for Firmware Protection, also called Code Readout Protection. ++ ++use crate::returncode::ReturnCode; ++ ++pub trait FirmwareProtection { ++ /// Disable debug ports and protects the device. ++ fn protect(&self) -> ReturnCode; ++} +diff --git a/kernel/src/hil/mod.rs b/kernel/src/hil/mod.rs +index 4f42afa..83e7702 100644 +--- a/kernel/src/hil/mod.rs ++++ b/kernel/src/hil/mod.rs +@@ -8,6 +8,7 @@ pub mod dac; + pub mod digest; + pub mod eic; + pub mod entropy; ++pub mod firmware_protection; + pub mod flash; + pub mod gpio; + pub mod gpio_async; + From 218188ad497f07d024994f47c66b8a20985f2968 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Tue, 1 Dec 2020 12:34:22 +0100 Subject: [PATCH 094/192] Add CRP support in libtock-rs --- third_party/libtock-drivers/src/crp.rs | 19 +++++++++++++++++++ third_party/libtock-drivers/src/lib.rs | 1 + 2 files changed, 20 insertions(+) create mode 100644 third_party/libtock-drivers/src/crp.rs diff --git a/third_party/libtock-drivers/src/crp.rs b/third_party/libtock-drivers/src/crp.rs new file mode 100644 index 0000000..fab747f --- /dev/null +++ b/third_party/libtock-drivers/src/crp.rs @@ -0,0 +1,19 @@ +use crate::result::TockResult; +use libtock_core::syscalls; + +const DRIVER_NUMBER: usize = 0x00008; + +mod command_nr { + pub const AVAILABLE: usize = 0; + pub const PROTECT: usize = 1; +} + +pub fn is_available() -> TockResult<()> { + syscalls::command(DRIVER_NUMBER, command_nr::AVAILABLE, 0, 0)?; + Ok(()) +} + +pub fn protect() -> TockResult<()> { + syscalls::command(DRIVER_NUMBER, command_nr::PROTECT, 0, 0)?; + Ok(()) +} diff --git a/third_party/libtock-drivers/src/lib.rs b/third_party/libtock-drivers/src/lib.rs index 8b8983c..f014996 100644 --- a/third_party/libtock-drivers/src/lib.rs +++ b/third_party/libtock-drivers/src/lib.rs @@ -2,6 +2,7 @@ pub mod buttons; pub mod console; +pub mod crp; pub mod led; #[cfg(feature = "with_nfc")] pub mod nfc; From efb63783113266a5aff33f2bc640e1c9205af056 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Tue, 1 Dec 2020 15:32:32 +0100 Subject: [PATCH 095/192] Add vendor command to load certificate and priv key --- src/ctap/command.rs | 78 ++++++++++++++++-- src/ctap/mod.rs | 190 ++++++++++++++++++++++++++++++++++++++++++- src/ctap/response.rs | 26 ++++++ 3 files changed, 287 insertions(+), 7 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 1b4b96a..f2f2537 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -13,14 +13,17 @@ // limitations under the License. use super::data_formats::{ - extract_array, extract_byte_string, extract_map, extract_text_string, extract_unsigned, - ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, + extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, + GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; +use super::key_material; use super::status_code::Ctap2StatusCode; use alloc::string::String; use alloc::vec::Vec; +use arrayref::array_ref; use cbor::destructure_cbor_map; use core::convert::TryFrom; @@ -41,6 +44,8 @@ pub enum Command { #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) + // Vendor specific commands + AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), } impl From for Ctap2StatusCode { @@ -63,7 +68,8 @@ impl Command { const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; const AUTHENTICATOR_SELECTION: u8 = 0xB0; const AUTHENTICATOR_CONFIG: u8 = 0xC0; - const AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40; + const AUTHENTICATOR_VENDOR_CONFIGURE: u8 = 0x40; + const AUTHENTICATOR_VENDOR_FIRST_UNUSED: u8 = 0x41; const AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF; pub fn deserialize(bytes: &[u8]) -> Result { @@ -109,6 +115,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorSelection) } + Command::AUTHENTICATOR_VENDOR_CONFIGURE => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorVendorConfigure( + AuthenticatorVendorConfigureParameters::try_from(decoded_cbor)?, + )) + } _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND), } } @@ -372,6 +384,62 @@ impl TryFrom for AuthenticatorClientPinParameters { } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorAttestationMaterial { + pub certificate: Vec, + pub private_key: [u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], +} + +impl TryFrom for AuthenticatorAttestationMaterial { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 1 => certificate, + 2 => private_key, + } = extract_map(cbor_value)?; + } + let certificate = certificate.map(extract_byte_string).transpose()?.unwrap(); + let private_key = private_key.map(extract_byte_string).transpose()?.unwrap(); + if private_key.len() != key_material::ATTESTATION_PRIVATE_KEY_LENGTH { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); + } + let private_key = array_ref!(private_key, 0, key_material::ATTESTATION_PRIVATE_KEY_LENGTH); + Ok(AuthenticatorAttestationMaterial { + certificate, + private_key: *private_key, + }) + } +} + +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorVendorConfigureParameters { + pub lockdown: bool, + pub attestation_material: Option, +} + +impl TryFrom for AuthenticatorVendorConfigureParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 1 => lockdown, + 2 => attestation_material, + } = extract_map(cbor_value)?; + } + let lockdown = lockdown.map_or(Ok(false), extract_bool)?; + let attestation_material = attestation_material + .map(AuthenticatorAttestationMaterial::try_from) + .transpose()?; + Ok(AuthenticatorVendorConfigureParameters { + lockdown, + attestation_material, + }) + } +} + #[cfg(test)] mod test { use super::super::data_formats::{ diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4c5687a..50e0bbf 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -29,7 +29,7 @@ mod timed_permission; use self::command::MAX_CREDENTIAL_COUNT_IN_LIST; use self::command::{ AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, - AuthenticatorMakeCredentialParameters, Command, + AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, }; #[cfg(feature = "with_ctap2_1")] use self::data_formats::AuthenticatorTransport; @@ -44,7 +44,7 @@ use self::pin_protocol_v1::PinPermission; use self::pin_protocol_v1::PinProtocolV1; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, - AuthenticatorMakeCredentialResponse, ResponseData, + AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; use self::storage::PersistentStore; @@ -358,6 +358,10 @@ where #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands + // Vendor specific commands + Command::AuthenticatorVendorConfigure(params) => { + self.process_vendor_configure(params, cid) + } }; #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Sending response: {:#?}", response).unwrap(); @@ -919,6 +923,63 @@ where Ok(ResponseData::AuthenticatorSelection) } + fn process_vendor_configure( + &mut self, + params: AuthenticatorVendorConfigureParameters, + cid: ChannelID, + ) -> Result { + (self.check_user_presence)(cid)?; + + // Sanity checks + let has_priv_key = self.persistent_store.attestation_private_key()?.is_some(); + let has_cert = self.persistent_store.attestation_certificate()?.is_some(); + + if params.attestation_material.is_some() { + let data = params.attestation_material.unwrap(); + if !has_cert { + self.persistent_store + .set_attestation_certificate(&data.certificate)?; + } + if !has_priv_key { + self.persistent_store + .set_attestation_private_key(&data.private_key)?; + } + }; + let has_priv_key = self.persistent_store.attestation_private_key()?.is_some(); + let has_cert = self.persistent_store.attestation_certificate()?.is_some(); + if params.lockdown { + // To avoid bricking the authenticator, we only allow lockdown + // to happen if both values are programmed or if both U2F/CTAP1 and + // batch attestation are disabled. + #[cfg(feature = "with_ctap1")] + let need_certificate = true; + #[cfg(not(feature = "with_ctap1"))] + let need_certificate = USE_BATCH_ATTESTATION; + + if (need_certificate && !(has_priv_key && has_cert)) + || libtock_drivers::crp::protect().is_err() + { + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } else { + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: has_cert, + pkey_programmed: has_priv_key, + lockdown_enabled: true, + }, + )) + } + } else { + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: has_cert, + pkey_programmed: has_priv_key, + lockdown_enabled: false, + }, + )) + } + } + pub fn generate_auth_data( &self, rp_id_hash: &[u8], @@ -941,6 +1002,7 @@ where #[cfg(test)] mod test { + use super::command::AuthenticatorAttestationMaterial; use super::data_formats::{ CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, @@ -2052,4 +2114,128 @@ mod test { last_counter = next_counter; } } + + #[test] + fn test_vendor_configure() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // Nothing should be configured at the beginning + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: None, + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: false, + pkey_programmed: false, + lockdown_enabled: false + } + )) + ); + + // Inject dummy values + let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let dummy_cert = [0xddu8; 20]; + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: dummy_key, + }), + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + lockdown_enabled: false + } + )) + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_certificate() + .unwrap() + .unwrap(), + dummy_cert + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_private_key() + .unwrap() + .unwrap(), + dummy_key + ); + + // Try to inject other dummy values and check that intial values are retained. + let other_dummy_key = [0x44u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: other_dummy_key, + }), + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + lockdown_enabled: false + } + )) + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_certificate() + .unwrap() + .unwrap(), + dummy_cert + ); + assert_eq!( + ctap_state + .persistent_store + .attestation_private_key() + .unwrap() + .unwrap(), + dummy_key + ); + + // Now try to lock the device + let response = ctap_state.process_vendor_configure( + AuthenticatorVendorConfigureParameters { + lockdown: true, + attestation_material: None, + }, + DUMMY_CHANNEL_ID, + ); + assert_eq!( + response, + Ok(ResponseData::AuthenticatorVendor( + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + lockdown_enabled: true + } + )) + ); + } } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 47e1d54..674a1f5 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -34,6 +34,7 @@ pub enum ResponseData { AuthenticatorReset, #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, + AuthenticatorVendor(AuthenticatorVendorResponse), } impl From for Option { @@ -48,6 +49,7 @@ impl From for Option { ResponseData::AuthenticatorReset => None, #[cfg(feature = "with_ctap2_1")] ResponseData::AuthenticatorSelection => None, + ResponseData::AuthenticatorVendor(data) => Some(data.into()), } } } @@ -231,6 +233,30 @@ impl From for cbor::Value { } } +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub struct AuthenticatorVendorResponse { + pub cert_programmed: bool, + pub pkey_programmed: bool, + pub lockdown_enabled: bool, +} + +impl From for cbor::Value { + fn from(vendor_response: AuthenticatorVendorResponse) -> Self { + let AuthenticatorVendorResponse { + cert_programmed, + pkey_programmed, + lockdown_enabled, + } = vendor_response; + + cbor_map_options! { + 1 => cert_programmed, + 2 => pkey_programmed, + 3 => lockdown_enabled, + } + } +} + #[cfg(test)] mod test { use super::super::data_formats::PackedAttestationStatement; From 3c93c8ddc649d07a580b59f78915b80e4f6d5c3e Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Tue, 1 Dec 2020 15:34:15 +0100 Subject: [PATCH 096/192] Remove compile time crypto material. --- Cargo.toml | 1 - build.rs | 59 ---------------------------------------- src/ctap/key_material.rs | 6 ---- src/ctap/storage.rs | 35 +++++------------------- 4 files changed, 7 insertions(+), 94 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed293cc..611b2a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ elf2tab = "0.6.0" enum-iterator = "0.6.0" [build-dependencies] -openssl = "0.10" uuid = { version = "0.8", features = ["v4"] } [profile.dev] diff --git a/build.rs b/build.rs index e981555..b581d8e 100644 --- a/build.rs +++ b/build.rs @@ -12,11 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use openssl::asn1; -use openssl::ec; -use openssl::nid::Nid; -use openssl::pkey::PKey; -use openssl::x509; use std::env; use std::fs::File; use std::io::Read; @@ -25,65 +20,11 @@ use std::path::Path; use uuid::Uuid; fn main() { - println!("cargo:rerun-if-changed=crypto_data/opensk.key"); - println!("cargo:rerun-if-changed=crypto_data/opensk_cert.pem"); println!("cargo:rerun-if-changed=crypto_data/aaguid.txt"); let out_dir = env::var_os("OUT_DIR").unwrap(); - let priv_key_bin_path = Path::new(&out_dir).join("opensk_pkey.bin"); - let cert_bin_path = Path::new(&out_dir).join("opensk_cert.bin"); let aaguid_bin_path = Path::new(&out_dir).join("opensk_aaguid.bin"); - // Load the OpenSSL PEM ECC key - let ecc_data = include_bytes!("crypto_data/opensk.key"); - let pkey = - ec::EcKey::private_key_from_pem(ecc_data).expect("Failed to load OpenSK private key file"); - - // Check key validity - pkey.check_key().unwrap(); - assert_eq!(pkey.group().curve_name(), Some(Nid::X9_62_PRIME256V1)); - - // Private keys generated by OpenSSL have variable size but we only handle - // constant size. Serialization is done in big endian so if the size is less - // than 32 bytes, we need to prepend with null bytes. - // If the size is 33 bytes, this means the serialized BigInt is negative. - // Any other size is invalid. - let priv_key_hex = pkey.private_key().to_hex_str().unwrap(); - let priv_key_vec = pkey.private_key().to_vec(); - let key_len = priv_key_vec.len(); - - assert!( - key_len <= 33, - "Invalid private key (too big): {} ({:#?})", - priv_key_hex, - priv_key_vec, - ); - - // Copy OpenSSL generated key to our vec, starting from the end - let mut output_vec = [0u8; 32]; - let min_key_len = std::cmp::min(key_len, 32); - output_vec[32 - min_key_len..].copy_from_slice(&priv_key_vec[key_len - min_key_len..]); - - // Create the raw private key out of the OpenSSL data - let mut priv_key_bin_file = File::create(&priv_key_bin_path).unwrap(); - priv_key_bin_file.write_all(&output_vec).unwrap(); - - // Convert the PEM certificate to DER and extract the serial for AAGUID - let input_pem_cert = include_bytes!("crypto_data/opensk_cert.pem"); - let cert = x509::X509::from_pem(input_pem_cert).expect("Failed to load OpenSK certificate"); - - // Do some sanity check on the certificate - assert!(cert - .public_key() - .unwrap() - .public_eq(&PKey::from_ec_key(pkey).unwrap())); - let now = asn1::Asn1Time::days_from_now(0).unwrap(); - assert!(cert.not_after() > now); - assert!(cert.not_before() <= now); - - let mut cert_bin_file = File::create(&cert_bin_path).unwrap(); - cert_bin_file.write_all(&cert.to_der().unwrap()).unwrap(); - let mut aaguid_bin_file = File::create(&aaguid_bin_path).unwrap(); let mut aaguid_txt_file = File::open("crypto_data/aaguid.txt").unwrap(); let mut content = String::new(); diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index 2563798..eec5456 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -17,9 +17,3 @@ pub const AAGUID_LENGTH: usize = 16; pub const AAGUID: &[u8; AAGUID_LENGTH] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); - -pub const ATTESTATION_CERTIFICATE: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/opensk_cert.bin")); - -pub const ATTESTATION_PRIVATE_KEY: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH] = - include_bytes!(concat!(env!("OUT_DIR"), "/opensk_pkey.bin")); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 5f17052..110184f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -115,36 +115,12 @@ impl PersistentStore { self.store.insert(key::CRED_RANDOM_SECRET, &cred_random)?; } - // TODO(jmichel): remove this when vendor command is in place - #[cfg(not(test))] - self.load_attestation_data_from_firmware()?; if self.store.find_handle(key::AAGUID)?.is_none() { self.set_aaguid(key_material::AAGUID)?; } Ok(()) } - // TODO(jmichel): remove this function when vendor command is in place. - #[cfg(not(test))] - fn load_attestation_data_from_firmware(&mut self) -> Result<(), Ctap2StatusCode> { - // The following 2 entries are meant to be written by vendor-specific commands. - if self - .store - .find_handle(key::ATTESTATION_PRIVATE_KEY)? - .is_none() - { - self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY)?; - } - if self - .store - .find_handle(key::ATTESTATION_CERTIFICATE)? - .is_none() - { - self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE)?; - } - Ok(()) - } - /// Returns the first matching credential. /// /// Returns `None` if no credentials are matched or if `check_cred_protect` is set and the first @@ -989,11 +965,14 @@ mod test { .is_none()); // Make sure the persistent keys are initialized. + // Put dummy values + let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + let dummy_cert = [0xddu8; 20]; persistent_store - .set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) + .set_attestation_private_key(&dummy_key) .unwrap(); persistent_store - .set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) + .set_attestation_certificate(&dummy_cert) .unwrap(); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); @@ -1001,11 +980,11 @@ mod test { persistent_store.reset(&mut rng).unwrap(); assert_eq!( &persistent_store.attestation_private_key().unwrap().unwrap(), - key_material::ATTESTATION_PRIVATE_KEY + &dummy_key ); assert_eq!( persistent_store.attestation_certificate().unwrap().unwrap(), - key_material::ATTESTATION_CERTIFICATE + &dummy_cert ); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); } From e35c41578ea61d31e07eefe0ba44fc6854387145 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Thu, 10 Dec 2020 16:17:09 +0100 Subject: [PATCH 097/192] Add configuration tool --- .gitignore | 1 + deploy.py | 43 ++++++++++ setup.sh | 3 + tools/configure.py | 197 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100755 tools/configure.py diff --git a/.gitignore b/.gitignore index 1b32046..611b278 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ Cargo.lock /reproducible/binaries.sha256sum /reproducible/elf2tab.txt /reproducible/reproduced.tar +__pycache__ diff --git a/deploy.py b/deploy.py index 42579a2..b3daa6f 100755 --- a/deploy.py +++ b/deploy.py @@ -710,6 +710,22 @@ class OpenSKInstaller: check=False, timeout=None, ).returncode + + # Configure OpenSK through vendor specific command if needed + if any([ + self.args.lock_device, + self.args.config_cert, + self.args.config_pkey, + ]): + # pylint: disable=g-import-not-at-top,import-outside-toplevel + import tools.configure + tools.configure.main( + argparse.Namespace( + batch=False, + certificate=self.args.config_cert, + priv_key=self.args.config_pkey, + lock=self.args.lock_device, + )) return 0 @@ -770,6 +786,33 @@ if __name__ == "__main__": help=("Erases the persistent storage when installing an application. " "All stored data will be permanently lost."), ) + main_parser.add_argument( + "--lock-device", + action="store_true", + default=False, + dest="lock_device", + help=("Try to disable JTAG at the end of the operations. This " + "operation may fail if the device is already locked or if " + "the certificate/private key are not programmed."), + ) + main_parser.add_argument( + "--inject-certificate", + default=None, + metavar="PEM_FILE", + type=argparse.FileType("rb"), + dest="config_cert", + help=("If this option is set, the corresponding certificate " + "will be programmed into the key as the last operation."), + ) + main_parser.add_argument( + "--inject-private-key", + default=None, + metavar="PEM_FILE", + type=argparse.FileType("rb"), + dest="config_pkey", + help=("If this option is set, the corresponding private key " + "will be programmed into the key as the last operation."), + ) main_parser.add_argument( "--programmer", metavar="METHOD", diff --git a/setup.sh b/setup.sh index 903e0e3..13ad9b0 100755 --- a/setup.sh +++ b/setup.sh @@ -44,3 +44,6 @@ rustup target add thumbv7em-none-eabi # Install dependency to create applications. mkdir -p elf2tab cargo install elf2tab --version 0.6.0 --root elf2tab/ + +# Install python dependencies to factory configure OpenSK (crypto, JTAG lockdown) +pip3 install --user --upgrade colorama tqdm cryptography fido2 diff --git a/tools/configure.py b/tools/configure.py new file mode 100755 index 0000000..eff1166 --- /dev/null +++ b/tools/configure.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# 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. +# 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. +# Lint as: python3 + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import argparse +import getpass +import datetime +import sys +import uuid + +import colorama +from tqdm.auto import tqdm + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + +from fido2 import ctap +from fido2 import ctap2 +from fido2 import hid + +OPENSK_VID_PID = (0x1915, 0x521F) +OPENSK_VENDOR_CONFIGURE = 0x40 + + +def fatal(msg): + tqdm.write("{style_begin}fatal:{style_end} {message}".format( + style_begin=colorama.Fore.RED + colorama.Style.BRIGHT, + style_end=colorama.Style.RESET_ALL, + message=msg)) + sys.exit(1) + + +def error(msg): + tqdm.write("{style_begin}error:{style_end} {message}".format( + style_begin=colorama.Fore.RED, + style_end=colorama.Style.RESET_ALL, + message=msg)) + + +def info(msg): + tqdm.write("{style_begin}info:{style_end} {message}".format( + style_begin=colorama.Fore.GREEN + colorama.Style.BRIGHT, + style_end=colorama.Style.RESET_ALL, + message=msg)) + + +def get_opensk_devices(batch_mode): + devices = [] + for dev in hid.CtapHidDevice.list_devices(): + if (dev.descriptor["vendor_id"], + dev.descriptor["product_id"]) == OPENSK_VID_PID: + if dev.capabilities & hid.CAPABILITY.CBOR: + if batch_mode: + devices.append(ctap2.CTAP2(dev)) + else: + return [ctap2.CTAP2(dev)] + return devices + + +def get_private_key(data, password=None): + # First we try without password + try: + return serialization.load_pem_private_key(data, password=None) + except TypeError: + # Maybe we need a password then + if sys.stdin.isatty(): + password = getpass.getpass(prompt="Private key password: ") + else: + password = sys.stdin.readline().rstrip() + return get_private_key(data, password=password.encode(sys.stdin.encoding)) + + +def main(args): + colorama.init() + # We need either both the certificate and the key or none + if bool(args.priv_key) ^ bool(args.certificate): + fatal("Certificate and private key must be set together or both omitted.") + + cbor_data = {1: args.lock} + + if args.priv_key: + cbor_data[1] = args.lock + priv_key = get_private_key(args.priv_key.read()) + if not isinstance(priv_key, ec.EllipticCurvePrivateKey): + fatal("Private key must be an Elliptic Curve one.") + if not isinstance(priv_key.curve, ec.SECP256R1): + fatal("Private key must use Secp256r1 curve.") + if priv_key.key_size != 256: + fatal("Private key must be 256 bits long.") + info("Private key is valid.") + + cert = x509.load_pem_x509_certificate(args.certificate.read()) + # Some sanity/validity checks + now = datetime.datetime.now() + if cert.not_valid_before > now: + fatal("Certificate validity starts in the future.") + if cert.not_valid_after <= now: + fatal("Certificate expired.") + pub_key = cert.public_key() + if not isinstance(pub_key, ec.EllipticCurvePublicKey): + fatal("Certificate public key must be an Elliptic Curve one.") + if not isinstance(pub_key.curve, ec.SECP256R1): + fatal("Certificate public key must use Secp256r1 curve.") + if pub_key.key_size != 256: + fatal("Certificate public key must be 256 bits long.") + if pub_key.public_numbers() != priv_key.public_key().public_numbers(): + fatal("Certificate public doesn't match with the private key.") + info("Certificate is valid.") + + cbor_data[2] = { + 1: + cert.public_bytes(serialization.Encoding.DER), + 2: + priv_key.private_numbers().private_value.to_bytes( + length=32, byteorder='big', signed=False) + } + + for authenticator in tqdm(get_opensk_devices(args.batch)): + # If the device supports it, wink to show which device + # we're going to program + if authenticator.device.capabilities & hid.CAPABILITY.WINK: + authenticator.device.wink() + aaguid = uuid.UUID(bytes=authenticator.get_info().aaguid) + info(("Programming device {} AAGUID {} ({}). " + "Please touch the device to confirm...").format( + authenticator.device.descriptor.get("product_string", "Unknown"), + aaguid, authenticator.device)) + try: + result = authenticator.send_cbor( + OPENSK_VENDOR_CONFIGURE, + data=cbor_data, + ) + info("Certificate: {}".format("Present" if result[1] else "Missing")) + info("Private Key: {}".format("Present" if result[2] else "Missing")) + if result[3]: + info("Device locked down!") + except ctap.CtapError as ex: + if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND: + error("Failed to configure OpenSK (unsupported command).") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--batch", + default=False, + action="store_true", + help=( + "When batch processing is used, all plugged OpenSK devices will " + "be programmed the same way. Otherwise (default) only the first seen " + "device will be programmed."), + ) + parser.add_argument( + "--certificate", + type=argparse.FileType("rb"), + default=None, + metavar="PEM_FILE", + dest="certificate", + help=("PEM file containing the certificate to inject into " + "OpenSK authenticator."), + ) + parser.add_argument( + "--private-key", + type=argparse.FileType("rb"), + default=None, + metavar="PEM_FILE", + dest="priv_key", + help=("PEM file containing the private key associated " + "with the certificate."), + ) + parser.add_argument( + "--lock-device", + default=False, + action="store_true", + dest="lock", + help=("Locks the device (i.e. bootloader and JTAG access). " + "This command can fail if the certificate or the private key " + "haven't been both programmed yet."), + ) + main(parser.parse_args()) From a1854bb98ae0edea56ee54ee6eea779f6e2e436a Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Fri, 11 Dec 2020 03:16:17 +0100 Subject: [PATCH 098/192] Update documentation --- README.md | 43 +++++++++++++++++++++++++++---------------- docs/install.md | 9 ++++++--- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ccb6fc7..cca4eac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # OpenSK logo -[![Build Status](https://travis-ci.org/google/OpenSK.svg?branch=master)](https://travis-ci.org/google/OpenSK) ![markdownlint](https://github.com/google/OpenSK/workflows/markdownlint/badge.svg?branch=master) ![pylint](https://github.com/google/OpenSK/workflows/pylint/badge.svg?branch=master) ![Cargo check](https://github.com/google/OpenSK/workflows/Cargo%20check/badge.svg?branch=master) @@ -31,7 +30,8 @@ our implementation was not reviewed nor officially tested and doesn't claim to be FIDO Certified. We started adding features of the upcoming next version of the [CTAP2.1 specifications](https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html). -The development is currently between 2.0 and 2.1, with updates hidden behind a feature flag. +The development is currently between 2.0 and 2.1, with updates hidden behind +a feature flag. Please add the flag `--ctap2.1` to the deploy command to include them. ### Cryptography @@ -58,8 +58,8 @@ For a more detailed guide, please refer to our ./setup.sh ``` -2. Next step is to install Tock OS as well as the OpenSK application on your - board (**Warning**: it will erase the locally stored credentials). Run: +1. Next step is to install Tock OS as well as the OpenSK application on your + board. Run: ```shell # Nordic nRF52840-DK board @@ -68,7 +68,17 @@ For a more detailed guide, please refer to our ./deploy.py --board=nrf52840_dongle --opensk ``` -3. On Linux, you may want to avoid the need for `root` privileges to interact +1. Finally you need to inejct the cryptographic material if you enabled + batch attestation or CTAP1/U2F compatibility (which is the case by + default): + + ```shell + ./tools/configure.py \ + --certificate=crypto_data/opensk_cert.pem \ + --private-key=crypto_data/opensk.key + ``` + +1. On Linux, you may want to avoid the need for `root` privileges to interact with the key. For that purpose we provide a udev rule file that can be installed with the following command: @@ -148,7 +158,7 @@ operation. The additional output looks like the following. -``` +```text # Allocation of 256 byte(s), aligned on 1 byte(s). The allocated address is # 0x2002401c. After this operation, 2 pointers have been allocated, totalling # 384 bytes (the total heap usage may be larger, due to alignment and @@ -163,12 +173,12 @@ A tool is provided to analyze such reports, in `tools/heapviz`. This tool parses the console output, identifies the lines corresponding to (de)allocation operations, and first computes some statistics: -- Address range used by the heap over this run of the program, -- Peak heap usage (how many useful bytes are allocated), -- Peak heap consumption (how many bytes are used by the heap, including - unavailable bytes between allocated blocks, due to alignment constraints and - memory fragmentation), -- Fragmentation overhead (difference between heap consumption and usage). +* Address range used by the heap over this run of the program, +* Peak heap usage (how many useful bytes are allocated), +* Peak heap consumption (how many bytes are used by the heap, including + unavailable bytes between allocated blocks, due to alignment constraints and + memory fragmentation), +* Fragmentation overhead (difference between heap consumption and usage). Then, the `heapviz` tool displays an animated "movie" of the allocated bytes in heap memory. Each frame in this "movie" shows bytes that are currently @@ -177,10 +187,11 @@ allocated. A new frame is generated for each (de)allocation operation. This tool uses the `ncurses` library, that you may have to install beforehand. You can control the tool with the following parameters: -- `--logfile` (required) to provide the file which contains the console output - to parse, -- `--fps` (optional) to customize the number of frames per second in the movie - animation. + +* `--logfile` (required) to provide the file which contains the console output + to parse, +* `--fps` (optional) to customize the number of frames per second in the movie + animation. ```shell cargo run --manifest-path tools/heapviz/Cargo.toml -- --logfile console.log --fps 50 diff --git a/docs/install.md b/docs/install.md index cf355fa..61866ff 100644 --- a/docs/install.md +++ b/docs/install.md @@ -125,6 +125,7 @@ This is the expected content after running our `setup.sh` script: File | Purpose ----------------- | -------------------------------------------------------- +`aaguid.txt` | Text file containaing the AAGUID value `opensk_ca.csr` | Certificate sign request for the Root CA `opensk_ca.key` | ECC secp256r1 private key used for the Root CA `opensk_ca.pem` | PEM encoded certificate of the Root CA @@ -136,9 +137,11 @@ File | Purpose If you want to use your own attestation certificate and private key, simply replace `opensk_cert.pem` and `opensk.key` files. -Our build script `build.rs` is responsible for converting `opensk_cert.pem` and -`opensk.key` files into raw data that is then used by the Rust file: -`src/ctap/key_material.rs`. +Our build script `build.rs` is responsible for converting `aaguid.txt` file +into raw data that is then used by the Rust file `src/ctap/key_material.rs`. + +Our configuration script `tools/configure.py` is responsible for configuring +an OpenSK device with the correct certificate and private key. ### Flashing a firmware From ca0606a5576a59b65cd9ed1b4f590f3084e4f317 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Fri, 11 Dec 2020 01:52:52 +0100 Subject: [PATCH 099/192] Bump versions to 1.0 for FIDO2 certification. --- Cargo.toml | 2 +- boards/nordic/nrf52840_mdk_dfu/src/main.rs | 2 +- patches/tock/02-usb.patch | 4 ++-- src/ctap/hid/mod.rs | 9 ++++----- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 611b2a0..15984a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ctap2" -version = "0.1.0" +version = "1.0.0" authors = [ "Fabian Kaczmarczyck ", "Guillaume Endignoux ", diff --git a/boards/nordic/nrf52840_mdk_dfu/src/main.rs b/boards/nordic/nrf52840_mdk_dfu/src/main.rs index a346da5..1eccb0e 100644 --- a/boards/nordic/nrf52840_mdk_dfu/src/main.rs +++ b/boards/nordic/nrf52840_mdk_dfu/src/main.rs @@ -48,7 +48,7 @@ static STRINGS: &'static [&'static str] = &[ // Product "OpenSK", // Serial number - "v0.1", + "v1.0", ]; // State for loading and holding applications. diff --git a/patches/tock/02-usb.patch b/patches/tock/02-usb.patch index 135369d..6220f6a 100644 --- a/patches/tock/02-usb.patch +++ b/patches/tock/02-usb.patch @@ -117,7 +117,7 @@ index d72d20482..118ea6d68 100644 + // Product + "OpenSK", + // Serial number -+ "v0.1", ++ "v1.0", +]; + // State for loading and holding applications. @@ -189,7 +189,7 @@ index 2ebb384d8..4a7bfffdd 100644 + // Product + "OpenSK", + // Serial number -+ "v0.1", ++ "v1.0", +]; + // State for loading and holding applications. diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 1ae8334..ef96eef 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -117,9 +117,8 @@ impl CtapHid { // CTAP specification (version 20190130) section 8.1.9.1.3 const PROTOCOL_VERSION: u8 = 2; - // The device version number is vendor-defined. For now we define them to be zero. - // TODO: Update with device version? - const DEVICE_VERSION_MAJOR: u8 = 0; + // The device version number is vendor-defined. + const DEVICE_VERSION_MAJOR: u8 = 1; const DEVICE_VERSION_MINOR: u8 = 0; const DEVICE_VERSION_BUILD: u8 = 0; @@ -574,7 +573,7 @@ mod test { 0x00, 0x01, 0x02, // Protocol version - 0x00, // Device version + 0x01, // Device version 0x00, 0x00, CtapHid::CAPABILITIES @@ -634,7 +633,7 @@ mod test { cid[2], cid[3], 0x02, // Protocol version - 0x00, // Device version + 0x01, // Device version 0x00, 0x00, CtapHid::CAPABILITIES From 7213c4ee996b5dc9481ffd18904177e519c33f27 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Fri, 11 Dec 2020 12:58:26 +0100 Subject: [PATCH 100/192] Address first round of comments. --- README.md | 2 +- docs/install.md | 2 +- src/ctap/command.rs | 85 +++++++++++++++++++++++++++++++++++++++-- src/ctap/mod.rs | 91 +++++++++++++++++++++++++++----------------- src/ctap/response.rs | 3 -- src/ctap/storage.rs | 3 +- tools/configure.py | 21 +++++++--- 7 files changed, 157 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index cca4eac..68e9f71 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ For a more detailed guide, please refer to our ./deploy.py --board=nrf52840_dongle --opensk ``` -1. Finally you need to inejct the cryptographic material if you enabled +1. Finally you need to inject the cryptographic material if you enabled batch attestation or CTAP1/U2F compatibility (which is the case by default): diff --git a/docs/install.md b/docs/install.md index 61866ff..d00b991 100644 --- a/docs/install.md +++ b/docs/install.md @@ -137,7 +137,7 @@ File | Purpose If you want to use your own attestation certificate and private key, simply replace `opensk_cert.pem` and `opensk.key` files. -Our build script `build.rs` is responsible for converting `aaguid.txt` file +Our build script `build.rs` is responsible for converting the `aaguid.txt` file into raw data that is then used by the Rust file `src/ctap/key_material.rs`. Our configuration script `tools/configure.py` is responsible for configuring diff --git a/src/ctap/command.rs b/src/ctap/command.rs index f2f2537..5dbd930 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -400,10 +400,10 @@ impl TryFrom for AuthenticatorAttestationMaterial { 2 => private_key, } = extract_map(cbor_value)?; } - let certificate = certificate.map(extract_byte_string).transpose()?.unwrap(); - let private_key = private_key.map(extract_byte_string).transpose()?.unwrap(); + let certificate = extract_byte_string(ok_or_missing(certificate)?)?; + let private_key = extract_byte_string(ok_or_missing(private_key)?)?; if private_key.len() != key_material::ATTESTATION_PRIVATE_KEY_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR); + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let private_key = array_ref!(private_key, 0, key_material::ATTESTATION_PRIVATE_KEY_LENGTH); Ok(AuthenticatorAttestationMaterial { @@ -638,4 +638,83 @@ mod test { let command = Command::deserialize(&cbor_bytes); assert_eq!(command, Ok(Command::AuthenticatorSelection)); } + + #[test] + fn test_vendor_configure() { + // Incomplete command + let mut cbor_bytes = vec![Command::AUTHENTICATOR_VENDOR_CONFIGURE]; + let command = Command::deserialize(&cbor_bytes); + assert_eq!(command, Err(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)); + + cbor_bytes.extend(&[0xA1, 0x01, 0xF5]); + let command = Command::deserialize(&cbor_bytes); + assert_eq!( + command, + Ok(Command::AuthenticatorVendorConfigure( + AuthenticatorVendorConfigureParameters { + lockdown: true, + attestation_material: None + } + )) + ); + + let dummy_cert = [0xddu8; 20]; + let dummy_pkey = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + + // Attestation key is too short. + let cbor_value = cbor_map! { + 1 => false, + 2 => cbor_map! { + 1 => dummy_cert, + 2 => dummy_pkey[..key_material::ATTESTATION_PRIVATE_KEY_LENGTH - 1] + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // Missing private key + let cbor_value = cbor_map! { + 1 => false, + 2 => cbor_map! { + 1 => dummy_cert + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + ); + + // Missing certificate + let cbor_value = cbor_map! { + 1 => false, + 2 => cbor_map! { + 2 => dummy_pkey + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + ); + + // Valid + let cbor_value = cbor_map! { + 1 => false, + 2 => cbor_map! { + 1 => dummy_cert, + 2 => dummy_pkey + } + }; + assert_eq!( + AuthenticatorVendorConfigureParameters::try_from(cbor_value), + Ok(AuthenticatorVendorConfigureParameters { + lockdown: false, + attestation_material: Some(AuthenticatorAttestationMaterial { + certificate: dummy_cert.to_vec(), + private_key: dummy_pkey + }) + }) + ); + } } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 50e0bbf..b6cf5b4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -931,22 +931,64 @@ where (self.check_user_presence)(cid)?; // Sanity checks - let has_priv_key = self.persistent_store.attestation_private_key()?.is_some(); - let has_cert = self.persistent_store.attestation_certificate()?.is_some(); + let current_priv_key = self.persistent_store.attestation_private_key()?; + let current_cert = self.persistent_store.attestation_certificate()?; - if params.attestation_material.is_some() { + let response = if params.attestation_material.is_some() { let data = params.attestation_material.unwrap(); - if !has_cert { - self.persistent_store - .set_attestation_certificate(&data.certificate)?; + + match (current_cert, current_priv_key) { + (Some(_), Some(_)) => { + // Device is fully programmed. + // We don't compare values to avoid giving an oracle + // about the private key. + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } + // Device is not programmed. + (None, None) => { + self.persistent_store + .set_attestation_certificate(&data.certificate)?; + self.persistent_store + .set_attestation_private_key(&data.private_key)?; + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } + // Device is partially programmed. Ensure the programmed value + // matched the given one before programming anything. + (Some(cert), None) => { + if cert != data.certificate { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + self.persistent_store + .set_attestation_private_key(&data.private_key)?; + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } + (None, Some(key)) => { + if key != data.private_key { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + self.persistent_store + .set_attestation_certificate(&data.certificate)?; + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } } - if !has_priv_key { - self.persistent_store - .set_attestation_private_key(&data.private_key)?; + } else { + AuthenticatorVendorResponse { + cert_programmed: current_cert.is_some(), + pkey_programmed: current_priv_key.is_some(), } }; - let has_priv_key = self.persistent_store.attestation_private_key()?.is_some(); - let has_cert = self.persistent_store.attestation_certificate()?.is_some(); if params.lockdown { // To avoid bricking the authenticator, we only allow lockdown // to happen if both values are programmed or if both U2F/CTAP1 and @@ -956,28 +998,13 @@ where #[cfg(not(feature = "with_ctap1"))] let need_certificate = USE_BATCH_ATTESTATION; - if (need_certificate && !(has_priv_key && has_cert)) + if (need_certificate && !(response.pkey_programmed && response.cert_programmed)) || libtock_drivers::crp::protect().is_err() { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) - } else { - Ok(ResponseData::AuthenticatorVendor( - AuthenticatorVendorResponse { - cert_programmed: has_cert, - pkey_programmed: has_priv_key, - lockdown_enabled: true, - }, - )) + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - } else { - Ok(ResponseData::AuthenticatorVendor( - AuthenticatorVendorResponse { - cert_programmed: has_cert, - pkey_programmed: has_priv_key, - lockdown_enabled: false, - }, - )) } + Ok(ResponseData::AuthenticatorVendor(response)) } pub fn generate_auth_data( @@ -2135,7 +2162,6 @@ mod test { AuthenticatorVendorResponse { cert_programmed: false, pkey_programmed: false, - lockdown_enabled: false } )) ); @@ -2159,7 +2185,6 @@ mod test { AuthenticatorVendorResponse { cert_programmed: true, pkey_programmed: true, - lockdown_enabled: false } )) ); @@ -2180,7 +2205,7 @@ mod test { dummy_key ); - // Try to inject other dummy values and check that intial values are retained. + // Try to inject other dummy values and check that initial values are retained. let other_dummy_key = [0x44u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; let response = ctap_state.process_vendor_configure( AuthenticatorVendorConfigureParameters { @@ -2198,7 +2223,6 @@ mod test { AuthenticatorVendorResponse { cert_programmed: true, pkey_programmed: true, - lockdown_enabled: false } )) ); @@ -2233,7 +2257,6 @@ mod test { AuthenticatorVendorResponse { cert_programmed: true, pkey_programmed: true, - lockdown_enabled: true } )) ); diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 674a1f5..9b9efc1 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -238,7 +238,6 @@ impl From for cbor::Value { pub struct AuthenticatorVendorResponse { pub cert_programmed: bool, pub pkey_programmed: bool, - pub lockdown_enabled: bool, } impl From for cbor::Value { @@ -246,13 +245,11 @@ impl From for cbor::Value { let AuthenticatorVendorResponse { cert_programmed, pkey_programmed, - lockdown_enabled, } = vendor_response; cbor_map_options! { 1 => cert_programmed, 2 => pkey_programmed, - 3 => lockdown_enabled, } } } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 110184f..a701325 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -964,8 +964,7 @@ mod test { .unwrap() .is_none()); - // Make sure the persistent keys are initialized. - // Put dummy values + // Make sure the persistent keys are initialized to dummy values. let dummy_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; let dummy_cert = [0xddu8; 20]; persistent_store diff --git a/tools/configure.py b/tools/configure.py index eff1166..cc00a1a 100755 --- a/tools/configure.py +++ b/tools/configure.py @@ -75,11 +75,11 @@ def get_opensk_devices(batch_mode): def get_private_key(data, password=None): - # First we try without password + # First we try without password. try: return serialization.load_pem_private_key(data, password=None) except TypeError: - # Maybe we need a password then + # Maybe we need a password then. if sys.stdin.isatty(): password = getpass.getpass(prompt="Private key password: ") else: @@ -134,7 +134,7 @@ def main(args): for authenticator in tqdm(get_opensk_devices(args.batch)): # If the device supports it, wink to show which device - # we're going to program + # we're going to program. if authenticator.device.capabilities & hid.CAPABILITY.WINK: authenticator.device.wink() aaguid = uuid.UUID(bytes=authenticator.get_info().aaguid) @@ -149,11 +149,20 @@ def main(args): ) info("Certificate: {}".format("Present" if result[1] else "Missing")) info("Private Key: {}".format("Present" if result[2] else "Missing")) - if result[3]: - info("Device locked down!") + if args.lock: + info("Device is now locked down!") except ctap.CtapError as ex: if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND: error("Failed to configure OpenSK (unsupported command).") + elif ex.code.value == 0xF2: # VENDOR_INTERNAL_ERROR + error(("Failed to configure OpenSK (lockdown conditions not met " + "or hardware error).")) + elif ex.code.value == ctap.CtapError.ERR.INVALID_PARAMETER: + error( + ("Failed to configure OpenSK (device is partially programmed but " + "the given cert/key don't match the ones currently programmed).")) + else: + error("Failed to configure OpenSK (unknown error: {}".format(ex)) if __name__ == "__main__": @@ -174,7 +183,7 @@ if __name__ == "__main__": metavar="PEM_FILE", dest="certificate", help=("PEM file containing the certificate to inject into " - "OpenSK authenticator."), + "the OpenSK authenticator."), ) parser.add_argument( "--private-key", From 8595ed5e286d3aaa6735d8c89facc2da3d0cb6d6 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Fri, 11 Dec 2020 23:56:53 +0100 Subject: [PATCH 101/192] Addressing review comments. --- patches/tock/07-firmware-protect.patch | 209 +++++++++++++++++-------- src/ctap/mod.rs | 89 +++++------ src/ctap/response.rs | 30 ++++ third_party/libtock-drivers/src/crp.rs | 39 ++++- 4 files changed, 244 insertions(+), 123 deletions(-) diff --git a/patches/tock/07-firmware-protect.patch b/patches/tock/07-firmware-protect.patch index c062648..d002647 100644 --- a/patches/tock/07-firmware-protect.patch +++ b/patches/tock/07-firmware-protect.patch @@ -1,9 +1,9 @@ diff --git a/boards/components/src/firmware_protection.rs b/boards/components/src/firmware_protection.rs new file mode 100644 -index 0000000..5eda591 +index 0000000..58695af --- /dev/null +++ b/boards/components/src/firmware_protection.rs -@@ -0,0 +1,67 @@ +@@ -0,0 +1,70 @@ +//! Component for firmware protection syscall interface. +//! +//! This provides one Component, `FirmwareProtectionComponent`, which implements a @@ -35,12 +35,13 @@ index 0000000..5eda591 + ($C:ty) => {{ + use capsules::firmware_protection; + use core::mem::MaybeUninit; -+ static mut BUF: MaybeUninit> = MaybeUninit::uninit(); ++ static mut BUF: MaybeUninit> = ++ MaybeUninit::uninit(); + &mut BUF + };}; +} + -+pub struct FirmwareProtectionComponent { ++pub struct FirmwareProtectionComponent { + board_kernel: &'static kernel::Kernel, + crp: C, +} @@ -54,16 +55,18 @@ index 0000000..5eda591 + } +} + -+impl Component for FirmwareProtectionComponent { -+ type StaticInput = &'static mut MaybeUninit>; -+ type Output = &'static firmware_protection::FirmwareProtection<'static, C>; ++impl Component ++ for FirmwareProtectionComponent ++{ ++ type StaticInput = &'static mut MaybeUninit>; ++ type Output = &'static firmware_protection::FirmwareProtection; + + unsafe fn finalize(self, static_buffer: Self::StaticInput) -> Self::Output { + let grant_cap = create_capability!(capabilities::MemoryAllocationCapability); + + static_init_half!( + static_buffer, -+ firmware_protection::FirmwareProtection<'static, C>, ++ firmware_protection::FirmwareProtection, + firmware_protection::FirmwareProtection::new( + self.crp, + self.board_kernel.create_grant(&grant_cap), @@ -84,21 +87,18 @@ index 917497a..520408f 100644 pub mod gpio; pub mod hd44780; diff --git a/boards/nordic/nrf52840_dongle/src/main.rs b/boards/nordic/nrf52840_dongle/src/main.rs -index 118ea6d..f340dd1 100644 +index 118ea6d..76436f3 100644 --- a/boards/nordic/nrf52840_dongle/src/main.rs +++ b/boards/nordic/nrf52840_dongle/src/main.rs -@@ -112,6 +112,10 @@ pub struct Platform { +@@ -112,6 +112,7 @@ pub struct Platform { 'static, nrf52840::usbd::Usbd<'static>, >, -+ crp: &'static capsules::firmware_protection::FirmwareProtection< -+ 'static, -+ nrf52840::uicr::Uicr, -+ >, ++ crp: &'static capsules::firmware_protection::FirmwareProtection, } impl kernel::Platform for Platform { -@@ -132,6 +136,7 @@ impl kernel::Platform for Platform { +@@ -132,6 +133,7 @@ impl kernel::Platform for Platform { capsules::analog_comparator::DRIVER_NUM => f(Some(self.analog_comparator)), nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)), capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)), @@ -106,7 +106,7 @@ index 118ea6d..f340dd1 100644 kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)), _ => f(None), } -@@ -355,6 +360,12 @@ pub unsafe fn reset_handler() { +@@ -355,6 +357,14 @@ pub unsafe fn reset_handler() { ) .finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd)); @@ -114,12 +114,14 @@ index 118ea6d..f340dd1 100644 + board_kernel, + nrf52840::uicr::Uicr::new(), + ) -+ .finalize(components::firmware_protection_component_helper!(nrf52840::uicr::Uicr)); ++ .finalize(components::firmware_protection_component_helper!( ++ nrf52840::uicr::Uicr ++ )); + nrf52_components::NrfClockComponent::new().finalize(()); let platform = Platform { -@@ -371,6 +382,7 @@ pub unsafe fn reset_handler() { +@@ -371,6 +381,7 @@ pub unsafe fn reset_handler() { analog_comparator, nvmc, usb, @@ -128,21 +130,18 @@ index 118ea6d..f340dd1 100644 }; diff --git a/boards/nordic/nrf52840dk/src/main.rs b/boards/nordic/nrf52840dk/src/main.rs -index b1d0d3c..a37d180 100644 +index b1d0d3c..3cfb38d 100644 --- a/boards/nordic/nrf52840dk/src/main.rs +++ b/boards/nordic/nrf52840dk/src/main.rs -@@ -180,6 +180,10 @@ pub struct Platform { +@@ -180,6 +180,7 @@ pub struct Platform { 'static, nrf52840::usbd::Usbd<'static>, >, -+ crp: &'static capsules::firmware_protection::FirmwareProtection< -+ 'static, -+ nrf52840::uicr::Uicr, -+ >, ++ crp: &'static capsules::firmware_protection::FirmwareProtection, } impl kernel::Platform for Platform { -@@ -201,6 +205,7 @@ impl kernel::Platform for Platform { +@@ -201,6 +202,7 @@ impl kernel::Platform for Platform { capsules::nonvolatile_storage_driver::DRIVER_NUM => f(Some(self.nonvolatile_storage)), nrf52840::nvmc::DRIVER_NUM => f(Some(self.nvmc)), capsules::usb::usb_ctap::DRIVER_NUM => f(Some(self.usb)), @@ -150,7 +149,7 @@ index b1d0d3c..a37d180 100644 kernel::ipc::DRIVER_NUM => f(Some(&self.ipc)), _ => f(None), } -@@ -480,6 +485,12 @@ pub unsafe fn reset_handler() { +@@ -480,6 +482,14 @@ pub unsafe fn reset_handler() { ) .finalize(components::usb_ctap_component_buf!(nrf52840::usbd::Usbd)); @@ -158,12 +157,14 @@ index b1d0d3c..a37d180 100644 + board_kernel, + nrf52840::uicr::Uicr::new(), + ) -+ .finalize(components::firmware_protection_component_helper!(nrf52840::uicr::Uicr)); ++ .finalize(components::firmware_protection_component_helper!( ++ nrf52840::uicr::Uicr ++ )); + nrf52_components::NrfClockComponent::new().finalize(()); let platform = Platform { -@@ -497,6 +508,7 @@ pub unsafe fn reset_handler() { +@@ -497,6 +507,7 @@ pub unsafe fn reset_handler() { nonvolatile_storage, nvmc, usb, @@ -185,10 +186,10 @@ index ae458b3..f536dad 100644 Ipc = 0x10000, diff --git a/capsules/src/firmware_protection.rs b/capsules/src/firmware_protection.rs new file mode 100644 -index 0000000..2c61d06 +index 0000000..dc46a13 --- /dev/null +++ b/capsules/src/firmware_protection.rs -@@ -0,0 +1,87 @@ +@@ -0,0 +1,85 @@ +//! Provides userspace control of firmware protection on a board. +//! +//! This allows an application to enable firware readout protection, @@ -213,7 +214,7 @@ index 0000000..2c61d06 +//! Syscall Interface +//! ----------------- +//! -+//! - Stability: 2 - Stable ++//! - Stability: 0 - Draft +//! +//! ### Command +//! @@ -222,10 +223,10 @@ index 0000000..2c61d06 +//! #### `command_num` +//! +//! - `0`: Driver check. -+//! - `1`: Enable firmware readout protection (aka CRP). ++//! - `1`: Get current firmware readout protection (aka CRP) state. ++//! - `2`: Set current firmware readout protection (aka CRP) state. +//! + -+use core::marker::PhantomData; +use kernel::hil; +use kernel::{AppId, Callback, Driver, Grant, ReturnCode}; + @@ -233,43 +234,41 @@ index 0000000..2c61d06 +use crate::driver; +pub const DRIVER_NUM: usize = driver::NUM::FirmwareProtection as usize; + -+pub struct FirmwareProtection<'a, C: hil::firmware_protection::FirmwareProtection> { ++pub struct FirmwareProtection { + crp_unit: C, + apps: Grant>, -+ _phantom: PhantomData<&'a C>, +} + -+impl<'a, C: hil::firmware_protection::FirmwareProtection> FirmwareProtection<'a, C> { -+ pub fn new( -+ crp_unit: C, -+ apps: Grant>, -+ ) -> Self { -+ Self { -+ crp_unit, -+ apps, -+ _phantom: PhantomData, -+ } ++impl FirmwareProtection { ++ pub fn new(crp_unit: C, apps: Grant>) -> Self { ++ Self { crp_unit, apps } + } +} + -+impl<'a, C: hil::firmware_protection::FirmwareProtection> Driver for FirmwareProtection<'a, C> { ++impl Driver for FirmwareProtection { + /// + /// ### Command numbers + /// + /// * `0`: Returns non-zero to indicate the driver is present. -+ /// * `1`: Enable firmware protection. -+ fn command(&self, command_num: usize, _: usize, _: usize, appid: AppId) -> ReturnCode { ++ /// * `1`: Gets firmware protection state. ++ /// * `2`: Sets firmware protection state. ++ fn command(&self, command_num: usize, data: usize, _: usize, appid: AppId) -> ReturnCode { + match command_num { + // return if driver is available + 0 => ReturnCode::SUCCESS, + -+ // enable firmware protection -+ 1 => { -+ self.apps.enter(appid, |_, _| { -+ self.crp_unit.protect() ++ 1 => self ++ .apps ++ .enter(appid, |_, _| ReturnCode::SuccessWithValue { ++ value: self.crp_unit.get_protection() as usize, + }) -+ .unwrap_or_else(|err| err.into()) -+ } ++ .unwrap_or_else(|err| err.into()), ++ ++ // sets firmware protection ++ 2 => self ++ .apps ++ .enter(appid, |_, _| self.crp_unit.set_protection(data.into())) ++ .unwrap_or_else(|err| err.into()), + + // default + _ => ReturnCode::ENOSUPPORT, @@ -289,10 +288,18 @@ index e4423fe..7538aad 100644 pub mod ft6x06; pub mod fxos8700cq; diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs -index 3bb8b5a..b8895d3 100644 +index 3bb8b5a..19c2e90 100644 --- a/chips/nrf52/src/uicr.rs +++ b/chips/nrf52/src/uicr.rs -@@ -8,6 +8,7 @@ use kernel::hil; +@@ -1,13 +1,14 @@ + //! User information configuration registers + +- + use enum_primitive::cast::FromPrimitive; ++use hil::firmware_protection::ProtectionLevel; + use kernel::common::registers::{register_bitfields, register_structs, ReadWrite}; + use kernel::common::StaticRef; + use kernel::hil; use kernel::ReturnCode; use crate::gpio::Pin; @@ -300,21 +307,47 @@ index 3bb8b5a..b8895d3 100644 const UICR_BASE: StaticRef = unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) }; -@@ -210,3 +211,20 @@ impl Uicr { +@@ -210,3 +211,46 @@ impl Uicr { self.registers.approtect.write(ApProtect::PALL::ENABLED); } } + +impl hil::firmware_protection::FirmwareProtection for Uicr { -+ fn protect(&self) -> ReturnCode { ++ fn get_protection(&self) -> ProtectionLevel { ++ let ap_protect_state = self.is_ap_protect_enabled(); ++ let cpu_debug_state = self ++ .registers ++ .debugctrl ++ .matches_all(DebugControl::CPUNIDEN::ENABLED + DebugControl::CPUFPBEN::ENABLED); ++ match (ap_protect_state, cpu_debug_state) { ++ (false, _) => ProtectionLevel::NoProtection, ++ (true, true) => ProtectionLevel::JtagDisabled, ++ (true, false) => ProtectionLevel::FullyLocked, ++ } ++ } ++ ++ fn set_protection(&self, level: ProtectionLevel) -> ReturnCode { ++ let current_level = self.get_protection(); ++ if current_level > level || level == ProtectionLevel::Unknown { ++ return ReturnCode::EINVAL; ++ } ++ if current_level == level { ++ return ReturnCode::EALREADY; ++ } + unsafe { NVMC.configure_writeable() }; -+ self.set_ap_protect(); -+ // Prevent CPU debug -+ self.registers.debugctrl.write( -+ DebugControl::CPUNIDEN::DISABLED + DebugControl::CPUFPBEN::DISABLED); -+ // TODO(jmichel): Kill bootloader if present ++ if level >= ProtectionLevel::JtagDisabled { ++ self.set_ap_protect(); ++ } ++ if level >= ProtectionLevel::FullyLocked { ++ // Prevent CPU debug and flash patching. Leaving these enabled could ++ // allow to circumvent protection. ++ self.registers ++ .debugctrl ++ .write(DebugControl::CPUNIDEN::DISABLED + DebugControl::CPUFPBEN::DISABLED); ++ // TODO(jmichel): Kill bootloader if present ++ } + unsafe { NVMC.configure_readonly() }; -+ if self.is_ap_protect_enabled() { ++ if self.get_protection() == level { + ReturnCode::SUCCESS + } else { + ReturnCode::FAIL @@ -323,17 +356,57 @@ index 3bb8b5a..b8895d3 100644 +} diff --git a/kernel/src/hil/firmware_protection.rs b/kernel/src/hil/firmware_protection.rs new file mode 100644 -index 0000000..e43fdf0 +index 0000000..de08246 --- /dev/null +++ b/kernel/src/hil/firmware_protection.rs -@@ -0,0 +1,8 @@ +@@ -0,0 +1,48 @@ +//! Interface for Firmware Protection, also called Code Readout Protection. + +use crate::returncode::ReturnCode; + ++#[derive(PartialOrd, PartialEq)] ++pub enum ProtectionLevel { ++ /// Unsupported feature ++ Unknown = 0, ++ /// This should be the factory default for the chip. ++ NoProtection = 1, ++ /// At this level, only JTAG/SWD are disabled but other debugging ++ /// features may still be enabled. ++ JtagDisabled = 2, ++ /// This is the maximum level of protection the chip supports. ++ /// At this level, JTAG and all other features are expected to be ++ /// disabled and only a full chip erase may allow to recover from ++ /// that state. ++ FullyLocked = 0xff, ++} ++ ++impl From for ProtectionLevel { ++ fn from(value: usize) -> Self { ++ match value { ++ 1 => ProtectionLevel::NoProtection, ++ 2 => ProtectionLevel::JtagDisabled, ++ 0xff => ProtectionLevel::FullyLocked, ++ _ => ProtectionLevel::Unknown, ++ } ++ } ++} ++ +pub trait FirmwareProtection { -+ /// Disable debug ports and protects the device. -+ fn protect(&self) -> ReturnCode; ++ /// Gets the current firmware protection level. ++ /// This doesn't fail and always returns a value. ++ fn get_protection(&self) -> ProtectionLevel; ++ ++ /// Sets the firmware protection level. ++ /// There are four valid return values: ++ /// - SUCCESS: protection level has been set to `level` ++ /// - FAIL: something went wrong while setting the protection ++ /// level and the effective protection level is not the one ++ /// that was requested. ++ /// - EALREADY: the requested protection level is already the ++ /// level that is set. ++ /// - EINVAL: unsupported protection level or the requested ++ /// protection level is lower than the currently set one. ++ fn set_protection(&self, level: ProtectionLevel) -> ReturnCode; +} diff --git a/kernel/src/hil/mod.rs b/kernel/src/hil/mod.rs index 4f42afa..83e7702 100644 diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index b6cf5b4..dfa2d9b 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -67,6 +67,7 @@ use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(feature = "debug_ctap")] use libtock_drivers::console::Console; +use libtock_drivers::crp; use libtock_drivers::timer::{ClockValue, Duration}; // This flag enables or disables basic attestation for FIDO2. U2F is unaffected by @@ -934,59 +935,43 @@ where let current_priv_key = self.persistent_store.attestation_private_key()?; let current_cert = self.persistent_store.attestation_certificate()?; - let response = if params.attestation_material.is_some() { - let data = params.attestation_material.unwrap(); - - match (current_cert, current_priv_key) { - (Some(_), Some(_)) => { - // Device is fully programmed. - // We don't compare values to avoid giving an oracle - // about the private key. - AuthenticatorVendorResponse { - cert_programmed: true, - pkey_programmed: true, - } - } - // Device is not programmed. - (None, None) => { - self.persistent_store - .set_attestation_certificate(&data.certificate)?; - self.persistent_store - .set_attestation_private_key(&data.private_key)?; - AuthenticatorVendorResponse { - cert_programmed: true, - pkey_programmed: true, - } - } - // Device is partially programmed. Ensure the programmed value - // matched the given one before programming anything. - (Some(cert), None) => { - if cert != data.certificate { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - self.persistent_store - .set_attestation_private_key(&data.private_key)?; - AuthenticatorVendorResponse { - cert_programmed: true, - pkey_programmed: true, - } - } - (None, Some(key)) => { - if key != data.private_key { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - self.persistent_store - .set_attestation_certificate(&data.certificate)?; - AuthenticatorVendorResponse { - cert_programmed: true, - pkey_programmed: true, - } - } - } - } else { - AuthenticatorVendorResponse { + let response = match params.attestation_material { + // Only reading values. + None => AuthenticatorVendorResponse { cert_programmed: current_cert.is_some(), pkey_programmed: current_priv_key.is_some(), + }, + // Device is already fully programmed. We don't leak information. + Some(_) if current_cert.is_some() && current_priv_key.is_some() => { + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } + } + // Device is partially or not programmed. We complete the process. + Some(data) => { + if let Some(current_cert) = ¤t_cert { + if current_cert != &data.certificate { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + } + if let Some(current_priv_key) = ¤t_priv_key { + if current_priv_key != &data.private_key { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + } + if current_cert.is_none() { + self.persistent_store + .set_attestation_certificate(&data.certificate)?; + } + if current_priv_key.is_none() { + self.persistent_store + .set_attestation_private_key(&data.private_key)?; + } + AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: true, + } } }; if params.lockdown { @@ -999,7 +984,7 @@ where let need_certificate = USE_BATCH_ATTESTATION; if (need_certificate && !(response.pkey_programmed && response.cert_programmed)) - || libtock_drivers::crp::protect().is_err() + || crp::set_protection(crp::ProtectionLevel::FullyLocked).is_err() { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 9b9efc1..6422959 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -424,4 +424,34 @@ mod test { let response_cbor: Option = ResponseData::AuthenticatorSelection.into(); assert_eq!(response_cbor, None); } + + #[test] + fn test_vendor_response_into_cbor() { + let response_cbor: Option = + ResponseData::AuthenticatorVendor(AuthenticatorVendorResponse { + cert_programmed: true, + pkey_programmed: false, + }) + .into(); + assert_eq!( + response_cbor, + Some(cbor_map_options! { + 1 => true, + 2 => false, + }) + ); + let response_cbor: Option = + ResponseData::AuthenticatorVendor(AuthenticatorVendorResponse { + cert_programmed: false, + pkey_programmed: true, + }) + .into(); + assert_eq!( + response_cbor, + Some(cbor_map_options! { + 1 => false, + 2 => true, + }) + ); + } } diff --git a/third_party/libtock-drivers/src/crp.rs b/third_party/libtock-drivers/src/crp.rs index fab747f..3b686ca 100644 --- a/third_party/libtock-drivers/src/crp.rs +++ b/third_party/libtock-drivers/src/crp.rs @@ -5,7 +5,35 @@ const DRIVER_NUMBER: usize = 0x00008; mod command_nr { pub const AVAILABLE: usize = 0; - pub const PROTECT: usize = 1; + pub const GET_PROTECTION: usize = 1; + pub const SET_PROTECTION: usize = 2; +} + +#[derive(PartialOrd, PartialEq)] +pub enum ProtectionLevel { + /// Unsupported feature + Unknown = 0, + /// This should be the factory default for the chip. + NoProtection = 1, + /// At this level, only JTAG/SWD are disabled but other debugging + /// features may still be enabled. + JtagDisabled = 2, + /// This is the maximum level of protection the chip supports. + /// At this level, JTAG and all other features are expected to be + /// disabled and only a full chip erase may allow to recover from + /// that state. + FullyLocked = 0xff, +} + +impl From for ProtectionLevel { + fn from(value: usize) -> Self { + match value { + 1 => ProtectionLevel::NoProtection, + 2 => ProtectionLevel::JtagDisabled, + 0xff => ProtectionLevel::FullyLocked, + _ => ProtectionLevel::Unknown, + } + } } pub fn is_available() -> TockResult<()> { @@ -13,7 +41,12 @@ pub fn is_available() -> TockResult<()> { Ok(()) } -pub fn protect() -> TockResult<()> { - syscalls::command(DRIVER_NUMBER, command_nr::PROTECT, 0, 0)?; +pub fn get_protection() -> TockResult { + let current_level = syscalls::command(DRIVER_NUMBER, command_nr::GET_PROTECTION, 0, 0)?; + Ok(current_level.into()) +} + +pub fn set_protection(level: ProtectionLevel) -> TockResult<()> { + syscalls::command(DRIVER_NUMBER, command_nr::SET_PROTECTION, level as usize, 0)?; Ok(()) } From 712fa0f6a2bc3ecf602f5729647ed6f52b6352fc Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Mon, 14 Dec 2020 19:43:59 +0100 Subject: [PATCH 102/192] Small improvements on kernel patch --- patches/tock/07-firmware-protect.patch | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/patches/tock/07-firmware-protect.patch b/patches/tock/07-firmware-protect.patch index d002647..365b20a 100644 --- a/patches/tock/07-firmware-protect.patch +++ b/patches/tock/07-firmware-protect.patch @@ -186,7 +186,7 @@ index ae458b3..f536dad 100644 Ipc = 0x10000, diff --git a/capsules/src/firmware_protection.rs b/capsules/src/firmware_protection.rs new file mode 100644 -index 0000000..dc46a13 +index 0000000..8cf63d6 --- /dev/null +++ b/capsules/src/firmware_protection.rs @@ -0,0 +1,85 @@ @@ -204,8 +204,8 @@ index 0000000..dc46a13 +//! # use kernel::static_init; +//! +//! let crp = static_init!( -+//! capsules::firware_protection::FirmwareProtection<'static>, -+//! capsules::firware_protection::FirmwareProtection::new( ++//! capsules::firmware_protection::FirmwareProtection, ++//! capsules::firmware_protection::FirmwareProtection::new( +//! nrf52840::uicr::Uicr, +//! board_kernel.create_grant(&grant_cap), +//! ); @@ -288,7 +288,7 @@ index e4423fe..7538aad 100644 pub mod ft6x06; pub mod fxos8700cq; diff --git a/chips/nrf52/src/uicr.rs b/chips/nrf52/src/uicr.rs -index 3bb8b5a..19c2e90 100644 +index 3bb8b5a..ea96cb2 100644 --- a/chips/nrf52/src/uicr.rs +++ b/chips/nrf52/src/uicr.rs @@ -1,13 +1,14 @@ @@ -307,7 +307,7 @@ index 3bb8b5a..19c2e90 100644 const UICR_BASE: StaticRef = unsafe { StaticRef::new(0x10001000 as *const UicrRegisters) }; -@@ -210,3 +211,46 @@ impl Uicr { +@@ -210,3 +211,49 @@ impl Uicr { self.registers.approtect.write(ApProtect::PALL::ENABLED); } } @@ -334,19 +334,22 @@ index 3bb8b5a..19c2e90 100644 + if current_level == level { + return ReturnCode::EALREADY; + } ++ + unsafe { NVMC.configure_writeable() }; + if level >= ProtectionLevel::JtagDisabled { + self.set_ap_protect(); + } ++ + if level >= ProtectionLevel::FullyLocked { + // Prevent CPU debug and flash patching. Leaving these enabled could + // allow to circumvent protection. + self.registers + .debugctrl + .write(DebugControl::CPUNIDEN::DISABLED + DebugControl::CPUFPBEN::DISABLED); -+ // TODO(jmichel): Kill bootloader if present ++ // TODO(jmichel): prevent returning into bootloader if present + } + unsafe { NVMC.configure_readonly() }; ++ + if self.get_protection() == level { + ReturnCode::SUCCESS + } else { From 763bc031aa9a6895222515105456684cdf2b2a76 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 18 Dec 2020 12:45:19 +0100 Subject: [PATCH 103/192] updates command bytes --- src/ctap/command.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 5dbd930..ecfae9e 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -65,12 +65,13 @@ impl Command { const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; // TODO(kaczmarczyck) use or remove those constants const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; - const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; - const AUTHENTICATOR_SELECTION: u8 = 0xB0; - const AUTHENTICATOR_CONFIG: u8 = 0xC0; + const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0x0A; + const AUTHENTICATOR_SELECTION: u8 = 0x0B; + const AUTHENTICATOR_LARGE_BLOBS: u8 = 0x0C; + const AUTHENTICATOR_CONFIG: u8 = 0x0D; + const _AUTHENTICATOR_VENDOR_FIRST: u8 = 0x40; const AUTHENTICATOR_VENDOR_CONFIGURE: u8 = 0x40; - const AUTHENTICATOR_VENDOR_FIRST_UNUSED: u8 = 0x41; - const AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF; + const _AUTHENTICATOR_VENDOR_LAST: u8 = 0xBF; pub fn deserialize(bytes: &[u8]) -> Result { if bytes.is_empty() { From d6adab4381f43cfe3188ec68761fe23006c95838 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 18 Dec 2020 11:52:29 +0100 Subject: [PATCH 104/192] updates status codes for RD02 --- src/ctap/hid/mod.rs | 2 +- src/ctap/mod.rs | 22 ++++++++++------------ src/ctap/pin_protocol_v1.rs | 15 ++++++--------- src/ctap/status_code.rs | 23 +++++++++++------------ src/ctap/storage.rs | 4 ++-- 5 files changed, 30 insertions(+), 36 deletions(-) diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index ef96eef..01c0b11 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -219,7 +219,7 @@ impl CtapHid { cid, cmd: CtapHid::COMMAND_CBOR, payload: vec![ - Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG as u8, + Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8, ], }) .unwrap() diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index dfa2d9b..7a23a1d 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -371,10 +371,8 @@ where let mut response_vec = vec![0x00]; if let Some(value) = response_data.into() { if !cbor::write(value, &mut response_vec) { - response_vec = vec![ - Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR - as u8, - ]; + response_vec = + vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8]; } } response_vec @@ -496,7 +494,7 @@ where } None => { if self.persistent_store.pin_hash()?.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED); + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); } if options.uv { return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); @@ -542,13 +540,13 @@ where auth_data.extend(&self.persistent_store.aaguid()?); // The length is fixed to 0x20 or 0x70 and fits one byte. if credential_id.len() > 0xFF { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } auth_data.extend(vec![0x00, credential_id.len() as u8]); auth_data.extend(&credential_id); let cose_key = match pk.to_cose_key() { Some(cose_key) => cose_key, - None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR), + None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), }; auth_data.extend(cose_key); if has_extension_output { @@ -558,7 +556,7 @@ where "credProtect" => cred_protect_policy, }; if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } @@ -639,7 +637,7 @@ where "hmac-secret" => encrypted_output, }; if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } @@ -722,7 +720,7 @@ where let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); if hmac_secret_input.is_some() && !options.up { // The extension is actually supported, but we need user presence. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION); } // The user verification bit depends on the existance of PIN auth, since we do @@ -1592,7 +1590,7 @@ mod test { assert_eq!( get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION) ); } @@ -1643,7 +1641,7 @@ mod test { assert_eq!( get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION) ); } diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 410dac7..96e92b3 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -59,7 +59,7 @@ fn encrypt_hmac_secret_output( cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { if salt_enc.len() != 32 && salt_enc.len() != 64 { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); @@ -232,7 +232,7 @@ impl PinProtocolV1 { } } // This status code is not explicitly mentioned in the specification. - None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED), + None => return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED), } persistent_store.reset_pin_retries()?; self.consecutive_pin_mismatches = 0; @@ -400,7 +400,7 @@ impl PinProtocolV1 { pin_auth: Option>, ) -> Result<(), Ctap2StatusCode> { if min_pin_length_rp_ids.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } if persistent_store.pin_hash()?.is_some() { match pin_auth { @@ -419,7 +419,7 @@ impl PinProtocolV1 { // 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); + // return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); // } if !verify_pin_auth(&self.pin_uv_auth_token, &message, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); @@ -593,7 +593,7 @@ impl PinProtocolV1 { // HMAC-secret does the same 16 byte truncated check. 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); + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } @@ -1174,10 +1174,7 @@ mod test { 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) - ); + assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); let salt_enc = [0x5E; 64]; let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 097d7ec..40f258e 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -31,11 +31,10 @@ pub enum Ctap2StatusCode { CTAP2_ERR_INVALID_CBOR = 0x12, 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_LARGE_BLOB_STORAGE_FULL = 0x18, CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19, CTAP2_ERR_PROCESSING = 0x21, CTAP2_ERR_INVALID_CREDENTIAL = 0x22, @@ -57,7 +56,7 @@ pub enum Ctap2StatusCode { CTAP2_ERR_PIN_AUTH_INVALID = 0x33, CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34, CTAP2_ERR_PIN_NOT_SET = 0x35, - CTAP2_ERR_PIN_REQUIRED = 0x36, + CTAP2_ERR_PUAT_REQUIRED = 0x36, CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37, CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38, CTAP2_ERR_REQUEST_TOO_LARGE = 0x39, @@ -68,14 +67,15 @@ pub enum Ctap2StatusCode { CTAP2_ERR_INTEGRITY_FAILURE = 0x3D, #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_INVALID_SUBCOMMAND = 0x3E, + #[cfg(feature = "with_ctap2_1")] + CTAP2_ERR_UV_INVALID = 0x3F, + #[cfg(feature = "with_ctap2_1")] + CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40, CTAP1_ERR_OTHER = 0x7F, - CTAP2_ERR_SPEC_LAST = 0xDF, - CTAP2_ERR_EXTENSION_FIRST = 0xE0, - CTAP2_ERR_EXTENSION_LAST = 0xEF, - // CTAP2_ERR_VENDOR_FIRST = 0xF0, - CTAP2_ERR_VENDOR_RESPONSE_TOO_LONG = 0xF0, - CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR = 0xF1, - + _CTAP2_ERR_SPEC_LAST = 0xDF, + _CTAP2_ERR_EXTENSION_FIRST = 0xE0, + _CTAP2_ERR_EXTENSION_LAST = 0xEF, + _CTAP2_ERR_VENDOR_FIRST = 0xF0, /// An internal invariant is broken. /// /// This type of error is unexpected and the current state is undefined. @@ -85,6 +85,5 @@ pub enum Ctap2StatusCode { /// /// It may be possible that some of those errors are actually internal errors. CTAP2_ERR_VENDOR_HARDWARE_FAILURE = 0xF3, - - CTAP2_ERR_VENDOR_LAST = 0xFF, + _CTAP2_ERR_VENDOR_LAST = 0xFF, } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a701325..73bbc16 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -577,7 +577,7 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result if cbor::write(credential.into(), &mut data) { Ok(data) } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } } @@ -600,7 +600,7 @@ fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap if cbor::write(cbor_array_vec!(rp_ids), &mut data) { Ok(data) } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } } From f67fdbc451963edf8b64e5ceeb14e0138078b334 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 22 Dec 2020 15:33:14 +0100 Subject: [PATCH 105/192] Add erase_storage application example --- deploy.py | 11 +++++++- examples/erase_storage.rs | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 examples/erase_storage.rs diff --git a/deploy.py b/deploy.py index b3daa6f..d5dcf2d 100755 --- a/deploy.py +++ b/deploy.py @@ -947,7 +947,16 @@ if __name__ == "__main__": dest="application", action="store_const", const="store_latency", - help=("Compiles and installs the store_latency example.")) + help=("Compiles and installs the store_latency example which print " + "latency statistics of the persistent store library.")) + apps_group.add_argument( + "--erase_storage", + dest="application", + action="store_const", + const="erase_storage", + help=("Compiles and installs the erase_storage example which erases " + "the storage. During operation the dongle red light is on. Once " + "the operation is completed the dongle green light is on.")) apps_group.add_argument( "--panic_test", dest="application", diff --git a/examples/erase_storage.rs b/examples/erase_storage.rs new file mode 100644 index 0000000..6076348 --- /dev/null +++ b/examples/erase_storage.rs @@ -0,0 +1,53 @@ +// 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. +// 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. + +#![no_std] + +extern crate lang_items; + +use core::fmt::Write; +use ctap2::embedded_flash::new_storage; +use libtock_drivers::console::Console; +use libtock_drivers::led; +use libtock_drivers::result::FlexUnwrap; +use persistent_store::{Storage, StorageIndex}; + +fn is_page_erased(storage: &dyn Storage, page: usize) -> bool { + let index = StorageIndex { page, byte: 0 }; + let length = storage.page_size(); + storage + .read_slice(index, length) + .unwrap() + .iter() + .all(|&x| x == 0xff) +} + +fn main() { + led::get(1).flex_unwrap().on().flex_unwrap(); // red on dongle + const NUM_PAGES: usize = 20; // should be at least ctap::storage::NUM_PAGES + let mut storage = new_storage(NUM_PAGES); + writeln!(Console::new(), "Erase {} pages of storage:", NUM_PAGES).unwrap(); + for page in 0..NUM_PAGES { + write!(Console::new(), "- Page {} ", page).unwrap(); + if is_page_erased(&storage, page) { + writeln!(Console::new(), "skipped (was already erased).").unwrap(); + } else { + storage.erase_page(page).unwrap(); + writeln!(Console::new(), "erased.").unwrap(); + } + } + writeln!(Console::new(), "Done.").unwrap(); + led::get(1).flex_unwrap().off().flex_unwrap(); + led::get(0).flex_unwrap().on().flex_unwrap(); // green on dongle +} From de360a6cb6714cf6989bbebbf1cb1ce418686a74 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 6 Jan 2021 19:24:56 +0100 Subject: [PATCH 106/192] removes all occurences of CTAP 2.1 flags from workflows --- .github/workflows/cargo_check.yml | 16 ++-------------- .github/workflows/opensk_test.yml | 24 ------------------------ run_desktop_tests.sh | 13 ------------- 3 files changed, 2 insertions(+), 51 deletions(-) diff --git a/.github/workflows/cargo_check.yml b/.github/workflows/cargo_check.yml index fd39614..7151806 100644 --- a/.github/workflows/cargo_check.yml +++ b/.github/workflows/cargo_check.yml @@ -42,12 +42,6 @@ jobs: command: check args: --target thumbv7em-none-eabi --release --features with_ctap1 - - name: Check OpenSK with_ctap2_1 - uses: actions-rs/cargo@v1 - with: - command: check - args: --target thumbv7em-none-eabi --release --features with_ctap2_1 - - name: Check OpenSK debug_ctap uses: actions-rs/cargo@v1 with: @@ -78,17 +72,11 @@ jobs: command: check args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1 - - name: Check OpenSK debug_ctap,with_ctap2_1 + - name: Check OpenSK debug_ctap,with_ctap1,panic_console,debug_allocations,verbose uses: actions-rs/cargo@v1 with: command: check - args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap2_1 - - - name: Check OpenSK debug_ctap,with_ctap1,with_ctap2_1,panic_console,debug_allocations,verbose - uses: actions-rs/cargo@v1 - with: - command: check - args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1,with_ctap2_1,panic_console,debug_allocations,verbose + args: --target thumbv7em-none-eabi --release --features debug_ctap,with_ctap1,,panic_console,debug_allocations,verbose - name: Check examples uses: actions-rs/cargo@v1 diff --git a/.github/workflows/opensk_test.yml b/.github/workflows/opensk_test.yml index 588dab6..406a7f3 100644 --- a/.github/workflows/opensk_test.yml +++ b/.github/workflows/opensk_test.yml @@ -51,27 +51,3 @@ jobs: command: test args: --features std,with_ctap1 - - name: Unit testing of CTAP2 (release mode + CTAP2.1) - uses: actions-rs/cargo@v1 - with: - command: test - args: --release --features std,with_ctap2_1 - - - name: Unit testing of CTAP2 (debug mode + CTAP2.1) - uses: actions-rs/cargo@v1 - with: - command: test - args: --features std,with_ctap2_1 - - - name: Unit testing of CTAP2 (release mode + CTAP1 + CTAP2.1) - uses: actions-rs/cargo@v1 - with: - command: test - args: --release --features std,with_ctap1,with_ctap2_1 - - - name: Unit testing of CTAP2 (debug mode + CTAP1 + CTAP2.1) - uses: actions-rs/cargo@v1 - with: - command: test - args: --features std,with_ctap1,with_ctap2_1 - diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index 2e80b3d..24771f7 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -44,7 +44,6 @@ cargo test --manifest-path tools/heapviz/Cargo.toml echo "Checking that CTAP2 builds properly..." cargo check --release --target=thumbv7em-none-eabi cargo check --release --target=thumbv7em-none-eabi --features with_ctap1 -cargo check --release --target=thumbv7em-none-eabi --features with_ctap2_1 cargo check --release --target=thumbv7em-none-eabi --features debug_ctap cargo check --release --target=thumbv7em-none-eabi --features panic_console cargo check --release --target=thumbv7em-none-eabi --features debug_allocations @@ -116,16 +115,4 @@ then echo "Running unit tests on the desktop (debug mode + CTAP1)..." cargo test --features std,with_ctap1 - - echo "Running unit tests on the desktop (release mode + CTAP2.1)..." - cargo test --release --features std,with_ctap2_1 - - echo "Running unit tests on the desktop (debug mode + CTAP2.1)..." - cargo test --features std,with_ctap2_1 - - echo "Running unit tests on the desktop (release mode + CTAP1 + CTAP2.1)..." - cargo test --release --features std,with_ctap1,with_ctap2_1 - - echo "Running unit tests on the desktop (debug mode + CTAP1 + CTAP2.1)..." - cargo test --features std,with_ctap1,with_ctap2_1 fi From c873d3b6147e81f1be7518718308d15deec04b85 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 6 Jan 2021 19:24:56 +0100 Subject: [PATCH 107/192] removes all occurences of CTAP 2.1 flags --- Cargo.toml | 1 - README.md | 15 ++++--- deploy.py | 10 +---- src/ctap/command.rs | 43 ------------------- src/ctap/data_formats.rs | 11 ----- src/ctap/mod.rs | 82 +++++++++++-------------------------- src/ctap/pin_protocol_v1.rs | 79 ++++------------------------------- src/ctap/response.rs | 61 +-------------------------- src/ctap/status_code.rs | 6 --- src/ctap/storage.rs | 22 ++-------- src/ctap/storage/key.rs | 2 - 11 files changed, 47 insertions(+), 285 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 15984a0..bca9210 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,6 @@ panic_console = ["lang_items/panic_console"] std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std", "persistent_store/std"] verbose = ["debug_ctap", "libtock_drivers/verbose_usb"] with_ctap1 = ["crypto/with_ctap1"] -with_ctap2_1 = [] with_nfc = ["libtock_drivers/with_nfc"] [dev-dependencies] diff --git a/README.md b/README.md index 68e9f71..decee76 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,14 @@ few limitations: ### FIDO2 -Although we tested and implemented our firmware based on the published +The stable branch implements the published [CTAP2.0 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html), -our implementation was not reviewed nor officially tested and doesn't claim to -be FIDO Certified. -We started adding features of the upcoming next version of the -[CTAP2.1 specifications](https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html). -The development is currently between 2.0 and 2.1, with updates hidden behind -a feature flag. -Please add the flag `--ctap2.1` to the deploy command to include them. +but our implementation was not reviewed nor officially tested and doesn't claim +to be FIDO Certified. It already contains some preview features of 2.1, that you +can try by adding the flag `--ctap2.1` to the deploy command. +The develop branch offers only the +[CTAP2.1 specifications](https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html). +The new features of 2.1 are currently work in progress. ### Cryptography diff --git a/deploy.py b/deploy.py index d5dcf2d..e8d5ffd 100755 --- a/deploy.py +++ b/deploy.py @@ -881,14 +881,6 @@ if __name__ == "__main__": help=("Compiles the OpenSK application without backward compatible " "support for U2F/CTAP1 protocol."), ) - main_parser.add_argument( - "--ctap2.1", - action="append_const", - const="with_ctap2_1", - dest="features", - help=("Compiles the OpenSK application with backward compatible " - "support for CTAP2.1 protocol."), - ) main_parser.add_argument( "--nfc", action="append_const", @@ -947,7 +939,7 @@ if __name__ == "__main__": dest="application", action="store_const", const="store_latency", - help=("Compiles and installs the store_latency example which print " + help=("Compiles and installs the store_latency example which prints " "latency statistics of the persistent store library.")) apps_group.add_argument( "--erase_storage", diff --git a/src/ctap/command.rs b/src/ctap/command.rs index ecfae9e..0a86093 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -41,7 +41,6 @@ pub enum Command { AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorReset, AuthenticatorGetNextAssertion, - #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands @@ -111,7 +110,6 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorGetNextAssertion) } - #[cfg(feature = "with_ctap2_1")] Command::AUTHENTICATOR_SELECTION => { // Parameters are ignored. Ok(Command::AuthenticatorSelection) @@ -292,13 +290,9 @@ 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, } @@ -306,18 +300,6 @@ 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, - 2 => sub_command, - 3 => key_agreement, - 4 => pin_auth, - 5 => new_pin_enc, - 6 => pin_hash_enc, - } = extract_map(cbor_value)?; - } - #[cfg(feature = "with_ctap2_1")] destructure_cbor_map! { let { 1 => pin_protocol, @@ -339,14 +321,12 @@ 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()?; - #[cfg(feature = "with_ctap2_1")] let min_pin_length = min_pin_length .map(extract_unsigned) .transpose()? .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( extract_array(entry)? @@ -356,14 +336,12 @@ 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 = permissions .map(extract_unsigned) .transpose()? .map(|p| p as u8); - #[cfg(feature = "with_ctap2_1")] let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?; Ok(AuthenticatorClientPinParameters { @@ -373,13 +351,9 @@ 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, }) } @@ -560,18 +534,6 @@ mod test { #[test] fn test_from_cbor_client_pin_parameters() { - // 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, - 2 => ClientPinSubCommand::GetPinRetries, - 3 => cbor_map!{}, - 4 => vec! [0xBB], - 5 => vec! [0xCC], - 6 => vec! [0xDD], - }; - #[cfg(feature = "with_ctap2_1")] let cbor_value = cbor_map! { 1 => 1, 2 => ClientPinSubCommand::GetPinRetries, @@ -594,13 +556,9 @@ 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()), }; @@ -632,7 +590,6 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_deserialize_selection() { let cbor_bytes = [Command::AUTHENTICATOR_SELECTION]; diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index a2b490d..8081567 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -704,13 +704,9 @@ pub enum ClientPinSubCommand { SetPin = 0x03, ChangePin = 0x04, 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, } @@ -731,18 +727,11 @@ impl TryFrom for ClientPinSubCommand { 0x03 => Ok(ClientPinSubCommand::SetPin), 0x04 => Ok(ClientPinSubCommand::ChangePin), 0x05 => Ok(ClientPinSubCommand::GetPinToken), - #[cfg(feature = "with_ctap2_1")] 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions), - #[cfg(feature = "with_ctap2_1")] 0x07 => Ok(ClientPinSubCommand::GetUvRetries), - #[cfg(feature = "with_ctap2_1")] 0x08 => Ok(ClientPinSubCommand::SetMinPinLength), - #[cfg(feature = "with_ctap2_1")] 0x09 => Ok(ClientPinSubCommand::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 7a23a1d..4168f8f 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -25,23 +25,19 @@ pub mod status_code; mod storage; mod timed_permission; -#[cfg(feature = "with_ctap2_1")] -use self::command::MAX_CREDENTIAL_COUNT_IN_LIST; use self::command::{ AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, + MAX_CREDENTIAL_COUNT_IN_LIST, }; -#[cfg(feature = "with_ctap2_1")] -use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, - PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, + AuthenticatorTransport, CredentialProtectionPolicy, GetAssertionHmacSecretInput, + PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialSource, PublicKeyCredentialType, 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::pin_protocol_v1::{PinPermission, PinProtocolV1}; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, @@ -108,7 +104,6 @@ pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] pub const U2F_VERSION_STRING: &str = "U2F_V2"; // TODO(#106) change to final string when ready -#[cfg(feature = "with_ctap2_1")] pub const FIDO2_1_VERSION_STRING: &str = "FIDO_2_1_PRE"; // We currently only support one algorithm for signatures: ES256. @@ -339,7 +334,6 @@ where // GetInfo does not reset stateful commands. (Command::AuthenticatorGetInfo, _) => (), // AuthenticatorSelection does not reset stateful commands. - #[cfg(feature = "with_ctap2_1")] (Command::AuthenticatorSelection, _) => (), (_, _) => { self.stateful_command_type = None; @@ -356,7 +350,6 @@ where Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid, now), - #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands @@ -484,12 +477,9 @@ where { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - #[cfg(feature = "with_ctap2_1")] - { - self.pin_protocol_v1 - .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.has_permission_for_rp_id(&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 => { @@ -738,12 +728,9 @@ where { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - #[cfg(feature = "with_ctap2_1")] - { - self.pin_protocol_v1 - .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; - } + self.pin_protocol_v1 + .has_permission(PinPermission::GetAssertion)?; + self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; UV_FLAG } None => { @@ -851,7 +838,6 @@ where #[cfg(feature = "with_ctap1")] String::from(U2F_VERSION_STRING), String::from(FIDO2_VERSION_STRING), - #[cfg(feature = "with_ctap2_1")] String::from(FIDO2_1_VERSION_STRING), ], extensions: Some(vec![String::from("hmac-secret")]), @@ -861,19 +847,13 @@ where pin_protocols: Some(vec![ CtapState::::PIN_PROTOCOL_VERSION, ]), - #[cfg(feature = "with_ctap2_1")] max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), // #TODO(106) update with version 2.1 of HMAC-secret - #[cfg(feature = "with_ctap2_1")] max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), - #[cfg(feature = "with_ctap2_1")] transports: Some(vec![AuthenticatorTransport::Usb]), - #[cfg(feature = "with_ctap2_1")] 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, }, )) @@ -916,7 +896,6 @@ where Ok(ResponseData::AuthenticatorReset) } - #[cfg(feature = "with_ctap2_1")] fn process_selection(&self, cid: ChannelID) -> Result { (self.check_user_presence)(cid)?; Ok(ResponseData::AuthenticatorSelection) @@ -1036,42 +1015,31 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - #[cfg(feature = "with_ctap2_1")] let mut expected_response = vec![0x00, 0xAA, 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. let mut version_count = 0; - // CTAP 2 is always supported - version_count += 1; + // CTAP 2.0 and 2.1 are always supported + version_count += 2; #[cfg(feature = "with_ctap1")] { version_count += 1; } - #[cfg(feature = "with_ctap2_1")] - { - version_count += 1; - } expected_response.push(0x80 + version_count); #[cfg(feature = "with_ctap1")] expected_response.extend(&[0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32]); - expected_response.extend(&[0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30]); - #[cfg(feature = "with_ctap2_1")] - expected_response.extend(&[ - 0x6C, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, - ]); - expected_response.extend(&[ - 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x03, 0x50, - ]); - expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_response.extend(&[ - 0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, - 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, - ]); - #[cfg(feature = "with_ctap2_1")] expected_response.extend( [ + 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, + 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, + 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x03, 0x50, + ] + .iter(), + ); + expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); + expected_response.extend( + [ + 0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, + 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 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, 0x0D, 0x04, diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 96e92b3..b8aeb21 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -17,7 +17,6 @@ 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; use alloc::vec::Vec; @@ -28,7 +27,7 @@ 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"))] +#[cfg(test)] use enum_iterator::IntoEnumIterator; use subtle::ConstantTimeEq; @@ -141,10 +140,7 @@ fn check_and_store_new_pin( 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; - #[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 return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); @@ -155,7 +151,6 @@ fn check_and_store_new_pin( Ok(()) } -#[cfg(feature = "with_ctap2_1")] #[cfg_attr(test, derive(IntoEnumIterator))] // TODO remove when all variants are used #[allow(dead_code)] @@ -173,9 +168,7 @@ 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, } @@ -187,9 +180,7 @@ 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, } } @@ -345,11 +336,8 @@ impl PinProtocolV1 { cbc_encrypt(&token_encryption_key, iv, &mut blocks); let pin_token: Vec = blocks.iter().flatten().cloned().collect(); - #[cfg(feature = "with_ctap2_1")] - { - self.permissions = 0x03; - self.permissions_rp_id = None; - } + self.permissions = 0x03; + self.permissions_rp_id = None; Ok(AuthenticatorClientPinResponse { key_agreement: None, @@ -358,7 +346,6 @@ impl PinProtocolV1 { }) } - #[cfg(feature = "with_ctap2_1")] fn process_get_pin_uv_auth_token_using_uv_with_permissions( &self, // If you want to support local user verification, implement this function. @@ -368,30 +355,14 @@ impl PinProtocolV1 { _permissions_rp_id: Option, ) -> Result { // 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) - } + 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. - #[cfg(not(feature = "with_ctap2_1"))] - { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) - } - #[cfg(feature = "with_ctap2_1")] - { - Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) - } + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } - #[cfg(feature = "with_ctap2_1")] fn process_set_min_pin_length( &mut self, persistent_store: &mut PersistentStore, @@ -440,7 +411,6 @@ impl PinProtocolV1 { Ok(()) } - #[cfg(feature = "with_ctap2_1")] fn process_get_pin_uv_auth_token_using_pin_with_permissions( &mut self, rng: &mut impl Rng256, @@ -480,20 +450,13 @@ impl PinProtocolV1 { 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 { - #[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); } @@ -528,7 +491,6 @@ impl PinProtocolV1 { 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)?, @@ -536,9 +498,7 @@ impl PinProtocolV1 { permissions_rp_id, )?, ), - #[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( persistent_store, @@ -548,7 +508,6 @@ impl PinProtocolV1 { )?; None } - #[cfg(feature = "with_ctap2_1")] ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( self.process_get_pin_uv_auth_token_using_pin_with_permissions( rng, @@ -571,11 +530,8 @@ 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; - } + self.permissions = 0; + self.permissions_rp_id = None; } pub fn process_hmac_secret( @@ -598,7 +554,6 @@ impl PinProtocolV1 { encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } - #[cfg(feature = "with_ctap2_1")] pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { // Relies on the fact that all permissions are represented by powers of two. if permission as u8 & self.permissions != 0 { @@ -608,7 +563,6 @@ impl PinProtocolV1 { } } - #[cfg(feature = "with_ctap2_1")] 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 { @@ -629,9 +583,7 @@ impl PinProtocolV1 { key_agreement_key, pin_uv_auth_token, consecutive_pin_mismatches: 0, - #[cfg(feature = "with_ctap2_1")] permissions: 0xFF, - #[cfg(feature = "with_ctap2_1")] permissions_rp_id: None, } } @@ -919,7 +871,6 @@ mod test { ); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { let mut rng = ThreadRng256 {}; @@ -963,7 +914,7 @@ mod test { &mut rng, &mut persistent_store, key_agreement.clone(), - pin_hash_enc.clone(), + pin_hash_enc, 0x03, None, ), @@ -984,7 +935,6 @@ mod test { ); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_process_set_min_pin_length() { let mut rng = ThreadRng256 {}; @@ -1031,13 +981,9 @@ mod test { 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 @@ -1051,18 +997,11 @@ mod test { 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_subcommand(&mut rng, &mut persistent_store, client_pin_params), @@ -1231,7 +1170,6 @@ mod test { assert_eq!(&output_dec[..32], &expected_output1); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_has_permission() { let mut rng = ThreadRng256 {}; @@ -1249,7 +1187,6 @@ mod test { } } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_has_permission_for_rp_id() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 6422959..390b0cb 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(feature = "with_ctap2_1")] -use super::data_formats::{AuthenticatorTransport, PublicKeyCredentialParameter}; use super::data_formats::{ - CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, - PublicKeyCredentialUserEntity, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialUserEntity, }; use alloc::collections::BTreeMap; use alloc::string::String; @@ -32,7 +30,6 @@ pub enum ResponseData { AuthenticatorGetInfo(AuthenticatorGetInfoResponse), AuthenticatorClientPin(Option), AuthenticatorReset, - #[cfg(feature = "with_ctap2_1")] AuthenticatorSelection, AuthenticatorVendor(AuthenticatorVendorResponse), } @@ -47,7 +44,6 @@ impl From for Option { ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()), ResponseData::AuthenticatorClientPin(None) => None, ResponseData::AuthenticatorReset => None, - #[cfg(feature = "with_ctap2_1")] ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } @@ -118,23 +114,16 @@ pub struct AuthenticatorGetInfoResponse { pub options: Option>, pub max_msg_size: Option, pub pin_protocols: Option>, - #[cfg(feature = "with_ctap2_1")] pub max_credential_count_in_list: Option, - #[cfg(feature = "with_ctap2_1")] pub max_credential_id_length: Option, - #[cfg(feature = "with_ctap2_1")] pub transports: Option>, - #[cfg(feature = "with_ctap2_1")] 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, } impl From for cbor::Value { - #[cfg(feature = "with_ctap2_1")] fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self { let AuthenticatorGetInfoResponse { versions, @@ -176,37 +165,6 @@ impl From for cbor::Value { 0x0E => firmware_version, } } - - #[cfg(not(feature = "with_ctap2_1"))] - fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self { - let AuthenticatorGetInfoResponse { - versions, - extensions, - aaguid, - options, - max_msg_size, - pin_protocols, - default_cred_protect, - } = get_info_response; - - let options_cbor: Option = options.map(|options| { - let option_map: BTreeMap<_, _> = options - .into_iter() - .map(|(key, value)| (cbor_text!(key), cbor_bool!(value))) - .collect(); - cbor_map_btree!(option_map) - }); - - cbor_map_options! { - 0x01 => cbor_array_vec!(versions), - 0x02 => extensions.map(|vec| cbor_array_vec!(vec)), - 0x03 => &aaguid, - 0x04 => options_cbor, - 0x05 => max_msg_size, - 0x06 => pin_protocols.map(|vec| cbor_array_vec!(vec)), - 0x0C => default_cred_protect.map(|p| p as u64), - } - } } #[cfg_attr(test, derive(PartialEq))] @@ -257,7 +215,6 @@ impl From for cbor::Value { #[cfg(test)] mod test { use super::super::data_formats::PackedAttestationStatement; - #[cfg(feature = "with_ctap2_1")] use super::super::ES256_CRED_PARAM; use super::*; use cbor::{cbor_bytes, cbor_map}; @@ -321,28 +278,16 @@ mod test { options: None, max_msg_size: None, pin_protocols: None, - #[cfg(feature = "with_ctap2_1")] max_credential_count_in_list: None, - #[cfg(feature = "with_ctap2_1")] max_credential_id_length: None, - #[cfg(feature = "with_ctap2_1")] transports: None, - #[cfg(feature = "with_ctap2_1")] 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![versions], - 0x03 => vec![0x00; 16], - }; - #[cfg(feature = "with_ctap2_1")] let expected_cbor = cbor_map_options! { 0x01 => cbor_array_vec![versions], 0x03 => vec![0x00; 16], @@ -352,7 +297,6 @@ mod test { } #[test] - #[cfg(feature = "with_ctap2_1")] fn test_get_info_optionals_into_cbor() { let mut options_map = BTreeMap::new(); options_map.insert(String::from("rk"), true); @@ -418,7 +362,6 @@ mod test { assert_eq!(response_cbor, None); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_selection_into_cbor() { let response_cbor: Option = ResponseData::AuthenticatorSelection.into(); diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 40f258e..5a9ec71 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -31,9 +31,7 @@ pub enum Ctap2StatusCode { CTAP2_ERR_INVALID_CBOR = 0x12, CTAP2_ERR_MISSING_PARAMETER = 0x14, CTAP2_ERR_LIMIT_EXCEEDED = 0x15, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_FP_DATABASE_FULL = 0x17, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_LARGE_BLOB_STORAGE_FULL = 0x18, CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19, CTAP2_ERR_PROCESSING = 0x21, @@ -63,13 +61,9 @@ 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, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_UV_INVALID = 0x3F, - #[cfg(feature = "with_ctap2_1")] CTAP2_ERR_UNAUTHORIZED_PERMISSION = 0x40, CTAP1_ERR_OTHER = 0x7F, _CTAP2_ERR_SPEC_LAST = 0xDF, diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 73bbc16..28c1599 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -14,20 +14,18 @@ mod key; -#[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::data_formats::{ + extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, +}; use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::INITIAL_SIGNATURE_COUNTER; use crate::embedded_flash::{new_storage, Storage}; -#[cfg(feature = "with_ctap2_1")] use alloc::string::String; 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; @@ -54,14 +52,11 @@ const NUM_PAGES: usize = 20; const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; 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; /// Wrapper for master keys. @@ -348,7 +343,6 @@ impl PersistentStore { } /// Returns the minimum PIN length. - #[cfg(feature = "with_ctap2_1")] pub fn min_pin_length(&self) -> Result { match self.store.find(key::MIN_PIN_LENGTH)? { None => Ok(DEFAULT_MIN_PIN_LENGTH), @@ -358,14 +352,12 @@ impl PersistentStore { } /// Sets the minimum PIN length. - #[cfg(feature = "with_ctap2_1")] pub fn set_min_pin_length(&mut self, min_pin_length: u8) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::MIN_PIN_LENGTH, &[min_pin_length])?) } /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// allowed. - #[cfg(feature = "with_ctap2_1")] pub fn _min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { let rp_ids = self .store @@ -374,11 +366,10 @@ impl PersistentStore { _deserialize_min_pin_length_rp_ids(&value) }); debug_assert!(rp_ids.is_some()); - Ok(rp_ids.unwrap_or(vec![])) + Ok(rp_ids.unwrap_or_default()) } /// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed. - #[cfg(feature = "with_ctap2_1")] pub fn _set_min_pin_length_rp_ids( &mut self, min_pin_length_rp_ids: Vec, @@ -582,7 +573,6 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result } /// Deserializes a list of RP IDs from storage representation. -#[cfg(feature = "with_ctap2_1")] fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { let cbor = cbor::read(data).ok()?; extract_array(cbor) @@ -594,7 +584,6 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { } /// Serializes a list of RP IDs to storage representation. -#[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) { @@ -988,7 +977,6 @@ 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 {}; @@ -1011,7 +999,6 @@ mod test { ); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_min_pin_length_rp_ids() { let mut rng = ThreadRng256 {}; @@ -1080,7 +1067,6 @@ mod test { 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")]; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 5c5b20e..ec39efa 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -92,13 +92,11 @@ make_partition! { CRED_RANDOM_SECRET = 2041; /// List of RP IDs allowed to read the minimum PIN length. - #[cfg(feature = "with_ctap2_1")] _MIN_PIN_LENGTH_RP_IDS = 2042; /// The minimum PIN length. /// /// If the entry is absent, the minimum PIN length is `DEFAULT_MIN_PIN_LENGTH`. - #[cfg(feature = "with_ctap2_1")] MIN_PIN_LENGTH = 2043; /// The number of PIN retries. From da03f77a32e059491ee8e1e2b4a3a0b027e8fa45 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 8 Jan 2021 13:13:52 +0100 Subject: [PATCH 108/192] small readbility fix for variable assignment with cfg --- src/ctap/mod.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4168f8f..fe60e80 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1016,14 +1016,11 @@ mod test { let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let mut expected_response = vec![0x00, 0xAA, 0x01]; - // The difference here is a longer array of supported versions. - let mut version_count = 0; - // CTAP 2.0 and 2.1 are always supported - version_count += 2; + // The version array differs with CTAP1, always including 2.0 and 2.1. + #[cfg(not(feature = "with_ctap1"))] + let version_count = 2; #[cfg(feature = "with_ctap1")] - { - version_count += 1; - } + let version_count = 3; expected_response.push(0x80 + version_count); #[cfg(feature = "with_ctap1")] expected_response.extend(&[0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32]); From f4eb6c938e5105c9c0039f160146d21dfe18a101 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 7 Jan 2021 18:17:21 +0100 Subject: [PATCH 109/192] adds the config command --- README.md | 15 +- src/ctap/command.rs | 80 +++++--- src/ctap/config_command.rs | 269 +++++++++++++++++++++++++++ src/ctap/data_formats.rs | 160 +++++++++++++++- src/ctap/mod.rs | 361 +++++++++++++++++++++--------------- src/ctap/pin_protocol_v1.rs | 114 ++---------- src/ctap/response.rs | 9 + src/ctap/storage.rs | 114 ++++++++---- src/ctap/storage/key.rs | 9 +- 9 files changed, 796 insertions(+), 335 deletions(-) create mode 100644 src/ctap/config_command.rs diff --git a/README.md b/README.md index decee76..48f6c6e 100644 --- a/README.md +++ b/README.md @@ -93,32 +93,37 @@ a few things you can personalize: 1. If you have multiple buttons, choose the buttons responsible for user presence in `main.rs`. -2. Decide whether you want to use batch attestation. There is a boolean flag in +1. Decide whether you want to use batch attestation. There is a boolean flag in `ctap/mod.rs`. It is mandatory for U2F, and you can create your own self-signed certificate. The flag is used for FIDO2 and has some privacy implications. Please check [WebAuthn](https://www.w3.org/TR/webauthn/#attestation) for more information. -3. Decide whether you want to use signature counters. Currently, only global +1. Decide whether you want to use signature counters. Currently, only global signature counters are implemented, as they are the default option for U2F. The flag in `ctap/mod.rs` only turns them off for FIDO2. The most privacy preserving solution is individual or no signature counters. Again, please check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for documentation. -4. Depending on your available flash storage, choose an appropriate maximum +1. Depending on your available flash storage, choose an appropriate maximum number of supported residential keys and number of pages in `ctap/storage.rs`. -5. Change the default level for the credProtect extension in `ctap/mod.rs`. +1. Change the default level for the credProtect extension in `ctap/mod.rs`. 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`. +1. Increase the default minimum length for PINs in `ctap/storage.rs`. 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 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. +1. In an enterprise setting, you can adapt `DEFAULT_MIN_PIN_LENGTH_RP_IDS` and + `MAX_RP_IDS_LENGTH` for tuning the `minPinLength` extension. The former + allows some relying parties to read the minimum PIN length by default. The + latter allows storing more relying parties that may check the minimum PIN + length. ### 3D printed enclosure diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 0a86093..6b6ab54 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -14,10 +14,10 @@ use super::data_formats::{ extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, - extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, - GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, + CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + MakeCredentialOptions, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams, }; use super::key_material; use super::status_code::Ctap2StatusCode; @@ -42,6 +42,7 @@ pub enum Command { AuthenticatorReset, AuthenticatorGetNextAssertion, AuthenticatorSelection, + AuthenticatorConfig(AuthenticatorConfigParameters), // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), @@ -114,6 +115,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorSelection) } + Command::AUTHENTICATOR_CONFIG => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorConfig( + AuthenticatorConfigParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_VENDOR_CONFIGURE => { let decoded_cbor = cbor::read(&bytes[1..])?; Ok(Command::AuthenticatorVendorConfigure( @@ -290,8 +297,6 @@ pub struct AuthenticatorClientPinParameters { pub pin_auth: Option>, pub new_pin_enc: Option>, pub pin_hash_enc: Option>, - pub min_pin_length: Option, - pub min_pin_length_rp_ids: Option>, pub permissions: Option, pub permissions_rp_id: Option, } @@ -308,8 +313,6 @@ impl TryFrom for AuthenticatorClientPinParameters { 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)?; @@ -321,21 +324,6 @@ 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()?; - let min_pin_length = min_pin_length - .map(extract_unsigned) - .transpose()? - .map(u8::try_from) - .transpose() - .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; - let min_pin_length_rp_ids = match min_pin_length_rp_ids { - Some(entry) => Some( - extract_array(entry)? - .into_iter() - .map(extract_text_string) - .collect::, Ctap2StatusCode>>()?, - ), - None => None, - }; // We expect a bit field of 8 bits, and drop everything else. // This means we ignore extensions in future versions. let permissions = permissions @@ -351,14 +339,52 @@ impl TryFrom for AuthenticatorClientPinParameters { pin_auth, new_pin_enc, pin_hash_enc, - min_pin_length, - min_pin_length_rp_ids, permissions, permissions_rp_id, }) } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorConfigParameters { + pub sub_command: ConfigSubCommand, + pub sub_command_params: Option, + pub pin_uv_auth_param: Option>, + pub pin_uv_auth_protocol: Option, +} + +impl TryFrom for AuthenticatorConfigParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => sub_command, + 0x02 => sub_command_params, + 0x03 => pin_uv_auth_param, + 0x04 => pin_uv_auth_protocol, + } = extract_map(cbor_value)?; + } + + let sub_command = ConfigSubCommand::try_from(ok_or_missing(sub_command)?)?; + let sub_command_params = match sub_command { + ConfigSubCommand::SetMinPinLength => Some(ConfigSubCommandParams::SetMinPinLength( + SetMinPinLengthParams::try_from(ok_or_missing(sub_command_params)?)?, + )), + _ => None, + }; + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; + + Ok(AuthenticatorConfigParameters { + sub_command, + sub_command_params, + pin_uv_auth_param, + pin_uv_auth_protocol, + }) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct AuthenticatorAttestationMaterial { pub certificate: Vec, @@ -541,8 +567,6 @@ mod test { 4 => vec! [0xBB], 5 => vec! [0xCC], 6 => vec! [0xDD], - 7 => 4, - 8 => cbor_array!["example.com"], 9 => 0x03, 10 => "example.com", }; @@ -556,8 +580,6 @@ mod test { pin_auth: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), - min_pin_length: Some(4), - min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), permissions: Some(0x03), permissions_rp_id: Some("example.com".to_string()), }; diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs new file mode 100644 index 0000000..873a9b1 --- /dev/null +++ b/src/ctap/config_command.rs @@ -0,0 +1,269 @@ +// 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. +// 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::check_pin_uv_auth_protocol; +use super::command::AuthenticatorConfigParameters; +use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; +use super::pin_protocol_v1::PinProtocolV1; +use super::response::ResponseData; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use alloc::vec; + +fn process_set_min_pin_length( + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + params: SetMinPinLengthParams, +) -> Result { + let SetMinPinLengthParams { + new_min_pin_length, + min_pin_length_rp_ids, + force_change_pin, + } = params; + let store_min_pin_length = persistent_store.min_pin_length()?; + let new_min_pin_length = new_min_pin_length.unwrap_or(store_min_pin_length); + if new_min_pin_length < store_min_pin_length { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION); + } + let mut force_change_pin = force_change_pin.unwrap_or(false); + if force_change_pin && persistent_store.pin_hash()?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); + } + if let Some(old_length) = persistent_store.pin_code_point_length()? { + force_change_pin |= new_min_pin_length > old_length; + } + pin_protocol_v1.force_pin_change |= force_change_pin; + // TODO(kaczmarczyck) actually force a PIN change + persistent_store.set_min_pin_length(new_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(ResponseData::AuthenticatorConfig) +} + +pub fn process_config( + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + params: AuthenticatorConfigParameters, +) -> Result { + let AuthenticatorConfigParameters { + sub_command, + sub_command_params, + pin_uv_auth_param, + pin_uv_auth_protocol, + } = params; + + if persistent_store.pin_hash()?.is_some() { + // TODO(kaczmarczyck) The error code is specified inconsistently with other commands. + check_pin_uv_auth_protocol(pin_uv_auth_protocol) + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let mut config_data = vec![0xFF; 32]; + config_data.extend(&[0x0D, sub_command as u8]); + if let Some(sub_command_params) = sub_command_params.clone() { + if !cbor::write(sub_command_params.into(), &mut config_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + } + if !pin_protocol_v1.verify_pin_auth_token(&config_data, &auth_param) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + } + + match sub_command { + ConfigSubCommand::SetMinPinLength => { + if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { + process_set_min_pin_length(persistent_store, pin_protocol_v1, params) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + } + } + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } +} + +#[cfg(test)] +mod test { + use super::super::command::AuthenticatorConfigParameters; + use super::*; + use crypto::rng256::ThreadRng256; + + fn create_min_pin_config_params( + min_pin_length: u8, + min_pin_length_rp_ids: Option>, + ) -> AuthenticatorConfigParameters { + let set_min_pin_length_params = SetMinPinLengthParams { + new_min_pin_length: Some(min_pin_length), + min_pin_length_rp_ids, + force_change_pin: None, + }; + AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::SetMinPinLength, + sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength( + set_min_pin_length_params, + )), + pin_uv_auth_param: None, + pin_uv_auth_protocol: Some(1), + } + } + + #[test] + fn test_process_set_min_pin_length() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + // First, increase minimum PIN length from 4 to 6 without PIN auth. + let min_pin_length = 6; + let config_params = create_min_pin_config_params(min_pin_length, None); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + + // Second, increase minimum PIN length from 6 to 8 with PIN auth. + // The stored PIN or its length don't matter since we control the token. + persistent_store.set_pin(&[0x88; 16], 8).unwrap(); + let min_pin_length = 8; + let mut config_params = create_min_pin_config_params(min_pin_length, None); + let pin_auth = vec![ + 0x5C, 0x69, 0x71, 0x29, 0xBD, 0xCC, 0x53, 0xE8, 0x3C, 0x97, 0x62, 0xDD, 0x90, 0x29, + 0xB2, 0xDE, + ]; + config_params.pin_uv_auth_param = Some(pin_auth); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + + // Third, decreasing the minimum PIN length from 8 to 7 fails. + let mut config_params = create_min_pin_config_params(7, None); + let pin_auth = vec![ + 0xC5, 0xEA, 0xC1, 0x5E, 0x7F, 0x80, 0x70, 0x1A, 0x4E, 0xC4, 0xAD, 0x85, 0x35, 0xD8, + 0xA7, 0x71, + ]; + config_params.pin_uv_auth_param = Some(pin_auth); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION) + ); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + } + + #[test] + fn test_process_set_min_pin_length_rp_ids() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + // First, set RP IDs without PIN auth. + let min_pin_length = 6; + let min_pin_length_rp_ids = vec!["example.com".to_string()]; + let config_params = + create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone())); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!( + persistent_store.min_pin_length_rp_ids(), + Ok(min_pin_length_rp_ids) + ); + + // Second, change the RP IDs with PIN auth. + let min_pin_length = 8; + let min_pin_length_rp_ids = vec!["another.example.com".to_string()]; + // The stored PIN or its length don't matter since we control the token. + persistent_store.set_pin(&[0x88; 16], 8).unwrap(); + let mut config_params = + create_min_pin_config_params(min_pin_length, Some(min_pin_length_rp_ids.clone())); + let pin_auth = vec![ + 0x40, 0x51, 0x2D, 0xAC, 0x2D, 0xE2, 0x15, 0x77, 0x5C, 0xF9, 0x5B, 0x62, 0x9A, 0x2D, + 0xD6, 0xDA, + ]; + config_params.pin_uv_auth_param = Some(pin_auth.clone()); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!( + persistent_store.min_pin_length_rp_ids(), + Ok(min_pin_length_rp_ids.clone()) + ); + + // Third, changing RP IDs with bad PIN auth fails. + // One PIN auth shouldn't work for different lengths. + let mut config_params = + create_min_pin_config_params(9, Some(min_pin_length_rp_ids.clone())); + config_params.pin_uv_auth_param = Some(pin_auth.clone()); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!( + persistent_store.min_pin_length_rp_ids(), + Ok(min_pin_length_rp_ids.clone()) + ); + + // Forth, changing RP IDs with bad PIN auth fails. + // One PIN auth shouldn't work for different RP IDs. + let mut config_params = create_min_pin_config_params( + min_pin_length, + Some(vec!["counter.example.com".to_string()]), + ); + config_params.pin_uv_auth_param = Some(pin_auth); + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!( + persistent_store.min_pin_length_rp_ids(), + Ok(min_pin_length_rp_ids) + ); + } + + #[test] + fn test_process_config_vendor_prototype() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::VendorPrototype, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } +} diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 8081567..9cf149f 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -262,6 +262,7 @@ impl From for cbor::Value { pub struct MakeCredentialExtensions { pub hmac_secret: bool, pub cred_protect: Option, + pub min_pin_length: bool, } impl TryFrom for MakeCredentialExtensions { @@ -272,6 +273,7 @@ impl TryFrom for MakeCredentialExtensions { let { "credProtect" => cred_protect, "hmac-secret" => hmac_secret, + "minPinLength" => min_pin_length, } = extract_map(cbor_value)?; } @@ -279,9 +281,11 @@ impl TryFrom for MakeCredentialExtensions { let cred_protect = cred_protect .map(CredentialProtectionPolicy::try_from) .transpose()?; + let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; Ok(Self { hmac_secret, cred_protect, + min_pin_length, }) } } @@ -706,7 +710,6 @@ pub enum ClientPinSubCommand { GetPinToken = 0x05, GetPinUvAuthTokenUsingUvWithPermissions = 0x06, GetUvRetries = 0x07, - SetMinPinLength = 0x08, GetPinUvAuthTokenUsingPinWithPermissions = 0x09, } @@ -729,13 +732,114 @@ impl TryFrom for ClientPinSubCommand { 0x05 => Ok(ClientPinSubCommand::GetPinToken), 0x06 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions), 0x07 => Ok(ClientPinSubCommand::GetUvRetries), - 0x08 => Ok(ClientPinSubCommand::SetMinPinLength), 0x09 => Ok(ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions), _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), } } } +#[derive(Clone, Copy)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum ConfigSubCommand { + EnableEnterpriseAttestation = 0x01, + ToggleAlwaysUv = 0x02, + SetMinPinLength = 0x03, + VendorPrototype = 0xFF, +} + +impl From for cbor::Value { + fn from(subcommand: ConfigSubCommand) -> Self { + (subcommand as u64).into() + } +} + +impl TryFrom for ConfigSubCommand { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + let subcommand_int = extract_unsigned(cbor_value)?; + match subcommand_int { + 0x01 => Ok(ConfigSubCommand::EnableEnterpriseAttestation), + 0x02 => Ok(ConfigSubCommand::ToggleAlwaysUv), + 0x03 => Ok(ConfigSubCommand::SetMinPinLength), + 0xFF => Ok(ConfigSubCommand::VendorPrototype), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), + } + } +} + +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub enum ConfigSubCommandParams { + SetMinPinLength(SetMinPinLengthParams), +} + +impl From for cbor::Value { + fn from(params: ConfigSubCommandParams) -> Self { + match params { + ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params) => { + set_min_pin_length_params.into() + } + } + } +} + +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct SetMinPinLengthParams { + pub new_min_pin_length: Option, + pub min_pin_length_rp_ids: Option>, + pub force_change_pin: Option, +} + +impl TryFrom for SetMinPinLengthParams { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => new_min_pin_length, + 0x02 => min_pin_length_rp_ids, + 0x03 => force_change_pin, + } = extract_map(cbor_value)?; + } + + let new_min_pin_length = new_min_pin_length + .map(extract_unsigned) + .transpose()? + .map(u8::try_from) + .transpose() + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; + let min_pin_length_rp_ids = match min_pin_length_rp_ids { + Some(entry) => Some( + extract_array(entry)? + .into_iter() + .map(extract_text_string) + .collect::, Ctap2StatusCode>>()?, + ), + None => None, + }; + let force_change_pin = force_change_pin.map(extract_bool).transpose()?; + + Ok(Self { + new_min_pin_length, + min_pin_length_rp_ids, + force_change_pin, + }) + } +} + +impl From for cbor::Value { + fn from(params: SetMinPinLengthParams) -> Self { + cbor_map_options! { + 0x01 => params.new_min_pin_length.map(|u| u as u64), + 0x02 => params.min_pin_length_rp_ids.map(|vec| cbor_array_vec!(vec)), + 0x03 => params.force_change_pin, + } + } +} + pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { match cbor_value { cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), @@ -1240,11 +1344,13 @@ mod test { let cbor_extensions = cbor_map! { "hmac-secret" => true, "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, + "minPinLength" => true, }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), + min_pin_length: true, }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1347,6 +1453,56 @@ mod test { } } + #[test] + fn test_from_into_config_sub_command() { + let cbor_sub_command: cbor::Value = cbor_int!(0x01); + let sub_command = ConfigSubCommand::try_from(cbor_sub_command.clone()); + let expected_sub_command = ConfigSubCommand::EnableEnterpriseAttestation; + assert_eq!(sub_command, Ok(expected_sub_command)); + let created_cbor: cbor::Value = sub_command.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command); + + for command in ConfigSubCommand::into_enum_iter() { + let created_cbor: cbor::Value = command.clone().into(); + let reconstructed = ConfigSubCommand::try_from(created_cbor).unwrap(); + assert_eq!(command, reconstructed); + } + } + + #[test] + fn test_from_set_min_pin_length_params() { + let params = SetMinPinLengthParams { + new_min_pin_length: Some(6), + min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), + force_change_pin: Some(true), + }; + let cbor_params = cbor_map! { + 0x01 => 6, + 0x02 => cbor_array_vec!(vec!["example.com".to_string()]), + 0x03 => true, + }; + assert_eq!(cbor::Value::from(params.clone()), cbor_params); + let reconstructed_params = SetMinPinLengthParams::try_from(cbor_params); + assert_eq!(reconstructed_params, Ok(params)); + } + + #[test] + fn test_from_config_sub_command_params() { + let set_min_pin_length_params = SetMinPinLengthParams { + new_min_pin_length: Some(6), + min_pin_length_rp_ids: Some(vec!["example.com".to_string()]), + force_change_pin: Some(true), + }; + let config_sub_command_params = + ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params); + let cbor_params = cbor_map! { + 0x01 => 6, + 0x02 => cbor_array_vec!(vec!["example.com".to_string()]), + 0x03 => true, + }; + assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); + } + #[test] fn test_credential_source_cbor_round_trip() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index fe60e80..233e5b7 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -14,6 +14,7 @@ pub mod apdu; pub mod command; +mod config_command; #[cfg(feature = "with_ctap1")] mod ctap1; pub mod data_formats; @@ -30,6 +31,7 @@ use self::command::{ AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, MAX_CREDENTIAL_COUNT_IN_LIST, }; +use self::config_command::process_config; use self::data_formats::{ AuthenticatorTransport, CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, @@ -106,6 +108,9 @@ pub const U2F_VERSION_STRING: &str = "U2F_V2"; // TODO(#106) change to final string when ready pub const FIDO2_1_VERSION_STRING: &str = "FIDO_2_1_PRE"; +// This is the currently supported PIN protocol version. +const PIN_PROTOCOL_VERSION: u64 = 1; + // We currently only support one algorithm for signatures: ES256. // This algorithm is requested in MakeCredential and advertized in GetInfo. pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialParameter { @@ -117,6 +122,17 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa // - Some(CredentialProtectionPolicy::UserVerificationRequired) const DEFAULT_CRED_PROTECT: Option = None; +// Checks the PIN protocol parameter against all supported versions. +pub fn check_pin_uv_auth_protocol( + pin_uv_auth_protocol: Option, +) -> Result<(), Ctap2StatusCode> { + match pin_uv_auth_protocol { + Some(PIN_PROTOCOL_VERSION) => Ok(()), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + None => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } +} + // 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. @@ -172,8 +188,6 @@ where R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>, { - pub const PIN_PROTOCOL_VERSION: u64 = 1; - pub fn new( rng: &'a mut R, check_user_presence: CheckUserPresence, @@ -351,6 +365,11 @@ where Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid, now), Command::AuthenticatorSelection => self.process_selection(cid), + Command::AuthenticatorConfig(params) => process_config( + &mut self.persistent_store, + &mut self.pin_protocol_v1, + params, + ), // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands Command::AuthenticatorVendorConfigure(params) => { @@ -394,11 +413,7 @@ where } } - match pin_uv_auth_protocol { - Some(CtapState::::PIN_PROTOCOL_VERSION) => Ok(()), - Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - None => Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), - } + check_pin_uv_auth_protocol(pin_uv_auth_protocol) } else { Ok(()) } @@ -427,22 +442,29 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } - let (use_hmac_extension, cred_protect_policy) = if let Some(extensions) = extensions { - let mut cred_protect = extensions.cred_protect; - if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - < DEFAULT_CRED_PROTECT - .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - { - cred_protect = DEFAULT_CRED_PROTECT; - } - (extensions.hmac_secret, cred_protect) - } else { - (false, DEFAULT_CRED_PROTECT) - }; - - let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); - let rp_id = rp.rp_id; + let (use_hmac_extension, cred_protect_policy, min_pin_length) = + if let Some(extensions) = extensions { + let mut cred_protect = extensions.cred_protect; + if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + < DEFAULT_CRED_PROTECT + .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + { + cred_protect = DEFAULT_CRED_PROTECT; + } + let min_pin_length = extensions.min_pin_length + && self + .persistent_store + .min_pin_length_rp_ids()? + .contains(&rp_id); + (extensions.hmac_secret, cred_protect, min_pin_length) + } else { + (false, DEFAULT_CRED_PROTECT, false) + }; + + let has_extension_output = + use_hmac_extension || cred_protect_policy.is_some() || min_pin_length; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { for cred_desc in exclude_list { @@ -541,9 +563,15 @@ where auth_data.extend(cose_key); if has_extension_output { let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; + let min_pin_length_output = if min_pin_length { + Some(self.persistent_store.min_pin_length()? as u64) + } else { + None + }; let extensions_output = cbor_map_options! { "hmac-secret" => hmac_secret_output, "credProtect" => cred_protect_policy, + "minPinLength" => min_pin_length_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -832,6 +860,7 @@ where String::from("clientPin"), self.persistent_store.pin_hash()?.is_some(), ); + options_map.insert(String::from("setMinPINLength"), true); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -840,13 +869,15 @@ where String::from(FIDO2_VERSION_STRING), String::from(FIDO2_1_VERSION_STRING), ], - extensions: Some(vec![String::from("hmac-secret")]), + extensions: Some(vec![ + String::from("hmac-secret"), + String::from("credProtect"), + String::from("minPinLength"), + ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(1024), - pin_protocols: Some(vec![ - CtapState::::PIN_PROTOCOL_VERSION, - ]), + pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), // #TODO(106) update with version 2.1 of HMAC-secret max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), @@ -1008,6 +1039,49 @@ mod test { // ID is irrelevant, so we pass this (dummy but valid) value. const DUMMY_CHANNEL_ID: ChannelID = [0x12, 0x34, 0x56, 0x78]; + fn check_make_response( + make_credential_response: Result, + flags: u8, + expected_aaguid: &[u8], + expected_credential_id_size: u8, + expected_extension_cbor: &[u8], + ) { + match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let AuthenticatorMakeCredentialResponse { + fmt, + auth_data, + att_stmt, + } = make_credential_response; + // The expected response is split to only assert the non-random parts. + assert_eq!(fmt, "packed"); + let mut expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, + ]; + expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); + expected_auth_data.extend(expected_aaguid); + expected_auth_data.extend(&[0x00, expected_credential_id_size]); + assert_eq!( + auth_data[0..expected_auth_data.len()], + expected_auth_data[..] + ); + /*assert_eq!( + &auth_data[expected_auth_data.len() + ..expected_auth_data.len() + expected_attested_cred_data.len()], + expected_attested_cred_data + );*/ + assert_eq!( + &auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], + expected_extension_cbor + ); + assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + } + _ => panic!("Invalid response type"), + } + } + #[test] fn test_get_info() { let mut rng = ThreadRng256 {}; @@ -1027,19 +1101,22 @@ mod test { expected_response.extend( [ 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, - 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x81, 0x6B, 0x68, 0x6D, 0x61, - 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x03, 0x50, + 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x83, 0x6B, 0x68, 0x6D, 0x61, + 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50, + 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, + 0x65, 0x6E, 0x67, 0x74, 0x68, 0x03, 0x50, ] .iter(), ); expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend( [ - 0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, - 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, - 0x08, 0x18, 0x70, 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, 0x0D, 0x04, + 0x04, 0xA4, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, + 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, + 0x50, 0x49, 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, + 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 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, 0x0D, 0x04, ] .iter(), ); @@ -1098,6 +1175,7 @@ mod test { let extensions = Some(MakeCredentialExtensions { hmac_secret: false, cred_protect: Some(policy), + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1114,31 +1192,13 @@ mod test { let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, - ]; - expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, 0x20]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); } #[test] @@ -1152,31 +1212,13 @@ mod test { let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, - ]; - expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + CREDENTIAL_ID_SIZE as u8, + &[], + ); } #[test] @@ -1294,6 +1336,7 @@ mod test { let extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1301,39 +1344,16 @@ mod test { let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, - ]; - expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, CREDENTIAL_ID_SIZE as u8]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - let expected_extension_cbor = vec![ - 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, - 0xF5, - ]; - assert_eq!( - auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], - expected_extension_cbor[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + let expected_extension_cbor = [ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + CREDENTIAL_ID_SIZE as u8, + &expected_extension_cbor, + ); } #[test] @@ -1345,45 +1365,80 @@ mod test { let extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - match make_credential_response.unwrap() { - ResponseData::AuthenticatorMakeCredential(make_credential_response) => { - let AuthenticatorMakeCredentialResponse { - fmt, - auth_data, - att_stmt, - } = make_credential_response; - // The expected response is split to only assert the non-random parts. - assert_eq!(fmt, "packed"); - let mut expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, - ]; - expected_auth_data.push(INITIAL_SIGNATURE_COUNTER as u8); - expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, 0x20]); - assert_eq!( - auth_data[0..expected_auth_data.len()], - expected_auth_data[..] - ); - let expected_extension_cbor = vec![ - 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, - 0xF5, - ]; - assert_eq!( - auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], - expected_extension_cbor[..] - ); - assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); - } - _ => panic!("Invalid response type"), - } + let expected_extension_cbor = [ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + } + + #[test] + fn test_process_make_credential_min_pin_length() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // First part: The extension is ignored, since the RP ID is not on the list. + let extensions = Some(MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: true, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let mut expected_attested_cred_data = + ctap_state.persistent_store.aaguid().unwrap().to_vec(); + expected_attested_cred_data.extend(&[0x00, 0x20]); + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); + + // Second part: The extension is used. + assert_eq!( + ctap_state + .persistent_store + .set_min_pin_length_rp_ids(vec!["example.com".to_string()]), + Ok(()) + ); + + let extensions = Some(MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: true, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, + 0x04, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); } #[test] @@ -1502,6 +1557,7 @@ mod test { let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1569,6 +1625,7 @@ mod test { let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, + min_pin_length: false, }); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; @@ -1761,10 +1818,8 @@ mod test { .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); - ctap_state - .persistent_store - .set_pin_hash(&[0u8; 16]) - .unwrap(); + // The PIN length is outside of the test scope and most likely incorrect. + ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ 0x6F, 0x52, 0x83, 0xBF, 0x1A, 0x91, 0xEE, 0x67, 0xE9, 0xD4, 0x4C, 0x80, 0x08, 0x79, 0x90, 0x8D, diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index b8aeb21..d4f148d 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; +use alloc::str; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; @@ -141,13 +142,14 @@ fn check_and_store_new_pin( .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; let min_pin_length = persistent_store.min_pin_length()? as usize; - if pin.len() < min_pin_length || pin.len() == PIN_PADDED_LENGTH { - // TODO(kaczmarczyck) check 4 code point minimum instead + let pin_length = str::from_utf8(&pin).unwrap_or("").chars().count(); + if pin_length < min_pin_length || pin.len() == PIN_PADDED_LENGTH { 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)?; + // The PIN length is always < 64. + persistent_store.set_pin(&pin_hash, pin_length as u8)?; Ok(()) } @@ -170,6 +172,7 @@ pub struct PinProtocolV1 { consecutive_pin_mismatches: u8, permissions: u8, permissions_rp_id: Option, + pub force_pin_change: bool, } impl PinProtocolV1 { @@ -182,6 +185,7 @@ impl PinProtocolV1 { consecutive_pin_mismatches: 0, permissions: 0, permissions_rp_id: None, + force_pin_change: false, } } @@ -363,54 +367,6 @@ impl PinProtocolV1 { Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } - fn process_set_min_pin_length( - &mut self, - 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_VENDOR_INTERNAL_ERROR); - } - if persistent_store.pin_hash()?.is_some() { - match pin_auth { - Some(pin_auth) => { - 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. - // 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]); - // 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_INTERNAL_ERROR); - // } - if !verify_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)?; - // 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)?; - // } - Ok(()) - } - fn process_get_pin_uv_auth_token_using_pin_with_permissions( &mut self, rng: &mut impl Rng256, @@ -450,8 +406,6 @@ impl PinProtocolV1 { pin_auth, new_pin_enc, pin_hash_enc, - min_pin_length, - min_pin_length_rp_ids, permissions, permissions_rp_id, } = client_pin_params; @@ -499,15 +453,6 @@ impl PinProtocolV1 { )?, ), ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), - 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, - pin_auth, - )?; - None - } ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( self.process_get_pin_uv_auth_token_using_pin_with_permissions( rng, @@ -577,7 +522,7 @@ impl PinProtocolV1 { #[cfg(test)] pub fn new_test( key_agreement_key: crypto::ecdh::SecKey, - pin_uv_auth_token: [u8; 32], + pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], ) -> PinProtocolV1 { PinProtocolV1 { key_agreement_key, @@ -585,6 +530,7 @@ impl PinProtocolV1 { consecutive_pin_mismatches: 0, permissions: 0xFF, permissions_rp_id: None, + force_pin_change: false, } } } @@ -600,7 +546,7 @@ mod test { pin[..4].copy_from_slice(b"1234"); let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); - persistent_store.set_pin_hash(&pin_hash).unwrap(); + persistent_store.set_pin(&pin_hash, 4).unwrap(); } // Encrypts the message with a zero IV and key derived from shared_secret. @@ -662,7 +608,7 @@ mod test { 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, 0xC4, 0x12, ]; - persistent_store.set_pin_hash(&pin_hash).unwrap(); + persistent_store.set_pin(&pin_hash, 4).unwrap(); 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); @@ -935,40 +881,6 @@ mod test { ); } - #[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 - // https://github.com/google/OpenSK/issues/129 - 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().unwrap(), 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().unwrap(), min_pin_length); - } - #[test] fn test_process() { let mut rng = ThreadRng256 {}; @@ -981,8 +893,6 @@ mod test { pin_auth: None, new_pin_enc: None, pin_hash_enc: None, - min_pin_length: None, - min_pin_length_rp_ids: None, permissions: None, permissions_rp_id: None, }; @@ -997,8 +907,6 @@ mod test { pin_auth: None, new_pin_enc: None, pin_hash_enc: None, - min_pin_length: None, - min_pin_length_rp_ids: None, permissions: None, permissions_rp_id: None, }; diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 390b0cb..0fa8e1e 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -31,6 +31,8 @@ pub enum ResponseData { AuthenticatorClientPin(Option), AuthenticatorReset, AuthenticatorSelection, + // TODO(kaczmarczyck) dummy, extend + AuthenticatorConfig, AuthenticatorVendor(AuthenticatorVendorResponse), } @@ -45,6 +47,7 @@ impl From for Option { ResponseData::AuthenticatorClientPin(None) => None, ResponseData::AuthenticatorReset => None, ResponseData::AuthenticatorSelection => None, + ResponseData::AuthenticatorConfig => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } } @@ -368,6 +371,12 @@ mod test { assert_eq!(response_cbor, None); } + #[test] + fn test_config_into_cbor() { + let response_cbor: Option = ResponseData::AuthenticatorConfig.into(); + assert_eq!(response_cbor, None); + } + #[test] fn test_vendor_response_into_cbor() { let response_cbor: Option = diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 28c1599..bc5ef95 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -53,11 +53,10 @@ const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; const MAX_PIN_RETRIES: u8 = 8; const DEFAULT_MIN_PIN_LENGTH: u8 = 4; -// TODO(kaczmarczyck) use this for the minPinLength extension -// https://github.com/google/OpenSK/issues/129 -const _DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); -// TODO(kaczmarczyck) Check whether this constant is necessary, or replace it accordingly. -const _MAX_RP_IDS_LENGTH: usize = 8; +const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); +// This constant is an attempt to limit storage requirements. If you don't set it to 0, +// the stored strings can still be unbounded, but that is true for all RP IDs. +const MAX_RP_IDS_LENGTH: usize = 8; /// Wrapper for master keys. pub struct MasterKeys { @@ -68,6 +67,15 @@ pub struct MasterKeys { pub hmac: [u8; 32], } +/// Wrapper for PIN properties. +struct PinProperties { + /// 16 byte prefix of SHA256 of the currently set PIN. + hash: [u8; PIN_AUTH_LENGTH], + + /// Length of the current PIN in code points. + code_point_length: u8, +} + /// CTAP persistent storage. pub struct PersistentStore { store: persistent_store::Store, @@ -296,26 +304,44 @@ impl PersistentStore { Ok(*array_ref![cred_random_secret, offset, 32]) } - /// Returns the PIN hash if defined. - pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { - let pin_hash = match self.store.find(key::PIN_HASH)? { + /// Reads the PIN properties and wraps them into PinProperties. + fn pin_properties(&self) -> Result, Ctap2StatusCode> { + let pin_properties = match self.store.find(key::PIN_PROPERTIES)? { None => return Ok(None), - Some(pin_hash) => pin_hash, + Some(pin_properties) => pin_properties, }; - if pin_hash.len() != PIN_AUTH_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + const PROPERTIES_LENGTH: usize = PIN_AUTH_LENGTH + 1; + match pin_properties.len() { + PROPERTIES_LENGTH => Ok(Some(PinProperties { + hash: *array_ref![pin_properties, 1, PIN_AUTH_LENGTH], + code_point_length: pin_properties[0], + })), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } - Ok(Some(*array_ref![pin_hash, 0, PIN_AUTH_LENGTH])) } - /// Sets the PIN hash. + /// Returns the PIN hash if defined. + pub fn pin_hash(&self) -> Result, Ctap2StatusCode> { + Ok(self.pin_properties()?.map(|p| p.hash)) + } + + /// Returns the length of the currently set PIN if defined. + pub fn pin_code_point_length(&self) -> Result, Ctap2StatusCode> { + Ok(self.pin_properties()?.map(|p| p.code_point_length)) + } + + /// Sets the PIN hash and length. /// /// If it was already defined, it is updated. - pub fn set_pin_hash( + pub fn set_pin( &mut self, pin_hash: &[u8; PIN_AUTH_LENGTH], + pin_code_point_length: u8, ) -> Result<(), Ctap2StatusCode> { - Ok(self.store.insert(key::PIN_HASH, pin_hash)?) + let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; + pin_properties[0] = pin_code_point_length; + pin_properties[1..].clone_from_slice(pin_hash); + Ok(self.store.insert(key::PIN_PROPERTIES, &pin_properties)?) } /// Returns the number of remaining PIN retries. @@ -358,34 +384,34 @@ impl PersistentStore { /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// allowed. - pub fn _min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { + pub fn min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { let rp_ids = self .store - .find(key::_MIN_PIN_LENGTH_RP_IDS)? - .map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { - _deserialize_min_pin_length_rp_ids(&value) + .find(key::MIN_PIN_LENGTH_RP_IDS)? + .map_or(Some(DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { + deserialize_min_pin_length_rp_ids(&value) }); debug_assert!(rp_ids.is_some()); Ok(rp_ids.unwrap_or_default()) } /// Sets the list of RP IDs that are used to check if reading the minimum PIN length is allowed. - pub fn _set_min_pin_length_rp_ids( + 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 { + 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 { + if min_pin_length_rp_ids.len() > MAX_RP_IDS_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); } Ok(self.store.insert( - key::_MIN_PIN_LENGTH_RP_IDS, - &_serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, + key::MIN_PIN_LENGTH_RP_IDS, + &serialize_min_pin_length_rp_ids(min_pin_length_rp_ids)?, )?) } @@ -573,7 +599,7 @@ fn serialize_credential(credential: PublicKeyCredentialSource) -> Result } /// Deserializes a list of RP IDs from storage representation. -fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { +fn deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { let cbor = cbor::read(data).ok()?; extract_array(cbor) .ok()? @@ -584,7 +610,7 @@ fn _deserialize_min_pin_length_rp_ids(data: &[u8]) -> Option> { } /// Serializes a list of RP IDs to storage representation. -fn _serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { +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) @@ -891,28 +917,38 @@ mod test { } #[test] - fn test_pin_hash() { + fn test_pin_hash_and_length() { 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()); + assert!(persistent_store.pin_code_point_length().unwrap().is_none()); - // Setting the pin hash sets the pin hash. + // Setting the pin 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(); + let pin_length_1 = 4; + let pin_length_2 = 63; + persistent_store.set_pin(&pin_hash_1, pin_length_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_code_point_length().unwrap(), + Some(pin_length_1) + ); + persistent_store.set_pin(&pin_hash_2, pin_length_2).unwrap(); assert_eq!(persistent_store.pin_hash().unwrap(), Some(pin_hash_2)); + assert_eq!( + persistent_store.pin_code_point_length().unwrap(), + Some(pin_length_2) + ); // Resetting the storage resets the pin hash. persistent_store.reset(&mut rng).unwrap(); assert!(persistent_store.pin_hash().unwrap().is_none()); + assert!(persistent_store.pin_code_point_length().unwrap().is_none()); } #[test] @@ -1006,22 +1042,22 @@ mod test { // 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 + 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()), + persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); - for rp_id in _DEFAULT_MIN_PIN_LENGTH_RP_IDS { + 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); + assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids); } #[test] @@ -1070,8 +1106,8 @@ mod test { #[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(); + 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); } } diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index ec39efa..dfe44fc 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -92,7 +92,7 @@ make_partition! { CRED_RANDOM_SECRET = 2041; /// List of RP IDs allowed to read the minimum PIN length. - _MIN_PIN_LENGTH_RP_IDS = 2042; + MIN_PIN_LENGTH_RP_IDS = 2042; /// The minimum PIN length. /// @@ -104,10 +104,11 @@ make_partition! { /// If the entry is absent, the number of PIN retries is `MAX_PIN_RETRIES`. PIN_RETRIES = 2044; - /// The PIN hash. + /// The PIN hash and length. /// - /// If the entry is absent, there is no PIN set. - PIN_HASH = 2045; + /// If the entry is absent, there is no PIN set. The first byte represents + /// the length, the following are an array with the hash. + PIN_PROPERTIES = 2045; /// The encryption and hmac keys. /// From ec259d8428fa0536abcd6c2dcdab11b2ef9a3b64 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 7 Jan 2021 18:50:34 +0100 Subject: [PATCH 110/192] adds comments to new config command file --- src/ctap/config_command.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 873a9b1..f270513 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -21,6 +21,7 @@ use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; use alloc::vec; +/// Processes the subcommand setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, pin_protocol_v1: &mut PinProtocolV1, @@ -52,6 +53,7 @@ fn process_set_min_pin_length( Ok(ResponseData::AuthenticatorConfig) } +/// Processes the AuthenticatorConfig command. pub fn process_config( persistent_store: &mut PersistentStore, pin_protocol_v1: &mut PinProtocolV1, From 6f9f833c0b6c6b94cc1a3d3ff75769d130ad0288 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 8 Jan 2021 15:42:35 +0100 Subject: [PATCH 111/192] moves COSE related conversion from crypto to data_formats --- libraries/crypto/src/ecdh.rs | 8 +++- libraries/crypto/src/ecdsa.rs | 59 ++++++++++----------------- src/ctap/data_formats.rs | 75 +++++++++++++++++++++++++++-------- src/ctap/mod.rs | 10 ++--- 4 files changed, 88 insertions(+), 64 deletions(-) diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index c735d11..9645f66 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -62,8 +62,10 @@ impl SecKey { // - https://www.secg.org/sec1-v2.pdf } - // DH key agreement method defined in the FIDO2 specification, Section 5.5.4. "Getting - // sharedSecret from Authenticator" + /// Creates a shared key using the Diffie Hellman key agreement. + /// + /// The key agreement is defined in the FIDO2 specification, + /// Section 6.5.5.4. "Obtaining the Shared Secret" pub fn exchange_x_sha256(&self, other: &PubKey) -> [u8; 32] { let p = self.exchange_raw(other); let mut x: [u8; 32] = [Default::default(); 32]; @@ -83,11 +85,13 @@ impl PubKey { self.p.to_bytes_uncompressed(bytes); } + /// Creates a new PubKey from their coordinates. pub fn from_coordinates(x: &[u8; NBYTES], y: &[u8; NBYTES]) -> Option { PointP256::new_checked_vartime(Int256::from_bin(x), Int256::from_bin(y)) .map(|p| PubKey { p }) } + /// Writes the coordinates into the passed in arrays. pub fn to_coordinates(&self, x: &mut [u8; NBYTES], y: &mut [u8; NBYTES]) { self.p.getx().to_int().to_bin(x); self.p.gety().to_int().to_bin(y); diff --git a/libraries/crypto/src/ecdsa.rs b/libraries/crypto/src/ecdsa.rs index 52949e3..8fef458 100644 --- a/libraries/crypto/src/ecdsa.rs +++ b/libraries/crypto/src/ecdsa.rs @@ -21,12 +21,15 @@ use super::rng256::Rng256; use super::{Hash256, HashBlockSize64Bytes}; use alloc::vec; use alloc::vec::Vec; +#[cfg(test)] +use arrayref::array_mut_ref; #[cfg(feature = "std")] use arrayref::array_ref; -use arrayref::{array_mut_ref, mut_array_refs}; -use cbor::{cbor_bytes, cbor_map_options}; +use arrayref::mut_array_refs; use core::marker::PhantomData; +pub const NBYTES: usize = int256::NBYTES; + #[derive(Clone, PartialEq)] #[cfg_attr(feature = "derive_debug", derive(Debug))] pub struct SecKey { @@ -38,6 +41,7 @@ pub struct Signature { s: NonZeroExponentP256, } +#[cfg_attr(feature = "derive_debug", derive(Clone))] pub struct PubKey { p: PointP256, } @@ -58,10 +62,11 @@ impl SecKey { } } - // ECDSA signature based on a RNG to generate a suitable randomization parameter. - // Under the hood, rejection sampling is used to make sure that the randomization parameter is - // uniformly distributed. - // The provided RNG must be cryptographically secure; otherwise this method is insecure. + /// Creates an ECDSA signature based on a RNG. + /// + /// Under the hood, rejection sampling is used to make sure that the + /// randomization parameter is uniformly distributed. The provided RNG must + /// be cryptographically secure; otherwise this method is insecure. pub fn sign_rng(&self, msg: &[u8], rng: &mut R) -> Signature where H: Hash256, @@ -77,8 +82,7 @@ impl SecKey { } } - // Deterministic ECDSA signature based on RFC 6979 to generate a suitable randomization - // parameter. + /// Creates a deterministic ECDSA signature based on RFC 6979. pub fn sign_rfc6979(&self, msg: &[u8]) -> Signature where H: Hash256 + HashBlockSize64Bytes, @@ -101,8 +105,10 @@ impl SecKey { } } - // Try signing a curve element given a randomization parameter k. If no signature can be - // obtained from this k, None is returned and the caller should try again with another value. + /// Try signing a curve element given a randomization parameter k. + /// + /// If no signature can be obtained from this k, None is returned and the + /// caller should try again with another value. fn try_sign(&self, k: &NonZeroExponentP256, msg: &ExponentP256) -> Option { let r = ExponentP256::modn(PointP256::base_point_mul(k.as_exponent()).getx().to_int()); // The branching here is fine because all this reveals is that k generated an unsuitable r. @@ -242,35 +248,10 @@ impl PubKey { representation } - // Encodes the key according to CBOR Object Signing and Encryption, defined in RFC 8152. - pub fn to_cose_key(&self) -> Option> { - const EC2_KEY_TYPE: i64 = 2; - const P_256_CURVE: i64 = 1; - let mut x_bytes = vec![0; int256::NBYTES]; - self.p - .getx() - .to_int() - .to_bin(array_mut_ref![x_bytes.as_mut_slice(), 0, int256::NBYTES]); - let x_byte_cbor: cbor::Value = cbor_bytes!(x_bytes); - let mut y_bytes = vec![0; int256::NBYTES]; - self.p - .gety() - .to_int() - .to_bin(array_mut_ref![y_bytes.as_mut_slice(), 0, int256::NBYTES]); - let y_byte_cbor: cbor::Value = cbor_bytes!(y_bytes); - let cbor_value = cbor_map_options! { - 1 => EC2_KEY_TYPE, - 3 => PubKey::ES256_ALGORITHM, - -1 => P_256_CURVE, - -2 => x_byte_cbor, - -3 => y_byte_cbor, - }; - let mut encoded_key = Vec::new(); - if cbor::write(cbor_value, &mut encoded_key) { - Some(encoded_key) - } else { - None - } + /// Writes the coordinates into the passed in arrays. + pub fn to_coordinates(&self, x: &mut [u8; NBYTES], y: &mut [u8; NBYTES]) { + self.p.getx().to_int().to_bin(x); + self.p.gety().to_int().to_bin(y); } #[cfg(feature = "std")] diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 8081567..ac1fb73 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -18,7 +18,7 @@ use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; use cbor::{cbor_array_vec, cbor_bytes_lit, cbor_map_options, destructure_cbor_map}; -use core::convert::TryFrom; +use core::convert::{TryFrom, TryInto}; use crypto::{ecdh, ecdsa}; #[cfg(test)] use enum_iterator::IntoEnumIterator; @@ -631,26 +631,39 @@ const ES256_ALGORITHM: i64 = -7; const EC2_KEY_TYPE: i64 = 2; const P_256_CURVE: i64 = 1; +impl TryFrom for CoseKey { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + if let cbor::Value::Map(cose_map) = cbor_value { + Ok(CoseKey(cose_map)) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } + } +} + +fn cose_key_from_bytes(x_bytes: [u8; ecdh::NBYTES], y_bytes: [u8; ecdh::NBYTES]) -> CoseKey { + let x_byte_cbor: cbor::Value = cbor_bytes_lit!(&x_bytes); + let y_byte_cbor: cbor::Value = cbor_bytes_lit!(&y_bytes); + // TODO(kaczmarczyck) do not write optional parameters, spec is unclear + let cose_cbor_value = cbor_map_options! { + 1 => EC2_KEY_TYPE, + 3 => ECDH_ALGORITHM, + -1 => P_256_CURVE, + -2 => x_byte_cbor, + -3 => y_byte_cbor, + }; + // Unwrap is safe here since we know it's a map. + cose_cbor_value.try_into().unwrap() +} + impl From for CoseKey { fn from(pk: ecdh::PubKey) -> Self { let mut x_bytes = [0; ecdh::NBYTES]; let mut y_bytes = [0; ecdh::NBYTES]; pk.to_coordinates(&mut x_bytes, &mut y_bytes); - let x_byte_cbor: cbor::Value = cbor_bytes_lit!(&x_bytes); - let y_byte_cbor: cbor::Value = cbor_bytes_lit!(&y_bytes); - // TODO(kaczmarczyck) do not write optional parameters, spec is unclear - let cose_cbor_value = cbor_map_options! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, - -2 => x_byte_cbor, - -3 => y_byte_cbor, - }; - if let cbor::Value::Map(cose_map) = cose_cbor_value { - CoseKey(cose_map) - } else { - unreachable!(); - } + cose_key_from_bytes(x_bytes, y_bytes) } } @@ -696,6 +709,15 @@ impl TryFrom for ecdh::PubKey { } } +impl From for CoseKey { + fn from(pk: ecdsa::PubKey) -> Self { + let mut x_bytes = [0; ecdh::NBYTES]; + let mut y_bytes = [0; ecdh::NBYTES]; + pk.to_coordinates(&mut x_bytes, &mut y_bytes); + cose_key_from_bytes(x_bytes, y_bytes) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ClientPinSubCommand { @@ -1322,7 +1344,7 @@ mod test { } #[test] - fn test_from_into_cose_key() { + fn test_from_into_cose_key_ecdh() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let pk = sk.genpk(); @@ -1331,6 +1353,25 @@ mod test { assert_eq!(created_pk, Ok(pk)); } + #[test] + fn test_into_cose_key_ecdsa() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdsa::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + let cose_map = cose_key.0; + let template = cbor_map! { + 1 => 0, + 3 => 0, + -1 => 0, + -2 => 0, + -3 => 0, + }; + for key in CoseKey::try_from(template).unwrap().0.keys() { + assert!(cose_map.contains_key(key)); + } + } + #[test] fn test_from_into_client_pin_sub_command() { let cbor_sub_command: cbor::Value = cbor_int!(0x01); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index fe60e80..f9229ac 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -31,7 +31,7 @@ use self::command::{ MAX_CREDENTIAL_COUNT_IN_LIST, }; use self::data_formats::{ - AuthenticatorTransport, CredentialProtectionPolicy, GetAssertionHmacSecretInput, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, @@ -534,11 +534,9 @@ where } auth_data.extend(vec![0x00, credential_id.len() as u8]); auth_data.extend(&credential_id); - let cose_key = match pk.to_cose_key() { - Some(cose_key) => cose_key, - None => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), - }; - auth_data.extend(cose_key); + if !cbor::write(cbor::Value::Map(CoseKey::from(pk).0), &mut auth_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } if has_extension_output { let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; let extensions_output = cbor_map_options! { From 18ebeebb3e4ddb3e376bc2ff56de5fc1042e5837 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 11 Jan 2021 11:51:01 +0100 Subject: [PATCH 112/192] adds storage changes for credential management --- src/ctap/mod.rs | 12 +++- src/ctap/response.rs | 19 ++++- src/ctap/storage.rs | 164 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 163 insertions(+), 32 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index fe60e80..4e93210 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -848,13 +848,19 @@ where CtapState::::PIN_PROTOCOL_VERSION, ]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // #TODO(106) update with version 2.1 of HMAC-secret + // TODO(#106) update with version 2.1 of HMAC-secret max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), default_cred_protect: DEFAULT_CRED_PROTECT, min_pin_length: self.persistent_store.min_pin_length()?, firmware_version: None, + max_cred_blob_length: None, + // TODO(kaczmarczyck) update when extension is implemented + max_rp_ids_for_set_min_pin_length: None, + remaining_discoverable_credentials: Some( + self.persistent_store.remaining_credentials()? as u64, + ), }, )) } @@ -1015,7 +1021,7 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - let mut expected_response = vec![0x00, 0xAA, 0x01]; + let mut expected_response = vec![0x00, 0xAB, 0x01]; // The version array differs with CTAP1, always including 2.0 and 2.1. #[cfg(not(feature = "with_ctap1"))] let version_count = 2; @@ -1039,7 +1045,7 @@ mod test { 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 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, 0x0D, 0x04, + 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x14, 0x18, 0x96, ] .iter(), ); diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 390b0cb..3ebab3b 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -107,7 +107,6 @@ impl From for cbor::Value { #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub struct AuthenticatorGetInfoResponse { - // TODO(kaczmarczyck) add maxAuthenticatorConfigLength and defaultCredProtect pub versions: Vec, pub extensions: Option>, pub aaguid: [u8; 16], @@ -121,6 +120,9 @@ pub struct AuthenticatorGetInfoResponse { pub default_cred_protect: Option, pub min_pin_length: u8, pub firmware_version: Option, + pub max_cred_blob_length: Option, + pub max_rp_ids_for_set_min_pin_length: Option, + pub remaining_discoverable_credentials: Option, } impl From for cbor::Value { @@ -139,6 +141,9 @@ impl From for cbor::Value { default_cred_protect, min_pin_length, firmware_version, + max_cred_blob_length, + max_rp_ids_for_set_min_pin_length, + remaining_discoverable_credentials, } = get_info_response; let options_cbor: Option = options.map(|options| { @@ -163,6 +168,9 @@ impl From for cbor::Value { 0x0C => default_cred_protect.map(|p| p as u64), 0x0D => min_pin_length as u64, 0x0E => firmware_version, + 0x0F => max_cred_blob_length, + 0x10 => max_rp_ids_for_set_min_pin_length, + 0x14 => remaining_discoverable_credentials, } } } @@ -285,6 +293,9 @@ mod test { default_cred_protect: None, min_pin_length: 4, firmware_version: None, + max_cred_blob_length: None, + max_rp_ids_for_set_min_pin_length: None, + remaining_discoverable_credentials: None, }; let response_cbor: Option = ResponseData::AuthenticatorGetInfo(get_info_response).into(); @@ -314,6 +325,9 @@ mod test { default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), min_pin_length: 4, firmware_version: Some(0), + max_cred_blob_length: Some(1024), + max_rp_ids_for_set_min_pin_length: Some(8), + remaining_discoverable_credentials: Some(150), }; let response_cbor: Option = ResponseData::AuthenticatorGetInfo(get_info_response).into(); @@ -331,6 +345,9 @@ mod test { 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, 0x0D => 4, 0x0E => 0, + 0x0F => 1024, + 0x10 => 8, + 0x14 => 150, }; assert_eq!(response_cbor, Some(expected_cbor)); } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 28c1599..76c2fd6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -16,6 +16,7 @@ mod key; use crate::ctap::data_formats::{ extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, + PublicKeyCredentialUserEntity, }; use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; @@ -116,6 +117,29 @@ impl PersistentStore { Ok(()) } + /// Finds the key and value for a given credential ID. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + fn find_credential_item( + &self, + credential_id: &[u8], + ) -> Result<(usize, PublicKeyCredentialSource), Ctap2StatusCode> { + let mut iter_result = Ok(()); + let iter = self.iter_credentials(&mut iter_result)?; + let mut credentials: Vec<(usize, PublicKeyCredentialSource)> = iter + .filter(|(_, credential)| credential.credential_id == credential_id) + .collect(); + iter_result?; + if credentials.len() > 1 { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + } + /// Returns the first matching credential. /// /// Returns `None` if no credentials are matched or if `check_cred_protect` is set and the first @@ -126,22 +150,17 @@ impl PersistentStore { credential_id: &[u8], check_cred_protect: bool, ) -> Result, Ctap2StatusCode> { - let mut iter_result = Ok(()); - let iter = self.iter_credentials(&mut iter_result)?; - // We don't check whether there is more than one matching credential to be able to exit - // early. - let result = iter.map(|(_, credential)| credential).find(|credential| { - credential.rp_id == rp_id && credential.credential_id == credential_id - }); - iter_result?; - if let Some(cred) = &result { - let user_verification_required = cred.cred_protect_policy - == Some(CredentialProtectionPolicy::UserVerificationRequired); - if check_cred_protect && user_verification_required { - return Ok(None); - } + let credential = match self.find_credential_item(credential_id) { + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) => return Ok(None), + Err(e) => return Err(e), + Ok(credential) => credential.1, + }; + let is_protected = credential.cred_protect_policy + == Some(CredentialProtectionPolicy::UserVerificationRequired); + if credential.rp_id != rp_id || (check_cred_protect && is_protected) { + return Ok(None); } - Ok(result) + Ok(Some(credential)) } /// Stores or updates a credential. @@ -196,6 +215,34 @@ impl PersistentStore { Ok(()) } + /// Deletes a credential. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + pub fn _delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { + let (key, _) = self.find_credential_item(credential_id)?; + Ok(self.store.remove(key)?) + } + + /// Updates a credential's user information. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + pub fn _update_credential( + &mut self, + credential_id: &[u8], + user: PublicKeyCredentialUserEntity, + ) -> Result<(), Ctap2StatusCode> { + let (key, mut credential) = self.find_credential_item(credential_id)?; + credential.user_name = user.user_name; + credential.user_display_name = user.user_display_name; + credential.user_icon = user.user_icon; + let value = serialize_credential(credential)?; + Ok(self.store.insert(key, &value)?) + } + /// Returns the list of matching credentials. /// /// Does not return credentials that are not discoverable if `check_cred_protect` is set. @@ -221,7 +268,6 @@ impl PersistentStore { } /// Returns the number of credentials. - #[cfg(test)] pub fn count_credentials(&self) -> Result { let mut iter_result = Ok(()); let iter = self.iter_credentials(&mut iter_result)?; @@ -230,10 +276,17 @@ impl PersistentStore { Ok(result) } + /// Returns the estimated number of credentials that can still be stored. + pub fn remaining_credentials(&self) -> Result { + MAX_SUPPORTED_RESIDENTIAL_KEYS + .checked_sub(self.count_credentials()?) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } + /// Iterates through the credentials. /// /// If an error is encountered during iteration, it is written to `result`. - fn iter_credentials<'a>( + pub fn iter_credentials<'a>( &'a self, result: &'a mut Result<(), Ctap2StatusCode>, ) -> Result, Ctap2StatusCode> { @@ -494,7 +547,7 @@ impl From for Ctap2StatusCode { } /// Iterator for credentials. -struct IterCredentials<'a> { +pub struct IterCredentials<'a> { /// The store being iterated. store: &'a persistent_store::Store, @@ -629,6 +682,66 @@ mod test { assert!(persistent_store.count_credentials().unwrap() > 0); } + #[test] + fn test_delete_credential() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + assert_eq!(persistent_store.count_credentials().unwrap(), 0); + + let mut credential_ids = vec![]; + for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { + let user_handle = i.to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); + credential_ids.push(credential_source.credential_id.clone()); + assert!(persistent_store.store_credential(credential_source).is_ok()); + assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); + } + let mut count = persistent_store.count_credentials().unwrap(); + for credential_id in credential_ids { + assert!(persistent_store._delete_credential(&credential_id).is_ok()); + count -= 1; + assert_eq!(persistent_store.count_credentials().unwrap(), count); + } + } + + #[test] + fn test_update_credential() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let user = PublicKeyCredentialUserEntity { + // User ID is ignored. + user_id: vec![0x00], + user_name: Some("name".to_string()), + user_display_name: Some("display_name".to_string()), + user_icon: Some("icon".to_string()), + }; + assert_eq!( + persistent_store._update_credential(&[0x1D], user.clone()), + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + ); + + let credential_source = create_credential_source(&mut rng, "example.com", vec![0x1D]); + let credential_id = credential_source.credential_id.clone(); + assert!(persistent_store.store_credential(credential_source).is_ok()); + let stored_credential = persistent_store + .find_credential("example.com", &credential_id, false) + .unwrap() + .unwrap(); + assert_eq!(stored_credential.user_name, None); + assert_eq!(stored_credential.user_display_name, None); + assert_eq!(stored_credential.user_icon, None); + assert!(persistent_store + ._update_credential(&credential_id, user.clone()) + .is_ok()); + let stored_credential = persistent_store + .find_credential("example.com", &credential_id, false) + .unwrap() + .unwrap(); + assert_eq!(stored_credential.user_name, user.user_name); + assert_eq!(stored_credential.user_display_name, user.user_display_name); + assert_eq!(stored_credential.user_icon, user.user_icon); + } + #[test] fn test_credential_order() { let mut rng = ThreadRng256 {}; @@ -645,17 +758,14 @@ mod test { } #[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]); + let user_handle = i.to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); } @@ -675,7 +785,6 @@ mod test { } #[test] - #[allow(clippy::assertions_on_constants)] fn test_overwrite() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); @@ -699,11 +808,10 @@ mod test { &[expected_credential] ); - // To make this test work for bigger storages, implement better int -> Vec conversion. - assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS < 256); + let mut persistent_store = PersistentStore::new(&mut rng); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { - let credential_source = - create_credential_source(&mut rng, "example.com", vec![i as u8]); + let user_handle = i.to_ne_bytes().to_vec(); + let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), i + 1); } From 4cee0c4c656374664426a8c7ed5d8221ceb9b983 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 11 Jan 2021 14:31:13 +0100 Subject: [PATCH 113/192] only keeps keys instead of credentials as state --- src/ctap/mod.rs | 76 ++++++++++++++++++++++++++------------------- src/ctap/storage.rs | 64 ++++++++++++++++++++++++++------------ 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4e93210..5072a47 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -142,7 +142,7 @@ struct AssertionInput { struct AssertionState { assertion_input: AssertionInput, // Sorted by ascending order of creation, so the last element is the most recent one. - next_credentials: Vec, + next_credential_keys: Vec, } enum StatefulCommand { @@ -606,7 +606,7 @@ where // and returns the correct Get(Next)Assertion response. fn assertion_response( &mut self, - credential: PublicKeyCredentialSource, + mut credential: PublicKeyCredentialSource, assertion_input: AssertionInput, number_of_credentials: Option, ) -> Result { @@ -642,6 +642,12 @@ where key_id: credential.credential_id, transports: None, // You can set USB as a hint here. }; + // Remove user identifiable information without uv. + if !has_uv { + credential.user_name = None; + credential.user_display_name = None; + credential.user_icon = None; + } let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { user_id: credential.user_handle, @@ -749,26 +755,23 @@ where } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut applicable_credentials = if let Some(allow_list) = allow_list { - if let Some(credential) = - self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)? - { - vec![credential] - } else { - vec![] - } + let (credential, next_credential_keys) = if let Some(allow_list) = allow_list { + ( + self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)?, + vec![], + ) } else { - self.persistent_store.filter_credential(&rp_id, !has_uv)? + let mut stored_credentials = + self.persistent_store.filter_credentials(&rp_id, !has_uv)?; + stored_credentials.sort_unstable_by_key(|c| c.1); + let mut stored_credentials: Vec = + stored_credentials.into_iter().map(|c| c.0).collect(); + let credential = stored_credentials + .pop() + .map(|key| self.persistent_store.get_credential(key)) + .transpose()?; + (credential, stored_credentials) }; - // Remove user identifiable information without uv. - if !has_uv { - for credential in &mut applicable_credentials { - credential.user_name = None; - credential.user_display_name = None; - credential.user_icon = None; - } - } - applicable_credentials.sort_unstable_by_key(|c| c.creation_order); // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. // For CTAP 2.1, it was moved to a later protocol step. @@ -776,9 +779,7 @@ where (self.check_user_presence)(cid)?; } - let credential = applicable_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; self.increment_global_signature_counter()?; @@ -788,15 +789,15 @@ where hmac_secret_input, has_uv, }; - let number_of_credentials = if applicable_credentials.is_empty() { + let number_of_credentials = if next_credential_keys.is_empty() { None } else { - let number_of_credentials = Some(applicable_credentials.len() + 1); + let number_of_credentials = Some(next_credential_keys.len() + 1); self.stateful_command_permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { assertion_input: assertion_input.clone(), - next_credentials: applicable_credentials, + next_credential_keys, })); number_of_credentials }; @@ -812,10 +813,11 @@ where if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.stateful_command_type { - let credential = assertion_state - .next_credentials + let credential_key = assertion_state + .next_credential_keys .pop() .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + let credential = self.persistent_store.get_credential(credential_key)?; (assertion_state.assertion_input.clone(), credential) } else { return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); @@ -1250,11 +1252,16 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let stored_credential = ctap_state + let credential_key = ctap_state .persistent_store - .filter_credential("example.com", false) + .filter_credentials("example.com", false) .unwrap() .pop() + .unwrap() + .0; + let stored_credential = ctap_state + .persistent_store + .get_credential(credential_key) .unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); @@ -1275,11 +1282,16 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let stored_credential = ctap_state + let credential_key = ctap_state .persistent_store - .filter_credential("example.com", false) + .filter_credentials("example.com", false) .unwrap() .pop() + .unwrap() + .0; + let stored_credential = ctap_state + .persistent_store + .get_credential(credential_key) .unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 76c2fd6..343751a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -117,6 +117,24 @@ impl PersistentStore { Ok(()) } + /// Returns the credential at the given key. + /// + /// # Errors + /// + /// Returns `CTAP2_ERR_VENDOR_INTERNAL_ERROR` if the key does not hold a valid credential. + pub fn get_credential(&self, key: usize) -> Result { + let min_key = key::CREDENTIALS.start; + if key < min_key || key >= min_key + MAX_SUPPORTED_RESIDENTIAL_KEYS { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let credential_entry = self + .store + .find(key)? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + deserialize_credential(&credential_entry) + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) + } + /// Finds the key and value for a given credential ID. /// /// # Errors @@ -246,22 +264,23 @@ impl PersistentStore { /// Returns the list of matching credentials. /// /// Does not return credentials that are not discoverable if `check_cred_protect` is set. - pub fn filter_credential( + pub fn filter_credentials( &self, rp_id: &str, check_cred_protect: bool, - ) -> Result, Ctap2StatusCode> { + ) -> Result, Ctap2StatusCode> { let mut iter_result = Ok(()); let iter = self.iter_credentials(&mut iter_result)?; let result = iter - .filter_map(|(_, credential)| { - if credential.rp_id == rp_id { - Some(credential) + .filter_map(|(key, credential)| { + if credential.rp_id == rp_id + && (!check_cred_protect || credential.is_discoverable()) + { + Some((key, credential.creation_order)) } else { None } }) - .filter(|cred| !check_cred_protect || cred.is_discoverable()) .collect(); iter_result?; Ok(result) @@ -801,12 +820,13 @@ mod test { .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] - ); + let filtered_credentials = persistent_store + .filter_credentials("example.com", false) + .unwrap(); + let retrieved_credential_source = persistent_store + .get_credential(filtered_credentials[0].0) + .unwrap(); + assert_eq!(retrieved_credential_source, expected_credential); let mut persistent_store = PersistentStore::new(&mut rng); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { @@ -831,7 +851,7 @@ mod test { } #[test] - fn test_filter() { + fn test_filter_get_credentials() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); @@ -852,14 +872,20 @@ mod test { .is_ok()); let filtered_credentials = persistent_store - .filter_credential("example.com", false) + .filter_credentials("example.com", false) .unwrap(); assert_eq!(filtered_credentials.len(), 2); + let retrieved_credential0 = persistent_store + .get_credential(filtered_credentials[0].0) + .unwrap(); + let retrieved_credential1 = persistent_store + .get_credential(filtered_credentials[1].0) + .unwrap(); 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) + (retrieved_credential0.credential_id == id0 + && retrieved_credential1.credential_id == id1) + || (retrieved_credential1.credential_id == id0 + && retrieved_credential0.credential_id == id1) ); } @@ -886,7 +912,7 @@ mod test { assert!(persistent_store.store_credential(credential).is_ok()); let no_credential = persistent_store - .filter_credential("example.com", true) + .filter_credentials("example.com", true) .unwrap(); assert_eq!(no_credential, vec![]); } From 27a7108328fcea9bba7bfe1b1411167e9a7551ef Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 07:01:25 +0100 Subject: [PATCH 114/192] moves filter_credentials to call side --- src/ctap/mod.rs | 52 +++++++++++--------- src/ctap/storage.rs | 112 ++++++++------------------------------------ 2 files changed, 49 insertions(+), 115 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 5072a47..2047500 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -761,11 +761,23 @@ where vec![], ) } else { - let mut stored_credentials = - self.persistent_store.filter_credentials(&rp_id, !has_uv)?; - stored_credentials.sort_unstable_by_key(|c| c.1); - let mut stored_credentials: Vec = - stored_credentials.into_iter().map(|c| c.0).collect(); + let mut iter_result = Ok(()); + let iter = self.persistent_store.iter_credentials(&mut iter_result)?; + let mut stored_credentials: Vec<(usize, u64)> = iter + .filter_map(|(key, credential)| { + if credential.rp_id == rp_id && (has_uv || credential.is_discoverable()) { + Some((key, credential.creation_order)) + } else { + None + } + }) + .collect(); + iter_result?; + stored_credentials.sort_unstable_by_key(|&(_key, order)| order); + let mut stored_credentials: Vec = stored_credentials + .into_iter() + .map(|(key, _order)| key) + .collect(); let credential = stored_credentials .pop() .map(|key| self.persistent_store.get_credential(key)) @@ -1252,17 +1264,14 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let credential_key = ctap_state + let mut iter_result = Ok(()); + let iter = ctap_state .persistent_store - .filter_credentials("example.com", false) - .unwrap() - .pop() - .unwrap() - .0; - let stored_credential = ctap_state - .persistent_store - .get_credential(credential_key) + .iter_credentials(&mut iter_result) .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); @@ -1282,17 +1291,14 @@ mod test { ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert!(make_credential_response.is_ok()); - let credential_key = ctap_state + let mut iter_result = Ok(()); + let iter = ctap_state .persistent_store - .filter_credentials("example.com", false) - .unwrap() - .pop() - .unwrap() - .0; - let stored_credential = ctap_state - .persistent_store - .get_credential(credential_key) + .iter_credentials(&mut iter_result) .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); let credential_id = stored_credential.credential_id; assert_eq!(stored_credential.cred_protect_policy, Some(test_policy)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 343751a..8df987d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -171,7 +171,7 @@ impl PersistentStore { let credential = match self.find_credential_item(credential_id) { Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) => return Ok(None), Err(e) => return Err(e), - Ok(credential) => credential.1, + Ok((_key, credential)) => credential, }; let is_protected = credential.cred_protect_policy == Some(CredentialProtectionPolicy::UserVerificationRequired); @@ -261,31 +261,6 @@ impl PersistentStore { Ok(self.store.insert(key, &value)?) } - /// Returns the list of matching credentials. - /// - /// Does not return credentials that are not discoverable if `check_cred_protect` is set. - pub fn filter_credentials( - &self, - rp_id: &str, - check_cred_protect: bool, - ) -> Result, Ctap2StatusCode> { - let mut iter_result = Ok(()); - let iter = self.iter_credentials(&mut iter_result)?; - let result = iter - .filter_map(|(key, credential)| { - if credential.rp_id == rp_id - && (!check_cred_protect || credential.is_discoverable()) - { - Some((key, credential.creation_order)) - } else { - None - } - }) - .collect(); - iter_result?; - Ok(result) - } - /// Returns the number of credentials. pub fn count_credentials(&self) -> Result { let mut iter_result = Ok(()); @@ -811,7 +786,8 @@ mod test { // 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(); + let credential_id0 = credential_source0.credential_id.clone(); + let credential_id1 = credential_source1.credential_id.clone(); assert!(persistent_store .store_credential(credential_source0) @@ -820,13 +796,14 @@ mod test { .store_credential(credential_source1) .is_ok()); assert_eq!(persistent_store.count_credentials().unwrap(), 1); - let filtered_credentials = persistent_store - .filter_credentials("example.com", false) - .unwrap(); - let retrieved_credential_source = persistent_store - .get_credential(filtered_credentials[0].0) - .unwrap(); - assert_eq!(retrieved_credential_source, expected_credential); + assert!(persistent_store + .find_credential("example.com", &credential_id0, false) + .unwrap() + .is_none()); + assert!(persistent_store + .find_credential("example.com", &credential_id1, false) + .unwrap() + .is_some()); let mut persistent_store = PersistentStore::new(&mut rng); for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { @@ -851,70 +828,21 @@ mod test { } #[test] - fn test_filter_get_credentials() { + fn test_get_credential() { 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_credentials("example.com", false) - .unwrap(); - assert_eq!(filtered_credentials.len(), 2); - let retrieved_credential0 = persistent_store - .get_credential(filtered_credentials[0].0) - .unwrap(); - let retrieved_credential1 = persistent_store - .get_credential(filtered_credentials[1].0) - .unwrap(); - assert!( - (retrieved_credential0.credential_id == id0 - && retrieved_credential1.credential_id == id1) - || (retrieved_credential1.credential_id == id0 - && retrieved_credential0.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_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_credentials("example.com", true) - .unwrap(); - assert_eq!(no_credential, vec![]); + let credential_sources = vec![credential_source0, credential_source1, credential_source2]; + for credential_source in credential_sources.into_iter() { + let cred_id = credential_source.credential_id.clone(); + assert!(persistent_store.store_credential(credential_source).is_ok()); + let (key, _) = persistent_store.find_credential_item(&cred_id).unwrap(); + let cred = persistent_store.get_credential(key).unwrap(); + assert_eq!(&cred_id, &cred.credential_id); + } } #[test] From 2776bd9b8ec7af7f486b4db2cb1100625a810e28 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 15:11:20 +0100 Subject: [PATCH 115/192] new CoseKey data format --- libraries/crypto/src/ecdh.rs | 2 +- libraries/crypto/src/ecdsa.rs | 1 - src/ctap/command.rs | 13 +- src/ctap/data_formats.rs | 275 +++++++++++++++++++++++----------- src/ctap/mod.rs | 2 +- src/ctap/response.rs | 2 +- 6 files changed, 196 insertions(+), 99 deletions(-) diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index 9645f66..a1e3736 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -85,7 +85,7 @@ impl PubKey { self.p.to_bytes_uncompressed(bytes); } - /// Creates a new PubKey from their coordinates. + /// Creates a new PubKey from its coordinates on the elliptic curve. pub fn from_coordinates(x: &[u8; NBYTES], y: &[u8; NBYTES]) -> Option { PointP256::new_checked_vartime(Int256::from_bin(x), Int256::from_bin(y)) .map(|p| PubKey { p }) diff --git a/libraries/crypto/src/ecdsa.rs b/libraries/crypto/src/ecdsa.rs index 8fef458..b6a1708 100644 --- a/libraries/crypto/src/ecdsa.rs +++ b/libraries/crypto/src/ecdsa.rs @@ -220,7 +220,6 @@ impl Signature { } impl PubKey { - pub const ES256_ALGORITHM: i64 = -7; #[cfg(feature = "with_ctap1")] const UNCOMPRESSED_LENGTH: usize = 1 + 2 * int256::NBYTES; diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 0a86093..6f0aa72 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -317,7 +317,7 @@ impl TryFrom for AuthenticatorClientPinParameters { let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?; let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; - let key_agreement = key_agreement.map(extract_map).transpose()?.map(CoseKey); + let key_agreement = key_agreement.map(CoseKey::try_from).transpose()?; 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()?; @@ -423,8 +423,8 @@ mod test { }; use super::super::ES256_CRED_PARAM; use super::*; - use alloc::collections::BTreeMap; use cbor::{cbor_array, cbor_map}; + use crypto::rng256::ThreadRng256; #[test] fn test_from_cbor_make_credential_parameters() { @@ -534,10 +534,15 @@ mod test { #[test] fn test_from_cbor_client_pin_parameters() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + let cbor_value = cbor_map! { 1 => 1, 2 => ClientPinSubCommand::GetPinRetries, - 3 => cbor_map!{}, + 3 => cbor::Value::from(cose_key.clone()), 4 => vec! [0xBB], 5 => vec! [0xCC], 6 => vec! [0xDD], @@ -552,7 +557,7 @@ mod test { let expected_pin_protocol_parameters = AuthenticatorClientPinParameters { pin_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: Some(CoseKey(BTreeMap::new())), + key_agreement: Some(cose_key), pin_auth: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index ac1fb73..87bc6b4 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -17,12 +17,23 @@ use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; -use cbor::{cbor_array_vec, cbor_bytes_lit, cbor_map_options, destructure_cbor_map}; -use core::convert::{TryFrom, TryInto}; +use cbor::{cbor_array_vec, cbor_map, cbor_map_options, destructure_cbor_map}; +use core::convert::TryFrom; use crypto::{ecdh, ecdsa}; #[cfg(test)] use enum_iterator::IntoEnumIterator; +// This is the algorithm specifier that is supposed to be used in a COSE key +// map in ECDH. CTAP requests -25 which represents ECDH-ES + HKDF-256 here: +// https://www.iana.org/assignments/cose/cose.xhtml#algorithms +const ECDH_ALGORITHM: i64 = -25; +// This is the identifier used for ECDSA and ECDH in OpenSSH. +const ES256_ALGORITHM: i64 = -7; +// The COSE key parameter behind map key 1. +const EC2_KEY_TYPE: i64 = 2; +// The COSE key parameter behind map key -1. +const P_256_CURVE: i64 = 1; + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialRpEntity { @@ -322,17 +333,17 @@ impl TryFrom for GetAssertionHmacSecretInput { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => cose_key, + 1 => key_agreement, 2 => salt_enc, 3 => salt_auth, } = extract_map(cbor_value)?; } - let cose_key = extract_map(ok_or_missing(cose_key)?)?; + let key_agreement = CoseKey::try_from(ok_or_missing(key_agreement)?)?; let salt_enc = extract_byte_string(ok_or_missing(salt_enc)?)?; let salt_auth = extract_byte_string(ok_or_missing(salt_auth)?)?; Ok(Self { - key_agreement: CoseKey(cose_key), + key_agreement, salt_enc, salt_auth, }) @@ -432,7 +443,7 @@ impl From for cbor::Value { #[derive(PartialEq)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub enum SignatureAlgorithm { - ES256 = ecdsa::PubKey::ES256_ALGORITHM as isize, + ES256 = ES256_ALGORITHM as isize, // This is the default for all numbers not covered above. // Unknown types should be ignored, instead of returning errors. Unknown = 0, @@ -449,7 +460,7 @@ impl TryFrom for SignatureAlgorithm { fn try_from(cbor_value: cbor::Value) -> Result { match extract_integer(cbor_value)? { - ecdsa::PubKey::ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256), + ES256_ALGORITHM => Ok(SignatureAlgorithm::ES256), _ => Ok(SignatureAlgorithm::Unknown), } } @@ -614,85 +625,31 @@ impl PublicKeyCredentialSource { } } -// TODO(kaczmarczyck) we could decide to split this data type up -// It depends on the algorithm though, I think. -// So before creating a mess, this is my workaround. +// The COSE key is used for both ECDH and ECDSA public keys for transmission. #[derive(Clone)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] -pub struct CoseKey(pub BTreeMap); - -// This is the algorithm specifier that is supposed to be used in a COSE key -// map. The CTAP specification says -25 which represents ECDH-ES + HKDF-256 -// here: https://www.iana.org/assignments/cose/cose.xhtml#algorithms -// In fact, this is just used for compatibility with older specification versions. -const ECDH_ALGORITHM: i64 = -25; -// This is the identifier used by OpenSSH. To be compatible, we accept both. -const ES256_ALGORITHM: i64 = -7; -const EC2_KEY_TYPE: i64 = 2; -const P_256_CURVE: i64 = 1; +pub struct CoseKey { + x_bytes: [u8; ecdh::NBYTES], + y_bytes: [u8; ecdh::NBYTES], + algorithm: i64, +} +// This conversion accepts both ECDH and ECDSA. impl TryFrom for CoseKey { type Error = Ctap2StatusCode; fn try_from(cbor_value: cbor::Value) -> Result { - if let cbor::Value::Map(cose_map) = cbor_value { - Ok(CoseKey(cose_map)) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) - } - } -} - -fn cose_key_from_bytes(x_bytes: [u8; ecdh::NBYTES], y_bytes: [u8; ecdh::NBYTES]) -> CoseKey { - let x_byte_cbor: cbor::Value = cbor_bytes_lit!(&x_bytes); - let y_byte_cbor: cbor::Value = cbor_bytes_lit!(&y_bytes); - // TODO(kaczmarczyck) do not write optional parameters, spec is unclear - let cose_cbor_value = cbor_map_options! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, - -2 => x_byte_cbor, - -3 => y_byte_cbor, - }; - // Unwrap is safe here since we know it's a map. - cose_cbor_value.try_into().unwrap() -} - -impl From for CoseKey { - fn from(pk: ecdh::PubKey) -> Self { - let mut x_bytes = [0; ecdh::NBYTES]; - let mut y_bytes = [0; ecdh::NBYTES]; - pk.to_coordinates(&mut x_bytes, &mut y_bytes); - cose_key_from_bytes(x_bytes, y_bytes) - } -} - -impl TryFrom for ecdh::PubKey { - type Error = Ctap2StatusCode; - - fn try_from(cose_key: CoseKey) -> Result { destructure_cbor_map! { let { + // This is sorted correctly, negative encoding is bigger. 1 => key_type, 3 => algorithm, -1 => curve, -2 => x_bytes, -3 => y_bytes, - } = cose_key.0; + } = extract_map(cbor_value)?; } - let key_type = extract_integer(ok_or_missing(key_type)?)?; - if key_type != EC2_KEY_TYPE { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); - } - let algorithm = extract_integer(ok_or_missing(algorithm)?)?; - if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); - } - let curve = extract_integer(ok_or_missing(curve)?)?; - if curve != P_256_CURVE { - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); - } let x_bytes = extract_byte_string(ok_or_missing(x_bytes)?)?; if x_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); @@ -701,11 +658,55 @@ impl TryFrom for ecdh::PubKey { if y_bytes.len() != ecdh::NBYTES { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } + let curve = extract_integer(ok_or_missing(curve)?)?; + if curve != P_256_CURVE { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + let key_type = extract_integer(ok_or_missing(key_type)?)?; + if key_type != EC2_KEY_TYPE { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + let algorithm = extract_integer(ok_or_missing(algorithm)?)?; + if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } - let x_array_ref = array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES]; - let y_array_ref = array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES]; - ecdh::PubKey::from_coordinates(x_array_ref, y_array_ref) - .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + Ok(CoseKey { + x_bytes: *array_ref![x_bytes.as_slice(), 0, ecdh::NBYTES], + y_bytes: *array_ref![y_bytes.as_slice(), 0, ecdh::NBYTES], + algorithm, + }) + } +} + +impl From for cbor::Value { + fn from(cose_key: CoseKey) -> Self { + let CoseKey { + x_bytes, + y_bytes, + algorithm, + } = cose_key; + + cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => algorithm, + -1 => P_256_CURVE, + -2 => x_bytes, + -3 => y_bytes, + } + } +} + +impl From for CoseKey { + fn from(pk: ecdh::PubKey) -> Self { + let mut x_bytes = [0; ecdh::NBYTES]; + let mut y_bytes = [0; ecdh::NBYTES]; + pk.to_coordinates(&mut x_bytes, &mut y_bytes); + CoseKey { + x_bytes, + y_bytes, + algorithm: ECDH_ALGORITHM, + } } } @@ -714,7 +715,32 @@ impl From for CoseKey { let mut x_bytes = [0; ecdh::NBYTES]; let mut y_bytes = [0; ecdh::NBYTES]; pk.to_coordinates(&mut x_bytes, &mut y_bytes); - cose_key_from_bytes(x_bytes, y_bytes) + CoseKey { + x_bytes, + y_bytes, + algorithm: ES256_ALGORITHM, + } + } +} + +impl TryFrom for ecdh::PubKey { + type Error = Ctap2StatusCode; + + fn try_from(cose_key: CoseKey) -> Result { + let CoseKey { + x_bytes, + y_bytes, + algorithm, + } = cose_key; + + // Since algorithm can be used for different COSE key types, we check + // whether the current type is correct for ECDH. For an OpenSSH bugfix, + // the algorithm ES256_ALGORITHM is allowed here too. + if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + ecdh::PubKey::from_coordinates(&x_bytes, &y_bytes) + .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) } } @@ -827,8 +853,8 @@ mod test { use super::*; use alloc::collections::BTreeMap; use cbor::{ - cbor_array, cbor_bool, cbor_bytes, cbor_false, cbor_int, cbor_map, cbor_null, cbor_text, - cbor_unsigned, + cbor_array, cbor_bool, cbor_bytes, cbor_bytes_lit, cbor_false, cbor_int, cbor_null, + cbor_text, cbor_unsigned, }; use crypto::rng256::{Rng256, ThreadRng256}; @@ -1151,7 +1177,7 @@ mod test { #[test] fn test_from_into_signature_algorithm() { - let cbor_signature_algorithm: cbor::Value = cbor_int!(ecdsa::PubKey::ES256_ALGORITHM); + let cbor_signature_algorithm: cbor::Value = cbor_int!(ES256_ALGORITHM); let signature_algorithm = SignatureAlgorithm::try_from(cbor_signature_algorithm.clone()); let expected_signature_algorithm = SignatureAlgorithm::ES256; assert_eq!(signature_algorithm, Ok(expected_signature_algorithm)); @@ -1225,7 +1251,7 @@ mod test { fn test_from_into_public_key_credential_parameter() { let cbor_credential_parameter = cbor_map! { "type" => "public-key", - "alg" => ecdsa::PubKey::ES256_ALGORITHM, + "alg" => ES256_ALGORITHM, }; let credential_parameter = PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); @@ -1279,7 +1305,7 @@ mod test { let cose_key = CoseKey::from(pk); let cbor_extensions = cbor_map! { "hmac-secret" => cbor_map! { - 1 => cbor::Value::Map(cose_key.0.clone()), + 1 => cbor::Value::from(cose_key.clone()), 2 => vec![0x02; 32], 3 => vec![0x03; 16], }, @@ -1343,6 +1369,83 @@ mod test { assert_eq!(created_cbor, cbor_packed_attestation_statement); } + #[test] + fn test_from_into_cose_key_cbor() { + for algorithm in &[ECDH_ALGORITHM, ES256_ALGORITHM] { + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => algorithm, + -1 => P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + let cose_key = CoseKey::try_from(cbor_value.clone()).unwrap(); + let created_cbor_value = cbor::Value::from(cose_key); + assert_eq!(created_cbor_value, cbor_value); + } + + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + // unknown algorithm + 3 => 0, + -1 => P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + let cbor_value = cbor_map! { + // unknown type + 1 => 0, + 3 => ECDH_ALGORITHM, + -1 => P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => ECDH_ALGORITHM, + // unknown curve + -1 => 0, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => ECDH_ALGORITHM, + -1 => P_256_CURVE, + // wrong length + -2 => [0u8; 31], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + let cbor_value = cbor_map! { + 1 => EC2_KEY_TYPE, + 3 => ECDH_ALGORITHM, + -1 => P_256_CURVE, + -2 => [0u8; 32], + // wrong length + -3 => [0u8; 33], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + #[test] fn test_from_into_cose_key_ecdh() { let mut rng = ThreadRng256 {}; @@ -1359,17 +1462,7 @@ mod test { let sk = crypto::ecdsa::SecKey::gensk(&mut rng); let pk = sk.genpk(); let cose_key = CoseKey::from(pk); - let cose_map = cose_key.0; - let template = cbor_map! { - 1 => 0, - 3 => 0, - -1 => 0, - -2 => 0, - -3 => 0, - }; - for key in CoseKey::try_from(template).unwrap().0.keys() { - assert!(cose_map.contains_key(key)); - } + assert_eq!(cose_key.algorithm, ES256_ALGORITHM); } #[test] diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 676f4c2..8895605 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -534,7 +534,7 @@ where } auth_data.extend(vec![0x00, credential_id.len() as u8]); auth_data.extend(&credential_id); - if !cbor::write(cbor::Value::Map(CoseKey::from(pk).0), &mut auth_data) { + if !cbor::write(cbor::Value::from(CoseKey::from(pk)), &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } if has_extension_output { diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 3ebab3b..f40dc21 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -192,7 +192,7 @@ impl From for cbor::Value { } = client_pin_response; cbor_map_options! { - 1 => key_agreement.map(|cose_key| cbor_map_btree!(cose_key.0)), + 1 => key_agreement.map(cbor::Value::from), 2 => pin_token, 3 => retries, } From da27848c27c7a56cb6265d61ddc6eda50838f3dd Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 17:17:23 +0100 Subject: [PATCH 116/192] updates license header to 2021 in ctap --- src/ctap/apdu.rs | 2 +- src/ctap/command.rs | 2 +- src/ctap/config_command.rs | 2 +- src/ctap/ctap1.rs | 2 +- src/ctap/data_formats.rs | 2 +- src/ctap/hid/mod.rs | 2 +- src/ctap/hid/receive.rs | 2 +- src/ctap/hid/send.rs | 2 +- src/ctap/key_material.rs | 2 +- src/ctap/mod.rs | 2 +- src/ctap/pin_protocol_v1.rs | 2 +- src/ctap/response.rs | 2 +- src/ctap/status_code.rs | 2 +- src/ctap/storage.rs | 2 +- src/ctap/timed_permission.rs | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index c99bf84..f475308 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2020-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 6b6ab54..dcb96d6 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index f270513..57e2a97 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2020-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 0932e2c..0bf43b5 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 9cf149f..05b8374 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 01c0b11..71bd7c8 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/hid/receive.rs b/src/ctap/hid/receive.rs index b522837..8efdb1c 100644 --- a/src/ctap/hid/receive.rs +++ b/src/ctap/hid/receive.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/hid/send.rs b/src/ctap/hid/send.rs index 434d633..22f9c61 100644 --- a/src/ctap/hid/send.rs +++ b/src/ctap/hid/send.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index eec5456..a8ae6da 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 7b5b2de..98ed569 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index d4f148d..e2a84eb 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC +// Copyright 2020-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/response.rs b/src/ctap/response.rs index df38148..b4d4ed4 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 5a9ec71..a593dad 100644 --- a/src/ctap/status_code.rs +++ b/src/ctap/status_code.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0c6d15f..0bc9c5f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/ctap/timed_permission.rs b/src/ctap/timed_permission.rs index fcc0ada..868e9da 100644 --- a/src/ctap/timed_permission.rs +++ b/src/ctap/timed_permission.rs @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2 (the "License"); // you may not use this file except in compliance with the License. From c30268a099c53c3f3219070525ba777bbe1c7bf9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 17:57:58 +0100 Subject: [PATCH 117/192] code cleanups and clarifications --- src/ctap/config_command.rs | 9 +++++---- src/ctap/mod.rs | 11 +---------- src/ctap/pin_protocol_v1.rs | 3 --- src/ctap/storage.rs | 5 +++++ 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 57e2a97..e09bab3 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -24,7 +24,6 @@ use alloc::vec; /// Processes the subcommand setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, params: SetMinPinLengthParams, ) -> Result { let SetMinPinLengthParams { @@ -44,8 +43,10 @@ fn process_set_min_pin_length( if let Some(old_length) = persistent_store.pin_code_point_length()? { force_change_pin |= new_min_pin_length > old_length; } - pin_protocol_v1.force_pin_change |= force_change_pin; - // TODO(kaczmarczyck) actually force a PIN change + if force_change_pin { + // TODO(kaczmarczyck) actually force a PIN change in PinProtocolV1 + persistent_store.force_pin_change()?; + } persistent_store.set_min_pin_length(new_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)?; @@ -86,7 +87,7 @@ pub fn process_config( match sub_command { ConfigSubCommand::SetMinPinLength => { if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { - process_set_min_pin_length(persistent_store, pin_protocol_v1, params) + process_set_min_pin_length(persistent_store, params) } else { Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 98ed569..1d7e286 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -128,8 +128,7 @@ pub fn check_pin_uv_auth_protocol( ) -> Result<(), Ctap2StatusCode> { match pin_uv_auth_protocol { Some(PIN_PROTOCOL_VERSION) => Ok(()), - Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - None => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), } } @@ -1087,11 +1086,6 @@ mod test { auth_data[0..expected_auth_data.len()], expected_auth_data[..] ); - /*assert_eq!( - &auth_data[expected_auth_data.len() - ..expected_auth_data.len() + expected_attested_cred_data.len()], - expected_attested_cred_data - );*/ assert_eq!( &auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], expected_extension_cbor @@ -1424,9 +1418,6 @@ mod test { make_credential_params.extensions = extensions; let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); - let mut expected_attested_cred_data = - ctap_state.persistent_store.aaguid().unwrap().to_vec(); - expected_attested_cred_data.extend(&[0x00, 0x20]); check_make_response( make_credential_response, 0x41, diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index e2a84eb..eef0440 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -172,7 +172,6 @@ pub struct PinProtocolV1 { consecutive_pin_mismatches: u8, permissions: u8, permissions_rp_id: Option, - pub force_pin_change: bool, } impl PinProtocolV1 { @@ -185,7 +184,6 @@ impl PinProtocolV1 { consecutive_pin_mismatches: 0, permissions: 0, permissions_rp_id: None, - force_pin_change: false, } } @@ -530,7 +528,6 @@ impl PinProtocolV1 { consecutive_pin_mismatches: 0, permissions: 0xFF, permissions_rp_id: None, - force_pin_change: false, } } } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0bc9c5f..cf9afbc 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -544,6 +544,11 @@ impl PersistentStore { self.init(rng)?; Ok(()) } + + pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { + // TODO(kaczmarczyck) implement storage logic + Ok(()) + } } impl From for Ctap2StatusCode { From 78167282f90937a4656d1e84ddbd3b37e877abc7 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 12 Jan 2021 19:11:32 +0100 Subject: [PATCH 118/192] comment for constants --- src/ctap/config_command.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index e09bab3..19d0cc2 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -72,6 +72,7 @@ pub fn process_config( check_pin_uv_auth_protocol(pin_uv_auth_protocol) .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + // Constants are taken from the specification, section 6.11, step 4.2. let mut config_data = vec![0xFF; 32]; config_data.extend(&[0x0D, sub_command as u8]); if let Some(sub_command_params) = sub_command_params.clone() { From cc86fc274201143e5b7b213657aeac15a75af605 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 08:52:00 +0100 Subject: [PATCH 119/192] removes unused import --- src/ctap/config_command.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 19d0cc2..726634c 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -99,7 +99,6 @@ pub fn process_config( #[cfg(test)] mod test { - use super::super::command::AuthenticatorConfigParameters; use super::*; use crypto::rng256::ThreadRng256; From a26de3b720e953a2e01371539b3b77c1d7bd3e9b Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 14:00:34 +0100 Subject: [PATCH 120/192] moves constants to CoseKey --- src/ctap/data_formats.rs | 86 +++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 87bc6b4..ffe2336 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -23,16 +23,8 @@ use crypto::{ecdh, ecdsa}; #[cfg(test)] use enum_iterator::IntoEnumIterator; -// This is the algorithm specifier that is supposed to be used in a COSE key -// map in ECDH. CTAP requests -25 which represents ECDH-ES + HKDF-256 here: -// https://www.iana.org/assignments/cose/cose.xhtml#algorithms -const ECDH_ALGORITHM: i64 = -25; -// This is the identifier used for ECDSA and ECDH in OpenSSH. +// Used as the identifier for ECDSA in assertion signatures and COSE. const ES256_ALGORITHM: i64 = -7; -// The COSE key parameter behind map key 1. -const EC2_KEY_TYPE: i64 = 2; -// The COSE key parameter behind map key -1. -const P_256_CURVE: i64 = 1; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] @@ -634,6 +626,17 @@ pub struct CoseKey { algorithm: i64, } +impl CoseKey { + // This is the algorithm specifier for ECDH. + // CTAP requests -25 which represents ECDH-ES + HKDF-256 here: + // https://www.iana.org/assignments/cose/cose.xhtml#algorithms + const ECDH_ALGORITHM: i64 = -25; + // The parameter behind map key 1. + const EC2_KEY_TYPE: i64 = 2; + // The parameter behind map key -1. + const P_256_CURVE: i64 = 1; +} + // This conversion accepts both ECDH and ECDSA. impl TryFrom for CoseKey { type Error = Ctap2StatusCode; @@ -659,15 +662,15 @@ impl TryFrom for CoseKey { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } let curve = extract_integer(ok_or_missing(curve)?)?; - if curve != P_256_CURVE { + if curve != CoseKey::P_256_CURVE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } let key_type = extract_integer(ok_or_missing(key_type)?)?; - if key_type != EC2_KEY_TYPE { + if key_type != CoseKey::EC2_KEY_TYPE { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } let algorithm = extract_integer(ok_or_missing(algorithm)?)?; - if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } @@ -688,9 +691,9 @@ impl From for cbor::Value { } = cose_key; cbor_map! { - 1 => EC2_KEY_TYPE, + 1 => CoseKey::EC2_KEY_TYPE, 3 => algorithm, - -1 => P_256_CURVE, + -1 => CoseKey::P_256_CURVE, -2 => x_bytes, -3 => y_bytes, } @@ -705,7 +708,7 @@ impl From for CoseKey { CoseKey { x_bytes, y_bytes, - algorithm: ECDH_ALGORITHM, + algorithm: CoseKey::ECDH_ALGORITHM, } } } @@ -735,8 +738,8 @@ impl TryFrom for ecdh::PubKey { // Since algorithm can be used for different COSE key types, we check // whether the current type is correct for ECDH. For an OpenSSH bugfix, - // the algorithm ES256_ALGORITHM is allowed here too. - if algorithm != ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { + // the algorithm ES256_ALGORITHM is allowed here too. See #90. + if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } ecdh::PubKey::from_coordinates(&x_bytes, &y_bytes) @@ -1371,11 +1374,11 @@ mod test { #[test] fn test_from_into_cose_key_cbor() { - for algorithm in &[ECDH_ALGORITHM, ES256_ALGORITHM] { + for algorithm in &[CoseKey::ECDH_ALGORITHM, ES256_ALGORITHM] { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, + 1 => CoseKey::EC2_KEY_TYPE, 3 => algorithm, - -1 => P_256_CURVE, + -1 => CoseKey::P_256_CURVE, -2 => [0u8; 32], -3 => [0u8; 32], }; @@ -1383,12 +1386,15 @@ mod test { let created_cbor_value = cbor::Value::from(cose_key); assert_eq!(created_cbor_value, cbor_value); } + } + #[test] + fn test_cose_key_unknown_algorithm() { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, + 1 => CoseKey::EC2_KEY_TYPE, // unknown algorithm 3 => 0, - -1 => P_256_CURVE, + -1 => CoseKey::P_256_CURVE, -2 => [0u8; 32], -3 => [0u8; 32], }; @@ -1396,11 +1402,15 @@ mod test { CoseKey::try_from(cbor_value), Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) ); + } + + #[test] + fn test_cose_key_unknown_type() { let cbor_value = cbor_map! { // unknown type 1 => 0, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, -2 => [0u8; 32], -3 => [0u8; 32], }; @@ -1408,9 +1418,13 @@ mod test { CoseKey::try_from(cbor_value), Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) ); + } + + #[test] + fn test_cose_key_unknown_curve() { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, // unknown curve -1 => 0, -2 => [0u8; 32], @@ -1420,10 +1434,14 @@ mod test { CoseKey::try_from(cbor_value), Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) ); + } + + #[test] + fn test_cose_key_wrong_length_x() { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, // wrong length -2 => [0u8; 31], -3 => [0u8; 32], @@ -1432,10 +1450,14 @@ mod test { CoseKey::try_from(cbor_value), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); + } + + #[test] + fn test_cose_key_wrong_length_y() { let cbor_value = cbor_map! { - 1 => EC2_KEY_TYPE, - 3 => ECDH_ALGORITHM, - -1 => P_256_CURVE, + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, -2 => [0u8; 32], // wrong length -3 => [0u8; 33], From 3e42531011d554b1b39d3dcb66fe8536e990c7b9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 14:26:59 +0100 Subject: [PATCH 121/192] full URL --- src/ctap/data_formats.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index ffe2336..dfdf4ed 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -738,7 +738,8 @@ impl TryFrom for ecdh::PubKey { // Since algorithm can be used for different COSE key types, we check // whether the current type is correct for ECDH. For an OpenSSH bugfix, - // the algorithm ES256_ALGORITHM is allowed here too. See #90. + // the algorithm ES256_ALGORITHM is allowed here too. + // https://github.com/google/OpenSK/issues/90 if algorithm != CoseKey::ECDH_ALGORITHM && algorithm != ES256_ALGORITHM { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); } From c6726660ac09ea05766d09960f93761fe3651d6c Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 13 Jan 2021 11:22:41 +0100 Subject: [PATCH 122/192] adds the command logic for credential management --- src/ctap/command.rs | 91 ++- src/ctap/credential_management.rs | 912 ++++++++++++++++++++++++++++++ src/ctap/data_formats.rs | 148 ++++- src/ctap/mod.rs | 86 ++- src/ctap/pin_protocol_v1.rs | 20 + src/ctap/response.rs | 141 ++++- src/ctap/storage.rs | 10 +- 7 files changed, 1366 insertions(+), 42 deletions(-) create mode 100644 src/ctap/credential_management.rs diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 6f0aa72..57056a7 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -14,10 +14,10 @@ use super::data_formats::{ extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, - extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionExtensions, - GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + extract_unsigned, ok_or_missing, ClientPinSubCommand, CoseKey, CredentialManagementSubCommand, + CredentialManagementSubCommandParameters, GetAssertionExtensions, GetAssertionOptions, + MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::key_material; use super::status_code::Ctap2StatusCode; @@ -41,6 +41,7 @@ pub enum Command { AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorReset, AuthenticatorGetNextAssertion, + AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters), AuthenticatorSelection, // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands @@ -110,6 +111,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorGetNextAssertion) } + Command::AUTHENTICATOR_CREDENTIAL_MANAGEMENT => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorCredentialManagement( + AuthenticatorCredentialManagementParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_SELECTION => { // Parameters are ignored. Ok(Command::AuthenticatorSelection) @@ -388,6 +395,43 @@ impl TryFrom for AuthenticatorAttestationMaterial { } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorCredentialManagementParameters { + pub sub_command: CredentialManagementSubCommand, + pub sub_command_params: Option, + pub pin_protocol: Option, + pub pin_auth: Option>, +} + +impl TryFrom for AuthenticatorCredentialManagementParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => sub_command, + 0x02 => sub_command_params, + 0x03 => pin_protocol, + 0x04 => pin_auth, + } = extract_map(cbor_value)?; + } + + let sub_command = CredentialManagementSubCommand::try_from(ok_or_missing(sub_command)?)?; + let sub_command_params = sub_command_params + .map(CredentialManagementSubCommandParameters::try_from) + .transpose()?; + let pin_protocol = pin_protocol.map(extract_unsigned).transpose()?; + let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + + Ok(AuthenticatorCredentialManagementParameters { + sub_command, + sub_command_params, + pin_protocol, + pin_auth, + }) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct AuthenticatorVendorConfigureParameters { pub lockdown: bool, @@ -551,10 +595,10 @@ mod test { 9 => 0x03, 10 => "example.com", }; - let returned_pin_protocol_parameters = + let returned_client_pin_parameters = AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); - let expected_pin_protocol_parameters = AuthenticatorClientPinParameters { + let expected_client_pin_parameters = AuthenticatorClientPinParameters { pin_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: Some(cose_key), @@ -568,8 +612,8 @@ mod test { }; assert_eq!( - returned_pin_protocol_parameters, - expected_pin_protocol_parameters + returned_client_pin_parameters, + expected_client_pin_parameters ); } @@ -595,6 +639,37 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); } + #[test] + fn test_from_cbor_cred_management_parameters() { + let cbor_value = cbor_map! { + 1 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, + 2 => cbor_map!{ + 0x01 => vec![0x1D; 32], + }, + 3 => 1, + 4 => vec! [0x9A; 16], + }; + let returned_cred_management_parameters = + AuthenticatorCredentialManagementParameters::try_from(cbor_value).unwrap(); + + let params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(vec![0x1D; 32]), + credential_id: None, + user: None, + }; + let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, + sub_command_params: Some(params), + pin_protocol: Some(1), + pin_auth: Some(vec![0x9A; 16]), + }; + + assert_eq!( + returned_cred_management_parameters, + expected_cred_management_parameters + ); + } + #[test] fn test_deserialize_selection() { let cbor_bytes = [Command::AUTHENTICATOR_SELECTION]; diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs new file mode 100644 index 0000000..4a0d29c --- /dev/null +++ b/src/ctap/credential_management.rs @@ -0,0 +1,912 @@ +// Copyright 2020-2021 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::AuthenticatorCredentialManagementParameters; +use super::data_formats::{ + CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, + PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource, + PublicKeyCredentialUserEntity, +}; +use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; +use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use super::timed_permission::TimedPermission; +use super::{check_command_permission, StatefulCommand, STATEFUL_COMMAND_TIMEOUT_DURATION}; +use alloc::collections::BTreeSet; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::iter::FromIterator; +use crypto::sha256::Sha256; +use crypto::Hash256; +use libtock_drivers::timer::ClockValue; + +/// Generates the response for subcommands enumerating RPs. +fn enumerate_rps_response( + rp_id: Option, + total_rps: Option, +) -> Result { + let rp = rp_id.clone().map(|rp_id| PublicKeyCredentialRpEntity { + rp_id, + rp_name: None, + rp_icon: None, + }); + let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec()); + + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp, + rp_id_hash, + total_rps, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }) +} + +/// Generates the response for subcommands enumerating credentials. +fn enumerate_credentials_response( + credential: PublicKeyCredentialSource, + total_credentials: Option, +) -> Result { + let PublicKeyCredentialSource { + key_type, + credential_id, + private_key, + rp_id: _, + user_handle, + user_display_name, + cred_protect_policy, + creation_order: _, + user_name, + user_icon, + } = credential; + let user = PublicKeyCredentialUserEntity { + user_id: user_handle, + user_name, + user_display_name, + user_icon, + }; + let credential_id = PublicKeyCredentialDescriptor { + key_type, + key_id: credential_id, + transports: None, // You can set USB as a hint here. + }; + let public_key = CoseKey::from(private_key.genpk()); + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: Some(user), + credential_id: Some(credential_id), + public_key: Some(public_key), + total_credentials, + cred_protect: cred_protect_policy, + // TODO(kaczmarczyck) add when largeBlobKey is implemented + large_blob_key: None, + }) +} + +/// Processes the subcommand getCredsMetadata for CredentialManagement. +fn process_get_creds_metadata( + persistent_store: &PersistentStore, +) -> Result { + Ok(AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: Some(persistent_store.count_credentials()? as u64), + max_possible_remaining_resident_credentials_count: Some( + persistent_store.remaining_credentials()? as u64, + ), + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }) +} + +/// Processes the subcommand enumerateRPsBegin for CredentialManagement. +fn process_enumerate_rps_begin( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + let mut rp_set = BTreeSet::new(); + let mut iter_result = Ok(()); + for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { + rp_set.insert(credential.rp_id); + } + iter_result?; + let mut rp_ids = Vec::from_iter(rp_set); + let total_rps = rp_ids.len(); + + // TODO(kaczmarczyck) behaviour with empty list? + let rp_id = rp_ids.pop(); + if total_rps > 1 { + *stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + *stateful_command_type = Some(StatefulCommand::EnumerateRps(rp_ids)); + } + enumerate_rps_response(rp_id, Some(total_rps as u64)) +} + +/// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. +fn process_enumerate_rps_get_next_rp( + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + check_command_permission(stateful_command_permission, now)?; + if let Some(StatefulCommand::EnumerateRps(rp_ids)) = stateful_command_type { + let rp_id = rp_ids.pop().ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + enumerate_rps_response(Some(rp_id), None) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } +} + +/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. +fn process_enumerate_credentials_begin( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + stateful_command_type: &mut Option, + sub_command_params: CredentialManagementSubCommandParameters, + now: ClockValue, +) -> Result { + let rp_id_hash = sub_command_params + .rp_id_hash + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + let mut iter_result = Ok(()); + let iter = persistent_store.iter_credentials(&mut iter_result)?; + let mut rp_credentials: Vec = iter + .filter_map(|(key, credential)| { + let cred_rp_id_hash = Sha256::hash(credential.rp_id.as_bytes()); + if cred_rp_id_hash == rp_id_hash.as_slice() { + Some(key) + } else { + None + } + }) + .collect(); + iter_result?; + let total_credentials = rp_credentials.len(); + let current_key = rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + let credential = persistent_store.get_credential(current_key)?; + if total_credentials > 1 { + *stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + *stateful_command_type = Some(StatefulCommand::EnumerateCredentials(rp_credentials)); + } + enumerate_credentials_response(credential, Some(total_credentials as u64)) +} + +/// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement. +fn process_enumerate_credentials_get_next_credential( + persistent_store: &PersistentStore, + stateful_command_permission: &mut TimedPermission, + mut stateful_command_type: &mut Option, + now: ClockValue, +) -> Result { + check_command_permission(stateful_command_permission, now)?; + if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut stateful_command_type + { + let current_key = rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + let credential = persistent_store.get_credential(current_key)?; + enumerate_credentials_response(credential, None) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } +} + +/// Processes the subcommand deleteCredential for CredentialManagement. +fn process_delete_credential( + persistent_store: &mut PersistentStore, + sub_command_params: CredentialManagementSubCommandParameters, +) -> Result<(), Ctap2StatusCode> { + let credential_id = sub_command_params + .credential_id + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? + .key_id; + persistent_store.delete_credential(&credential_id) +} + +/// Processes the subcommand updateUserInformation for CredentialManagement. +fn process_update_user_information( + persistent_store: &mut PersistentStore, + sub_command_params: CredentialManagementSubCommandParameters, +) -> Result<(), Ctap2StatusCode> { + let credential_id = sub_command_params + .credential_id + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? + .key_id; + let user = sub_command_params + .user + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + persistent_store.update_credential(&credential_id, user) +} + +/// Checks the PIN protocol. +/// +/// TODO(#246) refactor after #246 is merged +fn pin_uv_auth_protocol_check(pin_uv_auth_protocol: Option) -> Result<(), Ctap2StatusCode> { + match pin_uv_auth_protocol { + Some(1) => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } +} + +/// Processes the CredentialManagement command and all its subcommands. +pub fn process_credential_management( + persistent_store: &mut PersistentStore, + stateful_command_permission: &mut TimedPermission, + mut stateful_command_type: &mut Option, + pin_protocol_v1: &mut PinProtocolV1, + cred_management_params: AuthenticatorCredentialManagementParameters, + now: ClockValue, +) -> Result { + let AuthenticatorCredentialManagementParameters { + sub_command, + sub_command_params, + pin_protocol, + pin_auth, + } = cred_management_params; + + match (sub_command, &mut stateful_command_type) { + ( + CredentialManagementSubCommand::EnumerateRpsGetNextRp, + Some(StatefulCommand::EnumerateRps(_)), + ) => (), + ( + CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + Some(StatefulCommand::EnumerateCredentials(_)), + ) => (), + (_, _) => { + *stateful_command_type = None; + } + } + + match sub_command { + CredentialManagementSubCommand::GetCredsMetadata + | CredentialManagementSubCommand::EnumerateRpsBegin + | CredentialManagementSubCommand::DeleteCredential + | CredentialManagementSubCommand::EnumerateCredentialsBegin + | CredentialManagementSubCommand::UpdateUserInformation => { + pin_uv_auth_protocol_check(pin_protocol)?; + persistent_store + .pin_hash()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let mut management_data = vec![sub_command as u8]; + if let Some(sub_command_params) = sub_command_params.clone() { + if !cbor::write(sub_command_params.into(), &mut management_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + } + if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + pin_protocol_v1.has_permission(PinPermission::CredentialManagement)?; + pin_protocol_v1.has_no_permission_rp_id()?; + // TODO(kaczmarczyck) sometimes allow a RP ID + } + CredentialManagementSubCommand::EnumerateRpsGetNextRp + | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} + } + + let response = match sub_command { + CredentialManagementSubCommand::GetCredsMetadata => { + Some(process_get_creds_metadata(persistent_store)?) + } + CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( + persistent_store, + stateful_command_permission, + stateful_command_type, + now, + )?), + CredentialManagementSubCommand::EnumerateRpsGetNextRp => { + Some(process_enumerate_rps_get_next_rp( + stateful_command_permission, + stateful_command_type, + now, + )?) + } + CredentialManagementSubCommand::EnumerateCredentialsBegin => { + Some(process_enumerate_credentials_begin( + persistent_store, + stateful_command_permission, + stateful_command_type, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + now, + )?) + } + CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => { + Some(process_enumerate_credentials_get_next_credential( + persistent_store, + stateful_command_permission, + stateful_command_type, + now, + )?) + } + CredentialManagementSubCommand::DeleteCredential => { + process_delete_credential( + persistent_store, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + CredentialManagementSubCommand::UpdateUserInformation => { + process_update_user_information( + persistent_store, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + }; + Ok(ResponseData::AuthenticatorCredentialManagement(response)) +} + +#[cfg(test)] +mod test { + use super::super::data_formats::PublicKeyCredentialType; + use super::super::CtapState; + use super::*; + use crypto::rng256::{Rng256, ThreadRng256}; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + + fn create_credential_source(rng: &mut impl Rng256) -> 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("example.com"), + user_handle: vec![0x01], + user_display_name: Some("display_name".to_string()), + cred_protect_policy: None, + creation_order: 0, + user_name: Some("name".to_string()), + user_icon: Some("icon".to_string()), + } + } + + #[test] + fn test_process_get_creds_metadata() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source = create_credential_source(&mut rng); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, + 0xFB, 0x00, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth: pin_auth.clone(), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let initial_capacity = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.existing_resident_credentials_count, Some(0)); + response + .max_possible_remaining_resident_credentials_count + .unwrap() + } + _ => panic!("Invalid response type"), + }; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.existing_resident_credentials_count, Some(1)); + assert_eq!( + response.max_possible_remaining_resident_credentials_count, + Some(initial_capacity - 1) + ); + } + _ => panic!("Invalid response type"), + }; + } + + #[test] + fn test_process_enumerate_rps_with_uv() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source1 = create_credential_source(&mut rng); + let mut credential_source2 = create_credential_source(&mut rng); + credential_source2.rp_id = "another.example.com".to_string(); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source1) + .unwrap(); + ctap_state + .persistent_store + .store_credential(credential_source2) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, + 0xD0, 0xD1, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let first_rp_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.total_rps, Some(2)); + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + rp_id + } + _ => panic!("Invalid response type"), + }; + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let second_rp_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert_eq!(response.total_rps, None); + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + rp_id + } + _ => panic!("Invalid response type"), + }; + + assert!(first_rp_id != second_rp_id); + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_enumerate_credentials_with_uv() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source1 = create_credential_source(&mut rng); + let mut credential_source2 = create_credential_source(&mut rng); + credential_source2.user_handle = vec![0x02]; + credential_source2.user_name = Some("user2".to_string()); + credential_source2.user_display_name = Some("User Two".to_string()); + credential_source2.user_icon = Some("icon2".to_string()); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source1) + .unwrap(); + ctap_state + .persistent_store + .store_credential(credential_source2) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28, + 0x2B, 0x68, + ]); + + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(Sha256::hash(b"example.com").to_vec()), + credential_id: None, + user: None, + }; + // RP ID hash: + // A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947 + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let first_credential_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert!(response.user.is_some()); + assert!(response.public_key.is_some()); + assert_eq!(response.total_credentials, Some(2)); + response.credential_id.unwrap().key_id + } + _ => panic!("Invalid response type"), + }; + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + let second_credential_id = match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + assert!(response.user.is_some()); + assert!(response.public_key.is_some()); + assert_eq!(response.total_credentials, None); + response.credential_id.unwrap().key_id + } + _ => panic!("Invalid response type"), + }; + + assert!(first_credential_id != second_credential_id); + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_delete_credential() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut credential_source = create_credential_source(&mut rng); + credential_source.credential_id = vec![0x1D; 32]; + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89, + 0x9C, 0x64, + ]); + + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, // You can set USB as a hint here. + }; + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: None, + credential_id: Some(credential_id), + user: None, + }; + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::DeleteCredential, + sub_command_params: Some(sub_command_params.clone()), + pin_protocol: Some(1), + pin_auth: pin_auth.clone(), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Ok(ResponseData::AuthenticatorCredentialManagement(None)) + ); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::DeleteCredential, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) + ); + } + + #[test] + fn test_process_update_user_information() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut credential_source = create_credential_source(&mut rng); + credential_source.credential_id = vec![0x1D; 32]; + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .store_credential(credential_source) + .unwrap(); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD, + 0x9D, 0xB7, + ]); + + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, // You can set USB as a hint here. + }; + let new_user = PublicKeyCredentialUserEntity { + user_id: vec![0xFF], + user_name: Some("new_name".to_string()), + user_display_name: Some("new_display_name".to_string()), + user_icon: Some("new_icon".to_string()), + }; + let sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: None, + credential_id: Some(credential_id), + user: Some(new_user), + }; + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::UpdateUserInformation, + sub_command_params: Some(sub_command_params), + pin_protocol: Some(1), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Ok(ResponseData::AuthenticatorCredentialManagement(None)) + ); + + let updated_credential = ctap_state + .persistent_store + .find_credential("example.com", &[0x1D; 32], false) + .unwrap() + .unwrap(); + assert_eq!(updated_credential.user_handle, vec![0x01]); + assert_eq!(&updated_credential.user_name.unwrap(), "new_name"); + assert_eq!( + &updated_credential.user_display_name.unwrap(), + "new_display_name" + ); + assert_eq!(&updated_credential.user_icon.unwrap(), "new_icon"); + } + + #[test] + fn test_process_credential_management_invalid_pin_protocol() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_auth = Some(vec![ + 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, + 0xFB, 0x00, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(123456), + pin_auth, + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_process_credential_management_invalid_pin_auth() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth: Some(vec![0u8; 16]), + }; + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } +} diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index dfdf4ed..bb330ef 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -27,6 +27,7 @@ use enum_iterator::IntoEnumIterator; const ES256_ALGORITHM: i64 = -7; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity +#[derive(Clone)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialRpEntity { pub rp_id: String, @@ -58,8 +59,19 @@ impl TryFrom for PublicKeyCredentialRpEntity { } } +impl From for cbor::Value { + fn from(entity: PublicKeyCredentialRpEntity) -> Self { + cbor_map_options! { + "id" => entity.rp_id, + "name" => entity.rp_name, + "icon" => entity.rp_icon, + } + } +} + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -173,7 +185,8 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum AuthenticatorTransport { Usb, @@ -210,7 +223,8 @@ impl TryFrom for AuthenticatorTransport { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct PublicKeyCredentialDescriptor { pub key_type: PublicKeyCredentialType, pub key_id: Vec, @@ -788,6 +802,88 @@ impl TryFrom for ClientPinSubCommand { } } +#[derive(Clone, Copy)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum CredentialManagementSubCommand { + GetCredsMetadata = 0x01, + EnumerateRpsBegin = 0x02, + EnumerateRpsGetNextRp = 0x03, + EnumerateCredentialsBegin = 0x04, + EnumerateCredentialsGetNextCredential = 0x05, + DeleteCredential = 0x06, + UpdateUserInformation = 0x07, +} + +impl From for cbor::Value { + fn from(subcommand: CredentialManagementSubCommand) -> Self { + (subcommand as u64).into() + } +} + +impl TryFrom for CredentialManagementSubCommand { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + let subcommand_int = extract_unsigned(cbor_value)?; + match subcommand_int { + 0x01 => Ok(CredentialManagementSubCommand::GetCredsMetadata), + 0x02 => Ok(CredentialManagementSubCommand::EnumerateRpsBegin), + 0x03 => Ok(CredentialManagementSubCommand::EnumerateRpsGetNextRp), + 0x04 => Ok(CredentialManagementSubCommand::EnumerateCredentialsBegin), + 0x05 => Ok(CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential), + 0x06 => Ok(CredentialManagementSubCommand::DeleteCredential), + 0x07 => Ok(CredentialManagementSubCommand::UpdateUserInformation), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND), + } + } +} + +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct CredentialManagementSubCommandParameters { + pub rp_id_hash: Option>, + pub credential_id: Option, + pub user: Option, +} + +impl TryFrom for CredentialManagementSubCommandParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 0x01 => rp_id_hash, + 0x02 => credential_id, + 0x03 => user, + } = extract_map(cbor_value)?; + } + + let rp_id_hash = rp_id_hash.map(extract_byte_string).transpose()?; + let credential_id = credential_id + .map(PublicKeyCredentialDescriptor::try_from) + .transpose()?; + let user = user + .map(PublicKeyCredentialUserEntity::try_from) + .transpose()?; + Ok(Self { + rp_id_hash, + credential_id, + user, + }) + } +} + +impl From for cbor::Value { + fn from(sub_command_params: CredentialManagementSubCommandParameters) -> Self { + cbor_map_options! { + 0x01 => sub_command_params.rp_id_hash, + 0x02 => sub_command_params.credential_id, + 0x03 => sub_command_params.user, + } + } +} + pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { match cbor_value { cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), @@ -1504,6 +1600,52 @@ mod test { } } + #[test] + fn test_from_into_cred_management_sub_command() { + let cbor_sub_command: cbor::Value = cbor_int!(0x01); + let sub_command = CredentialManagementSubCommand::try_from(cbor_sub_command.clone()); + let expected_sub_command = CredentialManagementSubCommand::GetCredsMetadata; + assert_eq!(sub_command, Ok(expected_sub_command)); + let created_cbor: cbor::Value = sub_command.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command); + + for command in CredentialManagementSubCommand::into_enum_iter() { + let created_cbor: cbor::Value = command.clone().into(); + let reconstructed = CredentialManagementSubCommand::try_from(created_cbor).unwrap(); + assert_eq!(command, reconstructed); + } + } + + #[test] + fn test_from_into_cred_management_sub_command_params() { + let credential_id = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x2D, 0x2D, 0x2D, 0x2D], + transports: Some(vec![AuthenticatorTransport::Usb]), + }; + let user_entity = PublicKeyCredentialUserEntity { + user_id: vec![0x1D, 0x1D, 0x1D, 0x1D], + user_name: Some("foo".to_string()), + user_display_name: Some("bar".to_string()), + user_icon: Some("example.com/foo/icon.png".to_string()), + }; + let cbor_sub_command_params = cbor_map! { + 0x01 => vec![0x1D; 32], + 0x02 => credential_id.clone(), + 0x03 => user_entity.clone(), + }; + let sub_command_params = + CredentialManagementSubCommandParameters::try_from(cbor_sub_command_params.clone()); + let expected_sub_command_params = CredentialManagementSubCommandParameters { + rp_id_hash: Some(vec![0x1D; 32]), + credential_id: Some(credential_id), + user: Some(user_entity), + }; + assert_eq!(sub_command_params, Ok(expected_sub_command_params)); + let created_cbor: cbor::Value = sub_command_params.unwrap().into(); + assert_eq!(created_cbor, cbor_sub_command_params); + } + #[test] fn test_credential_source_cbor_round_trip() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8895605..d4b73f4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -14,6 +14,7 @@ pub mod apdu; pub mod command; +mod credential_management; #[cfg(feature = "with_ctap1")] mod ctap1; pub mod data_formats; @@ -30,6 +31,7 @@ use self::command::{ AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, MAX_CREDENTIAL_COUNT_IN_LIST, }; +use self::credential_management::process_credential_management; use self::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, @@ -98,7 +100,7 @@ pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); -const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); +pub const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -139,15 +141,29 @@ struct AssertionInput { has_uv: bool, } -struct AssertionState { +pub struct AssertionState { assertion_input: AssertionInput, // Sorted by ascending order of creation, so the last element is the most recent one. next_credential_keys: Vec, } -enum StatefulCommand { +pub enum StatefulCommand { Reset, GetAssertion(AssertionState), + EnumerateRps(Vec), + EnumerateCredentials(Vec), +} + +pub fn check_command_permission( + stateful_command_permission: &mut TimedPermission, + now: ClockValue, +) -> Result<(), Ctap2StatusCode> { + *stateful_command_permission = stateful_command_permission.check_expiration(now); + if stateful_command_permission.is_granted(now) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } } // This struct currently holds all state, not only the persistent memory. The persistent members are @@ -200,15 +216,6 @@ where self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); } - fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { - self.update_command_permission(now); - if self.stateful_command_permission.is_granted(now) { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } - } - pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { if USE_SIGNATURE_COUNTER { let increment = self.rng.gen_uniform_u32x8()[0] % 8 + 1; @@ -330,6 +337,14 @@ where Command::AuthenticatorGetNextAssertion, Some(StatefulCommand::GetAssertion(_)), ) => (), + ( + Command::AuthenticatorCredentialManagement(_), + Some(StatefulCommand::EnumerateRps(_)), + ) => (), + ( + Command::AuthenticatorCredentialManagement(_), + Some(StatefulCommand::EnumerateCredentials(_)), + ) => (), (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), // GetInfo does not reset stateful commands. (Command::AuthenticatorGetInfo, _) => (), @@ -350,6 +365,16 @@ where Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid, now), + Command::AuthenticatorCredentialManagement(params) => { + process_credential_management( + &mut self.persistent_store, + &mut self.stateful_command_permission, + &mut self.stateful_command_type, + &mut self.pin_protocol_v1, + params, + now, + ) + } Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands @@ -818,7 +843,7 @@ where &mut self, now: ClockValue, ) -> Result { - self.check_command_permission(now)?; + check_command_permission(&mut self.stateful_command_permission, now)?; let (assertion_input, credential) = if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.stateful_command_type @@ -844,6 +869,7 @@ where String::from("clientPin"), self.persistent_store.pin_hash()?.is_some(), ); + options_map.insert(String::from("credMgmt"), true); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -895,7 +921,7 @@ where ) -> Result { // Resets are only possible in the first 10 seconds after booting. // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? - self.check_command_permission(now)?; + check_command_permission(&mut self.stateful_command_permission, now)?; match &self.stateful_command_type { Some(StatefulCommand::Reset) => (), _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), @@ -1053,11 +1079,12 @@ mod test { expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend( [ - 0x04, 0xA3, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x69, 0x63, 0x6C, 0x69, - 0x65, 0x6E, 0x74, 0x50, 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, - 0x08, 0x18, 0x70, 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, 0x0D, 0x04, 0x14, 0x18, 0x96, + 0x04, 0xA4, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, + 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, + 0x69, 0x6E, 0xF4, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 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, 0x0D, 0x04, 0x14, 0x18, 0x96, ] .iter(), ); @@ -2034,6 +2061,22 @@ mod test { assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); } + #[test] + fn test_process_credential_management_unknown_subcommand() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // The subcommand 0xEE does not exist. + let reponse = ctap_state.process_command( + &[0x0A, 0xA1, 0x01, 0x18, 0xEE], + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let expected_response = vec![Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND as u8]; + assert_eq!(reponse, expected_response); + } + #[test] fn test_process_unknown_command() { let mut rng = ThreadRng256 {}; @@ -2041,10 +2084,9 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // This command does not exist. - let reset_reponse = - ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + let reponse = ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8]; - assert_eq!(reset_reponse, expected_response); + assert_eq!(reponse, expected_response); } #[test] diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index b8aeb21..14c3d56 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -563,6 +563,13 @@ impl PinProtocolV1 { } } + pub fn has_no_permission_rp_id(&self) -> Result<(), Ctap2StatusCode> { + if self.permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + Ok(()) + } + 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 { @@ -1187,6 +1194,19 @@ mod test { } } + #[test] + fn test_has_no_permission_rp_id() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + assert_eq!(pin_protocol_v1.has_no_permission_rp_id(), Ok(())); + assert_eq!(pin_protocol_v1.permissions_rp_id, None,); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_permission_rp_id(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_has_permission_for_rp_id() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/response.rs b/src/ctap/response.rs index f40dc21..8d5386b 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -14,7 +14,8 @@ use super::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialUserEntity, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; use alloc::collections::BTreeMap; use alloc::string::String; @@ -30,6 +31,7 @@ pub enum ResponseData { AuthenticatorGetInfo(AuthenticatorGetInfoResponse), AuthenticatorClientPin(Option), AuthenticatorReset, + AuthenticatorCredentialManagement(Option), AuthenticatorSelection, AuthenticatorVendor(AuthenticatorVendorResponse), } @@ -41,9 +43,9 @@ impl From for Option { ResponseData::AuthenticatorGetAssertion(data) => Some(data.into()), ResponseData::AuthenticatorGetNextAssertion(data) => Some(data.into()), ResponseData::AuthenticatorGetInfo(data) => Some(data.into()), - ResponseData::AuthenticatorClientPin(Some(data)) => Some(data.into()), - ResponseData::AuthenticatorClientPin(None) => None, + ResponseData::AuthenticatorClientPin(data) => data.map(|d| d.into()), ResponseData::AuthenticatorReset => None, + ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()), ResponseData::AuthenticatorSelection => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } @@ -199,6 +201,54 @@ impl From for cbor::Value { } } +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub struct AuthenticatorCredentialManagementResponse { + pub existing_resident_credentials_count: Option, + pub max_possible_remaining_resident_credentials_count: Option, + pub rp: Option, + pub rp_id_hash: Option>, + pub total_rps: Option, + pub user: Option, + pub credential_id: Option, + pub public_key: Option, + pub total_credentials: Option, + pub cred_protect: Option, + pub large_blob_key: Option>, +} + +impl From for cbor::Value { + fn from(cred_management_response: AuthenticatorCredentialManagementResponse) -> Self { + let AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count, + max_possible_remaining_resident_credentials_count, + rp, + rp_id_hash, + total_rps, + user, + credential_id, + public_key, + total_credentials, + cred_protect, + large_blob_key, + } = cred_management_response; + + cbor_map_options! { + 0x01 => existing_resident_credentials_count, + 0x02 => max_possible_remaining_resident_credentials_count, + 0x03 => rp, + 0x04 => rp_id_hash, + 0x05 => total_rps, + 0x06 => user, + 0x07 => credential_id, + 0x08 => public_key.map(cbor::Value::from), + 0x09 => total_credentials, + 0x0A => cred_protect, + 0x0B => large_blob_key, + } + } +} + #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub struct AuthenticatorVendorResponse { @@ -222,10 +272,11 @@ impl From for cbor::Value { #[cfg(test)] mod test { - use super::super::data_formats::PackedAttestationStatement; + use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType}; use super::super::ES256_CRED_PARAM; use super::*; use cbor::{cbor_bytes, cbor_map}; + use crypto::rng256::ThreadRng256; #[test] fn test_make_credential_into_cbor() { @@ -379,6 +430,88 @@ mod test { assert_eq!(response_cbor, None); } + #[test] + fn test_used_credential_management_into_cbor() { + let cred_management_response = AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: None, + max_possible_remaining_resident_credentials_count: None, + rp: None, + rp_id_hash: None, + total_rps: None, + user: None, + credential_id: None, + public_key: None, + total_credentials: None, + cred_protect: None, + large_blob_key: None, + }; + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); + let expected_cbor = cbor_map_options! {}; + assert_eq!(response_cbor, Some(expected_cbor)); + } + + #[test] + fn test_used_credential_management_optionals_into_cbor() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let rp = PublicKeyCredentialRpEntity { + rp_id: String::from("example.com"), + rp_name: None, + rp_icon: None, + }; + let user = PublicKeyCredentialUserEntity { + user_id: vec![0xFA, 0xB1, 0xA2], + user_name: None, + user_display_name: None, + user_icon: None, + }; + let cred_descriptor = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x1D; 32], + transports: None, + }; + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + + let cred_management_response = AuthenticatorCredentialManagementResponse { + existing_resident_credentials_count: Some(100), + max_possible_remaining_resident_credentials_count: Some(96), + rp: Some(rp.clone()), + rp_id_hash: Some(vec![0x1D; 32]), + total_rps: Some(3), + user: Some(user.clone()), + credential_id: Some(cred_descriptor.clone()), + public_key: Some(cose_key.clone()), + total_credentials: Some(2), + cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional), + large_blob_key: Some(vec![0xBB; 64]), + }; + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); + let expected_cbor = cbor_map_options! { + 0x01 => 100, + 0x02 => 96, + 0x03 => rp, + 0x04 => vec![0x1D; 32], + 0x05 => 3, + 0x06 => user, + 0x07 => cred_descriptor, + 0x08 => cbor::Value::from(cose_key), + 0x09 => 2, + 0x0A => 0x01, + 0x0B => vec![0xBB; 64], + }; + assert_eq!(response_cbor, Some(expected_cbor)); + } + + #[test] + fn test_empty_credential_management_into_cbor() { + let response_cbor: Option = + ResponseData::AuthenticatorCredentialManagement(None).into(); + assert_eq!(response_cbor, None); + } + #[test] fn test_selection_into_cbor() { let response_cbor: Option = ResponseData::AuthenticatorSelection.into(); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 8df987d..7902f34 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -238,7 +238,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - pub fn _delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { + pub fn delete_credential(&mut self, credential_id: &[u8]) -> Result<(), Ctap2StatusCode> { let (key, _) = self.find_credential_item(credential_id)?; Ok(self.store.remove(key)?) } @@ -248,7 +248,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - pub fn _update_credential( + pub fn update_credential( &mut self, credential_id: &[u8], user: PublicKeyCredentialUserEntity, @@ -692,7 +692,7 @@ mod test { } let mut count = persistent_store.count_credentials().unwrap(); for credential_id in credential_ids { - assert!(persistent_store._delete_credential(&credential_id).is_ok()); + assert!(persistent_store.delete_credential(&credential_id).is_ok()); count -= 1; assert_eq!(persistent_store.count_credentials().unwrap(), count); } @@ -710,7 +710,7 @@ mod test { user_icon: Some("icon".to_string()), }; assert_eq!( - persistent_store._update_credential(&[0x1D], user.clone()), + persistent_store.update_credential(&[0x1D], user.clone()), Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS) ); @@ -725,7 +725,7 @@ mod test { assert_eq!(stored_credential.user_display_name, None); assert_eq!(stored_credential.user_icon, None); assert!(persistent_store - ._update_credential(&credential_id, user.clone()) + .update_credential(&credential_id, user.clone()) .is_ok()); let stored_credential = persistent_store .find_credential("example.com", &credential_id, false) From a17ee39bb6e4f3ae922baf7373ab95db40e28e2b Mon Sep 17 00:00:00 2001 From: Geoffrey Date: Thu, 14 Jan 2021 19:10:42 +0800 Subject: [PATCH 123/192] Add Feitian OpenSK USB Dongle (#257) Co-authored-by: superskybird --- docs/install.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/install.md b/docs/install.md index d00b991..166d5b5 100644 --- a/docs/install.md +++ b/docs/install.md @@ -17,6 +17,7 @@ You will need one the following supported boards: * [Nordic nRF52840 Dongle](https://www.nordicsemi.com/Software-and-tools/Development-Kits/nRF52840-Dongle) to have a more practical form factor. * [Makerdiary nRF52840-MDK USB dongle](https://wiki.makerdiary.com/nrf52840-mdk/). +* [Feitian OpenSK dongle](https://feitiantech.github.io/OpenSK_USB/). In the case of the Nordic USB dongle, you may also need the following extra hardware: From 182afc7c3f76b2a185f5536ce9d2c1d131305261 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Thu, 14 Jan 2021 12:33:03 +0100 Subject: [PATCH 124/192] Add Feitian OpenSK USB Dongle (#257) (#259) Co-authored-by: superskybird Co-authored-by: Geoffrey Co-authored-by: superskybird --- docs/install.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/install.md b/docs/install.md index d00b991..166d5b5 100644 --- a/docs/install.md +++ b/docs/install.md @@ -17,6 +17,7 @@ You will need one the following supported boards: * [Nordic nRF52840 Dongle](https://www.nordicsemi.com/Software-and-tools/Development-Kits/nRF52840-Dongle) to have a more practical form factor. * [Makerdiary nRF52840-MDK USB dongle](https://wiki.makerdiary.com/nrf52840-mdk/). +* [Feitian OpenSK dongle](https://feitiantech.github.io/OpenSK_USB/). In the case of the Nordic USB dongle, you may also need the following extra hardware: From 0bb6ee32fc96d32faa45a64127c7ae806a159019 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 14 Jan 2021 16:45:38 +0100 Subject: [PATCH 125/192] removes unused duplicate PIN protocol check helper --- src/ctap/credential_management.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index dfc004e..c83c955 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -23,7 +23,10 @@ use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; use super::timed_permission::TimedPermission; -use super::{check_command_permission, StatefulCommand, STATEFUL_COMMAND_TIMEOUT_DURATION}; +use super::{ + check_command_permission, check_pin_uv_auth_protocol, StatefulCommand, + STATEFUL_COMMAND_TIMEOUT_DURATION, +}; use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; @@ -251,16 +254,6 @@ fn process_update_user_information( persistent_store.update_credential(&credential_id, user) } -/// Checks the PIN protocol. -/// -/// TODO(#246) refactor after #246 is merged -fn pin_uv_auth_protocol_check(pin_uv_auth_protocol: Option) -> Result<(), Ctap2StatusCode> { - match pin_uv_auth_protocol { - Some(1) => Ok(()), - _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), - } -} - /// Processes the CredentialManagement command and all its subcommands. pub fn process_credential_management( persistent_store: &mut PersistentStore, @@ -297,7 +290,7 @@ pub fn process_credential_management( | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::EnumerateCredentialsBegin | CredentialManagementSubCommand::UpdateUserInformation => { - pin_uv_auth_protocol_check(pin_protocol)?; + check_pin_uv_auth_protocol(pin_protocol)?; persistent_store .pin_hash()? .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; From 7268a9474b09fb2aa1c710e1b2769bcd9ab8044f Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 18 Dec 2020 12:04:05 +0100 Subject: [PATCH 126/192] renames residential to resident --- src/ctap/mod.rs | 10 +++++----- src/ctap/storage.rs | 38 +++++++++++++++++--------------------- src/ctap/storage/key.rs | 8 ++++---- 3 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d98f09d..fc8324c 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1195,7 +1195,7 @@ mod test { } #[test] - fn test_residential_process_make_credential() { + fn test_resident_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -1214,7 +1214,7 @@ mod test { } #[test] - fn test_non_residential_process_make_credential() { + fn test_non_resident_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -1526,7 +1526,7 @@ mod test { } #[test] - fn test_residential_process_get_assertion() { + fn test_resident_process_get_assertion() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -1629,7 +1629,7 @@ mod test { } #[test] - fn test_residential_process_get_assertion_hmac_secret() { + fn test_resident_process_get_assertion_hmac_secret() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); @@ -1681,7 +1681,7 @@ mod test { } #[test] - fn test_residential_process_get_assertion_with_cred_protect() { + fn test_resident_process_get_assertion_with_cred_protect() { let mut rng = ThreadRng256 {}; let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let credential_id = rng.gen_uniform_u8x32().to_vec(); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index cf9afbc..022a3c5 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -38,11 +38,11 @@ use crypto::rng256::Rng256; // number of pages. This may improve in the future. Currently, using 20 pages gives between 20ms and // 240ms per operation. The rule of thumb is between 1ms and 12ms per additional page. // -// Limiting the number of residential keys permits to ensure a minimum number of counter increments. +// Limiting the number of resident 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) +// - K the maximum number of resident keys (MAX_SUPPORTED_RESIDENT_KEYS) +// - S the maximum size of a resident key (about 500) // - C the number of erase cycles (10000) // - I the minimum number of counter increments // @@ -50,7 +50,7 @@ use crypto::rng256::Rng256; // // With P=20 and K=150, we have I=2M which is enough for 500 increments per day for 10 years. const NUM_PAGES: usize = 20; -const MAX_SUPPORTED_RESIDENTIAL_KEYS: usize = 150; +const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150; const MAX_PIN_RETRIES: u8 = 8; const DEFAULT_MIN_PIN_LENGTH: u8 = 4; @@ -132,7 +132,7 @@ impl PersistentStore { /// Returns `CTAP2_ERR_VENDOR_INTERNAL_ERROR` if the key does not hold a valid credential. pub fn get_credential(&self, key: usize) -> Result { let min_key = key::CREDENTIALS.start; - if key < min_key || key >= min_key + MAX_SUPPORTED_RESIDENTIAL_KEYS { + if key < min_key || key >= min_key + MAX_SUPPORTED_RESIDENT_KEYS { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } let credential_entry = self @@ -200,13 +200,11 @@ impl PersistentStore { let mut old_key = None; let min_key = key::CREDENTIALS.start; // Holds whether a key is used (indices are shifted by min_key). - let mut keys = vec![false; MAX_SUPPORTED_RESIDENTIAL_KEYS]; + let mut keys = vec![false; MAX_SUPPORTED_RESIDENT_KEYS]; let mut iter_result = Ok(()); let iter = self.iter_credentials(&mut iter_result)?; for (key, credential) in iter { - if key < min_key - || key - min_key >= MAX_SUPPORTED_RESIDENTIAL_KEYS - || keys[key - min_key] + if key < min_key || key - min_key >= MAX_SUPPORTED_RESIDENT_KEYS || keys[key - min_key] { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } @@ -221,16 +219,14 @@ impl PersistentStore { } } iter_result?; - if old_key.is_none() - && keys.iter().filter(|&&x| x).count() >= MAX_SUPPORTED_RESIDENTIAL_KEYS - { + if old_key.is_none() && keys.iter().filter(|&&x| x).count() >= MAX_SUPPORTED_RESIDENT_KEYS { return Err(Ctap2StatusCode::CTAP2_ERR_KEY_STORE_FULL); } let key = match old_key { // This is a new credential being added, we need to allocate a free key. We choose the // first available key. None => key::CREDENTIALS - .take(MAX_SUPPORTED_RESIDENTIAL_KEYS) + .take(MAX_SUPPORTED_RESIDENT_KEYS) .find(|key| !keys[key - min_key]) .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?, // This is an existing credential being updated, we reuse its key. @@ -280,7 +276,7 @@ impl PersistentStore { /// Returns the estimated number of credentials that can still be stored. pub fn remaining_credentials(&self) -> Result { - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS .checked_sub(self.count_credentials()?) .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } @@ -714,7 +710,7 @@ mod test { assert_eq!(persistent_store.count_credentials().unwrap(), 0); let mut credential_ids = vec![]; - for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { let user_handle = i.to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); credential_ids.push(credential_source.credential_id.clone()); @@ -788,7 +784,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!(persistent_store.count_credentials().unwrap(), 0); - for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { let user_handle = i.to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); @@ -797,7 +793,7 @@ mod test { let credential_source = create_credential_source( &mut rng, "example.com", - vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], + vec![MAX_SUPPORTED_RESIDENT_KEYS as u8], ); assert_eq!( persistent_store.store_credential(credential_source), @@ -805,7 +801,7 @@ mod test { ); assert_eq!( persistent_store.count_credentials().unwrap(), - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS ); } @@ -837,7 +833,7 @@ mod test { .is_some()); let mut persistent_store = PersistentStore::new(&mut rng); - for i in 0..MAX_SUPPORTED_RESIDENTIAL_KEYS { + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { let user_handle = i.to_ne_bytes().to_vec(); let credential_source = create_credential_source(&mut rng, "example.com", user_handle); assert!(persistent_store.store_credential(credential_source).is_ok()); @@ -846,7 +842,7 @@ mod test { let credential_source = create_credential_source( &mut rng, "example.com", - vec![MAX_SUPPORTED_RESIDENTIAL_KEYS as u8], + vec![MAX_SUPPORTED_RESIDENT_KEYS as u8], ); assert_eq!( persistent_store.store_credential(credential_source), @@ -854,7 +850,7 @@ mod test { ); assert_eq!( persistent_store.count_credentials().unwrap(), - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS ); } diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index dfe44fc..c6e46e2 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -84,8 +84,8 @@ make_partition! { /// The credentials. /// - /// Depending on `MAX_SUPPORTED_RESIDENTIAL_KEYS`, only a prefix of those keys is used. Each - /// board may configure `MAX_SUPPORTED_RESIDENTIAL_KEYS` depending on the storage size. + /// Depending on `MAX_SUPPORTED_RESIDENT_KEYS`, only a prefix of those keys is used. Each + /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; /// The secret of the CredRandom feature. @@ -127,8 +127,8 @@ mod test { #[test] fn enough_credentials() { - use super::super::MAX_SUPPORTED_RESIDENTIAL_KEYS; - assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS <= CREDENTIALS.end - CREDENTIALS.start); + use super::super::MAX_SUPPORTED_RESIDENT_KEYS; + assert!(MAX_SUPPORTED_RESIDENT_KEYS <= CREDENTIALS.end - CREDENTIALS.start); } #[test] From 69bdd8c615934be5b4526950cb4d02099058597d Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 14 Jan 2021 18:05:38 +0100 Subject: [PATCH 127/192] renames to resident in README --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 48f6c6e..0691fb8 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,7 @@ a few things you can personalize: check [WebAuthn](https://www.w3.org/TR/webauthn/#signature-counter) for documentation. 1. Depending on your available flash storage, choose an appropriate maximum - number of supported residential keys and number of pages in - `ctap/storage.rs`. + number of supported resident keys and number of pages in `ctap/storage.rs`. 1. Change the default level for the credProtect extension in `ctap/mod.rs`. When changing the default, resident credentials become undiscoverable without user verification. This helps privacy, but can make usage less comfortable From 3702b61ce7c19dde35a5d9b74e2f9a61b3b0c972 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 15 Jan 2021 17:41:16 +0100 Subject: [PATCH 128/192] implements Default for Response type --- src/ctap/credential_management.rs | 25 +++---------------------- src/ctap/response.rs | 15 ++------------- 2 files changed, 5 insertions(+), 35 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index c83c955..71c1e39 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -49,17 +49,10 @@ fn enumerate_rps_response( let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec()); Ok(AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, rp, rp_id_hash, total_rps, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, + ..Default::default() }) } @@ -93,11 +86,6 @@ fn enumerate_credentials_response( }; let public_key = CoseKey::from(private_key.genpk()); Ok(AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, - rp: None, - rp_id_hash: None, - total_rps: None, user: Some(user), credential_id: Some(credential_id), public_key: Some(public_key), @@ -105,6 +93,7 @@ fn enumerate_credentials_response( cred_protect: cred_protect_policy, // TODO(kaczmarczyck) add when largeBlobKey is implemented large_blob_key: None, + ..Default::default() }) } @@ -117,15 +106,7 @@ fn process_get_creds_metadata( max_possible_remaining_resident_credentials_count: Some( persistent_store.remaining_credentials()? as u64, ), - rp: None, - rp_id_hash: None, - total_rps: None, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, + ..Default::default() }) } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 03bc70d..e4cda5e 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -204,6 +204,7 @@ impl From for cbor::Value { } } +#[derive(Default)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] pub struct AuthenticatorCredentialManagementResponse { @@ -435,19 +436,7 @@ mod test { #[test] fn test_used_credential_management_into_cbor() { - let cred_management_response = AuthenticatorCredentialManagementResponse { - existing_resident_credentials_count: None, - max_possible_remaining_resident_credentials_count: None, - rp: None, - rp_id_hash: None, - total_rps: None, - user: None, - credential_id: None, - public_key: None, - total_credentials: None, - cred_protect: None, - large_blob_key: None, - }; + let cred_management_response = AuthenticatorCredentialManagementResponse::default(); let response_cbor: Option = ResponseData::AuthenticatorCredentialManagement(Some(cred_management_response)).into(); let expected_cbor = cbor_map_options! {}; From 55038cc084bedb493cd7d3a901a0c7b7ff22199f Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 18 Jan 2021 16:13:01 +0100 Subject: [PATCH 129/192] Add bound-test in addition to equality-test --- libraries/persistent_store/src/format.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index 8de88e4..9b5631b 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -1080,9 +1080,12 @@ mod tests { #[test] fn position_offsets_fit_in_a_halfword() { - // The store stores the entry positions as their offset from the head. Those offsets are - // represented as u16. The bound below is a large over-approximation of the maximal offset - // but it already fits. - assert_eq!((MAX_PAGE_INDEX + 1) * MAX_VIRT_PAGE_SIZE, 0xff80); + // The store stores in RAM the entry positions as their offset from the head. Those offsets + // are represented as u16. The bound below is a large over-approximation of the maximal + // offset. We first make sure it fits in a u16. + const MAX_POS: Nat = (MAX_PAGE_INDEX + 1) * MAX_VIRT_PAGE_SIZE; + assert!(MAX_POS <= u16::MAX as Nat); + // We also check the actual value for up-to-date documentation, since it's a constant. + assert_eq!(MAX_POS, 0xff80); } } From a712d1476b373ed6295115d464f8bc357aba68f5 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 16 Dec 2020 19:40:55 +0100 Subject: [PATCH 130/192] Return error instead of debug assert With dirty storage we hit the assert. Returning an error permits to continue to catch if the invariant is broken for normal operation while being able to continue fuzzing with dirty storage. --- libraries/persistent_store/src/format.rs | 56 +++++++++---------- .../persistent_store/src/format/bitfield.rs | 29 ++++++---- libraries/persistent_store/src/store.rs | 36 +++++++----- 3 files changed, 67 insertions(+), 54 deletions(-) diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index 9b5631b..f575750 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -335,12 +335,12 @@ impl Format { } /// Builds the storage representation of an init info. - pub fn build_init(&self, init: InitInfo) -> WordSlice { + pub fn build_init(&self, init: InitInfo) -> StoreResult { let mut word = ERASED_WORD; - INIT_CYCLE.set(&mut word, init.cycle); - INIT_PREFIX.set(&mut word, init.prefix); - WORD_CHECKSUM.set(&mut word, 0); - word.as_slice() + INIT_CYCLE.set(&mut word, init.cycle)?; + INIT_PREFIX.set(&mut word, init.prefix)?; + WORD_CHECKSUM.set(&mut word, 0)?; + Ok(word.as_slice()) } /// Returns the storage index of the compact info of a page. @@ -368,36 +368,36 @@ impl Format { } /// Builds the storage representation of a compact info. - pub fn build_compact(&self, compact: CompactInfo) -> WordSlice { + pub fn build_compact(&self, compact: CompactInfo) -> StoreResult { let mut word = ERASED_WORD; - COMPACT_TAIL.set(&mut word, compact.tail); - WORD_CHECKSUM.set(&mut word, 0); - word.as_slice() + COMPACT_TAIL.set(&mut word, compact.tail)?; + WORD_CHECKSUM.set(&mut word, 0)?; + Ok(word.as_slice()) } /// Builds the storage representation of an internal entry. - pub fn build_internal(&self, internal: InternalEntry) -> WordSlice { + pub fn build_internal(&self, internal: InternalEntry) -> StoreResult { let mut word = ERASED_WORD; match internal { InternalEntry::Erase { page } => { - ID_ERASE.set(&mut word); - ERASE_PAGE.set(&mut word, page); + ID_ERASE.set(&mut word)?; + ERASE_PAGE.set(&mut word, page)?; } InternalEntry::Clear { min_key } => { - ID_CLEAR.set(&mut word); - CLEAR_MIN_KEY.set(&mut word, min_key); + ID_CLEAR.set(&mut word)?; + CLEAR_MIN_KEY.set(&mut word, min_key)?; } InternalEntry::Marker { count } => { - ID_MARKER.set(&mut word); - MARKER_COUNT.set(&mut word, count); + ID_MARKER.set(&mut word)?; + MARKER_COUNT.set(&mut word, count)?; } InternalEntry::Remove { key } => { - ID_REMOVE.set(&mut word); - REMOVE_KEY.set(&mut word, key); + ID_REMOVE.set(&mut word)?; + REMOVE_KEY.set(&mut word, key)?; } } - WORD_CHECKSUM.set(&mut word, 0); - word.as_slice() + WORD_CHECKSUM.set(&mut word, 0)?; + Ok(word.as_slice()) } /// Parses the first word of an entry from its storage representation. @@ -459,31 +459,31 @@ impl Format { } /// Builds the storage representation of a user entry. - pub fn build_user(&self, key: Nat, value: &[u8]) -> Vec { + pub fn build_user(&self, key: Nat, value: &[u8]) -> StoreResult> { let length = usize_to_nat(value.len()); let word_size = self.word_size(); let footer = self.bytes_to_words(length); let mut result = vec![0xff; ((1 + footer) * word_size) as usize]; result[word_size as usize..][..length as usize].copy_from_slice(value); let mut word = ERASED_WORD; - ID_HEADER.set(&mut word); + ID_HEADER.set(&mut word)?; if footer > 0 && is_erased(&result[(footer * word_size) as usize..]) { HEADER_FLIPPED.set(&mut word); *result.last_mut().unwrap() = 0x7f; } - HEADER_LENGTH.set(&mut word, length); - HEADER_KEY.set(&mut word, key); + HEADER_LENGTH.set(&mut word, length)?; + HEADER_KEY.set(&mut word, key)?; HEADER_CHECKSUM.set( &mut word, count_zeros(&result[(footer * word_size) as usize..]), - ); + )?; result[..word_size as usize].copy_from_slice(&word.as_slice()); - result + Ok(result) } /// Sets the padding bit in the first word of a user entry. - pub fn set_padding(&self, word: &mut Word) { - ID_PADDING.set(word); + pub fn set_padding(&self, word: &mut Word) -> StoreResult<()> { + ID_PADDING.set(word) } /// Sets the deleted bit in the first word of a user entry. diff --git a/libraries/persistent_store/src/format/bitfield.rs b/libraries/persistent_store/src/format/bitfield.rs index 2cffc4b..32c0ae5 100644 --- a/libraries/persistent_store/src/format/bitfield.rs +++ b/libraries/persistent_store/src/format/bitfield.rs @@ -42,15 +42,20 @@ impl Field { /// Sets the value of a bit field. /// - /// # Preconditions + /// # Errors /// /// - The value must fit in the bit field: `num_bits(value) < self.len`. /// - The value must only change bits from 1 to 0: `self.get(*word) & value == value`. - pub fn set(&self, word: &mut Word, value: Nat) { - debug_assert_eq!(value & self.mask(), value); + pub fn set(&self, word: &mut Word, value: Nat) -> StoreResult<()> { + if value & self.mask() != value { + return Err(StoreError::InvalidStorage); + } let mask = !(self.mask() << self.pos); word.0 &= mask | (value << self.pos); - debug_assert_eq!(self.get(*word), value); + if self.get(*word) != value { + return Err(StoreError::InvalidStorage); + } + Ok(()) } /// Returns a bit mask the length of the bit field. @@ -82,8 +87,8 @@ impl ConstField { } /// Sets the bit field to its value. - pub fn set(&self, word: &mut Word) { - self.field.set(word, self.value); + pub fn set(&self, word: &mut Word) -> StoreResult<()> { + self.field.set(word, self.value) } } @@ -135,15 +140,15 @@ impl Checksum { /// Sets the checksum to the external increment value. /// - /// # Preconditions + /// # Errors /// /// - The bits of the checksum bit field should be set to one: `self.field.get(*word) == /// self.field.mask()`. /// - The checksum value should fit in the checksum bit field: `num_bits(word.count_zeros() + /// value) < self.field.len`. - pub fn set(&self, word: &mut Word, value: Nat) { + pub fn set(&self, word: &mut Word, value: Nat) -> StoreResult<()> { debug_assert_eq!(self.field.get(*word), self.field.mask()); - self.field.set(word, word.0.count_zeros() + value); + self.field.set(word, word.0.count_zeros() + value) } } @@ -290,7 +295,7 @@ mod tests { assert_eq!(field.get(Word(0x000000f8)), 0x1f); assert_eq!(field.get(Word(0x0000ff37)), 6); let mut word = Word(0xffffffff); - field.set(&mut word, 3); + field.set(&mut word, 3).unwrap(); assert_eq!(word, Word(0xffffff1f)); } @@ -305,7 +310,7 @@ mod tests { assert!(field.check(Word(0x00000048))); assert!(field.check(Word(0x0000ff4f))); let mut word = Word(0xffffffff); - field.set(&mut word); + field.set(&mut word).unwrap(); assert_eq!(word, Word(0xffffff4f)); } @@ -333,7 +338,7 @@ mod tests { assert_eq!(field.get(Word(0x00ffff67)), Ok(4)); assert_eq!(field.get(Word(0x7fffff07)), Err(StoreError::InvalidStorage)); let mut word = Word(0x0fffffff); - field.set(&mut word, 4); + field.set(&mut word, 4).unwrap(); assert_eq!(word, Word(0x0fffff47)); } diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index bc4258a..f707a89 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -300,7 +300,9 @@ impl Store { self.reserve(self.format.transaction_capacity(updates))?; // Write the marker entry. let marker = self.tail()?; - let entry = self.format.build_internal(InternalEntry::Marker { count }); + let entry = self + .format + .build_internal(InternalEntry::Marker { count })?; self.write_slice(marker, &entry)?; self.init_page(marker, marker)?; // Write the updates. @@ -308,7 +310,7 @@ impl Store { for update in updates { let length = match *update { StoreUpdate::Insert { key, ref value } => { - let entry = self.format.build_user(usize_to_nat(key), value); + let entry = self.format.build_user(usize_to_nat(key), value)?; let word_size = self.format.word_size(); let footer = usize_to_nat(entry.len()) / word_size - 1; self.write_slice(tail, &entry[..(footer * word_size) as usize])?; @@ -317,7 +319,7 @@ impl Store { } StoreUpdate::Remove { key } => { let key = usize_to_nat(key); - let remove = self.format.build_internal(InternalEntry::Remove { key }); + let remove = self.format.build_internal(InternalEntry::Remove { key })?; self.write_slice(tail, &remove)?; 0 } @@ -337,7 +339,9 @@ impl Store { if min_key > self.format.max_key() { return Err(StoreError::InvalidArgument); } - let clear = self.format.build_internal(InternalEntry::Clear { min_key }); + let clear = self + .format + .build_internal(InternalEntry::Clear { min_key })?; // We always have one word available. We can't use `reserve` because this is internal // capacity, not user capacity. while self.immediate_capacity()? < 1 { @@ -403,7 +407,7 @@ impl Store { if key > self.format.max_key() || value_len > self.format.max_value_len() { return Err(StoreError::InvalidArgument); } - let entry = self.format.build_user(key, value); + let entry = self.format.build_user(key, value)?; let entry_len = usize_to_nat(entry.len()); self.reserve(entry_len / self.format.word_size())?; let tail = self.tail()?; @@ -469,7 +473,7 @@ impl Store { let init_info = self.format.build_init(InitInfo { cycle: 0, prefix: 0, - }); + })?; self.storage_write_slice(index, &init_info) } @@ -681,7 +685,9 @@ impl Store { } let tail = max(self.tail()?, head.next_page(&self.format)); let index = self.format.index_compact(head.page(&self.format)); - let compact_info = self.format.build_compact(CompactInfo { tail: tail - head }); + let compact_info = self + .format + .build_compact(CompactInfo { tail: tail - head })?; self.storage_write_slice(index, &compact_info)?; self.compact_copy() } @@ -721,7 +727,7 @@ impl Store { self.init_page(tail, tail + (length - 1))?; tail += length; } - let erase = self.format.build_internal(InternalEntry::Erase { page }); + let erase = self.format.build_internal(InternalEntry::Erase { page })?; self.write_slice(tail, &erase)?; self.init_page(tail, tail)?; self.compact_erase(tail) @@ -851,7 +857,7 @@ impl Store { let init_info = self.format.build_init(InitInfo { cycle: new_first.cycle(&self.format), prefix: new_first.word(&self.format), - }); + })?; self.storage_write_slice(index, &init_info)?; Ok(()) } @@ -859,7 +865,7 @@ impl Store { /// Sets the padding bit of a user header. fn set_padding(&mut self, pos: Position) -> StoreResult<()> { let mut word = Word::from_slice(self.read_word(pos)); - self.format.set_padding(&mut word); + self.format.set_padding(&mut word)?; self.write_slice(pos, &word.as_slice())?; Ok(()) } @@ -1195,10 +1201,12 @@ impl Store { let format = Format::new(storage).unwrap(); // Write the init info of the first page. let mut index = format.index_init(0); - let init_info = format.build_init(InitInfo { - cycle: usize_to_nat(cycle), - prefix: 0, - }); + let init_info = format + .build_init(InitInfo { + cycle: usize_to_nat(cycle), + prefix: 0, + }) + .unwrap(); storage.write_slice(index, &init_info).unwrap(); // Pad the first word of the page. This makes the store looks used, otherwise we may confuse // it with a partially initialized store. From e3353cb232e2892de0df43e6a04a8d4a2d376b6c Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 19 Jan 2021 12:42:41 +0100 Subject: [PATCH 131/192] only stores the RP ID index as state --- src/ctap/credential_management.rs | 128 ++++++++++++++++++++++++++---- src/ctap/mod.rs | 2 +- 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 71c1e39..dba7d36 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -31,11 +31,23 @@ use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use core::iter::FromIterator; use crypto::sha256::Sha256; use crypto::Hash256; use libtock_drivers::timer::ClockValue; +/// Generates a set with all existing RP IDs. +fn get_stored_rp_ids( + persistent_store: &PersistentStore, +) -> Result, Ctap2StatusCode> { + let mut rp_set = BTreeSet::new(); + let mut iter_result = Ok(()); + for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { + rp_set.insert(credential.rp_id); + } + iter_result?; + Ok(rp_set) +} + /// Generates the response for subcommands enumerating RPs. fn enumerate_rps_response( rp_id: Option, @@ -117,34 +129,35 @@ fn process_enumerate_rps_begin( stateful_command_type: &mut Option, now: ClockValue, ) -> Result { - let mut rp_set = BTreeSet::new(); - let mut iter_result = Ok(()); - for (_, credential) in persistent_store.iter_credentials(&mut iter_result)? { - rp_set.insert(credential.rp_id); - } - iter_result?; - let mut rp_ids = Vec::from_iter(rp_set); - let total_rps = rp_ids.len(); + let rp_set = get_stored_rp_ids(persistent_store)?; + let total_rps = rp_set.len(); - // TODO(kaczmarczyck) behaviour with empty list? - let rp_id = rp_ids.pop(); + // TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty? if total_rps > 1 { *stateful_command_permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateRps(rp_ids)); + *stateful_command_type = Some(StatefulCommand::EnumerateRps(1)); } - enumerate_rps_response(rp_id, Some(total_rps as u64)) + // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() + enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64)) } /// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. fn process_enumerate_rps_get_next_rp( + persistent_store: &PersistentStore, stateful_command_permission: &mut TimedPermission, stateful_command_type: &mut Option, now: ClockValue, ) -> Result { check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateRps(rp_ids)) = stateful_command_type { - let rp_id = rp_ids.pop().ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = stateful_command_type { + let rp_set = get_stored_rp_ids(persistent_store)?; + // A BTreeSet is already sorted. + let rp_id = rp_set + .into_iter() + .nth(*rp_id_index) + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + *stateful_command_type = Some(StatefulCommand::EnumerateRps(*rp_id_index + 1)); enumerate_rps_response(Some(rp_id), None) } else { Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -305,6 +318,7 @@ pub fn process_credential_management( )?), CredentialManagementSubCommand::EnumerateRpsGetNextRp => { Some(process_enumerate_rps_get_next_rp( + persistent_store, stateful_command_permission, stateful_command_type, now, @@ -544,6 +558,90 @@ mod test { ); } + #[test] + fn test_process_enumerate_rps_completeness() { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let credential_source = create_credential_source(&mut rng); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; + + const NUM_CREDENTIALS: usize = 20; + for i in 0..NUM_CREDENTIALS { + let mut credential = credential_source.clone(); + credential.rp_id = i.to_string(); + ctap_state + .persistent_store + .store_credential(credential) + .unwrap(); + } + + ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); + let pin_auth = Some(vec![ + 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, + 0xD0, 0xD1, + ]); + + let mut rp_set = BTreeSet::new(); + // This mut is just to make the test code shorter. + // The command is different on the first loop iteration. + let mut cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, + sub_command_params: None, + pin_protocol: Some(1), + pin_auth, + }; + + for _ in 0..NUM_CREDENTIALS { + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + match cred_management_response.unwrap() { + ResponseData::AuthenticatorCredentialManagement(Some(response)) => { + if rp_set.is_empty() { + assert_eq!(response.total_rps, Some(NUM_CREDENTIALS as u64)); + } else { + assert_eq!(response.total_rps, None); + } + let rp_id = response.rp.unwrap().rp_id; + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + assert_eq!(rp_id_hash, response.rp_id_hash.unwrap().as_slice()); + assert!(!rp_set.contains(&rp_id)); + rp_set.insert(rp_id); + } + _ => panic!("Invalid response type"), + }; + cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, + sub_command_params: None, + pin_protocol: None, + pin_auth: None, + }; + } + + let cred_management_response = process_credential_management( + &mut ctap_state.persistent_store, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.stateful_command_type, + &mut ctap_state.pin_protocol_v1, + cred_management_params, + DUMMY_CLOCK_VALUE, + ); + assert_eq!( + cred_management_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + #[test] fn test_process_enumerate_credentials_with_uv() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 96dbd41..49d14f4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -165,7 +165,7 @@ pub struct AssertionState { pub enum StatefulCommand { Reset, GetAssertion(AssertionState), - EnumerateRps(Vec), + EnumerateRps(usize), EnumerateCredentials(Vec), } From 134c880212cb91f400052c33c28be38eac4e3ab8 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 19 Jan 2021 15:07:15 +0100 Subject: [PATCH 132/192] reworks command state to its own struct --- src/ctap/credential_management.rs | 85 +++++------------- src/ctap/mod.rs | 145 ++++++++++++++++++------------ 2 files changed, 111 insertions(+), 119 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index dba7d36..2dbe961 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -22,11 +22,7 @@ use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::timed_permission::TimedPermission; -use super::{ - check_command_permission, check_pin_uv_auth_protocol, StatefulCommand, - STATEFUL_COMMAND_TIMEOUT_DURATION, -}; +use super::{check_pin_uv_auth_protocol, StatefulCommand, StatefulPermission}; use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; @@ -125,8 +121,7 @@ fn process_get_creds_metadata( /// Processes the subcommand enumerateRPsBegin for CredentialManagement. fn process_enumerate_rps_begin( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, now: ClockValue, ) -> Result { let rp_set = get_stored_rp_ids(persistent_store)?; @@ -134,9 +129,7 @@ fn process_enumerate_rps_begin( // TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty? if total_rps > 1 { - *stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateRps(1)); + stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1)); } // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64)) @@ -145,19 +138,16 @@ fn process_enumerate_rps_begin( /// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. fn process_enumerate_rps_get_next_rp( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, - now: ClockValue, + stateful_command_permission: &mut StatefulPermission, ) -> Result { - check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = stateful_command_type { + if let StatefulCommand::EnumerateRps(rp_id_index) = stateful_command_permission.get_command()? { let rp_set = get_stored_rp_ids(persistent_store)?; // A BTreeSet is already sorted. let rp_id = rp_set .into_iter() .nth(*rp_id_index) .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - *stateful_command_type = Some(StatefulCommand::EnumerateRps(*rp_id_index + 1)); + *rp_id_index += 1; enumerate_rps_response(Some(rp_id), None) } else { Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -167,8 +157,7 @@ fn process_enumerate_rps_get_next_rp( /// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. fn process_enumerate_credentials_begin( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, sub_command_params: CredentialManagementSubCommandParameters, now: ClockValue, ) -> Result { @@ -194,9 +183,8 @@ fn process_enumerate_credentials_begin( .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; let credential = persistent_store.get_credential(current_key)?; if total_credentials > 1 { - *stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - *stateful_command_type = Some(StatefulCommand::EnumerateCredentials(rp_credentials)); + stateful_command_permission + .set_command(now, StatefulCommand::EnumerateCredentials(rp_credentials)); } enumerate_credentials_response(credential, Some(total_credentials as u64)) } @@ -204,12 +192,10 @@ fn process_enumerate_credentials_begin( /// Processes the subcommand enumerateCredentialsGetNextCredential for CredentialManagement. fn process_enumerate_credentials_get_next_credential( persistent_store: &PersistentStore, - stateful_command_permission: &mut TimedPermission, - mut stateful_command_type: &mut Option, - now: ClockValue, + stateful_command_permission: &mut StatefulPermission, ) -> Result { - check_command_permission(stateful_command_permission, now)?; - if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut stateful_command_type + if let StatefulCommand::EnumerateCredentials(rp_credentials) = + stateful_command_permission.get_command()? { let current_key = rp_credentials .pop() @@ -251,8 +237,7 @@ fn process_update_user_information( /// Processes the CredentialManagement command and all its subcommands. pub fn process_credential_management( persistent_store: &mut PersistentStore, - stateful_command_permission: &mut TimedPermission, - mut stateful_command_type: &mut Option, + stateful_command_permission: &mut StatefulPermission, pin_protocol_v1: &mut PinProtocolV1, cred_management_params: AuthenticatorCredentialManagementParameters, now: ClockValue, @@ -264,17 +249,17 @@ pub fn process_credential_management( pin_auth, } = cred_management_params; - match (sub_command, &mut stateful_command_type) { + match (sub_command, stateful_command_permission.get_command()) { ( CredentialManagementSubCommand::EnumerateRpsGetNextRp, - Some(StatefulCommand::EnumerateRps(_)), - ) => (), - ( + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, - Some(StatefulCommand::EnumerateCredentials(_)), - ) => (), + Ok(StatefulCommand::EnumerateCredentials(_)), + ) => stateful_command_permission.check_command_permission(now)?, (_, _) => { - *stateful_command_type = None; + stateful_command_permission.clear(); } } @@ -313,22 +298,15 @@ pub fn process_credential_management( CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( persistent_store, stateful_command_permission, - stateful_command_type, now, )?), - CredentialManagementSubCommand::EnumerateRpsGetNextRp => { - Some(process_enumerate_rps_get_next_rp( - persistent_store, - stateful_command_permission, - stateful_command_type, - now, - )?) - } + CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some( + process_enumerate_rps_get_next_rp(persistent_store, stateful_command_permission)?, + ), CredentialManagementSubCommand::EnumerateCredentialsBegin => { Some(process_enumerate_credentials_begin( persistent_store, stateful_command_permission, - stateful_command_type, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, now, )?) @@ -337,8 +315,6 @@ pub fn process_credential_management( Some(process_enumerate_credentials_get_next_credential( persistent_store, stateful_command_permission, - stateful_command_type, - now, )?) } CredentialManagementSubCommand::DeleteCredential => { @@ -412,7 +388,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -441,7 +416,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -496,7 +470,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -521,7 +494,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -547,7 +519,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -600,7 +571,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -631,7 +601,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -690,7 +659,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -714,7 +682,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -739,7 +706,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -793,7 +759,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -812,7 +777,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -872,7 +836,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -922,7 +885,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, @@ -950,7 +912,6 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.stateful_command_type, &mut ctap_state.pin_protocol_v1, cred_management_params, DUMMY_CLOCK_VALUE, diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 49d14f4..46281fa 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -101,8 +101,9 @@ const ED_FLAG: u8 = 0x80; pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); +// TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); -pub const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); +const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -169,15 +170,50 @@ pub enum StatefulCommand { EnumerateCredentials(Vec), } -pub fn check_command_permission( - stateful_command_permission: &mut TimedPermission, - now: ClockValue, -) -> Result<(), Ctap2StatusCode> { - *stateful_command_permission = stateful_command_permission.check_expiration(now); - if stateful_command_permission.is_granted(now) { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) +pub struct StatefulPermission { + permission: TimedPermission, + command_type: Option, +} + +impl StatefulPermission { + // Resets are only possible in the first 10 seconds after booting. + // Therefore, initialization includes allowing Reset. + pub fn new_reset(now: ClockValue) -> StatefulPermission { + StatefulPermission { + permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), + command_type: Some(StatefulCommand::Reset), + } + } + + pub fn clear(&mut self) { + self.permission = TimedPermission::waiting(); + self.command_type = None; + } + + pub fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { + if self.permission.is_granted(now) { + Ok(()) + } else { + self.clear(); + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + pub fn get_command(&mut self) -> Result<&mut StatefulCommand, Ctap2StatusCode> { + self.command_type + .as_mut() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + + pub fn set_command(&mut self, now: ClockValue, new_command_type: StatefulCommand) { + match &new_command_type { + // Reset is only allowed after a power cycle. + StatefulCommand::Reset => unreachable!(), + _ => { + self.permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + self.command_type = Some(new_command_type); + } + } } } @@ -194,8 +230,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, // The state initializes to Reset and its timeout, and never goes back to Reset. - stateful_command_permission: TimedPermission, - stateful_command_type: Option, + stateful_command_permission: StatefulPermission, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -220,13 +255,15 @@ where U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), - stateful_command_permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), - stateful_command_type: Some(StatefulCommand::Reset), + stateful_command_permission: StatefulPermission::new_reset(now), } } pub fn update_command_permission(&mut self, now: ClockValue) { - self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + // Ignore the result, just update. + let _ = self + .stateful_command_permission + .check_command_permission(now); } pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { @@ -345,27 +382,23 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ); } - match (&command, &self.stateful_command_type) { - ( - Command::AuthenticatorGetNextAssertion, - Some(StatefulCommand::GetAssertion(_)), - ) => (), - ( + match (&command, self.stateful_command_permission.get_command()) { + (Command::AuthenticatorGetNextAssertion, Ok(StatefulCommand::GetAssertion(_))) + | (Command::AuthenticatorReset, Ok(StatefulCommand::Reset)) + // AuthenticatorGetInfo still allows Reset. + | (Command::AuthenticatorGetInfo, Ok(StatefulCommand::Reset)) + // AuthenticatorSelection still allows Reset. + | (Command::AuthenticatorSelection, Ok(StatefulCommand::Reset)) + // AuthenticatorCredentialManagement handles its subcommands later. + | ( Command::AuthenticatorCredentialManagement(_), - Some(StatefulCommand::EnumerateRps(_)), - ) => (), - ( + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( Command::AuthenticatorCredentialManagement(_), - Some(StatefulCommand::EnumerateCredentials(_)), + Ok(StatefulCommand::EnumerateCredentials(_)), ) => (), - (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), - // GetInfo does not reset stateful commands. - (Command::AuthenticatorGetInfo, _) => (), - // AuthenticatorSelection does not reset stateful commands. - (Command::AuthenticatorSelection, _) => (), - (_, _) => { - self.stateful_command_type = None; - } + (_, _) => self.stateful_command_permission.clear(), } let response = match command { Command::AuthenticatorMakeCredential(params) => { @@ -382,7 +415,6 @@ where process_credential_management( &mut self.persistent_store, &mut self.stateful_command_permission, - &mut self.stateful_command_type, &mut self.pin_protocol_v1, params, now, @@ -855,12 +887,12 @@ where None } else { let number_of_credentials = Some(next_credential_keys.len() + 1); - self.stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { + let assertion_state = StatefulCommand::GetAssertion(AssertionState { assertion_input: assertion_input.clone(), next_credential_keys, - })); + }); + self.stateful_command_permission + .set_command(now, assertion_state); number_of_credentials }; self.assertion_response(credential, assertion_input, number_of_credentials) @@ -870,20 +902,20 @@ where &mut self, now: ClockValue, ) -> Result { - check_command_permission(&mut self.stateful_command_permission, now)?; - let (assertion_input, credential) = - if let Some(StatefulCommand::GetAssertion(assertion_state)) = - &mut self.stateful_command_type - { - let credential_key = assertion_state - .next_credential_keys - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self.persistent_store.get_credential(credential_key)?; - (assertion_state.assertion_input.clone(), credential) - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); - }; + self.stateful_command_permission + .check_command_permission(now)?; + let (assertion_input, credential) = if let StatefulCommand::GetAssertion(assertion_state) = + self.stateful_command_permission.get_command()? + { + let credential_key = assertion_state + .next_credential_keys + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + let credential = self.persistent_store.get_credential(credential_key)?; + (assertion_state.assertion_input.clone(), credential) + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + }; self.assertion_response(credential, assertion_input, None) } @@ -949,11 +981,10 @@ where cid: ChannelID, now: ClockValue, ) -> Result { - // Resets are only possible in the first 10 seconds after booting. - // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? - check_command_permission(&mut self.stateful_command_permission, now)?; - match &self.stateful_command_type { - Some(StatefulCommand::Reset) => (), + self.stateful_command_permission + .check_command_permission(now)?; + match self.stateful_command_permission.get_command()? { + StatefulCommand::Reset => (), _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), } (self.check_user_presence)(cid)?; From 9296f51e19daa839fe7bf3167ea0b2b6b7aafcd1 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 12:08:07 +0100 Subject: [PATCH 133/192] stricter API for StatefulCommandPermission --- src/ctap/credential_management.rs | 34 ++++--------- src/ctap/mod.rs | 85 ++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 2dbe961..25fe3b9 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -140,18 +140,14 @@ fn process_enumerate_rps_get_next_rp( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, ) -> Result { - if let StatefulCommand::EnumerateRps(rp_id_index) = stateful_command_permission.get_command()? { - let rp_set = get_stored_rp_ids(persistent_store)?; - // A BTreeSet is already sorted. - let rp_id = rp_set - .into_iter() - .nth(*rp_id_index) - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - *rp_id_index += 1; - enumerate_rps_response(Some(rp_id), None) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + let rp_id_index = stateful_command_permission.next_enumerate_rp()?; + let rp_set = get_stored_rp_ids(persistent_store)?; + // A BTreeSet is already sorted. + let rp_id = rp_set + .into_iter() + .nth(rp_id_index) + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + enumerate_rps_response(Some(rp_id), None) } /// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. @@ -194,17 +190,9 @@ fn process_enumerate_credentials_get_next_credential( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, ) -> Result { - if let StatefulCommand::EnumerateCredentials(rp_credentials) = - stateful_command_permission.get_command()? - { - let current_key = rp_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = persistent_store.get_credential(current_key)?; - enumerate_credentials_response(credential, None) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + let credential_key = stateful_command_permission.next_enumerate_credential()?; + let credential = persistent_store.get_credential(credential_key)?; + enumerate_credentials_response(credential, None) } /// Processes the subcommand deleteCredential for CredentialManagement. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 46281fa..96d7b25 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -149,20 +149,23 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { } } +/// Holds data necessary to sign an assertion for a credential. #[derive(Clone)] -struct AssertionInput { +pub struct AssertionInput { client_data_hash: Vec, auth_data: Vec, hmac_secret_input: Option, has_uv: bool, } +/// Contains the state we need to store for GetNextAssertion. pub struct AssertionState { assertion_input: AssertionInput, // Sorted by ascending order of creation, so the last element is the most recent one. next_credential_keys: Vec, } +/// Stores which command currently holds state for subsequent calls. pub enum StatefulCommand { Reset, GetAssertion(AssertionState), @@ -170,14 +173,25 @@ pub enum StatefulCommand { EnumerateCredentials(Vec), } +/// Stores the current CTAP command state and when it times out. +/// +/// Some commands are executed in a series of calls to the authenticator. +/// Interleaving calls to other commands interrupt the current command and +/// remove all state and permissions. Power cycling allows the Reset command, +/// and to prevent misuse or accidents, we disallow Reset after receiving +/// different commands. Therefore, Reset behaves just like all other stateful +/// commands and is included here. Please not that the allowed time for Reset +/// differs from all other stateful commands. pub struct StatefulPermission { permission: TimedPermission, command_type: Option, } impl StatefulPermission { - // Resets are only possible in the first 10 seconds after booting. - // Therefore, initialization includes allowing Reset. + /// Creates the command state at device startup. + /// + /// Resets are only possible after a power cycle. Therefore, initialization + /// means allowing Reset, and Reset cannot be granted later. pub fn new_reset(now: ClockValue) -> StatefulPermission { StatefulPermission { permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), @@ -185,11 +199,13 @@ impl StatefulPermission { } } + /// Clears all permissions and state. pub fn clear(&mut self) { self.permission = TimedPermission::waiting(); self.command_type = None; } + /// Checks the permission timeout. pub fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { if self.permission.is_granted(now) { Ok(()) @@ -199,12 +215,14 @@ impl StatefulPermission { } } - pub fn get_command(&mut self) -> Result<&mut StatefulCommand, Ctap2StatusCode> { + /// Gets a reference to the current command state, if any exists. + pub fn get_command(&self) -> Result<&StatefulCommand, Ctap2StatusCode> { self.command_type - .as_mut() + .as_ref() .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) } + /// Sets a new command state, and starts a new clock for timeouts. pub fn set_command(&mut self, now: ClockValue, new_command_type: StatefulCommand) { match &new_command_type { // Reset is only allowed after a power cycle. @@ -215,6 +233,47 @@ impl StatefulPermission { } } } + + /// Returns the state for the next assertion and advances it. + /// + /// The state includes all information from GetAssertion and the storage key + /// to the next credential that needs to be processed. + pub fn next_assertion_credential( + &mut self, + ) -> Result<(AssertionInput, usize), Ctap2StatusCode> { + if let Some(StatefulCommand::GetAssertion(assertion_state)) = &mut self.command_type { + let credential_key = assertion_state + .next_credential_keys + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + Ok((assertion_state.assertion_input.clone(), credential_key)) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Returns the index to the next RP ID for enumeration and advances it. + pub fn next_enumerate_rp(&mut self) -> Result { + if let Some(StatefulCommand::EnumerateRps(rp_id_index)) = &mut self.command_type { + let current_index = *rp_id_index; + *rp_id_index += 1; + Ok(current_index) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Returns the next storage credential key for enumeration and advances it. + pub fn next_enumerate_credential(&mut self) -> Result { + if let Some(StatefulCommand::EnumerateCredentials(rp_credentials)) = &mut self.command_type + { + rp_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } } // This struct currently holds all state, not only the persistent memory. The persistent members are @@ -904,18 +963,10 @@ where ) -> Result { self.stateful_command_permission .check_command_permission(now)?; - let (assertion_input, credential) = if let StatefulCommand::GetAssertion(assertion_state) = - self.stateful_command_permission.get_command()? - { - let credential_key = assertion_state - .next_credential_keys - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self.persistent_store.get_credential(credential_key)?; - (assertion_state.assertion_input.clone(), credential) - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); - }; + let (assertion_input, credential_key) = self + .stateful_command_permission + .next_assertion_credential()?; + let credential = self.persistent_store.get_credential(credential_key)?; self.assertion_response(credential, assertion_input, None) } From 6bf4a7edec3370f1bb3b452888f04f5f8c6caec1 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 13:22:24 +0100 Subject: [PATCH 134/192] fix typo --- src/ctap/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 96d7b25..8ac324c 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -180,7 +180,7 @@ pub enum StatefulCommand { /// remove all state and permissions. Power cycling allows the Reset command, /// and to prevent misuse or accidents, we disallow Reset after receiving /// different commands. Therefore, Reset behaves just like all other stateful -/// commands and is included here. Please not that the allowed time for Reset +/// commands and is included here. Please note that the allowed time for Reset /// differs from all other stateful commands. pub struct StatefulPermission { permission: TimedPermission, From 8634e2ec2437eed88229f0a28a00c78016d6e3dc Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 20 Jan 2021 15:56:06 +0100 Subject: [PATCH 135/192] Make StoreUpdate generic over the byte slice ownership This permits to call it without having to create a Vec when possible. --- libraries/persistent_store/fuzz/src/store.rs | 2 +- libraries/persistent_store/src/format.rs | 17 ++++++++++++----- libraries/persistent_store/src/model.rs | 4 ++-- libraries/persistent_store/src/store.rs | 18 +++++++++++------- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs index 3113f77..006532b 100644 --- a/libraries/persistent_store/fuzz/src/store.rs +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -303,7 +303,7 @@ impl<'a> Fuzzer<'a> { } /// Generates a possibly invalid update. - fn update(&mut self) -> StoreUpdate { + fn update(&mut self) -> StoreUpdate> { match self.entropy.read_range(0, 1) { 0 => { let key = self.key(); diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index f575750..a70dcc4 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -20,6 +20,7 @@ use self::bitfield::Length; use self::bitfield::{count_zeros, num_bits, Bit, Checksum, ConstField, Field}; use crate::{usize_to_nat, Nat, Storage, StorageIndex, StoreError, StoreResult, StoreUpdate}; use alloc::vec::Vec; +use core::borrow::Borrow; use core::cmp::min; use core::convert::TryFrom; @@ -492,13 +493,16 @@ impl Format { } /// Returns the capacity required by a transaction. - pub fn transaction_capacity(&self, updates: &[StoreUpdate]) -> Nat { + pub fn transaction_capacity>( + &self, + updates: &[StoreUpdate], + ) -> Nat { match updates.len() { // An empty transaction doesn't consume anything. 0 => 0, // Transactions with a single update are optimized by avoiding a marker entry. 1 => match &updates[0] { - StoreUpdate::Insert { value, .. } => self.entry_size(value), + StoreUpdate::Insert { value, .. } => self.entry_size(value.borrow()), // Transactions with a single update which is a removal don't consume anything. StoreUpdate::Remove { .. } => 0, }, @@ -508,9 +512,9 @@ impl Format { } /// Returns the capacity of an update. - fn update_capacity(&self, update: &StoreUpdate) -> Nat { + fn update_capacity>(&self, update: &StoreUpdate) -> Nat { match update { - StoreUpdate::Insert { value, .. } => self.entry_size(value), + StoreUpdate::Insert { value, .. } => self.entry_size(value.borrow()), StoreUpdate::Remove { .. } => 1, } } @@ -523,7 +527,10 @@ impl Format { /// Checks if a transaction is valid and returns its sorted keys. /// /// Returns `None` if the transaction is invalid. - pub fn transaction_valid(&self, updates: &[StoreUpdate]) -> Option> { + pub fn transaction_valid>( + &self, + updates: &[StoreUpdate], + ) -> Option> { if usize_to_nat(updates.len()) > self.max_updates() { return None; } diff --git a/libraries/persistent_store/src/model.rs b/libraries/persistent_store/src/model.rs index c509b03..eebc329 100644 --- a/libraries/persistent_store/src/model.rs +++ b/libraries/persistent_store/src/model.rs @@ -34,7 +34,7 @@ pub enum StoreOperation { /// Applies a transaction. Transaction { /// The list of updates to be applied. - updates: Vec, + updates: Vec>>, }, /// Deletes all keys above a threshold. @@ -89,7 +89,7 @@ impl StoreModel { } /// Applies a transaction. - fn transaction(&mut self, updates: Vec) -> StoreResult<()> { + fn transaction(&mut self, updates: Vec>>) -> StoreResult<()> { // Fail if the transaction is invalid. if self.format.transaction_valid(&updates).is_none() { return Err(StoreError::InvalidArgument); diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index f707a89..224eeb9 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -25,6 +25,7 @@ pub use crate::{ }; use alloc::boxed::Box; use alloc::vec::Vec; +use core::borrow::Borrow; use core::cmp::{max, min, Ordering}; use core::convert::TryFrom; use core::option::NoneError; @@ -159,15 +160,15 @@ impl StoreHandle { /// Represents an update to the store as part of a transaction. #[derive(Clone, Debug)] -pub enum StoreUpdate { +pub enum StoreUpdate> { /// Inserts or replaces an entry in the store. - Insert { key: usize, value: Vec }, + Insert { key: usize, value: ByteSlice }, /// Removes an entry from the store. Remove { key: usize }, } -impl StoreUpdate { +impl> StoreUpdate { /// Returns the key affected by the update. pub fn key(&self) -> usize { match *self { @@ -179,7 +180,7 @@ impl StoreUpdate { /// Returns the value written by the update. pub fn value(&self) -> Option<&[u8]> { match self { - StoreUpdate::Insert { value, .. } => Some(value), + StoreUpdate::Insert { value, .. } => Some(value.borrow()), StoreUpdate::Remove { .. } => None, } } @@ -280,14 +281,17 @@ impl Store { /// - There are too many updates. /// - The updates overlap, i.e. their keys are not disjoint. /// - The updates are invalid, e.g. key out of bound or value too long. - pub fn transaction(&mut self, updates: &[StoreUpdate]) -> StoreResult<()> { + pub fn transaction>( + &mut self, + updates: &[StoreUpdate], + ) -> StoreResult<()> { let count = usize_to_nat(updates.len()); if count == 0 { return Ok(()); } if count == 1 { match updates[0] { - StoreUpdate::Insert { key, ref value } => return self.insert(key, value), + StoreUpdate::Insert { key, ref value } => return self.insert(key, value.borrow()), StoreUpdate::Remove { key } => return self.remove(key), } } @@ -310,7 +314,7 @@ impl Store { for update in updates { let length = match *update { StoreUpdate::Insert { key, ref value } => { - let entry = self.format.build_user(usize_to_nat(key), value)?; + let entry = self.format.build_user(usize_to_nat(key), value.borrow())?; let word_size = self.format.word_size(); let footer = usize_to_nat(entry.len()) / word_size - 1; self.write_slice(tail, &entry[..(footer * word_size) as usize])?; From 14189a398addfc273e179b84d43ecce4f3666258 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 20 Jan 2021 18:46:38 +0100 Subject: [PATCH 136/192] implements the credBlob extensions --- README.md | 2 + src/ctap/command.rs | 42 ++-- src/ctap/credential_management.rs | 2 + src/ctap/data_formats.rs | 47 +++- src/ctap/mod.rs | 359 +++++++++++++++++++++++------- src/ctap/storage.rs | 6 +- 6 files changed, 356 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 0691fb8..da46d7f 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,8 @@ a few things you can personalize: allows some relying parties to read the minimum PIN length by default. The latter allows storing more relying parties that may check the minimum PIN length. +1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs + bigger than the default value. ### 3D printed enclosure diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 620a06f..128c4b4 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -147,8 +147,9 @@ pub struct AuthenticatorMakeCredentialParameters { pub user: PublicKeyCredentialUserEntity, pub pub_key_cred_params: Vec, pub exclude_list: Option>, - pub extensions: Option, - // Even though options are optional, we can use the default if not present. + // Extensions are optional, but we can use defaults for all missing fields. + pub extensions: MakeCredentialExtensions, + // Same for options, use defaults when not present. pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, @@ -198,15 +199,13 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { let extensions = extensions .map(MakeCredentialExtensions::try_from) - .transpose()?; + .transpose()? + .unwrap_or_default(); - let options = match options { - Some(entry) => MakeCredentialOptions::try_from(entry)?, - None => MakeCredentialOptions { - rk: false, - uv: false, - }, - }; + let options = options + .map(MakeCredentialOptions::try_from) + .transpose()? + .unwrap_or_default(); let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; @@ -230,8 +229,9 @@ pub struct AuthenticatorGetAssertionParameters { pub rp_id: String, pub client_data_hash: Vec, pub allow_list: Option>, - pub extensions: Option, - // Even though options are optional, we can use the default if not present. + // Extensions are optional, but we can use defaults for all missing fields. + pub extensions: GetAssertionExtensions, + // Same for options, use defaults when not present. pub options: GetAssertionOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, @@ -272,15 +272,13 @@ impl TryFrom for AuthenticatorGetAssertionParameters { let extensions = extensions .map(GetAssertionExtensions::try_from) - .transpose()?; + .transpose()? + .unwrap_or_default(); - let options = match options { - Some(entry) => GetAssertionOptions::try_from(entry)?, - None => GetAssertionOptions { - up: true, - uv: false, - }, - }; + let options = options + .map(GetAssertionOptions::try_from) + .transpose()? + .unwrap_or_default(); let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; @@ -545,7 +543,7 @@ mod test { user, pub_key_cred_params: vec![ES256_CRED_PARAM], exclude_list: Some(vec![]), - extensions: None, + extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), @@ -591,7 +589,7 @@ mod test { rp_id, client_data_hash, allow_list: Some(vec![pub_key_cred_descriptor]), - extensions: None, + extensions: GetAssertionExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 25fe3b9..7681fa1 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -80,6 +80,7 @@ fn enumerate_credentials_response( creation_order: _, user_name, user_icon, + cred_blob: _, } = credential; let user = PublicKeyCredentialUserEntity { user_id: user_handle, @@ -346,6 +347,7 @@ mod test { creation_order: 0, user_name: Some("name".to_string()), user_icon: Some("icon".to_string()), + cred_blob: None, } } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 9d4d10f..da992f8 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -275,11 +275,13 @@ impl From for cbor::Value { } } +#[derive(Default)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub struct MakeCredentialExtensions { pub hmac_secret: bool, pub cred_protect: Option, pub min_pin_length: bool, + pub cred_blob: Option>, } impl TryFrom for MakeCredentialExtensions { @@ -288,6 +290,7 @@ impl TryFrom for MakeCredentialExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "credProtect" => cred_protect, "hmac-secret" => hmac_secret, "minPinLength" => min_pin_length, @@ -299,17 +302,21 @@ impl TryFrom for MakeCredentialExtensions { .map(CredentialProtectionPolicy::try_from) .transpose()?; let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; + let cred_blob = cred_blob.map(extract_byte_string).transpose()?; Ok(Self { hmac_secret, cred_protect, min_pin_length, + cred_blob, }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Default)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct GetAssertionExtensions { pub hmac_secret: Option, + pub cred_blob: bool, } impl TryFrom for GetAssertionExtensions { @@ -318,6 +325,7 @@ impl TryFrom for GetAssertionExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "hmac-secret" => hmac_secret, } = extract_map(cbor_value)?; } @@ -325,7 +333,11 @@ impl TryFrom for GetAssertionExtensions { let hmac_secret = hmac_secret .map(GetAssertionHmacSecretInput::try_from) .transpose()?; - Ok(Self { hmac_secret }) + let cred_blob = cred_blob.map_or(Ok(false), extract_bool)?; + Ok(Self { + hmac_secret, + cred_blob, + }) } } @@ -361,6 +373,7 @@ impl TryFrom for GetAssertionHmacSecretInput { } // Even though options are optional, we can use the default if not present. +#[derive(Default)] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct MakeCredentialOptions { pub rk: bool, @@ -400,6 +413,15 @@ pub struct GetAssertionOptions { pub uv: bool, } +impl Default for GetAssertionOptions { + fn default() -> Self { + GetAssertionOptions { + up: true, + uv: false, + } + } +} + impl TryFrom for GetAssertionOptions { type Error = Ctap2StatusCode; @@ -523,6 +545,7 @@ pub struct PublicKeyCredentialSource { pub creation_order: u64, pub user_name: Option, pub user_icon: Option, + pub cred_blob: Option>, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -537,6 +560,7 @@ enum PublicKeyCredentialSourceField { CreationOrder = 7, UserName = 8, UserIcon = 9, + CredBlob = 10, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: @@ -563,6 +587,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, PublicKeyCredentialSourceField::UserName => credential.user_name, PublicKeyCredentialSourceField::UserIcon => credential.user_icon, + PublicKeyCredentialSourceField::CredBlob => credential.cred_blob, } } } @@ -582,6 +607,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::CreationOrder => creation_order, PublicKeyCredentialSourceField::UserName => user_name, PublicKeyCredentialSourceField::UserIcon => user_icon, + PublicKeyCredentialSourceField::CredBlob => cred_blob, } = extract_map(cbor_value)?; } @@ -601,6 +627,7 @@ impl TryFrom for PublicKeyCredentialSource { let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; let user_name = user_name.map(extract_text_string).transpose()?; let user_icon = user_icon.map(extract_text_string).transpose()?; + let cred_blob = cred_blob.map(extract_byte_string).transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -622,6 +649,7 @@ impl TryFrom for PublicKeyCredentialSource { creation_order, user_name, user_icon, + cred_blob, }) } } @@ -1493,12 +1521,14 @@ mod test { "hmac-secret" => true, "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, "minPinLength" => true, + "credBlob" => vec![0xCB], }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), min_pin_length: true, + cred_blob: Some(vec![0xCB]), }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1515,6 +1545,7 @@ mod test { 2 => vec![0x02; 32], 3 => vec![0x03; 16], }, + "credBlob" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); let expected_input = GetAssertionHmacSecretInput { @@ -1524,6 +1555,7 @@ mod test { }; let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), + cred_blob: true, }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1816,6 +1848,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert_eq!( @@ -1858,6 +1891,16 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + cred_blob: Some(vec![0xCB]), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8ac324c..6ffd108 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -35,7 +35,7 @@ use self::command::{ use self::config_command::process_config; use self::credential_management::process_credential_management; use self::data_formats::{ - AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionHmacSecretInput, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, @@ -47,17 +47,18 @@ use self::response::{ AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; -use self::storage::PersistentStore; +use self::storage::{PersistentStore, MAX_RP_IDS_LENGTH}; use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; +use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; use byteorder::{BigEndian, ByteOrder}; -use cbor::{cbor_map, cbor_map_options}; +use cbor::cbor_map_options; #[cfg(feature = "debug_ctap")] use core::fmt::Write; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; @@ -124,6 +125,8 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa // - Some(CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList) // - Some(CredentialProtectionPolicy::UserVerificationRequired) const DEFAULT_CRED_PROTECT: Option = None; +// Maximum size stored with the credBlob extension. Must be at least 32. +const MAX_CRED_BLOB_LENGTH: usize = 32; // Checks the PIN protocol parameter against all supported versions. pub fn check_pin_uv_auth_protocol( @@ -154,7 +157,7 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { pub struct AssertionInput { client_data_hash: Vec, auth_data: Vec, - hmac_secret_input: Option, + extensions: GetAssertionExtensions, has_uv: bool, } @@ -168,7 +171,7 @@ pub struct AssertionState { /// Stores which command currently holds state for subsequent calls. pub enum StatefulCommand { Reset, - GetAssertion(AssertionState), + GetAssertion(Box), EnumerateRps(usize), EnumerateCredentials(Vec), } @@ -419,6 +422,7 @@ where creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, })) } @@ -558,27 +562,31 @@ where } let rp_id = rp.rp_id; - let (use_hmac_extension, cred_protect_policy, min_pin_length) = - if let Some(extensions) = extensions { - let mut cred_protect = extensions.cred_protect; - if cred_protect.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - < DEFAULT_CRED_PROTECT - .unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) - { - cred_protect = DEFAULT_CRED_PROTECT; - } - let min_pin_length = extensions.min_pin_length - && self - .persistent_store - .min_pin_length_rp_ids()? - .contains(&rp_id); - (extensions.hmac_secret, cred_protect, min_pin_length) - } else { - (false, DEFAULT_CRED_PROTECT, false) - }; - - let has_extension_output = - use_hmac_extension || cred_protect_policy.is_some() || min_pin_length; + let mut cred_protect_policy = extensions.cred_protect; + if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) + { + cred_protect_policy = DEFAULT_CRED_PROTECT; + } + let min_pin_length = extensions.min_pin_length + && self + .persistent_store + .min_pin_length_rp_ids()? + .contains(&rp_id); + // None for no input, false for invalid input, true for valid input. + let has_cred_blob_output = extensions.cred_blob.is_some(); + let cred_blob = extensions + .cred_blob + .filter(|c| options.rk && c.len() <= MAX_CRED_BLOB_LENGTH); + let cred_blob_output = if has_cred_blob_output { + Some(cred_blob.is_some()) + } else { + None + }; + let has_extension_output = extensions.hmac_secret + || cred_protect_policy.is_some() + || min_pin_length + || has_cred_blob_output; let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { @@ -656,6 +664,7 @@ where user_icon: user .user_icon .map(|s| truncate_to_char_boundary(&s, 64).to_string()), + cred_blob, }; self.persistent_store.store_credential(credential_source)?; random_id @@ -675,7 +684,11 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } if has_extension_output { - let hmac_secret_output = if use_hmac_extension { Some(true) } else { None }; + let hmac_secret_output = if extensions.hmac_secret { + Some(true) + } else { + None + }; let min_pin_length_output = if min_pin_length { Some(self.persistent_store.min_pin_length()? as u64) } else { @@ -685,6 +698,7 @@ where "hmac-secret" => hmac_secret_output, "credProtect" => cred_protect_policy, "minPinLength" => min_pin_length_output, + "credBlob" => cred_blob_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -754,18 +768,30 @@ where let AssertionInput { client_data_hash, mut auth_data, - hmac_secret_input, + extensions, has_uv, } = assertion_input; // Process extensions. - if let Some(hmac_secret_input) = hmac_secret_input { - let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; - let encrypted_output = self - .pin_protocol_v1 - .process_hmac_secret(hmac_secret_input, &cred_random)?; - let extensions_output = cbor_map! { + if extensions.hmac_secret.is_some() || extensions.cred_blob { + let encrypted_output = if let Some(hmac_secret_input) = extensions.hmac_secret { + let cred_random = self.generate_cred_random(&credential.private_key, has_uv)?; + Some( + self.pin_protocol_v1 + .process_hmac_secret(hmac_secret_input, &cred_random)?, + ) + } else { + None + }; + // This could be written more nicely with `then_some` when stable. + let cred_blob = if extensions.cred_blob { + Some(credential.cred_blob.unwrap_or_default()) + } else { + None + }; + let extensions_output = cbor_map_options! { "hmac-secret" => encrypted_output, + "credBlob" => cred_blob, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -854,8 +880,7 @@ where self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; - let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); - if hmac_secret_input.is_some() && !options.up { + if extensions.hmac_secret.is_some() && !options.up { // The extension is actually supported, but we need user presence. return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION); } @@ -891,7 +916,7 @@ where if options.up { flags |= UP_FLAG; } - if hmac_secret_input.is_some() { + if extensions.hmac_secret.is_some() || extensions.cred_blob { flags |= ED_FLAG; } @@ -939,17 +964,17 @@ where let assertion_input = AssertionInput { client_data_hash, auth_data: self.generate_auth_data(&rp_id_hash, flags)?, - hmac_secret_input, + extensions, has_uv, }; let number_of_credentials = if next_credential_keys.is_empty() { None } else { let number_of_credentials = Some(next_credential_keys.len() + 1); - let assertion_state = StatefulCommand::GetAssertion(AssertionState { + let assertion_state = StatefulCommand::GetAssertion(Box::new(AssertionState { assertion_input: assertion_input.clone(), next_credential_keys, - }); + })); self.stateful_command_permission .set_command(now, assertion_state); number_of_credentials @@ -993,22 +1018,21 @@ where String::from("hmac-secret"), String::from("credProtect"), String::from("minPinLength"), + String::from("credBlob"), ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(1024), pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // TODO(#106) update with version 2.1 of HMAC-secret max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), default_cred_protect: DEFAULT_CRED_PROTECT, min_pin_length: self.persistent_store.min_pin_length()?, firmware_version: None, - max_cred_blob_length: None, - // TODO(kaczmarczyck) update when extension is implemented - max_rp_ids_for_set_min_pin_length: None, + max_cred_blob_length: Some(MAX_CRED_BLOB_LENGTH as u64), + max_rp_ids_for_set_min_pin_length: Some(MAX_RP_IDS_LENGTH as u64), remaining_discoverable_credentials: Some( self.persistent_store.remaining_credentials()? as u64, ), @@ -1149,11 +1173,11 @@ where mod test { use super::command::AuthenticatorAttestationMaterial; use super::data_formats::{ - CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; - use cbor::cbor_array; + use cbor::{cbor_array, cbor_map}; use crypto::rng256::ThreadRng256; const CLOCK_FREQUENCY_HZ: usize = 32768; @@ -1209,7 +1233,7 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - let mut expected_response = vec![0x00, 0xAB, 0x01]; + let mut expected_response = vec![0x00, 0xAD, 0x01]; // The version array differs with CTAP1, always including 2.0 and 2.1. #[cfg(not(feature = "with_ctap1"))] let version_count = 2; @@ -1221,10 +1245,11 @@ mod test { expected_response.extend( [ 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, - 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x83, 0x6B, 0x68, 0x6D, 0x61, + 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x84, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, - 0x65, 0x6E, 0x67, 0x74, 0x68, 0x03, 0x50, + 0x65, 0x6E, 0x67, 0x74, 0x68, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, + 0x03, 0x50, ] .iter(), ); @@ -1237,7 +1262,7 @@ mod test { 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, 0x18, 0x70, 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, 0x0D, 0x04, 0x14, 0x18, 0x96, + 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, 0x18, 0x96, ] .iter(), ); @@ -1269,7 +1294,7 @@ mod test { user, pub_key_cred_params, exclude_list: None, - extensions: None, + extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: None, pin_uv_auth_protocol: None, @@ -1293,11 +1318,12 @@ mod test { fn create_make_credential_parameters_with_cred_protect_policy( policy: CredentialProtectionPolicy, ) -> AuthenticatorMakeCredentialParameters { - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: Some(policy), min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; make_credential_params @@ -1380,6 +1406,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1458,11 +1485,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; make_credential_params.extensions = extensions; @@ -1487,11 +1515,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1516,11 +1545,12 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // First part: The extension is ignored, since the RP ID is not on the list. - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: None, min_pin_length: true, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1541,11 +1571,12 @@ mod test { Ok(()) ); - let extensions = Some(MakeCredentialExtensions { + let extensions = MakeCredentialExtensions { hmac_secret: false, cred_protect: None, min_pin_length: true, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; let make_credential_response = @@ -1563,6 +1594,82 @@ mod test { ); } + #[test] + fn test_process_make_credential_cred_blob_ok() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: false, + cred_blob: Some(vec![0xCB]), + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF5, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.cred_blob, Some(vec![0xCB])); + } + + #[test] + fn test_process_make_credential_cred_blob_too_big() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + hmac_secret: false, + cred_protect: None, + min_pin_length: false, + cred_blob: Some(vec![0xCB; MAX_CRED_BLOB_LENGTH + 1]), + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0xF4, + ]; + check_make_response( + make_credential_response, + 0xC1, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &expected_extension_cbor, + ); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.cred_blob, None); + } + #[test] fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; @@ -1586,6 +1693,7 @@ mod test { flags: u8, signature_counter: u32, expected_number_of_credentials: Option, + expected_extension_cbor: &[u8], ) { match response.unwrap() { ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { @@ -1605,6 +1713,7 @@ mod test { &mut expected_auth_data[signature_counter_position..], signature_counter, ); + expected_auth_data.extend(expected_extension_cbor); assert_eq!(auth_data, expected_auth_data); assert_eq!(user, Some(expected_user)); assert_eq!(number_of_credentials, expected_number_of_credentials); @@ -1613,6 +1722,29 @@ mod test { } } + fn check_assertion_response_with_extension( + response: Result, + expected_user_id: Vec, + signature_counter: u32, + expected_number_of_credentials: Option, + expected_extension_cbor: &[u8], + ) { + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + check_assertion_response_with_user( + response, + expected_user, + 0x80, + signature_counter, + expected_number_of_credentials, + expected_extension_cbor, + ); + } + fn check_assertion_response( response: Result, expected_user_id: Vec, @@ -1631,6 +1763,7 @@ mod test { 0x00, signature_counter, expected_number_of_credentials, + &[], ); } @@ -1649,7 +1782,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1676,11 +1809,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; make_credential_params.extensions = make_extensions; @@ -1704,9 +1838,10 @@ mod test { salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], }; - let get_extensions = Some(GetAssertionExtensions { + let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - }); + cred_blob: false, + }; let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, @@ -1744,11 +1879,12 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - let make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, cred_protect: None, min_pin_length: false, - }); + cred_blob: None, + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; assert!(ctap_state @@ -1761,9 +1897,10 @@ mod test { salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], }; - let get_extensions = Some(GetAssertionExtensions { + let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - }); + cred_blob: false, + }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), @@ -1800,7 +1937,7 @@ mod test { let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, key_id: credential_id.clone(), - transports: None, // You can set USB as a hint here. + transports: None, }; let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, @@ -1815,6 +1952,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1825,7 +1963,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1847,7 +1985,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: Some(vec![cred_desc.clone()]), - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1877,6 +2015,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store @@ -1887,7 +2026,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: Some(vec![cred_desc]), - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -1906,6 +2045,69 @@ mod test { ); } + #[test] + fn test_process_get_assertion_with_cred_blob() { + let mut rng = ThreadRng256 {}; + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential_id = rng.gen_uniform_u8x32().to_vec(); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x1D], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: Some(vec![0xCB]), + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let extensions = GetAssertionExtensions { + hmac_secret: None, + cred_blob: true, + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let signature_counter = ctap_state + .persistent_store + .global_signature_counter() + .unwrap(); + let expected_extension_cbor = [ + 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0x41, 0xCB, + ]; + check_assertion_response_with_extension( + get_assertion_response, + vec![0x1D], + signature_counter, + None, + &expected_extension_cbor, + ); + } + #[test] fn test_process_get_next_assertion_two_credentials_with_uv() { let mut rng = ThreadRng256 {}; @@ -1951,7 +2153,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: true, @@ -1974,6 +2176,7 @@ mod test { 0x04, signature_counter, Some(2), + &[], ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); @@ -1983,6 +2186,7 @@ mod test { 0x04, signature_counter, None, + &[], ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); @@ -2027,7 +2231,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -2091,7 +2295,7 @@ mod test { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], allow_list: None, - extensions: None, + extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: false, @@ -2147,6 +2351,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 18dd199..ffc5dd6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -57,7 +57,7 @@ const DEFAULT_MIN_PIN_LENGTH: u8 = 4; const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. -const MAX_RP_IDS_LENGTH: usize = 8; +pub const MAX_RP_IDS_LENGTH: usize = 8; /// Wrapper for master keys. pub struct MasterKeys { @@ -690,6 +690,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, } } @@ -906,6 +907,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -927,6 +929,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1160,6 +1163,7 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From de3addba74530ca34f8eb5d26e12c8a0c7c2184a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 21 Jan 2021 17:38:22 +0100 Subject: [PATCH 137/192] force PIN changes --- src/ctap/config_command.rs | 56 +++++++++++++++++++++++++++++++++++++ src/ctap/mod.rs | 18 ++++++++---- src/ctap/pin_protocol_v1.rs | 49 ++++++++++++++++++++++++++++++++ src/ctap/storage.rs | 29 +++++++++++++++++-- src/ctap/storage/key.rs | 3 ++ 5 files changed, 146 insertions(+), 9 deletions(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 726634c..e96ce7a 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -248,6 +248,62 @@ mod test { ); } + #[test] + fn test_process_set_min_pin_length_force_pin_change_implicit() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + // Increase min PIN, force PIN change. + let min_pin_length = 6; + let mut config_params = create_min_pin_config_params(min_pin_length, None); + let pin_uv_auth_param = Some(vec![ + 0x81, 0x37, 0x37, 0xF3, 0xD8, 0x69, 0xBD, 0x74, 0xFE, 0x88, 0x30, 0x8C, 0xC4, 0x2E, + 0xA8, 0xC8, + ]); + config_params.pin_uv_auth_param = pin_uv_auth_param; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); + assert_eq!(persistent_store.has_force_pin_change(), Ok(true)); + } + + #[test] + fn test_process_set_min_pin_length_force_pin_change_explicit() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + let pin_uv_auth_param = Some(vec![ + 0xE3, 0x74, 0xF4, 0x27, 0xBE, 0x7D, 0x40, 0xB5, 0x71, 0xB6, 0xB4, 0x1A, 0xD2, 0xC1, + 0x53, 0xD7, + ]); + let set_min_pin_length_params = SetMinPinLengthParams { + new_min_pin_length: Some(persistent_store.min_pin_length().unwrap()), + min_pin_length_rp_ids: None, + force_change_pin: Some(true), + }; + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::SetMinPinLength, + sub_command_params: Some(ConfigSubCommandParams::SetMinPinLength( + set_min_pin_length_params, + )), + pin_uv_auth_param, + pin_uv_auth_protocol: Some(1), + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.has_force_pin_change(), Ok(true)); + } + #[test] fn test_process_config_vendor_prototype() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6ffd108..8a4479a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1006,6 +1006,10 @@ where ); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); + options_map.insert( + String::from("forcePINChange"), + self.persistent_store.has_force_pin_change()?, + ); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -1256,13 +1260,15 @@ mod test { expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); expected_response.extend( [ - 0x04, 0xA5, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, + 0x04, 0xA6, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, - 0x69, 0x6E, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, 0x4E, 0x4C, - 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, 0x01, 0x08, - 0x18, 0x70, 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, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, 0x18, 0x96, + 0x69, 0x6E, 0xF4, 0x6E, 0x66, 0x6F, 0x72, 0x63, 0x65, 0x50, 0x49, 0x4E, 0x43, 0x68, + 0x61, 0x6E, 0x67, 0x65, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, + 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, + 0x01, 0x08, 0x18, 0x70, 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, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, + 0x18, 0x96, ] .iter(), ); diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 0e46573..ae1b1df 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -328,6 +328,9 @@ impl PinProtocolV1 { 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)?; + if persistent_store.has_force_pin_change()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } // Assuming PIN_TOKEN_LENGTH % block_size == 0 here. let iv = [0u8; 16]; @@ -821,6 +824,28 @@ mod test { ); } + #[test] + fn test_process_get_pin_token_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + 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_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_get_pin_uv_auth_token_using_pin_with_permissions() { let mut rng = ThreadRng256 {}; @@ -885,6 +910,30 @@ mod test { ); } + #[test] + fn test_process_get_pin_token_force_pin_change_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + 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_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), + ); + } + #[test] fn test_process() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index ffc5dd6..74c11a6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -384,7 +384,9 @@ impl PersistentStore { let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; pin_properties[0] = pin_code_point_length; pin_properties[1..].clone_from_slice(pin_hash); - Ok(self.store.insert(key::PIN_PROPERTIES, &pin_properties)?) + self.store.insert(key::PIN_PROPERTIES, &pin_properties)?; + // If this second transaction fails, you are forced to retry. + Ok(self.store.remove(key::FORCE_PIN_CHANGE)?) } /// Returns the number of remaining PIN retries. @@ -541,9 +543,18 @@ impl PersistentStore { Ok(()) } + /// Returns whether the PIN needs to be changed before its next usage. + pub fn has_force_pin_change(&self) -> Result { + match self.store.find(key::FORCE_PIN_CHANGE)? { + None => Ok(false), + Some(value) if value.len() == 1 && value[0] == 1 => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Marks the PIN as outdated with respect to the new PIN policy. pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { - // TODO(kaczmarczyck) implement storage logic - Ok(()) + Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[1])?) } } @@ -1148,6 +1159,18 @@ mod test { } } + #[test] + fn test_force_pin_change() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.has_force_pin_change().unwrap()); + assert_eq!(persistent_store.force_pin_change(), Ok(())); + assert!(persistent_store.has_force_pin_change().unwrap()); + assert_eq!(persistent_store.set_pin(&[0x88; 16], 8), Ok(())); + assert!(!persistent_store.has_force_pin_change().unwrap()); + } + #[test] fn test_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index c6e46e2..1c0e21e 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -88,6 +88,9 @@ make_partition! { /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; + /// If this entry exists and equals 1, the PIN needs to be changed. + FORCE_PIN_CHANGE = 2040; + /// The secret of the CredRandom feature. CRED_RANDOM_SECRET = 2041; From 3408c0a2edf7f6f880803c9d844e779c954a5322 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 21 Jan 2021 18:24:25 +0100 Subject: [PATCH 138/192] makes test_get_info more readable --- src/ctap/mod.rs | 75 +++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8a4479a..6f10589 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1181,7 +1181,7 @@ mod test { MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; - use cbor::{cbor_array, cbor_map}; + use cbor::{cbor_array, cbor_array_vec, cbor_map}; use crypto::rng256::ThreadRng256; const CLOCK_FREQUENCY_HZ: usize = 32768; @@ -1237,43 +1237,44 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); - let mut expected_response = vec![0x00, 0xAD, 0x01]; - // The version array differs with CTAP1, always including 2.0 and 2.1. - #[cfg(not(feature = "with_ctap1"))] - let version_count = 2; - #[cfg(feature = "with_ctap1")] - let version_count = 3; - expected_response.push(0x80 + version_count); - #[cfg(feature = "with_ctap1")] - expected_response.extend(&[0x66, 0x55, 0x32, 0x46, 0x5F, 0x56, 0x32]); - expected_response.extend( - [ - 0x68, 0x46, 0x49, 0x44, 0x4F, 0x5F, 0x32, 0x5F, 0x30, 0x6C, 0x46, 0x49, 0x44, 0x4F, - 0x5F, 0x32, 0x5F, 0x31, 0x5F, 0x50, 0x52, 0x45, 0x02, 0x84, 0x6B, 0x68, 0x6D, 0x61, - 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x63, 0x72, 0x65, 0x64, 0x50, - 0x72, 0x6F, 0x74, 0x65, 0x63, 0x74, 0x6C, 0x6D, 0x69, 0x6E, 0x50, 0x69, 0x6E, 0x4C, - 0x65, 0x6E, 0x67, 0x74, 0x68, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, - 0x03, 0x50, - ] - .iter(), - ); - expected_response.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_response.extend( - [ - 0x04, 0xA6, 0x62, 0x72, 0x6B, 0xF5, 0x62, 0x75, 0x70, 0xF5, 0x68, 0x63, 0x72, 0x65, - 0x64, 0x4D, 0x67, 0x6D, 0x74, 0xF5, 0x69, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x50, - 0x69, 0x6E, 0xF4, 0x6E, 0x66, 0x6F, 0x72, 0x63, 0x65, 0x50, 0x49, 0x4E, 0x43, 0x68, - 0x61, 0x6E, 0x67, 0x65, 0xF4, 0x6F, 0x73, 0x65, 0x74, 0x4D, 0x69, 0x6E, 0x50, 0x49, - 0x4E, 0x4C, 0x65, 0x6E, 0x67, 0x74, 0x68, 0xF5, 0x05, 0x19, 0x04, 0x00, 0x06, 0x81, - 0x01, 0x08, 0x18, 0x70, 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, 0x0D, 0x04, 0x0F, 0x18, 0x20, 0x10, 0x08, 0x14, - 0x18, 0x96, - ] - .iter(), - ); + let expected_cbor = cbor_map_options! { + 0x01 => cbor_array_vec![vec![ + #[cfg(feature = "with_ctap1")] + String::from(U2F_VERSION_STRING), + String::from(FIDO2_VERSION_STRING), + String::from(FIDO2_1_VERSION_STRING), + ]], + 0x02 => cbor_array_vec![vec![ + String::from("hmac-secret"), + String::from("credProtect"), + String::from("minPinLength"), + String::from("credBlob"), + ]], + 0x03 => ctap_state.persistent_store.aaguid().unwrap(), + 0x04 => cbor_map! { + "rk" => true, + "up" => true, + "clientPin" => false, + "credMgmt" => true, + "setMinPINLength" => true, + "forcePINChange" => false, + }, + 0x05 => 1024, + 0x06 => cbor_array_vec![vec![1]], + 0x07 => MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), + 0x08 => CREDENTIAL_ID_SIZE as u64, + 0x09 => cbor_array_vec![vec!["usb"]], + 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], + 0x0C => DEFAULT_CRED_PROTECT.map(|c| c as u64), + 0x0D => ctap_state.persistent_store.min_pin_length().unwrap() as u64, + 0x0F => MAX_CRED_BLOB_LENGTH as u64, + 0x10 => MAX_RP_IDS_LENGTH as u64, + 0x14 => ctap_state.persistent_store.remaining_credentials().unwrap() as u64, + }; - assert_eq!(info_reponse, expected_response); + let mut response_cbor = vec![0x00]; + assert!(cbor::write(expected_cbor, &mut response_cbor)); + assert_eq!(info_reponse, response_cbor); } fn create_minimal_make_credential_parameters() -> AuthenticatorMakeCredentialParameters { From 5fe111698bb6993cd72255075bb6d407c45a837d Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 21 Jan 2021 18:47:00 +0100 Subject: [PATCH 139/192] remove resolved TODO --- src/ctap/config_command.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index e96ce7a..5e4daf3 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -44,7 +44,6 @@ fn process_set_min_pin_length( force_change_pin |= new_min_pin_length > old_length; } if force_change_pin { - // TODO(kaczmarczyck) actually force a PIN change in PinProtocolV1 persistent_store.force_pin_change()?; } persistent_store.set_min_pin_length(new_min_pin_length)?; From c38f00624a1a315d5533952c4be45faf8f1943c5 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 10:55:11 +0100 Subject: [PATCH 140/192] use transactions, and how to store a bool --- src/ctap/pin_protocol_v1.rs | 1 + src/ctap/storage.rs | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index ae1b1df..eb537f0 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -328,6 +328,7 @@ impl PinProtocolV1 { 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)?; + // TODO(kaczmarczyck) can this be moved up in the specification? if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 74c11a6..b85f918 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -30,6 +30,7 @@ use arrayref::array_ref; use cbor::cbor_array_vec; use core::convert::TryInto; use crypto::rng256::Rng256; +use persistent_store::StoreUpdate; // Those constants may be modified before compilation to tune the behavior of the key. // @@ -384,9 +385,15 @@ impl PersistentStore { let mut pin_properties = [0; 1 + PIN_AUTH_LENGTH]; pin_properties[0] = pin_code_point_length; pin_properties[1..].clone_from_slice(pin_hash); - self.store.insert(key::PIN_PROPERTIES, &pin_properties)?; - // If this second transaction fails, you are forced to retry. - Ok(self.store.remove(key::FORCE_PIN_CHANGE)?) + Ok(self.store.transaction(&[ + StoreUpdate::Insert { + key: key::PIN_PROPERTIES, + value: &pin_properties[..], + }, + StoreUpdate::Remove { + key: key::FORCE_PIN_CHANGE, + }, + ])?) } /// Returns the number of remaining PIN retries. @@ -547,14 +554,14 @@ impl PersistentStore { pub fn has_force_pin_change(&self) -> Result { match self.store.find(key::FORCE_PIN_CHANGE)? { None => Ok(false), - Some(value) if value.len() == 1 && value[0] == 1 => Ok(true), + Some(value) if value.is_empty() => Ok(true), _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } } /// Marks the PIN as outdated with respect to the new PIN policy. pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { - Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[1])?) + Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } } From b2c8c5a12879cf8421e2ab5c18a074bf26746ec9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 6 Jan 2021 13:39:59 +0100 Subject: [PATCH 141/192] adds the new command AuthenticatorLargeBlobs --- src/ctap/command.rs | 236 ++++++++++++++++- src/ctap/credential_management.rs | 2 +- src/ctap/large_blobs.rs | 422 ++++++++++++++++++++++++++++++ src/ctap/mod.rs | 12 +- src/ctap/pin_protocol_v1.rs | 2 +- src/ctap/response.rs | 35 +++ src/ctap/storage.rs | 209 +++++++++++++++ src/ctap/storage/key.rs | 5 + 8 files changed, 914 insertions(+), 9 deletions(-) create mode 100644 src/ctap/large_blobs.rs diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 128c4b4..2e1fe3b 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -22,6 +22,7 @@ use super::data_formats::{ }; use super::key_material; use super::status_code::Ctap2StatusCode; +use super::storage::MAX_LARGE_BLOB_ARRAY_SIZE; use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; @@ -33,6 +34,9 @@ use core::convert::TryFrom; // You might also want to set the max credential size in process_get_info then. pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option = None; +// This constant is a consequence of the structure of messages. +const MIN_LARGE_BLOB_LEN: usize = 17; + // CTAP specification (version 20190130) section 6.1 #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub enum Command { @@ -44,8 +48,8 @@ pub enum Command { AuthenticatorGetNextAssertion, AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters), AuthenticatorSelection, + AuthenticatorLargeBlobs(AuthenticatorLargeBlobsParameters), AuthenticatorConfig(AuthenticatorConfigParameters), - // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) // Vendor specific commands AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), } @@ -56,8 +60,6 @@ impl From for Ctap2StatusCode { } } -// TODO: Remove this `allow(dead_code)` once the constants are used. -#[allow(dead_code)] impl Command { const AUTHENTICATOR_MAKE_CREDENTIAL: u8 = 0x01; const AUTHENTICATOR_GET_ASSERTION: u8 = 0x02; @@ -65,8 +67,8 @@ impl Command { const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06; const AUTHENTICATOR_RESET: u8 = 0x07; const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; - // TODO(kaczmarczyck) use or remove those constants - const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; + // Implement Bio Enrollment when your hardware supports biometrics. + const _AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0x0A; const AUTHENTICATOR_SELECTION: u8 = 0x0B; const AUTHENTICATOR_LARGE_BLOBS: u8 = 0x0C; @@ -123,6 +125,12 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorSelection) } + Command::AUTHENTICATOR_LARGE_BLOBS => { + let decoded_cbor = cbor::read(&bytes[1..])?; + Ok(Command::AuthenticatorLargeBlobs( + AuthenticatorLargeBlobsParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_CONFIG => { let decoded_cbor = cbor::read(&bytes[1..])?; Ok(Command::AuthenticatorConfig( @@ -351,6 +359,81 @@ impl TryFrom for AuthenticatorClientPinParameters { } } +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +pub struct AuthenticatorLargeBlobsParameters { + pub get: Option, + pub set: Option>, + pub offset: usize, + pub length: Option, + pub pin_uv_auth_param: Option>, + pub pin_uv_auth_protocol: Option, +} + +impl TryFrom for AuthenticatorLargeBlobsParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + destructure_cbor_map! { + let { + 1 => get, + 2 => set, + 3 => offset, + 4 => length, + 5 => pin_uv_auth_param, + 6 => pin_uv_auth_protocol, + } = extract_map(cbor_value)?; + } + + // careful: some missing parameters here are CTAP1_ERR_INVALID_PARAMETER + let get = get.map(extract_unsigned).transpose()?.map(|u| u as usize); + let set = set.map(extract_byte_string).transpose()?; + let offset = + extract_unsigned(offset.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?)? as usize; + let length = length + .map(extract_unsigned) + .transpose()? + .map(|u| u as usize); + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; + + if get.is_none() && set.is_none() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if get.is_some() && set.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if get.is_some() + && (length.is_some() || pin_uv_auth_param.is_some() || pin_uv_auth_protocol.is_some()) + { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if set.is_some() && offset == 0 { + match length { + None => return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + Some(len) if len > MAX_LARGE_BLOB_ARRAY_SIZE => { + return Err(Ctap2StatusCode::CTAP2_ERR_LARGE_BLOB_STORAGE_FULL) + } + Some(len) if len < MIN_LARGE_BLOB_LEN => { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + } + Some(_) => (), + } + } + if set.is_some() && offset != 0 && length.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + + Ok(AuthenticatorLargeBlobsParameters { + get, + set, + offset, + length, + pin_uv_auth_param, + pin_uv_auth_protocol, + }) + } +} + #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct AuthenticatorConfigParameters { pub sub_command: ConfigSubCommand, @@ -698,6 +781,149 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorSelection)); } + #[test] + fn test_from_cbor_large_blobs_parameters() { + // successful get + let cbor_value = cbor_map! { + 1 => 2, + 3 => 4, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: Some(2), + set: None, + offset: 4, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // successful first set + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 0, + 4 => MIN_LARGE_BLOB_LEN as u64, + 5 => vec! [0xA9], + 6 => 1, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(vec![0x5E]), + offset: 0, + length: Some(MIN_LARGE_BLOB_LEN), + pin_uv_auth_param: Some(vec![0xA9]), + pin_uv_auth_protocol: Some(1), + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // successful next set + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 1, + 5 => vec! [0xA9], + 6 => 1, + }; + let returned_large_blobs_parameters = + AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); + let expected_large_blobs_parameters = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(vec![0x5E]), + offset: 1, + length: None, + pin_uv_auth_param: Some(vec![0xA9]), + pin_uv_auth_protocol: Some(1), + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // failing with neither get nor set + let cbor_value = cbor_map! { + 3 => 4, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with get and set + let cbor_value = cbor_map! { + 1 => 2, + 2 => vec! [0x5E], + 3 => 4, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with get and length + let cbor_value = cbor_map! { + 1 => 2, + 3 => 4, + 4 => MIN_LARGE_BLOB_LEN as u64, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with zero offset and no length present + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 0, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with length smaller than minimum + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 0, + 4 => MIN_LARGE_BLOB_LEN as u64 - 1, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with non-zero offset and length present + let cbor_value = cbor_map! { + 2 => vec! [0x5E], + 3 => 4, + 4 => MIN_LARGE_BLOB_LEN as u64, + 5 => vec! [0xA9], + 6 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + #[test] fn test_vendor_configure() { // Incomplete command diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 7681fa1..7665ea7 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -100,7 +100,7 @@ fn enumerate_credentials_response( public_key: Some(public_key), total_credentials, cred_protect: cred_protect_policy, - // TODO(kaczmarczyck) add when largeBlobKey is implemented + // TODO(kaczmarczyck) add when largeBlobKey extension is implemented large_blob_key: None, ..Default::default() }) diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs new file mode 100644 index 0000000..32934e9 --- /dev/null +++ b/src/ctap/large_blobs.rs @@ -0,0 +1,422 @@ +// Copyright 2020-2021 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::check_pin_uv_auth_protocol; +use super::command::AuthenticatorLargeBlobsParameters; +use super::pin_protocol_v1::{PinPermission, PinProtocolV1}; +use super::response::{AuthenticatorLargeBlobsResponse, ResponseData}; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use alloc::vec; +use alloc::vec::Vec; +use byteorder::{ByteOrder, LittleEndian}; +use crypto::sha256::Sha256; +use crypto::Hash256; + +/// This is maximum message size supported by the authenticator. 1024 is the default. +/// Increasing this values can speed up commands with longer responses, but lead to +/// packets dropping or unexpected failures. +pub const MAX_MSG_SIZE: usize = 1024; +/// The length of the truncated hash that as appended to the large blob data. +const TRUNCATED_HASH_LEN: usize = 16; + +pub struct LargeBlobs { + buffer: Vec, + expected_length: usize, + expected_next_offset: usize, +} + +/// Implements the logic for the AuthenticatorLargeBlobs command and keeps its state. +impl LargeBlobs { + pub fn new() -> LargeBlobs { + LargeBlobs { + buffer: Vec::new(), + expected_length: 0, + expected_next_offset: 0, + } + } + + /// Process the large blob command. + pub fn process_command( + &mut self, + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + large_blobs_params: AuthenticatorLargeBlobsParameters, + ) -> Result { + let AuthenticatorLargeBlobsParameters { + get, + set, + offset, + length, + pin_uv_auth_param, + pin_uv_auth_protocol, + } = large_blobs_params; + + const MAX_FRAGMENT_LENGTH: usize = MAX_MSG_SIZE - 64; + + if let Some(get) = get { + if get > MAX_FRAGMENT_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH); + } + let config = persistent_store.get_large_blob_array(get, offset)?; + return Ok(ResponseData::AuthenticatorLargeBlobs(Some( + AuthenticatorLargeBlobsResponse { config }, + ))); + } + + if let Some(mut set) = set { + if set.len() > MAX_FRAGMENT_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH); + } + if offset == 0 { + // Checks for offset and length are already done in command. + self.expected_length = + length.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + self.expected_next_offset = 0; + } + if offset != self.expected_next_offset { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ); + } + if persistent_store.pin_hash()?.is_some() { + let pin_uv_auth_param = + pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + // TODO(kaczmarczyck) Error codes for PIN protocol differ across commands. + // Change to Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED for None? + check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; + pin_protocol_v1.has_permission(PinPermission::LargeBlobWrite)?; + let mut message = vec![0xFF; 32]; + message.extend(&[0x0C, 0x00]); + let mut offset_bytes = [0u8; 4]; + LittleEndian::write_u32(&mut offset_bytes, offset as u32); + message.extend(&offset_bytes); + message.extend(&Sha256::hash(set.as_slice())); + if !pin_protocol_v1.verify_pin_auth_token(&message, &pin_uv_auth_param) { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + } + if offset + set.len() > self.expected_length { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if offset == 0 { + self.buffer = Vec::with_capacity(self.expected_length); + } + self.buffer.append(&mut set); + self.expected_next_offset = self.buffer.len(); + if self.expected_next_offset == self.expected_length { + self.expected_length = 0; + self.expected_next_offset = 0; + // Must be a positive number. + let buffer_hash_index = self.buffer.len() - TRUNCATED_HASH_LEN; + if Sha256::hash(&self.buffer[..buffer_hash_index])[..TRUNCATED_HASH_LEN] + != self.buffer[buffer_hash_index..] + { + self.buffer = Vec::new(); + return Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE); + } + persistent_store.commit_large_blob_array(&self.buffer)?; + self.buffer = Vec::new(); + } + return Ok(ResponseData::AuthenticatorLargeBlobs(None)); + } + + // This should be unreachable, since the command has either get or set. + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crypto::rng256::ThreadRng256; + + #[test] + fn test_process_command_get_empty() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + let large_blob = vec![ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, + 0x7a, 0x6d, 0x3c, + ]; + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: Some(large_blob.len()), + set: None, + offset: 0, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + match large_blobs_response.unwrap() { + ResponseData::AuthenticatorLargeBlobs(Some(response)) => { + assert_eq!(response.config, large_blob); + } + _ => panic!("Invalid response type"), + }; + } + + #[test] + fn test_process_command_commit_and_get() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 200; + const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN; + let mut large_blob = vec![0x1B; DATA_LEN]; + large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[..BLOB_LEN / 2].to_vec()), + offset: 0, + length: Some(BLOB_LEN), + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[BLOB_LEN / 2..].to_vec()), + offset: BLOB_LEN / 2, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: Some(BLOB_LEN), + set: None, + offset: 0, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + match large_blobs_response.unwrap() { + ResponseData::AuthenticatorLargeBlobs(Some(response)) => { + assert_eq!(response.config, large_blob); + } + _ => panic!("Invalid response type"), + }; + } + + #[test] + fn test_process_command_commit_unexpected_offset() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 200; + const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN; + let mut large_blob = vec![0x1B; DATA_LEN]; + large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[..BLOB_LEN / 2].to_vec()), + offset: 0, + length: Some(BLOB_LEN), + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[BLOB_LEN / 2..].to_vec()), + // The offset is 1 too big. + offset: BLOB_LEN / 2 + 1, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ), + ); + } + + #[test] + fn test_process_command_commit_unexpected_length() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 200; + const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN; + let mut large_blob = vec![0x1B; DATA_LEN]; + large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[..BLOB_LEN / 2].to_vec()), + offset: 0, + // The length is 1 too small. + length: Some(BLOB_LEN - 1), + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob[BLOB_LEN / 2..].to_vec()), + offset: BLOB_LEN / 2, + length: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + ); + } + + #[test] + fn test_process_command_commit_unexpected_hash() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 20; + // This blob does not have an appropriate hash. + let large_blob = vec![0x1B; BLOB_LEN]; + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob.to_vec()), + offset: 0, + length: Some(BLOB_LEN), + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE), + ); + } + + #[test] + fn test_process_command_commit_with_pin() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let mut large_blobs = LargeBlobs::new(); + + const BLOB_LEN: usize = 20; + const DATA_LEN: usize = BLOB_LEN - TRUNCATED_HASH_LEN; + let mut large_blob = vec![0x1B; DATA_LEN]; + large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); + + persistent_store.set_pin(&[0u8; 16], 4).unwrap(); + let pin_uv_auth_param = Some(vec![ + 0x68, 0x0C, 0x3F, 0x6A, 0x62, 0x47, 0xE6, 0x7C, 0x23, 0x1F, 0x79, 0xE3, 0xDC, 0x6D, + 0xC3, 0xDE, + ]); + + let large_blobs_params = AuthenticatorLargeBlobsParameters { + get: None, + set: Some(large_blob), + offset: 0, + length: Some(BLOB_LEN), + pin_uv_auth_param, + pin_uv_auth_protocol: Some(1), + }; + let large_blobs_response = large_blobs.process_command( + &mut persistent_store, + &mut pin_protocol_v1, + large_blobs_params, + ); + assert_eq!( + large_blobs_response, + Ok(ResponseData::AuthenticatorLargeBlobs(None)) + ); + } +} diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6f10589..95b5b0a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -21,6 +21,7 @@ mod ctap1; pub mod data_formats; pub mod hid; mod key_material; +mod large_blobs; mod pin_protocol_v1; pub mod response; pub mod status_code; @@ -41,6 +42,7 @@ use self::data_formats::{ SignatureAlgorithm, }; use self::hid::ChannelID; +use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; use self::pin_protocol_v1::{PinPermission, PinProtocolV1}; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, @@ -293,6 +295,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( pub u2f_up_state: U2fUserPresenceState, // The state initializes to Reset and its timeout, and never goes back to Reset. stateful_command_permission: StatefulPermission, + large_blobs: LargeBlobs, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -318,6 +321,7 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ), stateful_command_permission: StatefulPermission::new_reset(now), + large_blobs: LargeBlobs::new(), } } @@ -484,12 +488,16 @@ where ) } Command::AuthenticatorSelection => self.process_selection(cid), + Command::AuthenticatorLargeBlobs(params) => self.large_blobs.process_command( + &mut self.persistent_store, + &mut self.pin_protocol_v1, + params, + ), Command::AuthenticatorConfig(params) => process_config( &mut self.persistent_store, &mut self.pin_protocol_v1, params, ), - // TODO(kaczmarczyck) implement FIDO 2.1 commands // Vendor specific commands Command::AuthenticatorVendorConfigure(params) => { self.process_vendor_configure(params, cid) @@ -1026,7 +1034,7 @@ where ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), - max_msg_size: Some(1024), + max_msg_size: Some(MAX_MSG_SIZE as u64), pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index eb537f0..6ec5644 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -162,7 +162,7 @@ pub enum PinPermission { GetAssertion = 0x02, CredentialManagement = 0x04, BioEnrollment = 0x08, - PlatformConfiguration = 0x10, + LargeBlobWrite = 0x10, AuthenticatorConfiguration = 0x20, } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index e4cda5e..245218f 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -33,6 +33,7 @@ pub enum ResponseData { AuthenticatorReset, AuthenticatorCredentialManagement(Option), AuthenticatorSelection, + AuthenticatorLargeBlobs(Option), // TODO(kaczmarczyck) dummy, extend AuthenticatorConfig, AuthenticatorVendor(AuthenticatorVendorResponse), @@ -49,6 +50,7 @@ impl From for Option { ResponseData::AuthenticatorReset => None, ResponseData::AuthenticatorCredentialManagement(data) => data.map(|d| d.into()), ResponseData::AuthenticatorSelection => None, + ResponseData::AuthenticatorLargeBlobs(data) => data.map(|d| d.into()), ResponseData::AuthenticatorConfig => None, ResponseData::AuthenticatorVendor(data) => Some(data.into()), } @@ -204,6 +206,22 @@ impl From for cbor::Value { } } +#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +pub struct AuthenticatorLargeBlobsResponse { + pub config: Vec, +} + +impl From for cbor::Value { + fn from(platform_large_blobs_response: AuthenticatorLargeBlobsResponse) -> Self { + let AuthenticatorLargeBlobsResponse { config } = platform_large_blobs_response; + + cbor_map_options! { + 0x01 => config, + } + } +} + #[derive(Default)] #[cfg_attr(test, derive(PartialEq))] #[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] @@ -510,6 +528,23 @@ mod test { assert_eq!(response_cbor, None); } + #[test] + fn test_large_blobs_into_cbor() { + let large_blobs_response = AuthenticatorLargeBlobsResponse { config: vec![0xC0] }; + let response_cbor: Option = + ResponseData::AuthenticatorLargeBlobs(Some(large_blobs_response)).into(); + let expected_cbor = cbor_map_options! { + 0x01 => vec![0xC0], + }; + assert_eq!(response_cbor, Some(expected_cbor)); + } + + #[test] + fn test_empty_large_blobs_into_cbor() { + let response_cbor: Option = ResponseData::AuthenticatorLargeBlobs(None).into(); + assert_eq!(response_cbor, None); + } + #[test] fn test_config_into_cbor() { let response_cbor: Option = ResponseData::AuthenticatorConfig.into(); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b85f918..a89c02d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -28,6 +28,7 @@ use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; use cbor::cbor_array_vec; +use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; use persistent_store::StoreUpdate; @@ -59,6 +60,9 @@ const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. pub const MAX_RP_IDS_LENGTH: usize = 8; +const SHARD_SIZE: usize = 128; +pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = + SHARD_SIZE * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start); /// Wrapper for master keys. pub struct MasterKeys { @@ -467,6 +471,70 @@ impl PersistentStore { )?) } + /// Reads the byte vector stored as the serialized large blobs array. + /// + /// If more data is requested than stored, return as many bytes as possible. + pub fn get_large_blob_array( + &self, + mut byte_count: usize, + mut offset: usize, + ) -> Result, Ctap2StatusCode> { + if self.store.find(key::LARGE_BLOB_SHARDS.start)?.is_none() { + return Ok(vec![ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, + 0x7a, 0x6d, 0x3c, + ]); + } + let mut output = Vec::with_capacity(byte_count); + while byte_count > 0 { + let shard = offset / SHARD_SIZE; + let shard_offset = offset % SHARD_SIZE; + let shard_length = cmp::min(SHARD_SIZE - shard_offset, byte_count); + + let shard_key = key::LARGE_BLOB_SHARDS.start + shard; + if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { + // This request should have been caught at application level. + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let shard_entry = self.store.find(shard_key)?.unwrap_or_default(); + if shard_entry.len() < shard_offset + shard_length { + output.extend(&shard_entry[..]); + return Ok(output); + } + output.extend(&shard_entry[shard_offset..shard_offset + shard_length]); + offset += shard_length; + byte_count -= shard_length; + } + Ok(output) + } + + /// Sets a byte vector as the serialized large blobs array. + pub fn commit_large_blob_array( + &mut self, + large_blob_array: &[u8], + ) -> Result<(), Ctap2StatusCode> { + let mut large_blob_index = 0; + let mut shard_key = key::LARGE_BLOB_SHARDS.start; + while large_blob_index < large_blob_array.len() { + if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index); + self.store.insert( + shard_key, + &large_blob_array[large_blob_index..large_blob_index + shard_length], + )?; + large_blob_index += shard_length; + shard_key += 1; + } + // The length is not stored, so overwrite old entries explicitly. + for key in shard_key..key::LARGE_BLOB_SHARDS.end { + // Assuming the store optimizes out unnecessary writes. + self.store.remove(key)?; + } + Ok(()) + } + /// Returns the attestation private key if defined. pub fn attestation_private_key( &self, @@ -1144,6 +1212,147 @@ mod test { assert_eq!(persistent_store.min_pin_length_rp_ids().unwrap(), rp_ids); } + #[test] + #[allow(clippy::assertions_on_constants)] + fn test_max_large_blob_array_size() { + assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); + } + + #[test] + fn test_commit_get_large_blob_array_1_shard() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let large_blob_array = vec![0xC0; 1]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store.get_large_blob_array(1, 0).unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + + let large_blob_array = vec![0xC0; SHARD_SIZE]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + } + + #[test] + fn test_commit_get_large_blob_array_2_shards() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let large_blob_array = vec![0xC0; SHARD_SIZE + 1]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE, 0) + .unwrap(); + assert_eq!( + large_blob_array[..SHARD_SIZE], + restored_large_blob_array[..] + ); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + + let large_blob_array = vec![0xC0; 2 * SHARD_SIZE]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(2 * SHARD_SIZE, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store + .get_large_blob_array(2 * SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + } + + #[test] + fn test_commit_get_large_blob_array_3_shards() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let mut large_blob_array = vec![0x11; SHARD_SIZE]; + large_blob_array.extend([0x22; SHARD_SIZE].iter()); + large_blob_array.extend([0x33; 1].iter()); + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(2 * SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store + .get_large_blob_array(3 * SHARD_SIZE, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let shard1 = persistent_store + .get_large_blob_array(SHARD_SIZE, 0) + .unwrap(); + let shard2 = persistent_store + .get_large_blob_array(SHARD_SIZE, SHARD_SIZE) + .unwrap(); + let shard3 = persistent_store + .get_large_blob_array(1, 2 * SHARD_SIZE) + .unwrap(); + assert_eq!(large_blob_array[..SHARD_SIZE], shard1[..]); + assert_eq!(large_blob_array[SHARD_SIZE..2 * SHARD_SIZE], shard2[..]); + assert_eq!(large_blob_array[2 * SHARD_SIZE..], shard3[..]); + let shard12 = persistent_store + .get_large_blob_array(2, SHARD_SIZE - 1) + .unwrap(); + let shard23 = persistent_store + .get_large_blob_array(2, 2 * SHARD_SIZE - 1) + .unwrap(); + assert_eq!(vec![0x11, 0x22], shard12); + assert_eq!(vec![0x22, 0x33], shard23); + } + + #[test] + fn test_commit_get_large_blob_array_overwrite() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + let large_blob_array = vec![0x11; SHARD_SIZE + 1]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let large_blob_array = vec![0x22; SHARD_SIZE]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE + 1, 0) + .unwrap(); + assert_eq!(large_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store + .get_large_blob_array(1, SHARD_SIZE) + .unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); + + assert!(persistent_store.commit_large_blob_array(&[]).is_ok()); + let restored_large_blob_array = persistent_store + .get_large_blob_array(SHARD_SIZE + 1, 0) + .unwrap(); + let empty_blob_array = vec![ + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, + 0x7a, 0x6d, 0x3c, + ]; + assert_eq!(empty_blob_array, restored_large_blob_array); + } + #[test] fn test_global_signature_counter() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 1c0e21e..4f6ba51 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -88,6 +88,11 @@ make_partition! { /// board may configure `MAX_SUPPORTED_RESIDENT_KEYS` depending on the storage size. CREDENTIALS = 1700..2000; + /// Storage for the serialized large blob array. + /// + /// The stored large blob can be too big for one key, so it has to be sharded. + LARGE_BLOB_SHARDS = 2000..2016; + /// If this entry exists and equals 1, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; From 3517b1163d9e76243923153d743f33c8977cc15f Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 13:48:27 +0100 Subject: [PATCH 142/192] bigger shards, fixed get_large_blob --- src/ctap/large_blobs.rs | 4 ++-- src/ctap/storage.rs | 27 +++++++++++++++++---------- src/ctap/storage/key.rs | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index 32934e9..6dc80ce 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -150,8 +150,8 @@ mod test { let mut large_blobs = LargeBlobs::new(); let large_blob = vec![ - 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, - 0x7a, 0x6d, 0x3c, + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, ]; let large_blobs_params = AuthenticatorLargeBlobsParameters { get: Some(large_blob.len()), diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a89c02d..9b6ce31 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -60,7 +60,7 @@ const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. pub const MAX_RP_IDS_LENGTH: usize = 8; -const SHARD_SIZE: usize = 128; +const SHARD_SIZE: usize = 1023; pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = SHARD_SIZE * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start); @@ -473,7 +473,8 @@ impl PersistentStore { /// Reads the byte vector stored as the serialized large blobs array. /// - /// If more data is requested than stored, return as many bytes as possible. + /// If too few bytes exist at that offset, return the maximum number + /// available. This includes cases of offset being beyond the stored array. pub fn get_large_blob_array( &self, mut byte_count: usize, @@ -481,24 +482,24 @@ impl PersistentStore { ) -> Result, Ctap2StatusCode> { if self.store.find(key::LARGE_BLOB_SHARDS.start)?.is_none() { return Ok(vec![ - 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, - 0x7a, 0x6d, 0x3c, + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, ]); } let mut output = Vec::with_capacity(byte_count); while byte_count > 0 { - let shard = offset / SHARD_SIZE; let shard_offset = offset % SHARD_SIZE; let shard_length = cmp::min(SHARD_SIZE - shard_offset, byte_count); - let shard_key = key::LARGE_BLOB_SHARDS.start + shard; + let shard_key = key::LARGE_BLOB_SHARDS.start + offset / SHARD_SIZE; if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { // This request should have been caught at application level. return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } let shard_entry = self.store.find(shard_key)?.unwrap_or_default(); if shard_entry.len() < shard_offset + shard_length { - output.extend(&shard_entry[..]); + // If fewer bytes exist than requested, return them all. + output.extend(&shard_entry[shard_offset..]); return Ok(output); } output.extend(&shard_entry[shard_offset..shard_offset + shard_length]); @@ -529,7 +530,7 @@ impl PersistentStore { } // The length is not stored, so overwrite old entries explicitly. for key in shard_key..key::LARGE_BLOB_SHARDS.end { - // Assuming the store optimizes out unnecessary writes. + // Assuming the store optimizes out unnecessary removes. self.store.remove(key)?; } Ok(()) @@ -1223,12 +1224,18 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let large_blob_array = vec![0xC0; 1]; + let large_blob_array = vec![0x01, 0x02, 0x03]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store.get_large_blob_array(1, 0).unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); + assert_eq!(vec![0x01], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(1, 1).unwrap(); + assert_eq!(vec![0x02], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(1, 2).unwrap(); + assert_eq!(vec![0x03], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(2, 2).unwrap(); + assert_eq!(vec![0x03], restored_large_blob_array); let large_blob_array = vec![0xC0; SHARD_SIZE]; assert!(persistent_store diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 4f6ba51..2093685 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -91,7 +91,7 @@ make_partition! { /// Storage for the serialized large blob array. /// /// The stored large blob can be too big for one key, so it has to be sharded. - LARGE_BLOB_SHARDS = 2000..2016; + LARGE_BLOB_SHARDS = 2000..2004; /// If this entry exists and equals 1, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; From cf8b54b39c1f0ab7b5232ba4af8572628b46fc4b Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 14:16:34 +0100 Subject: [PATCH 143/192] large blob commit is one transaction --- src/ctap/storage.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 9b6ce31..0b66f4f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -514,26 +514,25 @@ impl PersistentStore { &mut self, large_blob_array: &[u8], ) -> Result<(), Ctap2StatusCode> { - let mut large_blob_index = 0; - let mut shard_key = key::LARGE_BLOB_SHARDS.start; - while large_blob_index < large_blob_array.len() { - if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + const MIN_SHARD_KEY: usize = key::LARGE_BLOB_SHARDS.start; + const SHARD_COUNT: usize = key::LARGE_BLOB_SHARDS.end - MIN_SHARD_KEY; + let mut transactions = Vec::with_capacity(SHARD_COUNT); + for shard_key in MIN_SHARD_KEY..key::LARGE_BLOB_SHARDS.end { + let large_blob_index = (shard_key - MIN_SHARD_KEY) * SHARD_SIZE; + if large_blob_array.len() > large_blob_index { + let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index); + transactions.push(StoreUpdate::Insert { + key: shard_key, + value: &large_blob_array[large_blob_index..large_blob_index + shard_length], + }); + } else { + transactions.push(StoreUpdate::Remove { key: shard_key }); } - let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index); - self.store.insert( - shard_key, - &large_blob_array[large_blob_index..large_blob_index + shard_length], - )?; - large_blob_index += shard_length; - shard_key += 1; } - // The length is not stored, so overwrite old entries explicitly. - for key in shard_key..key::LARGE_BLOB_SHARDS.end { - // Assuming the store optimizes out unnecessary removes. - self.store.remove(key)?; - } - Ok(()) + Ok(self.store.transaction(&transactions)?) } /// Returns the attestation private key if defined. From 7d04c5c6d0140115ea5914469f8b09365385ece6 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 14:23:32 +0100 Subject: [PATCH 144/192] fixes const usage in test_get_info --- src/ctap/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 95b5b0a..8fa622c 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1267,7 +1267,7 @@ mod test { "setMinPINLength" => true, "forcePINChange" => false, }, - 0x05 => 1024, + 0x05 => MAX_MSG_SIZE as u64, 0x06 => cbor_array_vec![vec![1]], 0x07 => MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), 0x08 => CREDENTIAL_ID_SIZE as u64, From 19c089e955547d34ce5857c1661f5f62252e07e7 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 22 Jan 2021 18:54:45 +0100 Subject: [PATCH 145/192] improvements to large blob storage --- src/ctap/large_blobs.rs | 2 +- src/ctap/storage.rs | 188 +++++++++++++++++++++++++--------------- 2 files changed, 120 insertions(+), 70 deletions(-) diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index 6dc80ce..ab38df0 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -69,7 +69,7 @@ impl LargeBlobs { if get > MAX_FRAGMENT_LENGTH { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_LENGTH); } - let config = persistent_store.get_large_blob_array(get, offset)?; + let config = persistent_store.get_large_blob_array(offset, get)?; return Ok(ResponseData::AuthenticatorLargeBlobs(Some( AuthenticatorLargeBlobsResponse { config }, ))); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0b66f4f..934d533 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -60,9 +60,7 @@ const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. pub const MAX_RP_IDS_LENGTH: usize = 8; -const SHARD_SIZE: usize = 1023; -pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = - SHARD_SIZE * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start); +pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = 2048; /// Wrapper for master keys. pub struct MasterKeys { @@ -471,38 +469,55 @@ impl PersistentStore { )?) } + /// The size used for shards of large blobs. + /// + /// This value is constant during the lifetime of the device. + fn shard_size(&self) -> usize { + self.store.max_value_length() + } + /// Reads the byte vector stored as the serialized large blobs array. /// /// If too few bytes exist at that offset, return the maximum number /// available. This includes cases of offset being beyond the stored array. + /// + /// If no large blob is committed to the store, get responds as if an empty + /// CBOR array (0x80) was written, together with the 16 byte prefix of its + /// SHA256, to a total length of 17 byte (which is the shortest legitemate + /// large blob entry possible). pub fn get_large_blob_array( &self, - mut byte_count: usize, mut offset: usize, + mut byte_count: usize, ) -> Result, Ctap2StatusCode> { - if self.store.find(key::LARGE_BLOB_SHARDS.start)?.is_none() { - return Ok(vec![ - 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, - 0x7A, 0x6D, 0x3C, - ]); - } let mut output = Vec::with_capacity(byte_count); while byte_count > 0 { - let shard_offset = offset % SHARD_SIZE; - let shard_length = cmp::min(SHARD_SIZE - shard_offset, byte_count); - - let shard_key = key::LARGE_BLOB_SHARDS.start + offset / SHARD_SIZE; + let shard_key = key::LARGE_BLOB_SHARDS.start + offset / self.shard_size(); if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { // This request should have been caught at application level. return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - let shard_entry = self.store.find(shard_key)?.unwrap_or_default(); - if shard_entry.len() < shard_offset + shard_length { - // If fewer bytes exist than requested, return them all. - output.extend(&shard_entry[shard_offset..]); - return Ok(output); + let shard_entry = self.store.find(shard_key)?; + let shard_entry = if shard_key == key::LARGE_BLOB_SHARDS.start { + shard_entry.unwrap_or_else(|| { + vec![ + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, + 0x6F, 0xA5, 0x7A, 0x6D, 0x3C, + ] + }) + } else { + shard_entry.unwrap_or_default() + }; + + let shard_offset = offset % self.shard_size(); + if shard_entry.len() < shard_offset { + break; + } + let shard_length = cmp::min(shard_entry.len() - shard_offset, byte_count); + output.extend(&shard_entry[shard_offset..][..shard_length]); + if shard_entry.len() < self.shard_size() { + break; } - output.extend(&shard_entry[shard_offset..shard_offset + shard_length]); offset += shard_length; byte_count -= shard_length; } @@ -517,22 +532,18 @@ impl PersistentStore { if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - const MIN_SHARD_KEY: usize = key::LARGE_BLOB_SHARDS.start; - const SHARD_COUNT: usize = key::LARGE_BLOB_SHARDS.end - MIN_SHARD_KEY; - let mut transactions = Vec::with_capacity(SHARD_COUNT); - for shard_key in MIN_SHARD_KEY..key::LARGE_BLOB_SHARDS.end { - let large_blob_index = (shard_key - MIN_SHARD_KEY) * SHARD_SIZE; - if large_blob_array.len() > large_blob_index { - let shard_length = cmp::min(SHARD_SIZE, large_blob_array.len() - large_blob_index); - transactions.push(StoreUpdate::Insert { - key: shard_key, - value: &large_blob_array[large_blob_index..large_blob_index + shard_length], - }); - } else { - transactions.push(StoreUpdate::Remove { key: shard_key }); - } + + let mut shards = large_blob_array.chunks(self.shard_size()); + let mut updates = Vec::with_capacity(shards.len()); + for key in key::LARGE_BLOB_SHARDS { + let update = match shards.next() { + Some(value) => StoreUpdate::Insert { key, value }, + None if self.store.find(key)?.is_some() => StoreUpdate::Remove { key }, + _ => break, + }; + updates.push(update); } - Ok(self.store.transaction(&transactions)?) + Ok(self.store.transaction(&updates)?) } /// Returns the attestation private key if defined. @@ -1213,9 +1224,19 @@ mod test { } #[test] - #[allow(clippy::assertions_on_constants)] fn test_max_large_blob_array_size() { - assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); + let mut rng = ThreadRng256 {}; + let persistent_store = PersistentStore::new(&mut rng); + + #[allow(clippy::assertions_on_constants)] + { + assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); + } + assert!( + MAX_LARGE_BLOB_ARRAY_SIZE + <= persistent_store.shard_size() + * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start) + ); } #[test] @@ -1227,25 +1248,29 @@ mod test { assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); - let restored_large_blob_array = persistent_store.get_large_blob_array(1, 0).unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 1).unwrap(); assert_eq!(vec![0x01], restored_large_blob_array); let restored_large_blob_array = persistent_store.get_large_blob_array(1, 1).unwrap(); assert_eq!(vec![0x02], restored_large_blob_array); - let restored_large_blob_array = persistent_store.get_large_blob_array(1, 2).unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(2, 1).unwrap(); assert_eq!(vec![0x03], restored_large_blob_array); let restored_large_blob_array = persistent_store.get_large_blob_array(2, 2).unwrap(); assert_eq!(vec![0x03], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(3, 1).unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(4, 1).unwrap(); + assert_eq!(Vec::::new(), restored_large_blob_array); - let large_blob_array = vec![0xC0; SHARD_SIZE]; + let large_blob_array = vec![0xC0; persistent_store.shard_size()]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE, 0) + .get_large_blob_array(0, persistent_store.shard_size()) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE + 1, 0) + .get_large_blob_array(0, persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); } @@ -1255,32 +1280,32 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let large_blob_array = vec![0xC0; SHARD_SIZE + 1]; + let large_blob_array = vec![0xC0; persistent_store.shard_size() + 1]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE, 0) + .get_large_blob_array(0, persistent_store.shard_size()) .unwrap(); assert_eq!( - large_blob_array[..SHARD_SIZE], + large_blob_array[..persistent_store.shard_size()], restored_large_blob_array[..] ); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE + 1, 0) + .get_large_blob_array(0, persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); - let large_blob_array = vec![0xC0; 2 * SHARD_SIZE]; + let large_blob_array = vec![0xC0; 2 * persistent_store.shard_size()]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(2 * SHARD_SIZE, 0) + .get_large_blob_array(0, 2 * persistent_store.shard_size()) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store - .get_large_blob_array(2 * SHARD_SIZE + 1, 0) + .get_large_blob_array(0, 2 * persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); } @@ -1290,37 +1315,46 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let mut large_blob_array = vec![0x11; SHARD_SIZE]; - large_blob_array.extend([0x22; SHARD_SIZE].iter()); - large_blob_array.extend([0x33; 1].iter()); + let mut large_blob_array = vec![0x11; persistent_store.shard_size()]; + large_blob_array.extend(vec![0x22; persistent_store.shard_size()]); + large_blob_array.extend(&[0x33; 1]); assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(2 * SHARD_SIZE + 1, 0) + .get_large_blob_array(0, 2 * persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store - .get_large_blob_array(3 * SHARD_SIZE, 0) + .get_large_blob_array(0, 3 * persistent_store.shard_size()) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let shard1 = persistent_store - .get_large_blob_array(SHARD_SIZE, 0) + .get_large_blob_array(0, persistent_store.shard_size()) .unwrap(); let shard2 = persistent_store - .get_large_blob_array(SHARD_SIZE, SHARD_SIZE) + .get_large_blob_array(persistent_store.shard_size(), persistent_store.shard_size()) .unwrap(); let shard3 = persistent_store - .get_large_blob_array(1, 2 * SHARD_SIZE) + .get_large_blob_array(2 * persistent_store.shard_size(), 1) .unwrap(); - assert_eq!(large_blob_array[..SHARD_SIZE], shard1[..]); - assert_eq!(large_blob_array[SHARD_SIZE..2 * SHARD_SIZE], shard2[..]); - assert_eq!(large_blob_array[2 * SHARD_SIZE..], shard3[..]); + assert_eq!( + large_blob_array[..persistent_store.shard_size()], + shard1[..] + ); + assert_eq!( + large_blob_array[persistent_store.shard_size()..2 * persistent_store.shard_size()], + shard2[..] + ); + assert_eq!( + large_blob_array[2 * persistent_store.shard_size()..], + shard3[..] + ); let shard12 = persistent_store - .get_large_blob_array(2, SHARD_SIZE - 1) + .get_large_blob_array(persistent_store.shard_size() - 1, 2) .unwrap(); let shard23 = persistent_store - .get_large_blob_array(2, 2 * SHARD_SIZE - 1) + .get_large_blob_array(2 * persistent_store.shard_size() - 1, 2) .unwrap(); assert_eq!(vec![0x11, 0x22], shard12); assert_eq!(vec![0x22, 0x33], shard23); @@ -1331,32 +1365,48 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let large_blob_array = vec![0x11; SHARD_SIZE + 1]; + let large_blob_array = vec![0x11; persistent_store.shard_size() + 1]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); - let large_blob_array = vec![0x22; SHARD_SIZE]; + let large_blob_array = vec![0x22; persistent_store.shard_size()]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE + 1, 0) + .get_large_blob_array(0, persistent_store.shard_size() + 1) .unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store - .get_large_blob_array(1, SHARD_SIZE) + .get_large_blob_array(persistent_store.shard_size(), 1) .unwrap(); assert_eq!(Vec::::new(), restored_large_blob_array); assert!(persistent_store.commit_large_blob_array(&[]).is_ok()); let restored_large_blob_array = persistent_store - .get_large_blob_array(SHARD_SIZE + 1, 0) + .get_large_blob_array(0, persistent_store.shard_size() + 1) .unwrap(); + // Committing an empty array resets to the default blob of 17 byte. + assert_eq!(restored_large_blob_array.len(), 17); + } + + #[test] + fn test_commit_get_large_blob_array_no_commit() { + let mut rng = ThreadRng256 {}; + let persistent_store = PersistentStore::new(&mut rng); + let empty_blob_array = vec![ - 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, - 0x7a, 0x6d, 0x3c, + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, ]; + let restored_large_blob_array = persistent_store + .get_large_blob_array(0, persistent_store.shard_size()) + .unwrap(); assert_eq!(empty_blob_array, restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 1).unwrap(); + assert_eq!(vec![0x80], restored_large_blob_array); + let restored_large_blob_array = persistent_store.get_large_blob_array(16, 1).unwrap(); + assert_eq!(vec![0x3C], restored_large_blob_array); } #[test] From f0c51950cb92be099e76ad51b2c8758464399a2b Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Fri, 22 Jan 2021 19:19:52 +0100 Subject: [PATCH 146/192] Add fragmentation support --- libraries/persistent_store/src/fragment.rs | 179 ++++++++++++++++++ libraries/persistent_store/src/lib.rs | 1 + libraries/persistent_store/src/store.rs | 22 ++- libraries/persistent_store/tests/config.rs | 49 +++++ libraries/persistent_store/tests/fragment.rs | 188 +++++++++++++++++++ 5 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 libraries/persistent_store/src/fragment.rs create mode 100644 libraries/persistent_store/tests/config.rs create mode 100644 libraries/persistent_store/tests/fragment.rs diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs new file mode 100644 index 0000000..5bab46f --- /dev/null +++ b/libraries/persistent_store/src/fragment.rs @@ -0,0 +1,179 @@ +// Copyright 2021 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. + +//! Helper functions for fragmented entries. +//! +//! This module permits to handle entries larger than the [maximum value +//! length](Store::max_value_length) by storing ordered consecutive fragments in a sequence of keys. +//! The first keys hold fragments of maximal length, followed by a possibly partial fragment. The +//! remaining keys are not used. + +use crate::{Storage, Store, StoreError, StoreHandle, StoreResult, StoreUpdate}; +use alloc::vec::Vec; +use core::ops::Range; + +/// Represents a sequence of keys. +pub trait Keys { + /// Returns the number of keys. + fn len(&self) -> usize; + + /// Returns the position of a key in the sequence. + fn pos(&self, key: usize) -> Option; + + /// Returns the key of a position in the sequence. + /// + /// # Preconditions + /// + /// The position must be within the length: `pos < len()`. + fn key(&self, pos: usize) -> usize; +} + +impl Keys for Range { + fn len(&self) -> usize { + self.end - self.start + } + + fn pos(&self, key: usize) -> Option { + if self.start <= key && key < self.end { + Some(key - self.start) + } else { + None + } + } + + fn key(&self, pos: usize) -> usize { + debug_assert!(pos < Keys::len(self)); + self.start + pos + } +} + +/// Reads the concatenated value of a sequence of keys. +pub fn read(store: &Store, keys: &impl Keys) -> StoreResult>> { + let handles = get_handles(store, keys)?; + if handles.is_empty() { + return Ok(None); + } + let mut result = Vec::with_capacity(handles.len() * store.max_value_length()); + for handle in handles { + result.extend(handle.get_value(store)?); + } + Ok(Some(result)) +} + +/// Reads a range from the concatenated value of a sequence of keys. +/// +/// This is equivalent to calling [`read`] then taking the range except that: +/// - Only the needed chunks are read. +/// - The range is truncated to fit in the value. +pub fn read_range( + store: &Store, + keys: &impl Keys, + range: Range, +) -> StoreResult>> { + let range_len = match range.end.checked_sub(range.start) { + None => return Err(StoreError::InvalidArgument), + Some(x) => x, + }; + let handles = get_handles(store, keys)?; + if handles.is_empty() { + return Ok(None); + } + let mut result = Vec::with_capacity(range_len); + let mut offset = 0; + for handle in handles { + let start = range.start.saturating_sub(offset); + let length = handle.get_length(store)?; + let end = core::cmp::min(range.end.saturating_sub(offset), length); + offset += length; + if start < end && end <= length { + result.extend(&handle.get_value(store)?[start..end]); + } + } + Ok(Some(result)) +} + +/// Writes a value to a sequence of keys as chunks. +pub fn write(store: &mut Store, keys: &impl Keys, value: &[u8]) -> StoreResult<()> { + let handles = get_handles(store, keys)?; + let keys_len = keys.len(); + let mut updates = Vec::with_capacity(keys_len); + let mut chunks = value.chunks(store.max_value_length()); + for pos in 0..keys_len { + let key = keys.key(pos); + match (handles.get(pos), chunks.next()) { + // No existing handle and no new chunk: nothing to do. + (None, None) => (), + // Existing handle and no new chunk: remove old handle. + (Some(_), None) => updates.push(StoreUpdate::Remove { key }), + // Existing handle with same value as new chunk: nothing to do. + (Some(handle), Some(value)) if handle.get_value(store)? == value => (), + // New chunk: Write (or overwrite) the new value. + (_, Some(value)) => updates.push(StoreUpdate::Insert { key, value }), + } + } + if chunks.next().is_some() { + // The value is too long. + return Err(StoreError::InvalidArgument); + } + store.transaction(&updates) +} + +/// Deletes the value of a sequence of keys. +pub fn delete(store: &mut Store, keys: &impl Keys) -> StoreResult<()> { + let updates: Vec>> = get_handles(store, keys)? + .iter() + .map(|handle| StoreUpdate::Remove { + key: handle.get_key(), + }) + .collect(); + store.transaction(&updates) +} + +/// Returns the handles of a sequence of keys. +/// +/// The handles are truncated to the keys that are present. +fn get_handles(store: &Store, keys: &impl Keys) -> StoreResult> { + let keys_len = keys.len(); + let mut handles: Vec> = vec![None; keys_len as usize]; + for handle in store.iter()? { + let handle = handle?; + let pos = match keys.pos(handle.get_key()) { + Some(pos) => pos, + None => continue, + }; + if pos >= keys_len { + return Err(StoreError::InvalidArgument); + } + if let Some(old_handle) = &handles[pos] { + if old_handle.get_key() != handle.get_key() { + // The user provided a non-injective `pos` function. + return Err(StoreError::InvalidArgument); + } else { + return Err(StoreError::InvalidStorage); + } + } + handles[pos] = Some(handle); + } + let num_handles = handles.iter().filter(|x| x.is_some()).count(); + let mut result = Vec::with_capacity(num_handles); + for (i, handle) in handles.into_iter().enumerate() { + match (i < num_handles, handle) { + (true, Some(handle)) => result.push(handle), + (false, None) => (), + // We should have `num_handles` Somes followed by Nones. + _ => return Err(StoreError::InvalidStorage), + } + } + Ok(result) +} diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index 06a3a68..41acbaf 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -354,6 +354,7 @@ mod buffer; #[cfg(feature = "std")] mod driver; mod format; +pub mod fragment; #[cfg(feature = "std")] mod model; mod storage; diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index 224eeb9..2630950 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -100,7 +100,7 @@ pub type StoreResult = Result; /// /// [capacity]: struct.Store.html#method.capacity /// [lifetime]: struct.Store.html#method.lifetime -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct StoreRatio { /// How much of the metric is used. pub(crate) used: Nat, @@ -148,6 +148,15 @@ impl StoreHandle { self.key as usize } + /// Returns the length of value of the entry. + /// + /// # Errors + /// + /// Returns `InvalidArgument` if the entry has been deleted or compacted. + pub fn get_length(&self, store: &Store) -> StoreResult { + store.get_length(self) + } + /// Returns the value of the entry. /// /// # Errors @@ -446,6 +455,17 @@ impl Store { self.format.max_value_len() as usize } + /// Returns the length of the value of an entry given its handle. + fn get_length(&self, handle: &StoreHandle) -> StoreResult { + self.check_handle(handle)?; + let mut pos = handle.pos; + match self.parse_entry(&mut pos)? { + ParsedEntry::User(header) => Ok(header.length as usize), + ParsedEntry::Padding => Err(StoreError::InvalidArgument), + _ => Err(StoreError::InvalidStorage), + } + } + /// Returns the value of an entry given its handle. fn get_value(&self, handle: &StoreHandle) -> StoreResult> { self.check_handle(handle)?; diff --git a/libraries/persistent_store/tests/config.rs b/libraries/persistent_store/tests/config.rs new file mode 100644 index 0000000..8812743 --- /dev/null +++ b/libraries/persistent_store/tests/config.rs @@ -0,0 +1,49 @@ +// Copyright 2021 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 persistent_store::{BufferOptions, BufferStorage, Store, StoreDriverOff}; + +#[derive(Clone)] +pub struct Config { + word_size: usize, + page_size: usize, + num_pages: usize, + max_word_writes: usize, + max_page_erases: usize, +} + +impl Config { + pub fn new_driver(&self) -> StoreDriverOff { + let options = BufferOptions { + word_size: self.word_size, + page_size: self.page_size, + max_word_writes: self.max_word_writes, + max_page_erases: self.max_page_erases, + strict_mode: true, + }; + StoreDriverOff::new(options, self.num_pages) + } + + pub fn new_store(&self) -> Store { + self.new_driver().power_on().unwrap().extract_store() + } +} + +pub const MINIMAL: Config = Config { + word_size: 4, + page_size: 64, + num_pages: 5, + max_word_writes: 2, + max_page_erases: 9, +}; diff --git a/libraries/persistent_store/tests/fragment.rs b/libraries/persistent_store/tests/fragment.rs new file mode 100644 index 0000000..fd045bc --- /dev/null +++ b/libraries/persistent_store/tests/fragment.rs @@ -0,0 +1,188 @@ +// Copyright 2021 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 persistent_store::fragment; + +mod config; + +#[test] +fn read_empty_entry() { + let store = config::MINIMAL.new_store(); + assert_eq!(fragment::read(&store, &(0..4)), Ok(None)); +} + +#[test] +fn read_single_chunk() { + let mut store = config::MINIMAL.new_store(); + let value = b"hello".to_vec(); + assert_eq!(store.insert(0, &value), Ok(())); + assert_eq!(fragment::read(&store, &(0..4)), Ok(Some(value))); +} + +#[test] +fn read_multiple_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(fragment::read(&store, &(0..4)), Ok(Some(value))); +} + +#[test] +fn read_range_first_chunk() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + fragment::read_range(&store, &(0..4), 0..10), + Ok(Some((0..10).collect())) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 10..20), + Ok(Some((10..20).collect())) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 40..52), + Ok(Some((40..52).collect())) + ); +} + +#[test] +fn read_range_second_chunk() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + fragment::read_range(&store, &(0..4), 52..53), + Ok(Some(vec![52])) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 53..54), + Ok(Some(vec![53])) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 59..60), + Ok(Some(vec![59])) + ); +} + +#[test] +fn read_range_both_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + fragment::read_range(&store, &(0..4), 40..60), + Ok(Some((40..60).collect())) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 0..60), + Ok(Some((0..60).collect())) + ); +} + +#[test] +fn read_range_outside() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + fragment::read_range(&store, &(0..4), 40..100), + Ok(Some((40..60).collect())) + ); + assert_eq!( + fragment::read_range(&store, &(0..4), 60..100), + Ok(Some(vec![])) + ); +} + +#[test] +fn write_single_chunk() { + let mut store = config::MINIMAL.new_store(); + let value = b"hello".to_vec(); + assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some(value))); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} + +#[test] +fn write_multiple_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some((0..52).collect()))); + assert_eq!(store.find(1), Ok(Some((52..60).collect()))); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} + +#[test] +fn overwrite_less_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + let value: Vec<_> = (42..69).collect(); + assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some((42..69).collect()))); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} + +#[test] +fn overwrite_needed_chunks() { + let mut store = config::MINIMAL.new_store(); + let mut value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + // Current lifetime is 2 words of overhead (2 insert) and 60 bytes of data. + let mut lifetime = 2 + 60 / 4; + assert_eq!(store.lifetime().unwrap().used(), lifetime); + // Update the value. + value.extend(60..80); + assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); + // Added lifetime is 1 word of overhead (1 insert) and (80 - 52) bytes of data. + lifetime += 1 + (80 - 52) / 4; + assert_eq!(store.lifetime().unwrap().used(), lifetime); +} + +#[test] +fn delete_empty() { + let mut store = config::MINIMAL.new_store(); + assert_eq!(fragment::delete(&mut store, &(0..4)), Ok(())); + assert_eq!(store.find(0), Ok(None)); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} + +#[test] +fn delete_chunks() { + let mut store = config::MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(fragment::delete(&mut store, &(0..4)), Ok(())); + assert_eq!(store.find(0), Ok(None)); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); +} From 41a3f512c81b074b1e2ffb5bb6ceb1cfd12ac0b5 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 25 Jan 2021 11:31:42 +0100 Subject: [PATCH 147/192] Remove useless check --- libraries/persistent_store/src/fragment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs index 5bab46f..73b6d29 100644 --- a/libraries/persistent_store/src/fragment.rs +++ b/libraries/persistent_store/src/fragment.rs @@ -96,7 +96,7 @@ pub fn read_range( let length = handle.get_length(store)?; let end = core::cmp::min(range.end.saturating_sub(offset), length); offset += length; - if start < end && end <= length { + if start < end { result.extend(&handle.get_value(store)?[start..end]); } } From 0e537733f12314b7edf3a4113bdf2bacd3ae4575 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 25 Jan 2021 17:04:01 +0100 Subject: [PATCH 148/192] Improve count_credentials by not deserializing them --- src/ctap/storage.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b85f918..c5182bc 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -268,11 +268,11 @@ impl PersistentStore { /// Returns the number of credentials. pub fn count_credentials(&self) -> Result { - let mut iter_result = Ok(()); - let iter = self.iter_credentials(&mut iter_result)?; - let result = iter.count(); - iter_result?; - Ok(result) + let mut count = 0; + for handle in self.store.iter()? { + count += key::CREDENTIALS.contains(&handle?.get_key()) as usize; + } + Ok(count) } /// Returns the estimated number of credentials that can still be stored. From ae0156d2876871fa13b3ee6b41717767c3960cc2 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 25 Jan 2021 17:30:50 +0100 Subject: [PATCH 149/192] Factor test tools between store and fragment Those need the driver to deal with the fact that the store is stateful. Those tests can't be moved to the test suite because they use private functions. --- libraries/persistent_store/src/fragment.rs | 165 +++++++++++++++ libraries/persistent_store/src/lib.rs | 4 +- libraries/persistent_store/src/store.rs | 70 +------ .../{tests/config.rs => src/test.rs} | 37 +++- libraries/persistent_store/tests/fragment.rs | 188 ------------------ 5 files changed, 211 insertions(+), 253 deletions(-) rename libraries/persistent_store/{tests/config.rs => src/test.rs} (60%) delete mode 100644 libraries/persistent_store/tests/fragment.rs diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs index 73b6d29..9aec377 100644 --- a/libraries/persistent_store/src/fragment.rs +++ b/libraries/persistent_store/src/fragment.rs @@ -177,3 +177,168 @@ fn get_handles(store: &Store, keys: &impl Keys) -> StoreResult = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(read(&store, &(0..4)), Ok(Some(value))); + } + + #[test] + fn read_range_first_chunk() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + read_range(&store, &(0..4), 0..10), + Ok(Some((0..10).collect())) + ); + assert_eq!( + read_range(&store, &(0..4), 10..20), + Ok(Some((10..20).collect())) + ); + assert_eq!( + read_range(&store, &(0..4), 40..52), + Ok(Some((40..52).collect())) + ); + } + + #[test] + fn read_range_second_chunk() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(read_range(&store, &(0..4), 52..53), Ok(Some(vec![52]))); + assert_eq!(read_range(&store, &(0..4), 53..54), Ok(Some(vec![53]))); + assert_eq!(read_range(&store, &(0..4), 59..60), Ok(Some(vec![59]))); + } + + #[test] + fn read_range_both_chunks() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + read_range(&store, &(0..4), 40..60), + Ok(Some((40..60).collect())) + ); + assert_eq!( + read_range(&store, &(0..4), 0..60), + Ok(Some((0..60).collect())) + ); + } + + #[test] + fn read_range_outside() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!( + read_range(&store, &(0..4), 40..100), + Ok(Some((40..60).collect())) + ); + assert_eq!(read_range(&store, &(0..4), 60..100), Ok(Some(vec![]))); + } + + #[test] + fn write_single_chunk() { + let mut store = MINIMAL.new_store(); + let value = b"hello".to_vec(); + assert_eq!(write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some(value))); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } + + #[test] + fn write_multiple_chunks() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some((0..52).collect()))); + assert_eq!(store.find(1), Ok(Some((52..60).collect()))); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } + + #[test] + fn overwrite_less_chunks() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + let value: Vec<_> = (42..69).collect(); + assert_eq!(write(&mut store, &(0..4), &value), Ok(())); + assert_eq!(store.find(0), Ok(Some((42..69).collect()))); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } + + #[test] + fn overwrite_needed_chunks() { + let mut store = MINIMAL.new_store(); + let mut value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + // Current lifetime is 2 words of overhead (2 insert) and 60 bytes of data. + let mut lifetime = 2 + 60 / 4; + assert_eq!(store.lifetime().unwrap().used(), lifetime); + // Update the value. + value.extend(60..80); + assert_eq!(write(&mut store, &(0..4), &value), Ok(())); + // Added lifetime is 1 word of overhead (1 insert) and (80 - 52) bytes of data. + lifetime += 1 + (80 - 52) / 4; + assert_eq!(store.lifetime().unwrap().used(), lifetime); + } + + #[test] + fn delete_empty() { + let mut store = MINIMAL.new_store(); + assert_eq!(delete(&mut store, &(0..4)), Ok(())); + assert_eq!(store.find(0), Ok(None)); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } + + #[test] + fn delete_chunks() { + let mut store = MINIMAL.new_store(); + let value: Vec<_> = (0..60).collect(); + assert_eq!(store.insert(0, &value[..52]), Ok(())); + assert_eq!(store.insert(1, &value[52..]), Ok(())); + assert_eq!(delete(&mut store, &(0..4)), Ok(())); + assert_eq!(store.find(0), Ok(None)); + assert_eq!(store.find(1), Ok(None)); + assert_eq!(store.find(2), Ok(None)); + assert_eq!(store.find(3), Ok(None)); + } +} diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index 41acbaf..a8adc2b 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2019-2020 Google LLC +// Copyright 2019-2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -359,6 +359,8 @@ pub mod fragment; mod model; mod storage; mod store; +#[cfg(test)] +mod test; #[cfg(feature = "std")] pub use self::buffer::{BufferCorruptFunction, BufferOptions, BufferStorage}; diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index 2630950..f19f463 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -1301,71 +1301,15 @@ fn is_write_needed(source: &[u8], target: &[u8]) -> StoreResult { #[cfg(test)] mod tests { use super::*; - use crate::BufferOptions; - - #[derive(Clone)] - struct Config { - word_size: usize, - page_size: usize, - num_pages: usize, - max_word_writes: usize, - max_page_erases: usize, - } - - impl Config { - fn new_driver(&self) -> StoreDriverOff { - let options = BufferOptions { - word_size: self.word_size, - page_size: self.page_size, - max_word_writes: self.max_word_writes, - max_page_erases: self.max_page_erases, - strict_mode: true, - }; - StoreDriverOff::new(options, self.num_pages) - } - } - - const MINIMAL: Config = Config { - word_size: 4, - page_size: 64, - num_pages: 5, - max_word_writes: 2, - max_page_erases: 9, - }; - - const NORDIC: Config = Config { - word_size: 4, - page_size: 0x1000, - num_pages: 20, - max_word_writes: 2, - max_page_erases: 10000, - }; - - const TITAN: Config = Config { - word_size: 4, - page_size: 0x800, - num_pages: 10, - max_word_writes: 2, - max_page_erases: 10000, - }; + use crate::test::MINIMAL; #[test] - fn nordic_capacity() { - let driver = NORDIC.new_driver().power_on().unwrap(); - assert_eq!(driver.model().capacity().total, 19123); - } - - #[test] - fn titan_capacity() { - let driver = TITAN.new_driver().power_on().unwrap(); - assert_eq!(driver.model().capacity().total, 4315); - } - - #[test] - fn minimal_virt_page_size() { - // Make sure a virtual page has 14 words. We use this property in the other tests below to - // know whether entries are spanning, starting, and ending pages. - assert_eq!(MINIMAL.new_driver().model().format().virt_page_size(), 14); + fn is_write_needed_ok() { + assert_eq!(is_write_needed(&[], &[]), Ok(false)); + assert_eq!(is_write_needed(&[0], &[0]), Ok(false)); + assert_eq!(is_write_needed(&[0], &[1]), Err(StoreError::InvalidStorage)); + assert_eq!(is_write_needed(&[1], &[0]), Ok(true)); + assert_eq!(is_write_needed(&[1], &[1]), Ok(false)); } #[test] diff --git a/libraries/persistent_store/tests/config.rs b/libraries/persistent_store/src/test.rs similarity index 60% rename from libraries/persistent_store/tests/config.rs rename to libraries/persistent_store/src/test.rs index 8812743..2d20574 100644 --- a/libraries/persistent_store/tests/config.rs +++ b/libraries/persistent_store/src/test.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use persistent_store::{BufferOptions, BufferStorage, Store, StoreDriverOff}; +use crate::{BufferOptions, BufferStorage, Store, StoreDriverOff}; #[derive(Clone)] pub struct Config { @@ -47,3 +47,38 @@ pub const MINIMAL: Config = Config { max_word_writes: 2, max_page_erases: 9, }; + +const NORDIC: Config = Config { + word_size: 4, + page_size: 0x1000, + num_pages: 20, + max_word_writes: 2, + max_page_erases: 10000, +}; + +const TITAN: Config = Config { + word_size: 4, + page_size: 0x800, + num_pages: 10, + max_word_writes: 2, + max_page_erases: 10000, +}; + +#[test] +fn nordic_capacity() { + let driver = NORDIC.new_driver().power_on().unwrap(); + assert_eq!(driver.model().capacity().total, 19123); +} + +#[test] +fn titan_capacity() { + let driver = TITAN.new_driver().power_on().unwrap(); + assert_eq!(driver.model().capacity().total, 4315); +} + +#[test] +fn minimal_virt_page_size() { + // Make sure a virtual page has 14 words. We use this property in the other tests below to + // know whether entries are spanning, starting, and ending pages. + assert_eq!(MINIMAL.new_driver().model().format().virt_page_size(), 14); +} diff --git a/libraries/persistent_store/tests/fragment.rs b/libraries/persistent_store/tests/fragment.rs deleted file mode 100644 index fd045bc..0000000 --- a/libraries/persistent_store/tests/fragment.rs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2021 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 persistent_store::fragment; - -mod config; - -#[test] -fn read_empty_entry() { - let store = config::MINIMAL.new_store(); - assert_eq!(fragment::read(&store, &(0..4)), Ok(None)); -} - -#[test] -fn read_single_chunk() { - let mut store = config::MINIMAL.new_store(); - let value = b"hello".to_vec(); - assert_eq!(store.insert(0, &value), Ok(())); - assert_eq!(fragment::read(&store, &(0..4)), Ok(Some(value))); -} - -#[test] -fn read_multiple_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!(fragment::read(&store, &(0..4)), Ok(Some(value))); -} - -#[test] -fn read_range_first_chunk() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!( - fragment::read_range(&store, &(0..4), 0..10), - Ok(Some((0..10).collect())) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 10..20), - Ok(Some((10..20).collect())) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 40..52), - Ok(Some((40..52).collect())) - ); -} - -#[test] -fn read_range_second_chunk() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!( - fragment::read_range(&store, &(0..4), 52..53), - Ok(Some(vec![52])) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 53..54), - Ok(Some(vec![53])) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 59..60), - Ok(Some(vec![59])) - ); -} - -#[test] -fn read_range_both_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!( - fragment::read_range(&store, &(0..4), 40..60), - Ok(Some((40..60).collect())) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 0..60), - Ok(Some((0..60).collect())) - ); -} - -#[test] -fn read_range_outside() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!( - fragment::read_range(&store, &(0..4), 40..100), - Ok(Some((40..60).collect())) - ); - assert_eq!( - fragment::read_range(&store, &(0..4), 60..100), - Ok(Some(vec![])) - ); -} - -#[test] -fn write_single_chunk() { - let mut store = config::MINIMAL.new_store(); - let value = b"hello".to_vec(); - assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); - assert_eq!(store.find(0), Ok(Some(value))); - assert_eq!(store.find(1), Ok(None)); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} - -#[test] -fn write_multiple_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); - assert_eq!(store.find(0), Ok(Some((0..52).collect()))); - assert_eq!(store.find(1), Ok(Some((52..60).collect()))); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} - -#[test] -fn overwrite_less_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - let value: Vec<_> = (42..69).collect(); - assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); - assert_eq!(store.find(0), Ok(Some((42..69).collect()))); - assert_eq!(store.find(1), Ok(None)); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} - -#[test] -fn overwrite_needed_chunks() { - let mut store = config::MINIMAL.new_store(); - let mut value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - // Current lifetime is 2 words of overhead (2 insert) and 60 bytes of data. - let mut lifetime = 2 + 60 / 4; - assert_eq!(store.lifetime().unwrap().used(), lifetime); - // Update the value. - value.extend(60..80); - assert_eq!(fragment::write(&mut store, &(0..4), &value), Ok(())); - // Added lifetime is 1 word of overhead (1 insert) and (80 - 52) bytes of data. - lifetime += 1 + (80 - 52) / 4; - assert_eq!(store.lifetime().unwrap().used(), lifetime); -} - -#[test] -fn delete_empty() { - let mut store = config::MINIMAL.new_store(); - assert_eq!(fragment::delete(&mut store, &(0..4)), Ok(())); - assert_eq!(store.find(0), Ok(None)); - assert_eq!(store.find(1), Ok(None)); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} - -#[test] -fn delete_chunks() { - let mut store = config::MINIMAL.new_store(); - let value: Vec<_> = (0..60).collect(); - assert_eq!(store.insert(0, &value[..52]), Ok(())); - assert_eq!(store.insert(1, &value[52..]), Ok(())); - assert_eq!(fragment::delete(&mut store, &(0..4)), Ok(())); - assert_eq!(store.find(0), Ok(None)); - assert_eq!(store.find(1), Ok(None)); - assert_eq!(store.find(2), Ok(None)); - assert_eq!(store.find(3), Ok(None)); -} From 563f35184ac8e97849b677f4d4f026e849329aa3 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 17:50:01 +0100 Subject: [PATCH 150/192] use new store fragments --- src/ctap/storage.rs | 79 ++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 54 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 934d533..dab6620 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -32,6 +32,7 @@ use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; use persistent_store::StoreUpdate; +use persistent_store::fragment::{read_range, write}; // Those constants may be modified before compilation to tune the behavior of the key. // @@ -469,13 +470,6 @@ impl PersistentStore { )?) } - /// The size used for shards of large blobs. - /// - /// This value is constant during the lifetime of the device. - fn shard_size(&self) -> usize { - self.store.max_value_length() - } - /// Reads the byte vector stored as the serialized large blobs array. /// /// If too few bytes exist at that offset, return the maximum number @@ -483,45 +477,23 @@ impl PersistentStore { /// /// If no large blob is committed to the store, get responds as if an empty /// CBOR array (0x80) was written, together with the 16 byte prefix of its - /// SHA256, to a total length of 17 byte (which is the shortest legitemate + /// SHA256, to a total length of 17 byte (which is the shortest legitimate /// large blob entry possible). pub fn get_large_blob_array( &self, - mut offset: usize, - mut byte_count: usize, + offset: usize, + byte_count: usize, ) -> Result, Ctap2StatusCode> { - let mut output = Vec::with_capacity(byte_count); - while byte_count > 0 { - let shard_key = key::LARGE_BLOB_SHARDS.start + offset / self.shard_size(); - if !key::LARGE_BLOB_SHARDS.contains(&shard_key) { - // This request should have been caught at application level. - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); - } - let shard_entry = self.store.find(shard_key)?; - let shard_entry = if shard_key == key::LARGE_BLOB_SHARDS.start { - shard_entry.unwrap_or_else(|| { - vec![ - 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, - 0x6F, 0xA5, 0x7A, 0x6D, 0x3C, - ] - }) - } else { - shard_entry.unwrap_or_default() - }; - - let shard_offset = offset % self.shard_size(); - if shard_entry.len() < shard_offset { - break; - } - let shard_length = cmp::min(shard_entry.len() - shard_offset, byte_count); - output.extend(&shard_entry[shard_offset..][..shard_length]); - if shard_entry.len() < self.shard_size() { - break; - } - offset += shard_length; - byte_count -= shard_length; - } - Ok(output) + let byte_range = offset..offset + byte_count; + let output = read_range(&self.store, &key::LARGE_BLOB_SHARDS, byte_range)?; + Ok(output.unwrap_or_else(|| { + let empty_large_blob = vec![ + 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, + 0x7A, 0x6D, 0x3C, + ]; + let last_index = cmp::min(empty_large_blob.len(), offset + byte_count); + empty_large_blob.get(offset..last_index).unwrap_or_default().to_vec() + })) } /// Sets a byte vector as the serialized large blobs array. @@ -529,21 +501,11 @@ impl PersistentStore { &mut self, large_blob_array: &[u8], ) -> Result<(), Ctap2StatusCode> { + // This input should have been caught at caller level. if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - - let mut shards = large_blob_array.chunks(self.shard_size()); - let mut updates = Vec::with_capacity(shards.len()); - for key in key::LARGE_BLOB_SHARDS { - let update = match shards.next() { - Some(value) => StoreUpdate::Insert { key, value }, - None if self.store.find(key)?.is_some() => StoreUpdate::Remove { key }, - _ => break, - }; - updates.push(update); - } - Ok(self.store.transaction(&updates)?) + Ok(write(&mut self.store, &key::LARGE_BLOB_SHARDS, large_blob_array)?) } /// Returns the attestation private key if defined. @@ -642,6 +604,15 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } + + /// The size used for shards of large blobs. + /// + /// This value is constant during the lifetime of the device. + #[cfg(test)] + fn shard_size(&self) -> usize { + self.store.max_value_length() + } + } impl From for Ctap2StatusCode { From 4f3c773b15ccc6f7f04bc871b0e760dbb016aab2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 18:08:48 +0100 Subject: [PATCH 151/192] formats code, clippy --- libraries/persistent_store/src/fragment.rs | 1 + src/ctap/storage.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs index 73b6d29..851e3d2 100644 --- a/libraries/persistent_store/src/fragment.rs +++ b/libraries/persistent_store/src/fragment.rs @@ -24,6 +24,7 @@ use alloc::vec::Vec; use core::ops::Range; /// Represents a sequence of keys. +#[allow(clippy::len_without_is_empty)] pub trait Keys { /// Returns the number of keys. fn len(&self) -> usize; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index dab6620..0408041 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -31,8 +31,8 @@ use cbor::cbor_array_vec; use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; -use persistent_store::StoreUpdate; use persistent_store::fragment::{read_range, write}; +use persistent_store::StoreUpdate; // Those constants may be modified before compilation to tune the behavior of the key. // @@ -492,7 +492,10 @@ impl PersistentStore { 0x7A, 0x6D, 0x3C, ]; let last_index = cmp::min(empty_large_blob.len(), offset + byte_count); - empty_large_blob.get(offset..last_index).unwrap_or_default().to_vec() + empty_large_blob + .get(offset..last_index) + .unwrap_or_default() + .to_vec() })) } @@ -505,7 +508,11 @@ impl PersistentStore { if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(write(&mut self.store, &key::LARGE_BLOB_SHARDS, large_blob_array)?) + Ok(write( + &mut self.store, + &key::LARGE_BLOB_SHARDS, + large_blob_array, + )?) } /// Returns the attestation private key if defined. @@ -612,7 +619,6 @@ impl PersistentStore { fn shard_size(&self) -> usize { self.store.max_value_length() } - } impl From for Ctap2StatusCode { From 2af85ad9d0eebe8ef11d837b935f9d7739e1f9ea Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 18:29:38 +0100 Subject: [PATCH 152/192] style fix --- src/ctap/storage.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 7fdf50c..6f75461 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -31,8 +31,7 @@ use cbor::cbor_array_vec; use core::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; -use persistent_store::fragment::{read_range, write}; -use persistent_store::StoreUpdate; +use persistent_store::{fragment, StoreUpdate}; // Those constants may be modified before compilation to tune the behavior of the key. // @@ -485,14 +484,14 @@ impl PersistentStore { byte_count: usize, ) -> Result, Ctap2StatusCode> { let byte_range = offset..offset + byte_count; - let output = read_range(&self.store, &key::LARGE_BLOB_SHARDS, byte_range)?; + let output = fragment::read_range(&self.store, &key::LARGE_BLOB_SHARDS, byte_range)?; Ok(output.unwrap_or_else(|| { - let empty_large_blob = vec![ + const EMPTY_LARGE_BLOB: [u8; 17] = [ 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, 0x7A, 0x6D, 0x3C, ]; - let last_index = cmp::min(empty_large_blob.len(), offset + byte_count); - empty_large_blob + let last_index = cmp::min(EMPTY_LARGE_BLOB.len(), offset + byte_count); + EMPTY_LARGE_BLOB .get(offset..last_index) .unwrap_or_default() .to_vec() @@ -508,7 +507,7 @@ impl PersistentStore { if large_blob_array.len() > MAX_LARGE_BLOB_ARRAY_SIZE { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(write( + Ok(fragment::write( &mut self.store, &key::LARGE_BLOB_SHARDS, large_blob_array, From 769a2ae1c543a731a3daae1e386de72d842ff019 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 18:43:51 +0100 Subject: [PATCH 153/192] reduce testing to not account for shard size --- src/ctap/storage.rs | 130 +++----------------------------------------- 1 file changed, 8 insertions(+), 122 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 6f75461..43a00c7 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -610,14 +610,6 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } - - /// The size used for shards of large blobs. - /// - /// This value is constant during the lifetime of the device. - #[cfg(test)] - fn shard_size(&self) -> usize { - self.store.max_value_length() - } } impl From for Ctap2StatusCode { @@ -1210,13 +1202,13 @@ mod test { } assert!( MAX_LARGE_BLOB_ARRAY_SIZE - <= persistent_store.shard_size() + <= persistent_store.store.max_value_length() * (key::LARGE_BLOB_SHARDS.end - key::LARGE_BLOB_SHARDS.start) ); } #[test] - fn test_commit_get_large_blob_array_1_shard() { + fn test_commit_get_large_blob_array() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); @@ -1236,104 +1228,6 @@ mod test { assert_eq!(Vec::::new(), restored_large_blob_array); let restored_large_blob_array = persistent_store.get_large_blob_array(4, 1).unwrap(); assert_eq!(Vec::::new(), restored_large_blob_array); - - let large_blob_array = vec![0xC0; persistent_store.shard_size()]; - assert!(persistent_store - .commit_large_blob_array(&large_blob_array) - .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size()) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size() + 1) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - } - - #[test] - fn test_commit_get_large_blob_array_2_shards() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - - let large_blob_array = vec![0xC0; persistent_store.shard_size() + 1]; - assert!(persistent_store - .commit_large_blob_array(&large_blob_array) - .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size()) - .unwrap(); - assert_eq!( - large_blob_array[..persistent_store.shard_size()], - restored_large_blob_array[..] - ); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size() + 1) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - - let large_blob_array = vec![0xC0; 2 * persistent_store.shard_size()]; - assert!(persistent_store - .commit_large_blob_array(&large_blob_array) - .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, 2 * persistent_store.shard_size()) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, 2 * persistent_store.shard_size() + 1) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - } - - #[test] - fn test_commit_get_large_blob_array_3_shards() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - - let mut large_blob_array = vec![0x11; persistent_store.shard_size()]; - large_blob_array.extend(vec![0x22; persistent_store.shard_size()]); - large_blob_array.extend(&[0x33; 1]); - assert!(persistent_store - .commit_large_blob_array(&large_blob_array) - .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, 2 * persistent_store.shard_size() + 1) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, 3 * persistent_store.shard_size()) - .unwrap(); - assert_eq!(large_blob_array, restored_large_blob_array); - let shard1 = persistent_store - .get_large_blob_array(0, persistent_store.shard_size()) - .unwrap(); - let shard2 = persistent_store - .get_large_blob_array(persistent_store.shard_size(), persistent_store.shard_size()) - .unwrap(); - let shard3 = persistent_store - .get_large_blob_array(2 * persistent_store.shard_size(), 1) - .unwrap(); - assert_eq!( - large_blob_array[..persistent_store.shard_size()], - shard1[..] - ); - assert_eq!( - large_blob_array[persistent_store.shard_size()..2 * persistent_store.shard_size()], - shard2[..] - ); - assert_eq!( - large_blob_array[2 * persistent_store.shard_size()..], - shard3[..] - ); - let shard12 = persistent_store - .get_large_blob_array(persistent_store.shard_size() - 1, 2) - .unwrap(); - let shard23 = persistent_store - .get_large_blob_array(2 * persistent_store.shard_size() - 1, 2) - .unwrap(); - assert_eq!(vec![0x11, 0x22], shard12); - assert_eq!(vec![0x22, 0x33], shard23); } #[test] @@ -1341,27 +1235,21 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let large_blob_array = vec![0x11; persistent_store.shard_size() + 1]; + let large_blob_array = vec![0x11; 5]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); - let large_blob_array = vec![0x22; persistent_store.shard_size()]; + let large_blob_array = vec![0x22; 4]; assert!(persistent_store .commit_large_blob_array(&large_blob_array) .is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size() + 1) - .unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 5).unwrap(); assert_eq!(large_blob_array, restored_large_blob_array); - let restored_large_blob_array = persistent_store - .get_large_blob_array(persistent_store.shard_size(), 1) - .unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(4, 1).unwrap(); assert_eq!(Vec::::new(), restored_large_blob_array); assert!(persistent_store.commit_large_blob_array(&[]).is_ok()); - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size() + 1) - .unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 20).unwrap(); // Committing an empty array resets to the default blob of 17 byte. assert_eq!(restored_large_blob_array.len(), 17); } @@ -1375,9 +1263,7 @@ mod test { 0x80, 0x76, 0xBE, 0x8B, 0x52, 0x8D, 0x00, 0x75, 0xF7, 0xAA, 0xE9, 0x8D, 0x6F, 0xA5, 0x7A, 0x6D, 0x3C, ]; - let restored_large_blob_array = persistent_store - .get_large_blob_array(0, persistent_store.shard_size()) - .unwrap(); + let restored_large_blob_array = persistent_store.get_large_blob_array(0, 17).unwrap(); assert_eq!(empty_blob_array, restored_large_blob_array); let restored_large_blob_array = persistent_store.get_large_blob_array(0, 1).unwrap(); assert_eq!(vec![0x80], restored_large_blob_array); From 2dbe1c5f075a2f563f1cd880ad355c03e0a38eab Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 20:59:26 +0100 Subject: [PATCH 154/192] adds enterprise for make, byte keys --- src/ctap/command.rs | 226 ++++++++++++++++++++++--------------------- src/ctap/mod.rs | 2 + src/ctap/response.rs | 46 ++++----- 3 files changed, 141 insertions(+), 133 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 2e1fe3b..8a2cade 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -161,6 +161,7 @@ pub struct AuthenticatorMakeCredentialParameters { pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, + pub enterprise_attestation: Option, } impl TryFrom for AuthenticatorMakeCredentialParameters { @@ -169,15 +170,16 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => client_data_hash, - 2 => rp, - 3 => user, - 4 => cred_param_vec, - 5 => exclude_list, - 6 => extensions, - 7 => options, - 8 => pin_uv_auth_param, - 9 => pin_uv_auth_protocol, + 0x01 => client_data_hash, + 0x02 => rp, + 0x03 => user, + 0x04 => cred_param_vec, + 0x05 => exclude_list, + 0x06 => extensions, + 0x07 => options, + 0x08 => pin_uv_auth_param, + 0x09 => pin_uv_auth_protocol, + 0x0A => enterprise_attestation, } = extract_map(cbor_value)?; } @@ -217,6 +219,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; + let enterprise_attestation = enterprise_attestation.map(extract_bool).transpose()?; Ok(AuthenticatorMakeCredentialParameters { client_data_hash, @@ -228,6 +231,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { options, pin_uv_auth_param, pin_uv_auth_protocol, + enterprise_attestation, }) } } @@ -251,13 +255,13 @@ impl TryFrom for AuthenticatorGetAssertionParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => rp_id, - 2 => client_data_hash, - 3 => allow_list, - 4 => extensions, - 5 => options, - 6 => pin_uv_auth_param, - 7 => pin_uv_auth_protocol, + 0x01 => rp_id, + 0x02 => client_data_hash, + 0x03 => allow_list, + 0x04 => extensions, + 0x05 => options, + 0x06 => pin_uv_auth_param, + 0x07 => pin_uv_auth_protocol, } = extract_map(cbor_value)?; } @@ -321,14 +325,14 @@ impl TryFrom for AuthenticatorClientPinParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => pin_protocol, - 2 => sub_command, - 3 => key_agreement, - 4 => pin_auth, - 5 => new_pin_enc, - 6 => pin_hash_enc, - 9 => permissions, - 10 => permissions_rp_id, + 0x01 => pin_protocol, + 0x02 => sub_command, + 0x03 => key_agreement, + 0x04 => pin_auth, + 0x05 => new_pin_enc, + 0x06 => pin_hash_enc, + 0x09 => permissions, + 0x0A => permissions_rp_id, } = extract_map(cbor_value)?; } @@ -375,12 +379,12 @@ impl TryFrom for AuthenticatorLargeBlobsParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => get, - 2 => set, - 3 => offset, - 4 => length, - 5 => pin_uv_auth_param, - 6 => pin_uv_auth_protocol, + 0x01 => get, + 0x02 => set, + 0x03 => offset, + 0x04 => length, + 0x05 => pin_uv_auth_param, + 0x06 => pin_uv_auth_protocol, } = extract_map(cbor_value)?; } @@ -486,8 +490,8 @@ impl TryFrom for AuthenticatorAttestationMaterial { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => certificate, - 2 => private_key, + 0x01 => certificate, + 0x02 => private_key, } = extract_map(cbor_value)?; } let certificate = extract_byte_string(ok_or_missing(certificate)?)?; @@ -552,8 +556,8 @@ impl TryFrom for AuthenticatorVendorConfigureParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 1 => lockdown, - 2 => attestation_material, + 0x01 => lockdown, + 0x02 => attestation_material, } = extract_map(cbor_value)?; } let lockdown = lockdown.map_or(Ok(false), extract_bool)?; @@ -581,22 +585,23 @@ mod test { #[test] fn test_from_cbor_make_credential_parameters() { let cbor_value = cbor_map! { - 1 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], - 2 => cbor_map! { + 0x01 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], + 0x02 => cbor_map! { "id" => "example.com", "name" => "Example", "icon" => "example.com/icon.png", }, - 3 => cbor_map! { + 0x03 => cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], "name" => "foo", "displayName" => "bar", "icon" => "example.com/foo/icon.png", }, - 4 => cbor_array![ES256_CRED_PARAM], - 5 => cbor_array![], - 8 => vec![0x12, 0x34], - 9 => 1, + 0x04 => cbor_array![ES256_CRED_PARAM], + 0x05 => cbor_array![], + 0x08 => vec![0x12, 0x34], + 0x09 => 1, + 0x0A => true, }; let returned_make_credential_parameters = AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap(); @@ -630,6 +635,7 @@ mod test { options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), + enterprise_attestation: Some(true), }; assert_eq!( @@ -641,15 +647,15 @@ mod test { #[test] fn test_from_cbor_get_assertion_parameters() { let cbor_value = cbor_map! { - 1 => "example.com", - 2 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], - 3 => cbor_array![ cbor_map! { + 0x01 => "example.com", + 0x02 => vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F], + 0x03 => cbor_array![ cbor_map! { "type" => "public-key", "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], "transports" => cbor_array!["usb"], } ], - 6 => vec![0x12, 0x34], - 7 => 1, + 0x06 => vec![0x12, 0x34], + 0x07 => 1, }; let returned_get_assertion_parameters = AuthenticatorGetAssertionParameters::try_from(cbor_value).unwrap(); @@ -692,14 +698,14 @@ mod test { let cose_key = CoseKey::from(pk); let cbor_value = cbor_map! { - 1 => 1, - 2 => ClientPinSubCommand::GetPinRetries, - 3 => cbor::Value::from(cose_key.clone()), - 4 => vec! [0xBB], - 5 => vec! [0xCC], - 6 => vec! [0xDD], - 9 => 0x03, - 10 => "example.com", + 0x01 => 1, + 0x02 => ClientPinSubCommand::GetPinRetries, + 0x03 => cbor::Value::from(cose_key.clone()), + 0x04 => vec! [0xBB], + 0x05 => vec! [0xCC], + 0x06 => vec! [0xDD], + 0x09 => 0x03, + 0x0A => "example.com", }; let returned_client_pin_parameters = AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); @@ -746,12 +752,12 @@ mod test { #[test] fn test_from_cbor_cred_management_parameters() { let cbor_value = cbor_map! { - 1 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, - 2 => cbor_map!{ + 0x01 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, + 0x02 => cbor_map!{ 0x01 => vec![0x1D; 32], }, - 3 => 1, - 4 => vec! [0x9A; 16], + 0x03 => 1, + 0x04 => vec! [0x9A; 16], }; let returned_cred_management_parameters = AuthenticatorCredentialManagementParameters::try_from(cbor_value).unwrap(); @@ -785,8 +791,8 @@ mod test { fn test_from_cbor_large_blobs_parameters() { // successful get let cbor_value = cbor_map! { - 1 => 2, - 3 => 4, + 0x01 => 2, + 0x03 => 4, }; let returned_large_blobs_parameters = AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); @@ -805,11 +811,11 @@ mod test { // successful first set let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 0, - 4 => MIN_LARGE_BLOB_LEN as u64, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, }; let returned_large_blobs_parameters = AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); @@ -828,10 +834,10 @@ mod test { // successful next set let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 1, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 1, + 0x05 => vec! [0xA9], + 0x06 => 1, }; let returned_large_blobs_parameters = AuthenticatorLargeBlobsParameters::try_from(cbor_value).unwrap(); @@ -850,9 +856,9 @@ mod test { // failing with neither get nor set let cbor_value = cbor_map! { - 3 => 4, - 5 => vec! [0xA9], - 6 => 1, + 0x03 => 4, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -861,11 +867,11 @@ mod test { // failing with get and set let cbor_value = cbor_map! { - 1 => 2, - 2 => vec! [0x5E], - 3 => 4, - 5 => vec! [0xA9], - 6 => 1, + 0x01 => 2, + 0x02 => vec! [0x5E], + 0x03 => 4, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -874,11 +880,11 @@ mod test { // failing with get and length let cbor_value = cbor_map! { - 1 => 2, - 3 => 4, - 4 => MIN_LARGE_BLOB_LEN as u64, - 5 => vec! [0xA9], - 6 => 1, + 0x01 => 2, + 0x03 => 4, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -887,10 +893,10 @@ mod test { // failing with zero offset and no length present let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 0, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -899,11 +905,11 @@ mod test { // failing with length smaller than minimum let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 0, - 4 => MIN_LARGE_BLOB_LEN as u64 - 1, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x04 => MIN_LARGE_BLOB_LEN as u64 - 1, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -912,11 +918,11 @@ mod test { // failing with non-zero offset and length present let cbor_value = cbor_map! { - 2 => vec! [0x5E], - 3 => 4, - 4 => MIN_LARGE_BLOB_LEN as u64, - 5 => vec! [0xA9], - 6 => 1, + 0x02 => vec! [0x5E], + 0x03 => 4, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, }; assert_eq!( AuthenticatorLargeBlobsParameters::try_from(cbor_value), @@ -948,10 +954,10 @@ mod test { // Attestation key is too short. let cbor_value = cbor_map! { - 1 => false, - 2 => cbor_map! { - 1 => dummy_cert, - 2 => dummy_pkey[..key_material::ATTESTATION_PRIVATE_KEY_LENGTH - 1] + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert, + 0x02 => dummy_pkey[..key_material::ATTESTATION_PRIVATE_KEY_LENGTH - 1] } }; assert_eq!( @@ -961,9 +967,9 @@ mod test { // Missing private key let cbor_value = cbor_map! { - 1 => false, - 2 => cbor_map! { - 1 => dummy_cert + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert } }; assert_eq!( @@ -973,9 +979,9 @@ mod test { // Missing certificate let cbor_value = cbor_map! { - 1 => false, - 2 => cbor_map! { - 2 => dummy_pkey + 0x01 => false, + 0x02 => cbor_map! { + 0x02 => dummy_pkey } }; assert_eq!( @@ -985,10 +991,10 @@ mod test { // Valid let cbor_value = cbor_map! { - 1 => false, - 2 => cbor_map! { - 1 => dummy_cert, - 2 => dummy_pkey + 0x01 => false, + 0x02 => cbor_map! { + 0x01 => dummy_cert, + 0x02 => dummy_pkey } }; assert_eq!( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8fa622c..7b426c1 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -561,6 +561,7 @@ where options, pin_uv_auth_param, pin_uv_auth_protocol, + enterprise_attestation: _, } = make_credential_params; self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; @@ -1313,6 +1314,7 @@ mod test { options, pin_uv_auth_param: None, pin_uv_auth_protocol: None, + enterprise_attestation: None, } } diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 245218f..87d94cc 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -74,9 +74,9 @@ impl From for cbor::Value { } = make_credential_response; cbor_map_options! { - 1 => fmt, - 2 => auth_data, - 3 => att_stmt, + 0x01 => fmt, + 0x02 => auth_data, + 0x03 => att_stmt, } } } @@ -102,11 +102,11 @@ impl From for cbor::Value { } = get_assertion_response; cbor_map_options! { - 1 => credential, - 2 => auth_data, - 3 => signature, - 4 => user, - 5 => number_of_credentials, + 0x01 => credential, + 0x02 => auth_data, + 0x03 => signature, + 0x04 => user, + 0x05 => number_of_credentials, } } } @@ -199,9 +199,9 @@ impl From for cbor::Value { } = client_pin_response; cbor_map_options! { - 1 => key_agreement.map(cbor::Value::from), - 2 => pin_token, - 3 => retries, + 0x01 => key_agreement.map(cbor::Value::from), + 0x02 => pin_token, + 0x03 => retries, } } } @@ -286,8 +286,8 @@ impl From for cbor::Value { } = vendor_response; cbor_map_options! { - 1 => cert_programmed, - 2 => pkey_programmed, + 0x01 => cert_programmed, + 0x02 => pkey_programmed, } } } @@ -324,9 +324,9 @@ mod test { let response_cbor: Option = ResponseData::AuthenticatorMakeCredential(make_credential_response).into(); let expected_cbor = cbor_map_options! { - 1 => "packed", - 2 => vec![0xAD], - 3 => cbor_packed_attestation_statement, + 0x01 => "packed", + 0x02 => vec![0xAD], + 0x03 => cbor_packed_attestation_statement, }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -343,8 +343,8 @@ mod test { let response_cbor: Option = ResponseData::AuthenticatorGetAssertion(get_assertion_response).into(); let expected_cbor = cbor_map_options! { - 2 => vec![0xAD], - 3 => vec![0x51], + 0x02 => vec![0xAD], + 0x03 => vec![0x51], }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -435,7 +435,7 @@ mod test { let response_cbor: Option = ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into(); let expected_cbor = cbor_map_options! { - 2 => vec![70], + 0x02 => vec![70], }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -562,8 +562,8 @@ mod test { assert_eq!( response_cbor, Some(cbor_map_options! { - 1 => true, - 2 => false, + 0x01 => true, + 0x02 => false, }) ); let response_cbor: Option = @@ -575,8 +575,8 @@ mod test { assert_eq!( response_cbor, Some(cbor_map_options! { - 1 => false, - 2 => true, + 0x01 => false, + 0x02 => true, }) ); } From 5741595e575205211ccf4831acbf18c7e620afde Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 25 Jan 2021 21:45:06 +0100 Subject: [PATCH 155/192] new extension entry for largeBlobKey --- src/ctap/credential_management.rs | 5 +- src/ctap/data_formats.rs | 39 +++++++ src/ctap/mod.rs | 179 +++++++++++++++++++++++------- src/ctap/response.rs | 59 ++++++++-- src/ctap/storage.rs | 4 + 5 files changed, 233 insertions(+), 53 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 7665ea7..acc9278 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -81,6 +81,7 @@ fn enumerate_credentials_response( user_name, user_icon, cred_blob: _, + large_blob_key, } = credential; let user = PublicKeyCredentialUserEntity { user_id: user_handle, @@ -100,8 +101,7 @@ fn enumerate_credentials_response( public_key: Some(public_key), total_credentials, cred_protect: cred_protect_policy, - // TODO(kaczmarczyck) add when largeBlobKey extension is implemented - large_blob_key: None, + large_blob_key, ..Default::default() }) } @@ -348,6 +348,7 @@ mod test { user_name: Some("name".to_string()), user_icon: Some("icon".to_string()), cred_blob: None, + large_blob_key: None, } } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index da992f8..ba9ca1a 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -282,6 +282,7 @@ pub struct MakeCredentialExtensions { pub cred_protect: Option, pub min_pin_length: bool, pub cred_blob: Option>, + pub large_blob_key: Option, } impl TryFrom for MakeCredentialExtensions { @@ -293,6 +294,7 @@ impl TryFrom for MakeCredentialExtensions { "credBlob" => cred_blob, "credProtect" => cred_protect, "hmac-secret" => hmac_secret, + "largeBlobKey" => large_blob_key, "minPinLength" => min_pin_length, } = extract_map(cbor_value)?; } @@ -303,11 +305,18 @@ impl TryFrom for MakeCredentialExtensions { .transpose()?; let min_pin_length = min_pin_length.map_or(Ok(false), extract_bool)?; let cred_blob = cred_blob.map(extract_byte_string).transpose()?; + let large_blob_key = large_blob_key.map(extract_bool).transpose()?; + if let Some(large_blob_key) = large_blob_key { + if !large_blob_key { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + } Ok(Self { hmac_secret, cred_protect, min_pin_length, cred_blob, + large_blob_key, }) } } @@ -317,6 +326,7 @@ impl TryFrom for MakeCredentialExtensions { pub struct GetAssertionExtensions { pub hmac_secret: Option, pub cred_blob: bool, + pub large_blob_key: Option, } impl TryFrom for GetAssertionExtensions { @@ -327,6 +337,7 @@ impl TryFrom for GetAssertionExtensions { let { "credBlob" => cred_blob, "hmac-secret" => hmac_secret, + "largeBlobKey" => large_blob_key, } = extract_map(cbor_value)?; } @@ -334,9 +345,16 @@ impl TryFrom for GetAssertionExtensions { .map(GetAssertionHmacSecretInput::try_from) .transpose()?; let cred_blob = cred_blob.map_or(Ok(false), extract_bool)?; + let large_blob_key = large_blob_key.map(extract_bool).transpose()?; + if let Some(large_blob_key) = large_blob_key { + if !large_blob_key { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + } Ok(Self { hmac_secret, cred_blob, + large_blob_key, }) } } @@ -546,6 +564,7 @@ pub struct PublicKeyCredentialSource { pub user_name: Option, pub user_icon: Option, pub cred_blob: Option>, + pub large_blob_key: Option>, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -561,6 +580,7 @@ enum PublicKeyCredentialSourceField { UserName = 8, UserIcon = 9, CredBlob = 10, + LargeBlobKey = 11, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: @@ -588,6 +608,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::UserName => credential.user_name, PublicKeyCredentialSourceField::UserIcon => credential.user_icon, PublicKeyCredentialSourceField::CredBlob => credential.cred_blob, + PublicKeyCredentialSourceField::LargeBlobKey => credential.large_blob_key, } } } @@ -608,6 +629,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::UserName => user_name, PublicKeyCredentialSourceField::UserIcon => user_icon, PublicKeyCredentialSourceField::CredBlob => cred_blob, + PublicKeyCredentialSourceField::LargeBlobKey => large_blob_key, } = extract_map(cbor_value)?; } @@ -628,6 +650,7 @@ impl TryFrom for PublicKeyCredentialSource { let user_name = user_name.map(extract_text_string).transpose()?; let user_icon = user_icon.map(extract_text_string).transpose()?; let cred_blob = cred_blob.map(extract_byte_string).transpose()?; + let large_blob_key = large_blob_key.map(extract_byte_string).transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -650,6 +673,7 @@ impl TryFrom for PublicKeyCredentialSource { user_name, user_icon, cred_blob, + large_blob_key, }) } } @@ -1522,6 +1546,7 @@ mod test { "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, "minPinLength" => true, "credBlob" => vec![0xCB], + "largeBlobKey" => true, }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { @@ -1529,6 +1554,7 @@ mod test { cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), min_pin_length: true, cred_blob: Some(vec![0xCB]), + large_blob_key: Some(true), }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1546,6 +1572,7 @@ mod test { 3 => vec![0x03; 16], }, "credBlob" => true, + "largeBlobKey" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); let expected_input = GetAssertionHmacSecretInput { @@ -1556,6 +1583,7 @@ mod test { let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), cred_blob: true, + large_blob_key: Some(true), }; assert_eq!(extensions, Ok(expected_extensions)); } @@ -1849,6 +1877,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert_eq!( @@ -1901,6 +1930,16 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + large_blob_key: Some(vec![0x1B]), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 7b426c1..ab66177 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -49,7 +49,7 @@ use self::response::{ AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; -use self::storage::{PersistentStore, MAX_RP_IDS_LENGTH}; +use self::storage::{PersistentStore, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_RP_IDS_LENGTH}; use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; @@ -427,6 +427,7 @@ where user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, })) } @@ -596,6 +597,10 @@ where || cred_protect_policy.is_some() || min_pin_length || has_cred_blob_output; + let large_blob_key = match (options.rk, extensions.large_blob_key) { + (true, Some(true)) => Some(self.rng.gen_uniform_u8x32().to_vec()), + _ => None, + }; let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { @@ -674,6 +679,7 @@ where .user_icon .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_blob, + large_blob_key: large_blob_key.clone(), }; self.persistent_store.store_credential(credential_source)?; random_id @@ -749,6 +755,7 @@ where fmt: String::from("packed"), auth_data, att_stmt: attestation_statement, + large_blob_key, }, )) } @@ -806,6 +813,10 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } + let large_blob_key = match extensions.large_blob_key { + Some(true) => credential.large_blob_key, + _ => None, + }; let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); @@ -841,6 +852,7 @@ where signature: signature.to_asn1_der(), user, number_of_credentials: number_of_credentials.map(|n| n as u64), + large_blob_key, }, )) } @@ -1006,19 +1018,18 @@ where fn process_get_info(&self) -> Result { let mut options_map = BTreeMap::new(); - // TODO(kaczmarczyck) add authenticatorConfig and credProtect options options_map.insert(String::from("rk"), true); - options_map.insert(String::from("up"), true); options_map.insert( String::from("clientPin"), self.persistent_store.pin_hash()?.is_some(), ); + options_map.insert(String::from("up"), true); + options_map.insert(String::from("pinUvAuthToken"), true); + options_map.insert(String::from("largeBlobs"), true); + options_map.insert(String::from("authnrCfg"), true); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); - options_map.insert( - String::from("forcePINChange"), - self.persistent_store.has_force_pin_change()?, - ); + options_map.insert(String::from("makeCredUvNotRqd"), true); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions: vec![ @@ -1032,6 +1043,7 @@ where String::from("credProtect"), String::from("minPinLength"), String::from("credBlob"), + String::from("largeBlobKey"), ]), aaguid: self.persistent_store.aaguid()?, options: Some(options_map), @@ -1041,7 +1053,8 @@ where max_credential_id_length: Some(CREDENTIAL_ID_SIZE as u64), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), - default_cred_protect: DEFAULT_CRED_PROTECT, + max_serialized_large_blob_array: Some(MAX_LARGE_BLOB_ARRAY_SIZE as u64), + force_pin_change: Some(self.persistent_store.has_force_pin_change()?), min_pin_length: self.persistent_store.min_pin_length()?, firmware_version: None, max_cred_blob_length: Some(MAX_CRED_BLOB_LENGTH as u64), @@ -1214,6 +1227,7 @@ mod test { fmt, auth_data, att_stmt, + large_blob_key, } = make_credential_response; // The expected response is split to only assert the non-random parts. assert_eq!(fmt, "packed"); @@ -1234,6 +1248,7 @@ mod test { expected_extension_cbor ); assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + assert_eq!(large_blob_key, None); } _ => panic!("Invalid response type"), } @@ -1258,15 +1273,19 @@ mod test { String::from("credProtect"), String::from("minPinLength"), String::from("credBlob"), + String::from("largeBlobKey"), ]], 0x03 => ctap_state.persistent_store.aaguid().unwrap(), 0x04 => cbor_map! { "rk" => true, - "up" => true, "clientPin" => false, + "up" => true, + "pinUvAuthToken" => true, + "largeBlobs" => true, + "authnrCfg" => true, "credMgmt" => true, "setMinPINLength" => true, - "forcePINChange" => false, + "makeCredUvNotRqd" => true, }, 0x05 => MAX_MSG_SIZE as u64, 0x06 => cbor_array_vec![vec![1]], @@ -1274,7 +1293,8 @@ mod test { 0x08 => CREDENTIAL_ID_SIZE as u64, 0x09 => cbor_array_vec![vec!["usb"]], 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], - 0x0C => DEFAULT_CRED_PROTECT.map(|c| c as u64), + 0x0B => MAX_LARGE_BLOB_ARRAY_SIZE as u64, + 0x0C => false, 0x0D => ctap_state.persistent_store.min_pin_length().unwrap() as u64, 0x0F => MAX_CRED_BLOB_LENGTH as u64, 0x10 => MAX_RP_IDS_LENGTH as u64, @@ -1336,10 +1356,8 @@ mod test { policy: CredentialProtectionPolicy, ) -> AuthenticatorMakeCredentialParameters { let extensions = MakeCredentialExtensions { - hmac_secret: false, cred_protect: Some(policy), - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1424,6 +1442,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1504,9 +1523,7 @@ mod test { let extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1534,9 +1551,7 @@ mod test { let extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1563,10 +1578,8 @@ mod test { // First part: The extension is ignored, since the RP ID is not on the list. let extensions = MakeCredentialExtensions { - hmac_secret: false, - cred_protect: None, min_pin_length: true, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1589,10 +1602,8 @@ mod test { ); let extensions = MakeCredentialExtensions { - hmac_secret: false, - cred_protect: None, min_pin_length: true, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1618,10 +1629,8 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = MakeCredentialExtensions { - hmac_secret: false, - cred_protect: None, - min_pin_length: false, cred_blob: Some(vec![0xCB]), + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1656,10 +1665,8 @@ mod test { let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = MakeCredentialExtensions { - hmac_secret: false, - cred_protect: None, - min_pin_length: false, cred_blob: Some(vec![0xCB; MAX_CRED_BLOB_LENGTH + 1]), + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; @@ -1687,6 +1694,39 @@ mod test { assert_eq!(stored_credential.cred_blob, None); } + #[test] + fn test_process_make_credential_large_blob_key() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let extensions = MakeCredentialExtensions { + large_blob_key: Some(true), + ..Default::default() + }; + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + let large_blob_key = match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + make_credential_response.large_blob_key.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!(large_blob_key.len(), 32); + + let mut iter_result = Ok(()); + let iter = ctap_state + .persistent_store + .iter_credentials(&mut iter_result) + .unwrap(); + // There is only 1 credential, so last is good enough. + let (_, stored_credential) = iter.last().unwrap(); + iter_result.unwrap(); + assert_eq!(stored_credential.large_blob_key.unwrap(), large_blob_key); + } + #[test] fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; @@ -1828,9 +1868,7 @@ mod test { let make_extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1857,7 +1895,7 @@ mod test { }; let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - cred_blob: false, + ..Default::default() }; let cred_desc = PublicKeyCredentialDescriptor { @@ -1898,9 +1936,7 @@ mod test { let make_extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - min_pin_length: false, - cred_blob: None, + ..Default::default() }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; @@ -1916,7 +1952,7 @@ mod test { }; let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), - cred_blob: false, + ..Default::default() }; let get_assertion_params = AuthenticatorGetAssertionParameters { @@ -1970,6 +2006,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -2033,6 +2070,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -2082,6 +2120,7 @@ mod test { user_name: None, user_icon: None, cred_blob: Some(vec![0xCB]), + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -2089,8 +2128,8 @@ mod test { .is_ok()); let extensions = GetAssertionExtensions { - hmac_secret: None, cred_blob: true, + ..Default::default() }; let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), @@ -2125,6 +2164,63 @@ mod test { ); } + #[test] + fn test_process_get_assertion_with_large_blob_key() { + let mut rng = ThreadRng256 {}; + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let credential_id = rng.gen_uniform_u8x32().to_vec(); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let credential = PublicKeyCredentialSource { + key_type: PublicKeyCredentialType::PublicKey, + credential_id, + private_key, + rp_id: String::from("example.com"), + user_handle: vec![0x1D], + user_display_name: None, + cred_protect_policy: None, + creation_order: 0, + user_name: None, + user_icon: None, + cred_blob: None, + large_blob_key: Some(vec![0x1C; 32]), + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let extensions = GetAssertionExtensions { + large_blob_key: Some(true), + ..Default::default() + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + let large_blob_key = match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + get_assertion_response.large_blob_key.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!(large_blob_key, vec![0x1C; 32]); + } + #[test] fn test_process_get_next_assertion_two_credentials_with_uv() { let mut rng = ThreadRng256 {}; @@ -2369,6 +2465,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 87d94cc..346a348 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -63,6 +63,7 @@ pub struct AuthenticatorMakeCredentialResponse { pub fmt: String, pub auth_data: Vec, pub att_stmt: PackedAttestationStatement, + pub large_blob_key: Option>, } impl From for cbor::Value { @@ -71,12 +72,14 @@ impl From for cbor::Value { fmt, auth_data, att_stmt, + large_blob_key, } = make_credential_response; cbor_map_options! { 0x01 => fmt, 0x02 => auth_data, 0x03 => att_stmt, + 0x05 => large_blob_key, } } } @@ -89,6 +92,7 @@ pub struct AuthenticatorGetAssertionResponse { pub signature: Vec, pub user: Option, pub number_of_credentials: Option, + pub large_blob_key: Option>, } impl From for cbor::Value { @@ -99,6 +103,7 @@ impl From for cbor::Value { signature, user, number_of_credentials, + large_blob_key, } = get_assertion_response; cbor_map_options! { @@ -107,6 +112,7 @@ impl From for cbor::Value { 0x03 => signature, 0x04 => user, 0x05 => number_of_credentials, + 0x07 => large_blob_key, } } } @@ -124,7 +130,8 @@ pub struct AuthenticatorGetInfoResponse { pub max_credential_id_length: Option, pub transports: Option>, pub algorithms: Option>, - pub default_cred_protect: Option, + pub max_serialized_large_blob_array: Option, + pub force_pin_change: Option, pub min_pin_length: u8, pub firmware_version: Option, pub max_cred_blob_length: Option, @@ -145,7 +152,8 @@ impl From for cbor::Value { max_credential_id_length, transports, algorithms, - default_cred_protect, + max_serialized_large_blob_array, + force_pin_change, min_pin_length, firmware_version, max_cred_blob_length, @@ -172,7 +180,8 @@ impl From for cbor::Value { 0x08 => max_credential_id_length, 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), + 0x0B => max_serialized_large_blob_array, + 0x0C => force_pin_change, 0x0D => min_pin_length as u64, 0x0E => firmware_version, 0x0F => max_cred_blob_length, @@ -297,7 +306,7 @@ mod test { use super::super::data_formats::{PackedAttestationStatement, PublicKeyCredentialType}; use super::super::ES256_CRED_PARAM; use super::*; - use cbor::{cbor_bytes, cbor_map}; + use cbor::{cbor_array, cbor_bytes, cbor_map}; use crypto::rng256::ThreadRng256; #[test] @@ -320,6 +329,7 @@ mod test { fmt: "packed".to_string(), auth_data: vec![0xAD], att_stmt, + large_blob_key: Some(vec![0x1B]), }; let response_cbor: Option = ResponseData::AuthenticatorMakeCredential(make_credential_response).into(); @@ -327,24 +337,50 @@ mod test { 0x01 => "packed", 0x02 => vec![0xAD], 0x03 => cbor_packed_attestation_statement, + 0x05 => vec![0x1B], }; assert_eq!(response_cbor, Some(expected_cbor)); } #[test] fn test_get_assertion_into_cbor() { + let pub_key_cred_descriptor = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: vec![0x2D, 0x2D, 0x2D, 0x2D], + transports: Some(vec![AuthenticatorTransport::Usb]), + }; + let user = PublicKeyCredentialUserEntity { + user_id: vec![0x1D, 0x1D, 0x1D, 0x1D], + user_name: Some("foo".to_string()), + user_display_name: Some("bar".to_string()), + user_icon: Some("example.com/foo/icon.png".to_string()), + }; let get_assertion_response = AuthenticatorGetAssertionResponse { - credential: None, + credential: Some(pub_key_cred_descriptor), auth_data: vec![0xAD], signature: vec![0x51], - user: None, - number_of_credentials: None, + user: Some(user), + number_of_credentials: Some(2), + large_blob_key: Some(vec![0x1B]), }; let response_cbor: Option = ResponseData::AuthenticatorGetAssertion(get_assertion_response).into(); let expected_cbor = cbor_map_options! { + 0x01 => cbor_map! { + "type" => "public-key", + "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "transports" => cbor_array!["usb"], + }, 0x02 => vec![0xAD], 0x03 => vec![0x51], + 0x04 => cbor_map! { + "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "name" => "foo".to_string(), + "displayName" => "bar".to_string(), + "icon" => "example.com/foo/icon.png".to_string(), + }, + 0x05 => 2, + 0x07 => vec![0x1B], }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -363,7 +399,8 @@ mod test { max_credential_id_length: None, transports: None, algorithms: None, - default_cred_protect: None, + max_serialized_large_blob_array: None, + force_pin_change: None, min_pin_length: 4, firmware_version: None, max_cred_blob_length: None, @@ -395,7 +432,8 @@ mod test { max_credential_id_length: Some(256), transports: Some(vec![AuthenticatorTransport::Usb]), algorithms: Some(vec![ES256_CRED_PARAM]), - default_cred_protect: Some(CredentialProtectionPolicy::UserVerificationRequired), + max_serialized_large_blob_array: Some(1024), + force_pin_change: Some(false), min_pin_length: 4, firmware_version: Some(0), max_cred_blob_length: Some(1024), @@ -415,7 +453,8 @@ mod test { 0x08 => 256, 0x09 => cbor_array_vec![vec!["usb"]], 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], - 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, + 0x0B => 1024, + 0x0C => false, 0x0D => 4, 0x0E => 0, 0x0F => 1024, diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 43a00c7..b982922 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -756,6 +756,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, } } @@ -973,6 +974,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -995,6 +997,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1321,6 +1324,7 @@ mod test { user_name: None, user_icon: None, cred_blob: None, + large_blob_key: None, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From 371e8b6f35efc8b489a2101cb8f078bd717652f4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 2 Feb 2021 05:46:03 +0100 Subject: [PATCH 156/192] remove conditional trait implementation --- libraries/crypto/src/ec/exponent256.rs | 6 +-- libraries/crypto/src/ec/gfp256.rs | 1 - libraries/crypto/src/ec/int256.rs | 1 - libraries/crypto/src/ec/point.rs | 2 - libraries/crypto/src/ecdh.rs | 2 +- libraries/crypto/src/ecdsa.rs | 5 +- src/ctap/apdu.rs | 15 ++---- src/ctap/command.rs | 18 +++---- src/ctap/ctap1.rs | 5 +- src/ctap/data_formats.rs | 65 +++++++++----------------- src/ctap/response.rs | 25 ++++------ 11 files changed, 51 insertions(+), 94 deletions(-) diff --git a/libraries/crypto/src/ec/exponent256.rs b/libraries/crypto/src/ec/exponent256.rs index 8638eaa..4a31aee 100644 --- a/libraries/crypto/src/ec/exponent256.rs +++ b/libraries/crypto/src/ec/exponent256.rs @@ -18,11 +18,10 @@ use core::ops::Mul; use subtle::{self, Choice, ConditionallySelectable, CtOption}; // An exponent on the elliptic curve, that is an element modulo the curve order N. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] // TODO: remove this Default once https://github.com/dalek-cryptography/subtle/issues/63 is // resolved. #[derive(Default)] -#[cfg_attr(feature = "derive_debug", derive(Debug))] pub struct ExponentP256 { int: Int256, } @@ -92,11 +91,10 @@ impl Mul for &ExponentP256 { } // A non-zero exponent on the elliptic curve. -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] // TODO: remove this Default once https://github.com/dalek-cryptography/subtle/issues/63 is // resolved. #[derive(Default)] -#[cfg_attr(feature = "derive_debug", derive(Debug))] pub struct NonZeroExponentP256 { e: ExponentP256, } diff --git a/libraries/crypto/src/ec/gfp256.rs b/libraries/crypto/src/ec/gfp256.rs index bb3232c..0e6179f 100644 --- a/libraries/crypto/src/ec/gfp256.rs +++ b/libraries/crypto/src/ec/gfp256.rs @@ -111,7 +111,6 @@ impl Mul for &GFP256 { } } -#[cfg(feature = "derive_debug")] impl core::fmt::Debug for GFP256 { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "GFP256::{:?}", self.int) diff --git a/libraries/crypto/src/ec/int256.rs b/libraries/crypto/src/ec/int256.rs index 9954c37..8927b19 100644 --- a/libraries/crypto/src/ec/int256.rs +++ b/libraries/crypto/src/ec/int256.rs @@ -636,7 +636,6 @@ impl SubAssign<&Int256> for Int256 { } } -#[cfg(feature = "derive_debug")] impl core::fmt::Debug for Int256 { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { write!(f, "Int256 {{ digits: {:08x?} }}", self.digits) diff --git a/libraries/crypto/src/ec/point.rs b/libraries/crypto/src/ec/point.rs index 11c6cde..1038808 100644 --- a/libraries/crypto/src/ec/point.rs +++ b/libraries/crypto/src/ec/point.rs @@ -542,7 +542,6 @@ impl Add for &PointProjective { } } -#[cfg(feature = "derive_debug")] impl core::fmt::Debug for PointP256 { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { f.debug_struct("PointP256") @@ -552,7 +551,6 @@ impl core::fmt::Debug for PointP256 { } } -#[cfg(feature = "derive_debug")] impl PartialEq for PointP256 { fn eq(&self, other: &PointP256) -> bool { self.x == other.x && self.y == other.y diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index a1e3736..705aee0 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -26,7 +26,7 @@ pub struct SecKey { a: NonZeroExponentP256, } -#[cfg_attr(feature = "derive_debug", derive(Clone, PartialEq, Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct PubKey { p: PointP256, } diff --git a/libraries/crypto/src/ecdsa.rs b/libraries/crypto/src/ecdsa.rs index b6a1708..eb61365 100644 --- a/libraries/crypto/src/ecdsa.rs +++ b/libraries/crypto/src/ecdsa.rs @@ -30,8 +30,7 @@ use core::marker::PhantomData; pub const NBYTES: usize = int256::NBYTES; -#[derive(Clone, PartialEq)] -#[cfg_attr(feature = "derive_debug", derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct SecKey { k: NonZeroExponentP256, } @@ -41,7 +40,7 @@ pub struct Signature { s: NonZeroExponentP256, } -#[cfg_attr(feature = "derive_debug", derive(Clone))] +#[derive(Clone)] pub struct PubKey { p: PointP256, } diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index f475308..f12ded2 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -18,9 +18,8 @@ use core::convert::TryFrom; const APDU_HEADER_LEN: usize = 4; -#[cfg_attr(test, derive(Clone, Debug))] +#[derive(Clone, Debug, PartialEq)] #[allow(non_camel_case_types, dead_code)] -#[derive(PartialEq)] pub enum ApduStatusCode { SW_SUCCESS = 0x90_00, /// Command successfully executed; 'XX' bytes of data are @@ -51,9 +50,8 @@ pub enum ApduInstructions { GetResponse = 0xC0, } -#[cfg_attr(test, derive(Clone, Debug))] +#[derive(Clone, Debug, Default, PartialEq)] #[allow(dead_code)] -#[derive(Default, PartialEq)] pub struct ApduHeader { pub cla: u8, pub ins: u8, @@ -72,8 +70,7 @@ impl From<&[u8; APDU_HEADER_LEN]> for ApduHeader { } } -#[cfg_attr(test, derive(Clone, Debug))] -#[derive(PartialEq)] +#[derive(Clone, Debug, PartialEq)] /// The APDU cases pub enum Case { Le1, @@ -85,18 +82,16 @@ pub enum Case { Le3, } -#[cfg_attr(test, derive(Clone, Debug))] +#[derive(Clone, Debug, PartialEq)] #[allow(dead_code)] -#[derive(PartialEq)] pub enum ApduType { Instruction, Short(Case), Extended(Case), } -#[cfg_attr(test, derive(Clone, Debug))] +#[derive(Clone, Debug, PartialEq)] #[allow(dead_code)] -#[derive(PartialEq)] pub struct APDU { pub header: ApduHeader, pub lc: u16, diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 8a2cade..a76254a 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -38,7 +38,7 @@ pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option = None; const MIN_LARGE_BLOB_LEN: usize = 17; // CTAP specification (version 20190130) section 6.1 -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub enum Command { AuthenticatorMakeCredential(AuthenticatorMakeCredentialParameters), AuthenticatorGetAssertion(AuthenticatorGetAssertionParameters), @@ -148,7 +148,7 @@ impl Command { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorMakeCredentialParameters { pub client_data_hash: Vec, pub rp: PublicKeyCredentialRpEntity, @@ -236,7 +236,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorGetAssertionParameters { pub rp_id: String, pub client_data_hash: Vec, @@ -307,7 +307,7 @@ impl TryFrom for AuthenticatorGetAssertionParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorClientPinParameters { pub pin_protocol: u64, pub sub_command: ClientPinSubCommand, @@ -363,7 +363,7 @@ impl TryFrom for AuthenticatorClientPinParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorLargeBlobsParameters { pub get: Option, pub set: Option>, @@ -438,7 +438,7 @@ impl TryFrom for AuthenticatorLargeBlobsParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorConfigParameters { pub sub_command: ConfigSubCommand, pub sub_command_params: Option, @@ -478,7 +478,7 @@ impl TryFrom for AuthenticatorConfigParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorAttestationMaterial { pub certificate: Vec, pub private_key: [u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], @@ -507,7 +507,7 @@ impl TryFrom for AuthenticatorAttestationMaterial { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorCredentialManagementParameters { pub sub_command: CredentialManagementSubCommand, pub sub_command_params: Option, @@ -544,7 +544,7 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorVendorConfigureParameters { pub lockdown: bool, pub attestation_material: Option, diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 0bf43b5..09a3c6c 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -29,8 +29,7 @@ pub type Ctap1StatusCode = ApduStatusCode; // The specification referenced in this file is at: // https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.pdf -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug))] -#[derive(PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum Ctap1Flags { CheckOnly = 0x07, EnforceUpAndSign = 0x03, @@ -56,7 +55,7 @@ impl Into for Ctap1Flags { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] // TODO: remove #allow when https://github.com/rust-lang/rust/issues/64362 is fixed enum U2fCommand { #[allow(dead_code)] diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index ba9ca1a..1992469 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -27,8 +27,7 @@ use enum_iterator::IntoEnumIterator; const ES256_ALGORITHM: i64 = -7; // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialRpEntity { pub rp_id: String, pub rp_name: Option, @@ -70,8 +69,7 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -118,8 +116,7 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#enumdef-publickeycredentialtype -#[derive(Clone, PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub enum PublicKeyCredentialType { PublicKey, // This is the default for all strings not covered above. @@ -151,8 +148,7 @@ impl TryFrom for PublicKeyCredentialType { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters -#[derive(PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct PublicKeyCredentialParameter { pub cred_type: PublicKeyCredentialType, pub alg: SignatureAlgorithm, @@ -185,8 +181,7 @@ impl From for cbor::Value { } // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum AuthenticatorTransport { Usb, @@ -223,8 +218,7 @@ impl TryFrom for AuthenticatorTransport { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialDescriptor { pub key_type: PublicKeyCredentialType, pub key_id: Vec, @@ -275,8 +269,7 @@ impl From for cbor::Value { } } -#[derive(Default)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct MakeCredentialExtensions { pub hmac_secret: bool, pub cred_protect: Option, @@ -321,8 +314,7 @@ impl TryFrom for MakeCredentialExtensions { } } -#[derive(Clone, Default)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct GetAssertionExtensions { pub hmac_secret: Option, pub cred_blob: bool, @@ -359,8 +351,7 @@ impl TryFrom for GetAssertionExtensions { } } -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, @@ -391,8 +382,7 @@ impl TryFrom for GetAssertionHmacSecretInput { } // Even though options are optional, we can use the default if not present. -#[derive(Default)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, Default, PartialEq)] pub struct MakeCredentialOptions { pub rk: bool, pub uv: bool, @@ -425,7 +415,7 @@ impl TryFrom for MakeCredentialOptions { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Debug, PartialEq)] pub struct GetAssertionOptions { pub up: bool, pub uv: bool, @@ -470,8 +460,7 @@ impl TryFrom for GetAssertionOptions { } // https://www.w3.org/TR/webauthn/#packed-attestation -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct PackedAttestationStatement { pub alg: i64, pub sig: Vec, @@ -490,8 +479,7 @@ impl From for cbor::Value { } } -#[derive(PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub enum SignatureAlgorithm { ES256 = ES256_ALGORITHM as isize, // This is the default for all numbers not covered above. @@ -516,8 +504,7 @@ impl TryFrom for SignatureAlgorithm { } } -#[derive(Clone, Copy, PartialEq, PartialOrd)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialProtectionPolicy { UserVerificationOptional = 0x01, @@ -548,9 +535,7 @@ impl TryFrom for CredentialProtectionPolicy { // // Note that we only use the WebAuthn definition as an example. This data-structure is not specified // by FIDO. In particular we may choose how we serialize and deserialize it. -#[derive(Clone)] -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialSource { // TODO function to convert to / from Vec pub key_type: PublicKeyCredentialType, @@ -688,8 +673,7 @@ impl PublicKeyCredentialSource { } // The COSE key is used for both ECDH and ECDSA public keys for transmission. -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct CoseKey { x_bytes: [u8; ecdh::NBYTES], y_bytes: [u8; ecdh::NBYTES], @@ -818,7 +802,7 @@ impl TryFrom for ecdh::PubKey { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ClientPinSubCommand { GetPinRetries = 0x01, @@ -856,8 +840,7 @@ impl TryFrom for ClientPinSubCommand { } } -#[derive(Clone, Copy)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ConfigSubCommand { EnableEnterpriseAttestation = 0x01, @@ -887,8 +870,7 @@ impl TryFrom for ConfigSubCommand { } } -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub enum ConfigSubCommandParams { SetMinPinLength(SetMinPinLengthParams), } @@ -903,8 +885,7 @@ impl From for cbor::Value { } } -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct SetMinPinLengthParams { pub new_min_pin_length: Option, pub min_pin_length_rp_ids: Option>, @@ -958,8 +939,7 @@ impl From for cbor::Value { } } -#[derive(Clone, Copy)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialManagementSubCommand { GetCredsMetadata = 0x01, @@ -995,8 +975,7 @@ impl TryFrom for CredentialManagementSubCommand { } } -#[derive(Clone)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct CredentialManagementSubCommandParameters { pub rp_id_hash: Option>, pub credential_id: Option, diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 346a348..093d4c9 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -22,8 +22,7 @@ use alloc::string::String; use alloc::vec::Vec; use cbor::{cbor_array_vec, cbor_bool, cbor_map_btree, cbor_map_options, cbor_text}; -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub enum ResponseData { AuthenticatorMakeCredential(AuthenticatorMakeCredentialResponse), AuthenticatorGetAssertion(AuthenticatorGetAssertionResponse), @@ -57,8 +56,7 @@ impl From for Option { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorMakeCredentialResponse { pub fmt: String, pub auth_data: Vec, @@ -84,8 +82,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorGetAssertionResponse { pub credential: Option, pub auth_data: Vec, @@ -117,8 +114,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorGetInfoResponse { pub versions: Vec, pub extensions: Option>, @@ -191,8 +187,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorClientPinResponse { pub key_agreement: Option, pub pin_token: Option>, @@ -215,8 +210,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorLargeBlobsResponse { pub config: Vec, } @@ -231,9 +225,7 @@ impl From for cbor::Value { } } -#[derive(Default)] -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, Default, PartialEq)] pub struct AuthenticatorCredentialManagementResponse { pub existing_resident_credentials_count: Option, pub max_possible_remaining_resident_credentials_count: Option, @@ -280,8 +272,7 @@ impl From for cbor::Value { } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorVendorResponse { pub cert_programmed: bool, pub pkey_programmed: bool, From 9270afbc21022f862a7d3c15802754b60a4ac66a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 2 Feb 2021 06:42:49 +0100 Subject: [PATCH 157/192] remove derive_debug feature --- Cargo.toml | 4 ++-- libraries/crypto/Cargo.toml | 1 - run_desktop_tests.sh | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bca9210..b7b0360 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,9 @@ subtle = { version = "2.2", default-features = false, features = ["nightly"] } [features] debug_allocations = ["lang_items/debug_allocations"] -debug_ctap = ["crypto/derive_debug", "libtock_drivers/debug_ctap"] +debug_ctap = ["libtock_drivers/debug_ctap"] panic_console = ["lang_items/panic_console"] -std = ["cbor/std", "crypto/std", "crypto/derive_debug", "lang_items/std", "persistent_store/std"] +std = ["cbor/std", "crypto/std", "lang_items/std", "persistent_store/std"] verbose = ["debug_ctap", "libtock_drivers/verbose_usb"] with_ctap1 = ["crypto/with_ctap1"] with_nfc = ["libtock_drivers/with_nfc"] diff --git a/libraries/crypto/Cargo.toml b/libraries/crypto/Cargo.toml index ead1294..aa1a597 100644 --- a/libraries/crypto/Cargo.toml +++ b/libraries/crypto/Cargo.toml @@ -25,5 +25,4 @@ regex = { version = "1", optional = true } [features] std = ["cbor/std", "hex", "rand", "ring", "untrusted", "serde", "serde_json", "regex"] -derive_debug = [] with_ctap1 = [] diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index 24771f7..b54d3a8 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -91,7 +91,7 @@ then cargo test --release --features std cd ../.. cd libraries/crypto - RUSTFLAGS='-C target-feature=+aes' cargo test --release --features std,derive_debug + RUSTFLAGS='-C target-feature=+aes' cargo test --release --features std cd ../.. cd libraries/persistent_store cargo test --release --features std @@ -103,7 +103,7 @@ then cargo test --features std cd ../.. cd libraries/crypto - RUSTFLAGS='-C target-feature=+aes' cargo test --features std,derive_debug + RUSTFLAGS='-C target-feature=+aes' cargo test --features std cd ../.. cd libraries/persistent_store cargo test --features std From f64567febc45fc738b2fc16d3b2e79b4faad3a72 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 2 Feb 2021 06:52:01 +0100 Subject: [PATCH 158/192] fix crypto workflow --- .github/workflows/crypto_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/crypto_test.yml b/.github/workflows/crypto_test.yml index 50fdf88..5abfce9 100644 --- a/.github/workflows/crypto_test.yml +++ b/.github/workflows/crypto_test.yml @@ -33,10 +33,10 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --manifest-path libraries/crypto/Cargo.toml --release --features std,derive_debug + args: --manifest-path libraries/crypto/Cargo.toml --release --features std - name: Unit testing of crypto library (debug mode) uses: actions-rs/cargo@v1 with: command: test - args: --manifest-path libraries/crypto/Cargo.toml --features std,derive_debug + args: --manifest-path libraries/crypto/Cargo.toml --features std From db7ed10f5f45aaa8173a43d4f8560bac9cd4dfe0 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 2 Feb 2021 18:04:29 +0100 Subject: [PATCH 159/192] changes the handling of 0 credentials --- src/ctap/credential_management.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index acc9278..f0721ac 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -46,16 +46,15 @@ fn get_stored_rp_ids( /// Generates the response for subcommands enumerating RPs. fn enumerate_rps_response( - rp_id: Option, + rp_id: String, total_rps: Option, ) -> Result { - let rp = rp_id.clone().map(|rp_id| PublicKeyCredentialRpEntity { + let rp_id_hash = Some(Sha256::hash(rp_id.as_bytes()).to_vec()); + let rp = Some(PublicKeyCredentialRpEntity { rp_id, rp_name: None, rp_icon: None, }); - let rp_id_hash = rp_id.map(|rp_id| Sha256::hash(rp_id.as_bytes()).to_vec()); - Ok(AuthenticatorCredentialManagementResponse { rp, rp_id_hash, @@ -128,12 +127,15 @@ fn process_enumerate_rps_begin( let rp_set = get_stored_rp_ids(persistent_store)?; let total_rps = rp_set.len(); - // TODO(kaczmarczyck) should we return CTAP2_ERR_NO_CREDENTIALS if empty? if total_rps > 1 { stateful_command_permission.set_command(now, StatefulCommand::EnumerateRps(1)); } // TODO https://github.com/rust-lang/rust/issues/62924 replace with pop_first() - enumerate_rps_response(rp_set.into_iter().next(), Some(total_rps as u64)) + let rp_id = rp_set + .into_iter() + .next() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + enumerate_rps_response(rp_id, Some(total_rps as u64)) } /// Processes the subcommand enumerateRPsGetNextRP for CredentialManagement. @@ -148,7 +150,7 @@ fn process_enumerate_rps_get_next_rp( .into_iter() .nth(rp_id_index) .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - enumerate_rps_response(Some(rp_id), None) + enumerate_rps_response(rp_id, None) } /// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. From e3148319c5dc60dcacbefac49b79c57ce902a0db Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 4 Feb 2021 16:06:25 +0100 Subject: [PATCH 160/192] allow RP ID permissions for some subcommands --- src/ctap/credential_management.rs | 47 +++++++++++---- src/ctap/mod.rs | 4 +- src/ctap/pin_protocol_v1.rs | 99 ++++++++++++++++++++++++++----- src/ctap/storage.rs | 2 +- 4 files changed, 122 insertions(+), 30 deletions(-) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index acc9278..b4ecf6f 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -106,6 +106,22 @@ fn enumerate_credentials_response( }) } +/// Check if the token permissions have the correct associated RP ID. +/// +/// Either no RP ID is associated, or the RP ID matches the stored credential. +fn check_rp_id_permissions( + persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, + credential_id: &[u8], +) -> Result<(), Ctap2StatusCode> { + // Pre-check a sufficient condition before calling the store. + if pin_protocol_v1.has_no_rp_id_permission().is_ok() { + return Ok(()); + } + let (_, credential) = persistent_store.find_credential_item(credential_id)?; + pin_protocol_v1.has_no_or_rp_id_permission(&credential.rp_id) +} + /// Processes the subcommand getCredsMetadata for CredentialManagement. fn process_get_creds_metadata( persistent_store: &PersistentStore, @@ -155,12 +171,14 @@ fn process_enumerate_rps_get_next_rp( fn process_enumerate_credentials_begin( persistent_store: &PersistentStore, stateful_command_permission: &mut StatefulPermission, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, now: ClockValue, ) -> Result { let rp_id_hash = sub_command_params .rp_id_hash .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash[..])?; let mut iter_result = Ok(()); let iter = persistent_store.iter_credentials(&mut iter_result)?; let mut rp_credentials: Vec = iter @@ -199,18 +217,21 @@ fn process_enumerate_credentials_get_next_credential( /// Processes the subcommand deleteCredential for CredentialManagement. fn process_delete_credential( persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params .credential_id .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)? .key_id; + check_rp_id_permissions(persistent_store, pin_protocol_v1, &credential_id)?; persistent_store.delete_credential(&credential_id) } /// Processes the subcommand updateUserInformation for CredentialManagement. fn process_update_user_information( persistent_store: &mut PersistentStore, + pin_protocol_v1: &mut PinProtocolV1, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params @@ -220,6 +241,7 @@ fn process_update_user_information( let user = sub_command_params .user .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + check_rp_id_permissions(persistent_store, pin_protocol_v1, &credential_id)?; persistent_store.update_credential(&credential_id, user) } @@ -255,13 +277,10 @@ pub fn process_credential_management( match sub_command { CredentialManagementSubCommand::GetCredsMetadata | CredentialManagementSubCommand::EnumerateRpsBegin - | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::EnumerateCredentialsBegin + | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::UpdateUserInformation => { check_pin_uv_auth_protocol(pin_protocol)?; - persistent_store - .pin_hash()? - .ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let mut management_data = vec![sub_command as u8]; if let Some(sub_command_params) = sub_command_params.clone() { @@ -272,9 +291,8 @@ pub fn process_credential_management( if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } + // The RP ID permission is handled differently per subcommand below. pin_protocol_v1.has_permission(PinPermission::CredentialManagement)?; - pin_protocol_v1.has_no_permission_rp_id()?; - // TODO(kaczmarczyck) sometimes allow a RP ID } CredentialManagementSubCommand::EnumerateRpsGetNextRp | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} @@ -282,13 +300,17 @@ pub fn process_credential_management( let response = match sub_command { CredentialManagementSubCommand::GetCredsMetadata => { + pin_protocol_v1.has_no_rp_id_permission()?; Some(process_get_creds_metadata(persistent_store)?) } - CredentialManagementSubCommand::EnumerateRpsBegin => Some(process_enumerate_rps_begin( - persistent_store, - stateful_command_permission, - now, - )?), + CredentialManagementSubCommand::EnumerateRpsBegin => { + pin_protocol_v1.has_no_rp_id_permission()?; + Some(process_enumerate_rps_begin( + persistent_store, + stateful_command_permission, + now, + )?) + } CredentialManagementSubCommand::EnumerateRpsGetNextRp => Some( process_enumerate_rps_get_next_rp(persistent_store, stateful_command_permission)?, ), @@ -296,6 +318,7 @@ pub fn process_credential_management( Some(process_enumerate_credentials_begin( persistent_store, stateful_command_permission, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, now, )?) @@ -309,6 +332,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::DeleteCredential => { process_delete_credential( persistent_store, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None @@ -316,6 +340,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::UpdateUserInformation => { process_update_user_information( persistent_store, + pin_protocol_v1, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index ab66177..0579f62 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -638,7 +638,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; + self.pin_protocol_v1.require_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -923,7 +923,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.has_permission_for_rp_id(&rp_id)?; + self.pin_protocol_v1.require_rp_id_permission(&rp_id)?; UV_FLAG } None => { diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 6ec5644..d95b5d1 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -501,6 +501,7 @@ impl PinProtocolV1 { encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) } + /// Check if the required command's token permission is granted. pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { // Relies on the fact that all permissions are represented by powers of two. if permission as u8 & self.permissions != 0 { @@ -510,22 +511,47 @@ impl PinProtocolV1 { } } - pub fn has_no_permission_rp_id(&self) -> Result<(), Ctap2StatusCode> { + /// Check if no RP ID is associated with the token permission. + pub fn has_no_rp_id_permission(&self) -> Result<(), Ctap2StatusCode> { if self.permissions_rp_id.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } Ok(()) } - 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); + /// Check if no or the passed RP ID is associated with the token permission. + pub fn has_no_or_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + _ => Ok(()), + } + } + + /// Check if no RP ID is associated with the token permission, or it matches the hash. + pub fn has_no_or_rp_id_hash_permission( + &self, + rp_id_hash: &[u8], + ) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id_hash != Sha256::hash(p.as_bytes()) => { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) } - } else { - self.permissions_rp_id = Some(String::from(rp_id)); + _ => Ok(()), + } + } + + /// Check if the passed RP ID is associated with the token permission. + /// + /// If no RP ID is associated, associate the passed RP ID as a side effect. + pub fn require_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + None => { + self.permissions_rp_id = Some(String::from(rp_id)); + Ok(()) + } + _ => Ok(()), } - Ok(()) } #[cfg(test)] @@ -1150,24 +1176,65 @@ mod test { } #[test] - fn test_has_no_permission_rp_id() { + fn test_has_no_rp_id_permission() { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - assert_eq!(pin_protocol_v1.has_no_permission_rp_id(), Ok(())); - assert_eq!(pin_protocol_v1.permissions_rp_id, None,); + assert_eq!(pin_protocol_v1.has_no_rp_id_permission(), Ok(())); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); assert_eq!( - pin_protocol_v1.has_no_permission_rp_id(), + pin_protocol_v1.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } #[test] - fn test_has_permission_for_rp_id() { + fn test_has_no_or_rp_id_permission() { 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"), + pin_protocol_v1.has_no_or_rp_id_permission("example.com"), + Ok(()) + ); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_permission("example.com"), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_permission("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_has_no_or_rp_id_hash_permission() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let rp_id_hash = Sha256::hash(b"example.com"); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + assert_eq!(pin_protocol_v1.permissions_rp_id, None); + pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + assert_eq!( + pin_protocol_v1.has_no_or_rp_id_hash_permission(&[0x4A; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_require_rp_id_permission() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + assert_eq!( + pin_protocol_v1.require_rp_id_permission("example.com"), Ok(()) ); assert_eq!( @@ -1175,11 +1242,11 @@ mod test { Some(String::from("example.com")) ); assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("example.com"), + pin_protocol_v1.require_rp_id_permission("example.com"), Ok(()) ); assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("counter-example.com"), + pin_protocol_v1.require_rp_id_permission("counter-example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b982922..6231e3c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -151,7 +151,7 @@ impl PersistentStore { /// # Errors /// /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. - fn find_credential_item( + pub fn find_credential_item( &self, credential_id: &[u8], ) -> Result<(usize, PublicKeyCredentialSource), Ctap2StatusCode> { From 44b7c3cdc1e4926531f19332be4b5512ce6895ba Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 4 Feb 2021 21:26:00 +0100 Subject: [PATCH 161/192] dummy implementation for enterprise attestation --- src/ctap/command.rs | 8 +++--- src/ctap/config_command.rs | 45 ++++++++++++++++++++++++++++- src/ctap/data_formats.rs | 34 ++++++++++++++++++++++ src/ctap/mod.rs | 58 +++++++++++++++++++++++++++++++++----- src/ctap/response.rs | 5 ++++ src/ctap/storage.rs | 29 +++++++++++++++++++ src/ctap/storage/key.rs | 5 +++- 7 files changed, 171 insertions(+), 13 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index a76254a..eb16a1f 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -161,7 +161,7 @@ pub struct AuthenticatorMakeCredentialParameters { pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, pub pin_uv_auth_protocol: Option, - pub enterprise_attestation: Option, + pub enterprise_attestation: Option, } impl TryFrom for AuthenticatorMakeCredentialParameters { @@ -219,7 +219,7 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; - let enterprise_attestation = enterprise_attestation.map(extract_bool).transpose()?; + let enterprise_attestation = enterprise_attestation.map(extract_unsigned).transpose()?; Ok(AuthenticatorMakeCredentialParameters { client_data_hash, @@ -601,7 +601,7 @@ mod test { 0x05 => cbor_array![], 0x08 => vec![0x12, 0x34], 0x09 => 1, - 0x0A => true, + 0x0A => 2, }; let returned_make_credential_parameters = AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap(); @@ -635,7 +635,7 @@ mod test { options, pin_uv_auth_param: Some(vec![0x12, 0x34]), pin_uv_auth_protocol: Some(1), - enterprise_attestation: Some(true), + enterprise_attestation: Some(2), }; assert_eq!( diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 5e4daf3..351ac1e 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,15 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::check_pin_uv_auth_protocol; use super::command::AuthenticatorConfigParameters; use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; use super::pin_protocol_v1::PinProtocolV1; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use super::{check_pin_uv_auth_protocol, ENTERPRISE_ATTESTATION_MODE}; use alloc::vec; +/// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig. +fn process_enable_enterprise_attestation( + persistent_store: &mut PersistentStore, +) -> Result { + if ENTERPRISE_ATTESTATION_MODE.is_some() { + persistent_store.enable_enterprise_attestation()?; + Ok(ResponseData::AuthenticatorConfig) + } else { + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + } +} + /// Processes the subcommand setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, @@ -85,6 +97,9 @@ pub fn process_config( } match sub_command { + ConfigSubCommand::EnableEnterpriseAttestation => { + process_enable_enterprise_attestation(persistent_store) + } ConfigSubCommand::SetMinPinLength => { if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { process_set_min_pin_length(persistent_store, params) @@ -101,6 +116,34 @@ mod test { use super::*; use crypto::rng256::ThreadRng256; + #[test] + fn test_process_enable_enterprise_attestation() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::EnableEnterpriseAttestation, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + + if ENTERPRISE_ATTESTATION_MODE.is_some() { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert_eq!(persistent_store.enterprise_attestation(), Ok(true)); + } else { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + } + fn create_min_pin_config_params( min_pin_length: u8, min_pin_length_rp_ids: Option>, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 1992469..9f4b68c 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -939,6 +939,24 @@ impl From for cbor::Value { } } +#[derive(Debug, PartialEq)] +pub enum EnterpriseAttestationMode { + VendorFacilitated = 0x01, + PlatformManaged = 0x02, +} + +impl TryFrom for EnterpriseAttestationMode { + type Error = Ctap2StatusCode; + + fn try_from(value: u64) -> Result { + match value { + 1 => Ok(EnterpriseAttestationMode::VendorFacilitated), + 2 => Ok(EnterpriseAttestationMode::PlatformManaged), + _ => Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialManagementSubCommand { @@ -1795,6 +1813,22 @@ mod test { assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); } + #[test] + fn test_from_enterprise_attestation_mode() { + assert_eq!( + EnterpriseAttestationMode::try_from(1), + Ok(EnterpriseAttestationMode::VendorFacilitated), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(2), + Ok(EnterpriseAttestationMode::PlatformManaged), + ); + assert_eq!( + EnterpriseAttestationMode::try_from(3), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + ); + } + #[test] fn test_from_into_cred_management_sub_command() { let cbor_sub_command: cbor::Value = cbor_int!(0x01); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index ab66177..eeb43bc 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -36,10 +36,10 @@ use self::command::{ use self::config_command::process_config; use self::credential_management::process_credential_management; use self::data_formats::{ - AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, GetAssertionExtensions, - PackedAttestationStatement, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, - PublicKeyCredentialSource, PublicKeyCredentialType, PublicKeyCredentialUserEntity, - SignatureAlgorithm, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, + GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, + PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, + PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; @@ -61,6 +61,7 @@ use alloc::vec::Vec; use arrayref::array_ref; use byteorder::{BigEndian, ByteOrder}; use cbor::cbor_map_options; +use core::convert::TryFrom; #[cfg(feature = "debug_ctap")] use core::fmt::Write; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; @@ -86,6 +87,18 @@ const USE_BATCH_ATTESTATION: bool = false; // solution is a compromise to be compatible with U2F and not wasting storage. const USE_SIGNATURE_COUNTER: bool = true; pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; +// This flag allows usage of enterprise attestation. For privacy reasons, it is +// disabled by default. You can choose between +// - EnterpriseAttestationMode::VendorFacilitated, +// - EnterpriseAttestationMode::PlatformManaged. +// For VendorFacilitated, choose an appriopriate ENTERPRISE_RP_ID_LIST. +// To enable the feature, send the subcommand enableEnterpriseAttestation in +// AuthenticatorConfig. An enterprise might want to customize the type of +// attestation that is used. OpenSK defaults to batch attestation. Configuring +// individual certificates then makes authenticators identifiable. Do NOT set +// USE_BATCH_ATTESTATION to true at the same time in this case! +pub const ENTERPRISE_ATTESTATION_MODE: Option = None; +const ENTERPRISE_RP_ID_LIST: Vec = Vec::new(); // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -562,7 +575,7 @@ where options, pin_uv_auth_param, pin_uv_auth_protocol, - enterprise_attestation: _, + enterprise_attestation, } = make_credential_params; self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; @@ -572,6 +585,26 @@ where } let rp_id = rp.rp_id; + let ep_att = if let Some(enterprise_attestation) = enterprise_attestation { + let authenticator_mode = + ENTERPRISE_ATTESTATION_MODE.ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + if !self.persistent_store.enterprise_attestation()? { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + match ( + EnterpriseAttestationMode::try_from(enterprise_attestation)?, + authenticator_mode, + ) { + ( + EnterpriseAttestationMode::PlatformManaged, + EnterpriseAttestationMode::PlatformManaged, + ) => ENTERPRISE_RP_ID_LIST.contains(&rp_id), + _ => true, + } + } else { + false + }; + let mut cred_protect_policy = extensions.cred_protect; if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) @@ -723,7 +756,7 @@ where let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); - let (signature, x5c) = if USE_BATCH_ATTESTATION { + let (signature, x5c) = if USE_BATCH_ATTESTATION || ep_att { let attestation_private_key = self .persistent_store .attestation_private_key()? @@ -750,11 +783,13 @@ where x5c, ecdaa_key_id: None, }; + let ep_att = if ep_att { Some(true) } else { None }; Ok(ResponseData::AuthenticatorMakeCredential( AuthenticatorMakeCredentialResponse { fmt: String::from("packed"), auth_data, att_stmt: attestation_statement, + ep_att, large_blob_key, }, )) @@ -1026,6 +1061,12 @@ where options_map.insert(String::from("up"), true); options_map.insert(String::from("pinUvAuthToken"), true); options_map.insert(String::from("largeBlobs"), true); + if ENTERPRISE_ATTESTATION_MODE.is_some() { + options_map.insert( + String::from("ep"), + self.persistent_store.enterprise_attestation()?, + ); + } options_map.insert(String::from("authnrCfg"), true); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); @@ -1227,6 +1268,7 @@ mod test { fmt, auth_data, att_stmt, + ep_att, large_blob_key, } = make_credential_response; // The expected response is split to only assert the non-random parts. @@ -1247,6 +1289,7 @@ mod test { &auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], expected_extension_cbor ); + assert!(ep_att.is_none()); assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); assert_eq!(large_blob_key, None); } @@ -1276,12 +1319,13 @@ mod test { String::from("largeBlobKey"), ]], 0x03 => ctap_state.persistent_store.aaguid().unwrap(), - 0x04 => cbor_map! { + 0x04 => cbor_map_options! { "rk" => true, "clientPin" => false, "up" => true, "pinUvAuthToken" => true, "largeBlobs" => true, + "ep" => ENTERPRISE_ATTESTATION_MODE.map(|_| false), "authnrCfg" => true, "credMgmt" => true, "setMinPINLength" => true, diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 093d4c9..b6b3d25 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -61,6 +61,7 @@ pub struct AuthenticatorMakeCredentialResponse { pub fmt: String, pub auth_data: Vec, pub att_stmt: PackedAttestationStatement, + pub ep_att: Option, pub large_blob_key: Option>, } @@ -70,6 +71,7 @@ impl From for cbor::Value { fmt, auth_data, att_stmt, + ep_att, large_blob_key, } = make_credential_response; @@ -77,6 +79,7 @@ impl From for cbor::Value { 0x01 => fmt, 0x02 => auth_data, 0x03 => att_stmt, + 0x04 => ep_att, 0x05 => large_blob_key, } } @@ -320,6 +323,7 @@ mod test { fmt: "packed".to_string(), auth_data: vec![0xAD], att_stmt, + ep_att: Some(true), large_blob_key: Some(vec![0x1B]), }; let response_cbor: Option = @@ -328,6 +332,7 @@ mod test { 0x01 => "packed", 0x02 => vec![0xAD], 0x03 => cbor_packed_attestation_statement, + 0x04 => true, 0x05 => vec![0x1B], }; assert_eq!(response_cbor, Some(expected_cbor)); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b982922..c38146a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -610,6 +610,23 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } + + /// Returns whether enterprise attestation is enabled. + pub fn enterprise_attestation(&self) -> Result { + match self.store.find(key::ENTERPRISE_ATTESTATION)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Marks enterprise attestation as enabled. + pub fn enable_enterprise_attestation(&mut self) -> Result<(), Ctap2StatusCode> { + if !self.enterprise_attestation()? { + self.store.insert(key::ENTERPRISE_ATTESTATION, &[])?; + } + Ok(()) + } } impl From for Ctap2StatusCode { @@ -1308,6 +1325,18 @@ mod test { assert!(!persistent_store.has_force_pin_change().unwrap()); } + #[test] + fn test_enterprise_attestation() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.enterprise_attestation().unwrap()); + assert_eq!(persistent_store.enable_enterprise_attestation(), Ok(())); + assert!(persistent_store.enterprise_attestation().unwrap()); + persistent_store.reset(&mut rng).unwrap(); + assert!(!persistent_store.enterprise_attestation().unwrap()); + } + #[test] fn test_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 2093685..dd9f67e 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -93,7 +93,10 @@ make_partition! { /// The stored large blob can be too big for one key, so it has to be sharded. LARGE_BLOB_SHARDS = 2000..2004; - /// If this entry exists and equals 1, the PIN needs to be changed. + /// If this entry exists and is empty, enterprise attestation is enabled. + ENTERPRISE_ATTESTATION = 2039; + + /// If this entry exists and is empty, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; /// The secret of the CredRandom feature. From 53e05913634fe93bfb9887b2eb354fa56baeba86 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 4 Feb 2021 21:33:01 +0100 Subject: [PATCH 162/192] adds some documenation for enterprise attestation --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index da46d7f..92ddac0 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,9 @@ a few things you can personalize: length. 1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs bigger than the default value. +1. Implement enterprise attestation. This can be as easy as setting + ENTERPRISE_ATTESTATION_MODE in `ctap/mod.rs`. If you want to use a different + attestation type than batch attestation, you have to implement it first. ### 3D printed enclosure From 49cccfd270aa7fe0bfdca13ac9959d580e1ec8db Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 11:23:12 +0100 Subject: [PATCH 163/192] correct const arrays of strings --- src/ctap/data_formats.rs | 4 ++++ src/ctap/mod.rs | 4 ++-- src/ctap/storage.rs | 25 ++++++++++++++++--------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 9f4b68c..04e9a36 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -1815,6 +1815,10 @@ mod test { #[test] fn test_from_enterprise_attestation_mode() { + assert_eq!( + EnterpriseAttestationMode::try_from(0), + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION), + ); assert_eq!( EnterpriseAttestationMode::try_from(1), Ok(EnterpriseAttestationMode::VendorFacilitated), diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index eeb43bc..b9a88b4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -98,7 +98,7 @@ pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; // individual certificates then makes authenticators identifiable. Do NOT set // USE_BATCH_ATTESTATION to true at the same time in this case! pub const ENTERPRISE_ATTESTATION_MODE: Option = None; -const ENTERPRISE_RP_ID_LIST: Vec = Vec::new(); +const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; // Our credential ID consists of // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -598,7 +598,7 @@ where ( EnterpriseAttestationMode::PlatformManaged, EnterpriseAttestationMode::PlatformManaged, - ) => ENTERPRISE_RP_ID_LIST.contains(&rp_id), + ) => ENTERPRISE_RP_ID_LIST.contains(&rp_id.as_str()), _ => true, } } else { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index c38146a..777f71f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -56,7 +56,7 @@ const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150; const MAX_PIN_RETRIES: u8 = 8; const DEFAULT_MIN_PIN_LENGTH: u8 = 4; -const DEFAULT_MIN_PIN_LENGTH_RP_IDS: Vec = Vec::new(); +const DEFAULT_MIN_PIN_LENGTH_RP_IDS: &[&str] = &[]; // This constant is an attempt to limit storage requirements. If you don't set it to 0, // the stored strings can still be unbounded, but that is true for all RP IDs. pub const MAX_RP_IDS_LENGTH: usize = 8; @@ -439,12 +439,17 @@ impl PersistentStore { /// Returns the list of RP IDs that are used to check if reading the minimum PIN length is /// allowed. pub fn min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { - let rp_ids = self - .store - .find(key::MIN_PIN_LENGTH_RP_IDS)? - .map_or(Some(DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { - deserialize_min_pin_length_rp_ids(&value) - }); + let rp_ids = self.store.find(key::MIN_PIN_LENGTH_RP_IDS)?.map_or_else( + || { + Some( + DEFAULT_MIN_PIN_LENGTH_RP_IDS + .iter() + .map(|&s| String::from(s)) + .collect(), + ) + }, + |value| deserialize_min_pin_length_rp_ids(&value), + ); debug_assert!(rp_ids.is_some()); Ok(rp_ids.unwrap_or_default()) } @@ -455,7 +460,8 @@ impl PersistentStore { 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 { + for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(*rp_id); if !min_pin_length_rp_ids.contains(&rp_id) { min_pin_length_rp_ids.push(rp_id); } @@ -1203,7 +1209,8 @@ mod test { persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); - for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS { + for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = rp_id.to_string().to_string(); if !rp_ids.contains(&rp_id) { rp_ids.push(rp_id); } From 502006e29ead0e39425e4707b3b323a950c38c7f Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 11:57:47 +0100 Subject: [PATCH 164/192] fix string conversion style --- src/ctap/storage.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 777f71f..f2f8220 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -460,8 +460,8 @@ impl PersistentStore { 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.iter() { - let rp_id = String::from(*rp_id); + for &rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(rp_id); if !min_pin_length_rp_ids.contains(&rp_id) { min_pin_length_rp_ids.push(rp_id); } @@ -1209,8 +1209,8 @@ mod test { persistent_store.set_min_pin_length_rp_ids(rp_ids.clone()), Ok(()) ); - for rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { - let rp_id = rp_id.to_string().to_string(); + for &rp_id in DEFAULT_MIN_PIN_LENGTH_RP_IDS.iter() { + let rp_id = String::from(rp_id); if !rp_ids.contains(&rp_id) { rp_ids.push(rp_id); } From 604f0848157f660dff850b190f1fc42375e4fcb4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 14:52:38 +0100 Subject: [PATCH 165/192] rename require_ to ensure --- src/ctap/mod.rs | 4 ++-- src/ctap/pin_protocol_v1.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 0579f62..4d153f4 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -638,7 +638,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.require_rp_id_permission(&rp_id)?; + self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -923,7 +923,7 @@ where } self.pin_protocol_v1 .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.require_rp_id_permission(&rp_id)?; + self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; UV_FLAG } None => { diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index d95b5d1..3b00a35 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -543,7 +543,7 @@ impl PinProtocolV1 { /// Check if the passed RP ID is associated with the token permission. /// /// If no RP ID is associated, associate the passed RP ID as a side effect. - pub fn require_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + pub fn ensure_rp_id_permission(&mut self, rp_id: &str) -> Result<(), Ctap2StatusCode> { match &self.permissions_rp_id { Some(p) if rp_id != p => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), None => { @@ -1230,11 +1230,11 @@ mod test { } #[test] - fn test_require_rp_id_permission() { + fn test_ensure_rp_id_permission() { let mut rng = ThreadRng256 {}; let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); assert_eq!( - pin_protocol_v1.require_rp_id_permission("example.com"), + pin_protocol_v1.ensure_rp_id_permission("example.com"), Ok(()) ); assert_eq!( @@ -1242,11 +1242,11 @@ mod test { Some(String::from("example.com")) ); assert_eq!( - pin_protocol_v1.require_rp_id_permission("example.com"), + pin_protocol_v1.ensure_rp_id_permission("example.com"), Ok(()) ); assert_eq!( - pin_protocol_v1.require_rp_id_permission("counter-example.com"), + pin_protocol_v1.ensure_rp_id_permission("counter-example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } From f90d43a6a1e8c27e7bef162f0d67d84e732d12e2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 18:43:27 +0100 Subject: [PATCH 166/192] implements alwaysUv and makeCredUvNotRqd --- src/ctap/apdu.rs | 1 + src/ctap/config_command.rs | 105 ++++++++++++++++++++++++++++++++++- src/ctap/ctap1.rs | 21 +++++++ src/ctap/hid/mod.rs | 2 +- src/ctap/large_blobs.rs | 2 +- src/ctap/mod.rs | 110 ++++++++++++++++++++++++++++++++++--- src/ctap/storage.rs | 30 ++++++++++ src/ctap/storage/key.rs | 3 + 8 files changed, 262 insertions(+), 12 deletions(-) diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index f12ded2..455e574 100644 --- a/src/ctap/apdu.rs +++ b/src/ctap/apdu.rs @@ -29,6 +29,7 @@ pub enum ApduStatusCode { SW_WRONG_DATA = 0x6a_80, SW_WRONG_LENGTH = 0x67_00, SW_COND_USE_NOT_SATISFIED = 0x69_85, + SW_COMMAND_NOT_ALLOWED = 0x69_86, SW_FILE_NOT_FOUND = 0x6a_82, SW_INCORRECT_P1P2 = 0x6a_86, /// Instruction code not supported or invalid diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 5e4daf3..edff8f0 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -21,6 +21,21 @@ use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; use alloc::vec; +/// The specification mandates that authenticators support users disabling +/// alwaysUv unless required not to by specific external certifications. +const CAN_DISABLE_ALWAYS_UV: bool = true; + +/// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig. +fn process_toggle_always_uv( + persistent_store: &mut PersistentStore, +) -> Result { + if !CAN_DISABLE_ALWAYS_UV && persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); + } + persistent_store.toggle_always_uv()?; + Ok(ResponseData::AuthenticatorConfig) +} + /// Processes the subcommand setMinPINLength for AuthenticatorConfig. fn process_set_min_pin_length( persistent_store: &mut PersistentStore, @@ -66,7 +81,11 @@ pub fn process_config( pin_uv_auth_protocol, } = params; - if persistent_store.pin_hash()?.is_some() { + let enforce_uv = match sub_command { + ConfigSubCommand::ToggleAlwaysUv => false, + _ => true, + } && persistent_store.has_always_uv()?; + if persistent_store.pin_hash()?.is_some() || enforce_uv { // TODO(kaczmarczyck) The error code is specified inconsistently with other commands. check_pin_uv_auth_protocol(pin_uv_auth_protocol) .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; @@ -85,6 +104,7 @@ pub fn process_config( } match sub_command { + ConfigSubCommand::ToggleAlwaysUv => process_toggle_always_uv(persistent_store), ConfigSubCommand::SetMinPinLength => { if let Some(ConfigSubCommandParams::SetMinPinLength(params)) = sub_command_params { process_set_min_pin_length(persistent_store, params) @@ -101,6 +121,89 @@ mod test { use super::*; use crypto::rng256::ThreadRng256; + #[test] + fn test_process_toggle_always_uv() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(persistent_store.has_always_uv().unwrap()); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + if CAN_DISABLE_ALWAYS_UV { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); + } else { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); + assert!(persistent_store.has_always_uv().unwrap()); + } + } + + #[test] + fn test_process_toggle_always_uv_with_pin() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x55; 32]; + let mut pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + let pin_uv_auth_param = Some(vec![ + 0x99, 0xBA, 0x0A, 0x57, 0x9D, 0x95, 0x5A, 0x44, 0xE3, 0x77, 0xCF, 0x95, 0x51, 0x3F, + 0xFD, 0xBE, + ]); + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + pin_uv_auth_param: pin_uv_auth_param.clone(), + pin_uv_auth_protocol: Some(1), + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(persistent_store.has_always_uv().unwrap()); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + pin_uv_auth_param, + pin_uv_auth_protocol: Some(1), + }; + let config_response = + process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + if CAN_DISABLE_ALWAYS_UV { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); + } else { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); + assert!(persistent_store.has_always_uv().unwrap()); + } + } + fn create_min_pin_config_params( min_pin_length: u8, min_pin_length_rp_ids: Option>, diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 09a3c6c..d5e60ee 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -189,6 +189,12 @@ impl Ctap1Command { R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>, { + if !ctap_state + .allows_ctap1() + .map_err(|_| Ctap1StatusCode::SW_INTERNAL_EXCEPTION)? + { + return Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED); + } let command = U2fCommand::try_from(message)?; match command { U2fCommand::Register { @@ -398,6 +404,21 @@ mod test { message } + #[test] + fn test_process_allowed() { + let mut rng = ThreadRng256 {}; + let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); + ctap_state.persistent_store.toggle_always_uv().unwrap(); + + let application = [0x0A; 32]; + let message = create_register_message(&application); + ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); + ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_NOT_ALLOWED)); + } + #[test] fn test_process_register() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 71bd7c8..03cb637 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -177,7 +177,7 @@ impl CtapHid { match message.cmd { // CTAP specification (version 20190130) section 8.1.9.1.1 CtapHid::COMMAND_MSG => { - // If we don't have CTAP1 backward compatibilty, this command in invalid. + // If we don't have CTAP1 backward compatibilty, this command is invalid. #[cfg(not(feature = "with_ctap1"))] return CtapHid::error_message(cid, CtapHid::ERR_INVALID_CMD); diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index ab38df0..f84c1a9 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -88,7 +88,7 @@ impl LargeBlobs { if offset != self.expected_next_offset { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ); } - if persistent_store.pin_hash()?.is_some() { + if persistent_store.pin_hash()?.is_some() || persistent_store.has_always_uv()? { let pin_uv_auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; // TODO(kaczmarczyck) Error codes for PIN protocol differ across commands. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index ab66177..8b90b68 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -341,6 +341,14 @@ where Ok(()) } + // Returns whether CTAP1 commands are currently supported. + // If alwaysUv is enabled and the authenticator does not support internal UV, + // CTAP1 needs to be disabled. + #[cfg(feature = "with_ctap1")] + pub fn allows_ctap1(&self) -> Result { + Ok(!self.persistent_store.has_always_uv()?) + } + // Encrypts the private key and relying party ID hash into a credential ID. Other // information, such as a user name, are not stored, because encrypted credential IDs // are used for credentials stored server-side. Also, we want the key handle to be @@ -642,7 +650,11 @@ where UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { - if self.persistent_store.pin_hash()?.is_some() { + if self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } + // Corresponds to makeCredUvNotRqd set to true. + if options.rk && self.persistent_store.pin_hash()?.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); } if options.uv { @@ -927,8 +939,10 @@ where UV_FLAG } None => { + if self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } if options.uv { - // The specification (inconsistently) wants CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } 0x00 @@ -1017,6 +1031,23 @@ where } fn process_get_info(&self) -> Result { + let has_always_uv = self.persistent_store.has_always_uv()?; + #[cfg(feature = "with_ctap1")] + let mut versions = vec![ + String::from(FIDO2_VERSION_STRING), + String::from(FIDO2_1_VERSION_STRING), + ]; + #[cfg(feature = "with_ctap1")] + { + if !has_always_uv { + versions.insert(0, String::from(U2F_VERSION_STRING)) + } + } + #[cfg(not(feature = "with_ctap1"))] + let versions = vec![ + String::from(FIDO2_VERSION_STRING), + String::from(FIDO2_1_VERSION_STRING), + ]; let mut options_map = BTreeMap::new(); options_map.insert(String::from("rk"), true); options_map.insert( @@ -1029,15 +1060,11 @@ where options_map.insert(String::from("authnrCfg"), true); options_map.insert(String::from("credMgmt"), true); options_map.insert(String::from("setMinPINLength"), true); - options_map.insert(String::from("makeCredUvNotRqd"), true); + options_map.insert(String::from("makeCredUvNotRqd"), !has_always_uv); + options_map.insert(String::from("alwaysUv"), has_always_uv); Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { - versions: vec![ - #[cfg(feature = "with_ctap1")] - String::from(U2F_VERSION_STRING), - String::from(FIDO2_VERSION_STRING), - String::from(FIDO2_1_VERSION_STRING), - ], + versions, extensions: Some(vec![ String::from("hmac-secret"), String::from("credProtect"), @@ -1286,6 +1313,7 @@ mod test { "credMgmt" => true, "setMinPINLength" => true, "makeCredUvNotRqd" => true, + "alwaysUv" => false, }, 0x05 => MAX_MSG_SIZE as u64, 0x06 => cbor_array_vec![vec![1]], @@ -1727,6 +1755,70 @@ mod test { assert_eq!(stored_credential.large_blob_key.unwrap(), large_blob_key); } + #[test] + fn test_non_resident_process_make_credential_with_pin() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + check_make_response( + make_credential_response, + 0x41, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x70, + &[], + ); + } + + #[test] + fn test_resident_process_make_credential_with_pin() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + let make_credential_params = create_minimal_make_credential_parameters(); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED) + ); + } + + #[test] + fn test_process_make_credential_with_pin_always_uv() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + ctap_state.persistent_store.toggle_always_uv().unwrap(); + let make_credential_params = create_minimal_make_credential_parameters(); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED) + ); + + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.pin_uv_auth_param = Some(vec![0xA4; 16]); + make_credential_params.pin_uv_auth_protocol = Some(1); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert_eq!( + make_credential_response, + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b982922..b586d6d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -610,6 +610,24 @@ impl PersistentStore { pub fn force_pin_change(&mut self) -> Result<(), Ctap2StatusCode> { Ok(self.store.insert(key::FORCE_PIN_CHANGE, &[])?) } + + /// Returns whether alwaysUv is enabled. + pub fn has_always_uv(&self) -> Result { + match self.store.find(key::ALWAYS_UV)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Enables alwaysUv, when disabled, and vice versa. + pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> { + if self.has_always_uv()? { + Ok(self.store.remove(key::ALWAYS_UV)?) + } else { + Ok(self.store.insert(key::ALWAYS_UV, &[])?) + } + } } impl From for Ctap2StatusCode { @@ -1308,6 +1326,18 @@ mod test { assert!(!persistent_store.has_force_pin_change().unwrap()); } + #[test] + fn test_always_uv() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + assert!(!persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(!persistent_store.has_always_uv().unwrap()); + } + #[test] fn test_serialize_deserialize_credential() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 2093685..9387931 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -93,6 +93,9 @@ make_partition! { /// The stored large blob can be too big for one key, so it has to be sharded. LARGE_BLOB_SHARDS = 2000..2004; + /// If this entry exists and is empty, alwaysUv is enabled. + ALWAYS_UV = 2038; + /// If this entry exists and equals 1, the PIN needs to be changed. FORCE_PIN_CHANGE = 2040; From 842c592c9fab0d6f7f0147c4112bb5333f4934b9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 5 Feb 2021 18:51:56 +0100 Subject: [PATCH 167/192] adds changes to README --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index da46d7f..14c3dc6 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,12 @@ a few things you can personalize: length. 1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs bigger than the default value. +1. If a certification (additional to FIDO's) requires that all requests are + protected with user verification, set `CAN_DISABLE_ALWAYS_UV` in + `ctap/config_command.rs` to `false`. In that case, consider deploying + authenticators after calling `toggleAlwaysUv` to activate the feature. + Alternatively, you could change `ctap/storage.rs` to set `alwaysUv` in its + initialization. ### 3D printed enclosure From 54e9da7a5b265349f6e27f26ce04887954673740 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 07:49:58 +0100 Subject: [PATCH 168/192] conditional allow instead of cfg not --- src/ctap/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8b90b68..748afe6 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1032,7 +1032,7 @@ where fn process_get_info(&self) -> Result { let has_always_uv = self.persistent_store.has_always_uv()?; - #[cfg(feature = "with_ctap1")] + #[cfg_attr(not(feature = "with_ctap1"), allow(unused_mut))] let mut versions = vec![ String::from(FIDO2_VERSION_STRING), String::from(FIDO2_1_VERSION_STRING), @@ -1043,11 +1043,6 @@ where versions.insert(0, String::from(U2F_VERSION_STRING)) } } - #[cfg(not(feature = "with_ctap1"))] - let versions = vec![ - String::from(FIDO2_VERSION_STRING), - String::from(FIDO2_1_VERSION_STRING), - ]; let mut options_map = BTreeMap::new(); options_map.insert(String::from("rk"), true); options_map.insert( From e941073a3157520dcf653679a824f45c08d17519 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 13:10:18 +0100 Subject: [PATCH 169/192] new test for attestation configuration --- src/ctap/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index e89b15b..da6ba72 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -2764,4 +2764,19 @@ mod test { )) ); } + + #[test] + #[allow(clippy::assertions_on_constants)] + /// Make sure that privacy guarantees are uphold. + /// + /// The current enterprise attestation implementation reuses batch + /// attestation. Enterprise attestation would imply a batch size of 1, but + /// batch attestation needs a batch size of at least 100k. To prevent + /// accidential misconfiguration, this test allows only one of the constants + /// to be set. If you implement your own enterprise attestation mechanism, + /// and you want batch attestation at the same time, feel free to proceed + /// carefully and remove this test. + fn check_attestation_privacy() { + assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); + } } From 88a3c0fc803ce2444226c62eb43d7f309a3800f4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 16:30:14 +0100 Subject: [PATCH 170/192] assert correct const usage in code --- src/ctap/mod.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index da6ba72..f2c3ea6 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -75,9 +75,11 @@ use libtock_drivers::crp; use libtock_drivers::timer::{ClockValue, Duration}; // 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 -// as a batch key. Turn it on if you want attestation. In this case, be aware that -// it is your responsibility to generate your own key material and keep it secret. +// this setting. The basic attestation uses the signing key configured with a +// vendor command as a batch key. If you turn batch attestation on, be aware that +// it is your responsibility to safely generate and store the key material. Also, +// the batches must have size of at least 100k authenticators before using new +// key material. const USE_BATCH_ATTESTATION: bool = false; // The signature counter is currently implemented as a global counter, if you set // this flag to true. The spec strongly suggests to have per-credential-counters, @@ -96,7 +98,10 @@ pub const INITIAL_SIGNATURE_COUNTER: u32 = 1; // AuthenticatorConfig. An enterprise might want to customize the type of // attestation that is used. OpenSK defaults to batch attestation. Configuring // individual certificates then makes authenticators identifiable. Do NOT set -// USE_BATCH_ATTESTATION to true at the same time in this case! +// USE_BATCH_ATTESTATION to true at the same time in this case! The code asserts +// that you don't use the same key material for batch and enterprise attestation. +// If you implement your own enterprise attestation mechanism, and you want batch +// attestation at the same time, proceed carefully and remove the assertion. pub const ENTERPRISE_ATTESTATION_MODE: Option = None; const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; // Our credential ID consists of @@ -321,6 +326,11 @@ where check_user_presence: CheckUserPresence, now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { + #[allow(clippy::assertions_on_constants)] + { + assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); + } + let persistent_store = PersistentStore::new(rng); let pin_protocol_v1 = PinProtocolV1::new(rng); CtapState { @@ -2764,19 +2774,4 @@ mod test { )) ); } - - #[test] - #[allow(clippy::assertions_on_constants)] - /// Make sure that privacy guarantees are uphold. - /// - /// The current enterprise attestation implementation reuses batch - /// attestation. Enterprise attestation would imply a batch size of 1, but - /// batch attestation needs a batch size of at least 100k. To prevent - /// accidential misconfiguration, this test allows only one of the constants - /// to be set. If you implement your own enterprise attestation mechanism, - /// and you want batch attestation at the same time, feel free to proceed - /// carefully and remove this test. - fn check_attestation_privacy() { - assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); - } } From 160c83d242bc7d7609d3297074aa33eb013d6e7a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 17:53:30 +0100 Subject: [PATCH 171/192] changes always uv constant to a clearer version --- src/ctap/config_command.rs | 36 +++++++++++++++--------------------- src/ctap/mod.rs | 3 +++ src/ctap/storage.rs | 22 ++++++++++++++++------ 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index edff8f0..0d55717 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,24 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::check_pin_uv_auth_protocol; use super::command::AuthenticatorConfigParameters; use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; use super::pin_protocol_v1::PinProtocolV1; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use super::{check_pin_uv_auth_protocol, ENFORCE_ALWAYS_UV}; use alloc::vec; -/// The specification mandates that authenticators support users disabling -/// alwaysUv unless required not to by specific external certifications. -const CAN_DISABLE_ALWAYS_UV: bool = true; - /// Processes the subcommand toggleAlwaysUv for AuthenticatorConfig. fn process_toggle_always_uv( persistent_store: &mut PersistentStore, ) -> Result { - if !CAN_DISABLE_ALWAYS_UV && persistent_store.has_always_uv()? { + if ENFORCE_ALWAYS_UV { return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); } persistent_store.toggle_always_uv()?; @@ -148,15 +144,14 @@ mod test { }; let config_response = process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); - if CAN_DISABLE_ALWAYS_UV { - assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); - assert!(!persistent_store.has_always_uv().unwrap()); - } else { + if ENFORCE_ALWAYS_UV { assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) ); - assert!(persistent_store.has_always_uv().unwrap()); + } else { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); } } @@ -181,6 +176,13 @@ mod test { }; let config_response = process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + if ENFORCE_ALWAYS_UV { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); + return; + } assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert!(persistent_store.has_always_uv().unwrap()); @@ -192,16 +194,8 @@ mod test { }; let config_response = process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); - if CAN_DISABLE_ALWAYS_UV { - assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); - assert!(!persistent_store.has_always_uv().unwrap()); - } else { - assert_eq!( - config_response, - Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) - ); - assert!(persistent_store.has_always_uv().unwrap()); - } + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); } fn create_min_pin_config_params( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 748afe6..2180eb7 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -129,6 +129,9 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa const DEFAULT_CRED_PROTECT: Option = None; // Maximum size stored with the credBlob extension. Must be at least 32. const MAX_CRED_BLOB_LENGTH: usize = 32; +// Enforce the alwaysUv option. With this constant set to true, commands require +// a PIN to be set up. The command toggleAlwaysUv will fail to disable alwaysUv. +pub const ENFORCE_ALWAYS_UV: bool = false; // Checks the PIN protocol parameter against all supported versions. pub fn check_pin_uv_auth_protocol( diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b586d6d..3b0fb9e 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -18,10 +18,10 @@ use crate::ctap::data_formats::{ extract_array, extract_text_string, CredentialProtectionPolicy, PublicKeyCredentialSource, PublicKeyCredentialUserEntity, }; -use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; use crate::ctap::INITIAL_SIGNATURE_COUNTER; +use crate::ctap::{key_material, ENFORCE_ALWAYS_UV}; use crate::embedded_flash::{new_storage, Storage}; use alloc::string::String; use alloc::vec; @@ -613,6 +613,9 @@ impl PersistentStore { /// Returns whether alwaysUv is enabled. pub fn has_always_uv(&self) -> Result { + if ENFORCE_ALWAYS_UV { + return Ok(true); + } match self.store.find(key::ALWAYS_UV)? { None => Ok(false), Some(value) if value.is_empty() => Ok(true), @@ -622,6 +625,9 @@ impl PersistentStore { /// Enables alwaysUv, when disabled, and vice versa. pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> { + if ENFORCE_ALWAYS_UV { + return Ok(()); + } if self.has_always_uv()? { Ok(self.store.remove(key::ALWAYS_UV)?) } else { @@ -1331,11 +1337,15 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - assert!(!persistent_store.has_always_uv().unwrap()); - assert_eq!(persistent_store.toggle_always_uv(), Ok(())); - assert!(persistent_store.has_always_uv().unwrap()); - assert_eq!(persistent_store.toggle_always_uv(), Ok(())); - assert!(!persistent_store.has_always_uv().unwrap()); + if ENFORCE_ALWAYS_UV { + assert!(persistent_store.has_always_uv().unwrap()); + } else { + assert!(!persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(persistent_store.has_always_uv().unwrap()); + assert_eq!(persistent_store.toggle_always_uv(), Ok(())); + assert!(!persistent_store.has_always_uv().unwrap()); + } } #[test] From b9072047b3267bf166215454e11cdf442e2bd2c8 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 17:56:27 +0100 Subject: [PATCH 172/192] update README to new constant --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 14c3dc6..41f5378 100644 --- a/README.md +++ b/README.md @@ -126,11 +126,8 @@ a few things you can personalize: 1. Increase the `MAX_CRED_BLOB_LENGTH` in `ctap/mod.rs`, if you expect blobs bigger than the default value. 1. If a certification (additional to FIDO's) requires that all requests are - protected with user verification, set `CAN_DISABLE_ALWAYS_UV` in - `ctap/config_command.rs` to `false`. In that case, consider deploying - authenticators after calling `toggleAlwaysUv` to activate the feature. - Alternatively, you could change `ctap/storage.rs` to set `alwaysUv` in its - initialization. + protected with user verification, set `ENFORCE_ALWAYS_UV` in + `ctap/config_mod.rs` to `true`. ### 3D printed enclosure From 6a31e06a5557fbdf2bd701638418a5dd7bca5138 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 8 Feb 2021 21:54:22 +0100 Subject: [PATCH 173/192] move some logic into storage.rs --- src/ctap/config_command.rs | 6 ++---- src/ctap/mod.rs | 2 +- src/ctap/storage.rs | 6 +++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index b4c7cc0..c65fc56 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -18,7 +18,7 @@ use super::pin_protocol_v1::PinProtocolV1; use super::response::ResponseData; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::{check_pin_uv_auth_protocol, ENFORCE_ALWAYS_UV, ENTERPRISE_ATTESTATION_MODE}; +use super::{check_pin_uv_auth_protocol, ENTERPRISE_ATTESTATION_MODE}; use alloc::vec; /// Processes the subcommand enableEnterpriseAttestation for AuthenticatorConfig. @@ -37,9 +37,6 @@ fn process_enable_enterprise_attestation( fn process_toggle_always_uv( persistent_store: &mut PersistentStore, ) -> Result { - if ENFORCE_ALWAYS_UV { - return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); - } persistent_store.toggle_always_uv()?; Ok(ResponseData::AuthenticatorConfig) } @@ -130,6 +127,7 @@ pub fn process_config( #[cfg(test)] mod test { use super::*; + use crate::ctap::ENFORCE_ALWAYS_UV; use crypto::rng256::ThreadRng256; #[test] diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 85deafe..18b0345 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -148,7 +148,7 @@ const DEFAULT_CRED_PROTECT: Option = None; // Maximum size stored with the credBlob extension. Must be at least 32. const MAX_CRED_BLOB_LENGTH: usize = 32; // Enforce the alwaysUv option. With this constant set to true, commands require -// a PIN to be set up. The command toggleAlwaysUv will fail to disable alwaysUv. +// a PIN to be set up. alwaysUv can not be disabled by commands. pub const ENFORCE_ALWAYS_UV: bool = false; // Checks the PIN protocol parameter against all supported versions. diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a8be6f1..b1aa4ec 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -649,7 +649,7 @@ impl PersistentStore { /// Enables alwaysUv, when disabled, and vice versa. pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> { if ENFORCE_ALWAYS_UV { - return Ok(()); + return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); } if self.has_always_uv()? { Ok(self.store.remove(key::ALWAYS_UV)?) @@ -1375,6 +1375,10 @@ mod test { if ENFORCE_ALWAYS_UV { assert!(persistent_store.has_always_uv().unwrap()); + assert_eq!( + persistent_store.toggle_always_uv(), + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); } else { assert!(!persistent_store.has_always_uv().unwrap()); assert_eq!(persistent_store.toggle_always_uv(), Ok(())); From 958d7a29dc8ab84da7e4f6eae1a97d45e78e04e6 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Thu, 11 Feb 2021 17:44:49 +0100 Subject: [PATCH 174/192] Fix `config.py` tool according to the new API of fido2 python package (#284) * Fix fido2 API update. Since fido2 0.8.1 the device descriptor moved to NamedTuple, breaking our configuration tool. Code is now updated accordingly and the setup script ensure we're using the correct version for fido2 package. * Make Yapf happy * Fix missing update for fido2 0.9.1 Also split the comment into 2 lines so that the touch is not hidden at the end of the screen. --- setup.sh | 2 +- tools/configure.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/setup.sh b/setup.sh index 13ad9b0..6d58053 100755 --- a/setup.sh +++ b/setup.sh @@ -46,4 +46,4 @@ mkdir -p elf2tab cargo install elf2tab --version 0.6.0 --root elf2tab/ # Install python dependencies to factory configure OpenSK (crypto, JTAG lockdown) -pip3 install --user --upgrade colorama tqdm cryptography fido2 +pip3 install --user --upgrade colorama tqdm cryptography "fido2>=0.9.1" diff --git a/tools/configure.py b/tools/configure.py index cc00a1a..9343490 100755 --- a/tools/configure.py +++ b/tools/configure.py @@ -64,8 +64,7 @@ def info(msg): def get_opensk_devices(batch_mode): devices = [] for dev in hid.CtapHidDevice.list_devices(): - if (dev.descriptor["vendor_id"], - dev.descriptor["product_id"]) == OPENSK_VID_PID: + if (dev.descriptor.vid, dev.descriptor.pid) == OPENSK_VID_PID: if dev.capabilities & hid.CAPABILITY.CBOR: if batch_mode: devices.append(ctap2.CTAP2(dev)) @@ -138,10 +137,9 @@ def main(args): if authenticator.device.capabilities & hid.CAPABILITY.WINK: authenticator.device.wink() aaguid = uuid.UUID(bytes=authenticator.get_info().aaguid) - info(("Programming device {} AAGUID {} ({}). " - "Please touch the device to confirm...").format( - authenticator.device.descriptor.get("product_string", "Unknown"), - aaguid, authenticator.device)) + info("Programming OpenSK device AAGUID {} ({}).".format( + aaguid, authenticator.device)) + info("Please touch the device to confirm...") try: result = authenticator.send_cbor( OPENSK_VENDOR_CONFIGURE, From c014d21ff8d19c26a0ffffb00aca6fc79f1ce92f Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Thu, 11 Feb 2021 19:53:45 +0100 Subject: [PATCH 175/192] adds README changes, logo and certificate (#285) --- README.md | 19 +++++++++--------- ... Certificate Google FIDO20020210209001.pdf | Bin 0 -> 612439 bytes docs/img/FIDO2_Certified_L1.png | Bin 0 -> 30553 bytes 3 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 docs/FIDO2 Certificate Google FIDO20020210209001.pdf create mode 100644 docs/img/FIDO2_Certified_L1.png diff --git a/README.md b/README.md index 68e9f71..8177220 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,16 @@ few limitations: ### FIDO2 -Although we tested and implemented our firmware based on the published -[CTAP2.0 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html), -our implementation was not reviewed nor officially tested and doesn't claim to -be FIDO Certified. -We started adding features of the upcoming next version of the -[CTAP2.1 specifications](https://fidoalliance.org/specs/fido2/fido-client-to-authenticator-protocol-v2.1-rd-20191217.html). -The development is currently between 2.0 and 2.1, with updates hidden behind -a feature flag. -Please add the flag `--ctap2.1` to the deploy command to include them. +The stable branch implements the published +[CTAP2.0 specifications](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html) +and is FIDO certified. + +FIDO2 certified L1 + +It already contains some preview features of 2.1, that you can try by adding the +flag `--ctap2.1` to the deploy command. The full +[CTAP2.1 specification](https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html) +is work in progress in the develop branch and is tested less thoroughly. ### Cryptography diff --git a/docs/FIDO2 Certificate Google FIDO20020210209001.pdf b/docs/FIDO2 Certificate Google FIDO20020210209001.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9749108b1da0c83b159eb119507c16cf0a016ddf GIT binary patch literal 612439 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

EZG0) zlJiCOhd;UQR`e;k+L!<6UtWJ){>-I`yIv$#xZ0a1_$R&(*0D*g{<`(pl8a|wnQeMq z{&KnF@72;imFw80|1<2e^^ggb4KUfXu65ConjPog&-4#|SG#)EZws+a95Sv~LmKDK zeC58nK5Esr{N&7Uv1WoYn`&Ri&)xsl@ZG(pov#va@2)Mc&AllqdE-#qD}C-`ISU+1 zS9rP4aK9Vx`u#t{>+7MpqK8$CVm-`TPcPDAOn&|4Wz3UlA6^ETNagU>t~S@V=Dp4N zu9`W#p;#&Lg_rUIz@_wos@t>i3o_+kKwG{!ve^)QJKdpG;;GdBH z3|>zdC*7-Q3W~VBzQ9#8wB+UGX}|BEU;Kw{(Nax)=K#Cqnd!w#I%Jq`-{(HTy#7If zz1IDWJ1YMu=ceZ|owqizDU(#4{qN_$(w$50_J+H5c~+fjjj8@(dhDjjG;^U$Jn@F(tI8zbGIwDwfA_!oKh*MsJg#k@u;=nUkFwjVCJNdf56eH^Q_ba_XxNn9 z8FK8>_IFo$rZ`@h`1kS2UlT*ty;s@NbnWb^(<}TAbR5@enYaA!)VE(&eAO3?&itgi zMJsQaW1GUq`RmsPuLbp3w?8`>n4gjntLI`6&AVAt!P4?m_>bAIZ@>O=)w*9Gv&u#3 zp7`f^hFg=n=b!mFR1CJM_FI3O!dK@vm`fy^7o_>7Wr(;3( z-)HZXpBf_FYJboBUcGgd-lMEf7c%wr^*>V19){mYHoY)s=dIkT% zm*RpiZ%*^vCd6Qu;Z!x}Q}K_4(3Wj&)#tY?@~kzm@i)B9Q~&PQKgnO0zjB}M)-`(d zOjO9j;kHT^*W@|EjguQ+WvQ%ZuNB)}ANFs%)ZK_@I(uxC3-#8UlwD+;D13t1{s-5* zY?EtwK^$AH6DF?w^q^BdCCm0>|B-9C`zEh^^<6HdTOrJ{^4W>_Ghf$6ua3^P4%@W# zO0H7x{_9>TcU9)y2q>^!eXb>3{b0t=z41@~xxIb8Z%Na|b$U4pm)0woeLKT{QcY(5 zqa^9#+dd*}X={CroMH;T9Hs`;%u zpB|LuBu@GLeAdqD#gP?F=KC+D#ar&2r1n_-m;9wobF%DY=6S>!nl5RdaC*j4`Nu(r znNHo$zmQt7P9XKI=g)@s+kYQg=xKeZaDUjY<#+2R+$6J#o;vB{r8=7*9^Ec7Bz6a|0iGJ5r1me-anPwdiNJg^Qk1>`OW`4W_#P0NYl4@ z+^Q#f=3d(HPeiu<@W=Jb|Hv-9?JBTqqL{U`Dx=7k6Y`gK6qcskTI^rGf0Ef!(T{s4 zE?yQ9{iE9d-_$9qADu=F>-hYOBb#5nZ@BL@E z*8HID`;qIB_PaK>_paEllYG&=IC@&gEEi*sMfXmr#CCl$sFXbK^y8duS=ZW?QTLD7 z9bUF#>*dFR(<_&iT`Q9{`RdIPeROsCiyWkD}NT< zyXfZWC2Jq=eUoi;jI+z;q-ynZ##=D$|n0W7HPNf{-XLzUU zg^2!psWAV8$?3hebr-)aF^=-7G`{`k2$WW?b$sCJ7RXUdQ7(W`_JISn>wQ; zx9r0vZxi#2f6g2%Gdx}+XYA@5>{qZ{|oOv7W()FLgzan?jy7@L9RhnPq8vcdOTz=-)y$RR&m0edxeinYt zQ~0l-p6kWYI~R`xy}JMX@W0AWo9cJZ`*&=o_Cwaa-@onaH`EtStB?>l`TF2Z>{Q=(-$6V&%aP_wR!35=~Bl#78Nbhj!-VU#m|tu@~?I; zvkK4Sd%JAyZawo>-*Q`*}%Z(DZP7b*` zOS|+xL$spu--+|KZhTaI;=%Uq7jvV(7citK@2;wkKej8@*tOkz!&F__xVIOYHNPI0 zcbxTb>4xm+Wm0>}4d1E!>{z&|UT*)~P%qzu40%x$^Xd-OD0QuFNnOyZGI<1SNLW+xsUG0l7VjvXh?-L}}y|NF(qs1NR$wacTAeekpi)Vp!}l!E-h zc**6*m)$;4oSUPRA1pHePi(e~PE6?4FYkZVA5^_ku6aB5X2gxku#4U5$uq^?S#sLV zH~P<@wQIUr)!H?O9!XQvJ`cKJ5G}Uk{~gc1NYu z7BbE;)cfSxaCiel(yl~F{jyupoBlJ{Uue($&#?Z)>2~)G)hA}%x*gQb!}m0Hh5h?= zSx>$hM#ay*?eX@^o1^`+3qpQ>yfpWltk0QA`@a>RXjkm};P~g|{11K6cTSuyPRv*m7gN&YXH?o(WaW=k2EkY}nkG=QAt! z$Te}_(@`<{r|#W&XE$|q=EEO4@}+aHp0RwgqTp0LleYiKI}P5y&y*oYYp5j$lYOnZS|9aio?1)W3iK8Tw80*MCvC-0nAHMXXO(XjAJueoOgFyM>;H zx#T(8-db#Rq{+KQ(lDLp^xM}V^S(LNU09K}aq^R?9ET6x<#S)YVy*n9;~U+~r&v!o zQ55pG>PpdS*~8}|wm)Vq+!_B<{@A7|Y2brkzqHLVI@@|X`fRevlS&ou?9zD^Z|*%_ ze&Nc>T|1%u?%e7mpKJC_AJ#S`>Px*S>x3_<<;9-B^>R(uzGx_|= z2bI&J7o>hY8B-q>k#y~a0lBH>FxCnG4+l?&b(WGwf6op5L}~Xo$~MF;rcg=zb;I_ zygub~$lGZ@<#?LUT|IAq`pa^Ml55jWd89B4?kPKc)8X8+znV+ME{DDR`SR!Ax6FIH zRxi7=t}j=4zHqEx)H9#I2kJhYU+?(iYI}C;?##Q9IXcsG0$2Q8R+GH^h3(60A0vL2 zb!~TDeCN^%^_dYe)7394%=ep`rWziz_TBT@&-FM?2L8Qxqxy2MX==XS{_EkN*-m}T z5;tTKH<-3(hh^VgE4O`Xs;g&R?s_Kqq@=fbOO0h*!>{yf4ZF7+Y8*TuAyczI?#ty= zu`67=gY0)pJS=|Y@H$p7I5y8YbK35(m}{S1&VQ3g_KcstbH;P|EB~6dwEgRCK7Q{% z!|NcUHt9%n<|_$F>jHzE{EjPrtIn>TTqqMRb@pG~H~X^XuP^`nC%QbpcGa6{-Y;}> z{qEmc?0wpN=XuAdOD<0fCYhxFKFrd8;A3QQMclDR&bp`lZRTzAj+u9CiGBM&@2@$R zXWE{~`Lf&glJ_RXLM@}oxrTX*=Ph6VF!JNeU8OsIe!2Znt)hCynJ#-)i`5@r96J_o zbm-J6^%ruHvFHCYL`}1`4sA%f!& z7x!+NGk?*?uH`Fi=Pi{{^0w~YIsK_|w9CoH{|rp8qlJ&Jt`u06<+WL_=2!${*q66~ z6IWQh-LbPT@IOP`8vBVNenwL-M^)*6uAiVbZU6Nr#g{5VB(6o|?U|a~J3ag9hW`vw z%O7dXlRlx8aYx5M$6J}Fa>hH`o$DrUSuJk8bR*yKU56h3XGmXm_r`Ivc{)M2URgNj z9VvM7{C4r2a8n+V?A!hCO>Dj2a$Y&$ckMsJK7RHOYrW$oMe3D9wq`7m5p%r!YT3?> z3$so*>&{uw*OF|ylBep(i4Cu$vg4dA*n&i_zqng(Rg!hBZK9vO+V!6crX+p8@^;Pp zwfT$g=NftJ{Wj12O4(|g@R?ZxvV8wEb@Wc8#ol&iy7=47!t_>V)UCM8ZJ*DWop?I6 z;cI}W{e-{SmlpN}PhK7v<^9fZuD15t-pI5*j#-|uf#RzT4~U+P|8eOOQ_wlJx%>Ck zh5ly{j+wW9O-$BxopVQ=_}@L26{+vdQJbK%z*Fa~aNykOc9{jq!Ebgx|F|ym@v6|= zz>9jj_wU&1W_Zmd?LR~OwTUyOPi+6E9{hBH{QL4%D|44Q8ZADlx3uIqlSR=A|KG;#zq3B^F7>#>$nc$g<2v*6n(I>T&-VQ% z|KZsz)y4WT+4VOr%&rh(*`>^85&oq8Rn_LYsi%ESt{a7un~zZo&(*PjinQ z4(^z@z{u1$b>v_f8>N?An>y)mk&OBpYw$E99 znx(QO^S$j)r%Zkp{klkl{!*+i_R>%DmprRP)*fcyw|u-`VzpQRul?Dk zi@9QEEX&`YPW_!C|I$Y)^|jluY8Ev3$p|tb4OrHXH~$^;ZAQE{U{27pO zj$3Ga=r_Or;jXzKH#`wCybv_!!Q;udR-ev)E)d&sTr1?^l)u$oueVN}q+Gr-@T-Q? zVGX(5&1RRC>QAfvXUHg%Keft3|D9Mir}XI^&d;Lm^z|{mf8WP{ZJW&!_cT}6ba~?v z_51f0BqeLTFY12UKc}8E_`<~7uOrS0)eHaIzqY2!w?yNoxqo$X9s6`ef30Wof%EqL zuwS_9Ql?hP@89M#D$T{E7uNi}uKsTRk>ro7C%Na#q(*&MxP5|1Qf0p_#;RHnyjI1{jV;pe){V>OKwb;a|!?a%2FQ2v)?S&>|IZNr$z)dV-CO^*{}I2}7*k}r^z6}PD{5{~4Tm9XCrB?qLQjbkMwuE)-?pw{aDfu6Ce+693d@pkG@6v52t8DJb z?|UwdVEU#YP_h-#yix!K6H~Sw3zCQWYX_8L~wK0vHja8+@5jfdgQ2!Bv!`)}(0+p#H)H~XX3gT*EwgmtWdATRWadfM2F;`!da*CIbUwS;}yLn1}}fLFwTBDQTdd7?egW% zqc$d2@ZUrL(XkckYYuc~ft%o0=%yE>)e+uCT<<{#(_` zl`p+l#?J0sypMN_o^qqZfnVFQr)?Ac6vcJx`_3g<*(<$$KdMY}&{_ZVXO+si-3PuE zRQ1IiSg6P}P zFN2Qq?t8oR3D0r6fAg*yg;wgCmTkMZ|Mj&8bMlw9Z2NBYY_|C9Y^(dX^h>P|$oucv zx-s#wwsw!|rL9}$8db)h-&(XG)9UV;MG?Bodyl(0N3BdOUGZnvy4g>6{%4rJMXSg5 zMmpP;#bJ9&daqC39bfnQ*OlNNx#xv46HhgH+>Df-aH@#cGcG=TWlVUZHQUzI zJm$;_e^;4}f3&CV`MBlWy}0>VY!$rfD*u=^=qUWVe&yTU*f)CICViFpiSxd0RFUeP zVE8gBv9U_>lbqY1>N?S*HmO;sXGyuA4*y$w?9>$H#aE|3Zw}rQXtuugTLiyiYM%c^ zR;xq0Hk)hvZl8;2OWnQacZaQ0-+r&fYTawjJoD65E4Ph2ul{+hw72%b<+V+py>?`@ zv^jh@A@clAiRoEZb?Y7b!^{3N=-NG+yLKnr+DDe_l76c$-sG^y`0usJA7t6L240wR zd~LKv=aa)Ft|w(T_h((Rli8klble|2T<>}>|(=66=`7rI6Bsl-T>L#tJ zQlD2Xw`+RV)H`vWjk4vuWYs%49d~{@?4JLFKRGz!;PTAv)k`+-qE{YIMymM6jKZAJKM1AGhX`fu>m*w91&tPVHna3pQ-Qr*W$`sG;mDX}ybLg_f zvRxadiRaI<*9$uSnSEC3z3EH0T++YsuI9oGwo791&yQx@TDC?{^63@9%ZXg`({GuV zsrNkoxXgCmW{umnM_W$+yCgXO)V6}6{=L$F_CMH^HyL!hUcvb{(dL&P^tnv=nL1bO zS!_%C#^aZkMsDT!^3~hAI#R83+nS`@oBjcwufMK{_nh?1;?eF+8>U$t6F)ui!ir!2 z8N6!hL@qb$Y+Q4}%aYBq^Z4VCh1S0(ZkyNtN%VU7-`Z!jrw-OzonCq4PUdy*OV8}W zZ8z1QzqW1d+IMm)U2l$7ibt#yZj)>I{ku6lY5O!0uIoa+eeZkz%{c2TrKQO=yYf5( z&%e;ER$}F~^=qr_ceLwuR6Jm?JRUPIOY&LUUG{kglx6Y~5B!=nb9;dZU-k2)PP!Ki zldo>x!N2Q%;H6!$zl6GsW6mp^?#X^~cjaBdy5uSR^~)b=?Vnk4Y3ZLpt0JY{+v+@y zf8>v0{?T>ilTTqwi(8}r34Z05Z-0bVDhcJsZ%<75xlqgPiSj!B!nZM>T6XP!{p<3X zB^T$F9R61QnltS`gLC~nPUADa^LPJ{sVmir60Tl;=gf`W+gdu0Ri$&Ng zr%Kw+d(+FFcuLKm=EEZ<*DjO%M{{CrgLk(1EuTrMn{$)uuesm1d$jpUSN(^Ycl*~* z3{Nd>I{zj=tIuX`nA;>H`N?-U;Z{YlyE~?Zo!1sP zaD1yxZHL#UUy(kLRLVOM{?q~-#0zW z)s;l$A7~xeIf;`c;mwB4@%QeSM@p2*_s3VwQrYIxDfFUaVZ3>=d5~YZ<*zx9)-IA^ zxV)ylAWer|u7U4rz_M%SfBk1@`uLRJbS6b zSKhk3SsuPyg)i*X72)}>>h^^D2Ua?>ta_{Roa$LqyzP6`DlO;K zC9c_N%isGIHokIm^(@cLUv)R_^_zaKLdnQ;f9lvp>pr{ry5#QKwWH!c!}D1iLjSUx zI{wj5{tTN_LqGz>2g`Q@;KL2NurkC|sr7I<;&G!lWrmpanbp6i|;^@F| z%z!WR%jDI&ZUc$TAW1t7+z>qBwn51`K|j0 z*OnB8F8y71d%aqh-0IyI{~_T0(sgNSOY;j^E}r=idtAYK@%C*KjOFgkWBk76#vg0m zA7>2WZp{Bx;d}DuhwMu62*=-N9aZA1o&OpJo=lec&+zdplkY1RA^yMfHci#fm~OTI z;g7qUrmWrAt~Xt8?t(at(p|>8ni8z187#L{zqVm*-aDQ2$hA*1p2R)gzwzt3XZJ4M zI(lZ?HlZamjy>(%KXLM`6ZL$DJ{iy0suVHFp_BdJ*_f>xE`N`;n}6-u#1&h28eA2R z*{*W?uHoX}NuC+TE8@Tkk%(&hDRLv@>aS56?f7Rn=~jr{w*We`fLNOO@2gN3!W2TC=9^ zpDXxM;r*AygX^08Kl068@h~V~FZe%$&i&?pY&%|C#|uOUBqow;V zC7B7uc{07c&SCttvSG>mcRf$!v+hk(+9P`PR;>I_(7LM@`9I7THP)#g+V$n>ymK#S zg+`n{c6`NwC&7Qh*PogfJ9qJvNM*iHd`sjS!t<}M{4JmTL$X@3xM;Rjf2YY`lPQ%o zH{Jhi?w@40e8IY+c+pq8diMFvWjphQYxnKXYyOIRZQt7z-5GJ}<2Ke!7Gd=@^X_|X zFP7VLQ9?II>e7w>45uyF<5!oO9<=eT+W73M`o;Y3{~6-iPR&tzd;QFM+ll@{~5Mr)PKvm)^^JD^5TutZawlyR$2F> z@aNmYS*t3lHm!Ib{Odo1qE}(y7N3+4E4x_dJf#|z2~0(TJiX0xbkIPMe7Ay1h=p3@_+F)rPn*{M&6>%`qwu*|9Y;Q5fJjVYy0D$ zH`=Y=Ic8Vpu3UXP#2KA(Zf5F}xkTgtRBc_<9rjy! z`L~wbf_TMyG7LLj_LQ!ecQ|B9+uSWLXUH_2I{Z`WQplZ2&wB4K|0CvAR%Mu9azp6z z+X-uQi#VQTo+`3!^(?Mk?RMhZG=n>bPVh9UNQS*Lo|mO*BJo}=Ge0NSr}wm{$h#%; zml+1%{*ia>?4Q$~?{aT^UA)$P90rrPa)0?m838s)y#jrx3UTXwJ7&;DO4 z!=L0QzOvo#_2TWqvI*YBd7VYgY!T=A?B_qoDioIZcB4;|sOl-IY%eaJyL;a`c++&A9RbU)q}vAu)=tuGl@N=0$Msy% ze|ur{>6vf5HqQCg^0s?+#mr0g(;nSyQ;9G9SGl5f&YI0PPb!V=tPW5*{AHD zkoozvg7~#hqAGH#{TBAi_@6s&-qKkraL(>*wL<^jlYAk6EsuIWxp7paaP~I&^qtb2 zHpl!w%uhb?Cv(&BrBinHEfXY9!GI%`yIdUGe>)uS zf2N^yFH)t?$mVXGSmD1Df{lND?V@`Pm+VTDH$AiHQeJg~gURaRWwpyJ8yFu=xar>f zx=UC3-Tt-qU*nx5~$p?p=*IzC6-xy-xd@HEe}n-eq}w_3pTGC)Vjznsx5lfYcLT z?VopTF+25MD{keaFpc&m5x#})#qYyHWSZ8kV|rNmH{dA?+3S z;#H;^uX=j5lKZP^)~eYWK8Z|kdt{FWJTUxzJZdiUI*|wcfyeB*?%CY+EPD~SqxDj- z>e@}Vr~AKdnkP_ie;|zI+qTmsX*-Nxi?6<<5`TB$7u)vBU)Jqx3ui6j4-7Q%-7Gj+ zexbzU4=*FGE?WM#-uwN|z^Ogi(QhN??ESiQ@7i4}Z%8>#s^)p#l{&X|&GgmPM$xm^ z-aNbK=uX>nz7ab<++F)*Uh19e;yhODlbJs;cdav#c=I$;r{uTmM8D8yGjGecCOix@ zJ+p3lZTy3-Jynvgmu7x@_bk^upp50=vv|XLRnww(zwZ0BSyw#YvQ4jXhVABtn_sy9 zdae}rzp7mHVcT5pji*K3)J=6JygGD7@k&Sxr|~lth29-6dqUj!G(R1?um3}1*}Jc4 zp?9v8e=|I)yZW9-J4dc!#kc;|^;!$nTc<_vTsW(`LjGXYLi@m2hFI^so9iA=slUFu z-bsu7vaYK2^39RbR}%{VGi<&VykJ$fYW?9)JH5(o&iS@CTXpW%-i=`;ZN|Sp*oEm7 zx7szWdTDj{+GPEAH@4`0f27|0<%{W6?{x8RHPZs3Vq8V`SEpG1*={d7`9A~4-3|X4 zrdiFd6=eUqE^9{B#udx`fBk1L_Bf|!Hc|D>weqt2xz(Y&wyN{%RCq45>+R#ewkpb5 zYx~TFSt%QzzxgEF|5HwC?eSFIDZy*|CYQZ8Ni)lQ5|i+sVUyK0^KWcxEPsp4U+U{u zz5I0f)6yUR84kDPX2-l(yC=RdQ~l=GY_l*KF~=?c+<%4pCa;QoW%zE}%=o_WAMIDd zg>&V0KJCc+@@@Av`Ga;wcD+|ZbEA%$OmBL1Ev?Pn@{>=)9`{$);l4JpS%uH0tv>Jm zpMmA<<(|3Io-YfQyehDJ!YVdT^~MM*wmtbwMOI;-A+zugU&(F)@qB z;6H<~`=?g9HyN8RDYYH5UXgn}Xvak7+|LXie>={3uDV_}PhsyN-(2%)c5gD67X4*- zwCC5>jl$t`zrK|J{GXxStS4dHA2;b-t6M(z?w+hI@c*^C_*m_o{|sJNa|2B*?7SAa^lX+0>YOTXwSJ&h(+9_4+k(~23$spUwS4?`Pl&8sGFZJYUhZleTajoR4 z%j}k%zw*yQcIL0%c`i2ge6_W2p|+=Tx$WN8{9dJ8Z#!P#zgN--P}bzHJvs_ZSkjjeqUX&=Gy&#wuv5v zuX)=4Go&r|*mY)(`wDl7x81>ii(>VEJ=vFf0@FYFfI<)4IH{1NRpwZ1lGqI|?U$@bS#X0tpt`c8siCB*-QiaC$yVDc*G;ASOfS#HW53h5>@xPAczrZ# zn*0{Mn-+09f*jee7%Uag^4P@6t7bOcM#%PW*3&cJgf>pq+%YXOSH*Jsj@WD0z3N`R zV*FLgwx~jXY1gr*=WgE)*H-GAtogb>rg7Glf>XxdT7za5HXhdB@o&+p^RnBg8rk0D zVwhV{**|~J@*V3cop=AKt8dmd;xu`2DL4G=^uJg zdv+YPe);|9{eS%T#8V7yS07nYVA@p9_{Uz2|Mj1jw{zxc#LgDX46bzkyGVrpnfp`6 z>E4?z~WXpkCqH z<-6>bCCvV7^rDHc(&Zfc*FP)mH%Dxpx+S;Q?k>xc<}>R!6>6>jnW~<;@jOJg=+q^S zBX9WcN*oA$SNWe|->NXNkLQio-aFo=$kcr5=={7l1@>O+JnsMFpIyIf{|DAGhe`Kc zj(y6Q`A+@s-My>kDaY+he*G%zi1;LSv2`^+C(rJ5KP^-HasKlBbIbY+B}1b3o)+hk z+?j5F&;HUff%YDi)zZDU=lAe@efiIHO`_^%;f&fn7yad4^d+-1G&a=DyQ(QYWu;2` zh0g)AOF!Ej@OyZ(>u%&z_q*2CZuzX^ zTU;+cNxS&vcK6BI&BgYomcN`69(6)mQu5#ln}V!8-zNF&X1%gezg}O`zVqa|tSNav zr~AM5ezEV-VqKjxE7kW-I^bM2P19yg&5_J)t$ug;JXb8axMsR%MN(JTomcO+O=45| z^0+)};vV(o#fn{?Pi!Wu97*}>YLIDsnAtHmeYrdLgh<)DCy)Eg``W$Se!KL`SN|E5 zF2_p79rwK~R&n{aT}s`WyoLI6imzMz)u_!=wupGYW}0dKB>Tt&(P~)qpEfH@V0LTE$81 zeQ()Ew^U8 zHLG9s{7K=`!22)HCUg0%S?XT8t9Je$p*cH)CU%QHxlmzut=j#z>eln;`;V{RuCz3_ z-2Lj~(5typmJP>e&3XLs^`BRsCEdNU&!a2;-ClmqP=(>ogZhrI>w;ccESj;#=czS& z>>Ar%*Qlu(OXg>re7n5jvBc@%RhPeLtMBc5?Yp<_m8rdrvA$~ce}*}se}f$^p1HUn ze`Cza`MQ%EJpS5GEtqgtch6}Zy^~6F+JaB4TOYDV>U@jxo&uH&Ud)fLFMmBT+-6(|<#~+$^y(+xDXj;bh>4)29{(B{# z|8M*Akh~h-O1r6|6I3I3H=UdpQzk#veZh-OS=Z~$!fqMQl>fzFb$*`BD~r;yU;F0G ze-=7-y>0JVt?c%u*y^yUvn&pN3SOXoh5yprhp#nDwNGx{edb-7lHkjCc5_)nuB>C* zUZwN)iFd8hnVL)UcfXwfg0;lT>OTWZjKP74Hid`IoIlfI#`&M2a@SR{N#9TXwwszMlVxFS^`^J}uB`vHEn?O? zIWuFE`?q*r|Cs;b__bAc)s|aoX1&a7?>BRv_Mf3)#e+JV>p9nEseih2R6=}L+Wdt7 z3|ZIKr|#a7=i*g3|KIkfQD(tvn{)s5ioV@CCxTl_)ZstFn~&$O&pN$#<3*F5+^@uq z?%$m}Y4+~lkC#8n4))(^b#`Y&!me#wdJ=24b4lMQoPTY{%9|mD7b|t+&(1piaL25+ znagh9)BU%+po~W+(BfyfEE{`()+5`!Kicxm&$pgGB^k}q?{duWU2&+Xt+r3n^j*D& z*92-f3KWFawz2s}ZQr%^N^ayMy=@mC@SE}!KEA!IUbyR?*5#~|@$c($+Bc3g z=k3m2)1Hca%*--d@SmY_^Pdx|KWcQP$oXh*EPLxFzuUj&KZE0stJ*Uc?>Qn?9Amrd zopOsN9$DzL z!#!ZREw|jxbz!;8LA_U0cg~ebO=*7?WViouTxo=c_dLyWf9oIiK6VEvXg3ENxCoP~DguKro%n=Wxu-O^G0dELG>xyu)&^-WeuJCtj8J81i^ zhy!+87=GY?AT2{r8E(E55zB{3BzFwRx z3cFi*>|%qcii74`_I;eKU{ynLcha~&8o{& z&|%ix9cTP+Illa2yI1>=O7EkIizj;RnqxHagy}b%Q}Z-0*gRp#5MP+)I7>)YI#*}Qtkowh#nWH3tpvHx%S&v4@p{~3HD(k|(I z`?KvMoAMpK`ne@)g@uo`cW-~HD|&vvkw-1n{)YJdH(d^P1iQPS(l!*^K@J zIo@!*XZ|wZjJq)o*X*-4^huvyohs;SvU2hkJ{ir&a$o1@J=E&Fx&LGt`>t-|tsQ!$ z>dk?Ts8CYg;} zn06|BeI0b6Mi z$+gcKZFuMWoOXUs-Ta4fllKH|JAM6gczosg#nQ!G`FB_TsNa9qbm56V4y(&9XPO5d z&lWw++y2zZ=cG>h1tF_ClOO9#PM!%^d%9-%`_tC$TEC0f+#iR0&Az?EGqNONonriM zV_~(2uV+nr%<^nj_ND&}UCviun7xmxwF~_n)~`5~|Fp^a$v+Hau6|tea=O^GTP+_1 zydR(8-f`sLOqnZX^AyBy#cbks7dqlF$>P}K?Hd`siY`trmOaB}D0+l-I`dzNXb9$I_kp3jp6qm9o085nK< zGeoV;u3vKJcAnK|>`uKk?- zk8$$eucDK_S#ag|<tYnRY)^N4zBW)24aG^B;X#*Yx;} zVRVnbPTBK?(i{l|9Q-lUwNy`t?$TMx#|@;Ifc|Q?XEss z#_g+WD%P@QGXLFU_bc}rzwOKJ*!_`hd8K07u8QlM+{zk%hMO4%b_EobMXUar74)pU zO#Pxzwr=2p9bF#2XZ$(tEh#$Oe}(IG+ojC+LKnlopZU#T+Z(g>%GYqan0(z!Uk!K7 zWcAfswDHx&EKSC?r!tZYZ8_h$U)%AYpFIlMz6D3#O*~y3mcKr7Vb?J+?Od*g zkdpL&YIb`rPmS>M`_I62Q&D5fo^zZ1KU|wD&95Ik^O^kTV+s2gJX`a6R_nC6%WRi? zlAbx2@5mH3%i}DdQ@f9SSYvqe<@8-!R&n--#(X>Q{PTk_6aVD0OLwx$#g#H;=HFeQ zEFTYZt7a=UHpCjXZL&YH?z|JvP=K_v;D`#Z?PsN*W*FVOSSVI z|1-?{)&BQa6ps|c>%tZMztgt7+3o(m{+WEme}>I3AOB1KRDXZje+DU=ZvI~qs{a}G z`~NdJuk5I-^f=%@!+vk~i3{t`A6xjJp>A6Ll)CT#62DJs*&H`dX-4uT^ZyJ|lWX_g z-Tz^7%-b(F{xdud?SWfTDaHBH4yE+%6_Nv`^xSOZ$S^RzfXZ}3}+b%b z9=aw+Z*PxD+%joL;|}%pRnB}3R(D=8|GT+XKK;V}=lj4e@2-Rd)S}Biu{*9y3-*uXpWzzrC9GeA^`I^G~(~EdQ>1LY6&ls{G9_`aj>VsK38-@ta@b zfBt02e}9>=9TM1+|1&H~?f;`vTysS1@8#mYxi61-a47tk^?dT=Pxk7cS1pt0Z|>i6 z|3Ab1txJE~wf<+Y{mTFQ%T~|*`g{K~%*cHHU5xwo28JKAo;+R__@Cj}jO0zq>_>v- zf2aIscpQk7I-L0$`0Os%|7WPXHud(uc#!7W{|trdlN0|lIJ4~k{_;P=v3_O${|pQM z%>Vv!|FN0hYWe?FzXG`wY%L_#!ahO$?7MDO)$4WjZ@cSz zJbp|6^M8hzYt!HS;{Wsgiv9ab7gKN9gVV=Rl+<23+2=pQg2(mur~PM;^56Wz{_}ls zg7ic(q#K<2+wWN;r+!5C$wbOw^63}qVbKcK)%w>Jw|)7O7o^K)?mz#xI4T}twd?eZ zzwiGXey@}{@) zpx~^z@o`zkP3f&$kJmWdTS8P1FR?A-C}IB3pl|-M2Xb`oU$Gw%^ZlDL z=YNsO|NZIyV_$(6>`RaS{Lip0+xuHBH#8AF1zW_8zx3n(@dISTC5{r|OOO8iXSi*; z=rpL*O#I%55|#VYF7JO~^Pd5n<`8xmWH|qe1LxA{Q;-yQ{HK`w7x~Hm{+j=|^wUiK zi-GrlhPwHGvJ}~xpaHNSqco_r5L^Ad#?#`L+kb|~{+|`^?*1oKJ@@dwjn@t5Rir!wcU#oycCU*DVg^u`3nk1R`A zZ~j}l@BQ+)sp9Z*V2MG7^4g<6{xfXz_KtrCPIJFIdH2n8WREMJdZU=TXy4qbAn9Dw zX141w?-*nb-D$kM*Zjl3;R~^--+b=Ikz9x8v6B=>pr?udk2Fzxl-;QhtHmeQbf<#jAGj+lUU<<@*~gke>d%aT&YKt1rrW0;M*?#_}|U7y73;+fJUwf=civj zA6w@SPPk&f#OxnKYel%1m*#7LwLvOiaIJe0sRqwnCd*K|W54{jvzc}07krx!F2W$? z$f19W{vwxgb|;tBUnt!F{&an>IwB=}Dq+0#?2r7fXRr!I{}t}iZecy7Ie}Ez^~HnS zhi(_^ycvG>+av04i!GZ`!ch2a_J4-o-#}#syy}CNk)Eu5Gps7w<-Z+$DeZr0_WSGy ze-pF%5oI^HFwO^;=yM^ZTso1ZAm+L+3K~=0dsM!FiOF=CWNVN>fz4d|rT-*P$ zKZUgG7HJC<**I(bXV~Wd&s3n@!B$WI{NK%>Dh^h0`9B3$R<%!CvrB&OQ{552$o|Kr z_68>VhiW?xzB047UmFIl?;#aMlPtrR9sBLSoz1-apP~GH{WGb`zen4CD?cyqcxI_E z=g438y1kjptc8kd9M#J6&qIojZba&VyEZfH7r3caf5#^J<%5+{pL^FtB#Awkw|uR_ z*2`{pve%hC&r<}oy$qZe$W0f2{!;4F-}%n4x(D9us5`lgCpjihWX0dZ1^j{6m#g(} z%A9fh+vD21`(H_e%8z+&O;&e6?W;_iUZ~SEp!IY7mGYY<_v#;2-C5!Md)kw#(w7I8 zn(fNYJGbk30ptFnJB=4gm;FW5cQ(B&JhOX2ZFguht{5c?@vQQ{K%6i;mMCI&tC&I$n%j~Y>Sa9b#U)t zzx=n8nc2|x*1t#oN{9b7fBMgGfBAm~Dd*hfa=X6F2UVK06fX4t*^?#T;_Z&3po8`_ zklI?yK)n-iL08ku(x+$2{GTCb8l-`Sx2ffi$oZa6GMLvMTVVfo7OYLUf5m%5%fTIy zWtMfA2pQ>t>vE(Tx-S&sGnA&s)SNAc|1%gqvfHrL;2^lLe=T+KNrw8rXmINl8vC_R z?k8O??Em56XaBnUGT-ba!jspnwEJLc{^l2-{dJJaOdczS*M)WVZ)aWJ^e+Nb=07{j zZE`Uu_xY#NguGN|iKz>7l;-V_+Y+nRS)G|TIcq_T)4YhOi8)4nGv%&2ID-0m(0&x6 zMHznX?dgo$*Z*;vUHhK&pCK(%{)=qkN+TbVjs#*`tp(IFu2CI^J|<_y9nk= zP*i~ZXAdsHzyCct_uE6S{|twGi<__H)PG!jrkSDskZ)n*WerfLb|0ua30AKT=@$HF z*q;r_ePVI-g_hAq1~tx_Kg&J~mwb7BX;$Kf1n2BeGV2b{h@SZJ8gdpya&go*NKffM z!;a$U%(I}TSpnl_?&7|g&)T+KjxT##dhU@Vq@O$U$qUH?J7Im1dT0;B>-tMbN&UEY z-7HV_*dp(I`<;2wpyt^+XmicJ=#KgNxGndQGuR832Ql%W-r}Nrpr~!XXPdj_>h0~Z zgpAlR@Ib;pc$h1+JE(VefBw>&dG}}S`#*;DWa zo{{g4^O}3IE5`~YT}uoynAaZt@$1-@HPBHmoE3=hB}iMIK$s%eIx|WHuRZ!xf7^8FCli_a zhWo0<@s}WTJN& zRPAISHMbDmRzzPl^Ffrpzu-B$iTi(CVT1HFB9I1GF2{j8Qun`$)toTm|D*EVQr6D> z@z(`=cgxHdx+BE*pl18)*Vk5~js#iVi3g2fz5BE8`@h80=ii0jY&tdnhezs9Pwn5! zK!pN}R0S8?p8pIQTJ}YEn6H$+^g(K9AeC~_Um&I2$NOoQ3;W+*KX!NitLS&XN?&^9 zmR#bndR(;7|DE)Lx7U}NpS-}%_6$}&!%G}(=m6ZZ%Rad`uS>jRkU4Ot@$&wyyxGgP zy}k8V_K4US-g8#v$oZ;Gmf7OB>wkv7zoQ_5EMM_Cce!EGt=DtU?%d2>R9p3RsX*p~ zC)wvG{7b8W3>+dVJS;_=*72lPuOCg0umbYhoc*Z|3vJwu-kviQ?nz%?wf@Nq$%8xh zpI<%=)PaJv>t^LIg7_U&&h6r#{!c5rfBNzhf9kgM?R}bn`z=rIQ#*iVIrT@Ktg?q@Rjgvg`%G^E)Zc5s}(028bD<#p<8~42q*u_;P zY{309S@h_P5~U@oXTQBEoH1?Nmau;}KRmrQxA3zk!{3Xx?(Fgt*PYRgK7PYjeec#k zXR_SmN&@$NxO1)Jx>fG=TgraX$u+Nkmah6{btKs6cGgGTGrGrT9sWM?_q*3!r?>8& zcj{qUlUtvw?f$ja0U1m7Rw@RpRN&uI66By#SGi6+O#9xlwRiJ+cg9`P&%UMq&G(Q= zQ025wy(f;Zzci=s!jI?^2ldMfrvzAPEuQ@Iit#JAU#E_pD+<$2C^rwPo8K;)_B?d| zr>Sps<1XC{QL&!#L~l>PZm+}BB|q8GQCOGZnc-#;yT@ugDF+(oha zAsfv;c-U-IaGG23{7?4m{_rb{Cs*B%$@@NG{`1)K_B~#u;$q>i+QQ_OAGd!~zP9b) zuDDAVC;NWC`BON2)~&^2-W(;h>2J#am27kQIdPTzzUq|nFYAsSd_R|Y>z=R+j|C6+ z&)~b=@A!pj=YG+wu|v92-}t9DGc!EES0?Cmz3$aFt0We!w2R*mc;NWT zb-VaPUV8~v)kl?I-Z+?-o5uy{@t-?F8jNF{~3a3^*y_ky;Lhd>Yh&V zQyxpXH&t~`OTzlplrCLdHgVsdmIrluRsSTH-8-Ky;{4llVdyKRlR5un|1)U4o31HzQExAI>3MZ z`KL3s3vNgD?lhW}o=lLBy!{}=peG^kypI*5h zwDDwW&-RB0UWP8Xs@N6X$-CyFt5jZD221h{z9+M8WrcaYGXE6+>}1&6!lsNzUDmg+ zntd{~PTH~KKyR(nUU-}n%>M`zvsmkjc3KjJaq2Q z_qsjpnT43-d!9fa_p`z=b2t7KXIKBQX3FBwjH}bb_RFsQA(}2EJF9=vzWQ%(cDU`-%wOSJw!2vSq@k+AI}zT+Y@46NYW<2g%~}6dlH>Qy-2F3;e_A~+ z=5+3pt*vcybMCradZ+u9S@Mr>O!VH%yMFG!bizOX^*@=%TRpzA?z+38vRC=jn&XP? z8(;P>H9GCNcmE7`-re)V5?keiV=wHi_O;8@JyLFXSW+V?ltcQ0){}o*kKX;!9K3sS zajZ@9rX!$(1@lLVLSw-yVK%G;i^& z=HT#4rZY;lLyzvcF_Fze;qO!X=X<46zn)vIKeb-O-0Xz0&Zc#jrGCAWxq8?0!TyW- zc?xrEcg;AHahR8zf4Y3f+IO!UPuiQ_jsBwkuxummo|B7y^hwX~(^(&4A=|V)|G+i< z{yp|Qm+nOx^>4oC7FqFo$$y5%XKM|Ahqq4b*Uv26BL6KS{b$#ekBq;|+e}VBKHWdD z|Ka&pTh^cT3VWHUf5mLl{)L}-kML&~>+V@BoFg~>YHVl!?H%X*{m;zye|E+Z^88;*rO%fGH$_s#V3p0s=EV&5Cu?9Lg~M}2&`I>__OWdCPHaw>N1mz#U!1=1 z&(XZB^TpXUFUuwOUS4;s*=4#uAt-DTYb043ez?1vf;nKQl!C5(0 zcYAx%?w{DZqo*KgXX@>r36EBOUjHacf7kXW``0UK+qVbpnWti2)ON+>w&#IYPE+&G zn_hL9R(w25S7D}TfTy6-A;WoldV0QDt_y!_C%X?k4u4YGb!eKMkD;r< zq-zle`tD5Sa(HLlJpb7CLp!}^p7M&H|FlReGV0XP=(}>~+>QK~cu9Op*-}vazR!ND z9rtW)3n{UkyBYd~K5VJ3>)o#Wc}3rB);Y5uP7m4qEg;4JvT5<#Ym<&`oa-{v@#%Mt z^N-dZe;K`Ga^6Y%>3{f-3hq+eHTB=IwdpKTGmf88nPIK}^292s`i^H?=j!I0T+HND zVVbzEZ6z`zO<5aQ zeXVYm{p%~4)w^T1$}P1C=M|Y%dYhq0r90F4{XF}qb5~7fM_+e+EqwCu4{OUarvvroMnZZfpT>gKr`}i+B z+R3tU%ayI(+1VV+SSIC3Iq_LOmv}7yIDC)&;^^1YF8P+)o-Cb9kpeyitvd7Zb-tY=d1MAK_~`Fi=k)lKM}^2qZ>dU5&jwZ+rTr;Bg;IbCSW zrjpZP^QJF3^3CW~`R&#<_N5{ZPTiaNM%(V$*X1fIfzI+Ny$lC_eKi$b9u(-8ck<1i zc3+h*T32V=+Pw~#;hN3uarcA_|H`-imrY)7^}g};F+0oC+cH|;P1o|yTl?^DL}lQq zkNFH!fZ`fR%>GgI%JVDN*Gus&SvGl7+`GGc;dUWgK3nW% z5BqoO^4UGNN-uAmurJKcz~Sq!u;qVL1M4qneg5~i>xISQ7uw52v<_(*&g(uY@LcTO z#UrnOt-QJ>h{NRfikm4s-tKQ|wQ_}{{++q3yluMg?(!hd)c!u-GMP*NOy$E?tg7~_ zExS>7vFOu{=l>ZVYIXl>44?n~O_+H{soB-HNnNsbFD2y*b>-u1SMPj$8;586=SyP<@LOeP24&)a-RRTR{s;4@t@&^Xyt!~x}*7@UwZ$z z+^e;*t83mp55K^?Zx;H3@!K{`d&JE>eY|0i$AaE6I+sH)m61$aaZTMXqo>Ee^d>j{yy+g^!01ryo8NA`%kl5FMd6zD!jfw z%ktZzb#hB1K6zDDDIP2A-sP^J{>XIerY81F?Cs4H~yDem+_<|HD+*{|pYR&;S0Wz2LzYwdpUk zEfQA+Pg$~j^?j$iE|+|BivKfwnCcEwGvkWdw9hfWt0L}jeAmt~ob$2kOI^mFjMZ%` zLsd2ZF-mn=EkV(sX^-Tb<4g5zTeIK1uD5^C`K&+rocgRt8y8d2O*;fDSN*oxS6~=x zk4TO8pYL4Ub!wW_QnNeh`*w?Jb%pH$2hJjR;HX^c7yQqVFg)v)6Yd;+;?=h z+Y)*>WMy9F+jR#eSG?(~|0lEpo_^F3ri0UZd^$K}{7tImiqlQD-TMBmIpU7QpJTaA z#&cx;GxV0+M+%*V&n9=BnkH5|jGjUI_urlM)xFW-UYr>t6N`4Vtzj^JS{|rs) zvuyjVFRD%Vxiafjv~;`ElH7=&&vyC$@Pd2&Lc;gOUFZ6wN*15y{Kx9GX!34%OZ(2p z)9!g(_uj*LVTE19J8kB}--R;8i+pXT9_f0yg|+|wy}jJ(TP18?`g2?Q`UPLDR1vvr-sSo6j=x<`pU&msM~aE*>t3^m zT;6wKzt ze{F01LrrKd4f|KCanK}qVVGcG@Wo1n;_mW|N3(xz|F*U1_TlAs-3~e2%=p{C)O%5O zvJbe_-32doxj0uwWX-#LYvt>0hre}CpLQIayzHUn(Z)T_D`n&ENi5i~vuE)%)y1!m zZ!Nc2yxgnQ;H#GF!Sk>}w`6Wf=C2g9O)t0Sz2J**Klp5YJv>3I{kLVu@u(FSS97db z`SwZUIo*55*&k_tyC!zWF|lY~q$RYxy)U3UZ7%DUS*f>8JVYkSv+Y`x!E7pc=$vt> z<<~lgS86*UWeB{SVLlSxzs7dAV`8!RG!AHadtk}(VBVlLm8!P3Os2}>C4EV!k7Q>j zvhIF4J?G*v5m0Hn8I*bIp}yPx@wKDf)U2r?YnJ~|v~=J2e4gUY$>F=ZGIa8kL;5p$ z<>QKPZGZd6SH|q(k&@kq0-3npb>Pt{r3lqZ;_10a61# z+xoiGH2TLo#f>|9LzyOA{Cs8J{irWi0k#=yTjxf6I#*T2lBKbGN%84NGY)C}XGo2c z;i+FOvLC6GSa5!+YSzlIfAt52&&nO2n{Dj9GV9SNZS58xNO=n?QTl(f#?{~77|Sl- zt5zHteCV9el*P8+FRToKIR{!VJRxO_Hf(3fw9t>7SA6FlpZa{J+0U8>-`2aIPxQRk zH6vj$HEQe|d6Ac-Gb6HUI8IivBgrrrkcGyTW$rVV9Kh8_Lhw z?H_{+e^^Z1XEsUSux4dqx~IjV>em~CW=&fQuCoIk|8oR2D#DGIoYkAZwsu>zc6$0MI&@{ul{;Uf zGq@IcuFgw9ggLmpH47I|n_00|C#PY?FCoKN0}U_GS>TucJ*@qoLQ;2wyX#dmS@^* zV_kR4Vu$dJn3K=m9=q`?*=~Q-drgP0OLcVaTy?h%pPGHGBqX2-5@J;GNtQ%f{eY^m*^iCTqzFJHizAB1JoOgX<>8E>l@q}w%H?DkHbnEEySmm8T-0sI&q?*Bn zZAufUu&v~7+W75&?7ykE8}D53^}o98)o<*L$-JFwWP^RpHoUwZxUt~&EIW%|;mOyg zFK1e{lxuT$^Spl^UWI<&EY5OAScT2~mbvBiO6khn)ABNZv;D#DgEk~DHQu@0$~EQD zqUGBsv0H2}>r+1>wesqkDX8Y}$cy-3Kh#ya` zC;4SCn{soS2H*Pp`h|VQ&dMC52wjse@G3ogTbG8(?-@6tE!`a!FEq85x33Q3Gn&_b z!VA=pR9%w2*#FYQt&{zBm-PrlCO>#G`ES;LP=gen`d^+^wFq1-vS|6DPfkVM5u+#T&<}%N-K1i{|A@P?i-(5ow@WXYX5DeO*>o;{aGt-6Ato#PsD$Q3E*b+KDG?? z4Qn2@&b2g`eD(dp@<5ZxkRAc7C-iLn@@enmrJ5q{=r~^7Ch_xU;ryp@o!*yUyXNS; zTC&vsfO4RFNDn`vr7nWpj;@=+_iXF8(&@J$Maz*(r`0>$7U`^GNh+P3Km8n=#b52^ zAlK_dd>_kbxq#bR=>2smY}MTKZ~qxsKrP+UM3Zg%6s@{%#+bNyN+1uuBVU6udSvk(d^A0_8R%T;OetCRvE~H+`w|{?QU9;rk#$bQ`pgAGRwtLz&kNLfo zsoTDm*V|{(EnTZu=1EJ8-H)@I^0$ADe=}PgT>NH&i{G!m!gEiG6lmI*{9V4~`SslP zx8nM`TR?RKtgRZidyQfoQHoA$Emo|2`$XMhd$o*zhiTZ=H;bEi?OvHh znhRb0E$=nk<=U~C%iMGI=9au$GU>Fu%&a%YHv;>kT3ctWd|UbTcGBKOPoHi?X>)9> zab77KcS~Zy<#!Vnzk{@S?Y#_NhX(TpEs06Bm7lQK;^f&xt&_{{?mot$ zv++Zv+WPuK!O+CI_D|!b9^q@NOtOw?X;tZ~sB3JmmXYsRe>)<&5LEuAJTIv&?)SQU z`H$1KsuHpH%VIaXGs%@vvkkS+dj3b(+pnDqDs=La z1y{0{)E0kP@9>~x@#JfBV@$m^KArRBiwvX;p5G~S>y(7N&+fE7gC!lm9)$KI3LDY= z2Nq8bTN&nc2i!`~U-*0`A@UvdhtV~ zsOnnb;gFU)SH9M{T6Hd2yAhm4p`E{V`LVNNg9D;x?t4ZkLkX4iB{AXb3b(IYQ2QO^AM5O*uC^Qn+HNVibcvl?tDGcTs zn@!t0_HS{WdVK10PP59IN4tfz#hXE(T?w_G=8*%vEBia~Ah$x~7_-)?JIh1vN16D+N*|1I>nviz%~C6-?nzeaM; zS*}OE+JV(G*W{^$IW5qs`MTOHYSFEVh3|TV4(67XZ91{zSmF8SvDa#~r+u8Lr=#2c zXUmk&KlxwBgc@90=;`VGusllYSXR;2?S?gu3t}Q-J#7Lv-`Xt}7C3k2n{2k-`JyLR z*j#w|t?#yzTdo6DEye(Yj@s0lsy@^MS58a-e|A*62er@ipNXPZJ ze4n^%$P!rhcI6>2yScro?WH37OWyo0*Q(j}eOha6-YJ$z+PnF3_1XgXALPA`oZ$2A zy5L#K!^aM`UkTgR%d`F3_gO_xyt==2IKFv(%wFd5(cXD$jvTcqD?jikLiJ(f?y0u> zrUos0cIDyH{|t89{xc|rtX-`CW8O8*^etD9)TORDwJ2fv{Jgq3b)w}iySp^FA`XdN z*l4}fdP#o5>XU!6j>Ui)oZfcwS^|w`=l)^?yWwi7qmTFh0`MIP0#Io-VXJf zk+#mm8?aTaW=k6x8o%&Vk*l=;egTIxQe_V@=GNfX+?AF_Qp4^@6{XGd(E+Za3?76=iR@z`l?>TucFzmClrinwspGoVdgqT%D9U!K&A1u$ciFph9^Qo_*3HK9HGKBM zksDskYFlZ=S)ww@pu*^h#`lwtzpS1!eM|W4y0AZAlDvI;#rALRT-tG8X5)d<`DZ57 ziZHPT#OnL+n0@v)-}18KKb~1T{)y5; z_9bN(MdQ!#$e&x4X{(z4bouLlPEk40*`>`VC%#STRI=aQb$8~5+CvTY#xd_UuFSj^ z)pPOI%d+!Y+oC)zrFVaf-f;8wO|9enbJH%(shngrQ~pNE{HGDS?*CE#m?dn|^eB4H zzmG|Z;pKIAvvvH9mM@*Y{*U>K5Z$wP7OQvWz5C{_J9W;2f{w&zHO&y6tCUYa5PZF0kP zU8$3bvf|GUHNL-cwNgP!+nKk=?a`-)#j@8JP58w)e@}r|bRhmpkTNiWXV(lJG99@|e?wCDEGdCEKFG_MHV{m)P~DM#je0OMcBMcbD*ta`AOJxbmA z!}gf4?X%sdepE?6+dOZ<--kcs>ZQJCPx;T#(to%x=IGhCD-Jha-SPbM>v)Bi*FV3U z)ndNw?6!y*2H(G}Uu?twm}7a9tk;!i>e70O=e4T)SG%X)8#4moX%vq=xyj$bNe}>25f6Ujdj41hil4+H}r@6=6e=om$Mm+3J)t4{pB$Zqk~?1{g3eI{gumi zr{sM5({0t)@p8qfTcw9h4;dzNZ@XFfZuyV)OKY^>G}=E@sxG^;Go<3=9R3x@?4_12 ze`R+4;?ilWBE3GcUa&W*JMt^+-fr11cF}*Q={j9FW}fFWRiVgHrswObmV`&I4(Huo zKTA08mCj*t7zCNLRr42r4EE<{(>WXr0^P&*u zqrqKoA5YGGwlrI(_Rs60-k)FA{by*ry?_3HhTLBg@Bh{x5PNYtTVl4?#%(($^v`ll zkp2{t{=ww5zF^FWL(-=?bDt--s7fcxJ0Ej@9X8=*zp@Q~+S{IGH5-h-GMk2OjV-!W z{H@neZM%BgR@FXx`wvT=Jo-6h&7P^(A6Umm9qmk?esW@|uH8NBRD0t+x2ODNGmd$B zs>1jAPqiIA!sn-P2L9~19xz>8Xv)kWj>l7EIwve?m)&^(^BUQp@9uq%#jQk6-F({h zv+~cbYZX&==H2?wuzmVZzIp!{o>(1syC}Oo@^!FE`vT9<=ugM8j;&m<^=jYc6*1>k z5@zhV!jk^?f&JRR?K?j7+_;~sqW*X9{R^{hZ~Aw_N18L(wkbyNXOpY1M(~`<+Pl@} zKl%JT?+4GnRC3lhA~@foI{L+qROg0&KWzVIreFLbYnSGI`t;n>muzR8zucFd9eN?< zN_zB)nCMt_Ptn31-%he^es)z)S0wsJv$ybq2VSfAYX90#P1iD9w$@_3danH1ZJM5^ z(?1#O{c(R9{kPwv<5qOv+CRDfL|b=7rk;y^l6-xVZ>fCdht8k-&T81-JzU=_|6$^d zD5hO*Qx^HuuStFW<@wijSrdZGR|ZC(j^8ORp5LZgYkt^0{kCbj*tXLr!!6SOGw?r; zWm}bN8qX7>H|NDm4%Se1-#w4zeXeWHjXtv7hg;H%=dF5{{^fh?GQZ1z=zDc>`kmZ7 zMQ4rUKK(Bo|7b^&HA+%|qV zHv7|^JEm8f-?Lgi|L~tdDsBIxh@kWV~(cwFn-BbG16!l^nciyB8p%br9^8fjM*8a^$r#)w? z&}001`ybP$ysuZUnev*7-99Pkx4i9Kk^9#2LPAyE+}>ZT>3PCJ?n}z0pvK8jH%k>PO%kdu z_^OH}=+&}Hs`VtVm0Wxwu|L>dWXg`O3z+SirqA;E6?k&d0r_Bek>qPVeTOEn`6qgx zU#sRTVf$hOyX;w+s=%X?3s|3D>Z{GPG)X8oVJLj^2^faI~Op|dp5btVzFIQ1OI}h&-yYg-etng z*>joU*<`t8Jyo+T7BKf6nm)^C-l6I2I~TAVU%42fM|_q~)vUNoi?0iqyDS&mHTC&2 zNPIOZ&^Yiej94Vk9`CbMYUj>9`F)VI$^IUv^+2W<=wdrzO z(l7Np@FcHgxp)$6ZUS4zgObH2N&SJG-e80LwlFNY*mJR`bg@YlLxHBI!&w2R~+EEIKe8wmmv+J!$M?k#?^);&l-%~Y%|pQRTwWXRn?GO+~{_jAz3y<-DGZN z;cK_s4UDsl`=$sxNG`s@vh3S3hRZ9!*sbtV?>2^) z*CtyoUaMz#t+#<;!MsCF#SS8s8-%7Wzt;0BsC04d0ZoUmiyPgF-F`C&d-hy<3<{Me z4OS&fmNJ_ZSOsoiGrZPR?0ItO0ojb(g$ z($OUOT7Q7>TBt#<6wYNFUn|2mr6_|ztwG~K;G~Q%66vxUuib1j&M)OY!QAMs)>CzH zzLzb7Vc(Xt9$|Opr3}z`dQg4w6=TmuaQrr2>kr`bmEkh-_cCCQvURXDDX_XY=gVS= z*B;Cu+udq4j<02qR9_0#n|!T-=T~5Vl<`Y9TX13kMLbNAphJBgUU+o3#BFVPeR#zSe=^5ReDrvEM@l2zN zo&RFDeSH3-?VkPIkojTPN~*VPzhblcqE_B5@$GfZzrqu{p47P3{+<6wTYK&7KRY^a z&q<%8-7f5VsoiS!-Msfdq%Kc4-a3(e?!)ZIoeGmKIbUaJ;D0#rSJ=MRg|~0q-}`jw ztvk^+)!{5ceC~xYzq15h-}*c6cfiC4i#09pUb)Jv95&lyKhK(DOgARkd~pA9T}AtH zudAlY)$d;S+s|A)DxJz@7$dB>8(fD#Ax0*Odfw; zy}H^u*TQ|4`R<>umoE}Y`lt5h%RPWfD8}<=;iYulOV$N4#EWqvHLblH|g zPVQ}A#qPyrYeuhH^(6iF+L(IYOfj+g+}%5N7$z%9u;{)pyl!%%v{vixw->*h&ldjC z30J8%4ik2{vNiJ3`_Kst%Qf@Z9OQj^!b1`zb44{Pb?s_--d&GY>g!zcchk$Q400El z>mDZe7s~xpF+HCvBgFip5KpYxb! z?Iinxp0mHMz3b6*o_hP)tv{U4ipq`WT|RWdBDl8P+-8bQz1F?UOD+c)|Lx4?Imdp+ ze9Gj{%D=8D-TzU3KD60=US4hS{KKY~U6oDuc&F#)=kygmxwGt0T|?=L!y2E><>Dum z|Gx1m?$-Th?sl8i<}>~L=A&9T<6+D6^H&}-e>fYmZU5C$OS5|!A9c4C|9%uMKR^D| zvYppV1zh*GP1osLo;mTc#{7o!Uso2TZoR)+b(Wu1uGWqJ4EuhF?w&kl&5}2Jw)}qM zIw@L6=T_~U_uU;zkDto#W{_X-bJdhfr^^+!*ZKw9mtDHJf4-^BcC(GglsE0&(EIUZ z3SYhIft5O}XEw7<4xW}%&vbjng}bDs&lf2kysvxn z;LAQv<@~2HQ?Jck5Ire6bXM9sC5I#bY_*ePFU7s{pC7z^+qU2-t@{G@J-@dpx5hqQ zCFeiGEvA>#=c`@eeg1OCe+I9oM~b~-x;Ss|{;QYGty(=f>f{2I#GF%W&!_M0+3?pw zH156sHN9uaEAHq7xYb`faid@UP2=?s%OiHqN)+o~qES$gKBuy^_UD7JZ9A?-)toFY z`_GWQJK}0$>%JEXwa?_EPCEL{p0dSzTlYWy6XjW9&exuuKKx6EXOm5jw~4pX!~1sg z?*?Dnx!WQ>bl9=uUzRl8dLOfM(z{o=6ZDPF{bwj;U+}MKy({N^v#?98Io^UcvrNkO_1C1%J|Rg zXXj;pEdE^`BoJ-*n|G(8{maGW`5*o>yuS8f>D#mwYlF%szdKR%H0FNcsgugy${x>k zo@|y6%2;+W?&c}c%@6kr-mNGO6c_mOVY|>Fe?I$T8&|BH6Y}V>?u^TuciGMRklwp+ z(#^azOFpkv`Ogr|X|wQGnf*KDUcsSHI=@5*u-P z<1V9lp~n}>zP`Oj^dpz_PtmujzfC{&-;3V)@c8x>f3r`nb_)?*`rdi*YvD!4!MELi z&fR@9d#$zJ(M?wl9(Jqn^#6A%R&nSa{l$xR~|^Th5cukHsc@L*Kek0HBRaJ&A$5P+WC_||D`UQwb||N zo5EMKa`$Yw{huM@0H;pqpPeG}6A#+$31g|!UHbK4+okLoX|jns8b1DKV3NNQY$6-D zrTcr^13{MdlvRh{&5x-L_ph%AN=_Bo$ace6^kk|~^SLme=ld+btQFsU#C=}$o9kcK zKl9(qo|1SwBkJD#GTtR0wfSCa9Q@V%PxRW6?e`Od3zZ!0Jj?r^$0s+(Y`CDbd*bHI z>SP6*%@aQUOK1Oi$$feJjXUdaZhh_3d}!}M$zN+@#a_G>Pk;U^($MSDtNhLzp1*(Z zH7Y%?dOf*5YCG4lMPVB!DMeP4a&GkBcSkrd{*~wQh~u_bR!(B%7QFjxP7#|sv+e7i z*#TGnIG28_5LuJ*#8sY)edT`!;pnUDrSzwl-(LU9C*kRO=N0|&-WGRFrGL9ksJ)!S zEq|nBRr}Fz&f3cU1#_<)S|ERI#m$N=ehYW|TY54)8#~L6?Ej!-RL>ZA&wI%&zYWnZ zHhJCNz_WCE;WycA_Gm$gd%mjA%lr|UTh1WcBFAe@s4T%EBQmSSBGhP zuKvZY6Yu7$-@Df9&$i8*t50sJn>xF&c=eBk(<~N!dh7Udj$ZAJucjL=Wd?dat)1=P z_hGr%yY8*g7k0?juG(^I@3Wc9+xKnT7Txe;dxIKN%KM=IrtK-J7rMcfxJc19u#nb}xQ?u*auY zPx3d;IlTD9Z?VXCi?b&!W}Ut2`G*yyYs@BBJSgqsslQ(oy4tkTwEO9dl_ArtSDc#D z+aQzt;b_3#eB(p?iqA^|KL2N6u6i;5z+d?XYtm(}pE$Os$>NKxS!jTqv)Z%c>MTiH z6kTiW51)D^8TH7)QAU1FsIBuq*SG4g_5ZbKz1{w&vZDOozLftA7mgMl)GOPw_&>w8 z*|z)gzna!~X6NVEF8OJ2T<&$_+%C3C(JTDUG4H$fKmO0){-0re|EsMR|7PD>G-++1 zzW#|P(+>-B9A#dn@uXJc_|nptGhTu2alEn`_Z@^9uT5X8YJG>*HdE8z=2E~mMt5eD zDfZoU#~y$ztQS|6+Amj%o$7mrG)_3%Aw;Q7U6 zX_9^+@ZWwA(s1~@#PTsWivSk3br$GS=HVve#KY(Yln{CDkS&b*42Dhb20^gRj)+dYy zUOQSY7Add_VC*vPn^FYst$=%w#xLDC7ciS7^|8v#vIVv3ElrX^{hb$#4OSO?CC