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/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 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/.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: diff --git a/Cargo.toml b/Cargo.toml index 15984a0..b7b0360 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,12 +22,11 @@ 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_ctap2_1 = [] with_nfc = ["libtock_drivers/with_nfc"] [dev-dependencies] diff --git a/README.md b/README.md index 8177220..9a49826 100644 --- a/README.md +++ b/README.md @@ -94,33 +94,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`. -2. 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 - 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 - 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`. - When changing the default, resident credentials become undiscoverable without - user verification. This helps privacy, but can make usage less comfortable - for credentials that need less protection. -6. Increase the default minimum length for PINs in `ctap/storage.rs`. - The current minimum is 4. Values from 4 to 63 are allowed. 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. + 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 b3daa6f..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"), @@ -881,14 +887,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 +945,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 prints " + "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 +} 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/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/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/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 c735d11..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; @@ -26,7 +24,7 @@ pub struct SecKey { a: NonZeroExponentP256, } -#[cfg_attr(feature = "derive_debug", derive(Clone, PartialEq, Debug))] +#[derive(Clone, Debug, PartialEq)] pub struct PubKey { p: PointP256, } @@ -62,13 +60,15 @@ 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" - pub fn exchange_x_sha256(&self, other: &PubKey) -> [u8; 32] { + /// Performs the handshake using the Diffie Hellman key agreement. + /// + /// 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 } } @@ -83,11 +83,13 @@ impl PubKey { self.p.to_bytes_uncompressed(bytes); } + /// 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 }) } + /// 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); @@ -119,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 { @@ -127,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 { @@ -146,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/libraries/crypto/src/ecdsa.rs b/libraries/crypto/src/ecdsa.rs index 52949e3..eb61365 100644 --- a/libraries/crypto/src/ecdsa.rs +++ b/libraries/crypto/src/ecdsa.rs @@ -21,14 +21,16 @@ 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; -#[derive(Clone, PartialEq)] -#[cfg_attr(feature = "derive_debug", derive(Debug))] +pub const NBYTES: usize = int256::NBYTES; + +#[derive(Clone, Debug, PartialEq)] pub struct SecKey { k: NonZeroExponentP256, } @@ -38,6 +40,7 @@ pub struct Signature { s: NonZeroExponentP256, } +#[derive(Clone)] pub struct PubKey { p: PointP256, } @@ -58,10 +61,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 +81,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 +104,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. @@ -214,7 +219,6 @@ impl Signature { } impl PubKey { - pub const ES256_ALGORITHM: i64 = -7; #[cfg(feature = "with_ctap1")] const UNCOMPRESSED_LENGTH: usize = 1 + 2 * int256::NBYTES; @@ -242,35 +246,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/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; 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/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 1f87ef3..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; @@ -20,18 +22,20 @@ 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; /// 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); @@ -46,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())) } @@ -59,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. @@ -108,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. @@ -120,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 { @@ -162,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()); @@ -185,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 @@ -198,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 } @@ -234,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(), @@ -254,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) } @@ -335,12 +328,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 +361,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 +452,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. @@ -492,13 +485,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 +504,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 +519,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; } @@ -550,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()) } @@ -564,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. @@ -711,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); @@ -756,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) } @@ -921,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 } @@ -1077,4 +1076,15 @@ mod tests { 0xff800000 ); } + + #[test] + fn position_offsets_fit_in_a_halfword() { + // 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); + } } 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/fragment.rs b/libraries/persistent_store/src/fragment.rs new file mode 100644 index 0000000..661d5db --- /dev/null +++ b/libraries/persistent_store/src/fragment.rs @@ -0,0 +1,345 @@ +// 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. + +//! 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. +//! 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. +#[allow(clippy::len_without_is_empty)] +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` < [`Self::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 { + 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) +} + +#[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..4)), Ok(None)); + } + + #[test] + fn read_single_chunk() { + let mut store = MINIMAL.new_store(); + let value = b"hello".to_vec(); + assert_eq!(store.insert(0, &value), Ok(())); + assert_eq!(read(&store, &(0..4)), Ok(Some(value))); + } + + #[test] + fn read_multiple_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(&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 c8be44b..4be15e3 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. @@ -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,36 +312,30 @@ //! | `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)] #[macro_use] extern crate alloc; @@ -353,10 +345,13 @@ mod buffer; #[cfg(feature = "std")] mod driver; mod format; +pub mod fragment; #[cfg(feature = "std")] 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/model.rs b/libraries/persistent_store/src/model.rs index c509b03..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. @@ -34,7 +37,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 +92,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/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 2559485..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, @@ -23,8 +25,12 @@ 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::borrow::Borrow; use core::cmp::{max, min, Ordering}; +use core::convert::TryFrom; +use core::option::NoneError; #[cfg(feature = "std")] use std::collections::HashSet; @@ -51,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, } @@ -75,20 +78,26 @@ 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; /// 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 -#[derive(Copy, Clone, PartialEq, Eq)] +/// - 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. pub(crate) used: Nat, @@ -136,11 +145,20 @@ impl StoreHandle { self.key as usize } + /// Returns the value length of the entry. + /// + /// # Errors + /// + /// Returns [`StoreError::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 /// - /// 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) } @@ -148,15 +166,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 { @@ -168,12 +186,14 @@ 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, } } } +pub type StoreIter<'a> = Box> + 'a>; + /// Implements a store with a map interface over a storage. #[derive(Clone)] pub struct Store { @@ -182,6 +202,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::head). + entries: Option>, } impl Store { @@ -193,13 +221,19 @@ 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)), 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,31 +241,35 @@ 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. + /// Returns the current and total capacity in words. /// /// The capacity represents the size of what is stored. 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 }) } - /// 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 @@ -246,18 +284,22 @@ 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. - pub fn transaction(&mut self, updates: &[StoreUpdate]) -> StoreResult<()> { + /// - 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], + ) -> 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), } } @@ -270,7 +312,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. @@ -278,7 +322,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])?; @@ -287,7 +331,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 } @@ -307,7 +351,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 { @@ -373,7 +419,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()?; @@ -381,6 +427,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 +445,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. @@ -406,6 +454,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)?; @@ -437,7 +496,7 @@ impl Store { let init_info = self.format.build_init(InitInfo { cycle: 0, prefix: 0, - }); + })?; self.storage_write_slice(index, &init_info) } @@ -460,7 +519,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 +531,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 +672,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,20 +702,22 @@ 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); } 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() } /// 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 +731,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,11 +744,13 @@ 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; } - 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) @@ -688,14 +758,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 +791,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 +814,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(()) } @@ -792,7 +880,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(()) } @@ -800,7 +888,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(()) } @@ -836,19 +924,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 +952,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 +1174,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 +1200,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)), } } @@ -1110,10 +1224,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. @@ -1165,61 +1281,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. @@ -1239,71 +1300,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] @@ -1438,4 +1443,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])); + } } diff --git a/libraries/persistent_store/src/test.rs b/libraries/persistent_store/src/test.rs new file mode 100644 index 0000000..2d20574 --- /dev/null +++ b/libraries/persistent_store/src/test.rs @@ -0,0 +1,84 @@ +// 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::{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, +}; + +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/run_desktop_tests.sh b/run_desktop_tests.sh index 2e80b3d..b54d3a8 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 @@ -92,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 @@ -104,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 @@ -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 diff --git a/src/ctap/apdu.rs b/src/ctap/apdu.rs index c99bf84..455e574 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. @@ -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 @@ -30,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 @@ -51,9 +51,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 +71,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 +83,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/client_pin.rs b/src/ctap/client_pin.rs new file mode 100644 index 0000000..8b6588e --- /dev/null +++ b/src/ctap/client_pin.rs @@ -0,0 +1,1760 @@ +// 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::AuthenticatorClientPinParameters; +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 super::token_state::PinUvAuthTokenState; +use alloc::boxed::Box; +use alloc::str; +use alloc::string::String; +use alloc::vec::Vec; +use crypto::hmac::hmac_256; +use crypto::rng256::Rng256; +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. +/// +/// 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; + +/// Decrypts the new_pin_enc and outputs the found PIN. +fn decrypt_pin( + shared_secret: &dyn SharedSecret, + new_pin_enc: Vec, +) -> 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); + } + // 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. + Ok(decrypted_pin.into_iter().take_while(|&c| c != 0).collect()) +} + +/// 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, + shared_secret: &dyn SharedSecret, + new_pin_enc: Vec, +) -> Result<(), Ctap2StatusCode> { + 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; 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(()) +} + +#[cfg_attr(test, derive(IntoEnumIterator))] +pub enum PinPermission { + // All variants should use integers with a single bit set. + MakeCredential = 0x01, + GetAssertion = 0x02, + CredentialManagement = 0x04, + _BioEnrollment = 0x08, + LargeBlobWrite = 0x10, + AuthenticatorConfiguration = 0x20, +} + +pub struct ClientPin { + pin_protocol_v1: PinProtocol, + pin_protocol_v2: PinProtocol, + consecutive_pin_mismatches: u8, + pin_uv_auth_token_state: PinUvAuthTokenState, +} + +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, + pin_uv_auth_token_state: PinUvAuthTokenState::new(), + } + } + + /// 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. + /// Resets or decreases the PIN retries, depending on success or failure. + /// Also, in case of failure, the key agreement key is randomly reset. + fn verify_pin_hash_enc( + &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> { + match persistent_store.pin_hash()? { + Some(pin_hash) => { + if self.consecutive_pin_mismatches >= 3 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); + } + persistent_store.decr_pin_retries()?; + 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.get_mut_pin_protocol(pin_uv_auth_protocol) + .regenerate(rng); + if persistent_store.pin_retries()? == 0 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); + } + self.consecutive_pin_mismatches += 1; + if self.consecutive_pin_mismatches >= 3 { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); + } + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } + } + // This status code is not explicitly mentioned in the specification. + None => return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED), + } + persistent_store.reset_pin_retries()?; + self.consecutive_pin_mismatches = 0; + Ok(()) + } + + fn process_get_pin_retries( + &self, + persistent_store: &PersistentStore, + ) -> Result { + Ok(AuthenticatorClientPinResponse { + key_agreement: None, + pin_uv_auth_token: None, + retries: Some(persistent_store.pin_retries()? as u64), + power_cycle_state: Some(self.consecutive_pin_mismatches >= 3), + }) + } + + 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, + pin_uv_auth_token: None, + retries: None, + power_cycle_state: None, + }) + } + + fn process_set_pin( + &mut self, + persistent_store: &mut PersistentStore, + 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.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()?; + Ok(()) + } + + fn process_change_pin( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + 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.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_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(()) + } + + fn process_get_pin_token( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, + ) -> Result { + let AuthenticatorClientPinParameters { + 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); + } + 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); + } + + 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_uv_auth_token: Some(pin_uv_auth_token), + retries: None, + power_cycle_state: None, + }) + } + + fn process_get_pin_uv_auth_token_using_uv_with_permissions( + &self, + // If you want to support local user verification, implement this function. + // Lacking a fingerprint reader, this subcommand is currently unsupported. + _client_pin_params: AuthenticatorClientPinParameters, + ) -> Result { + // User verification is only supported through PIN currently. + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) + } + + fn process_get_uv_retries(&self) -> Result { + // User verification is only supported through PIN currently. + Err(Ctap2StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND) + } + + fn process_get_pin_uv_auth_token_using_pin_with_permissions( + &mut self, + rng: &mut impl Rng256, + persistent_store: &mut PersistentStore, + mut client_pin_params: AuthenticatorClientPinParameters, + now: ClockValue, + ) -> Result { + // 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 { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + // This check is not mentioned protocol steps, but mentioned in a side note. + if permissions & 0x03 != 0 && permissions_rp_id.is_none() { + return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); + } + + 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) + } + + /// Processes the authenticatorClientPin command. + pub fn process_command( + &mut self, + 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 => { + Some(self.process_get_pin_retries(persistent_store)?) + } + ClientPinSubCommand::GetKeyAgreement => { + Some(self.process_get_key_agreement(client_pin_params)?) + } + ClientPinSubCommand::SetPin => { + self.process_set_pin(persistent_store, client_pin_params)?; + None + } + ClientPinSubCommand::ChangePin => { + self.process_change_pin(rng, persistent_store, client_pin_params)?; + None + } + ClientPinSubCommand::GetPinToken => { + 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)?, + ), + 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, + client_pin_params, + now, + )?, + ), + }; + Ok(ResponseData::AuthenticatorClientPin(response)) + } + + /// 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> { + 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(), + hmac_contents, + pin_uv_auth_param, + pin_uv_auth_protocol, + ) + } + + /// 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.pin_uv_auth_token_state.stop_using_pin_uv_auth_token(); + } + + /// 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, + hmac_secret_input: GetAssertionHmacSecretInput, + cred_random: &[u8; 32], + ) -> Result, Ctap2StatusCode> { + let GetAssertionHmacSecretInput { + key_agreement, + salt_enc, + salt_auth, + pin_uv_auth_protocol, + } = hmac_secret_input; + 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)?; + + 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) + } + + /// 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> { + 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> { + 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. + pub fn has_no_or_rp_id_hash_permission( + &self, + rp_id_hash: &[u8], + ) -> Result<(), Ctap2StatusCode> { + 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> { + 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)] + 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), + }; + 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, + pin_uv_auth_token_state, + } + } +} + +#[cfg(test)] +mod test { + use super::super::pin_protocol::authenticate_pin_uv_auth_token; + 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) { + let mut pin = [0u8; 64]; + 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(&pin_hash, 4).unwrap(); + } + + /// Fails on PINs bigger than 64 bytes. + 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[..]); + shared_secret.encrypt(&mut rng, &padded_pin).unwrap() + } + + /// 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) + } + + /// 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 (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, + 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, + permissions_rp_id, + }; + (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); + 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 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, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Ok(()) + ); + + let pin_hash_enc = vec![0xEE; 16]; + assert_eq!( + client_pin.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + + 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, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) + ); + client_pin.consecutive_pin_mismatches = 0; + + let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; + assert_eq!( + client_pin.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + + let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1]; + assert_eq!( + client_pin.verify_pin_hash_enc( + &mut rng, + &mut persistent_store, + pin_uv_auth_protocol, + shared_secret.as_ref(), + pin_hash_enc + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + 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 mut persistent_store = PersistentStore::new(&mut rng); + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: None, + pin_uv_auth_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_uv_auth_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), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) + ); + } + + #[test] + 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 mut persistent_store = PersistentStore::new(&mut rng); + let expected_response = Some(AuthenticatorClientPinResponse { + key_agreement: params.key_agreement.clone(), + pin_uv_auth_token: None, + retries: None, + power_cycle_state: None, + }); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Ok(ResponseData::AuthenticatorClientPin(expected_response)) + ); + } + + #[test] + 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, DUMMY_CLOCK_VALUE), + 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 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_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, + DUMMY_CLOCK_VALUE + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + + while persistent_store.pin_retries().unwrap() > 0 { + persistent_store.decr_pin_retries().unwrap(); + } + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) + ); + } + + #[test] + 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 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 response = client_pin + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE, + ) + .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 + .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, + DUMMY_CLOCK_VALUE + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + 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(())); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID), + ); + } + + #[test] + 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 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 response = client_pin + .process_command( + &mut rng, + &mut persistent_store, + params.clone(), + DUMMY_CLOCK_VALUE, + ) + .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 + .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, + 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, + 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, + DUMMY_CLOCK_VALUE + ), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + 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(())); + assert_eq!( + client_pin.process_command(&mut rng, &mut persistent_store, params, DUMMY_CLOCK_VALUE), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) + ); + } + + #[test] + 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 pin_protocol = PinProtocol::new(&mut rng); + let shared_secret = pin_protocol + .decapsulate(pin_protocol.get_public_key(), pin_uv_auth_protocol) + .unwrap(); + + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"1234".to_vec()); + assert_eq!( + decrypt_pin(shared_secret.as_ref(), new_pin_enc), + Ok(b"1234".to_vec()), + ); + + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), b"123".to_vec()); + assert_eq!( + 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.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.as_ref(), new_pin_enc), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + + #[test] + 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 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". + (b"1234".to_vec(), Ok(())), + // Reject PIN "123" since it is too short. + ( + b"123".to_vec(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), + // Reject PIN "12'\0'4" (a zero byte at index 2). + ( + b"12\04".to_vec(), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), + // PINs must be at most 63 bytes long, to allow for a trailing 0u8 padding. + ( + vec![0x30; 64], + Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), + ), + ]; + for (pin, result) in test_cases { + let old_pin_hash = persistent_store.pin_hash().unwrap(); + let new_pin_enc = encrypt_pin(shared_secret.as_ref(), pin); + + assert_eq!( + check_and_store_new_pin(&mut persistent_store, shared_secret.as_ref(), new_pin_enc), + result + ); + if result.is_ok() { + assert_ne!(old_pin_hash, persistent_store.pin_hash().unwrap()); + } else { + assert_eq!(old_pin_hash, persistent_store.pin_hash().unwrap()); + } + } + } + + #[test] + 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 (client_pin, shared_secret) = create_client_pin_and_shared_secret(pin_uv_auth_protocol); + + 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()) + } + + 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] ^= 0x01; + 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]; + + 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 mut salt12 = vec![0x00; 64]; + salt12[..32].copy_from_slice(&salt1); + salt12[32..].copy_from_slice(&salt2); + let output = + 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 salt02 = vec![0x00; 64]; + salt02[32..].copy_from_slice(&salt2); + let output = + get_process_hmac_secret_decrypted_output(pin_uv_auth_protocol, &cred_random, salt02) + .unwrap(); + assert_eq!(&output[32..], &expected_output2); + + let mut salt10 = vec![0x00; 64]; + salt10[..32].copy_from_slice(&salt1); + let output = + 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] + fn test_has_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + client_pin.pin_uv_auth_token_state.set_permissions(0x7F); + for permission in PinPermission::into_enum_iter() { + assert_eq!( + 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) + ); + } + } + + #[test] + fn test_has_no_rp_id_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.has_no_rp_id_permission(), Ok(())); + 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) + ); + } + + #[test] + fn test_has_no_or_rp_id_permission() { + 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(())); + 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"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_has_no_or_rp_id_hash_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + let rp_id_hash = Sha256::hash(b"example.com"); + assert_eq!( + client_pin.has_no_or_rp_id_hash_permission(&rp_id_hash), + Ok(()) + ); + 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(()) + ); + assert_eq!( + client_pin.has_no_or_rp_id_hash_permission(&[0x4A; 32]), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_ensure_rp_id_permission() { + let mut rng = ThreadRng256 {}; + let mut client_pin = ClientPin::new(&mut rng); + assert_eq!(client_pin.ensure_rp_id_permission("example.com"), Ok(())); + assert_eq!( + 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("another.example.com"), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } + + #[test] + fn test_verify_pin_uv_auth_token() { + let mut rng = ThreadRng256 {}; + 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) + .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_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 {}; + 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.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()); + 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(())); + } + + #[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 ecfae9e..387a687 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. @@ -12,12 +12,14 @@ // 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, CoseKey, GetAssertionExtensions, - GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, - PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + extract_unsigned, ok_or_missing, ClientPinSubCommand, ConfigSubCommand, ConfigSubCommandParams, + CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, + GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, MakeCredentialOptions, + PinUvAuthProtocol, PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, + PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, SetMinPinLengthParams, }; use super::key_material; use super::status_code::Ctap2StatusCode; @@ -27,13 +29,11 @@ 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; // 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), @@ -41,9 +41,10 @@ pub enum Command { AuthenticatorClientPin(AuthenticatorClientPinParameters), AuthenticatorReset, AuthenticatorGetNextAssertion, - #[cfg(feature = "with_ctap2_1")] + AuthenticatorCredentialManagement(AuthenticatorCredentialManagementParameters), AuthenticatorSelection, - // TODO(kaczmarczyck) implement FIDO 2.1 commands (see below consts) + AuthenticatorLargeBlobs(AuthenticatorLargeBlobsParameters), + AuthenticatorConfig(AuthenticatorConfigParameters), // Vendor specific commands AuthenticatorVendorConfigure(AuthenticatorVendorConfigureParameters), } @@ -54,8 +55,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; @@ -63,8 +62,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; @@ -111,11 +110,28 @@ impl Command { // Parameters are ignored. Ok(Command::AuthenticatorGetNextAssertion) } - #[cfg(feature = "with_ctap2_1")] + 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) } + 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( + AuthenticatorConfigParameters::try_from(decoded_cbor)?, + )) + } Command::AUTHENTICATOR_VENDOR_CONFIGURE => { let decoded_cbor = cbor::read(&bytes[1..])?; Ok(Command::AuthenticatorVendorConfigure( @@ -127,18 +143,20 @@ impl Command { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorMakeCredentialParameters { pub client_data_hash: Vec, pub rp: PublicKeyCredentialRpEntity, 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, + pub pin_uv_auth_protocol: Option, + pub enterprise_attestation: Option, } impl TryFrom for AuthenticatorMakeCredentialParameters { @@ -147,15 +165,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)?; } @@ -185,18 +204,19 @@ 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()?; + 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 { client_data_hash, @@ -208,20 +228,22 @@ impl TryFrom for AuthenticatorMakeCredentialParameters { options, pin_uv_auth_param, pin_uv_auth_protocol, + enterprise_attestation, }) } } -#[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, 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, + pub pin_uv_auth_protocol: Option, } impl TryFrom for AuthenticatorGetAssertionParameters { @@ -230,13 +252,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)?; } @@ -259,18 +281,18 @@ 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()?; + let pin_uv_auth_protocol = pin_uv_auth_protocol + .map(PinUvAuthProtocol::try_from) + .transpose()?; Ok(AuthenticatorGetAssertionParameters { rp_id, @@ -284,21 +306,15 @@ impl TryFrom for AuthenticatorGetAssertionParameters { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct AuthenticatorClientPinParameters { - pub pin_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>, - #[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,86 +322,167 @@ 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, - 2 => sub_command, - 3 => key_agreement, - 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, + 0x01 => pin_uv_auth_protocol, + 0x02 => sub_command, + 0x03 => key_agreement, + 0x04 => pin_uv_auth_param, + 0x05 => new_pin_enc, + 0x06 => pin_hash_enc, + 0x09 => permissions, + 0x0A => permissions_rp_id, } = extract_map(cbor_value)?; } - let pin_protocol = extract_unsigned(ok_or_missing(pin_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(extract_map).transpose()?.map(CoseKey); - let pin_auth = pin_auth.map(extract_byte_string).transpose()?; + let key_agreement = key_agreement.map(CoseKey::try_from).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()?; - #[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)? - .into_iter() - .map(extract_text_string) - .collect::, Ctap2StatusCode>>()?, - ), - 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 { - pin_protocol, + pin_uv_auth_protocol, sub_command, key_agreement, - pin_auth, + pin_uv_auth_param, 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, }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[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 { + 0x01 => get, + 0x02 => set, + 0x03 => offset, + 0x04 => length, + 0x05 => pin_uv_auth_param, + 0x06 => 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(PinUvAuthProtocol::try_from) + .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, + }) + } +} + +#[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(PinUvAuthProtocol::try_from) + .transpose()?; + + Ok(AuthenticatorConfigParameters { + sub_command, + sub_command_params, + pin_uv_auth_param, + pin_uv_auth_protocol, + }) + } +} + +#[derive(Debug, PartialEq)] pub struct AuthenticatorAttestationMaterial { pub certificate: Vec, pub private_key: [u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], @@ -397,8 +494,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)?)?; @@ -414,7 +511,46 @@ 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, + pub pin_uv_auth_protocol: Option, + pub pin_uv_auth_param: 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_uv_auth_protocol, + 0x04 => pin_uv_auth_param, + } = 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_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_uv_auth_param, + }) + } +} + +#[derive(Debug, PartialEq)] pub struct AuthenticatorVendorConfigureParameters { pub lockdown: bool, pub attestation_material: Option, @@ -426,8 +562,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)?; @@ -449,28 +585,29 @@ 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() { 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", + "name" => "Example", }, - 3 => cbor_map! { + 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", }, - 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 => 2, }; let returned_make_credential_parameters = AuthenticatorMakeCredentialParameters::try_from(cbor_value).unwrap(); @@ -500,10 +637,11 @@ 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), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + enterprise_attestation: Some(2), }; assert_eq!( @@ -515,15 +653,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! { - "type" => "public-key", + 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! { "id" => vec![0x2D, 0x2D, 0x2D, 0x2D], + "type" => "public-key", "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(); @@ -546,10 +684,10 @@ 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), + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), }; assert_eq!( @@ -560,53 +698,38 @@ 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 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!{}, - 4 => vec! [0xBB], - 5 => vec! [0xCC], - 6 => vec! [0xDD], + 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", }; - #[cfg(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], - 7 => 4, - 8 => cbor_array!["example.com"], - 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 { - pin_protocol: 1, + let expected_client_pin_parameters = AuthenticatorClientPinParameters { + pin_uv_auth_protocol: PinUvAuthProtocol::V1, sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: Some(CoseKey(BTreeMap::new())), - pin_auth: Some(vec![0xBB]), + key_agreement: Some(cose_key), + pin_uv_auth_param: 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()), }; assert_eq!( - returned_pin_protocol_parameters, - expected_pin_protocol_parameters + returned_client_pin_parameters, + expected_client_pin_parameters ); } @@ -632,7 +755,37 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorGetNextAssertion)); } - #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_from_cbor_cred_management_parameters() { + let cbor_value = cbor_map! { + 0x01 => CredentialManagementSubCommand::EnumerateCredentialsBegin as u64, + 0x02 => cbor_map!{ + 0x01 => vec![0x1D; 32], + }, + 0x03 => 1, + 0x04 => 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_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param: 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]; @@ -640,6 +793,149 @@ mod test { assert_eq!(command, Ok(Command::AuthenticatorSelection)); } + #[test] + fn test_from_cbor_large_blobs_parameters() { + // successful get + let cbor_value = cbor_map! { + 0x01 => 2, + 0x03 => 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! { + 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(); + 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(PinUvAuthProtocol::V1), + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // successful next set + let cbor_value = cbor_map! { + 0x02 => vec! [0x5E], + 0x03 => 1, + 0x05 => vec! [0xA9], + 0x06 => 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(PinUvAuthProtocol::V1), + }; + assert_eq!( + returned_large_blobs_parameters, + expected_large_blobs_parameters + ); + + // failing with neither get nor set + let cbor_value = cbor_map! { + 0x03 => 4, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with get and set + let cbor_value = cbor_map! { + 0x01 => 2, + 0x02 => vec! [0x5E], + 0x03 => 4, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with get and length + let cbor_value = cbor_map! { + 0x01 => 2, + 0x03 => 4, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 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! { + 0x02 => vec! [0x5E], + 0x03 => 0, + 0x05 => vec! [0xA9], + 0x06 => 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! { + 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), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // failing with non-zero offset and length present + let cbor_value = cbor_map! { + 0x02 => vec! [0x5E], + 0x03 => 4, + 0x04 => MIN_LARGE_BLOB_LEN as u64, + 0x05 => vec! [0xA9], + 0x06 => 1, + }; + assert_eq!( + AuthenticatorLargeBlobsParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + } + #[test] fn test_vendor_configure() { // Incomplete command @@ -664,10 +960,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!( @@ -677,9 +973,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!( @@ -689,9 +985,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!( @@ -701,10 +997,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/config_command.rs b/src/ctap/config_command.rs new file mode 100644 index 0000000..bdedc6b --- /dev/null +++ b/src/ctap/config_command.rs @@ -0,0 +1,472 @@ +// 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::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 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 toggleAlwaysUv for AuthenticatorConfig. +fn process_toggle_always_uv( + persistent_store: &mut PersistentStore, +) -> Result { + persistent_store.toggle_always_uv()?; + Ok(ResponseData::AuthenticatorConfig) +} + +/// Processes the subcommand setMinPINLength for AuthenticatorConfig. +fn process_set_min_pin_length( + persistent_store: &mut PersistentStore, + 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; + } + if force_change_pin { + 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)?; + } + Ok(ResponseData::AuthenticatorConfig) +} + +/// Processes the AuthenticatorConfig command. +pub fn process_config( + persistent_store: &mut PersistentStore, + client_pin: &mut ClientPin, + params: AuthenticatorConfigParameters, +) -> Result { + let AuthenticatorConfigParameters { + sub_command, + sub_command_params, + pin_uv_auth_param, + pin_uv_auth_protocol, + } = params; + + let enforce_uv = match sub_command { + ConfigSubCommand::ToggleAlwaysUv => false, + _ => true, + } && persistent_store.has_always_uv()?; + if persistent_store.pin_hash()?.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 = + 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]); + 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); + } + } + 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 { + ConfigSubCommand::EnableEnterpriseAttestation => { + process_enable_enterprise_attestation(persistent_store) + } + 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) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER) + } + } + _ => Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER), + } +} + +#[cfg(test)] +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] + 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 client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + + 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 client_pin, 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) + ); + } + } + + #[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 client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + + 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 client_pin, 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 client_pin, config_params); + if ENFORCE_ALWAYS_UV { + assert_eq!( + config_response, + Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED) + ); + } else { + assert_eq!(config_response, Ok(ResponseData::AuthenticatorConfig)); + assert!(!persistent_store.has_always_uv().unwrap()); + } + } + + 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, pin_uv_auth_protocol); + persistent_store.set_pin(&[0x88; 16], 4).unwrap(); + + 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: 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 { + 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()); + + let config_params = AuthenticatorConfigParameters { + sub_command: ConfigSubCommand::ToggleAlwaysUv, + sub_command_params: None, + 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>, + ) -> 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(PinUvAuthProtocol::V1), + } + } + + #[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 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; + let config_params = create_min_pin_config_params(min_pin_length, None); + 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)); + + // 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_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_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_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_uv_auth_param); + let config_response = process_config(&mut persistent_store, &mut client_pin, 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 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; + 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 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.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_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_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)); + 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_uv_auth_param.clone()); + let config_response = process_config(&mut persistent_store, &mut client_pin, 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_uv_auth_param); + let config_response = process_config(&mut persistent_store, &mut client_pin, 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_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 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. + 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 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)); + } + + #[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 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![ + 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(PinUvAuthProtocol::V1), + }; + 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)); + } + + #[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 client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + + 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 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 new file mode 100644 index 0000000..b81648a --- /dev/null +++ b/src/ctap/credential_management.rs @@ -0,0 +1,928 @@ +// 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::client_pin::{ClientPin, PinPermission}; +use super::command::AuthenticatorCredentialManagementParameters; +use super::data_formats::{ + CoseKey, CredentialManagementSubCommand, CredentialManagementSubCommandParameters, + PublicKeyCredentialDescriptor, PublicKeyCredentialRpEntity, PublicKeyCredentialSource, + PublicKeyCredentialUserEntity, +}; +use super::response::{AuthenticatorCredentialManagementResponse, ResponseData}; +use super::status_code::Ctap2StatusCode; +use super::storage::PersistentStore; +use super::{StatefulCommand, StatefulPermission}; +use alloc::collections::BTreeSet; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +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: String, + total_rps: Option, +) -> Result { + 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, + }); + Ok(AuthenticatorCredentialManagementResponse { + rp, + rp_id_hash, + total_rps, + ..Default::default() + }) +} + +/// 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, + cred_blob: _, + large_blob_key, + } = 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 { + user: Some(user), + credential_id: Some(credential_id), + public_key: Some(public_key), + total_credentials, + cred_protect: cred_protect_policy, + large_blob_key, + ..Default::default() + }) +} + +/// 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, + client_pin: &mut ClientPin, + credential_id: &[u8], +) -> Result<(), Ctap2StatusCode> { + // Pre-check a sufficient condition before calling the store. + if client_pin.has_no_rp_id_permission().is_ok() { + return Ok(()); + } + let (_, credential) = persistent_store.find_credential_item(credential_id)?; + client_pin.has_no_or_rp_id_permission(&credential.rp_id) +} + +/// 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, + ), + ..Default::default() + }) +} + +/// Processes the subcommand enumerateRPsBegin for CredentialManagement. +fn process_enumerate_rps_begin( + persistent_store: &PersistentStore, + stateful_command_permission: &mut StatefulPermission, + now: ClockValue, +) -> Result { + let rp_set = get_stored_rp_ids(persistent_store)?; + let total_rps = rp_set.len(); + + 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() + 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. +fn process_enumerate_rps_get_next_rp( + persistent_store: &PersistentStore, + stateful_command_permission: &mut StatefulPermission, +) -> Result { + 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(rp_id, None) +} + +/// Processes the subcommand enumerateCredentialsBegin for CredentialManagement. +fn process_enumerate_credentials_begin( + persistent_store: &PersistentStore, + stateful_command_permission: &mut StatefulPermission, + 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)?; + 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 + .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 + .set_command(now, 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 StatefulPermission, +) -> Result { + 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. +fn process_delete_credential( + persistent_store: &mut PersistentStore, + 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, 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, + 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; + let user = sub_command_params + .user + .ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; + check_rp_id_permissions(persistent_store, client_pin, &credential_id)?; + persistent_store.update_credential(&credential_id, user) +} + +/// Processes the CredentialManagement command and all its subcommands. +pub fn process_credential_management( + persistent_store: &mut PersistentStore, + stateful_command_permission: &mut StatefulPermission, + client_pin: &mut ClientPin, + cred_management_params: AuthenticatorCredentialManagementParameters, + now: ClockValue, +) -> Result { + let AuthenticatorCredentialManagementParameters { + sub_command, + sub_command_params, + pin_uv_auth_protocol, + pin_uv_auth_param, + } = cred_management_params; + + match (sub_command, stateful_command_permission.get_command()) { + ( + CredentialManagementSubCommand::EnumerateRpsGetNextRp, + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( + CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential, + Ok(StatefulCommand::EnumerateCredentials(_)), + ) => stateful_command_permission.check_command_permission(now)?, + (_, _) => { + stateful_command_permission.clear(); + } + } + + match sub_command { + CredentialManagementSubCommand::GetCredsMetadata + | CredentialManagementSubCommand::EnumerateRpsBegin + | CredentialManagementSubCommand::EnumerateCredentialsBegin + | CredentialManagementSubCommand::DeleteCredential + | CredentialManagementSubCommand::UpdateUserInformation => { + 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_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)?; + } + CredentialManagementSubCommand::EnumerateRpsGetNextRp + | CredentialManagementSubCommand::EnumerateCredentialsGetNextCredential => {} + } + + let response = match sub_command { + CredentialManagementSubCommand::GetCredsMetadata => { + client_pin.has_no_rp_id_permission()?; + Some(process_get_creds_metadata(persistent_store)?) + } + CredentialManagementSubCommand::EnumerateRpsBegin => { + client_pin.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)?, + ), + CredentialManagementSubCommand::EnumerateCredentialsBegin => { + Some(process_enumerate_credentials_begin( + persistent_store, + stateful_command_permission, + client_pin, + 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, + )?) + } + CredentialManagementSubCommand::DeleteCredential => { + process_delete_credential( + persistent_store, + client_pin, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + CredentialManagementSubCommand::UpdateUserInformation => { + process_update_user_information( + persistent_store, + client_pin, + sub_command_params.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, + )?; + None + } + }; + Ok(ResponseData::AuthenticatorCredentialManagement(response)) +} + +#[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}; + + 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()), + cred_blob: None, + large_blob_key: None, + } + } + + 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, pin_uv_auth_protocol); + 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.client_pin = client_pin; + + ctap_state.persistent_store.set_pin(&[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, + &management_data, + pin_uv_auth_protocol, + ); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + 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, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.client_pin, + 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_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, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.client_pin, + 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_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 {}; + 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 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.client_pin = client_pin; + + ctap_state + .persistent_store + .store_credential(credential_source1) + .unwrap(); + ctap_state + .persistent_store + .store_credential(credential_source2) + .unwrap(); + + ctap_state.persistent_store.set_pin(&[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, + ]); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::EnumerateRpsBegin, + sub_command_params: None, + pin_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, + }; + 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, + ); + 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_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + 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, + ); + 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_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + 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_NOT_ALLOWED) + ); + } + + #[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 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(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.client_pin = client_pin; + + 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_uv_auth_param = 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_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, + }; + + 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.client_pin, + 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_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + } + + 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_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 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]; + 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.client_pin = client_pin; + + ctap_state + .persistent_store + .store_credential(credential_source1) + .unwrap(); + ctap_state + .persistent_store + .store_credential(credential_source2) + .unwrap(); + + ctap_state.persistent_store.set_pin(&[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, + ]); + + 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_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, + }; + 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, + ); + 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_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + 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, + ); + 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_uv_auth_protocol: None, + pin_uv_auth_param: None, + }; + 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_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 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]; + + 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 + .store_credential(credential_source) + .unwrap(); + + ctap_state.persistent_store.set_pin(&[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, + ]); + + 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_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, + &mut ctap_state.stateful_command_permission, + &mut ctap_state.client_pin, + 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_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, + }; + 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_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 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]; + + 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 + .store_credential(credential_source) + .unwrap(); + + ctap_state.persistent_store.set_pin(&[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, + ]); + + 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_uv_auth_protocol: Some(PinUvAuthProtocol::V1), + pin_uv_auth_param, + }; + 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, + 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_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); + + ctap_state.persistent_store.set_pin(&[0u8; 16], 4).unwrap(); + + let cred_management_params = AuthenticatorCredentialManagementParameters { + sub_command: CredentialManagementSubCommand::GetCredsMetadata, + sub_command_params: None, + 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, + &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) + ); + } +} 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/ctap1.rs b/src/ctap/ctap1.rs index 0932e2c..d5e60ee 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. @@ -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)] @@ -190,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 { @@ -399,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/customization.rs b/src/ctap/customization.rs new file mode 100644 index 0000000..7c28a2f --- /dev/null +++ b/src/ctap/customization.rs @@ -0,0 +1,280 @@ +// 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] = &[]; + +/// 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 +/// +/// - 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_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 { + 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 a2b490d..673b850 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. @@ -13,18 +13,20 @@ // 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; -use cbor::{cbor_array_vec, cbor_bytes_lit, cbor_map_options, destructure_cbor_map}; +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; +// Used as the identifier for ECDSA in assertion signatures and COSE. +const ES256_ALGORITHM: i64 = -7; + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialrpentity -#[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, @@ -55,8 +57,18 @@ impl TryFrom for PublicKeyCredentialRpEntity { } } +impl From for cbor::Value { + fn from(entity: PublicKeyCredentialRpEntity) -> Self { + cbor_map_options! { + "id" => entity.rp_id, + "icon" => entity.rp_icon, + "name" => entity.rp_name, + } + } +} + // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -95,16 +107,15 @@ 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, } } } // 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. @@ -136,8 +147,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(Clone, Debug, PartialEq)] pub struct PublicKeyCredentialParameter { pub cred_type: PublicKeyCredentialType, pub alg: SignatureAlgorithm, @@ -163,14 +173,14 @@ 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, } } } // https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, PartialEq)] #[cfg_attr(test, derive(IntoEnumIterator))] pub enum AuthenticatorTransport { Usb, @@ -207,7 +217,7 @@ 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, Debug, PartialEq)] pub struct PublicKeyCredentialDescriptor { pub key_type: PublicKeyCredentialType, pub key_id: Vec, @@ -251,17 +261,20 @@ 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)), } } } -#[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, + pub min_pin_length: bool, + pub cred_blob: Option>, + pub large_blob_key: Option, } impl TryFrom for MakeCredentialExtensions { @@ -270,8 +283,11 @@ 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, + "largeBlobKey" => large_blob_key, + "minPinLength" => min_pin_length, } = extract_map(cbor_value)?; } @@ -279,16 +295,29 @@ 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)?; + 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, }) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct GetAssertionExtensions { pub hmac_secret: Option, + pub cred_blob: bool, + pub large_blob_key: Option, } impl TryFrom for GetAssertionExtensions { @@ -297,23 +326,36 @@ impl TryFrom for GetAssertionExtensions { fn try_from(cbor_value: cbor::Value) -> Result { destructure_cbor_map! { let { + "credBlob" => cred_blob, "hmac-secret" => hmac_secret, + "largeBlobKey" => large_blob_key, } = extract_map(cbor_value)?; } 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)?; + 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, + }) } } -#[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, pub salt_auth: Vec, + pub pin_uv_auth_protocol: PinUvAuthProtocol, } impl TryFrom for GetAssertionHmacSecretInput { @@ -322,25 +364,29 @@ 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, + 4 => pin_uv_auth_protocol, } = 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)?)?; + let pin_uv_auth_protocol = + pin_uv_auth_protocol.map_or(Ok(PinUvAuthProtocol::V1), PinUvAuthProtocol::try_from)?; Ok(Self { - key_agreement: CoseKey(cose_key), + key_agreement, salt_enc, salt_auth, + pin_uv_auth_protocol, }) } } // Even though options are optional, we can use the default if not present. -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[derive(Clone, Debug, Default, PartialEq)] pub struct MakeCredentialOptions { pub rk: bool, pub uv: bool, @@ -362,8 +408,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)?, @@ -373,12 +422,21 @@ 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, } +impl Default for GetAssertionOptions { + fn default() -> Self { + GetAssertionOptions { + up: true, + uv: false, + } + } +} + impl TryFrom for GetAssertionOptions { type Error = Ctap2StatusCode; @@ -409,8 +467,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, @@ -429,10 +486,9 @@ impl From for cbor::Value { } } -#[derive(PartialEq)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Clone, Debug, PartialEq)] 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,18 +505,24 @@ 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), } } } -#[derive(Clone, Copy, PartialEq, PartialOrd)] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +/// 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, } @@ -487,9 +549,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, @@ -502,6 +562,8 @@ pub struct PublicKeyCredentialSource { pub creation_order: u64, 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 @@ -516,6 +578,8 @@ enum PublicKeyCredentialSourceField { CreationOrder = 7, 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: @@ -542,6 +606,8 @@ 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, + PublicKeyCredentialSourceField::LargeBlobKey => credential.large_blob_key, } } } @@ -561,6 +627,8 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::CreationOrder => creation_order, PublicKeyCredentialSourceField::UserName => user_name, PublicKeyCredentialSourceField::UserIcon => user_icon, + PublicKeyCredentialSourceField::CredBlob => cred_blob, + PublicKeyCredentialSourceField::LargeBlobKey => large_blob_key, } = extract_map(cbor_value)?; } @@ -577,9 +645,11 @@ 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()?; + 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: @@ -601,6 +671,8 @@ impl TryFrom for PublicKeyCredentialSource { creation_order, user_name, user_icon, + cred_blob, + large_blob_key, }) } } @@ -614,72 +686,41 @@ 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. -#[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; - -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!(); - } - } +// The COSE key is used for both ECDH and ECDSA public keys for transmission. +#[derive(Clone, Debug, PartialEq)] +pub struct CoseKey { + x_bytes: [u8; ecdh::NBYTES], + y_bytes: [u8; ecdh::NBYTES], + algorithm: i64, } -impl TryFrom for ecdh::PubKey { +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; - fn try_from(cose_key: CoseKey) -> Result { + fn try_from(cbor_value: cbor::Value) -> 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); @@ -688,15 +729,112 @@ 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 != CoseKey::P_256_CURVE { + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); + } + let key_type = extract_integer(ok_or_missing(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 != CoseKey::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(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 => CoseKey::EC2_KEY_TYPE, + 3 => algorithm, + -1 => CoseKey::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: CoseKey::ECDH_ALGORITHM, + } + } +} + +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); + 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. + // https://github.com/google/OpenSK/issues/90 + if algorithm != CoseKey::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) } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PinUvAuthProtocol { + V1 = 1, + V2 = 2, +} + +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 { GetPinRetries = 0x01, @@ -704,13 +842,8 @@ 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 +864,212 @@ 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), + } + } +} + +#[derive(Clone, Copy, 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, 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, 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, + } + } +} + +/// 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, +} + +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 { + 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, 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, } } } @@ -791,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), @@ -814,10 +1141,9 @@ 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")), + ]) ); } @@ -1091,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 { @@ -1107,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 { @@ -1140,7 +1463,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)); @@ -1213,8 +1536,8 @@ mod test { #[test] fn test_from_into_public_key_credential_parameter() { let cbor_credential_parameter = cbor_map! { + "alg" => ES256_ALGORITHM, "type" => "public-key", - "alg" => ecdsa::PubKey::ES256_ALGORITHM, }; let credential_parameter = PublicKeyCredentialParameter::try_from(cbor_credential_parameter.clone()); @@ -1230,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 = @@ -1249,42 +1572,85 @@ mod test { #[test] fn test_from_make_credential_extensions() { let cbor_extensions = cbor_map! { - "hmac-secret" => 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 { hmac_secret: true, 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)); } #[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(); let cose_key = CoseKey::from(pk); let cbor_extensions = cbor_map! { + "credBlob" => true, "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], }, + "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::V1, }; let expected_extensions = GetAssertionExtensions { hmac_secret: Some(expected_input), + cred_blob: true, + large_blob_key: Some(true), }; 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! { + "credBlob" => true, + "hmac-secret" => cbor_map! { + 1 => cbor::Value::from(cose_key.clone()), + 2 => vec![0x02; 32], + 3 => vec![0x03; 16], + 4 => 2, + }, + "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! { @@ -1319,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 { @@ -1333,7 +1699,103 @@ mod test { } #[test] - fn test_from_into_cose_key() { + fn test_from_into_cose_key_cbor() { + for algorithm in &[CoseKey::ECDH_ALGORITHM, ES256_ALGORITHM] { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + 3 => algorithm, + -1 => CoseKey::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); + } + } + + #[test] + fn test_cose_key_unknown_algorithm() { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + // unknown algorithm + 3 => 0, + -1 => CoseKey::P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + 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 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, + -2 => [0u8; 32], + -3 => [0u8; 32], + }; + assert_eq!( + CoseKey::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM) + ); + } + + #[test] + fn test_cose_key_unknown_curve() { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::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) + ); + } + + #[test] + fn test_cose_key_wrong_length_x() { + let cbor_value = cbor_map! { + 1 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::P_256_CURVE, + // wrong length + -2 => [0u8; 31], + -3 => [0u8; 32], + }; + assert_eq!( + 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 => CoseKey::EC2_KEY_TYPE, + 3 => CoseKey::ECDH_ALGORITHM, + -1 => CoseKey::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 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let pk = sk.genpk(); @@ -1342,6 +1804,34 @@ 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); + 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); @@ -1358,6 +1848,122 @@ 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!("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!("example.com".to_string()), + 0x03 => true, + }; + assert_eq!(cbor::Value::from(config_sub_command_params), cbor_params); + } + + #[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), + ); + 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); + 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 {}; @@ -1372,6 +1978,8 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert_eq!( @@ -1414,6 +2022,26 @@ 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.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/hid/mod.rs b/src/ctap/hid/mod.rs index ef96eef..c6fc418 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. @@ -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); @@ -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() @@ -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 b522837..caf9ffc 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. @@ -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/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/large_blobs.rs b/src/ctap/large_blobs.rs new file mode 100644 index 0000000..846bc33 --- /dev/null +++ b/src/ctap/large_blobs.rs @@ -0,0 +1,411 @@ +// 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::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; +use alloc::vec; +use alloc::vec::Vec; +use byteorder::{ByteOrder, LittleEndian}; +use crypto::sha256::Sha256; +use crypto::Hash256; + +/// 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, + client_pin: &mut ClientPin, + 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(offset, get)?; + 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() || persistent_store.has_always_uv()? { + 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 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); + 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); + } + 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::super::data_formats::PinUvAuthProtocol; + use super::super::pin_protocol::authenticate_pin_uv_auth_token; + 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 client_pin = + ClientPin::new_test(key_agreement_key, pin_uv_auth_token, PinUvAuthProtocol::V1); + 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 client_pin, 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 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; + 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 client_pin, 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 client_pin, 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 client_pin, 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 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; + 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 client_pin, 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 client_pin, 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 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; + 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 client_pin, 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 client_pin, 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 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; + // 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 client_pin, large_blobs_params); + assert_eq!( + large_blobs_response, + Err(Ctap2StatusCode::CTAP2_ERR_INTEGRITY_FAILURE), + ); + } + + 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, pin_uv_auth_protocol); + 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 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: 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); + assert_eq!( + large_blobs_response, + 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 dfa2d9b..eaca021 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. @@ -13,35 +13,46 @@ // limitations under the License. pub mod apdu; +mod client_pin; pub mod command; +mod config_command; +mod credential_management; +mod crypto_wrapper; #[cfg(feature = "with_ctap1")] mod ctap1; +mod customization; pub mod data_formats; pub mod hid; mod key_material; -mod pin_protocol_v1; +mod large_blobs; +mod pin_protocol; pub mod response; pub mod status_code; mod storage; mod timed_permission; +mod token_state; -#[cfg(feature = "with_ctap2_1")] -use self::command::MAX_CREDENTIAL_COUNT_IN_LIST; +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; +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_MSG_SIZE, + MAX_RP_IDS_LENGTH, USE_BATCH_ATTESTATION, USE_SIGNATURE_COUNTER, }; -#[cfg(feature = "with_ctap2_1")] -use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, EnterpriseAttestationMode, + GetAssertionExtensions, PackedAttestationStatement, PinUvAuthProtocol, 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::large_blobs::LargeBlobs; use self::response::{ AuthenticatorGetAssertionResponse, AuthenticatorGetInfoResponse, AuthenticatorMakeCredentialResponse, AuthenticatorVendorResponse, ResponseData, @@ -51,16 +62,16 @@ use self::storage::PersistentStore; use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; -use alloc::collections::BTreeMap; +use alloc::boxed::Box; 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; +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; @@ -70,18 +81,6 @@ 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 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. -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; // Our credential ID consists of // - 16 byte initialization vector for AES-256, @@ -101,6 +100,7 @@ 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); const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); @@ -108,7 +108,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. @@ -117,10 +116,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; // 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. @@ -136,23 +131,131 @@ 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, + extensions: GetAssertionExtensions, has_uv: bool, } -struct AssertionState { +/// 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_credentials: Vec, + next_credential_keys: Vec, } -enum StatefulCommand { +/// Stores which command currently holds state for subsequent calls. +pub enum StatefulCommand { Reset, - GetAssertion(AssertionState), + GetAssertion(Box), + EnumerateRps(usize), + 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 note that the allowed time for Reset +/// differs from all other stateful commands. +pub struct StatefulPermission { + permission: TimedPermission, + command_type: Option, +} + +impl StatefulPermission { + /// 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), + command_type: Some(StatefulCommand::Reset), + } + } + + /// 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(()) + } else { + self.clear(); + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } + } + + /// Gets a reference to the current command state, if any exists. + pub fn get_command(&self) -> Result<&StatefulCommand, Ctap2StatusCode> { + self.command_type + .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. + StatefulCommand::Reset => unreachable!(), + _ => { + self.permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + self.command_type = Some(new_command_type); + } + } + } + + /// 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 @@ -164,12 +267,12 @@ 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. - stateful_command_permission: TimedPermission, - stateful_command_type: Option, + stateful_command_permission: StatefulPermission, + large_blobs: LargeBlobs, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -177,41 +280,34 @@ 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, 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, 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), + large_blobs: LargeBlobs::new(), } } - 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.update_command_permission(now); - if self.stateful_command_permission.is_granted(now) { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + 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> { @@ -223,6 +319,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 @@ -234,23 +338,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) @@ -277,26 +369,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, @@ -308,6 +386,8 @@ where creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, })) } @@ -330,20 +410,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(_), + Ok(StatefulCommand::EnumerateRps(_)), + ) + | ( + Command::AuthenticatorCredentialManagement(_), + Ok(StatefulCommand::EnumerateCredentials(_)), ) => (), - (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; - } + (_, _) => self.stateful_command_permission.clear(), } let response = match command { Command::AuthenticatorMakeCredential(params) => { @@ -354,11 +437,31 @@ 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, + now, + ), Command::AuthenticatorReset => self.process_reset(cid, now), - #[cfg(feature = "with_ctap2_1")] + Command::AuthenticatorCredentialManagement(params) => { + process_credential_management( + &mut self.persistent_store, + &mut self.stateful_command_permission, + &mut self.client_pin, + params, + now, + ) + } Command::AuthenticatorSelection => self.process_selection(cid), - // TODO(kaczmarczyck) implement FIDO 2.1 commands + Command::AuthenticatorLargeBlobs(params) => self.large_blobs.process_command( + &mut self.persistent_store, + &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) @@ -371,10 +474,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 @@ -389,7 +490,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 { @@ -402,15 +503,9 @@ where 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(()) + pin_uv_auth_protocol.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?; } + Ok(()) } fn process_make_credential( @@ -428,6 +523,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)?; @@ -436,28 +532,73 @@ 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; + 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.as_str()), + _ => true, } - (extensions.hmac_secret, cred_protect) } else { - (false, DEFAULT_CRED_PROTECT) + false }; - let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); + // 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 = rp.rp_id; 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())? + .find_credential(&rp_id, &cred_desc.key_id, !has_uv)? .is_some() || self .decrypt_credential_source(cred_desc.key_id, &rp_id_hash)? @@ -465,47 +606,47 @@ where { // Perform this check, so bad actors can't brute force exclude_list // without user interaction. - (self.check_user_presence)(cid)?; + let _ = (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_auth) => { - 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); - } - if !self - .pin_protocol_v1 - .verify_pin_auth_token(&client_data_hash, &pin_auth) - { - 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)?; - } - UP_FLAG | UV_FLAG | AT_FLAG | ed_flag - } - None => { - if self.persistent_store.pin_hash()?.is_some() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_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 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 + || 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 sk = crypto::ecdsa::SecKey::gensk(self.rng); let pk = sk.genpk(); @@ -531,6 +672,8 @@ where user_icon: user .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 @@ -542,30 +685,40 @@ 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), - }; - auth_data.extend(cose_key); + if !cbor::write(cbor::Value::from(CoseKey::from(pk)), &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 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 { + None + }; + let cred_protect_output = extensions.cred_protect.and(cred_protect_policy); let extensions_output = cbor_map_options! { + "credBlob" => cred_blob_output, + "credProtect" => cred_protect_output, "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_RESPONSE_CANNOT_WRITE_CBOR); + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } } 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()? @@ -592,11 +745,14 @@ 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, }, )) } @@ -618,30 +774,47 @@ 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 { 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.client_pin.process_hmac_secret( + self.rng, + 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! { + "credBlob" => cred_blob, "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); } } + 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); @@ -654,6 +827,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, @@ -671,6 +850,7 @@ where signature: signature.to_asn1_der(), user, number_of_credentials: number_of_credentials.map(|n| n as u64), + large_blob_key, }, )) } @@ -719,100 +899,108 @@ 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_EXTENSION); + return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION); } // The user verification bit depends on the existance of PIN auth, since we do // 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) => { + // 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); } - if !self - .pin_protocol_v1 - .verify_pin_auth_token(&client_data_hash, &pin_auth) - { - 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.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)?; + // 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 } None => { if options.uv { - // The specification (inconsistently) wants CTAP2_ERR_UNSUPPORTED_OPTION. return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } + if options.up && self.persistent_store.has_always_uv()? { + return Err(Ctap2StatusCode::CTAP2_ERR_PUAT_REQUIRED); + } 0x00 } }; if options.up { flags |= UP_FLAG; } - if hmac_secret_input.is_some() { + if extensions.hmac_secret.is_some() || extensions.cred_blob { flags |= ED_FLAG; } 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 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)) + .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); + + 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 = applicable_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; - self.increment_global_signature_counter()?; 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 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); - self.stateful_command_permission = - TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); - self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { + let number_of_credentials = Some(next_credential_keys.len() + 1); + let assertion_state = StatefulCommand::GetAssertion(Box::new(AssertionState { assertion_input: assertion_input.clone(), - next_credentials: applicable_credentials, + next_credential_keys, })); + self.stateful_command_permission + .set_command(now, assertion_state); number_of_credentials }; self.assertion_response(credential, assertion_input, number_of_credentials) @@ -822,92 +1010,102 @@ where &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.stateful_command_permission + .check_command_permission(now)?; + 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) } 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(), - ); + let has_always_uv = self.persistent_store.has_always_uv()?; + #[cfg_attr(not(feature = "with_ctap1"), allow(unused_mut))] + 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)) + } + } + let mut options = vec![]; + if ENTERPRISE_ATTESTATION_MODE.is_some() { + options.push(( + String::from("ep"), + self.persistent_store.enterprise_attestation()?, + )); + } + 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: vec![ - #[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")]), - aaguid: self.persistent_store.aaguid()?, - options: Some(options_map), - max_msg_size: Some(1024), - pin_protocols: Some(vec![ - CtapState::::PIN_PROTOCOL_VERSION, + versions, + extensions: Some(vec![ + String::from("hmac-secret"), + String::from("credProtect"), + String::from("minPinLength"), + String::from("credBlob"), + String::from("largeBlobKey"), + ]), + aaguid: self.persistent_store.aaguid()?, + 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![ + PinUvAuthProtocol::V2 as u64, + PinUvAuthProtocol::V1 as u64, ]), - #[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")] + 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()?, - #[cfg(feature = "with_ctap2_1")] 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, + ), }, )) } - 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, 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? - self.check_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)?; 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( @@ -918,7 +1116,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) @@ -1014,13 +1211,16 @@ 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, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, - MakeCredentialOptions, 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; + use cbor::{cbor_array, cbor_array_vec, cbor_map}; use crypto::rng256::ThreadRng256; const CLOCK_FREQUENCY_HZ: usize = 32768; @@ -1031,6 +1231,48 @@ 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, + ep_att, + large_blob_key, + } = 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[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); + } + _ => panic!("Invalid response type"), + } + } + #[test] fn test_get_info() { let mut rng = ThreadRng256 {}; @@ -1038,50 +1280,51 @@ 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; - #[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( - [ - 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(), - ); + 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![ + 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! { + "ep" => ENTERPRISE_ATTESTATION_MODE.map(|_| false), + "rk" => true, + "up" => true, + "alwaysUv" => false, + "credMgmt" => true, + "authnrCfg" => true, + "clientPin" => false, + "largeBlobs" => true, + "pinUvAuthToken" => true, + "setMinPINLength" => true, + "makeCredUvNotRqd" => true, + }, + 0x05 => MAX_MSG_SIZE as u64, + 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!["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, + 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 { @@ -1108,10 +1351,11 @@ 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, + enterprise_attestation: None, } } @@ -1132,17 +1376,17 @@ mod test { fn create_make_credential_parameters_with_cred_protect_policy( policy: CredentialProtectionPolicy, ) -> AuthenticatorMakeCredentialParameters { - let extensions = Some(MakeCredentialExtensions { - hmac_secret: false, + let extensions = MakeCredentialExtensions { cred_protect: Some(policy), - }); + ..Default::default() + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = extensions; make_credential_params } #[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); @@ -1151,35 +1395,17 @@ 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] - 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); @@ -1189,31 +1415,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] @@ -1254,6 +1462,8 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1281,12 +1491,14 @@ 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 mut iter_result = Ok(()); + let iter = ctap_state .persistent_store - .filter_credential("example.com", false) - .unwrap() - .pop() + .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)); @@ -1306,12 +1518,14 @@ 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 mut iter_result = Ok(()); + let iter = ctap_state .persistent_store - .filter_credential("example.com", false) - .unwrap() - .pop() + .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)); @@ -1328,49 +1542,26 @@ 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, - }); + ..Default::default() + }; 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, - ]; - 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] @@ -1379,48 +1570,299 @@ 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, - }); + ..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); - match make_credential_response.unwrap() { + 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 = MakeCredentialExtensions { + min_pin_length: 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); + 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 = MakeCredentialExtensions { + min_pin_length: 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 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] + 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 { + cred_blob: Some(vec![0xCB]), + ..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 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 { + 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; + 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_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) => { - 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); + 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); + } + + 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.clone(), DUMMY_CHANNEL_ID); + + check_make_response( + make_credential_response, + 0x45, + &ctap_state.persistent_store.aaguid().unwrap(), + 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] + 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 {}; + 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(PinUvAuthProtocol::V1); + 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] @@ -1446,6 +1888,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) => { @@ -1465,6 +1908,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); @@ -1473,6 +1917,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, @@ -1491,11 +1958,12 @@ mod test { 0x00, signature_counter, expected_number_of_credentials, + &[], ); } #[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); @@ -1509,7 +1977,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, @@ -1529,17 +1997,69 @@ 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); - let make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - }); + ..Default::default() + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; make_credential_params.extensions = make_extensions; @@ -1557,98 +2077,106 @@ 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], - }; - 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, - }, + 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, + DUMMY_CLOCK_VALUE, + ); + 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_EXTENSION) - ); + assert!(get_assertion_response.is_ok()); } #[test] - fn test_residential_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); - let make_extensions = Some(MakeCredentialExtensions { + let make_extensions = MakeCredentialExtensions { hmac_secret: true, - cred_protect: None, - }); + ..Default::default() + }; let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.extensions = make_extensions; assert!(ctap_state .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], - }; - let get_extensions = Some(GetAssertionExtensions { - hmac_secret: Some(hmac_secret_input), - }); - - 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, + DUMMY_CLOCK_VALUE, + ); + 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_eq!( - get_assertion_response, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) - ); + assert!(get_assertion_response.is_ok()); } #[test] - fn test_residential_process_get_assertion_with_cred_protect() { + 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] + 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(); @@ -1658,7 +2186,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, @@ -1673,6 +2201,8 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1683,7 +2213,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, @@ -1705,7 +2235,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, @@ -1735,6 +2265,8 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -1745,7 +2277,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, @@ -1765,15 +2297,137 @@ mod test { } #[test] - fn test_process_get_next_assertion_two_credentials_with_uv() { + 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]), + large_blob_key: None, + }; + assert!(ctap_state + .persistent_store + .store_credential(credential) + .is_ok()); + + let extensions = GetAssertionExtensions { + cred_blob: 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 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_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]); + } + + 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 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, 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.pin_protocol_v1 = pin_protocol_v1; let mut make_credential_params = create_minimal_make_credential_parameters(); let user1 = PublicKeyCredentialUserEntity { @@ -1798,26 +2452,27 @@ mod test { .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, - ]); + 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]; + 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: 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, @@ -1834,6 +2489,7 @@ mod test { 0x04, signature_counter, Some(2), + &[], ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); @@ -1843,6 +2499,7 @@ mod test { 0x04, signature_counter, None, + &[], ); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); @@ -1852,6 +2509,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 {}; @@ -1887,7 +2554,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, @@ -1951,7 +2618,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, @@ -2007,6 +2674,8 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(ctap_state .persistent_store @@ -2049,6 +2718,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 {}; @@ -2056,10 +2741,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.rs b/src/ctap/pin_protocol.rs new file mode 100644 index 0000000..d1e8f2b --- /dev/null +++ b/src/ctap/pin_protocol.rs @@ -0,0 +1,408 @@ +// 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::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::Vec; +use core::convert::TryInto; +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; +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: 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 { + PinUvAuthProtocol::V1 => Ok(Box::new(SharedSecretV1::new(handshake))), + PinUvAuthProtocol::V2 => Ok(Box::new(SharedSecretV2::new(handshake))), + } + } + + /// 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, + } + } +} + +/// 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], + message: &[u8], + signature: &[u8], + pin_uv_auth_protocol: PinUvAuthProtocol, +) -> Result<(), Ctap2StatusCode> { + match pin_uv_auth_protocol { + PinUvAuthProtocol::V1 => verify_v1(token, message, signature), + PinUvAuthProtocol::V2 => verify_v2(token, message, signature), + } +} + +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>; + + /// Creates a signature that matches verify. + #[cfg(test)] + fn authenticate(&self, message: &[u8]) -> Vec; +} + +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, + } + } +} + +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) + } + + #[cfg(test)] + fn authenticate(&self, message: &[u8]) -> Vec { + hmac_256::(&self.common_secret, message)[..16].to_vec() + } +} + +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)] + fn authenticate(&self, message: &[u8]) -> Vec { + hmac_256::(&self.hmac_key, message).to_vec() + } +} + +#[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_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]); + 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_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]); + 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_symmetric() { + let mut rng = ThreadRng256 {}; + let pin_protocol1 = PinProtocol::new(&mut rng); + let pin_protocol2 = PinProtocol::new(&mut rng); + for &protocol in &[PinUvAuthProtocol::V1, PinUvAuthProtocol::V2] { + 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, PinUvAuthProtocol::V1), + Ok(()) + ); + assert_eq!( + 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, PinUvAuthProtocol::V1), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &[0x12; 16], PinUvAuthProtocol::V1), + 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, PinUvAuthProtocol::V2), + Ok(()) + ); + assert_eq!( + 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, PinUvAuthProtocol::V2), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + assert_eq!( + verify_pin_uv_auth_token(&token, &message, &[0x12; 32], PinUvAuthProtocol::V2), + Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) + ); + } +} diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs deleted file mode 100644 index 410dac7..0000000 --- a/src/ctap/pin_protocol_v1.rs +++ /dev/null @@ -1,1277 +0,0 @@ -// 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::command::AuthenticatorClientPinParameters; -use super::data_formats::{ClientPinSubCommand, CoseKey, GetAssertionHmacSecretInput}; -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; -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::rng256::Rng256; -use crypto::sha256::Sha256; -use crypto::Hash256; -#[cfg(all(test, feature = "with_ctap2_1"))] -use enum_iterator::IntoEnumIterator; -use subtle::ConstantTimeEq; - -// Those constants have to be multiples of 16, the AES block size. -pub const PIN_AUTH_LENGTH: usize = 16; -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. -fn encrypt_hmac_secret_output( - shared_secret: &[u8; 32], - salt_enc: &[u8], - cred_random: &[u8; 32], -) -> Result, Ctap2StatusCode> { - if salt_enc.len() != 32 && salt_enc.len() != 64 { - 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]; - - // 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)]); - } - 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) -} - -/// Decrypts the new_pin_enc and outputs the found PIN. -fn decrypt_pin( - aes_dec_key: &crypto::aes256::DecryptionKey, - new_pin_enc: Vec, -) -> Option> { - if new_pin_enc.len() != PIN_PADDED_LENGTH { - return None; - } - 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::>(), - ) -} - -/// 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. -fn check_and_store_new_pin( - persistent_store: &mut PersistentStore, - aes_dec_key: &crypto::aes256::DecryptionKey, - new_pin_enc: Vec, -) -> Result<(), Ctap2StatusCode> { - 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); - } - let mut pin_hash = [0u8; 16]; - pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); - persistent_store.set_pin_hash(&pin_hash)?; - Ok(()) -} - -#[cfg(feature = "with_ctap2_1")] -#[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, - PlatformConfiguration = 0x10, - AuthenticatorConfiguration = 0x20, -} - -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, -} - -impl PinProtocolV1 { - pub fn new(rng: &mut impl Rng256) -> PinProtocolV1 { - let key_agreement_key = crypto::ecdh::SecKey::gensk(rng); - let pin_uv_auth_token = rng.gen_uniform_u8x32(); - 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, - } - } - - /// 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. - fn verify_pin_hash_enc( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - aes_dec_key: &crypto::aes256::DecryptionKey, - pin_hash_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - match persistent_store.pin_hash()? { - Some(pin_hash) => { - if self.consecutive_pin_mismatches >= 3 { - 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 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 persistent_store.pin_retries()? == 0 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); - } - self.consecutive_pin_mismatches += 1; - if self.consecutive_pin_mismatches >= 3 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED); - } - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - } - // This status code is not explicitly mentioned in the specification. - None => return Err(Ctap2StatusCode::CTAP2_ERR_PIN_REQUIRED), - } - persistent_store.reset_pin_retries()?; - self.consecutive_pin_mismatches = 0; - 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, - ) -> Result { - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: None, - retries: Some(persistent_store.pin_retries()? as u64), - }) - } - - fn process_get_key_agreement(&self) -> Result { - let pk = self.key_agreement_key.genpk(); - Ok(AuthenticatorClientPinResponse { - key_agreement: Some(CoseKey::from(pk)), - pin_token: None, - retries: None, - }) - } - - fn process_set_pin( - &mut self, - persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - 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)?; - persistent_store.reset_pin_retries()?; - Ok(()) - } - - fn process_change_pin( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_auth: Vec, - new_pin_enc: Vec, - pin_hash_enc: Vec, - ) -> Result<(), Ctap2StatusCode> { - if persistent_store.pin_retries()? == 0 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED); - } - 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)?; - - check_and_store_new_pin(persistent_store, &pin_decryption_key, new_pin_enc)?; - self.pin_uv_auth_token = rng.gen_uniform_u8x32(); - Ok(()) - } - - fn process_get_pin_token( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_hash_enc: Vec, - ) -> Result { - 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)?; - - // 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(); - - #[cfg(feature = "with_ctap2_1")] - { - self.permissions = 0x03; - self.permissions_rp_id = None; - } - - Ok(AuthenticatorClientPinResponse { - key_agreement: None, - pin_token: Some(pin_token), - retries: None, - }) - } - - #[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. - // Lacking a fingerprint reader, this subcommand is currently unsupported. - _key_agreement: CoseKey, - _permissions: u8, - _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) - } - } - - #[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) - } - } - - #[cfg(feature = "with_ctap2_1")] - 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_UNSUPPORTED_EXTENSION); - } - 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_RESPONSE_CANNOT_WRITE_CBOR); - // } - 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(()) - } - - #[cfg(feature = "with_ctap2_1")] - fn process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - key_agreement: CoseKey, - pin_hash_enc: Vec, - permissions: u8, - permissions_rp_id: Option, - ) -> Result { - if permissions == 0 { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - // This check is not mentioned protocol steps, but mentioned in a side note. - if permissions & 0x03 != 0 && permissions_rp_id.is_none() { - return Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER); - } - - let response = - self.process_get_pin_token(rng, persistent_store, key_agreement, pin_hash_enc)?; - - self.permissions = permissions; - self.permissions_rp_id = permissions_rp_id; - - Ok(response) - } - - pub fn process_subcommand( - &mut self, - rng: &mut impl Rng256, - persistent_store: &mut PersistentStore, - client_pin_params: AuthenticatorClientPinParameters, - ) -> Result { - let AuthenticatorClientPinParameters { - pin_protocol, - sub_command, - key_agreement, - 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); - } - - let response = match sub_command { - ClientPinSubCommand::GetPinRetries => { - Some(self.process_get_pin_retries(persistent_store)?) - } - ClientPinSubCommand::GetKeyAgreement => Some(self.process_get_key_agreement()?), - 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)?, - )?; - 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)?, - )?; - 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)?, - )?), - #[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)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - 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, - min_pin_length.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - min_pin_length_rp_ids, - pin_auth, - )?; - None - } - #[cfg(feature = "with_ctap2_1")] - ClientPinSubCommand::GetPinUvAuthTokenUsingPinWithPermissions => Some( - self.process_get_pin_uv_auth_token_using_pin_with_permissions( - rng, - persistent_store, - key_agreement.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - pin_hash_enc.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions.ok_or(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER)?, - permissions_rp_id, - )?, - ), - }; - Ok(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 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.consecutive_pin_mismatches = 0; - #[cfg(feature = "with_ctap2_1")] - { - self.permissions = 0; - self.permissions_rp_id = None; - } - } - - pub fn process_hmac_secret( - &self, - hmac_secret_input: GetAssertionHmacSecretInput, - cred_random: &[u8; 32], - ) -> Result, Ctap2StatusCode> { - let GetAssertionHmacSecretInput { - key_agreement, - 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_UNSUPPORTED_EXTENSION); - } - 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 { - Ok(()) - } else { - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - } - } - - #[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 { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } else { - self.permissions_rp_id = Some(String::from(rp_id)); - } - 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)] -mod test { - use super::*; - 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[..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[..]); - encrypt_message(shared_secret, &padded_pin) - } - - // 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". - 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]) - } - - #[test] - fn test_verify_pin_hash_enc() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - // 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_hash(&pin_hash).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 mut pin_protocol_v1 = PinProtocolV1::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( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Ok(()) - ); - - let pin_hash_enc = vec![0xEE; 16]; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - 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, - ]; - pin_protocol_v1.consecutive_pin_mismatches = 3; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED) - ); - pin_protocol_v1.consecutive_pin_mismatches = 0; - - let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH - 1]; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) - ); - - let pin_hash_enc = vec![0x77; PIN_AUTH_LENGTH + 1]; - assert_eq!( - pin_protocol_v1.verify_pin_hash_enc( - &mut rng, - &mut persistent_store, - &aes_dec_key, - pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID) - ); - } - - #[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 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), - expected_response - ); - } - - #[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 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 - ); - } - - #[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 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 - ), - Ok(()) - ); - } - - #[test] - fn test_process_change_pin() { - 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 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!( - pin_protocol_v1.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_auth.clone(), - new_pin_enc.clone(), - pin_hash_enc.clone() - ), - Ok(()) - ); - - let bad_pin_hash_enc = vec![0xEE; 16]; - assert_eq!( - pin_protocol_v1.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_auth.clone(), - new_pin_enc.clone(), - bad_pin_hash_enc - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - - while persistent_store.pin_retries().unwrap() > 0 { - persistent_store.decr_pin_retries().unwrap(); - } - assert_eq!( - pin_protocol_v1.process_change_pin( - &mut rng, - &mut persistent_store, - key_agreement, - pin_auth, - new_pin_enc, - pin_hash_enc, - ), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_BLOCKED) - ); - } - - #[test] - fn test_process_get_pin_token() { - 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 key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - assert!(pin_protocol_v1 - .process_get_pin_token( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc - ) - .is_ok()); - - let pin_hash_enc = vec![0xEE; 16]; - 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) - ); - } - - #[cfg(feature = "with_ctap2_1")] - #[test] - fn test_process_get_pin_uv_auth_token_using_pin_with_permissions() { - 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 key_agreement = CoseKey::from(pk); - let pin_hash_enc = encrypt_standard_pin_hash(&shared_secret); - assert!(pin_protocol_v1 - .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")), - ) - .is_ok()); - assert_eq!(pin_protocol_v1.permissions, 0x03); - assert_eq!( - pin_protocol_v1.permissions_rp_id, - Some(String::from("example.com")) - ); - - assert_eq!( - pin_protocol_v1.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")), - ), - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - - assert_eq!( - pin_protocol_v1.process_get_pin_uv_auth_token_using_pin_with_permissions( - &mut rng, - &mut persistent_store, - key_agreement.clone(), - pin_hash_enc.clone(), - 0x03, - None, - ), - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) - ); - - let pin_hash_enc = vec![0xEE; 16]; - 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) - ); - } - - #[cfg(feature = "with_ctap2_1")] - #[test] - fn test_process_set_min_pin_length() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let min_pin_length = 8; - pin_protocol_v1.pin_uv_auth_token = [0x55; PIN_TOKEN_LENGTH]; - let pin_auth = vec![ - 0x94, 0x86, 0xEF, 0x4C, 0xB3, 0x84, 0x2C, 0x85, 0x72, 0x02, 0xBF, 0xE4, 0x36, 0x22, - 0xFE, 0xC9, - ]; - // TODO(kaczmarczyck) implement test for the min PIN length extension - // 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 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - let client_pin_params = AuthenticatorClientPinParameters { - pin_protocol: 1, - sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: None, - 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 - .process_subcommand(&mut rng, &mut persistent_store, client_pin_params) - .is_ok()); - - let client_pin_params = AuthenticatorClientPinParameters { - pin_protocol: 2, - sub_command: ClientPinSubCommand::GetPinRetries, - key_agreement: None, - 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), - Err(error_code) - ); - } - - #[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); - - // "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(&aes_dec_key, new_pin_enc), - Some(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, - ]; - assert_eq!( - decrypt_pin(&aes_dec_key, new_pin_enc), - Some(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,); - - // Encrypted PIN is too long. - let new_pin_enc = vec![0x44; 65]; - assert_eq!(decrypt_pin(&aes_dec_key, new_pin_enc), None,); - } - - #[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 test_cases = vec![ - // Accept PIN "1234". - (b"1234".to_vec(), Ok(())), - // Reject PIN "123" since it is too short. - ( - b"123".to_vec(), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), - ), - // Reject PIN "12'\0'4" (a zero byte at index 2). - ( - b"12\04".to_vec(), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), - ), - // PINs must be at most 63 bytes long, to allow for a trailing 0u8 padding. - ( - vec![0x30; 64], - Err(Ctap2StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION), - ), - ]; - for (pin, result) in test_cases { - let old_pin_hash = persistent_store.pin_hash().unwrap(); - let new_pin_enc = encrypt_pin(&shared_secret, pin); - assert_eq!( - check_and_store_new_pin(&mut persistent_store, &aes_dec_key, new_pin_enc), - result - ); - if result.is_ok() { - assert_ne!(old_pin_hash, persistent_store.pin_hash().unwrap()); - } else { - assert_eq!(old_pin_hash, persistent_store.pin_hash().unwrap()); - } - } - } - - #[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 salt_enc = [0x5E; 32]; - let cred_random = [0xC9; 32]; - let output = encrypt_hmac_secret_output(&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); - assert_eq!( - output, - Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) - ); - - let salt_enc = [0x5E; 64]; - let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random); - assert_eq!(output.unwrap().len(), 64); - - 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")] - #[test] - fn test_has_permission() { - let mut rng = ThreadRng256 {}; - let mut pin_protocol_v1 = PinProtocolV1::new(&mut rng); - pin_protocol_v1.permissions = 0x7F; - for permission in PinPermission::into_enum_iter() { - assert_eq!(pin_protocol_v1.has_permission(permission), Ok(())); - } - pin_protocol_v1.permissions = 0x00; - for permission in PinPermission::into_enum_iter() { - assert_eq!( - pin_protocol_v1.has_permission(permission), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - } - } - - #[cfg(feature = "with_ctap2_1")] - #[test] - fn test_has_permission_for_rp_id() { - 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"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.permissions_rp_id, - Some(String::from("example.com")) - ); - assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("example.com"), - Ok(()) - ); - assert_eq!( - pin_protocol_v1.has_permission_for_rp_id("counter-example.com"), - Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID) - ); - } -} diff --git a/src/ctap/response.rs b/src/ctap/response.rs index 6422959..2568bae 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. @@ -12,19 +12,16 @@ // 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, + AuthenticatorTransport, CoseKey, CredentialProtectionPolicy, PackedAttestationStatement, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; -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_collection, 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), @@ -32,8 +29,11 @@ pub enum ResponseData { AuthenticatorGetInfo(AuthenticatorGetInfoResponse), AuthenticatorClientPin(Option), AuthenticatorReset, - #[cfg(feature = "with_ctap2_1")] + AuthenticatorCredentialManagement(Option), AuthenticatorSelection, + AuthenticatorLargeBlobs(Option), + // TODO(kaczmarczyck) dummy, extend + AuthenticatorConfig, AuthenticatorVendor(AuthenticatorVendorResponse), } @@ -44,22 +44,24 @@ 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, - #[cfg(feature = "with_ctap2_1")] + 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()), } } } -#[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, pub att_stmt: PackedAttestationStatement, + pub ep_att: Option, + pub large_blob_key: Option>, } impl From for cbor::Value { @@ -68,24 +70,29 @@ impl From for cbor::Value { fmt, auth_data, att_stmt, + ep_att, + large_blob_key, } = make_credential_response; cbor_map_options! { - 1 => fmt, - 2 => auth_data, - 3 => att_stmt, + 0x01 => fmt, + 0x02 => auth_data, + 0x03 => att_stmt, + 0x04 => ep_att, + 0x05 => large_blob_key, } } } -#[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, 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>, } impl From for cbor::Value { @@ -96,45 +103,49 @@ impl From for cbor::Value { signature, user, number_of_credentials, + large_blob_key, } = 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, + 0x07 => large_blob_key, } } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] pub struct AuthenticatorGetInfoResponse { - // TODO(kaczmarczyck) add maxAuthenticatorConfigLength and defaultCredProtect 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>, - #[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 max_serialized_large_blob_array: Option, + pub force_pin_change: Option, pub min_pin_length: u8, - #[cfg(feature = "with_ctap2_1")] 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 { - #[cfg(feature = "with_ctap2_1")] fn from(get_info_response: AuthenticatorGetInfoResponse) -> Self { let AuthenticatorGetInfoResponse { versions, @@ -147,17 +158,30 @@ 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, + 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: Vec<(_, _)> = options .into_iter() .map(|(key, value)| (cbor_text!(key), cbor_bool!(value))) .collect(); - cbor_map_btree!(option_map) + cbor_map_collection!(options_map) + }); + + let certifications_cbor: Option = certifications.map(|certifications| { + let certifications_map: Vec<(_, _)> = certifications + .into_iter() + .map(|(key, value)| (cbor_text!(key), cbor_int!(value))) + .collect(); + cbor_map_collection!(certifications_map) }); cbor_map_options! { @@ -171,70 +195,108 @@ 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, - } - } - - #[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), + 0x0F => max_cred_blob_length, + 0x10 => max_rp_ids_for_set_min_pin_length, + 0x13 => certifications_cbor, + 0x14 => remaining_discoverable_credentials, } } } -#[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>, + pub pin_uv_auth_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 { 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! { - 1 => key_agreement.map(|cose_key| cbor_map_btree!(cose_key.0)), - 2 => pin_token, - 3 => retries, + 0x01 => key_agreement.map(cbor::Value::from), + 0x02 => pin_uv_auth_token, + 0x03 => retries, + 0x04 => power_cycle_state, } } } -#[cfg_attr(test, derive(PartialEq))] -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug))] +#[derive(Debug, PartialEq)] +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(Debug, Default, PartialEq)] +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, + } + } +} + +#[derive(Debug, PartialEq)] pub struct AuthenticatorVendorResponse { pub cert_programmed: bool, pub pkey_programmed: bool, @@ -248,19 +310,19 @@ impl From for cbor::Value { } = vendor_response; cbor_map_options! { - 1 => cert_programmed, - 2 => pkey_programmed, + 0x01 => cert_programmed, + 0x02 => pkey_programmed, } } } #[cfg(test)] mod test { - use super::super::data_formats::PackedAttestationStatement; - #[cfg(feature = "with_ctap2_1")] + 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] fn test_make_credential_into_cbor() { @@ -274,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], }; @@ -282,31 +344,60 @@ 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 = 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, + 0x04 => true, + 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! { - 2 => vec![0xAD], - 3 => vec![0x51], + 0x01 => cbor_map! { + "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(), + }, + 0x05 => 2, + 0x07 => vec![0x1B], }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -321,28 +412,21 @@ 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")] + max_serialized_large_blob_array: None, + force_pin_change: None, min_pin_length: 4, - #[cfg(feature = "with_ctap2_1")] 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 = 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,56 +436,71 @@ 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); 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), 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), + max_rp_ids_for_set_min_pin_length: Some(8), + 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]], - 0x0C => CredentialProtectionPolicy::UserVerificationRequired as u64, + 0x09 => cbor_array!["usb"], + 0x0A => cbor_array![ES256_CRED_PARAM], + 0x0B => 1024, + 0x0C => false, 0x0D => 4, 0x0E => 0, + 0x0F => 1024, + 0x10 => 8, + 0x13 => cbor_map! {"example-cert" => 1}, + 0x14 => 150, }; assert_eq!(response_cbor, Some(expected_cbor)); } #[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, - pin_token: Some(vec![70]), - retries: None, + key_agreement: Some(cose_key.clone()), + pin_uv_auth_token: Some(vec![70]), + 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! { - 2 => vec![70], + 0x01 => cbor::Value::from(cose_key), + 0x02 => vec![70], + 0x03 => 8, + 0x04 => false, }; assert_eq!(response_cbor, Some(expected_cbor)); } @@ -418,13 +517,105 @@ mod test { assert_eq!(response_cbor, None); } - #[cfg(feature = "with_ctap2_1")] + #[test] + fn test_used_credential_management_into_cbor() { + let cred_management_response = AuthenticatorCredentialManagementResponse::default(); + 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(); 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(); + assert_eq!(response_cbor, None); + } + #[test] fn test_vendor_response_into_cbor() { let response_cbor: Option = @@ -436,8 +627,8 @@ mod test { assert_eq!( response_cbor, Some(cbor_map_options! { - 1 => true, - 2 => false, + 0x01 => true, + 0x02 => false, }) ); let response_cbor: Option = @@ -449,8 +640,8 @@ mod test { assert_eq!( response_cbor, Some(cbor_map_options! { - 1 => false, - 2 => true, + 0x01 => false, + 0x02 => true, }) ); } diff --git a/src/ctap/status_code.rs b/src/ctap/status_code.rs index 097d7ec..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. @@ -31,11 +31,8 @@ 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,25 +54,22 @@ 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, 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, + CTAP2_ERR_UV_INVALID = 0x3F, + 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 +79,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..2ba8d77 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. @@ -14,55 +14,29 @@ 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::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, + 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::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::cmp; use core::convert::TryInto; use crypto::rng256::Rng256; - -// 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 residential keys permits to ensure a minimum number of counter increments. -// Let: -// - P the number of pages (NUM_PAGES) -// - K the maximum number of residential keys (MAX_SUPPORTED_RESIDENTIAL_KEYS) -// - S the maximum size of a residential key (about 500) -// - C the number of erase cycles (10000) -// - I the minimum number of counter increments -// -// We have: I = (P * 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_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; +use persistent_store::{fragment, StoreUpdate}; /// Wrapper for master keys. pub struct MasterKeys { @@ -73,6 +47,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, @@ -121,6 +104,47 @@ 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_RESIDENT_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 + /// + /// Returns `CTAP2_ERR_NO_CREDENTIALS` if the credential is not found. + pub 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 @@ -131,22 +155,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((_key, credential)) => credential, + }; + 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. @@ -160,13 +179,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); } @@ -181,16 +198,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. @@ -201,44 +216,54 @@ impl PersistentStore { Ok(()) } - /// Returns the list of matching credentials. + /// Deletes a credential. /// - /// 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> { - 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(); - iter_result?; - Ok(result) + /// # 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 number of credentials. - #[cfg(test)] 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. + pub fn remaining_credentials(&self) -> Result { + MAX_SUPPORTED_RESIDENT_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> { @@ -301,26 +326,52 @@ 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.transaction(&[ + StoreUpdate::Insert { + key: key::PIN_PROPERTIES, + value: &pin_properties[..], + }, + StoreUpdate::Remove { + key: key::FORCE_PIN_CHANGE, + }, + ])?) } /// Returns the number of remaining PIN retries. @@ -348,7 +399,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,43 +408,91 @@ 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 - .find(key::_MIN_PIN_LENGTH_RP_IDS)? - .map_or(Some(_DEFAULT_MIN_PIN_LENGTH_RP_IDS), |value| { - _deserialize_min_pin_length_rp_ids(&value) - }); + pub fn min_pin_length_rp_ids(&self) -> Result, Ctap2StatusCode> { + 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(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( + 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.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); } } - 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)?, + )?) + } + + /// 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 legitimate + /// large blob entry possible). + pub fn get_large_blob_array( + &self, + offset: usize, + byte_count: usize, + ) -> Result, Ctap2StatusCode> { + let byte_range = offset..offset + byte_count; + let output = fragment::read_range(&self.store, &key::LARGE_BLOB_SHARDS, byte_range)?; + Ok(output.unwrap_or_else(|| { + 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 + .get(offset..last_index) + .unwrap_or_default() + .to_vec() + })) + } + + /// Sets a byte vector as the serialized large blobs array. + pub fn commit_large_blob_array( + &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); + } + Ok(fragment::write( + &mut self.store, + &key::LARGE_BLOB_SHARDS, + large_blob_array, )?) } @@ -480,6 +578,61 @@ impl PersistentStore { self.init(rng)?; 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.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, &[])?) + } + + /// 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(()) + } + + /// 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), + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Enables alwaysUv, when disabled, and vice versa. + pub fn toggle_always_uv(&mut self) -> Result<(), Ctap2StatusCode> { + if ENFORCE_ALWAYS_UV { + return Err(Ctap2StatusCode::CTAP2_ERR_OPERATION_DENIED); + } + if self.has_always_uv()? { + Ok(self.store.remove(key::ALWAYS_UV)?) + } else { + Ok(self.store.insert(key::ALWAYS_UV, &[])?) + } + } } impl From for Ctap2StatusCode { @@ -503,12 +656,12 @@ impl From for Ctap2StatusCode { } /// Iterator for credentials. -struct IterCredentials<'a> { +pub struct IterCredentials<'a> { /// The store being iterated. store: &'a persistent_store::Store, /// The store iterator. - iter: persistent_store::StoreIter<'a, Storage>, + iter: persistent_store::StoreIter<'a>, /// The iteration result. /// @@ -577,13 +730,12 @@ 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) } } /// Deserializes a list of RP IDs from storage representation. -#[cfg(feature = "with_ctap2_1")] -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()? @@ -594,13 +746,12 @@ 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> { +fn serialize_min_pin_length_rp_ids(rp_ids: Vec) -> Result, Ctap2StatusCode> { let mut data = Vec::new(); if cbor::write(cbor_array_vec!(rp_ids), &mut data) { Ok(data) } else { - Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR) + Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR) } } @@ -627,6 +778,8 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, } } @@ -640,6 +793,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_RESIDENT_KEYS { + 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()); + 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 {}; @@ -656,24 +869,21 @@ 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]); + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { + 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); } 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), @@ -681,12 +891,11 @@ mod test { ); assert_eq!( persistent_store.count_credentials().unwrap(), - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS ); } #[test] - #[allow(clippy::assertions_on_constants)] fn test_overwrite() { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); @@ -694,7 +903,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) @@ -703,25 +913,26 @@ 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] - ); + 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()); - // 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 mut persistent_store = PersistentStore::new(&mut rng); + for i in 0..MAX_SUPPORTED_RESIDENT_KEYS { + 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); } 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), @@ -729,69 +940,26 @@ mod test { ); assert_eq!( persistent_store.count_credentials().unwrap(), - MAX_SUPPORTED_RESIDENTIAL_KEYS + MAX_SUPPORTED_RESIDENT_KEYS ); } #[test] - fn test_filter() { + 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_credential("example.com", false) - .unwrap(); - assert_eq!(filtered_credentials.len(), 2); - assert!( - (filtered_credentials[0].credential_id == id0 - && filtered_credentials[1].credential_id == id1) - || (filtered_credentials[1].credential_id == id0 - && filtered_credentials[0].credential_id == id1) - ); - } - - #[test] - fn test_filter_with_cred_protect() { - let mut rng = ThreadRng256 {}; - let mut persistent_store = PersistentStore::new(&mut rng); - assert_eq!(persistent_store.count_credentials().unwrap(), 0); - let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let credential = PublicKeyCredentialSource { - key_type: PublicKeyCredentialType::PublicKey, - credential_id: rng.gen_uniform_u8x32().to_vec(), - private_key, - rp_id: String::from("example.com"), - user_handle: vec![0x00], - user_display_name: None, - cred_protect_policy: Some( - CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, - ), - creation_order: 0, - user_name: None, - user_icon: None, - }; - assert!(persistent_store.store_credential(credential).is_ok()); - - let no_credential = persistent_store - .filter_credential("example.com", true) - .unwrap(); - assert_eq!(no_credential, vec![]); + 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] @@ -828,6 +996,8 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -849,6 +1019,8 @@ mod test { creation_order: 0, user_name: None, user_icon: None, + cred_blob: None, + large_blob_key: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -902,28 +1074,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] @@ -988,7 +1170,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 +1192,6 @@ mod test { ); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_min_pin_length_rp_ids() { let mut rng = ThreadRng256 {}; @@ -1019,22 +1199,99 @@ 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.iter() { + let rp_id = String::from(rp_id); 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] + fn test_max_large_blob_array_size() { + let mut rng = ThreadRng256 {}; + let persistent_store = PersistentStore::new(&mut rng); + + assert!( + MAX_LARGE_BLOB_ARRAY_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() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + 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(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(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); + } + + #[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; 5]; + assert!(persistent_store + .commit_large_blob_array(&large_blob_array) + .is_ok()); + 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, 5).unwrap(); + assert_eq!(large_blob_array, 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); + + assert!(persistent_store.commit_large_blob_array(&[]).is_ok()); + 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); + } + + #[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, + ]; + 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); + let restored_large_blob_array = persistent_store.get_large_blob_array(16, 1).unwrap(); + assert_eq!(vec![0x3C], restored_large_blob_array); } #[test] @@ -1059,6 +1316,50 @@ 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_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_always_uv() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + + 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(())); + 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 {}; @@ -1069,23 +1370,24 @@ 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, + 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(); assert_eq!(credential, reconstructed); } - #[cfg(feature = "with_ctap2_1")] #[test] fn test_serialize_deserialize_min_pin_length_rp_ids() { let rp_ids = vec![String::from("example.com")]; - let serialized = _serialize_min_pin_length_rp_ids(rp_ids.clone()).unwrap(); - let reconstructed = _deserialize_min_pin_length_rp_ids(&serialized).unwrap(); + 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 5c5b20e..38a2a8b 100644 --- a/src/ctap/storage/key.rs +++ b/src/ctap/storage/key.rs @@ -84,21 +84,33 @@ 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; + /// 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..2004; + + /// If this entry exists and is empty, alwaysUv is enabled. + ALWAYS_UV = 2038; + + /// 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. 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; + 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. @@ -106,10 +118,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. /// @@ -128,8 +141,8 @@ mod test { #[test] fn enough_credentials() { - use super::super::MAX_SUPPORTED_RESIDENTIAL_KEYS; - assert!(MAX_SUPPORTED_RESIDENTIAL_KEYS <= CREDENTIALS.end - CREDENTIALS.start); + use crate::ctap::customization::MAX_SUPPORTED_RESIDENT_KEYS; + assert!(MAX_SUPPORTED_RESIDENT_KEYS <= CREDENTIALS.end - CREDENTIALS.start); } #[test] 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. 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 {