From e4d16ff6b303c0792a9e39f3abfef787ef9f0d51 Mon Sep 17 00:00:00 2001 From: Martin Sirringhaus <> Date: Thu, 7 Dec 2023 12:00:00 +0100 Subject: [PATCH] Add logging-possiblity to send_msg()-functiongroup --- examples/ctap2.rs | 9 ++++ examples/ctap2_discoverable_creds.rs | 23 ++++++++-- examples/interactive_management.rs | 13 +++++- examples/prf.rs | 9 ++++ examples/set_pin.rs | 9 ++++ examples/test_exclude_list.rs | 9 ++++ src/authenticatorservice.rs | 5 ++- src/ctap2/commands/get_assertion.rs | 19 +++++++-- src/ctap2/commands/large_blobs.rs | 4 +- src/ctap2/commands/mod.rs | 4 +- src/ctap2/commands/reset.rs | 2 +- src/ctap2/commands/selection.rs | 2 +- src/ctap2/mod.rs | 39 +++++++++++------ src/ctap2/preflight.rs | 45 +++++++++++++------ src/ctap2/server.rs | 12 +++--- src/lib.rs | 4 +- src/status_update.rs | 8 ++++ src/transport/hid.rs | 64 ++++++++++++++++++++++------ src/transport/mod.rs | 44 ++++++++++++------- 19 files changed, 245 insertions(+), 79 deletions(-) diff --git a/examples/ctap2.rs b/examples/ctap2.rs index 78701bdc..f2d8e8f6 100644 --- a/examples/ctap2.rs +++ b/examples/ctap2.rs @@ -48,6 +48,7 @@ fn main() { opts.optflag("s", "hmac_secret", "With hmac-secret"); opts.optflag("h", "help", "print this help menu"); opts.optflag("f", "fallback", "Use CTAP1 fallback implementation"); + opts.optflag("l", "logging", "Active request/response logging"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => panic!("{}", f.to_string()), @@ -81,6 +82,7 @@ fn main() { let mut chall_bytes = [0u8; 32]; thread_rng().fill_bytes(&mut chall_bytes); + let do_logging = matches.opt_present("logging"); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { match status_rx.recv() { @@ -139,6 +141,13 @@ fn main() { Ok(StatusUpdate::LargeBlobData(_, _)) => { panic!("Unexpected large blob data request") } + Ok(StatusUpdate::RequestLogging(dir, msg)) => { + if do_logging { + println!("{dir:?} -> "); + println!(" {msg}"); + println!("--------------------------------------"); + } + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/examples/ctap2_discoverable_creds.rs b/examples/ctap2_discoverable_creds.rs index 49660960..0f9ffe69 100644 --- a/examples/ctap2_discoverable_creds.rs +++ b/examples/ctap2_discoverable_creds.rs @@ -74,6 +74,7 @@ fn register_user( username: &str, timeout_ms: u64, matches: &Matches, + do_logging: bool, ) { println!(); println!("*********************************************************************"); @@ -184,6 +185,13 @@ fn register_user( panic!("Unexpected large blob data request"); } } + Ok(StatusUpdate::RequestLogging(dir, msg)) => { + if do_logging { + println!("{dir:?} -> "); + println!(" {msg}"); + println!("--------------------------------------"); + } + } Err(RecvError) => { println!("STATUS: end"); return; @@ -301,9 +309,10 @@ fn main() { "SEC", ); opts.optflag("s", "skip_reg", "Skip registration"); - opts.optflag("b", "cred_blob", "With credBlob"); - opts.optflag("l", "large_blob_key", "With largeBlobKey-extension"); + opts.optflag("b", "cred_blob", "With credBlob-extension"); + opts.optflag("k", "large_blob_key", "With largeBlobKey-extension"); opts.optflag("h", "help", "print this help menu"); + opts.optflag("l", "logging", "Active request/response logging"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => panic!("{}", f.to_string()), @@ -329,9 +338,10 @@ fn main() { } }; + let do_logging = matches.opt_present("logging"); if !matches.opt_present("skip_reg") { for username in &["A. User", "A. Nother", "Dr. Who"] { - register_user(&mut manager, username, timeout_ms, &matches) + register_user(&mut manager, username, timeout_ms, &matches, do_logging) } } @@ -409,6 +419,13 @@ fn main() { Ok(StatusUpdate::LargeBlobData(..)) => { panic!("Unexpected large blob data request") } + Ok(StatusUpdate::RequestLogging(dir, msg)) => { + if do_logging { + println!("{dir:?} -> "); + println!("{msg}"); + println!("--------------------------------------"); + } + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/examples/interactive_management.rs b/examples/interactive_management.rs index c0dbe4bd..9e193370 100644 --- a/examples/interactive_management.rs +++ b/examples/interactive_management.rs @@ -573,7 +573,7 @@ fn handle_bio_enrollments( } } -fn interactive_status_callback(status_rx: Receiver) { +fn interactive_status_callback(status_rx: Receiver, do_logging: bool) { let mut tx = None; let mut auth_info = None; loop { @@ -733,6 +733,13 @@ fn interactive_status_callback(status_rx: Receiver) { Ok(StatusUpdate::LargeBlobData(_, _)) => { panic!("Unexpected large blob data request") } + Ok(StatusUpdate::RequestLogging(dir, msg)) => { + if do_logging { + println!("{dir:?} -> "); + println!(" {msg}"); + println!("--------------------------------------"); + } + } Err(RecvError) => { println!("STATUS: end"); return; @@ -755,6 +762,7 @@ fn main() { "SEC", ); opts.optflag("h", "help", "print this help menu"); + opts.optflag("l", "logging", "Active request/response logging"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => panic!("{}", f.to_string()), @@ -780,8 +788,9 @@ fn main() { } }; + let do_logging = matches.opt_present("logging"); let (status_tx, status_rx) = channel::(); - thread::spawn(move || interactive_status_callback(status_rx)); + thread::spawn(move || interactive_status_callback(status_rx, do_logging)); let (manage_tx, manage_rx) = channel(); let state_callback = diff --git a/examples/prf.rs b/examples/prf.rs index 4a118529..dd442fd6 100644 --- a/examples/prf.rs +++ b/examples/prf.rs @@ -45,6 +45,7 @@ fn main() { "hmac-secret", "Return hmac-secret outputs instead of prf outputs (i.e., do not prefix and hash the inputs)", ); + opts.optflag("l", "logging", "Active request/response logging"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => panic!("{}", f.to_string()), @@ -93,6 +94,7 @@ fn main() { println!("Asking a security key to register now..."); let mut chall_bytes = [0u8; 32]; thread_rng().fill_bytes(&mut chall_bytes); + let do_logging = matches.opt_present("logging"); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { @@ -152,6 +154,13 @@ fn main() { Ok(StatusUpdate::LargeBlobData(..)) => { panic!("Unexpected large blob data request") } + Ok(StatusUpdate::RequestLogging(dir, msg)) => { + if do_logging { + println!("{dir:?} -> "); + println!(" {msg}"); + println!("--------------------------------------"); + } + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/examples/set_pin.rs b/examples/set_pin.rs index 98cf8343..08514a82 100644 --- a/examples/set_pin.rs +++ b/examples/set_pin.rs @@ -33,6 +33,7 @@ fn main() { Ok(m) => m, Err(f) => panic!("{}", f.to_string()), }; + opts.optflag("l", "logging", "Active request/response logging"); if matches.opt_present("help") { print_usage(&program, opts); return; @@ -62,6 +63,7 @@ fn main() { return; } + let do_logging = matches.opt_present("logging"); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { match status_rx.recv() { @@ -120,6 +122,13 @@ fn main() { Ok(StatusUpdate::LargeBlobData(_, _)) => { panic!("Unexpected large blob data request") } + Ok(StatusUpdate::RequestLogging(dir, msg)) => { + if do_logging { + println!("{dir:?} -> "); + println!(" {msg}"); + println!("--------------------------------------"); + } + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/examples/test_exclude_list.rs b/examples/test_exclude_list.rs index fa7716b9..9ebc380d 100644 --- a/examples/test_exclude_list.rs +++ b/examples/test_exclude_list.rs @@ -44,6 +44,7 @@ fn main() { Ok(m) => m, Err(f) => panic!("{}", f.to_string()), }; + opts.optflag("l", "logging", "Active request/response logging"); if matches.opt_present("help") { print_usage(&program, opts); return; @@ -74,6 +75,7 @@ fn main() { ); let chall_bytes = Sha256::digest(challenge_str.as_bytes()).into(); + let do_logging = matches.opt_present("logging"); let (status_tx, status_rx) = channel::(); thread::spawn(move || loop { match status_rx.recv() { @@ -132,6 +134,13 @@ fn main() { Ok(StatusUpdate::LargeBlobData(_, _)) => { panic!("Unexpected large blob data request") } + Ok(StatusUpdate::RequestLogging(dir, msg)) => { + if do_logging { + println!("{dir:?} -> "); + println!(" {msg}"); + println!("--------------------------------------"); + } + } Err(RecvError) => { println!("STATUS: end"); return; diff --git a/src/authenticatorservice.rs b/src/authenticatorservice.rs index e5935150..084d9e17 100644 --- a/src/authenticatorservice.rs +++ b/src/authenticatorservice.rs @@ -11,9 +11,10 @@ use crate::ctap2::server::{ use crate::errors::*; use crate::manager::Manager; use crate::statecallback::StateCallback; +use serde::Serialize; use std::sync::{mpsc::Sender, Arc, Mutex}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct RegisterArgs { pub client_data_hash: [u8; 32], pub relying_party: RelyingParty, @@ -28,7 +29,7 @@ pub struct RegisterArgs { pub use_ctap1_fallback: bool, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct SignArgs { pub client_data_hash: [u8; 32], pub origin: String, diff --git a/src/ctap2/commands/get_assertion.rs b/src/ctap2/commands/get_assertion.rs index bfb98486..3e4831e0 100644 --- a/src/ctap2/commands/get_assertion.rs +++ b/src/ctap2/commands/get_assertion.rs @@ -643,7 +643,7 @@ impl RequestCtap2 for GetAssertion { let msg = GetNextAssertion; // We already have one, so skipping 0 for _ in 1..number_of_credentials { - let assertion = dev.send_cbor(&msg)?; + let assertion = dev.send_cbor(&msg, None)?; let user_selected = assertion.user_selected; let large_blob_key = assertion.large_blob_key.clone(); results.push(GetAssertionResult { @@ -896,6 +896,7 @@ pub mod test { use crate::transport::{FidoDevice, FidoDeviceIO, FidoProtocol}; use crate::u2ftypes::U2FDeviceInfo; use rand::{thread_rng, RngCore}; + use std::sync::mpsc::channel; #[test] fn test_get_assertion_ctap2() { @@ -1056,7 +1057,7 @@ pub mod test { large_blob_key: None, large_blob_array: None, }]; - let response = device.send_cbor(&assertion).unwrap(); + let response = device.send_cbor(&assertion, None).unwrap(); assert_eq!(response, expected); } @@ -1332,6 +1333,7 @@ pub mod test { device.set_cid(cid); // ctap1 request + let (tx, _rx) = channel(); fill_device_ctap1( &mut device, cid, @@ -1343,6 +1345,7 @@ pub mod test { &assertion.allow_list, &assertion.rp, &assertion.client_data_hash, + &tx, ) .expect("Did not find a key_handle, even though it should have"); assertion.allow_list = vec![key_handle]; @@ -1355,7 +1358,7 @@ pub mod test { // Pre-flighting is not done automatically fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR); - let response = device.send_ctap1(&assertion).unwrap(); + let response = device.send_ctap1(&assertion, None).unwrap(); // Check if response is correct let expected_auth_data = AuthenticatorData { @@ -1424,12 +1427,14 @@ pub mod test { device.set_cid(cid); + let (tx, _rx) = channel(); assert_matches!( do_credential_list_filtering_ctap1( &mut device, &assertion.allow_list, &assertion.rp, &assertion.client_data_hash, + &tx, ), None ); @@ -1447,12 +1452,14 @@ pub mod test { for allow_list in [vec![], vec![too_long_key_handle.clone(); 5]] { assertion.allow_list = allow_list; + let (tx, _rx) = channel(); assert_matches!( do_credential_list_filtering_ctap1( &mut device, &assertion.allow_list, &assertion.rp, &assertion.client_data_hash, + &tx, ), None ); @@ -1483,11 +1490,13 @@ pub mod test { U2F_CHECK_IS_REGISTERED, SW_CONDITIONS_NOT_SATISFIED, ); + let (tx, _rx) = channel(); let key_handle = do_credential_list_filtering_ctap1( &mut device, &assertion.allow_list, &assertion.rp, &assertion.client_data_hash, + &tx, ) .expect("Did not find a key_handle, even though it should have"); assertion.allow_list = vec![key_handle]; @@ -1500,7 +1509,7 @@ pub mod test { // Pre-flighting is not done automatically fill_device_ctap1(&mut device, cid, U2F_REQUEST_USER_PRESENCE, SW_NO_ERROR); - let response = device.send_ctap1(&assertion).unwrap(); + let response = device.send_ctap1(&assertion, None).unwrap(); // Check if response is correct let expected_auth_data = AuthenticatorData { @@ -1768,12 +1777,14 @@ pub mod test { msg.extend(&GET_ASSERTION_SAMPLE_RESPONSE_CTAP2[293..]); device.add_read(&msg, 0); + let (tx, _rx) = channel(); assert_matches!( do_credential_list_filtering_ctap2( &mut device, &assertion.allow_list, &assertion.rp, None, + &tx, ), Ok(..) ); diff --git a/src/ctap2/commands/large_blobs.rs b/src/ctap2/commands/large_blobs.rs index e93e6218..a16e8813 100644 --- a/src/ctap2/commands/large_blobs.rs +++ b/src/ctap2/commands/large_blobs.rs @@ -377,7 +377,7 @@ where length: None, pin_uv_auth_param: None, }; - let mut segment = dev.send_cbor_cancellable(&cmd, keep_alive)?; + let mut segment = dev.send_cbor_cancellable(&cmd, keep_alive, None)?; let segment_len = segment.len(); bytes.append(&mut segment); // Spec: @@ -440,7 +440,7 @@ where pin_uv_auth_param: None, }; cmd.set_pin_uv_auth_param(pin_uv_auth_token.clone())?; - dev.send_cbor_cancellable(&cmd, keep_alive)?; + dev.send_cbor_cancellable(&cmd, keep_alive, None)?; offset += chunk_len as u64; } Ok(()) diff --git a/src/ctap2/commands/mod.rs b/src/ctap2/commands/mod.rs index b33562f2..8040f5ed 100644 --- a/src/ctap2/commands/mod.rs +++ b/src/ctap2/commands/mod.rs @@ -162,7 +162,7 @@ pub(crate) fn repackage_pin_errors( let cmd = GetPinRetries::new(); // Treat any error as if the device returned a valid response without a pinRetries // field. - let resp = dev.send_cbor(&cmd).unwrap_or_default(); + let resp = dev.send_cbor(&cmd, None).unwrap_or_default(); AuthenticatorError::PinError(PinError::InvalidPin(resp.pin_retries)) } HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthBlocked, _)) => { @@ -185,7 +185,7 @@ pub(crate) fn repackage_pin_errors( let cmd = GetUvRetries::new(); // Treat any error as if the device returned a valid response without a uvRetries // field. - let resp = dev.send_cbor(&cmd).unwrap_or_default(); + let resp = dev.send_cbor(&cmd, None).unwrap_or_default(); AuthenticatorError::PinError(PinError::InvalidUv(resp.uv_retries)) } HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) => { diff --git a/src/ctap2/commands/reset.rs b/src/ctap2/commands/reset.rs index a1006800..51a376ca 100644 --- a/src/ctap2/commands/reset.rs +++ b/src/ctap2/commands/reset.rs @@ -79,7 +79,7 @@ pub mod tests { msg.extend(add); // + maybe additional data device.add_read(&msg, 0); - device.send_cbor(&Reset {}) + device.send_cbor(&Reset {}, None) } #[test] diff --git a/src/ctap2/commands/selection.rs b/src/ctap2/commands/selection.rs index 4e9fc521..b1550361 100644 --- a/src/ctap2/commands/selection.rs +++ b/src/ctap2/commands/selection.rs @@ -79,7 +79,7 @@ pub mod tests { msg.extend(add); // + maybe additional data device.add_read(&msg, 0); - device.send_cbor(&Selection {}) + device.send_cbor(&Selection {}, None) } #[test] diff --git a/src/ctap2/mod.rs b/src/ctap2/mod.rs index e8e05cd5..36432dea 100644 --- a/src/ctap2/mod.rs +++ b/src/ctap2/mod.rs @@ -531,6 +531,7 @@ pub fn register( &makecred.exclude_list, &makecred.rp, pin_uv_auth_result.get_pin_uv_auth_token(), + &status, ), callback ); @@ -540,6 +541,7 @@ pub fn register( &makecred.exclude_list, &makecred.rp, &makecred.client_data_hash, + &status, ); // That handle was already registered with the token if key_handle.is_some() { @@ -548,7 +550,7 @@ pub fn register( // making the token blink upon device selection. send_status(&status, crate::StatusUpdate::PresenceRequired); let msg = dummy_make_credentials_cmd(); - let _ = dev.send_msg_cancellable(&msg, alive); // Ignore answer, return "CredentialExcluded" + let _ = dev.send_msg_cancellable(&msg, alive, Some(&status)); // Ignore answer, return "CredentialExcluded" callback.call(Err(AuthenticatorError::CredentialExcluded)); return false; } @@ -558,7 +560,7 @@ pub fn register( debug!("{makecred:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); send_status(&status, crate::StatusUpdate::PresenceRequired); - let resp = dev.send_msg_cancellable(&makecred, alive); + let resp = dev.send_msg_cancellable(&makecred, alive, Some(&status)); match resp { Ok(result) => { if has_large_blob && result.large_blob_key.is_some() { @@ -621,8 +623,13 @@ pub fn sign( // Try to silently discover U2F credentials that require the FIDO App ID extension. If // any are found, we should use the alternate RP ID instead of the provided RP ID. let alt_rp_id = RelyingParty::from(app_id); - let silent_creds = - silently_discover_credentials(dev, &allow_list, &alt_rp_id, &client_data_hash); + let silent_creds = silently_discover_credentials( + dev, + &allow_list, + &alt_rp_id, + &client_data_hash, + &status, + ); if !silent_creds.is_empty() { allow_list = silent_creds; rp_id = alt_rp_id; @@ -668,6 +675,7 @@ pub fn sign( &get_assertion.allow_list, &get_assertion.rp, pin_uv_auth_result.get_pin_uv_auth_token(), + &status, ), callback ); @@ -677,6 +685,7 @@ pub fn sign( &get_assertion.allow_list, &get_assertion.rp, &get_assertion.client_data_hash, + &status, ); match key_handle { Some(key_handle) => { @@ -693,7 +702,7 @@ pub fn sign( // We have to collect a user interaction send_status(&status, crate::StatusUpdate::PresenceRequired); let msg = dummy_make_credentials_cmd(); - let _ = dev.send_msg_cancellable(&msg, alive); // Ignore answer, return "NoCredentials" + let _ = dev.send_msg_cancellable(&msg, alive, Some(&status)); // Ignore answer, return "NoCredentials" callback.call(Err(HIDError::Command(CommandError::StatusCode( StatusCode::NoCredentials, None, @@ -717,7 +726,7 @@ pub fn sign( debug!("{get_assertion:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); send_status(&status, crate::StatusUpdate::PresenceRequired); - let mut results = match dev.send_msg_cancellable(&get_assertion, alive) { + let mut results = match dev.send_msg_cancellable(&get_assertion, alive, Some(&status)) { Ok(results) => results, Err(e) => { handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv); @@ -783,7 +792,7 @@ pub(crate) fn reset_helper>( debug!("{:?}", reset); debug!("------------------------------------------------------------------"); send_status(&status, crate::StatusUpdate::PresenceRequired); - let resp = dev.send_cbor_cancellable(&reset, keep_alive); + let resp = dev.send_cbor_cancellable(&reset, keep_alive, Some(&status)); if resp.is_ok() { // The DeviceSelector could already be dead, but it might also wait // for us to respond, in order to cancel all other tokens in case @@ -864,7 +873,7 @@ pub(crate) fn set_or_change_pin_helper>( res = ChangeExistingPin::new(&authinfo, &shared_secret, &curr_pin, &new_pin) .map_err(HIDError::Command) - .and_then(|msg| dev.send_cbor_cancellable(&msg, alive)) + .and_then(|msg| dev.send_cbor_cancellable(&msg, alive, Some(&status))) .map_err(|e| repackage_pin_errors(dev, e)); if let Err(AuthenticatorError::PinError(PinError::InvalidPin(r))) = res { @@ -888,8 +897,12 @@ pub(crate) fn set_or_change_pin_helper>( } res } else { - dev.send_cbor_cancellable(&SetNewPin::new(&shared_secret, &new_pin), alive) - .map_err(AuthenticatorError::HIDError) + dev.send_cbor_cancellable( + &SetNewPin::new(&shared_secret, &new_pin), + alive, + Some(&status), + ) + .map_err(AuthenticatorError::HIDError) }; // the callback is expecting `Result<(), AuthenticatorError>`, but `ChangeExistingPin` // and `SetNewPin` return the default `ClientPinResponse` on success. Just discard it. @@ -994,7 +1007,7 @@ pub(crate) fn bio_enrollment( debug!("{bio_cmd:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); - let resp = dev.send_cbor_cancellable(&bio_cmd, alive); + let resp = dev.send_cbor_cancellable(&bio_cmd, alive, Some(&status)); match resp { Ok(result) => { skip_puap = true; @@ -1264,7 +1277,7 @@ pub(crate) fn credential_management( debug!("{cred_management:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); - let resp = dev.send_cbor_cancellable(&cred_management, alive); + let resp = dev.send_cbor_cancellable(&cred_management, alive, Some(&status)); match resp { Ok(result) => { skip_puap = true; @@ -1548,7 +1561,7 @@ pub(crate) fn configure_authenticator( debug!("{authcfg:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); - let resp = dev.send_cbor_cancellable(&authcfg, alive); + let resp = dev.send_cbor_cancellable(&authcfg, alive, Some(&status)); match resp { Ok(()) => { let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); diff --git a/src/ctap2/preflight.rs b/src/ctap2/preflight.rs index baf11275..b8ed5794 100644 --- a/src/ctap2/preflight.rs +++ b/src/ctap2/preflight.rs @@ -8,7 +8,9 @@ use crate::errors::AuthenticatorError; use crate::transport::errors::{ApduErrorStatus, HIDError}; use crate::transport::{FidoDevice, FidoProtocol, VirtualFidoDevice}; use crate::u2ftypes::CTAP1RequestAPDU; +use crate::StatusUpdate; use sha2::{Digest, Sha256}; +use std::sync::mpsc::Sender; /// This command is used to check which key_handle is valid for this /// token. This is sent before a GetAssertion command, to determine which @@ -87,6 +89,7 @@ pub(crate) fn do_credential_list_filtering_ctap1( cred_list: &[PublicKeyCredentialDescriptor], rp: &RelyingParty, client_data_hash: &ClientDataHash, + status: &Sender, ) -> Option { let key_handle = cred_list .iter() @@ -100,7 +103,7 @@ pub(crate) fn do_credential_list_filtering_ctap1( client_data_hash: client_data_hash.as_ref(), rp, }; - let res = dev.send_ctap1(&check_command); + let res = dev.send_ctap1(&check_command, Some(status)); match res { Ok(_) => Some(key_handle.clone()), _ => None, @@ -118,6 +121,7 @@ pub(crate) fn do_credential_list_filtering_ctap2( cred_list: &[PublicKeyCredentialDescriptor], rp: &RelyingParty, pin_uv_auth_token: Option, + status: &Sender, ) -> Result, AuthenticatorError> { let info = dev .get_authenticator_info() @@ -164,7 +168,7 @@ pub(crate) fn do_credential_list_filtering_ctap2( GetAssertionExtensions::default(), ); silent_assert.set_pin_uv_auth_param(pin_uv_auth_token.clone())?; - match dev.send_msg(&silent_assert) { + match dev.send_msg(&silent_assert, Some(status)) { Ok(mut response) => { // This chunk contains a key_handle that is already known to the device. // Filter out all credentials the device returned. Those are valid. @@ -205,13 +209,15 @@ pub(crate) fn silently_discover_credentials( cred_list: &[PublicKeyCredentialDescriptor], rp: &RelyingParty, client_data_hash: &ClientDataHash, + status: &Sender, ) -> Vec { if dev.get_protocol() == FidoProtocol::CTAP2 { - if let Ok(cred_list) = do_credential_list_filtering_ctap2(dev, cred_list, rp, None) { + if let Ok(cred_list) = do_credential_list_filtering_ctap2(dev, cred_list, rp, None, status) + { return cred_list; } } else if let Some(key_handle) = - do_credential_list_filtering_ctap1(dev, cred_list, rp, client_data_hash) + do_credential_list_filtering_ctap1(dev, cred_list, rp, client_data_hash, status) { return vec![key_handle]; } @@ -238,6 +244,7 @@ pub mod tests { }, Assertion, GetAssertionResult, }; + use std::sync::mpsc::channel; fn new_relying_party(name: &str) -> RelyingParty { RelyingParty { @@ -324,7 +331,8 @@ pub mod tests { make_device_simple_u2f(&mut dev); let client_data_hash = ClientDataHash(Sha256::digest("").into()); let rp = new_relying_party("preflight test"); - let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash, &tx); assert!(res.is_empty()); } @@ -351,7 +359,8 @@ pub mod tests { dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2])); dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one - let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh, &tx); assert_eq!(res, vec![allow_list[2].clone()]); } @@ -375,7 +384,8 @@ pub mod tests { dev.add_upcoming_ctap1_request(&new_check_key_handle(&rp, &cdh, &allow_list[2])); dev.add_upcoming_ctap_response(()); // Valid credential - the code exits here now and doesn't even look at the last one - let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &cdh, &tx); assert_eq!(res, vec![allow_list[2].clone()]); } @@ -385,7 +395,8 @@ pub mod tests { make_device_with_pin(&mut dev); let rp = new_relying_party("preflight test"); let client_data_hash = ClientDataHash(Sha256::digest("").into()); - let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &[], &rp, &client_data_hash, &tx); assert!(res.is_empty()); } @@ -400,7 +411,8 @@ pub mod tests { let allow_list = vec![new_credential(1, 4)]; dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, None)]); - let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash, &tx); assert_eq!(res, allow_list); } @@ -413,7 +425,8 @@ pub mod tests { let allow_list = vec![new_credential(1, 4)]; dev.add_upcoming_ctap2_request(&new_silent_assert(&rp, &allow_list)); dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[0]))]); - let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash, &tx); assert_eq!(res, allow_list); } @@ -444,7 +457,8 @@ pub mod tests { None, ))); dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[2]))]); - let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash, &tx); assert_eq!(res, vec![allow_list[2].clone()]); } @@ -471,7 +485,8 @@ pub mod tests { new_assertion_response(&rp, Some(&allow_list[2])), new_assertion_response(&rp, Some(&allow_list[3])), ]); - let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash, &tx); assert_eq!(res, allow_list[1..].to_vec()); } @@ -499,7 +514,8 @@ pub mod tests { new_assertion_response(&rp, Some(&allow_list[2])), new_assertion_response(&rp, None), // This will be ignored ]); - let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + let (tx, _rx) = channel(); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash, &tx); assert_eq!(res, allow_list[1..=2].to_vec()); } @@ -526,8 +542,9 @@ pub mod tests { &rp, &[allow_list[1].clone(), allow_list[3].clone()], )); + let (tx, _rx) = channel(); dev.add_upcoming_ctap_response(vec![new_assertion_response(&rp, Some(&allow_list[1]))]); - let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash); + let res = silently_discover_credentials(&mut dev, &allow_list, &rp, &client_data_hash, &tx); assert_eq!(res, vec![allow_list[1].clone()]); } } diff --git a/src/ctap2/server.rs b/src/ctap2/server.rs index ff75f592..f5d6e6e7 100644 --- a/src/ctap2/server.rs +++ b/src/ctap2/server.rs @@ -288,14 +288,14 @@ impl From<&KeyHandle> for PublicKeyCredentialDescriptor { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize)] pub enum ResidentKeyRequirement { Discouraged, Preferred, Required, } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize)] pub enum UserVerificationRequirement { Discouraged, Preferred, @@ -383,7 +383,7 @@ where Ok(bytes.to_vec()) } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize)] pub struct AuthenticationExtensionsClientInputs { pub app_id: Option, pub cred_props: Option, @@ -406,7 +406,7 @@ pub struct CredentialProperties { /// Salt inputs for the `hmac-secret` extension. /// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#dictdef-hmacgetsecretinput -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] pub struct HMACGetSecretInput { pub salt1: [u8; 32], pub salt2: Option<[u8; 32]>, @@ -420,7 +420,7 @@ pub struct HMACGetSecretOutput { pub output2: Option<[u8; 32]>, } -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq, Serialize)] pub struct AuthenticationExtensionsPRFInputs { pub eval: Option, pub eval_by_credential: Option, AuthenticationExtensionsPRFValues>>, @@ -500,7 +500,7 @@ impl AuthenticationExtensionsPRFInputs { } } -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] pub struct AuthenticationExtensionsPRFValues { pub first: Vec, pub second: Option>, diff --git a/src/lib.rs b/src/lib.rs index 5dd4133b..50a0c69c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,8 +49,8 @@ pub use ctap2::commands::make_credentials::MakeCredentialsResult; use serde::Serialize; pub use statemachine::StateMachine; pub use status_update::{ - BioEnrollmentCmd, CredManagementCmd, InteractiveRequest, InteractiveUpdate, StatusPinUv, - StatusUpdate, + BioEnrollmentCmd, CredManagementCmd, InteractiveRequest, InteractiveUpdate, MessageDirection, + StatusPinUv, StatusUpdate, }; pub use transport::{FidoDevice, FidoDeviceIO, FidoProtocol, VirtualFidoDevice}; diff --git a/src/status_update.rs b/src/status_update.rs index 14312c8e..714bfcad 100644 --- a/src/status_update.rs +++ b/src/status_update.rs @@ -85,6 +85,12 @@ pub enum StatusPinUv { UvBlocked, } +#[derive(Debug, DeriveSer)] +pub enum MessageDirection { + Request, + Response, +} + #[derive(Debug)] pub enum InteractiveUpdate { StartManagement((Sender, Option)), @@ -111,6 +117,8 @@ pub enum StatusUpdate { /// After MakeCredential, supply the user with the large blob key and let /// them calculate the payload, to send back to us. LargeBlobData(Sender, Vec), + /// Logging of requests being sent to the device + RequestLogging(MessageDirection, String), } pub(crate) fn send_status(status: &Sender, msg: StatusUpdate) { diff --git a/src/transport/hid.rs b/src/transport/hid.rs index 339cd05d..3b4771cd 100644 --- a/src/transport/hid.rs +++ b/src/transport/hid.rs @@ -1,16 +1,19 @@ use super::TestDevice; use crate::consts::{HIDCmd, CID_BROADCAST}; use crate::ctap2::commands::{CommandError, RequestCtap1, RequestCtap2, Retryable, StatusCode}; +use crate::status_update::{send_status, MessageDirection}; use crate::transport::errors::{ApduErrorStatus, HIDError}; use crate::transport::{FidoDevice, FidoDeviceIO, FidoProtocol}; use crate::u2ftypes::{U2FDeviceInfo, U2FHIDCont, U2FHIDInit, U2FHIDInitResp}; use crate::util::io_err; +use crate::StatusUpdate; use rand::{thread_rng, RngCore}; use std::cmp::Eq; use std::fmt; use std::hash::Hash; use std::io; use std::io::{Read, Write}; +use std::sync::mpsc::Sender; use std::thread; use std::time::Duration; @@ -164,14 +167,14 @@ impl FidoDeviceIO for T { &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, + status: Option<&Sender>, ) -> Result { if !self.initialized() { return Err(HIDError::DeviceNotInitialized); } - match self.get_protocol() { - FidoProtocol::CTAP1 => self.send_ctap1_cancellable(msg, keep_alive), - FidoProtocol::CTAP2 => self.send_cbor_cancellable(msg, keep_alive), + FidoProtocol::CTAP1 => self.send_ctap1_cancellable(msg, keep_alive, status), + FidoProtocol::CTAP2 => self.send_cbor_cancellable(msg, keep_alive, status), } } @@ -179,6 +182,7 @@ impl FidoDeviceIO for T { &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, + status: Option<&Sender>, ) -> Result { debug!("sending {:?} to {:?}", msg, self); #[cfg(test)] @@ -188,6 +192,13 @@ impl FidoDeviceIO for T { } } + if let Some(status) = status { + send_status( + status, + StatusUpdate::RequestLogging(MessageDirection::Request, format!("{msg:?}")), + ); + } + let mut data = msg.wire_format()?; let mut buf: Vec = Vec::with_capacity(data.len() + 1); // CTAP2 command @@ -197,17 +208,26 @@ impl FidoDeviceIO for T { let buf = buf; let (cmd, resp) = self.sendrecv(HIDCmd::Cbor, &buf, keep_alive)?; - if cmd == HIDCmd::Cbor { - Ok(msg.handle_response_ctap2(self, &resp)?) + let response = if cmd == HIDCmd::Cbor { + let response = msg.handle_response_ctap2(self, &resp)?; + Ok(response) } else { Err(HIDError::UnexpectedCmd(cmd.into())) + }; + if let Some(status) = status { + send_status( + status, + StatusUpdate::RequestLogging(MessageDirection::Response, format!("{response:?}")), + ); } + response } fn send_ctap1_cancellable( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, + status: Option<&Sender>, ) -> Result { debug!("sending {:?} to {:?}", msg, self); #[cfg(test)] @@ -216,32 +236,52 @@ impl FidoDeviceIO for T { return self.send_ctap1_unserialized(msg); } } + + if let Some(status) = status { + send_status( + status, + StatusUpdate::RequestLogging(MessageDirection::Request, format!("{msg:?}")), + ); + } + let (data, add_info) = msg.ctap1_format()?; while keep_alive() { // sendrecv will not block with a CTAP1 device let (cmd, mut data) = self.sendrecv(HIDCmd::Msg, &data, &|| true)?; - if cmd == HIDCmd::Msg { + let response = if cmd == HIDCmd::Msg { if data.len() < 2 { return Err(io_err("Unexpected Response: shorter than expected").into()); } let split_at = data.len() - 2; - let status = data.split_off(split_at); + let msg_status = data.split_off(split_at); // This will bubble up error if status != no error - let status = ApduErrorStatus::from([status[0], status[1]]); + let msg_status = ApduErrorStatus::from([msg_status[0], msg_status[1]]); - match msg.handle_response_ctap1(self, status, &data, &add_info) { - Ok(out) => return Ok(out), + match msg.handle_response_ctap1(self, msg_status, &data, &add_info) { + Ok(out) => Ok(out), Err(Retryable::Retry) => { // sleep 100ms then loop again // TODO(baloo): meh, use tokio instead? thread::sleep(Duration::from_millis(100)); + continue; } - Err(Retryable::Error(e)) => return Err(e), + Err(Retryable::Error(e)) => Err(e), } } else { - return Err(HIDError::UnexpectedCmd(cmd.into())); + Err(HIDError::UnexpectedCmd(cmd.into())) + }; + + if let Some(status) = status { + send_status( + status, + StatusUpdate::RequestLogging( + MessageDirection::Response, + format!("{response:?}"), + ), + ); } + return response; } Err(HIDError::Command(CommandError::StatusCode( diff --git a/src/transport/mod.rs b/src/transport/mod.rs index 318934ed..b743ab5f 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -17,9 +17,10 @@ use crate::ctap2::preflight::CheckKeyHandle; use crate::transport::device_selector::BlinkResult; use crate::transport::errors::HIDError; -use crate::Pin; +use crate::{Pin, StatusUpdate}; use std::convert::TryFrom; use std::fmt; +use std::sync::mpsc::Sender; pub mod device_selector; pub mod errors; @@ -81,34 +82,46 @@ pub trait FidoDeviceIO { fn send_msg + RequestCtap2>( &mut self, msg: &Req, + status: Option<&Sender>, ) -> Result { - self.send_msg_cancellable(msg, &|| true) + self.send_msg_cancellable(msg, &|| true, status) } - fn send_cbor(&mut self, msg: &Req) -> Result { - self.send_cbor_cancellable(msg, &|| true) + fn send_cbor( + &mut self, + msg: &Req, + status: Option<&Sender>, + ) -> Result { + self.send_cbor_cancellable(msg, &|| true, status) } - fn send_ctap1(&mut self, msg: &Req) -> Result { - self.send_ctap1_cancellable(msg, &|| true) + fn send_ctap1( + &mut self, + msg: &Req, + status: Option<&Sender>, + ) -> Result { + self.send_ctap1_cancellable(msg, &|| true, status) } fn send_msg_cancellable + RequestCtap2>( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, + status: Option<&Sender>, ) -> Result; fn send_cbor_cancellable( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, + status: Option<&Sender>, ) -> Result; fn send_ctap1_cancellable( &mut self, msg: &Req, keep_alive: &dyn Fn() -> bool, + status: Option<&Sender>, ) -> Result; } @@ -142,7 +155,7 @@ where fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo); fn refresh_authenticator_info(&mut self) -> Option<&AuthenticatorInfo> { let command = GetInfo::default(); - if let Ok(info) = self.send_cbor(&command) { + if let Ok(info) = self.send_cbor(&command, None) { debug!("Refreshed authenticator info: {:?}", info); self.set_authenticator_info(info); } @@ -167,7 +180,7 @@ where if self.should_try_ctap2() { let command = GetInfo::default(); - if let Ok(info) = self.send_cbor(&command) { + if let Ok(info) = self.send_cbor(&command, None) { debug!("{:?}", info); if info.max_supported_version() == AuthenticatorVersion::U2F_V2 { self.downgrade_to_ctap1(); @@ -181,7 +194,7 @@ where // We want to return an error here if this device doesn't support CTAP1, // so we send a U2F_VERSION command. let command = GetVersion::default(); - self.send_ctap1(&command)?; + self.send_ctap1(&command, None)?; Ok(()) } @@ -192,14 +205,15 @@ where }); let resp = if supports_select_cmd { let msg = Selection {}; - self.send_cbor_cancellable(&msg, keep_alive) + self.send_cbor_cancellable(&msg, keep_alive, None) } else { // We need to fake a blink-request, because FIDO2.0 forgot to specify one // See: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential let msg = dummy_make_credentials_cmd(); info!("Trying to blink: {:?}", &msg); // We don't care about the Ok-value, just if it is Ok or not - self.send_msg_cancellable(&msg, keep_alive).map(|_| ()) + self.send_msg_cancellable(&msg, keep_alive, None) + .map(|_| ()) }; match resp { @@ -244,7 +258,7 @@ where // Not reusing the shared secret here, if it exists, since we might start again // with a different PIN (e.g. if the last one was wrong) let pin_command = GetKeyAgreement::new(pin_protocol.clone()); - let resp = self.send_cbor_cancellable(&pin_command, alive)?; + let resp = self.send_cbor_cancellable(&pin_command, alive, None)?; if let Some(device_key_agreement_key) = resp.key_agreement { let shared_secret = pin_protocol .encapsulate(&device_key_agreement_key) @@ -275,7 +289,7 @@ where let shared_secret = self.establish_shared_secret(alive)?; let pin_command = GetPinToken::new(&shared_secret, pin); - let resp = self.send_cbor_cancellable(&pin_command, alive)?; + let resp = self.send_cbor_cancellable(&pin_command, alive, None)?; if let Some(encrypted_pin_token) = resp.pin_token { // CTAP 2.1 spec: // If authenticatorClientPIN's getPinToken subcommand is invoked, default permissions @@ -306,7 +320,7 @@ where rp_id.cloned(), ); - let resp = self.send_cbor_cancellable(&pin_command, alive)?; + let resp = self.send_cbor_cancellable(&pin_command, alive, None)?; if let Some(encrypted_pin_token) = resp.pin_token { let pin_token = shared_secret @@ -342,7 +356,7 @@ where rp_id.cloned(), ); - let resp = self.send_cbor_cancellable(&pin_command, alive)?; + let resp = self.send_cbor_cancellable(&pin_command, alive, None)?; if let Some(encrypted_pin_token) = resp.pin_token { let pin_token = shared_secret