diff --git a/.github/workflows/cargo_fuzz.yml b/.github/workflows/cargo_fuzz.yml index 4143b5a..b71b10a 100644 --- a/.github/workflows/cargo_fuzz.yml +++ b/.github/workflows/cargo_fuzz.yml @@ -29,3 +29,5 @@ jobs: run: cargo fuzz build - name: Cargo fuzz build (libraries/cbor) run: cd libraries/cbor && cargo fuzz build && cd ../.. + - name: Cargo fuzz build (libraries/persistent_store) + run: cd libraries/persistent_store && cargo fuzz build && cd ../.. diff --git a/.github/workflows/crypto_test.yml b/.github/workflows/crypto_test.yml index 1740280..50fdf88 100644 --- a/.github/workflows/crypto_test.yml +++ b/.github/workflows/crypto_test.yml @@ -27,7 +27,7 @@ jobs: - name: Set up OpenSK run: ./setup.sh - - run: echo "::set-env name=RUSTFLAGS::-C target-feature=+aes" + - run: echo "RUSTFLAGS=-C target-feature=+aes" >> $GITHUB_ENV - name: Unit testing of crypto library (release mode) uses: actions-rs/cargo@v1 diff --git a/deploy.py b/deploy.py index 358355d..e1ec38f 100755 --- a/deploy.py +++ b/deploy.py @@ -928,6 +928,13 @@ if __name__ == "__main__": const="console_test", help=("Compiles and installs the console_test example that tests the " "console driver with messages of various lengths.")) + apps_group.add_argument( + "--nfct_test", + dest="application", + action="store_const", + const="nfct_test", + help=("Compiles and installs the nfct_test example that tests the " + "NFC driver.")) main_parser.set_defaults(features=["with_ctap1"]) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs new file mode 100644 index 0000000..924237d --- /dev/null +++ b/examples/nfct_test.rs @@ -0,0 +1,249 @@ +#![no_std] + +extern crate alloc; +extern crate lang_items; +extern crate libtock_drivers; + +use core::fmt::Write; +use libtock_drivers::console::Console; + +#[cfg(not(feature = "with_nfc"))] +mod example { + use super::Console; + use super::Write; + + pub fn nfc(console: &mut Console) { + writeln!(console, "NFC feature flag is missing!").unwrap(); + } +} + +#[cfg(feature = "with_nfc")] +mod example { + use super::Console; + use super::Write; + use libtock_core::result::CommandError; + use libtock_drivers::nfc::NfcTag; + use libtock_drivers::nfc::RecvOp; + use libtock_drivers::result::FlexUnwrap; + use libtock_drivers::result::TockError; + use libtock_drivers::timer; + use libtock_drivers::timer::Timer; + use libtock_drivers::timer::Timestamp; + + #[derive(Copy, Clone, Debug, PartialEq)] + enum ReturnCode { + /// Operation completed successfully + SUCCESS, + /// Generic failure condition + FAIL, + /// Underlying system is busy; retry + EBUSY, + /// The component is powered down + EOFF, + /// An invalid parameter was passed + EINVAL, + /// Operation canceled by a call + ECANCEL, + /// Memory required not available + ENOMEM, + /// Operation or command is unsupported + ENOSUPPORT, + } + + impl From for ReturnCode { + fn from(original: isize) -> ReturnCode { + match original { + 0 => ReturnCode::SUCCESS, + -1 => ReturnCode::FAIL, + -2 => ReturnCode::EBUSY, + -4 => ReturnCode::EOFF, + -6 => ReturnCode::EINVAL, + -8 => ReturnCode::ECANCEL, + -9 => ReturnCode::ENOMEM, + _ => ReturnCode::ENOSUPPORT, + } + } + } + + /// Helper function to write on console the received packet. + fn print_rx_buffer(buf: &mut [u8]) { + if let Some((last, bytes)) = buf.split_last() { + let mut console = Console::new(); + write!(console, "RX:").unwrap(); + for byte in bytes { + write!(console, " {:02x?}", byte).unwrap(); + } + writeln!(console, " {:02x?}", last).unwrap(); + console.flush(); + } + } + + /// Function to identify the time elapsed for a transmission request. + fn bench_transmit( + console: &mut Console, + timer: &Timer, + title: &str, + mut buf: &mut [u8], + ) -> ReturnCode { + let amount = buf.len(); + let start = Timestamp::::from_clock_value(timer.get_current_clock().flex_unwrap()); + match NfcTag::transmit(&mut buf, amount) { + Ok(_) => (), + Err(TockError::Command(CommandError { + return_code: -8, /* ECANCEL: No Field*/ + .. + })) => return ReturnCode::ECANCEL, + Err(_) => writeln!(Console::new(), " -- tx error!").unwrap(), + } + let end = Timestamp::::from_clock_value(timer.get_current_clock().flex_unwrap()); + let elapsed = (end - start).ms(); + writeln!( + console, + "{}\n{:.2} ms elapsed for {} bytes ({:.2} kbit/s)", + title, + elapsed, + amount, + (amount as f64) / elapsed * 8. + ) + .unwrap(); + console.flush(); + ReturnCode::SUCCESS + } + + fn receive_packet(console: &mut Console, mut buf: &mut [u8; 256]) -> ReturnCode { + match NfcTag::receive(&mut buf) { + Ok(RecvOp { + recv_amount: amount, + .. + }) => { + if amount <= buf.len() { + print_rx_buffer(&mut buf[..amount]); + } + } + Err(TockError::Command(CommandError { return_code, .. })) => return return_code.into(), + Err(_) => { + writeln!(console, " -- RX Err").unwrap(); + return ReturnCode::ECANCEL; + } + } + ReturnCode::SUCCESS + } + + fn transmit_reply(mut console: &mut Console, timer: &Timer, buf: &[u8]) -> ReturnCode { + let mut return_code = ReturnCode::SUCCESS; + match buf[0] { + 0xe0 /* RATS */=> { + let mut answer_to_select = [0x05, 0x78, 0x80, 0xB1, 0x00]; + return_code = bench_transmit(&mut console, &timer, "TX: ATS", &mut answer_to_select); + } + 0xc2 /* DESELECT */ => { + // Ignore the request + let mut command_error = [0x6A, 0x81]; + return_code = bench_transmit(&mut console, &timer, "TX: DESELECT", &mut command_error); + } + 0x02 | 0x03 /* APDU Prefix */ => match buf[2] { + // If the received packet is applet selection command (FIDO 2) + 0xa4 /* SELECT */ => if buf[3] == 0x04 && buf[5] == 0x08 && buf[6] == 0xa0 { + // Vesion: "FIDO_2_0" + let mut reply = [buf[0], 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, 0x90, 0x00,]; + return_code = bench_transmit(&mut console, &timer, "TX: Version Str", &mut reply); + } else if (buf[6] == 0xd2 && buf[7] == 0x76) || (buf[6] == 0xe1 && (buf[7] == 0x03 || buf[7] == 0x04)){ + let mut reply = [buf[0], 0x90, 0x00]; + return_code = bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + } else /* Unknown file */ { + let mut reply = [buf[0], 0x6a, 0x82]; + return_code = bench_transmit(&mut console, &timer, "TX: 0x6A82", &mut reply); + } + 0xb0 /* READ */ => match buf[5] { + 0x02 => { + let mut reply = [buf[0], 0x12, 0x90, 0x00,]; + return_code = bench_transmit(&mut console, &timer, "TX: File Size", &mut reply); + } + 0x12 => { + let mut reply = [buf[0], 0xd1, 0x01, 0x0e, 0x55, 0x77, 0x77, 0x77, 0x2e, 0x6f, 0x70, 0x65, + 0x6e, 0x73, 0x6b, 0x2e, 0x64, 0x65, 0x76, 0x90, 0x00,]; + return_code = bench_transmit(&mut console, &timer, "TX: NDEF", &mut reply); + } + 0x0f => { + let mut reply = [buf[0], 0x00, 0x0f, 0x20, 0x00, 0x7f, 0x00, 0x7f, 0x04, 0x06, 0xe1, 0x04, + 0x00, 0x7f, 0x00, 0x00, 0x90, 0x00,]; + return_code = bench_transmit(&mut console, &timer, "TX: CC", &mut reply); + } + _ => { + let mut reply = [buf[0], 0x90, 0x00]; + return_code = bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + } + } + _ => { + let mut reply = [buf[0], 0x90, 0x00]; + return_code = bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + } + } + 0x26 | 0x52 | 0x50 /* REQA | WUPA | Halt */ => { + return ReturnCode::EOFF; + } + _ => (), + } + return_code + } + + pub fn nfc(mut console: &mut Console) { + // Setup the timer with a dummy callback (we only care about reading the current time, but the + // API forces us to set an alarm callback too). + let mut with_callback = timer::with_callback(|_, _| {}); + let timer = with_callback.init().flex_unwrap(); + + writeln!( + console, + "Clock frequency: {} Hz", + timer.clock_frequency().hz() + ) + .unwrap(); + + let mut state_change_counter = 0; + loop { + let mut rx_buf = [0; 256]; + match receive_packet(&mut console, &mut rx_buf) { + ReturnCode::EOFF => { + // Not configured + while !NfcTag::enable_emulation() {} + // Configure Type 4 tag + while !NfcTag::configure(4) {} + } + ReturnCode::ECANCEL /* field lost */ => { + NfcTag::disable_emulation(); + } + ReturnCode::EBUSY /* awaiting select*/ => (), + ReturnCode::ENOMEM => { + writeln!(console, " -- Amount more than buffer limit").unwrap() + } + ReturnCode::FAIL => writeln!(console, " -- Invalid CRC").unwrap(), + ReturnCode::EINVAL /* covered in driver interface */ => (), + ReturnCode::ENOSUPPORT => (), + ReturnCode::SUCCESS => { + // If the reader restarts the communication then disable the tag. + match transmit_reply(&mut console, &timer, &rx_buf) { + ReturnCode::ECANCEL | ReturnCode::EOFF => { + if NfcTag::disable_emulation() { + writeln!(console, " -- TAG DISABLED").unwrap(); + } + state_change_counter += 1; + } + _ => (), + } + } + } + if state_change_counter > 100 { + break; + } + } + } +} + +fn main() { + let mut console = Console::new(); + writeln!(console, "****************************************").unwrap(); + writeln!(console, "nfct_test application is installed").unwrap(); + example::nfc(&mut console); + writeln!(console, "****************************************").unwrap(); +} diff --git a/libraries/persistent_store/fuzz/.gitignore b/libraries/persistent_store/fuzz/.gitignore new file mode 100644 index 0000000..110126b --- /dev/null +++ b/libraries/persistent_store/fuzz/.gitignore @@ -0,0 +1,4 @@ +/Cargo.lock +/artifacts/ +/corpus/ +/target/ diff --git a/libraries/persistent_store/fuzz/Cargo.toml b/libraries/persistent_store/fuzz/Cargo.toml new file mode 100644 index 0000000..fdc4f89 --- /dev/null +++ b/libraries/persistent_store/fuzz/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "fuzz-store" +version = "0.0.0" +authors = ["Julien Cretin "] +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.3" +persistent_store = { path = "..", features = ["std"] } +strum = { version = "0.19", features = ["derive"] } + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "store" +path = "fuzz_targets/store.rs" diff --git a/libraries/persistent_store/fuzz/fuzz_targets/store.rs b/libraries/persistent_store/fuzz/fuzz_targets/store.rs new file mode 100644 index 0000000..1cff2a4 --- /dev/null +++ b/libraries/persistent_store/fuzz/fuzz_targets/store.rs @@ -0,0 +1,21 @@ +// Copyright 2019-2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // TODO(ia0): Call fuzzing when implemented. +}); diff --git a/libraries/persistent_store/fuzz/src/histogram.rs b/libraries/persistent_store/fuzz/src/histogram.rs new file mode 100644 index 0000000..8237055 --- /dev/null +++ b/libraries/persistent_store/fuzz/src/histogram.rs @@ -0,0 +1,92 @@ +// Copyright 2019-2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::num_bits; +use std::collections::HashMap; + +/// Histogram with logarithmic buckets. +/// +/// This is used to compute coverage statistics of the fuzzing runs of a corpus. This is not used +/// during actual fuzzing, only when replaying a corpus to compute statistics. +#[derive(Default)] +pub struct Histogram { + /// Maps each bucket to its count. + /// + /// Buckets are numbers sharing the same highest bit. The first buckets are: only 0, only 1, 2 + /// to 3, 4 to 7, 8 to 15. Buckets are identified by their lower-bound. + buckets: HashMap, +} + +impl Histogram { + /// Increases the count of the bucket of an item. + /// + /// The bucket of `item` is the highest power of two, lower or equal to `item`. If `item` is + /// zero, then its bucket is also zero. + pub fn add(&mut self, item: usize) { + *self.buckets.entry(get_bucket(item)).or_insert(0) += 1; + } + + /// Merges another histogram into this one. + pub fn merge(&mut self, other: &Histogram) { + for (&bucket, &count) in &other.buckets { + *self.buckets.entry(bucket).or_insert(0) += count; + } + } + + /// Returns the bit-width of one past the highest non-empty bucket. + /// + /// In other words, all non-empty buckets of the histogram have a bit-width smaller than the + /// returned width. + pub fn width_lim(&self) -> usize { + self.buckets.keys().max().map_or(0, |&x| num_bits(x) + 1) + } + + /// Returns the count of a bucket. + pub fn get(&self, bucket: usize) -> Option { + self.buckets.get(&bucket).cloned() + } + + /// Returns the total count. + pub fn count(&self) -> usize { + self.buckets.values().sum() + } +} + +/// Returns the bucket of an item. +fn get_bucket(item: usize) -> usize { + let bucket = bucket_from_width(num_bits(item)); + assert!(bucket <= item && (item == 0 || item / 2 < bucket)); + bucket +} + +/// Returns the bucket of an item given its bit-width. +pub fn bucket_from_width(width: usize) -> usize { + if width == 0 { + 0 + } else { + 1 << (width - 1) + } +} + +#[test] +fn get_bucket_ok() { + assert_eq!(get_bucket(0), 0); + assert_eq!(get_bucket(1), 1); + assert_eq!(get_bucket(2), 2); + assert_eq!(get_bucket(3), 2); + assert_eq!(get_bucket(4), 4); + assert_eq!(get_bucket(7), 4); + assert_eq!(get_bucket(8), 8); + assert_eq!(get_bucket(15), 8); +} diff --git a/libraries/persistent_store/fuzz/src/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs new file mode 100644 index 0000000..11645f3 --- /dev/null +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -0,0 +1,196 @@ +// Copyright 2019-2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Fuzzing library for the persistent store. +//! +//! The overall design principles are (in order of precedence): +//! - Determinism: fuzzing is a function from seeds (byte slices) to sequences of store +//! manipulations (things like creating a store, applying operations, interrupting operations, +//! interrupting reboots, checking invariant, etc). We can replay this function on the same input +//! to get the same sequence of manipulations (for the same fuzzing and store code). +//! - Coverage: fuzzing tries to coverage as much different behaviors as possible for small seeds. +//! Ideally, each seed bit would control a branch decision in the tree of execution paths. +//! - Surjectivity: all sequences of manipulations are reachable by fuzzing for some seed. The only +//! situation where coverage takes precedence over surjectivity is for the value of insert updates +//! where a pseudo-random generator is used to avoid wasting entropy. + +// TODO(ia0): Remove when used. +#![allow(dead_code)] + +mod histogram; +mod stats; + +pub use stats::{StatKey, Stats}; + +/// Bit-level entropy source based on a byte slice shared reference. +/// +/// This is used to convert the byte slice provided by the fuzzer into the entropy used by the +/// fuzzing code to generate a sequence of store manipulations, among other things. Entropy +/// operations use the shortest necessary sequence of bits from the byte slice, such that fuzzer +/// mutations of the byte slice have local impact or cascading effects towards future operations +/// only. +/// +/// The entropy has the following properties (in order of precedence): +/// - It always returns a result. +/// - It is deterministic: for a given slice and a given sequence of operations, the same results +/// are returned. This permits to replay and debug fuzzing artifacts. +/// - It uses the slice as a bit stream. In particular, it doesn't do big number arithmetic. This +/// permits to have a simple implementation. +/// - It doesn't waste information: for a given operation, the minimum integer number of bits is +/// used to produce the result. As a consequence fractional bits can be wasted at each operation. +/// - It uses the information uniformly: each bit is used exactly once, except when only a fraction +/// of it is used. In particular, a bit is not used more than once. A consequence of each bit +/// being used essentially once, is that the results are mostly uniformly distributed. +/// +/// # Invariant +/// +/// - The bit is a valid position in the slice, or one past: `bit <= 8 * data.len()`. +struct Entropy<'a> { + /// The byte slice shared reference providing the entropy. + data: &'a [u8], + + /// The bit position in the byte slice of the next entropy bit. + bit: usize, +} + +impl Entropy<'_> { + /// Creates a bit-level entropy given a byte slice. + fn new(data: &[u8]) -> Entropy { + let bit = 0; + Entropy { data, bit } + } + + /// Consumes the remaining entropy. + fn consume_all(&mut self) { + self.bit = 8 * self.data.len(); + } + + /// Returns whether there is entropy remaining. + fn is_empty(&self) -> bool { + assert!(self.bit <= 8 * self.data.len()); + self.bit == 8 * self.data.len() + } + + /// Reads a bit. + fn read_bit(&mut self) -> bool { + if self.is_empty() { + return false; + } + let b = self.bit; + self.bit += 1; + self.data[b / 8] & 1 << (b % 8) != 0 + } + + /// Reads a number with a given bit-width. + /// + /// # Preconditions + /// + /// - The number should fit in the return type: `n <= 8 * size_of::()`. + fn read_bits(&mut self, n: usize) -> usize { + assert!(n <= 8 * std::mem::size_of::()); + let mut r = 0; + for i in 0..n { + r |= (self.read_bit() as usize) << i; + } + r + } + + /// Reads a byte. + fn read_byte(&mut self) -> u8 { + self.read_bits(8) as u8 + } + + /// Reads a slice. + fn read_slice(&mut self, length: usize) -> Vec { + let mut result = Vec::with_capacity(length); + for _ in 0..length { + result.push(self.read_byte()); + } + result + } + + /// Reads a number between `min` and `max` (inclusive bounds). + /// + /// The distribution is uniform if the range width is a power of two. Otherwise, the minimum + /// amount of entropy is used (the next power of two) and the distribution is the closest to + /// uniform for that entropy. + /// + /// # Preconditions + /// + /// - The bounds should be correctly ordered: `min <= max`. + /// - The upper-bound should not be too large: `max < usize::max_value()`. + fn read_range(&mut self, min: usize, max: usize) -> usize { + assert!(min <= max && max < usize::max_value()); + let count = max - min + 1; + let delta = self.read_bits(num_bits(count - 1)) % count; + min + delta + } +} + +/// Returns the number of bits necessary to represent a number. +fn num_bits(x: usize) -> usize { + 8 * std::mem::size_of::() - x.leading_zeros() as usize +} + +#[test] +fn num_bits_ok() { + assert_eq!(num_bits(0), 0); + assert_eq!(num_bits(1), 1); + assert_eq!(num_bits(2), 2); + assert_eq!(num_bits(3), 2); + assert_eq!(num_bits(4), 3); + assert_eq!(num_bits(7), 3); + assert_eq!(num_bits(8), 4); + assert_eq!(num_bits(15), 4); + assert_eq!(num_bits(16), 5); + assert_eq!( + num_bits(usize::max_value()), + 8 * std::mem::size_of::() + ); +} + +#[test] +fn read_bit_ok() { + let mut entropy = Entropy::new(&[0b10110010]); + assert!(!entropy.read_bit()); + assert!(entropy.read_bit()); + assert!(!entropy.read_bit()); + assert!(!entropy.read_bit()); + assert!(entropy.read_bit()); + assert!(entropy.read_bit()); + assert!(!entropy.read_bit()); + assert!(entropy.read_bit()); +} + +#[test] +fn read_bits_ok() { + let mut entropy = Entropy::new(&[0x83, 0x92]); + assert_eq!(entropy.read_bits(4), 0x3); + assert_eq!(entropy.read_bits(8), 0x28); + assert_eq!(entropy.read_bits(2), 0b01); + assert_eq!(entropy.read_bits(2), 0b10); +} + +#[test] +fn read_range_ok() { + let mut entropy = Entropy::new(&[0b00101011]); + assert_eq!(entropy.read_range(0, 7), 0b011); + assert_eq!(entropy.read_range(1, 8), 1 + 0b101); + assert_eq!(entropy.read_range(4, 6), 4 + 0b00); + let mut entropy = Entropy::new(&[0b00101011]); + assert_eq!(entropy.read_range(0, 8), 0b1011 % 9); + assert_eq!(entropy.read_range(3, 15), 3 + 0b0010); + let mut entropy = Entropy::new(&[0x12, 0x34, 0x56, 0x78]); + assert_eq!(entropy.read_range(0, usize::max_value() - 1), 0x78563412); +} diff --git a/libraries/persistent_store/fuzz/src/stats.rs b/libraries/persistent_store/fuzz/src/stats.rs new file mode 100644 index 0000000..7bef07e --- /dev/null +++ b/libraries/persistent_store/fuzz/src/stats.rs @@ -0,0 +1,187 @@ +// Copyright 2019-2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Helpers to compute and display fuzzing coverage statistics. +//! +//! This is not used during actual fuzzing, only when replaying a corpus to compute statistics. + +use crate::histogram::{bucket_from_width, Histogram}; +use std::collections::HashMap; +use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; + +/// Statistics for each fuzzing run. +#[derive(Copy, Clone, PartialEq, Eq, Hash, EnumIter, EnumString, Display)] +pub enum StatKey { + /// The available entropy in bytes. + Entropy, + + /// The size of a page in bytes. + PageSize, + + /// The number of pages. + NumPages, + + /// The maximum number times a page can be erased. + MaxPageErases, + + /// The dirty length of the initial storage in bytes. + /// + /// This is the length of the prefix of the storage that is written using entropy before the + /// store is initialized. This permits to check the store against an invalid storage: it should + /// not crash but may misbehave. + DirtyLength, + + /// The number of used erase cycles of the initial storage. + /// + /// This permits to check the store as if it already consumed lifetime. In particular it permits + /// to check the store when lifetime is almost out. + InitCycles, + + /// The number of words written during fuzzing. + /// + /// This permits to get an idea of how much lifetime was exercised during fuzzing. + UsedLifetime, + + /// Whether the store reached the end of the lifetime during fuzzing. + FinishedLifetime, + + /// The number of times the store was fully compacted. + /// + /// The store is considered fully compacted when all pages have been compacted once. So each + /// page has been compacted at least that number of times. + NumCompactions, + + /// The number of times the store was powered on. + PowerOnCount, + + /// The number of times a transaction was applied. + TransactionCount, + + /// The number of times a clear operation was applied. + ClearCount, + + /// The number of times a prepare operation was applied. + PrepareCount, + + /// The number of times an insert update was applied. + InsertCount, + + /// The number of times a remove update was applied. + RemoveCount, + + /// The number of times a store operation was interrupted. + InterruptionCount, +} + +/// Statistics about multiple fuzzing runs. +#[derive(Default)] +pub struct Stats { + /// Maps each statistics to its histogram. + stats: HashMap, +} + +impl Stats { + /// Adds a measure for a statistics. + pub fn add(&mut self, key: StatKey, value: usize) { + self.stats.entry(key).or_default().add(value); + } + + /// Merges another statistics into this one. + pub fn merge(&mut self, other: &Stats) { + for (&key, other) in &other.stats { + self.stats.entry(key).or_default().merge(other); + } + } + + /// Returns the count of a bucket for a given key. + pub fn get_count(&self, key: StatKey, bucket: usize) -> Option { + self.stats.get(&key).and_then(|h| h.get(bucket)) + } + + /// Returns the bit-width of one past the highest non-empty bucket. + /// + /// In other words, all non-empty buckets of the histogram have a bit-width smaller than the + /// returned width. + fn width_lim(&self) -> usize { + self.stats + .values() + .map(|h| h.width_lim()) + .max() + .unwrap_or(0) + } +} + +impl std::fmt::Display for Stats { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + let mut matrix: Vec> = Vec::new(); + let bits = self.width_lim(); + + let mut header = Vec::new(); + header.push(String::new()); + for width in 0..bits { + header.push(format!(" {}", bucket_from_width(width))); + } + header.push(" count".into()); + matrix.push(header); + + for key in StatKey::iter() { + let mut row = Vec::new(); + row.push(format!("{}:", key)); + for width in 0..bits { + row.push(match self.get_count(key, bucket_from_width(width)) { + None => String::new(), + Some(x) => format!(" {}", x), + }); + } + let count = self.stats.get(&key).map_or(0, |h| h.count()); + row.push(format!(" {}", count)); + matrix.push(row); + } + + write_matrix(f, matrix) + } +} + +/// Prints a string aligned to the right for a given width. +fn align(f: &mut std::fmt::Formatter, x: &str, n: usize) -> Result<(), std::fmt::Error> { + for _ in 0..n.saturating_sub(x.len()) { + write!(f, " ")?; + } + write!(f, "{}", x) +} + +/// Prints a matrix with columns of minimal width to fit all elements. +fn write_matrix( + f: &mut std::fmt::Formatter, + mut m: Vec>, +) -> Result<(), std::fmt::Error> { + if m.is_empty() { + return Ok(()); + } + let num_cols = m.iter().map(|r| r.len()).max().unwrap(); + let mut col_len = vec![0; num_cols]; + for row in &mut m { + row.resize(num_cols, String::new()); + for col in 0..num_cols { + col_len[col] = std::cmp::max(col_len[col], row[col].len()); + } + } + for row in m { + for col in 0..num_cols { + align(f, &row[col], col_len[col])?; + } + writeln!(f)?; + } + Ok(()) +} diff --git a/libraries/persistent_store/src/driver.rs b/libraries/persistent_store/src/driver.rs index 15001cb..e529274 100644 --- a/libraries/persistent_store/src/driver.rs +++ b/libraries/persistent_store/src/driver.rs @@ -181,6 +181,12 @@ pub enum StoreInvariant { }, } +impl From for StoreInvariant { + fn from(error: StoreError) -> StoreInvariant { + StoreInvariant::StoreError(error) + } +} + impl StoreDriver { /// Provides read-only access to the storage. pub fn storage(&self) -> &BufferStorage { @@ -249,6 +255,10 @@ impl StoreDriverOff { } /// Powers on the store without interruption. + /// + /// # Panics + /// + /// Panics if the store cannot be powered on. pub fn power_on(self) -> Result { Ok(self .partial_power_on(StoreInterruption::none()) @@ -506,8 +516,8 @@ impl StoreDriverOn { /// Checks that the store and model are in sync. fn check_model(&self) -> Result<(), StoreInvariant> { let mut model_content = self.model.content().clone(); - for handle in self.store.iter().unwrap() { - let handle = handle.unwrap(); + for handle in self.store.iter()? { + let handle = handle?; let model_value = match model_content.remove(&handle.get_key()) { None => { return Err(StoreInvariant::OnlyInStore { @@ -516,7 +526,7 @@ impl StoreDriverOn { } Some(x) => x, }; - let store_value = handle.get_value(&self.store).unwrap().into_boxed_slice(); + let store_value = handle.get_value(&self.store)?.into_boxed_slice(); if store_value != model_value { return Err(StoreInvariant::DifferentValue { key: handle.get_key(), @@ -528,7 +538,7 @@ impl StoreDriverOn { if let Some(&key) = model_content.keys().next() { return Err(StoreInvariant::OnlyInModel { key }); } - let store_capacity = self.store.capacity().unwrap().remaining(); + let store_capacity = self.store.capacity()?.remaining(); let model_capacity = self.model.capacity().remaining(); if store_capacity != model_capacity { return Err(StoreInvariant::DifferentCapacity { @@ -544,8 +554,8 @@ impl StoreDriverOn { let format = self.model.format(); let storage = self.store.storage(); let num_words = format.page_size() / format.word_size(); - let head = self.store.head().unwrap(); - let tail = self.store.tail().unwrap(); + let head = self.store.head()?; + let tail = self.store.tail()?; for page in 0..format.num_pages() { // Check the erase cycle of the page. let store_erase = head.cycle(format) + (page < head.page(format)) as Nat; diff --git a/metadata/metadata.json b/metadata/metadata.json new file mode 100644 index 0000000..eedeed9 --- /dev/null +++ b/metadata/metadata.json @@ -0,0 +1,46 @@ +{ + "assertionScheme": "FIDOV2", + "keyProtection": 1, + "attestationRootCertificates": [], + "aaguid": "664d9f67-84a2-412a-9ff7-b4f7d8ee6d05", + "publicKeyAlgAndEncoding": 260, + "protocolFamily": "fido2", + "upv": [ + { + "major": 1, + "minor": 0 + } + ], + "icon": "data:image/png;base64,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", + "matcherProtection": 1, + "supportedExtensions": [ + { + "id": "hmac-secret", + "fail_if_unknown": false + }, + { + "id": "credProtect", + "fail_if_unknown": false + } + ], + "cryptoStrength": 128, + "description": "OpenSK authenticator", + "authenticatorVersion": 1, + "isSecondFactorOnly": false, + "userVerificationDetails": [ + [ + { + "userVerification": 1 + }, + { + "userVerification": 4 + } + ] + ], + "attachmentHint": 6, + "attestationTypes": [ + 15880 + ], + "authenticationAlgorithm": 1, + "tcDisplay": 0 +} diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index b22e1d8..7f3b8f3 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -32,6 +32,7 @@ cd ../.. echo "Running Clippy lints..." cargo clippy --all-targets --features std -- -A clippy::new_without_default -D warnings +cargo clippy --all-targets --features std,with_nfc -- -A clippy::new_without_default -D warnings echo "Building sha256sum tool..." cargo build --manifest-path third_party/tock/tools/sha256sum/Cargo.toml @@ -54,12 +55,16 @@ cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ct echo "Checking that examples build properly..." cargo check --release --target=thumbv7em-none-eabi --examples +cargo check --release --target=thumbv7em-none-eabi --examples --features with_nfc echo "Checking that fuzz targets build properly..." cargo fuzz build cd libraries/cbor cargo fuzz build cd ../.. +cd libraries/persistent_store +cargo fuzz build +cd ../.. echo "Checking that CTAP2 builds and links properly (1 set of features)..." cargo build --release --target=thumbv7em-none-eabi --features with_ctap1 diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 84c6fb0..a5a7921 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -288,7 +288,7 @@ impl Ctap1Command { let sk = crypto::ecdsa::SecKey::gensk(ctap_state.rng); let pk = sk.genpk(); let key_handle = ctap_state - .encrypt_key_handle(sk, &application) + .encrypt_key_handle(sk, &application, None) .map_err(|_| Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. @@ -373,7 +373,7 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::{ENCRYPTED_CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER}; + use super::super::{CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; use super::*; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -413,12 +413,12 @@ mod test { 0x00, 0x00, 0x00, - 65 + ENCRYPTED_CREDENTIAL_ID_SIZE as u8, + 65 + CREDENTIAL_ID_BASE_SIZE as u8, ]; let challenge = [0x0C; 32]; message.extend(&challenge); message.extend(application); - message.push(ENCRYPTED_CREDENTIAL_ID_SIZE as u8); + message.push(CREDENTIAL_ID_BASE_SIZE as u8); message.extend(key_handle); message } @@ -437,15 +437,15 @@ mod test { Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); assert_eq!(response[0], Ctap1Command::LEGACY_BYTE); - assert_eq!(response[66], ENCRYPTED_CREDENTIAL_ID_SIZE as u8); + assert_eq!(response[66], CREDENTIAL_ID_BASE_SIZE as u8); assert!(ctap_state .decrypt_credential_source( - response[67..67 + ENCRYPTED_CREDENTIAL_ID_SIZE].to_vec(), + response[67..67 + CREDENTIAL_ID_BASE_SIZE].to_vec(), &application ) .unwrap() .is_some()); - const CERT_START: usize = 67 + ENCRYPTED_CREDENTIAL_ID_SIZE; + const CERT_START: usize = 67 + CREDENTIAL_ID_BASE_SIZE; assert_eq!( &response[CERT_START..CERT_START + ATTESTATION_CERTIFICATE.len()], &ATTESTATION_CERTIFICATE[..] @@ -494,7 +494,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); @@ -510,7 +512,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let application = [0x55; 32]; let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -527,7 +531,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -551,7 +557,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[0] = 0xEE; @@ -569,7 +577,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[1] = 0xEE; @@ -587,7 +597,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[2] = 0xEE; @@ -605,7 +617,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -630,7 +644,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, @@ -650,7 +666,7 @@ mod test { #[test] fn test_process_authenticate_bad_key_handle() { let application = [0x0A; 32]; - let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -667,7 +683,7 @@ mod test { #[test] fn test_process_authenticate_without_up() { let application = [0x0A; 32]; - let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index dacafe5..45ebf9f 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -361,10 +361,8 @@ impl TryFrom for MakeCredentialOptions { Some(options_entry) => extract_bool(options_entry)?, None => false, }; - if let Some(options_entry) = up { - if !extract_bool(options_entry)? { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); - } + if up.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } let uv = match uv { Some(options_entry) => extract_bool(options_entry)?, diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 7489f6e..a6a18f7 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -227,44 +227,35 @@ impl CtapHid { } // CTAP specification (version 20190130) section 8.1.9.1.3 CtapHid::COMMAND_INIT => { - if cid == CtapHid::CHANNEL_BROADCAST { - if message.payload.len() != 8 { - return CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN); - } + if message.payload.len() != 8 { + return CtapHid::error_message(cid, CtapHid::ERR_INVALID_LEN); + } + let new_cid = if cid == CtapHid::CHANNEL_BROADCAST { // TODO: Prevent allocating 2^32 channels. self.allocated_cids += 1; - let allocated_cid = (self.allocated_cids as u32).to_ne_bytes(); - - let mut payload = vec![0; 17]; - payload[..8].copy_from_slice(&message.payload); - payload[8..12].copy_from_slice(&allocated_cid); - payload[12] = CtapHid::PROTOCOL_VERSION; - payload[13] = CtapHid::DEVICE_VERSION_MAJOR; - payload[14] = CtapHid::DEVICE_VERSION_MINOR; - payload[15] = CtapHid::DEVICE_VERSION_BUILD; - payload[16] = CtapHid::CAPABILITIES; - - // This unwrap is safe because the payload length is 17 <= 7609 bytes. - CtapHid::split_message(Message { - cid, - cmd: CtapHid::COMMAND_INIT, - payload, - }) - .unwrap() + (self.allocated_cids as u32).to_ne_bytes() } else { // Sync the channel and discard the current transaction. - // TODO: The specification (version 20190130) wording isn't clear about - // the payload format in this case. - // - // This unwrap is safe because the payload length is 0 <= 7609 bytes. - CtapHid::split_message(Message { - cid, - cmd: CtapHid::COMMAND_INIT, - payload: vec![], - }) - .unwrap() - } + cid + }; + + let mut payload = vec![0; 17]; + payload[..8].copy_from_slice(&message.payload); + payload[8..12].copy_from_slice(&new_cid); + payload[12] = CtapHid::PROTOCOL_VERSION; + payload[13] = CtapHid::DEVICE_VERSION_MAJOR; + payload[14] = CtapHid::DEVICE_VERSION_MINOR; + payload[15] = CtapHid::DEVICE_VERSION_BUILD; + payload[16] = CtapHid::CAPABILITIES; + + // This unwrap is safe because the payload length is 17 <= 7609 bytes. + CtapHid::split_message(Message { + cid, + cmd: CtapHid::COMMAND_INIT, + payload, + }) + .unwrap() } // CTAP specification (version 20190130) section 8.1.9.1.4 CtapHid::COMMAND_PING => { @@ -307,7 +298,9 @@ impl CtapHid { HidPacketIterator::none() } Err((cid, error)) => { - if !self.is_allocated_channel(cid) { + if !self.is_allocated_channel(cid) + && error != receive::Error::UnexpectedContinuation + { CtapHid::error_message(cid, CtapHid::ERR_INVALID_CHANNEL) } else { match error { @@ -523,6 +516,27 @@ mod test { } } + #[test] + fn test_spurious_continuation_packet() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_hid = CtapHid::new(); + + let mut packet = [0x00; 64]; + packet[0..7].copy_from_slice(&[0xC1, 0xC1, 0xC1, 0xC1, 0x00, 0x51, 0x51]); + let mut assembler_reply = MessageAssembler::new(); + for pkt_reply in ctap_hid.process_hid_packet(&packet, DUMMY_CLOCK_VALUE, &mut ctap_state) { + // Continuation packets are silently ignored. + assert_eq!( + assembler_reply + .parse_packet(&pkt_reply, DUMMY_TIMESTAMP) + .unwrap(), + None + ); + } + } + #[test] fn test_command_init() { let mut rng = ThreadRng256 {}; @@ -568,6 +582,66 @@ mod test { ); } + #[test] + fn test_command_init_for_sync() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_hid = CtapHid::new(); + let cid = cid_from_init(&mut ctap_hid, &mut ctap_state); + + // Ping packet with a length longer than one packet. + let mut packet1 = [0x51; 64]; + packet1[..4].copy_from_slice(&cid); + packet1[4..7].copy_from_slice(&[0x81, 0x02, 0x00]); + // Init packet on the same channel. + let mut packet2 = [0x00; 64]; + packet2[..4].copy_from_slice(&cid); + packet2[4..15].copy_from_slice(&[ + 0x86, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, + ]); + let mut result = Vec::new(); + let mut assembler_reply = MessageAssembler::new(); + for pkt_request in &[packet1, packet2] { + for pkt_reply in + ctap_hid.process_hid_packet(&pkt_request, DUMMY_CLOCK_VALUE, &mut ctap_state) + { + if let Some(message) = assembler_reply + .parse_packet(&pkt_reply, DUMMY_TIMESTAMP) + .unwrap() + { + result.push(message); + } + } + } + assert_eq!( + result, + vec![Message { + cid, + cmd: CtapHid::COMMAND_INIT, + payload: vec![ + 0x12, // Nonce + 0x34, + 0x56, + 0x78, + 0x9A, + 0xBC, + 0xDE, + 0xF0, + cid[0], // Allocated CID + cid[1], + cid[2], + cid[3], + 0x02, // Protocol version + 0x00, // Device version + 0x00, + 0x00, + CtapHid::CAPABILITIES + ] + }] + ); + } + #[test] fn test_command_ping() { let mut rng = ThreadRng256 {}; diff --git a/src/ctap/hid/receive.rs b/src/ctap/hid/receive.rs index fef51a4..b522837 100644 --- a/src/ctap/hid/receive.rs +++ b/src/ctap/hid/receive.rs @@ -586,5 +586,33 @@ mod test { ); } + #[test] + fn test_init_sync() { + let mut assembler = MessageAssembler::new(); + // Ping packet with a length longer than one packet. + assert_eq!( + assembler.parse_packet( + &byte_extend(&[0x12, 0x34, 0x56, 0x78, 0x81, 0x02, 0x00], 0x51), + DUMMY_TIMESTAMP + ), + Ok(None) + ); + // Init packet on the same channel. + assert_eq!( + assembler.parse_packet( + &zero_extend(&[ + 0x12, 0x34, 0x56, 0x78, 0x86, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, + 0xDE, 0xF0 + ]), + DUMMY_TIMESTAMP + ), + Ok(Some(Message { + cid: [0x12, 0x34, 0x56, 0x78], + cmd: 0x06, + payload: vec![0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0] + })) + ); + } + // TODO: more tests } diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 06e3a57..caea0a0 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -83,8 +83,10 @@ const USE_SIGNATURE_COUNTER: bool = true; // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, // - 32 byte relying party ID hashed with SHA256, +// - (optional) 32 byte for HMAC-secret, // - 32 byte HMAC-SHA256 over everything else. -pub const ENCRYPTED_CREDENTIAL_ID_SIZE: usize = 112; +pub const CREDENTIAL_ID_BASE_SIZE: usize = 112; +pub const CREDENTIAL_ID_MAX_SIZE: usize = CREDENTIAL_ID_BASE_SIZE + 32; // Set this bit when checking user presence. const UP_FLAG: u8 = 0x01; // Set this bit when checking user verification. @@ -195,6 +197,7 @@ where &mut self, private_key: crypto::ecdsa::SecKey, application: &[u8; 32], + cred_random: Option<&[u8; 32]>, ) -> Result, Ctap2StatusCode> { let master_keys = self.persistent_store.master_keys()?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); @@ -203,14 +206,19 @@ where let mut iv = [0; 16]; iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); - let mut blocks = [[0u8; 16]; 4]; + let block_len = if cred_random.is_some() { 6 } else { 4 }; + let mut blocks = vec![[0u8; 16]; block_len]; blocks[0].copy_from_slice(&sk_bytes[..16]); blocks[1].copy_from_slice(&sk_bytes[16..]); blocks[2].copy_from_slice(&application[..16]); blocks[3].copy_from_slice(&application[16..]); + if let Some(cred_random) = cred_random { + blocks[4].copy_from_slice(&cred_random[..16]); + blocks[5].copy_from_slice(&cred_random[16..]); + } cbc_encrypt(&aes_enc_key, iv, &mut blocks); - let mut encrypted_id = Vec::with_capacity(ENCRYPTED_CREDENTIAL_ID_SIZE); + let mut encrypted_id = Vec::with_capacity(16 * (block_len + 3)); encrypted_id.extend(&iv); for b in &blocks { encrypted_id.extend(b); @@ -228,11 +236,13 @@ where credential_id: Vec, rp_id_hash: &[u8], ) -> Result, Ctap2StatusCode> { - if credential_id.len() != ENCRYPTED_CREDENTIAL_ID_SIZE { - return Ok(None); - } + let has_cred_random = match credential_id.len() { + CREDENTIAL_ID_BASE_SIZE => false, + CREDENTIAL_ID_MAX_SIZE => true, + _ => return Ok(None), + }; let master_keys = self.persistent_store.master_keys()?; - let payload_size = ENCRYPTED_CREDENTIAL_ID_SIZE - 32; + let payload_size = credential_id.len() - 32; if !verify_hmac_256::( &master_keys.hmac, &credential_id[..payload_size], @@ -244,8 +254,9 @@ where let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let mut iv = [0; 16]; iv.copy_from_slice(&credential_id[..16]); - let mut blocks = [[0u8; 16]; 4]; - for i in 0..4 { + let block_len = if has_cred_random { 6 } else { 4 }; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); } @@ -256,6 +267,14 @@ where decrypted_sk[16..].clone_from_slice(&blocks[1]); decrypted_rp_id_hash[..16].clone_from_slice(&blocks[2]); decrypted_rp_id_hash[16..].clone_from_slice(&blocks[3]); + let cred_random = if has_cred_random { + let mut decrypted_cred_random = [0; 32]; + decrypted_cred_random[..16].clone_from_slice(&blocks[4]); + decrypted_cred_random[16..].clone_from_slice(&blocks[5]); + Some(decrypted_cred_random.to_vec()) + } else { + None + }; if rp_id_hash != decrypted_rp_id_hash { return Ok(None); @@ -269,7 +288,7 @@ where rp_id: String::from(""), user_handle: vec![], other_ui: None, - cred_random: None, + cred_random, cred_protect_policy: None, })) } @@ -381,11 +400,7 @@ where }; let cred_random = if use_hmac_extension { - if !options.rk { - // The extension is actually supported, but we need resident keys. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - Some(self.rng.gen_uniform_u8x32().to_vec()) + Some(self.rng.gen_uniform_u8x32()) } else { None }; @@ -393,12 +408,16 @@ where let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); 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())? .is_some() + || self + .decrypt_credential_source(cred_desc.key_id, &rp_id_hash)? + .is_some() { // Perform this check, so bad actors can't brute force exclude_list // without user interaction. @@ -447,7 +466,6 @@ where let sk = crypto::ecdsa::SecKey::gensk(self.rng); let pk = sk.genpk(); - let rp_id_hash = Sha256::hash(rp_id.as_bytes()); let credential_id = if options.rk { let random_id = self.rng.gen_uniform_u8x32().to_vec(); let credential_source = PublicKeyCredentialSource { @@ -461,13 +479,13 @@ where other_ui: user .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), - cred_random, + cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, }; self.persistent_store.store_credential(credential_source)?; random_id } else { - self.encrypt_key_handle(sk.clone(), &rp_id_hash)? + self.encrypt_key_handle(sk.clone(), &rp_id_hash, cred_random.as_ref())? }; let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; @@ -727,10 +745,9 @@ where ]), #[cfg(feature = "with_ctap2_1")] max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // You can use ENCRYPTED_CREDENTIAL_ID_SIZE here, but if your - // browser passes that value, it might be used to fingerprint. + // #TODO(106) update with version 2.1 of HMAC-secret #[cfg(feature = "with_ctap2_1")] - max_credential_id_length: None, + max_credential_id_length: Some(CREDENTIAL_ID_BASE_SIZE as u64 + 32), #[cfg(feature = "with_ctap2_1")] transports: Some(vec![AuthenticatorTransport::Usb]), #[cfg(feature = "with_ctap2_1")] @@ -828,7 +845,7 @@ mod test { let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID); #[cfg(feature = "with_ctap2_1")] - let mut expected_response = vec![0x00, 0xA9, 0x01]; + let mut expected_response = vec![0x00, 0xAA, 0x01]; #[cfg(not(feature = "with_ctap2_1"))] let mut expected_response = vec![0x00, 0xA6, 0x01]; // The difference here is a longer array of supported versions. @@ -863,9 +880,9 @@ mod test { #[cfg(feature = "with_ctap2_1")] expected_response.extend( [ - 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, - 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, - 0x65, 0x79, 0x0D, 0x04, + 0x08, 0x18, 0x90, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, + 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, + 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, ] .iter(), ); @@ -992,7 +1009,7 @@ mod test { 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, ENCRYPTED_CREDENTIAL_ID_SIZE as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_BASE_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1113,6 +1130,56 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let extensions = Some(MakeCredentialExtensions { + hmac_secret: true, + cred_protect: None, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let AuthenticatorMakeCredentialResponse { + fmt, + auth_data, + att_stmt, + } = make_credential_response; + // The expected response is split to only assert the non-random parts. + assert_eq!(fmt, "packed"); + let mut expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, + ]; + expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_MAX_SIZE as u8]); + assert_eq!( + auth_data[0..expected_auth_data.len()], + expected_auth_data[..] + ); + let expected_extension_cbor = vec![ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, + 0xF5, + ]; + assert_eq!( + auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], + expected_extension_cbor[..] + ); + assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + } + _ => panic!("Invalid response type"), + } + } + + #[test] + fn test_process_make_credential_hmac_secret_resident_key() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, @@ -1219,6 +1286,70 @@ mod test { } } + #[test] + fn test_process_get_assertion_hmac_secret() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + let make_extensions = Some(MakeCredentialExtensions { + hmac_secret: true, + cred_protect: None, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + make_credential_params.extensions = make_extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert!(make_credential_response.is_ok()); + let credential_id = match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let auth_data = make_credential_response.auth_data; + let offset = 37 + ctap_state.persistent_store.aaguid().unwrap().len(); + assert_eq!(auth_data[offset], 0x00); + assert_eq!(auth_data[offset + 1] as usize, CREDENTIAL_ID_MAX_SIZE); + auth_data[offset + 2..offset + 2 + CREDENTIAL_ID_MAX_SIZE].to_vec() + } + _ => panic!("Invalid response type"), + }; + + let pk = sk.genpk(); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: CoseKey::from(pk), + salt_enc: vec![0x02; 32], + salt_auth: vec![0x03; 16], + }; + let get_extensions = Some(GetAssertionExtensions { + hmac_secret: Some(hmac_secret_input), + }); + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential_id, + transports: None, + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: Some(vec![cred_desc]), + extensions: get_extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = + ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + ); + } + #[test] fn test_residential_process_get_assertion_hmac_secret() { let mut rng = ThreadRng256 {}; @@ -1434,7 +1565,7 @@ mod test { // We are not testing the correctness of our SHA256 here, only if it is checked. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key.clone(), &rp_id_hash) + .encrypt_key_handle(private_key.clone(), &rp_id_hash, None) .unwrap(); let decrypted_source = ctap_state .decrypt_credential_source(encrypted_id, &rp_id_hash) @@ -1444,6 +1575,29 @@ mod test { assert_eq!(private_key, decrypted_source.private_key); } + #[test] + fn test_encrypt_decrypt_credential_with_cred_random() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + // Usually, the relying party ID or its hash is provided by the client. + // We are not testing the correctness of our SHA256 here, only if it is checked. + let rp_id_hash = [0x55; 32]; + let cred_random = [0xC9; 32]; + let encrypted_id = ctap_state + .encrypt_key_handle(private_key.clone(), &rp_id_hash, Some(&cred_random)) + .unwrap(); + let decrypted_source = ctap_state + .decrypt_credential_source(encrypted_id, &rp_id_hash) + .unwrap() + .unwrap(); + + assert_eq!(private_key, decrypted_source.private_key); + assert_eq!(Some(cred_random.to_vec()), decrypted_source.cred_random); + } + #[test] fn test_encrypt_decrypt_bad_hmac() { let mut rng = ThreadRng256 {}; @@ -1454,7 +1608,30 @@ mod test { // Same as above. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key, &rp_id_hash) + .encrypt_key_handle(private_key, &rp_id_hash, None) + .unwrap(); + for i in 0..encrypted_id.len() { + let mut modified_id = encrypted_id.clone(); + modified_id[i] ^= 0x01; + assert!(ctap_state + .decrypt_credential_source(modified_id, &rp_id_hash) + .unwrap() + .is_none()); + } + } + + #[test] + fn test_encrypt_decrypt_bad_hmac_with_cred_random() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + // Same as above. + let rp_id_hash = [0x55; 32]; + let cred_random = [0xC9; 32]; + let encrypted_id = ctap_state + .encrypt_key_handle(private_key, &rp_id_hash, Some(&cred_random)) .unwrap(); for i in 0..encrypted_id.len() { let mut modified_id = encrypted_id.clone(); diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index c03b81b..564ca6d 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -19,7 +19,6 @@ use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; #[cfg(feature = "with_ctap2_1")] use alloc::string::String; -#[cfg(feature = "with_ctap2_1")] use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; @@ -74,10 +73,9 @@ fn encrypt_hmac_secret_output( let mut cred_random_secret = [0u8; 32]; cred_random_secret.copy_from_slice(cred_random); - // Initialization of 4 blocks in any case makes this function more readable. - let mut blocks = [[0u8; 16]; 4]; // With the if clause restriction above, block_len can only be 2 or 4. let block_len = salt_enc.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; for i in 0..block_len { blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]); } @@ -85,8 +83,8 @@ fn encrypt_hmac_secret_output( let mut decrypted_salt1 = [0u8; 32]; decrypted_salt1[..16].copy_from_slice(&blocks[0]); - let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); decrypted_salt1[16..].copy_from_slice(&blocks[1]); + let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); for i in 0..2 { blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); } @@ -638,36 +636,52 @@ impl PinProtocolV1 { #[cfg(test)] mod test { use super::*; - use arrayref::array_refs; use crypto::rng256::ThreadRng256; // Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(persistent_store: &mut PersistentStore) { let mut pin = [0u8; 64]; - pin[0] = 0x31; - pin[1] = 0x32; - pin[2] = 0x33; - pin[3] = 0x34; + pin[..4].copy_from_slice(b"1234"); let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); persistent_store.set_pin_hash(&pin_hash).unwrap(); } + // Encrypts the message with a zero IV and key derived from shared_secret. + fn encrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { + assert!(message.len() % 16 == 0); + let block_len = message.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { + blocks[i][..].copy_from_slice(&message[i * 16..(i + 1) * 16]); + } + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let iv = [0u8; 16]; + cbc_encrypt(&aes_enc_key, iv, &mut blocks); + blocks.iter().flatten().cloned().collect::>() + } + + // Decrypts the message with a zero IV and key derived from shared_secret. + fn decrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { + assert!(message.len() % 16 == 0); + let block_len = message.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { + blocks[i][..].copy_from_slice(&message[i * 16..(i + 1) * 16]); + } + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + let iv = [0u8; 16]; + cbc_decrypt(&aes_dec_key, iv, &mut blocks); + blocks.iter().flatten().cloned().collect::>() + } + // Fails on PINs bigger than 64 bytes. fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec) -> Vec { assert!(pin.len() <= 64); let mut padded_pin = [0u8; 64]; padded_pin[..pin.len()].copy_from_slice(&pin[..]); - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let mut blocks = [[0u8; 16]; 4]; - let (b0, b1, b2, b3) = array_refs!(&padded_pin, 16, 16, 16, 16); - blocks[0][..].copy_from_slice(b0); - blocks[1][..].copy_from_slice(b1); - blocks[2][..].copy_from_slice(b2); - blocks[3][..].copy_from_slice(b3); - let iv = [0u8; 16]; - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - blocks.iter().flatten().cloned().collect::>() + encrypt_message(shared_secret, &padded_pin) } // Encrypts the dummy PIN "1234". @@ -677,22 +691,10 @@ mod test { // Encrypts the PIN hash corresponding to the dummy PIN "1234". fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); let mut pin = [0u8; 64]; - pin[0] = 0x31; - pin[1] = 0x32; - pin[2] = 0x33; - pin[3] = 0x34; + pin[..4].copy_from_slice(b"1234"); let pin_hash = Sha256::hash(&pin); - - let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash[..16]); - let iv = [0u8; 16]; - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - - let mut encrypted_pin_hash = Vec::with_capacity(16); - encrypted_pin_hash.extend(&blocks[0]); - encrypted_pin_hash + encrypt_message(shared_secret, &pin_hash[..16]) } #[test] @@ -1184,6 +1186,56 @@ mod test { output, Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) ); + + let mut salt_enc = [0x00; 32]; + let cred_random = [0xC9; 32]; + + // Test values to check for reproducibility. + let salt1 = [0x01; 32]; + let salt2 = [0x02; 32]; + let expected_output1 = hmac_256::(&cred_random, &salt1); + let expected_output2 = hmac_256::(&cred_random, &salt2); + + let salt_enc1 = encrypt_message(&shared_secret, &salt1); + salt_enc.copy_from_slice(salt_enc1.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec, &expected_output1); + + let salt_enc2 = &encrypt_message(&shared_secret, &salt2); + salt_enc.copy_from_slice(salt_enc2.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec, &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt12 = [0x00; 64]; + salt12[..32].copy_from_slice(&salt1); + salt12[32..].copy_from_slice(&salt2); + let salt_enc12 = encrypt_message(&shared_secret, &salt12); + salt_enc.copy_from_slice(salt_enc12.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[..32], &expected_output1); + assert_eq!(&output_dec[32..], &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt02 = [0x00; 64]; + salt02[32..].copy_from_slice(&salt2); + let salt_enc02 = encrypt_message(&shared_secret, &salt02); + salt_enc.copy_from_slice(salt_enc02.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[32..], &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt10 = [0x00; 64]; + salt10[..32].copy_from_slice(&salt1); + let salt_enc10 = encrypt_message(&shared_secret, &salt10); + salt_enc.copy_from_slice(salt_enc10.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[..32], &expected_output1); } #[cfg(feature = "with_ctap2_1")]