From 2ed520ec380f2913c950fde74c6448719a0fd401 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Mon, 12 Dec 2022 16:18:32 +0100 Subject: [PATCH 1/2] Using CTAP1-devices and allow_lists, responses should contain the correct key-handle and not None --- src/ctap2/commands/get_assertion.rs | 87 ++++++++++++++++---------- src/ctap2/commands/get_version.rs | 6 +- src/ctap2/commands/make_credentials.rs | 10 +-- src/ctap2/commands/mod.rs | 5 +- src/ctap2/mod.rs | 2 +- src/lib.rs | 2 + src/statemachine.rs | 6 -- src/transport/mod.rs | 4 +- 8 files changed, 74 insertions(+), 48 deletions(-) diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index 19589fff..77761721 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -316,8 +316,9 @@ pub(crate) struct CheckKeyHandle<'assertion> { impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> { type Output = (); + type AdditionalInfo = (); - fn ctap1_format(&self, _dev: &mut Dev) -> Result, HIDError> + fn ctap1_format(&self, _dev: &mut Dev) -> Result<(Vec, Self::AdditionalInfo), HIDError> where Dev: U2FDevice + io::Read + io::Write + fmt::Debug, { @@ -332,13 +333,14 @@ impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> { auth_data.extend_from_slice(self.key_handle); let cmd = U2F_AUTHENTICATE; let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?; - Ok(apdu) + Ok((apdu, ())) } fn handle_response_ctap1( &self, status: Result<(), ApduErrorStatus>, _input: &[u8], + _add_info: &Self::AdditionalInfo, ) -> Result> { // From the U2F-spec: https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html#registration-request-message---u2f_register // if the control byte is set to 0x07 by the FIDO Client, the U2F token is supposed to @@ -357,8 +359,9 @@ impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> { impl RequestCtap1 for GetAssertion { type Output = GetAssertionResult; + type AdditionalInfo = PublicKeyCredentialDescriptor; - fn ctap1_format(&self, dev: &mut Dev) -> Result, HIDError> + fn ctap1_format(&self, dev: &mut Dev) -> Result<(Vec, Self::AdditionalInfo), HIDError> where Dev: io::Read + io::Write + fmt::Debug + FidoDevice, { @@ -377,11 +380,14 @@ impl RequestCtap1 for GetAssertion { }; let res = dev.send_ctap1(&check_command); match res { - Ok(_) => Some(allowed_handle.id.clone()), + Ok(_) => Some(allowed_handle.clone()), _ => None, } }) - .ok_or(HIDError::DeviceNotSupported)?; + .ok_or(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + )))?; debug!("sending key_handle = {:?}", key_handle); @@ -391,7 +397,7 @@ impl RequestCtap1 for GetAssertion { 0 }; let mut auth_data = - Vec::with_capacity(2 * PARAMETER_SIZE + 1 /* key_handle_len */ + key_handle.len()); + Vec::with_capacity(2 * PARAMETER_SIZE + 1 /* key_handle_len */ + key_handle.id.len()); if self.is_ctap2_request() { auth_data.extend_from_slice(self.client_data_hash().as_ref()); @@ -404,18 +410,19 @@ impl RequestCtap1 for GetAssertion { auth_data.extend_from_slice(&decoded); } auth_data.extend_from_slice(self.rp.hash().as_ref()); - auth_data.extend_from_slice(&[key_handle.len() as u8]); - auth_data.extend_from_slice(key_handle.as_ref()); + auth_data.extend_from_slice(&[key_handle.id.len() as u8]); + auth_data.extend_from_slice(key_handle.id.as_ref()); let cmd = U2F_AUTHENTICATE; let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?; - Ok(apdu) + Ok((apdu, key_handle)) } fn handle_response_ctap1( &self, status: Result<(), ApduErrorStatus>, input: &[u8], + add_info: &PublicKeyCredentialDescriptor, ) -> Result> { if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { return Err(Retryable::Retry); @@ -451,7 +458,7 @@ impl RequestCtap1 for GetAssertion { extensions: Default::default(), }; let assertion = Assertion { - credentials: None, + credentials: Some(add_info.clone()), signature, user: None, auth_data, @@ -658,7 +665,10 @@ impl<'de> Deserialize<'de> for GetAssertionResponse { #[cfg(test)] pub mod test { - use super::{Assertion, GetAssertion, GetAssertionOptions, GetAssertionResult, HIDError}; + use super::{ + Assertion, CommandError, GetAssertion, GetAssertionOptions, GetAssertionResult, HIDError, + StatusCode, + }; use crate::consts::{ HIDCmd, SW_CONDITIONS_NOT_SATISFIED, SW_NO_ERROR, U2F_CHECK_IS_REGISTERED, U2F_REQUEST_USER_PRESENCE, @@ -895,6 +905,16 @@ pub mod test { cross_origin: false, token_binding: Some(TokenBinding::Present(String::from("AAECAw"))), }; + let allowed_key = PublicKeyCredentialDescriptor { + id: vec![ + 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, + 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, 0xA4, 0x85, + 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, 0xCC, 0x4E, 0x78, + 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, 0xF9, 0x62, 0x00, 0xCC, + 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38, + ], + transports: vec![Transport::USB], + }; let assertion = GetAssertion::new( client_data.clone(), RelyingPartyWrapper::Data(RelyingParty { @@ -902,16 +922,7 @@ pub mod test { name: Some(String::from("Acme")), icon: None, }), - vec![PublicKeyCredentialDescriptor { - id: vec![ - 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, - 0xEF, 0xAA, 0xAC, 0x7B, 0x2B, 0x9C, 0x5C, 0xEF, 0x17, 0x36, 0xC3, 0x71, 0x7D, - 0xA4, 0x85, 0x34, 0xC8, 0xC6, 0xB6, 0x54, 0xD7, 0xFF, 0x94, 0x5F, 0x50, 0xB5, - 0xCC, 0x4E, 0x78, 0x05, 0x5B, 0xDD, 0x39, 0x6B, 0x64, 0xF7, 0x8D, 0xA2, 0xC5, - 0xF9, 0x62, 0x00, 0xCC, 0xD4, 0x15, 0xCD, 0x08, 0xFE, 0x42, 0x00, 0x38, - ], - transports: vec![Transport::USB], - }], + vec![allowed_key.clone()], GetAssertionOptions { user_presence: Some(true), user_verification: None, @@ -934,9 +945,10 @@ pub mod test { U2F_CHECK_IS_REGISTERED, SW_CONDITIONS_NOT_SATISFIED, ); - let ctap1_request = assertion.ctap1_format(&mut device).unwrap(); + let (ctap1_request, key_handle) = assertion.ctap1_format(&mut device).unwrap(); // Check if the request is going to be correct assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1); + assert_eq!(key_handle, allowed_key); // Now do it again, but parse the actual response fill_device_ctap1( @@ -959,7 +971,7 @@ pub mod test { }; let expected_assertion = Assertion { - credentials: None, + credentials: Some(allowed_key), signature: vec![ 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, @@ -1022,15 +1034,25 @@ pub mod test { assert_matches!( assertion.ctap1_format(&mut device), - Err(HIDError::DeviceNotSupported) + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + .. + ))) ); - assertion.allow_list = vec![too_long_key_handle.clone(); 5]; + // Test also multiple too long keys and an empty allow list + for allow_list in [vec![], vec![too_long_key_handle.clone(); 5]] { + assertion.allow_list = allow_list; + + assert_matches!( + assertion.ctap1_format(&mut device), + Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + .. + ))) + ); + } - assert_matches!( - assertion.ctap1_format(&mut device), - Err(HIDError::DeviceNotSupported) - ); let ok_key_handle = PublicKeyCredentialDescriptor { id: vec![ 0x3E, 0xBD, 0x89, 0xBF, 0x77, 0xEC, 0x50, 0x97, 0x55, 0xEE, 0x9C, 0x26, 0x35, 0xEF, @@ -1045,7 +1067,7 @@ pub mod test { too_long_key_handle.clone(), too_long_key_handle.clone(), too_long_key_handle.clone(), - ok_key_handle, + ok_key_handle.clone(), too_long_key_handle, ]; @@ -1056,9 +1078,10 @@ pub mod test { U2F_CHECK_IS_REGISTERED, SW_CONDITIONS_NOT_SATISFIED, ); - let ctap1_request = assertion.ctap1_format(&mut device).unwrap(); + let (ctap1_request, key_handle) = assertion.ctap1_format(&mut device).unwrap(); // Check if the request is going to be correct assert_eq!(ctap1_request, GET_ASSERTION_SAMPLE_REQUEST_CTAP1); + assert_eq!(key_handle, ok_key_handle); // Now do it again, but parse the actual response fill_device_ctap1( @@ -1081,7 +1104,7 @@ pub mod test { }; let expected_assertion = Assertion { - credentials: None, + credentials: Some(ok_key_handle), signature: vec![ 0x30, 0x44, 0x02, 0x20, 0x7B, 0xDE, 0x0A, 0x52, 0xAC, 0x1F, 0x4C, 0x8B, 0x27, 0xE0, 0x03, 0xA3, 0x70, 0xCD, 0x66, 0xA4, 0xC7, 0x11, 0x8D, 0xD2, 0x2D, 0x54, 0x47, 0x83, diff --git a/src/ctap2/commands/get_version.rs b/src/ctap2/commands/get_version.rs index e6d70704..8ec92c68 100644 --- a/src/ctap2/commands/get_version.rs +++ b/src/ctap2/commands/get_version.rs @@ -16,11 +16,13 @@ pub struct GetVersion {} impl RequestCtap1 for GetVersion { type Output = U2FInfo; + type AdditionalInfo = (); fn handle_response_ctap1( &self, _status: Result<(), ApduErrorStatus>, input: &[u8], + _add_info: &(), ) -> Result> { if input.is_empty() { return Err(Retryable::Error(HIDError::Command( @@ -36,7 +38,7 @@ impl RequestCtap1 for GetVersion { } } - fn ctap1_format(&self, _dev: &mut Dev) -> Result, HIDError> + fn ctap1_format(&self, _dev: &mut Dev) -> Result<(Vec, ()), HIDError> where Dev: U2FDevice, { @@ -44,7 +46,7 @@ impl RequestCtap1 for GetVersion { let cmd = U2F_VERSION; let data = CTAP1RequestAPDU::serialize(cmd, flags, &[])?; - Ok(data) + Ok((data, ())) } } diff --git a/src/ctap2/commands/make_credentials.rs b/src/ctap2/commands/make_credentials.rs index acd16bce..1b8a815a 100644 --- a/src/ctap2/commands/make_credentials.rs +++ b/src/ctap2/commands/make_credentials.rs @@ -243,8 +243,9 @@ impl Request for MakeCredentials { impl RequestCtap1 for MakeCredentials { type Output = MakeCredentialsResult; + type AdditionalInfo = (); - fn ctap1_format(&self, dev: &mut Dev) -> Result, HIDError> + fn ctap1_format(&self, dev: &mut Dev) -> Result<(Vec, ()), HIDError> where Dev: io::Read + io::Write + fmt::Debug + FidoDevice, { @@ -306,13 +307,14 @@ impl RequestCtap1 for MakeCredentials { let cmd = U2F_REGISTER; let apdu = CTAP1RequestAPDU::serialize(cmd, flags, ®ister_data)?; - Ok(apdu) + Ok((apdu, ())) } fn handle_response_ctap1( &self, status: Result<(), ApduErrorStatus>, input: &[u8], + _add_info: &(), ) -> Result> { if Err(ApduErrorStatus::ConditionsNotSatisfied) == status { return Err(Retryable::Retry); @@ -674,7 +676,7 @@ pub mod test { .expect("Failed to create MakeCredentials"); let mut device = Device::new("commands/make_credentials").unwrap(); // not really used (all functions ignore it) - let req_serialized = req + let (req_serialized, _) = req .ctap1_format(&mut device) .expect("Failed to serialize MakeCredentials request"); assert_eq!( @@ -683,7 +685,7 @@ pub mod test { req_serialized, MAKE_CREDENTIALS_SAMPLE_REQUEST_CTAP1 ); let (attestation_object, _collected_client_data) = match req - .handle_response_ctap1(Ok(()), &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1) + .handle_response_ctap1(Ok(()), &MAKE_CREDENTIALS_SAMPLE_RESPONSE_CTAP1, &()) .expect("Failed to handle CTAP1 response") { MakeCredentialsResult::CTAP2(attestation_object, _collected_client_data) => { diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index 4c44d81f..22477655 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -55,11 +55,13 @@ impl From for Retryable { pub trait RequestCtap1: fmt::Debug { type Output; + // E.g.: For GetAssertion, which key-handle is currently being tested + type AdditionalInfo; /// Serializes a request into FIDO v1.x / CTAP1 / U2F format. /// /// See [`crate::u2ftypes::CTAP1RequestAPDU::serialize()`] - fn ctap1_format(&self, dev: &mut Dev) -> Result, HIDError> + fn ctap1_format(&self, dev: &mut Dev) -> Result<(Vec, Self::AdditionalInfo), HIDError> where Dev: FidoDevice + Read + Write + fmt::Debug; @@ -68,6 +70,7 @@ pub trait RequestCtap1: fmt::Debug { &self, status: Result<(), ApduErrorStatus>, input: &[u8], + add_info: &Self::AdditionalInfo, ) -> Result>; } diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index aa5dd731..f92a5b0e 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -2,7 +2,7 @@ pub mod commands; pub use commands::get_assertion::AssertionObject; -pub(crate) mod attestation; +pub mod attestation; pub mod client_data; pub mod server; diff --git a/src/lib.rs b/src/lib.rs index 0c9bb76c..16d1da5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,11 +88,13 @@ pub struct KeyHandle { pub type AppId = Vec; +#[derive(Debug)] pub enum RegisterResult { CTAP1(Vec, u2ftypes::U2FDeviceInfo), CTAP2(AttestationObject, CollectedClientDataWrapper), } +#[derive(Debug)] pub enum SignResult { CTAP1(AppId, Vec, Vec, u2ftypes::U2FDeviceInfo), CTAP2(AssertionObject, CollectedClientDataWrapper), diff --git a/src/statemachine.rs b/src/statemachine.rs index bab25364..62a42a9d 100644 --- a/src/statemachine.rs +++ b/src/statemachine.rs @@ -526,7 +526,6 @@ impl StateMachineCtap2 { callback.call(Ok(RegisterResult::CTAP1(data, dev.get_device_info()))) } - Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {} Err(HIDError::Command(CommandError::StatusCode( StatusCode::ChannelBusy, _, @@ -636,11 +635,6 @@ impl StateMachineCtap2 { Ok(GetAssertionResult::CTAP2(assertion, client_data)) => { callback.call(Ok(SignResult::CTAP2(assertion, client_data))) } - // TODO(baloo): if key_handle is invalid for this device, it - // should reply something like: - // CTAP2_ERR_INVALID_CREDENTIAL - // have to check - Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {} Err(HIDError::Command(CommandError::StatusCode( StatusCode::ChannelBusy, _, diff --git a/src/transport/mod.rs b/src/transport/mod.rs index f54ead05..31b6da24 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -114,7 +114,7 @@ pub trait FidoDevice: HIDDevice { fn send_ctap1(&mut self, msg: &Req) -> Result { debug!("sending {:?} to {:?}", msg, self); - let data = msg.ctap1_format(self)?; + let (data, add_info) = msg.ctap1_format(self)?; loop { let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data)?; @@ -133,7 +133,7 @@ pub trait FidoDevice: HIDDevice { // This will bubble up error if status != no error let status = ApduErrorStatus::from([status[0], status[1]]); - match msg.handle_response_ctap1(status, &data) { + match msg.handle_response_ctap1(status, &data, &add_info) { Ok(out) => return Ok(out), Err(Retryable::Retry) => { // sleep 100ms then loop again From 345db9e51e43fac78e87b87971a1678a9347b5d7 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Fri, 13 Jan 2023 11:47:05 +0100 Subject: [PATCH 2/2] Add test_battery --- examples/test_battery.rs | 822 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 822 insertions(+) create mode 100644 examples/test_battery.rs diff --git a/examples/test_battery.rs b/examples/test_battery.rs new file mode 100644 index 00000000..b301d2d9 --- /dev/null +++ b/examples/test_battery.rs @@ -0,0 +1,822 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use authenticator::{ + authenticatorservice::{ + AuthenticatorService, CtapVersion, GetAssertionOptions, MakeCredentialsOptions, + RegisterArgsCtap2, SignArgsCtap2, + }, + ctap2::attestation::AuthenticatorDataFlags, + ctap2::commands::StatusCode, + ctap2::server::{ + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, RelyingParty, Transport, User, + }, + errors::{AuthenticatorError, CommandError, HIDError, PinError}, + statecallback::StateCallback, + AttestationObject, COSEAlgorithm, Pin, RegisterResult, SignResult, StatusUpdate, +}; +use getopts::{Matches, Options}; +use log::debug; +use rand::{self, Rng}; +use std::sync::mpsc::{channel, RecvError, Sender}; +use std::{env, thread}; + +macro_rules! extract { + ($source:expr, $matchpattern:pat, $returnval:expr) => { + match $source { + $matchpattern => $returnval, + _ => { + panic!( + "Could not extract expected pattern. Expected {:?}, got {:?}", + stringify!($matchpattern), + $source + ); + } + } + }; +} + +const PIN: &str = "1234"; + +fn print_usage(program: &str, opts: Options) { + println!("------------------------------------------------------------------------"); + println!("This program runs a variety of tests on the token."); + println!("ATTENTION: This involves R E S E T T I N G your token!"); + println!("------------------------------------------------------------------------"); + let brief = format!("Usage: {} [options]", program); + print!("{}", opts.usage(&brief)); +} + +fn main() { + env_logger::init(); + + let (_program, _opts, matches) = parse_command_line_options(); + let tests_str = matches.opt_str("tests").unwrap_or_default(); + let tests: Vec<_> = if tests_str.is_empty() { + Vec::new() + } else { + tests_str.split(',').collect() + }; + + println!("------------------------------------------------------------------------"); + println!("This program runs a variety of tests on the token."); + println!("ATTENTION: This involves R E S E T T I N G your token!"); + println!("------------------------------------------------------------------------"); + + // CTAP1 tests + // TODO: Once Manager can return AuthenticatorInfo: Check if only CTAP1 is active + if tests.is_empty() || tests.contains(&"ctap1") { + ask_to_de_activate_ctap2(TestCases::CTAP1); + test_ctap2_code_with_ctap1_token(); + } + + // CTAP2 tests + // TODO: Once Manager can return AuthenticatorInfo: Check that CTAP2 is active + if tests.is_empty() || tests.contains(&"ctap2") { + ask_to_de_activate_ctap2(TestCases::CTAP2); + // Pre-requisite: Reset token so it doesn't have a PIN set + reset_ctap2_token(); + test_ctap2_code_with_ctap2_token(None); + // Repeat tests with known PIN + set_pin(); + test_ctap2_code_with_ctap2_token(Some(PIN)); + test_ctap2_code_additional_pin_tests(); + } + + if tests.is_empty() || tests.contains(&"multi_tokens") { + ask_to_de_activate_ctap2(TestCases::MultipleTokens); + test_ctap2_multiple_tokens(); + } + println!("SUCCESS! Testsuite done."); +} + +fn test_ctap2_code_with_ctap1_token() { + // Test 1 - Normal sign in with only the key-handle we just registered + let key_handle = test_ctap2_register_exclude_list(None, CtapVersion::CTAP1); + test_ctap2_sign_allow_list("https://example.com", vec![key_handle.clone()], None); + println!("Sign in - OK"); + + // Test 2 - Sign in with no key-handle. Should return an error + let failed_sign = test_sign_raw_result("https://example.com", vec![], None); + assert!( + matches!( + failed_sign, + Err(AuthenticatorError::HIDError(HIDError::Command( + CommandError::StatusCode(StatusCode::NoCredentials, ..) + ))) + ), + "Got: {:?}", + failed_sign + ); + println!("Sign in expectedly failed - OK"); + + // Test 3 - Sign in with longer AllowList, only one of which is valid + test_ctap2_sign_allow_list( + "https://example.com", + vec![ + generate_dummy_credential(), + generate_dummy_credential(), + key_handle, + generate_dummy_credential(), + ], + None, + ); + println!("Sign in - OK"); + + // Test 4 - Sign in with longer AllowList, all of which are valid + let key_handles = test_ctap2_register_multiple(None, None, CtapVersion::CTAP1); + test_ctap2_sign_allow_list("https://multiregister-example.com", key_handles, None); + println!("Sign in - OK"); +} + +fn test_ctap2_code_with_ctap2_token(pin: Option<&'static str>) { + // Test 1 - Normal sign in with only the key-handle we just registered + let key_handle = test_ctap2_register_exclude_list(pin, CtapVersion::CTAP2); + test_ctap2_sign_allow_list("https://example.com", vec![key_handle.clone()], pin); + println!("Sign in - OK"); + + // Test 2 - Sign in with no key-handle. Should return an error + let failed_sign = test_sign_raw_result("https://example.com", vec![], pin); + assert!( + matches!( + failed_sign, + Err(AuthenticatorError::HIDError(HIDError::Command( + CommandError::StatusCode(StatusCode::NoCredentials, ..) + ))) + ), + "Got: {:?}", + failed_sign + ); + println!("Sign in expectedly failed - OK"); + + // Test 3 - Sign in with longer AllowList, only one of which is valid + test_ctap2_sign_allow_list( + "https://example.com", + vec![ + generate_dummy_credential(), + generate_dummy_credential(), + key_handle, + generate_dummy_credential(), + ], + pin, + ); + println!("Sign in - OK"); + + // Test 4 - Sign in with longer AllowList, all of which are valid + let key_handles = test_ctap2_register_multiple(None, pin, CtapVersion::CTAP2); + test_ctap2_sign_allow_list("https://multiregister-example.com", key_handles, pin); + println!("Sign in - OK"); + + // Test 5 - Sign in with longer AllowList, all of which are valid using resident keys + let key_handles = test_ctap2_register_multiple(Some(true), pin, CtapVersion::CTAP2); + // With allow list + test_ctap2_sign_allow_list("https://multiregister-example.com", key_handles, pin); + // Without allow list + test_ctap2_sign_allow_list("https://multiregister-example.com", vec![], pin); + println!("Sign in - OK"); +} + +fn test_ctap2_code_additional_pin_tests() { + let (mut manager, timeout_ms) = parse_args_and_setup(CtapVersion::CTAP2); + let (status_tx, status_rx) = channel::(); + let args = register_args_ctap2("https://example.com", "A. User"); + let (register_tx, register_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + register_tx.send(rv).unwrap(); + })); + + manager + .register(timeout_ms, args.into(), status_tx, callback) + .expect("Couldn't register"); + + assert!(matches!( + status_rx.recv(), + Ok(StatusUpdate::DeviceAvailable { .. }) + )); + assert!(matches!( + status_rx.recv(), + Ok(StatusUpdate::DeviceSelected(..)) + )); + let sender = extract!( + status_rx.recv(), + Ok(StatusUpdate::PinError(PinError::PinRequired, sender)), + sender + ); + sender + .send(Pin::new("wrong PIN")) + .expect("Failed to send PIN"); + let sender = extract!( + status_rx.recv(), + Ok(StatusUpdate::PinError(PinError::InvalidPin(..), sender)), + sender + ); + sender + .send(Pin::new("another wrong PIN")) + .expect("Failed to send PIN"); + let sender = extract!( + status_rx.recv(), + Ok(StatusUpdate::PinError(PinError::InvalidPin(..), sender)), + sender + ); + sender.send(Pin::new(PIN)).expect("Failed to send PIN"); + extract!(status_rx.recv(), Ok(StatusUpdate::Success { .. }), ()); + let res = register_rx + .recv() + .expect("Problem receiving, unable to continue"); + + // Test 1 - Normal register with empty ExcludeList + let a = extract!(res, Ok(RegisterResult::CTAP2(a, _)), a); + println!("Additional Test 1: Ok"); + check_attestation_object(&a, Some(PIN), CtapVersion::CTAP2); +} + +fn test_ctap2_multiple_tokens() { + let (mut manager, timeout_ms) = parse_args_and_setup(CtapVersion::CTAP2); + let (status_tx, status_rx) = channel::(); + let args = register_args_ctap2("https://example.com", "A. User"); + let (register_tx, register_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + register_tx.send(rv).unwrap(); + })); + + manager + .register(timeout_ms, args.into(), status_tx, callback) + .expect("Couldn't register"); + + for _ in [0, 1] { + assert!(matches!( + status_rx.recv(), + Ok(StatusUpdate::DeviceAvailable { .. }) + )); + } + assert!(matches!( + status_rx.recv(), + Ok(StatusUpdate::SelectDeviceNotice) + )); + + println!("Please touch the old token."); + assert!(matches!( + status_rx.recv(), + Ok(StatusUpdate::DeviceSelected(..)) + )); + let sender = extract!( + status_rx.recv(), + Ok(StatusUpdate::PinError(PinError::PinRequired, sender)), + sender + ); + sender.send(Pin::new(PIN)).expect("Failed to send PIN"); + extract!(status_rx.recv(), Ok(StatusUpdate::Success { .. }), ()); + let res = register_rx + .recv() + .expect("Problem receiving, unable to continue"); + + // Test 1 - Normal register with empty ExcludeList + let a = extract!(res, Ok(RegisterResult::CTAP2(a, _)), a); + println!("Multitoken test 1: Ok"); + check_attestation_object(&a, Some(PIN), CtapVersion::CTAP2); +} + +fn test_ctap2_register_multiple( + resident_key: Option, + pin: Option<&'static str>, + token_version: CtapVersion, +) -> Vec { + let (mut manager, timeout_ms) = parse_args_and_setup(CtapVersion::CTAP2); + let status_tx = spawn_normal_status_update_channels(pin); + + // Register 3 different users + let mut key_handles = Vec::new(); + for username in &["A. User", "A. Nother", "Dr. Who"] { + let mut args = register_args_ctap2("https://multiregister-example.com", username); + args.options.resident_key = resident_key; + + let res = ctap2_register(&mut manager, &args, timeout_ms, &status_tx); + let a = extract!(res, Ok(RegisterResult::CTAP2(a, _)), a); + check_attestation_object(&a, pin, token_version.clone()); + let handle = a + .auth_data + .credential_data + .expect("No credential data found!") + .credential_id; + key_handles.push(PublicKeyCredentialDescriptor { + id: handle, + transports: vec![Transport::USB], + }); + println!("Registering {}: Ok", username); + } + + key_handles +} + +fn test_ctap2_register_exclude_list( + pin: Option<&'static str>, + token_version: CtapVersion, +) -> PublicKeyCredentialDescriptor { + let (mut manager, timeout_ms) = parse_args_and_setup(CtapVersion::CTAP2); + let status_tx = spawn_normal_status_update_channels(pin); + + // Test 1 - Normal register with empty ExcludeList + let mut args = register_args_ctap2("https://example.com", "A. User"); + let res = ctap2_register(&mut manager, &args, timeout_ms, &status_tx); + let a = extract!(res, Ok(RegisterResult::CTAP2(a, _)), a); + check_attestation_object(&a, pin, token_version.clone()); + println!("Test 1: Ok"); + + // Test 2 - Register with already registered key-handle in the ExcludeList + let registered_key_handle = a.auth_data.credential_data.unwrap().credential_id; + args.exclude_list = vec![PublicKeyCredentialDescriptor { + id: registered_key_handle, + transports: vec![Transport::USB], + }]; + let res = ctap2_register(&mut manager, &args, timeout_ms, &status_tx); + extract!( + res, + Err(AuthenticatorError::HIDError(HIDError::Command( + CommandError::StatusCode(StatusCode::CredentialExcluded, None,) + ))), + () + ); + println!("Test 2: Ok"); + + // Test 3 - Register with irrelevant entries in ExcludeList + args.exclude_list = vec![ + PublicKeyCredentialDescriptor { + id: vec![0x54; 32], + transports: vec![Transport::USB], + }, + PublicKeyCredentialDescriptor { + id: vec![0x50; 32], + transports: vec![Transport::USB], + }, + ]; + let res = ctap2_register(&mut manager, &args, timeout_ms, &status_tx); + let a = extract!(res, Ok(RegisterResult::CTAP2(a, _)), a); + check_attestation_object(&a, pin, token_version); + println!("Test 3: Ok"); + PublicKeyCredentialDescriptor { + id: a.auth_data.credential_data.unwrap().credential_id, + transports: vec![Transport::USB], + } +} + +fn test_sign_raw_result( + origin: &str, + key_handles: Vec, + pin: Option<&'static str>, +) -> Result { + let (mut manager, timeout_ms) = parse_args_and_setup(CtapVersion::CTAP2); + let status_tx = spawn_normal_status_update_channels(pin); + + // Test 1 - Normal sign with AllowList + let args = sign_args_ctap2(origin, key_handles); + + ctap2_sign(&mut manager, &args, timeout_ms, &status_tx) +} + +fn test_ctap2_sign_allow_list( + origin: &str, + key_handles: Vec, + pin: Option<&'static str>, +) { + let res = test_sign_raw_result(origin, key_handles, pin); + let a = extract!(res, Ok(SignResult::CTAP2(a, _)), a); + for x in &a.0 { + assert!(!x + .credentials + .as_ref() + .expect("No credentials!") + .id + .is_empty()); + assert!(!x.signature.is_empty()); + assert_eq!( + x.auth_data.rp_id_hash.0.len(), + 32, + "Expected rpid-hash to be 32 long, got: {}", + x.auth_data.rp_id_hash.0.len() + ); + assert!(x + .auth_data + .flags + .contains(AuthenticatorDataFlags::USER_PRESENT)); + if pin.is_some() { + assert!(x + .auth_data + .flags + .contains(AuthenticatorDataFlags::USER_VERIFIED)); + } else { + assert!(!x + .auth_data + .flags + .contains(AuthenticatorDataFlags::USER_VERIFIED)); + } + assert!(!x.auth_data.flags.contains(AuthenticatorDataFlags::ATTESTED)); + } +} + +fn reset_ctap2_token() { + loop { + let (mut manager, timeout_ms) = parse_args_and_setup(CtapVersion::CTAP2); + println!( + "NOTE: Please unplug all devices, type in 'yes' and plug in the device that should be reset." + ); + loop { + let mut s = String::new(); + println!("ATTENTION: Resetting a device will wipe all credentials! Do you wish to continue? [yes/N]"); + std::io::stdin() + .read_line(&mut s) + .expect("Did not enter a correct string"); + let trimmed = s.trim(); + if trimmed.is_empty() || trimmed == "N" || trimmed == "n" { + panic!("Exiting without reset."); + } + if trimmed == "y" { + println!("Please type in the whole word 'yes'"); + continue; + } + if trimmed == "yes" { + break; + } + } + + let (status_tx, status_rx) = channel::(); + let (reset_tx, reset_rx) = channel(); + let rs_tx = reset_tx.clone(); + let callback = StateCallback::new(Box::new(move |rv| { + let _ = rs_tx.send(rv); + })); + + if let Err(e) = manager.reset(timeout_ms, status_tx.clone(), callback.clone()) { + panic!("Couldn't register: {:?}", e); + }; + + loop { + match status_rx.recv() { + Ok(StatusUpdate::SelectDeviceNotice) => { + // Needed to give the tokens enough time to start blinking + // otherwise we may cancel pre-maturely and this binary will hang + std::thread::sleep(std::time::Duration::from_millis(200)); + manager.cancel().unwrap(); + println!("ERROR: Please unplug all other tokens that should not be reset!"); + continue; + } + Ok(StatusUpdate::DeviceSelected(dev_info)) => { + debug!("STATUS: Continuing with device: {}", dev_info); + break; + } + Ok(StatusUpdate::PinError(..)) => panic!("Reset should never ask for a PIN!"), + Ok(_) => { /* Ignore all other updates */ } + Err(RecvError) => { + panic!("RecvError"); + } + } + } + + let reset_result = reset_rx + .recv() + .expect("Problem receiving, unable to continue"); + match reset_result { + Ok(()) => { + println!("Token successfully reset!"); + break; + } + Err(AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::NotAllowed, + _, + )))) => { + println!( + "Resetting is only allowed within the first 10 seconds after powering up." + ); + println!("Please unplug your device, plug it back in and try again."); + continue; + } + Err(e) => panic!("Reset failed: {:?}", e), + }; + } +} + +fn set_pin() { + let (mut manager, timeout_ms) = parse_args_and_setup(CtapVersion::CTAP2); + let status_tx = spawn_normal_status_update_channels(None); + let (reset_tx, reset_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + let _ = reset_tx.send(rv); + })); + + if let Err(e) = manager.set_pin(timeout_ms, Pin::new(PIN), status_tx, callback) { + panic!("Couldn't call set_pin: {:?}", e); + }; + + let reset_result = reset_rx + .recv() + .expect("Problem receiving, unable to continue"); + match reset_result { + Ok(()) => { + println!("PIN successfully set!"); + } + Err(e) => panic!("Setting PIN failed: {:?}", e), + }; +} + +fn spawn_normal_status_update_channels(pin: Option<&'static str>) -> Sender { + let (status_tx, status_rx) = channel::(); + thread::spawn(move || loop { + match status_rx.recv() { + Ok(StatusUpdate::DeviceAvailable { dev_info }) => { + debug!("STATUS: device available: {}", dev_info) + } + Ok(StatusUpdate::DeviceUnavailable { dev_info }) => { + debug!("STATUS: device unavailable: {}", dev_info) + } + Ok(StatusUpdate::Success { dev_info }) => { + debug!("STATUS: success using device: {}", dev_info); + } + Ok(StatusUpdate::SelectDeviceNotice) => { + println!("STATUS: Please select a device by touching one of them."); + } + Ok(StatusUpdate::DeviceSelected(dev_info)) => { + debug!("STATUS: Continuing with device: {}", dev_info); + } + Ok(StatusUpdate::PinError(error, sender)) => match error { + PinError::PinRequired => { + if let Some(pin) = pin { + sender.send(Pin::new(pin)).expect("Failed to send PIN"); + } else { + panic!("Was asked for PIN, but should not have been asked!"); + } + continue; + } + PinError::InvalidPin(attempts) => { + panic!( + "PIN was not accepted! (Your token has {} attempts left).", + attempts.map_or("unkown".to_string(), |a| format!( + "You have {} attempts left.", + a + )) + ); + } + PinError::PinAuthBlocked => { + panic!("Too many failed attempts in one row. Your device has been temporarily blocked. Please unplug it and plug in again.") + } + PinError::PinBlocked => { + panic!("Too many failed attempts. Your device has been blocked. Reset it.") + } + e => { + panic!("Unexpected error: {:?}", e) + } + }, + Err(RecvError) => { + debug!("STATUS: end"); + return; + } + } + }); + status_tx +} + +fn ctap2_register( + manager: &mut AuthenticatorService, + ctap_args: &RegisterArgsCtap2, + timeout_ms: u64, + status_tx: &Sender, +) -> Result { + let (register_tx, register_rx) = channel(); + let callback = StateCallback::new(Box::new(move |rv| { + register_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.register( + timeout_ms, + ctap_args.clone().into(), + status_tx.clone(), + callback, + ) { + panic!("Couldn't register: {:?}", e); + }; + + register_rx + .recv() + .expect("Problem receiving, unable to continue") +} + +fn ctap2_sign( + manager: &mut AuthenticatorService, + ctap_args: &SignArgsCtap2, + timeout_ms: u64, + status_tx: &Sender, +) -> Result { + let (sign_tx, sign_rx) = channel(); + + let callback = StateCallback::new(Box::new(move |rv| { + sign_tx.send(rv).unwrap(); + })); + + if let Err(e) = manager.sign( + timeout_ms, + ctap_args.clone().into(), + status_tx.clone(), + callback, + ) { + panic!("Couldn't sign: {:?}", e); + } + + sign_rx + .recv() + .expect("Problem receiving, unable to continue") +} + +fn generate_challenge() -> Vec { + let mut res = vec![u8::default(); 32]; + rand::thread_rng().fill(&mut res[..]); + res +} + +fn generate_dummy_credential() -> PublicKeyCredentialDescriptor { + let mut res = vec![0; 32]; + rand::thread_rng().fill(&mut res[..]); + PublicKeyCredentialDescriptor { + id: res, + transports: vec![Transport::USB], + } +} + +fn register_args_ctap2(origin: &str, username: &str) -> RegisterArgsCtap2 { + let chall_bytes = generate_challenge(); + let user = User { + id: username.as_bytes().to_vec(), + icon: None, + name: Some(username.to_string()), + display_name: None, + }; + + RegisterArgsCtap2 { + challenge: chall_bytes, + relying_party: RelyingParty { + // Removing https:// + id: origin[8..].to_string(), + name: None, + icon: None, + }, + origin: origin.to_string(), + user, + pub_cred_params: vec![ + PublicKeyCredentialParameters { + alg: COSEAlgorithm::ES256, + }, + PublicKeyCredentialParameters { + alg: COSEAlgorithm::RS256, + }, + ], + exclude_list: vec![], + options: MakeCredentialsOptions { + resident_key: None, + user_verification: None, + }, + extensions: Default::default(), + pin: None, + } +} + +fn sign_args_ctap2(origin: &str, allow_list: Vec) -> SignArgsCtap2 { + let chall_bytes = generate_challenge(); + SignArgsCtap2 { + challenge: chall_bytes, + origin: origin.to_string(), + relying_party_id: origin[8..].to_string(), + allow_list, + options: GetAssertionOptions::default(), + extensions: Default::default(), + // GetAssertionExtensions { + // hmac_secret: None, + // }, + pin: None, + } +} + +fn parse_command_line_options() -> (String, Options, Matches) { + let args: Vec = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.optflag("x", "no-u2f-usb-hid", "do not enable u2f-usb-hid platforms"); + opts.optflag("h", "help", "print this help menu").optopt( + "t", + "timeout", + "timeout in seconds", + "SEC", + ); + + opts.optflag("h", "help", "print this help menu"); + opts.optopt( + "t", + "tests", + "Which tests should be run. Seperated by commas. Default: ctap1,ctap2,multi_tokens", + "TESTS", + ); + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => panic!("{}", f.to_string()), + }; + + if matches.opt_present("help") { + print_usage(&program, opts); + panic!(); + } + + (program, opts, matches) +} + +fn parse_args_and_setup(ctap_version: CtapVersion) -> (AuthenticatorService, u64) { + let (program, opts, matches) = parse_command_line_options(); + let mut manager = + AuthenticatorService::new(ctap_version).expect("The auth service should initialize safely"); + + if !matches.opt_present("no-u2f-usb-hid") { + manager.add_u2f_usb_hid_platform_transports(); + } + + let timeout_ms = match matches.opt_get_default::("timeout", 15) { + Ok(timeout_s) => { + debug!("Using {}s as the timeout", &timeout_s); + timeout_s * 1_000 + } + Err(e) => { + println!("{}", e); + print_usage(&program, opts); + panic!(); + } + }; + (manager, timeout_ms) +} + +fn check_attestation_object( + res: &AttestationObject, + pin: Option<&'static str>, + token_version: CtapVersion, +) { + let cred_data = res + .auth_data + .credential_data + .as_ref() + .expect("No credential data found!"); + assert!(!cred_data.credential_id.is_empty()); + assert!(res + .auth_data + .flags + .contains(AuthenticatorDataFlags::USER_PRESENT)); + assert!(res + .auth_data + .flags + .contains(AuthenticatorDataFlags::ATTESTED)); + if pin.is_some() { + // User verified with PIN was provided, otherwise not + assert!(res + .auth_data + .flags + .contains(AuthenticatorDataFlags::USER_VERIFIED)); + } else { + assert!(!res + .auth_data + .flags + .contains(AuthenticatorDataFlags::USER_VERIFIED)); + } + // Check that the RP-id has the right length + assert_eq!( + res.auth_data.rp_id_hash.0.len(), + 32, + "Expected rpid-hash to be 32 long, got: {}", + res.auth_data.rp_id_hash.0.len() + ); + if token_version == CtapVersion::CTAP1 { + // Check that AAGUID is all zeros + assert!(cred_data.aaguid.0.iter().any(|x| *x == 0)); + } else { + // Check that AAGUID is not all zeros + assert!(cred_data.aaguid.0.iter().any(|x| *x != 0)); + } +} + +fn ask_to_de_activate_ctap2(tests: TestCases) { + println!("------------------------------------------------------------------------"); + let print_single_token = |first, second| { + println!("Please plug in a token {first} or {second} CTAP2 on your token!"); + }; + match tests { + TestCases::CTAP1 => print_single_token("without CTAP2", "DEactivate"), + TestCases::CTAP2 => print_single_token("with CTAP2", "activate"), + TestCases::MultipleTokens => { + println!("Please plug in two CTAP2 tokens! And always select the old one.") + } + } + println!("------------------------------------------------------------------------"); + println!("Press Return to continue..."); + + let mut s = String::new(); + let _ = std::io::stdin().read_line(&mut s); +} + +enum TestCases { + CTAP1, + CTAP2, + MultipleTokens, +}