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,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAIQ0lEQVR4Ae1aCVSUVRT+kGVYBBQFBYzYFJFNLdPQVksz85QnszRNbaPNzDI0OaIH27VUUnOpzAqXMJNIszKTUEQWRXBnExRiUYEUBATs3lfzJw3LDP/MMOfMPI/M++97///uve+++9797jO7TgVGXLoYsexCdJMCTBZg5BowLQEjNwCYLMBkAUauAdMSMHIDgEVnKqC8/AKOZh2Do6MDAgMGwMbaWu/s6FUBTU1NyMnNQ8bRTPqfheI/SySBzc3N4devLwaGBGFgcBBcXJylNl1WzHQdDVbX1CDr2HEcJYEz6be6ukYteVxdewtFsEL6+vqgSxfduCudKaCgsBCbt27Dmexc8MzLKba2tggOCkDYszNgZmYm51Mq7+pGrTRMcXEJTp3Oli08c1xDVpR8KBW6gC50pgAVVRsoQWcKcHd3w4jht6N7924GKvo/bGl1F+C1fu78eWH+TdebcOeIUEyfOhkHk1OwJXY7OcBqg1OG1hRwICkZ38fF48LFS82EdHLqjkmPT8DihRF4b8nH4L3fkIrsJcCO6cuvYrD+i40qwrOgly5VYNWn65GUfAjhb7wGKysrQ5Jffji8a/ev2PfH/naF2rY9jma/HA+PG9tuX312kLUErly5grj4H9XmN3b7Dix4Kxz33n2H2u+czs5B9Mo1sLS01MlhSJYC0g5noL7+WjNh+NAydsxoMnVL/ETWcamiQmrPzy9AZWUV2C+oW/hY7KTDnUSWDygoKFSRY/pTk0kBo3D/yHvwyovPq7SXlpWr0Noi/PZ7gvAtDg4ObXXrcJssBdTV16sM7O7mJtFaDmhUE1HFxX/SqfGM9J6ykpySim82bRWPHjf1UZK1+itLAT1aMOWkg4ckBhMSVZ2ju5ur1M47yO5f9iAy6l18sHQ59tJsK0vigYNYu36DdPz18vJUNmn1V5YP4Bg+fufuZgz5+nhLzzY2NlKdKwED+qOJhN7xw04h2PETJ0V4rOz0VcwWnDh1WgQ8qWmHlWTxHBIcKD1rsyJLARy/e3t5Ii//rODJx9sLgwYGS/zdessgxGz+Fo2NjWL/f2LiBPxICtuzd5/U5/+VtPQj/yfB368fujk6qtC1QZC1BJiBZ5+eBtt/Z/qxRx9pxpODvT2G3z4UFhYWCHtuBi5fvgx2apqWUaNGavqK2v21ggcUFJ4Th6FpUyapDHzh4kXU1taK7W/l6nWoratT6dMWwfNmDyxa8FZbXWS1aUUB7XGQkZmF5dGr2+um0s7gx8KIufD0vFmlTVsE2UtAHUaCAwMI1vrPOarzDvcZN3aMToXnMfSiAMbzXnj+GXTrpr4jGzwoBOMffoh51GnRiwJYgh5OTpj35utqefOgwAGE/z2tdfyvJU3qxQfcOHAZHYU/Wb2WgJOiG8lSfXjoMMx4agrtHOYSTZcVvSuAham/dg2bt8Ti94RESTYbG2tMfXISQofdJtH0UekUBSgFY+g89rs4uLn1xrgHx8DevquySW+/naoAvUnZxkB6c4Jt8NCpTSYFdKr6DWDwDltAQ0Mjjh0/ifQjGWBsUFflfFERODTOyzsrDVFRUYnsnFzpuZ6AmRMnT3UIcu9QOMwBzocfrSDBq2FHGGBlVRVeCnuGQuEQiSltVDZs/AaHUtLg4XGTSLj08/XFrJkvIjX9MIGxu7BqxVKBKzAkn5uXT3HDPI2H7ZACNm2OFZcZoiLnw5ouNTDau/7zjVi29H1crb2KSpohOzs7nKVtjpnmCxDKwtgBzyBjCV272lGIfAWlZWXo5eKCMzk56EOQWq9eLigimCwh8QDmz52Dfn19UFpahrkRC8nqTig/JX7j4nciM+s4IubNaTZOs05tPGisAAY3+FbH1MmPC+H526PvH4mdu36mVHi2SITE0CHHxbkneJn8RRjA4kUR4ij8+YavxZLp2cNJoMVRkRHIzc8X0FcfyiU2NV0nwYso/J0vhOFLEympaXB3dxVKWfdpNCyIVkLK4JKSli4s4dWXw9BRzFBjH8D5PVbCjYENAx8c8FRV/SUY4z8L5ofjnagFQpB9dOLjmU88kIRIokdRmsy1d2/8smev6N/Q0IDXX3uF6Cy4o1jP/E1GlY9kZOLV2eGIXrUGZWQpyosSdYQrfEam70hocf/+ftK4mlY0VoBC8c89ntra/4ANFoATowprhRifESCFQgGeQR8vTzLxchQSaMLx/ScEikRELhYmXkaZIjP6x4UF5sLoEjs1LgyvLXl/MebMnolGsqa3310ilg+38Zh33TEC1+lfzL/IMdM1LRovAYXCSpgbz8ywoUPEeMp16evtTevxWDMeKigRwibPCuHZmzXzBVhZWgnGrSjbc/KUKhzOH2BInBMrbEn+NMPeXl4Ie3mWBKJyAubJSRPFzZGPlq9ECF2lGXLL4GZjq/OgsQL4oxMnjMey6FVY95k5nJ17CJCT/YDyLgDf6NhEfoADHN6ewt+YJYANPuszzs+MJlHK/B5KkXUxa9kI/f38sGXrd1i6LBpBgQG07eUJ6/D29kT64QwpVOa2kffeJRK0PAFKHtQRnvuYL6KibmdlP0548OUl9sx8BuAs0AOj7xPNnC3KpT2bEWEOeR98YJTYHi1pWQy5dTBKSkpxlvoM8PcjwHSYgMl5yfAdIC41NVfhRRAYO7XQ0KGEJ9aJJcROddqUyXDuyc61ATa2Ngjw7y/eYdSYcUcubjfkHQShnT9aD4YS/tiP7TviseLjD9oZ2jCaW7Y/GbzZkzPz8NBNGksGW62+qnULaHUkA23QugUYqJytsmVSQKuqMZIGkwUYyUS3KqbJAlpVjZE0mCzASCa6VTH/Bnoy/0KF7w+OAAAAAElFTkSuQmCC", + "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 | 101 +++++++++++++++++++++++++----- src/ctap/storage.rs | 2 +- 4 files changed, 123 insertions(+), 31 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); - } - } else { - self.permissions_rp_id = Some(String::from(rp_id)); + /// 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) + } + _ => 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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 z 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

y+8H4=YlN|TR@g%l) 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

Rxiv+Z3m=T1A&7)r{ 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

+Aj0e$GR41xX@>TBjf+c-x~zT!+IYKy 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 z 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%0At`Gc|K&xP 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 na$s9*DaWnxcCvSg( Date: Fri, 19 Feb 2021 14:20:23 +0100 Subject: [PATCH 176/192] Improved documentation for customization (#287) * move constants to new file, and update documentation * documentation improvements, deploy checks tests * fix pylint * improved code style * swap build and check --- README.md | 52 ++----- deploy.py | 6 + src/ctap/command.rs | 7 +- src/ctap/config_command.rs | 2 +- src/ctap/customization.rs | 269 +++++++++++++++++++++++++++++++++++++ src/ctap/data_formats.rs | 12 ++ src/ctap/mod.rs | 52 +------ src/ctap/storage.rs | 46 ++----- src/ctap/storage/key.rs | 2 +- 9 files changed, 319 insertions(+), 129 deletions(-) create mode 100644 src/ctap/customization.rs diff --git a/README.md b/README.md index d6c47f5..6abf799 100644 --- a/README.md +++ b/README.md @@ -92,45 +92,19 @@ If you build your own security key, depending on the hardware you use, there are a few things you can personalize: 1. If you have multiple buttons, choose the buttons responsible for user - presence in `main.rs`. -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. -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. -1. Depending on your available flash storage, choose an appropriate maximum - 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 - for credentials that need less protection. -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. -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. -1. If a certification (additional to FIDO's) requires that all requests are - protected with user verification, set `ENFORCE_ALWAYS_UV` in - `ctap/config_mod.rs` to `true`. + presence in `src/main.rs`. +1. If you have colored LEDs, like different blinking patterns and want to play + around with the code in `src/main.rs` more, take a look at e.g. `wink_leds`. +1. You find more options and documentation in `src/ctap/customization.rs`, + including: + - The default level for the credProtect extension. + - The default minimum PIN length, and what relying parties can set it. + - Whether you want to enforce alwaysUv. + - Settings for enterprise attestation. + - The maximum PIN retries. + - Whether you want to use batch attestation. + - Whether you want to use signature counters. + - Various constants to adapt to different hardware. ### 3D printed enclosure diff --git a/deploy.py b/deploy.py index e8d5ffd..0c8d998 100755 --- a/deploy.py +++ b/deploy.py @@ -352,6 +352,7 @@ class OpenSKInstaller: def build_opensk(self): info("Building OpenSK application") + self._check_invariants() self._build_app_or_example(is_example=False) def _build_app_or_example(self, is_example): @@ -390,6 +391,11 @@ class OpenSKInstaller: # Create a TAB file self.create_tab_file({props.arch: app_path}) + def _check_invariants(self): + print("Testing invariants in customization.rs...") + self.checked_command_output( + ["cargo", "test", "--features=std", "--lib", "customization"]) + def generate_crypto_materials(self, force_regenerate): has_error = subprocess.call([ os.path.join("tools", "gen_key_materials.sh"), diff --git a/src/ctap/command.rs b/src/ctap/command.rs index eb16a1f..78a80aa 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::customization::{MAX_CREDENTIAL_COUNT_IN_LIST, MAX_LARGE_BLOB_ARRAY_SIZE}; use super::data_formats::{ extract_array, extract_bool, extract_byte_string, extract_map, extract_text_string, extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, @@ -22,18 +23,12 @@ 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; use cbor::destructure_cbor_map; use core::convert::TryFrom; -// Depending on your memory, you can use Some(n) to limit request sizes in -// MakeCredential and GetAssertion. This affects allowList and excludeList. -// 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; diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index c65fc56..cf98889 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -127,7 +127,7 @@ pub fn process_config( #[cfg(test)] mod test { use super::*; - use crate::ctap::ENFORCE_ALWAYS_UV; + use crate::ctap::customization::ENFORCE_ALWAYS_UV; use crypto::rng256::ThreadRng256; #[test] diff --git a/src/ctap/customization.rs b/src/ctap/customization.rs new file mode 100644 index 0000000..1aebadf --- /dev/null +++ b/src/ctap/customization.rs @@ -0,0 +1,269 @@ +// 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. + +//! This file contains all customizable constants. +//! +//! If you adapt them, make sure to run the tests before flashing the firmware. +//! Our deploy script enforces the invariants. + +use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestationMode}; + +// ########################################################################### +// Constants for adjusting privacy and protection levels. +// ########################################################################### + +/// Changes the default level for the credProtect extension. +/// +/// You can change this value to one of the following for more privacy: +/// - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList +/// - CredentialProtectionPolicy::UserVerificationRequired +/// +/// UserVerificationOptionalWithCredentialIdList +/// Resident credentials are discoverable with +/// - an allowList, +/// - an excludeList, +/// - user verification. +/// +/// UserVerificationRequired +/// Resident credentials are discoverable with user verification only. +/// +/// This can improve privacy, but can make usage less comfortable. +pub const DEFAULT_CRED_PROTECT: Option = None; + +/// Sets the initial minimum PIN length in code points. +/// +/// # Invariant +/// +/// - The minimum PIN length must be at least 4. +/// - The minimum PIN length must be at most 63. +/// - DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty if MAX_RP_IDS_LENGTH is 0. +/// +/// 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 +/// +/// Reset reverts the minimum PIN length to this DEFAULT_MIN_PIN_LENGTH. +pub const DEFAULT_MIN_PIN_LENGTH: u8 = 4; + +/// Lists relying parties that can read the minimum PIN length. +/// +/// # Invariant +/// +/// - DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty if MAX_RP_IDS_LENGTH is 0 +/// +/// Only the RP IDs listed in DEFAULT_MIN_PIN_LENGTH_RP_IDS are allowed to read +/// the minimum PIN length with the minPinLength extension. +pub const DEFAULT_MIN_PIN_LENGTH_RP_IDS: &[&str] = &[]; + +/// Enforces the alwaysUv option. +/// +/// When setting to true, commands require a PIN. +/// Also, alwaysUv can not be disabled by commands. +/// +/// A certification (additional to FIDO Alliance's) might require enforcing +/// alwaysUv. Otherwise, users should have the choice to configure alwaysUv. +/// Calling toggleAlwaysUv is preferred over enforcing alwaysUv here. +pub const ENFORCE_ALWAYS_UV: bool = false; + +/// Allows usage of enterprise attestation. +/// +/// # Invariant +/// +/// - Enterprise and batch attestation can not both be active. +/// - If the mode is VendorFacilitated, ENTERPRISE_RP_ID_LIST must be non-empty. +/// +/// For privacy reasons, it is disabled by default. You can choose between: +/// - EnterpriseAttestationMode::VendorFacilitated +/// - EnterpriseAttestationMode::PlatformManaged +/// +/// VendorFacilitated +/// Enterprise attestation is restricted to ENTERPRISE_RP_ID_LIST. Add your +/// enterprises domain, e.g. "example.com", to the list below. +/// +/// PlatformManaged +/// All relying parties can request an enterprise attestation. The authenticator +/// trusts the platform to filter requests. +/// +/// 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. +/// +/// OpenSK prevents activating batch and enterprise attestation together. The +/// current implementation uses the same key material at the moment, and these +/// two modes have conflicting privacy guarantees. +/// 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; + +/// Lists relying party IDs that can perform enterprise attestation. +/// +/// # Invariant +/// +/// - If the mode is VendorFacilitated, ENTERPRISE_RP_ID_LIST must be non-empty. +/// +/// This list is only considered if the enterprise attestation mode is +/// VendorFacilitated. +pub const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; + +/// Sets the number of consecutive failed PINs before blocking interaction. +/// +/// # Invariant +/// +/// - CTAP2.0: Maximum PIN retries must be 8. +/// - CTAP2.1: Maximum PIN retries must be 8 at most. +/// +/// The fail retry counter is reset after entering the correct PIN. +pub const MAX_PIN_RETRIES: u8 = 8; + +/// Enables or disables basic attestation for FIDO2. +/// +/// # Invariant +/// +/// - Enterprise and batch attestation can not both be active (see above). +/// +/// 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. +/// U2F is unaffected by this setting. +/// +/// https://www.w3.org/TR/webauthn/#attestation +pub const USE_BATCH_ATTESTATION: bool = false; + +/// Enables or disables signature counters. +/// +/// The signature counter is currently implemented as a global counter. +/// The specification strongly suggests to have per-credential counters. +/// Implementing those means you can't have an infinite amount of server-side +/// credentials anymore. Also, since counters need frequent writes on the +/// persistent storage, we might need a flash friendly implementation. This +/// solution is a compromise to be compatible with U2F and not wasting storage. +/// +/// https://www.w3.org/TR/webauthn/#signature-counter +pub const USE_SIGNATURE_COUNTER: bool = true; + +// ########################################################################### +// Constants for performance optimization or adapting to different hardware. +// +// Those constants may be modified before compilation to tune the behavior of +// the key. +// ########################################################################### + +/// Sets the maximum blob size stored with the credBlob extension. +/// +/// # Invariant +/// +/// - The length must be at least 32. +pub const MAX_CRED_BLOB_LENGTH: usize = 32; + +/// Limits the number of considered entries in credential lists. +/// +/// # Invariant +/// +/// - This value, if present, must be at least 1 (more is preferred). +/// +/// Depending on your memory, you can use Some(n) to limit request sizes in +/// MakeCredential and GetAssertion. This affects allowList and excludeList. +pub const MAX_CREDENTIAL_COUNT_IN_LIST: Option = None; + +/// Limits the size of largeBlobs the authenticator stores. +/// +/// # Invariant +/// +/// - The allowed size must be at least 1024. +/// - The array must fit into the shards reserved in storage/key.rs. +pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = 2048; + +/// Limits the number of RP IDs that can change the minimum PIN length. +/// +/// # Invariant +/// +/// - If this value is 0, DEFAULT_MIN_PIN_LENGTH_RP_IDS must be non-empty. +/// +/// You can use this constant to have an upper limit in storage requirements. +/// This might be useful if you want to more reliably predict the remaining +/// storage. Stored string can still be of arbitrary length though, until RP ID +/// truncation is implemented. +/// Outside of memory considerations, you can set this value to 0 if only RP IDs +/// in DEFAULT_MIN_PIN_LENGTH_RP_IDS should be allowed to change the minimum PIN +/// length. +pub const MAX_RP_IDS_LENGTH: usize = 8; + +/// Sets the number of resident keys you can store. +/// +/// # Invariant +/// +/// - The storage key CREDENTIALS must fit at least this number of credentials. +/// +/// This value has implications on the flash lifetime, please see the +/// documentation for NUM_PAGES below. +pub const MAX_SUPPORTED_RESIDENT_KEYS: usize = 150; + +/// Sets the number of pages used for persistent storage. +/// +/// 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 resident keys permits to ensure a minimum number of +/// counter increments. +/// Let: +/// - P the number of pages (NUM_PAGES) +/// - 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 +/// +/// 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. +pub const NUM_PAGES: usize = 20; + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[allow(clippy::assertions_on_constants)] + fn test_invariants() { + // Two invariants are currently tested in different files: + // - storage.rs: if MAX_LARGE_BLOB_ARRAY_SIZE fits the shards + // - storage/key.rs: if MAX_SUPPORTED_RESIDENT_KEYS fits CREDENTIALS + assert!(DEFAULT_MIN_PIN_LENGTH >= 4); + assert!(DEFAULT_MIN_PIN_LENGTH <= 63); + assert!(!USE_BATCH_ATTESTATION || ENTERPRISE_ATTESTATION_MODE.is_none()); + if let Some(EnterpriseAttestationMode::VendorFacilitated) = ENTERPRISE_ATTESTATION_MODE { + assert!(!ENTERPRISE_RP_ID_LIST.is_empty()); + } else { + assert!(ENTERPRISE_RP_ID_LIST.is_empty()); + } + assert!(MAX_PIN_RETRIES <= 8); + assert!(MAX_CRED_BLOB_LENGTH >= 32); + if let Some(count) = MAX_CREDENTIAL_COUNT_IN_LIST { + assert!(count >= 1); + } + assert!(MAX_LARGE_BLOB_ARRAY_SIZE >= 1024); + if MAX_RP_IDS_LENGTH == 0 { + assert!(!DEFAULT_MIN_PIN_LENGTH_RP_IDS.is_empty()); + } + } +} diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 04e9a36..97ee858 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -504,11 +504,18 @@ impl TryFrom for SignatureAlgorithm { } } +/// The credProtect extension's policies for resident credentials. #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum CredentialProtectionPolicy { + /// The credential is always discoverable, as if it had no protection level. UserVerificationOptional = 0x01, + /// The credential is discoverable with + /// - an allowList, + /// - an excludeList, + /// - user verification. UserVerificationOptionalWithCredentialIdList = 0x02, + /// The credentials is discoverable with user verification only. UserVerificationRequired = 0x03, } @@ -939,9 +946,14 @@ impl From for cbor::Value { } } +/// The level of enterprise attestation allowed in MakeCredential. #[derive(Debug, PartialEq)] pub enum EnterpriseAttestationMode { + /// Enterprise attestation is restricted to a list of RP IDs. Add your + /// enterprises domain, e.g. "example.com", to the list below. VendorFacilitated = 0x01, + /// All relying parties can request an enterprise attestation. The authenticator + /// trusts the platform to filter requests. PlatformManaged = 0x02, } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 18b0345..f74b16e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -18,6 +18,7 @@ mod config_command; mod credential_management; #[cfg(feature = "with_ctap1")] mod ctap1; +mod customization; pub mod data_formats; pub mod hid; mod key_material; @@ -31,10 +32,14 @@ mod timed_permission; use self::command::{ AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, - MAX_CREDENTIAL_COUNT_IN_LIST, }; use self::config_command::process_config; use self::credential_management::process_credential_management; +use self::customization::{ + DEFAULT_CRED_PROTECT, ENTERPRISE_ATTESTATION_MODE, ENTERPRISE_RP_ID_LIST, + MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, + MAX_RP_IDS_LENGTH, USE_BATCH_ATTESTATION, USE_SIGNATURE_COUNTER, +}; use self::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, @@ -49,7 +54,7 @@ use self::response::{ AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, }; use self::status_code::Ctap2StatusCode; -use self::storage::{PersistentStore, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_RP_IDS_LENGTH}; +use self::storage::PersistentStore; use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; @@ -74,36 +79,7 @@ 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 -// 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, -// but it means you can't have an infinite amount of credentials anymore. Also, -// since this is the only piece of information that needs writing often, we might -// 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; -// 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! 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 // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, @@ -141,15 +117,6 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa cred_type: PublicKeyCredentialType::PublicKey, alg: SignatureAlgorithm::ES256, }; -// You can change this value to one of the following for more privacy. -// - 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; -// Enforce the alwaysUv option. With this constant set to true, commands require -// 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. pub fn check_pin_uv_auth_protocol( @@ -329,11 +296,6 @@ 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 { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index b1aa4ec..0f6657f 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -14,14 +14,19 @@ mod key; +use crate::ctap::customization::{ + DEFAULT_MIN_PIN_LENGTH, DEFAULT_MIN_PIN_LENGTH_RP_IDS, ENFORCE_ALWAYS_UV, + MAX_LARGE_BLOB_ARRAY_SIZE, MAX_PIN_RETRIES, MAX_RP_IDS_LENGTH, MAX_SUPPORTED_RESIDENT_KEYS, + NUM_PAGES, +}; 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; @@ -33,35 +38,6 @@ use core::convert::TryInto; use crypto::rng256::Rng256; use persistent_store::{fragment, StoreUpdate}; -// 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 -// 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 resident keys permits to ensure a minimum number of counter increments. -// Let: -// - P the number of pages (NUM_PAGES) -// - 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 -// -// 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. -const NUM_PAGES: usize = 20; -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: &[&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; -pub const MAX_LARGE_BLOB_ARRAY_SIZE: usize = 2048; - /// Wrapper for master keys. pub struct MasterKeys { /// Master encryption key. @@ -825,7 +801,7 @@ mod test { let mut credential_ids = vec![]; for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { - let user_handle = i.to_ne_bytes().to_vec(); + let user_handle = (i as u32).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()); @@ -899,7 +875,7 @@ mod test { assert_eq!(persistent_store.count_credentials().unwrap(), 0); for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { - let user_handle = i.to_ne_bytes().to_vec(); + let user_handle = (i as u32).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); @@ -948,7 +924,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { - let user_handle = i.to_ne_bytes().to_vec(); + let user_handle = (i as u32).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); @@ -1247,10 +1223,6 @@ mod test { 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.store.max_value_length() diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 7c6bea5..38a2a8b 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -141,7 +141,7 @@ mod test { #[test] fn enough_credentials() { - use super::super::MAX_SUPPORTED_RESIDENT_KEYS; + use crate::ctap::customization::MAX_SUPPORTED_RESIDENT_KEYS; assert!(MAX_SUPPORTED_RESIDENT_KEYS <= CREDENTIALS.end - CREDENTIALS.start); } From 5e9c32dff53c07e621c0dd3447c956e1b3109dd0 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 3 Mar 2021 16:33:25 +0100 Subject: [PATCH 177/192] HKDF for CTAP2.1 (#290) * implements hkdf, both regular and FIDO specific * improved documentation * constant usage in function return type --- libraries/crypto/src/hkdf.rs | 226 +++++++++++++++++++++++++++++++++++ libraries/crypto/src/lib.rs | 1 + 2 files changed, 227 insertions(+) create mode 100644 libraries/crypto/src/hkdf.rs diff --git a/libraries/crypto/src/hkdf.rs b/libraries/crypto/src/hkdf.rs new file mode 100644 index 0000000..ee276a3 --- /dev/null +++ b/libraries/crypto/src/hkdf.rs @@ -0,0 +1,226 @@ +// 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 super::hmac::hmac_256; +use super::{Hash256, HashBlockSize64Bytes}; + +const HASH_SIZE: usize = 32; + +/// Computes the HKDF with empty salt and 256 bit (one block) output. +/// +/// # Arguments +/// +/// * `ikm` - Input keying material +/// * `info` - Optional context and application specific information +/// +/// This implementation is equivalent to the below hkdf, with `salt` set to the +/// default block of zeros and the output length l as 32. +pub fn hkdf_empty_salt_256(ikm: &[u8], info: &[u8]) -> [u8; HASH_SIZE] +where + H: Hash256 + HashBlockSize64Bytes, +{ + // Salt is a zero block here. + let prk = hmac_256::(&[0; HASH_SIZE], ikm); + // l is implicitly the block size, so we iterate exactly once. + let mut t = info.to_vec(); + t.push(1); + hmac_256::(&prk, t.as_slice()) +} + +/// Computes the HKDF. +/// +/// # Arguments +/// +/// * `salt` - Optional salt value (a non-secret random value) +/// * `ikm` - Input keying material +/// * `l` - Length of output keying material in octets +/// * `info` - Optional context and application specific information +/// +/// Defined in RFC: https://tools.ietf.org/html/rfc5869 +/// +/// `salt` and `info` can be be empty. `salt` then defaults to one block of +/// zeros of size `HASH_SIZE`. Argument order is taken from: +/// https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#pinProto2 +#[cfg(test)] +pub fn hkdf(salt: &[u8], ikm: &[u8], l: u8, info: &[u8]) -> Vec +where + H: Hash256 + HashBlockSize64Bytes, +{ + let prk = if salt.is_empty() { + hmac_256::(&[0; HASH_SIZE], ikm) + } else { + hmac_256::(salt, ikm) + }; + let mut t = vec![]; + let mut okm = vec![]; + for i in 0..(l as usize + HASH_SIZE - 1) / HASH_SIZE { + t.extend_from_slice(info); + t.push((i + 1) as u8); + t = hmac_256::(&prk, t.as_slice()).to_vec(); + okm.extend_from_slice(t.as_slice()); + } + okm.truncate(l as usize); + okm +} + +#[cfg(test)] +mod test { + use super::super::sha256::Sha256; + use super::*; + use arrayref::array_ref; + + #[test] + fn test_hkdf_sha256_vectors() { + // Test vectors taken from https://tools.ietf.org/html/rfc5869. + let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(); + let salt = hex::decode("000102030405060708090a0b0c").unwrap(); + let info = hex::decode("f0f1f2f3f4f5f6f7f8f9").unwrap(); + let l = 42; + let okm = hex::decode( + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865", + ) + .unwrap(); + assert_eq!( + hkdf::(salt.as_slice(), ikm.as_slice(), l, info.as_slice()), + okm + ); + + let ikm = hex::decode( + "000102030405060708090a0b0c0d0e0f\ + 101112131415161718191a1b1c1d1e1f\ + 202122232425262728292a2b2c2d2e2f\ + 303132333435363738393a3b3c3d3e3f\ + 404142434445464748494a4b4c4d4e4f", + ) + .unwrap(); + let salt = hex::decode( + "606162636465666768696a6b6c6d6e6f\ + 707172737475767778797a7b7c7d7e7f\ + 808182838485868788898a8b8c8d8e8f\ + 909192939495969798999a9b9c9d9e9f\ + a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", + ) + .unwrap(); + let info = hex::decode( + "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf\ + c0c1c2c3c4c5c6c7c8c9cacbcccdcecf\ + d0d1d2d3d4d5d6d7d8d9dadbdcdddedf\ + e0e1e2e3e4e5e6e7e8e9eaebecedeeef\ + f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + ) + .unwrap(); + let l = 82; + let okm = hex::decode( + "b11e398dc80327a1c8e7f78c596a4934\ + 4f012eda2d4efad8a050cc4c19afa97c\ + 59045a99cac7827271cb41c65e590e09\ + da3275600c2f09b8367793a9aca3db71\ + cc30c58179ec3e87c14c01d5c1f3434f\ + 1d87", + ) + .unwrap(); + assert_eq!( + hkdf::(salt.as_slice(), ikm.as_slice(), l, info.as_slice()), + okm + ); + + let ikm = hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(); + let salt = hex::decode("").unwrap(); + let info = hex::decode("").unwrap(); + let l = 42; + let okm = hex::decode( + "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8", + ) + .unwrap(); + assert_eq!( + hkdf::(salt.as_slice(), ikm.as_slice(), l, info.as_slice()), + okm + ); + } + + #[test] + fn test_hkdf_empty_salt_256_sha256_vectors() { + // Test vectors generated by pycryptodome using: + // HKDF(b'0', 32, b'', SHA256, context=b'\x00').hex() + let test_okms = [ + hex::decode("f9be72116cb97f41828210289caafeabde1f3dfb9723bf43538ab18f3666783a") + .unwrap(), + hex::decode("f50f964f5b94d62fd1da9356ab8662b0a0f5b8e36e277178b69b6ffecf50cf44") + .unwrap(), + hex::decode("fc8772ceb5592d67442dcb4353cdd28519e82d6e55b4cf664b5685252c2d2998") + .unwrap(), + hex::decode("62831b924839a180f53be5461eeea1b89dc21779f50142b5a54df0f0cc86d61a") + .unwrap(), + hex::decode("6991f00a12946a4e3b8315cdcf0132c2ca508fd17b769f08d1454d92d33733e0") + .unwrap(), + hex::decode("0f9bb7dddd1ec61f91d8c4f5369b5870f9d44c4ceabccca1b83f06fec115e4e3") + .unwrap(), + hex::decode("235367e2ab6cca2aba1a666825458dba6b272a215a2537c05feebe4b80dab709") + .unwrap(), + hex::decode("96e8edad661da48d1a133b38c255d33e05555bc9aa442579dea1cd8d8b8d2aef") + .unwrap(), + ]; + for (i, okm) in test_okms.iter().enumerate() { + // String of number i. + let ikm = i.to_string(); + // Byte i. + let info = [i as u8]; + assert_eq!( + &hkdf_empty_salt_256::(&ikm.as_bytes(), &info[..]), + array_ref!(okm, 0, 32) + ); + } + } + + #[test] + fn test_hkdf_length() { + let salt = []; + let mut input = Vec::new(); + for l in 0..128 { + assert_eq!( + hkdf::(&salt, input.as_slice(), l, input.as_slice()).len(), + l as usize + ); + input.push(b'A'); + } + } + + #[test] + fn test_hkdf_empty_salt() { + let salt = []; + let mut input = Vec::new(); + for l in 0..128 { + assert_eq!( + hkdf::(&salt, input.as_slice(), l, input.as_slice()), + hkdf::(&[0; 32], input.as_slice(), l, input.as_slice()) + ); + input.push(b'A'); + } + } + + #[test] + fn test_hkdf_compare_implementations() { + let salt = []; + let l = 32; + + let mut input = Vec::new(); + for _ in 0..128 { + assert_eq!( + hkdf::(&salt, input.as_slice(), l, input.as_slice()), + hkdf_empty_salt_256::(input.as_slice(), input.as_slice()) + ); + input.push(b'A'); + } + } +} diff --git a/libraries/crypto/src/lib.rs b/libraries/crypto/src/lib.rs index 7b35e99..da13180 100644 --- a/libraries/crypto/src/lib.rs +++ b/libraries/crypto/src/lib.rs @@ -22,6 +22,7 @@ pub mod cbc; mod ec; pub mod ecdh; pub mod ecdsa; +pub mod hkdf; pub mod hmac; pub mod rng256; pub mod sha256; From 351e6c12c6e09e92efdeef94deaaf5568451c090 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Thu, 4 Mar 2021 10:37:19 +0100 Subject: [PATCH 178/192] renames PIN protocol related variables to prepare PIN protocol v2 (#291) --- .../{pin_protocol_v1.rs => client_pin.rs} | 194 ++++++++---------- src/ctap/command.rs | 20 +- src/ctap/config_command.rs | 67 +++--- src/ctap/credential_management.rs | 130 ++++++------ src/ctap/large_blobs.rs | 90 +++----- src/ctap/mod.rs | 63 +++--- src/ctap/storage.rs | 2 +- 7 files changed, 246 insertions(+), 320 deletions(-) rename src/ctap/{pin_protocol_v1.rs => client_pin.rs} (88%) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/client_pin.rs similarity index 88% rename from src/ctap/pin_protocol_v1.rs rename to src/ctap/client_pin.rs index 3b00a35..ec309ed 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/client_pin.rs @@ -166,7 +166,7 @@ pub enum PinPermission { AuthenticatorConfiguration = 0x20, } -pub struct PinProtocolV1 { +pub struct ClientPin { key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], consecutive_pin_mismatches: u8, @@ -174,11 +174,11 @@ pub struct PinProtocolV1 { permissions_rp_id: Option, } -impl PinProtocolV1 { - pub fn new(rng: &mut impl Rng256) -> PinProtocolV1 { +impl ClientPin { + pub fn new(rng: &mut impl Rng256) -> ClientPin { let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); let pin_uv_auth_token = rng.gen_uniform_u8x32(); - PinProtocolV1 { + ClientPin { key_agreement_key, pin_uv_auth_token, consecutive_pin_mismatches: 0, @@ -395,14 +395,14 @@ impl PinProtocolV1 { Ok(response) } - pub fn process_subcommand( + pub fn process_command( &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, ) -> Result { let AuthenticatorClientPinParameters { - pin_protocol, + pin_uv_auth_protocol, sub_command, key_agreement, pin_auth, @@ -412,7 +412,7 @@ impl PinProtocolV1 { permissions_rp_id, } = client_pin_params; - if pin_protocol != 1 { + if pin_uv_auth_protocol != 1 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } @@ -558,8 +558,8 @@ impl PinProtocolV1 { pub fn new_test( key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], - ) -> PinProtocolV1 { - PinProtocolV1 { + ) -> ClientPin { + ClientPin { key_agreement_key, pin_uv_auth_token, consecutive_pin_mismatches: 0, @@ -647,13 +647,13 @@ mod test { let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); let pin_hash_enc = vec![ 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, 0x99, 0x66, ]; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -664,7 +664,7 @@ mod test { let pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -677,9 +677,9 @@ mod test { 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, 0x99, 0x66, ]; - pin_protocol_v1.consecutive_pin_mismatches = 3; + client_pin.consecutive_pin_mismatches = 3; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -687,11 +687,11 @@ mod test { ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) ); - pin_protocol_v1.consecutive_pin_mismatches = 0; + client_pin.consecutive_pin_mismatches = 0; let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -702,7 +702,7 @@ mod test { let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1]; assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( + client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, &aes_dec_key, @@ -716,14 +716,14 @@ mod test { fn test_process_get_pin_retries() { let mut rng = ThreadRng256 {}; let persistent_store = PersistentStore::new(&mut rng); - let pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let client_pin = ClientPin::new(&mut rng); let expected_response = Ok(AuthenticatorClientPinResponse { key_agreement: None, pin_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), }); assert_eq!( - pin_protocol_v1.process_get_pin_retries(&persistent_store), + client_pin.process_get_pin_retries(&persistent_store), expected_response ); } @@ -731,36 +731,28 @@ mod test { #[test] fn test_process_get_key_agreement() { let mut rng = ThreadRng256 {}; - let pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); + let client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); let expected_response = Ok(AuthenticatorClientPinResponse { key_agreement: Some(CoseKey::from(pk)), pin_token: None, retries: None, }); - assert_eq!( - pin_protocol_v1.process_get_key_agreement(), - expected_response - ); + assert_eq!(client_pin.process_get_key_agreement(), expected_response); } #[test] fn test_process_set_pin() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let new_pin_enc = encrypt_standard_pin(&shared_secret); let pin_auth = hmac_256::(&shared_secret, &new_pin_enc[..])[..16].to_vec(); assert_eq!( - pin_protocol_v1.process_set_pin( - &mut persistent_store, - key_agreement, - pin_auth, - new_pin_enc - ), + client_pin.process_set_pin(&mut persistent_store, key_agreement, pin_auth, new_pin_enc), Ok(()) ); } @@ -770,9 +762,9 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let new_pin_enc = encrypt_standard_pin(&shared_secret); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); @@ -780,7 +772,7 @@ mod test { auth_param_data.extend(&pin_hash_enc); let pin_auth = hmac_256::(&shared_secret, &auth_param_data[..])[..16].to_vec(); assert_eq!( - pin_protocol_v1.process_change_pin( + client_pin.process_change_pin( &mut rng, &mut persistent_store, key_agreement.clone(), @@ -793,7 +785,7 @@ mod test { let bad_pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.process_change_pin( + client_pin.process_change_pin( &mut rng, &mut persistent_store, key_agreement.clone(), @@ -808,7 +800,7 @@ mod test { persistent_store.decr_pin_retries().unwrap(); } assert_eq!( - pin_protocol_v1.process_change_pin( + client_pin.process_change_pin( &mut rng, &mut persistent_store, key_agreement, @@ -825,12 +817,12 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - assert!(pin_protocol_v1 + assert!(client_pin .process_get_pin_token( &mut rng, &mut persistent_store, @@ -841,7 +833,7 @@ mod test { let pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.process_get_pin_token( + client_pin.process_get_pin_token( &mut rng, &mut persistent_store, key_agreement, @@ -857,13 +849,13 @@ mod test { 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 mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.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( + client_pin.process_get_pin_token( &mut rng, &mut persistent_store, key_agreement, @@ -878,12 +870,12 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let pk = pin_protocol_v1.key_agreement_key.genpk(); - let shared_secret = pin_protocol_v1.key_agreement_key.exchange_x_sha256(&pk); + let mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); let key_agreement = CoseKey::from(pk); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - assert!(pin_protocol_v1 + assert!(client_pin .process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, @@ -893,14 +885,14 @@ mod test { Some(String::from("example.com")), ) .is_ok()); - assert_eq!(pin_protocol_v1.permissions, 0x03); + assert_eq!(client_pin.permissions, 0x03); assert_eq!( - pin_protocol_v1.permissions_rp_id, + client_pin.permissions_rp_id, Some(String::from("example.com")) ); assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, key_agreement.clone(), @@ -912,7 +904,7 @@ mod test { ); assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, key_agreement.clone(), @@ -925,7 +917,7 @@ mod test { let pin_hash_enc = vec![0xEE; 16]; assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( + client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, key_agreement, @@ -943,13 +935,13 @@ mod test { 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 mut client_pin = ClientPin::new(&mut rng); + let pk = client_pin.key_agreement_key.genpk(); + let shared_secret = client_pin.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( + client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( &mut rng, &mut persistent_store, key_agreement, @@ -965,9 +957,9 @@ mod test { fn test_process() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); let client_pin_params = AuthenticatorClientPinParameters { - pin_protocol: 1, + pin_uv_auth_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: None, pin_auth: None, @@ -976,12 +968,12 @@ mod test { permissions: None, permissions_rp_id: None, }; - assert!(pin_protocol_v1 - .process_subcommand(&mut rng, &mut persistent_store, client_pin_params) + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, client_pin_params) .is_ok()); let client_pin_params = AuthenticatorClientPinParameters { - pin_protocol: 2, + pin_uv_auth_protocol: 2, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: None, pin_auth: None, @@ -992,7 +984,7 @@ mod test { }; let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; assert_eq!( - pin_protocol_v1.process_subcommand(&mut rng, &mut persistent_store, client_pin_params), + client_pin.process_command(&mut rng, &mut persistent_store, client_pin_params), Err(error_code) ); } @@ -1161,15 +1153,15 @@ mod test { #[test] fn test_has_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - pin_protocol_v1.permissions = 0x7F; + let mut client_pin = ClientPin::new(&mut rng); + client_pin.permissions = 0x7F; for permission in PinPermission::into_enum_iter() { - assert_eq!(pin_protocol_v1.has_permission(permission), Ok(())); + assert_eq!(client_pin.has_permission(permission), Ok(())); } - pin_protocol_v1.permissions = 0x00; + client_pin.permissions = 0x00; for permission in PinPermission::into_enum_iter() { assert_eq!( - pin_protocol_v1.has_permission(permission), + client_pin.has_permission(permission), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1178,12 +1170,12 @@ mod test { #[test] 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_rp_id_permission(), Ok(())); - assert_eq!(pin_protocol_v1.permissions_rp_id, None); - pin_protocol_v1.permissions_rp_id = Some("example.com".to_string()); + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); + assert_eq!(client_pin.permissions_rp_id, None); + client_pin.permissions_rp_id = Some("example.com".to_string()); assert_eq!( - pin_protocol_v1.has_no_rp_id_permission(), + client_pin.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1191,19 +1183,13 @@ mod test { #[test] fn test_has_no_or_rp_id_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); + assert_eq!(client_pin.permissions_rp_id, None); + client_pin.permissions_rp_id = Some("example.com".to_string()); + assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); assert_eq!( - 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"), + client_pin.has_no_or_rp_id_permission("another.example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1211,20 +1197,20 @@ mod test { #[test] fn test_has_no_or_rp_id_hash_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::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), + client_pin.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!(client_pin.permissions_rp_id, None); + client_pin.permissions_rp_id = Some("example.com".to_string()); assert_eq!( - pin_protocol_v1.has_no_or_rp_id_hash_permission(&rp_id_hash), + client_pin.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]), + client_pin.has_no_or_rp_id_hash_permission(&[0x4A; 32]), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1232,21 +1218,15 @@ mod test { #[test] fn test_ensure_rp_id_permission() { let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - pin_protocol_v1.ensure_rp_id_permission("example.com"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.permissions_rp_id, + client_pin.permissions_rp_id, Some(String::from("example.com")) ); + assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - pin_protocol_v1.ensure_rp_id_permission("example.com"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.ensure_rp_id_permission("counter-example.com"), + client_pin.ensure_rp_id_permission("counter-example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 78a80aa..8c5aaec 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -304,7 +304,7 @@ impl TryFrom for AuthenticatorGetAssertionParameters { #[derive(Debug, PartialEq)] pub struct AuthenticatorClientPinParameters { - pub pin_protocol: u64, + pub pin_uv_auth_protocol: u64, pub sub_command: ClientPinSubCommand, pub key_agreement: Option, pub pin_auth: Option>, @@ -320,7 +320,7 @@ impl TryFrom for AuthenticatorClientPinParameters { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { - 0x01 => pin_protocol, + 0x01 => pin_uv_auth_protocol, 0x02 => sub_command, 0x03 => key_agreement, 0x04 => pin_auth, @@ -331,7 +331,7 @@ impl TryFrom for AuthenticatorClientPinParameters { } = extract_map(cbor_value)?; } - let pin_protocol = extract_unsigned(ok_or_missing(pin_protocol)?)?; + let pin_uv_auth_protocol = extract_unsigned(ok_or_missing(pin_uv_auth_protocol)?)?; let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; let key_agreement = key_agreement.map(CoseKey::try_from).transpose()?; let pin_auth = pin_auth.map(extract_byte_string).transpose()?; @@ -346,7 +346,7 @@ impl TryFrom for AuthenticatorClientPinParameters { let permissions_rp_id = permissions_rp_id.map(extract_text_string).transpose()?; Ok(AuthenticatorClientPinParameters { - pin_protocol, + pin_uv_auth_protocol, sub_command, key_agreement, pin_auth, @@ -506,7 +506,7 @@ impl TryFrom for AuthenticatorAttestationMaterial { pub struct AuthenticatorCredentialManagementParameters { pub sub_command: CredentialManagementSubCommand, pub sub_command_params: Option, - pub pin_protocol: Option, + pub pin_uv_auth_protocol: Option, pub pin_auth: Option>, } @@ -518,7 +518,7 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { let { 0x01 => sub_command, 0x02 => sub_command_params, - 0x03 => pin_protocol, + 0x03 => pin_uv_auth_protocol, 0x04 => pin_auth, } = extract_map(cbor_value)?; } @@ -527,13 +527,13 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { let sub_command_params = sub_command_params .map(CredentialManagementSubCommandParameters::try_from) .transpose()?; - let pin_protocol = pin_protocol.map(extract_unsigned).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_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_uv_auth_protocol, pin_auth, }) } @@ -706,7 +706,7 @@ mod test { AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); let expected_client_pin_parameters = AuthenticatorClientPinParameters { - pin_protocol: 1, + pin_uv_auth_protocol: 1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: Some(cose_key), pin_auth: Some(vec![0xBB]), @@ -765,7 +765,7 @@ mod test { let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, sub_command_params: Some(params), - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth: Some(vec![0x9A; 16]), }; diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index cf98889..4a7f868 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::client_pin::ClientPin; 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; @@ -76,7 +76,7 @@ fn process_set_min_pin_length( /// Processes the AuthenticatorConfig command. pub fn process_config( persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, params: AuthenticatorConfigParameters, ) -> Result { let AuthenticatorConfigParameters { @@ -103,7 +103,7 @@ pub fn process_config( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - if !pin_protocol_v1.verify_pin_auth_token(&config_data, &auth_param) { + if !client_pin.verify_pin_auth_token(&config_data, &auth_param) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } } @@ -136,7 +136,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::EnableEnterpriseAttestation, @@ -144,8 +144,7 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); if ENTERPRISE_ATTESTATION_MODE.is_some() { assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); @@ -164,7 +163,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::ToggleAlwaysUv, @@ -172,8 +171,7 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert!(persistent_store.has_always_uv().unwrap()); @@ -183,8 +181,7 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); if ENFORCE_ALWAYS_UV { assert_eq!( config_response, @@ -202,7 +199,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ @@ -215,8 +212,7 @@ mod test { 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); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); if ENFORCE_ALWAYS_UV { assert_eq!( config_response, @@ -233,8 +229,7 @@ mod test { pin_uv_auth_param, pin_uv_auth_protocol: Some(1), }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert!(!persistent_store.has_always_uv().unwrap()); } @@ -264,13 +259,12 @@ mod test { 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 client_pin = ClientPin::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); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); @@ -284,8 +278,7 @@ mod test { 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); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); @@ -296,8 +289,7 @@ mod test { 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); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION) @@ -311,15 +303,14 @@ mod test { 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 client_pin = ClientPin::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); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); assert_eq!( @@ -339,8 +330,7 @@ mod test { 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); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); assert_eq!( @@ -353,8 +343,7 @@ mod test { 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); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) @@ -372,8 +361,7 @@ mod test { 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); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) @@ -391,7 +379,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); // Increase min PIN, force PIN change. @@ -402,8 +390,7 @@ mod test { 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); + let config_response = process_config(&mut persistent_store, &mut client_pin, 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)); @@ -415,7 +402,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ @@ -435,8 +422,7 @@ mod test { pin_uv_auth_param, pin_uv_auth_protocol: Some(1), }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.has_force_pin_change(), Ok(true)); } @@ -447,7 +433,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::VendorPrototype, @@ -455,8 +441,7 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let config_response = - process_config(&mut persistent_store, &mut pin_protocol_v1, config_params); + let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 5a2cc4b..e392ead 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::client_pin::{ClientPin, PinPermission}; 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; @@ -110,15 +110,15 @@ fn enumerate_credentials_response( /// 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, + client_pin: &mut ClientPin, 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() { + if client_pin.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) + client_pin.has_no_or_rp_id_permission(&credential.rp_id) } /// Processes the subcommand getCredsMetadata for CredentialManagement. @@ -173,14 +173,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, + client_pin: &mut ClientPin, 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[..])?; + client_pin.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 @@ -219,21 +219,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, + client_pin: &mut ClientPin, 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)?; + check_rp_id_permissions(persistent_store, client_pin, &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, + client_pin: &mut ClientPin, sub_command_params: CredentialManagementSubCommandParameters, ) -> Result<(), Ctap2StatusCode> { let credential_id = sub_command_params @@ -243,7 +243,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)?; + check_rp_id_permissions(persistent_store, client_pin, &credential_id)?; persistent_store.update_credential(&credential_id, user) } @@ -251,14 +251,14 @@ fn process_update_user_information( pub fn process_credential_management( persistent_store: &mut PersistentStore, stateful_command_permission: &mut StatefulPermission, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, cred_management_params: AuthenticatorCredentialManagementParameters, now: ClockValue, ) -> Result { let AuthenticatorCredentialManagementParameters { sub_command, sub_command_params, - pin_protocol, + pin_uv_auth_protocol, pin_auth, } = cred_management_params; @@ -282,7 +282,7 @@ pub fn process_credential_management( | CredentialManagementSubCommand::EnumerateCredentialsBegin | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::UpdateUserInformation => { - check_pin_uv_auth_protocol(pin_protocol)?; + check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; 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() { @@ -290,11 +290,11 @@ pub fn process_credential_management( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - if !pin_protocol_v1.verify_pin_auth_token(&management_data, &pin_auth) { + if !client_pin.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)?; + client_pin.has_permission(PinPermission::CredentialManagement)?; } CredentialManagementSubCommand::EnumerateRpsGetNextRp | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} @@ -302,11 +302,11 @@ pub fn process_credential_management( let response = match sub_command { CredentialManagementSubCommand::GetCredsMetadata => { - pin_protocol_v1.has_no_rp_id_permission()?; + client_pin.has_no_rp_id_permission()?; Some(process_get_creds_metadata(persistent_store)?) } CredentialManagementSubCommand::EnumerateRpsBegin => { - pin_protocol_v1.has_no_rp_id_permission()?; + client_pin.has_no_rp_id_permission()?; Some(process_enumerate_rps_begin( persistent_store, stateful_command_permission, @@ -320,7 +320,7 @@ pub fn process_credential_management( Some(process_enumerate_credentials_begin( persistent_store, stateful_command_permission, - pin_protocol_v1, + client_pin, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, now, )?) @@ -334,7 +334,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::DeleteCredential => { process_delete_credential( persistent_store, - pin_protocol_v1, + client_pin, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None @@ -342,7 +342,7 @@ pub fn process_credential_management( CredentialManagementSubCommand::UpdateUserInformation => { process_update_user_information( persistent_store, - pin_protocol_v1, + client_pin, sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, )?; None @@ -384,12 +384,12 @@ mod test { 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 client_pin = ClientPin::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.client_pin = client_pin; ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let pin_auth = Some(vec![ @@ -400,13 +400,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -428,13 +428,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -455,14 +455,14 @@ mod test { 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 client_pin = ClientPin::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.client_pin = client_pin; ctap_state .persistent_store @@ -482,13 +482,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -506,13 +506,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -531,13 +531,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -552,12 +552,12 @@ mod test { 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 client_pin = ClientPin::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.client_pin = client_pin; const NUM_CREDENTIALS: usize = 20; for i in 0..NUM_CREDENTIALS { @@ -581,7 +581,7 @@ mod test { let mut cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_protocol: Some(1), pin_auth, }; @@ -589,7 +589,7 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -611,7 +611,7 @@ mod test { cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_protocol: None, pin_auth: None, }; } @@ -619,7 +619,7 @@ mod test { let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, &mut ctap_state.stateful_command_permission, - &mut ctap_state.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -634,7 +634,7 @@ mod test { 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 client_pin = ClientPin::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]; @@ -644,7 +644,7 @@ mod test { 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.client_pin = client_pin; ctap_state .persistent_store @@ -671,13 +671,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, sub_command_params: Some(sub_command_params), - pin_protocol: Some(1), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -694,13 +694,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -718,13 +718,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, sub_command_params: None, - pin_protocol: None, + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -739,13 +739,13 @@ mod test { 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 client_pin = ClientPin::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.client_pin = client_pin; ctap_state .persistent_store @@ -771,13 +771,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::DeleteCredential, sub_command_params: Some(sub_command_params.clone()), - pin_protocol: Some(1), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -789,13 +789,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::DeleteCredential, sub_command_params: Some(sub_command_params), - pin_protocol: Some(1), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -810,13 +810,13 @@ mod test { 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 client_pin = ClientPin::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.client_pin = client_pin; ctap_state .persistent_store @@ -848,13 +848,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::UpdateUserInformation, sub_command_params: Some(sub_command_params), - pin_protocol: Some(1), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -878,15 +878,15 @@ mod test { } #[test] - fn test_process_credential_management_invalid_pin_protocol() { + fn test_process_credential_management_invalid_pin_uv_auth_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 client_pin = ClientPin::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.client_pin = client_pin; ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let pin_auth = Some(vec![ @@ -897,13 +897,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_protocol: Some(123456), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); @@ -924,13 +924,13 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_protocol: Some(1), + pin_uv_auth_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.pin_protocol_v1, + &mut ctap_state.client_pin, cred_management_params, DUMMY_CLOCK_VALUE, ); diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index f84c1a9..b74f982 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -13,8 +13,8 @@ // limitations under the License. use super::check_pin_uv_auth_protocol; +use super::client_pin::{ClientPin, PinPermission}; 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; @@ -51,7 +51,7 @@ impl LargeBlobs { pub fn process_command( &mut self, persistent_store: &mut PersistentStore, - pin_protocol_v1: &mut PinProtocolV1, + client_pin: &mut ClientPin, large_blobs_params: AuthenticatorLargeBlobsParameters, ) -> Result { let AuthenticatorLargeBlobsParameters { @@ -94,14 +94,14 @@ impl LargeBlobs { // 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)?; + client_pin.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) { + if !client_pin.verify_pin_auth_token(&message, &pin_uv_auth_param) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } } @@ -146,7 +146,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); let large_blob = vec![ @@ -161,11 +161,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); match large_blobs_response.unwrap() { ResponseData::AuthenticatorLargeBlobs(Some(response)) => { assert_eq!(response.config, large_blob); @@ -180,7 +177,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -196,11 +193,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) @@ -214,11 +208,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) @@ -232,11 +223,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); match large_blobs_response.unwrap() { ResponseData::AuthenticatorLargeBlobs(Some(response)) => { assert_eq!(response.config, large_blob); @@ -251,7 +239,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -267,11 +255,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) @@ -286,11 +271,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ), @@ -303,7 +285,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -320,11 +302,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Ok(ResponseData::AuthenticatorLargeBlobs(None)) @@ -338,11 +317,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), @@ -355,7 +331,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -370,11 +346,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); assert_eq!( large_blobs_response, Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE), @@ -387,7 +360,7 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -409,11 +382,8 @@ mod test { 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, - ); + let large_blobs_response = + large_blobs.process_command(&mut persistent_store, &mut client_pin, 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 f74b16e..d9606e8 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. pub mod apdu; +mod client_pin; pub mod command; mod config_command; mod credential_management; @@ -23,15 +24,15 @@ pub mod data_formats; pub mod hid; mod key_material; mod large_blobs; -mod pin_protocol_v1; pub mod response; pub mod status_code; mod storage; mod timed_permission; +use self::client_pin::{ClientPin, PinPermission}; use self::command::{ - AuthenticatorClientPinParameters, AuthenticatorGetAssertionParameters, - AuthenticatorMakeCredentialParameters, AuthenticatorVendorConfigureParameters, Command, + AuthenticatorGetAssertionParameters, AuthenticatorMakeCredentialParameters, + AuthenticatorVendorConfigureParameters, Command, }; use self::config_command::process_config; use self::credential_management::process_credential_management; @@ -48,7 +49,6 @@ use self::data_formats::{ }; use self::hid::ChannelID; use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; -use self::pin_protocol_v1::{PinPermission, PinProtocolV1}; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, @@ -278,7 +278,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( // false otherwise. check_user_presence: CheckUserPresence, persistent_store: PersistentStore, - pin_protocol_v1: PinProtocolV1, + client_pin: ClientPin, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, // The state initializes to Reset and its timeout, and never goes back to Reset. @@ -297,12 +297,12 @@ where now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { let persistent_store = PersistentStore::new(rng); - let pin_protocol_v1 = PinProtocolV1::new(rng); + let client_pin = ClientPin::new(rng); CtapState { rng, check_user_presence, persistent_store, - pin_protocol_v1, + client_pin, #[cfg(feature = "with_ctap1")] u2f_up_state: U2fUserPresenceState::new( U2F_UP_PROMPT_TIMEOUT, @@ -473,13 +473,17 @@ where } Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(now), Command::AuthenticatorGetInfo => self.process_get_info(), - Command::AuthenticatorClientPin(params) => self.process_client_pin(params), + Command::AuthenticatorClientPin(params) => self.client_pin.process_command( + self.rng, + &mut self.persistent_store, + 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.pin_protocol_v1, + &mut self.client_pin, params, now, ) @@ -487,14 +491,12 @@ 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, + &mut self.client_pin, params, ), + Command::AuthenticatorConfig(params) => { + process_config(&mut self.persistent_store, &mut self.client_pin, params) + } // Vendor specific commands Command::AuthenticatorVendorConfigure(params) => { self.process_vendor_configure(params, cid) @@ -647,14 +649,14 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } if !self - .pin_protocol_v1 + .client_pin .verify_pin_auth_token(&client_data_hash, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - self.pin_protocol_v1 + self.client_pin .has_permission(PinPermission::MakeCredential)?; - self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; + self.client_pin.ensure_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } None => { @@ -815,7 +817,7 @@ where 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 + self.client_pin .process_hmac_secret(hmac_secret_input, &cred_random)?, ) } else { @@ -938,14 +940,14 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } if !self - .pin_protocol_v1 + .client_pin .verify_pin_auth_token(&client_data_hash, &pin_auth) { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - self.pin_protocol_v1 + self.client_pin .has_permission(PinPermission::GetAssertion)?; - self.pin_protocol_v1.ensure_rp_id_permission(&rp_id)?; + self.client_pin.ensure_rp_id_permission(&rp_id)?; UV_FLAG } None => { @@ -1104,17 +1106,6 @@ where )) } - fn process_client_pin( - &mut self, - client_pin_params: AuthenticatorClientPinParameters, - ) -> Result { - self.pin_protocol_v1.process_subcommand( - self.rng, - &mut self.persistent_store, - client_pin_params, - ) - } - fn process_reset( &mut self, cid: ChannelID, @@ -1129,7 +1120,7 @@ where (self.check_user_presence)(cid)?; self.persistent_store.reset(self.rng)?; - self.pin_protocol_v1.reset(self.rng); + self.client_pin.reset(self.rng); #[cfg(feature = "with_ctap1")] { self.u2f_up_state = U2fUserPresenceState::new( @@ -2332,11 +2323,11 @@ mod test { 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 client_pin = ClientPin::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.client_pin = client_pin; let mut make_credential_params = create_minimal_make_credential_parameters(); let user1 = PublicKeyCredentialUserEntity { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 0f6657f..a650d4d 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -14,6 +14,7 @@ mod key; +use crate::ctap::client_pin::PIN_AUTH_LENGTH; use crate::ctap::customization::{ DEFAULT_MIN_PIN_LENGTH, DEFAULT_MIN_PIN_LENGTH_RP_IDS, ENFORCE_ALWAYS_UV, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_PIN_RETRIES, MAX_RP_IDS_LENGTH, MAX_SUPPORTED_RESIDENT_KEYS, @@ -24,7 +25,6 @@ use crate::ctap::data_formats::{ 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::embedded_flash::{new_storage, Storage}; From eb0a0770dda54ff76bd40467f3b0901a6f57ef1c Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 10 Mar 2021 13:20:29 +0100 Subject: [PATCH 179/192] adds the PIN protocol trait (#292) * adds the PIN protocol trait * improved documentation * SharedSecret not mutable --- libraries/crypto/src/ecdh.rs | 20 +- src/ctap/client_pin.rs | 460 +++++++++++++----------------- src/ctap/config_command.rs | 4 +- src/ctap/credential_management.rs | 4 +- src/ctap/large_blobs.rs | 4 +- src/ctap/mod.rs | 26 +- src/ctap/pin_protocol.rs | 443 ++++++++++++++++++++++++++++ 7 files changed, 660 insertions(+), 301 deletions(-) create mode 100644 src/ctap/pin_protocol.rs diff --git a/libraries/crypto/src/ecdh.rs b/libraries/crypto/src/ecdh.rs index 705aee0..4a03679 100644 --- a/libraries/crypto/src/ecdh.rs +++ b/libraries/crypto/src/ecdh.rs @@ -17,8 +17,6 @@ use super::ec::int256; use super::ec::int256::Int256; use super::ec::point::PointP256; use super::rng256::Rng256; -use super::sha256::Sha256; -use super::Hash256; pub const NBYTES: usize = int256::NBYTES; @@ -62,15 +60,15 @@ impl SecKey { // - https://www.secg.org/sec1-v2.pdf } - /// Creates a shared key using the Diffie Hellman key agreement. + /// Performs the handshake 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] { + /// This function generates the Z in the PIN protocol v1 specification. + /// https://drafts.fidoalliance.org/fido-2/stable-links-to-latest/fido-client-to-authenticator-protocol.html#pinProto1 + pub fn exchange_x(&self, other: &PubKey) -> [u8; 32] { let p = self.exchange_raw(other); let mut x: [u8; 32] = [Default::default(); 32]; p.getx().to_int().to_bin(&mut x); - Sha256::hash(&x) + x } } @@ -123,7 +121,7 @@ mod test { /** Test that the exchanged key is the same on both sides **/ #[test] - fn test_exchange_x_sha256_is_symmetric() { + fn test_exchange_x_is_symmetric() { let mut rng = ThreadRng256 {}; for _ in 0..ITERATIONS { @@ -131,12 +129,12 @@ mod test { let pk_a = sk_a.genpk(); let sk_b = SecKey::gensk(&mut rng); let pk_b = sk_b.genpk(); - assert_eq!(sk_a.exchange_x_sha256(&pk_b), sk_b.exchange_x_sha256(&pk_a)); + assert_eq!(sk_a.exchange_x(&pk_b), sk_b.exchange_x(&pk_a)); } } #[test] - fn test_exchange_x_sha256_bytes_is_symmetric() { + fn test_exchange_x_bytes_is_symmetric() { let mut rng = ThreadRng256 {}; for _ in 0..ITERATIONS { @@ -150,7 +148,7 @@ mod test { let pk_a = PubKey::from_bytes_uncompressed(&pk_bytes_a).unwrap(); let pk_b = PubKey::from_bytes_uncompressed(&pk_bytes_b).unwrap(); - assert_eq!(sk_a.exchange_x_sha256(&pk_b), sk_b.exchange_x_sha256(&pk_a)); + assert_eq!(sk_a.exchange_x(&pk_b), sk_b.exchange_x(&pk_a)); } } diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index ec309ed..245f020 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -14,17 +14,14 @@ use super::command::AuthenticatorClientPinParameters; use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput}; +use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; 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; -use arrayref::array_ref; -use core::convert::TryInto; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; -use crypto::hmac::{hmac_256, verify_hmac_256_first_128bits}; +use crypto::hmac::hmac_256; use crypto::rng256::Rng256; use crypto::sha256::Sha256; use crypto::Hash256; @@ -32,123 +29,82 @@ use crypto::Hash256; use enum_iterator::IntoEnumIterator; use subtle::ConstantTimeEq; -// Those constants have to be multiples of 16, the AES block size. +/// The prefix length of the PIN hash that is stored and compared. +/// +/// The code assumes that this value is a multiple of the AES block length, fits +/// an u8 and is at most as long as a SHA256. The value is fixed for all PIN +/// protocols. pub const PIN_AUTH_LENGTH: usize = 16; + +/// The length of the pinUvAuthToken used throughout PIN protocols. +/// +/// The code assumes that this value is a multiple of the AES block length. It +/// is fixed since CTAP2.1. +pub const PIN_TOKEN_LENGTH: usize = 32; + +/// The length of the encrypted PINs when received by SetPin or ChangePin. +/// +/// The code assumes that this value is a multiple of the AES block length. It +/// is fixed since CTAP2.1. const PIN_PADDED_LENGTH: usize = 64; -const PIN_TOKEN_LENGTH: usize = 32; -/// Checks the given pin_auth against the truncated output of HMAC-SHA256. -/// Returns LEFT(HMAC(hmac_key, hmac_contents), 16) == pin_auth). -fn verify_pin_auth(hmac_key: &[u8], hmac_contents: &[u8], pin_auth: &[u8]) -> bool { - if pin_auth.len() != PIN_AUTH_LENGTH { - return false; - } - verify_hmac_256_first_128bits::( - hmac_key, - hmac_contents, - array_ref![pin_auth, 0, PIN_AUTH_LENGTH], - ) -} - -/// Encrypts the HMAC-secret outputs. To compute them, we first have to -/// decrypt the HMAC secret salt(s) that were encrypted with the shared secret. -/// The credRandom is used as a secret to HMAC those salts. +/// Computes and encrypts the HMAC-secret outputs. +/// +/// To compute them, we first have to decrypt the HMAC secret salt(s) that were +/// encrypted with the shared secret. The credRandom is used as a secret in HMAC +/// for those salts. fn encrypt_hmac_secret_output( - shared_secret: &[u8; 32], + rng: &mut impl Rng256, + shared_secret: &dyn SharedSecret, salt_enc: &[u8], cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { - if salt_enc.len() != 32 && salt_enc.len() != 64 { + let decrypted_salts = shared_secret.decrypt(salt_enc)?; + if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 { 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); - // The specification specifically asks for a zero IV. - let iv = [0u8; 16]; - - // 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)]); + let mut output = hmac_256::(&cred_random[..], &decrypted_salts[..32]).to_vec(); + if decrypted_salts.len() == 64 { + let mut output2 = hmac_256::(&cred_random[..], &decrypted_salts[32..]).to_vec(); + output.append(&mut output2); } - cbc_decrypt(&aes_dec_key, iv, &mut blocks[..block_len]); - - 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[..], &decrypted_salt1[..]); - for i in 0..2 { - blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); - } - - if block_len == 4 { - let mut decrypted_salt2 = [0u8; 32]; - decrypted_salt2[..16].copy_from_slice(&blocks[2]); - decrypted_salt2[16..].copy_from_slice(&blocks[3]); - 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)]); - } - } - - cbc_encrypt(&aes_enc_key, iv, &mut blocks[..block_len]); - let mut encrypted_output = Vec::with_capacity(salt_enc.len()); - for b in &blocks[..block_len] { - encrypted_output.extend(b); - } - Ok(encrypted_output) + shared_secret.encrypt(rng, &output) } /// Decrypts the new_pin_enc and outputs the found PIN. fn decrypt_pin( - aes_dec_key: &crypto::aes256::DecryptionKey, + shared_secret: &dyn SharedSecret, new_pin_enc: Vec, -) -> Option> { - if new_pin_enc.len() != PIN_PADDED_LENGTH { - return None; +) -> Result, Ctap2StatusCode> { + let decrypted_pin = shared_secret.decrypt(&new_pin_enc)?; + if decrypted_pin.len() != PIN_PADDED_LENGTH { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let iv = [0u8; 16]; - // Assuming PIN_PADDED_LENGTH % block_size == 0 here. - const BLOCK_COUNT: usize = PIN_PADDED_LENGTH / 16; - let mut blocks = [[0u8; 16]; BLOCK_COUNT]; - for i in 0..BLOCK_COUNT { - blocks[i].copy_from_slice(&new_pin_enc[i * 16..(i + 1) * 16]); - } - cbc_decrypt(aes_dec_key, iv, &mut blocks); // In CTAP 2.1, the specification changed. The new wording might lead to // different behavior when there are non-zero bytes after zero bytes. // This implementation consistently ignores those degenerate cases. - Some( - blocks - .iter() - .flatten() - .cloned() - .take_while(|&c| c != 0) - .collect::>(), - ) + Ok(decrypted_pin.into_iter().take_while(|&c| c != 0).collect()) } -/// Stores the encrypted new PIN in the persistent storage, if it satisfies the -/// PIN policy. The PIN is decrypted and stripped from its padding. Next, the -/// length of the PIN is checked to fulfill policy requirements. Last, the PIN -/// is hashed, truncated to 16 bytes and persistently stored. +/// Stores a hash prefix of the new PIN in the persistent storage, if correct. +/// +/// The new PIN is passed encrypted, so it is first decrypted and stripped from +/// padding. Next, it is checked against the PIN policy. Last, it is hashed and +/// truncated for persistent storage. fn check_and_store_new_pin( persistent_store: &mut PersistentStore, - aes_dec_key: &crypto::aes256::DecryptionKey, + shared_secret: &dyn SharedSecret, new_pin_enc: Vec, ) -> Result<(), Ctap2StatusCode> { - let pin = decrypt_pin(aes_dec_key, new_pin_enc) - .ok_or(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION)?; - + let pin = decrypt_pin(shared_secret, new_pin_enc)?; let min_pin_length = persistent_store.min_pin_length()? as usize; 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]); - // The PIN length is always < 64. + let mut pin_hash = [0u8; PIN_AUTH_LENGTH]; + pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..PIN_AUTH_LENGTH]); + // The PIN length is always < PIN_PADDED_LENGTH < 256. persistent_store.set_pin(&pin_hash, pin_length as u8)?; Ok(()) } @@ -167,8 +123,7 @@ pub enum PinPermission { } pub struct ClientPin { - key_agreement_key: crypto::ecdh::SecKey, - pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], + pin_protocol_v1: PinProtocol, consecutive_pin_mismatches: u8, permissions: u8, permissions_rp_id: Option, @@ -176,17 +131,16 @@ pub struct ClientPin { impl ClientPin { pub fn new(rng: &mut impl Rng256) -> ClientPin { - let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - let pin_uv_auth_token = rng.gen_uniform_u8x32(); ClientPin { - key_agreement_key, - pin_uv_auth_token, + pin_protocol_v1: PinProtocol::new(rng), consecutive_pin_mismatches: 0, permissions: 0, permissions_rp_id: None, } } + /// Checks the given encrypted PIN hash against the stored PIN hash. + /// /// Decrypts the encrypted pin_hash and compares it to the stored pin_hash. /// Resets or decreases the PIN retries, depending on success or failure. /// Also, in case of failure, the key agreement key is randomly reset. @@ -194,7 +148,7 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, - aes_dec_key: &crypto::aes256::DecryptionKey, + shared_secret: &dyn SharedSecret, pin_hash_enc: Vec, ) -> Result<(), Ctap2StatusCode> { match persistent_store.pin_hash()? { @@ -206,14 +160,10 @@ impl ClientPin { if pin_hash_enc.len() != PIN_AUTH_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } + let pin_hash_dec = shared_secret.decrypt(&pin_hash_enc)?; - let iv = [0u8; 16]; - let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash_enc); - cbc_decrypt(aes_dec_key, iv, &mut blocks); - - if !bool::from(pin_hash.ct_eq(&blocks[0])) { - self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + if !bool::from(pin_hash.ct_eq(&pin_hash_dec)) { + self.pin_protocol_v1.regenerate(rng); if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } @@ -232,26 +182,6 @@ impl ClientPin { Ok(()) } - /// Uses the self-owned and passed halves of the key agreement to generate the - /// shared secret for checking pin_auth and generating a decryption key. - fn exchange_decryption_key( - &self, - key_agreement: CoseKey, - pin_auth: &[u8], - authenticated_message: &[u8], - ) -> Result { - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - if !verify_pin_auth(&shared_secret, authenticated_message, pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); - Ok(aes_dec_key) - } - fn process_get_pin_retries( &self, persistent_store: &PersistentStore, @@ -264,9 +194,8 @@ impl ClientPin { } fn process_get_key_agreement(&self) -> Result { - let pk = self.key_agreement_key.genpk(); Ok(AuthenticatorClientPinResponse { - key_agreement: Some(CoseKey::from(pk)), + key_agreement: Some(self.pin_protocol_v1.get_public_key()), pin_token: None, retries: None, }) @@ -282,9 +211,10 @@ impl ClientPin { if persistent_store.pin_hash()?.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - let pin_decryption_key = - self.exchange_decryption_key(key_agreement, &pin_auth, &new_pin_enc)?; - check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; + let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + shared_secret.verify(&new_pin_enc, &pin_auth)?; + + check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; persistent_store.reset_pin_retries()?; Ok(()) } @@ -301,14 +231,14 @@ impl ClientPin { if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } + let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; let mut auth_param_data = new_pin_enc.clone(); auth_param_data.extend(&pin_hash_enc); - let pin_decryption_key = - self.exchange_decryption_key(key_agreement, &pin_auth, &auth_param_data)?; - self.verify_pin_hash_enc(rng, persistent_store, &pin_decryption_key, pin_hash_enc)?; + shared_secret.verify(&auth_param_data, &pin_auth)?; + self.verify_pin_hash_enc(rng, persistent_store, shared_secret.as_ref(), pin_hash_enc)?; - check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; - self.pin_uv_auth_token = rng.gen_uniform_u8x32(); + check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); Ok(()) } @@ -322,26 +252,13 @@ impl ClientPin { if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - - let 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? + let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + self.verify_pin_hash_enc(rng, persistent_store, shared_secret.as_ref(), 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]; - let mut blocks = [[0u8; 16]; PIN_TOKEN_LENGTH / 16]; - for (i, item) in blocks.iter_mut().take(PIN_TOKEN_LENGTH / 16).enumerate() { - item.copy_from_slice(&self.pin_uv_auth_token[i * 16..(i + 1) * 16]); - } - cbc_encrypt(&token_encryption_key, iv, &mut blocks); - let pin_token: Vec = blocks.iter().flatten().cloned().collect(); - + let pin_token = shared_secret.encrypt(rng, self.pin_protocol_v1.get_pin_uv_auth_token())?; self.permissions = 0x03; self.permissions_rp_id = None; @@ -469,13 +386,23 @@ impl ClientPin { Ok(ResponseData::AuthenticatorClientPin(response)) } - pub fn verify_pin_auth_token(&self, hmac_contents: &[u8], pin_auth: &[u8]) -> bool { - verify_pin_auth(&self.pin_uv_auth_token, &hmac_contents, &pin_auth) + pub fn verify_pin_auth_token( + &self, + hmac_contents: &[u8], + pin_auth: &[u8], + ) -> Result<(), Ctap2StatusCode> { + // TODO(kaczmarczyck) pass the protocol number + verify_pin_uv_auth_token( + self.pin_protocol_v1.get_pin_uv_auth_token(), + hmac_contents, + pin_auth, + 1, + ) } pub fn reset(&mut self, rng: &mut impl Rng256) { - self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - self.pin_uv_auth_token = rng.gen_uniform_u8x32(); + self.pin_protocol_v1.regenerate(rng); + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); self.consecutive_pin_mismatches = 0; self.permissions = 0; self.permissions_rp_id = None; @@ -483,6 +410,7 @@ impl ClientPin { pub fn process_hmac_secret( &self, + rng: &mut impl Rng256, hmac_secret_input: GetAssertionHmacSecretInput, cred_random: &[u8; 32], ) -> Result, Ctap2StatusCode> { @@ -491,14 +419,9 @@ impl ClientPin { salt_enc, salt_auth, } = hmac_secret_input; - let pk: crypto::ecdh::PubKey = CoseKey::try_into(key_agreement)?; - let shared_secret = self.key_agreement_key.exchange_x_sha256(&pk); - // HMAC-secret does the same 16 byte truncated check. - if !verify_pin_auth(&shared_secret, &salt_enc, &salt_auth) { - // Hard to tell what the correct error code here is. - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - encrypt_hmac_secret_output(&shared_secret, &salt_enc[..], cred_random) + let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + shared_secret.verify(&salt_enc, &salt_auth)?; + encrypt_hmac_secret_output(rng, shared_secret.as_ref(), &salt_enc[..], cred_random) } /// Check if the required command's token permission is granted. @@ -560,8 +483,7 @@ impl ClientPin { pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], ) -> ClientPin { ClientPin { - key_agreement_key, - pin_uv_auth_token, + pin_protocol_v1: PinProtocol::new_test(key_agreement_key, pin_uv_auth_token), consecutive_pin_mismatches: 0, permissions: 0xFF, permissions_rp_id: None, @@ -571,10 +493,12 @@ impl ClientPin { #[cfg(test)] mod test { + use super::super::pin_protocol::SharedSecretV1; use super::*; + use alloc::vec; use crypto::rng256::ThreadRng256; - // Stores a PIN hash corresponding to the dummy PIN "1234". + /// Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(persistent_store: &mut PersistentStore) { let mut pin = [0u8; 64]; pin[..4].copy_from_slice(b"1234"); @@ -583,36 +507,20 @@ mod test { persistent_store.set_pin(&pin_hash, 4).unwrap(); } - // Encrypts the message with a zero IV and key derived from shared_secret. + /// 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::>() + let mut rng = ThreadRng256 {}; + let shared_secret = SharedSecretV1::new_test(*shared_secret); + shared_secret.encrypt(&mut rng, message).unwrap() } - // Decrypts the message with a zero IV and key derived from shared_secret. + /// 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::>() + let shared_secret = SharedSecretV1::new_test(*shared_secret); + shared_secret.decrypt(message).unwrap() } - // Fails on PINs bigger than 64 bytes. + /// 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]; @@ -620,12 +528,12 @@ mod test { encrypt_message(shared_secret, &padded_pin) } - // Encrypts the dummy PIN "1234". + /// Encrypts the dummy PIN "1234". fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { encrypt_pin(shared_secret, b"1234".to_vec()) } - // Encrypts the PIN hash corresponding to the dummy PIN "1234". + /// Encrypts the PIN hash corresponding to the dummy PIN "1234". fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { let mut pin = [0u8; 64]; pin[..4].copy_from_slice(b"1234"); @@ -643,9 +551,7 @@ mod test { 0xC4, 0x12, ]; 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); + let shared_secret = SharedSecretV1::new_test([0x88; 32]); let mut client_pin = ClientPin::new(&mut rng); let pin_hash_enc = vec![ @@ -656,7 +562,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Ok(()) @@ -667,7 +573,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -682,7 +588,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) @@ -694,7 +600,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -705,7 +611,7 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &aes_dec_key, + &shared_secret, pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -731,8 +637,10 @@ mod test { #[test] fn test_process_get_key_agreement() { let mut rng = ThreadRng256 {}; - let client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); let expected_response = Ok(AuthenticatorClientPinResponse { key_agreement: Some(CoseKey::from(pk)), pin_token: None, @@ -745,9 +653,12 @@ mod test { fn test_process_set_pin() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); let new_pin_enc = encrypt_standard_pin(&shared_secret); let pin_auth = hmac_256::(&shared_secret, &new_pin_enc[..])[..16].to_vec(); @@ -762,14 +673,18 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); let new_pin_enc = encrypt_standard_pin(&shared_secret); let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); let mut auth_param_data = new_pin_enc.clone(); auth_param_data.extend(&pin_hash_enc); + let pin_auth = hmac_256::(&shared_secret, &auth_param_data[..])[..16].to_vec(); assert_eq!( client_pin.process_change_pin( @@ -817,10 +732,14 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert!(client_pin .process_get_pin_token( @@ -849,10 +768,14 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); assert_eq!(persistent_store.force_pin_change(), Ok(())); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert_eq!( client_pin.process_get_pin_token( @@ -870,10 +793,14 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert!(client_pin .process_get_pin_uv_auth_token_using_pin_with_permissions( @@ -935,10 +862,14 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); assert_eq!(persistent_store.force_pin_change(), Ok(())); - let mut client_pin = ClientPin::new(&mut rng); - let pk = client_pin.key_agreement_key.genpk(); - let shared_secret = client_pin.key_agreement_key.exchange_x_sha256(&pk); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let pre_secret = key_agreement_key.exchange_x(&pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let shared_secret = Sha256::hash(&pre_secret); let key_agreement = CoseKey::from(pk); + let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert_eq!( client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( @@ -991,9 +922,7 @@ mod test { #[test] fn test_decrypt_pin() { - let shared_secret = [0x88; 32]; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + let shared_secret = SharedSecretV1::new_test([0x88; 32]); // "1234" let new_pin_enc = vec![ @@ -1004,8 +933,8 @@ mod test { 0x18, 0x35, 0x06, 0x66, 0x97, 0x84, 0x68, 0xC2, ]; assert_eq!( - decrypt_pin(&aes_dec_key, new_pin_enc), - Some(b"1234".to_vec()), + decrypt_pin(&shared_secret, new_pin_enc), + Ok(b"1234".to_vec()), ); // "123" @@ -1017,26 +946,31 @@ mod test { 0x7C, 0xC7, 0x2D, 0x43, 0x74, 0x4C, 0x1D, 0x7E, ]; assert_eq!( - decrypt_pin(&aes_dec_key, new_pin_enc), - Some(b"123".to_vec()), + decrypt_pin(&shared_secret, new_pin_enc), + Ok(b"123".to_vec()), ); // Encrypted PIN is too short. let new_pin_enc = vec![0x44; 63]; - assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); + assert_eq!( + decrypt_pin(&shared_secret, new_pin_enc), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); // Encrypted PIN is too long. let new_pin_enc = vec![0x44; 65]; - assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); + assert_eq!( + decrypt_pin(&shared_secret, new_pin_enc), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); } #[test] fn test_check_and_store_new_pin() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let shared_secret = [0x88; 32]; - let aes_enc_key = crypto::aes256::EncryptionKey::new(&shared_secret); - let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + let shared_secret_hash = [0x88; 32]; + let shared_secret = SharedSecretV1::new_test(shared_secret_hash); let test_cases = vec![ // Accept PIN "1234". @@ -1059,9 +993,9 @@ mod test { ]; for (pin, result) in test_cases { let old_pin_hash = persistent_store.pin_hash().unwrap(); - let new_pin_enc = encrypt_pin(&shared_secret, pin); + let new_pin_enc = encrypt_pin(&shared_secret_hash, pin); assert_eq!( - check_and_store_new_pin(&mut persistent_store, &aes_dec_key, new_pin_enc), + check_and_store_new_pin(&mut persistent_store, &shared_secret, new_pin_enc), result ); if result.is_ok() { @@ -1072,31 +1006,22 @@ mod test { } } - #[test] - fn test_verify_pin_auth() { - let hmac_key = [0x88; 16]; - let pin_auth = [ - 0x88, 0x09, 0x41, 0x13, 0xF7, 0x97, 0x32, 0x0B, 0x3E, 0xD9, 0xBC, 0x76, 0x4F, 0x18, - 0x56, 0x5D, - ]; - assert!(verify_pin_auth(&hmac_key, &[], &pin_auth)); - assert!(!verify_pin_auth(&hmac_key, &[0x00], &pin_auth)); - } - #[test] fn test_encrypt_hmac_secret_output() { - let shared_secret = [0x55; 32]; + let mut rng = ThreadRng256 {}; + let shared_secret_hash = [0x88; 32]; + let shared_secret = SharedSecretV1::new_test(shared_secret_hash); let salt_enc = [0x5E; 32]; let cred_random = [0xC9; 32]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); assert_eq!(output.unwrap().len(), 32); let salt_enc = [0x5E; 48]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); + let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); 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); + let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); assert_eq!(output.unwrap().len(), 64); let mut salt_enc = [0x00; 32]; @@ -1108,45 +1033,50 @@ mod test { let expected_output1 = hmac_256::(&cred_random, &salt1); let expected_output2 = hmac_256::(&cred_random, &salt2); - let salt_enc1 = encrypt_message(&shared_secret, &salt1); + let salt_enc1 = encrypt_message(&shared_secret_hash, &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); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &output); assert_eq!(&output_dec, &expected_output1); - let salt_enc2 = &encrypt_message(&shared_secret, &salt2); + let salt_enc2 = &encrypt_message(&shared_secret_hash, &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); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &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); + let salt_enc12 = encrypt_message(&shared_secret_hash, &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); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &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); + let salt_enc02 = encrypt_message(&shared_secret_hash, &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); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &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); + let salt_enc10 = encrypt_message(&shared_secret_hash, &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); + let output = + encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret_hash, &output); assert_eq!(&output_dec[..32], &expected_output1); } diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 4a7f868..2f90f11 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -103,9 +103,7 @@ pub fn process_config( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - if !client_pin.verify_pin_auth_token(&config_data, &auth_param) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + client_pin.verify_pin_auth_token(&config_data, &auth_param)?; } match sub_command { diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index e392ead..5b07294 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -290,9 +290,7 @@ pub fn process_credential_management( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - if !client_pin.verify_pin_auth_token(&management_data, &pin_auth) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + client_pin.verify_pin_auth_token(&management_data, &pin_auth)?; // The RP ID permission is handled differently per subcommand below. client_pin.has_permission(PinPermission::CredentialManagement)?; } diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index b74f982..05cec58 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -101,9 +101,7 @@ impl LargeBlobs { LittleEndian::write_u32(&mut offset_bytes, offset as u32); message.extend(&offset_bytes); message.extend(&Sha256::hash(set.as_slice())); - if !client_pin.verify_pin_auth_token(&message, &pin_uv_auth_param) { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + client_pin.verify_pin_auth_token(&message, &pin_uv_auth_param)?; } if offset + set.len() > self.expected_length { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d9606e8..6d8e2d0 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -24,6 +24,7 @@ pub mod data_formats; pub mod hid; mod key_material; mod large_blobs; +mod pin_protocol; pub mod response; pub mod status_code; mod storage; @@ -648,12 +649,8 @@ where // Specification is unclear, could be CTAP2_ERR_INVALID_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - if !self - .client_pin - .verify_pin_auth_token(&client_data_hash, &pin_auth) - { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + self.client_pin + .verify_pin_auth_token(&client_data_hash, &pin_auth)?; self.client_pin .has_permission(PinPermission::MakeCredential)?; self.client_pin.ensure_rp_id_permission(&rp_id)?; @@ -816,10 +813,11 @@ where 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.client_pin - .process_hmac_secret(hmac_secret_input, &cred_random)?, - ) + Some(self.client_pin.process_hmac_secret( + self.rng, + hmac_secret_input, + &cred_random, + )?) } else { None }; @@ -939,12 +937,8 @@ where // Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - if !self - .client_pin - .verify_pin_auth_token(&client_data_hash, &pin_auth) - { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } + self.client_pin + .verify_pin_auth_token(&client_data_hash, &pin_auth)?; self.client_pin .has_permission(PinPermission::GetAssertion)?; self.client_pin.ensure_rp_id_permission(&rp_id)?; diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs new file mode 100644 index 0000000..88d608a --- /dev/null +++ b/src/ctap/pin_protocol.rs @@ -0,0 +1,443 @@ +// 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 crate::ctap::client_pin::PIN_TOKEN_LENGTH; +use crate::ctap::data_formats::CoseKey; +use crate::ctap::status_code::Ctap2StatusCode; +use alloc::boxed::Box; +use alloc::vec; +use alloc::vec::Vec; +use core::convert::TryInto; +use crypto::cbc::{cbc_decrypt, cbc_encrypt}; +use crypto::hkdf::hkdf_empty_salt_256; +use crypto::hmac::{verify_hmac_256, verify_hmac_256_first_128bits}; +use crypto::rng256::Rng256; +use crypto::sha256::Sha256; +use crypto::Hash256; + +/// Implements common functions between existing PIN protocols for handshakes. +pub struct PinProtocol { + key_agreement_key: crypto::ecdh::SecKey, + pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], +} + +impl PinProtocol { + /// This process is run by the authenticator at power-on. + /// + /// This function implements "initialize" from the specification. + pub fn new(rng: &mut impl Rng256) -> PinProtocol { + let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + let pin_uv_auth_token = rng.gen_uniform_u8x32(); + PinProtocol { + key_agreement_key, + pin_uv_auth_token, + } + } + + /// Generates a fresh public key. + pub fn regenerate(&mut self, rng: &mut impl Rng256) { + self.key_agreement_key = crypto::ecdh::SecKey::gensk(rng); + } + + /// Generates a fresh pinUvAuthToken. + pub fn reset_pin_uv_auth_token(&mut self, rng: &mut impl Rng256) { + self.pin_uv_auth_token = rng.gen_uniform_u8x32(); + } + + /// Returns the authenticator’s public key as a CoseKey structure. + pub fn get_public_key(&self) -> CoseKey { + CoseKey::from(self.key_agreement_key.genpk()) + } + + /// Processes the peer's encapsulated CoseKey and returns the shared secret. + pub fn decapsulate( + &self, + peer_cose_key: CoseKey, + pin_uv_auth_protocol: u64, + ) -> Result, Ctap2StatusCode> { + let pk: crypto::ecdh::PubKey = CoseKey::try_into(peer_cose_key)?; + let handshake = self.key_agreement_key.exchange_x(&pk); + match pin_uv_auth_protocol { + 1 => Ok(Box::new(SharedSecretV1::new(handshake))), + 2 => Ok(Box::new(SharedSecretV2::new(handshake))), + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } + } + + /// Getter for pinUvAuthToken. + pub fn get_pin_uv_auth_token(&self) -> &[u8; PIN_TOKEN_LENGTH] { + &self.pin_uv_auth_token + } + + /// This is used for debugging to inject key material. + #[cfg(test)] + pub fn new_test( + key_agreement_key: crypto::ecdh::SecKey, + pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], + ) -> PinProtocol { + PinProtocol { + key_agreement_key, + pin_uv_auth_token, + } + } +} + +/// Verifies the pinUvAuthToken for the given PIN protocol. +pub fn verify_pin_uv_auth_token( + token: &[u8; PIN_TOKEN_LENGTH], + message: &[u8], + signature: &[u8], + pin_uv_auth_protocol: u64, +) -> Result<(), Ctap2StatusCode> { + match pin_uv_auth_protocol { + 1 => verify_v1(token, message, signature), + 2 => verify_v2(token, message, signature), + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } +} + +pub trait SharedSecret { + /// Returns the encrypted plaintext. + fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result, Ctap2StatusCode>; + + /// Returns the decrypted ciphertext. + fn decrypt(&self, ciphertext: &[u8]) -> Result, Ctap2StatusCode>; + + /// Verifies that the signature is a valid MAC for the given message. + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode>; +} + +fn aes256_cbc_encrypt( + rng: &mut dyn Rng256, + aes_enc_key: &crypto::aes256::EncryptionKey, + plaintext: &[u8], + has_iv: bool, +) -> Result, Ctap2StatusCode> { + if plaintext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let iv = if has_iv { + let random_bytes = rng.gen_uniform_u8x32(); + *array_ref!(random_bytes, 0, 16) + } else { + [0u8; 16] + }; + let mut blocks = Vec::with_capacity(plaintext.len() / 16); + // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. + for block in plaintext.chunks_exact(16) { + blocks.push(*array_ref!(block, 0, 16)); + } + cbc_encrypt(aes_enc_key, iv, &mut blocks); + let mut ciphertext = if has_iv { iv.to_vec() } else { vec![] }; + ciphertext.extend(blocks.iter().flatten()); + Ok(ciphertext) +} + +fn aes256_cbc_decrypt( + aes_enc_key: &crypto::aes256::EncryptionKey, + ciphertext: &[u8], + has_iv: bool, +) -> Result, Ctap2StatusCode> { + if ciphertext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let mut block_len = ciphertext.len() / 16; + // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. + let mut block_iter = ciphertext.chunks_exact(16); + let iv = if has_iv { + block_len -= 1; + let iv_block = block_iter + .next() + .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + *array_ref!(iv_block, 0, 16) + } else { + [0u8; 16] + }; + let mut blocks = Vec::with_capacity(block_len); + for block in block_iter { + blocks.push(*array_ref!(block, 0, 16)); + } + let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key); + cbc_decrypt(&aes_dec_key, iv, &mut blocks); + Ok(blocks.iter().flatten().cloned().collect::>()) +} + +fn verify_v1(key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { + if signature.len() != 16 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if verify_hmac_256_first_128bits::(key, message, array_ref![signature, 0, 16]) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } +} + +fn verify_v2(key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { + if signature.len() != 32 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + if verify_hmac_256::(key, message, array_ref![signature, 0, 32]) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } +} + +pub struct SharedSecretV1 { + common_secret: [u8; 32], + aes_enc_key: crypto::aes256::EncryptionKey, +} + +impl SharedSecretV1 { + /// Creates a new shared secret from the handshake result. + fn new(handshake: [u8; 32]) -> SharedSecretV1 { + let common_secret = Sha256::hash(&handshake); + let aes_enc_key = crypto::aes256::EncryptionKey::new(&common_secret); + SharedSecretV1 { + common_secret, + aes_enc_key, + } + } + + /// Creates a new shared secret for testing. + #[cfg(test)] + pub fn new_test(hash: [u8; 32]) -> SharedSecretV1 { + let aes_enc_key = crypto::aes256::EncryptionKey::new(&hash); + SharedSecretV1 { + common_secret: hash, + aes_enc_key, + } + } +} + +impl SharedSecret for SharedSecretV1 { + fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, false) + } + + fn decrypt(&self, ciphertext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, false) + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { + verify_v1(&self.common_secret, message, signature) + } +} + +pub struct SharedSecretV2 { + aes_enc_key: crypto::aes256::EncryptionKey, + hmac_key: [u8; 32], +} + +impl SharedSecretV2 { + /// Creates a new shared secret from the handshake result. + fn new(handshake: [u8; 32]) -> SharedSecretV2 { + let aes_key = hkdf_empty_salt_256::(&handshake, b"CTAP2 AES key"); + SharedSecretV2 { + aes_enc_key: crypto::aes256::EncryptionKey::new(&aes_key), + hmac_key: hkdf_empty_salt_256::(&handshake, b"CTAP2 HMAC key"), + } + } +} + +impl SharedSecret for SharedSecretV2 { + fn encrypt(&self, rng: &mut dyn Rng256, plaintext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_encrypt(rng, &self.aes_enc_key, plaintext, true) + } + + fn decrypt(&self, ciphertext: &[u8]) -> Result, Ctap2StatusCode> { + aes256_cbc_decrypt(&self.aes_enc_key, ciphertext, true) + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { + verify_v2(&self.hmac_key, message, signature) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crypto::rng256::ThreadRng256; + + #[test] + fn test_pin_protocol_public_key() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol = PinProtocol::new(&mut rng); + let public_key = pin_protocol.get_public_key(); + pin_protocol.regenerate(&mut rng); + let new_public_key = pin_protocol.get_public_key(); + assert_ne!(public_key, new_public_key); + } + + #[test] + fn test_pin_protocol_pin_uv_auth_token() { + let mut rng = ThreadRng256 {}; + let mut pin_protocol = PinProtocol::new(&mut rng); + let token = *pin_protocol.get_pin_uv_auth_token(); + pin_protocol.reset_pin_uv_auth_token(&mut rng); + let new_token = pin_protocol.get_pin_uv_auth_token(); + assert_ne!(&token, new_token); + } + + #[test] + fn test_shared_secret_v1_encrypt_decrypt() { + let mut rng = ThreadRng256 {}; + let shared_secret = SharedSecretV1::new([0x55; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = shared_secret.encrypt(&mut rng, &plaintext).unwrap(); + assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext)); + } + + #[test] + fn test_shared_secret_v1_verify() { + let shared_secret = SharedSecretV1::new([0x55; 32]); + let message = [0xAA]; + let signature = [ + 0x8B, 0x60, 0x15, 0x7D, 0xF3, 0x44, 0x82, 0x2E, 0x54, 0x34, 0x7A, 0x01, 0xFB, 0x02, + 0x48, 0xA6, + ]; + assert_eq!(shared_secret.verify(&message, &signature), Ok(())); + assert_eq!( + shared_secret.verify(&[0xBB], &signature), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + shared_secret.verify(&message, &[0x12; 16]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_shared_secret_v2_encrypt_decrypt() { + let mut rng = ThreadRng256 {}; + let shared_secret = SharedSecretV2::new([0x55; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = shared_secret.encrypt(&mut rng, &plaintext).unwrap(); + assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext)); + } + + #[test] + fn test_shared_secret_v2_verify() { + let shared_secret = SharedSecretV2::new([0x55; 32]); + let message = [0xAA]; + let signature = [ + 0xC0, 0x3F, 0x2A, 0x22, 0x5C, 0xC3, 0x4E, 0x05, 0xC1, 0x0E, 0x72, 0x9C, 0x8D, 0xD5, + 0x7D, 0xE5, 0x98, 0x9C, 0x68, 0x15, 0xEC, 0xE2, 0x3A, 0x95, 0xD5, 0x90, 0xE1, 0xE9, + 0x3F, 0xF0, 0x1A, 0xAF, + ]; + assert_eq!(shared_secret.verify(&message, &signature), Ok(())); + assert_eq!( + shared_secret.verify(&[0xBB], &signature), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + shared_secret.verify(&message, &[0x12; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_decapsulate_invalid() { + let mut rng = ThreadRng256 {}; + let pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol.decapsulate(pin_protocol.get_public_key(), 3); + assert_eq!( + shared_secret.err(), + Some(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + + #[test] + fn test_decapsulate_symmetric() { + let mut rng = ThreadRng256 {}; + let pin_protocol1 = PinProtocol::new(&mut rng); + let pin_protocol2 = PinProtocol::new(&mut rng); + for protocol in 1..=2 { + let shared_secret1 = pin_protocol1 + .decapsulate(pin_protocol2.get_public_key(), protocol) + .unwrap(); + let shared_secret2 = pin_protocol2 + .decapsulate(pin_protocol1.get_public_key(), protocol) + .unwrap(); + let plaintext = vec![0xAA; 64]; + let ciphertext = shared_secret1.encrypt(&mut rng, &plaintext).unwrap(); + assert_eq!(plaintext, shared_secret2.decrypt(&ciphertext).unwrap()); + } + } + + #[test] + fn test_verify_pin_uv_auth_token_v1() { + let token = [0x91; PIN_TOKEN_LENGTH]; + let message = [0xAA]; + let signature = [ + 0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50, + 0x49, 0x68, + ]; + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &signature, 1), + Ok(()) + ); + assert_eq!( + verify_pin_uv_auth_token(&[0x12; PIN_TOKEN_LENGTH], &message, &signature, 1), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &[0xBB], &signature, 1), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &[0x12; 16], 1), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_verify_pin_uv_auth_token_v2() { + let token = [0x91; PIN_TOKEN_LENGTH]; + let message = [0xAA]; + let signature = [ + 0x9C, 0x1C, 0xFE, 0x9D, 0xD7, 0x64, 0x6A, 0x06, 0xB9, 0xA8, 0x0F, 0x96, 0xAD, 0x50, + 0x49, 0x68, 0x94, 0x90, 0x20, 0x53, 0x0F, 0xA3, 0xD2, 0x7A, 0x9F, 0xFD, 0xFA, 0x62, + 0x36, 0x93, 0xF7, 0x84, + ]; + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &signature, 2), + Ok(()) + ); + assert_eq!( + verify_pin_uv_auth_token(&[0x12; PIN_TOKEN_LENGTH], &message, &signature, 2), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &[0xBB], &signature, 2), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &[0x12; 32], 2), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_verify_pin_uv_auth_token_invalid_protocol() { + let token = [0x91; PIN_TOKEN_LENGTH]; + let message = [0xAA]; + let signature = []; + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &signature, 3), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } +} From 3c7c5a4810965f774a055aada4f32bcf9387ad80 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Sat, 13 Mar 2021 13:16:57 +0100 Subject: [PATCH 180/192] Update the documentation to use linking by name See https://doc.rust-lang.org/stable/rustdoc/linking-to-items-by-name.html --- libraries/persistent_store/src/buffer.rs | 38 +- libraries/persistent_store/src/driver.rs | 4 + libraries/persistent_store/src/format.rs | 176 ++++---- libraries/persistent_store/src/fragment.rs | 4 +- libraries/persistent_store/src/lib.rs | 467 ++++++++++----------- libraries/persistent_store/src/model.rs | 5 +- libraries/persistent_store/src/storage.rs | 8 +- libraries/persistent_store/src/store.rs | 43 +- 8 files changed, 368 insertions(+), 377 deletions(-) diff --git a/libraries/persistent_store/src/buffer.rs b/libraries/persistent_store/src/buffer.rs index ae35089..3acd39a 100644 --- a/libraries/persistent_store/src/buffer.rs +++ b/libraries/persistent_store/src/buffer.rs @@ -12,6 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Flash storage for testing. +//! +//! [`BufferStorage`] implements the flash [`Storage`] interface but doesn't interface with an +//! actual flash storage. Instead it uses a buffer in memory to represent the storage state. + use crate::{Storage, StorageError, StorageIndex, StorageResult}; use alloc::borrow::Borrow; use alloc::boxed::Box; @@ -63,8 +68,8 @@ pub struct BufferOptions { /// /// 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`. + /// - A word is written more than [`Self::max_word_writes`]. + /// - A page is erased more than [`Self::max_page_erases`]. pub strict_mode: bool, } @@ -110,15 +115,13 @@ impl BufferStorage { /// /// Before each subsequent mutable operation (write or erase), the delay is decremented if /// positive. If the delay is elapsed, the operation is saved and an error is returned. - /// Subsequent operations will panic until the interrupted operation is [corrupted] or the - /// interruption is [reset]. + /// Subsequent operations will panic until either of: + /// - The interrupted operation is [corrupted](BufferStorage::corrupt_operation). + /// - The interruption is [reset](BufferStorage::reset_interruption). /// /// # Panics /// /// Panics if an interruption is already armed. - /// - /// [corrupted]: struct.BufferStorage.html#method.corrupt_operation - /// [reset]: struct.BufferStorage.html#method.reset_interruption pub fn arm_interruption(&mut self, delay: usize) { self.interruption.arm(delay); } @@ -130,10 +133,8 @@ impl BufferStorage { /// # Panics /// /// Panics if any of the following conditions hold: - /// - An interruption was not [armed]. + /// - An interruption was not [armed](BufferStorage::arm_interruption). /// - An interruption was armed and it has triggered. - /// - /// [armed]: struct.BufferStorage.html#method.arm_interruption pub fn disarm_interruption(&mut self) -> usize { self.interruption.get().err().unwrap() } @@ -142,16 +143,14 @@ impl BufferStorage { /// /// # Panics /// - /// Panics if an interruption was not [armed]. - /// - /// [armed]: struct.BufferStorage.html#method.arm_interruption + /// Panics if an interruption was not [armed](BufferStorage::arm_interruption). pub fn reset_interruption(&mut self) { let _ = self.interruption.get(); } /// Corrupts an interrupted operation. /// - /// Applies the [corruption function] to the storage. Counters are updated accordingly: + /// Applies the corruption function to the storage. Counters are updated accordingly: /// - If a word is fully written, its counter is incremented regardless of whether other words /// of the same operation have been fully written. /// - If a page is fully erased, its counter is incremented (and its word counters are reset). @@ -159,13 +158,10 @@ impl BufferStorage { /// # Panics /// /// Panics if any of the following conditions hold: - /// - An interruption was not [armed]. + /// - An interruption was not [armed](BufferStorage::arm_interruption). /// - An interruption was armed but did not trigger. /// - The corruption function corrupts more bits than allowed. /// - The interrupted operation itself would have panicked. - /// - /// [armed]: struct.BufferStorage.html#method.arm_interruption - /// [corruption function]: type.BufferCorruptFunction.html pub fn corrupt_operation(&mut self, corrupt: BufferCorruptFunction) { let operation = self.interruption.get().unwrap(); let range = self.operation_range(&operation).unwrap(); @@ -217,7 +213,8 @@ impl BufferStorage { /// /// # Panics /// - /// Panics if the maximum number of erase cycles per page is reached. + /// Panics if the [maximum number of erase cycles per page](BufferOptions::max_page_erases) is + /// reached. fn incr_page_erases(&mut self, page: usize) { // Check that pages are not erased too many times. if self.options.strict_mode { @@ -243,7 +240,8 @@ impl BufferStorage { /// /// # Panics /// - /// Panics if the maximum number of writes per word is reached. + /// Panics if the [maximum number of writes per word](BufferOptions::max_word_writes) is + /// reached. fn incr_word_writes(&mut self, index: usize, value: &[u8], complete: &[u8]) { let word_size = self.word_size(); for i in 0..value.len() / word_size { diff --git a/libraries/persistent_store/src/driver.rs b/libraries/persistent_store/src/driver.rs index f17f576..d2baee0 100644 --- a/libraries/persistent_store/src/driver.rs +++ b/libraries/persistent_store/src/driver.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Store wrapper for testing. +//! +//! [`StoreDriver`] wraps a [`Store`] and compares its behavior with its associated [`StoreModel`]. + use crate::format::{Format, Position}; #[cfg(test)] use crate::StoreUpdate; diff --git a/libraries/persistent_store/src/format.rs b/libraries/persistent_store/src/format.rs index a70dcc4..a7dd4f5 100644 --- a/libraries/persistent_store/src/format.rs +++ b/libraries/persistent_store/src/format.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Storage representation of a store. + #[macro_use] mod bitfield; @@ -26,13 +28,14 @@ use core::convert::TryFrom; /// Internal representation of a word in flash. /// -/// Currently, the store only supports storages where a word is 32 bits. +/// Currently, the store only supports storages where a word is 32 bits, i.e. the [word +/// size](Storage::word_size) is 4 bytes. type WORD = u32; /// Abstract representation of a word in flash. /// -/// This type is kept abstract to avoid possible confusion with `Nat` if they happen to have the -/// same representation. This is because they have different semantics, `Nat` represents natural +/// This type is kept abstract to avoid possible confusion with [`Nat`] if they happen to have the +/// same representation. This is because they have different semantics, [`Nat`] represents natural /// numbers while `Word` represents sequences of bits (and thus has no arithmetic). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Word(WORD); @@ -47,7 +50,7 @@ impl Word { /// /// # Panics /// - /// Panics if `slice.len() != WORD_SIZE`. + /// Panics if `slice.len()` is not [`WORD_SIZE`] bytes. pub fn from_slice(slice: &[u8]) -> Word { Word(WORD::from_le_bytes(::try_from(slice).unwrap())) } @@ -60,47 +63,49 @@ impl Word { /// Size of a word in bytes. /// -/// Currently, the store only supports storages where a word is 4 bytes. +/// Currently, the store only supports storages where the [word size](Storage::word_size) is 4 +/// bytes. const WORD_SIZE: Nat = core::mem::size_of::() as Nat; /// Minimum number of words per page. /// -/// Currently, the store only supports storages where pages have at least 8 words. -const MIN_NUM_WORDS_PER_PAGE: Nat = 8; +/// Currently, the store only supports storages where pages have at least 8 [words](WORD_SIZE), i.e. +/// the [page size](Storage::page_size) is at least 32 bytes. +const MIN_PAGE_SIZE: Nat = 8; /// Maximum size of a page in bytes. /// -/// Currently, the store only supports storages where pages are between 8 and 1024 [words]. -/// -/// [words]: constant.WORD_SIZE.html +/// Currently, the store only supports storages where pages have at most 1024 [words](WORD_SIZE), +/// i.e. the [page size](Storage::page_size) is at most 4096 bytes. const MAX_PAGE_SIZE: Nat = 4096; /// Maximum number of erase cycles. /// -/// Currently, the store only supports storages where the maximum number of erase cycles fits on 16 -/// bits. +/// Currently, the store only supports storages where the [maximum number of erase +/// cycles](Storage::max_page_erases) fits in 16 bits, i.e. it is at most 65535. const MAX_ERASE_CYCLE: Nat = 65535; /// Minimum number of pages. /// -/// Currently, the store only supports storages with at least 3 pages. +/// Currently, the store only supports storages where the [number of pages](Storage::num_pages) is +/// at least 3. const MIN_NUM_PAGES: Nat = 3; /// Maximum page index. /// -/// Thus the maximum number of pages is one more than this number. Currently, the store only -/// supports storages where the number of pages is between 3 and 64. +/// Currently, the store only supports storages where the [number of pages](Storage::num_pages) is +/// at most 64, i.e. the maximum page index is 63. const MAX_PAGE_INDEX: Nat = 63; /// Maximum key index. /// -/// Thus the number of keys is one more than this number. Currently, the store only supports 4096 -/// keys. +/// Currently, the store only supports 4096 keys, i.e. the maximum key index is 4095. const MAX_KEY_INDEX: Nat = 4095; /// Maximum length in bytes of a user payload. /// -/// Currently, the store only supports values smaller than 1024 bytes. +/// Currently, the store only supports values at most 1023 bytes long. This may be further reduced +/// depending on the [page size](Storage::page_size), see [`Format::max_value_len`]. const MAX_VALUE_LEN: Nat = 1023; /// Maximum number of updates per transaction. @@ -109,9 +114,15 @@ const MAX_VALUE_LEN: Nat = 1023; const MAX_UPDATES: Nat = 31; /// Maximum number of words per virtual page. -const MAX_VIRT_PAGE_SIZE: Nat = div_ceil(MAX_PAGE_SIZE, WORD_SIZE) - CONTENT_WORD; +/// +/// A virtual page has [`CONTENT_WORD`] less [words](WORD_SIZE) than the storage [page +/// size](Storage::page_size). Those words are used to store the page header. Since a page has at +/// least [8](MIN_PAGE_SIZE) words, a virtual page has at least 6 words. +const MAX_VIRT_PAGE_SIZE: Nat = MAX_PAGE_SIZE / WORD_SIZE - CONTENT_WORD; /// Word with all bits set to one. +/// +/// After a page is erased, all words are equal to this value. const ERASED_WORD: Word = Word(!(0 as WORD)); /// Helpers for a given storage configuration. @@ -121,33 +132,31 @@ pub struct Format { /// /// # Invariant /// - /// - Words divide a page evenly. - /// - There are at least 8 words in a page. - /// - There are at most `MAX_PAGE_SIZE` bytes in a page. + /// - [Words](WORD_SIZE) divide a page evenly. + /// - There are at least [`MIN_PAGE_SIZE`] words in a page. + /// - There are at most [`MAX_PAGE_SIZE`] bytes in a page. page_size: Nat, /// The number of pages in the storage. /// /// # Invariant /// - /// - There are at least 3 pages. - /// - There are at most `MAX_PAGE_INDEX + 1` pages. + /// - There are at least [`MIN_NUM_PAGES`] pages. + /// - There are at most [`MAX_PAGE_INDEX`] + 1 pages. num_pages: Nat, /// The maximum number of times a page can be erased. /// /// # Invariant /// - /// - A page can be erased at most `MAX_ERASE_CYCLE` times. + /// - A page can be erased at most [`MAX_ERASE_CYCLE`] times. max_page_erases: Nat, } impl Format { /// Extracts the format from a storage. /// - /// Returns `None` if the storage is not [supported]. - /// - /// [supported]: struct.Format.html#method.is_storage_supported + /// Returns `None` if the storage is not [supported](Format::is_storage_supported). pub fn new(storage: &S) -> Option { if Format::is_storage_supported(storage) { Some(Format { @@ -163,21 +172,12 @@ impl Format { /// Returns whether a storage is supported. /// /// A storage is supported if the following conditions hold: - /// - The size of a word is [`WORD_SIZE`] bytes. - /// - The size of a word evenly divides the size of a page. - /// - A page contains at least [`MIN_NUM_WORDS_PER_PAGE`] words. - /// - A page contains at most [`MAX_PAGE_SIZE`] bytes. - /// - There are at least [`MIN_NUM_PAGES`] pages. - /// - There are at most [`MAX_PAGE_INDEX`]` + 1` pages. - /// - A word can be written at least twice between erase cycles. - /// - The maximum number of erase cycles is at most [`MAX_ERASE_CYCLE`]. - /// - /// [`WORD_SIZE`]: constant.WORD_SIZE.html - /// [`MIN_NUM_WORDS_PER_PAGE`]: constant.MIN_NUM_WORDS_PER_PAGE.html - /// [`MAX_PAGE_SIZE`]: constant.MAX_PAGE_SIZE.html - /// [`MIN_NUM_PAGES`]: constant.MIN_NUM_PAGES.html - /// [`MAX_PAGE_INDEX`]: constant.MAX_PAGE_INDEX.html - /// [`MAX_ERASE_CYCLE`]: constant.MAX_ERASE_CYCLE.html + /// - The [`Storage::word_size`] is [`WORD_SIZE`] bytes. + /// - The [`Storage::word_size`] evenly divides the [`Storage::page_size`]. + /// - The [`Storage::page_size`] is between [`MIN_PAGE_SIZE`] words and [`MAX_PAGE_SIZE`] bytes. + /// - The [`Storage::num_pages`] is between [`MIN_NUM_PAGES`] and [`MAX_PAGE_INDEX`] + 1. + /// - The [`Storage::max_word_writes`] is at least 2. + /// - The [`Storage::max_page_erases`] is at most [`MAX_ERASE_CYCLE`]. fn is_storage_supported(storage: &S) -> bool { let word_size = usize_to_nat(storage.word_size()); let page_size = usize_to_nat(storage.page_size()); @@ -186,7 +186,7 @@ impl Format { let max_page_erases = usize_to_nat(storage.max_page_erases()); word_size == WORD_SIZE && page_size % word_size == 0 - && (MIN_NUM_WORDS_PER_PAGE * word_size <= page_size && page_size <= MAX_PAGE_SIZE) + && (MIN_PAGE_SIZE * word_size <= page_size && page_size <= MAX_PAGE_SIZE) && (MIN_NUM_PAGES <= num_pages && num_pages <= MAX_PAGE_INDEX + 1) && max_word_writes >= 2 && max_page_erases <= MAX_ERASE_CYCLE @@ -199,28 +199,28 @@ impl Format { /// The size of a page in bytes. /// - /// We have `MIN_NUM_WORDS_PER_PAGE * self.word_size() <= self.page_size() <= MAX_PAGE_SIZE`. + /// This is at least [`MIN_PAGE_SIZE`] [words](WORD_SIZE) and at most [`MAX_PAGE_SIZE`] bytes. pub fn page_size(&self) -> Nat { self.page_size } - /// The number of pages in the storage, denoted by `N`. + /// The number of pages in the storage, denoted by N. /// - /// We have `MIN_NUM_PAGES <= N <= MAX_PAGE_INDEX + 1`. + /// We have [`MIN_NUM_PAGES`] ≤ N ≤ [`MAX_PAGE_INDEX`] + 1. pub fn num_pages(&self) -> Nat { self.num_pages } /// The maximum page index. /// - /// We have `2 <= self.max_page() <= MAX_PAGE_INDEX`. + /// This is at least [`MIN_NUM_PAGES`] - 1 and at most [`MAX_PAGE_INDEX`]. pub fn max_page(&self) -> Nat { self.num_pages - 1 } - /// The maximum number of times a page can be erased, denoted by `E`. + /// The maximum number of times a page can be erased, denoted by E. /// - /// We have `E <= MAX_ERASE_CYCLE`. + /// We have E ≤ [`MAX_ERASE_CYCLE`]. pub fn max_page_erases(&self) -> Nat { self.max_page_erases } @@ -235,19 +235,18 @@ impl Format { MAX_UPDATES } - /// The size of a virtual page in words, denoted by `Q`. + /// The size of a virtual page in words, denoted by Q. /// /// A virtual page is stored in a physical page after the page header. /// - /// We have `MIN_NUM_WORDS_PER_PAGE - 2 <= Q <= MAX_VIRT_PAGE_SIZE`. + /// We have [`MIN_PAGE_SIZE`] - 2 ≤ Q ≤ [`MAX_VIRT_PAGE_SIZE`]. pub fn virt_page_size(&self) -> Nat { self.page_size() / self.word_size() - CONTENT_WORD } /// The maximum length in bytes of a user payload. /// - /// We have `(MIN_NUM_WORDS_PER_PAGE - 3) * self.word_size() <= self.max_value_len() <= - /// MAX_VALUE_LEN`. + /// This is at least [`MIN_PAGE_SIZE`] - 3 [words](WORD_SIZE) and at most [`MAX_VALUE_LEN`]. pub fn max_value_len(&self) -> Nat { min( (self.virt_page_size() - 1) * self.word_size(), @@ -255,57 +254,50 @@ impl Format { ) } - /// The maximum prefix length in words, denoted by `M`. + /// The maximum prefix length in words, denoted by M. /// /// A prefix is the first words of a virtual page that belong to the last entry of the previous /// virtual page. This happens because entries may overlap up to 2 virtual pages. /// - /// We have `MIN_NUM_WORDS_PER_PAGE - 3 <= M < Q`. + /// We have [`MIN_PAGE_SIZE`] - 3 ≤ M < Q. pub fn max_prefix_len(&self) -> Nat { self.bytes_to_words(self.max_value_len()) } - /// The total virtual capacity in words, denoted by `V`. + /// The total virtual capacity in words, denoted by V. /// - /// We have `V = (N - 1) * (Q - 1) - M`. + /// We have V = (N - 1) × (Q - 1) - M. /// - /// We can show `V >= (N - 2) * (Q - 1)` with the following steps: - /// - `M <= Q - 1` from `M < Q` from [`M`] definition - /// - `-M >= -(Q - 1)` from above - /// - `V >= (N - 1) * (Q - 1) - (Q - 1)` from `V` definition - /// - /// [`M`]: struct.Format.html#method.max_prefix_len + /// We can show V ≥ (N - 2) × (Q - 1) with the following steps: + /// - M ≤ Q - 1 from M < Q from [M](Format::max_prefix_len)'s definition + /// - -M ≥ -(Q - 1) from above + /// - V ≥ (N - 1) × (Q - 1) - (Q - 1) from V's definition pub fn virt_size(&self) -> Nat { (self.num_pages() - 1) * (self.virt_page_size() - 1) - self.max_prefix_len() } - /// The total user capacity in words, denoted by `C`. + /// The total user capacity in words, denoted by C. /// - /// We have `C = V - N = (N - 1) * (Q - 2) - M - 1`. + /// We have C = V - N = (N - 1) × (Q - 2) - M - 1. /// - /// We can show `C >= (N - 2) * (Q - 2) - 2` with the following steps: - /// - `V >= (N - 2) * (Q - 1)` from [`V`] definition - /// - `C >= (N - 2) * (Q - 1) - N` from `C` definition - /// - `(N - 2) * (Q - 1) - N = (N - 2) * (Q - 2) - 2` by calculus - /// - /// [`V`]: struct.Format.html#method.virt_size + /// We can show C ≥ (N - 2) × (Q - 2) - 2 with the following steps: + /// - V ≥ (N - 2) × (Q - 1) from [V](Format::virt_size)'s definition + /// - C ≥ (N - 2) × (Q - 1) - N from C's definition + /// - (N - 2) × (Q - 1) - N = (N - 2) × (Q - 2) - 2 by calculus pub fn total_capacity(&self) -> Nat { // From the virtual capacity, we reserve N - 1 words for `Erase` entries and 1 word for a // `Clear` entry. self.virt_size() - self.num_pages() } - /// The total virtual lifetime in words, denoted by `L`. + /// The total virtual lifetime in words, denoted by L. /// - /// We have `L = (E * N + N - 1) * Q`. + /// We have L = (E × N + N - 1) × Q. pub fn total_lifetime(&self) -> Position { Position::new(self, self.max_page_erases(), self.num_pages() - 1, 0) } /// Returns the word position of the first entry of a page. - /// - /// The init info of the page must be provided to know where the first entry of the page - /// starts. pub fn page_head(&self, init: InitInfo, page: Nat) -> Position { Position::new(self, init.cycle, page, init.prefix) } @@ -557,7 +549,7 @@ impl Format { /// /// # Preconditions /// - /// - `bytes + self.word_size()` does not overflow. + /// - `bytes` + [`Self::word_size`] does not overflow. pub fn bytes_to_words(&self, bytes: Nat) -> Nat { div_ceil(bytes, self.word_size()) } @@ -571,7 +563,7 @@ const COMPACT_WORD: Nat = 1; /// The word index of the content of a page. /// -/// Since a page is at least 8 words, there is always at least 6 words of content. +/// This is also the length in words of the page header. const CONTENT_WORD: Nat = 2; /// The checksum for a single word. @@ -718,21 +710,21 @@ bitfield! { /// The position of a word in the virtual storage. /// -/// With the notations defined in `Format`, let: -/// - `w` a virtual word offset in a page which is between `0` and `Q - 1` -/// - `p` a page offset which is between `0` and `N - 1` -/// - `c` the number of erase cycles of a page which is between `0` and `E` +/// With the notations defined in [`Format`], let: +/// - w denote a word offset in a virtual page, thus between 0 and Q - 1 +/// - p denote a page offset, thus between 0 and N - 1 +/// - c denote the number of times a page was erased, thus between 0 and E /// -/// Then the position of a word is `(c*N + p)*Q + w`. This position monotonically increases and +/// The position of a word is (c × N + p) × Q + w. This position monotonically increases and /// represents the consumed lifetime of the storage. /// -/// This type is kept abstract to avoid possible confusion with `Nat` and `Word` if they happen to -/// have the same representation. Here is an overview of their semantics: +/// This type is kept abstract to avoid possible confusion with [`Nat`] and [`Word`] if they happen +/// to have the same representation. Here is an overview of their semantics: /// /// | Name | Semantics | Arithmetic operations | Bit-wise operations | /// | ---------- | --------------------------- | --------------------- | ------------------- | -/// | `Nat` | Natural numbers | Yes (no overflow) | No | -/// | `Word` | Word in flash | No | Yes | +/// | [`Nat`] | Natural numbers | Yes (no overflow) | No | +/// | [`Word`] | Word in flash | No | Yes | /// | `Position` | Position in virtual storage | Yes (no overflow) | No | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Position(Nat); @@ -763,9 +755,9 @@ impl Position { /// Create a word position given its coordinates. /// /// The coordinates of a word are: - /// - Its word index in its page. + /// - Its word index in its virtual page. /// - Its page index in the storage. - /// - The number of times that page was erased. + /// - The number of times its page was erased. pub fn new(format: &Format, cycle: Nat, page: Nat, word: Nat) -> Position { Position((cycle * format.num_pages() + page) * format.virt_page_size() + word) } @@ -928,11 +920,11 @@ pub fn is_erased(slice: &[u8]) -> bool { /// Divides then takes ceiling. /// -/// Returns `ceil(x / m)` in mathematical notations (not Rust code). +/// Returns ⌈x / m⌉, i.e. the lowest natural number r such that r ≥ x / m. /// /// # Preconditions /// -/// - `x + m` does not overflow. +/// - x + m does not overflow. const fn div_ceil(x: Nat, m: Nat) -> Nat { (x + m - 1) / m } diff --git a/libraries/persistent_store/src/fragment.rs b/libraries/persistent_store/src/fragment.rs index d19b511..661d5db 100644 --- a/libraries/persistent_store/src/fragment.rs +++ b/libraries/persistent_store/src/fragment.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Helper functions for fragmented entries. +//! Support 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. @@ -36,7 +36,7 @@ pub trait Keys { /// /// # Preconditions /// - /// The position must be within the length: `pos < len()`. + /// The position must be within the length: `pos` < [`Self::len`]. fn key(&self, pos: usize) -> usize; } diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index a8adc2b..e3735c3 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -12,191 +12,191 @@ // See the License for the specific language governing permissions and // limitations under the License. -// TODO(ia0): Add links once the code is complete. +// The documentation is easier to read from a browser: +// - Run: cargo doc --document-private-items --features=std +// - Open: target/doc/persistent_store/index.html + //! Store abstraction for flash storage //! //! # Specification //! -//! The store provides a partial function from keys to values on top of a storage -//! interface. The store total capacity depends on the size of the storage. Store -//! updates may be bundled in transactions. Mutable operations are atomic, including -//! when interrupted. +//! The [store](Store) provides a partial function from keys to values on top of a +//! [storage](Storage) interface. The store total [capacity](Store::capacity) depends on the size of +//! the storage. Store [updates](StoreUpdate) may be bundled in [transactions](Store::transaction). +//! Mutable operations are atomic, including when interrupted. //! -//! The store is flash-efficient in the sense that it uses the storage lifetime -//! efficiently. For each page, all words are written at least once between erase -//! cycles and all erase cycles are used. However, not all written words are user -//! content: lifetime is also consumed with metadata and compaction. +//! The store is flash-efficient in the sense that it uses the storage [lifetime](Store::lifetime) +//! efficiently. For each page, all words are written at least once between erase cycles and all +//! erase cycles are used. However, not all written words are user content: lifetime is also +//! consumed with metadata and compaction. //! -//! The store is extendable with other entries than key-values. It is essentially a -//! framework providing access to the storage lifetime. The partial function is -//! simply the most common usage and can be used to encode other usages. +//! The store is extendable with other entries than key-values. It is essentially a framework +//! providing access to the storage lifetime. The partial function is simply the most common usage +//! and can be used to encode other usages. //! //! ## Definitions //! -//! An _entry_ is a pair of a key and a value. A _key_ is a number between 0 -//! and 4095. A _value_ is a byte slice with a length between 0 and 1023 bytes (for -//! large enough pages). +//! An _entry_ is a pair of a key and a value. A _key_ is a number between 0 and +//! [4095](format::MAX_KEY_INDEX). A _value_ is a byte slice with a length between 0 and +//! [1023](format::Format::max_value_len) bytes (for large enough pages). //! //! The store provides the following _updates_: -//! - Given a key and a value, `Insert` updates the store such that the value is +//! - Given a key and a value, [`StoreUpdate::Insert`] updates the store such that the value is //! associated with the key. The values for other keys are left unchanged. -//! - Given a key, `Remove` updates the store such that no value is associated with -//! the key. The values for other keys are left unchanged. Additionally, if there -//! was a value associated with the key, the value is wiped from the storage -//! (all its bits are set to 0). +//! - Given a key, [`StoreUpdate::Remove`] updates the store such that no value is associated with +//! the key. The values for other keys are left unchanged. Additionally, if there was a value +//! associated with the key, the value is wiped from the storage (all its bits are set to 0). //! //! The store provides the following _read-only operations_: -//! - `Iter` iterates through the store returning all entries exactly once. The -//! iteration order is not specified but stable between mutable operations. -//! - `Capacity` returns how many words can be stored before the store is full. -//! - `Lifetime` returns how many words can be written before the storage lifetime -//! is consumed. +//! - [`Store::iter`] iterates through the store returning all entries exactly once. The iteration +//! order is not specified but stable between mutable operations. +//! - [`Store::capacity`] returns how many words can be stored before the store is full. +//! - [`Store::lifetime`] returns how many words can be written before the storage lifetime is +//! consumed. //! //! The store provides the following _mutable operations_: -//! - Given a set of independent updates, `Transaction` applies the sequence of -//! updates. -//! - Given a threshold, `Clear` removes all entries with a key greater or equal -//! to the threshold. -//! - Given a length in words, `Prepare` makes one step of compaction unless that -//! many words can be written without compaction. This operation has no effect -//! on the store but may still mutate its storage. In particular, the store has -//! the same capacity but a possibly reduced lifetime. +//! - Given a set of independent updates, [`Store::transaction`] applies the sequence of updates. +//! - Given a threshold, [`Store::clear`] removes all entries with a key greater or equal to the +//! threshold. +//! - Given a length in words, [`Store::prepare`] makes one step of compaction unless that many +//! words can be written without compaction. This operation has no effect on the store but may +//! still mutate its storage. In particular, the store has the same capacity but a possibly +//! reduced lifetime. //! -//! A mutable operation is _atomic_ if, when power is lost during the operation, the -//! store is either updated (as if the operation succeeded) or left unchanged (as if -//! the operation did not occur). If the store is left unchanged, lifetime may still -//! be consumed. +//! A mutable operation is _atomic_ if, when power is lost during the operation, the store is either +//! updated (as if the operation succeeded) or left unchanged (as if the operation did not occur). +//! If the store is left unchanged, lifetime may still be consumed. //! //! The store relies on the following _storage interface_: -//! - It is possible to read a byte slice. The slice won't span multiple pages. -//! - It is possible to write a word slice. The slice won't span multiple pages. -//! - It is possible to erase a page. -//! - The pages are sequentially indexed from 0. If the actual underlying storage -//! is segmented, then the storage layer should translate those indices to -//! actual page addresses. +//! - It is possible to [read](Storage::read_slice) a byte slice. The slice won't span multiple +//! pages. +//! - It is possible to [write](Storage::write_slice) a word slice. The slice won't span multiple +//! pages. +//! - It is possible to [erase](Storage::erase_page) a page. +//! - The pages are sequentially indexed from 0. If the actual underlying storage is segmented, +//! then the storage layer should translate those indices to actual page addresses. //! -//! The store has a _total capacity_ of `C = (N - 1) * (P - 4) - M - 1` words, where -//! `P` is the number of words per page, `N` is the number of pages, and `M` is the -//! maximum length in words of a value (256 for large enough pages). The capacity -//! used by each mutable operation is given below (a transient word only uses -//! capacity during the operation): -//! - `Insert` uses `1 + ceil(len / 4)` words where `len` is the length of the -//! value in bytes. If an entry was replaced, the words used by its insertion -//! are freed. -//! - `Remove` doesn't use capacity if alone in the transaction and 1 transient -//! word otherwise. If an entry was deleted, the words used by its insertion are -//! freed. -//! - `Transaction` uses 1 transient word. In addition, the updates of the -//! transaction use and free words as described above. -//! - `Clear` doesn't use capacity and frees the words used by the insertion of -//! the deleted entries. -//! - `Prepare` doesn't use capacity. +//! The store has a _total capacity_ of C = (N - 1) × (P - 4) - M - 1 words, where: +//! - P is the number of words per page +//! - [N](format::Format::num_pages) is the number of pages +//! - [M](format::Format::max_prefix_len) is the maximum length in words of a value (256 for large +//! enough pages) //! -//! The _total lifetime_ of the store is below `L = ((E + 1) * N - 1) * (P - 2)` and -//! above `L - M` words, where `E` is the maximum number of erase cycles. The -//! lifetime is used when capacity is used, including transiently, as well as when -//! compaction occurs. Compaction frequency and lifetime consumption are positively -//! correlated to the store load factor (the ratio of used capacity to total capacity). +//! The capacity used by each mutable operation is given below (a transient word only uses capacity +//! during the operation): //! -//! It is possible to approximate the cost of transient words in terms of capacity: -//! `L` transient words are equivalent to `C - x` words of capacity where `x` is the -//! average capacity (including transient) of operations. +//! | Operation/Update | Used capacity | Freed capacity | Transient capacity | +//! | ----------------------- | ---------------- | ----------------- | ------------------ | +//! | [`StoreUpdate::Insert`] | 1 + value length | overwritten entry | 0 | +//! | [`StoreUpdate::Remove`] | 0 | deleted entry | see below\* | +//! | [`Store::transaction`] | 0 + updates | 0 + updates | 1 | +//! | [`Store::clear`] | 0 | deleted entries | 0 | +//! | [`Store::prepare`] | 0 | 0 | 0 | +//! +//! \*0 if the update is alone in the transaction, otherwise 1. +//! +//! The _total lifetime_ of the store is below L = ((E + 1) × N - 1) × (P - 2) and above L - M +//! words, where E is the maximum number of erase cycles. The lifetime is used when capacity is +//! used, including transiently, as well as when compaction occurs. Compaction frequency and +//! lifetime consumption are positively correlated to the store load factor (the ratio of used +//! capacity to total capacity). +//! +//! It is possible to approximate the cost of transient words in terms of capacity: L transient +//! words are equivalent to C - x words of capacity where x is the average capacity (including +//! transient) of operations. //! //! ## Preconditions //! //! The following assumptions need to hold, or the store may behave in unexpected ways: -//! - A word can be written twice between erase cycles. -//! - A page can be erased `E` times after the first boot of the store. -//! - When power is lost while writing a slice or erasing a page, the next read -//! returns a slice where a subset (possibly none or all) of the bits that -//! should have been modified have been modified. -//! - Reading a slice is deterministic. When power is lost while writing a slice -//! or erasing a slice (erasing a page containing that slice), reading that -//! slice repeatedly returns the same result (until it is overwritten 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. -//! - When power is lost while writing a slice or erasing a page, that operation -//! does not count towards the limits. However, completing that write or erase -//! operation would count towards the limits, as if the number of writes per -//! word and number of erase cycles could be fractional. -//! - The storage is only modified by the store. Note that completely erasing the -//! storage is supported, essentially losing all content and lifetime tracking. -//! It is preferred to use `Clear` with a threshold of 0 to keep the lifetime -//! tracking. +//! - A word can be written [twice](Storage::max_word_writes) between erase cycles. +//! - A page can be erased [E](Storage::max_page_erases) times after the first boot of the store. +//! - When power is lost while writing a slice or erasing a page, the next read returns a slice +//! where a subset (possibly none or all) of the bits that should have been modified have been +//! modified. +//! - Reading a slice is deterministic. When power is lost while writing a slice or erasing a +//! slice (erasing a page containing that slice), reading that slice repeatedly returns the same +//! result (until it is overwritten 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. +//! - When power is lost while writing a slice or erasing a page, that operation does not count +//! towards the limits. However, completing that write or erase operation would count towards +//! the limits, as if the number of writes per word and number of erase cycles could be +//! fractional. +//! - The storage is only modified by the store. Note that completely erasing the storage is +//! supported, essentially losing all content and lifetime tracking. It is preferred to use +//! [`Store::clear`] with a threshold of 0 to keep the lifetime tracking. //! -//! The store properties may still hold outside some of those assumptions, but with -//! an increasing chance of failure. +//! The store properties may still hold outside some of those assumptions, but with an increasing +//! chance of failure. //! //! # Implementation //! //! We define the following constants: -//! - `E < 65536` the number of times a page can be erased. -//! - `3 <= N < 64` the number of pages in the storage. -//! - `8 <= P <= 1024` the number of words in a page. -//! - `Q = P - 2` the number of words in a virtual page. -//! - `K = 4096` the maximum number of keys. -//! - `M = min(Q - 1, 256)` the maximum length in words of a value. -//! - `V = (N - 1) * (Q - 1) - M` the virtual capacity. -//! - `C = V - N` the user capacity. +//! - [E](format::Format::max_page_erases) ≤ [65535](format::MAX_ERASE_CYCLE) the number of times +//! a page can be erased. +//! - 3 ≤ [N](format::Format::num_pages) < 64 the number of pages in the storage. +//! - 8 ≤ P ≤ 1024 the number of words in a page. +//! - [Q](format::Format::virt_page_size) = P - 2 the number of words in a virtual page. +//! - [M](format::Format::max_prefix_len) = min(Q - 1, 256) the maximum length in words of a +//! value. +//! - [V](format::Format::virt_size) = (N - 1) × (Q - 1) - M the virtual capacity. +//! - [C](format::Format::total_capacity) = V - N the user capacity. //! -//! We build a virtual storage from the physical storage using the first 2 words of -//! each page: +//! We build a virtual storage from the physical storage using the first 2 words of each page: //! - The first word contains the number of times the page has been erased. -//! - The second word contains the starting word to which this page is being moved -//! during compaction. +//! - The second word contains the starting word to which this page is being moved during +//! compaction. //! -//! The virtual storage has a length of `(E + 1) * N * Q` words and represents the -//! lifetime of the store. (We reserve the last `Q + M` words to support adding -//! emergency lifetime.) This virtual storage has a linear address space. +//! The virtual storage has a length of (E + 1) × N × Q words and represents the lifetime of the +//! store. (We reserve the last Q + M words to support adding emergency lifetime.) This virtual +//! storage has a linear address space. //! -//! We define a set of overlapping windows of `N * Q` words at each `Q`-aligned -//! boundary. We call `i` the window spanning from `i * Q` to `(i + N) * Q`. Only -//! those windows actually exist in the underlying storage. We use compaction to -//! shift the current window from `i` to `i + 1`, preserving the content of the -//! store. +//! We define a set of overlapping windows of N × Q words at each Q-aligned boundary. We call i the +//! window spanning from i × Q to (i + N) × Q. Only those windows actually exist in the underlying +//! storage. We use compaction to shift the current window from i to i + 1, preserving the content +//! of the store. //! -//! For a given state of the virtual storage, we define `h_i` as the position of the -//! first entry of the window `i`. We call it the head of the window `i`. Because -//! entries are at most `M + 1` words, they can overlap on the next page only by `M` -//! words. So we have `i * Q <= h_i <= i * Q + M` . Since there are no entries -//! before the first page, we have `h_0 = 0`. +//! For a given state of the virtual storage, we define h\_i as the position of the first entry of +//! the window i. We call it the head of the window i. Because entries are at most M + 1 words, they +//! can overlap on the next page only by M words. So we have i × Q ≤ h_i ≤ i × Q + M . Since there +//! are no entries before the first page, we have h\_0 = 0. //! -//! We define `t_i` as one past the last entry of the window `i`. If there are no -//! entries in that window, we have `t_i = h_i`. We call `t_i` the tail of the -//! window `i`. We define the compaction invariant as `t_i - h_i <= V`. +//! We define t\_i as one past the last entry of the window i. If there are no entries in that +//! window, we have t\_i = h\_i. We call t\_i the tail of the window i. We define the compaction +//! invariant as t\_i - h\_i ≤ V. //! -//! We define `|x|` as the capacity used before position `x`. We have `|x| <= x`. We -//! define the capacity invariant as `|t_i| - |h_i| <= C`. +//! We define |x| as the capacity used before position x. We have |x| ≤ x. We define the capacity +//! invariant as |t\_i| - |h\_i| ≤ C. //! -//! Using this virtual storage, entries are appended to the tail as long as there is -//! both virtual capacity to preserve the compaction invariant and capacity to -//! preserve the capacity invariant. When virtual capacity runs out, the first page -//! of the window is compacted and the window is shifted. +//! Using this virtual storage, entries are appended to the tail as long as there is both virtual +//! capacity to preserve the compaction invariant and capacity to preserve the capacity invariant. +//! When virtual capacity runs out, the first page of the window is compacted and the window is +//! shifted. //! -//! Entries are identified by a prefix of bits. The prefix has to contain at least -//! one bit set to zero to differentiate from the tail. Entries can be one of: -//! - Padding: A word whose first bit is set to zero. The rest is arbitrary. This -//! entry is used to mark words partially written after an interrupted operation -//! as padding such that they are ignored by future operations. -//! - Header: A word whose second bit is set to zero. It contains the following fields: -//! - A bit indicating whether the entry is deleted. -//! - A bit indicating whether the value is word-aligned and has all bits set -//! to 1 in its last word. The last word of an entry is used to detect that -//! an entry has been fully written. As such it must contain at least one -//! bit equal to zero. -//! - The key of the entry. -//! - The length in bytes of the value. The value follows the header. The -//! entry is word-aligned if the value is not. -//! - The checksum of the first and last word of the entry. -//! - Erase: A word used during compaction. It contains the page to be erased and -//! a checksum. -//! - Clear: A word used during the `Clear` operation. It contains the threshold -//! and a checksum. -//! - Marker: A word used during the `Transaction` operation. It contains the -//! number of updates following the marker and a checksum. -//! - Remove: A word used during the `Transaction` operation. It contains the key -//! of the entry to be removed and a checksum. +//! Entries are identified by a prefix of bits. The prefix has to contain at least one bit set to +//! zero to differentiate from the tail. Entries can be one of: +//! - [Padding](format::ID_PADDING): A word whose first bit is set to zero. The rest is arbitrary. +//! This entry is used to mark words partially written after an interrupted operation as padding +//! such that they are ignored by future operations. +//! - [Header](format::ID_HEADER): A word whose second bit is set to zero. It contains the +//! following fields: +//! - A [bit](format::HEADER_DELETED) indicating whether the entry is deleted. +//! - A [bit](format::HEADER_FLIPPED) indicating whether the value is word-aligned and has all +//! bits set to 1 in its last word. The last word of an entry is used to detect that an +//! entry has been fully written. As such it must contain at least one bit equal to zero. +//! - The [key](format::HEADER_KEY) of the entry. +//! - The [length](format::HEADER_LENGTH) in bytes of the value. The value follows the header. +//! The entry is word-aligned if the value is not. +//! - The [checksum](format::HEADER_CHECKSUM) of the first and last word of the entry. +//! - [Erase](format::ID_ERASE): A word used during compaction. It contains the +//! [page](format::ERASE_PAGE) to be erased and a [checksum](format::WORD_CHECKSUM). +//! - [Clear](format::ID_CLEAR): A word used during the clear operation. It contains the +//! [threshold](format::CLEAR_MIN_KEY) and a [checksum](format::WORD_CHECKSUM). +//! - [Marker](format::ID_MARKER): A word used during a transaction. It contains the [number of +//! updates](format::MARKER_COUNT) following the marker and a [checksum](format::WORD_CHECKSUM). +//! - [Remove](format::ID_REMOVE): A word used inside a transaction. It contains the +//! [key](format::REMOVE_KEY) of the entry to be removed and a +//! [checksum](format::WORD_CHECKSUM). //! //! Checksums are the number of bits equal to 0. //! @@ -204,107 +204,105 @@ //! //! ## Compaction //! -//! It should always be possible to fully compact the store, after what the -//! remaining capacity should be available in the current window (restoring the -//! compaction invariant). We consider all notations on the virtual storage after -//! the full compaction. We will use the `|x|` notation although we update the state -//! of the virtual storage. This is fine because compaction doesn't change the -//! status of an existing word. +//! It should always be possible to fully compact the store, after what the remaining capacity +//! should be available in the current window (restoring the compaction invariant). We consider all +//! notations on the virtual storage after the full compaction. We will use the |x| notation +//! although we update the state of the virtual storage. This is fine because compaction doesn't +//! change the status of an existing word. //! -//! We want to show that the next `N - 1` compactions won't move the tail past the -//! last page of their window, with `I` the initial window: +//! We want to show that the next N - 1 compactions won't move the tail past the last page of their +//! window, with I the initial window: //! -//! ```text -//! forall 1 <= i <= N - 1, t_{I + i} <= (I + i + N - 1) * Q -//! ``` +//! | | | | | +//! | ----------------:| ----------:|:-:|:------------------- | +//! | ∀(1 ≤ i ≤ N - 1) | t\_{I + i} | ≤ | (I + i + N - 1) × Q | //! -//! We assume `i` between `1` and `N - 1`. +//! We assume i between 1 and N - 1. //! -//! One step of compaction advances the tail by how many words were used in the -//! first page of the window with the last entry possibly overlapping on the next -//! page. +//! One step of compaction advances the tail by how many words were used in the first page of the +//! window with the last entry possibly overlapping on the next page. //! -//! ```text -//! forall j, t_{j + 1} = t_j + |h_{j + 1}| - |h_j| + 1 -//! ``` +//! | | | | | +//! | --:| ----------:|:-:|:------------------------------------ | +//! | ∀j | t\_{j + 1} | = | t\_j + \|h\_{j + 1}\| - \|h\_j\| + 1 | //! //! By induction, we have: //! -//! ```text -//! t_{I + i} <= t_I + |h_{I + i}| - |h_I| + i -//! ``` +//! | | | | +//! | ----------:|:-:|:------------------------------------ | +//! | t\_{I + i} | ≤ | t\_I + \|h\_{I + i}\| - \|h\_I\| + i | //! //! We have the following properties: //! -//! ```text -//! t_I <= h_I + V -//! |h_{I + i}| - |h_I| <= h_{I + i} - h_I -//! h_{I + i} <= (I + i) * Q + M -//! ``` +//! | | | | +//! | -------------------------:|:-:|:----------------- | +//! | t\_I | ≤ | h\_I + V | +//! | \|h\_{I + i}\| - \|h\_I\| | ≤ | h\_{I + i} - h\_I | +//! | h\_{I + i} | ≤ | (I + i) × Q + M | //! //! Replacing into our previous equality, we can conclude: //! -//! ```text -//! t_{I + i} = t_I + |h_{I + i}| - |h_I| + i -//! <= h_I + V + (I + i) * Q + M - h_I + i -//! = (N - 1) * (Q - 1) - M + (I + i) * Q + M + i -//! = (N - 1) * (Q - 1) + (I + i) * Q + i -//! = (I + i + N - 1) * Q + i - (N - 1) -//! <= (I + i + N - 1) * Q -//! ``` +//! | | | | +//! | ----------:|:-:| ------------------------------------------- | +//! | t\_{I + i} | = | t_I + \|h_{I + i}\| - \|h_I\| + i | +//! | | ≤ | h\_I + V + (I + i) * Q + M - h\_I + i | +//! | | = | (N - 1) × (Q - 1) - M + (I + i) × Q + M + i | +//! | | = | (N - 1) × (Q - 1) + (I + i) × Q + i | +//! | | = | (I + i + N - 1) × Q + i - (N - 1) | +//! | | ≤ | (I + i + N - 1) × Q | //! -//! We also want to show that after `N - 1` compactions, the remaining capacity is -//! available without compaction. +//! We also want to show that after N - 1 compactions, the remaining capacity is available without +//! compaction. //! -//! ```text -//! V - (t_{I + N - 1} - h_{I + N - 1}) >= // The available words in the window. -//! C - (|t_{I + N - 1}| - |h_{I + N - 1}|) // The remaining capacity. -//! + 1 // Reserved for Clear. -//! ``` +//! | | | | +//! | -:| --------------------------------------------- | --------------------------------- | +//! | | V - (t\_{I + N - 1} - h\_{I + N - 1}) | The available words in the window | +//! | ≥ | C - (\|t\_{I + N - 1}\| - \|h\_{I + N - 1}\|) | The remaining capacity | +//! | + | 1 | Reserved for clear | //! -//! We can replace the definition of `C` and simplify: +//! We can replace the definition of C and simplify: //! -//! ```text -//! V - (t_{I + N - 1} - h_{I + N - 1}) >= V - N - (|t_{I + N - 1}| - |h_{I + N - 1}|) + 1 -//! iff t_{I + N - 1} - h_{I + N - 1} <= |t_{I + N - 1}| - |h_{I + N - 1}| + N - 1 -//! ``` +//! | | | | | +//! | ---:| -------------------------------------:|:-:|:----------------------------------------------------- | +//! | | V - (t\_{I + N - 1} - h\_{I + N - 1}) | ≥ | V - N - (\|t\_{I + N - 1}\| - \|h\_{I + N - 1}\|) + 1 | +//! | iff | t\_{I + N - 1} - h\_{I + N - 1} | ≤ | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| + N - 1 | //! //! We have the following properties: //! -//! ```text -//! t_{I + N - 1} = t_I + |h_{I + N - 1}| - |h_I| + N - 1 -//! |t_{I + N - 1}| - |h_{I + N - 1}| = |t_I| - |h_I| // Compaction preserves capacity. -//! |h_{I + N - 1}| - |t_I| <= h_{I + N - 1} - t_I -//! ``` +//! +//! | | | | | +//! | ---------------------------------------:|:-:|:-------------------------------------------- |:------ | +//! | t\_{I + N - 1} | = | t\_I + \|h\_{I + N - 1}\| - \|h\_I\| + N - 1 | | +//! | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| | = | \|t\_I\| - \|h\_I\| | Compaction preserves capacity | +//! | \|h\_{I + N - 1}\| - \|t\_I\| | ≤ | h\_{I + N - 1} - t\_I | | //! //! From which we conclude: //! -//! ```text -//! t_{I + N - 1} - h_{I + N - 1} <= |t_{I + N - 1}| - |h_{I + N - 1}| + N - 1 -//! iff t_I + |h_{I + N - 1}| - |h_I| + N - 1 - h_{I + N - 1} <= |t_I| - |h_I| + N - 1 -//! iff t_I + |h_{I + N - 1}| - h_{I + N - 1} <= |t_I| -//! iff |h_{I + N - 1}| - |t_I| <= h_{I + N - 1} - t_I -//! ``` +//! | | | | | +//! | ---:| -------------------------------:|:-:|:----------------------------------------------- | +//! | | t\_{I + N - 1} - h\_{I + N - 1} | ≤ | \|t\_{I + N - 1}\| - \|h\_{I + N - 1}\| + N - 1 | +//! | iff | t\_I + \|h\_{I + N - 1}\| - \|h\_I\| + N - 1 - h\_{I + N - 1} | ≤ | \|t\_I\| - \|h\_I\| + N - 1 | +//! | iff | t\_I + \|h\_{I + N - 1}\| - h\_{I + N - 1} | ≤ | \|t\_I\| | +//! | iff | \|h\_{I + N - 1}\| - \|t\_I\| | ≤ | h\_{I + N - 1} - t\_I | //! //! //! ## Checksum //! -//! The main property we want is that all partially written/erased words are either -//! the initial word, the final word, or invalid. +//! The main property we want is that all partially written/erased words are either the initial +//! word, the final word, or invalid. //! -//! We say that a bit sequence `TARGET` is reachable from a bit sequence `SOURCE` if -//! both have the same length and `SOURCE & TARGET == TARGET` where `&` is the -//! bitwise AND operation on bit sequences of that length. In other words, when -//! `SOURCE` has a bit equal to 0 then `TARGET` also has that bit equal to 0. +//! We say that a bit sequence `TARGET` is reachable from a bit sequence `SOURCE` if both have the +//! same length and `SOURCE & TARGET == TARGET` where `&` is the bitwise AND operation on bit +//! sequences of that length. In other words, when `SOURCE` has a bit equal to 0 then `TARGET` also +//! has that bit equal to 0. //! -//! The only written entries start with `101` or `110` and are written from an -//! erased word. Marking an entry as padding or deleted is a single bit operation, -//! so the property trivially holds. For those cases, the proof relies on the fact -//! that there is exactly one bit equal to 0 in the 3 first bits. Either the 3 first -//! bits are still `111` in which case we expect the remaining bits to be equal -//! to 1. Otherwise we can use the checksum of the given type of entry because those -//! 2 types of entries are not reachable from each other. Here is a visualization of -//! the partitioning based on the first 3 bits: +//! The only written entries start with `101` or `110` and are written from an erased word. Marking +//! an entry as padding or deleted is a single bit operation, so the property trivially holds. For +//! those cases, the proof relies on the fact that there is exactly one bit equal to 0 in the 3 +//! first bits. Either the 3 first bits are still `111` in which case we expect the remaining bits +//! to be equal to 1. Otherwise we can use the checksum of the given type of entry because those 2 +//! types of entries are not reachable from each other. Here is a visualization of the partitioning +//! based on the first 3 bits: //! //! | First 3 bits | Description | How to check | //! | ------------:| ------------------ | ---------------------------- | @@ -314,34 +312,27 @@ //! | `100` | Deleted user entry | No check, atomically written | //! | `0??` | Padding entry | No check, atomically written | //! -//! To show that valid entries of a given type are not reachable from each other, we -//! show 3 lemmas: +//! To show that valid entries of a given type are not reachable from each other, we show 3 lemmas: //! -//! 1. A bit sequence is not reachable from another if its number of bits equal to -//! 0 is smaller. +//! 1. A bit sequence is not reachable from another if its number of bits equal to 0 is smaller. +//! 2. A bit sequence is not reachable from another if they have the same number of bits equals to +//! 0 and are different. +//! 3. A bit sequence is not reachable from another if it is bigger when they are interpreted as +//! numbers in binary representation. //! -//! 2. A bit sequence is not reachable from another if they have the same number of -//! bits equals to 0 and are different. -//! -//! 3. A bit sequence is not reachable from another if it is bigger when they are -//! interpreted as numbers in binary representation. -//! -//! From those lemmas we consider the 2 cases. If both entries have the same number -//! of bits equal to 0, they are either equal or not reachable from each other -//! because of the second lemma. If they don't have the same number of bits equal to -//! 0, then the one with less bits equal to 0 is not reachable from the other -//! because of the first lemma and the one with more bits equal to 0 is not -//! reachable from the other because of the third lemma and the definition of the -//! checksum. +//! From those lemmas we consider the 2 cases. If both entries have the same number of bits equal to +//! 0, they are either equal or not reachable from each other because of the second lemma. If they +//! don't have the same number of bits equal to 0, then the one with less bits equal to 0 is not +//! reachable from the other because of the first lemma and the one with more bits equal to 0 is not +//! reachable from the other because of the third lemma and the definition of the checksum. //! //! # Fuzzing //! -//! For any sequence of operations and interruptions starting from an erased -//! storage, the store is checked against its model and some internal invariant at -//! each step. +//! For any sequence of operations and interruptions starting from an erased storage, the store is +//! checked against its model and some internal invariant at each step. //! -//! For any sequence of operations and interruptions starting from an arbitrary -//! storage, the store is checked not to crash. +//! For any sequence of operations and interruptions starting from an arbitrary storage, the store +//! is checked not to crash. #![cfg_attr(not(feature = "std"), no_std)] #![feature(try_trait)] diff --git a/libraries/persistent_store/src/model.rs b/libraries/persistent_store/src/model.rs index eebc329..e0bf9e3 100644 --- a/libraries/persistent_store/src/model.rs +++ b/libraries/persistent_store/src/model.rs @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Store specification. + use crate::format::Format; use crate::{usize_to_nat, StoreError, StoreRatio, StoreResult, StoreUpdate}; use std::collections::HashMap; /// Models the mutable operations of a store. /// -/// The model doesn't model the storage and read-only operations. This is done by the driver. +/// The model doesn't model the storage and read-only operations. This is done by the +/// [driver](crate::StoreDriver). #[derive(Clone, Debug)] pub struct StoreModel { /// Represents the content of the store. diff --git a/libraries/persistent_store/src/storage.rs b/libraries/persistent_store/src/storage.rs index becd900..fb5b0cb 100644 --- a/libraries/persistent_store/src/storage.rs +++ b/libraries/persistent_store/src/storage.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Flash storage abstraction. + /// Represents a byte position in a storage. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct StorageIndex { @@ -65,12 +67,14 @@ pub trait Storage { /// The following pre-conditions must hold: /// - The `index` must designate `value.len()` bytes in the storage. /// - Both `index` and `value.len()` must be word-aligned. - /// - The written words should not have been written too many times since last page erasure. + /// - The written words should not have been written [too many](Self::max_word_writes) times + /// since the last page erasure. fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> StorageResult<()>; /// Erases a page of the storage. /// - /// The `page` must be in the storage. + /// The `page` must be in the storage, i.e. less than [`Storage::num_pages`]. And the page + /// should not have been erased [too many](Self::max_page_erases) times. fn erase_page(&mut self, page: usize) -> StorageResult<()>; } diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index f19f463..c143c89 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//! Store implementation. + use crate::format::{ is_erased, CompactInfo, Format, Header, InitInfo, InternalEntry, Padding, ParsedWord, Position, Word, WordState, @@ -55,17 +57,14 @@ pub enum StoreError { /// /// The consequences depend on the storage failure. In particular, the operation may or may not /// have succeeded, and the storage may have become invalid. Before doing any other operation, - /// the store should be [recovered]. The operation may then be retried if idempotent. - /// - /// [recovered]: struct.Store.html#method.recover + /// the store should be [recovered](Store::recover). The operation may then be retried if + /// idempotent. StorageError, /// Storage is invalid. /// - /// The storage should be erased and the store [recovered]. The store would be empty and have - /// lost track of lifetime. - /// - /// [recovered]: struct.Store.html#method.recover + /// The storage should be erased and the store [recovered](Store::recover). The store would be + /// empty and have lost track of lifetime. InvalidStorage, } @@ -92,14 +91,12 @@ pub type StoreResult = Result; /// Progression ratio for store metrics. /// -/// This is used for the [capacity] and [lifetime] metrics. Those metrics are measured in words. +/// This is used for the [`Store::capacity`] and [`Store::lifetime`] metrics. Those metrics are +/// measured in words. /// /// # Invariant /// -/// - The used value does not exceed the total: `used <= total`. -/// -/// [capacity]: struct.Store.html#method.capacity -/// [lifetime]: struct.Store.html#method.lifetime +/// - The used value does not exceed the total: `used` ≤ `total`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct StoreRatio { /// How much of the metric is used. @@ -148,11 +145,11 @@ impl StoreHandle { self.key as usize } - /// Returns the length of value of the entry. + /// Returns the value length of the entry. /// /// # Errors /// - /// Returns `InvalidArgument` if the entry has been deleted or compacted. + /// Returns [`StoreError::InvalidArgument`] if the entry has been deleted or compacted. pub fn get_length(&self, store: &Store) -> StoreResult { store.get_length(self) } @@ -161,7 +158,7 @@ impl StoreHandle { /// /// # Errors /// - /// Returns `InvalidArgument` if the entry has been deleted or compacted. + /// Returns [`StoreError::InvalidArgument`] if the entry has been deleted or compacted. pub fn get_value(&self, store: &Store) -> StoreResult> { store.get_value(self) } @@ -211,7 +208,7 @@ pub struct Store { /// The list of the position of the user entries. /// - /// The position is encoded as the word offset from the [head](Store#structfield.head). + /// The position is encoded as the word offset from the [head](Store::head). entries: Option>, } @@ -224,7 +221,8 @@ impl Store { /// /// # Errors /// - /// Returns `InvalidArgument` if the storage is not supported. + /// Returns [`StoreError::InvalidArgument`] if the storage is not + /// [supported](Format::is_storage_supported). pub fn new(storage: S) -> Result, (StoreError, S)> { let format = match Format::new(&storage) { None => return Err((StoreError::InvalidArgument, storage)), @@ -258,7 +256,7 @@ impl Store { ))) } - /// Returns the current capacity in words. + /// Returns the current and total capacity in words. /// /// The capacity represents the size of what is stored. pub fn capacity(&self) -> StoreResult { @@ -271,7 +269,7 @@ impl Store { Ok(StoreRatio { used, total }) } - /// Returns the current lifetime in words. + /// Returns the current and total lifetime in words. /// /// The lifetime represents the age of the storage. The limit is an over-approximation by at /// most the maximum length of a value (the actual limit depends on the length of the prefix of @@ -286,10 +284,11 @@ impl Store { /// /// # Errors /// - /// Returns `InvalidArgument` in the following circumstances: - /// - There are too many updates. + /// Returns [`StoreError::InvalidArgument`] in the following circumstances: + /// - There are [too many](Format::max_updates) 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. + /// - The updates are invalid, e.g. key [out of bound](Format::max_key) or value [too + /// long](Format::max_value_len). pub fn transaction>( &mut self, updates: &[StoreUpdate], From 6cb6538db6782d6ea2dce5fe685395cfb28b0316 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 15 Mar 2021 12:10:13 +0100 Subject: [PATCH 181/192] Fix typography --- libraries/persistent_store/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index e3735c3..4be15e3 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -27,7 +27,7 @@ //! //! The store is flash-efficient in the sense that it uses the storage [lifetime](Store::lifetime) //! efficiently. For each page, all words are written at least once between erase cycles and all -//! erase cycles are used. However, not all written words are user content: lifetime is also +//! erase cycles are used. However, not all written words are user content: Lifetime is also //! consumed with metadata and compaction. //! //! The store is extendable with other entries than key-values. It is essentially a framework From e5313057f9874bd8a0715c4f006cf5b96bbb407c Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Mon, 15 Mar 2021 13:36:28 +0100 Subject: [PATCH 182/192] PIN protocol V2 in ClientPin (#293) * PIN protocol V2 in ClientPin * the test ClientPin has a random second private key --- src/ctap/client_pin.rs | 1055 +++++++++++++++++------------ src/ctap/command.rs | 23 +- src/ctap/config_command.rs | 25 +- src/ctap/credential_management.rs | 23 +- src/ctap/data_formats.rs | 79 ++- src/ctap/large_blobs.rs | 19 +- src/ctap/mod.rs | 8 +- src/ctap/pin_protocol.rs | 108 +-- 8 files changed, 815 insertions(+), 525 deletions(-) diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index 245f020..12e049c 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -13,11 +13,14 @@ // limitations under the License. use super::command::AuthenticatorClientPinParameters; -use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput}; +use super::data_formats::{ + ok_or_missing, ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, PinUvAuthProtocol, +}; use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use alloc::boxed::Box; use alloc::str; use alloc::string::String; use alloc::vec::Vec; @@ -48,29 +51,6 @@ pub const PIN_TOKEN_LENGTH: usize = 32; /// is fixed since CTAP2.1. const PIN_PADDED_LENGTH: usize = 64; -/// Computes and encrypts the HMAC-secret outputs. -/// -/// To compute them, we first have to decrypt the HMAC secret salt(s) that were -/// encrypted with the shared secret. The credRandom is used as a secret in HMAC -/// for those salts. -fn encrypt_hmac_secret_output( - rng: &mut impl Rng256, - shared_secret: &dyn SharedSecret, - salt_enc: &[u8], - cred_random: &[u8; 32], -) -> Result, Ctap2StatusCode> { - let decrypted_salts = shared_secret.decrypt(salt_enc)?; - if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - let mut output = hmac_256::(&cred_random[..], &decrypted_salts[..32]).to_vec(); - if decrypted_salts.len() == 64 { - let mut output2 = hmac_256::(&cred_random[..], &decrypted_salts[32..]).to_vec(); - output.append(&mut output2); - } - shared_secret.encrypt(rng, &output) -} - /// Decrypts the new_pin_enc and outputs the found PIN. fn decrypt_pin( shared_secret: &dyn SharedSecret, @@ -124,6 +104,7 @@ pub enum PinPermission { pub struct ClientPin { pin_protocol_v1: PinProtocol, + pin_protocol_v2: PinProtocol, consecutive_pin_mismatches: u8, permissions: u8, permissions_rp_id: Option, @@ -133,12 +114,42 @@ impl ClientPin { pub fn new(rng: &mut impl Rng256) -> ClientPin { ClientPin { pin_protocol_v1: PinProtocol::new(rng), + pin_protocol_v2: PinProtocol::new(rng), consecutive_pin_mismatches: 0, permissions: 0, permissions_rp_id: None, } } + /// Gets a reference to the PIN protocol of the given version. + fn get_pin_protocol(&self, pin_uv_auth_protocol: PinUvAuthProtocol) -> &PinProtocol { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => &self.pin_protocol_v1, + PinUvAuthProtocol::V2 => &self.pin_protocol_v2, + } + } + + /// Gets a mutable reference to the PIN protocol of the given version. + fn get_mut_pin_protocol( + &mut self, + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> &mut PinProtocol { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => &mut self.pin_protocol_v1, + PinUvAuthProtocol::V2 => &mut self.pin_protocol_v2, + } + } + + /// Computes the shared secret for the given version. + fn get_shared_secret( + &self, + pin_uv_auth_protocol: PinUvAuthProtocol, + key_agreement: CoseKey, + ) -> Result, Ctap2StatusCode> { + self.get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol) + } + /// Checks the given encrypted PIN hash against the stored PIN hash. /// /// Decrypts the encrypted pin_hash and compares it to the stored pin_hash. @@ -148,6 +159,7 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, + pin_uv_auth_protocol: PinUvAuthProtocol, shared_secret: &dyn SharedSecret, pin_hash_enc: Vec, ) -> Result<(), Ctap2StatusCode> { @@ -157,13 +169,13 @@ impl ClientPin { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); } persistent_store.decr_pin_retries()?; - if pin_hash_enc.len() != PIN_AUTH_LENGTH { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - let pin_hash_dec = shared_secret.decrypt(&pin_hash_enc)?; + let pin_hash_dec = shared_secret + .decrypt(&pin_hash_enc) + .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)?; if !bool::from(pin_hash.ct_eq(&pin_hash_dec)) { - self.pin_protocol_v1.regenerate(rng); + self.get_mut_pin_protocol(pin_uv_auth_protocol) + .regenerate(rng); if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } @@ -193,9 +205,16 @@ impl ClientPin { }) } - fn process_get_key_agreement(&self) -> Result { + fn process_get_key_agreement( + &self, + client_pin_params: AuthenticatorClientPinParameters, + ) -> Result { + let key_agreement = Some( + self.get_pin_protocol(client_pin_params.pin_uv_auth_protocol) + .get_public_key(), + ); Ok(AuthenticatorClientPinResponse { - key_agreement: Some(self.pin_protocol_v1.get_public_key()), + key_agreement, pin_token: None, retries: None, }) @@ -204,15 +223,24 @@ impl ClientPin { fn process_set_pin( &mut self, persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, + client_pin_params: AuthenticatorClientPinParameters, ) -> Result<(), Ctap2StatusCode> { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_uv_auth_param, + new_pin_enc, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?; + let new_pin_enc = ok_or_missing(new_pin_enc)?; + if persistent_store.pin_hash()?.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } - let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; - shared_secret.verify(&new_pin_enc, &pin_auth)?; + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; + shared_secret.verify(&new_pin_enc, &pin_uv_auth_param)?; check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; persistent_store.reset_pin_retries()?; @@ -223,22 +251,39 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, - pin_hash_enc: Vec, + client_pin_params: AuthenticatorClientPinParameters, ) -> Result<(), Ctap2StatusCode> { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_uv_auth_param, + new_pin_enc, + pin_hash_enc, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_uv_auth_param = ok_or_missing(pin_uv_auth_param)?; + let new_pin_enc = ok_or_missing(new_pin_enc)?; + let pin_hash_enc = ok_or_missing(pin_hash_enc)?; + if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } - let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; let mut auth_param_data = new_pin_enc.clone(); auth_param_data.extend(&pin_hash_enc); - shared_secret.verify(&auth_param_data, &pin_auth)?; - self.verify_pin_hash_enc(rng, persistent_store, shared_secret.as_ref(), pin_hash_enc)?; + shared_secret.verify(&auth_param_data, &pin_uv_auth_param)?; + self.verify_pin_hash_enc( + rng, + persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc, + )?; check_and_store_new_pin(persistent_store, shared_secret.as_ref(), new_pin_enc)?; self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); Ok(()) } @@ -246,19 +291,37 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_hash_enc: Vec, + client_pin_params: AuthenticatorClientPinParameters, ) -> Result { + let AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + key_agreement, + pin_hash_enc, + .. + } = client_pin_params; + let key_agreement = ok_or_missing(key_agreement)?; + let pin_hash_enc = ok_or_missing(pin_hash_enc)?; + if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } - let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; - self.verify_pin_hash_enc(rng, persistent_store, shared_secret.as_ref(), pin_hash_enc)?; + let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; + self.verify_pin_hash_enc( + rng, + persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc, + )?; if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - let pin_token = shared_secret.encrypt(rng, self.pin_protocol_v1.get_pin_uv_auth_token())?; + let pin_token = shared_secret.encrypt( + rng, + self.get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token(), + )?; self.permissions = 0x03; self.permissions_rp_id = None; @@ -273,16 +336,14 @@ impl ClientPin { &self, // If you want to support local user verification, implement this function. // Lacking a fingerprint reader, this subcommand is currently unsupported. - _key_agreement: CoseKey, - _permissions: u8, - _permissions_rp_id: Option, + _client_pin_params: AuthenticatorClientPinParameters, ) -> Result { - // User verifications is only supported through PIN currently. + // User verification is only supported through PIN currently. Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } fn process_get_uv_retries(&self) -> Result { - // User verifications is only supported through PIN currently. + // User verification is only supported through PIN currently. Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) } @@ -290,11 +351,13 @@ impl ClientPin { &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_hash_enc: Vec, - permissions: u8, - permissions_rp_id: Option, + mut client_pin_params: AuthenticatorClientPinParameters, ) -> Result { + let permissions = ok_or_missing(client_pin_params.permissions)?; + // Mutating client_pin_params is just an optimization to move it into + // process_get_pin_token, without cloning permissions_rp_id here. + let permissions_rp_id = client_pin_params.permissions_rp_id.take(); + if permissions == 0 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } @@ -303,8 +366,7 @@ impl ClientPin { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let response = - self.process_get_pin_token(rng, persistent_store, key_agreement, pin_hash_enc)?; + let response = self.process_get_pin_token(rng, persistent_store, client_pin_params)?; self.permissions = permissions; self.permissions_rp_id = permissions_rp_id; @@ -312,102 +374,80 @@ impl ClientPin { Ok(response) } + /// Processes the authenticatorClientPin command. pub fn process_command( &mut self, rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, ) -> Result { - let AuthenticatorClientPinParameters { - pin_uv_auth_protocol, - sub_command, - key_agreement, - pin_auth, - new_pin_enc, - pin_hash_enc, - permissions, - permissions_rp_id, - } = client_pin_params; - - if pin_uv_auth_protocol != 1 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - - let response = match sub_command { + let response = match client_pin_params.sub_command { ClientPinSubCommand::GetPinRetries => { Some(self.process_get_pin_retries(persistent_store)?) } - ClientPinSubCommand::GetKeyAgreement => Some(self.process_get_key_agreement()?), + ClientPinSubCommand::GetKeyAgreement => { + Some(self.process_get_key_agreement(client_pin_params)?) + } ClientPinSubCommand::SetPin => { - self.process_set_pin( - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; + self.process_set_pin(persistent_store, client_pin_params)?; None } ClientPinSubCommand::ChangePin => { - self.process_change_pin( - rng, - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - new_pin_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; + self.process_change_pin(rng, persistent_store, client_pin_params)?; None } - ClientPinSubCommand::GetPinToken => Some(self.process_get_pin_token( - rng, - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?), + ClientPinSubCommand::GetPinToken => { + Some(self.process_get_pin_token(rng, persistent_store, client_pin_params)?) + } ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( - self.process_get_pin_uv_auth_token_using_uv_with_permissions( - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions_rp_id, - )?, + self.process_get_pin_uv_auth_token_using_uv_with_permissions(client_pin_params)?, ), ClientPinSubCommand::GetUvRetries => Some(self.process_get_uv_retries()?), ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( self.process_get_pin_uv_auth_token_using_pin_with_permissions( rng, persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions_rp_id, + client_pin_params, )?, ), }; Ok(ResponseData::AuthenticatorClientPin(response)) } + /// Verifies the HMAC for the PIN protocol V1 pinUvAuthToken. pub fn verify_pin_auth_token( &self, hmac_contents: &[u8], - pin_auth: &[u8], + pin_uv_auth_param: &[u8], ) -> Result<(), Ctap2StatusCode> { - // TODO(kaczmarczyck) pass the protocol number verify_pin_uv_auth_token( - self.pin_protocol_v1.get_pin_uv_auth_token(), + self.get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(), hmac_contents, - pin_auth, - 1, + pin_uv_auth_param, + PinUvAuthProtocol::V1, ) } + /// Resets all held state. pub fn reset(&mut self, rng: &mut impl Rng256) { self.pin_protocol_v1.regenerate(rng); self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.regenerate(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); self.consecutive_pin_mismatches = 0; self.permissions = 0; self.permissions_rp_id = None; } + /// Verifies, computes and encrypts the HMAC-secret outputs. + /// + /// The salt_enc is + /// - verified with the shared secret and salt_auth, + /// - decrypted with the shared secret, + /// - HMAC'ed with cred_random. + /// The length of the output matches salt_enc and has to be 1 or 2 blocks of + /// 32 byte. pub fn process_hmac_secret( &self, rng: &mut impl Rng256, @@ -418,10 +458,23 @@ impl ClientPin { key_agreement, salt_enc, salt_auth, + pin_uv_auth_protocol, } = hmac_secret_input; - let shared_secret = self.pin_protocol_v1.decapsulate(key_agreement, 1)?; + let shared_secret = self + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol)?; shared_secret.verify(&salt_enc, &salt_auth)?; - encrypt_hmac_secret_output(rng, shared_secret.as_ref(), &salt_enc[..], cred_random) + + let decrypted_salts = shared_secret.decrypt(&salt_enc)?; + if decrypted_salts.len() != 32 && decrypted_salts.len() != 64 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let mut output = hmac_256::(&cred_random[..], &decrypted_salts[..32]).to_vec(); + if decrypted_salts.len() == 64 { + let mut output2 = hmac_256::(&cred_random[..], &decrypted_salts[32..]).to_vec(); + output.append(&mut output2); + } + shared_secret.encrypt(rng, &output) } /// Check if the required command's token permission is granted. @@ -481,9 +534,17 @@ impl ClientPin { pub fn new_test( key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], + pin_uv_auth_protocol: PinUvAuthProtocol, ) -> ClientPin { + use crypto::rng256::ThreadRng256; + let mut rng = ThreadRng256 {}; + let (key_agreement_key_v1, key_agreement_key_v2) = match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(&mut rng)), + PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(&mut rng), key_agreement_key), + }; ClientPin { - pin_protocol_v1: PinProtocol::new_test(key_agreement_key, pin_uv_auth_token), + pin_protocol_v1: PinProtocol::new_test(key_agreement_key_v1, pin_uv_auth_token), + pin_protocol_v2: PinProtocol::new_test(key_agreement_key_v2, pin_uv_auth_token), consecutive_pin_mismatches: 0, permissions: 0xFF, permissions_rp_id: None, @@ -493,7 +554,6 @@ impl ClientPin { #[cfg(test)] mod test { - use super::super::pin_protocol::SharedSecretV1; use super::*; use alloc::vec; use crypto::rng256::ThreadRng256; @@ -507,62 +567,100 @@ mod test { persistent_store.set_pin(&pin_hash, 4).unwrap(); } - /// Encrypts the message with a zero IV and key derived from shared_secret. - fn encrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { - let mut rng = ThreadRng256 {}; - let shared_secret = SharedSecretV1::new_test(*shared_secret); - shared_secret.encrypt(&mut rng, message).unwrap() - } - - /// Decrypts the message with a zero IV and key derived from shared_secret. - fn decrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { - let shared_secret = SharedSecretV1::new_test(*shared_secret); - shared_secret.decrypt(message).unwrap() - } - /// Fails on PINs bigger than 64 bytes. - fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec) -> Vec { + fn encrypt_pin(shared_secret: &dyn SharedSecret, pin: Vec) -> Vec { assert!(pin.len() <= 64); + let mut rng = ThreadRng256 {}; let mut padded_pin = [0u8; 64]; padded_pin[..pin.len()].copy_from_slice(&pin[..]); - encrypt_message(shared_secret, &padded_pin) + shared_secret.encrypt(&mut rng, &padded_pin).unwrap() } - /// Encrypts the dummy PIN "1234". - fn encrypt_standard_pin(shared_secret: &[u8; 32]) -> Vec { - encrypt_pin(shared_secret, b"1234".to_vec()) + /// Generates a ClientPin instance and a shared secret for testing. + /// + /// The shared secret for the desired PIN protocol is generated in a + /// handshake with itself. The other protocol has a random private key, so + /// tests using the wrong combination of PIN protocol and shared secret + /// should fail. + fn create_client_pin_and_shared_secret( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> (ClientPin, Box) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = key_agreement_key.genpk(); + let key_agreement = CoseKey::from(pk); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate(key_agreement, pin_uv_auth_protocol) + .unwrap(); + (client_pin, shared_secret) } - /// Encrypts the PIN hash corresponding to the dummy PIN "1234". - fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { - let mut pin = [0u8; 64]; - pin[..4].copy_from_slice(b"1234"); - let pin_hash = Sha256::hash(&pin); - encrypt_message(shared_secret, &pin_hash[..16]) + /// Generates standard input parameters to the ClientPin command. + /// + /// All fields are populated for simplicity, even though most are unused. + fn create_client_pin_and_parameters( + pin_uv_auth_protocol: PinUvAuthProtocol, + sub_command: ClientPinSubCommand, + ) -> (ClientPin, AuthenticatorClientPinParameters) { + let mut rng = ThreadRng256 {}; + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); + + let pin = b"1234"; + let mut padded_pin = [0u8; 64]; + padded_pin[..pin.len()].copy_from_slice(&pin[..]); + let pin_hash = Sha256::hash(&padded_pin); + let new_pin_enc = shared_secret + .as_ref() + .encrypt(&mut rng, &padded_pin) + .unwrap(); + let pin_uv_auth_param = shared_secret.as_ref().authenticate(&new_pin_enc); + let pin_hash_enc = shared_secret + .as_ref() + .encrypt(&mut rng, &pin_hash[..16]) + .unwrap(); + let params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command, + key_agreement: Some( + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + ), + pin_uv_auth_param: Some(pin_uv_auth_param), + new_pin_enc: Some(new_pin_enc), + pin_hash_enc: Some(pin_hash_enc), + permissions: Some(0x03), + permissions_rp_id: Some("example.com".to_string()), + }; + (client_pin, params) } - #[test] - fn test_verify_pin_hash_enc() { + fn test_helper_verify_pin_hash_enc(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); + let pin_protocol = client_pin.get_pin_protocol(pin_uv_auth_protocol); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); // The PIN is "1234". let pin_hash = [ 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, 0xC4, 0x12, ]; persistent_store.set_pin(&pin_hash, 4).unwrap(); - let shared_secret = SharedSecretV1::new_test([0x88; 32]); - let mut client_pin = ClientPin::new(&mut rng); - let pin_hash_enc = vec![ - 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, - 0x99, 0x66, - ]; + let pin_hash_enc = shared_secret.as_ref().encrypt(&mut rng, &pin_hash).unwrap(); assert_eq!( client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Ok(()) @@ -573,22 +671,21 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); - let pin_hash_enc = vec![ - 0x8D, 0x7A, 0xA3, 0x9F, 0x7F, 0xC6, 0x08, 0x13, 0x9A, 0xC8, 0x56, 0x97, 0x70, 0x74, - 0x99, 0x66, - ]; + let pin_hash_enc = shared_secret.as_ref().encrypt(&mut rng, &pin_hash).unwrap(); client_pin.consecutive_pin_mismatches = 3; assert_eq!( client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) @@ -600,7 +697,8 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -611,7 +709,8 @@ mod test { client_pin.verify_pin_hash_enc( &mut rng, &mut persistent_store, - &shared_secret, + pin_uv_auth_protocol, + shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -619,95 +718,119 @@ mod test { } #[test] - fn test_process_get_pin_retries() { + fn test_verify_pin_hash_enc_v1() { + test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V1); + } + + #[test] + fn test_verify_pin_hash_enc_v2() { + test_helper_verify_pin_hash_enc(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_retries(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinRetries, + ); let mut rng = ThreadRng256 {}; - let persistent_store = PersistentStore::new(&mut rng); - let client_pin = ClientPin::new(&mut rng); - let expected_response = Ok(AuthenticatorClientPinResponse { + let mut persistent_store = PersistentStore::new(&mut rng); + let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, pin_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), }); assert_eq!( - client_pin.process_get_pin_retries(&persistent_store), - expected_response + client_pin.process_command(&mut rng, &mut persistent_store, params), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } #[test] - fn test_process_get_key_agreement() { + fn test_process_get_pin_retries_v1() { + test_helper_process_get_pin_retries(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_retries_v2() { + test_helper_process_get_pin_retries(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_key_agreement(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetKeyAgreement, + ); let mut rng = ThreadRng256 {}; - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let expected_response = Ok(AuthenticatorClientPinResponse { - key_agreement: Some(CoseKey::from(pk)), + let mut persistent_store = PersistentStore::new(&mut rng); + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: params.key_agreement.clone(), pin_token: None, retries: None, }); - assert_eq!(client_pin.process_get_key_agreement(), expected_response); - } - - #[test] - fn test_process_set_pin() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let new_pin_enc = encrypt_standard_pin(&shared_secret); - let pin_auth = hmac_256::(&shared_secret, &new_pin_enc[..])[..16].to_vec(); assert_eq!( - client_pin.process_set_pin(&mut persistent_store, key_agreement, pin_auth, new_pin_enc), - Ok(()) + client_pin.process_command(&mut rng, &mut persistent_store, params), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } #[test] - fn test_process_change_pin() { + fn test_process_get_key_agreement_v1() { + test_helper_process_get_key_agreement(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_key_agreement_v2() { + test_helper_process_get_key_agreement(PinUvAuthProtocol::V2); + } + + fn test_helper_process_set_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = + create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::SetPin); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params), + Ok(ResponseData::AuthenticatorClientPin(None)) + ); + } + + #[test] + fn test_process_set_pin_v1() { + test_helper_process_set_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_set_pin_v2() { + test_helper_process_set_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_process_change_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, mut params) = + create_client_pin_and_parameters(pin_uv_auth_protocol, ClientPinSubCommand::ChangePin); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let new_pin_enc = encrypt_standard_pin(&shared_secret); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - let mut auth_param_data = new_pin_enc.clone(); - auth_param_data.extend(&pin_hash_enc); - let pin_auth = hmac_256::(&shared_secret, &auth_param_data[..])[..16].to_vec(); + let mut auth_param_data = params.new_pin_enc.clone().unwrap(); + auth_param_data.extend(params.pin_hash_enc.as_ref().unwrap()); + let pin_uv_auth_param = shared_secret.authenticate(&auth_param_data); + params.pin_uv_auth_param = Some(pin_uv_auth_param); assert_eq!( - client_pin.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_auth.clone(), - new_pin_enc.clone(), - pin_hash_enc.clone() - ), - Ok(()) + client_pin.process_command(&mut rng, &mut persistent_store, params.clone()), + Ok(ResponseData::AuthenticatorClientPin(None)) ); - let bad_pin_hash_enc = vec![0xEE; 16]; + let mut bad_params = params.clone(); + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_auth.clone(), - new_pin_enc.clone(), - bad_pin_hash_enc - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); @@ -715,102 +838,91 @@ mod test { persistent_store.decr_pin_retries().unwrap(); } assert_eq!( - client_pin.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement, - pin_auth, - new_pin_enc, - pin_hash_enc, - ), + client_pin.process_command(&mut rng, &mut persistent_store, params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) ); } #[test] - fn test_process_get_pin_token() { + fn test_process_change_pin_v1() { + test_helper_process_change_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_change_pin_v2() { + test_helper_process_change_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_token(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinToken, + ); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert!(client_pin - .process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc - ) + .process_command(&mut rng, &mut persistent_store, params.clone()) .is_ok()); - let pin_hash_enc = vec![0xEE; 16]; + let mut bad_params = params; + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement, - pin_hash_enc - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] - fn test_process_get_pin_token_force_pin_change() { + fn test_process_get_pin_token_v1() { + test_helper_process_get_pin_token(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_token_v2() { + test_helper_process_get_pin_token(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_token_force_pin_change(pin_uv_auth_protocol: PinUvAuthProtocol) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinToken, + ); 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 key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); + assert_eq!(persistent_store.force_pin_change(), Ok(())); assert_eq!( - client_pin.process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement, - pin_hash_enc - ), + client_pin.process_command(&mut rng, &mut persistent_store, params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), ); } #[test] - fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { + fn test_process_get_pin_token_force_pin_change_v1() { + test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_token_force_pin_change_v2() { + test_helper_process_get_pin_token_force_pin_change(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert!(client_pin - .process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc.clone(), - 0x03, - Some(String::from("example.com")), - ) + .process_command(&mut rng, &mut persistent_store, params.clone()) .is_ok()); assert_eq!(client_pin.permissions, 0x03); assert_eq!( @@ -818,159 +930,121 @@ mod test { Some(String::from("example.com")) ); + let mut bad_params = params.clone(); + bad_params.permissions = Some(0x00); assert_eq!( - client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc.clone(), - 0x00, - Some(String::from("example.com")), - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); + let mut bad_params = params.clone(); + bad_params.permissions_rp_id = None; assert_eq!( - client_pin.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc, - 0x03, - None, - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); - let pin_hash_enc = vec![0xEE; 16]; + let mut bad_params = params; + bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.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")), - ), + client_pin.process_command(&mut rng, &mut persistent_store, bad_params), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] - fn test_process_get_pin_token_force_pin_change_force_pin_change() { + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v1() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_v2() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions(PinUvAuthProtocol::V2); + } + + fn test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let (mut client_pin, params) = create_client_pin_and_parameters( + pin_uv_auth_protocol, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); 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 key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pk = key_agreement_key.genpk(); - let pre_secret = key_agreement_key.exchange_x(&pk); - let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; - let mut client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); - let shared_secret = Sha256::hash(&pre_secret); - let key_agreement = CoseKey::from(pk); - - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); assert_eq!( - client_pin.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), + client_pin.process_command(&mut rng, &mut persistent_store, params), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } #[test] - fn test_process() { + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v1() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + PinUvAuthProtocol::V1, + ); + } + + #[test] + fn test_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change_v2() { + test_helper_process_get_pin_uv_auth_token_using_pin_with_permissions_force_pin_change( + PinUvAuthProtocol::V2, + ); + } + + fn test_helper_decrypt_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let mut client_pin = ClientPin::new(&mut rng); - let client_pin_params = AuthenticatorClientPinParameters { - pin_uv_auth_protocol: 1, - sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: None, - pin_auth: None, - new_pin_enc: None, - pin_hash_enc: None, - permissions: None, - permissions_rp_id: None, - }; - assert!(client_pin - .process_command(&mut rng, &mut persistent_store, client_pin_params) - .is_ok()); + let pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); - let client_pin_params = AuthenticatorClientPinParameters { - pin_uv_auth_protocol: 2, - sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: None, - pin_auth: None, - new_pin_enc: None, - pin_hash_enc: None, - permissions: None, - permissions_rp_id: None, - }; - let error_code = Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER; + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"1234".to_vec()); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, client_pin_params), - Err(error_code) - ); - } - - #[test] - fn test_decrypt_pin() { - let shared_secret = SharedSecretV1::new_test([0x88; 32]); - - // "1234" - let new_pin_enc = vec![ - 0xC0, 0xCF, 0xAE, 0x4C, 0x79, 0x56, 0x87, 0x99, 0xE5, 0x83, 0x4F, 0xE6, 0x4D, 0xFE, - 0x53, 0x32, 0x36, 0x0D, 0xF9, 0x1E, 0x47, 0x66, 0x10, 0x5C, 0x63, 0x30, 0x1D, 0xCC, - 0x00, 0x09, 0x91, 0xA4, 0x20, 0x6B, 0x78, 0x10, 0xFE, 0xC6, 0x2E, 0x7E, 0x75, 0x14, - 0xEE, 0x01, 0x99, 0x6C, 0xD7, 0xE5, 0x2B, 0xA5, 0x7A, 0x5A, 0xE1, 0xEC, 0x69, 0x31, - 0x18, 0x35, 0x06, 0x66, 0x97, 0x84, 0x68, 0xC2, - ]; - assert_eq!( - decrypt_pin(&shared_secret, new_pin_enc), + decrypt_pin(shared_secret.as_ref(), new_pin_enc), Ok(b"1234".to_vec()), ); - // "123" - let new_pin_enc = vec![ - 0xF3, 0x54, 0x29, 0x17, 0xD4, 0xF8, 0xCD, 0x23, 0x1D, 0x59, 0xED, 0xE5, 0x33, 0x42, - 0x13, 0x39, 0x22, 0xBB, 0x91, 0x28, 0x87, 0x6A, 0xF9, 0xB1, 0x80, 0x9C, 0x9D, 0x76, - 0xFF, 0xDD, 0xB8, 0xD6, 0x8D, 0x66, 0x99, 0xA2, 0x42, 0x67, 0xB0, 0x5C, 0x82, 0x3F, - 0x08, 0x55, 0x8C, 0x04, 0xC5, 0x91, 0xF0, 0xF9, 0x58, 0x44, 0x00, 0x1B, 0x99, 0xA6, - 0x7C, 0xC7, 0x2D, 0x43, 0x74, 0x4C, 0x1D, 0x7E, - ]; + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"123".to_vec()); assert_eq!( - decrypt_pin(&shared_secret, new_pin_enc), + decrypt_pin(shared_secret.as_ref(), new_pin_enc), Ok(b"123".to_vec()), ); // Encrypted PIN is too short. let new_pin_enc = vec![0x44; 63]; assert_eq!( - decrypt_pin(&shared_secret, new_pin_enc), + decrypt_pin(shared_secret.as_ref(), new_pin_enc), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); // Encrypted PIN is too long. let new_pin_enc = vec![0x44; 65]; assert_eq!( - decrypt_pin(&shared_secret, new_pin_enc), + decrypt_pin(shared_secret.as_ref(), new_pin_enc), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); } #[test] - fn test_check_and_store_new_pin() { + fn test_decrypt_pin_v1() { + test_helper_decrypt_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_decrypt_pin_v2() { + test_helper_decrypt_pin(PinUvAuthProtocol::V2); + } + + fn test_helper_check_and_store_new_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - let shared_secret_hash = [0x88; 32]; - let shared_secret = SharedSecretV1::new_test(shared_secret_hash); + let pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); let test_cases = vec![ // Accept PIN "1234". @@ -993,9 +1067,10 @@ mod test { ]; for (pin, result) in test_cases { let old_pin_hash = persistent_store.pin_hash().unwrap(); - let new_pin_enc = encrypt_pin(&shared_secret_hash, pin); + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), pin); + assert_eq!( - check_and_store_new_pin(&mut persistent_store, &shared_secret, new_pin_enc), + check_and_store_new_pin(&mut persistent_store, shared_secret.as_ref(), new_pin_enc), result ); if result.is_ok() { @@ -1007,77 +1082,151 @@ mod test { } #[test] - fn test_encrypt_hmac_secret_output() { + fn test_check_and_store_new_pin_v1() { + test_helper_check_and_store_new_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_check_and_store_new_pin_v2() { + test_helper_check_and_store_new_pin(PinUvAuthProtocol::V2); + } + + /// Generates valid inputs for process_hmac_secret and returns the output. + fn get_process_hmac_secret_decrypted_output( + pin_uv_auth_protocol: PinUvAuthProtocol, + cred_random: &[u8; 32], + salt: Vec, + ) -> Result, Ctap2StatusCode> { let mut rng = ThreadRng256 {}; - let shared_secret_hash = [0x88; 32]; - let shared_secret = SharedSecretV1::new_test(shared_secret_hash); - let salt_enc = [0x5E; 32]; - let cred_random = [0xC9; 32]; - let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 32); + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); - let salt_enc = [0x5E; 48]; - let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); - assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); + let salt_enc = shared_secret.as_ref().encrypt(&mut rng, &salt).unwrap(); + let salt_auth = shared_secret.authenticate(&salt_enc); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let output = client_pin.process_hmac_secret(&mut rng, hmac_secret_input, cred_random); + output.map(|v| shared_secret.as_ref().decrypt(&v).unwrap()) + } - let salt_enc = [0x5E; 64]; - let output = encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 64); - - let mut salt_enc = [0x00; 32]; + fn test_helper_process_hmac_secret_bad_salt_auth(pin_uv_auth_protocol: PinUvAuthProtocol) { + let mut rng = ThreadRng256 {}; + let (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); + let cred_random = [0xC9; 32]; + + let salt_enc = vec![0x01; 32]; + let mut salt_auth = shared_secret.authenticate(&salt_enc); + salt_auth[0] = 0x00; + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_public_key(), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let output = client_pin.process_hmac_secret(&mut rng, hmac_secret_input, &cred_random); + assert_eq!(output, Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID)); + } + + #[test] + fn test_process_hmac_secret_bad_salt_auth_v1() { + test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_bad_salt_auth_v2() { + test_helper_process_hmac_secret_bad_salt_auth(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_one_salt(pin_uv_auth_protocol: PinUvAuthProtocol) { + let cred_random = [0xC9; 32]; + + let salt = vec![0x01; 32]; + let expected_output = hmac_256::(&cred_random, &salt); + + let output = + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt) + .unwrap(); + assert_eq!(&output, &expected_output); + } + + #[test] + fn test_process_hmac_secret_one_salt_v1() { + test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_one_salt_v2() { + test_helper_process_hmac_secret_one_salt(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_two_salts(pin_uv_auth_protocol: PinUvAuthProtocol) { 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_hash, &salt1); - salt_enc.copy_from_slice(salt_enc1.as_slice()); - let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec, &expected_output1); - - let salt_enc2 = &encrypt_message(&shared_secret_hash, &salt2); - salt_enc.copy_from_slice(salt_enc2.as_slice()); - let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec, &expected_output2); - - let mut salt_enc = [0x00; 64]; - let mut salt12 = [0x00; 64]; + let mut salt12 = vec![0x00; 64]; salt12[..32].copy_from_slice(&salt1); salt12[32..].copy_from_slice(&salt2); - let salt_enc12 = encrypt_message(&shared_secret_hash, &salt12); - salt_enc.copy_from_slice(salt_enc12.as_slice()); let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec[..32], &expected_output1); - assert_eq!(&output_dec[32..], &expected_output2); + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt12) + .unwrap(); + assert_eq!(&output[..32], &expected_output1); + assert_eq!(&output[32..], &expected_output2); - let mut salt_enc = [0x00; 64]; - let mut salt02 = [0x00; 64]; + let mut salt02 = vec![0x00; 64]; salt02[32..].copy_from_slice(&salt2); - let salt_enc02 = encrypt_message(&shared_secret_hash, &salt02); - salt_enc.copy_from_slice(salt_enc02.as_slice()); let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec[32..], &expected_output2); + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt02) + .unwrap(); + assert_eq!(&output[32..], &expected_output2); - let mut salt_enc = [0x00; 64]; - let mut salt10 = [0x00; 64]; + let mut salt10 = vec![0x00; 64]; salt10[..32].copy_from_slice(&salt1); - let salt_enc10 = encrypt_message(&shared_secret_hash, &salt10); - salt_enc.copy_from_slice(salt_enc10.as_slice()); let output = - encrypt_hmac_secret_output(&mut rng, &shared_secret, &salt_enc, &cred_random).unwrap(); - let output_dec = decrypt_message(&shared_secret_hash, &output); - assert_eq!(&output_dec[..32], &expected_output1); + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt10) + .unwrap(); + assert_eq!(&output[..32], &expected_output1); + } + + #[test] + fn test_process_hmac_secret_two_salts_v1() { + test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_two_salts_v2() { + test_helper_process_hmac_secret_two_salts(PinUvAuthProtocol::V2); + } + + fn test_helper_process_hmac_secret_wrong_length(pin_uv_auth_protocol: PinUvAuthProtocol) { + let cred_random = [0xC9; 32]; + + let output = get_process_hmac_secret_decrypted_output( + pin_uv_auth_protocol, + &cred_random, + vec![0x5E; 48], + ); + assert_eq!(output, Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)); + } + + #[test] + fn test_process_hmac_secret_wrong_length_v1() { + test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_hmac_secret_wrong_length_v2() { + test_helper_process_hmac_secret_wrong_length(PinUvAuthProtocol::V2); } #[test] @@ -1160,4 +1309,34 @@ mod test { Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } + + #[test] + fn test_reset() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + let public_key_v1 = client_pin.pin_protocol_v1.get_public_key(); + let public_key_v2 = client_pin.pin_protocol_v2.get_public_key(); + let token_v1 = *client_pin.pin_protocol_v1.get_pin_uv_auth_token(); + let token_v2 = *client_pin.pin_protocol_v2.get_pin_uv_auth_token(); + client_pin.permissions = 0xFF; + client_pin.permissions_rp_id = Some(String::from("example.com")); + client_pin.reset(&mut rng); + assert_ne!(public_key_v1, client_pin.pin_protocol_v1.get_public_key()); + assert_ne!(public_key_v2, client_pin.pin_protocol_v2.get_public_key()); + assert_ne!( + &token_v1, + client_pin.pin_protocol_v1.get_pin_uv_auth_token() + ); + assert_ne!( + &token_v2, + client_pin.pin_protocol_v2.get_pin_uv_auth_token() + ); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin.has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); + } } diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 8c5aaec..748c33e 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -18,8 +18,8 @@ use super::data_formats::{ extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, SetMinPinLengthParams, + PinUvAuthProtocol, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams, }; use super::key_material; use super::status_code::Ctap2StatusCode; @@ -302,12 +302,12 @@ impl TryFrom for AuthenticatorGetAssertionParameters { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorClientPinParameters { - pub pin_uv_auth_protocol: u64, + pub pin_uv_auth_protocol: PinUvAuthProtocol, pub sub_command: ClientPinSubCommand, pub key_agreement: Option, - pub pin_auth: Option>, + pub pin_uv_auth_param: Option>, pub new_pin_enc: Option>, pub pin_hash_enc: Option>, pub permissions: Option, @@ -323,7 +323,7 @@ impl TryFrom for AuthenticatorClientPinParameters { 0x01 => pin_uv_auth_protocol, 0x02 => sub_command, 0x03 => key_agreement, - 0x04 => pin_auth, + 0x04 => pin_uv_auth_param, 0x05 => new_pin_enc, 0x06 => pin_hash_enc, 0x09 => permissions, @@ -331,10 +331,11 @@ impl TryFrom for AuthenticatorClientPinParameters { } = extract_map(cbor_value)?; } - let pin_uv_auth_protocol = extract_unsigned(ok_or_missing(pin_uv_auth_protocol)?)?; + let pin_uv_auth_protocol = + PinUvAuthProtocol::try_from(ok_or_missing(pin_uv_auth_protocol)?)?; let sub_command = ClientPinSubCommand::try_from(ok_or_missing(sub_command)?)?; let key_agreement = key_agreement.map(CoseKey::try_from).transpose()?; - let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + let pin_uv_auth_param = pin_uv_auth_param.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()?; // We expect a bit field of 8 bits, and drop everything else. @@ -349,7 +350,7 @@ impl TryFrom for AuthenticatorClientPinParameters { pin_uv_auth_protocol, sub_command, key_agreement, - pin_auth, + pin_uv_auth_param, new_pin_enc, pin_hash_enc, permissions, @@ -706,10 +707,10 @@ mod test { AuthenticatorClientPinParameters::try_from(cbor_value).unwrap(); let expected_client_pin_parameters = AuthenticatorClientPinParameters { - pin_uv_auth_protocol: 1, + pin_uv_auth_protocol: PinUvAuthProtocol::V1, sub_command: ClientPinSubCommand::GetPinRetries, key_agreement: Some(cose_key), - pin_auth: Some(vec![0xBB]), + pin_uv_auth_param: Some(vec![0xBB]), new_pin_enc: Some(vec![0xCC]), pin_hash_enc: Some(vec![0xDD]), permissions: Some(0x03), diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 2f90f11..3cbefd5 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -126,6 +126,7 @@ pub fn process_config( mod test { use super::*; use crate::ctap::customization::ENFORCE_ALWAYS_UV; + use crate::ctap::data_formats::PinUvAuthProtocol; use crypto::rng256::ThreadRng256; #[test] @@ -134,7 +135,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::EnableEnterpriseAttestation, @@ -161,7 +163,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::ToggleAlwaysUv, @@ -197,7 +200,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ @@ -257,7 +261,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); // First, increase minimum PIN length from 4 to 6 without PIN auth. let min_pin_length = 6; @@ -301,7 +306,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); // First, set RP IDs without PIN auth. let min_pin_length = 6; @@ -377,7 +383,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); // Increase min PIN, force PIN change. @@ -400,7 +407,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); persistent_store.set_pin(&[0x88; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ @@ -431,7 +439,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::VendorPrototype, diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index 5b07294..bbfb320 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -351,7 +351,7 @@ pub fn process_credential_management( #[cfg(test)] mod test { - use super::super::data_formats::PublicKeyCredentialType; + use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType}; use super::super::CtapState; use super::*; use crypto::rng256::{Rng256, ThreadRng256}; @@ -382,7 +382,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let credential_source = create_credential_source(&mut rng); let user_immediately_present = |_| Ok(()); @@ -453,7 +454,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); 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(); @@ -550,7 +552,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let credential_source = create_credential_source(&mut rng); let user_immediately_present = |_| Ok(()); @@ -632,7 +635,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let credential_source1 = create_credential_source(&mut rng); let mut credential_source2 = create_credential_source(&mut rng); credential_source2.user_handle = vec![0x02]; @@ -737,7 +741,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut credential_source = create_credential_source(&mut rng); credential_source.credential_id = vec![0x1D; 32]; @@ -808,7 +813,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut credential_source = create_credential_source(&mut rng); credential_source.credential_id = vec![0x1D; 32]; @@ -880,7 +886,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 97ee858..138dd3a 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -356,6 +356,7 @@ pub struct GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, pub salt_auth: Vec, + pub pin_uv_auth_protocol: PinUvAuthProtocol, } impl TryFrom for GetAssertionHmacSecretInput { @@ -367,16 +368,20 @@ impl TryFrom for GetAssertionHmacSecretInput { 1 => key_agreement, 2 => salt_enc, 3 => salt_auth, + 4 => pin_uv_auth_protocol, } = extract_map(cbor_value)?; } 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)?)?; + let pin_uv_auth_protocol = + pin_uv_auth_protocol.map_or(Ok(PinUvAuthProtocol::V1), PinUvAuthProtocol::try_from)?; Ok(Self { key_agreement, salt_enc, salt_auth, + pin_uv_auth_protocol, }) } } @@ -638,7 +643,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))?; + let creation_order = creation_order.map_or(Ok(0), extract_unsigned)?; 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()?; @@ -809,6 +814,24 @@ impl TryFrom for ecdh::PubKey { } } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PinUvAuthProtocol { + V1, + V2, +} + +impl TryFrom for PinUvAuthProtocol { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: cbor::Value) -> Result { + match extract_unsigned(cbor_value)? { + 1 => Ok(PinUvAuthProtocol::V1), + 2 => Ok(PinUvAuthProtocol::V2), + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } + } +} + #[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum ClientPinSubCommand { @@ -1569,7 +1592,7 @@ mod test { } #[test] - fn test_from_get_assertion_extensions() { + fn test_from_get_assertion_extensions_default_protocol() { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let pk = sk.genpk(); @@ -1588,6 +1611,7 @@ mod test { key_agreement: cose_key, salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V1, }; let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), @@ -1597,6 +1621,38 @@ mod test { assert_eq!(extensions, Ok(expected_extensions)); } + #[test] + fn test_from_get_assertion_extensions_with_protocol() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); + let cbor_extensions = cbor_map! { + "hmac-secret" => cbor_map! { + 1 => cbor::Value::from(cose_key.clone()), + 2 => vec![0x02; 32], + 3 => vec![0x03; 16], + 4 => 2, + }, + "credBlob" => true, + "largeBlobKey" => true, + }; + let extensions = GetAssertionExtensions::try_from(cbor_extensions); + let expected_input = GetAssertionHmacSecretInput { + key_agreement: cose_key, + salt_enc: vec![0x02; 32], + salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V2, + }; + let expected_extensions = GetAssertionExtensions { + hmac_secret: Some(expected_input), + cred_blob: true, + large_blob_key: Some(true), + }; + assert_eq!(extensions, Ok(expected_extensions)); + // TODO more tests, check default + } + #[test] fn test_from_make_credential_options() { let cbor_make_options = cbor_map! { @@ -1759,6 +1815,25 @@ mod test { assert_eq!(cose_key.algorithm, ES256_ALGORITHM); } + #[test] + fn test_from_pin_uv_auth_protocol() { + let cbor_protocol: cbor::Value = cbor_int!(0x01); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Ok(PinUvAuthProtocol::V1) + ); + let cbor_protocol: cbor::Value = cbor_int!(0x02); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Ok(PinUvAuthProtocol::V2) + ); + let cbor_protocol: cbor::Value = cbor_int!(0x03); + assert_eq!( + PinUvAuthProtocol::try_from(cbor_protocol), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + #[test] fn test_from_into_client_pin_sub_command() { let cbor_sub_command: cbor::Value = cbor_int!(0x01); diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index 05cec58..d366ce4 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -135,6 +135,7 @@ impl LargeBlobs { #[cfg(test)] mod test { + use super::super::data_formats::PinUvAuthProtocol; use super::*; use crypto::rng256::ThreadRng256; @@ -144,7 +145,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); let large_blob = vec![ @@ -175,7 +177,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -237,7 +240,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -283,7 +287,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -329,7 +334,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -358,7 +364,8 @@ mod test { 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 client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let mut client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 6d8e2d0..8432f4e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1223,7 +1223,8 @@ mod test { use super::command::AuthenticatorAttestationMaterial; use super::data_formats::{ CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions, - MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, + MakeCredentialOptions, PinUvAuthProtocol, PublicKeyCredentialRpEntity, + PublicKeyCredentialUserEntity, }; use super::*; use cbor::{cbor_array, cbor_array_vec, cbor_map}; @@ -1983,6 +1984,7 @@ mod test { key_agreement: CoseKey::from(pk), salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V1, }; let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), @@ -2040,6 +2042,7 @@ mod test { key_agreement: CoseKey::from(pk), salt_enc: vec![0x02; 32], salt_auth: vec![0x03; 16], + pin_uv_auth_protocol: PinUvAuthProtocol::V1, }; let get_extensions = GetAssertionExtensions { hmac_secret: Some(hmac_secret_input), @@ -2317,7 +2320,8 @@ mod test { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x88; 32]; - let client_pin = ClientPin::new_test(key_agreement_key, pin_uv_auth_token); + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs index 88d608a..912f7aa 100644 --- a/src/ctap/pin_protocol.rs +++ b/src/ctap/pin_protocol.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::ctap::client_pin::PIN_TOKEN_LENGTH; -use crate::ctap::data_formats::CoseKey; +use crate::ctap::data_formats::{CoseKey, PinUvAuthProtocol}; use crate::ctap::status_code::Ctap2StatusCode; use alloc::boxed::Box; use alloc::vec; @@ -21,6 +21,8 @@ use alloc::vec::Vec; use core::convert::TryInto; use crypto::cbc::{cbc_decrypt, cbc_encrypt}; use crypto::hkdf::hkdf_empty_salt_256; +#[cfg(test)] +use crypto::hmac::hmac_256; use crypto::hmac::{verify_hmac_256, verify_hmac_256_first_128bits}; use crypto::rng256::Rng256; use crypto::sha256::Sha256; @@ -64,14 +66,13 @@ impl PinProtocol { pub fn decapsulate( &self, peer_cose_key: CoseKey, - pin_uv_auth_protocol: u64, + pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result, Ctap2StatusCode> { let pk: crypto::ecdh::PubKey = CoseKey::try_into(peer_cose_key)?; let handshake = self.key_agreement_key.exchange_x(&pk); match pin_uv_auth_protocol { - 1 => Ok(Box::new(SharedSecretV1::new(handshake))), - 2 => Ok(Box::new(SharedSecretV2::new(handshake))), - _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + PinUvAuthProtocol::V1 => Ok(Box::new(SharedSecretV1::new(handshake))), + PinUvAuthProtocol::V2 => Ok(Box::new(SharedSecretV2::new(handshake))), } } @@ -98,12 +99,11 @@ pub fn verify_pin_uv_auth_token( token: &[u8; PIN_TOKEN_LENGTH], message: &[u8], signature: &[u8], - pin_uv_auth_protocol: u64, + pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result<(), Ctap2StatusCode> { match pin_uv_auth_protocol { - 1 => verify_v1(token, message, signature), - 2 => verify_v2(token, message, signature), - _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + PinUvAuthProtocol::V1 => verify_v1(token, message, signature), + PinUvAuthProtocol::V2 => verify_v2(token, message, signature), } } @@ -116,6 +116,10 @@ pub trait SharedSecret { /// Verifies that the signature is a valid MAC for the given message. fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode>; + + /// Creates a signature that matches verify. + #[cfg(test)] + fn authenticate(&self, message: &[u8]) -> Vec; } fn aes256_cbc_encrypt( @@ -210,16 +214,6 @@ impl SharedSecretV1 { aes_enc_key, } } - - /// Creates a new shared secret for testing. - #[cfg(test)] - pub fn new_test(hash: [u8; 32]) -> SharedSecretV1 { - let aes_enc_key = crypto::aes256::EncryptionKey::new(&hash); - SharedSecretV1 { - common_secret: hash, - aes_enc_key, - } - } } impl SharedSecret for SharedSecretV1 { @@ -234,6 +228,11 @@ impl SharedSecret for SharedSecretV1 { fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { verify_v1(&self.common_secret, message, signature) } + + #[cfg(test)] + fn authenticate(&self, message: &[u8]) -> Vec { + hmac_256::(&self.common_secret, message)[..16].to_vec() + } } pub struct SharedSecretV2 { @@ -264,6 +263,11 @@ impl SharedSecret for SharedSecretV2 { fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { verify_v2(&self.hmac_key, message, signature) } + + #[cfg(test)] + fn authenticate(&self, message: &[u8]) -> Vec { + hmac_256::(&self.hmac_key, message).to_vec() + } } #[cfg(test)] @@ -300,6 +304,14 @@ mod test { assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext)); } + #[test] + fn test_shared_secret_v1_authenticate_verify() { + let shared_secret = SharedSecretV1::new([0x55; 32]); + let message = [0xAA; 32]; + let signature = shared_secret.authenticate(&message); + assert_eq!(shared_secret.verify(&message, &signature), Ok(())); + } + #[test] fn test_shared_secret_v1_verify() { let shared_secret = SharedSecretV1::new([0x55; 32]); @@ -328,6 +340,14 @@ mod test { assert_eq!(shared_secret.decrypt(&ciphertext), Ok(plaintext)); } + #[test] + fn test_shared_secret_v2_authenticate_verify() { + let shared_secret = SharedSecretV2::new([0x55; 32]); + let message = [0xAA; 32]; + let signature = shared_secret.authenticate(&message); + assert_eq!(shared_secret.verify(&message, &signature), Ok(())); + } + #[test] fn test_shared_secret_v2_verify() { let shared_secret = SharedSecretV2::new([0x55; 32]); @@ -348,23 +368,12 @@ mod test { ); } - #[test] - fn test_decapsulate_invalid() { - let mut rng = ThreadRng256 {}; - let pin_protocol = PinProtocol::new(&mut rng); - let shared_secret = pin_protocol.decapsulate(pin_protocol.get_public_key(), 3); - assert_eq!( - shared_secret.err(), - Some(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - } - #[test] fn test_decapsulate_symmetric() { let mut rng = ThreadRng256 {}; let pin_protocol1 = PinProtocol::new(&mut rng); let pin_protocol2 = PinProtocol::new(&mut rng); - for protocol in 1..=2 { + for &protocol in &[PinUvAuthProtocol::V1, PinUvAuthProtocol::V2] { let shared_secret1 = pin_protocol1 .decapsulate(pin_protocol2.get_public_key(), protocol) .unwrap(); @@ -386,19 +395,24 @@ mod test { 0x49, 0x68, ]; assert_eq!( - verify_pin_uv_auth_token(&token, &message, &signature, 1), + verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V1), Ok(()) ); assert_eq!( - verify_pin_uv_auth_token(&[0x12; PIN_TOKEN_LENGTH], &message, &signature, 1), + verify_pin_uv_auth_token( + &[0x12; PIN_TOKEN_LENGTH], + &message, + &signature, + PinUvAuthProtocol::V1 + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( - verify_pin_uv_auth_token(&token, &[0xBB], &signature, 1), + verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V1), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( - verify_pin_uv_auth_token(&token, &message, &[0x12; 16], 1), + verify_pin_uv_auth_token(&token, &message, &[0x12; 16], PinUvAuthProtocol::V1), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -413,31 +427,25 @@ mod test { 0x36, 0x93, 0xF7, 0x84, ]; assert_eq!( - verify_pin_uv_auth_token(&token, &message, &signature, 2), + verify_pin_uv_auth_token(&token, &message, &signature, PinUvAuthProtocol::V2), Ok(()) ); assert_eq!( - verify_pin_uv_auth_token(&[0x12; PIN_TOKEN_LENGTH], &message, &signature, 2), + verify_pin_uv_auth_token( + &[0x12; PIN_TOKEN_LENGTH], + &message, + &signature, + PinUvAuthProtocol::V2 + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( - verify_pin_uv_auth_token(&token, &[0xBB], &signature, 2), + verify_pin_uv_auth_token(&token, &[0xBB], &signature, PinUvAuthProtocol::V2), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); assert_eq!( - verify_pin_uv_auth_token(&token, &message, &[0x12; 32], 2), + verify_pin_uv_auth_token(&token, &message, &[0x12; 32], PinUvAuthProtocol::V2), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } - - #[test] - fn test_verify_pin_uv_auth_token_invalid_protocol() { - let token = [0x91; PIN_TOKEN_LENGTH]; - let message = [0xAA]; - let signature = []; - assert_eq!( - verify_pin_uv_auth_token(&token, &message, &signature, 3), - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - } } From aec1e0a4092007eaabfe2c7f0eb2c51625f8f5d5 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Thu, 18 Mar 2021 17:29:32 +0100 Subject: [PATCH 183/192] adds PIN protocol V2 to all commands (#295) --- src/ctap/client_pin.rs | 127 +++++++++++- src/ctap/command.rs | 50 +++-- src/ctap/config_command.rs | 71 ++++--- src/ctap/credential_management.rs | 137 ++++++------- src/ctap/data_formats.rs | 4 +- src/ctap/large_blobs.rs | 55 +++-- src/ctap/mod.rs | 324 ++++++++++++++++++++---------- src/ctap/pin_protocol.rs | 13 ++ 8 files changed, 520 insertions(+), 261 deletions(-) diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index 12e049c..a580b1b 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -90,14 +90,12 @@ fn check_and_store_new_pin( } #[cfg_attr(test, derive(IntoEnumIterator))] -// TODO remove when all variants are used -#[allow(dead_code)] pub enum PinPermission { // All variants should use integers with a single bit set. MakeCredential = 0x01, GetAssertion = 0x02, CredentialManagement = 0x04, - BioEnrollment = 0x08, + _BioEnrollment = 0x08, LargeBlobWrite = 0x10, AuthenticatorConfiguration = 0x20, } @@ -414,18 +412,19 @@ impl ClientPin { Ok(ResponseData::AuthenticatorClientPin(response)) } - /// Verifies the HMAC for the PIN protocol V1 pinUvAuthToken. - pub fn verify_pin_auth_token( + /// Verifies the HMAC for the pinUvAuthToken of the given version. + pub fn verify_pin_uv_auth_token( &self, hmac_contents: &[u8], pin_uv_auth_param: &[u8], + pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result<(), Ctap2StatusCode> { verify_pin_uv_auth_token( - self.get_pin_protocol(PinUvAuthProtocol::V1) + self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), hmac_contents, pin_uv_auth_param, - PinUvAuthProtocol::V1, + pin_uv_auth_protocol, ) } @@ -554,6 +553,7 @@ impl ClientPin { #[cfg(test)] mod test { + use super::super::pin_protocol::authenticate_pin_uv_auth_token; use super::*; use alloc::vec; use crypto::rng256::ThreadRng256; @@ -639,6 +639,48 @@ mod test { (client_pin, params) } + #[test] + fn test_mix_pin_protocols() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let pin_protocol_v1 = client_pin.get_pin_protocol(PinUvAuthProtocol::V1); + let pin_protocol_v2 = client_pin.get_pin_protocol(PinUvAuthProtocol::V2); + let message = vec![0xAA; 16]; + + let shared_secret_v1 = pin_protocol_v1 + .decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V1) + .unwrap(); + let shared_secret_v2 = pin_protocol_v2 + .decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V2) + .unwrap(); + let ciphertext = shared_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = shared_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + + let fake_secret_v1 = pin_protocol_v1 + .decapsulate(pin_protocol_v2.get_public_key(), PinUvAuthProtocol::V1) + .unwrap(); + let ciphertext = shared_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = fake_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = fake_secret_v1.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v1.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + + let fake_secret_v2 = pin_protocol_v2 + .decapsulate(pin_protocol_v1.get_public_key(), PinUvAuthProtocol::V2) + .unwrap(); + let ciphertext = shared_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = fake_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + let ciphertext = fake_secret_v2.encrypt(&mut rng, &message).unwrap(); + let plaintext = shared_secret_v2.decrypt(&ciphertext).unwrap(); + assert_ne!(&message, &plaintext); + } + fn test_helper_verify_pin_hash_enc(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); @@ -1310,6 +1352,77 @@ mod test { ); } + #[test] + fn test_verify_pin_uv_auth_token() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let message = [0xAA]; + + let pin_uv_auth_token_v1 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v1 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); + let pin_uv_auth_token_v2 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V2) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v2 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V2); + let pin_uv_auth_param_v1_from_v2_token = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v2, &message, PinUvAuthProtocol::V1); + let pin_uv_auth_param_v2_from_v1_token = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V2); + + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V1 + ), + Ok(()) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2, + PinUvAuthProtocol::V2 + ), + Ok(()) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V2 + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1_from_v2_token, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v2_from_v1_token, + PinUvAuthProtocol::V2 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_reset() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 748c33e..c0ecb2d 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -155,7 +155,7 @@ pub struct AuthenticatorMakeCredentialParameters { // Same for options, use defaults when not present. pub options: MakeCredentialOptions, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, pub enterprise_attestation: Option, } @@ -213,7 +213,9 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { .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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; let enterprise_attestation = enterprise_attestation.map(extract_unsigned).transpose()?; Ok(AuthenticatorMakeCredentialParameters { @@ -241,7 +243,7 @@ pub struct AuthenticatorGetAssertionParameters { // Same for options, use defaults when not present. pub options: GetAssertionOptions, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, } impl TryFrom for AuthenticatorGetAssertionParameters { @@ -288,7 +290,9 @@ impl TryFrom for AuthenticatorGetAssertionParameters { .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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; Ok(AuthenticatorGetAssertionParameters { rp_id, @@ -366,7 +370,7 @@ pub struct AuthenticatorLargeBlobsParameters { pub offset: usize, pub length: Option, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, } impl TryFrom for AuthenticatorLargeBlobsParameters { @@ -394,7 +398,9 @@ impl TryFrom for AuthenticatorLargeBlobsParameters { .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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; if get.is_none() && set.is_none() { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); @@ -439,7 +445,7 @@ pub struct AuthenticatorConfigParameters { pub sub_command: ConfigSubCommand, pub sub_command_params: Option, pub pin_uv_auth_param: Option>, - pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_protocol: Option, } impl TryFrom for AuthenticatorConfigParameters { @@ -463,7 +469,9 @@ impl TryFrom for AuthenticatorConfigParameters { _ => 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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; Ok(AuthenticatorConfigParameters { sub_command, @@ -507,8 +515,8 @@ impl TryFrom for AuthenticatorAttestationMaterial { pub struct AuthenticatorCredentialManagementParameters { pub sub_command: CredentialManagementSubCommand, pub sub_command_params: Option, - pub pin_uv_auth_protocol: Option, - pub pin_auth: Option>, + pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_param: Option>, } impl TryFrom for AuthenticatorCredentialManagementParameters { @@ -520,7 +528,7 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { 0x01 => sub_command, 0x02 => sub_command_params, 0x03 => pin_uv_auth_protocol, - 0x04 => pin_auth, + 0x04 => pin_uv_auth_param, } = extract_map(cbor_value)?; } @@ -528,14 +536,16 @@ impl TryFrom for AuthenticatorCredentialManagementParameters { let sub_command_params = sub_command_params .map(CredentialManagementSubCommandParameters::try_from) .transpose()?; - let pin_uv_auth_protocol = pin_uv_auth_protocol.map(extract_unsigned).transpose()?; - let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; + let pin_uv_auth_param = pin_uv_auth_param.map(extract_byte_string).transpose()?; Ok(AuthenticatorCredentialManagementParameters { sub_command, sub_command_params, pin_uv_auth_protocol, - pin_auth, + pin_uv_auth_param, }) } } @@ -630,7 +640,7 @@ mod test { extensions: MakeCredentialExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), enterprise_attestation: Some(2), }; @@ -677,7 +687,7 @@ mod test { extensions: GetAssertionExtensions::default(), options, pin_uv_auth_param: Some(vec![0x12, 0x34]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; assert_eq!( @@ -766,8 +776,8 @@ mod test { let expected_cred_management_parameters = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, sub_command_params: Some(params), - pin_uv_auth_protocol: Some(1), - pin_auth: Some(vec![0x9A; 16]), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param: Some(vec![0x9A; 16]), }; assert_eq!( @@ -821,7 +831,7 @@ mod test { offset: 0, length: Some(MIN_LARGE_BLOB_LEN), pin_uv_auth_param: Some(vec![0xA9]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; assert_eq!( returned_large_blobs_parameters, @@ -843,7 +853,7 @@ mod test { offset: 1, length: None, pin_uv_auth_param: Some(vec![0xA9]), - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; assert_eq!( returned_large_blobs_parameters, diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 3cbefd5..bdedc6b 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::client_pin::ClientPin; +use super::client_pin::{ClientPin, PinPermission}; use super::command::AuthenticatorConfigParameters; +use super::customization::ENTERPRISE_ATTESTATION_MODE; use super::data_formats::{ConfigSubCommand, ConfigSubCommandParams, SetMinPinLengthParams}; 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. @@ -91,10 +91,10 @@ pub fn process_config( _ => 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)?; - let auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_uv_auth_param = + pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_uv_auth_protocol = + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; // 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]); @@ -103,7 +103,12 @@ pub fn process_config( return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } - client_pin.verify_pin_auth_token(&config_data, &auth_param)?; + client_pin.verify_pin_uv_auth_token( + &config_data, + &pin_uv_auth_param, + pin_uv_auth_protocol, + )?; + client_pin.has_permission(PinPermission::AuthenticatorConfiguration)?; } match sub_command { @@ -127,6 +132,7 @@ mod test { use super::*; use crate::ctap::customization::ENFORCE_ALWAYS_UV; use crate::ctap::data_formats::PinUvAuthProtocol; + use crate::ctap::pin_protocol::authenticate_pin_uv_auth_token; use crypto::rng256::ThreadRng256; #[test] @@ -194,25 +200,24 @@ mod test { } } - #[test] - fn test_process_toggle_always_uv_with_pin() { + fn test_helper_process_toggle_always_uv_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { 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 client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); 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 mut config_data = vec![0xFF; 32]; + config_data.extend(&[0x0D, ConfigSubCommand::ToggleAlwaysUv as u8]); + let pin_uv_auth_param = + authenticate_pin_uv_auth_token(&pin_uv_auth_token, &config_data, pin_uv_auth_protocol); 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), + pin_uv_auth_param: Some(pin_uv_auth_param.clone()), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); if ENFORCE_ALWAYS_UV { @@ -228,14 +233,24 @@ mod test { let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::ToggleAlwaysUv, sub_command_params: None, - pin_uv_auth_param, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_param: Some(pin_uv_auth_param), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert!(!persistent_store.has_always_uv().unwrap()); } + #[test] + fn test_process_toggle_always_uv_with_pin_v1() { + test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_toggle_always_uv_with_pin_v2() { + test_helper_process_toggle_always_uv_with_pin(PinUvAuthProtocol::V2); + } + fn create_min_pin_config_params( min_pin_length: u8, min_pin_length_rp_ids: Option>, @@ -251,7 +266,7 @@ mod test { set_min_pin_length_params, )), pin_uv_auth_param: None, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), } } @@ -276,22 +291,22 @@ mod test { 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![ + let pin_uv_auth_param = 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); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param); let config_response = process_config(&mut persistent_store, &mut client_pin, 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![ + let pin_uv_auth_param = 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); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, @@ -329,11 +344,11 @@ mod test { 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![ + let pin_uv_auth_param = 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()); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone()); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(persistent_store.min_pin_length(), Ok(min_pin_length)); @@ -346,7 +361,7 @@ mod test { // 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()); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param.clone()); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, @@ -364,7 +379,7 @@ mod test { min_pin_length, Some(vec!["counter.example.com".to_string()]), ); - config_params.pin_uv_auth_param = Some(pin_auth); + config_params.pin_uv_auth_param = Some(pin_uv_auth_param); let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!( config_response, @@ -426,7 +441,7 @@ mod test { set_min_pin_length_params, )), pin_uv_auth_param, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; let config_response = process_config(&mut persistent_store, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); diff --git a/src/ctap/credential_management.rs b/src/ctap/credential_management.rs index bbfb320..b81648a 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -22,7 +22,7 @@ use super::data_formats::{ use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; -use super::{check_pin_uv_auth_protocol, StatefulCommand, StatefulPermission}; +use super::{StatefulCommand, StatefulPermission}; use alloc::collections::BTreeSet; use alloc::string::String; use alloc::vec; @@ -259,7 +259,7 @@ pub fn process_credential_management( sub_command, sub_command_params, pin_uv_auth_protocol, - pin_auth, + pin_uv_auth_param, } = cred_management_params; match (sub_command, stateful_command_permission.get_command()) { @@ -282,15 +282,21 @@ pub fn process_credential_management( | CredentialManagementSubCommand::EnumerateCredentialsBegin | CredentialManagementSubCommand::DeleteCredential | CredentialManagementSubCommand::UpdateUserInformation => { - check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; - let pin_auth = pin_auth.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_uv_auth_param = + pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; + let pin_uv_auth_protocol = + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; 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); } } - client_pin.verify_pin_auth_token(&management_data, &pin_auth)?; + client_pin.verify_pin_uv_auth_token( + &management_data, + &pin_uv_auth_param, + pin_uv_auth_protocol, + )?; // The RP ID permission is handled differently per subcommand below. client_pin.has_permission(PinPermission::CredentialManagement)?; } @@ -352,6 +358,7 @@ pub fn process_credential_management( #[cfg(test)] mod test { use super::super::data_formats::{PinUvAuthProtocol, PublicKeyCredentialType}; + use super::super::pin_protocol::authenticate_pin_uv_auth_token; use super::super::CtapState; use super::*; use crypto::rng256::{Rng256, ThreadRng256}; @@ -377,13 +384,12 @@ mod test { } } - #[test] - fn test_process_get_creds_metadata() { + fn test_helper_process_get_creds_metadata(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x55; 32]; let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); let credential_source = create_credential_source(&mut rng); let user_immediately_present = |_| Ok(()); @@ -391,16 +397,18 @@ mod test { ctap_state.client_pin = client_pin; ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ - 0xC5, 0xFB, 0x75, 0x55, 0x98, 0xB5, 0x19, 0x01, 0xB3, 0x31, 0x7D, 0xFE, 0x1D, 0xF5, - 0xFB, 0x00, - ]); + let management_data = vec![CredentialManagementSubCommand::GetCredsMetadata as u8]; + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &management_data, + pin_uv_auth_protocol, + ); let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth: pin_auth.clone(), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + pin_uv_auth_param: Some(pin_uv_auth_param.clone()), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -427,8 +435,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), + pin_uv_auth_param: Some(pin_uv_auth_param), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -449,6 +457,16 @@ mod test { }; } + #[test] + fn test_process_get_creds_metadata_v1() { + test_helper_process_get_creds_metadata(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_creds_metadata_v2() { + test_helper_process_get_creds_metadata(PinUvAuthProtocol::V2); + } + #[test] fn test_process_enumerate_rps_with_uv() { let mut rng = ThreadRng256 {}; @@ -474,7 +492,7 @@ mod test { .unwrap(); ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, 0xD0, 0xD1, ]); @@ -482,8 +500,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -507,7 +525,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -532,7 +550,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -571,7 +589,7 @@ mod test { } ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, 0xD0, 0xD1, ]); @@ -582,8 +600,8 @@ mod test { let mut cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; for _ in 0..NUM_CREDENTIALS { @@ -613,7 +631,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateRpsGetNextRp, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; } @@ -658,7 +676,7 @@ mod test { .unwrap(); ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28, 0x2B, 0x68, ]); @@ -673,8 +691,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::EnumerateCredentialsBegin, sub_command_params: Some(sub_command_params), - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -697,7 +715,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -721,7 +739,7 @@ mod test { sub_command: CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, sub_command_params: None, pin_uv_auth_protocol: None, - pin_auth: None, + pin_uv_auth_param: None, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -756,7 +774,7 @@ mod test { .unwrap(); ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89, 0x9C, 0x64, ]); @@ -774,8 +792,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::DeleteCredential, sub_command_params: Some(sub_command_params.clone()), - pin_uv_auth_protocol: Some(1), - pin_auth: pin_auth.clone(), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param: pin_uv_auth_param.clone(), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -792,8 +810,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::DeleteCredential, sub_command_params: Some(sub_command_params), - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -828,7 +846,7 @@ mod test { .unwrap(); ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); - let pin_auth = Some(vec![ + let pin_uv_auth_param = Some(vec![ 0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD, 0x9D, 0xB7, ]); @@ -852,8 +870,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::UpdateUserInformation, sub_command_params: Some(sub_command_params), - pin_uv_auth_protocol: Some(1), - pin_auth, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, @@ -882,44 +900,7 @@ mod test { } #[test] - fn test_process_credential_management_invalid_pin_uv_auth_protocol() { - let mut rng = ThreadRng256 {}; - let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); - let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); - - let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.client_pin = client_pin; - - ctap_state.persistent_store.set_pin(&[0u8; 16], 4).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_uv_auth_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.client_pin, - 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() { + fn test_process_credential_management_invalid_pin_uv_auth_param() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -929,8 +910,8 @@ mod test { let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, sub_command_params: None, - pin_uv_auth_protocol: Some(1), - pin_auth: Some(vec![0u8; 16]), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param: Some(vec![0u8; 16]), }; let cred_management_response = process_credential_management( &mut ctap_state.persistent_store, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 138dd3a..84fe721 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -816,8 +816,8 @@ impl TryFrom for ecdh::PubKey { #[derive(Clone, Copy, Debug, PartialEq)] pub enum PinUvAuthProtocol { - V1, - V2, + V1 = 1, + V2 = 2, } impl TryFrom for PinUvAuthProtocol { diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index d366ce4..ffb98cb 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::check_pin_uv_auth_protocol; use super::client_pin::{ClientPin, PinPermission}; use super::command::AuthenticatorLargeBlobsParameters; use super::response::{AuthenticatorLargeBlobsResponse, ResponseData}; @@ -91,17 +90,20 @@ impl LargeBlobs { 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. - // Change to Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED for None? - check_pin_uv_auth_protocol(pin_uv_auth_protocol)?; - client_pin.has_permission(PinPermission::LargeBlobWrite)?; - let mut message = vec![0xFF; 32]; - message.extend(&[0x0C, 0x00]); + let pin_uv_auth_protocol = + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + let mut large_blob_data = vec![0xFF; 32]; + large_blob_data.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())); - client_pin.verify_pin_auth_token(&message, &pin_uv_auth_param)?; + large_blob_data.extend(&offset_bytes); + large_blob_data.extend(&Sha256::hash(set.as_slice())); + client_pin.verify_pin_uv_auth_token( + &large_blob_data, + &pin_uv_auth_param, + pin_uv_auth_protocol, + )?; + client_pin.has_permission(PinPermission::LargeBlobWrite)?; } if offset + set.len() > self.expected_length { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); @@ -136,6 +138,7 @@ impl LargeBlobs { #[cfg(test)] mod test { use super::super::data_formats::PinUvAuthProtocol; + use super::super::pin_protocol::authenticate_pin_uv_auth_token; use super::*; use crypto::rng256::ThreadRng256; @@ -358,14 +361,13 @@ mod test { ); } - #[test] - fn test_process_command_commit_with_pin() { + fn test_helper_process_command_commit_with_pin(pin_uv_auth_protocol: PinUvAuthProtocol) { 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 client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -374,18 +376,23 @@ mod test { 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 mut large_blob_data = vec![0xFF; 32]; + // Command constant and offset bytes. + large_blob_data.extend(&[0x0C, 0x00, 0x00, 0x00, 0x00, 0x00]); + large_blob_data.extend(&Sha256::hash(&large_blob)); + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &large_blob_data, + pin_uv_auth_protocol, + ); 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), + pin_uv_auth_param: Some(pin_uv_auth_param), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let large_blobs_response = large_blobs.process_command(&mut persistent_store, &mut client_pin, large_blobs_params); @@ -394,4 +401,14 @@ mod test { Ok(ResponseData::AuthenticatorLargeBlobs(None)) ); } + + #[test] + fn test_process_command_commit_with_pin_v1() { + test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_command_commit_with_pin_v2() { + test_helper_process_command_commit_with_pin(PinUvAuthProtocol::V2); + } } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 8432f4e..83db5a9 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -44,9 +44,9 @@ use self::customization::{ }; use self::data_formats::{ AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, - GetAssertionExtensions, PackedAttestationStatement, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, SignatureAlgorithm, + GetAssertionExtensions, PackedAttestationStatement, PinUvAuthProtocol, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, + PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; @@ -109,9 +109,6 @@ 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 { @@ -119,16 +116,6 @@ pub const ES256_CRED_PARAM: PublicKeyCredentialParameter = PublicKeyCredentialPa alg: SignatureAlgorithm::ES256, }; -// 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(()), - _ => 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. @@ -526,7 +513,7 @@ where fn pin_uv_auth_precheck( &mut self, pin_uv_auth_param: &Option>, - pin_uv_auth_protocol: Option, + pin_uv_auth_protocol: Option, cid: ChannelID, ) -> Result<(), Ctap2StatusCode> { if let Some(auth_param) = &pin_uv_auth_param { @@ -539,11 +526,9 @@ where return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } } - - check_pin_uv_auth_protocol(pin_uv_auth_protocol) - } else { - Ok(()) + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; } + Ok(()) } fn process_make_credential( @@ -644,13 +629,16 @@ where // User verification depends on the PIN auth inputs, which are checked here. let ed_flag = if has_extension_output { ED_FLAG } else { 0 }; let flags = match pin_uv_auth_param { - Some(pin_auth) => { + Some(pin_uv_auth_param) => { if self.persistent_store.pin_hash()?.is_none() { // Specification is unclear, could be CTAP2_ERR_INVALID_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - self.client_pin - .verify_pin_auth_token(&client_data_hash, &pin_auth)?; + self.client_pin.verify_pin_uv_auth_token( + &client_data_hash, + &pin_uv_auth_param, + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; self.client_pin .has_permission(PinPermission::MakeCredential)?; self.client_pin.ensure_rp_id_permission(&rp_id)?; @@ -932,13 +920,16 @@ where // not support internal UV. User presence is requested as an option. let has_uv = pin_uv_auth_param.is_some(); let mut flags = match pin_uv_auth_param { - Some(pin_auth) => { + Some(pin_uv_auth_param) => { if self.persistent_store.pin_hash()?.is_none() { // Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - self.client_pin - .verify_pin_auth_token(&client_data_hash, &pin_auth)?; + self.client_pin.verify_pin_uv_auth_token( + &client_data_hash, + &pin_uv_auth_param, + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; self.client_pin .has_permission(PinPermission::GetAssertion)?; self.client_pin.ensure_rp_id_permission(&rp_id)?; @@ -1082,7 +1073,11 @@ where aaguid: self.persistent_store.aaguid()?, options: Some(options_map), max_msg_size: Some(MAX_MSG_SIZE as u64), - pin_protocols: Some(vec![PIN_PROTOCOL_VERSION]), + // The order implies preference. We favor the new V2. + pin_protocols: Some(vec![ + PinUvAuthProtocol::V2 as u64, + PinUvAuthProtocol::V1 as u64, + ]), 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), transports: Some(vec![AuthenticatorTransport::Usb]), @@ -1220,12 +1215,14 @@ where #[cfg(test)] mod test { - use super::command::AuthenticatorAttestationMaterial; + use super::client_pin::PIN_TOKEN_LENGTH; + use super::command::{AuthenticatorAttestationMaterial, AuthenticatorClientPinParameters}; use super::data_formats::{ - CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, MakeCredentialExtensions, - MakeCredentialOptions, PinUvAuthProtocol, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput, GetAssertionOptions, + MakeCredentialExtensions, MakeCredentialOptions, PinUvAuthProtocol, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; + use super::pin_protocol::{authenticate_pin_uv_auth_token, PinProtocol}; use super::*; use cbor::{cbor_array, cbor_array_vec, cbor_map}; use crypto::rng256::ThreadRng256; @@ -1316,7 +1313,7 @@ mod test { "alwaysUv" => false, }, 0x05 => MAX_MSG_SIZE as u64, - 0x06 => cbor_array_vec![vec![1]], + 0x06 => cbor_array_vec![vec![2, 1]], 0x07 => MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), 0x08 => CREDENTIAL_ID_SIZE as u64, 0x09 => cbor_array_vec![vec!["usb"]], @@ -1755,6 +1752,52 @@ mod test { assert_eq!(stored_credential.large_blob_key.unwrap(), large_blob_key); } + fn test_helper_process_make_credential_with_pin_and_uv( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x91; PIN_TOKEN_LENGTH]; + let client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.client_pin = client_pin; + ctap_state.persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + let client_data_hash = [0xCD]; + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &client_data_hash, + pin_uv_auth_protocol, + ); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.uv = true; + make_credential_params.pin_uv_auth_param = Some(pin_uv_auth_param); + make_credential_params.pin_uv_auth_protocol = Some(pin_uv_auth_protocol); + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + check_make_response( + make_credential_response, + 0x45, + &ctap_state.persistent_store.aaguid().unwrap(), + 0x20, + &[], + ); + } + + #[test] + fn test_process_make_credential_with_pin_and_uv_v1() { + test_helper_process_make_credential_with_pin_and_uv(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_make_credential_with_pin_and_uv_v2() { + test_helper_process_make_credential_with_pin_and_uv(PinUvAuthProtocol::V2); + } + #[test] fn test_non_resident_process_make_credential_with_pin() { let mut rng = ThreadRng256 {}; @@ -1810,7 +1853,7 @@ mod test { 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); + make_credential_params.pin_uv_auth_protocol = Some(PinUvAuthProtocol::V1); let make_credential_response = ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); assert_eq!( @@ -1951,10 +1994,62 @@ mod test { check_assertion_response(get_assertion_response, vec![0x1D], signature_counter, None); } - #[test] - fn test_process_get_assertion_hmac_secret() { + fn get_assertion_hmac_secret_params( + key_agreement_key: crypto::ecdh::SecKey, + key_agreement_response: ResponseData, + credential_id: Option>, + pin_uv_auth_protocol: PinUvAuthProtocol, + ) -> AuthenticatorGetAssertionParameters { let mut rng = ThreadRng256 {}; - let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let platform_public_key = key_agreement_key.genpk(); + let public_key = match key_agreement_response { + ResponseData::AuthenticatorClientPin(Some(client_pin_response)) => { + client_pin_response.key_agreement.unwrap() + } + _ => panic!("Invalid response type"), + }; + let pin_protocol = PinProtocol::new_test(key_agreement_key, [0x91; 32]); + let shared_secret = pin_protocol + .decapsulate(public_key, pin_uv_auth_protocol) + .unwrap(); + + let salt = vec![0x01; 32]; + let salt_enc = shared_secret.as_ref().encrypt(&mut rng, &salt).unwrap(); + let salt_auth = shared_secret.authenticate(&salt_enc); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: CoseKey::from(platform_public_key), + salt_enc, + salt_auth, + pin_uv_auth_protocol, + }; + let get_extensions = GetAssertionExtensions { + hmac_secret: Some(hmac_secret_input), + ..Default::default() + }; + + let credential_descriptor = credential_id.map(|key_id| PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id, + transports: None, + }); + let allow_list = credential_descriptor.map(|c| vec![c]); + AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list, + extensions: get_extensions, + options: GetAssertionOptions { + up: true, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + } + } + + fn test_helper_process_get_assertion_hmac_secret(pin_uv_auth_protocol: PinUvAuthProtocol) { + let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -1979,51 +2074,50 @@ mod test { _ => 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], - pin_uv_auth_protocol: PinUvAuthProtocol::V1, - }; - let get_extensions = GetAssertionExtensions { - hmac_secret: Some(hmac_secret_input), - ..Default::default() - }; - - 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, - }, + let client_pin_params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command: ClientPinSubCommand::GetKeyAgreement, + key_agreement: None, pin_uv_auth_param: None, - pin_uv_auth_protocol: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + permissions_rp_id: None, }; + let key_agreement_response = ctap_state.client_pin.process_command( + ctap_state.rng, + &mut ctap_state.persistent_store, + client_pin_params, + ); + let get_assertion_params = get_assertion_hmac_secret_params( + key_agreement_key, + key_agreement_response.unwrap(), + Some(credential_id), + pin_uv_auth_protocol, + ); 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_UNSUPPORTED_OPTION) - ); + assert!(get_assertion_response.is_ok()); } #[test] - fn test_resident_process_get_assertion_hmac_secret() { + fn test_process_get_assertion_hmac_secret_v1() { + test_helper_process_get_assertion_hmac_secret(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_assertion_hmac_secret_v2() { + test_helper_process_get_assertion_hmac_secret(PinUvAuthProtocol::V2); + } + + fn test_helper_resident_process_get_assertion_hmac_secret( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { let mut rng = ThreadRng256 {}; - let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -2037,40 +2131,43 @@ mod test { .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); - let pk = sk.genpk(); - let hmac_secret_input = GetAssertionHmacSecretInput { - key_agreement: CoseKey::from(pk), - salt_enc: vec![0x02; 32], - salt_auth: vec![0x03; 16], - pin_uv_auth_protocol: PinUvAuthProtocol::V1, - }; - let get_extensions = GetAssertionExtensions { - hmac_secret: Some(hmac_secret_input), - ..Default::default() - }; - - let get_assertion_params = AuthenticatorGetAssertionParameters { - rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], - allow_list: None, - extensions: get_extensions, - options: GetAssertionOptions { - up: false, - uv: false, - }, + let client_pin_params = AuthenticatorClientPinParameters { + pin_uv_auth_protocol, + sub_command: ClientPinSubCommand::GetKeyAgreement, + key_agreement: None, pin_uv_auth_param: None, - pin_uv_auth_protocol: None, + new_pin_enc: None, + pin_hash_enc: None, + permissions: None, + permissions_rp_id: None, }; + let key_agreement_response = ctap_state.client_pin.process_command( + ctap_state.rng, + &mut ctap_state.persistent_store, + client_pin_params, + ); + let get_assertion_params = get_assertion_hmac_secret_params( + key_agreement_key, + key_agreement_response.unwrap(), + None, + pin_uv_auth_protocol, + ); let get_assertion_response = ctap_state.process_get_assertion( get_assertion_params, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); + assert!(get_assertion_response.is_ok()); + } - assert_eq!( - get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION) - ); + #[test] + fn test_process_resident_get_assertion_hmac_secret_v1() { + test_helper_resident_process_get_assertion_hmac_secret(PinUvAuthProtocol::V1); + } + + #[test] + fn test_resident_process_get_assertion_hmac_secret_v2() { + test_helper_resident_process_get_assertion_hmac_secret(PinUvAuthProtocol::V2); } #[test] @@ -2315,13 +2412,14 @@ mod test { assert_eq!(large_blob_key, vec![0x1C; 32]); } - #[test] - fn test_process_get_next_assertion_two_credentials_with_uv() { + fn test_helper_process_get_next_assertion_two_credentials_with_uv( + pin_uv_auth_protocol: PinUvAuthProtocol, + ) { let mut rng = ThreadRng256 {}; let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); let pin_uv_auth_token = [0x88; 32]; let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); @@ -2352,22 +2450,24 @@ mod test { // 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, - ]); + let client_data_hash = vec![0xCD]; + let pin_uv_auth_param = authenticate_pin_uv_auth_token( + &pin_uv_auth_token, + &client_data_hash, + pin_uv_auth_protocol, + ); let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), - client_data_hash: vec![0xCD], + client_data_hash, allow_list: None, extensions: GetAssertionExtensions::default(), options: GetAssertionOptions { up: false, uv: true, }, - pin_uv_auth_param, - pin_uv_auth_protocol: Some(1), + pin_uv_auth_param: Some(pin_uv_auth_param), + pin_uv_auth_protocol: Some(pin_uv_auth_protocol), }; let get_assertion_response = ctap_state.process_get_assertion( get_assertion_params, @@ -2404,6 +2504,16 @@ mod test { ); } + #[test] + fn test_process_get_next_assertion_two_credentials_with_uv_v1() { + test_helper_process_get_next_assertion_two_credentials_with_uv(PinUvAuthProtocol::V1); + } + + #[test] + fn test_process_get_next_assertion_two_credentials_with_uv_v2() { + test_helper_process_get_next_assertion_two_credentials_with_uv(PinUvAuthProtocol::V2); + } + #[test] fn test_process_get_next_assertion_three_credentials_no_uv() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs index 912f7aa..44ae53d 100644 --- a/src/ctap/pin_protocol.rs +++ b/src/ctap/pin_protocol.rs @@ -94,6 +94,19 @@ impl PinProtocol { } } +/// Authenticates the pinUvAuthToken for the given PIN protocol. +#[cfg(test)] +pub fn authenticate_pin_uv_auth_token( + token: &[u8; PIN_TOKEN_LENGTH], + message: &[u8], + pin_uv_auth_protocol: PinUvAuthProtocol, +) -> Vec { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => hmac_256::(token, message)[..16].to_vec(), + PinUvAuthProtocol::V2 => hmac_256::(token, message).to_vec(), + } +} + /// Verifies the pinUvAuthToken for the given PIN protocol. pub fn verify_pin_uv_auth_token( token: &[u8; PIN_TOKEN_LENGTH], From 63232cfe60ced13c6abf6876715a02e83c540f73 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Mon, 22 Mar 2021 14:03:51 +0100 Subject: [PATCH 184/192] adds PIN token state with timeouts (#296) --- src/ctap/client_pin.rs | 384 +++++++++++++++++++++++++++++++-------- src/ctap/command.rs | 2 +- src/ctap/data_formats.rs | 6 +- src/ctap/mod.rs | 28 ++- src/ctap/token_state.rs | 277 ++++++++++++++++++++++++++++ src/main.rs | 4 +- 6 files changed, 616 insertions(+), 85 deletions(-) create mode 100644 src/ctap/token_state.rs diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index a580b1b..82e7904 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -20,6 +20,7 @@ use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; +use super::token_state::PinUvAuthTokenState; use alloc::boxed::Box; use alloc::str; use alloc::string::String; @@ -30,6 +31,7 @@ use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(test)] use enum_iterator::IntoEnumIterator; +use libtock_drivers::timer::ClockValue; use subtle::ConstantTimeEq; /// The prefix length of the PIN hash that is stored and compared. @@ -104,8 +106,7 @@ pub struct ClientPin { pin_protocol_v1: PinProtocol, pin_protocol_v2: PinProtocol, consecutive_pin_mismatches: u8, - permissions: u8, - permissions_rp_id: Option, + pin_uv_auth_token_state: PinUvAuthTokenState, } impl ClientPin { @@ -114,8 +115,7 @@ impl ClientPin { pin_protocol_v1: PinProtocol::new(rng), pin_protocol_v2: PinProtocol::new(rng), consecutive_pin_mismatches: 0, - permissions: 0, - permissions_rp_id: None, + pin_uv_auth_token_state: PinUvAuthTokenState::new(), } } @@ -290,6 +290,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let AuthenticatorClientPinParameters { pin_uv_auth_protocol, @@ -314,14 +315,17 @@ impl ClientPin { if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - let pin_token = shared_secret.encrypt( rng, self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), )?; - self.permissions = 0x03; - self.permissions_rp_id = None; + + self.pin_protocol_v1.reset_pin_uv_auth_token(rng); + self.pin_protocol_v2.reset_pin_uv_auth_token(rng); + self.pin_uv_auth_token_state + .begin_using_pin_uv_auth_token(now); + self.pin_uv_auth_token_state.set_default_permissions(); Ok(AuthenticatorClientPinResponse { key_agreement: None, @@ -350,6 +354,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, mut client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let permissions = ok_or_missing(client_pin_params.permissions)?; // Mutating client_pin_params is just an optimization to move it into @@ -364,10 +369,10 @@ impl ClientPin { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); } - let response = self.process_get_pin_token(rng, persistent_store, client_pin_params)?; - - self.permissions = permissions; - self.permissions_rp_id = permissions_rp_id; + let response = self.process_get_pin_token(rng, persistent_store, client_pin_params, now)?; + self.pin_uv_auth_token_state.set_permissions(permissions); + self.pin_uv_auth_token_state + .set_permissions_rp_id(permissions_rp_id); Ok(response) } @@ -378,6 +383,7 @@ impl ClientPin { rng: &mut impl Rng256, persistent_store: &mut PersistentStore, client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, ) -> Result { let response = match client_pin_params.sub_command { ClientPinSubCommand::GetPinRetries => { @@ -395,7 +401,7 @@ impl ClientPin { None } ClientPinSubCommand::GetPinToken => { - Some(self.process_get_pin_token(rng, persistent_store, client_pin_params)?) + Some(self.process_get_pin_token(rng, persistent_store, client_pin_params, now)?) } ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions => Some( self.process_get_pin_uv_auth_token_using_uv_with_permissions(client_pin_params)?, @@ -406,6 +412,7 @@ impl ClientPin { rng, persistent_store, client_pin_params, + now, )?, ), }; @@ -419,6 +426,9 @@ impl ClientPin { pin_uv_auth_param: &[u8], pin_uv_auth_protocol: PinUvAuthProtocol, ) -> Result<(), Ctap2StatusCode> { + if !self.pin_uv_auth_token_state.is_in_use() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } verify_pin_uv_auth_token( self.get_pin_protocol(pin_uv_auth_protocol) .get_pin_uv_auth_token(), @@ -435,8 +445,7 @@ impl ClientPin { self.pin_protocol_v2.regenerate(rng); self.pin_protocol_v2.reset_pin_uv_auth_token(rng); self.consecutive_pin_mismatches = 0; - self.permissions = 0; - self.permissions_rp_id = None; + self.pin_uv_auth_token_state.stop_using_pin_uv_auth_token(); } /// Verifies, computes and encrypts the HMAC-secret outputs. @@ -476,30 +485,43 @@ impl ClientPin { shared_secret.encrypt(rng, &output) } - /// 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 { + /// Consumes flags and permissions related to the pinUvAuthToken. + pub fn clear_token_flags(&mut self) { + self.pin_uv_auth_token_state.clear_user_verified_flag(); + self.pin_uv_auth_token_state + .clear_pin_uv_auth_token_permissions_except_lbw(); + } + + /// Updates the running timers, triggers timeout events. + pub fn update_timeouts(&mut self, now: ClockValue) { + self.pin_uv_auth_token_state + .pin_uv_auth_token_usage_timer_observer(now); + } + + /// Checks if user verification is cached for use of the pinUvAuthToken. + pub fn check_user_verified_flag(&mut self) -> Result<(), Ctap2StatusCode> { + if self.pin_uv_auth_token_state.get_user_verified_flag_value() { Ok(()) } else { Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) } } + /// Check if the required command's token permission is granted. + pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { + self.pin_uv_auth_token_state.has_permission(permission) + } + /// 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(()) + self.pin_uv_auth_token_state.has_no_permissions_rp_id() } /// 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(()), - } + self.pin_uv_auth_token_state + .has_no_permissions_rp_id() + .or_else(|_| self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id)) } /// Check if no RP ID is associated with the token permission, or it matches the hash. @@ -507,26 +529,28 @@ impl ClientPin { &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) - } - _ => Ok(()), - } + self.pin_uv_auth_token_state + .has_no_permissions_rp_id() + .or_else(|_| { + self.pin_uv_auth_token_state + .has_permissions_rp_id_hash(rp_id_hash) + }) } /// 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 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 => { - self.permissions_rp_id = Some(String::from(rp_id)); - Ok(()) - } - _ => Ok(()), + if self + .pin_uv_auth_token_state + .has_no_permissions_rp_id() + .is_ok() + { + self.pin_uv_auth_token_state + .set_permissions_rp_id(Some(String::from(rp_id))); + return Ok(()); } + self.pin_uv_auth_token_state.has_permissions_rp_id(rp_id) } #[cfg(test)] @@ -541,12 +565,16 @@ impl ClientPin { PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(&mut rng)), PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(&mut rng), key_agreement_key), }; + let mut pin_uv_auth_token_state = PinUvAuthTokenState::new(); + pin_uv_auth_token_state.set_permissions(0xFF); + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + pin_uv_auth_token_state.begin_using_pin_uv_auth_token(DUMMY_CLOCK_VALUE); ClientPin { pin_protocol_v1: PinProtocol::new_test(key_agreement_key_v1, pin_uv_auth_token), pin_protocol_v2: PinProtocol::new_test(key_agreement_key_v2, pin_uv_auth_token), consecutive_pin_mismatches: 0, - permissions: 0xFF, - permissions_rp_id: None, + pin_uv_auth_token_state, } } } @@ -557,6 +585,10 @@ mod test { use super::*; use alloc::vec; use crypto::rng256::ThreadRng256; + use libtock_drivers::timer::Duration; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); /// Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(persistent_store: &mut PersistentStore) { @@ -782,7 +814,7 @@ mod test { retries: Some(persistent_store.pin_retries().unwrap() as u64), }); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } @@ -810,7 +842,7 @@ mod test { retries: None, }); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); } @@ -831,7 +863,7 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Ok(ResponseData::AuthenticatorClientPin(None)) ); } @@ -865,14 +897,24 @@ mod test { let pin_uv_auth_param = shared_secret.authenticate(&auth_param_data); params.pin_uv_auth_param = Some(pin_uv_auth_param); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params.clone()), + client_pin.process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ), Ok(ResponseData::AuthenticatorClientPin(None)) ); let mut bad_params = params.clone(); bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); @@ -880,7 +922,7 @@ mod test { persistent_store.decr_pin_retries().unwrap(); } assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) ); } @@ -905,13 +947,41 @@ mod test { set_standard_pin(&mut persistent_store); assert!(client_pin - .process_command(&mut rng, &mut persistent_store, params.clone()) + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ) .is_ok()); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_no_permissions_rp_id(), + Ok(()) + ); let mut bad_params = params; bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -937,7 +1007,7 @@ mod test { assert_eq!(persistent_store.force_pin_change(), Ok(())); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), ); } @@ -964,32 +1034,65 @@ mod test { set_standard_pin(&mut persistent_store); assert!(client_pin - .process_command(&mut rng, &mut persistent_store, params.clone()) + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ) .is_ok()); - assert_eq!(client_pin.permissions, 0x03); assert_eq!( - client_pin.permissions_rp_id, - Some(String::from("example.com")) + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::MakeCredential), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::GetAssertion), + Ok(()) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) ); let mut bad_params = params.clone(); bad_params.permissions = Some(0x00); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); let mut bad_params = params.clone(); bad_params.permissions_rp_id = None; assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) ); let mut bad_params = params; bad_params.pin_hash_enc = Some(vec![0xEE; 16]); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, bad_params), + client_pin.process_command( + &mut rng, + &mut persistent_store, + bad_params, + DUMMY_CLOCK_VALUE + ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -1017,7 +1120,7 @@ mod test { assert_eq!(persistent_store.force_pin_change(), Ok(())); assert_eq!( - client_pin.process_command(&mut rng, &mut persistent_store, params), + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) ); } @@ -1275,14 +1378,21 @@ mod test { fn test_has_permission() { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); - client_pin.permissions = 0x7F; - for permission in PinPermission::into_enum_iter() { - assert_eq!(client_pin.has_permission(permission), Ok(())); - } - client_pin.permissions = 0x00; + client_pin.pin_uv_auth_token_state.set_permissions(0x7F); for permission in PinPermission::into_enum_iter() { assert_eq!( - client_pin.has_permission(permission), + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + client_pin.pin_uv_auth_token_state.set_permissions(0x00); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1293,8 +1403,9 @@ mod test { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!( client_pin.has_no_rp_id_permission(), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) @@ -1306,8 +1417,9 @@ mod test { let mut rng = ThreadRng256 {}; let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); assert_eq!( client_pin.has_no_or_rp_id_permission("another.example.com"), @@ -1324,8 +1436,9 @@ mod test { client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) ); - assert_eq!(client_pin.permissions_rp_id, None); - client_pin.permissions_rp_id = Some("example.com".to_string()); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some("example.com".to_string())); assert_eq!( client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), Ok(()) @@ -1342,12 +1455,14 @@ mod test { let mut client_pin = ClientPin::new(&mut rng); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - client_pin.permissions_rp_id, - Some(String::from("example.com")) + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) ); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( - client_pin.ensure_rp_id_permission("counter-example.com"), + client_pin.ensure_rp_id_permission("another.example.com"), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); } @@ -1355,8 +1470,11 @@ mod test { #[test] fn test_verify_pin_uv_auth_token() { let mut rng = ThreadRng256 {}; - let client_pin = ClientPin::new(&mut rng); + let mut client_pin = ClientPin::new(&mut rng); let message = [0xAA]; + client_pin + .pin_uv_auth_token_state + .begin_using_pin_uv_auth_token(DUMMY_CLOCK_VALUE); let pin_uv_auth_token_v1 = client_pin .get_pin_protocol(PinUvAuthProtocol::V1) @@ -1423,6 +1541,28 @@ mod test { ); } + #[test] + fn test_verify_pin_uv_auth_token_not_in_use() { + let mut rng = ThreadRng256 {}; + let client_pin = ClientPin::new(&mut rng); + let message = [0xAA]; + + let pin_uv_auth_token_v1 = client_pin + .get_pin_protocol(PinUvAuthProtocol::V1) + .get_pin_uv_auth_token(); + let pin_uv_auth_param_v1 = + authenticate_pin_uv_auth_token(&pin_uv_auth_token_v1, &message, PinUvAuthProtocol::V1); + + assert_eq!( + client_pin.verify_pin_uv_auth_token( + &message, + &pin_uv_auth_param_v1, + PinUvAuthProtocol::V1 + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + #[test] fn test_reset() { let mut rng = ThreadRng256 {}; @@ -1431,8 +1571,10 @@ mod test { let public_key_v2 = client_pin.pin_protocol_v2.get_public_key(); let token_v1 = *client_pin.pin_protocol_v1.get_pin_uv_auth_token(); let token_v2 = *client_pin.pin_protocol_v2.get_pin_uv_auth_token(); - client_pin.permissions = 0xFF; - client_pin.permissions_rp_id = Some(String::from("example.com")); + client_pin.pin_uv_auth_token_state.set_permissions(0xFF); + client_pin + .pin_uv_auth_token_state + .set_permissions_rp_id(Some(String::from("example.com"))); client_pin.reset(&mut rng); assert_ne!(public_key_v1, client_pin.pin_protocol_v1.get_public_key()); assert_ne!(public_key_v2, client_pin.pin_protocol_v2.get_public_key()); @@ -1452,4 +1594,94 @@ mod test { } assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); } + + #[test] + fn test_update_timeouts() { + let (mut client_pin, mut params) = create_client_pin_and_parameters( + PinUvAuthProtocol::V2, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + params.permissions = Some(0xFF); + + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE) + .is_ok()); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Ok(()) + ); + + let timeout = DUMMY_CLOCK_VALUE.wrapping_add(Duration::from_ms(30001)); + client_pin.update_timeouts(timeout); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_clear_token_flags() { + let (mut client_pin, mut params) = create_client_pin_and_parameters( + PinUvAuthProtocol::V2, + ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, + ); + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + set_standard_pin(&mut persistent_store); + params.permissions = Some(0xFF); + + assert!(client_pin + .process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE) + .is_ok()); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(permission), + Ok(()) + ); + } + assert_eq!(client_pin.check_user_verified_flag(), Ok(())); + + client_pin.clear_token_flags(); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::CredentialManagement), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + client_pin + .pin_uv_auth_token_state + .has_permission(PinPermission::LargeBlobWrite), + Ok(()) + ); + assert_eq!( + client_pin.check_user_verified_flag(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } } diff --git a/src/ctap/command.rs b/src/ctap/command.rs index c0ecb2d..4616b02 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -143,7 +143,7 @@ impl Command { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorMakeCredentialParameters { pub client_data_hash: Vec, pub rp: PublicKeyCredentialRpEntity, diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 84fe721..1721e62 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -148,7 +148,7 @@ impl TryFrom for PublicKeyCredentialType { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialparameters -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialParameter { pub cred_type: PublicKeyCredentialType, pub alg: SignatureAlgorithm, @@ -387,7 +387,7 @@ impl TryFrom for GetAssertionHmacSecretInput { } // Even though options are optional, we can use the default if not present. -#[derive(Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct MakeCredentialOptions { pub rk: bool, pub uv: bool, @@ -484,7 +484,7 @@ impl From for cbor::Value { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum SignatureAlgorithm { ES256 = ES256_ALGORITHM as isize, // This is the default for all numbers not covered above. diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 83db5a9..46d805b 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -29,6 +29,7 @@ pub mod response; pub mod status_code; mod storage; mod timed_permission; +mod token_state; use self::client_pin::{ClientPin, PinPermission}; use self::command::{ @@ -301,11 +302,12 @@ where } } - pub fn update_command_permission(&mut self, now: ClockValue) { + pub fn update_timeouts(&mut self, now: ClockValue) { // Ignore the result, just update. let _ = self .stateful_command_permission .check_command_permission(now); + self.client_pin.update_timeouts(now); } pub fn increment_global_signature_counter(&mut self) -> Result<(), Ctap2StatusCode> { @@ -465,6 +467,7 @@ where self.rng, &mut self.persistent_store, params, + now, ), Command::AuthenticatorReset => self.process_reset(cid, now), Command::AuthenticatorCredentialManagement(params) => { @@ -641,6 +644,10 @@ where )?; self.client_pin .has_permission(PinPermission::MakeCredential)?; + self.client_pin.check_user_verified_flag()?; + // Checking for the correct permissions_rp_id is specified earlier. + // Error codes are identical though, so the implementation can be identical with + // GetAssertion. self.client_pin.ensure_rp_id_permission(&rp_id)?; UP_FLAG | UV_FLAG | AT_FLAG | ed_flag } @@ -660,6 +667,7 @@ where }; (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); let sk = crypto::ecdsa::SecKey::gensk(self.rng); let pk = sk.genpk(); @@ -932,6 +940,10 @@ where )?; self.client_pin .has_permission(PinPermission::GetAssertion)?; + // Checking for the UV flag is specified earlier for GetAssertion. + // Error codes are identical though, so the implementation can be identical with + // MakeCredential. + self.client_pin.check_user_verified_flag()?; self.client_pin.ensure_rp_id_permission(&rp_id)?; UV_FLAG } @@ -987,6 +999,7 @@ where // For CTAP 2.1, it was moved to a later protocol step. if options.up { (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); } let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; @@ -1777,7 +1790,7 @@ mod test { make_credential_params.pin_uv_auth_param = Some(pin_uv_auth_param); make_credential_params.pin_uv_auth_protocol = Some(pin_uv_auth_protocol); let make_credential_response = - ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + ctap_state.process_make_credential(make_credential_params.clone(), DUMMY_CHANNEL_ID); check_make_response( make_credential_response, @@ -1786,6 +1799,13 @@ mod test { 0x20, &[], ); + + 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] @@ -2088,6 +2108,7 @@ mod test { ctap_state.rng, &mut ctap_state.persistent_store, client_pin_params, + DUMMY_CLOCK_VALUE, ); let get_assertion_params = get_assertion_hmac_secret_params( key_agreement_key, @@ -2145,6 +2166,7 @@ mod test { ctap_state.rng, &mut ctap_state.persistent_store, client_pin_params, + DUMMY_CLOCK_VALUE, ); let get_assertion_params = get_assertion_hmac_secret_params( key_agreement_key, @@ -2423,7 +2445,6 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); - ctap_state.client_pin = client_pin; let mut make_credential_params = create_minimal_make_credential_parameters(); let user1 = PublicKeyCredentialUserEntity { @@ -2448,6 +2469,7 @@ mod test { .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); + ctap_state.client_pin = client_pin; // The PIN length is outside of the test scope and most likely incorrect. ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); let client_data_hash = vec![0xCD]; diff --git a/src/ctap/token_state.rs b/src/ctap/token_state.rs new file mode 100644 index 0000000..fea0f3c --- /dev/null +++ b/src/ctap/token_state.rs @@ -0,0 +1,277 @@ +// 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 crate::ctap::client_pin::PinPermission; +use crate::ctap::status_code::Ctap2StatusCode; +use crate::ctap::timed_permission::TimedPermission; +use alloc::string::String; +use crypto::sha256::Sha256; +use crypto::Hash256; +use libtock_drivers::timer::{ClockValue, Duration}; + +/// Timeout for auth tokens. +/// +/// This usage time limit is correct for USB, BLE, and internal. +/// NFC only allows 19.8 seconds. +/// TODO(#15) multiplex over transports, add NFC +const INITIAL_USAGE_TIME_LIMIT: Duration = Duration::from_ms(30000); + +/// Implements pinUvAuthToken state from section 6.5.2.1. +/// +/// The userPresent flag is omitted as the only way to set it to true is +/// built-in user verification. Therefore, we never cache user presence. +/// +/// This implementation does not use a rolling timer. +pub struct PinUvAuthTokenState { + // Relies on the fact that all permissions are represented by powers of two. + permissions_set: u8, + permissions_rp_id: Option, + usage_timer: TimedPermission, + user_verified: bool, + in_use: bool, +} + +impl PinUvAuthTokenState { + /// Creates a pinUvAuthToken state without permissions. + pub fn new() -> PinUvAuthTokenState { + PinUvAuthTokenState { + permissions_set: 0, + permissions_rp_id: None, + usage_timer: TimedPermission::waiting(), + user_verified: false, + in_use: false, + } + } + + /// Returns whether the pinUvAuthToken is active. + pub fn is_in_use(&self) -> bool { + self.in_use + } + + /// Checks if the permission is granted. + pub fn has_permission(&self, permission: PinPermission) -> Result<(), Ctap2StatusCode> { + if permission as u8 & self.permissions_set != 0 { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + } + } + + /// Checks if there is no associated permissions RPID. + pub fn has_no_permissions_rp_id(&self) -> Result<(), Ctap2StatusCode> { + if self.permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); + } + Ok(()) + } + + /// Checks if the permissions RPID is associated. + pub fn has_permissions_rp_id(&self, rp_id: &str) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id == p => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } + } + + /// Checks if the permissions RPID's association matches the hash. + pub fn has_permissions_rp_id_hash(&self, rp_id_hash: &[u8]) -> Result<(), Ctap2StatusCode> { + match &self.permissions_rp_id { + Some(p) if rp_id_hash == Sha256::hash(p.as_bytes()) => Ok(()), + _ => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + } + } + + /// Sets the permissions, represented as bits in a byte. + pub fn set_permissions(&mut self, permissions: u8) { + self.permissions_set = permissions; + } + + /// Sets the permissions RPID. + pub fn set_permissions_rp_id(&mut self, permissions_rp_id: Option) { + self.permissions_rp_id = permissions_rp_id; + } + + /// Sets the default permissions. + /// + /// Allows MakeCredential and GetAssertion, without specifying a RP ID. + pub fn set_default_permissions(&mut self) { + self.set_permissions(0x03); + self.set_permissions_rp_id(None); + } + + /// Starts the timer for pinUvAuthToken usage. + pub fn begin_using_pin_uv_auth_token(&mut self, now: ClockValue) { + self.user_verified = true; + self.usage_timer = TimedPermission::granted(now, INITIAL_USAGE_TIME_LIMIT); + self.in_use = true; + } + + /// Updates the usage timer, and disables the pinUvAuthToken on timeout. + pub fn pin_uv_auth_token_usage_timer_observer(&mut self, now: ClockValue) { + if !self.in_use { + return; + } + self.usage_timer = self.usage_timer.check_expiration(now); + if !self.usage_timer.is_granted(now) { + self.stop_using_pin_uv_auth_token(); + } + } + + /// Returns whether the user is verified. + pub fn get_user_verified_flag_value(&self) -> bool { + self.in_use && self.user_verified + } + + /// Consumes the user verification. + pub fn clear_user_verified_flag(&mut self) { + self.user_verified = false; + } + + /// Clears all permissions except Large Blob Write. + pub fn clear_pin_uv_auth_token_permissions_except_lbw(&mut self) { + self.permissions_set &= PinPermission::LargeBlobWrite as u8; + } + + /// Resets to the initial state. + pub fn stop_using_pin_uv_auth_token(&mut self) { + self.permissions_rp_id = None; + self.permissions_set = 0; + self.usage_timer = TimedPermission::waiting(); + self.user_verified = false; + self.in_use = false; + } +} + +#[cfg(test)] +mod test { + use super::*; + use enum_iterator::IntoEnumIterator; + + const CLOCK_FREQUENCY_HZ: usize = 32768; + const START_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); + const SMALL_DURATION: Duration = Duration::from_ms(100); + + #[test] + fn test_observer() { + let mut token_state = PinUvAuthTokenState::new(); + let mut now = START_CLOCK_VALUE; + token_state.begin_using_pin_uv_auth_token(now); + assert!(token_state.is_in_use()); + now = now.wrapping_add(SMALL_DURATION); + token_state.pin_uv_auth_token_usage_timer_observer(now); + assert!(token_state.is_in_use()); + now = now.wrapping_add(INITIAL_USAGE_TIME_LIMIT); + token_state.pin_uv_auth_token_usage_timer_observer(now); + assert!(!token_state.is_in_use()); + } + + #[test] + fn test_stop() { + let mut token_state = PinUvAuthTokenState::new(); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.is_in_use()); + token_state.stop_using_pin_uv_auth_token(); + assert!(!token_state.is_in_use()); + } + + #[test] + fn test_permissions() { + let mut token_state = PinUvAuthTokenState::new(); + token_state.set_permissions(0xFF); + for permission in PinPermission::into_enum_iter() { + assert_eq!(token_state.has_permission(permission), Ok(())); + } + token_state.clear_pin_uv_auth_token_permissions_except_lbw(); + assert_eq!( + token_state.has_permission(PinPermission::CredentialManagement), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permission(PinPermission::LargeBlobWrite), + Ok(()) + ); + token_state.stop_using_pin_uv_auth_token(); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + token_state.has_permission(permission), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + } + + #[test] + fn test_permissions_rp_id_none() { + let mut token_state = PinUvAuthTokenState::new(); + let example_hash = Sha256::hash(b"example.com"); + token_state.set_permissions_rp_id(None); + assert_eq!(token_state.has_no_permissions_rp_id(), Ok(())); + assert_eq!( + token_state.has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_permissions_rp_id_some() { + let mut token_state = PinUvAuthTokenState::new(); + let example_hash = Sha256::hash(b"example.com"); + token_state.set_permissions_rp_id(Some(String::from("example.com"))); + + assert_eq!( + token_state.has_no_permissions_rp_id(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!(token_state.has_permissions_rp_id("example.com"), Ok(())); + assert_eq!( + token_state.has_permissions_rp_id("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Ok(()) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&[0x1D; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + + token_state.stop_using_pin_uv_auth_token(); + assert_eq!( + token_state.has_permissions_rp_id("example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + token_state.has_permissions_rp_id_hash(&example_hash), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_user_verified_flag() { + let mut token_state = PinUvAuthTokenState::new(); + assert!(!token_state.get_user_verified_flag_value()); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.get_user_verified_flag_value()); + token_state.clear_user_verified_flag(); + assert!(!token_state.get_user_verified_flag_value()); + token_state.begin_using_pin_uv_auth_token(START_CLOCK_VALUE); + assert!(token_state.get_user_verified_flag_value()); + token_state.stop_using_pin_uv_auth_token(); + assert!(!token_state.get_user_verified_flag_value()); + } +} diff --git a/src/main.rs b/src/main.rs index 2ca12a8..202a1ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,8 +120,8 @@ 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.update_command_permission(now); + // don't cause problems with timers. + ctap_state.update_timeouts(now); ctap_hid.wink_permission = ctap_hid.wink_permission.check_expiration(now); if has_packet { From c596f785fff92d7b96a472b1e09ae230afd6518c Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Tue, 23 Mar 2021 12:07:15 +0100 Subject: [PATCH 185/192] Output parameters for CTAP2.1 (#297) * finalizes output parameters for CTAP2.1 * explanation for internal UV --- src/ctap/client_pin.rs | 24 +++++++++++++++++++++- src/ctap/mod.rs | 1 + src/ctap/response.rs | 45 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index 82e7904..96c3f02 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -200,6 +200,7 @@ impl ClientPin { key_agreement: None, pin_token: None, retries: Some(persistent_store.pin_retries()? as u64), + power_cycle_state: Some(self.consecutive_pin_mismatches >= 3), }) } @@ -215,6 +216,7 @@ impl ClientPin { key_agreement, pin_token: None, retries: None, + power_cycle_state: None, }) } @@ -331,6 +333,7 @@ impl ClientPin { key_agreement: None, pin_token: Some(pin_token), retries: None, + power_cycle_state: None, }) } @@ -812,6 +815,24 @@ mod test { key_agreement: None, pin_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), + power_cycle_state: Some(false), + }); + assert_eq!( + client_pin.process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE + ), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) + ); + + client_pin.consecutive_pin_mismatches = 3; + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: None, + pin_token: None, + retries: Some(persistent_store.pin_retries().unwrap() as u64), + power_cycle_state: Some(true), }); assert_eq!( client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), @@ -840,6 +861,7 @@ mod test { key_agreement: params.key_agreement.clone(), pin_token: None, retries: None, + power_cycle_state: None, }); assert_eq!( client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), @@ -1266,7 +1288,7 @@ mod test { let salt_enc = vec![0x01; 32]; let mut salt_auth = shared_secret.authenticate(&salt_enc); - salt_auth[0] = 0x00; + salt_auth[0] ^= 0x01; let hmac_secret_input = GetAssertionHmacSecretInput { key_agreement: client_pin .get_pin_protocol(pin_uv_auth_protocol) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 46d805b..d47a248 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -1101,6 +1101,7 @@ where firmware_version: 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), + certifications: None, remaining_discoverable_credentials: Some( self.persistent_store.remaining_credentials()? as u64, ), diff --git a/src/ctap/response.rs b/src/ctap/response.rs index b6b3d25..ddc186b 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -20,7 +20,7 @@ use super::data_formats::{ use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; -use cbor::{cbor_array_vec, cbor_bool, cbor_map_btree, cbor_map_options, cbor_text}; +use cbor::{cbor_array_vec, cbor_bool, cbor_int, cbor_map_btree, cbor_map_options, cbor_text}; #[derive(Debug, PartialEq)] pub enum ResponseData { @@ -92,6 +92,7 @@ pub struct AuthenticatorGetAssertionResponse { pub signature: Vec, pub user: Option, pub number_of_credentials: Option, + // 0x06: userSelected missing as we don't support displays. pub large_blob_key: Option>, } @@ -135,7 +136,14 @@ pub struct AuthenticatorGetInfoResponse { pub firmware_version: Option, pub max_cred_blob_length: Option, pub max_rp_ids_for_set_min_pin_length: Option, + // Missing response fields as they are only relevant for internal UV: + // - 0x11: preferredPlatformUvAttempts + // - 0x12: uvModality + // Add them when your hardware supports any kind of user verification within + // the boundary of the device, e.g. fingerprint or built-in keyboard. + pub certifications: Option>, pub remaining_discoverable_credentials: Option, + // - 0x15: vendorPrototypeConfigCommands missing as we don't support it. } impl From for cbor::Value { @@ -157,15 +165,24 @@ impl From for cbor::Value { firmware_version, max_cred_blob_length, max_rp_ids_for_set_min_pin_length, + certifications, remaining_discoverable_credentials, } = get_info_response; let options_cbor: Option = options.map(|options| { - let option_map: BTreeMap<_, _> = options + let options_map: BTreeMap<_, _> = options .into_iter() .map(|(key, value)| (cbor_text!(key), cbor_bool!(value))) .collect(); - cbor_map_btree!(option_map) + cbor_map_btree!(options_map) + }); + + let certifications_cbor: Option = certifications.map(|certifications| { + let certifications_map: BTreeMap<_, _> = certifications + .into_iter() + .map(|(key, value)| (cbor_text!(key), cbor_int!(value))) + .collect(); + cbor_map_btree!(certifications_map) }); cbor_map_options! { @@ -185,6 +202,7 @@ impl From for cbor::Value { 0x0E => firmware_version, 0x0F => max_cred_blob_length, 0x10 => max_rp_ids_for_set_min_pin_length, + 0x13 => certifications_cbor, 0x14 => remaining_discoverable_credentials, } } @@ -195,6 +213,8 @@ pub struct AuthenticatorClientPinResponse { pub key_agreement: Option, pub pin_token: Option>, pub retries: Option, + pub power_cycle_state: Option, + // - 0x05: uvRetries missing as we don't support internal UV. } impl From for cbor::Value { @@ -203,12 +223,14 @@ impl From for cbor::Value { key_agreement, pin_token, retries, + power_cycle_state, } = client_pin_response; cbor_map_options! { 0x01 => key_agreement.map(cbor::Value::from), 0x02 => pin_token, 0x03 => retries, + 0x04 => power_cycle_state, } } } @@ -401,6 +423,7 @@ mod test { firmware_version: None, max_cred_blob_length: None, max_rp_ids_for_set_min_pin_length: None, + certifications: None, remaining_discoverable_credentials: None, }; let response_cbor: Option = @@ -417,6 +440,8 @@ mod test { fn test_get_info_optionals_into_cbor() { let mut options_map = BTreeMap::new(); options_map.insert(String::from("rk"), true); + let mut certifications_map = BTreeMap::new(); + certifications_map.insert(String::from("example-cert"), 1); let get_info_response = AuthenticatorGetInfoResponse { versions: vec!["FIDO_2_0".to_string()], extensions: Some(vec!["extension".to_string()]), @@ -434,6 +459,7 @@ mod test { firmware_version: Some(0), max_cred_blob_length: Some(1024), max_rp_ids_for_set_min_pin_length: Some(8), + certifications: Some(certifications_map), remaining_discoverable_credentials: Some(150), }; let response_cbor: Option = @@ -455,6 +481,7 @@ mod test { 0x0E => 0, 0x0F => 1024, 0x10 => 8, + 0x13 => cbor_map! {"example-cert" => 1}, 0x14 => 150, }; assert_eq!(response_cbor, Some(expected_cbor)); @@ -462,15 +489,23 @@ mod test { #[test] fn test_used_client_pin_into_cbor() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let pk = sk.genpk(); + let cose_key = CoseKey::from(pk); let client_pin_response = AuthenticatorClientPinResponse { - key_agreement: None, + key_agreement: Some(cose_key.clone()), pin_token: Some(vec![70]), - retries: None, + retries: Some(8), + power_cycle_state: Some(false), }; let response_cbor: Option = ResponseData::AuthenticatorClientPin(Some(client_pin_response)).into(); let expected_cbor = cbor_map_options! { + 0x01 => cbor::Value::from(cose_key), 0x02 => vec![70], + 0x03 => 8, + 0x04 => false, }; assert_eq!(response_cbor, Some(expected_cbor)); } From e7797a5683f4818cc3a90f3db353887911e337b6 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:41:20 +0200 Subject: [PATCH 186/192] Separate file crypto wrappers, starting with AES-CBC (#298) * refactor key wrapping with tests * remove backwards compatiblity tests * adds AES-CBC tests for IV and RNG --- src/ctap/crypto_wrapper.rs | 147 +++++++++++++++++++++++++++++++++++++ src/ctap/mod.rs | 43 +++-------- src/ctap/pin_protocol.rs | 58 +-------------- 3 files changed, 157 insertions(+), 91 deletions(-) create mode 100644 src/ctap/crypto_wrapper.rs diff --git a/src/ctap/crypto_wrapper.rs b/src/ctap/crypto_wrapper.rs new file mode 100644 index 0000000..2587e76 --- /dev/null +++ b/src/ctap/crypto_wrapper.rs @@ -0,0 +1,147 @@ +// 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 crate::ctap::status_code::Ctap2StatusCode; +use alloc::vec; +use alloc::vec::Vec; +use crypto::cbc::{cbc_decrypt, cbc_encrypt}; +use crypto::rng256::Rng256; + +/// Wraps the AES256-CBC encryption to match what we need in CTAP. +pub fn aes256_cbc_encrypt( + rng: &mut dyn Rng256, + aes_enc_key: &crypto::aes256::EncryptionKey, + plaintext: &[u8], + embeds_iv: bool, +) -> Result, Ctap2StatusCode> { + if plaintext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let iv = if embeds_iv { + let random_bytes = rng.gen_uniform_u8x32(); + *array_ref!(random_bytes, 0, 16) + } else { + [0u8; 16] + }; + let mut blocks = Vec::with_capacity(plaintext.len() / 16); + // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. + for block in plaintext.chunks_exact(16) { + blocks.push(*array_ref!(block, 0, 16)); + } + cbc_encrypt(aes_enc_key, iv, &mut blocks); + let mut ciphertext = if embeds_iv { iv.to_vec() } else { vec![] }; + ciphertext.extend(blocks.iter().flatten()); + Ok(ciphertext) +} + +/// Wraps the AES256-CBC decryption to match what we need in CTAP. +pub fn aes256_cbc_decrypt( + aes_enc_key: &crypto::aes256::EncryptionKey, + ciphertext: &[u8], + embeds_iv: bool, +) -> Result, Ctap2StatusCode> { + if ciphertext.len() % 16 != 0 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + let mut block_len = ciphertext.len() / 16; + // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. + let mut block_iter = ciphertext.chunks_exact(16); + let iv = if embeds_iv { + block_len -= 1; + let iv_block = block_iter + .next() + .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + *array_ref!(iv_block, 0, 16) + } else { + [0u8; 16] + }; + let mut blocks = Vec::with_capacity(block_len); + for block in block_iter { + blocks.push(*array_ref!(block, 0, 16)); + } + let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key); + cbc_decrypt(&aes_dec_key, iv, &mut blocks); + Ok(blocks.iter().flatten().cloned().collect::>()) +} + +#[cfg(test)] +mod test { + use super::*; + use crypto::rng256::ThreadRng256; + + #[test] + fn test_encrypt_decrypt_with_iv() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap(); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_encrypt_decrypt_without_iv() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, false).unwrap(); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, false).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_correct_iv_usage() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let mut ciphertext_no_iv = + aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, false).unwrap(); + let mut ciphertext_with_iv = vec![0u8; 16]; + ciphertext_with_iv.append(&mut ciphertext_no_iv); + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext_with_iv, true).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_iv_manipulation_property() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let mut ciphertext = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap(); + let mut expected_plaintext = plaintext; + for i in 0..16 { + ciphertext[i] ^= 0xBB; + expected_plaintext[i] ^= 0xBB; + } + let decrypted = aes256_cbc_decrypt(&aes_enc_key, &ciphertext, true).unwrap(); + assert_eq!(decrypted, expected_plaintext); + } + + #[test] + fn test_chaining() { + let mut rng = ThreadRng256 {}; + let aes_enc_key = crypto::aes256::EncryptionKey::new(&[0xC2; 32]); + let plaintext = vec![0xAA; 64]; + let ciphertext1 = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap(); + let ciphertext2 = aes256_cbc_encrypt(&mut rng, &aes_enc_key, &plaintext, true).unwrap(); + assert_eq!(ciphertext1.len(), 80); + assert_eq!(ciphertext2.len(), 80); + // The ciphertext should mutate in all blocks with a different IV. + let block_iter1 = ciphertext1.chunks_exact(16); + let block_iter2 = ciphertext2.chunks_exact(16); + for (block1, block2) in block_iter1.zip(block_iter2) { + assert_ne!(block1, block2); + } + } +} diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index d47a248..5d07789 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -17,6 +17,7 @@ mod client_pin; pub mod command; mod config_command; mod credential_management; +mod crypto_wrapper; #[cfg(feature = "with_ctap1")] mod ctap1; mod customization; @@ -38,6 +39,7 @@ use self::command::{ }; use self::config_command::process_config; use self::credential_management::process_credential_management; +use self::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt}; use self::customization::{ DEFAULT_CRED_PROTECT, ENTERPRISE_ATTESTATION_MODE, ENTERPRISE_RP_ID_LIST, MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, @@ -71,7 +73,6 @@ use cbor::cbor_map_options; use core::convert::TryFrom; #[cfg(feature = "debug_ctap")] use core::fmt::Write; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; use crypto::hmac::{hmac_256, verify_hmac_256}; use crypto::rng256::Rng256; use crypto::sha256::Sha256; @@ -338,23 +339,11 @@ where ) -> Result, Ctap2StatusCode> { let master_keys = self.persistent_store.master_keys()?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - let mut sk_bytes = [0; 32]; - private_key.to_bytes(&mut sk_bytes); - let mut iv = [0; 16]; - iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); + let mut plaintext = [0; 64]; + private_key.to_bytes(array_mut_ref!(plaintext, 0, 32)); + plaintext[32..64].copy_from_slice(application); - 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(0x70); - encrypted_id.extend(&iv); - for b in &blocks { - encrypted_id.extend(b); - } + let mut encrypted_id = aes256_cbc_encrypt(self.rng, &aes_enc_key, &plaintext, true)?; let id_hmac = hmac_256::(&master_keys.hmac, &encrypted_id[..]); encrypted_id.extend(&id_hmac); Ok(encrypted_id) @@ -381,26 +370,12 @@ where return Ok(None); } let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); - 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 { - blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); - } - cbc_decrypt(&aes_dec_key, iv, &mut blocks); - let mut decrypted_sk = [0; 32]; - let mut decrypted_rp_id_hash = [0; 32]; - decrypted_sk[..16].clone_from_slice(&blocks[0]); - 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]); - if rp_id_hash != decrypted_rp_id_hash { + let decrypted_id = aes256_cbc_decrypt(&aes_enc_key, &credential_id[..payload_size], true)?; + if rp_id_hash != &decrypted_id[32..64] { return Ok(None); } - - let sk_option = crypto::ecdsa::SecKey::from_bytes(&decrypted_sk); + let sk_option = crypto::ecdsa::SecKey::from_bytes(array_ref!(decrypted_id, 0, 32)); Ok(sk_option.map(|sk| PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id, diff --git a/src/ctap/pin_protocol.rs b/src/ctap/pin_protocol.rs index 44ae53d..d1e8f2b 100644 --- a/src/ctap/pin_protocol.rs +++ b/src/ctap/pin_protocol.rs @@ -13,13 +13,12 @@ // limitations under the License. use crate::ctap::client_pin::PIN_TOKEN_LENGTH; +use crate::ctap::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt}; use crate::ctap::data_formats::{CoseKey, PinUvAuthProtocol}; use crate::ctap::status_code::Ctap2StatusCode; use alloc::boxed::Box; -use alloc::vec; use alloc::vec::Vec; use core::convert::TryInto; -use crypto::cbc::{cbc_decrypt, cbc_encrypt}; use crypto::hkdf::hkdf_empty_salt_256; #[cfg(test)] use crypto::hmac::hmac_256; @@ -135,61 +134,6 @@ pub trait SharedSecret { fn authenticate(&self, message: &[u8]) -> Vec; } -fn aes256_cbc_encrypt( - rng: &mut dyn Rng256, - aes_enc_key: &crypto::aes256::EncryptionKey, - plaintext: &[u8], - has_iv: bool, -) -> Result, Ctap2StatusCode> { - if plaintext.len() % 16 != 0 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - let iv = if has_iv { - let random_bytes = rng.gen_uniform_u8x32(); - *array_ref!(random_bytes, 0, 16) - } else { - [0u8; 16] - }; - let mut blocks = Vec::with_capacity(plaintext.len() / 16); - // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. - for block in plaintext.chunks_exact(16) { - blocks.push(*array_ref!(block, 0, 16)); - } - cbc_encrypt(aes_enc_key, iv, &mut blocks); - let mut ciphertext = if has_iv { iv.to_vec() } else { vec![] }; - ciphertext.extend(blocks.iter().flatten()); - Ok(ciphertext) -} - -fn aes256_cbc_decrypt( - aes_enc_key: &crypto::aes256::EncryptionKey, - ciphertext: &[u8], - has_iv: bool, -) -> Result, Ctap2StatusCode> { - if ciphertext.len() % 16 != 0 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - let mut block_len = ciphertext.len() / 16; - // TODO(https://github.com/rust-lang/rust/issues/74985) Use array_chunks when stable. - let mut block_iter = ciphertext.chunks_exact(16); - let iv = if has_iv { - block_len -= 1; - let iv_block = block_iter - .next() - .ok_or(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; - *array_ref!(iv_block, 0, 16) - } else { - [0u8; 16] - }; - let mut blocks = Vec::with_capacity(block_len); - for block in block_iter { - blocks.push(*array_ref!(block, 0, 16)); - } - let aes_dec_key = crypto::aes256::DecryptionKey::new(aes_enc_key); - cbc_decrypt(&aes_dec_key, iv, &mut blocks); - Ok(blocks.iter().flatten().cloned().collect::>()) -} - fn verify_v1(key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Ctap2StatusCode> { if signature.len() != 16 { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); From 6216a3214d88de381407e49afa3c6a90ef2139bf Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 7 Apr 2021 09:07:46 +0200 Subject: [PATCH 187/192] final changes to CTAP2.1 algorithm (#299) --- src/ctap/client_pin.rs | 91 +++++++++++++++++++------ src/ctap/data_formats.rs | 7 +- src/ctap/mod.rs | 143 ++++++++++++++++++++------------------- src/ctap/response.rs | 8 +-- 4 files changed, 154 insertions(+), 95 deletions(-) diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index 96c3f02..8b6588e 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -198,7 +198,7 @@ impl ClientPin { ) -> Result { Ok(AuthenticatorClientPinResponse { key_agreement: None, - pin_token: None, + pin_uv_auth_token: None, retries: Some(persistent_store.pin_retries()? as u64), power_cycle_state: Some(self.consecutive_pin_mismatches >= 3), }) @@ -214,7 +214,7 @@ impl ClientPin { ); Ok(AuthenticatorClientPinResponse { key_agreement, - pin_token: None, + pin_uv_auth_token: None, retries: None, power_cycle_state: None, }) @@ -298,10 +298,15 @@ impl ClientPin { pin_uv_auth_protocol, key_agreement, pin_hash_enc, + permissions, + permissions_rp_id, .. } = client_pin_params; let key_agreement = ok_or_missing(key_agreement)?; let pin_hash_enc = ok_or_missing(pin_hash_enc)?; + if permissions.is_some() || permissions_rp_id.is_some() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } if persistent_store.pin_retries()? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); @@ -317,21 +322,21 @@ impl ClientPin { if persistent_store.has_force_pin_change()? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } - let pin_token = shared_secret.encrypt( - rng, - self.get_pin_protocol(pin_uv_auth_protocol) - .get_pin_uv_auth_token(), - )?; self.pin_protocol_v1.reset_pin_uv_auth_token(rng); self.pin_protocol_v2.reset_pin_uv_auth_token(rng); self.pin_uv_auth_token_state .begin_using_pin_uv_auth_token(now); self.pin_uv_auth_token_state.set_default_permissions(); + let pin_uv_auth_token = shared_secret.encrypt( + rng, + self.get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token(), + )?; Ok(AuthenticatorClientPinResponse { key_agreement: None, - pin_token: Some(pin_token), + pin_uv_auth_token: Some(pin_uv_auth_token), retries: None, power_cycle_state: None, }) @@ -359,9 +364,10 @@ impl ClientPin { mut client_pin_params: AuthenticatorClientPinParameters, now: ClockValue, ) -> Result { - let permissions = ok_or_missing(client_pin_params.permissions)?; // Mutating client_pin_params is just an optimization to move it into // process_get_pin_token, without cloning permissions_rp_id here. + // getPinToken requires permissions* to be None. + let permissions = ok_or_missing(client_pin_params.permissions.take())?; let permissions_rp_id = client_pin_params.permissions_rp_id.take(); if permissions == 0 { @@ -657,6 +663,13 @@ mod test { .as_ref() .encrypt(&mut rng, &pin_hash[..16]) .unwrap(); + let (permissions, permissions_rp_id) = match sub_command { + ClientPinSubCommand::GetPinUvAuthTokenUsingUvWithPermissions + | ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => { + (Some(0x03), Some("example.com".to_string())) + } + _ => (None, None), + }; let params = AuthenticatorClientPinParameters { pin_uv_auth_protocol, sub_command, @@ -668,8 +681,8 @@ mod test { pin_uv_auth_param: Some(pin_uv_auth_param), new_pin_enc: Some(new_pin_enc), pin_hash_enc: Some(pin_hash_enc), - permissions: Some(0x03), - permissions_rp_id: Some("example.com".to_string()), + permissions, + permissions_rp_id, }; (client_pin, params) } @@ -813,7 +826,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, - pin_token: None, + pin_uv_auth_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), power_cycle_state: Some(false), }); @@ -830,7 +843,7 @@ mod test { client_pin.consecutive_pin_mismatches = 3; let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, - pin_token: None, + pin_uv_auth_token: None, retries: Some(persistent_store.pin_retries().unwrap() as u64), power_cycle_state: Some(true), }); @@ -859,7 +872,7 @@ mod test { let mut persistent_store = PersistentStore::new(&mut rng); let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: params.key_agreement.clone(), - pin_token: None, + pin_uv_auth_token: None, retries: None, power_cycle_state: None, }); @@ -964,18 +977,37 @@ mod test { pin_uv_auth_protocol, ClientPinSubCommand::GetPinToken, ); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - assert!(client_pin + let response = client_pin .process_command( &mut rng, &mut persistent_store, params.clone(), - DUMMY_CLOCK_VALUE + DUMMY_CLOCK_VALUE, ) - .is_ok()); + .unwrap(); + let encrypted_token = match response { + ResponseData::AuthenticatorClientPin(Some(response)) => { + response.pin_uv_auth_token.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!( + &shared_secret.decrypt(&encrypted_token).unwrap(), + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token() + ); assert_eq!( client_pin .pin_uv_auth_token_state @@ -1051,18 +1083,37 @@ mod test { pin_uv_auth_protocol, ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions, ); + let shared_secret = client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .decapsulate( + params.key_agreement.clone().unwrap(), + params.pin_uv_auth_protocol, + ) + .unwrap(); let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); set_standard_pin(&mut persistent_store); - assert!(client_pin + let response = client_pin .process_command( &mut rng, &mut persistent_store, params.clone(), - DUMMY_CLOCK_VALUE + DUMMY_CLOCK_VALUE, ) - .is_ok()); + .unwrap(); + let encrypted_token = match response { + ResponseData::AuthenticatorClientPin(Some(response)) => { + response.pin_uv_auth_token.unwrap() + } + _ => panic!("Invalid response type"), + }; + assert_eq!( + &shared_secret.decrypt(&encrypted_token).unwrap(), + client_pin + .get_pin_protocol(pin_uv_auth_protocol) + .get_pin_uv_auth_token() + ); assert_eq!( client_pin .pin_uv_auth_token_state diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 1721e62..b135a11 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -409,8 +409,11 @@ impl TryFrom for MakeCredentialOptions { Some(options_entry) => extract_bool(options_entry)?, None => false, }; - if up.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + // In CTAP2.0, the up option is supposed to always fail when present. + if let Some(options_entry) = up { + if !extract_bool(options_entry)? { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } } let uv = match uv { Some(options_entry) => extract_bool(options_entry)?, diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 5d07789..1ecac53 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -554,6 +554,68 @@ where false }; + // MakeCredential always requires user presence. + // User verification depends on the PIN auth inputs, which are checked here. + // The ED flag is added later, if applicable. + let has_uv = pin_uv_auth_param.is_some(); + let mut flags = match pin_uv_auth_param { + Some(pin_uv_auth_param) => { + // This case is not mentioned in CTAP2.1, so we keep 2.0 logic. + if self.persistent_store.pin_hash()?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); + } + self.client_pin.verify_pin_uv_auth_token( + &client_data_hash, + &pin_uv_auth_param, + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + self.client_pin + .has_permission(PinPermission::MakeCredential)?; + self.client_pin.check_user_verified_flag()?; + // Checking for the correct permissions_rp_id is specified earlier. + // Error codes are identical though, so the implementation can be identical with + // GetAssertion. + self.client_pin.ensure_rp_id_permission(&rp_id)?; + UV_FLAG + } + None => { + if options.uv { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); + } + 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); + } + 0x00 + } + }; + flags |= UP_FLAG | AT_FLAG; + + let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + if let Some(exclude_list) = exclude_list { + for cred_desc in exclude_list { + if self + .persistent_store + .find_credential(&rp_id, &cred_desc.key_id, !has_uv)? + .is_some() + || self + .decrypt_credential_source(cred_desc.key_id, &rp_id_hash)? + .is_some() + { + // Perform this check, so bad actors can't brute force exclude_list + // without user interaction. + let _ = (self.check_user_presence)(cid); + return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED); + } + } + } + + (self.check_user_presence)(cid)?; + self.client_pin.clear_token_flags(); + let mut cred_protect_policy = extensions.cred_protect; if cred_protect_policy.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) < DEFAULT_CRED_PROTECT.unwrap_or(CredentialProtectionPolicy::UserVerificationOptional) @@ -576,74 +638,17 @@ where None }; let has_extension_output = extensions.hmac_secret - || cred_protect_policy.is_some() + || extensions.cred_protect.is_some() || min_pin_length || has_cred_blob_output; + if has_extension_output { + flags |= ED_FLAG + }; 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 { - for cred_desc in exclude_list { - if self - .persistent_store - .find_credential(&rp_id, &cred_desc.key_id, pin_uv_auth_param.is_none())? - .is_some() - || self - .decrypt_credential_source(cred_desc.key_id, &rp_id_hash)? - .is_some() - { - // Perform this check, so bad actors can't brute force exclude_list - // without user interaction. - (self.check_user_presence)(cid)?; - return Err(Ctap2StatusCode::CTAP2_ERR_CREDENTIAL_EXCLUDED); - } - } - } - - // MakeCredential always requires user presence. - // User verification depends on the PIN auth inputs, which are checked here. - let ed_flag = if has_extension_output { ED_FLAG } else { 0 }; - let flags = match pin_uv_auth_param { - Some(pin_uv_auth_param) => { - if self.persistent_store.pin_hash()?.is_none() { - // Specification is unclear, could be CTAP2_ERR_INVALID_OPTION. - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } - self.client_pin.verify_pin_uv_auth_token( - &client_data_hash, - &pin_uv_auth_param, - pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - )?; - self.client_pin - .has_permission(PinPermission::MakeCredential)?; - self.client_pin.check_user_verified_flag()?; - // Checking for the correct permissions_rp_id is specified earlier. - // Error codes are identical though, so the implementation can be identical with - // GetAssertion. - self.client_pin.ensure_rp_id_permission(&rp_id)?; - UP_FLAG | UV_FLAG | AT_FLAG | ed_flag - } - None => { - 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 { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); - } - UP_FLAG | AT_FLAG | ed_flag - } - }; - - (self.check_user_presence)(cid)?; - self.client_pin.clear_token_flags(); - let sk = crypto::ecdsa::SecKey::gensk(self.rng); let pk = sk.genpk(); @@ -699,9 +704,10 @@ where } else { None }; + let cred_protect_output = extensions.cred_protect.and(cred_protect_policy); let extensions_output = cbor_map_options! { "hmac-secret" => hmac_secret_output, - "credProtect" => cred_protect_policy, + "credProtect" => cred_protect_output, "minPinLength" => min_pin_length_output, "credBlob" => cred_blob_output, }; @@ -904,8 +910,8 @@ where let has_uv = pin_uv_auth_param.is_some(); let mut flags = match pin_uv_auth_param { Some(pin_uv_auth_param) => { + // This case is not mentioned in CTAP2.1, so we keep 2.0 logic. if self.persistent_store.pin_hash()?.is_none() { - // Specification is unclear, could be CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } self.client_pin.verify_pin_uv_auth_token( @@ -923,12 +929,12 @@ where UV_FLAG } None => { - if self.persistent_store.has_always_uv()? { - return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); - } if options.uv { return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } + if options.up && self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } 0x00 } }; @@ -970,15 +976,14 @@ where (credential, stored_credentials) }; + let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + // 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)?; self.client_pin.clear_token_flags(); } - let credential = credential.ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; - self.increment_global_signature_counter()?; let assertion_input = AssertionInput { diff --git a/src/ctap/response.rs b/src/ctap/response.rs index ddc186b..6a47172 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -211,7 +211,7 @@ impl From for cbor::Value { #[derive(Debug, PartialEq)] pub struct AuthenticatorClientPinResponse { pub key_agreement: Option, - pub pin_token: Option>, + pub pin_uv_auth_token: Option>, pub retries: Option, pub power_cycle_state: Option, // - 0x05: uvRetries missing as we don't support internal UV. @@ -221,14 +221,14 @@ impl From for cbor::Value { fn from(client_pin_response: AuthenticatorClientPinResponse) -> Self { let AuthenticatorClientPinResponse { key_agreement, - pin_token, + pin_uv_auth_token, retries, power_cycle_state, } = client_pin_response; cbor_map_options! { 0x01 => key_agreement.map(cbor::Value::from), - 0x02 => pin_token, + 0x02 => pin_uv_auth_token, 0x03 => retries, 0x04 => power_cycle_state, } @@ -495,7 +495,7 @@ mod test { let cose_key = CoseKey::from(pk); let client_pin_response = AuthenticatorClientPinResponse { key_agreement: Some(cose_key.clone()), - pin_token: Some(vec![70]), + pin_uv_auth_token: Some(vec![70]), retries: Some(8), power_cycle_state: Some(false), }; From 054e303d112a63c61b7c0b1041e34fe9e8a9cca6 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Fri, 9 Apr 2021 07:40:11 +0200 Subject: [PATCH 188/192] move MAX_MSG_SIZE to customization and use it in HID (#302) --- src/ctap/customization.rs | 11 +++++++++++ src/ctap/hid/mod.rs | 3 +++ src/ctap/hid/receive.rs | 22 +++++++++++++--------- src/ctap/large_blobs.rs | 5 +---- src/ctap/mod.rs | 4 ++-- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/ctap/customization.rs b/src/ctap/customization.rs index 1aebadf..7c28a2f 100644 --- a/src/ctap/customization.rs +++ b/src/ctap/customization.rs @@ -119,6 +119,15 @@ pub const ENTERPRISE_ATTESTATION_MODE: Option = None; /// VendorFacilitated. pub const ENTERPRISE_RP_ID_LIST: &[&str] = &[]; +/// Maximum message size send for CTAP commands. +/// +/// The maximum value is 7609, as HID packets can not encode longer messages. +/// 1024 is the default mentioned in the authenticatorLargeBlobs commands. +/// Larger values are preferred, as that allows more parameters in commands. +/// If long commands are too unreliable on your hardware, consider decreasing +/// this value. +pub const MAX_MSG_SIZE: usize = 7609; + /// Sets the number of consecutive failed PINs before blocking interaction. /// /// # Invariant @@ -256,6 +265,8 @@ mod test { } else { assert!(ENTERPRISE_RP_ID_LIST.is_empty()); } + assert!(MAX_MSG_SIZE >= 1024); + assert!(MAX_MSG_SIZE <= 7609); assert!(MAX_PIN_RETRIES <= 8); assert!(MAX_CRED_BLOB_LENGTH >= 32); if let Some(count) = MAX_CREDENTIAL_COUNT_IN_LIST { diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 03cb637..c6fc418 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -322,6 +322,9 @@ impl CtapHid { receive::Error::UnexpectedSeq => { CtapHid::error_message(cid, CtapHid::ERR_INVALID_SEQ) } + receive::Error::UnexpectedLen => { + CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN) + } receive::Error::Timeout => { CtapHid::error_message(cid, CtapHid::ERR_MSG_TIMEOUT) } diff --git a/src/ctap/hid/receive.rs b/src/ctap/hid/receive.rs index 8efdb1c..caf9ffc 100644 --- a/src/ctap/hid/receive.rs +++ b/src/ctap/hid/receive.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::super::customization::MAX_MSG_SIZE; use super::{ChannelID, CtapHid, HidPacket, Message, ProcessedPacket}; use alloc::vec::Vec; use core::mem::swap; @@ -45,6 +46,8 @@ pub enum Error { UnexpectedContinuation, // Expected a continuation packet with a specific sequence number, got another sequence number. UnexpectedSeq, + // The length of a message is too big. + UnexpectedLen, // This packet arrived after a timeout. Timeout, } @@ -107,7 +110,7 @@ impl MessageAssembler { // Expecting an initialization packet. match processed_packet { ProcessedPacket::InitPacket { cmd, len, data } => { - Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp)) + self.parse_init_packet(*cid, cmd, len, data, timestamp) } ProcessedPacket::ContinuationPacket { .. } => { // CTAP specification (version 20190130) section 8.1.5.4 @@ -129,7 +132,7 @@ impl MessageAssembler { ProcessedPacket::InitPacket { cmd, len, data } => { self.reset(); if cmd == CtapHid::COMMAND_INIT { - Ok(self.accept_init_packet(*cid, cmd, len, data, timestamp)) + self.parse_init_packet(*cid, cmd, len, data, timestamp) } else { Err((*cid, Error::UnexpectedInit)) } @@ -151,24 +154,25 @@ impl MessageAssembler { } } - fn accept_init_packet( + fn parse_init_packet( &mut self, cid: ChannelID, cmd: u8, len: usize, data: &[u8], timestamp: Timestamp, - ) -> Option { - // TODO: Should invalid commands/payload lengths be rejected early, i.e. as soon as the - // initialization packet is received, or should we build a message and then catch the - // error? - // The specification (version 20190130) isn't clear on this point. + ) -> Result, (ChannelID, Error)> { + // Reject invalid lengths early to reduce the risk of running out of memory. + // TODO: also reject invalid commands early? + if len > MAX_MSG_SIZE { + return Err((cid, Error::UnexpectedLen)); + } self.cid = cid; self.last_timestamp = timestamp; self.cmd = cmd; self.seq = 0; self.remaining_payload_len = len; - self.append_payload(data) + Ok(self.append_payload(data)) } fn append_payload(&mut self, data: &[u8]) -> Option { diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index ffb98cb..846bc33 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -14,6 +14,7 @@ use super::client_pin::{ClientPin, PinPermission}; use super::command::AuthenticatorLargeBlobsParameters; +use super::customization::MAX_MSG_SIZE; use super::response::{AuthenticatorLargeBlobsResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; @@ -23,10 +24,6 @@ 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; diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 1ecac53..548523a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -42,7 +42,7 @@ use self::credential_management::process_credential_management; use self::crypto_wrapper::{aes256_cbc_decrypt, aes256_cbc_encrypt}; use self::customization::{ DEFAULT_CRED_PROTECT, ENTERPRISE_ATTESTATION_MODE, ENTERPRISE_RP_ID_LIST, - MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, + MAX_CREDENTIAL_COUNT_IN_LIST, MAX_CRED_BLOB_LENGTH, MAX_LARGE_BLOB_ARRAY_SIZE, MAX_MSG_SIZE, MAX_RP_IDS_LENGTH, USE_BATCH_ATTESTATION, USE_SIGNATURE_COUNTER, }; use self::data_formats::{ @@ -52,7 +52,7 @@ use self::data_formats::{ PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; -use self::large_blobs::{LargeBlobs, MAX_MSG_SIZE}; +use self::large_blobs::LargeBlobs; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, From 78b7767682e4e3f0b36ae577882827f22db38176 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Tue, 13 Apr 2021 14:46:28 +0200 Subject: [PATCH 189/192] CBOR maps use Vec instead of BTreeMap (#303) * CBOR uses Vec for map internally * remove BTreeMap from get_info * rename cbor_map_btree and clean up cbor_array_vec * destructure now takes Vec, not BTreeMap * adds dedup in CBOR writer * fail to write CBOR maps with duplicates * CBOR interface refinements * macro documentation for CBOR map and array --- libraries/cbor/src/macros.rs | 130 ++++++++++++++++++++++------------- libraries/cbor/src/reader.rs | 9 ++- libraries/cbor/src/values.rs | 9 ++- libraries/cbor/src/writer.rs | 97 ++++++++++++++++++++++---- src/ctap/command.rs | 6 +- src/ctap/data_formats.rs | 49 ++++++------- src/ctap/mod.rs | 70 ++++++++++--------- src/ctap/response.rs | 39 +++++------ src/ctap/storage.rs | 12 ++-- 9 files changed, 258 insertions(+), 163 deletions(-) diff --git a/libraries/cbor/src/macros.rs b/libraries/cbor/src/macros.rs index 40669d1..758925e 100644 --- a/libraries/cbor/src/macros.rs +++ b/libraries/cbor/src/macros.rs @@ -13,14 +13,14 @@ // limitations under the License. use crate::values::{KeyType, Value}; -use alloc::collections::btree_map; +use alloc::vec; use core::cmp::Ordering; use core::iter::Peekable; -/// This macro generates code to extract multiple values from a `BTreeMap` at once -/// in an optimized manner, consuming the input map. +/// This macro generates code to extract multiple values from a `Vec<(KeyType, Value)>` at once +/// in an optimized manner, consuming the input vector. /// -/// It takes as input a `BTreeMap` as well as a list of identifiers and keys, and generates code +/// It takes as input a `Vec` as well as a list of identifiers and keys, and generates code /// that assigns the corresponding values to new variables using the given identifiers. Each of /// these variables has type `Option`, to account for the case where keys aren't found. /// @@ -32,16 +32,14 @@ use core::iter::Peekable; /// the keys are indeed sorted. This macro is therefore **not suitable for dynamic keys** that can /// change at runtime. /// -/// Semantically, provided that the keys are sorted as specified above, the following two snippets -/// of code are equivalent, but the `destructure_cbor_map!` version is more optimized, as it doesn't -/// re-balance the `BTreeMap` for each key, contrary to the `BTreeMap::remove` operations. +/// Example usage: /// /// ```rust /// # extern crate alloc; /// # use cbor::destructure_cbor_map; /// # /// # fn main() { -/// # let map = alloc::collections::BTreeMap::new(); +/// # let map = alloc::vec::Vec::new(); /// destructure_cbor_map! { /// let { /// 1 => x, @@ -50,17 +48,6 @@ use core::iter::Peekable; /// } /// # } /// ``` -/// -/// ```rust -/// # extern crate alloc; -/// # -/// # fn main() { -/// # let mut map = alloc::collections::BTreeMap::::new(); -/// use cbor::values::IntoCborKey; -/// let x: Option = map.remove(&1.into_cbor_key()); -/// let y: Option = map.remove(&"key".into_cbor_key()); -/// # } -/// ``` #[macro_export] macro_rules! destructure_cbor_map { ( let { $( $key:expr => $variable:ident, )+ } = $map:expr; ) => { @@ -100,7 +87,7 @@ macro_rules! destructure_cbor_map { /// would be inlined for every use case. As of June 2020, this saves ~40KB of binary size for the /// CTAP2 application of OpenSK. pub fn destructure_cbor_map_peek_value( - it: &mut Peekable>, + it: &mut Peekable>, needle: KeyType, ) -> Option { loop { @@ -145,6 +132,23 @@ macro_rules! assert_sorted_keys { }; } +/// Creates a CBOR Value of type Map with the specified key-value pairs. +/// +/// Keys and values are expressions and converted into CBOR Keys and Values. +/// The syntax for these pairs is `key_expression => value_expression,`. +/// Duplicate keys will lead to invalid CBOR, i.e. writing these values fails. +/// Keys do not have to be sorted. +/// +/// Example usage: +/// +/// ```rust +/// # extern crate alloc; +/// # use cbor::cbor_map; +/// let map = cbor_map! { +/// 0x01 => false, +/// "02" => -3, +/// }; +/// ``` #[macro_export] macro_rules! cbor_map { // trailing comma case @@ -157,15 +161,35 @@ macro_rules! cbor_map { // The import is unused if the list is empty. #[allow(unused_imports)] use $crate::values::{IntoCborKey, IntoCborValue}; - let mut _map = ::alloc::collections::BTreeMap::new(); + let mut _map = ::alloc::vec::Vec::new(); $( - _map.insert($key.into_cbor_key(), $value.into_cbor_value()); + _map.push(($key.into_cbor_key(), $value.into_cbor_value())); )* $crate::values::Value::Map(_map) } }; } +/// Creates a CBOR Value of type Map with key-value pairs where values can be Options. +/// +/// Keys and values are expressions and converted into CBOR Keys and Value Options. +/// The map entry is included iff the Value is not an Option or Option is Some. +/// The syntax for these pairs is `key_expression => value_expression,`. +/// Duplicate keys will lead to invalid CBOR, i.e. writing these values fails. +/// Keys do not have to be sorted. +/// +/// Example usage: +/// +/// ```rust +/// # extern crate alloc; +/// # use cbor::cbor_map_options; +/// let missing_value: Option = None; +/// let map = cbor_map_options! { +/// 0x01 => Some(false), +/// "02" => -3, +/// "not in map" => missing_value, +/// }; +/// ``` #[macro_export] macro_rules! cbor_map_options { // trailing comma case @@ -178,12 +202,12 @@ macro_rules! cbor_map_options { // The import is unused if the list is empty. #[allow(unused_imports)] use $crate::values::{IntoCborKey, IntoCborValueOption}; - let mut _map = ::alloc::collections::BTreeMap::<_, $crate::values::Value>::new(); + let mut _map = ::alloc::vec::Vec::<(_, $crate::values::Value)>::new(); $( { let opt: Option<$crate::values::Value> = $value.into_cbor_value_option(); if let Some(val) = opt { - _map.insert($key.into_cbor_key(), val); + _map.push(($key.into_cbor_key(), val)); } } )* @@ -192,13 +216,25 @@ macro_rules! cbor_map_options { }; } +/// Creates a CBOR Value of type Map from a Vec<(KeyType, Value)>. #[macro_export] -macro_rules! cbor_map_btree { - ( $tree:expr ) => { - $crate::values::Value::Map($tree) - }; +macro_rules! cbor_map_collection { + ( $tree:expr ) => {{ + $crate::values::Value::from($tree) + }}; } +/// Creates a CBOR Value of type Array with the given elements. +/// +/// Elements are expressions and converted into CBOR Values. Elements are comma-separated. +/// +/// Example usage: +/// +/// ```rust +/// # extern crate alloc; +/// # use cbor::cbor_array; +/// let array = cbor_array![1, "2"]; +/// ``` #[macro_export] macro_rules! cbor_array { // trailing comma case @@ -216,6 +252,7 @@ macro_rules! cbor_array { }; } +/// Creates a CBOR Value of type Array from a Vec. #[macro_export] macro_rules! cbor_array_vec { ( $vec:expr ) => {{ @@ -329,7 +366,6 @@ macro_rules! cbor_key_bytes { #[cfg(test)] mod test { use super::super::values::{KeyType, SimpleValue, Value}; - use alloc::collections::BTreeMap; #[test] fn test_cbor_simple_values() { @@ -421,7 +457,7 @@ mod test { Value::KeyValue(KeyType::Unsigned(0)), Value::KeyValue(KeyType::Unsigned(1)), ]), - Value::Map(BTreeMap::new()), + Value::Map(Vec::new()), Value::Map( [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] .iter() @@ -518,7 +554,7 @@ mod test { Value::KeyValue(KeyType::Unsigned(1)), ]), ), - (KeyType::Unsigned(9), Value::Map(BTreeMap::new())), + (KeyType::Unsigned(9), Value::Map(Vec::new())), ( KeyType::Unsigned(10), Value::Map( @@ -589,7 +625,7 @@ mod test { Value::KeyValue(KeyType::Unsigned(1)), ]), ), - (KeyType::Unsigned(9), Value::Map(BTreeMap::new())), + (KeyType::Unsigned(9), Value::Map(Vec::new())), ( KeyType::Unsigned(10), Value::Map( @@ -608,30 +644,26 @@ mod test { } #[test] - fn test_cbor_map_btree_empty() { - let a = cbor_map_btree!(BTreeMap::new()); - let b = Value::Map(BTreeMap::new()); + fn test_cbor_map_collection_empty() { + let a = cbor_map_collection!(Vec::<(_, _)>::new()); + let b = Value::Map(Vec::new()); assert_eq!(a, b); } #[test] - fn test_cbor_map_btree_foo() { - let a = cbor_map_btree!( - [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] - .iter() - .cloned() - .collect() - ); - let b = Value::Map( - [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] - .iter() - .cloned() - .collect(), - ); + fn test_cbor_map_collection_foo() { + let a = cbor_map_collection!(vec![( + KeyType::Unsigned(2), + Value::KeyValue(KeyType::Unsigned(3)) + )]); + let b = Value::Map(vec![( + KeyType::Unsigned(2), + Value::KeyValue(KeyType::Unsigned(3)), + )]); assert_eq!(a, b); } - fn extract_map(cbor_value: Value) -> BTreeMap { + fn extract_map(cbor_value: Value) -> Vec<(KeyType, Value)> { match cbor_value { Value::Map(map) => map, _ => panic!("Expected CBOR map."), diff --git a/libraries/cbor/src/reader.rs b/libraries/cbor/src/reader.rs index a1061a0..0634d84 100644 --- a/libraries/cbor/src/reader.rs +++ b/libraries/cbor/src/reader.rs @@ -13,8 +13,7 @@ // limitations under the License. use super::values::{Constants, KeyType, SimpleValue, Value}; -use crate::{cbor_array_vec, cbor_bytes_lit, cbor_map_btree, cbor_text, cbor_unsigned}; -use alloc::collections::BTreeMap; +use crate::{cbor_array_vec, cbor_bytes_lit, cbor_map_collection, cbor_text, cbor_unsigned}; use alloc::str; use alloc::vec::Vec; @@ -174,7 +173,7 @@ impl<'a> Reader<'a> { size_value: u64, remaining_depth: i8, ) -> Result { - let mut value_map = BTreeMap::new(); + let mut value_map = Vec::new(); let mut last_key_option = None; for _ in 0..size_value { let key_value = self.decode_complete_data_item(remaining_depth - 1)?; @@ -185,12 +184,12 @@ impl<'a> Reader<'a> { } } last_key_option = Some(key.clone()); - value_map.insert(key, self.decode_complete_data_item(remaining_depth - 1)?); + value_map.push((key, self.decode_complete_data_item(remaining_depth - 1)?)); } else { return Err(DecoderError::IncorrectMapKeyType); } } - Ok(cbor_map_btree!(value_map)) + Ok(cbor_map_collection!(value_map)) } fn decode_to_simple_value( diff --git a/libraries/cbor/src/values.rs b/libraries/cbor/src/values.rs index b20d109..c2f7b72 100644 --- a/libraries/cbor/src/values.rs +++ b/libraries/cbor/src/values.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::cmp::Ordering; @@ -21,7 +20,7 @@ use core::cmp::Ordering; pub enum Value { KeyValue(KeyType), Array(Vec), - Map(BTreeMap), + Map(Vec<(KeyType, Value)>), // TAG is omitted Simple(SimpleValue), } @@ -183,6 +182,12 @@ where } } +impl From> for Value { + fn from(map: Vec<(KeyType, Value)>) -> Self { + Value::Map(map) + } +} + impl From for Value { fn from(b: bool) -> Self { Value::bool_value(b) diff --git a/libraries/cbor/src/writer.rs b/libraries/cbor/src/writer.rs index 592048d..386f0b5 100644 --- a/libraries/cbor/src/writer.rs +++ b/libraries/cbor/src/writer.rs @@ -56,8 +56,14 @@ impl<'a> Writer<'a> { } } } - Value::Map(map) => { - self.start_item(5, map.len() as u64); + Value::Map(mut map) => { + map.sort_by(|a, b| a.0.cmp(&b.0)); + let map_len = map.len(); + map.dedup_by(|a, b| a.0.eq(&b.0)); + if map_len != map.len() { + return false; + } + self.start_item(5, map_len as u64); for (k, v) in map { if !self.encode_cbor(Value::KeyValue(k), remaining_depth - 1) { return false; @@ -209,9 +215,16 @@ mod test { #[test] fn test_write_map() { let value_map = cbor_map! { - "aa" => "AA", - "e" => "E", - "" => ".", + 0 => "a", + 23 => "b", + 24 => "c", + std::u8::MAX as i64 => "d", + 256 => "e", + std::u16::MAX as i64 => "f", + 65536 => "g", + std::u32::MAX as i64 => "h", + 4294967296_i64 => "i", + std::i64::MAX => "j", -1 => "k", -24 => "l", -25 => "m", @@ -224,16 +237,9 @@ mod test { b"a" => 2, b"bar" => 3, b"foo" => 4, - 0 => "a", - 23 => "b", - 24 => "c", - std::u8::MAX as i64 => "d", - 256 => "e", - std::u16::MAX as i64 => "f", - 65536 => "g", - std::u32::MAX as i64 => "h", - 4294967296_i64 => "i", - std::i64::MAX => "j", + "" => ".", + "e" => "E", + "aa" => "AA", }; let expected_cbor = vec![ 0xb8, 0x19, // map of 25 pairs: @@ -288,6 +294,67 @@ mod test { assert_eq!(write_return(value_map), Some(expected_cbor)); } + #[test] + fn test_write_map_sorted() { + let sorted_map = cbor_map! { + 0 => "a", + 1 => "b", + -1 => "c", + -2 => "d", + b"a" => "e", + b"b" => "f", + "" => "g", + "c" => "h", + }; + let unsorted_map = cbor_map! { + 1 => "b", + -2 => "d", + b"b" => "f", + "c" => "h", + "" => "g", + b"a" => "e", + -1 => "c", + 0 => "a", + }; + assert_eq!(write_return(sorted_map), write_return(unsorted_map)); + } + + #[test] + fn test_write_map_duplicates() { + let duplicate0 = cbor_map! { + 0 => "a", + -1 => "c", + b"a" => "e", + "c" => "g", + 0 => "b", + }; + assert_eq!(write_return(duplicate0), None); + let duplicate1 = cbor_map! { + 0 => "a", + -1 => "c", + b"a" => "e", + "c" => "g", + -1 => "d", + }; + assert_eq!(write_return(duplicate1), None); + let duplicate2 = cbor_map! { + 0 => "a", + -1 => "c", + b"a" => "e", + "c" => "g", + b"a" => "f", + }; + assert_eq!(write_return(duplicate2), None); + let duplicate3 = cbor_map! { + 0 => "a", + -1 => "c", + b"a" => "e", + "c" => "g", + "c" => "h", + }; + assert_eq!(write_return(duplicate3), None); + } + #[test] fn test_write_map_with_array() { let value_map = cbor_map! { diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 4616b02..387a687 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -594,14 +594,14 @@ mod test { 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", + "name" => "Example", }, 0x03 => cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "icon" => "example.com/foo/icon.png", "name" => "foo", "displayName" => "bar", - "icon" => "example.com/foo/icon.png", }, 0x04 => cbor_array![ES256_CRED_PARAM], 0x05 => cbor_array![], @@ -656,8 +656,8 @@ mod test { 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], + "type" => "public-key", "transports" => cbor_array!["usb"], } ], 0x06 => vec![0x12, 0x34], diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index b135a11..673b850 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -13,7 +13,6 @@ // limitations under the License. use super::status_code::Ctap2StatusCode; -use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; use arrayref::array_ref; @@ -62,8 +61,8 @@ 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, + "name" => entity.rp_name, } } } @@ -108,9 +107,9 @@ impl From for cbor::Value { fn from(entity: PublicKeyCredentialUserEntity) -> Self { cbor_map_options! { "id" => entity.user_id, + "icon" => entity.user_icon, "name" => entity.user_name, "displayName" => entity.user_display_name, - "icon" => entity.user_icon, } } } @@ -174,8 +173,8 @@ impl TryFrom for PublicKeyCredentialParameter { impl From for cbor::Value { fn from(cred_param: PublicKeyCredentialParameter) -> Self { cbor_map_options! { - "type" => cred_param.cred_type, "alg" => cred_param.alg, + "type" => cred_param.cred_type, } } } @@ -262,8 +261,8 @@ impl TryFrom for PublicKeyCredentialDescriptor { impl From for cbor::Value { fn from(desc: PublicKeyCredentialDescriptor) -> Self { cbor_map_options! { - "type" => desc.key_type, "id" => desc.key_id, + "type" => desc.key_type, "transports" => desc.transports.map(|vec| cbor_array_vec!(vec)), } } @@ -1119,7 +1118,7 @@ pub(super) fn extract_array(cbor_value: cbor::Value) -> Result, pub(super) fn extract_map( cbor_value: cbor::Value, -) -> Result, Ctap2StatusCode> { +) -> Result, Ctap2StatusCode> { match cbor_value { cbor::Value::Map(map) => Ok(map), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), @@ -1142,7 +1141,6 @@ pub(super) fn ok_or_missing(value_option: Option) -> Result cbor_false!(), - "foo" => b"bar", b"bin" => -42, + "foo" => b"bar", }), - Ok([ + Ok(vec![ (cbor_unsigned!(1), cbor_false!()), - (cbor_text!("foo"), cbor_bytes_lit!(b"bar")), (cbor_bytes_lit!(b"bin"), cbor_int!(-42)), - ] - .iter() - .cloned() - .collect::>()) + (cbor_text!("foo"), cbor_bytes_lit!(b"bar")), + ]) ); } @@ -1419,8 +1414,8 @@ mod test { fn test_from_public_key_credential_rp_entity() { let cbor_rp_entity = cbor_map! { "id" => "example.com", - "name" => "Example", "icon" => "example.com/icon.png", + "name" => "Example", }; let rp_entity = PublicKeyCredentialRpEntity::try_from(cbor_rp_entity); let expected_rp_entity = PublicKeyCredentialRpEntity { @@ -1435,9 +1430,9 @@ mod test { fn test_from_into_public_key_credential_user_entity() { let cbor_user_entity = cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "icon" => "example.com/foo/icon.png", "name" => "foo", "displayName" => "bar", - "icon" => "example.com/foo/icon.png", }; let user_entity = PublicKeyCredentialUserEntity::try_from(cbor_user_entity.clone()); let expected_user_entity = PublicKeyCredentialUserEntity { @@ -1541,8 +1536,8 @@ mod test { #[test] fn test_from_into_public_key_credential_parameter() { let cbor_credential_parameter = cbor_map! { - "type" => "public-key", "alg" => ES256_ALGORITHM, + "type" => "public-key", }; let credential_parameter = PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); @@ -1558,8 +1553,8 @@ mod test { #[test] fn test_from_into_public_key_credential_descriptor() { let cbor_credential_descriptor = cbor_map! { - "type" => "public-key", "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "type" => "public-key", "transports" => cbor_array!["usb"], }; let credential_descriptor = @@ -1577,11 +1572,11 @@ mod test { #[test] fn test_from_make_credential_extensions() { let cbor_extensions = cbor_map! { - "hmac-secret" => true, - "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, - "minPinLength" => true, "credBlob" => vec![0xCB], + "credProtect" => CredentialProtectionPolicy::UserVerificationRequired, + "hmac-secret" => true, "largeBlobKey" => true, + "minPinLength" => true, }; let extensions = MakeCredentialExtensions::try_from(cbor_extensions); let expected_extensions = MakeCredentialExtensions { @@ -1601,12 +1596,12 @@ mod test { let pk = sk.genpk(); let cose_key = CoseKey::from(pk); let cbor_extensions = cbor_map! { + "credBlob" => true, "hmac-secret" => cbor_map! { 1 => cbor::Value::from(cose_key.clone()), 2 => vec![0x02; 32], 3 => vec![0x03; 16], }, - "credBlob" => true, "largeBlobKey" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); @@ -1631,13 +1626,13 @@ mod test { let pk = sk.genpk(); let cose_key = CoseKey::from(pk); let cbor_extensions = cbor_map! { + "credBlob" => true, "hmac-secret" => cbor_map! { 1 => cbor::Value::from(cose_key.clone()), 2 => vec![0x02; 32], 3 => vec![0x03; 16], 4 => 2, }, - "credBlob" => true, "largeBlobKey" => true, }; let extensions = GetAssertionExtensions::try_from(cbor_extensions); @@ -1690,7 +1685,7 @@ mod test { let cbor_packed_attestation_statement = cbor_map! { "alg" => 1, "sig" => vec![0x55, 0x55, 0x55, 0x55], - "x5c" => cbor_array_vec![vec![certificate]], + "x5c" => cbor_array![certificate], "ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D], }; let packed_attestation_statement = PackedAttestationStatement { @@ -1878,7 +1873,7 @@ mod test { }; let cbor_params = cbor_map! { 0x01 => 6, - 0x02 => cbor_array_vec!(vec!["example.com".to_string()]), + 0x02 => cbor_array!("example.com".to_string()), 0x03 => true, }; assert_eq!(cbor::Value::from(params.clone()), cbor_params); @@ -1897,7 +1892,7 @@ mod test { ConfigSubCommandParams::SetMinPinLength(set_min_pin_length_params); let cbor_params = cbor_map! { 0x01 => 6, - 0x02 => cbor_array_vec!(vec!["example.com".to_string()]), + 0x02 => cbor_array!("example.com".to_string()), 0x03 => true, }; assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 548523a..eaca021 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -63,7 +63,6 @@ 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; @@ -706,10 +705,10 @@ where }; let cred_protect_output = extensions.cred_protect.and(cred_protect_policy); let extensions_output = cbor_map_options! { - "hmac-secret" => hmac_secret_output, - "credProtect" => cred_protect_output, - "minPinLength" => min_pin_length_output, "credBlob" => cred_blob_output, + "credProtect" => cred_protect_output, + "hmac-secret" => hmac_secret_output, + "minPinLength" => min_pin_length_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -805,8 +804,8 @@ where None }; let extensions_output = cbor_map_options! { - "hmac-secret" => encrypted_output, "credBlob" => cred_blob, + "hmac-secret" => encrypted_output, }; if !cbor::write(extensions_output, &mut auth_data) { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -1033,26 +1032,29 @@ where versions.insert(0, String::from(U2F_VERSION_STRING)) } } - let mut options_map = BTreeMap::new(); - options_map.insert(String::from("rk"), 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); + let mut options = vec![]; if ENTERPRISE_ATTESTATION_MODE.is_some() { - options_map.insert( + options.push(( 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); - options_map.insert(String::from("makeCredUvNotRqd"), !has_always_uv); - options_map.insert(String::from("alwaysUv"), has_always_uv); + options.append(&mut vec![ + (String::from("rk"), true), + (String::from("up"), true), + (String::from("alwaysUv"), has_always_uv), + (String::from("credMgmt"), true), + (String::from("authnrCfg"), true), + ( + String::from("clientPin"), + self.persistent_store.pin_hash()?.is_some(), + ), + (String::from("largeBlobs"), true), + (String::from("pinUvAuthToken"), true), + (String::from("setMinPINLength"), true), + (String::from("makeCredUvNotRqd"), !has_always_uv), + ]); + Ok(ResponseData::AuthenticatorGetInfo( AuthenticatorGetInfoResponse { versions, @@ -1064,7 +1066,7 @@ where String::from("largeBlobKey"), ]), aaguid: self.persistent_store.aaguid()?, - options: Some(options_map), + options: Some(options), max_msg_size: Some(MAX_MSG_SIZE as u64), // The order implies preference. We favor the new V2. pin_protocols: Some(vec![ @@ -1285,33 +1287,33 @@ mod test { String::from(FIDO2_VERSION_STRING), String::from(FIDO2_1_VERSION_STRING), ]], - 0x02 => cbor_array_vec![vec![ + 0x02 => cbor_array![ String::from("hmac-secret"), String::from("credProtect"), String::from("minPinLength"), String::from("credBlob"), String::from("largeBlobKey"), - ]], + ], 0x03 => ctap_state.persistent_store.aaguid().unwrap(), 0x04 => cbor_map_options! { - "rk" => true, - "clientPin" => false, - "up" => true, - "pinUvAuthToken" => true, - "largeBlobs" => true, "ep" => ENTERPRISE_ATTESTATION_MODE.map(|_| false), - "authnrCfg" => true, + "rk" => true, + "up" => true, + "alwaysUv" => false, "credMgmt" => true, + "authnrCfg" => true, + "clientPin" => false, + "largeBlobs" => true, + "pinUvAuthToken" => true, "setMinPINLength" => true, "makeCredUvNotRqd" => true, - "alwaysUv" => false, }, 0x05 => MAX_MSG_SIZE as u64, - 0x06 => cbor_array_vec![vec![2, 1]], + 0x06 => cbor_array![2, 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]], + 0x09 => cbor_array!["usb"], + 0x0A => cbor_array![ES256_CRED_PARAM], 0x0B => MAX_LARGE_BLOB_ARRAY_SIZE as u64, 0x0C => false, 0x0D => ctap_state.persistent_store.min_pin_length().unwrap() as u64, diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 6a47172..2568bae 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -17,10 +17,9 @@ use super::data_formats::{ PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; -use alloc::collections::BTreeMap; use alloc::string::String; use alloc::vec::Vec; -use cbor::{cbor_array_vec, cbor_bool, cbor_int, cbor_map_btree, cbor_map_options, cbor_text}; +use cbor::{cbor_array_vec, cbor_bool, cbor_int, cbor_map_collection, cbor_map_options, cbor_text}; #[derive(Debug, PartialEq)] pub enum ResponseData { @@ -123,7 +122,7 @@ pub struct AuthenticatorGetInfoResponse { pub versions: Vec, pub extensions: Option>, pub aaguid: [u8; 16], - pub options: Option>, + pub options: Option>, pub max_msg_size: Option, pub pin_protocols: Option>, pub max_credential_count_in_list: Option, @@ -141,7 +140,7 @@ pub struct AuthenticatorGetInfoResponse { // - 0x12: uvModality // Add them when your hardware supports any kind of user verification within // the boundary of the device, e.g. fingerprint or built-in keyboard. - pub certifications: Option>, + pub certifications: Option>, pub remaining_discoverable_credentials: Option, // - 0x15: vendorPrototypeConfigCommands missing as we don't support it. } @@ -170,19 +169,19 @@ impl From for cbor::Value { } = get_info_response; let options_cbor: Option = options.map(|options| { - let options_map: BTreeMap<_, _> = options + let options_map: Vec<(_, _)> = options .into_iter() .map(|(key, value)| (cbor_text!(key), cbor_bool!(value))) .collect(); - cbor_map_btree!(options_map) + cbor_map_collection!(options_map) }); let certifications_cbor: Option = certifications.map(|certifications| { - let certifications_map: BTreeMap<_, _> = certifications + let certifications_map: Vec<(_, _)> = certifications .into_iter() .map(|(key, value)| (cbor_text!(key), cbor_int!(value))) .collect(); - cbor_map_btree!(certifications_map) + cbor_map_collection!(certifications_map) }); cbor_map_options! { @@ -337,7 +336,7 @@ mod test { let cbor_packed_attestation_statement = cbor_map! { "alg" => 1, "sig" => vec![0x55, 0x55, 0x55, 0x55], - "x5c" => cbor_array_vec![vec![certificate]], + "x5c" => cbor_array![certificate], "ecdaaKeyId" => vec![0xEC, 0xDA, 0x1D], }; @@ -385,17 +384,17 @@ mod test { ResponseData::AuthenticatorGetAssertion(get_assertion_response).into(); let expected_cbor = cbor_map_options! { 0x01 => cbor_map! { - "type" => "public-key", "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "type" => "public-key", "transports" => cbor_array!["usb"], }, 0x02 => vec![0xAD], 0x03 => vec![0x51], 0x04 => cbor_map! { "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + "icon" => "example.com/foo/icon.png".to_string(), "name" => "foo".to_string(), "displayName" => "bar".to_string(), - "icon" => "example.com/foo/icon.png".to_string(), }, 0x05 => 2, 0x07 => vec![0x1B], @@ -438,15 +437,11 @@ mod test { #[test] fn test_get_info_optionals_into_cbor() { - let mut options_map = BTreeMap::new(); - options_map.insert(String::from("rk"), true); - let mut certifications_map = BTreeMap::new(); - certifications_map.insert(String::from("example-cert"), 1); let get_info_response = AuthenticatorGetInfoResponse { versions: vec!["FIDO_2_0".to_string()], extensions: Some(vec!["extension".to_string()]), aaguid: [0x00; 16], - options: Some(options_map), + options: Some(vec![(String::from("rk"), true)]), max_msg_size: Some(1024), pin_protocols: Some(vec![1]), max_credential_count_in_list: Some(20), @@ -459,22 +454,22 @@ mod test { firmware_version: Some(0), max_cred_blob_length: Some(1024), max_rp_ids_for_set_min_pin_length: Some(8), - certifications: Some(certifications_map), + certifications: Some(vec![(String::from("example-cert"), 1)]), remaining_discoverable_credentials: Some(150), }; let response_cbor: Option = ResponseData::AuthenticatorGetInfo(get_info_response).into(); let expected_cbor = cbor_map_options! { - 0x01 => cbor_array_vec![vec!["FIDO_2_0"]], - 0x02 => cbor_array_vec![vec!["extension"]], + 0x01 => cbor_array!["FIDO_2_0"], + 0x02 => cbor_array!["extension"], 0x03 => vec![0x00; 16], 0x04 => cbor_map! {"rk" => true}, 0x05 => 1024, - 0x06 => cbor_array_vec![vec![1]], + 0x06 => cbor_array![1], 0x07 => 20, 0x08 => 256, - 0x09 => cbor_array_vec![vec!["usb"]], - 0x0A => cbor_array_vec![vec![ES256_CRED_PARAM]], + 0x09 => cbor_array!["usb"], + 0x0A => cbor_array![ES256_CRED_PARAM], 0x0B => 1024, 0x0C => false, 0x0D => 4, diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index a650d4d..2ba8d77 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -1370,13 +1370,13 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - user_display_name: None, - cred_protect_policy: None, + user_display_name: Some(String::from("Display Name")), + cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationOptional), creation_order: 0, - user_name: None, - user_icon: None, - cred_blob: None, - large_blob_key: None, + user_name: Some(String::from("name")), + user_icon: Some(String::from("icon")), + cred_blob: Some(vec![0xCB]), + large_blob_key: Some(vec![0x1B]), }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From 9a1c060234bb065b48a753fe7426df4f9a9f3fee Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 14 Apr 2021 10:19:10 +0200 Subject: [PATCH 190/192] Remove KeyType from CBOR (#306) * removes KeyType from CBOR * type_label usage in writer --- libraries/cbor/src/lib.rs | 2 +- libraries/cbor/src/macros.rs | 248 ++++++++++++++--------------------- libraries/cbor/src/reader.rs | 34 ++--- libraries/cbor/src/values.rs | 229 +++++++++++++++++--------------- libraries/cbor/src/writer.rs | 25 ++-- src/ctap/data_formats.rs | 18 +-- src/ctap/response.rs | 2 +- 7 files changed, 256 insertions(+), 302 deletions(-) diff --git a/libraries/cbor/src/lib.rs b/libraries/cbor/src/lib.rs index 0a128fc..0667484 100644 --- a/libraries/cbor/src/lib.rs +++ b/libraries/cbor/src/lib.rs @@ -24,5 +24,5 @@ pub mod values; pub mod writer; pub use self::reader::read; -pub use self::values::{KeyType, SimpleValue, Value}; +pub use self::values::{SimpleValue, Value}; pub use self::writer::write; diff --git a/libraries/cbor/src/macros.rs b/libraries/cbor/src/macros.rs index 758925e..7dac984 100644 --- a/libraries/cbor/src/macros.rs +++ b/libraries/cbor/src/macros.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::values::{KeyType, Value}; +use crate::values::Value; use alloc::vec; use core::cmp::Ordering; use core::iter::Peekable; -/// This macro generates code to extract multiple values from a `Vec<(KeyType, Value)>` at once +/// This macro generates code to extract multiple values from a `Vec<(Value, Value)>` at once /// in an optimized manner, consuming the input vector. /// /// It takes as input a `Vec` as well as a list of identifiers and keys, and generates code @@ -57,7 +57,7 @@ macro_rules! destructure_cbor_map { #[cfg(test)] $crate::assert_sorted_keys!($( $key, )+); - use $crate::values::{IntoCborKey, Value}; + use $crate::values::{IntoCborValue, Value}; use $crate::macros::destructure_cbor_map_peek_value; // This algorithm first converts the map into a peekable iterator - whose items are sorted @@ -70,7 +70,7 @@ macro_rules! destructure_cbor_map { // to come in the same order (i.e. sorted). let mut it = $map.into_iter().peekable(); $( - let $variable: Option = destructure_cbor_map_peek_value(&mut it, $key.into_cbor_key()); + let $variable: Option = destructure_cbor_map_peek_value(&mut it, $key.into_cbor_value()); )+ }; } @@ -87,14 +87,14 @@ macro_rules! destructure_cbor_map { /// would be inlined for every use case. As of June 2020, this saves ~40KB of binary size for the /// CTAP2 application of OpenSK. pub fn destructure_cbor_map_peek_value( - it: &mut Peekable>, - needle: KeyType, + it: &mut Peekable>, + needle: Value, ) -> Option { loop { match it.peek() { None => return None, Some(item) => { - let key: &KeyType = &item.0; + let key: &Value = &item.0; match key.cmp(&needle) { Ordering::Less => { it.next(); @@ -118,9 +118,9 @@ macro_rules! assert_sorted_keys { ( $key1:expr, $key2:expr, $( $keys:expr, )* ) => { { - use $crate::values::{IntoCborKey, KeyType}; - let k1: KeyType = $key1.into_cbor_key(); - let k2: KeyType = $key2.into_cbor_key(); + use $crate::values::{IntoCborValue, Value}; + let k1: Value = $key1.into_cbor_value(); + let k2: Value = $key2.into_cbor_value(); assert!( k1 < k2, "{:?} < {:?} failed. The destructure_cbor_map! macro requires keys in sorted order.", @@ -160,10 +160,10 @@ macro_rules! cbor_map { { // The import is unused if the list is empty. #[allow(unused_imports)] - use $crate::values::{IntoCborKey, IntoCborValue}; + use $crate::values::IntoCborValue; let mut _map = ::alloc::vec::Vec::new(); $( - _map.push(($key.into_cbor_key(), $value.into_cbor_value())); + _map.push(($key.into_cbor_value(), $value.into_cbor_value())); )* $crate::values::Value::Map(_map) } @@ -201,13 +201,13 @@ macro_rules! cbor_map_options { { // The import is unused if the list is empty. #[allow(unused_imports)] - use $crate::values::{IntoCborKey, IntoCborValueOption}; + use $crate::values::{IntoCborValue, IntoCborValueOption}; let mut _map = ::alloc::vec::Vec::<(_, $crate::values::Value)>::new(); $( { let opt: Option<$crate::values::Value> = $value.into_cbor_value_option(); if let Some(val) = opt { - _map.push(($key.into_cbor_key(), val)); + _map.push(($key.into_cbor_value(), val)); } } )* @@ -216,7 +216,7 @@ macro_rules! cbor_map_options { }; } -/// Creates a CBOR Value of type Map from a Vec<(KeyType, Value)>. +/// Creates a CBOR Value of type Map from a Vec<(Value, Value)>. #[macro_export] macro_rules! cbor_map_collection { ( $tree:expr ) => {{ @@ -261,6 +261,7 @@ macro_rules! cbor_array_vec { }}; } +/// Creates a CBOR Value of type Simple with value true. #[macro_export] macro_rules! cbor_true { ( ) => { @@ -268,6 +269,7 @@ macro_rules! cbor_true { }; } +/// Creates a CBOR Value of type Simple with value false. #[macro_export] macro_rules! cbor_false { ( ) => { @@ -275,6 +277,7 @@ macro_rules! cbor_false { }; } +/// Creates a CBOR Value of type Simple with value null. #[macro_export] macro_rules! cbor_null { ( ) => { @@ -282,6 +285,7 @@ macro_rules! cbor_null { }; } +/// Creates a CBOR Value of type Simple with the undefined value. #[macro_export] macro_rules! cbor_undefined { ( ) => { @@ -289,6 +293,7 @@ macro_rules! cbor_undefined { }; } +/// Creates a CBOR Value of type Simple with the given bool value. #[macro_export] macro_rules! cbor_bool { ( $x:expr ) => { @@ -296,37 +301,47 @@ macro_rules! cbor_bool { }; } -// For key types, we construct a KeyType and call .into(), which will automatically convert it to a -// KeyType or a Value depending on the context. +/// Creates a CBOR Value of type Unsigned with the given numeric value. #[macro_export] macro_rules! cbor_unsigned { ( $x:expr ) => { - $crate::cbor_key_unsigned!($x).into() + $crate::values::Value::Unsigned($x) }; } +/// Creates a CBOR Value of type Unsigned or Negative with the given numeric value. #[macro_export] macro_rules! cbor_int { ( $x:expr ) => { - $crate::cbor_key_int!($x).into() + $crate::values::Value::integer($x) }; } +/// Creates a CBOR Value of type Text String with the given string. #[macro_export] macro_rules! cbor_text { ( $x:expr ) => { - $crate::cbor_key_text!($x).into() + $crate::values::Value::TextString($x.into()) }; } +/// Creates a CBOR Value of type Byte String with the given slice or vector. #[macro_export] macro_rules! cbor_bytes { ( $x:expr ) => { - $crate::cbor_key_bytes!($x).into() + $crate::values::Value::ByteString($x) }; } -// Macro to use with a literal, e.g. cbor_bytes_lit!(b"foo") +/// Creates a CBOR Value of type Byte String with the given byte string literal. +/// +/// Example usage: +/// +/// ```rust +/// # extern crate alloc; +/// # use cbor::cbor_bytes_lit; +/// let byte_array = cbor_bytes_lit!(b"foo"); +/// ``` #[macro_export] macro_rules! cbor_bytes_lit { ( $x:expr ) => { @@ -334,38 +349,9 @@ macro_rules! cbor_bytes_lit { }; } -// Some explicit macros are also available for contexts where the type is not explicit. -#[macro_export] -macro_rules! cbor_key_unsigned { - ( $x:expr ) => { - $crate::values::KeyType::Unsigned($x) - }; -} - -#[macro_export] -macro_rules! cbor_key_int { - ( $x:expr ) => { - $crate::values::KeyType::integer($x) - }; -} - -#[macro_export] -macro_rules! cbor_key_text { - ( $x:expr ) => { - $crate::values::KeyType::TextString($x.into()) - }; -} - -#[macro_export] -macro_rules! cbor_key_bytes { - ( $x:expr ) => { - $crate::values::KeyType::ByteString($x) - }; -} - #[cfg(test)] mod test { - use super::super::values::{KeyType, SimpleValue, Value}; + use super::super::values::{SimpleValue, Value}; #[test] fn test_cbor_simple_values() { @@ -383,23 +369,20 @@ mod test { #[test] fn test_cbor_int_unsigned() { - assert_eq!(cbor_key_int!(0), KeyType::Unsigned(0)); - assert_eq!(cbor_key_int!(1), KeyType::Unsigned(1)); - assert_eq!(cbor_key_int!(123456), KeyType::Unsigned(123456)); + assert_eq!(cbor_int!(0), Value::Unsigned(0)); + assert_eq!(cbor_int!(1), Value::Unsigned(1)); + assert_eq!(cbor_int!(123456), Value::Unsigned(123456)); assert_eq!( - cbor_key_int!(std::i64::MAX), - KeyType::Unsigned(std::i64::MAX as u64) + cbor_int!(std::i64::MAX), + Value::Unsigned(std::i64::MAX as u64) ); } #[test] fn test_cbor_int_negative() { - assert_eq!(cbor_key_int!(-1), KeyType::Negative(-1)); - assert_eq!(cbor_key_int!(-123456), KeyType::Negative(-123456)); - assert_eq!( - cbor_key_int!(std::i64::MIN), - KeyType::Negative(std::i64::MIN) - ); + assert_eq!(cbor_int!(-1), Value::Negative(-1)); + assert_eq!(cbor_int!(-123456), Value::Negative(-123456)); + assert_eq!(cbor_int!(std::i64::MIN), Value::Negative(std::i64::MIN)); } #[test] @@ -417,16 +400,16 @@ mod test { std::u64::MAX, ]; let b = Value::Array(vec![ - Value::KeyValue(KeyType::Negative(std::i64::MIN)), - Value::KeyValue(KeyType::Negative(std::i32::MIN as i64)), - Value::KeyValue(KeyType::Negative(-123456)), - Value::KeyValue(KeyType::Negative(-1)), - Value::KeyValue(KeyType::Unsigned(0)), - Value::KeyValue(KeyType::Unsigned(1)), - Value::KeyValue(KeyType::Unsigned(123456)), - Value::KeyValue(KeyType::Unsigned(std::i32::MAX as u64)), - Value::KeyValue(KeyType::Unsigned(std::i64::MAX as u64)), - Value::KeyValue(KeyType::Unsigned(std::u64::MAX)), + Value::Negative(std::i64::MIN), + Value::Negative(std::i32::MIN as i64), + Value::Negative(-123456), + Value::Negative(-1), + Value::Unsigned(0), + Value::Unsigned(1), + Value::Unsigned(123456), + Value::Unsigned(std::i32::MAX as u64), + Value::Unsigned(std::i64::MAX as u64), + Value::Unsigned(std::u64::MAX), ]); assert_eq!(a, b); } @@ -446,20 +429,17 @@ mod test { cbor_map! {2 => 3}, ]; let b = Value::Array(vec![ - Value::KeyValue(KeyType::Negative(-123)), - Value::KeyValue(KeyType::Unsigned(456)), + Value::Negative(-123), + Value::Unsigned(456), Value::Simple(SimpleValue::TrueValue), Value::Simple(SimpleValue::NullValue), - Value::KeyValue(KeyType::TextString(String::from("foo"))), - Value::KeyValue(KeyType::ByteString(b"bar".to_vec())), + Value::TextString(String::from("foo")), + Value::ByteString(b"bar".to_vec()), Value::Array(Vec::new()), - Value::Array(vec![ - Value::KeyValue(KeyType::Unsigned(0)), - Value::KeyValue(KeyType::Unsigned(1)), - ]), + Value::Array(vec![Value::Unsigned(0), Value::Unsigned(1)]), Value::Map(Vec::new()), Value::Map( - [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] + [(Value::Unsigned(2), Value::Unsigned(3))] .iter() .cloned() .collect(), @@ -479,10 +459,10 @@ mod test { fn test_cbor_array_vec_int() { let a = cbor_array_vec!(vec![1, 2, 3, 4]); let b = Value::Array(vec![ - Value::KeyValue(KeyType::Unsigned(1)), - Value::KeyValue(KeyType::Unsigned(2)), - Value::KeyValue(KeyType::Unsigned(3)), - Value::KeyValue(KeyType::Unsigned(4)), + Value::Unsigned(1), + Value::Unsigned(2), + Value::Unsigned(3), + Value::Unsigned(4), ]); assert_eq!(a, b); } @@ -491,9 +471,9 @@ mod test { fn test_cbor_array_vec_text() { let a = cbor_array_vec!(vec!["a", "b", "c"]); let b = Value::Array(vec![ - Value::KeyValue(KeyType::TextString(String::from("a"))), - Value::KeyValue(KeyType::TextString(String::from("b"))), - Value::KeyValue(KeyType::TextString(String::from("c"))), + Value::TextString(String::from("a")), + Value::TextString(String::from("b")), + Value::TextString(String::from("c")), ]); assert_eq!(a, b); } @@ -502,9 +482,9 @@ mod test { fn test_cbor_array_vec_bytes() { let a = cbor_array_vec!(vec![b"a", b"b", b"c"]); let b = Value::Array(vec![ - Value::KeyValue(KeyType::ByteString(b"a".to_vec())), - Value::KeyValue(KeyType::ByteString(b"b".to_vec())), - Value::KeyValue(KeyType::ByteString(b"c".to_vec())), + Value::ByteString(b"a".to_vec()), + Value::ByteString(b"b".to_vec()), + Value::ByteString(b"c".to_vec()), ]); assert_eq!(a, b); } @@ -525,40 +505,28 @@ mod test { }; let b = Value::Map( [ + (Value::Negative(-1), Value::Negative(-23)), + (Value::Unsigned(4), Value::Unsigned(56)), ( - KeyType::Negative(-1), - Value::KeyValue(KeyType::Negative(-23)), - ), - (KeyType::Unsigned(4), Value::KeyValue(KeyType::Unsigned(56))), - ( - KeyType::TextString(String::from("foo")), + Value::TextString(String::from("foo")), Value::Simple(SimpleValue::TrueValue), ), ( - KeyType::ByteString(b"bar".to_vec()), + Value::ByteString(b"bar".to_vec()), Value::Simple(SimpleValue::NullValue), ), + (Value::Unsigned(5), Value::TextString(String::from("foo"))), + (Value::Unsigned(6), Value::ByteString(b"bar".to_vec())), + (Value::Unsigned(7), Value::Array(Vec::new())), ( - KeyType::Unsigned(5), - Value::KeyValue(KeyType::TextString(String::from("foo"))), + Value::Unsigned(8), + Value::Array(vec![Value::Unsigned(0), Value::Unsigned(1)]), ), + (Value::Unsigned(9), Value::Map(Vec::new())), ( - KeyType::Unsigned(6), - Value::KeyValue(KeyType::ByteString(b"bar".to_vec())), - ), - (KeyType::Unsigned(7), Value::Array(Vec::new())), - ( - KeyType::Unsigned(8), - Value::Array(vec![ - Value::KeyValue(KeyType::Unsigned(0)), - Value::KeyValue(KeyType::Unsigned(1)), - ]), - ), - (KeyType::Unsigned(9), Value::Map(Vec::new())), - ( - KeyType::Unsigned(10), + Value::Unsigned(10), Value::Map( - [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] + [(Value::Unsigned(2), Value::Unsigned(3))] .iter() .cloned() .collect(), @@ -596,40 +564,28 @@ mod test { }; let b = Value::Map( [ + (Value::Negative(-1), Value::Negative(-23)), + (Value::Unsigned(4), Value::Unsigned(56)), ( - KeyType::Negative(-1), - Value::KeyValue(KeyType::Negative(-23)), - ), - (KeyType::Unsigned(4), Value::KeyValue(KeyType::Unsigned(56))), - ( - KeyType::TextString(String::from("foo")), + Value::TextString(String::from("foo")), Value::Simple(SimpleValue::TrueValue), ), ( - KeyType::ByteString(b"bar".to_vec()), + Value::ByteString(b"bar".to_vec()), Value::Simple(SimpleValue::NullValue), ), + (Value::Unsigned(5), Value::TextString(String::from("foo"))), + (Value::Unsigned(6), Value::ByteString(b"bar".to_vec())), + (Value::Unsigned(7), Value::Array(Vec::new())), ( - KeyType::Unsigned(5), - Value::KeyValue(KeyType::TextString(String::from("foo"))), + Value::Unsigned(8), + Value::Array(vec![Value::Unsigned(0), Value::Unsigned(1)]), ), + (Value::Unsigned(9), Value::Map(Vec::new())), ( - KeyType::Unsigned(6), - Value::KeyValue(KeyType::ByteString(b"bar".to_vec())), - ), - (KeyType::Unsigned(7), Value::Array(Vec::new())), - ( - KeyType::Unsigned(8), - Value::Array(vec![ - Value::KeyValue(KeyType::Unsigned(0)), - Value::KeyValue(KeyType::Unsigned(1)), - ]), - ), - (KeyType::Unsigned(9), Value::Map(Vec::new())), - ( - KeyType::Unsigned(10), + Value::Unsigned(10), Value::Map( - [(KeyType::Unsigned(2), Value::KeyValue(KeyType::Unsigned(3)))] + [(Value::Unsigned(2), Value::Unsigned(3))] .iter() .cloned() .collect(), @@ -652,18 +608,12 @@ mod test { #[test] fn test_cbor_map_collection_foo() { - let a = cbor_map_collection!(vec![( - KeyType::Unsigned(2), - Value::KeyValue(KeyType::Unsigned(3)) - )]); - let b = Value::Map(vec![( - KeyType::Unsigned(2), - Value::KeyValue(KeyType::Unsigned(3)), - )]); + let a = cbor_map_collection!(vec![(Value::Unsigned(2), Value::Unsigned(3))]); + let b = Value::Map(vec![(Value::Unsigned(2), Value::Unsigned(3))]); assert_eq!(a, b); } - fn extract_map(cbor_value: Value) -> Vec<(KeyType, Value)> { + fn extract_map(cbor_value: Value) -> Vec<(Value, Value)> { match cbor_value { Value::Map(map) => map, _ => panic!("Expected CBOR map."), diff --git a/libraries/cbor/src/reader.rs b/libraries/cbor/src/reader.rs index 0634d84..b11cf84 100644 --- a/libraries/cbor/src/reader.rs +++ b/libraries/cbor/src/reader.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::values::{Constants, KeyType, SimpleValue, Value}; +use super::values::{Constants, SimpleValue, Value}; use crate::{cbor_array_vec, cbor_bytes_lit, cbor_map_collection, cbor_text, cbor_unsigned}; use alloc::str; use alloc::vec::Vec; @@ -22,7 +22,6 @@ pub enum DecoderError { UnsupportedMajorType, UnknownAdditionalInfo, IncompleteCborData, - IncorrectMapKeyType, TooMuchNesting, InvalidUtf8, ExtranousData, @@ -134,7 +133,7 @@ impl<'a> Reader<'a> { if signed_size < 0 { Err(DecoderError::OutOfRangeIntegerValue) } else { - Ok(Value::KeyValue(KeyType::Negative(-(size_value as i64) - 1))) + Ok(Value::Negative(-(size_value as i64) - 1)) } } @@ -176,18 +175,14 @@ impl<'a> Reader<'a> { let mut value_map = Vec::new(); let mut last_key_option = None; for _ in 0..size_value { - let key_value = self.decode_complete_data_item(remaining_depth - 1)?; - if let Value::KeyValue(key) = key_value { - if let Some(last_key) = last_key_option { - if last_key >= key { - return Err(DecoderError::OutOfOrderKey); - } + let key = self.decode_complete_data_item(remaining_depth - 1)?; + if let Some(last_key) = last_key_option { + if last_key >= key { + return Err(DecoderError::OutOfOrderKey); } - last_key_option = Some(key.clone()); - value_map.push((key, self.decode_complete_data_item(remaining_depth - 1)?)); - } else { - return Err(DecoderError::IncorrectMapKeyType); } + last_key_option = Some(key.clone()); + value_map.push((key, self.decode_complete_data_item(remaining_depth - 1)?)); } Ok(cbor_map_collection!(value_map)) } @@ -614,19 +609,6 @@ mod test { } } - #[test] - fn test_read_unsupported_map_key_format_error() { - // While CBOR can handle all types as map keys, we only support a subset. - let bad_map_cbor = vec![ - 0xa2, // map of 2 pairs - 0x82, 0x01, 0x02, // invalid key : [1, 2] - 0x02, // value : 2 - 0x61, 0x64, // key : "d" - 0x03, // value : 3 - ]; - assert_eq!(read(&bad_map_cbor), Err(DecoderError::IncorrectMapKeyType)); - } - #[test] fn test_read_unknown_additional_info_error() { let cases = vec![ diff --git a/libraries/cbor/src/values.rs b/libraries/cbor/src/values.rs index c2f7b72..1e1324d 100644 --- a/libraries/cbor/src/values.rs +++ b/libraries/cbor/src/values.rs @@ -12,31 +12,25 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::writer::write; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::cmp::Ordering; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub enum Value { - KeyValue(KeyType), - Array(Vec), - Map(Vec<(KeyType, Value)>), - // TAG is omitted - Simple(SimpleValue), -} - -// The specification recommends to limit the available keys. -// Currently supported are both integer and string types. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum KeyType { Unsigned(u64), // We only use 63 bits of information here. Negative(i64), ByteString(Vec), TextString(String), + Array(Vec), + Map(Vec<(Value, Value)>), + // TAG is omitted + Simple(SimpleValue), } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum SimpleValue { FalseValue = 20, TrueValue = 21, @@ -57,6 +51,15 @@ impl Constants { } impl Value { + // For simplicity, this only takes i64. Construct directly for the last bit. + pub fn integer(int: i64) -> Value { + if int >= 0 { + Value::Unsigned(int as u64) + } else { + Value::Negative(int) + } + } + pub fn bool_value(b: bool) -> Value { if b { Value::Simple(SimpleValue::TrueValue) @@ -66,8 +69,13 @@ impl Value { } pub fn type_label(&self) -> u8 { + // TODO use enum discriminant instead when stable + // https://github.com/rust-lang/rust/issues/60553 match self { - Value::KeyValue(key) => key.type_label(), + Value::Unsigned(_) => 0, + Value::Negative(_) => 1, + Value::ByteString(_) => 2, + Value::TextString(_) => 3, Value::Array(_) => 4, Value::Map(_) => 5, Value::Simple(_) => 7, @@ -75,29 +83,11 @@ impl Value { } } -impl KeyType { - // For simplicity, this only takes i64. Construct directly for the last bit. - pub fn integer(int: i64) -> KeyType { - if int >= 0 { - KeyType::Unsigned(int as u64) - } else { - KeyType::Negative(int) - } - } - - pub fn type_label(&self) -> u8 { - match self { - KeyType::Unsigned(_) => 0, - KeyType::Negative(_) => 1, - KeyType::ByteString(_) => 2, - KeyType::TextString(_) => 3, - } - } -} - -impl Ord for KeyType { - fn cmp(&self, other: &KeyType) -> Ordering { - use super::values::KeyType::{ByteString, Negative, TextString, Unsigned}; +impl Ord for Value { + fn cmp(&self, other: &Value) -> Ordering { + use super::values::Value::{ + Array, ByteString, Map, Negative, Simple, TextString, Unsigned, + }; let self_type_value = self.type_label(); let other_type_value = other.type_label(); if self_type_value != other_type_value { @@ -108,17 +98,35 @@ impl Ord for KeyType { (Negative(n1), Negative(n2)) => n1.cmp(n2).reverse(), (ByteString(b1), ByteString(b2)) => b1.len().cmp(&b2.len()).then(b1.cmp(b2)), (TextString(t1), TextString(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)), - _ => unreachable!(), + (Array(a1), Array(a2)) if a1.len() != a2.len() => a1.len().cmp(&a2.len()), + (Map(m1), Map(m2)) if m1.len() != m2.len() => m1.len().cmp(&m2.len()), + (Simple(s1), Simple(s2)) => s1.cmp(s2), + (v1, v2) => { + // This case could handle all of the above as well. Checking individually is faster. + let mut encoding1 = Vec::new(); + write(v1.clone(), &mut encoding1); + let mut encoding2 = Vec::new(); + write(v2.clone(), &mut encoding2); + encoding1.cmp(&encoding2) + } } } } -impl PartialOrd for KeyType { - fn partial_cmp(&self, other: &KeyType) -> Option { +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Value) -> Option { Some(self.cmp(other)) } } +impl Eq for Value {} + +impl PartialEq for Value { + fn eq(&self, other: &Value) -> bool { + self.cmp(other) == Ordering::Equal + } +} + impl SimpleValue { pub fn from_integer(int: u64) -> Option { match int { @@ -131,59 +139,50 @@ impl SimpleValue { } } -impl From for KeyType { +impl From for Value { fn from(unsigned: u64) -> Self { - KeyType::Unsigned(unsigned) + Value::Unsigned(unsigned) } } -impl From for KeyType { +impl From for Value { fn from(i: i64) -> Self { - KeyType::integer(i) + Value::integer(i) } } -impl From for KeyType { +impl From for Value { fn from(i: i32) -> Self { - KeyType::integer(i as i64) + Value::integer(i as i64) } } -impl From> for KeyType { +impl From> for Value { fn from(bytes: Vec) -> Self { - KeyType::ByteString(bytes) + Value::ByteString(bytes) } } -impl From<&[u8]> for KeyType { +impl From<&[u8]> for Value { fn from(bytes: &[u8]) -> Self { - KeyType::ByteString(bytes.to_vec()) + Value::ByteString(bytes.to_vec()) } } -impl From for KeyType { +impl From for Value { fn from(text: String) -> Self { - KeyType::TextString(text) + Value::TextString(text) } } -impl From<&str> for KeyType { +impl From<&str> for Value { fn from(text: &str) -> Self { - KeyType::TextString(text.to_string()) + Value::TextString(text.to_string()) } } -impl From for Value -where - KeyType: From, -{ - fn from(t: T) -> Self { - Value::KeyValue(KeyType::from(t)) - } -} - -impl From> for Value { - fn from(map: Vec<(KeyType, Value)>) -> Self { +impl From> for Value { + fn from(map: Vec<(Value, Value)>) -> Self { Value::Map(map) } } @@ -194,19 +193,6 @@ impl From for Value { } } -pub trait IntoCborKey { - fn into_cbor_key(self) -> KeyType; -} - -impl IntoCborKey for T -where - KeyType: From, -{ - fn into_cbor_key(self) -> KeyType { - KeyType::from(self) - } -} - pub trait IntoCborValue { fn into_cbor_value(self) -> Value; } @@ -244,32 +230,69 @@ where #[cfg(test)] mod test { - use crate::{cbor_key_bytes, cbor_key_int, cbor_key_text}; + use super::*; + use crate::{cbor_array, cbor_bool, cbor_bytes, cbor_int, cbor_map, cbor_text}; #[test] - fn test_key_type_ordering() { - assert!(cbor_key_int!(0) < cbor_key_int!(23)); - assert!(cbor_key_int!(23) < cbor_key_int!(24)); - assert!(cbor_key_int!(24) < cbor_key_int!(1000)); - assert!(cbor_key_int!(1000) < cbor_key_int!(1000000)); - assert!(cbor_key_int!(1000000) < cbor_key_int!(std::i64::MAX)); - assert!(cbor_key_int!(std::i64::MAX) < cbor_key_int!(-1)); - assert!(cbor_key_int!(-1) < cbor_key_int!(-23)); - assert!(cbor_key_int!(-23) < cbor_key_int!(-24)); - assert!(cbor_key_int!(-24) < cbor_key_int!(-1000)); - assert!(cbor_key_int!(-1000) < cbor_key_int!(-1000000)); - assert!(cbor_key_int!(-1000000) < cbor_key_int!(std::i64::MIN)); - assert!(cbor_key_int!(std::i64::MIN) < cbor_key_bytes!(vec![])); - assert!(cbor_key_bytes!(vec![]) < cbor_key_bytes!(vec![0x00])); - assert!(cbor_key_bytes!(vec![0x00]) < cbor_key_bytes!(vec![0x01])); - assert!(cbor_key_bytes!(vec![0x01]) < cbor_key_bytes!(vec![0xFF])); - assert!(cbor_key_bytes!(vec![0xFF]) < cbor_key_bytes!(vec![0x00, 0x00])); - assert!(cbor_key_bytes!(vec![0x00, 0x00]) < cbor_key_text!("")); - assert!(cbor_key_text!("") < cbor_key_text!("a")); - assert!(cbor_key_text!("a") < cbor_key_text!("b")); - assert!(cbor_key_text!("b") < cbor_key_text!("aa")); - assert!(cbor_key_int!(1) < cbor_key_bytes!(vec![0x00])); - assert!(cbor_key_int!(1) < cbor_key_text!("s")); - assert!(cbor_key_int!(-1) < cbor_key_text!("s")); + fn test_value_ordering() { + assert!(cbor_int!(0) < cbor_int!(23)); + assert!(cbor_int!(23) < cbor_int!(24)); + assert!(cbor_int!(24) < cbor_int!(1000)); + assert!(cbor_int!(1000) < cbor_int!(1000000)); + assert!(cbor_int!(1000000) < cbor_int!(std::i64::MAX)); + assert!(cbor_int!(std::i64::MAX) < cbor_int!(-1)); + assert!(cbor_int!(-1) < cbor_int!(-23)); + assert!(cbor_int!(-23) < cbor_int!(-24)); + assert!(cbor_int!(-24) < cbor_int!(-1000)); + assert!(cbor_int!(-1000) < cbor_int!(-1000000)); + assert!(cbor_int!(-1000000) < cbor_int!(std::i64::MIN)); + assert!(cbor_int!(std::i64::MIN) < cbor_bytes!(vec![])); + assert!(cbor_bytes!(vec![]) < cbor_bytes!(vec![0x00])); + assert!(cbor_bytes!(vec![0x00]) < cbor_bytes!(vec![0x01])); + assert!(cbor_bytes!(vec![0x01]) < cbor_bytes!(vec![0xFF])); + assert!(cbor_bytes!(vec![0xFF]) < cbor_bytes!(vec![0x00, 0x00])); + assert!(cbor_bytes!(vec![0x00, 0x00]) < cbor_text!("")); + assert!(cbor_text!("") < cbor_text!("a")); + assert!(cbor_text!("a") < cbor_text!("b")); + assert!(cbor_text!("b") < cbor_text!("aa")); + assert!(cbor_text!("aa") < cbor_array![]); + assert!(cbor_array![] < cbor_array![0]); + assert!(cbor_array![0] < cbor_array![-1]); + assert!(cbor_array![1] < cbor_array![b""]); + assert!(cbor_array![b""] < cbor_array![""]); + assert!(cbor_array![""] < cbor_array![cbor_array![]]); + assert!(cbor_array![cbor_array![]] < cbor_array![cbor_map! {}]); + assert!(cbor_array![cbor_map! {}] < cbor_array![false]); + assert!(cbor_array![false] < cbor_array![0, 0]); + assert!(cbor_array![0, 0] < cbor_map! {}); + assert!(cbor_map! {} < cbor_map! {0 => 0}); + assert!(cbor_map! {0 => 0} < cbor_map! {0 => 1}); + assert!(cbor_map! {0 => 1} < cbor_map! {1 => 0}); + assert!(cbor_map! {1 => 0} < cbor_map! {-1 => 0}); + assert!(cbor_map! {-1 => 0} < cbor_map! {b"" => 0}); + assert!(cbor_map! {b"" => 0} < cbor_map! {"" => 0}); + assert!(cbor_map! {"" => 0} < cbor_map! {cbor_array![] => 0}); + assert!(cbor_map! {cbor_array![] => 0} < cbor_map! {cbor_map!{} => 0}); + assert!(cbor_map! {cbor_map!{} => 0} < cbor_map! {false => 0}); + assert!(cbor_map! {false => 0} < cbor_map! {0 => 0, 0 => 0}); + assert!(cbor_map! {0 => 0, 0 => 0} < cbor_bool!(false)); + assert!(cbor_bool!(false) < cbor_bool!(true)); + assert!(cbor_bool!(true) < Value::Simple(SimpleValue::NullValue)); + assert!(Value::Simple(SimpleValue::NullValue) < Value::Simple(SimpleValue::Undefined)); + assert!(cbor_int!(1) < cbor_bytes!(vec![0x00])); + assert!(cbor_int!(1) < cbor_text!("s")); + assert!(cbor_int!(1) < cbor_array![]); + assert!(cbor_int!(1) < cbor_map! {}); + assert!(cbor_int!(1) < cbor_bool!(false)); + assert!(cbor_int!(-1) < cbor_text!("s")); + assert!(cbor_int!(-1) < cbor_array![]); + assert!(cbor_int!(-1) < cbor_map! {}); + assert!(cbor_int!(-1) < cbor_bool!(false)); + assert!(cbor_bytes!(vec![0x00]) < cbor_array![]); + assert!(cbor_bytes!(vec![0x00]) < cbor_map! {}); + assert!(cbor_bytes!(vec![0x00]) < cbor_bool!(false)); + assert!(cbor_text!("s") < cbor_map! {}); + assert!(cbor_text!("s") < cbor_bool!(false)); + assert!(cbor_array![] < cbor_bool!(false)); } } diff --git a/libraries/cbor/src/writer.rs b/libraries/cbor/src/writer.rs index 386f0b5..c8a4808 100644 --- a/libraries/cbor/src/writer.rs +++ b/libraries/cbor/src/writer.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::values::{Constants, KeyType, Value}; +use super::values::{Constants, Value}; use alloc::vec::Vec; pub fn write(value: Value, encoded_cbor: &mut Vec) -> bool { @@ -35,21 +35,20 @@ impl<'a> Writer<'a> { if remaining_depth < 0 { return false; } + let type_label = value.type_label(); match value { - Value::KeyValue(KeyType::Unsigned(unsigned)) => self.start_item(0, unsigned), - Value::KeyValue(KeyType::Negative(negative)) => { - self.start_item(1, -(negative + 1) as u64) - } - Value::KeyValue(KeyType::ByteString(byte_string)) => { - self.start_item(2, byte_string.len() as u64); + Value::Unsigned(unsigned) => self.start_item(type_label, unsigned), + Value::Negative(negative) => self.start_item(type_label, -(negative + 1) as u64), + Value::ByteString(byte_string) => { + self.start_item(type_label, byte_string.len() as u64); self.encoded_cbor.extend(byte_string); } - Value::KeyValue(KeyType::TextString(text_string)) => { - self.start_item(3, text_string.len() as u64); + Value::TextString(text_string) => { + self.start_item(type_label, text_string.len() as u64); self.encoded_cbor.extend(text_string.into_bytes()); } Value::Array(array) => { - self.start_item(4, array.len() as u64); + self.start_item(type_label, array.len() as u64); for el in array { if !self.encode_cbor(el, remaining_depth - 1) { return false; @@ -63,9 +62,9 @@ impl<'a> Writer<'a> { if map_len != map.len() { return false; } - self.start_item(5, map_len as u64); + self.start_item(type_label, map_len as u64); for (k, v) in map { - if !self.encode_cbor(Value::KeyValue(k), remaining_depth - 1) { + if !self.encode_cbor(k, remaining_depth - 1) { return false; } if !self.encode_cbor(v, remaining_depth - 1) { @@ -73,7 +72,7 @@ impl<'a> Writer<'a> { } } } - Value::Simple(simple_value) => self.start_item(7, simple_value as u64), + Value::Simple(simple_value) => self.start_item(type_label, simple_value as u64), } true } diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 673b850..be76b65 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -586,8 +586,8 @@ enum PublicKeyCredentialSourceField { // - CredRandom = 5, } -impl From for cbor::KeyType { - fn from(field: PublicKeyCredentialSourceField) -> cbor::KeyType { +impl From for cbor::Value { + fn from(field: PublicKeyCredentialSourceField) -> cbor::Value { (field as u64).into() } } @@ -1076,35 +1076,35 @@ impl From for cbor::Value { pub(super) fn extract_unsigned(cbor_value: cbor::Value) -> Result { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => Ok(unsigned), + cbor::Value::Unsigned(unsigned) => Ok(unsigned), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } pub(super) fn extract_integer(cbor_value: cbor::Value) -> Result { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::Unsigned(unsigned)) => { + cbor::Value::Unsigned(unsigned) => { if unsigned <= core::i64::MAX as u64 { Ok(unsigned as i64) } else { Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE) } } - cbor::Value::KeyValue(cbor::KeyType::Negative(signed)) => Ok(signed), + cbor::Value::Negative(signed) => Ok(signed), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } pub fn extract_byte_string(cbor_value: cbor::Value) -> Result, Ctap2StatusCode> { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::ByteString(byte_string)) => Ok(byte_string), + cbor::Value::ByteString(byte_string) => Ok(byte_string), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } pub(super) fn extract_text_string(cbor_value: cbor::Value) -> Result { match cbor_value { - cbor::Value::KeyValue(cbor::KeyType::TextString(text_string)) => Ok(text_string), + cbor::Value::TextString(text_string) => Ok(text_string), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), } } @@ -1118,7 +1118,7 @@ pub(super) fn extract_array(cbor_value: cbor::Value) -> Result, pub(super) fn extract_map( cbor_value: cbor::Value, -) -> Result, Ctap2StatusCode> { +) -> Result, Ctap2StatusCode> { match cbor_value { cbor::Value::Map(map) => Ok(map), _ => Err(Ctap2StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE), @@ -1681,7 +1681,7 @@ mod test { #[test] fn test_into_packed_attestation_statement() { - let certificate: cbor::values::KeyType = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]]; + let certificate = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]]; let cbor_packed_attestation_statement = cbor_map! { "alg" => 1, "sig" => vec![0x55, 0x55, 0x55, 0x55], diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 2568bae..765403d 100644 --- a/src/ctap/response.rs +++ b/src/ctap/response.rs @@ -326,7 +326,7 @@ mod test { #[test] fn test_make_credential_into_cbor() { - let certificate: cbor::values::KeyType = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]]; + let certificate = cbor_bytes![vec![0x5C, 0x5C, 0x5C, 0x5C]]; let att_stmt = PackedAttestationStatement { alg: 1, sig: vec![0x55, 0x55, 0x55, 0x55], From 7c8894bb043e00de4be7f6ca2c99e90ad0b45ad5 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Thu, 15 Apr 2021 17:22:38 +0200 Subject: [PATCH 191/192] Compare all timestamps using UTC timezone (#308) --- tools/configure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/configure.py b/tools/configure.py index 9343490..5fa2da1 100755 --- a/tools/configure.py +++ b/tools/configure.py @@ -107,7 +107,7 @@ def main(args): cert = x509.load_pem_x509_certificate(args.certificate.read()) # Some sanity/validity checks - now = datetime.datetime.now() + now = datetime.datetime.utcnow() if cert.not_valid_before > now: fatal("Certificate validity starts in the future.") if cert.not_valid_after <= now: From c03605aa0ccecaeef339b05a85adcac77dd40465 Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Wed, 21 Apr 2021 11:45:01 +0200 Subject: [PATCH 192/192] opt level and no Debug by unwrap (#311) --- Cargo.toml | 1 + src/ctap/storage.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b7b0360..63b109a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,3 +43,4 @@ lto = true # Link Time Optimization usually reduces size of binaries and static [profile.release] panic = "abort" lto = true # Link Time Optimization usually reduces size of binaries and static libraries +opt-level = "z" diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 2ba8d77..084cc1b 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -72,7 +72,7 @@ impl PersistentStore { let mut store = PersistentStore { store: persistent_store::Store::new(storage).ok().unwrap(), }; - store.init(rng).unwrap(); + store.init(rng).ok().unwrap(); store }