Merge branch 'master' into empty-auth-up

This commit is contained in:
kaczmarczyck
2020-11-24 17:42:05 +01:00
committed by GitHub
19 changed files with 1309 additions and 123 deletions

View File

@@ -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 ../..

View File

@@ -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

View File

@@ -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"])

249
examples/nfct_test.rs Normal file
View File

@@ -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<isize> 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::<f64>::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::<f64>::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();
}

View File

@@ -0,0 +1,4 @@
/Cargo.lock
/artifacts/
/corpus/
/target/

View File

@@ -0,0 +1,22 @@
[package]
name = "fuzz-store"
version = "0.0.0"
authors = ["Julien Cretin <cretin@google.com>"]
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"

View File

@@ -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.
});

View File

@@ -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<usize, usize>,
}
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<usize> {
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);
}

View File

@@ -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::<usize>()`.
fn read_bits(&mut self, n: usize) -> usize {
assert!(n <= 8 * std::mem::size_of::<usize>());
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<u8> {
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::<usize>() - 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::<usize>()
);
}
#[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);
}

View File

@@ -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<StatKey, Histogram>,
}
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<usize> {
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<String>> = 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<Vec<String>>,
) -> 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(())
}

View File

@@ -181,6 +181,12 @@ pub enum StoreInvariant {
},
}
impl From<StoreError> 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<StoreDriverOn, StoreInvariant> {
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;

46
metadata/metadata.json Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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);

View File

@@ -361,11 +361,9 @@ impl TryFrom<cbor::Value> for MakeCredentialOptions {
Some(options_entry) => extract_bool(options_entry)?,
None => false,
};
if let Some(options_entry) = up {
if !extract_bool(options_entry)? {
if up.is_some() {
return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION);
}
}
let uv = match uv {
Some(options_entry) => extract_bool(options_entry)?,
None => false,

View File

@@ -227,18 +227,22 @@ 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);
}
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();
(self.allocated_cids as u32).to_ne_bytes()
} else {
// Sync the channel and discard the current transaction.
cid
};
let mut payload = vec![0; 17];
payload[..8].copy_from_slice(&message.payload);
payload[8..12].copy_from_slice(&allocated_cid);
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;
@@ -252,19 +256,6 @@ impl CtapHid {
payload,
})
.unwrap()
} 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()
}
}
// 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 {};

View File

@@ -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
}

View File

@@ -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<Vec<u8>, 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<u8>,
rp_id_hash: &[u8],
) -> Result<Option<PublicKeyCredentialSource>, 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::<Sha256>(
&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();

View File

@@ -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::<Sha256>(&cred_random_secret, &decrypted_salt1[..]);
decrypted_salt1[16..].copy_from_slice(&blocks[1]);
let output1 = hmac_256::<Sha256>(&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<u8> {
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::<Vec<u8>>()
}
// Decrypts the message with a zero IV and key derived from shared_secret.
fn decrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec<u8> {
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::<Vec<u8>>()
}
// Fails on PINs bigger than 64 bytes.
fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec<u8>) -> Vec<u8> {
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::<Vec<u8>>()
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<u8> {
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::<Sha256>(&cred_random, &salt1);
let expected_output2 = hmac_256::<Sha256>(&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")]