From d2ef3853e5191eb6497872004c4e7b315fb15af5 Mon Sep 17 00:00:00 2001 From: Mirna <29131616+MirnaMuhammad98@users.noreply.github.com> Date: Fri, 16 Oct 2020 09:29:23 +0200 Subject: [PATCH 01/54] Delete nfc.rs This branch is for the example application. --- third_party/libtock-drivers/src/nfc.rs | 117 ------------------------- 1 file changed, 117 deletions(-) delete mode 100644 third_party/libtock-drivers/src/nfc.rs diff --git a/third_party/libtock-drivers/src/nfc.rs b/third_party/libtock-drivers/src/nfc.rs deleted file mode 100644 index 977b49b..0000000 --- a/third_party/libtock-drivers/src/nfc.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::util; -use core::cell::Cell; -use libtock_core::{callback, syscalls}; - -const DRIVER_NUMBER: usize = 0x30003; - -mod command_nr { - pub const TRANSMIT: usize = 1; - pub const RECEIVE: usize = 2; - pub const EMULATE: usize = 3; - pub const CONFIGURE: usize = 4; -} - -mod subscribe_nr { - pub const TRANSMIT: usize = 1; - pub const RECEIVE: usize = 2; - pub const SELECT: usize = 3; -} - -mod allow_nr { - pub const TRANSMIT: usize = 1; - pub const RECEIVE: usize = 2; -} - -pub fn enable_emulation() { - emulate(true); -} - -pub fn disable_emulation() { - emulate(false); -} - -pub fn emulate(enabled: bool) -> bool { - let result_code = syscalls::command(DRIVER_NUMBER, command_nr::EMULATE, enabled as usize, 0); - if result_code.is_err() { - return false; - } - - true -} - -pub fn selected() -> bool { - let is_selected = Cell::new(false); - let mut is_selected_alarm = || is_selected.set(true); - let subscription = syscalls::subscribe::( - DRIVER_NUMBER, - subscribe_nr::SELECT, - &mut is_selected_alarm, - ); - if subscription.is_err() { - return false; - } - - util::yieldk_for(|| is_selected.get()); - true -} - -pub fn configure(tag_type: u8) -> bool { - let result_code = syscalls::command(DRIVER_NUMBER, command_nr::CONFIGURE, tag_type as usize, 0); - if result_code.is_err() { - return false; - } - - true -} - -pub fn receive(buf: &mut [u8]) -> bool { - let result = syscalls::allow(DRIVER_NUMBER, allow_nr::RECEIVE, buf); - if result.is_err() { - return false; - } - - let done = Cell::new(false); - let mut alarm = || done.set(true); - let subscription = syscalls::subscribe::( - DRIVER_NUMBER, - subscribe_nr::RECEIVE, - &mut alarm, - ); - if subscription.is_err() { - return false; - } - - let result_code = syscalls::command(DRIVER_NUMBER, command_nr::RECEIVE, 0, 0); - if result_code.is_err() { - return false; - } - - util::yieldk_for(|| done.get()); - true -} - -pub fn transmit(buf: &mut [u8]) -> bool { - let result = syscalls::allow(DRIVER_NUMBER, allow_nr::TRANSMIT, buf); - if result.is_err() { - return false; - } - - let done = Cell::new(false); - let mut alarm = || done.set(true); - let subscription = syscalls::subscribe::( - DRIVER_NUMBER, - subscribe_nr::TRANSMIT, - &mut alarm, - ); - if subscription.is_err() { - return false; - } - - let result_code = syscalls::command(DRIVER_NUMBER, command_nr::TRANSMIT, 0, 0); - if result_code.is_err() { - return false; - } - - util::yieldk_for(|| done.get()); - true -} From c520095ab7c2ebaf6885a7e68ed63575b5317eaa Mon Sep 17 00:00:00 2001 From: Mirna <29131616+MirnaMuhammad98@users.noreply.github.com> Date: Fri, 16 Oct 2020 09:29:44 +0200 Subject: [PATCH 02/54] Update lib.rs This branch is for the example application. --- third_party/libtock-drivers/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/third_party/libtock-drivers/src/lib.rs b/third_party/libtock-drivers/src/lib.rs index ec8a2d7..bf62d05 100644 --- a/third_party/libtock-drivers/src/lib.rs +++ b/third_party/libtock-drivers/src/lib.rs @@ -3,7 +3,6 @@ pub mod buttons; pub mod console; pub mod led; -pub mod nfc; pub mod result; pub mod rng; pub mod timer; From 16870d52c55c3e55344c2d4e2300157209c9a74f Mon Sep 17 00:00:00 2001 From: Mirna Date: Thu, 22 Oct 2020 12:15:24 +0200 Subject: [PATCH 03/54] Update the application logic --- examples/nfct_test.rs | 131 ++++++++++++++++++++++++++++++------------ 1 file changed, 95 insertions(+), 36 deletions(-) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs index 42176cd..f7ae3a4 100644 --- a/examples/nfct_test.rs +++ b/examples/nfct_test.rs @@ -6,6 +6,7 @@ extern crate lang_items; use core::fmt::Write; use libtock_drivers::console::Console; use libtock_drivers::nfc::NfcTag; +use libtock_drivers::nfc::RecvOp; #[allow(dead_code)] /// Helper function to write a slice into a fixed @@ -16,52 +17,110 @@ fn write_tx_buffer(buf: &mut [u8], slice: &[u8]) { } } +#[derive(PartialEq, Eq)] +enum State { + Enabled, + Disabled, +} + fn main() { let mut console = Console::new(); writeln!(console, "****************************************").unwrap(); writeln!(console, "nfct_test application is installed").unwrap(); - // 1. Configure Type 4 tag - if NfcTag::configure(4) { - writeln!(console, " -- TAG CONFIGURED").unwrap(); - } - // 2. Subscribe to a SELECTED CALLBACK - if NfcTag::selected() { - writeln!(console, " -- TAG SELECTED").unwrap(); - // 0xfffff results in 1048575 / 13.56e6 = 77ms - NfcTag::set_framedelaymax(0xfffff); - } - /* - [_.] TODO: Enable Tag emulation (currently the tag is always activated) - needs field detection support in the driver level. - */ - let mut rx_buf = [0; 64]; - let mut unknown_cmd_cntr = 0; + let mut state = State::Disabled; + let mut state_change_cntr = 0; loop { - NfcTag::receive(&mut rx_buf); - match rx_buf[0] { - 0xe0 /* RATS */=> { - let mut answer_to_select = [0x05, 0x78, 0x80, 0xB1, 0x00]; - let amount = answer_to_select.len(); - NfcTag::transmit(&mut answer_to_select, amount); + match state { + State::Enabled => { + let mut rx_buf = [0; 256]; + loop { + match NfcTag::receive(&mut rx_buf) { + Ok(RecvOp { + recv_amount: amount, + .. + }) => match amount { + 1 => writeln!(console, " -- RX Packet: {:02x?}", rx_buf[0],).unwrap(), + 2 => writeln!( + console, + " -- RX Packet: {:02x?} {:02x?}", + rx_buf[0], rx_buf[1], + ) + .unwrap(), + 3 => writeln!( + console, + " -- RX Packet: {:02x?} {:02x?} {:02x?}", + rx_buf[0], rx_buf[1], rx_buf[2], + ) + .unwrap(), + _ => writeln!( + console, + " -- RX Packet: {:02x?} {:02x?} {:02x?} {:02x?}", + rx_buf[0], rx_buf[1], rx_buf[2], rx_buf[3], + ) + .unwrap(), + }, + Err(_) => writeln!(console, " -- rx error!").unwrap(), + } + + match rx_buf[0] { + 0xe0 /* RATS */=> { + let mut answer_to_select = [0x05, 0x78, 0x80, 0xB1, 0x00]; + let amount = answer_to_select.len(); + match NfcTag::transmit(&mut answer_to_select, amount) { + Ok(_) => (), + Err(_) => writeln!(console, " -- tx error!").unwrap(), + } + } + 0xc2 /* DESELECT */ => { + // Ignore the request + let mut command_error = [0x6A, 0x81]; + let amount = command_error.len(); + match NfcTag::transmit(&mut command_error, amount) { + Ok(_) => (), + Err(_) => writeln!(console, " -- tx error!").unwrap(), + } + } + 0x02 | 0x03 /* APDU Prefix */ => { + let mut reply = [rx_buf[0], 0x90, 0x00]; + let amount = reply.len(); + match NfcTag::transmit(&mut reply, amount) { + Ok(_) => (), + Err(_) => writeln!(console, " -- tx error!").unwrap(), + } + } + 0x52 | 0x50 /* WUPA | Halt */ => { + if NfcTag::disable_emulation() { + writeln!(console, " -- TAG DISABLED").unwrap(); + } + state = State::Disabled; + break; + } + _ => { + } + } + } } - 0xc2 /* DESELECT */ => { - // Ignore the request - let mut command_error = [0x6A, 0x81]; - let amount = command_error.len(); - NfcTag::transmit(&mut command_error, amount); - } - 0x02 | 0x03 /* APDU Prefix */ => { - let mut reply = [rx_buf[0], 0x90, 0x00]; - let amount = reply.len(); - NfcTag::transmit(&mut reply, amount); - } - _ => { - unknown_cmd_cntr += 1; + State::Disabled => { + if NfcTag::enable_emulation() { + writeln!(console, " -- TAG ENABLED").unwrap(); + } + // 1. Configure Type 4 tag + if NfcTag::configure(4) { + writeln!(console, " -- TAG CONFIGURED").unwrap(); + } + // 2. Subscribe to a SELECTED CALLBACK + if NfcTag::selected() { + writeln!(console, " -- TAG SELECTED").unwrap(); + // 0xfffff results in 1048575 / 13.56e6 = 77ms + NfcTag::set_framedelaymax(0xfffff); + } + state = State::Enabled; } } - if unknown_cmd_cntr > 50 { + state_change_cntr += 1; + if state_change_cntr > 10 && state == State::Disabled { break; } } From 69194f3960c6b44ef881fa301b11511725f9da0e Mon Sep 17 00:00:00 2001 From: Mirna Date: Thu, 29 Oct 2020 23:07:23 +0200 Subject: [PATCH 04/54] Updates in the application logic --- examples/nfct_test.rs | 102 ++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs index f7ae3a4..82a1625 100644 --- a/examples/nfct_test.rs +++ b/examples/nfct_test.rs @@ -1,23 +1,44 @@ #![no_std] +#![allow(unused_imports)] extern crate alloc; extern crate lang_items; +extern crate libtock_drivers; use core::fmt::Write; +use libtock_core::result::CommandError; use libtock_drivers::console::Console; +#[cfg(feature = "with_nfc")] use libtock_drivers::nfc::NfcTag; +#[cfg(feature = "with_nfc")] use libtock_drivers::nfc::RecvOp; +use libtock_drivers::result::TockError; #[allow(dead_code)] -/// Helper function to write a slice into a fixed -/// length transmission buffer. +/// Helper function to write a slice into a transmission buffer. fn write_tx_buffer(buf: &mut [u8], slice: &[u8]) { for (i, &byte) in slice.iter().enumerate() { buf[i] = byte; } } +#[allow(dead_code)] +/// Helper function to write on console the received packet. +fn print_rx_buffer(buf: &mut [u8], amount: usize) { + if amount < 1 || amount > buf.len() { + return; + } + let mut console = Console::new(); + write!(console, " -- RX Packet:").unwrap(); + for byte in buf.iter().take(amount - 1) { + write!(console, " {:02x?}", byte).unwrap(); + } + writeln!(console, " {:02x?}", buf[amount - 1]).unwrap(); +} + +#[cfg(feature = "with_nfc")] #[derive(PartialEq, Eq)] +/// enum for reserving the NFC tag state. enum State { Enabled, Disabled, @@ -29,8 +50,11 @@ fn main() { writeln!(console, "****************************************").unwrap(); writeln!(console, "nfct_test application is installed").unwrap(); + #[cfg(feature = "with_nfc")] let mut state = State::Disabled; + #[cfg(feature = "with_nfc")] let mut state_change_cntr = 0; + #[cfg(feature = "with_nfc")] loop { match state { State::Enabled => { @@ -40,28 +64,15 @@ fn main() { Ok(RecvOp { recv_amount: amount, .. - }) => match amount { - 1 => writeln!(console, " -- RX Packet: {:02x?}", rx_buf[0],).unwrap(), - 2 => writeln!( - console, - " -- RX Packet: {:02x?} {:02x?}", - rx_buf[0], rx_buf[1], - ) - .unwrap(), - 3 => writeln!( - console, - " -- RX Packet: {:02x?} {:02x?} {:02x?}", - rx_buf[0], rx_buf[1], rx_buf[2], - ) - .unwrap(), - _ => writeln!( - console, - " -- RX Packet: {:02x?} {:02x?} {:02x?} {:02x?}", - rx_buf[0], rx_buf[1], rx_buf[2], rx_buf[3], - ) - .unwrap(), - }, - Err(_) => writeln!(console, " -- rx error!").unwrap(), + }) => print_rx_buffer(&mut rx_buf, amount), + Err(TockError::Command(CommandError { + return_code: -4, /* EOFF: Not Ready */ + .. + })) => (), + Err(TockError::Command(CommandError { + return_code: value, .. + })) => writeln!(console, " -- Err({})!", value).unwrap(), + Err(_) => writeln!(console, " -- RX ERROR").unwrap(), } match rx_buf[0] { @@ -83,11 +94,24 @@ fn main() { } } 0x02 | 0x03 /* APDU Prefix */ => { - let mut reply = [rx_buf[0], 0x90, 0x00]; - let amount = reply.len(); - match NfcTag::transmit(&mut reply, amount) { - Ok(_) => (), - Err(_) => writeln!(console, " -- tx error!").unwrap(), + // If the received packet is applet selection command (FIDO 2) + if rx_buf[1] == 0x00 && rx_buf[2] == 0xa4 && rx_buf[3] == 0x04 { + // Vesion: "U2F_V2" + // let mut reply = [rx_buf[0], 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, 0x90, 0x00,]; + // Vesion: "FIDO_2_0" + let mut reply = [rx_buf[0], 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, 0x90, 0x00,]; + let amount = reply.len(); + match NfcTag::transmit(&mut reply, amount) { + Ok(_) => (), + Err(_) => writeln!(console, " -- tx error!").unwrap(), + } + } else { + let mut reply = [rx_buf[0], 0x90, 0x00]; + let amount = reply.len(); + match NfcTag::transmit(&mut reply, amount) { + Ok(_) => (), + Err(_) => writeln!(console, " -- tx error!").unwrap(), + } } } 0x52 | 0x50 /* WUPA | Halt */ => { @@ -97,30 +121,20 @@ fn main() { state = State::Disabled; break; } - _ => { - } + _ => (), } } } State::Disabled => { - if NfcTag::enable_emulation() { - writeln!(console, " -- TAG ENABLED").unwrap(); - } - // 1. Configure Type 4 tag + NfcTag::enable_emulation(); + // Configure Type 4 tag if NfcTag::configure(4) { - writeln!(console, " -- TAG CONFIGURED").unwrap(); + state = State::Enabled; } - // 2. Subscribe to a SELECTED CALLBACK - if NfcTag::selected() { - writeln!(console, " -- TAG SELECTED").unwrap(); - // 0xfffff results in 1048575 / 13.56e6 = 77ms - NfcTag::set_framedelaymax(0xfffff); - } - state = State::Enabled; } } state_change_cntr += 1; - if state_change_cntr > 10 && state == State::Disabled { + if state_change_cntr > 100 && state == State::Disabled { break; } } From dd814b8ded7cbe8fa2bf00d8e8d970974feb5cd4 Mon Sep 17 00:00:00 2001 From: Mirna Date: Mon, 2 Nov 2020 10:49:00 +0200 Subject: [PATCH 05/54] Remove duplicate code --- examples/nfct_test.rs | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs index 82a1625..90d2dfa 100644 --- a/examples/nfct_test.rs +++ b/examples/nfct_test.rs @@ -36,6 +36,16 @@ fn print_rx_buffer(buf: &mut [u8], amount: usize) { writeln!(console, " {:02x?}", buf[amount - 1]).unwrap(); } +#[allow(dead_code)] +/// Helper function to write on console the received packet. +fn transmit_slice(buf: &mut [u8] { + let amount = buf.len(); + match NfcTag::transmit(&mut buf, amount) { + Ok(_) => (), + Err(_) => writeln!(console, " -- tx error!").unwrap(), + } +} + #[cfg(feature = "with_nfc")] #[derive(PartialEq, Eq)] /// enum for reserving the NFC tag state. @@ -72,46 +82,30 @@ fn main() { Err(TockError::Command(CommandError { return_code: value, .. })) => writeln!(console, " -- Err({})!", value).unwrap(), - Err(_) => writeln!(console, " -- RX ERROR").unwrap(), + Err(_) => writeln!(console, " -- RX Err").unwrap(), } match rx_buf[0] { 0xe0 /* RATS */=> { let mut answer_to_select = [0x05, 0x78, 0x80, 0xB1, 0x00]; - let amount = answer_to_select.len(); - match NfcTag::transmit(&mut answer_to_select, amount) { - Ok(_) => (), - Err(_) => writeln!(console, " -- tx error!").unwrap(), - } + transmit_slice(&mut answer_to_select); } 0xc2 /* DESELECT */ => { // Ignore the request let mut command_error = [0x6A, 0x81]; - let amount = command_error.len(); - match NfcTag::transmit(&mut command_error, amount) { - Ok(_) => (), - Err(_) => writeln!(console, " -- tx error!").unwrap(), - } + transmit_slice(&mut command_error); } 0x02 | 0x03 /* APDU Prefix */ => { // If the received packet is applet selection command (FIDO 2) if rx_buf[1] == 0x00 && rx_buf[2] == 0xa4 && rx_buf[3] == 0x04 { - // Vesion: "U2F_V2" + /// Vesion: "U2F_V2" // let mut reply = [rx_buf[0], 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, 0x90, 0x00,]; - // Vesion: "FIDO_2_0" + /// Vesion: "FIDO_2_0" let mut reply = [rx_buf[0], 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, 0x90, 0x00,]; - let amount = reply.len(); - match NfcTag::transmit(&mut reply, amount) { - Ok(_) => (), - Err(_) => writeln!(console, " -- tx error!").unwrap(), - } + transmit_slice(&mut reply); } else { let mut reply = [rx_buf[0], 0x90, 0x00]; - let amount = reply.len(); - match NfcTag::transmit(&mut reply, amount) { - Ok(_) => (), - Err(_) => writeln!(console, " -- tx error!").unwrap(), - } + transmit_slice(&mut reply); } } 0x52 | 0x50 /* WUPA | Halt */ => { From f9705d7c26ac4dfe507a0f0703cfb083a8d7dd3a Mon Sep 17 00:00:00 2001 From: Mirna Date: Mon, 2 Nov 2020 11:13:58 +0200 Subject: [PATCH 06/54] example app --- deploy.py | 7 +++ examples/nfct_test.rs | 136 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 examples/nfct_test.rs 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..90d2dfa --- /dev/null +++ b/examples/nfct_test.rs @@ -0,0 +1,136 @@ +#![no_std] +#![allow(unused_imports)] + +extern crate alloc; +extern crate lang_items; +extern crate libtock_drivers; + +use core::fmt::Write; +use libtock_core::result::CommandError; +use libtock_drivers::console::Console; +#[cfg(feature = "with_nfc")] +use libtock_drivers::nfc::NfcTag; +#[cfg(feature = "with_nfc")] +use libtock_drivers::nfc::RecvOp; +use libtock_drivers::result::TockError; + +#[allow(dead_code)] +/// Helper function to write a slice into a transmission buffer. +fn write_tx_buffer(buf: &mut [u8], slice: &[u8]) { + for (i, &byte) in slice.iter().enumerate() { + buf[i] = byte; + } +} + +#[allow(dead_code)] +/// Helper function to write on console the received packet. +fn print_rx_buffer(buf: &mut [u8], amount: usize) { + if amount < 1 || amount > buf.len() { + return; + } + let mut console = Console::new(); + write!(console, " -- RX Packet:").unwrap(); + for byte in buf.iter().take(amount - 1) { + write!(console, " {:02x?}", byte).unwrap(); + } + writeln!(console, " {:02x?}", buf[amount - 1]).unwrap(); +} + +#[allow(dead_code)] +/// Helper function to write on console the received packet. +fn transmit_slice(buf: &mut [u8] { + let amount = buf.len(); + match NfcTag::transmit(&mut buf, amount) { + Ok(_) => (), + Err(_) => writeln!(console, " -- tx error!").unwrap(), + } +} + +#[cfg(feature = "with_nfc")] +#[derive(PartialEq, Eq)] +/// enum for reserving the NFC tag state. +enum State { + Enabled, + Disabled, +} + +fn main() { + let mut console = Console::new(); + + writeln!(console, "****************************************").unwrap(); + writeln!(console, "nfct_test application is installed").unwrap(); + + #[cfg(feature = "with_nfc")] + let mut state = State::Disabled; + #[cfg(feature = "with_nfc")] + let mut state_change_cntr = 0; + #[cfg(feature = "with_nfc")] + loop { + match state { + State::Enabled => { + let mut rx_buf = [0; 256]; + loop { + match NfcTag::receive(&mut rx_buf) { + Ok(RecvOp { + recv_amount: amount, + .. + }) => print_rx_buffer(&mut rx_buf, amount), + Err(TockError::Command(CommandError { + return_code: -4, /* EOFF: Not Ready */ + .. + })) => (), + Err(TockError::Command(CommandError { + return_code: value, .. + })) => writeln!(console, " -- Err({})!", value).unwrap(), + Err(_) => writeln!(console, " -- RX Err").unwrap(), + } + + match rx_buf[0] { + 0xe0 /* RATS */=> { + let mut answer_to_select = [0x05, 0x78, 0x80, 0xB1, 0x00]; + transmit_slice(&mut answer_to_select); + } + 0xc2 /* DESELECT */ => { + // Ignore the request + let mut command_error = [0x6A, 0x81]; + transmit_slice(&mut command_error); + } + 0x02 | 0x03 /* APDU Prefix */ => { + // If the received packet is applet selection command (FIDO 2) + if rx_buf[1] == 0x00 && rx_buf[2] == 0xa4 && rx_buf[3] == 0x04 { + /// Vesion: "U2F_V2" + // let mut reply = [rx_buf[0], 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, 0x90, 0x00,]; + /// Vesion: "FIDO_2_0" + let mut reply = [rx_buf[0], 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, 0x90, 0x00,]; + transmit_slice(&mut reply); + } else { + let mut reply = [rx_buf[0], 0x90, 0x00]; + transmit_slice(&mut reply); + } + } + 0x52 | 0x50 /* WUPA | Halt */ => { + if NfcTag::disable_emulation() { + writeln!(console, " -- TAG DISABLED").unwrap(); + } + state = State::Disabled; + break; + } + _ => (), + } + } + } + State::Disabled => { + NfcTag::enable_emulation(); + // Configure Type 4 tag + if NfcTag::configure(4) { + state = State::Enabled; + } + } + } + state_change_cntr += 1; + if state_change_cntr > 100 && state == State::Disabled { + break; + } + } + writeln!(console, "****************************************").unwrap(); +} From eb3187680701dba632759160407dbce3c55bc232 Mon Sep 17 00:00:00 2001 From: Mirna Date: Wed, 4 Nov 2020 17:48:50 +0200 Subject: [PATCH 07/54] Update to calculate elapsed time for transmission --- examples/nfct_test.rs | 94 +++++++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 30 deletions(-) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs index 90d2dfa..9da0312 100644 --- a/examples/nfct_test.rs +++ b/examples/nfct_test.rs @@ -1,5 +1,4 @@ #![no_std] -#![allow(unused_imports)] extern crate alloc; extern crate lang_items; @@ -8,19 +7,13 @@ extern crate libtock_drivers; use core::fmt::Write; use libtock_core::result::CommandError; use libtock_drivers::console::Console; -#[cfg(feature = "with_nfc")] use libtock_drivers::nfc::NfcTag; -#[cfg(feature = "with_nfc")] use libtock_drivers::nfc::RecvOp; +use libtock_drivers::result::FlexUnwrap; use libtock_drivers::result::TockError; - -#[allow(dead_code)] -/// Helper function to write a slice into a transmission buffer. -fn write_tx_buffer(buf: &mut [u8], slice: &[u8]) { - for (i, &byte) in slice.iter().enumerate() { - buf[i] = byte; - } -} +use libtock_drivers::timer; +use libtock_drivers::timer::Timer; +use libtock_drivers::timer::Timestamp; #[allow(dead_code)] /// Helper function to write on console the received packet. @@ -29,24 +22,37 @@ fn print_rx_buffer(buf: &mut [u8], amount: usize) { return; } let mut console = Console::new(); - write!(console, " -- RX Packet:").unwrap(); + write!(console, "RX:").unwrap(); for byte in buf.iter().take(amount - 1) { write!(console, " {:02x?}", byte).unwrap(); } writeln!(console, " {:02x?}", buf[amount - 1]).unwrap(); + console.flush(); } #[allow(dead_code)] -/// Helper function to write on console the received packet. -fn transmit_slice(buf: &mut [u8] { +/// Function to identify the time elapsed for a transmission request. +fn bench_transmit(console: &mut Console, timer: &Timer, title: &str, mut buf: &mut [u8]) { let amount = buf.len(); + let start = Timestamp::::from_clock_value(timer.get_current_clock().flex_unwrap()); match NfcTag::transmit(&mut buf, amount) { Ok(_) => (), - Err(_) => writeln!(console, " -- tx error!").unwrap(), + 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(); } -#[cfg(feature = "with_nfc")] #[derive(PartialEq, Eq)] /// enum for reserving the NFC tag state. enum State { @@ -56,15 +62,23 @@ enum State { fn main() { let mut console = Console::new(); + // 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, "****************************************").unwrap(); writeln!(console, "nfct_test application is installed").unwrap(); + writeln!( + console, + "Clock frequency: {} Hz", + timer.clock_frequency().hz() + ) + .unwrap(); - #[cfg(feature = "with_nfc")] let mut state = State::Disabled; - #[cfg(feature = "with_nfc")] + // Variable to count the change in the tag's state let mut state_change_cntr = 0; - #[cfg(feature = "with_nfc")] loop { match state { State::Enabled => { @@ -88,24 +102,44 @@ fn main() { match rx_buf[0] { 0xe0 /* RATS */=> { let mut answer_to_select = [0x05, 0x78, 0x80, 0xB1, 0x00]; - transmit_slice(&mut answer_to_select); + bench_transmit(&mut console, &timer, "TX: ATS", &mut answer_to_select); } 0xc2 /* DESELECT */ => { // Ignore the request let mut command_error = [0x6A, 0x81]; - transmit_slice(&mut command_error); + bench_transmit(&mut console, &timer, "TX: DESELECT", &mut command_error); } - 0x02 | 0x03 /* APDU Prefix */ => { + 0x02 | 0x03 /* APDU Prefix */ => match rx_buf[2] { // If the received packet is applet selection command (FIDO 2) - if rx_buf[1] == 0x00 && rx_buf[2] == 0xa4 && rx_buf[3] == 0x04 { - /// Vesion: "U2F_V2" - // let mut reply = [rx_buf[0], 0x55, 0x32, 0x46, 0x5f, 0x56, 0x32, 0x90, 0x00,]; - /// Vesion: "FIDO_2_0" - let mut reply = [rx_buf[0], 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, 0x90, 0x00,]; - transmit_slice(&mut reply); - } else { + 0xa4 /* SELECT */ => if rx_buf[3] == 0x04 && rx_buf[5] == 0x08 && rx_buf[6] == 0xa0 { + // Vesion: "FIDO_2_0" + let mut reply = [rx_buf[0], 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, 0x90, 0x00,]; + bench_transmit(&mut console, &timer, "TX: Version Str", &mut reply); + } else { + let mut reply = [rx_buf[0], 0x90, 0x00]; + bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + } + 0xb0 /* READ */ => match rx_buf[5] { + 0x02 => { + let mut reply = [rx_buf[0], 0x12, 0x90, 0x00,]; + bench_transmit(&mut console, &timer, "TX: File Size", &mut reply); + } + 0x12 => { + let mut reply = [rx_buf[0], 0xd1, 0x01, 0x0e, 0x55, 0x77, 0x77, 0x77, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x73, 0x6b, 0x2e, 0x64, 0x65, 0x76, 0x90, 0x00,]; + bench_transmit(&mut console, &timer, "TX: NDEF", &mut reply); + } + 0x0f => { + let mut reply = [rx_buf[0], 0x00, 0x0f, 0x20, 0x00, 0x7f, 0x00, 0x7f, 0x04, 0x06, 0xe1, 0x04, 0x00, 0x7f, 0x00, 0x00, 0x90, 0x00,]; + bench_transmit(&mut console, &timer, "TX: CC", &mut reply); + } + _ => { + let mut reply = [rx_buf[0], 0x90, 0x00]; + bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + } + } + _ => { let mut reply = [rx_buf[0], 0x90, 0x00]; - transmit_slice(&mut reply); + bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); } } 0x52 | 0x50 /* WUPA | Halt */ => { From aa47d1c278419cf329af99cffe5f65183b398bc7 Mon Sep 17 00:00:00 2001 From: Mirna Date: Wed, 4 Nov 2020 17:49:28 +0200 Subject: [PATCH 08/54] Supply NFC feature flag to desktop checks --- run_desktop_tests.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index b22e1d8..696455f 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -31,7 +31,7 @@ cargo fmt --all -- --check cd ../.. echo "Running Clippy lints..." -cargo clippy --all-targets --features std -- -A clippy::new_without_default -D warnings +cargo clippy --all-targets --features std --features with_nfc -- -A clippy::new_without_default -D warnings echo "Building sha256sum tool..." cargo build --manifest-path third_party/tock/tools/sha256sum/Cargo.toml @@ -53,7 +53,7 @@ cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ct cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ctap1,panic_console,debug_allocations,verbose 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 From c232f2344c6232554d2a346660be1338aad2aacc Mon Sep 17 00:00:00 2001 From: Mirna Date: Wed, 4 Nov 2020 20:05:32 +0200 Subject: [PATCH 09/54] Resolve comments --- examples/nfct_test.rs | 326 +++++++++++++++++++++++------------------- run_desktop_tests.sh | 4 +- 2 files changed, 179 insertions(+), 151 deletions(-) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs index 9da0312..2e0df8a 100644 --- a/examples/nfct_test.rs +++ b/examples/nfct_test.rs @@ -5,166 +5,194 @@ extern crate lang_items; extern crate libtock_drivers; use core::fmt::Write; -use libtock_core::result::CommandError; use libtock_drivers::console::Console; -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; -#[allow(dead_code)] -/// Helper function to write on console the received packet. -fn print_rx_buffer(buf: &mut [u8], amount: usize) { - if amount < 1 || amount > buf.len() { - return; +#[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(); } - let mut console = Console::new(); - write!(console, "RX:").unwrap(); - for byte in buf.iter().take(amount - 1) { - write!(console, " {:02x?}", byte).unwrap(); - } - writeln!(console, " {:02x?}", buf[amount - 1]).unwrap(); - console.flush(); } -#[allow(dead_code)] -/// Function to identify the time elapsed for a transmission request. -fn bench_transmit(console: &mut Console, timer: &Timer, title: &str, mut buf: &mut [u8]) { - let amount = buf.len(); - let start = Timestamp::::from_clock_value(timer.get_current_clock().flex_unwrap()); - match NfcTag::transmit(&mut buf, amount) { - Ok(_) => (), - 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(); -} +#[cfg(feature = "with_nfc")] +mod example { -#[derive(PartialEq, Eq)] -/// enum for reserving the NFC tag state. -enum State { - Enabled, - Disabled, + 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; + + /// Helper function to write on console the received packet. + fn print_rx_buffer(buf: &mut [u8]) { + let mut console = Console::new(); + write!(console, "RX:").unwrap(); + if let Some((last, bytes)) = buf.split_last() { + 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]) { + let amount = buf.len(); + let start = Timestamp::::from_clock_value(timer.get_current_clock().flex_unwrap()); + match NfcTag::transmit(&mut buf, amount) { + Ok(_) => (), + 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(); + } + + fn receive_packet(console: &mut Console, mut buf: &mut [u8; 256]) { + match NfcTag::receive(&mut buf) { + Ok(RecvOp { + recv_amount: amount, + .. + }) => { + if amount > 0 && amount <= buf.len() { + print_rx_buffer(&mut buf[..amount]); + } + } + Err(TockError::Command(CommandError { + return_code: -4, /* EOFF: Not Ready */ + .. + })) => (), + Err(TockError::Command(CommandError { + return_code: value, .. + })) => writeln!(console, " -- Err({})!", value).unwrap(), + Err(_) => writeln!(console, " -- RX Err").unwrap(), + } + } + + fn transmit_reply(mut console: &mut Console, timer: &Timer, buf: &[u8]) -> bool { + match buf[0] { + 0xe0 /* RATS */=> { + let mut answer_to_select = [0x05, 0x78, 0x80, 0xB1, 0x00]; + bench_transmit(&mut console, &timer, "TX: ATS", &mut answer_to_select); + } + 0xc2 /* DESELECT */ => { + // Ignore the request + let mut command_error = [0x6A, 0x81]; + 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,]; + bench_transmit(&mut console, &timer, "TX: Version Str", &mut reply); + } else { + let mut reply = [buf[0], 0x90, 0x00]; + bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + } + 0xb0 /* READ */ => match buf[5] { + 0x02 => { + let mut reply = [buf[0], 0x12, 0x90, 0x00,]; + 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,]; + 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,]; + bench_transmit(&mut console, &timer, "TX: CC", &mut reply); + } + _ => { + let mut reply = [buf[0], 0x90, 0x00]; + bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + } + } + _ => { + let mut reply = [buf[0], 0x90, 0x00]; + bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + } + } + 0x26 | 0x52 | 0x50 /* REQA | WUPA | Halt */ => { + if NfcTag::disable_emulation() { + writeln!(console, " -- TAG DISABLED").unwrap(); + } + return false; + } + _ => (), + } + true + } + + 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 is_enabled = false; + let mut state_change_counter = 0; + loop { + match is_enabled { + true => { + let mut rx_buf = [0; 256]; + loop { + receive_packet(&mut console, &mut rx_buf); + // If the reader restarts the communication and we can't + // reply to the received packet, then disable the tag. + if !transmit_reply(&mut console, &timer, &rx_buf) { + is_enabled = false; + break; + } + } + } + false => { + NfcTag::enable_emulation(); + // Configure Type 4 tag + if NfcTag::configure(4) { + is_enabled = true; + } + } + } + state_change_counter += 1; + if state_change_counter > 100 && is_enabled == false { + break; + } + } + } } fn main() { let mut console = Console::new(); - // 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, "****************************************").unwrap(); writeln!(console, "nfct_test application is installed").unwrap(); - writeln!( - console, - "Clock frequency: {} Hz", - timer.clock_frequency().hz() - ) - .unwrap(); - - let mut state = State::Disabled; - // Variable to count the change in the tag's state - let mut state_change_cntr = 0; - loop { - match state { - State::Enabled => { - let mut rx_buf = [0; 256]; - loop { - match NfcTag::receive(&mut rx_buf) { - Ok(RecvOp { - recv_amount: amount, - .. - }) => print_rx_buffer(&mut rx_buf, amount), - Err(TockError::Command(CommandError { - return_code: -4, /* EOFF: Not Ready */ - .. - })) => (), - Err(TockError::Command(CommandError { - return_code: value, .. - })) => writeln!(console, " -- Err({})!", value).unwrap(), - Err(_) => writeln!(console, " -- RX Err").unwrap(), - } - - match rx_buf[0] { - 0xe0 /* RATS */=> { - let mut answer_to_select = [0x05, 0x78, 0x80, 0xB1, 0x00]; - bench_transmit(&mut console, &timer, "TX: ATS", &mut answer_to_select); - } - 0xc2 /* DESELECT */ => { - // Ignore the request - let mut command_error = [0x6A, 0x81]; - bench_transmit(&mut console, &timer, "TX: DESELECT", &mut command_error); - } - 0x02 | 0x03 /* APDU Prefix */ => match rx_buf[2] { - // If the received packet is applet selection command (FIDO 2) - 0xa4 /* SELECT */ => if rx_buf[3] == 0x04 && rx_buf[5] == 0x08 && rx_buf[6] == 0xa0 { - // Vesion: "FIDO_2_0" - let mut reply = [rx_buf[0], 0x46, 0x49, 0x44, 0x4f, 0x5f, 0x32, 0x5f, 0x30, 0x90, 0x00,]; - bench_transmit(&mut console, &timer, "TX: Version Str", &mut reply); - } else { - let mut reply = [rx_buf[0], 0x90, 0x00]; - bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); - } - 0xb0 /* READ */ => match rx_buf[5] { - 0x02 => { - let mut reply = [rx_buf[0], 0x12, 0x90, 0x00,]; - bench_transmit(&mut console, &timer, "TX: File Size", &mut reply); - } - 0x12 => { - let mut reply = [rx_buf[0], 0xd1, 0x01, 0x0e, 0x55, 0x77, 0x77, 0x77, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x73, 0x6b, 0x2e, 0x64, 0x65, 0x76, 0x90, 0x00,]; - bench_transmit(&mut console, &timer, "TX: NDEF", &mut reply); - } - 0x0f => { - let mut reply = [rx_buf[0], 0x00, 0x0f, 0x20, 0x00, 0x7f, 0x00, 0x7f, 0x04, 0x06, 0xe1, 0x04, 0x00, 0x7f, 0x00, 0x00, 0x90, 0x00,]; - bench_transmit(&mut console, &timer, "TX: CC", &mut reply); - } - _ => { - let mut reply = [rx_buf[0], 0x90, 0x00]; - bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); - } - } - _ => { - let mut reply = [rx_buf[0], 0x90, 0x00]; - bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); - } - } - 0x52 | 0x50 /* WUPA | Halt */ => { - if NfcTag::disable_emulation() { - writeln!(console, " -- TAG DISABLED").unwrap(); - } - state = State::Disabled; - break; - } - _ => (), - } - } - } - State::Disabled => { - NfcTag::enable_emulation(); - // Configure Type 4 tag - if NfcTag::configure(4) { - state = State::Enabled; - } - } - } - state_change_cntr += 1; - if state_change_cntr > 100 && state == State::Disabled { - break; - } - } + example::nfc(&mut console); writeln!(console, "****************************************").unwrap(); } diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index 696455f..b22e1d8 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -31,7 +31,7 @@ cargo fmt --all -- --check cd ../.. echo "Running Clippy lints..." -cargo clippy --all-targets --features std --features with_nfc -- -A clippy::new_without_default -D warnings +cargo clippy --all-targets --features std -- -A clippy::new_without_default -D warnings echo "Building sha256sum tool..." cargo build --manifest-path third_party/tock/tools/sha256sum/Cargo.toml @@ -53,7 +53,7 @@ cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ct cargo check --release --target=thumbv7em-none-eabi --features debug_ctap,with_ctap1,panic_console,debug_allocations,verbose echo "Checking that examples build properly..." -cargo check --release --target=thumbv7em-none-eabi --examples --features with_nfc +cargo check --release --target=thumbv7em-none-eabi --examples echo "Checking that fuzz targets build properly..." cargo fuzz build From 0eaa2b529103d743fe62ba5e25c7d05a7008f044 Mon Sep 17 00:00:00 2001 From: Mirna Date: Wed, 4 Nov 2020 20:25:30 +0200 Subject: [PATCH 10/54] Refactor a `match` expression --- examples/nfct_test.rs | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs index 2e0df8a..0a81751 100644 --- a/examples/nfct_test.rs +++ b/examples/nfct_test.rs @@ -19,7 +19,6 @@ mod example { #[cfg(feature = "with_nfc")] mod example { - use super::Console; use super::Write; use libtock_core::result::CommandError; @@ -160,29 +159,26 @@ mod example { let mut is_enabled = false; let mut state_change_counter = 0; loop { - match is_enabled { - true => { - let mut rx_buf = [0; 256]; - loop { - receive_packet(&mut console, &mut rx_buf); - // If the reader restarts the communication and we can't - // reply to the received packet, then disable the tag. - if !transmit_reply(&mut console, &timer, &rx_buf) { - is_enabled = false; - break; - } + if is_enabled { + let mut rx_buf = [0; 256]; + loop { + receive_packet(&mut console, &mut rx_buf); + // If the reader restarts the communication and we can't + // reply to the received packet, then disable the tag. + if !transmit_reply(&mut console, &timer, &rx_buf) { + is_enabled = false; + break; } } - false => { - NfcTag::enable_emulation(); - // Configure Type 4 tag - if NfcTag::configure(4) { - is_enabled = true; - } + } else { + NfcTag::enable_emulation(); + // Configure Type 4 tag + if NfcTag::configure(4) { + is_enabled = true; } } state_change_counter += 1; - if state_change_counter > 100 && is_enabled == false { + if !is_enabled && state_change_counter > 100 { break; } } From 203367b081b14ca6622c6dee2ca29e1194397cf7 Mon Sep 17 00:00:00 2001 From: Mirna Date: Thu, 5 Nov 2020 10:38:59 +0200 Subject: [PATCH 11/54] Updated control flow + cleaned some code --- examples/nfct_test.rs | 45 ++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs index 0a81751..a43a059 100644 --- a/examples/nfct_test.rs +++ b/examples/nfct_test.rs @@ -32,15 +32,15 @@ mod example { /// Helper function to write on console the received packet. fn print_rx_buffer(buf: &mut [u8]) { - let mut console = Console::new(); - write!(console, "RX:").unwrap(); 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(); } - console.flush(); } /// Function to identify the time elapsed for a transmission request. @@ -65,25 +65,27 @@ mod example { console.flush(); } - fn receive_packet(console: &mut Console, mut buf: &mut [u8; 256]) { + fn receive_packet(console: &mut Console, mut buf: &mut [u8; 256]) -> bool { match NfcTag::receive(&mut buf) { Ok(RecvOp { recv_amount: amount, .. }) => { - if amount > 0 && amount <= buf.len() { + if amount <= buf.len() { print_rx_buffer(&mut buf[..amount]); } } Err(TockError::Command(CommandError { return_code: -4, /* EOFF: Not Ready */ .. - })) => (), + })) => return false, + // For the example app, just print any other received error without handling. Err(TockError::Command(CommandError { return_code: value, .. })) => writeln!(console, " -- Err({})!", value).unwrap(), Err(_) => writeln!(console, " -- RX Err").unwrap(), } + true } fn transmit_reply(mut console: &mut Console, timer: &Timer, buf: &[u8]) -> bool { @@ -156,29 +158,24 @@ mod example { ) .unwrap(); - let mut is_enabled = false; let mut state_change_counter = 0; loop { - if is_enabled { + while !NfcTag::enable_emulation() {} + // Configure Type 4 tag + while !NfcTag::configure(4) {} + state_change_counter += 1; + loop { let mut rx_buf = [0; 256]; - loop { - receive_packet(&mut console, &mut rx_buf); - // If the reader restarts the communication and we can't - // reply to the received packet, then disable the tag. - if !transmit_reply(&mut console, &timer, &rx_buf) { - is_enabled = false; - break; - } - } - } else { - NfcTag::enable_emulation(); - // Configure Type 4 tag - if NfcTag::configure(4) { - is_enabled = true; + // Await a successful receive + while !receive_packet(&mut console, &mut rx_buf) {} + // If the reader restarts the communication and we can't + // reply to the received packet, then disable the tag. + if !transmit_reply(&mut console, &timer, &rx_buf) { + state_change_counter += 1; + break; } } - state_change_counter += 1; - if !is_enabled && state_change_counter > 100 { + if state_change_counter > 100 { break; } } From f48046d1e4cb3adde676c62c0c6b0db2f6f62e03 Mon Sep 17 00:00:00 2001 From: Mirna Date: Thu, 5 Nov 2020 10:39:51 +0200 Subject: [PATCH 12/54] Added checks using NFC feature flag --- run_desktop_tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index b22e1d8..49c2990 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,6 +55,7 @@ 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 From 77d1b63284b8518b3e6b0685e3abe9acbec17f74 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 6 Nov 2020 17:30:59 +0100 Subject: [PATCH 13/54] adds a UP check where 2.1 is asking for it --- src/ctap/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 442d942..3ec7c6a 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -342,6 +342,7 @@ where if let Some(auth_param) = &pin_uv_auth_param { // This case was added in FIDO 2.1. if auth_param.is_empty() { + let _ = (self.check_user_presence)(cid); if self.persistent_store.pin_hash()?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { @@ -545,6 +546,7 @@ where if let Some(auth_param) = &pin_uv_auth_param { // This case was added in FIDO 2.1. if auth_param.is_empty() { + let _ = (self.check_user_presence)(cid); if self.persistent_store.pin_hash()?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { From 84dec3f636fa7c261135ff8b0930621990f98b71 Mon Sep 17 00:00:00 2001 From: Mirna Date: Tue, 10 Nov 2020 16:57:31 +0200 Subject: [PATCH 14/54] Update in the application's workflow --- examples/nfct_test.rs | 150 +++++++++++++++++++++++++++++------------- 1 file changed, 104 insertions(+), 46 deletions(-) diff --git a/examples/nfct_test.rs b/examples/nfct_test.rs index a43a059..924237d 100644 --- a/examples/nfct_test.rs +++ b/examples/nfct_test.rs @@ -30,6 +30,41 @@ mod example { 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() { @@ -44,11 +79,20 @@ mod example { } /// Function to identify the time elapsed for a transmission request. - fn bench_transmit(console: &mut Console, timer: &Timer, title: &str, mut buf: &mut [u8]) { + 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()); @@ -63,9 +107,10 @@ mod example { ) .unwrap(); console.flush(); + ReturnCode::SUCCESS } - fn receive_packet(console: &mut Console, mut buf: &mut [u8; 256]) -> bool { + fn receive_packet(console: &mut Console, mut buf: &mut [u8; 256]) -> ReturnCode { match NfcTag::receive(&mut buf) { Ok(RecvOp { recv_amount: amount, @@ -75,74 +120,71 @@ mod example { print_rx_buffer(&mut buf[..amount]); } } - Err(TockError::Command(CommandError { - return_code: -4, /* EOFF: Not Ready */ - .. - })) => return false, - // For the example app, just print any other received error without handling. - Err(TockError::Command(CommandError { - return_code: value, .. - })) => writeln!(console, " -- Err({})!", value).unwrap(), - Err(_) => writeln!(console, " -- RX Err").unwrap(), + Err(TockError::Command(CommandError { return_code, .. })) => return return_code.into(), + Err(_) => { + writeln!(console, " -- RX Err").unwrap(); + return ReturnCode::ECANCEL; + } } - true + ReturnCode::SUCCESS } - fn transmit_reply(mut console: &mut Console, timer: &Timer, buf: &[u8]) -> bool { + 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]; - bench_transmit(&mut console, &timer, "TX: ATS", &mut answer_to_select); + return_code = bench_transmit(&mut console, &timer, "TX: ATS", &mut answer_to_select); } 0xc2 /* DESELECT */ => { // Ignore the request let mut command_error = [0x6A, 0x81]; - bench_transmit(&mut console, &timer, "TX: DESELECT", &mut command_error); + 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,]; - bench_transmit(&mut console, &timer, "TX: Version Str", &mut reply); - } else { - let mut reply = [buf[0], 0x90, 0x00]; - bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); - } + // 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 => { + 0x02 => { let mut reply = [buf[0], 0x12, 0x90, 0x00,]; - bench_transmit(&mut console, &timer, "TX: File Size", &mut reply); + 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,]; - bench_transmit(&mut console, &timer, "TX: NDEF", &mut reply); + 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,]; - bench_transmit(&mut console, &timer, "TX: CC", &mut reply); + return_code = bench_transmit(&mut console, &timer, "TX: CC", &mut reply); } _ => { let mut reply = [buf[0], 0x90, 0x00]; - bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + return_code = bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); } } _ => { let mut reply = [buf[0], 0x90, 0x00]; - bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); + return_code = bench_transmit(&mut console, &timer, "TX: 0x9000", &mut reply); } } 0x26 | 0x52 | 0x50 /* REQA | WUPA | Halt */ => { - if NfcTag::disable_emulation() { - writeln!(console, " -- TAG DISABLED").unwrap(); - } - return false; + return ReturnCode::EOFF; } _ => (), } - true + return_code } pub fn nfc(mut console: &mut Console) { @@ -160,19 +202,35 @@ mod example { let mut state_change_counter = 0; loop { - while !NfcTag::enable_emulation() {} - // Configure Type 4 tag - while !NfcTag::configure(4) {} - state_change_counter += 1; - loop { - let mut rx_buf = [0; 256]; - // Await a successful receive - while !receive_packet(&mut console, &mut rx_buf) {} - // If the reader restarts the communication and we can't - // reply to the received packet, then disable the tag. - if !transmit_reply(&mut console, &timer, &rx_buf) { - state_change_counter += 1; - break; + 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 { From 163e92fa6bfb05cfc3ba1cfb88ee5d38c9d948f9 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 11 Nov 2020 12:30:24 +0100 Subject: [PATCH 15/54] Create fuzzing and add entropy helpers --- .github/workflows/cargo_fuzz.yml | 2 + libraries/persistent_store/fuzz/.gitignore | 4 + libraries/persistent_store/fuzz/Cargo.toml | 21 +++ .../fuzz/fuzz_targets/store.rs | 21 +++ libraries/persistent_store/fuzz/src/lib.rs | 152 ++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 libraries/persistent_store/fuzz/.gitignore create mode 100644 libraries/persistent_store/fuzz/Cargo.toml create mode 100644 libraries/persistent_store/fuzz/fuzz_targets/store.rs create mode 100644 libraries/persistent_store/fuzz/src/lib.rs 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/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..18b986d --- /dev/null +++ b/libraries/persistent_store/fuzz/Cargo.toml @@ -0,0 +1,21 @@ +[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"] } + +# 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/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs new file mode 100644 index 0000000..87ca7aa --- /dev/null +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -0,0 +1,152 @@ +// 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. + +// TODO(ia0): Remove when used. +#![allow(dead_code)] + +/// Bit-level entropy source based on a byte slice shared reference. +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 bounded number. + /// + /// # 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); +} From d3ee698d6972e7c55fa4e8ee8be7b8fed82435ae Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 11 Nov 2020 12:56:36 +0100 Subject: [PATCH 16/54] Build store fuzzing in run_desktop_tests too --- run_desktop_tests.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run_desktop_tests.sh b/run_desktop_tests.sh index b22e1d8..c3a853c 100755 --- a/run_desktop_tests.sh +++ b/run_desktop_tests.sh @@ -60,6 +60,9 @@ 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 From c6f9270be1d7d5ac75a585ca267602d525dace32 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Wed, 11 Nov 2020 17:52:33 +0100 Subject: [PATCH 17/54] Update documentation --- libraries/persistent_store/fuzz/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/persistent_store/fuzz/src/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs index 87ca7aa..90e9dc4 100644 --- a/libraries/persistent_store/fuzz/src/lib.rs +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -80,7 +80,11 @@ impl Entropy<'_> { result } - /// Reads a bounded number. + /// 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 /// From db5b21a4ff96c6a0a19a37bd5e0ca5b0b25a3062 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 12 Nov 2020 10:52:47 +0100 Subject: [PATCH 18/54] Add more documentation --- libraries/persistent_store/fuzz/src/lib.rs | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/libraries/persistent_store/fuzz/src/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs index 90e9dc4..3f45620 100644 --- a/libraries/persistent_store/fuzz/src/lib.rs +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -12,10 +12,39 @@ // 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)] /// Bit-level entropy source based on a byte slice shared reference. +/// +/// 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], From 1c2e450660884c4c599e617d8203fb66eef7e117 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 12 Nov 2020 16:24:35 +0100 Subject: [PATCH 19/54] Improve documentation --- libraries/persistent_store/fuzz/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/persistent_store/fuzz/src/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs index 3f45620..1feeabc 100644 --- a/libraries/persistent_store/fuzz/src/lib.rs +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -30,6 +30,12 @@ /// 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 From 51681e49100887dc4b6d46ac86db8426d0df73ef Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 13 Nov 2020 06:51:53 +0100 Subject: [PATCH 20/54] changes operation touch behaviour --- src/ctap/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index b3bbd64..06e3a57 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -345,7 +345,7 @@ where if let Some(auth_param) = &pin_uv_auth_param { // This case was added in FIDO 2.1. if auth_param.is_empty() { - let _ = (self.check_user_presence)(cid); + (self.check_user_presence)(cid)?; if self.persistent_store.pin_hash()?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { @@ -549,7 +549,7 @@ where if let Some(auth_param) = &pin_uv_auth_param { // This case was added in FIDO 2.1. if auth_param.is_empty() { - let _ = (self.check_user_presence)(cid); + (self.check_user_presence)(cid)?; if self.persistent_store.pin_hash()?.is_none() { return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); } else { From de77d4fc0cd13b7b22916babad4dd4d9a0f0f702 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Fri, 13 Nov 2020 10:34:23 +0100 Subject: [PATCH 21/54] Add histogram for fuzzing --- .../persistent_store/fuzz/src/histogram.rs | 100 ++++++++++++++++++ libraries/persistent_store/fuzz/src/lib.rs | 2 + 2 files changed, 102 insertions(+) create mode 100644 libraries/persistent_store/fuzz/src/histogram.rs diff --git a/libraries/persistent_store/fuzz/src/histogram.rs b/libraries/persistent_store/fuzz/src/histogram.rs new file mode 100644 index 0000000..0d76fd7 --- /dev/null +++ b/libraries/persistent_store/fuzz/src/histogram.rs @@ -0,0 +1,100 @@ +// 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. + /// + /// # Panics + /// + /// Panics if the item is too big, i.e. it uses its most significant bit. + pub fn add(&mut self, item: usize) { + assert!(item <= usize::max_value() / 2); + *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 one past the highest non-empty bucket. + /// + /// In other words, all non-empty buckets of the histogram are smaller than the returned bucket. + pub fn bucket_lim(&self) -> usize { + match self.buckets.keys().max() { + None => 0, + Some(0) => 1, + Some(x) => 2 * x, + } + } + + /// 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 index 1feeabc..3dd5932 100644 --- a/libraries/persistent_store/fuzz/src/lib.rs +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -28,6 +28,8 @@ // TODO(ia0): Remove when used. #![allow(dead_code)] +mod histogram; + /// 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 From fcc94845100e08f7c67f3811124ab67a14690489 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 16 Nov 2020 22:34:42 +0100 Subject: [PATCH 22/54] Add stats for fuzzing --- libraries/persistent_store/fuzz/Cargo.toml | 1 + libraries/persistent_store/fuzz/src/lib.rs | 3 + libraries/persistent_store/fuzz/src/stats.rs | 187 +++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 libraries/persistent_store/fuzz/src/stats.rs diff --git a/libraries/persistent_store/fuzz/Cargo.toml b/libraries/persistent_store/fuzz/Cargo.toml index 18b986d..fdc4f89 100644 --- a/libraries/persistent_store/fuzz/Cargo.toml +++ b/libraries/persistent_store/fuzz/Cargo.toml @@ -11,6 +11,7 @@ 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] diff --git a/libraries/persistent_store/fuzz/src/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs index 3dd5932..11645f3 100644 --- a/libraries/persistent_store/fuzz/src/lib.rs +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -29,6 +29,9 @@ #![allow(dead_code)] mod histogram; +mod stats; + +pub use stats::{StatKey, Stats}; /// Bit-level entropy source based on a byte slice shared reference. /// diff --git a/libraries/persistent_store/fuzz/src/stats.rs b/libraries/persistent_store/fuzz/src/stats.rs new file mode 100644 index 0000000..fdb5983 --- /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 crate::num_bits; +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 one past the highest non-empty bucket. + /// + /// In other words, all non-empty buckets of the histogram are smaller than the returned bucket. + fn bucket_lim(&self) -> usize { + self.stats + .values() + .map(|h| h.bucket_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 mut header = Vec::new(); + header.push(String::new()); + let bits = num_bits(self.bucket_lim()); + 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(()) +} From bbb73c77a8d7f61470e93f4092ba28fd68ea6845 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 17 Nov 2020 10:16:39 +0100 Subject: [PATCH 23/54] Use width_lim instead of bucket_lim --- .../persistent_store/fuzz/src/histogram.rs | 18 +++++------------- libraries/persistent_store/fuzz/src/stats.rs | 12 ++++++------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/libraries/persistent_store/fuzz/src/histogram.rs b/libraries/persistent_store/fuzz/src/histogram.rs index 0d76fd7..8237055 100644 --- a/libraries/persistent_store/fuzz/src/histogram.rs +++ b/libraries/persistent_store/fuzz/src/histogram.rs @@ -33,12 +33,7 @@ impl Histogram { /// /// 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. - /// - /// # Panics - /// - /// Panics if the item is too big, i.e. it uses its most significant bit. pub fn add(&mut self, item: usize) { - assert!(item <= usize::max_value() / 2); *self.buckets.entry(get_bucket(item)).or_insert(0) += 1; } @@ -49,15 +44,12 @@ impl Histogram { } } - /// Returns one past the highest non-empty bucket. + /// Returns the bit-width of one past the highest non-empty bucket. /// - /// In other words, all non-empty buckets of the histogram are smaller than the returned bucket. - pub fn bucket_lim(&self) -> usize { - match self.buckets.keys().max() { - None => 0, - Some(0) => 1, - Some(x) => 2 * x, - } + /// 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. diff --git a/libraries/persistent_store/fuzz/src/stats.rs b/libraries/persistent_store/fuzz/src/stats.rs index fdb5983..7bef07e 100644 --- a/libraries/persistent_store/fuzz/src/stats.rs +++ b/libraries/persistent_store/fuzz/src/stats.rs @@ -17,7 +17,6 @@ //! This is not used during actual fuzzing, only when replaying a corpus to compute statistics. use crate::histogram::{bucket_from_width, Histogram}; -use crate::num_bits; use std::collections::HashMap; use strum::{Display, EnumIter, EnumString, IntoEnumIterator}; @@ -110,13 +109,14 @@ impl Stats { self.stats.get(&key).and_then(|h| h.get(bucket)) } - /// Returns one past the highest non-empty bucket. + /// Returns the bit-width of one past the highest non-empty bucket. /// - /// In other words, all non-empty buckets of the histogram are smaller than the returned bucket. - fn bucket_lim(&self) -> usize { + /// 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.bucket_lim()) + .map(|h| h.width_lim()) .max() .unwrap_or(0) } @@ -125,10 +125,10 @@ impl Stats { 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()); - let bits = num_bits(self.bucket_lim()); for width in 0..bits { header.push(format!(" {}", bucket_from_width(width))); } From e842da0de7abe8c4304986e6dfa0dbbf68983a15 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Thu, 19 Nov 2020 11:27:50 +0100 Subject: [PATCH 24/54] Add store fuzzing --- libraries/persistent_store/fuzz/Cargo.toml | 2 + .../fuzz/fuzz_targets/store.rs | 2 +- libraries/persistent_store/fuzz/src/lib.rs | 5 +- libraries/persistent_store/fuzz/src/store.rs | 533 ++++++++++++++++++ 4 files changed, 538 insertions(+), 4 deletions(-) create mode 100644 libraries/persistent_store/fuzz/src/store.rs diff --git a/libraries/persistent_store/fuzz/Cargo.toml b/libraries/persistent_store/fuzz/Cargo.toml index fdc4f89..f0b01f1 100644 --- a/libraries/persistent_store/fuzz/Cargo.toml +++ b/libraries/persistent_store/fuzz/Cargo.toml @@ -11,6 +11,8 @@ cargo-fuzz = true [dependencies] libfuzzer-sys = "0.3" persistent_store = { path = "..", features = ["std"] } +rand_core = "0.5" +rand_pcg = "0.2" strum = { version = "0.19", features = ["derive"] } # Prevent this from interfering with workspaces diff --git a/libraries/persistent_store/fuzz/fuzz_targets/store.rs b/libraries/persistent_store/fuzz/fuzz_targets/store.rs index 1cff2a4..d96874d 100644 --- a/libraries/persistent_store/fuzz/fuzz_targets/store.rs +++ b/libraries/persistent_store/fuzz/fuzz_targets/store.rs @@ -17,5 +17,5 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { - // TODO(ia0): Call fuzzing when implemented. + fuzz_store::fuzz(data, false, None); }); diff --git a/libraries/persistent_store/fuzz/src/lib.rs b/libraries/persistent_store/fuzz/src/lib.rs index 11645f3..3d32624 100644 --- a/libraries/persistent_store/fuzz/src/lib.rs +++ b/libraries/persistent_store/fuzz/src/lib.rs @@ -25,13 +25,12 @@ //! situation where coverage takes precedence over surjectivity is for the value of insert updates //! where a pseudo-random generator is used to avoid wasting entropy. -// TODO(ia0): Remove when used. -#![allow(dead_code)] - mod histogram; mod stats; +mod store; pub use stats::{StatKey, Stats}; +pub use store::fuzz; /// Bit-level entropy source based on a byte slice shared reference. /// diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs new file mode 100644 index 0000000..e51ed71 --- /dev/null +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -0,0 +1,533 @@ +// Copyright 2019-2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::stats::{StatKey, Stats}; +use crate::Entropy; +use persistent_store::{ + BufferOptions, BufferStorage, Store, StoreDriver, StoreDriverOff, StoreDriverOn, + StoreInterruption, StoreInvariant, StoreOperation, StoreUpdate, +}; +use rand_core::{RngCore, SeedableRng}; +use rand_pcg::Pcg32; +use std::collections::HashMap; +use std::convert::TryInto; + +// NOTE: We should be able to improve coverage by only checking the last operation. Because +// operations before the last could be checked with a shorter entropy. + +/// Checks the store against a sequence of manipulations. +/// +/// The entropy to generate the sequence of manipulation should be provided in `data`. Debugging +/// information is printed if `debug` is set. Statistics are gathered if `stats` is set. +pub fn fuzz(data: &[u8], debug: bool, stats: Option<&mut Stats>) { + let mut fuzzer = Fuzzer::new(data, debug, stats); + fuzzer.init_counters(); + fuzzer.record(StatKey::Entropy, data.len()); + let mut driver = fuzzer.init(); + let store = loop { + if fuzzer.debug { + print!("{}", driver.storage()); + } + if let StoreDriver::On(driver) = &driver { + if !fuzzer.init.is_dirty() { + driver.check().unwrap(); + } + if fuzzer.debug { + println!("----------------------------------------------------------------------"); + } + } + if fuzzer.entropy.is_empty() { + if fuzzer.debug { + println!("No more entropy."); + } + if fuzzer.init.is_dirty() { + return; + } + fuzzer.record(StatKey::FinishedLifetime, 0); + break driver.power_on().unwrap().extract_store(); + } + driver = match driver { + StoreDriver::On(driver) => match fuzzer.apply(driver) { + Ok(x) => x, + Err(store) => { + if fuzzer.debug { + println!("No more lifetime."); + } + if fuzzer.init.is_dirty() { + return; + } + fuzzer.record(StatKey::FinishedLifetime, 1); + break store; + } + }, + StoreDriver::Off(driver) => fuzzer.power_on(driver), + } + }; + let virt_window = (store.format().num_pages() * store.format().virt_page_size()) as usize; + let init_lifetime = fuzzer.init.used_cycles() * virt_window; + let lifetime = store.lifetime().unwrap().used() - init_lifetime; + fuzzer.record(StatKey::UsedLifetime, lifetime); + fuzzer.record(StatKey::NumCompactions, lifetime / virt_window); + fuzzer.record_counters(); +} + +/// Fuzzing state. +struct Fuzzer<'a> { + /// Remaining fuzzing entropy. + entropy: Entropy<'a>, + + /// Unlimited pseudo entropy. + /// + /// This source is only used to generate the values of entries. This is a compromise to avoid + /// consuming fuzzing entropy for low additional coverage. + values: Pcg32, + + /// The fuzzing mode. + init: Init, + + /// Whether debugging is enabled. + debug: bool, + + /// Whether statistics should be gathered. + stats: Option<&'a mut Stats>, + + /// Statistics counters (only used when gathering statistics). + /// + /// The counters are written to the statistics at the end of the fuzzing run, when their value + /// is final. + counters: HashMap, +} + +impl<'a> Fuzzer<'a> { + /// Creates an initial fuzzing state. + fn new(data: &'a [u8], debug: bool, stats: Option<&'a mut Stats>) -> Fuzzer<'a> { + let mut entropy = Entropy::new(data); + let seed = entropy.read_slice(16); + let values = Pcg32::from_seed(seed[..].try_into().unwrap()); + Fuzzer { + entropy, + values, + init: Init::Clean, + debug, + stats, + counters: HashMap::new(), + } + } + + /// Initializes the fuzzing state and returns the store driver. + fn init(&mut self) -> StoreDriver { + let mut options = BufferOptions { + word_size: 4, + page_size: 1 << self.entropy.read_range(5, 12), + max_word_writes: 2, + max_page_erases: self.entropy.read_range(0, 50000), + strict_write: true, + }; + let num_pages = self.entropy.read_range(3, 64); + self.record(StatKey::PageSize, options.page_size); + self.record(StatKey::MaxPageErases, options.max_page_erases); + self.record(StatKey::NumPages, num_pages); + if self.debug { + println!("page_size: {}", options.page_size); + println!("num_pages: {}", num_pages); + println!("max_cycle: {}", options.max_page_erases); + } + let storage_size = num_pages * options.page_size; + if self.entropy.read_bit() { + self.init = Init::Dirty; + let mut storage = vec![0xff; storage_size].into_boxed_slice(); + let length = self.entropy.read_range(0, storage_size); + self.record(StatKey::DirtyLength, length); + for byte in &mut storage[0..length] { + *byte = self.entropy.read_byte(); + } + if self.debug { + println!("Start with dirty storage."); + } + options.strict_write = false; + let storage = BufferStorage::new(storage, options); + StoreDriver::Off(StoreDriverOff::new_dirty(storage)) + } else if self.entropy.read_bit() { + let cycle = self.entropy.read_range(0, options.max_page_erases); + self.init = Init::Used { cycle }; + if self.debug { + println!("Start with {} consumed erase cycles.", cycle); + } + self.record(StatKey::InitCycles, cycle); + let storage = vec![0xff; storage_size].into_boxed_slice(); + let mut storage = BufferStorage::new(storage, options); + Store::init_with_cycle(&mut storage, cycle); + StoreDriver::Off(StoreDriverOff::new_dirty(storage)) + } else { + StoreDriver::Off(StoreDriverOff::new(options, num_pages)) + } + } + + /// Powers a driver with possible interruption. + fn power_on(&mut self, driver: StoreDriverOff) -> StoreDriver { + if self.debug { + println!("Power on the store."); + } + self.increment(StatKey::PowerOnCount); + let interruption = self.interruption(driver.delay_map()); + match driver.partial_power_on(interruption) { + Err((storage, _)) if self.init.is_dirty() => { + self.entropy.consume_all(); + StoreDriver::Off(StoreDriverOff::new_dirty(storage)) + } + Err(error) => self.crash(error), + Ok(driver) => driver, + } + } + + /// Generates and applies an operation with possible interruption. + fn apply(&mut self, driver: StoreDriverOn) -> Result> { + let operation = self.operation(&driver); + if self.debug { + println!("{:?}", operation); + } + let interruption = self.interruption(driver.delay_map(&operation)); + match driver.partial_apply(operation, interruption) { + Err((store, _)) if self.init.is_dirty() => { + self.entropy.consume_all(); + Err(store) + } + Err((store, StoreInvariant::NoLifetime)) => Err(store), + Err((store, error)) => self.crash((store.extract_storage(), error)), + Ok((error, driver)) => { + if self.debug { + if let Some(error) = error { + println!("{:?}", error); + } + } + Ok(driver) + } + } + } + + /// Reports a broken invariant and terminates fuzzing. + fn crash(&self, error: (BufferStorage, StoreInvariant)) -> ! { + let (storage, invariant) = error; + if self.debug { + print!("{}", storage); + } + panic!("{:?}", invariant); + } + + /// Records a statistics if enabled. + fn record(&mut self, key: StatKey, value: usize) { + if let Some(stats) = &mut self.stats { + stats.add(key, value); + } + } + + /// Increments a counter if statistics are enabled. + fn increment(&mut self, key: StatKey) { + if self.stats.is_some() { + *self.counters.get_mut(&key).unwrap() += 1; + } + } + + /// Initializes all counters if statistics are enabled. + fn init_counters(&mut self) { + if self.stats.is_some() { + use StatKey::*; + self.counters.insert(PowerOnCount, 0); + self.counters.insert(TransactionCount, 0); + self.counters.insert(ClearCount, 0); + self.counters.insert(PrepareCount, 0); + self.counters.insert(InsertCount, 0); + self.counters.insert(RemoveCount, 0); + self.counters.insert(InterruptionCount, 0); + } + } + + /// Records all counters if statistics are enabled. + fn record_counters(&mut self) { + if let Some(stats) = &mut self.stats { + for (&key, &value) in self.counters.iter() { + stats.add(key, value); + } + } + } + + /// Generates a possibly invalid operation. + fn operation(&mut self, driver: &StoreDriverOn) -> StoreOperation { + let format = driver.model().format(); + match self.entropy.read_range(0, 2) { + 0 => { + // We also generate an invalid count (one past the maximum value) to test the error + // scenario. Since the test for the error scenario is monotonic, this is a good + // compromise to keep entropy bounded. + let count = self + .entropy + .read_range(0, format.max_updates() as usize + 1); + let mut updates = Vec::with_capacity(count); + for _ in 0..count { + updates.push(self.update()); + } + self.increment(StatKey::TransactionCount); + StoreOperation::Transaction { updates } + } + 1 => { + let min_key = self.key(); + self.increment(StatKey::ClearCount); + StoreOperation::Clear { min_key } + } + 2 => { + // We also generate an invalid length (one past the total capacity) to test the + // error scenario. See the explanation for transactions above for why it's enough. + let length = self + .entropy + .read_range(0, format.total_capacity() as usize + 1); + self.increment(StatKey::PrepareCount); + StoreOperation::Prepare { length } + } + _ => unreachable!(), + } + } + + /// Generates a possibly invalid update. + fn update(&mut self) -> StoreUpdate { + match self.entropy.read_range(0, 1) { + 0 => { + let key = self.key(); + let value = self.value(); + self.increment(StatKey::InsertCount); + StoreUpdate::Insert { key, value } + } + 1 => { + let key = self.key(); + self.increment(StatKey::RemoveCount); + StoreUpdate::Remove { key } + } + _ => unreachable!(), + } + } + + /// Generates a possibly invalid key. + fn key(&mut self) -> usize { + // Use 4096 as the canonical invalid key. + self.entropy.read_range(0, 4096) + } + + /// Generates a possibly invalid value. + fn value(&mut self) -> Vec { + // Use 1024 as the canonical invalid length. + let length = self.entropy.read_range(0, 1024); + let mut value = vec![0; length]; + self.values.fill_bytes(&mut value); + value + } + + /// Generates an interruption. + /// + /// The `delay_map` describes the number of modified bits by the upcoming sequence of store + /// operations. + // TODO(ia0): We use too much CPU to compute the delay map. We should be able to just count the + // number of storage operations by checking the remaining delay. We can then use the entropy + // directly from the corruption function because it's called at most once. + fn interruption( + &mut self, + delay_map: Result, (usize, BufferStorage)>, + ) -> StoreInterruption { + if self.init.is_dirty() { + // We only test that the store can power on without crashing. If it would get + // interrupted then it's like powering up with a different initial state, which would be + // tested with another fuzzing input. + return StoreInterruption::none(); + } + let delay_map = match delay_map { + Ok(x) => x, + Err((delay, storage)) => { + print!("{}", storage); + panic!("delay={}", delay); + } + }; + let delay = self.entropy.read_range(0, delay_map.len() - 1); + let mut complete_bits = BitStack::default(); + for _ in 0..delay_map[delay] { + complete_bits.push(self.entropy.read_bit()); + } + if self.debug { + if delay == delay_map.len() - 1 { + assert!(complete_bits.is_empty()); + println!("Do not interrupt."); + } else { + println!( + "Interrupt after {} operations with complete mask {}.", + delay, complete_bits + ); + } + } + if delay < delay_map.len() - 1 { + self.increment(StatKey::InterruptionCount); + } + let corrupt = Box::new(move |old: &mut [u8], new: &[u8]| { + for (old, new) in old.iter_mut().zip(new.iter()) { + for bit in 0..8 { + let mask = 1 << bit; + if *old & mask == *new & mask { + continue; + } + if complete_bits.pop().unwrap() { + *old ^= mask; + } + } + } + }); + StoreInterruption { delay, corrupt } + } +} + +/// The initial fuzzing mode. +enum Init { + /// Fuzzing starts from a clean storage. + /// + /// All invariant are checked. + Clean, + + /// Fuzzing starts from a dirty storage. + /// + /// Only crashing is checked. + Dirty, + + /// Fuzzing starts from a simulated old storage. + /// + /// All invariant are checked. + Used { + /// Number of simulated used cycles. + cycle: usize, + }, +} + +impl Init { + /// Returns whether fuzzing is in dirty mode. + fn is_dirty(&self) -> bool { + match self { + Init::Dirty => true, + _ => false, + } + } + + /// Returns the number of used cycles. + /// + /// This is zero if the storage was not artificially aged. + fn used_cycles(&self) -> usize { + match self { + Init::Used { cycle } => *cycle, + _ => 0, + } + } +} + +/// Compact stack of bits. +// NOTE: This would probably go away once the delay map is simplified. +#[derive(Default, Clone, Debug)] +struct BitStack { + /// Bits stored in little-endian (for bytes and bits). + data: Vec, + + /// Number of bits stored. + len: usize, +} + +impl BitStack { + /// Returns whether the stack is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the length of the stack. + fn len(&self) -> usize { + if self.len == 0 { + 8 * self.data.len() + } else { + 8 * (self.data.len() - 1) + self.len + } + } + + /// Pushes a bit to the stack. + fn push(&mut self, value: bool) { + if self.len == 0 { + self.data.push(0); + } + if value { + *self.data.last_mut().unwrap() |= 1 << self.len; + } + self.len += 1; + if self.len == 8 { + self.len = 0; + } + } + + /// Pops a bit from the stack. + fn pop(&mut self) -> Option { + if self.len == 0 { + if self.data.is_empty() { + return None; + } + self.len = 8; + } + self.len -= 1; + let result = self.data.last().unwrap() & 1 << self.len; + if self.len == 0 { + self.data.pop().unwrap(); + } + Some(result != 0) + } +} + +impl std::fmt::Display for BitStack { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + let mut bits = self.clone(); + while let Some(bit) = bits.pop() { + write!(f, "{}", bit as usize)?; + } + write!(f, " ({} bits)", self.len())?; + Ok(()) + } +} + +#[test] +fn bit_stack_ok() { + let mut bits = BitStack::default(); + + assert_eq!(bits.pop(), None); + + bits.push(true); + assert_eq!(bits.pop(), Some(true)); + assert_eq!(bits.pop(), None); + + bits.push(false); + assert_eq!(bits.pop(), Some(false)); + assert_eq!(bits.pop(), None); + + bits.push(true); + bits.push(false); + assert_eq!(bits.pop(), Some(false)); + assert_eq!(bits.pop(), Some(true)); + assert_eq!(bits.pop(), None); + + bits.push(false); + bits.push(true); + assert_eq!(bits.pop(), Some(true)); + assert_eq!(bits.pop(), Some(false)); + assert_eq!(bits.pop(), None); + + for i in 0..27 { + assert_eq!(bits.len(), i); + bits.push(true); + } +} From 315016f55265621cc7448c31817b41e66b694e49 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 20 Nov 2020 02:50:47 +0100 Subject: [PATCH 25/54] unwraps credentials in the exclude list --- src/ctap/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 1a98ce5..66ef234 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -392,12 +392,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. @@ -446,7 +450,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 { From e1b419c104903cded5310f9313e5191675aa3092 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 20 Nov 2020 05:49:11 +0100 Subject: [PATCH 26/54] changes sync response and tests it --- src/ctap/hid/mod.rs | 117 ++++++++++++++++++++++++++++------------ src/ctap/hid/receive.rs | 28 ++++++++++ 2 files changed, 112 insertions(+), 33 deletions(-) diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 7489f6e..3a96699 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 => { @@ -568,6 +559,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 } From 9a29795ca65870508267b45d5744878357a97c62 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 20 Nov 2020 06:03:52 +0100 Subject: [PATCH 27/54] changes priority of error codes --- src/ctap/hid/mod.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index 7489f6e..e975f33 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -307,7 +307,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 +525,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 {}; From dab0077b87451fa448f72f1dadd1d2c42c142ca6 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Fri, 20 Nov 2020 11:58:39 +0100 Subject: [PATCH 28/54] Fix broken crypto_test workflow. The use of `::set-env` command in workflows is not being depreacted. Moving to the new way of setting environment variables. --- .github/workflows/crypto_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5bf73cb8fdcd4bcb33381992c8608fd77df7dce6 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 20 Nov 2020 03:26:26 +0100 Subject: [PATCH 29/54] fail on UP=true in make --- src/ctap/data_formats.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index dacafe5..45ebf9f 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -361,10 +361,8 @@ impl TryFrom for MakeCredentialOptions { Some(options_entry) => extract_bool(options_entry)?, None => false, }; - if let Some(options_entry) = up { - if !extract_bool(options_entry)? { - return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); - } + if up.is_some() { + return Err(Ctap2StatusCode::CTAP2_ERR_INVALID_OPTION); } let uv = match uv { Some(options_entry) => extract_bool(options_entry)?, From 9bb1aad45d526b29bb5f6477c56b5136a880cff2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 23 Nov 2020 12:02:51 +0100 Subject: [PATCH 30/54] wraps HMAC secret into credentials --- src/ctap/ctap1.rs | 50 ++++++---- src/ctap/mod.rs | 229 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 236 insertions(+), 43 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 84c6fb0..a5a7921 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -288,7 +288,7 @@ impl Ctap1Command { let sk = crypto::ecdsa::SecKey::gensk(ctap_state.rng); let pk = sk.genpk(); let key_handle = ctap_state - .encrypt_key_handle(sk, &application) + .encrypt_key_handle(sk, &application, None) .map_err(|_| Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. @@ -373,7 +373,7 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::{ENCRYPTED_CREDENTIAL_ID_SIZE, USE_SIGNATURE_COUNTER}; + use super::super::{CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; use super::*; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -413,12 +413,12 @@ mod test { 0x00, 0x00, 0x00, - 65 + ENCRYPTED_CREDENTIAL_ID_SIZE as u8, + 65 + CREDENTIAL_ID_BASE_SIZE as u8, ]; let challenge = [0x0C; 32]; message.extend(&challenge); message.extend(application); - message.push(ENCRYPTED_CREDENTIAL_ID_SIZE as u8); + message.push(CREDENTIAL_ID_BASE_SIZE as u8); message.extend(key_handle); message } @@ -437,15 +437,15 @@ mod test { Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); assert_eq!(response[0], Ctap1Command::LEGACY_BYTE); - assert_eq!(response[66], ENCRYPTED_CREDENTIAL_ID_SIZE as u8); + assert_eq!(response[66], CREDENTIAL_ID_BASE_SIZE as u8); assert!(ctap_state .decrypt_credential_source( - response[67..67 + ENCRYPTED_CREDENTIAL_ID_SIZE].to_vec(), + response[67..67 + CREDENTIAL_ID_BASE_SIZE].to_vec(), &application ) .unwrap() .is_some()); - const CERT_START: usize = 67 + ENCRYPTED_CREDENTIAL_ID_SIZE; + const CERT_START: usize = 67 + CREDENTIAL_ID_BASE_SIZE; assert_eq!( &response[CERT_START..CERT_START + ATTESTATION_CERTIFICATE.len()], &ATTESTATION_CERTIFICATE[..] @@ -494,7 +494,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); @@ -510,7 +512,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let application = [0x55; 32]; let message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -527,7 +531,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); @@ -551,7 +557,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[0] = 0xEE; @@ -569,7 +577,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[1] = 0xEE; @@ -587,7 +597,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let mut message = create_authenticate_message(&application, Ctap1Flags::CheckOnly, &key_handle); message[2] = 0xEE; @@ -605,7 +617,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -630,7 +644,9 @@ mod test { let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); - let key_handle = ctap_state.encrypt_key_handle(sk, &application).unwrap(); + let key_handle = ctap_state + .encrypt_key_handle(sk, &application, None) + .unwrap(); let message = create_authenticate_message( &application, Ctap1Flags::DontEnforceUpAndSign, @@ -650,7 +666,7 @@ mod test { #[test] fn test_process_authenticate_bad_key_handle() { let application = [0x0A; 32]; - let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); @@ -667,7 +683,7 @@ mod test { #[test] fn test_process_authenticate_without_up() { let application = [0x0A; 32]; - let key_handle = vec![0x00; ENCRYPTED_CREDENTIAL_ID_SIZE]; + let key_handle = vec![0x00; CREDENTIAL_ID_BASE_SIZE]; let message = create_authenticate_message(&application, Ctap1Flags::EnforceUpAndSign, &key_handle); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 66ef234..2e095ed 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -83,8 +83,9 @@ const USE_SIGNATURE_COUNTER: bool = true; // - 16 byte initialization vector for AES-256, // - 32 byte ECDSA private key for the credential, // - 32 byte relying party ID hashed with SHA256, +// - (optional) 32 byte for HMAC-secret, // - 32 byte HMAC-SHA256 over everything else. -pub const ENCRYPTED_CREDENTIAL_ID_SIZE: usize = 112; +pub const CREDENTIAL_ID_BASE_SIZE: usize = 112; // Set this bit when checking user presence. const UP_FLAG: u8 = 0x01; // Set this bit when checking user verification. @@ -195,6 +196,7 @@ where &mut self, private_key: crypto::ecdsa::SecKey, application: &[u8; 32], + cred_random: Option<&[u8; 32]>, ) -> Result, Ctap2StatusCode> { let master_keys = self.persistent_store.master_keys()?; let aes_enc_key = crypto::aes256::EncryptionKey::new(&master_keys.encryption); @@ -203,14 +205,19 @@ where let mut iv = [0; 16]; iv.copy_from_slice(&self.rng.gen_uniform_u8x32()[..16]); - let mut blocks = [[0u8; 16]; 4]; + let block_len = if cred_random.is_some() { 6 } else { 4 }; + let mut blocks = vec![[0u8; 16]; block_len]; blocks[0].copy_from_slice(&sk_bytes[..16]); blocks[1].copy_from_slice(&sk_bytes[16..]); blocks[2].copy_from_slice(&application[..16]); blocks[3].copy_from_slice(&application[16..]); + if let Some(cred_random) = cred_random { + blocks[4].copy_from_slice(&cred_random[..16]); + blocks[5].copy_from_slice(&cred_random[16..]); + } cbc_encrypt(&aes_enc_key, iv, &mut blocks); - let mut encrypted_id = Vec::with_capacity(ENCRYPTED_CREDENTIAL_ID_SIZE); + let mut encrypted_id = Vec::with_capacity(16 * (block_len + 3)); encrypted_id.extend(&iv); for b in &blocks { encrypted_id.extend(b); @@ -228,11 +235,15 @@ where credential_id: Vec, rp_id_hash: &[u8], ) -> Result, Ctap2StatusCode> { - if credential_id.len() != ENCRYPTED_CREDENTIAL_ID_SIZE { + let has_cred_random = if credential_id.len() == CREDENTIAL_ID_BASE_SIZE { + false + } else if credential_id.len() == CREDENTIAL_ID_BASE_SIZE + 32 { + true + } else { return Ok(None); - } + }; let master_keys = self.persistent_store.master_keys()?; - let payload_size = ENCRYPTED_CREDENTIAL_ID_SIZE - 32; + let payload_size = credential_id.len() - 32; if !verify_hmac_256::( &master_keys.hmac, &credential_id[..payload_size], @@ -244,8 +255,9 @@ where let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); let mut iv = [0; 16]; iv.copy_from_slice(&credential_id[..16]); - let mut blocks = [[0u8; 16]; 4]; - for i in 0..4 { + let block_len = if has_cred_random { 6 } else { 4 }; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { blocks[i].copy_from_slice(&credential_id[16 * (i + 1)..16 * (i + 2)]); } @@ -256,6 +268,14 @@ where decrypted_sk[16..].clone_from_slice(&blocks[1]); decrypted_rp_id_hash[..16].clone_from_slice(&blocks[2]); decrypted_rp_id_hash[16..].clone_from_slice(&blocks[3]); + let cred_random = if has_cred_random { + let mut decrypted_cred_random = [0; 32]; + decrypted_cred_random[..16].clone_from_slice(&blocks[4]); + decrypted_cred_random[16..].clone_from_slice(&blocks[5]); + Some(decrypted_cred_random.to_vec()) + } else { + None + }; if rp_id_hash != decrypted_rp_id_hash { return Ok(None); @@ -269,7 +289,7 @@ where rp_id: String::from(""), user_handle: vec![], other_ui: None, - cred_random: None, + cred_random, cred_protect_policy: None, })) } @@ -380,11 +400,7 @@ where }; let cred_random = if use_hmac_extension { - if !options.rk { - // The extension is actually supported, but we need resident keys. - return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION); - } - Some(self.rng.gen_uniform_u8x32().to_vec()) + Some(self.rng.gen_uniform_u8x32()) } else { None }; @@ -463,13 +479,13 @@ where other_ui: user .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), - cred_random, + cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, }; self.persistent_store.store_credential(credential_source)?; random_id } else { - self.encrypt_key_handle(sk.clone(), &rp_id_hash)? + self.encrypt_key_handle(sk.clone(), &rp_id_hash, cred_random.as_ref())? }; let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; @@ -728,10 +744,9 @@ where ]), #[cfg(feature = "with_ctap2_1")] max_credential_count_in_list: MAX_CREDENTIAL_COUNT_IN_LIST.map(|c| c as u64), - // You can use ENCRYPTED_CREDENTIAL_ID_SIZE here, but if your - // browser passes that value, it might be used to fingerprint. + // #TODO(106) update with version 2.1 of HMAC-secret #[cfg(feature = "with_ctap2_1")] - max_credential_id_length: None, + max_credential_id_length: Some(CREDENTIAL_ID_BASE_SIZE as u64 + 32), #[cfg(feature = "with_ctap2_1")] transports: Some(vec![AuthenticatorTransport::Usb]), #[cfg(feature = "with_ctap2_1")] @@ -829,7 +844,7 @@ mod test { let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID); #[cfg(feature = "with_ctap2_1")] - let mut expected_response = vec![0x00, 0xA9, 0x01]; + let mut expected_response = vec![0x00, 0xAA, 0x01]; #[cfg(not(feature = "with_ctap2_1"))] let mut expected_response = vec![0x00, 0xA6, 0x01]; // The difference here is a longer array of supported versions. @@ -864,9 +879,9 @@ mod test { #[cfg(feature = "with_ctap2_1")] expected_response.extend( [ - 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, 0x6C, 0x67, 0x26, - 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, 0x63, 0x2D, 0x6B, - 0x65, 0x79, 0x0D, 0x04, + 0x08, 0x18, 0x90, 0x09, 0x81, 0x63, 0x75, 0x73, 0x62, 0x0A, 0x81, 0xA2, 0x63, 0x61, + 0x6C, 0x67, 0x26, 0x64, 0x74, 0x79, 0x70, 0x65, 0x6A, 0x70, 0x75, 0x62, 0x6C, 0x69, + 0x63, 0x2D, 0x6B, 0x65, 0x79, 0x0D, 0x04, ] .iter(), ); @@ -993,7 +1008,7 @@ mod test { 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x41, 0x00, 0x00, 0x00, 0x00, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - expected_auth_data.extend(&[0x00, ENCRYPTED_CREDENTIAL_ID_SIZE as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_BASE_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1114,6 +1129,57 @@ mod test { let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let extensions = Some(MakeCredentialExtensions { + hmac_secret: true, + cred_protect: None, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + make_credential_params.extensions = extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + + match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let AuthenticatorMakeCredentialResponse { + fmt, + auth_data, + att_stmt, + } = make_credential_response; + // The expected response is split to only assert the non-random parts. + assert_eq!(fmt, "packed"); + let mut expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, + ]; + expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); + let credential_size = CREDENTIAL_ID_BASE_SIZE + 32; + expected_auth_data.extend(&[0x00, credential_size as u8]); + assert_eq!( + auth_data[0..expected_auth_data.len()], + expected_auth_data[..] + ); + let expected_extension_cbor = vec![ + 0xA1, 0x6B, 0x68, 0x6D, 0x61, 0x63, 0x2D, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, + 0xF5, + ]; + assert_eq!( + auth_data[auth_data.len() - expected_extension_cbor.len()..auth_data.len()], + expected_extension_cbor[..] + ); + assert_eq!(att_stmt.alg, SignatureAlgorithm::ES256 as i64); + } + _ => panic!("Invalid response type"), + } + } + + #[test] + fn test_process_make_credential_hmac_secret_resident_key() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let extensions = Some(MakeCredentialExtensions { hmac_secret: true, cred_protect: None, @@ -1220,6 +1286,71 @@ mod test { } } + #[test] + fn test_process_get_assertion_hmac_secret() { + let mut rng = ThreadRng256 {}; + let sk = crypto::ecdh::SecKey::gensk(&mut rng); + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + let make_extensions = Some(MakeCredentialExtensions { + hmac_secret: true, + cred_protect: None, + }); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.options.rk = false; + make_credential_params.extensions = make_extensions; + let make_credential_response = + ctap_state.process_make_credential(make_credential_params, DUMMY_CHANNEL_ID); + assert!(make_credential_response.is_ok()); + let credential_id = match make_credential_response.unwrap() { + ResponseData::AuthenticatorMakeCredential(make_credential_response) => { + let auth_data = make_credential_response.auth_data; + let offset = 37 + ctap_state.persistent_store.aaguid().unwrap().len(); + let credential_size = CREDENTIAL_ID_BASE_SIZE + 32; + assert_eq!(auth_data[offset], 0x00); + assert_eq!(auth_data[offset + 1] as usize, credential_size); + auth_data[offset + 2..offset + 2 + credential_size].to_vec() + } + _ => panic!("Invalid response type"), + }; + + let pk = sk.genpk(); + let hmac_secret_input = GetAssertionHmacSecretInput { + key_agreement: CoseKey::from(pk), + salt_enc: vec![0x02; 32], + salt_auth: vec![0x03; 16], + }; + let get_extensions = Some(GetAssertionExtensions { + hmac_secret: Some(hmac_secret_input), + }); + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential_id, + transports: None, + }; + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: Some(vec![cred_desc]), + extensions: get_extensions, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = + ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) + ); + } + #[test] fn test_residential_process_get_assertion_hmac_secret() { let mut rng = ThreadRng256 {}; @@ -1435,7 +1566,7 @@ mod test { // We are not testing the correctness of our SHA256 here, only if it is checked. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key.clone(), &rp_id_hash) + .encrypt_key_handle(private_key.clone(), &rp_id_hash, None) .unwrap(); let decrypted_source = ctap_state .decrypt_credential_source(encrypted_id, &rp_id_hash) @@ -1445,6 +1576,29 @@ mod test { assert_eq!(private_key, decrypted_source.private_key); } + #[test] + fn test_encrypt_decrypt_credential_with_cred_random() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + // Usually, the relying party ID or its hash is provided by the client. + // We are not testing the correctness of our SHA256 here, only if it is checked. + let rp_id_hash = [0x55; 32]; + let cred_random = [0xC9; 32]; + let encrypted_id = ctap_state + .encrypt_key_handle(private_key.clone(), &rp_id_hash, Some(&cred_random)) + .unwrap(); + let decrypted_source = ctap_state + .decrypt_credential_source(encrypted_id, &rp_id_hash) + .unwrap() + .unwrap(); + + assert_eq!(private_key, decrypted_source.private_key); + assert_eq!(Some(cred_random.to_vec()), decrypted_source.cred_random); + } + #[test] fn test_encrypt_decrypt_bad_hmac() { let mut rng = ThreadRng256 {}; @@ -1455,7 +1609,30 @@ mod test { // Same as above. let rp_id_hash = [0x55; 32]; let encrypted_id = ctap_state - .encrypt_key_handle(private_key, &rp_id_hash) + .encrypt_key_handle(private_key, &rp_id_hash, None) + .unwrap(); + for i in 0..encrypted_id.len() { + let mut modified_id = encrypted_id.clone(); + modified_id[i] ^= 0x01; + assert!(ctap_state + .decrypt_credential_source(modified_id, &rp_id_hash) + .unwrap() + .is_none()); + } + } + + #[test] + fn test_encrypt_decrypt_bad_hmac_with_cred_random() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + // Same as above. + let rp_id_hash = [0x55; 32]; + let cred_random = [0xC9; 32]; + let encrypted_id = ctap_state + .encrypt_key_handle(private_key, &rp_id_hash, Some(&cred_random)) .unwrap(); for i in 0..encrypted_id.len() { let mut modified_id = encrypted_id.clone(); From a099ddbabde0c208b821c5417d5ed16e86560bd9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 23 Nov 2020 14:34:38 +0100 Subject: [PATCH 31/54] introduce max credential size for readability --- src/ctap/mod.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 2e095ed..dd9cabe 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -86,6 +86,7 @@ const USE_SIGNATURE_COUNTER: bool = true; // - (optional) 32 byte for HMAC-secret, // - 32 byte HMAC-SHA256 over everything else. pub const CREDENTIAL_ID_BASE_SIZE: usize = 112; +pub const CREDENTIAL_ID_MAX_SIZE: usize = CREDENTIAL_ID_BASE_SIZE + 32; // Set this bit when checking user presence. const UP_FLAG: u8 = 0x01; // Set this bit when checking user verification. @@ -235,12 +236,10 @@ where credential_id: Vec, rp_id_hash: &[u8], ) -> Result, Ctap2StatusCode> { - let has_cred_random = if credential_id.len() == CREDENTIAL_ID_BASE_SIZE { - false - } else if credential_id.len() == CREDENTIAL_ID_BASE_SIZE + 32 { - true - } else { - return Ok(None); + let has_cred_random = match credential_id.len() { + CREDENTIAL_ID_BASE_SIZE => false, + CREDENTIAL_ID_MAX_SIZE => true, + _ => return Ok(None), }; let master_keys = self.persistent_store.master_keys()?; let payload_size = credential_id.len() - 32; @@ -1154,8 +1153,7 @@ mod test { 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0xC1, 0x00, 0x00, 0x00, 0x00, ]; expected_auth_data.extend(&ctap_state.persistent_store.aaguid().unwrap()); - let credential_size = CREDENTIAL_ID_BASE_SIZE + 32; - expected_auth_data.extend(&[0x00, credential_size as u8]); + expected_auth_data.extend(&[0x00, CREDENTIAL_ID_MAX_SIZE as u8]); assert_eq!( auth_data[0..expected_auth_data.len()], expected_auth_data[..] @@ -1307,10 +1305,9 @@ mod test { ResponseData::AuthenticatorMakeCredential(make_credential_response) => { let auth_data = make_credential_response.auth_data; let offset = 37 + ctap_state.persistent_store.aaguid().unwrap().len(); - let credential_size = CREDENTIAL_ID_BASE_SIZE + 32; assert_eq!(auth_data[offset], 0x00); - assert_eq!(auth_data[offset + 1] as usize, credential_size); - auth_data[offset + 2..offset + 2 + credential_size].to_vec() + assert_eq!(auth_data[offset + 1] as usize, CREDENTIAL_ID_MAX_SIZE); + auth_data[offset + 2..offset + 2 + CREDENTIAL_ID_MAX_SIZE].to_vec() } _ => panic!("Invalid response type"), }; From 174c292f2f700cbfc85dec8da6c0412180e50fff Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Mon, 23 Nov 2020 19:16:48 +0100 Subject: [PATCH 32/54] Adding metadata file used for certification. --- metadata/metadata.json | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 metadata/metadata.json diff --git a/metadata/metadata.json b/metadata/metadata.json new file mode 100644 index 0000000..bb81d0c --- /dev/null +++ b/metadata/metadata.json @@ -0,0 +1,47 @@ +{ + "assertionScheme" : "FIDOV2", + "keyProtection" : 1, + "attestationRootCertificates" : [ + ], + "aaguid" : "664d9f67-84a2-412a-9ff7-b4f7d8ee6d05", + "publicKeyAlgAndEncoding" : 260, + "protocolFamily" : "fido2", + "upv" : [ + { + "major" : 1, + "minor" : 0 + } + ], + "icon" : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAIQ0lEQVR4Ae1aCVSUVRT+kGVYBBQFBYzYFJFNLdPQVksz85QnszRNbaPNzDI0OaIH27VUUnOpzAqXMJNIszKTUEQWRXBnExRiUYEUBATs3lfzJw3LDP/MMOfMPI/M++97///uve+++9797jO7TgVGXLoYsexCdJMCTBZg5BowLQEjNwCYLMBkAUauAdMSMHIDgEVnKqC8/AKOZh2Do6MDAgMGwMbaWu/s6FUBTU1NyMnNQ8bRTPqfheI/SySBzc3N4devLwaGBGFgcBBcXJylNl1WzHQdDVbX1CDr2HEcJYEz6be6ukYteVxdewtFsEL6+vqgSxfduCudKaCgsBCbt27Dmexc8MzLKba2tggOCkDYszNgZmYm51Mq7+pGrTRMcXEJTp3Oli08c1xDVpR8KBW6gC50pgAVVRsoQWcKcHd3w4jht6N7924GKvo/bGl1F+C1fu78eWH+TdebcOeIUEyfOhkHk1OwJXY7OcBqg1OG1hRwICkZ38fF48LFS82EdHLqjkmPT8DihRF4b8nH4L3fkIrsJcCO6cuvYrD+i40qwrOgly5VYNWn65GUfAjhb7wGKysrQ5Jffji8a/ev2PfH/naF2rY9jma/HA+PG9tuX312kLUErly5grj4H9XmN3b7Dix4Kxz33n2H2u+czs5B9Mo1sLS01MlhSJYC0g5noL7+WjNh+NAydsxoMnVL/ETWcamiQmrPzy9AZWUV2C+oW/hY7KTDnUSWDygoKFSRY/pTk0kBo3D/yHvwyovPq7SXlpWr0Noi/PZ7gvAtDg4ObXXrcJssBdTV16sM7O7mJtFaDmhUE1HFxX/SqfGM9J6ykpySim82bRWPHjf1UZK1+itLAT1aMOWkg4ckBhMSVZ2ju5ur1M47yO5f9iAy6l18sHQ59tJsK0vigYNYu36DdPz18vJUNmn1V5YP4Bg+fufuZgz5+nhLzzY2NlKdKwED+qOJhN7xw04h2PETJ0V4rOz0VcwWnDh1WgQ8qWmHlWTxHBIcKD1rsyJLARy/e3t5Ii//rODJx9sLgwYGS/zdessgxGz+Fo2NjWL/f2LiBPxICtuzd5/U5/+VtPQj/yfB368fujk6qtC1QZC1BJiBZ5+eBtt/Z/qxRx9pxpODvT2G3z4UFhYWCHtuBi5fvgx2apqWUaNGavqK2v21ggcUFJ4Th6FpUyapDHzh4kXU1taK7W/l6nWoratT6dMWwfNmDyxa8FZbXWS1aUUB7XGQkZmF5dGr2+um0s7gx8KIufD0vFmlTVsE2UtAHUaCAwMI1vrPOarzDvcZN3aMToXnMfSiAMbzXnj+GXTrpr4jGzwoBOMffoh51GnRiwJYgh5OTpj35utqefOgwAGE/z2tdfyvJU3qxQfcOHAZHYU/Wb2WgJOiG8lSfXjoMMx4agrtHOYSTZcVvSuAham/dg2bt8Ti94RESTYbG2tMfXISQofdJtH0UekUBSgFY+g89rs4uLn1xrgHx8DevquySW+/naoAvUnZxkB6c4Jt8NCpTSYFdKr6DWDwDltAQ0Mjjh0/ifQjGWBsUFflfFERODTOyzsrDVFRUYnsnFzpuZ6AmRMnT3UIcu9QOMwBzocfrSDBq2FHGGBlVRVeCnuGQuEQiSltVDZs/AaHUtLg4XGTSLj08/XFrJkvIjX9MIGxu7BqxVKBKzAkn5uXT3HDPI2H7ZACNm2OFZcZoiLnw5ouNTDau/7zjVi29H1crb2KSpohOzs7nKVtjpnmCxDKwtgBzyBjCV272lGIfAWlZWXo5eKCMzk56EOQWq9eLigimCwh8QDmz52Dfn19UFpahrkRC8nqTig/JX7j4nciM+s4IubNaTZOs05tPGisAAY3+FbH1MmPC+H526PvH4mdu36mVHi2SITE0CHHxbkneJn8RRjA4kUR4ij8+YavxZLp2cNJoMVRkRHIzc8X0FcfyiU2NV0nwYso/J0vhOFLEympaXB3dxVKWfdpNCyIVkLK4JKSli4s4dWXw9BRzFBjH8D5PVbCjYENAx8c8FRV/SUY4z8L5ofjnagFQpB9dOLjmU88kIRIokdRmsy1d2/8smev6N/Q0IDXX3uF6Cy4o1jP/E1GlY9kZOLV2eGIXrUGZWQpyosSdYQrfEam70hocf/+ftK4mlY0VoBC8c89ntra/4ANFoATowprhRifESCFQgGeQR8vTzLxchQSaMLx/ScEikRELhYmXkaZIjP6x4UF5sLoEjs1LgyvLXl/MebMnolGsqa3310ilg+38Zh33TEC1+lfzL/IMdM1LRovAYXCSpgbz8ywoUPEeMp16evtTevxWDMeKigRwibPCuHZmzXzBVhZWgnGrSjbc/KUKhzOH2BInBMrbEn+NMPeXl4Ie3mWBKJyAubJSRPFzZGPlq9ECF2lGXLL4GZjq/OgsQL4oxMnjMey6FVY95k5nJ17CJCT/YDyLgDf6NhEfoADHN6ewt+YJYANPuszzs+MJlHK/B5KkXUxa9kI/f38sGXrd1i6LBpBgQG07eUJ6/D29kT64QwpVOa2kffeJRK0PAFKHtQRnvuYL6KibmdlP0548OUl9sx8BuAs0AOj7xPNnC3KpT2bEWEOeR98YJTYHi1pWQy5dTBKSkpxlvoM8PcjwHSYgMl5yfAdIC41NVfhRRAYO7XQ0KGEJ9aJJcROddqUyXDuyc61ATa2Ngjw7y/eYdSYcUcubjfkHQShnT9aD4YS/tiP7TviseLjD9oZ2jCaW7Y/GbzZkzPz8NBNGksGW62+qnULaHUkA23QugUYqJytsmVSQKuqMZIGkwUYyUS3KqbJAlpVjZE0mCzASCa6VTH/Bnoy/0KF7w+OAAAAAElFTkSuQmCC", + "matcherProtection" : 1, + "supportedExtensions" : [ + { + "id" : "hmac-secret", + "fail_if_unknown" : false + }, + { + "id" : "credProtect", + "fail_if_unknown" : false + } + ], + "cryptoStrength" : 128, + "description" : "OpenSK authenticator", + "authenticatorVersion" : 1, + "isSecondFactorOnly" : false, + "userVerificationDetails" : [ + [ + { + "userVerification" : 1 + }, + { + "userVerification" : 4 + } + ] + ], + "attachmentHint" : 6, + "attestationTypes" : [ + 15880 + ], + "authenticationAlgorithm" : 1, + "tcDisplay" : 0 +} From 90f2d4a24955544fe426eb79a201f18b20253aa3 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Mon, 23 Nov 2020 20:33:01 +0100 Subject: [PATCH 33/54] Fix indentation --- metadata/metadata.json | 55 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/metadata/metadata.json b/metadata/metadata.json index bb81d0c..eedeed9 100644 --- a/metadata/metadata.json +++ b/metadata/metadata.json @@ -1,47 +1,46 @@ { - "assertionScheme" : "FIDOV2", - "keyProtection" : 1, - "attestationRootCertificates" : [ - ], - "aaguid" : "664d9f67-84a2-412a-9ff7-b4f7d8ee6d05", - "publicKeyAlgAndEncoding" : 260, - "protocolFamily" : "fido2", - "upv" : [ + "assertionScheme": "FIDOV2", + "keyProtection": 1, + "attestationRootCertificates": [], + "aaguid": "664d9f67-84a2-412a-9ff7-b4f7d8ee6d05", + "publicKeyAlgAndEncoding": 260, + "protocolFamily": "fido2", + "upv": [ { - "major" : 1, - "minor" : 0 + "major": 1, + "minor": 0 } ], - "icon" : "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAIQ0lEQVR4Ae1aCVSUVRT+kGVYBBQFBYzYFJFNLdPQVksz85QnszRNbaPNzDI0OaIH27VUUnOpzAqXMJNIszKTUEQWRXBnExRiUYEUBATs3lfzJw3LDP/MMOfMPI/M++97///uve+++9797jO7TgVGXLoYsexCdJMCTBZg5BowLQEjNwCYLMBkAUauAdMSMHIDgEVnKqC8/AKOZh2Do6MDAgMGwMbaWu/s6FUBTU1NyMnNQ8bRTPqfheI/SySBzc3N4devLwaGBGFgcBBcXJylNl1WzHQdDVbX1CDr2HEcJYEz6be6ukYteVxdewtFsEL6+vqgSxfduCudKaCgsBCbt27Dmexc8MzLKba2tggOCkDYszNgZmYm51Mq7+pGrTRMcXEJTp3Oli08c1xDVpR8KBW6gC50pgAVVRsoQWcKcHd3w4jht6N7924GKvo/bGl1F+C1fu78eWH+TdebcOeIUEyfOhkHk1OwJXY7OcBqg1OG1hRwICkZ38fF48LFS82EdHLqjkmPT8DihRF4b8nH4L3fkIrsJcCO6cuvYrD+i40qwrOgly5VYNWn65GUfAjhb7wGKysrQ5Jffji8a/ev2PfH/naF2rY9jma/HA+PG9tuX312kLUErly5grj4H9XmN3b7Dix4Kxz33n2H2u+czs5B9Mo1sLS01MlhSJYC0g5noL7+WjNh+NAydsxoMnVL/ETWcamiQmrPzy9AZWUV2C+oW/hY7KTDnUSWDygoKFSRY/pTk0kBo3D/yHvwyovPq7SXlpWr0Noi/PZ7gvAtDg4ObXXrcJssBdTV16sM7O7mJtFaDmhUE1HFxX/SqfGM9J6ykpySim82bRWPHjf1UZK1+itLAT1aMOWkg4ckBhMSVZ2ju5ur1M47yO5f9iAy6l18sHQ59tJsK0vigYNYu36DdPz18vJUNmn1V5YP4Bg+fufuZgz5+nhLzzY2NlKdKwED+qOJhN7xw04h2PETJ0V4rOz0VcwWnDh1WgQ8qWmHlWTxHBIcKD1rsyJLARy/e3t5Ii//rODJx9sLgwYGS/zdessgxGz+Fo2NjWL/f2LiBPxICtuzd5/U5/+VtPQj/yfB368fujk6qtC1QZC1BJiBZ5+eBtt/Z/qxRx9pxpODvT2G3z4UFhYWCHtuBi5fvgx2apqWUaNGavqK2v21ggcUFJ4Th6FpUyapDHzh4kXU1taK7W/l6nWoratT6dMWwfNmDyxa8FZbXWS1aUUB7XGQkZmF5dGr2+um0s7gx8KIufD0vFmlTVsE2UtAHUaCAwMI1vrPOarzDvcZN3aMToXnMfSiAMbzXnj+GXTrpr4jGzwoBOMffoh51GnRiwJYgh5OTpj35utqefOgwAGE/z2tdfyvJU3qxQfcOHAZHYU/Wb2WgJOiG8lSfXjoMMx4agrtHOYSTZcVvSuAham/dg2bt8Ti94RESTYbG2tMfXISQofdJtH0UekUBSgFY+g89rs4uLn1xrgHx8DevquySW+/naoAvUnZxkB6c4Jt8NCpTSYFdKr6DWDwDltAQ0Mjjh0/ifQjGWBsUFflfFERODTOyzsrDVFRUYnsnFzpuZ6AmRMnT3UIcu9QOMwBzocfrSDBq2FHGGBlVRVeCnuGQuEQiSltVDZs/AaHUtLg4XGTSLj08/XFrJkvIjX9MIGxu7BqxVKBKzAkn5uXT3HDPI2H7ZACNm2OFZcZoiLnw5ouNTDau/7zjVi29H1crb2KSpohOzs7nKVtjpnmCxDKwtgBzyBjCV272lGIfAWlZWXo5eKCMzk56EOQWq9eLigimCwh8QDmz52Dfn19UFpahrkRC8nqTig/JX7j4nciM+s4IubNaTZOs05tPGisAAY3+FbH1MmPC+H526PvH4mdu36mVHi2SITE0CHHxbkneJn8RRjA4kUR4ij8+YavxZLp2cNJoMVRkRHIzc8X0FcfyiU2NV0nwYso/J0vhOFLEympaXB3dxVKWfdpNCyIVkLK4JKSli4s4dWXw9BRzFBjH8D5PVbCjYENAx8c8FRV/SUY4z8L5ofjnagFQpB9dOLjmU88kIRIokdRmsy1d2/8smev6N/Q0IDXX3uF6Cy4o1jP/E1GlY9kZOLV2eGIXrUGZWQpyosSdYQrfEam70hocf/+ftK4mlY0VoBC8c89ntra/4ANFoATowprhRifESCFQgGeQR8vTzLxchQSaMLx/ScEikRELhYmXkaZIjP6x4UF5sLoEjs1LgyvLXl/MebMnolGsqa3310ilg+38Zh33TEC1+lfzL/IMdM1LRovAYXCSpgbz8ywoUPEeMp16evtTevxWDMeKigRwibPCuHZmzXzBVhZWgnGrSjbc/KUKhzOH2BInBMrbEn+NMPeXl4Ie3mWBKJyAubJSRPFzZGPlq9ECF2lGXLL4GZjq/OgsQL4oxMnjMey6FVY95k5nJ17CJCT/YDyLgDf6NhEfoADHN6ewt+YJYANPuszzs+MJlHK/B5KkXUxa9kI/f38sGXrd1i6LBpBgQG07eUJ6/D29kT64QwpVOa2kffeJRK0PAFKHtQRnvuYL6KibmdlP0548OUl9sx8BuAs0AOj7xPNnC3KpT2bEWEOeR98YJTYHi1pWQy5dTBKSkpxlvoM8PcjwHSYgMl5yfAdIC41NVfhRRAYO7XQ0KGEJ9aJJcROddqUyXDuyc61ATa2Ngjw7y/eYdSYcUcubjfkHQShnT9aD4YS/tiP7TviseLjD9oZ2jCaW7Y/GbzZkzPz8NBNGksGW62+qnULaHUkA23QugUYqJytsmVSQKuqMZIGkwUYyUS3KqbJAlpVjZE0mCzASCa6VTH/Bnoy/0KF7w+OAAAAAElFTkSuQmCC", - "matcherProtection" : 1, - "supportedExtensions" : [ + "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAQKADAAQAAAABAAAAQAAAAABGUUKwAAAIQ0lEQVR4Ae1aCVSUVRT+kGVYBBQFBYzYFJFNLdPQVksz85QnszRNbaPNzDI0OaIH27VUUnOpzAqXMJNIszKTUEQWRXBnExRiUYEUBATs3lfzJw3LDP/MMOfMPI/M++97///uve+++9797jO7TgVGXLoYsexCdJMCTBZg5BowLQEjNwCYLMBkAUauAdMSMHIDgEVnKqC8/AKOZh2Do6MDAgMGwMbaWu/s6FUBTU1NyMnNQ8bRTPqfheI/SySBzc3N4devLwaGBGFgcBBcXJylNl1WzHQdDVbX1CDr2HEcJYEz6be6ukYteVxdewtFsEL6+vqgSxfduCudKaCgsBCbt27Dmexc8MzLKba2tggOCkDYszNgZmYm51Mq7+pGrTRMcXEJTp3Oli08c1xDVpR8KBW6gC50pgAVVRsoQWcKcHd3w4jht6N7924GKvo/bGl1F+C1fu78eWH+TdebcOeIUEyfOhkHk1OwJXY7OcBqg1OG1hRwICkZ38fF48LFS82EdHLqjkmPT8DihRF4b8nH4L3fkIrsJcCO6cuvYrD+i40qwrOgly5VYNWn65GUfAjhb7wGKysrQ5Jffji8a/ev2PfH/naF2rY9jma/HA+PG9tuX312kLUErly5grj4H9XmN3b7Dix4Kxz33n2H2u+czs5B9Mo1sLS01MlhSJYC0g5noL7+WjNh+NAydsxoMnVL/ETWcamiQmrPzy9AZWUV2C+oW/hY7KTDnUSWDygoKFSRY/pTk0kBo3D/yHvwyovPq7SXlpWr0Noi/PZ7gvAtDg4ObXXrcJssBdTV16sM7O7mJtFaDmhUE1HFxX/SqfGM9J6ykpySim82bRWPHjf1UZK1+itLAT1aMOWkg4ckBhMSVZ2ju5ur1M47yO5f9iAy6l18sHQ59tJsK0vigYNYu36DdPz18vJUNmn1V5YP4Bg+fufuZgz5+nhLzzY2NlKdKwED+qOJhN7xw04h2PETJ0V4rOz0VcwWnDh1WgQ8qWmHlWTxHBIcKD1rsyJLARy/e3t5Ii//rODJx9sLgwYGS/zdessgxGz+Fo2NjWL/f2LiBPxICtuzd5/U5/+VtPQj/yfB368fujk6qtC1QZC1BJiBZ5+eBtt/Z/qxRx9pxpODvT2G3z4UFhYWCHtuBi5fvgx2apqWUaNGavqK2v21ggcUFJ4Th6FpUyapDHzh4kXU1taK7W/l6nWoratT6dMWwfNmDyxa8FZbXWS1aUUB7XGQkZmF5dGr2+um0s7gx8KIufD0vFmlTVsE2UtAHUaCAwMI1vrPOarzDvcZN3aMToXnMfSiAMbzXnj+GXTrpr4jGzwoBOMffoh51GnRiwJYgh5OTpj35utqefOgwAGE/z2tdfyvJU3qxQfcOHAZHYU/Wb2WgJOiG8lSfXjoMMx4agrtHOYSTZcVvSuAham/dg2bt8Ti94RESTYbG2tMfXISQofdJtH0UekUBSgFY+g89rs4uLn1xrgHx8DevquySW+/naoAvUnZxkB6c4Jt8NCpTSYFdKr6DWDwDltAQ0Mjjh0/ifQjGWBsUFflfFERODTOyzsrDVFRUYnsnFzpuZ6AmRMnT3UIcu9QOMwBzocfrSDBq2FHGGBlVRVeCnuGQuEQiSltVDZs/AaHUtLg4XGTSLj08/XFrJkvIjX9MIGxu7BqxVKBKzAkn5uXT3HDPI2H7ZACNm2OFZcZoiLnw5ouNTDau/7zjVi29H1crb2KSpohOzs7nKVtjpnmCxDKwtgBzyBjCV272lGIfAWlZWXo5eKCMzk56EOQWq9eLigimCwh8QDmz52Dfn19UFpahrkRC8nqTig/JX7j4nciM+s4IubNaTZOs05tPGisAAY3+FbH1MmPC+H526PvH4mdu36mVHi2SITE0CHHxbkneJn8RRjA4kUR4ij8+YavxZLp2cNJoMVRkRHIzc8X0FcfyiU2NV0nwYso/J0vhOFLEympaXB3dxVKWfdpNCyIVkLK4JKSli4s4dWXw9BRzFBjH8D5PVbCjYENAx8c8FRV/SUY4z8L5ofjnagFQpB9dOLjmU88kIRIokdRmsy1d2/8smev6N/Q0IDXX3uF6Cy4o1jP/E1GlY9kZOLV2eGIXrUGZWQpyosSdYQrfEam70hocf/+ftK4mlY0VoBC8c89ntra/4ANFoATowprhRifESCFQgGeQR8vTzLxchQSaMLx/ScEikRELhYmXkaZIjP6x4UF5sLoEjs1LgyvLXl/MebMnolGsqa3310ilg+38Zh33TEC1+lfzL/IMdM1LRovAYXCSpgbz8ywoUPEeMp16evtTevxWDMeKigRwibPCuHZmzXzBVhZWgnGrSjbc/KUKhzOH2BInBMrbEn+NMPeXl4Ie3mWBKJyAubJSRPFzZGPlq9ECF2lGXLL4GZjq/OgsQL4oxMnjMey6FVY95k5nJ17CJCT/YDyLgDf6NhEfoADHN6ewt+YJYANPuszzs+MJlHK/B5KkXUxa9kI/f38sGXrd1i6LBpBgQG07eUJ6/D29kT64QwpVOa2kffeJRK0PAFKHtQRnvuYL6KibmdlP0548OUl9sx8BuAs0AOj7xPNnC3KpT2bEWEOeR98YJTYHi1pWQy5dTBKSkpxlvoM8PcjwHSYgMl5yfAdIC41NVfhRRAYO7XQ0KGEJ9aJJcROddqUyXDuyc61ATa2Ngjw7y/eYdSYcUcubjfkHQShnT9aD4YS/tiP7TviseLjD9oZ2jCaW7Y/GbzZkzPz8NBNGksGW62+qnULaHUkA23QugUYqJytsmVSQKuqMZIGkwUYyUS3KqbJAlpVjZE0mCzASCa6VTH/Bnoy/0KF7w+OAAAAAElFTkSuQmCC", + "matcherProtection": 1, + "supportedExtensions": [ { - "id" : "hmac-secret", - "fail_if_unknown" : false + "id": "hmac-secret", + "fail_if_unknown": false }, { - "id" : "credProtect", - "fail_if_unknown" : false + "id": "credProtect", + "fail_if_unknown": false } ], - "cryptoStrength" : 128, - "description" : "OpenSK authenticator", - "authenticatorVersion" : 1, - "isSecondFactorOnly" : false, - "userVerificationDetails" : [ + "cryptoStrength": 128, + "description": "OpenSK authenticator", + "authenticatorVersion": 1, + "isSecondFactorOnly": false, + "userVerificationDetails": [ [ { - "userVerification" : 1 + "userVerification": 1 }, { - "userVerification" : 4 + "userVerification": 4 } ] ], - "attachmentHint" : 6, - "attestationTypes" : [ + "attachmentHint": 6, + "attestationTypes": [ 15880 ], - "authenticationAlgorithm" : 1, - "tcDisplay" : 0 + "authenticationAlgorithm": 1, + "tcDisplay": 0 } From 29ee45de6cf809e1a78250476d914a5e34308163 Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Tue, 24 Nov 2020 11:29:14 +0100 Subject: [PATCH 34/54] Do not crash in the driver for store errors We prefer to return those errors to the fuzzer which can then decide whether they are expected or not (e.g. when starting from a dirty storage, the store is expected to have errors). --- libraries/persistent_store/src/driver.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/libraries/persistent_store/src/driver.rs b/libraries/persistent_store/src/driver.rs index 15001cb..e529274 100644 --- a/libraries/persistent_store/src/driver.rs +++ b/libraries/persistent_store/src/driver.rs @@ -181,6 +181,12 @@ pub enum StoreInvariant { }, } +impl From for StoreInvariant { + fn from(error: StoreError) -> StoreInvariant { + StoreInvariant::StoreError(error) + } +} + impl StoreDriver { /// Provides read-only access to the storage. pub fn storage(&self) -> &BufferStorage { @@ -249,6 +255,10 @@ impl StoreDriverOff { } /// Powers on the store without interruption. + /// + /// # Panics + /// + /// Panics if the store cannot be powered on. pub fn power_on(self) -> Result { Ok(self .partial_power_on(StoreInterruption::none()) @@ -506,8 +516,8 @@ impl StoreDriverOn { /// Checks that the store and model are in sync. fn check_model(&self) -> Result<(), StoreInvariant> { let mut model_content = self.model.content().clone(); - for handle in self.store.iter().unwrap() { - let handle = handle.unwrap(); + for handle in self.store.iter()? { + let handle = handle?; let model_value = match model_content.remove(&handle.get_key()) { None => { return Err(StoreInvariant::OnlyInStore { @@ -516,7 +526,7 @@ impl StoreDriverOn { } Some(x) => x, }; - let store_value = handle.get_value(&self.store).unwrap().into_boxed_slice(); + let store_value = handle.get_value(&self.store)?.into_boxed_slice(); if store_value != model_value { return Err(StoreInvariant::DifferentValue { key: handle.get_key(), @@ -528,7 +538,7 @@ impl StoreDriverOn { if let Some(&key) = model_content.keys().next() { return Err(StoreInvariant::OnlyInModel { key }); } - let store_capacity = self.store.capacity().unwrap().remaining(); + let store_capacity = self.store.capacity()?.remaining(); let model_capacity = self.model.capacity().remaining(); if store_capacity != model_capacity { return Err(StoreInvariant::DifferentCapacity { @@ -544,8 +554,8 @@ impl StoreDriverOn { let format = self.model.format(); let storage = self.store.storage(); let num_words = format.page_size() / format.word_size(); - let head = self.store.head().unwrap(); - let tail = self.store.tail().unwrap(); + let head = self.store.head()?; + let tail = self.store.tail()?; for page in 0..format.num_pages() { // Check the erase cycle of the page. let store_erase = head.cycle(format) + (page < head.page(format)) as Nat; From 0b2ea7d98be2dabe7925ed8c98504631d261b09e Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 24 Nov 2020 15:14:03 +0100 Subject: [PATCH 35/54] makes HMAC secret output reproducible --- src/ctap/pin_protocol_v1.rs | 118 ++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 33 deletions(-) diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index c03b81b..564ca6d 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -19,7 +19,6 @@ use super::status_code::Ctap2StatusCode; use super::storage::PersistentStore; #[cfg(feature = "with_ctap2_1")] use alloc::string::String; -#[cfg(feature = "with_ctap2_1")] use alloc::vec; use alloc::vec::Vec; use arrayref::array_ref; @@ -74,10 +73,9 @@ fn encrypt_hmac_secret_output( let mut cred_random_secret = [0u8; 32]; cred_random_secret.copy_from_slice(cred_random); - // Initialization of 4 blocks in any case makes this function more readable. - let mut blocks = [[0u8; 16]; 4]; // With the if clause restriction above, block_len can only be 2 or 4. let block_len = salt_enc.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; for i in 0..block_len { blocks[i].copy_from_slice(&salt_enc[16 * i..16 * (i + 1)]); } @@ -85,8 +83,8 @@ fn encrypt_hmac_secret_output( let mut decrypted_salt1 = [0u8; 32]; decrypted_salt1[..16].copy_from_slice(&blocks[0]); - let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); decrypted_salt1[16..].copy_from_slice(&blocks[1]); + let output1 = hmac_256::(&cred_random_secret, &decrypted_salt1[..]); for i in 0..2 { blocks[i].copy_from_slice(&output1[16 * i..16 * (i + 1)]); } @@ -638,36 +636,52 @@ impl PinProtocolV1 { #[cfg(test)] mod test { use super::*; - use arrayref::array_refs; use crypto::rng256::ThreadRng256; // Stores a PIN hash corresponding to the dummy PIN "1234". fn set_standard_pin(persistent_store: &mut PersistentStore) { let mut pin = [0u8; 64]; - pin[0] = 0x31; - pin[1] = 0x32; - pin[2] = 0x33; - pin[3] = 0x34; + pin[..4].copy_from_slice(b"1234"); let mut pin_hash = [0u8; 16]; pin_hash.copy_from_slice(&Sha256::hash(&pin[..])[..16]); persistent_store.set_pin_hash(&pin_hash).unwrap(); } + // Encrypts the message with a zero IV and key derived from shared_secret. + fn encrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { + assert!(message.len() % 16 == 0); + let block_len = message.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { + blocks[i][..].copy_from_slice(&message[i * 16..(i + 1) * 16]); + } + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let iv = [0u8; 16]; + cbc_encrypt(&aes_enc_key, iv, &mut blocks); + blocks.iter().flatten().cloned().collect::>() + } + + // Decrypts the message with a zero IV and key derived from shared_secret. + fn decrypt_message(shared_secret: &[u8; 32], message: &[u8]) -> Vec { + assert!(message.len() % 16 == 0); + let block_len = message.len() / 16; + let mut blocks = vec![[0u8; 16]; block_len]; + for i in 0..block_len { + blocks[i][..].copy_from_slice(&message[i * 16..(i + 1) * 16]); + } + let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); + let aes_dec_key = crypto::aes256::DecryptionKey::new(&aes_enc_key); + let iv = [0u8; 16]; + cbc_decrypt(&aes_dec_key, iv, &mut blocks); + blocks.iter().flatten().cloned().collect::>() + } + // Fails on PINs bigger than 64 bytes. fn encrypt_pin(shared_secret: &[u8; 32], pin: Vec) -> Vec { assert!(pin.len() <= 64); let mut padded_pin = [0u8; 64]; padded_pin[..pin.len()].copy_from_slice(&pin[..]); - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); - let mut blocks = [[0u8; 16]; 4]; - let (b0, b1, b2, b3) = array_refs!(&padded_pin, 16, 16, 16, 16); - blocks[0][..].copy_from_slice(b0); - blocks[1][..].copy_from_slice(b1); - blocks[2][..].copy_from_slice(b2); - blocks[3][..].copy_from_slice(b3); - let iv = [0u8; 16]; - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - blocks.iter().flatten().cloned().collect::>() + encrypt_message(shared_secret, &padded_pin) } // Encrypts the dummy PIN "1234". @@ -677,22 +691,10 @@ mod test { // Encrypts the PIN hash corresponding to the dummy PIN "1234". fn encrypt_standard_pin_hash(shared_secret: &[u8; 32]) -> Vec { - let aes_enc_key = crypto::aes256::EncryptionKey::new(shared_secret); let mut pin = [0u8; 64]; - pin[0] = 0x31; - pin[1] = 0x32; - pin[2] = 0x33; - pin[3] = 0x34; + pin[..4].copy_from_slice(b"1234"); let pin_hash = Sha256::hash(&pin); - - let mut blocks = [[0u8; 16]; 1]; - blocks[0].copy_from_slice(&pin_hash[..16]); - let iv = [0u8; 16]; - cbc_encrypt(&aes_enc_key, iv, &mut blocks); - - let mut encrypted_pin_hash = Vec::with_capacity(16); - encrypted_pin_hash.extend(&blocks[0]); - encrypted_pin_hash + encrypt_message(shared_secret, &pin_hash[..16]) } #[test] @@ -1184,6 +1186,56 @@ mod test { output, Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_EXTENSION) ); + + let mut salt_enc = [0x00; 32]; + let cred_random = [0xC9; 32]; + + // Test values to check for reproducibility. + let salt1 = [0x01; 32]; + let salt2 = [0x02; 32]; + let expected_output1 = hmac_256::(&cred_random, &salt1); + let expected_output2 = hmac_256::(&cred_random, &salt2); + + let salt_enc1 = encrypt_message(&shared_secret, &salt1); + salt_enc.copy_from_slice(salt_enc1.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec, &expected_output1); + + let salt_enc2 = &encrypt_message(&shared_secret, &salt2); + salt_enc.copy_from_slice(salt_enc2.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec, &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt12 = [0x00; 64]; + salt12[..32].copy_from_slice(&salt1); + salt12[32..].copy_from_slice(&salt2); + let salt_enc12 = encrypt_message(&shared_secret, &salt12); + salt_enc.copy_from_slice(salt_enc12.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[..32], &expected_output1); + assert_eq!(&output_dec[32..], &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt02 = [0x00; 64]; + salt02[32..].copy_from_slice(&salt2); + let salt_enc02 = encrypt_message(&shared_secret, &salt02); + salt_enc.copy_from_slice(salt_enc02.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[32..], &expected_output2); + + let mut salt_enc = [0x00; 64]; + let mut salt10 = [0x00; 64]; + salt10[..32].copy_from_slice(&salt1); + let salt_enc10 = encrypt_message(&shared_secret, &salt10); + salt_enc.copy_from_slice(salt_enc10.as_slice()); + let output = encrypt_hmac_secret_output(&shared_secret, &salt_enc, &cred_random).unwrap(); + let output_dec = decrypt_message(&shared_secret, &output); + assert_eq!(&output_dec[..32], &expected_output1); } #[cfg(feature = "with_ctap2_1")] From 65f4f2de25b813ccf07871c0e57d8c84db8fd3f9 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 24 Nov 2020 18:11:18 +0100 Subject: [PATCH 36/54] moves shared precheck into helper function --- src/ctap/mod.rs | 69 +++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index caea0a0..13fc951 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -344,6 +344,33 @@ where } } + fn pin_uv_auth_precheck( + &mut self, + pin_uv_auth_param: &Option>, + pin_uv_auth_protocol: Option, + cid: ChannelID, + ) -> Result<(), Ctap2StatusCode> { + if let Some(auth_param) = &pin_uv_auth_param { + // This case was added in FIDO 2.1. + if auth_param.is_empty() { + (self.check_user_presence)(cid)?; + if self.persistent_store.pin_hash()?.is_none() { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); + } + } + + match pin_uv_auth_protocol { + Some(CtapState::::PIN_PROTOCOL_VERSION) => Ok(()), + Some(_) => Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID), + None => Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), + } + } else { + Ok(()) + } + } + fn process_make_credential( &mut self, make_credential_params: AuthenticatorMakeCredentialParameters, @@ -361,26 +388,7 @@ where pin_uv_auth_protocol, } = make_credential_params; - if let Some(auth_param) = &pin_uv_auth_param { - // This case was added in FIDO 2.1. - if auth_param.is_empty() { - (self.check_user_presence)(cid)?; - if self.persistent_store.pin_hash()?.is_none() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - } - - match pin_uv_auth_protocol { - Some(protocol) => { - if protocol != CtapState::::PIN_PROTOCOL_VERSION { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } - None => return Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), - } - } + self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; if !pub_key_cred_params.contains(&ES256_CRED_PARAM) { return Err(Ctap2StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM); @@ -564,26 +572,7 @@ where pin_uv_auth_protocol, } = get_assertion_params; - if let Some(auth_param) = &pin_uv_auth_param { - // This case was added in FIDO 2.1. - if auth_param.is_empty() { - (self.check_user_presence)(cid)?; - if self.persistent_store.pin_hash()?.is_none() { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_NOT_SET); - } else { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_INVALID); - } - } - - match pin_uv_auth_protocol { - Some(protocol) => { - if protocol != CtapState::::PIN_PROTOCOL_VERSION { - return Err(Ctap2StatusCode::CTAP2_ERR_PIN_AUTH_INVALID); - } - } - None => return Err(Ctap2StatusCode::CTAP2_ERR_MISSING_PARAMETER), - } - } + self.pin_uv_auth_precheck(&pin_uv_auth_param, pin_uv_auth_protocol, cid)?; let hmac_secret_input = extensions.map(|e| e.hmac_secret).flatten(); if hmac_secret_input.is_some() && !options.up { From 3dbfae972fa5721e12c48deea2c538136d8e5824 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:12:03 +0100 Subject: [PATCH 37/54] Always insert attestation material in the store --- src/ctap/key_material.rs | 7 +++-- src/ctap/storage.rs | 63 ++++++++++++++++------------------------ 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index 1958040..f8a833f 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -12,10 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub const AAGUID: &[u8; 16] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); +pub const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; +pub const AAGUID_LENGTH: usize = 16; + +pub const AAGUID: &[u8; AAGUID_LENGTH] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); pub const ATTESTATION_CERTIFICATE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_cert.bin")); -pub const ATTESTATION_PRIVATE_KEY: &[u8; 32] = +pub const ATTESTATION_PRIVATE_KEY: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_pkey.bin")); diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 127dc23..5793e6c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -15,9 +15,9 @@ #[cfg(feature = "with_ctap2_1")] use crate::ctap::data_formats::{extract_array, extract_text_string}; use crate::ctap::data_formats::{CredentialProtectionPolicy, PublicKeyCredentialSource}; +use crate::ctap::key_material; use crate::ctap::pin_protocol_v1::PIN_AUTH_LENGTH; use crate::ctap::status_code::Ctap2StatusCode; -use crate::ctap::{key_material, USE_BATCH_ATTESTATION}; use crate::embedded_flash::{self, StoreConfig, StoreEntry, StoreError}; use alloc::string::String; #[cfg(any(test, feature = "ram_storage", feature = "with_ctap2_1"))] @@ -76,8 +76,6 @@ const MIN_PIN_LENGTH_RP_IDS: usize = 9; const NUM_TAGS: usize = 10; const MAX_PIN_RETRIES: u8 = 8; -const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; -const AAGUID_LENGTH: usize = 16; #[cfg(feature = "with_ctap2_1")] const DEFAULT_MIN_PIN_LENGTH: u8 = 4; // TODO(kaczmarczyck) use this for the minPinLength extension @@ -231,17 +229,16 @@ impl PersistentStore { }) .unwrap(); } - // The following 3 entries are meant to be written by vendor-specific commands. - if USE_BATCH_ATTESTATION { - if self.store.find_one(&Key::AttestationPrivateKey).is_none() { - self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) - .unwrap(); - } - if self.store.find_one(&Key::AttestationCertificate).is_none() { - self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) - .unwrap(); - } + // The following 2 entries are meant to be written by vendor-specific commands. + if self.store.find_one(&Key::AttestationPrivateKey).is_none() { + self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) + .unwrap(); } + if self.store.find_one(&Key::AttestationCertificate).is_none() { + self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) + .unwrap(); + } + if self.store.find_one(&Key::Aaguid).is_none() { self.set_aaguid(key_material::AAGUID).unwrap(); } @@ -525,20 +522,24 @@ impl PersistentStore { pub fn attestation_private_key( &self, - ) -> Result, Ctap2StatusCode> { + ) -> Result, Ctap2StatusCode> { let data = match self.store.find_one(&Key::AttestationPrivateKey) { None => return Ok(None), Some((_, entry)) => entry.data, }; - if data.len() != ATTESTATION_PRIVATE_KEY_LENGTH { + if data.len() != key_material::ATTESTATION_PRIVATE_KEY_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(Some(array_ref!(data, 0, ATTESTATION_PRIVATE_KEY_LENGTH))) + Ok(Some(array_ref!( + data, + 0, + key_material::ATTESTATION_PRIVATE_KEY_LENGTH + ))) } pub fn set_attestation_private_key( &mut self, - attestation_private_key: &[u8; ATTESTATION_PRIVATE_KEY_LENGTH], + attestation_private_key: &[u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH], ) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: ATTESTATION_PRIVATE_KEY, @@ -576,19 +577,22 @@ impl PersistentStore { Ok(()) } - pub fn aaguid(&self) -> Result<[u8; AAGUID_LENGTH], Ctap2StatusCode> { + pub fn aaguid(&self) -> Result<[u8; key_material::AAGUID_LENGTH], Ctap2StatusCode> { let (_, entry) = self .store .find_one(&Key::Aaguid) .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; let data = entry.data; - if data.len() != AAGUID_LENGTH { + if data.len() != key_material::AAGUID_LENGTH { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); } - Ok(*array_ref![data, 0, AAGUID_LENGTH]) + Ok(*array_ref![data, 0, key_material::AAGUID_LENGTH]) } - pub fn set_aaguid(&mut self, aaguid: &[u8; AAGUID_LENGTH]) -> Result<(), Ctap2StatusCode> { + pub fn set_aaguid( + &mut self, + aaguid: &[u8; key_material::AAGUID_LENGTH], + ) -> Result<(), Ctap2StatusCode> { let entry = StoreEntry { tag: AAGUID, data: aaguid, @@ -996,23 +1000,6 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); - // Make sure the attestation are absent. There is no batch attestation in tests. - assert!(persistent_store - .attestation_private_key() - .unwrap() - .is_none()); - assert!(persistent_store - .attestation_certificate() - .unwrap() - .is_none()); - - // Make sure the persistent keys are initialized. - persistent_store - .set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) - .unwrap(); - persistent_store - .set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) - .unwrap(); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); // The persistent keys stay initialized and preserve their value after a reset. From 026b4a66ac1d983c09d1bf9385adf52ad94411ba Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:26:08 +0100 Subject: [PATCH 38/54] Fix CTAP2 batch attestation --- src/ctap/mod.rs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 13fc951..e92afb7 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -522,25 +522,27 @@ where let mut signature_data = auth_data.clone(); signature_data.extend(client_data_hash); - // We currently use the presence of the attestation private key in the persistent storage to - // decide whether batch attestation is needed. - let (signature, x5c) = match self.persistent_store.attestation_private_key()? { - Some(attestation_private_key) => { - let attestation_key = - crypto::ecdsa::SecKey::from_bytes(attestation_private_key).unwrap(); - let attestation_certificate = self - .persistent_store - .attestation_certificate()? - .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; - ( - attestation_key.sign_rfc6979::(&signature_data), - Some(vec![attestation_certificate]), - ) - } - None => ( + + let (signature, x5c) = if USE_BATCH_ATTESTATION { + let attestation_private_key = self + .persistent_store + .attestation_private_key()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + let attestation_key = + crypto::ecdsa::SecKey::from_bytes(attestation_private_key).unwrap(); + let attestation_certificate = self + .persistent_store + .attestation_certificate()? + .ok_or(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR)?; + ( + attestation_key.sign_rfc6979::(&signature_data), + Some(vec![attestation_certificate]), + ) + } else { + ( sk.sign_rfc6979::(&signature_data), None, - ), + ) }; let attestation_statement = PackedAttestationStatement { alg: SignatureAlgorithm::ES256 as i64, From 41f7cc7b141db809d146d522be142635447b8d52 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:31:05 +0100 Subject: [PATCH 39/54] CTAP1/U2F accesses attestation material through the store. --- src/ctap/ctap1.rs | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index a5a7921..9eb1186 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -13,7 +13,6 @@ // limitations under the License. use super::hid::ChannelID; -use super::key_material::{ATTESTATION_CERTIFICATE, ATTESTATION_PRIVATE_KEY}; use super::status_code::Ctap2StatusCode; use super::CtapState; use alloc::vec::Vec; @@ -295,14 +294,22 @@ impl Ctap1Command { return Err(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG); } - let mut response = - Vec::with_capacity(105 + key_handle.len() + ATTESTATION_CERTIFICATE.len()); + let certificate = match ctap_state.persistent_store.attestation_certificate() { + Ok(Some(value)) => value, + _ => return Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), + }; + let private_key = match ctap_state.persistent_store.attestation_private_key() { + Ok(Some(value)) => value, + _ => return Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), + }; + + let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len()); response.push(Ctap1Command::LEGACY_BYTE); let user_pk = pk.to_uncompressed(); response.extend_from_slice(&user_pk); response.push(key_handle.len() as u8); response.extend(key_handle.clone()); - response.extend_from_slice(&ATTESTATION_CERTIFICATE); + response.extend_from_slice(&certificate); // The first byte is reserved. let mut signature_data = Vec::with_capacity(66 + key_handle.len()); @@ -312,7 +319,7 @@ impl Ctap1Command { signature_data.extend(key_handle); signature_data.extend_from_slice(&user_pk); - let attestation_key = crypto::ecdsa::SecKey::from_bytes(ATTESTATION_PRIVATE_KEY).unwrap(); + let attestation_key = crypto::ecdsa::SecKey::from_bytes(private_key).unwrap(); let signature = attestation_key.sign_rfc6979::(&signature_data); response.extend(signature.to_asn1_der()); @@ -373,7 +380,7 @@ impl Ctap1Command { #[cfg(test)] mod test { - use super::super::{CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; + use super::super::{key_material, CREDENTIAL_ID_BASE_SIZE, USE_SIGNATURE_COUNTER}; use super::*; use crypto::rng256::ThreadRng256; use crypto::Hash256; @@ -434,8 +441,25 @@ mod test { ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = - Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); + Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + // Certificate and private key are missing + assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; + assert!(ctap_state.persistent_store.set_attestation_private_key(&fake_key).is_ok()); + ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); + ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); + let response = + Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + // Certificate is still missing + assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + + let fake_cert = [0x99u8; 100]; // Arbitrary length + assert!(ctap_state.persistent_store.set_attestation_certificate(&fake_cert[..]).is_ok()); + ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); + ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); + let response = + Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE).unwrap(); assert_eq!(response[0], Ctap1Command::LEGACY_BYTE); assert_eq!(response[66], CREDENTIAL_ID_BASE_SIZE as u8); assert!(ctap_state @@ -447,8 +471,8 @@ mod test { .is_some()); const CERT_START: usize = 67 + CREDENTIAL_ID_BASE_SIZE; assert_eq!( - &response[CERT_START..CERT_START + ATTESTATION_CERTIFICATE.len()], - &ATTESTATION_CERTIFICATE[..] + &response[CERT_START..CERT_START + fake_cert.len()], + &fake_cert[..] ); } From f47e1e2a8663e60a3c9b61db246cebfeb193d8f5 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:44:19 +0100 Subject: [PATCH 40/54] Ensure store behaves as expected in prod --- src/ctap/storage.rs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 5793e6c..da8828c 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -229,6 +229,18 @@ impl PersistentStore { }) .unwrap(); } + // TODO(jmichel): remove this when vendor command is in place + #[cfg(not(any(test, feature = "ram_storage")))] + self.load_attestation_from_firmware(); + + if self.store.find_one(&Key::Aaguid).is_none() { + self.set_aaguid(key_material::AAGUID).unwrap(); + } + } + + // TODO(jmichel): remove this function when vendor command is in place. + #[cfg(not(any(test, feature = "ram_storage")))] + fn load_attestation_from_firmware(&mut self) { // The following 2 entries are meant to be written by vendor-specific commands. if self.store.find_one(&Key::AttestationPrivateKey).is_none() { self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) @@ -238,10 +250,6 @@ impl PersistentStore { self.set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) .unwrap(); } - - if self.store.find_one(&Key::Aaguid).is_none() { - self.set_aaguid(key_material::AAGUID).unwrap(); - } } pub fn find_credential( @@ -1000,6 +1008,23 @@ mod test { let mut rng = ThreadRng256 {}; let mut persistent_store = PersistentStore::new(&mut rng); + // Make sure the attestation are absent. There is no batch attestation in tests. + assert!(persistent_store + .attestation_private_key() + .unwrap() + .is_none()); + assert!(persistent_store + .attestation_certificate() + .unwrap() + .is_none()); + + // Make sure the persistent keys are initialized. + persistent_store + .set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) + .unwrap(); + persistent_store + .set_attestation_certificate(key_material::ATTESTATION_CERTIFICATE) + .unwrap(); assert_eq!(&persistent_store.aaguid().unwrap(), key_material::AAGUID); // The persistent keys stay initialized and preserve their value after a reset. From f2b3ca4029227e532eb88972c766fd8dc99aba5d Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:44:52 +0100 Subject: [PATCH 41/54] Make private key sensitive and ensure attestation is OTP --- src/ctap/storage.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index da8828c..8e396b6 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -552,11 +552,11 @@ impl PersistentStore { let entry = StoreEntry { tag: ATTESTATION_PRIVATE_KEY, data: attestation_private_key, - sensitive: false, + sensitive: true, }; match self.store.find_one(&Key::AttestationPrivateKey) { None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, + _ => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } Ok(()) } @@ -580,7 +580,7 @@ impl PersistentStore { }; match self.store.find_one(&Key::AttestationCertificate) { None => self.store.insert(entry)?, - Some((index, _)) => self.store.replace(index, entry)?, + _ => return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), } Ok(()) } From d491492554e5e86a40d1d71b524fec569144217f Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Wed, 25 Nov 2020 17:48:47 +0100 Subject: [PATCH 42/54] Format --- src/ctap/ctap1.rs | 16 ++++++++++------ src/ctap/key_material.rs | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 9eb1186..0fa4745 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -440,22 +440,26 @@ mod test { let message = create_register_message(&application); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); - let response = - Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate and private key are missing assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; - assert!(ctap_state.persistent_store.set_attestation_private_key(&fake_key).is_ok()); + assert!(ctap_state + .persistent_store + .set_attestation_private_key(&fake_key) + .is_ok()); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); - let response = - Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); + let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate is still missing assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); let fake_cert = [0x99u8; 100]; // Arbitrary length - assert!(ctap_state.persistent_store.set_attestation_certificate(&fake_cert[..]).is_ok()); + assert!(ctap_state + .persistent_store + .set_attestation_certificate(&fake_cert[..]) + .is_ok()); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = diff --git a/src/ctap/key_material.rs b/src/ctap/key_material.rs index f8a833f..2563798 100644 --- a/src/ctap/key_material.rs +++ b/src/ctap/key_material.rs @@ -15,7 +15,8 @@ pub const ATTESTATION_PRIVATE_KEY_LENGTH: usize = 32; pub const AAGUID_LENGTH: usize = 16; -pub const AAGUID: &[u8; AAGUID_LENGTH] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); +pub const AAGUID: &[u8; AAGUID_LENGTH] = + include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); pub const ATTESTATION_CERTIFICATE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/opensk_cert.bin")); From 3ae59ce1ecfdfaa7a2e84f86d0c8050d2b1e08e5 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 17 Sep 2020 09:23:17 +0200 Subject: [PATCH 43/54] GetNextAssertion command minimal implementation This still lacks order of credentials and timeouts. --- src/ctap/command.rs | 2 +- src/ctap/data_formats.rs | 6 +- src/ctap/mod.rs | 168 +++++++++++++++++++++++++-------------- 3 files changed, 112 insertions(+), 64 deletions(-) diff --git a/src/ctap/command.rs b/src/ctap/command.rs index 5c58234..1b4b96a 100644 --- a/src/ctap/command.rs +++ b/src/ctap/command.rs @@ -57,8 +57,8 @@ impl Command { const AUTHENTICATOR_GET_INFO: u8 = 0x04; const AUTHENTICATOR_CLIENT_PIN: u8 = 0x06; const AUTHENTICATOR_RESET: u8 = 0x07; - // TODO(kaczmarczyck) use or remove those constants const AUTHENTICATOR_GET_NEXT_ASSERTION: u8 = 0x08; + // TODO(kaczmarczyck) use or remove those constants const AUTHENTICATOR_BIO_ENROLLMENT: u8 = 0x09; const AUTHENTICATOR_CREDENTIAL_MANAGEMENT: u8 = 0xA0; const AUTHENTICATOR_SELECTION: u8 = 0xB0; diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 45ebf9f..35d5bcf 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -308,7 +308,8 @@ impl TryFrom for GetAssertionExtensions { } } -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct GetAssertionHmacSecretInput { pub key_agreement: CoseKey, pub salt_enc: Vec, @@ -603,7 +604,8 @@ impl PublicKeyCredentialSource { // TODO(kaczmarczyck) we could decide to split this data type up // It depends on the algorithm though, I think. // So before creating a mess, this is my workaround. -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] +#[derive(Clone)] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] pub struct CoseKey(pub BTreeMap); // This is the algorithm specifier that is supposed to be used in a COSE key diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 13fc951..f037655 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -33,9 +33,9 @@ use self::command::{ #[cfg(feature = "with_ctap2_1")] use self::data_formats::AuthenticatorTransport; use self::data_formats::{ - CredentialProtectionPolicy, PackedAttestationStatement, PublicKeyCredentialDescriptor, - PublicKeyCredentialParameter, PublicKeyCredentialSource, PublicKeyCredentialType, - PublicKeyCredentialUserEntity, SignatureAlgorithm, + CredentialProtectionPolicy, GetAssertionHmacSecretInput, PackedAttestationStatement, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameter, PublicKeyCredentialSource, + PublicKeyCredentialType, PublicKeyCredentialUserEntity, SignatureAlgorithm, }; use self::hid::ChannelID; #[cfg(feature = "with_ctap2_1")] @@ -133,6 +133,14 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { } } +#[derive(Clone)] +struct AssertionInput { + client_data_hash: Vec, + auth_data: Vec, + uv: bool, + hmac_secret_input: Option, +} + // This struct currently holds all state, not only the persistent memory. The persistent members are // in the persistent store field. pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>> @@ -147,6 +155,8 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( accepts_reset: bool, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, + next_credentials: Vec, + next_assertion_input: Option, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -173,6 +183,8 @@ where U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), + next_credentials: vec![], + next_assertion_input: None, } } @@ -314,13 +326,13 @@ where Command::AuthenticatorGetAssertion(params) => { self.process_get_assertion(params, cid) } + Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(), Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), Command::AuthenticatorReset => self.process_reset(cid), #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), - // TODO(kaczmarczyck) implement GetNextAssertion and FIDO 2.1 commands - _ => self.process_unknown_command(), + // TODO(kaczmarczyck) implement FIDO 2.1 commands }; #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Sending response: {:#?}", response).unwrap(); @@ -557,6 +569,63 @@ where )) } + fn assertion_response( + &self, + credential: &PublicKeyCredentialSource, + assertion_input: AssertionInput, + ) -> Result { + let AssertionInput { + client_data_hash, + mut auth_data, + uv, + hmac_secret_input, + } = assertion_input; + + // Process extensions. + if let Some(hmac_secret_input) = hmac_secret_input { + let encrypted_output = self + .pin_protocol_v1 + .process_hmac_secret(hmac_secret_input, &credential.cred_random)?; + let extensions_output = cbor_map! { + "hmac-secret" => encrypted_output, + }; + if !cbor::write(extensions_output, &mut auth_data) { + return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); + } + } + + let mut signature_data = auth_data.clone(); + signature_data.extend(client_data_hash); + let signature = credential + .private_key + .sign_rfc6979::(&signature_data); + + let cred_desc = PublicKeyCredentialDescriptor { + key_type: PublicKeyCredentialType::PublicKey, + key_id: credential.credential_id.clone(), + transports: None, // You can set USB as a hint here. + }; + let user = if uv && !credential.user_handle.is_empty() { + Some(PublicKeyCredentialUserEntity { + user_id: credential.user_handle.clone(), + user_name: None, + user_display_name: credential.other_ui.clone(), + user_icon: None, + }) + } else { + None + }; + Ok(ResponseData::AuthenticatorGetAssertion( + AuthenticatorGetAssertionResponse { + credential: Some(cred_desc), + auth_data, + signature: signature.to_asn1_der(), + user, + number_of_credentials: None, + }, + )) + } + fn process_get_assertion( &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, @@ -643,17 +712,19 @@ where } found_credentials } else { - // TODO(kaczmarczyck) use GetNextAssertion self.persistent_store.filter_credential(&rp_id, !has_uv)? }; - let credential = if let Some(credential) = credentials.first() { - credential - } else { - decrypted_credential - .as_ref() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? - }; + let credential = + if let Some((credential, remaining_credentials)) = credentials.split_first() { + // TODO(kaczmarczyck) correct credential order + self.next_credentials = remaining_credentials.to_vec(); + credential + } else { + decrypted_credential + .as_ref() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? + }; if options.up { (self.check_user_presence)(cid)?; @@ -661,50 +732,30 @@ where self.increment_global_signature_counter()?; - let mut auth_data = self.generate_auth_data(&rp_id_hash, flags)?; - // Process extensions. - if let Some(hmac_secret_input) = hmac_secret_input { - let encrypted_output = self - .pin_protocol_v1 - .process_hmac_secret(hmac_secret_input, &credential.cred_random)?; - let extensions_output = cbor_map! { - "hmac-secret" => encrypted_output, - }; - if !cbor::write(extensions_output, &mut auth_data) { - return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_RESPONSE_CANNOT_WRITE_CBOR); - } + let assertion_input = AssertionInput { + client_data_hash, + auth_data: self.generate_auth_data(&rp_id_hash, flags)?, + uv: flags & UV_FLAG != 0, + hmac_secret_input, + }; + if !self.next_credentials.is_empty() { + self.next_assertion_input = Some(assertion_input.clone()); } + self.assertion_response(credential, assertion_input) + } - let mut signature_data = auth_data.clone(); - signature_data.extend(client_data_hash); - let signature = credential - .private_key - .sign_rfc6979::(&signature_data); - - let cred_desc = PublicKeyCredentialDescriptor { - key_type: PublicKeyCredentialType::PublicKey, - key_id: credential.credential_id.clone(), - transports: None, // You can set USB as a hint here. - }; - let user = if (flags & UV_FLAG != 0) && !credential.user_handle.is_empty() { - Some(PublicKeyCredentialUserEntity { - user_id: credential.user_handle.clone(), - user_name: None, - user_display_name: credential.other_ui.clone(), - user_icon: None, - }) + fn process_get_next_assertion(&mut self) -> Result { + // TODO(kaczmarczyck) introduce timer + let assertion_input = self + .next_assertion_input + .clone() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if let Some(credential) = self.next_credentials.pop() { + self.assertion_response(&credential, assertion_input) } else { - None - }; - Ok(ResponseData::AuthenticatorGetAssertion( - AuthenticatorGetAssertionResponse { - credential: Some(cred_desc), - auth_data, - signature: signature.to_asn1_der(), - user, - number_of_credentials: None, - }, - )) + self.next_assertion_input = None; + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + } } fn process_get_info(&self) -> Result { @@ -786,10 +837,6 @@ where Ok(ResponseData::AuthenticatorSelection) } - fn process_unknown_command(&self) -> Result { - Err(Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND) - } - pub fn generate_auth_data( &self, rp_id_hash: &[u8], @@ -813,9 +860,8 @@ where #[cfg(test)] mod test { use super::data_formats::{ - CoseKey, GetAssertionExtensions, GetAssertionHmacSecretInput, GetAssertionOptions, - MakeCredentialExtensions, MakeCredentialOptions, PublicKeyCredentialRpEntity, - PublicKeyCredentialUserEntity, + CoseKey, GetAssertionExtensions, GetAssertionOptions, MakeCredentialExtensions, + MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; use crypto::rng256::ThreadRng256; From af4eef808519b10055a92cce3aa8a97960df3d2a Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 17 Nov 2020 16:57:03 +0100 Subject: [PATCH 44/54] adds credential ordering --- src/ctap/data_formats.rs | 7 +++++++ src/ctap/mod.rs | 11 +++++++++-- src/ctap/storage.rs | 25 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index 35d5bcf..fa747bc 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -500,6 +500,7 @@ pub struct PublicKeyCredentialSource { pub other_ui: Option, pub cred_random: Option>, pub cred_protect_policy: Option, + pub creation_order: u64, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -512,6 +513,7 @@ enum PublicKeyCredentialSourceField { OtherUi = 4, CredRandom = 5, CredProtectPolicy = 6, + CreationOrder = 7, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: none. @@ -535,6 +537,7 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::OtherUi => credential.other_ui, PublicKeyCredentialSourceField::CredRandom => credential.cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, + PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, } } } @@ -552,6 +555,7 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::OtherUi => other_ui, PublicKeyCredentialSourceField::CredRandom => cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, + PublicKeyCredentialSourceField::CreationOrder => creation_order, } = extract_map(cbor_value)?; } @@ -569,6 +573,7 @@ impl TryFrom for PublicKeyCredentialSource { let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; + let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -588,6 +593,7 @@ impl TryFrom for PublicKeyCredentialSource { other_ui, cred_random, cred_protect_policy, + creation_order, }) } } @@ -1357,6 +1363,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert_eq!( diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index f037655..331ccc1 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -155,6 +155,7 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( accepts_reset: bool, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, + // Sorted by ascending order of creation, so the last element is the most recent one. next_credentials: Vec, next_assertion_input: Option, } @@ -302,6 +303,7 @@ where other_ui: None, cred_random, cred_protect_policy: None, + creation_order: 0, })) } @@ -424,7 +426,6 @@ where } else { None }; - // TODO(kaczmarczyck) unsolicited output for default credProtect level let has_extension_output = use_hmac_extension || cred_protect_policy.is_some(); let rp_id = rp.rp_id; @@ -501,6 +502,7 @@ where .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, + creation_order: self.persistent_store.new_creation_order()?, }; self.persistent_store.store_credential(credential_source)?; random_id @@ -717,8 +719,9 @@ where let credential = if let Some((credential, remaining_credentials)) = credentials.split_first() { - // TODO(kaczmarczyck) correct credential order self.next_credentials = remaining_credentials.to_vec(); + self.next_credentials + .sort_unstable_by_key(|c| c.creation_order); credential } else { decrypted_credential @@ -1091,6 +1094,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1457,6 +1461,7 @@ mod test { cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1507,6 +1512,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + creation_order: 0, }; assert!(ctap_state .persistent_store @@ -1550,6 +1556,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 127dc23..6e4506a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -337,6 +337,26 @@ impl PersistentStore { .count()) } + pub fn new_creation_order(&self) -> Result { + Ok(self + .store + .find_all(&Key::Credential { + rp_id: None, + credential_id: None, + user_handle: None, + }) + .filter_map(|(_, entry)| { + debug_assert_eq!(entry.tag, TAG_CREDENTIAL); + let credential = deserialize_credential(entry.data); + debug_assert!(credential.is_some()); + credential + }) + .map(|c| c.creation_order) + .max() + .unwrap_or(0) + + 1) + } + pub fn global_signature_counter(&self) -> Result { Ok(self .store @@ -690,6 +710,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, } } @@ -853,6 +874,7 @@ mod test { cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), + creation_order: 0, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -894,6 +916,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -913,6 +936,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), + creation_order: 0, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1090,6 +1114,7 @@ mod test { other_ui: None, cred_random: None, cred_protect_policy: None, + creation_order: 0, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From 5ff381678251473c0417b77f4b9a657d95d870a4 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Mon, 23 Nov 2020 17:33:20 +0100 Subject: [PATCH 45/54] sets the correct user and number of credentials --- src/ctap/mod.rs | 209 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 162 insertions(+), 47 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 331ccc1..1f91f4f 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -137,7 +137,6 @@ fn truncate_to_char_boundary(s: &str, mut max: usize) -> &str { struct AssertionInput { client_data_hash: Vec, auth_data: Vec, - uv: bool, hmac_secret_input: Option, } @@ -571,15 +570,17 @@ where )) } + // Processes the input of a get_assertion operation for a given credential + // and returns the correct Get(Next)Assertion response. fn assertion_response( &self, credential: &PublicKeyCredentialSource, assertion_input: AssertionInput, + number_of_credentials: Option, ) -> Result { let AssertionInput { client_data_hash, mut auth_data, - uv, hmac_secret_input, } = assertion_input; @@ -607,7 +608,7 @@ where key_id: credential.credential_id.clone(), transports: None, // You can set USB as a hint here. }; - let user = if uv && !credential.user_handle.is_empty() { + let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { user_id: credential.user_handle.clone(), user_name: None, @@ -623,11 +624,37 @@ where auth_data, signature: signature.to_asn1_der(), user, - number_of_credentials: None, + number_of_credentials: number_of_credentials.map(|n| n as u64), }, )) } + // Returns the first applicable credential from the allow list. + fn get_any_credential_from_allow_list( + &mut self, + allow_list: Vec, + rp_id: &str, + rp_id_hash: &[u8], + has_uv: bool, + ) -> Result, Ctap2StatusCode> { + for allowed_credential in allow_list { + let credential = self.persistent_store.find_credential( + rp_id, + &allowed_credential.key_id, + !has_uv, + )?; + if credential.is_some() { + return Ok(credential); + } + let credential = + self.decrypt_credential_source(allowed_credential.key_id, &rp_id_hash)?; + if credential.is_some() { + return Ok(credential); + } + } + Ok(None) + } + fn process_get_assertion( &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, @@ -690,44 +717,29 @@ where } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut decrypted_credential = None; - let credentials = if let Some(allow_list) = allow_list { - let mut found_credentials = vec![]; - for allowed_credential in allow_list { - match self.persistent_store.find_credential( - &rp_id, - &allowed_credential.key_id, - !has_uv, - )? { - Some(credential) => { - found_credentials.push(credential); - } - None => { - if decrypted_credential.is_none() { - decrypted_credential = self.decrypt_credential_source( - allowed_credential.key_id, - &rp_id_hash, - )?; - } - } - } + let mut credentials = if let Some(allow_list) = allow_list { + if let Some(credential) = + self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)? + { + vec![credential] + } else { + vec![] } - found_credentials } else { self.persistent_store.filter_credential(&rp_id, !has_uv)? }; + // Remove user identifiable information without uv. + if !has_uv { + for credential in &mut credentials { + credential.other_ui = None; + } + } + credentials.sort_unstable_by_key(|c| c.creation_order); - let credential = - if let Some((credential, remaining_credentials)) = credentials.split_first() { - self.next_credentials = remaining_credentials.to_vec(); - self.next_credentials - .sort_unstable_by_key(|c| c.creation_order); - credential - } else { - decrypted_credential - .as_ref() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)? - }; + let (credential, remaining_credentials) = credentials + .split_last() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; + self.next_credentials = remaining_credentials.to_vec(); if options.up { (self.check_user_presence)(cid)?; @@ -738,13 +750,16 @@ where let assertion_input = AssertionInput { client_data_hash, auth_data: self.generate_auth_data(&rp_id_hash, flags)?, - uv: flags & UV_FLAG != 0, hmac_secret_input, }; - if !self.next_credentials.is_empty() { + let number_of_credentials = if self.next_credentials.is_empty() { + self.next_assertion_input = None; + None + } else { self.next_assertion_input = Some(assertion_input.clone()); - } - self.assertion_response(credential, assertion_input) + Some(self.next_credentials.len() + 1) + }; + self.assertion_response(credential, assertion_input, number_of_credentials) } fn process_get_next_assertion(&mut self) -> Result { @@ -753,12 +768,14 @@ where .next_assertion_input .clone() .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - if let Some(credential) = self.next_credentials.pop() { - self.assertion_response(&credential, assertion_input) - } else { + let credential = self + .next_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + if self.next_credentials.is_empty() { self.next_assertion_input = None; - Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) - } + }; + self.assertion_response(&credential, assertion_input, None) } fn process_get_info(&self) -> Result { @@ -1318,7 +1335,13 @@ mod test { 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, ]; assert_eq!(auth_data, expected_auth_data); - assert!(user.is_none()); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0xFA, 0xB1, 0xA2], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); assert!(number_of_credentials.is_none()); } _ => panic!("Invalid response type"), @@ -1539,6 +1562,98 @@ mod test { ); } + #[test] + fn test_process_get_next_assertion() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = + ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + + match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0x02], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert_eq!(number_of_credentials, Some(2)); + } + _ => panic!("Invalid response type"), + } + + let get_assertion_response = ctap_state.process_get_next_assertion(); + match get_assertion_response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: vec![0x01], + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert!(number_of_credentials.is_none()); + } + _ => panic!("Invalid response type"), + } + + let get_assertion_response = ctap_state.process_get_next_assertion(); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + #[test] fn test_process_reset() { let mut rng = ThreadRng256 {}; From ffe19e152b4bc334a188f7e35fcaf1bf51853ca2 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Tue, 24 Nov 2020 17:17:18 +0100 Subject: [PATCH 46/54] moves UP check in GetAssertion before NO_CREDENTIALS --- src/ctap/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 1f91f4f..98058ce 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -736,15 +736,17 @@ where } credentials.sort_unstable_by_key(|c| c.creation_order); + // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. + // For CTAP 2.1, it was moved to a later protocol step. + if options.up { + (self.check_user_presence)(cid)?; + } + let (credential, remaining_credentials) = credentials .split_last() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; self.next_credentials = remaining_credentials.to_vec(); - if options.up { - (self.check_user_presence)(cid)?; - } - self.increment_global_signature_counter()?; let assertion_input = AssertionInput { From ed59ebac0dd3dc5f44057e11c3b8a106d20a46e7 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 14:40:02 +0100 Subject: [PATCH 47/54] command timeout for GetNextAssertion --- fuzz/fuzz_helper/src/lib.rs | 4 +- src/ctap/ctap1.rs | 26 +- src/ctap/hid/mod.rs | 11 +- src/ctap/mod.rs | 490 ++++++++++++++++++++++++------------ src/ctap/storage.rs | 15 ++ src/main.rs | 11 +- 6 files changed, 373 insertions(+), 184 deletions(-) diff --git a/fuzz/fuzz_helper/src/lib.rs b/fuzz/fuzz_helper/src/lib.rs index a6dc1a7..1553f65 100644 --- a/fuzz/fuzz_helper/src/lib.rs +++ b/fuzz/fuzz_helper/src/lib.rs @@ -147,7 +147,7 @@ fn process_message( pub fn process_ctap_any_type(data: &[u8]) { // Initialize ctap state and hid and get the allocated cid. let mut rng = ThreadRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = initialize(&mut ctap_state, &mut ctap_hid); // Wrap input as message with the allocated cid. @@ -165,7 +165,7 @@ pub fn process_ctap_specific_type(data: &[u8], input_type: InputType) { } // Initialize ctap state and hid and get the allocated cid. let mut rng = ThreadRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = initialize(&mut ctap_state, &mut ctap_hid); // Wrap input as message with allocated cid and command type. diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index a5a7921..f299926 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -427,7 +427,7 @@ mod test { fn test_process_register() { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let application = [0x0A; 32]; let message = create_register_message(&application); @@ -456,7 +456,7 @@ mod test { fn test_process_register_bad_message() { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let application = [0x0A; 32]; let message = create_register_message(&application); @@ -476,7 +476,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); @@ -490,7 +490,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -508,7 +508,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -527,7 +527,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -553,7 +553,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -573,7 +573,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -593,7 +593,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -613,7 +613,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -640,7 +640,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); let sk = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); let rp_id = "example.com"; let application = crypto::sha256::Sha256::hash(rp_id.as_bytes()); @@ -672,7 +672,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); @@ -689,7 +689,7 @@ mod test { let mut rng = ThreadRng256 {}; let dummy_user_presence = |_| panic!("Unexpected user presence check in CTAP1"); - let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence); + let mut ctap_state = CtapState::new(&mut rng, dummy_user_presence, START_CLOCK_VALUE); ctap_state.u2f_up_state.consume_up(START_CLOCK_VALUE); ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); diff --git a/src/ctap/hid/mod.rs b/src/ctap/hid/mod.rs index a6a18f7..3874792 100644 --- a/src/ctap/hid/mod.rs +++ b/src/ctap/hid/mod.rs @@ -200,7 +200,8 @@ impl CtapHid { // Each transaction is atomic, so we process the command directly here and // don't handle any other packet in the meantime. // TODO: Send keep-alive packets in the meantime. - let response = ctap_state.process_command(&message.payload, cid); + let response = + ctap_state.process_command(&message.payload, cid, clock_value); if let Some(iterator) = CtapHid::split_message(Message { cid, cmd: CtapHid::COMMAND_CBOR, @@ -520,7 +521,7 @@ mod test { fn test_spurious_continuation_packet() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let mut packet = [0x00; 64]; @@ -541,7 +542,7 @@ mod test { fn test_command_init() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let reply = process_messages( @@ -586,7 +587,7 @@ mod test { fn test_command_init_for_sync() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = cid_from_init(&mut ctap_hid, &mut ctap_state); @@ -646,7 +647,7 @@ mod test { fn test_command_ping() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut ctap_hid = CtapHid::new(); let cid = cid_from_init(&mut ctap_hid, &mut ctap_state); diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 98058ce..4675350 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -47,6 +47,7 @@ use self::response::{ }; use self::status_code::Ctap2StatusCode; use self::storage::PersistentStore; +use self::timed_permission::TimedPermission; #[cfg(feature = "with_ctap1")] use self::timed_permission::U2fUserPresenceState; use alloc::collections::BTreeMap; @@ -65,7 +66,7 @@ use crypto::sha256::Sha256; use crypto::Hash256; #[cfg(feature = "debug_ctap")] use libtock_drivers::console::Console; -use libtock_drivers::timer::{Duration, Timestamp}; +use libtock_drivers::timer::{ClockValue, Duration}; // This flag enables or disables basic attestation for FIDO2. U2F is unaffected by // this setting. The basic attestation uses the signing key from key_material.rs @@ -99,7 +100,8 @@ const ED_FLAG: u8 = 0x80; pub const TOUCH_TIMEOUT_MS: isize = 30000; #[cfg(feature = "with_ctap1")] const U2F_UP_PROMPT_TIMEOUT: Duration = Duration::from_ms(10000); -const RESET_TIMEOUT_MS: isize = 10000; +const RESET_TIMEOUT_DURATION: Duration = Duration::from_ms(10000); +const STATEFUL_COMMAND_TIMEOUT_DURATION: Duration = Duration::from_ms(30000); pub const FIDO2_VERSION_STRING: &str = "FIDO_2_0"; #[cfg(feature = "with_ctap1")] @@ -140,6 +142,17 @@ struct AssertionInput { hmac_secret_input: Option, } +struct AssertionState { + assertion_input: AssertionInput, + // Sorted by ascending order of creation, so the last element is the most recent one. + next_credentials: Vec, +} + +enum StatefulCommand { + Reset, + GetAssertion(AssertionState), +} + // This struct currently holds all state, not only the persistent memory. The persistent members are // in the persistent store field. pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<(), Ctap2StatusCode>> @@ -150,13 +163,11 @@ pub struct CtapState<'a, R: Rng256, CheckUserPresence: Fn(ChannelID) -> Result<( check_user_presence: CheckUserPresence, persistent_store: PersistentStore, pin_protocol_v1: PinProtocolV1, - // This variable will be irreversibly set to false RESET_TIMEOUT_MS milliseconds after boot. - accepts_reset: bool, #[cfg(feature = "with_ctap1")] pub u2f_up_state: U2fUserPresenceState, - // Sorted by ascending order of creation, so the last element is the most recent one. - next_credentials: Vec, - next_assertion_input: Option, + // The state initializes to Reset and its timeout, and never goes back to Reset. + stateful_command_permission: TimedPermission, + stateful_command_type: Option, } impl<'a, R, CheckUserPresence> CtapState<'a, R, CheckUserPresence> @@ -169,6 +180,7 @@ where pub fn new( rng: &'a mut R, check_user_presence: CheckUserPresence, + now: ClockValue, ) -> CtapState<'a, R, CheckUserPresence> { let persistent_store = PersistentStore::new(rng); let pin_protocol_v1 = PinProtocolV1::new(rng); @@ -177,20 +189,26 @@ where check_user_presence, persistent_store, pin_protocol_v1, - accepts_reset: true, #[cfg(feature = "with_ctap1")] u2f_up_state: U2fUserPresenceState::new( U2F_UP_PROMPT_TIMEOUT, Duration::from_ms(TOUCH_TIMEOUT_MS), ), - next_credentials: vec![], - next_assertion_input: None, + stateful_command_permission: TimedPermission::granted(now, RESET_TIMEOUT_DURATION), + stateful_command_type: Some(StatefulCommand::Reset), } } - pub fn check_disable_reset(&mut self, timestamp: Timestamp) { - if timestamp - Timestamp::::from_ms(0) > Duration::from_ms(RESET_TIMEOUT_MS) { - self.accepts_reset = false; + pub fn update_command_permission(&mut self, now: ClockValue) { + self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + } + + fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { + self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + if self.stateful_command_permission.is_granted(now) { + Ok(()) + } else { + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) } } @@ -306,7 +324,12 @@ where })) } - pub fn process_command(&mut self, command_cbor: &[u8], cid: ChannelID) -> Vec { + pub fn process_command( + &mut self, + command_cbor: &[u8], + cid: ChannelID, + now: ClockValue, + ) -> Vec { let cmd = Command::deserialize(command_cbor); #[cfg(feature = "debug_ctap")] writeln!(&mut Console::new(), "Received command: {:#?}", cmd).unwrap(); @@ -320,17 +343,32 @@ where Duration::from_ms(TOUCH_TIMEOUT_MS), ); } + match (&command, &self.stateful_command_type) { + ( + Command::AuthenticatorGetNextAssertion, + Some(StatefulCommand::GetAssertion(_)), + ) => (), + (Command::AuthenticatorReset, Some(StatefulCommand::Reset)) => (), + // GetInfo does not reset stateful commands. + (Command::AuthenticatorGetInfo, _) => (), + // AuthenticatorSelection does not reset stateful commands. + #[cfg(feature = "with_ctap2_1")] + (Command::AuthenticatorSelection, _) => (), + (_, _) => { + self.stateful_command_type = None; + } + } let response = match command { Command::AuthenticatorMakeCredential(params) => { self.process_make_credential(params, cid) } Command::AuthenticatorGetAssertion(params) => { - self.process_get_assertion(params, cid) + self.process_get_assertion(params, cid, now) } - Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(), + Command::AuthenticatorGetNextAssertion => self.process_get_next_assertion(now), Command::AuthenticatorGetInfo => self.process_get_info(), Command::AuthenticatorClientPin(params) => self.process_client_pin(params), - Command::AuthenticatorReset => self.process_reset(cid), + Command::AuthenticatorReset => self.process_reset(cid, now), #[cfg(feature = "with_ctap2_1")] Command::AuthenticatorSelection => self.process_selection(cid), // TODO(kaczmarczyck) implement FIDO 2.1 commands @@ -659,6 +697,7 @@ where &mut self, get_assertion_params: AuthenticatorGetAssertionParameters, cid: ChannelID, + now: ClockValue, ) -> Result { let AuthenticatorGetAssertionParameters { rp_id, @@ -745,7 +784,6 @@ where let (credential, remaining_credentials) = credentials .split_last() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; - self.next_credentials = remaining_credentials.to_vec(); self.increment_global_signature_counter()?; @@ -754,29 +792,37 @@ where auth_data: self.generate_auth_data(&rp_id_hash, flags)?, hmac_secret_input, }; - let number_of_credentials = if self.next_credentials.is_empty() { - self.next_assertion_input = None; + let number_of_credentials = if remaining_credentials.is_empty() { None } else { - self.next_assertion_input = Some(assertion_input.clone()); - Some(self.next_credentials.len() + 1) + self.stateful_command_permission = + TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); + self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { + assertion_input: assertion_input.clone(), + next_credentials: remaining_credentials.to_vec(), + })); + Some(remaining_credentials.len() + 1) }; self.assertion_response(credential, assertion_input, number_of_credentials) } - fn process_get_next_assertion(&mut self) -> Result { - // TODO(kaczmarczyck) introduce timer - let assertion_input = self - .next_assertion_input - .clone() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - let credential = self - .next_credentials - .pop() - .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; - if self.next_credentials.is_empty() { - self.next_assertion_input = None; - }; + fn process_get_next_assertion( + &mut self, + now: ClockValue, + ) -> Result { + self.check_command_permission(now)?; + let (assertion_input, credential) = + if let Some(StatefulCommand::GetAssertion(assertion_state)) = + &mut self.stateful_command_type + { + let credential = assertion_state + .next_credentials + .pop() + .ok_or(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)?; + (assertion_state.assertion_input.clone(), credential) + } else { + return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + }; self.assertion_response(&credential, assertion_input, None) } @@ -834,10 +880,17 @@ where ) } - fn process_reset(&mut self, cid: ChannelID) -> Result { + fn process_reset( + &mut self, + cid: ChannelID, + now: ClockValue, + ) -> Result { // Resets are only possible in the first 10 seconds after booting. - if !self.accepts_reset { - return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); + // TODO(kaczmarczyck) 2.1 allows Reset after Reset and 15 seconds? + self.check_command_permission(now)?; + match &self.stateful_command_type { + Some(StatefulCommand::Reset) => (), + _ => return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED), } (self.check_user_presence)(cid)?; @@ -886,8 +939,11 @@ mod test { MakeCredentialOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, }; use super::*; + use cbor::cbor_array; use crypto::rng256::ThreadRng256; + const CLOCK_FREQUENCY_HZ: usize = 32768; + const DUMMY_CLOCK_VALUE: ClockValue = ClockValue::new(0, CLOCK_FREQUENCY_HZ); // The keep-alive logic in the processing of some commands needs a channel ID to send // keep-alive packets to. // In tests where we define a dummy user-presence check that immediately returns, the channel @@ -898,8 +954,8 @@ mod test { fn test_get_info() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); - let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + let info_reponse = ctap_state.process_command(&[0x04], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); #[cfg(feature = "with_ctap2_1")] let mut expected_response = vec![0x00, 0xAA, 0x01]; @@ -955,7 +1011,7 @@ mod test { rp_icon: None, }; let user = PublicKeyCredentialUserEntity { - user_id: vec![0xFA, 0xB1, 0xA2], + user_id: vec![0x1D], user_name: None, user_display_name: None, user_icon: None, @@ -1008,7 +1064,7 @@ mod test { fn test_residential_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = @@ -1044,7 +1100,7 @@ mod test { fn test_non_residential_process_make_credential() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.options.rk = false; @@ -1081,7 +1137,7 @@ mod test { fn test_process_make_credential_unsupported_algorithm() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.pub_key_cred_params = vec![]; @@ -1099,7 +1155,7 @@ mod test { let mut rng = ThreadRng256 {}; let excluded_private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let excluded_credential_id = vec![0x01, 0x23, 0x45, 0x67]; let make_credential_params = @@ -1132,7 +1188,7 @@ mod test { fn test_process_make_credential_credential_with_cred_protect() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let test_policy = CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList; let make_credential_params = @@ -1186,7 +1242,7 @@ mod test { fn test_process_make_credential_hmac_secret() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1236,7 +1292,7 @@ mod test { fn test_process_make_credential_hmac_secret_resident_key() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1285,7 +1341,8 @@ mod test { fn test_process_make_credential_cancelled() { let mut rng = ThreadRng256 {}; let user_presence_always_cancel = |_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL); - let mut ctap_state = CtapState::new(&mut rng, user_presence_always_cancel); + let mut ctap_state = + CtapState::new(&mut rng, user_presence_always_cancel, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); let make_credential_response = @@ -1297,11 +1354,43 @@ mod test { ); } + fn check_assertion_response( + response: Result, + expected_user_id: Vec, + expected_number_of_credentials: Option, + ) { + match response.unwrap() { + ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { + let AuthenticatorGetAssertionResponse { + auth_data, + user, + number_of_credentials, + .. + } = get_assertion_response; + let expected_auth_data = vec![ + 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, + 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + assert_eq!(auth_data, expected_auth_data); + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + assert_eq!(user, Some(expected_user)); + assert_eq!(number_of_credentials, expected_number_of_credentials); + } + _ => panic!("Invalid response type"), + } + } + #[test] fn test_residential_process_get_assertion() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_credential_params = create_minimal_make_credential_parameters(); assert!(ctap_state @@ -1320,34 +1409,12 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); - - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0xFA, 0xB1, 0xA2], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert!(number_of_credentials.is_none()); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x1D], None); } #[test] @@ -1355,7 +1422,7 @@ mod test { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1405,8 +1472,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, @@ -1419,7 +1489,7 @@ mod test { let mut rng = ThreadRng256 {}; let sk = crypto::ecdh::SecKey::gensk(&mut rng); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let make_extensions = Some(MakeCredentialExtensions { hmac_secret: true, @@ -1453,8 +1523,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, @@ -1468,7 +1541,7 @@ mod test { let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); let credential_id = rng.gen_uniform_u8x32().to_vec(); let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, @@ -1480,7 +1553,7 @@ mod test { credential_id: credential_id.clone(), private_key: private_key.clone(), rp_id: String::from("example.com"), - user_handle: vec![0x00], + user_handle: vec![0x1D], other_ui: None, cred_random: None, cred_protect_policy: Some( @@ -1505,8 +1578,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), @@ -1524,16 +1600,19 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); - assert!(get_assertion_response.is_ok()); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x1D], None); let credential = PublicKeyCredentialSource { key_type: PublicKeyCredentialType::PublicKey, credential_id, private_key, rp_id: String::from("example.com"), - user_handle: vec![0x00], + user_handle: vec![0x1D], other_ui: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), @@ -1556,8 +1635,11 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS), @@ -1565,10 +1647,10 @@ mod test { } #[test] - fn test_process_get_next_assertion() { + fn test_process_get_next_assertion_two_credentials() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x01]; @@ -1593,63 +1675,135 @@ mod test { pin_uv_auth_param: None, pin_uv_auth_protocol: None, }; - let get_assertion_response = - ctap_state.process_get_assertion(get_assertion_params, DUMMY_CHANNEL_ID); + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x02], Some(2)); - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0x02], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert_eq!(number_of_credentials, Some(2)); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x01], None); - let get_assertion_response = ctap_state.process_get_next_assertion(); - match get_assertion_response.unwrap() { - ResponseData::AuthenticatorGetAssertion(get_assertion_response) => { - let AuthenticatorGetAssertionResponse { - auth_data, - user, - number_of_credentials, - .. - } = get_assertion_response; - let expected_auth_data = vec![ - 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, - 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, - ]; - assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: vec![0x01], - user_name: None, - user_display_name: None, - user_icon: None, - }; - assert_eq!(user, Some(expected_user)); - assert!(number_of_credentials.is_none()); - } - _ => panic!("Invalid response type"), - } + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } - let get_assertion_response = ctap_state.process_get_next_assertion(); + #[test] + fn test_process_get_next_assertion_three_credentials() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x03]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + check_assertion_response(get_assertion_response, vec![0x03], Some(3)); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x02], None); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + check_assertion_response(get_assertion_response, vec![0x01], None); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + } + + #[test] + fn test_process_get_next_assertion_not_allowed() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); + assert_eq!( + get_assertion_response, + Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) + ); + + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x01]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + let mut make_credential_params = create_minimal_make_credential_parameters(); + make_credential_params.user.user_id = vec![0x02]; + assert!(ctap_state + .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) + .is_ok()); + + let get_assertion_params = AuthenticatorGetAssertionParameters { + rp_id: String::from("example.com"), + client_data_hash: vec![0xCD], + allow_list: None, + extensions: None, + options: GetAssertionOptions { + up: false, + uv: false, + }, + pin_uv_auth_param: None, + pin_uv_auth_protocol: None, + }; + let get_assertion_response = ctap_state.process_get_assertion( + get_assertion_params, + DUMMY_CHANNEL_ID, + DUMMY_CLOCK_VALUE, + ); + assert!(get_assertion_response.is_ok()); + + // This is a MakeCredential command. + let mut command_cbor = vec![0x01]; + let cbor_value = cbor_map! { + 1 => vec![0xCD; 16], + 2 => cbor_map! { + "id" => "example.com", + }, + 3 => cbor_map! { + "id" => vec![0x1D, 0x1D, 0x1D, 0x1D], + }, + 4 => cbor_array![ES256_CRED_PARAM], + }; + assert!(cbor::write(cbor_value, &mut command_cbor)); + ctap_state.process_command(&command_cbor, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + + let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); assert_eq!( get_assertion_response, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED) @@ -1661,7 +1815,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let credential_id = vec![0x01, 0x23, 0x45, 0x67]; let credential_source = PublicKeyCredentialSource { @@ -1681,7 +1835,8 @@ mod test { .is_ok()); assert!(ctap_state.persistent_store.count_credentials().unwrap() > 0); - let reset_reponse = ctap_state.process_command(&[0x07], DUMMY_CHANNEL_ID); + let reset_reponse = + ctap_state.process_command(&[0x07], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![0x00]; assert_eq!(reset_reponse, expected_response); assert!(ctap_state.persistent_store.count_credentials().unwrap() == 0); @@ -1691,9 +1846,10 @@ mod test { fn test_process_reset_cancelled() { let mut rng = ThreadRng256 {}; let user_presence_always_cancel = |_| Err(Ctap2StatusCode::CTAP2_ERR_KEEPALIVE_CANCEL); - let mut ctap_state = CtapState::new(&mut rng, user_presence_always_cancel); + let mut ctap_state = + CtapState::new(&mut rng, user_presence_always_cancel, DUMMY_CLOCK_VALUE); - let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID); + let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); assert_eq!( reset_reponse, @@ -1701,14 +1857,28 @@ mod test { ); } + #[test] + fn test_process_reset_not_first() { + let mut rng = ThreadRng256 {}; + let user_immediately_present = |_| Ok(()); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + + // This is a GetNextAssertion command. + ctap_state.process_command(&[0x08], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + + let reset_reponse = ctap_state.process_reset(DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); + assert_eq!(reset_reponse, Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED)); + } + #[test] fn test_process_unknown_command() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // This command does not exist. - let reset_reponse = ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID); + let reset_reponse = + ctap_state.process_command(&[0xDF], DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE); let expected_response = vec![Ctap2StatusCode::CTAP1_ERR_INVALID_COMMAND as u8]; assert_eq!(reset_reponse, expected_response); } @@ -1718,7 +1888,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Usually, the relying party ID or its hash is provided by the client. // We are not testing the correctness of our SHA256 here, only if it is checked. @@ -1739,7 +1909,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Usually, the relying party ID or its hash is provided by the client. // We are not testing the correctness of our SHA256 here, only if it is checked. @@ -1762,7 +1932,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Same as above. let rp_id_hash = [0x55; 32]; @@ -1784,7 +1954,7 @@ mod test { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let private_key = crypto::ecdsa::SecKey::gensk(&mut rng); - let mut ctap_state = CtapState::new(&mut rng, user_immediately_present); + let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); // Same as above. let rp_id_hash = [0x55; 32]; diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 6e4506a..aae6add 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -746,6 +746,21 @@ mod test { assert!(persistent_store.count_credentials().unwrap() > 0); } + #[test] + fn test_credential_order() { + let mut rng = ThreadRng256 {}; + let mut persistent_store = PersistentStore::new(&mut rng); + let credential_source = create_credential_source(&mut rng, "example.com", vec![]); + let current_latest_creation = credential_source.creation_order; + assert!(persistent_store.store_credential(credential_source).is_ok()); + let mut credential_source = create_credential_source(&mut rng, "example.com", vec![]); + credential_source.creation_order = persistent_store.new_creation_order().unwrap(); + assert!(credential_source.creation_order > current_latest_creation); + let current_latest_creation = credential_source.creation_order; + assert!(persistent_store.store_credential(credential_source).is_ok()); + assert!(persistent_store.new_creation_order().unwrap() > current_latest_creation); + } + #[test] #[allow(clippy::assertions_on_constants)] fn test_fill_store() { diff --git a/src/main.rs b/src/main.rs index 855325e..51c8305 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,9 +37,11 @@ use libtock_drivers::console::Console; use libtock_drivers::led; use libtock_drivers::result::{FlexUnwrap, TockError}; use libtock_drivers::timer; +use libtock_drivers::timer::Duration; #[cfg(feature = "debug_ctap")] use libtock_drivers::timer::Timer; -use libtock_drivers::timer::{Duration, Timestamp}; +#[cfg(feature = "debug_ctap")] +use libtock_drivers::timer::Timestamp; use libtock_drivers::usb_ctap_hid; const KEEPALIVE_DELAY_MS: isize = 100; @@ -57,12 +59,13 @@ fn main() { panic!("Cannot setup USB driver"); } + let boot_time = timer.get_current_clock().flex_unwrap(); let mut rng = TockRng256 {}; - let mut ctap_state = CtapState::new(&mut rng, check_user_presence); + let mut ctap_state = CtapState::new(&mut rng, check_user_presence, boot_time); let mut ctap_hid = CtapHid::new(); let mut led_counter = 0; - let mut last_led_increment = timer.get_current_clock().flex_unwrap(); + let mut last_led_increment = boot_time; // Main loop. If CTAP1 is used, we register button presses for U2F while receiving and waiting. // The way TockOS and apps currently interact, callbacks need a yield syscall to execute, @@ -115,7 +118,7 @@ fn main() { // These calls are making sure that even for long inactivity, wrapping clock values // never randomly wink or grant user presence for U2F. - ctap_state.check_disable_reset(Timestamp::::from_clock_value(now)); + ctap_state.update_command_permission(now); ctap_hid.wink_permission = ctap_hid.wink_permission.check_expiration(now); if has_packet { From 3aef7e8b19d6a522af175f73de1b239bae3bd682 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 15:56:59 +0100 Subject: [PATCH 48/54] reuse update_command_permission --- src/ctap/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 4675350..f2043d0 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -204,7 +204,7 @@ where } fn check_command_permission(&mut self, now: ClockValue) -> Result<(), Ctap2StatusCode> { - self.stateful_command_permission = self.stateful_command_permission.check_expiration(now); + self.update_command_permission(now); if self.stateful_command_permission.is_granted(now) { Ok(()) } else { From 3d1d8279847ef0dd8407b0a74e6c963a2eef4ce0 Mon Sep 17 00:00:00 2001 From: Jean-Michel Picod Date: Thu, 26 Nov 2020 16:29:14 +0100 Subject: [PATCH 49/54] Address PR comments --- src/ctap/ctap1.rs | 28 +++++++++++++++++----------- src/ctap/storage.rs | 8 ++++---- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/ctap/ctap1.rs b/src/ctap/ctap1.rs index 0fa4745..41df364 100644 --- a/src/ctap/ctap1.rs +++ b/src/ctap/ctap1.rs @@ -35,6 +35,8 @@ pub enum Ctap1StatusCode { SW_WRONG_LENGTH = 0x6700, SW_CLA_NOT_SUPPORTED = 0x6E00, SW_INS_NOT_SUPPORTED = 0x6D00, + SW_MEMERR = 0x6501, + SW_COMMAND_ABORTED = 0x6F00, SW_VENDOR_KEY_HANDLE_TOO_LONG = 0xF000, } @@ -49,6 +51,8 @@ impl TryFrom for Ctap1StatusCode { 0x6700 => Ok(Ctap1StatusCode::SW_WRONG_LENGTH), 0x6E00 => Ok(Ctap1StatusCode::SW_CLA_NOT_SUPPORTED), 0x6D00 => Ok(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), + 0x6501 => Ok(Ctap1StatusCode::SW_MEMERR), + 0x6F00 => Ok(Ctap1StatusCode::SW_COMMAND_ABORTED), 0xF000 => Ok(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG), _ => Err(()), } @@ -288,20 +292,22 @@ impl Ctap1Command { let pk = sk.genpk(); let key_handle = ctap_state .encrypt_key_handle(sk, &application, None) - .map_err(|_| Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG)?; + .map_err(|_| Ctap1StatusCode::SW_COMMAND_ABORTED)?; if key_handle.len() > 0xFF { // This is just being defensive with unreachable code. return Err(Ctap1StatusCode::SW_VENDOR_KEY_HANDLE_TOO_LONG); } - let certificate = match ctap_state.persistent_store.attestation_certificate() { - Ok(Some(value)) => value, - _ => return Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), - }; - let private_key = match ctap_state.persistent_store.attestation_private_key() { - Ok(Some(value)) => value, - _ => return Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED), - }; + let certificate = ctap_state + .persistent_store + .attestation_certificate() + .map_err(|_| Ctap1StatusCode::SW_MEMERR)? + .ok_or(Ctap1StatusCode::SW_COMMAND_ABORTED)?; + let private_key = ctap_state + .persistent_store + .attestation_private_key() + .map_err(|_| Ctap1StatusCode::SW_MEMERR)? + .ok_or(Ctap1StatusCode::SW_COMMAND_ABORTED)?; let mut response = Vec::with_capacity(105 + key_handle.len() + certificate.len()); response.push(Ctap1Command::LEGACY_BYTE); @@ -442,7 +448,7 @@ mod test { ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate and private key are missing - assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_ABORTED)); let fake_key = [0x41u8; key_material::ATTESTATION_PRIVATE_KEY_LENGTH]; assert!(ctap_state @@ -453,7 +459,7 @@ mod test { ctap_state.u2f_up_state.grant_up(START_CLOCK_VALUE); let response = Ctap1Command::process_command(&message, &mut ctap_state, START_CLOCK_VALUE); // Certificate is still missing - assert_eq!(response, Err(Ctap1StatusCode::SW_INS_NOT_SUPPORTED)); + assert_eq!(response, Err(Ctap1StatusCode::SW_COMMAND_ABORTED)); let fake_cert = [0x99u8; 100]; // Arbitrary length assert!(ctap_state diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 8e396b6..5ec6511 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -230,8 +230,8 @@ impl PersistentStore { .unwrap(); } // TODO(jmichel): remove this when vendor command is in place - #[cfg(not(any(test, feature = "ram_storage")))] - self.load_attestation_from_firmware(); + #[cfg(not(test))] + self.load_attestation_data_from_firmware(); if self.store.find_one(&Key::Aaguid).is_none() { self.set_aaguid(key_material::AAGUID).unwrap(); @@ -239,8 +239,8 @@ impl PersistentStore { } // TODO(jmichel): remove this function when vendor command is in place. - #[cfg(not(any(test, feature = "ram_storage")))] - fn load_attestation_from_firmware(&mut self) { + #[cfg(not(test))] + fn load_attestation_data_from_firmware(&mut self) { // The following 2 entries are meant to be written by vendor-specific commands. if self.store.find_one(&Key::AttestationPrivateKey).is_none() { self.set_attestation_private_key(key_material::ATTESTATION_PRIVATE_KEY) From 1571f58cd3e51849626cb3681bb15ccef10155b8 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Thu, 26 Nov 2020 19:21:41 +0100 Subject: [PATCH 50/54] wrapping_add in storage and more moving --- src/ctap/mod.rs | 27 ++++++++++++++------------- src/ctap/storage.rs | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 9626da4..5fdfa9e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -614,7 +614,7 @@ where // and returns the correct Get(Next)Assertion response. fn assertion_response( &self, - credential: &PublicKeyCredentialSource, + credential: PublicKeyCredentialSource, assertion_input: AssertionInput, number_of_credentials: Option, ) -> Result { @@ -645,14 +645,14 @@ where let cred_desc = PublicKeyCredentialDescriptor { key_type: PublicKeyCredentialType::PublicKey, - key_id: credential.credential_id.clone(), + key_id: credential.credential_id, transports: None, // You can set USB as a hint here. }; let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { - user_id: credential.user_handle.clone(), + user_id: credential.user_handle, user_name: None, - user_display_name: credential.other_ui.clone(), + user_display_name: credential.other_ui, user_icon: None, }) } else { @@ -758,7 +758,7 @@ where } let rp_id_hash = Sha256::hash(rp_id.as_bytes()); - let mut credentials = if let Some(allow_list) = allow_list { + let mut applicable_credentials = if let Some(allow_list) = allow_list { if let Some(credential) = self.get_any_credential_from_allow_list(allow_list, &rp_id, &rp_id_hash, has_uv)? { @@ -771,11 +771,11 @@ where }; // Remove user identifiable information without uv. if !has_uv { - for credential in &mut credentials { + for credential in &mut applicable_credentials { credential.other_ui = None; } } - credentials.sort_unstable_by_key(|c| c.creation_order); + applicable_credentials.sort_unstable_by_key(|c| c.creation_order); // This check comes before CTAP2_ERR_NO_CREDENTIALS in CTAP 2.0. // For CTAP 2.1, it was moved to a later protocol step. @@ -783,8 +783,8 @@ where (self.check_user_presence)(cid)?; } - let (credential, remaining_credentials) = credentials - .split_last() + let credential = applicable_credentials + .pop() .ok_or(Ctap2StatusCode::CTAP2_ERR_NO_CREDENTIALS)?; self.increment_global_signature_counter()?; @@ -794,16 +794,17 @@ where auth_data: self.generate_auth_data(&rp_id_hash, flags)?, hmac_secret_input, }; - let number_of_credentials = if remaining_credentials.is_empty() { + let number_of_credentials = if applicable_credentials.is_empty() { None } else { + let number_of_credentials = Some(applicable_credentials.len() + 1); self.stateful_command_permission = TimedPermission::granted(now, STATEFUL_COMMAND_TIMEOUT_DURATION); self.stateful_command_type = Some(StatefulCommand::GetAssertion(AssertionState { assertion_input: assertion_input.clone(), - next_credentials: remaining_credentials.to_vec(), + next_credentials: applicable_credentials, })); - Some(remaining_credentials.len() + 1) + number_of_credentials }; self.assertion_response(credential, assertion_input, number_of_credentials) } @@ -825,7 +826,7 @@ where } else { return Err(Ctap2StatusCode::CTAP2_ERR_NOT_ALLOWED); }; - self.assertion_response(&credential, assertion_input, None) + self.assertion_response(credential, assertion_input, None) } fn process_get_info(&self) -> Result { diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index fd91cce..7952d75 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -359,7 +359,7 @@ impl PersistentStore { .map(|c| c.creation_order) .max() .unwrap_or(0) - + 1) + .wrapping_add(1)) } pub fn global_signature_counter(&self) -> Result { From 2a4677c0b1a03229dd5049c0d32d12ea736e8167 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Fri, 27 Nov 2020 15:04:47 +0100 Subject: [PATCH 51/54] adds user data to persistent storage --- src/ctap/data_formats.rs | 52 ++++++++++++--- src/ctap/mod.rs | 123 ++++++++++++++++++++++++++++-------- src/ctap/pin_protocol_v1.rs | 16 +++++ src/ctap/storage.rs | 20 ++++-- 4 files changed, 169 insertions(+), 42 deletions(-) diff --git a/src/ctap/data_formats.rs b/src/ctap/data_formats.rs index fa747bc..e7419fd 100644 --- a/src/ctap/data_formats.rs +++ b/src/ctap/data_formats.rs @@ -56,7 +56,7 @@ impl TryFrom for PublicKeyCredentialRpEntity { } // https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialuserentity -#[cfg_attr(any(test, feature = "debug_ctap"), derive(Debug, PartialEq))] +#[cfg_attr(any(test, feature = "debug_ctap"), derive(Clone, Debug, PartialEq))] pub struct PublicKeyCredentialUserEntity { pub user_id: Vec, pub user_name: Option, @@ -497,10 +497,12 @@ pub struct PublicKeyCredentialSource { pub private_key: ecdsa::SecKey, // TODO(kaczmarczyck) open for other algorithms pub rp_id: String, pub user_handle: Vec, // not optional, but nullable - pub other_ui: Option, + pub user_display_name: Option, pub cred_random: Option>, pub cred_protect_policy: Option, pub creation_order: u64, + pub user_name: Option, + pub user_icon: Option, } // We serialize credentials for the persistent storage using CBOR maps. Each field of a credential @@ -510,10 +512,12 @@ enum PublicKeyCredentialSourceField { PrivateKey = 1, RpId = 2, UserHandle = 3, - OtherUi = 4, + UserDisplayName = 4, CredRandom = 5, CredProtectPolicy = 6, CreationOrder = 7, + UserName = 8, + UserIcon = 9, // When a field is removed, its tag should be reserved and not used for new fields. We document // those reserved tags below. // Reserved tags: none. @@ -534,10 +538,12 @@ impl From for cbor::Value { PublicKeyCredentialSourceField::PrivateKey => Some(private_key.to_vec()), PublicKeyCredentialSourceField::RpId => Some(credential.rp_id), PublicKeyCredentialSourceField::UserHandle => Some(credential.user_handle), - PublicKeyCredentialSourceField::OtherUi => credential.other_ui, + PublicKeyCredentialSourceField::UserDisplayName => credential.user_display_name, PublicKeyCredentialSourceField::CredRandom => credential.cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => credential.cred_protect_policy, PublicKeyCredentialSourceField::CreationOrder => credential.creation_order, + PublicKeyCredentialSourceField::UserName => credential.user_name, + PublicKeyCredentialSourceField::UserIcon => credential.user_icon, } } } @@ -552,10 +558,12 @@ impl TryFrom for PublicKeyCredentialSource { PublicKeyCredentialSourceField::PrivateKey => private_key, PublicKeyCredentialSourceField::RpId => rp_id, PublicKeyCredentialSourceField::UserHandle => user_handle, - PublicKeyCredentialSourceField::OtherUi => other_ui, + PublicKeyCredentialSourceField::UserDisplayName => user_display_name, PublicKeyCredentialSourceField::CredRandom => cred_random, PublicKeyCredentialSourceField::CredProtectPolicy => cred_protect_policy, PublicKeyCredentialSourceField::CreationOrder => creation_order, + PublicKeyCredentialSourceField::UserName => user_name, + PublicKeyCredentialSourceField::UserIcon => user_icon, } = extract_map(cbor_value)?; } @@ -568,12 +576,14 @@ impl TryFrom for PublicKeyCredentialSource { .ok_or(Ctap2StatusCode::CTAP2_ERR_INVALID_CBOR)?; let rp_id = extract_text_string(ok_or_missing(rp_id)?)?; let user_handle = extract_byte_string(ok_or_missing(user_handle)?)?; - let other_ui = other_ui.map(extract_text_string).transpose()?; + let user_display_name = user_display_name.map(extract_text_string).transpose()?; let cred_random = cred_random.map(extract_byte_string).transpose()?; let cred_protect_policy = cred_protect_policy .map(CredentialProtectionPolicy::try_from) .transpose()?; let creation_order = creation_order.map(extract_unsigned).unwrap_or(Ok(0))?; + let user_name = user_name.map(extract_text_string).transpose()?; + let user_icon = user_icon.map(extract_text_string).transpose()?; // We don't return whether there were unknown fields in the CBOR value. This means that // deserialization is not injective. In particular deserialization is only an inverse of // serialization at a given version of OpenSK. This is not a problem because: @@ -590,10 +600,12 @@ impl TryFrom for PublicKeyCredentialSource { private_key, rp_id, user_handle, - other_ui, + user_display_name, cred_random, cred_protect_policy, creation_order, + user_name, + user_icon, }) } } @@ -1360,10 +1372,12 @@ mod test { private_key: crypto::ecdsa::SecKey::gensk(&mut rng), rp_id: "example.com".to_string(), user_handle: b"foo".to_vec(), - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, }; assert_eq!( @@ -1372,7 +1386,7 @@ mod test { ); let credential = PublicKeyCredentialSource { - other_ui: Some("other".to_string()), + user_display_name: Some("Display Name".to_string()), ..credential }; @@ -1396,6 +1410,26 @@ mod test { ..credential }; + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + user_name: Some("name".to_string()), + ..credential + }; + + assert_eq!( + PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), + Ok(credential.clone()) + ); + + let credential = PublicKeyCredentialSource { + user_icon: Some("icon".to_string()), + ..credential + }; + assert_eq!( PublicKeyCredentialSource::try_from(cbor::Value::from(credential.clone())), Ok(credential) diff --git a/src/ctap/mod.rs b/src/ctap/mod.rs index 5fdfa9e..8efb92e 100644 --- a/src/ctap/mod.rs +++ b/src/ctap/mod.rs @@ -317,10 +317,12 @@ where private_key: sk, rp_id: String::from(""), user_handle: vec![], - other_ui: None, + user_display_name: None, cred_random, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, })) } @@ -534,12 +536,18 @@ where user_handle: user.user_id, // This input is user provided, so we crop it to 64 byte for storage. // The UTF8 encoding is always preserved, so the string might end up shorter. - other_ui: user + user_display_name: user .user_display_name .map(|s| truncate_to_char_boundary(&s, 64).to_string()), cred_random: cred_random.map(|c| c.to_vec()), cred_protect_policy, creation_order: self.persistent_store.new_creation_order()?, + user_name: user + .user_name + .map(|s| truncate_to_char_boundary(&s, 64).to_string()), + user_icon: user + .user_icon + .map(|s| truncate_to_char_boundary(&s, 64).to_string()), }; self.persistent_store.store_credential(credential_source)?; random_id @@ -651,9 +659,9 @@ where let user = if !credential.user_handle.is_empty() { Some(PublicKeyCredentialUserEntity { user_id: credential.user_handle, - user_name: None, - user_display_name: credential.other_ui, - user_icon: None, + user_name: credential.user_name, + user_display_name: credential.user_display_name, + user_icon: credential.user_icon, }) } else { None @@ -772,7 +780,9 @@ where // Remove user identifiable information without uv. if !has_uv { for credential in &mut applicable_credentials { - credential.other_ui = None; + credential.user_name = None; + credential.user_display_name = None; + credential.user_icon = None; } } applicable_credentials.sort_unstable_by_key(|c| c.creation_order); @@ -1169,10 +1179,12 @@ mod test { private_key: excluded_private_key, rp_id: String::from("example.com"), user_handle: vec![], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, }; assert!(ctap_state .persistent_store @@ -1357,9 +1369,10 @@ mod test { ); } - fn check_assertion_response( + fn check_assertion_response_with_user( response: Result, - expected_user_id: Vec, + expected_user: PublicKeyCredentialUserEntity, + flags: u8, expected_number_of_credentials: Option, ) { match response.unwrap() { @@ -1373,15 +1386,9 @@ mod test { let expected_auth_data = vec![ 0xA3, 0x79, 0xA6, 0xF6, 0xEE, 0xAF, 0xB9, 0xA5, 0x5E, 0x37, 0x8C, 0x11, 0x80, 0x34, 0xE2, 0x75, 0x1E, 0x68, 0x2F, 0xAB, 0x9F, 0x2D, 0x30, 0xAB, 0x13, 0xD2, - 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x12, 0x55, 0x86, 0xCE, 0x19, 0x47, flags, 0x00, 0x00, 0x00, 0x01, ]; assert_eq!(auth_data, expected_auth_data); - let expected_user = PublicKeyCredentialUserEntity { - user_id: expected_user_id, - user_name: None, - user_display_name: None, - user_icon: None, - }; assert_eq!(user, Some(expected_user)); assert_eq!(number_of_credentials, expected_number_of_credentials); } @@ -1389,6 +1396,25 @@ mod test { } } + fn check_assertion_response( + response: Result, + expected_user_id: Vec, + expected_number_of_credentials: Option, + ) { + let expected_user = PublicKeyCredentialUserEntity { + user_id: expected_user_id, + user_name: None, + user_display_name: None, + user_icon: None, + }; + check_assertion_response_with_user( + response, + expected_user, + 0x00, + expected_number_of_credentials, + ); + } + #[test] fn test_residential_process_get_assertion() { let mut rng = ThreadRng256 {}; @@ -1557,12 +1583,14 @@ mod test { private_key: private_key.clone(), rp_id: String::from("example.com"), user_handle: vec![0x1D], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), creation_order: 0, + user_name: None, + user_icon: None, }; assert!(ctap_state .persistent_store @@ -1616,10 +1644,12 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x1D], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), creation_order: 0, + user_name: None, + user_icon: None, }; assert!(ctap_state .persistent_store @@ -1650,22 +1680,48 @@ mod test { } #[test] - fn test_process_get_next_assertion_two_credentials() { + fn test_process_get_next_assertion_two_credentials_with_uv() { let mut rng = ThreadRng256 {}; + let key_agreement_key = crypto::ecdh::SecKey::gensk(&mut rng); + let pin_uv_auth_token = [0x88; 32]; + let pin_protocol_v1 = PinProtocolV1::new_test(key_agreement_key, pin_uv_auth_token); + let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); + ctap_state.pin_protocol_v1 = pin_protocol_v1; let mut make_credential_params = create_minimal_make_credential_parameters(); - make_credential_params.user.user_id = vec![0x01]; + let user1 = PublicKeyCredentialUserEntity { + user_id: vec![0x01], + user_name: Some("user1".to_string()), + user_display_name: Some("User One".to_string()), + user_icon: Some("icon1".to_string()), + }; + make_credential_params.user = user1.clone(); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); let mut make_credential_params = create_minimal_make_credential_parameters(); - make_credential_params.user.user_id = vec![0x02]; + let user2 = PublicKeyCredentialUserEntity { + user_id: vec![0x02], + user_name: Some("user2".to_string()), + user_display_name: Some("User Two".to_string()), + user_icon: Some("icon2".to_string()), + }; + make_credential_params.user = user2.clone(); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); + ctap_state + .persistent_store + .set_pin_hash(&[0u8; 16]) + .unwrap(); + let pin_uv_auth_param = Some(vec![ + 0x6F, 0x52, 0x83, 0xBF, 0x1A, 0x91, 0xEE, 0x67, 0xE9, 0xD4, 0x4C, 0x80, 0x08, 0x79, + 0x90, 0x8D, + ]); + let get_assertion_params = AuthenticatorGetAssertionParameters { rp_id: String::from("example.com"), client_data_hash: vec![0xCD], @@ -1673,20 +1729,20 @@ mod test { extensions: None, options: GetAssertionOptions { up: false, - uv: false, + uv: true, }, - pin_uv_auth_param: None, - pin_uv_auth_protocol: None, + pin_uv_auth_param, + pin_uv_auth_protocol: Some(1), }; let get_assertion_response = ctap_state.process_get_assertion( get_assertion_params, DUMMY_CHANNEL_ID, DUMMY_CLOCK_VALUE, ); - check_assertion_response(get_assertion_response, vec![0x02], Some(2)); + check_assertion_response_with_user(get_assertion_response, user2, 0x04, Some(2)); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); - check_assertion_response(get_assertion_response, vec![0x01], None); + check_assertion_response_with_user(get_assertion_response, user1, 0x04, None); let get_assertion_response = ctap_state.process_get_next_assertion(DUMMY_CLOCK_VALUE); assert_eq!( @@ -1696,23 +1752,32 @@ mod test { } #[test] - fn test_process_get_next_assertion_three_credentials() { + fn test_process_get_next_assertion_three_credentials_no_uv() { let mut rng = ThreadRng256 {}; let user_immediately_present = |_| Ok(()); let mut ctap_state = CtapState::new(&mut rng, user_immediately_present, DUMMY_CLOCK_VALUE); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x01]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x02]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); let mut make_credential_params = create_minimal_make_credential_parameters(); make_credential_params.user.user_id = vec![0x03]; + make_credential_params.user.user_name = Some("removed".to_string()); + make_credential_params.user.user_display_name = Some("removed".to_string()); + make_credential_params.user.user_icon = Some("removed".to_string()); assert!(ctap_state .process_make_credential(make_credential_params, DUMMY_CHANNEL_ID) .is_ok()); @@ -1827,10 +1892,12 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, }; assert!(ctap_state .persistent_store diff --git a/src/ctap/pin_protocol_v1.rs b/src/ctap/pin_protocol_v1.rs index 564ca6d..44aad71 100644 --- a/src/ctap/pin_protocol_v1.rs +++ b/src/ctap/pin_protocol_v1.rs @@ -631,6 +631,22 @@ impl PinProtocolV1 { } Ok(()) } + + #[cfg(test)] + pub fn new_test( + key_agreement_key: crypto::ecdh::SecKey, + pin_uv_auth_token: [u8; 32], + ) -> PinProtocolV1 { + PinProtocolV1 { + key_agreement_key, + pin_uv_auth_token, + consecutive_pin_mismatches: 0, + #[cfg(feature = "with_ctap2_1")] + permissions: 0xFF, + #[cfg(feature = "with_ctap2_1")] + permissions_rp_id: None, + } + } } #[cfg(test)] diff --git a/src/ctap/storage.rs b/src/ctap/storage.rs index 7952d75..0e4941a 100644 --- a/src/ctap/storage.rs +++ b/src/ctap/storage.rs @@ -719,10 +719,12 @@ mod test { private_key, rp_id: String::from(rp_id), user_handle, - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, } } @@ -896,12 +898,14 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: Some( CredentialProtectionPolicy::UserVerificationOptionalWithCredentialIdList, ), creation_order: 0, + user_name: None, + user_icon: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -940,10 +944,12 @@ mod test { private_key: key0, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, }; assert_eq!(found_credential, Some(expected_credential)); } @@ -960,10 +966,12 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: Some(CredentialProtectionPolicy::UserVerificationRequired), creation_order: 0, + user_name: None, + user_icon: None, }; assert!(persistent_store.store_credential(credential).is_ok()); @@ -1138,10 +1146,12 @@ mod test { private_key, rp_id: String::from("example.com"), user_handle: vec![0x00], - other_ui: None, + user_display_name: None, cred_random: None, cred_protect_policy: None, creation_order: 0, + user_name: None, + user_icon: None, }; let serialized = serialize_credential(credential.clone()).unwrap(); let reconstructed = deserialize_credential(&serialized).unwrap(); From ed5a9e5b24f6d1d629cd6a575f29e4d0b5c24ddb Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Sat, 28 Nov 2020 19:01:16 +0100 Subject: [PATCH 52/54] Apply review comments --- libraries/persistent_store/fuzz/src/store.rs | 27 ++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs index e51ed71..a008a4e 100644 --- a/libraries/persistent_store/fuzz/src/store.rs +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -32,8 +32,6 @@ use std::convert::TryInto; /// information is printed if `debug` is set. Statistics are gathered if `stats` is set. pub fn fuzz(data: &[u8], debug: bool, stats: Option<&mut Stats>) { let mut fuzzer = Fuzzer::new(data, debug, stats); - fuzzer.init_counters(); - fuzzer.record(StatKey::Entropy, data.len()); let mut driver = fuzzer.init(); let store = loop { if fuzzer.debug { @@ -115,14 +113,17 @@ impl<'a> Fuzzer<'a> { let mut entropy = Entropy::new(data); let seed = entropy.read_slice(16); let values = Pcg32::from_seed(seed[..].try_into().unwrap()); - Fuzzer { + let mut fuzzer = Fuzzer { entropy, values, init: Init::Clean, debug, stats, counters: HashMap::new(), - } + }; + fuzzer.init_counters(); + fuzzer.record(StatKey::Entropy, data.len()); + fuzzer } /// Initializes the fuzzing state and returns the store driver. @@ -395,7 +396,7 @@ impl<'a> Fuzzer<'a> { enum Init { /// Fuzzing starts from a clean storage. /// - /// All invariant are checked. + /// All invariants are checked. Clean, /// Fuzzing starts from a dirty storage. @@ -405,7 +406,7 @@ enum Init { /// Fuzzing starts from a simulated old storage. /// - /// All invariant are checked. + /// All invariants are checked. Used { /// Number of simulated used cycles. cycle: usize, @@ -437,9 +438,13 @@ impl Init { #[derive(Default, Clone, Debug)] struct BitStack { /// Bits stored in little-endian (for bytes and bits). + /// + /// The last byte only contains `len` bits. data: Vec, - /// Number of bits stored. + /// Number of bits stored in the last byte. + /// + /// It is 0 if the last byte is full, not 8. len: usize, } @@ -526,8 +531,14 @@ fn bit_stack_ok() { assert_eq!(bits.pop(), Some(false)); assert_eq!(bits.pop(), None); - for i in 0..27 { + let n = 27; + for i in 0..n { assert_eq!(bits.len(), i); bits.push(true); } + for i in (0..n).rev() { + assert_eq!(bits.pop(), Some(true)); + assert_eq!(bits.len(), i); + } + assert_eq!(bits.pop(), None); } From f548a35f011de034785a5bd68c8423eee236362b Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 30 Nov 2020 10:29:18 +0100 Subject: [PATCH 53/54] Do not crash with dirty init --- libraries/persistent_store/fuzz/src/store.rs | 4 +-- libraries/persistent_store/src/buffer.rs | 31 +++++++++++++------- libraries/persistent_store/src/store.rs | 2 +- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/libraries/persistent_store/fuzz/src/store.rs b/libraries/persistent_store/fuzz/src/store.rs index a008a4e..c18eb51 100644 --- a/libraries/persistent_store/fuzz/src/store.rs +++ b/libraries/persistent_store/fuzz/src/store.rs @@ -133,7 +133,7 @@ impl<'a> Fuzzer<'a> { page_size: 1 << self.entropy.read_range(5, 12), max_word_writes: 2, max_page_erases: self.entropy.read_range(0, 50000), - strict_write: true, + strict_mode: true, }; let num_pages = self.entropy.read_range(3, 64); self.record(StatKey::PageSize, options.page_size); @@ -156,7 +156,7 @@ impl<'a> Fuzzer<'a> { if self.debug { println!("Start with dirty storage."); } - options.strict_write = false; + options.strict_mode = false; let storage = BufferStorage::new(storage, options); StoreDriver::Off(StoreDriverOff::new_dirty(storage)) } else if self.entropy.read_bit() { diff --git a/libraries/persistent_store/src/buffer.rs b/libraries/persistent_store/src/buffer.rs index 0059826..ae35089 100644 --- a/libraries/persistent_store/src/buffer.rs +++ b/libraries/persistent_store/src/buffer.rs @@ -23,9 +23,9 @@ use alloc::vec; /// for tests and fuzzing, for which it has dedicated functionalities. /// /// This storage tracks how many times words are written between page erase cycles, how many times -/// pages are erased, and whether an operation flips bits in the wrong direction (optional). -/// Operations panic if those conditions are broken. This storage also permits to interrupt -/// operations for inspection or to corrupt the operation. +/// pages are erased, and whether an operation flips bits in the wrong direction. Operations panic +/// if those conditions are broken (optional). This storage also permits to interrupt operations for +/// inspection or to corrupt the operation. #[derive(Clone)] pub struct BufferStorage { /// Content of the storage. @@ -59,8 +59,13 @@ pub struct BufferOptions { /// How many times a page can be erased. pub max_page_erases: usize, - /// Whether bits cannot be written from 0 to 1. - pub strict_write: bool, + /// Whether the storage should check the flash invariant. + /// + /// When set, the following conditions would panic: + /// - A bit is written from 0 to 1. + /// - A word is written more than `max_word_writes`. + /// - A page is erased more than `max_page_erases`. + pub strict_mode: bool, } /// Corrupts a slice given actual and expected value. @@ -214,7 +219,10 @@ impl BufferStorage { /// /// Panics if the maximum number of erase cycles per page is reached. fn incr_page_erases(&mut self, page: usize) { - assert!(self.page_erases[page] < self.max_page_erases()); + // Check that pages are not erased too many times. + if self.options.strict_mode { + assert!(self.page_erases[page] < self.max_page_erases()); + } self.page_erases[page] += 1; let num_words = self.page_size() / self.word_size(); for word in 0..num_words { @@ -252,7 +260,10 @@ impl BufferStorage { continue; } let word = index / word_size + i; - assert!(self.word_writes[word] < self.max_word_writes()); + // Check that words are not written too many times. + if self.options.strict_mode { + assert!(self.word_writes[word] < self.max_word_writes()); + } self.word_writes[word] += 1; } } @@ -306,8 +317,8 @@ impl Storage for BufferStorage { self.interruption.tick(&operation)?; // Check and update counters. self.incr_word_writes(range.start, value, value); - // Check strict write. - if self.options.strict_write { + // Check that bits are correctly flipped. + if self.options.strict_mode { for (byte, &val) in range.clone().zip(value.iter()) { assert_eq!(self.storage[byte] & val, val); } @@ -472,7 +483,7 @@ mod tests { page_size: 16, max_word_writes: 2, max_page_erases: 3, - strict_write: true, + strict_mode: true, }; // Those words are decreasing bit patterns. Bits are only changed from 1 to 0 and at least one // bit is changed. diff --git a/libraries/persistent_store/src/store.rs b/libraries/persistent_store/src/store.rs index a3e084b..2559485 100644 --- a/libraries/persistent_store/src/store.rs +++ b/libraries/persistent_store/src/store.rs @@ -1257,7 +1257,7 @@ mod tests { page_size: self.page_size, max_word_writes: self.max_word_writes, max_page_erases: self.max_page_erases, - strict_write: true, + strict_mode: true, }; StoreDriverOff::new(options, self.num_pages) } From a0e3048f827b1e80e673ad959eb63f437a69f38e Mon Sep 17 00:00:00 2001 From: Julien Cretin Date: Mon, 30 Nov 2020 11:30:49 +0100 Subject: [PATCH 54/54] Add debug helper for fuzzing --- .../persistent_store/fuzz/examples/store.rs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 libraries/persistent_store/fuzz/examples/store.rs diff --git a/libraries/persistent_store/fuzz/examples/store.rs b/libraries/persistent_store/fuzz/examples/store.rs new file mode 100644 index 0000000..be9240b --- /dev/null +++ b/libraries/persistent_store/fuzz/examples/store.rs @@ -0,0 +1,116 @@ +// Copyright 2019-2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use fuzz_store::{fuzz, StatKey, Stats}; +use std::io::Write; +use std::io::{stdout, Read}; +use std::path::Path; + +fn usage(program: &str) { + println!( + r#"Usage: {} {{ [] | .. }} + +If is not provided, it is read from standard input. + +When .. are provided, only runs matching all predicates are shown. The format of +each is =."#, + program + ); +} + +fn debug(data: &[u8]) { + println!("{:02x?}", data); + fuzz(data, true, None); +} + +/// Bucket predicate. +struct Predicate { + /// Bucket key. + key: StatKey, + + /// Bucket value. + value: usize, +} + +impl std::str::FromStr for Predicate { + type Err = String; + + fn from_str(input: &str) -> Result { + let predicate: Vec<&str> = input.split('=').collect(); + if predicate.len() != 2 { + return Err("Predicate should have exactly one equal sign.".to_string()); + } + let key = predicate[0] + .parse() + .map_err(|_| format!("Predicate key `{}` is not recognized.", predicate[0]))?; + let value: usize = predicate[1] + .parse() + .map_err(|_| format!("Predicate value `{}` is not a number.", predicate[1]))?; + if value != 0 && !value.is_power_of_two() { + return Err(format!( + "Predicate value `{}` is not a bucket.", + predicate[1] + )); + } + Ok(Predicate { key, value }) + } +} + +fn analyze(corpus: &Path, predicates: Vec) { + let mut stats = Stats::default(); + let mut count = 0; + let total = std::fs::read_dir(corpus).unwrap().count(); + for entry in std::fs::read_dir(corpus).unwrap() { + let data = std::fs::read(entry.unwrap().path()).unwrap(); + let mut stat = Stats::default(); + fuzz(&data, false, Some(&mut stat)); + if predicates + .iter() + .all(|p| stat.get_count(p.key, p.value).is_some()) + { + stats.merge(&stat); + } + count += 1; + print!("\u{1b}[K{} / {}\r", count, total); + stdout().flush().unwrap(); + } + // NOTE: To avoid reloading the corpus each time we want to check a different filter, we can + // start an interactive loop here taking filters as input and printing the filtered stats. We + // would keep all individual stats for each run in a vector. + print!("{}", stats); +} + +fn main() { + let args: Vec = std::env::args().collect(); + // No arguments reads from stdin. + if args.len() <= 1 { + let stdin = std::io::stdin(); + let mut data = Vec::new(); + stdin.lock().read_to_end(&mut data).unwrap(); + return debug(&data); + } + let path = Path::new(&args[1]); + // File argument assumes artifact. + if path.is_file() && args.len() == 2 { + return debug(&std::fs::read(path).unwrap()); + } + // Directory argument assumes corpus. + if path.is_dir() { + match args[2..].iter().map(|x| x.parse()).collect() { + Ok(predicates) => return analyze(path, predicates), + Err(error) => eprintln!("Error: {}", error), + } + } + usage(&args[0]); +}