diff --git a/Cargo.toml b/Cargo.toml index b33cc1a..91e2211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,6 @@ 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"] } -# This import explicitly locks the version. -serde_json = { version = "=1.0.69", default-features = false, features = ["alloc"] } embedded-time = "0.12.1" arbitrary = { version = "0.4.7", features = ["derive"], optional = true } rand = { version = "0.8.4", optional = true } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 0e88562..e00bac3 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,8 +11,6 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = { version = "0.3" } fuzz_helper = { path = "fuzz_helper" } -# This import explicitly locks the version. -serde_json = { version = "=1.0.69" } # Prevent this from interfering with workspaces [workspace] diff --git a/libraries/crypto/Cargo.toml b/libraries/crypto/Cargo.toml index 12944d0..3763bc8 100644 --- a/libraries/crypto/Cargo.toml +++ b/libraries/crypto/Cargo.toml @@ -21,6 +21,11 @@ serde = { version = "1.0", optional = true, features = ["derive"] } serde_json = { version = "=1.0.69", optional = true } regex = { version = "1", optional = true } +# We explicitly lock the version of those transitive dependencies because their +# Cargo.toml don't parse with the nightly compiler used by Tock. Remove this +# whole block once CTAP is a library. +bumpalo = "=3.8.0" # transitive dependency of ring + [features] std = ["hex", "ring", "rng256/std", "untrusted", "serde", "serde_json", "regex"] with_ctap1 = [] diff --git a/libraries/persistent_store/src/concat.rs b/libraries/persistent_store/src/concat.rs new file mode 100644 index 0000000..7879fa6 --- /dev/null +++ b/libraries/persistent_store/src/concat.rs @@ -0,0 +1,242 @@ +// Copyright 2022 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. + +//! Support for concatenated entries. +//! +//! This module permits to store multiple indexed values under the same key by concatenation. Such +//! values must be at most 255 bytes and there can't be more than 255 such values under the same +//! key (they are indexed with a `u8`). +//! +//! The rationale for using those particular constraints is that we want the number of bits to store +//! the index and the number of bits to store the length to fit in an integer number of bytes +//! (because the values are an integer number of bytes). Using only one byte is too restrictive +//! (e.g. 8 values of at most 31 bytes or 16 values of at most 15 bytes). Using 2 bytes is plenty of +//! space, so using one byte for each field makes parsing simpler and faster. +//! +//! The format is thus `(index:u8 length:u8 payload:[u8; length])*`. The concatenation is not +//! particularly sorted. + +use crate::{Storage, Store, StoreError, StoreResult}; +use alloc::vec::Vec; +use core::cmp::Ordering; +use core::ops::Range; + +/// Reads a value from a concatenated entry. +pub fn read(store: &Store, key: usize, index: u8) -> StoreResult>> { + let values = match store.find(key)? { + None => return Ok(None), + Some(x) => x, + }; + Ok(find(&values, index)?.map(|range| values[range].to_vec())) +} + +/// Writes a value to a concatenated entry. +pub fn write( + store: &mut Store, + key: usize, + index: u8, + value: &[u8], +) -> StoreResult<()> { + if value.len() > 255 { + return Err(StoreError::InvalidArgument); + } + let mut values = store.find(key)?.unwrap_or(vec![]); + match find(&values, index)? { + None => { + values.push(index); + values.push(value.len() as u8); + values.extend_from_slice(value); + } + Some(mut range) => { + values[range.start - 1] = value.len() as u8; + match range.len().cmp(&value.len()) { + Ordering::Less => { + let diff = value.len() - range.len(); + values.resize(values.len() + diff, 0); + values[range.end..].rotate_right(diff); + range.end += diff; + } + Ordering::Equal => (), + Ordering::Greater => { + let diff = range.len() - value.len(); + range.end -= diff; + values[range.end..].rotate_left(diff); + values.truncate(values.len() - diff); + } + } + values[range].copy_from_slice(value); + } + } + store.insert(key, &values) +} + +/// Deletes the value from a concatenated entry. +pub fn delete(store: &mut Store, key: usize, index: u8) -> StoreResult<()> { + let mut values = match store.find(key)? { + None => return Ok(()), + Some(x) => x, + }; + let mut range = match find(&values, index)? { + None => return Ok(()), + Some(x) => x, + }; + range.start -= 2; + values[range.start..].rotate_left(range.len()); + values.truncate(values.len() - range.len()); + store.insert(key, &values) +} + +fn find(values: &[u8], index: u8) -> StoreResult>> { + let mut pos = 0; + while pos < values.len() { + if pos == values.len() - 1 { + return Err(StoreError::InvalidStorage); + } + let len = values[pos + 1] as usize; + if len > values.len() - 2 || pos > values.len() - 2 - len { + return Err(StoreError::InvalidStorage); + } + if index == values[pos] { + return Ok(Some(pos + 2..pos + 2 + len)); + } + pos += 2 + len; + } + Ok(None) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::MINIMAL; + + #[test] + fn read_empty_entry() { + let store = MINIMAL.new_store(); + assert_eq!(read(&store, 0, 0), Ok(None)); + assert_eq!(read(&store, 0, 1), Ok(None)); + } + + #[test] + fn read_missing_value() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo\x02\x05hello".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(read(&store, 0, 1), Ok(None)); + } + + #[test] + fn read_existing_value() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo\x02\x05hello".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(read(&store, 0, 0), Ok(Some(b"foo".to_vec()))); + assert_eq!(read(&store, 0, 2), Ok(Some(b"hello".to_vec()))); + } + + #[test] + fn read_invalid_entry_too_long() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo\x02\x08hello".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(read(&store, 0, 1), Err(StoreError::InvalidStorage)); + } + + #[test] + fn read_invalid_entry_too_short() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo\x02".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(read(&store, 0, 1), Err(StoreError::InvalidStorage)); + } + + #[test] + fn write_empty_entry() { + let mut store = MINIMAL.new_store(); + assert_eq!(write(&mut store, 0, 0, b"foo"), Ok(())); + assert_eq!(store.find(0), Ok(Some(b"\x00\x03foo".to_vec()))); + } + + #[test] + fn write_missing_value() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(write(&mut store, 0, 1, b"bar"), Ok(())); + assert_eq!(store.find(0), Ok(Some(b"\x00\x03foo\x01\x03bar".to_vec()))); + } + + #[test] + fn write_existing_value_same_size() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo\x02\x05hello".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(write(&mut store, 0, 0, b"bar"), Ok(())); + assert_eq!( + store.find(0), + Ok(Some(b"\x00\x03bar\x02\x05hello".to_vec())) + ); + } + + #[test] + fn write_existing_value_longer() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo\x02\x05hello".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(write(&mut store, 0, 0, b"barrage"), Ok(())); + assert_eq!( + store.find(0), + Ok(Some(b"\x00\x07barrage\x02\x05hello".to_vec())) + ); + } + + #[test] + fn write_existing_value_shorter() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x08football\x02\x05hello".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(write(&mut store, 0, 0, b"bar"), Ok(())); + assert_eq!( + store.find(0), + Ok(Some(b"\x00\x03bar\x02\x05hello".to_vec())) + ); + } + + #[test] + fn delete_empty_entry() { + let mut store = MINIMAL.new_store(); + assert_eq!(delete(&mut store, 0, 0), Ok(())); + assert_eq!(delete(&mut store, 0, 1), Ok(())); + } + + #[test] + fn delete_missing_value() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo\x02\x05hello".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(delete(&mut store, 0, 1), Ok(())); + assert_eq!( + store.find(0), + Ok(Some(b"\x00\x03foo\x02\x05hello".to_vec())) + ); + } + + #[test] + fn delete_existing_value() { + let mut store = MINIMAL.new_store(); + let value = b"\x00\x03foo\x02\x05hello".to_vec(); + store.insert(0, &value).unwrap(); + assert_eq!(delete(&mut store, 0, 0), Ok(())); + assert_eq!(store.find(0), Ok(Some(b"\x02\x05hello".to_vec()))); + } +} diff --git a/libraries/persistent_store/src/lib.rs b/libraries/persistent_store/src/lib.rs index 75eed03..418b36d 100644 --- a/libraries/persistent_store/src/lib.rs +++ b/libraries/persistent_store/src/lib.rs @@ -363,6 +363,7 @@ extern crate alloc; #[cfg(feature = "std")] mod buffer; +pub mod concat; #[cfg(feature = "std")] mod driver; #[cfg(feature = "std")] diff --git a/src/api/customization.rs b/src/api/customization.rs index 3531002..fa6fac3 100644 --- a/src/api/customization.rs +++ b/src/api/customization.rs @@ -241,6 +241,21 @@ pub trait Customization { /// With P=20 and K=150, we have I=2M which is enough for 500 increments per day /// for 10 years. fn max_supported_resident_keys(&self) -> usize; + + /// Sets the slot count of the multi-PIN feature. + /// + /// # Invariant + /// + /// - The slot count may not: + /// - make the storage entries that concatenate data of each slots + /// become larger than the storage page size, + /// - go over u8, as we only reserve 1 byte for the array index for + /// concatenated entries, or + /// - exceed the number of keys we reserve for the storage entries + /// that use unique keys for each slot. + /// + /// The upper bound of this is currently 8. + fn slot_count(&self) -> usize; } #[derive(Clone)] @@ -260,6 +275,7 @@ pub struct CustomizationImpl { pub max_large_blob_array_size: usize, pub max_rp_ids_length: usize, pub max_supported_resident_keys: usize, + pub slot_count: usize, } pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl { @@ -278,6 +294,7 @@ pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl { max_large_blob_array_size: 2048, max_rp_ids_length: 8, max_supported_resident_keys: 150, + slot_count: 1, }; impl Customization for CustomizationImpl { @@ -351,6 +368,10 @@ impl Customization for CustomizationImpl { fn max_supported_resident_keys(&self) -> usize { self.max_supported_resident_keys } + + fn slot_count(&self) -> usize { + self.slot_count + } } #[cfg(feature = "std")] @@ -423,6 +444,11 @@ pub fn is_valid(customization: &impl Customization) -> bool { return false; } + // Slot count should be at most 8. + if customization.slot_count() > 8 { + return false; + } + true } diff --git a/src/ctap/client_pin.rs b/src/ctap/client_pin.rs index e7b6361..0fdbcd5 100644 --- a/src/ctap/client_pin.rs +++ b/src/ctap/client_pin.rs @@ -21,12 +21,13 @@ use super::pin_protocol::{verify_pin_uv_auth_token, PinProtocol, SharedSecret}; use super::response::{AuthenticatorClientPinResponse, ResponseData}; use super::status_code::Ctap2StatusCode; use super::token_state::PinUvAuthTokenState; +use crate::api::customization::Customization; use crate::ctap::storage; use crate::env::Env; use alloc::boxed::Box; -use alloc::str; use alloc::string::String; use alloc::vec::Vec; +use alloc::{str, vec}; use crypto::hmac::hmac_256; use crypto::sha256::Sha256; use crypto::Hash256; @@ -78,6 +79,7 @@ fn decrypt_pin( /// truncated for persistent storage. fn check_and_store_new_pin( env: &mut impl Env, + slot_id: usize, shared_secret: &dyn SharedSecret, new_pin_enc: Vec, ) -> Result<(), Ctap2StatusCode> { @@ -90,7 +92,7 @@ fn check_and_store_new_pin( 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. - storage::set_pin(env, &pin_hash, pin_length as u8)?; + storage::set_pin(env, slot_id, &pin_hash, pin_length as u8)?; Ok(()) } @@ -108,16 +110,16 @@ pub enum PinPermission { pub struct ClientPin { pin_protocol_v1: PinProtocol, pin_protocol_v2: PinProtocol, - consecutive_pin_mismatches: u8, + consecutive_pin_mismatches: Vec, pin_uv_auth_token_state: PinUvAuthTokenState, } impl ClientPin { - pub fn new(rng: &mut impl Rng256) -> ClientPin { + pub fn new(env: &mut impl Env) -> ClientPin { ClientPin { - pin_protocol_v1: PinProtocol::new(rng), - pin_protocol_v2: PinProtocol::new(rng), - consecutive_pin_mismatches: 0, + pin_protocol_v1: PinProtocol::new(env.rng()), + pin_protocol_v2: PinProtocol::new(env.rng()), + consecutive_pin_mismatches: vec![0; env.customization().slot_count()], pin_uv_auth_token_state: PinUvAuthTokenState::new(), } } @@ -159,16 +161,21 @@ impl ClientPin { fn verify_pin_hash_enc( &mut self, env: &mut impl Env, + slot_id: usize, pin_uv_auth_protocol: PinUvAuthProtocol, shared_secret: &dyn SharedSecret, pin_hash_enc: Vec, ) -> Result<(), Ctap2StatusCode> { - match storage::pin_hash(env)? { + // To prevent out of bounds access in code below. + if slot_id >= self.consecutive_pin_mismatches.len() { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + match storage::pin_hash(env, slot_id)? { Some(pin_hash) => { - if self.consecutive_pin_mismatches >= 3 { + if self.consecutive_pin_mismatches[slot_id] >= 3 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); } - storage::decr_pin_retries(env)?; + storage::decr_pin_retries(env, slot_id)?; let pin_hash_dec = shared_secret .decrypt(&pin_hash_enc) .map_err(|_| Ctap2StatusCode::CTAP2_ERR_PIN_INVALID)?; @@ -176,11 +183,11 @@ impl ClientPin { if !bool::from(pin_hash.ct_eq(&pin_hash_dec)) { self.get_mut_pin_protocol(pin_uv_auth_protocol) .regenerate(env.rng()); - if storage::pin_retries(env)? == 0 { + if storage::pin_retries(env, slot_id)? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } - self.consecutive_pin_mismatches += 1; - if self.consecutive_pin_mismatches >= 3 { + self.consecutive_pin_mismatches[slot_id] += 1; + if self.consecutive_pin_mismatches[slot_id] >= 3 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); } return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); @@ -189,20 +196,26 @@ impl ClientPin { // This status code is not explicitly mentioned in the specification. None => return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED), } - storage::reset_pin_retries(env)?; - self.consecutive_pin_mismatches = 0; + storage::reset_pin_retries(env, slot_id)?; + self.consecutive_pin_mismatches[slot_id] = 0; Ok(()) } fn process_get_pin_retries( &self, env: &mut impl Env, + _client_pin_params: AuthenticatorClientPinParameters, ) -> Result { + // TODO: Parse slot_id from params if multi-PIN feature is enabled. + let slot_id = 0; + if slot_id >= self.consecutive_pin_mismatches.len() { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } Ok(AuthenticatorClientPinResponse { key_agreement: None, pin_uv_auth_token: None, - retries: Some(storage::pin_retries(env)? as u64), - power_cycle_state: Some(self.consecutive_pin_mismatches >= 3), + retries: Some(storage::pin_retries(env, slot_id)? as u64), + power_cycle_state: Some(self.consecutive_pin_mismatches[slot_id] >= 3), }) } @@ -234,18 +247,20 @@ impl ClientPin { new_pin_enc, .. } = client_pin_params; + // TODO: Parse slot_id from params if multi-PIN feature is enabled. + let slot_id = 0; 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 storage::pin_hash(env)?.is_some() { + if storage::pin_hash(env, slot_id)?.is_some() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); } 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(env, shared_secret.as_ref(), new_pin_enc)?; - storage::reset_pin_retries(env)?; + check_and_store_new_pin(env, slot_id, shared_secret.as_ref(), new_pin_enc)?; + storage::reset_pin_retries(env, slot_id)?; Ok(()) } @@ -262,12 +277,14 @@ impl ClientPin { pin_hash_enc, .. } = client_pin_params; + // TODO: Parse slot_id from params if multi-PIN feature is enabled. + let slot_id = 0; 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 storage::pin_retries(env)? == 0 { + if storage::pin_retries(env, slot_id)? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; @@ -276,12 +293,13 @@ impl ClientPin { shared_secret.verify(&auth_param_data, &pin_uv_auth_param)?; self.verify_pin_hash_enc( env, + slot_id, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc, )?; - check_and_store_new_pin(env, shared_secret.as_ref(), new_pin_enc)?; + check_and_store_new_pin(env, slot_id, shared_secret.as_ref(), new_pin_enc)?; self.pin_protocol_v1.reset_pin_uv_auth_token(env.rng()); self.pin_protocol_v2.reset_pin_uv_auth_token(env.rng()); Ok(()) @@ -301,28 +319,32 @@ impl ClientPin { permissions_rp_id, .. } = client_pin_params; + // TODO: Parse slot_id from params if multi-PIN feature is enabled. + let slot_id = 0; 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 storage::pin_retries(env)? == 0 { + if storage::pin_retries(env, slot_id)? == 0 { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); } let shared_secret = self.get_shared_secret(pin_uv_auth_protocol, key_agreement)?; self.verify_pin_hash_enc( env, + slot_id, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc, )?; - if storage::has_force_pin_change(env)? { + if storage::has_force_pin_change(env, slot_id)? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); } self.pin_protocol_v1.reset_pin_uv_auth_token(env.rng()); self.pin_protocol_v2.reset_pin_uv_auth_token(env.rng()); + // TODO: pass the slot_id parameter and store it in the state too. self.pin_uv_auth_token_state .begin_using_pin_uv_auth_token(now); self.pin_uv_auth_token_state.set_default_permissions(); @@ -391,7 +413,9 @@ impl ClientPin { now: CtapInstant, ) -> Result { let response = match client_pin_params.sub_command { - ClientPinSubCommand::GetPinRetries => Some(self.process_get_pin_retries(env)?), + ClientPinSubCommand::GetPinRetries => { + Some(self.process_get_pin_retries(env, client_pin_params)?) + } ClientPinSubCommand::GetKeyAgreement => { Some(self.process_get_key_agreement(client_pin_params)?) } @@ -446,7 +470,9 @@ impl ClientPin { 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; + for v in &mut self.consecutive_pin_mismatches { + *v = 0; + } self.pin_uv_auth_token_state.stop_using_pin_uv_auth_token(); } @@ -557,11 +583,11 @@ impl ClientPin { #[cfg(test)] pub fn new_test( + env: &mut impl Env, key_agreement_key: crypto::ecdh::SecKey, pin_uv_auth_token: [u8; PIN_TOKEN_LENGTH], pin_uv_auth_protocol: PinUvAuthProtocol, ) -> ClientPin { - let mut env = crate::env::test::TestEnv::new(); let (key_agreement_key_v1, key_agreement_key_v2) = match pin_uv_auth_protocol { PinUvAuthProtocol::V1 => (key_agreement_key, crypto::ecdh::SecKey::gensk(env.rng())), PinUvAuthProtocol::V2 => (crypto::ecdh::SecKey::gensk(env.rng()), key_agreement_key), @@ -572,7 +598,7 @@ impl ClientPin { 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, + consecutive_pin_mismatches: vec![0; env.customization().slot_count()], pin_uv_auth_token_state, } } @@ -592,7 +618,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]); - storage::set_pin(env, &pin_hash, 4).unwrap(); + storage::set_pin(env, 0, &pin_hash, 4).unwrap(); } /// Fails on PINs bigger than 64 bytes. @@ -618,8 +644,12 @@ mod test { 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 client_pin = ClientPin::new_test( + &mut env, + 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) @@ -677,7 +707,7 @@ mod test { #[test] fn test_mix_pin_protocols() { let mut env = TestEnv::new(); - let client_pin = ClientPin::new(env.rng()); + let client_pin = ClientPin::new(&mut env); 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]; @@ -718,7 +748,7 @@ mod test { fn test_helper_verify_pin_hash_enc(pin_uv_auth_protocol: PinUvAuthProtocol) { let mut env = TestEnv::new(); - let mut client_pin = ClientPin::new(env.rng()); + let mut client_pin = ClientPin::new(&mut env); 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) @@ -728,7 +758,7 @@ mod test { 0x01, 0xD9, 0x88, 0x40, 0x50, 0xBB, 0xD0, 0x7A, 0x23, 0x1A, 0xEB, 0x69, 0xD8, 0x36, 0xC4, 0x12, ]; - storage::set_pin(&mut env, &pin_hash, 4).unwrap(); + storage::set_pin(&mut env, 0, &pin_hash, 4).unwrap(); let pin_hash_enc = shared_secret .as_ref() @@ -737,6 +767,7 @@ mod test { assert_eq!( client_pin.verify_pin_hash_enc( &mut env, + 0, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc @@ -748,6 +779,7 @@ mod test { assert_eq!( client_pin.verify_pin_hash_enc( &mut env, + 0, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc @@ -759,22 +791,24 @@ mod test { .as_ref() .encrypt(env.rng(), &pin_hash) .unwrap(); - client_pin.consecutive_pin_mismatches = 3; + client_pin.consecutive_pin_mismatches[0] = 3; assert_eq!( client_pin.verify_pin_hash_enc( &mut env, + 0, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc ), Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) ); - client_pin.consecutive_pin_mismatches = 0; + client_pin.consecutive_pin_mismatches[0] = 0; let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; assert_eq!( client_pin.verify_pin_hash_enc( &mut env, + 0, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc @@ -786,6 +820,7 @@ mod test { assert_eq!( client_pin.verify_pin_hash_enc( &mut env, + 0, pin_uv_auth_protocol, shared_secret.as_ref(), pin_hash_enc @@ -813,7 +848,7 @@ mod test { let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, pin_uv_auth_token: None, - retries: Some(storage::pin_retries(&mut env).unwrap() as u64), + retries: Some(storage::pin_retries(&mut env, 0).unwrap() as u64), power_cycle_state: Some(false), }); assert_eq!( @@ -821,11 +856,11 @@ mod test { Ok(ResponseData::AuthenticatorClientPin(expected_response)) ); - client_pin.consecutive_pin_mismatches = 3; + client_pin.consecutive_pin_mismatches[0] = 3; let expected_response = Some(AuthenticatorClientPinResponse { key_agreement: None, pin_uv_auth_token: None, - retries: Some(storage::pin_retries(&mut env).unwrap() as u64), + retries: Some(storage::pin_retries(&mut env, 0).unwrap() as u64), power_cycle_state: Some(true), }); assert_eq!( @@ -921,8 +956,8 @@ mod test { Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) ); - while storage::pin_retries(&mut env).unwrap() > 0 { - storage::decr_pin_retries(&mut env).unwrap(); + while storage::pin_retries(&mut env, 0).unwrap() > 0 { + storage::decr_pin_retries(&mut env, 0).unwrap(); } assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), @@ -1015,7 +1050,7 @@ mod test { let mut env = TestEnv::new(); set_standard_pin(&mut env); - assert_eq!(storage::force_pin_change(&mut env), Ok(())); + assert_eq!(storage::force_pin_change(&mut env, 0), Ok(())); assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), @@ -1125,7 +1160,7 @@ mod test { let mut env = TestEnv::new(); set_standard_pin(&mut env); - assert_eq!(storage::force_pin_change(&mut env), Ok(())); + assert_eq!(storage::force_pin_change(&mut env, 0), Ok(())); assert_eq!( client_pin.process_command(&mut env, params, CtapInstant::new(0)), Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) @@ -1217,17 +1252,17 @@ mod test { ), ]; for (pin, result) in test_cases { - let old_pin_hash = storage::pin_hash(&mut env).unwrap(); + let old_pin_hash = storage::pin_hash(&mut env, 0).unwrap(); let new_pin_enc = encrypt_pin(shared_secret.as_ref(), pin); assert_eq!( - check_and_store_new_pin(&mut env, shared_secret.as_ref(), new_pin_enc), + check_and_store_new_pin(&mut env, 0, shared_secret.as_ref(), new_pin_enc), result ); if result.is_ok() { - assert_ne!(old_pin_hash, storage::pin_hash(&mut env).unwrap()); + assert_ne!(old_pin_hash, storage::pin_hash(&mut env, 0).unwrap()); } else { - assert_eq!(old_pin_hash, storage::pin_hash(&mut env).unwrap()); + assert_eq!(old_pin_hash, storage::pin_hash(&mut env, 0).unwrap()); } } } @@ -1383,7 +1418,7 @@ mod test { #[test] fn test_has_permission() { let mut env = TestEnv::new(); - let mut client_pin = ClientPin::new(env.rng()); + let mut client_pin = ClientPin::new(&mut env); client_pin.pin_uv_auth_token_state.set_permissions(0x7F); for permission in PinPermission::into_enum_iter() { assert_eq!( @@ -1407,7 +1442,7 @@ mod test { #[test] fn test_has_no_rp_id_permission() { let mut env = TestEnv::new(); - let mut client_pin = ClientPin::new(env.rng()); + let mut client_pin = ClientPin::new(&mut env); assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); client_pin .pin_uv_auth_token_state @@ -1421,7 +1456,7 @@ mod test { #[test] fn test_has_no_or_rp_id_permission() { let mut env = TestEnv::new(); - let mut client_pin = ClientPin::new(env.rng()); + let mut client_pin = ClientPin::new(&mut env); assert_eq!(client_pin.has_no_or_rp_id_permission("example.com"), Ok(())); client_pin .pin_uv_auth_token_state @@ -1436,7 +1471,7 @@ mod test { #[test] fn test_has_no_or_rp_id_hash_permission() { let mut env = TestEnv::new(); - let mut client_pin = ClientPin::new(env.rng()); + let mut client_pin = ClientPin::new(&mut env); let rp_id_hash = Sha256::hash(b"example.com"); assert_eq!( client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), @@ -1458,7 +1493,7 @@ mod test { #[test] fn test_ensure_rp_id_permission() { let mut env = TestEnv::new(); - let mut client_pin = ClientPin::new(env.rng()); + let mut client_pin = ClientPin::new(&mut env); assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); assert_eq!( client_pin @@ -1476,7 +1511,7 @@ mod test { #[test] fn test_verify_pin_uv_auth_token() { let mut env = TestEnv::new(); - let mut client_pin = ClientPin::new(env.rng()); + let mut client_pin = ClientPin::new(&mut env); let message = [0xAA]; client_pin .pin_uv_auth_token_state @@ -1550,7 +1585,7 @@ mod test { #[test] fn test_verify_pin_uv_auth_token_not_in_use() { let mut env = TestEnv::new(); - let client_pin = ClientPin::new(env.rng()); + let client_pin = ClientPin::new(&mut env); let message = [0xAA]; let pin_uv_auth_token_v1 = client_pin @@ -1572,7 +1607,7 @@ mod test { #[test] fn test_reset() { let mut env = TestEnv::new(); - let mut client_pin = ClientPin::new(env.rng()); + let mut client_pin = ClientPin::new(&mut env); 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(); diff --git a/src/ctap/config_command.rs b/src/ctap/config_command.rs index 3103b21..2a37623 100644 --- a/src/ctap/config_command.rs +++ b/src/ctap/config_command.rs @@ -55,15 +55,21 @@ fn process_set_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 && storage::pin_hash(env)?.is_none() { + let force_change_pin = force_change_pin.unwrap_or(false); + if force_change_pin && !storage::has_pin(env)? { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } - if let Some(old_length) = storage::pin_code_point_length(env)? { - force_change_pin |= new_min_pin_length > old_length; - } - if force_change_pin { - storage::force_pin_change(env)?; + for slot_id in 0..env.customization().slot_count() { + if storage::pin_hash(env, slot_id)?.is_none() { + continue; + } + let mut force_change_pin = force_change_pin; + if let Some(old_length) = storage::pin_code_point_length(env, slot_id)? { + force_change_pin |= new_min_pin_length > old_length; + } + if force_change_pin { + storage::force_pin_change(env, slot_id)?; + } } storage::set_min_pin_length(env, new_min_pin_length)?; if let Some(min_pin_length_rp_ids) = min_pin_length_rp_ids { @@ -85,9 +91,11 @@ pub fn process_config( pin_uv_auth_protocol, } = params; + // TODO: Get the slot id from active token state when multi-PIN is enabled. + let slot_id = Some(0); let enforce_uv = !matches!(sub_command, ConfigSubCommand::ToggleAlwaysUv) && storage::has_always_uv(env)?; - if storage::pin_hash(env)?.is_some() || enforce_uv { + if (slot_id.is_some() && storage::pin_hash(env, slot_id.unwrap())?.is_some()) || enforce_uv { let pin_uv_auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let pin_uv_auth_protocol = @@ -133,8 +141,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::EnableEnterpriseAttestation, @@ -160,8 +172,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let config_params = AuthenticatorConfigParameters { sub_command: ConfigSubCommand::ToggleAlwaysUv, @@ -195,9 +211,13 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); - storage::set_pin(&mut env, &[0x88; 16], 4).unwrap(); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + pin_uv_auth_protocol, + ); + storage::set_pin(&mut env, 0, &[0x88; 16], 4).unwrap(); let mut config_data = vec![0xFF; 32]; config_data.extend(&[0x0D, ConfigSubCommand::ToggleAlwaysUv as u8]); @@ -265,8 +285,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + 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; @@ -277,7 +301,7 @@ mod test { // 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. - storage::set_pin(&mut env, &[0x88; 16], 8).unwrap(); + storage::set_pin(&mut env, 0, &[0x88; 16], 8).unwrap(); let min_pin_length = 8; let mut config_params = create_min_pin_config_params(min_pin_length, None); let pin_uv_auth_param = vec![ @@ -309,8 +333,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); // First, set RP IDs without PIN auth. let min_pin_length = 6; @@ -329,7 +357,7 @@ mod test { 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. - storage::set_pin(&mut env, &[0x88; 16], 8).unwrap(); + storage::set_pin(&mut env, 0, &[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_uv_auth_param = vec![ @@ -385,10 +413,14 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); - storage::set_pin(&mut env, &[0x88; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[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); @@ -400,7 +432,7 @@ mod test { let config_response = process_config(&mut env, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); assert_eq!(storage::min_pin_length(&mut env), Ok(min_pin_length)); - assert_eq!(storage::has_force_pin_change(&mut env), Ok(true)); + assert_eq!(storage::has_force_pin_change(&mut env, 0), Ok(true)); } #[test] @@ -408,10 +440,14 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); - storage::set_pin(&mut env, &[0x88; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[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, @@ -431,7 +467,7 @@ mod test { }; let config_response = process_config(&mut env, &mut client_pin, config_params); assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); - assert_eq!(storage::has_force_pin_change(&mut env), Ok(true)); + assert_eq!(storage::has_force_pin_change(&mut env, 0), Ok(true)); } #[test] @@ -439,8 +475,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + 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 aee9e3a..095b79e 100644 --- a/src/ctap/credential_management.rs +++ b/src/ctap/credential_management.rs @@ -392,14 +392,18 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + let client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + pin_uv_auth_protocol, + ); let credential_source = create_credential_source(&mut env); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); ctap_state.client_pin = client_pin; - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let management_data = vec![CredentialManagementSubCommand::GetCredsMetadata as u8]; let pin_uv_auth_param = authenticate_pin_uv_auth_token( &pin_uv_auth_token, @@ -474,8 +478,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let credential_source1 = create_credential_source(&mut env); let mut credential_source2 = create_credential_source(&mut env); credential_source2.rp_id = "another.example.com".to_string(); @@ -486,7 +494,7 @@ mod test { storage::store_credential(&mut env, credential_source1).unwrap(); storage::store_credential(&mut env, credential_source2).unwrap(); - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, 0xD0, 0xD1, @@ -568,8 +576,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let credential_source = create_credential_source(&mut env); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); @@ -582,7 +594,7 @@ mod test { storage::store_credential(&mut env, credential).unwrap(); } - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, 0xD0, 0xD1, @@ -649,8 +661,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let credential_source1 = create_credential_source(&mut env); let mut credential_source2 = create_credential_source(&mut env); credential_source2.user_handle = vec![0x02]; @@ -664,7 +680,7 @@ mod test { storage::store_credential(&mut env, credential_source1).unwrap(); storage::store_credential(&mut env, credential_source2).unwrap(); - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ 0xF8, 0xB0, 0x3C, 0xC1, 0xD5, 0x58, 0x9C, 0xB7, 0x4D, 0x42, 0xA1, 0x64, 0x14, 0x28, 0x2B, 0x68, @@ -751,8 +767,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let mut credential_source = create_credential_source(&mut env); credential_source.credential_id = vec![0x1D; 32]; @@ -761,7 +781,7 @@ mod test { storage::store_credential(&mut env, credential_source).unwrap(); - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ 0xBD, 0xE3, 0xEF, 0x8A, 0x77, 0x01, 0xB1, 0x69, 0x19, 0xE6, 0x62, 0xB9, 0x9B, 0x89, 0x9C, 0x64, @@ -821,8 +841,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let mut credential_source = create_credential_source(&mut env); credential_source.credential_id = vec![0x1D; 32]; @@ -831,7 +855,7 @@ mod test { storage::store_credential(&mut env, credential_source).unwrap(); - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ 0xA5, 0x55, 0x8F, 0x03, 0xC3, 0xD3, 0x73, 0x1C, 0x07, 0xDA, 0x1F, 0x8C, 0xC7, 0xBD, 0x9D, 0xB7, @@ -889,7 +913,7 @@ mod test { let mut env = TestEnv::new(); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let cred_management_params = AuthenticatorCredentialManagementParameters { sub_command: CredentialManagementSubCommand::GetCredsMetadata, diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 5752d05..fa0f69a 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -320,11 +320,12 @@ impl Ctap1Command { return Err(Ctap1StatusCode::SW_COND_USE_NOT_SATISFIED); } ctap_state - .increment_global_signature_counter(env) + .increment_global_signature_counter(env, 0) .map_err(|_| Ctap1StatusCode::SW_WRONG_DATA)?; let mut signature_data = ctap_state .generate_auth_data( env, + 0, &application, Ctap1Command::USER_PRESENCE_INDICATOR_BYTE, ) @@ -651,7 +652,7 @@ mod test { Ctap1Command::process_command(&mut env, &message, &mut ctap_state, CtapInstant::new(0)) .unwrap(); assert_eq!(response[0], 0x01); - let global_signature_counter = storage::global_signature_counter(&mut env).unwrap(); + let global_signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); check_signature_counter( &mut env, array_ref!(response, 1, 4), @@ -684,7 +685,7 @@ mod test { ) .unwrap(); assert_eq!(response[0], 0x01); - let global_signature_counter = storage::global_signature_counter(&mut env).unwrap(); + let global_signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); check_signature_counter( &mut env, array_ref!(response, 1, 4), diff --git a/src/ctap/large_blobs.rs b/src/ctap/large_blobs.rs index d8588de..949cdce 100644 --- a/src/ctap/large_blobs.rs +++ b/src/ctap/large_blobs.rs @@ -87,7 +87,9 @@ impl LargeBlobs { if offset != self.expected_next_offset { return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_SEQ); } - if storage::pin_hash(env)?.is_some() || storage::has_always_uv(env)? { + // TODO: Get the slot id from active token state when multi-PIN is enabled. + let slot_id = 0; + if storage::pin_hash(env, slot_id)?.is_some() || storage::has_always_uv(env)? { let pin_uv_auth_param = pin_uv_auth_param.ok_or(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED)?; let pin_uv_auth_protocol = @@ -147,8 +149,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let mut large_blobs = LargeBlobs::new(); let large_blob = vec![ @@ -178,8 +184,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -240,8 +250,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -286,8 +300,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 200; @@ -332,8 +350,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let mut large_blobs = LargeBlobs::new(); let large_blobs_params = AuthenticatorLargeBlobsParameters { @@ -355,8 +377,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -383,8 +409,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let mut client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + let mut client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + pin_uv_auth_protocol, + ); let mut large_blobs = LargeBlobs::new(); const BLOB_LEN: usize = 20; @@ -392,7 +422,7 @@ mod test { let mut large_blob = vec![0x1B; DATA_LEN]; large_blob.extend_from_slice(&Sha256::hash(&large_blob[..])[..TRUNCATED_HASH_LEN]); - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let mut large_blob_data = vec![0xFF; 32]; // Command constant and offset bytes. large_blob_data.extend(&[0x0C, 0x00, 0x00, 0x00, 0x00, 0x00]); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index b47a8f2..dae7165 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -551,7 +551,7 @@ pub struct CtapState { impl CtapState { pub fn new(env: &mut impl Env, now: CtapInstant) -> Self { storage::init(env).ok().unwrap(); - let client_pin = ClientPin::new(env.rng()); + let client_pin = ClientPin::new(env); CtapState { client_pin, #[cfg(feature = "with_ctap1")] @@ -569,10 +569,11 @@ impl CtapState { pub fn increment_global_signature_counter( &mut self, env: &mut impl Env, + slot_id: usize, ) -> Result<(), Ctap2StatusCode> { if env.customization().use_signature_counter() { let increment = env.rng().gen_uniform_u32x8()[0] % 8 + 1; - storage::incr_global_signature_counter(env, increment)?; + storage::incr_global_signature_counter(env, slot_id, increment)?; } Ok(()) } @@ -722,6 +723,7 @@ impl CtapState { fn pin_uv_auth_precheck( &mut self, env: &mut impl Env, + slot_id: Option, pin_uv_auth_param: &Option>, pin_uv_auth_protocol: Option, channel: Channel, @@ -730,7 +732,7 @@ impl CtapState { // This case was added in FIDO 2.1. if auth_param.is_empty() { check_user_presence(env, channel)?; - if storage::pin_hash(env)?.is_none() { + if slot_id.is_none() || storage::pin_hash(env, slot_id.unwrap())?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); @@ -776,7 +778,16 @@ impl CtapState { enterprise_attestation, } = make_credential_params; - self.pin_uv_auth_precheck(env, &pin_uv_auth_param, pin_uv_auth_protocol, channel)?; + // TODO: Get the slot id from active token state when multi-PIN is enabled. + let slot_id = Some(0); + + self.pin_uv_auth_precheck( + env, + slot_id, + &pin_uv_auth_param, + pin_uv_auth_protocol, + channel, + )?; // When more algorithms are supported, iterate and pick the first match. let cred_param = get_preferred_cred_param(&pub_key_cred_params) @@ -813,7 +824,7 @@ impl CtapState { 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 storage::pin_hash(env)?.is_none() { + if slot_id.is_some() && storage::pin_hash(env, slot_id.unwrap())?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } self.client_pin.verify_pin_uv_auth_token( @@ -838,13 +849,19 @@ impl CtapState { return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); } // Corresponds to makeCredUvNotRqd set to true. - if options.rk && storage::pin_hash(env)?.is_some() { + if options.rk + && slot_id.is_some() + && storage::pin_hash(env, slot_id.unwrap())?.is_some() + { return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); } 0x00 } }; flags |= UP_FLAG | AT_FLAG; + // By this point, we should have already returned the suitable errors in all situations that + // slot_id is none. + let slot_id = slot_id.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; let rp_id_hash = Sha256::hash(rp_id.as_bytes()); if let Some(exclude_list) = exclude_list { @@ -901,6 +918,7 @@ impl CtapState { // We decide on the algorithm early, but delay key creation since it takes time. // We rather do that later so all intermediate checks may return faster. let private_key = PrivateKey::new(env, algorithm); + // TODO: persist slot_id in the credential. let credential_id = if options.rk { let random_id = env.rng().gen_uniform_u8x32().to_vec(); let credential_source = PublicKeyCredentialSource { @@ -937,7 +955,7 @@ impl CtapState { )? }; - let mut auth_data = self.generate_auth_data(env, &rp_id_hash, flags)?; + let mut auth_data = self.generate_auth_data(env, slot_id, &rp_id_hash, flags)?; auth_data.extend(&storage::aaguid(env)?); // The length is fixed to 0x20 or 0x80 and fits one byte. if credential_id.len() > 0xFF { @@ -1157,7 +1175,16 @@ impl CtapState { pin_uv_auth_protocol, } = get_assertion_params; - self.pin_uv_auth_precheck(env, &pin_uv_auth_param, pin_uv_auth_protocol, channel)?; + // TODO: Get the slot id from active token state when multi-PIN is enabled. + let slot_id = Some(0); + + self.pin_uv_auth_precheck( + env, + slot_id, + &pin_uv_auth_param, + pin_uv_auth_protocol, + channel, + )?; if extensions.hmac_secret.is_some() && !options.up { // The extension is actually supported, but we need user presence. @@ -1170,7 +1197,7 @@ impl CtapState { 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 storage::pin_hash(env)?.is_none() { + if slot_id.is_some() && storage::pin_hash(env, slot_id.unwrap())?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } self.client_pin.verify_pin_uv_auth_token( @@ -1203,8 +1230,12 @@ impl CtapState { if extensions.hmac_secret.is_some() || extensions.cred_blob { flags |= ED_FLAG; } + // By this point, we should have already returned the suitable errors in all situations that + // slot_id is none. + let slot_id = slot_id.ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; let rp_id_hash = Sha256::hash(rp_id.as_bytes()); + // TODO: check slot_id in the credential matches. let (credential, next_credential_keys) = if let Some(allow_list) = allow_list { ( self.get_any_credential_from_allow_list( @@ -1249,11 +1280,11 @@ impl CtapState { self.client_pin.clear_token_flags(); } - self.increment_global_signature_counter(env)?; + self.increment_global_signature_counter(env, slot_id)?; let assertion_input = AssertionInput { client_data_hash, - auth_data: self.generate_auth_data(env, &rp_id_hash, flags)?, + auth_data: self.generate_auth_data(env, slot_id, &rp_id_hash, flags)?, extensions, has_uv, }; @@ -1310,7 +1341,13 @@ impl CtapState { (String::from("alwaysUv"), has_always_uv), (String::from("credMgmt"), true), (String::from("authnrCfg"), true), - (String::from("clientPin"), storage::pin_hash(env)?.is_some()), + // TODO: Return in another field the actual slot names that have a PIN set when + // multi-PIN feature is enabled, and omit this field. As this field is only filled + // in when multi-PIN feature is disabled, just check PIN at slot 0. + ( + String::from("clientPin"), + storage::pin_hash(env, 0)?.is_some(), + ), (String::from("largeBlobs"), true), (String::from("pinUvAuthToken"), true), (String::from("setMinPINLength"), true), @@ -1345,7 +1382,10 @@ impl CtapState { max_serialized_large_blob_array: Some( env.customization().max_large_blob_array_size() as u64, ), - force_pin_change: Some(storage::has_force_pin_change(env)?), + // TODO: Return in another field the actual slot names that need to force change + // the PIN when multi-PIN feature is enabled, and omit this field. As this field + // is only filled in when multi-PIN feature is disabled, just check PIN at slot 0. + force_pin_change: Some(storage::has_force_pin_change(env, 0)?), min_pin_length: storage::min_pin_length(env)?, firmware_version: None, max_cred_blob_length: Some(env.customization().max_cred_blob_length() as u64), @@ -1503,6 +1543,7 @@ impl CtapState { pub fn generate_auth_data( &self, env: &mut impl Env, + slot_id: usize, rp_id_hash: &[u8], flag_byte: u8, ) -> Result, Ctap2StatusCode> { @@ -1514,7 +1555,7 @@ impl CtapState { let mut signature_counter = [0u8; 4]; BigEndian::write_u32( &mut signature_counter, - storage::global_signature_counter(env)?, + storage::global_signature_counter(env, slot_id)?, ); auth_data.extend(&signature_counter); Ok(auth_data) @@ -2111,12 +2152,16 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.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 client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + pin_uv_auth_protocol, + ); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); ctap_state.client_pin = client_pin; - storage::set_pin(&mut env, &[0x88; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0x88; 16], 4).unwrap(); let client_data_hash = [0xCD]; let pin_uv_auth_param = authenticate_pin_uv_auth_token( @@ -2164,7 +2209,7 @@ mod test { fn test_non_resident_process_make_credential_with_pin() { let mut env = TestEnv::new(); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); - storage::set_pin(&mut env, &[0x88; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0x88; 16], 4).unwrap(); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -2184,7 +2229,7 @@ mod test { fn test_resident_process_make_credential_with_pin() { let mut env = TestEnv::new(); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); - storage::set_pin(&mut env, &[0x88; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0x88; 16], 4).unwrap(); let make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = @@ -2209,7 +2254,7 @@ mod test { Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED) ); - storage::set_pin(&mut env, &[0x88; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[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(PinUvAuthProtocol::V1); @@ -2466,7 +2511,7 @@ mod test { DUMMY_CHANNEL, CtapInstant::new(0), ); - let signature_counter = storage::global_signature_counter(&mut env).unwrap(); + let signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); check_assertion_response(get_assertion_response, vec![0x1D], signature_counter, None); } @@ -2709,7 +2754,7 @@ mod test { DUMMY_CHANNEL, CtapInstant::new(0), ); - let signature_counter = storage::global_signature_counter(&mut env).unwrap(); + let signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); check_assertion_response(get_assertion_response, vec![0x1D], signature_counter, None); let credential = PublicKeyCredentialSource { @@ -2878,7 +2923,7 @@ mod test { DUMMY_CHANNEL, CtapInstant::new(0), ); - let signature_counter = storage::global_signature_counter(&mut env).unwrap(); + let signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); let expected_extension_cbor = [ 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0x41, 0xCB, ]; @@ -2947,7 +2992,7 @@ mod test { DUMMY_CHANNEL, CtapInstant::new(0), ); - let signature_counter = storage::global_signature_counter(&mut env).unwrap(); + let signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); let expected_extension_cbor = [ 0xA1, 0x68, 0x63, 0x72, 0x65, 0x64, 0x42, 0x6C, 0x6F, 0x62, 0x41, 0xCB, ]; @@ -3020,8 +3065,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x88; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, pin_uv_auth_protocol); + let client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + pin_uv_auth_protocol, + ); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); @@ -3050,7 +3099,7 @@ mod test { ctap_state.client_pin = client_pin; // The PIN length is outside of the test scope and most likely incorrect. - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let client_data_hash = vec![0xCD]; let pin_uv_auth_param = authenticate_pin_uv_auth_token( &pin_uv_auth_token, @@ -3076,7 +3125,7 @@ mod test { DUMMY_CHANNEL, CtapInstant::new(0), ); - let signature_counter = storage::global_signature_counter(&mut env).unwrap(); + let signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); check_assertion_response_with_user( get_assertion_response, Some(user2), @@ -3161,7 +3210,7 @@ mod test { DUMMY_CHANNEL, CtapInstant::new(0), ); - let signature_counter = storage::global_signature_counter(&mut env).unwrap(); + let signature_counter = storage::global_signature_counter(&mut env, 0).unwrap(); check_assertion_response( get_assertion_response, vec![0x03], @@ -3336,13 +3385,13 @@ mod test { let mut env = TestEnv::new(); let mut ctap_state = CtapState::new(&mut env, CtapInstant::new(0)); - let mut last_counter = storage::global_signature_counter(&mut env).unwrap(); + let mut last_counter = storage::global_signature_counter(&mut env, 0).unwrap(); assert!(last_counter > 0); for _ in 0..100 { assert!(ctap_state - .increment_global_signature_counter(&mut env) + .increment_global_signature_counter(&mut env, 0) .is_ok()); - let next_counter = storage::global_signature_counter(&mut env).unwrap(); + let next_counter = storage::global_signature_counter(&mut env, 0).unwrap(); assert!(next_counter > last_counter); last_counter = next_counter; } @@ -3749,8 +3798,12 @@ mod test { let mut env = TestEnv::new(); let key_agreement_key = crypto::ecdh::SecKey::gensk(env.rng()); let pin_uv_auth_token = [0x55; 32]; - let client_pin = - ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + let client_pin = ClientPin::new_test( + &mut env, + key_agreement_key, + pin_uv_auth_token, + PinUvAuthProtocol::V1, + ); let private_key = PrivateKey::new_ecdsa(&mut env); let credential_source = PublicKeyCredentialSource { @@ -3777,7 +3830,7 @@ mod test { storage::store_credential(&mut env, credential).unwrap(); } - storage::set_pin(&mut env, &[0u8; 16], 4).unwrap(); + storage::set_pin(&mut env, 0, &[0u8; 16], 4).unwrap(); let pin_uv_auth_param = Some(vec![ 0x1A, 0xA4, 0x96, 0xDA, 0x62, 0x80, 0x28, 0x13, 0xEB, 0x32, 0xB9, 0xF1, 0xD2, 0xA9, 0xD0, 0xD1, diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 2fb0939..fecf4c7 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -30,7 +30,7 @@ use alloc::vec::Vec; use arrayref::array_ref; use core::cmp; use core::convert::TryInto; -use persistent_store::{fragment, StoreUpdate}; +use persistent_store::{concat, fragment}; use rng256::Rng256; use sk_cbor::cbor_array_vec; @@ -237,9 +237,34 @@ pub fn new_creation_order(env: &mut impl Env) -> Result { Ok(max.unwrap_or(0).wrapping_add(1)) } +fn check_and_get_key_for_slot( + env: &mut impl Env, + slot_id: usize, + first_key: usize, + key_array: core::ops::Range, +) -> Result { + if slot_id >= env.customization().slot_count() { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); + } + Ok(if slot_id == 0 { + first_key + } else { + key_array.start + slot_id - 1 + }) +} + /// Returns the global signature counter. -pub fn global_signature_counter(env: &mut impl Env) -> Result { - match env.store().find(key::GLOBAL_SIGNATURE_COUNTER)? { +pub fn global_signature_counter( + env: &mut impl Env, + slot_id: usize, +) -> Result { + let key = check_and_get_key_for_slot( + env, + slot_id, + key::FIRST_GLOBAL_SIGNATURE_COUNTER, + key::GLOBAL_SIGNATURE_COUNTER, + )?; + match env.store().find(key)? { 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_INTERNAL_ERROR), @@ -249,13 +274,19 @@ pub fn global_signature_counter(env: &mut impl Env) -> Result Result<(), Ctap2StatusCode> { - let old_value = global_signature_counter(env)?; + let key = check_and_get_key_for_slot( + env, + slot_id, + key::FIRST_GLOBAL_SIGNATURE_COUNTER, + key::GLOBAL_SIGNATURE_COUNTER, + )?; + let old_value = global_signature_counter(env, slot_id)?; // In hopes that servers handle the wrapping gracefully. let new_value = old_value.wrapping_add(increment); - env.store() - .insert(key::GLOBAL_SIGNATURE_COUNTER, &new_value.to_ne_bytes())?; + env.store().insert(key, &new_value.to_ne_bytes())?; Ok(()) } @@ -273,9 +304,22 @@ pub fn cred_random_secret(env: &mut impl Env, has_uv: bool) -> Result<[u8; 32], } /// Reads the PIN properties and wraps them into PinProperties. -fn pin_properties(env: &mut impl Env) -> Result, Ctap2StatusCode> { - let pin_properties = match env.store().find(key::PIN_PROPERTIES)? { - None => return Ok(None), +fn pin_properties( + env: &mut impl Env, + slot_id: usize, +) -> Result, Ctap2StatusCode> { + let pin_properties = match concat::read(env.store(), key::PIN_PROPERTIES, slot_id as u8)? { + None => { + // Backward compatibility: old implementation where there is only 1 PIN slot + // uses the entry with key `FIRST_PIN_PROPERTIES`. + if slot_id != 0 { + return Ok(None); + } + match env.store().find(key::FIRST_PIN_PROPERTIES)? { + None => return Ok(None), + Some(pin_properties) => pin_properties, + } + } Some(pin_properties) => pin_properties, }; const PROPERTIES_LENGTH: usize = PIN_AUTH_LENGTH + 1; @@ -288,14 +332,30 @@ fn pin_properties(env: &mut impl Env) -> Result, Ctap2Stat } } +/// Returns if PIN is set for at least one slot. +pub fn has_pin(env: &mut impl Env) -> Result { + for slot_id in 0..env.customization().slot_count() { + if pin_hash(env, slot_id)?.is_some() { + return Ok(true); + } + } + Ok(false) +} + /// Returns the PIN hash if defined. -pub fn pin_hash(env: &mut impl Env) -> Result, Ctap2StatusCode> { - Ok(pin_properties(env)?.map(|p| p.hash)) +pub fn pin_hash( + env: &mut impl Env, + slot_id: usize, +) -> Result, Ctap2StatusCode> { + Ok(pin_properties(env, slot_id)?.map(|p| p.hash)) } /// Returns the length of the currently set PIN if defined. -pub fn pin_code_point_length(env: &mut impl Env) -> Result, Ctap2StatusCode> { - Ok(pin_properties(env)?.map(|p| p.code_point_length)) +pub fn pin_code_point_length( + env: &mut impl Env, + slot_id: usize, +) -> Result, Ctap2StatusCode> { + Ok(pin_properties(env, slot_id)?.map(|p| p.code_point_length)) } /// Sets the PIN hash and length. @@ -303,26 +363,31 @@ pub fn pin_code_point_length(env: &mut impl Env) -> Result, Ctap2Stat /// If it was already defined, it is updated. pub fn set_pin( env: &mut impl Env, + slot_id: usize, pin_hash: &[u8; PIN_AUTH_LENGTH], pin_code_point_length: u8, ) -> Result<(), Ctap2StatusCode> { + concat::delete(env.store(), key::FORCE_PIN_CHANGE, slot_id as u8)?; + if slot_id == 0 { + // Backward compatibility: data might be stored in this entry for slot 0 as well. + env.store().remove(key::FIRST_FORCE_PIN_CHANGE)?; + } 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(env.store().transaction(&[ - StoreUpdate::Insert { - key: key::PIN_PROPERTIES, - value: &pin_properties[..], - }, - StoreUpdate::Remove { - key: key::FORCE_PIN_CHANGE, - }, - ])?) + concat::write( + env.store(), + key::PIN_PROPERTIES, + slot_id as u8, + &pin_properties[..], + )?; + Ok(()) } /// Returns the number of remaining PIN retries. -pub fn pin_retries(env: &mut impl Env) -> Result { - match env.store().find(key::PIN_RETRIES)? { +pub fn pin_retries(env: &mut impl Env, slot_id: usize) -> Result { + let key = check_and_get_key_for_slot(env, slot_id, key::FIRST_PIN_RETRIES, key::PIN_RETRIES)?; + match env.store().find(key)? { None => Ok(env.customization().max_pin_retries()), Some(value) if value.len() == 1 => Ok(value[0]), _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), @@ -330,18 +395,20 @@ pub fn pin_retries(env: &mut impl Env) -> Result { } /// Decrements the number of remaining PIN retries. -pub fn decr_pin_retries(env: &mut impl Env) -> Result<(), Ctap2StatusCode> { - let old_value = pin_retries(env)?; +pub fn decr_pin_retries(env: &mut impl Env, slot_id: usize) -> Result<(), Ctap2StatusCode> { + let key = check_and_get_key_for_slot(env, slot_id, key::FIRST_PIN_RETRIES, key::PIN_RETRIES)?; + let old_value = pin_retries(env, slot_id)?; let new_value = old_value.saturating_sub(1); if new_value != old_value { - env.store().insert(key::PIN_RETRIES, &[new_value])?; + env.store().insert(key, &[new_value])?; } Ok(()) } /// Resets the number of remaining PIN retries. -pub fn reset_pin_retries(env: &mut impl Env) -> Result<(), Ctap2StatusCode> { - Ok(env.store().remove(key::PIN_RETRIES)?) +pub fn reset_pin_retries(env: &mut impl Env, slot_id: usize) -> Result<(), Ctap2StatusCode> { + let key = check_and_get_key_for_slot(env, slot_id, key::FIRST_PIN_RETRIES, key::PIN_RETRIES)?; + Ok(env.store().remove(key)?) } /// Returns the minimum PIN length. @@ -467,17 +534,34 @@ pub fn reset(env: &mut impl Env) -> Result<(), Ctap2StatusCode> { } /// Returns whether the PIN needs to be changed before its next usage. -pub fn has_force_pin_change(env: &mut impl Env) -> Result { - match env.store().find(key::FORCE_PIN_CHANGE)? { - None => Ok(false), +pub fn has_force_pin_change(env: &mut impl Env, slot_id: usize) -> Result { + match concat::read(env.store(), key::FORCE_PIN_CHANGE, slot_id as u8)? { + None => { + if slot_id != 0 { + Ok(false) + } else { + // Backward compatibility: old implementation where there is only 1 PIN slot + // uses the entry with key `FIRST_FORCE_PIN_CHANGE`. + match env.store().find(key::FIRST_FORCE_PIN_CHANGE)? { + None => Ok(false), + Some(value) if value.is_empty() => Ok(true), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + } 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(env: &mut impl Env) -> Result<(), Ctap2StatusCode> { - Ok(env.store().insert(key::FORCE_PIN_CHANGE, &[])?) +pub fn force_pin_change(env: &mut impl Env, slot_id: usize) -> Result<(), Ctap2StatusCode> { + Ok(concat::write( + env.store(), + key::FORCE_PIN_CHANGE, + slot_id as u8, + &[], + )?) } /// Returns whether enterprise attestation is enabled. @@ -899,8 +983,8 @@ mod test { let mut env = TestEnv::new(); // Pin hash is initially not set. - assert!(pin_hash(&mut env).unwrap().is_none()); - assert!(pin_code_point_length(&mut env).unwrap().is_none()); + assert!(pin_hash(&mut env, 0).unwrap().is_none()); + assert!(pin_code_point_length(&mut env, 0).unwrap().is_none()); // Setting the pin sets the pin hash. let random_data = env.rng().gen_uniform_u8x32(); @@ -909,17 +993,23 @@ mod test { let pin_hash_2 = *array_ref!(random_data, PIN_AUTH_LENGTH, PIN_AUTH_LENGTH); let pin_length_1 = 4; let pin_length_2 = 63; - set_pin(&mut env, &pin_hash_1, pin_length_1).unwrap(); - assert_eq!(pin_hash(&mut env).unwrap(), Some(pin_hash_1)); - assert_eq!(pin_code_point_length(&mut env).unwrap(), Some(pin_length_1)); - set_pin(&mut env, &pin_hash_2, pin_length_2).unwrap(); - assert_eq!(pin_hash(&mut env).unwrap(), Some(pin_hash_2)); - assert_eq!(pin_code_point_length(&mut env).unwrap(), Some(pin_length_2)); + set_pin(&mut env, 0, &pin_hash_1, pin_length_1).unwrap(); + assert_eq!(pin_hash(&mut env, 0).unwrap(), Some(pin_hash_1)); + assert_eq!( + pin_code_point_length(&mut env, 0).unwrap(), + Some(pin_length_1) + ); + set_pin(&mut env, 0, &pin_hash_2, pin_length_2).unwrap(); + assert_eq!(pin_hash(&mut env, 0).unwrap(), Some(pin_hash_2)); + assert_eq!( + pin_code_point_length(&mut env, 0).unwrap(), + Some(pin_length_2) + ); // Resetting the storage resets the pin hash. reset(&mut env).unwrap(); - assert!(pin_hash(&mut env).unwrap().is_none()); - assert!(pin_code_point_length(&mut env).unwrap().is_none()); + assert!(pin_hash(&mut env, 0).unwrap().is_none()); + assert!(pin_code_point_length(&mut env, 0).unwrap().is_none()); } #[test] @@ -928,24 +1018,24 @@ mod test { // The pin retries is initially at the maximum. assert_eq!( - pin_retries(&mut env), + pin_retries(&mut env, 0), Ok(env.customization().max_pin_retries()) ); // Decrementing the pin retries decrements the pin retries. for retries in (0..env.customization().max_pin_retries()).rev() { - decr_pin_retries(&mut env).unwrap(); - assert_eq!(pin_retries(&mut env), Ok(retries)); + decr_pin_retries(&mut env, 0).unwrap(); + assert_eq!(pin_retries(&mut env, 0), Ok(retries)); } // Decrementing the pin retries after zero does not modify the pin retries. - decr_pin_retries(&mut env).unwrap(); - assert_eq!(pin_retries(&mut env), Ok(0)); + decr_pin_retries(&mut env, 0).unwrap(); + assert_eq!(pin_retries(&mut env, 0), Ok(0)); // Resetting the pin retries resets the pin retries. - reset_pin_retries(&mut env).unwrap(); + reset_pin_retries(&mut env, 0).unwrap(); assert_eq!( - pin_retries(&mut env), + pin_retries(&mut env, 0), Ok(env.customization().max_pin_retries()) ); } @@ -1088,11 +1178,17 @@ mod test { let mut env = TestEnv::new(); let mut counter_value = 1; - assert_eq!(global_signature_counter(&mut env).unwrap(), counter_value); + assert_eq!( + global_signature_counter(&mut env, 0).unwrap(), + counter_value + ); for increment in 1..10 { - assert!(incr_global_signature_counter(&mut env, increment).is_ok()); + assert!(incr_global_signature_counter(&mut env, 0, increment).is_ok()); counter_value += increment; - assert_eq!(global_signature_counter(&mut env).unwrap(), counter_value); + assert_eq!( + global_signature_counter(&mut env, 0).unwrap(), + counter_value + ); } } @@ -1100,11 +1196,11 @@ mod test { fn test_force_pin_change() { let mut env = TestEnv::new(); - assert!(!has_force_pin_change(&mut env).unwrap()); - assert_eq!(force_pin_change(&mut env), Ok(())); - assert!(has_force_pin_change(&mut env).unwrap()); - assert_eq!(set_pin(&mut env, &[0x88; 16], 8), Ok(())); - assert!(!has_force_pin_change(&mut env).unwrap()); + assert!(!has_force_pin_change(&mut env, 0).unwrap()); + assert_eq!(force_pin_change(&mut env, 0), Ok(())); + assert!(has_force_pin_change(&mut env, 0).unwrap()); + assert_eq!(set_pin(&mut env, 0, &[0x88; 16], 8), Ok(())); + assert!(!has_force_pin_change(&mut env, 0).unwrap()); } #[test] diff --git a/src/ctap/storage/key.rs b/src/ctap/storage/key.rs index 6b6f28a..7d56598 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -73,6 +73,32 @@ make_partition! { // - When adding a (non-persistent) key below this message, make sure its value is bigger or // equal than NUM_PERSISTENT_KEYS. + // Start of key arrays for multi-PIN feature: these fields are separated for each slots, so + // a unique key is needed for each slot. However, we reuse the existing fields and rename them + // to `FIRST_{KEY_NAME}` so the upgrade is backward compatible. + // Depending on `Customization::slot_count()`, only a prefix of those keys is used. + + /// Whether the PIN needs to be changed each slot. + /// + /// The PIN needs to be changed if the slot exists and its data is empty. + FORCE_PIN_CHANGE = 984; + + /// The number of PIN retries for each slot, except the first. + PIN_RETRIES = 985..992; + + /// The PIN hash and length for each slot. + /// + /// If a slot is absent, there is no PIN set for that slot. The first byte represents + /// the length, the following are an array with the hash. + PIN_PROPERTIES = 992; + + /// The global signature counters for each slot, except the first. + /// + /// If the entry is absent, the counter is 0. + GLOBAL_SIGNATURE_COUNTER = 993..1000; + + // End of key arrays for multi-PIN feature. + /// Reserved for future credential-related objects. /// /// In particular, additional credentials could be added there by reducing the lower bound of @@ -97,8 +123,10 @@ make_partition! { /// 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; + /// Whether the PIN needs to be changed for the first slot. + /// + /// The PIN needs to be changed if this entry exists and is empty. + FIRST_FORCE_PIN_CHANGE = 2040; /// The secret of the CredRandom feature. CRED_RANDOM_SECRET = 2041; @@ -111,24 +139,22 @@ make_partition! { /// If the entry is absent, the minimum PIN length is `Customization::default_min_pin_length()`. MIN_PIN_LENGTH = 2043; - /// The number of PIN retries. + /// The number of PIN retries for the first slot. /// /// If the entry is absent, the number of PIN retries is `Customization::max_pin_retries()`. - PIN_RETRIES = 2044; + FIRST_PIN_RETRIES = 2044; - /// The PIN hash and length. + /// The PIN hash and length for the first slot. /// /// 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; + FIRST_PIN_PROPERTIES = 2045; /// Reserved for the key store implementation of the environment. _RESERVED_KEY_STORE = 2046; - /// The global signature counter. - /// - /// If the entry is absent, the counter is 0. - GLOBAL_SIGNATURE_COUNTER = 2047; + /// The global signature counter for the first slot. + FIRST_GLOBAL_SIGNATURE_COUNTER = 2047; } #[cfg(test)] diff --git a/src/env/test/customization.rs b/src/env/test/customization.rs index e6fe45e..1a24ce0 100644 --- a/src/env/test/customization.rs +++ b/src/env/test/customization.rs @@ -33,6 +33,7 @@ pub struct TestCustomization { max_large_blob_array_size: usize, max_rp_ids_length: usize, max_supported_resident_keys: usize, + slot_count: usize, } impl TestCustomization { @@ -112,6 +113,10 @@ impl Customization for TestCustomization { fn max_supported_resident_keys(&self) -> usize { self.max_supported_resident_keys } + + fn slot_count(&self) -> usize { + self.slot_count + } } impl From for TestCustomization { @@ -132,6 +137,7 @@ impl From for TestCustomization { max_large_blob_array_size, max_rp_ids_length, max_supported_resident_keys, + slot_count, } = c; let default_min_pin_length_rp_ids = default_min_pin_length_rp_ids @@ -160,6 +166,7 @@ impl From for TestCustomization { max_large_blob_array_size, max_rp_ids_length, max_supported_resident_keys, + slot_count, } } }