pub mod attestation; pub mod client_data; #[allow(dead_code)] // TODO(MS): Remove me asap pub mod commands; pub mod preflight; pub mod server; pub(crate) mod utils; use crate::authenticatorservice::{RegisterArgs, SignArgs}; use crate::crypto::COSEAlgorithm; use crate::ctap2::client_data::ClientDataHash; use crate::ctap2::commands::authenticator_config::{ AuthConfigCommand, AuthConfigResult, AuthenticatorConfig, }; use crate::ctap2::commands::bio_enrollment::{ BioEnrollment, BioEnrollmentCommand, BioEnrollmentResult, FingerprintSensorInfo, }; use crate::ctap2::commands::client_pin::{ ChangeExistingPin, Pin, PinError, PinUvAuthTokenPermission, SetNewPin, }; use crate::ctap2::commands::credential_management::{ CredManagementCommand, CredentialList, CredentialListEntry, CredentialManagement, CredentialManagementResult, CredentialRpListEntry, }; use crate::ctap2::commands::get_assertion::{GetAssertion, GetAssertionOptions}; use crate::ctap2::commands::make_credentials::{ dummy_make_credentials_cmd, MakeCredentials, MakeCredentialsOptions, }; use crate::ctap2::commands::reset::Reset; use crate::ctap2::commands::{ repackage_pin_errors, CommandError, PinUvAuthCommand, PinUvAuthResult, RequestCtap2, StatusCode, }; use crate::ctap2::preflight::{ do_credential_list_filtering_ctap1, do_credential_list_filtering_ctap2, silently_discover_credentials, }; use crate::ctap2::server::{ CredentialProtectionPolicy, RelyingParty, ResidentKeyRequirement, UserVerificationRequirement, }; use crate::errors::{AuthenticatorError, UnsupportedOption}; use crate::statecallback::StateCallback; use crate::status_update::{send_status, BioEnrollmentCmd, CredManagementCmd, InteractiveUpdate}; use crate::transport::device_selector::{Device, DeviceSelectorEvent}; use crate::transport::{errors::HIDError, hid::HIDDevice, FidoDevice, FidoDeviceIO, FidoProtocol}; use crate::{ManageResult, ResetResult, StatusPinUv, StatusUpdate}; use std::sync::mpsc::{channel, RecvError, Sender}; use std::thread; use std::time::Duration; use self::commands::get_info::AuthenticatorVersion; macro_rules! unwrap_option { ($item: expr, $callback: expr) => { match $item { Some(r) => r, None => { $callback.call(Err(AuthenticatorError::Platform)); return false; } } }; } macro_rules! unwrap_result { ($item: expr, $callback: expr) => { match $item { Ok(r) => r, Err(e) => { $callback.call(Err(e.into())); return false; } } }; } macro_rules! handle_errors { ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr) => { let mut _dummy_skip_puap = false; let mut _dummy_cached_puat = false; handle_errors!( $error, $status, $callback, $pin_uv_auth_result, $skip_uv, _dummy_skip_puap, _dummy_cached_puat ) }; ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr, $skip_puap: expr) => { let mut _dummy_cached_puat = false; handle_errors!( $error, $status, $callback, $pin_uv_auth_result, $skip_uv, $skip_puap, _dummy_cached_puat ) }; ($error: expr, $status: expr, $callback: expr, $pin_uv_auth_result: expr, $skip_uv: expr, $skip_puap: expr, $cached_puat: expr) => { match $error { HIDError::Command(CommandError::StatusCode(StatusCode::ChannelBusy, _)) => { // Channel busy. Client SHOULD retry the request after a short delay. thread::sleep(Duration::from_millis(100)); continue; } HIDError::Command(CommandError::StatusCode(StatusCode::OperationDenied, _)) | HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) if matches!($pin_uv_auth_result, PinUvAuthResult::UsingInternalUv) => { // This should only happen for CTAP2.0 tokens that use internal UV and failed // (e.g. wrong fingerprint used), while doing GetAssertion or MakeCredentials. send_status( &$status, StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)), ); $skip_puap = false; continue; } HIDError::Command(CommandError::StatusCode(StatusCode::PinRequired, _)) if matches!($pin_uv_auth_result, PinUvAuthResult::UsingInternalUv) => { // This should only happen for CTAP2.0 tokens that use internal UV and failed // repeatedly, so that we have to fall back to PINs $skip_uv = true; $skip_puap = false; continue; } HIDError::Command(CommandError::StatusCode(StatusCode::UvBlocked, _)) if matches!( $pin_uv_auth_result, PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(..) ) => { // This should only happen for CTAP2.1 tokens that use internal UV and failed // repeatedly, so that we have to fall back to PINs $skip_uv = true; $skip_puap = false; continue; } HIDError::Command(CommandError::StatusCode(StatusCode::CredentialExcluded, _)) => { $callback.call(Err(AuthenticatorError::CredentialExcluded)); break; } HIDError::Command(CommandError::StatusCode(StatusCode::PinAuthInvalid, _)) if $cached_puat => { // We used the cached PUAT, but it was invalid. So we just try again // without the cached one and potentially trigger a new PIN/UV entry from // the user. This happens e.g. if the PUAT expires, or we get an all-zeros // PUAT from outside for whatever reason, etc. $cached_puat = false; $skip_puap = false; continue; } e => { warn!("error happened: {e}"); $callback.call(Err(AuthenticatorError::HIDError(e))); break; } } }; } fn ask_user_for_pin( was_invalid: bool, retries: Option, status: &Sender, ) -> Result { info!("PIN Error that requires user interaction detected. Sending it back and waiting for a reply"); let (tx, rx) = channel(); if was_invalid { send_status( status, crate::StatusUpdate::PinUvError(StatusPinUv::InvalidPin(tx, retries)), ); } else { send_status( status, crate::StatusUpdate::PinUvError(StatusPinUv::PinRequired(tx)), ); } match rx.recv() { Ok(pin) => Ok(pin), Err(RecvError) => { // recv() can only fail, if the other side is dropping the Sender. info!("Callback dropped the channel. Aborting."); Err(AuthenticatorError::CancelledByUser) } } } /// Try to fetch PinUvAuthToken from the device and derive from it PinUvAuthParam. /// Prefer UV, fallback to PIN. /// Prefer newer pinUvAuth-methods, if supported by the device. fn get_pin_uv_auth_param( cmd: &mut T, dev: &mut Dev, permission: PinUvAuthTokenPermission, skip_uv: bool, uv_req: UserVerificationRequirement, alive: &dyn Fn() -> bool, pin: &Option, ) -> Result { // CTAP 2.1 is very specific that the request should either include pinUvAuthParam // OR uv=true, but not both at the same time. We now have to decide which (if either) // to send. We may omit both values. Will never send an explicit uv=false, because // a) this is the default, and // b) some CTAP 2.0 authenticators return UnsupportedOption when uv=false. // We ensure both pinUvAuthParam and uv are not set to start. cmd.set_pin_uv_auth_param(None)?; cmd.set_uv_option(None); // Skip user verification if we're using CTAP1 or if the device does not support CTAP2. let info = match (dev.get_protocol(), dev.get_authenticator_info()) { (FidoProtocol::CTAP2, Some(info)) => info, _ => return Ok(PinUvAuthResult::DeviceIsCtap1), }; // Only use UV, if the device supports it and we don't skip it // which happens as a fallback, if UV-usage failed too many times // Note: In theory, we could also repeatedly query GetInfo here and check // if uv is set to Some(true), as tokens should set it to Some(false) // if UV is blocked (too many failed attempts). But the CTAP2.0-spec is // vague and I don't trust all tokens to implement it that way. So we // keep track of it ourselves, using `skip_uv`. let supports_uv = info.options.user_verification == Some(true); let supports_pin = info.options.client_pin.is_some(); let pin_configured = info.options.client_pin == Some(true); // Check if the combination of device-protection and request-options // are allowing for 'discouraged', meaning no auth required. if cmd.can_skip_user_verification(info, uv_req) { return Ok(PinUvAuthResult::NoAuthRequired); } // Device does not support any (remaining) auth-method if (skip_uv || !supports_uv) && !supports_pin { if supports_uv && uv_req == UserVerificationRequirement::Required { // We should always set the uv option in the Required case, but the CTAP 2.1 spec // says 'Platforms MUST NOT include the "uv" option key if the authenticator does // not support built-in user verification.' This is to work around some CTAP 2.0 // authenticators which incorrectly error out with CTAP2_ERR_UNSUPPORTED_OPTION // when the "uv" option is set. The RP that requested UV will (hopefully) reject our // response in the !supports_uv case. cmd.set_uv_option(Some(true)); } return Ok(PinUvAuthResult::NoAuthTypeSupported); } // Device supports PINs, but a PIN is not configured. Signal that we // can complete the operation if the user sets a PIN first. if (skip_uv || !supports_uv) && !pin_configured { return Err(AuthenticatorError::PinError(PinError::PinNotSet)); } if info.options.pin_uv_auth_token == Some(true) { if !skip_uv && supports_uv { // CTAP 2.1 - UV let pin_auth_token = dev .get_pin_uv_auth_token_using_uv_with_permissions(permission, cmd.get_rp_id(), alive) .map_err(|e| repackage_pin_errors(dev, e))?; cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; Ok(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(pin_auth_token)) } else { // CTAP 2.1 - PIN // We did not take the `!skip_uv && supports_uv` branch, so we have // `(skip_uv || !supports_uv)`. Moreover we did not exit early in the // `(skip_uv || !supports_uv) && !pin_configured` case. So we have // `pin_configured`. let pin_auth_token = dev .get_pin_uv_auth_token_using_pin_with_permissions( pin, permission, cmd.get_rp_id(), alive, ) .map_err(|e| repackage_pin_errors(dev, e))?; cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; Ok(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(pin_auth_token)) } } else { // CTAP 2.0 fallback if !skip_uv && supports_uv && pin.is_none() { // If the device supports internal user-verification (e.g. fingerprints), // skip PIN-stuff // We may need the shared secret for HMAC-extension, so we // have to establish one if info.supports_hmac_secret() { let _shared_secret = dev.establish_shared_secret(alive)?; } // CTAP 2.1, Section 6.1.1, Step 1.1.2.1.2. cmd.set_uv_option(Some(true)); return Ok(PinUvAuthResult::UsingInternalUv); } let pin_auth_token = dev .get_pin_token(pin, alive) .map_err(|e| repackage_pin_errors(dev, e))?; cmd.set_pin_uv_auth_param(Some(pin_auth_token.clone()))?; Ok(PinUvAuthResult::SuccessGetPinToken(pin_auth_token)) } } /// PUAP, as per spec: PinUvAuthParam /// Determines, if we need to establish a PinUvAuthParam, based on the /// capabilities of the device and the incoming request. /// If it is needed, tries to establish one and save it inside the Request. /// Returns Ok() if we can proceed with sending the actual Request to /// the device, Err() otherwise. /// Handles asking the user for a PIN, if needed and sending StatusUpdates /// regarding PIN and UV usage. #[allow(clippy::too_many_arguments)] fn determine_puap_if_needed( cmd: &mut T, dev: &mut Dev, mut skip_uv: bool, permission: PinUvAuthTokenPermission, uv_req: UserVerificationRequirement, status: &Sender, alive: &dyn Fn() -> bool, pin: &mut Option, ) -> Result { while alive() { debug!("-----------------------------------------------------------------"); debug!("Getting pinUvAuthParam"); match get_pin_uv_auth_param(cmd, dev, permission, skip_uv, uv_req, alive, pin) { Ok(r) => { return Ok(r); } Err(AuthenticatorError::PinError(PinError::PinRequired)) => { let new_pin = ask_user_for_pin(false, None, status)?; *pin = Some(new_pin); skip_uv = true; continue; } Err(AuthenticatorError::PinError(PinError::InvalidPin(retries))) => { let new_pin = ask_user_for_pin(true, retries, status)?; *pin = Some(new_pin); continue; } Err(AuthenticatorError::PinError(PinError::InvalidUv(retries))) => { if retries == Some(0) { skip_uv = true; } send_status( status, StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries)), ) } Err(e @ AuthenticatorError::PinError(PinError::PinAuthBlocked)) => { send_status( status, StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked), ); error!("Error when determining pinAuth: {:?}", e); return Err(e); } Err(e @ AuthenticatorError::PinError(PinError::PinBlocked)) => { send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinBlocked)); error!("Error when determining pinAuth: {:?}", e); return Err(e); } Err(e @ AuthenticatorError::PinError(PinError::PinNotSet)) => { send_status(status, StatusUpdate::PinUvError(StatusPinUv::PinNotSet)); error!("Error when determining pinAuth: {:?}", e); return Err(e); } Err(AuthenticatorError::PinError(PinError::UvBlocked)) => { skip_uv = true; send_status(status, StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) } // Used for CTAP2.0 UV (fingerprints) Err(AuthenticatorError::PinError(PinError::PinAuthInvalid)) => { skip_uv = true; send_status( status, StatusUpdate::PinUvError(StatusPinUv::InvalidUv(None)), ) } Err(e) => { error!("Error when determining pinAuth: {:?}", e); return Err(e); } } } Err(AuthenticatorError::CancelledByUser) } pub fn register( dev: &mut Dev, args: RegisterArgs, status: Sender, callback: StateCallback>, alive: &dyn Fn() -> bool, ) -> bool { let mut options = MakeCredentialsOptions::default(); if dev.get_protocol() == FidoProtocol::CTAP2 { let info = match dev.get_authenticator_info() { Some(info) => info, None => { callback.call(Err(HIDError::DeviceNotInitialized.into())); return false; } }; // Set options based on the arguments and the device info. // The user verification option will be set in `determine_puap_if_needed`. options.resident_key = match args.resident_key_req { ResidentKeyRequirement::Required => Some(true), ResidentKeyRequirement::Preferred => { // Use a resident key if the authenticator supports it Some(info.options.resident_key) } ResidentKeyRequirement::Discouraged => Some(false), } } else { // Check that the request can be processed by a CTAP1 device. // See CTAP 2.1 Section 10.2. Some additional checks are performed in // MakeCredentials::RequestCtap1 if args.resident_key_req == ResidentKeyRequirement::Required { callback.call(Err(AuthenticatorError::UnsupportedOption( UnsupportedOption::ResidentKey, ))); return false; } if args.user_verification_req == UserVerificationRequirement::Required { callback.call(Err(AuthenticatorError::UnsupportedOption( UnsupportedOption::UserVerification, ))); return false; } if !args .pub_cred_params .iter() .any(|x| x.alg == COSEAlgorithm::ES256) { callback.call(Err(AuthenticatorError::UnsupportedOption( UnsupportedOption::PubCredParams, ))); return false; } } // Client extension processing for credProtect: // "When enforceCredentialProtectionPolicy is true, and credentialProtectionPolicy's value is // [not "Optional"], the platform SHOULD NOT create the credential in a way that does not // implement the requested protection policy. (For example, by creating it on an authenticator // that does not support this extension.)" let dev_supports_cred_protect = dev .get_authenticator_info() .map_or(false, |info| info.supports_cred_protect()); if args.extensions.enforce_credential_protection_policy == Some(true) && args.extensions.credential_protection_policy != Some(CredentialProtectionPolicy::UserVerificationOptional) && !dev_supports_cred_protect { callback.call(Err(AuthenticatorError::UnsupportedOption( UnsupportedOption::CredProtect, ))); return false; } let mut makecred = MakeCredentials::new( ClientDataHash(args.client_data_hash), args.relying_party, Some(args.user), args.pub_cred_params, args.exclude_list, options, args.extensions.into(), ); let mut skip_uv = false; let mut pin = args.pin; while alive() { // Requesting both because pre-flighting (credential list filtering) // can potentially send GetAssertion-commands let permissions = PinUvAuthTokenPermission::MakeCredential | PinUvAuthTokenPermission::GetAssertion; let pin_uv_auth_result = unwrap_result!( determine_puap_if_needed( &mut makecred, dev, skip_uv, permissions, args.user_verification_req, &status, alive, &mut pin, ), callback ); // Do "pre-flight": Filter the exclude-list if dev.get_protocol() == FidoProtocol::CTAP2 { makecred.exclude_list = unwrap_result!( do_credential_list_filtering_ctap2( dev, &makecred.exclude_list, &makecred.rp, pin_uv_auth_result.get_pin_uv_auth_token(), ), callback ); } else { let key_handle = do_credential_list_filtering_ctap1( dev, &makecred.exclude_list, &makecred.rp, &makecred.client_data_hash, ); // That handle was already registered with the token if key_handle.is_some() { // Now we need to send a dummy registration request, to make the token blink // Spec says "dummy appid and invalid challenge". We use the same, as we do for // 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" callback.call(Err(AuthenticatorError::CredentialExcluded)); return false; } } debug!("------------------------------------------------------------------"); debug!("{makecred:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); send_status(&status, crate::StatusUpdate::PresenceRequired); let resp = dev.send_msg_cancellable(&makecred, alive); match resp { Ok(result) => { callback.call(Ok(result)); return true; } Err(e) => { handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv); } } } false } pub fn sign( dev: &mut Dev, args: SignArgs, status: Sender, callback: StateCallback>, alive: &dyn Fn() -> bool, ) -> bool { if dev.get_protocol() == FidoProtocol::CTAP1 { // Check that the request can be processed by a CTAP1 device. // See CTAP 2.1 Section 10.3. Some additional checks are performed in // GetAssertion::RequestCtap1 if args.user_verification_req == UserVerificationRequirement::Required { callback.call(Err(AuthenticatorError::UnsupportedOption( UnsupportedOption::UserVerification, ))); return false; } if args.allow_list.is_empty() { callback.call(Err(AuthenticatorError::UnsupportedOption( UnsupportedOption::EmptyAllowList, ))); return false; } } let mut allow_list = args.allow_list; let mut rp_id = RelyingParty::from(args.relying_party_id); let client_data_hash = ClientDataHash(args.client_data_hash); if let Some(ref app_id) = args.extensions.app_id { if !allow_list.is_empty() { // 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); if !silent_creds.is_empty() { allow_list = silent_creds; rp_id = alt_rp_id; } } } let mut get_assertion = GetAssertion::new( client_data_hash, rp_id, allow_list, GetAssertionOptions { user_presence: Some(args.user_presence_req), user_verification: None, }, args.extensions.into(), ); let mut skip_uv = false; let mut pin = args.pin; while alive() { let pin_uv_auth_result = unwrap_result!( determine_puap_if_needed( &mut get_assertion, dev, skip_uv, PinUvAuthTokenPermission::GetAssertion, args.user_verification_req, &status, alive, &mut pin, ), callback ); // Do "pre-flight": Filter the allow-list let original_allow_list_was_empty = get_assertion.allow_list.is_empty(); if dev.get_protocol() == FidoProtocol::CTAP2 { get_assertion.allow_list = unwrap_result!( do_credential_list_filtering_ctap2( dev, &get_assertion.allow_list, &get_assertion.rp, pin_uv_auth_result.get_pin_uv_auth_token(), ), callback ); } else { let key_handle = do_credential_list_filtering_ctap1( dev, &get_assertion.allow_list, &get_assertion.rp, &get_assertion.client_data_hash, ); match key_handle { Some(key_handle) => { get_assertion.allow_list = vec![key_handle]; } None => { get_assertion.allow_list.clear(); } } } // If the incoming list was not empty, but the filtered list is, we have to error out if !original_allow_list_was_empty && get_assertion.allow_list.is_empty() { // 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" callback.call(Err(HIDError::Command(CommandError::StatusCode( StatusCode::NoCredentials, None, )) .into())); return false; } // Use the shared secret in the extensions, if requested get_assertion = match get_assertion.process_hmac_secret_and_prf_extension( dev.get_shared_secret().map(|s| (s, &pin_uv_auth_result)), ) { Ok(value) => value, Err(e) => { callback.call(Err(e)); return false; } }; debug!("------------------------------------------------------------------"); 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) { Ok(results) => results, Err(e) => { handle_errors!(e, status, callback, pin_uv_auth_result, skip_uv); } }; if results.len() == 1 { callback.call(Ok(results.swap_remove(0))); return true; } let (tx, rx) = channel(); let user_entities = results .iter() .filter_map(|x| x.assertion.user.clone()) .collect(); send_status( &status, crate::StatusUpdate::SelectResultNotice(tx, user_entities), ); match rx.recv() { Ok(Some(index)) if index < results.len() => { callback.call(Ok(results.swap_remove(index))); return true; } _ => { callback.call(Err(AuthenticatorError::CancelledByUser)); return true; } } } false } pub(crate) fn reset_helper>( dev: &mut Device, selector: Sender, status: Sender, callback: StateCallback>, keep_alive: &dyn Fn() -> bool, ) { let reset = Reset {}; info!("Device {:?} continues with the reset process", dev.id()); debug!("------------------------------------------------------------------"); debug!("{:?}", reset); debug!("------------------------------------------------------------------"); send_status(&status, crate::StatusUpdate::PresenceRequired); let resp = dev.send_cbor_cancellable(&reset, keep_alive); 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 // we skipped the "blinking"-action and went straight for the actual // request. let _ = selector.send(DeviceSelectorEvent::SelectedToken(dev.id())); } match resp { Ok(()) => callback.call(Ok(T::from(()))), Err(HIDError::DeviceNotSupported) | Err(HIDError::UnsupportedCommand) => {} Err(HIDError::Command(CommandError::StatusCode(StatusCode::ChannelBusy, _))) => {} Err(e) => { warn!("error happened: {}", e); callback.call(Err(AuthenticatorError::HIDError(e))); } } } pub(crate) fn set_or_change_pin_helper>( dev: &mut Device, mut current_pin: Option, new_pin: Pin, status: Sender, callback: StateCallback>, alive: &dyn Fn() -> bool, ) { let mut shared_secret = match dev.establish_shared_secret(alive) { Ok(s) => s, Err(e) => { callback.call(Err(AuthenticatorError::HIDError(e))); return; } }; let authinfo = match dev.get_authenticator_info() { Some(i) => i.clone(), None => { callback.call(Err(HIDError::DeviceNotInitialized.into())); return; } }; // If the device has a min PIN use that, otherwise default to 4 according to Spec if new_pin.as_bytes().len() < authinfo.min_pin_length.unwrap_or(4) as usize { callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooShort))); return; } // As per Spec: "Maximum PIN Length: UTF-8 representation MUST NOT exceed 63 bytes" if new_pin.as_bytes().len() >= 64 { callback.call(Err(AuthenticatorError::PinError(PinError::PinIsTooLong( new_pin.as_bytes().len(), )))); return; } // Check if a client-pin is already set, or if a new one should be created let res = if Some(true) == authinfo.options.client_pin { let mut res; let mut was_invalid = false; let mut retries = None; loop { // current_pin will only be Some() in the interactive mode (running `manage()`) // In case that PIN is wrong, we want to avoid an endless-loop here with re-trying // that wrong PIN all the time. So we `take()` it, and only test it once. // If that PIN is wrong, we fall back to the "ask_user_for_pin"-method. let curr_pin = match current_pin.take() { None => match ask_user_for_pin(was_invalid, retries, &status) { Ok(pin) => pin, Err(e) => { callback.call(Err(e)); return; } }, Some(pin) => pin, }; res = ChangeExistingPin::new(&authinfo, &shared_secret, &curr_pin, &new_pin) .map_err(HIDError::Command) .and_then(|msg| dev.send_cbor_cancellable(&msg, alive)) .map_err(|e| repackage_pin_errors(dev, e)); if let Err(AuthenticatorError::PinError(PinError::InvalidPin(r))) = res { was_invalid = true; retries = r; // We need to re-establish the shared secret for the next round. match dev.establish_shared_secret(alive) { Ok(s) => { shared_secret = s; } Err(e) => { callback.call(Err(AuthenticatorError::HIDError(e))); return; } }; continue; } else { break; } } res } else { dev.send_cbor_cancellable(&SetNewPin::new(&shared_secret, &new_pin), alive) .map_err(AuthenticatorError::HIDError) }; // the callback is expecting `Result<(), AuthenticatorError>`, but `ChangeExistingPin` // and `SetNewPin` return the default `ClientPinResponse` on success. Just discard it. callback.call(res.map(|_| T::from(()))); } pub(crate) fn bio_enrollment( dev: &mut Device, puat_result: Option, command: BioEnrollmentCmd, status: Sender, callback: StateCallback>, alive: &dyn Fn() -> bool, ) -> bool { let authinfo = match dev.get_authenticator_info() { Some(i) => i, None => { callback.call(Err(HIDError::DeviceNotInitialized.into())); return false; } }; if authinfo.options.bio_enroll.is_none() && authinfo.options.user_verification_mgmt_preview.is_none() { callback.call(Err(AuthenticatorError::HIDError( HIDError::UnsupportedCommand, ))); return false; } let use_legacy_preview = authinfo.options.bio_enroll.is_none(); // We are not allowed to request the BE-permission using UV, so we have to skip UV let mut skip_uv = authinfo.options.uv_bio_enroll != Some(true); // Currently not used, but if we want, we can just set the value here. let timeout = None; let mut bio_cmd = match &command { BioEnrollmentCmd::StartNewEnrollment(_name) => BioEnrollment::new( BioEnrollmentCommand::EnrollBegin(timeout), use_legacy_preview, ), BioEnrollmentCmd::DeleteEnrollment(id) => BioEnrollment::new( BioEnrollmentCommand::RemoveEnrollment(id.clone()), use_legacy_preview, ), BioEnrollmentCmd::ChangeName(id, name) => BioEnrollment::new( BioEnrollmentCommand::SetFriendlyName((id.clone(), name.clone())), use_legacy_preview, ), BioEnrollmentCmd::GetEnrollments => BioEnrollment::new( BioEnrollmentCommand::EnumerateEnrollments, use_legacy_preview, ), BioEnrollmentCmd::GetFingerprintSensorInfo => BioEnrollment::new( BioEnrollmentCommand::GetFingerprintSensorInfo, use_legacy_preview, ), }; let mut skip_puap = false; let mut cached_puat = false; // If we were provided with a cached puat from the outside let mut pin_uv_auth_result = puat_result .clone() .unwrap_or(PinUvAuthResult::NoAuthRequired); // See, if we have a cached PUAT with matching permissions. match puat_result { Some(PinUvAuthResult::SuccessGetPinToken(t)) | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) if !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) // Only 2.1 has a permission-system || use_legacy_preview // Preview doesn't use permissions || t.permissions .contains(PinUvAuthTokenPermission::BioEnrollment) => { skip_puap = true; cached_puat = true; unwrap_result!(bio_cmd.set_pin_uv_auth_param(Some(t)), callback); } _ => {} } let mut pin = None; while alive() { if !skip_puap { pin_uv_auth_result = unwrap_result!( determine_puap_if_needed( &mut bio_cmd, dev, skip_uv, PinUvAuthTokenPermission::BioEnrollment, UserVerificationRequirement::Preferred, &status, alive, &mut pin, ), callback ); } debug!("------------------------------------------------------------------"); debug!("{bio_cmd:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); let resp = dev.send_cbor_cancellable(&bio_cmd, alive); match resp { Ok(result) => { skip_puap = true; match bio_cmd.subcommand { BioEnrollmentCommand::EnrollBegin(..) | BioEnrollmentCommand::EnrollCaptureNextSample(..) => { let template_id = if let BioEnrollmentCommand::EnrollCaptureNextSample((id, ..)) = bio_cmd.subcommand { id } else { unwrap_option!(result.template_id, callback) }; let last_enroll_sample_status = unwrap_option!(result.last_enroll_sample_status, callback); let remaining_samples = unwrap_option!(result.remaining_samples, callback); send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::BioEnrollmentUpdate(( BioEnrollmentResult::SampleStatus( last_enroll_sample_status, remaining_samples, ), Some(pin_uv_auth_result.clone()), )), ), ); if remaining_samples == 0 { if let BioEnrollmentCmd::StartNewEnrollment(Some(ref name)) = command { bio_cmd.subcommand = BioEnrollmentCommand::SetFriendlyName(( template_id.to_vec(), name.clone(), )); // We have to regenerate PUAP here. PUAT hasn't changed, but the content // of the command has changed, and that is part of the PUAP-calculation unwrap_result!( bio_cmd.set_pin_uv_auth_param( pin_uv_auth_result.get_pin_uv_auth_token() ), callback ); continue; } else { let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::BioEnrollmentUpdate(( BioEnrollmentResult::AddSuccess(auth_info.clone()), Some(pin_uv_auth_result), )), ), ); return true; } } else { bio_cmd.subcommand = BioEnrollmentCommand::EnrollCaptureNextSample(( template_id, timeout, )); // We have to regenerate PUAP here. PUAT hasn't changed, but the content // of the command has changed, and that is part of the PUAP-calculation unwrap_result!( bio_cmd.set_pin_uv_auth_param( pin_uv_auth_result.get_pin_uv_auth_token() ), callback ); continue; } } BioEnrollmentCommand::EnumerateEnrollments => { let list = result.template_infos.iter().map(|x| x.into()).collect(); send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::BioEnrollmentUpdate(( BioEnrollmentResult::EnrollmentList(list), Some(pin_uv_auth_result), )), ), ); return true; } BioEnrollmentCommand::SetFriendlyName(_) => { let res = match command { BioEnrollmentCmd::StartNewEnrollment(..) => { let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); BioEnrollmentResult::AddSuccess(auth_info.clone()) } _ => BioEnrollmentResult::UpdateSuccess, }; send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::BioEnrollmentUpdate(( res, Some(pin_uv_auth_result), )), ), ); return true; } BioEnrollmentCommand::RemoveEnrollment(_) => { let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::BioEnrollmentUpdate(( BioEnrollmentResult::DeleteSuccess(auth_info.clone()), Some(pin_uv_auth_result), )), ), ); return true; } BioEnrollmentCommand::CancelCurrentEnrollment => { callback.call(Ok(ManageResult::Success)); return true; } BioEnrollmentCommand::GetFingerprintSensorInfo => { let fingerprint_kind = unwrap_option!(result.fingerprint_kind, callback); let max_capture_samples_required_for_enroll = unwrap_option!( result.max_capture_samples_required_for_enroll, callback ); // FIDO_2_1_PRE-devices do not report this field. So we leave it optional. let max_template_friendly_name = result.max_template_friendly_name; send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::BioEnrollmentUpdate(( BioEnrollmentResult::FingerprintSensorInfo( FingerprintSensorInfo { fingerprint_kind, max_capture_samples_required_for_enroll, max_template_friendly_name, }, ), Some(pin_uv_auth_result), )), ), ); return true; } }; } Err(e) => { handle_errors!( e, status, callback, pin_uv_auth_result, skip_uv, skip_puap, cached_puat ); } } } false } pub(crate) fn credential_management( dev: &mut Device, puat_result: Option, command: CredManagementCmd, status: Sender, callback: StateCallback>, alive: &dyn Fn() -> bool, ) -> bool { let mut skip_uv = false; let authinfo = match dev.get_authenticator_info() { Some(i) => i.clone(), None => { callback.call(Err(HIDError::DeviceNotInitialized.into())); return false; } }; if authinfo.options.cred_mgmt != Some(true) && authinfo.options.credential_mgmt_preview != Some(true) { callback.call(Err(AuthenticatorError::HIDError( HIDError::UnsupportedCommand, ))); return false; } let use_legacy_preview = authinfo.options.cred_mgmt != Some(true); // FIDO_2_1_PRE-devices do not support UpdateUserInformation. if use_legacy_preview && !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) && matches!(command, CredManagementCmd::UpdateUserInformation(..)) { callback.call(Err(AuthenticatorError::HIDError( HIDError::UnsupportedCommand, ))); return false; } // If puap is provided, we can skip puap-determination (i.e. PIN entry) let mut cred_management = match command { CredManagementCmd::GetCredentials => { CredentialManagement::new(CredManagementCommand::GetCredsMetadata, use_legacy_preview) } CredManagementCmd::DeleteCredential(cred_id) => CredentialManagement::new( CredManagementCommand::DeleteCredential(cred_id), use_legacy_preview, ), CredManagementCmd::UpdateUserInformation(cred_id, user) => CredentialManagement::new( CredManagementCommand::UpdateUserInformation((cred_id, user)), use_legacy_preview, ), }; let mut credential_result = CredentialList::new(); let mut remaining_rps = 0; let mut remaining_cred_ids = 0; let mut current_rp = 0; let mut skip_puap = false; let mut cached_puat = false; // If we were provided with a cached puat from the outside let mut pin_uv_auth_result = puat_result .clone() .unwrap_or(PinUvAuthResult::NoAuthRequired); // See, if we have a cached PUAT with matching permissions. match puat_result { Some(PinUvAuthResult::SuccessGetPinToken(t)) | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) if !authinfo.versions.contains(&AuthenticatorVersion::FIDO_2_1) // Only 2.1 has a permission-system || use_legacy_preview // Preview doesn't use permissions || t.permissions .contains(PinUvAuthTokenPermission::CredentialManagement) => { skip_puap = true; cached_puat = true; unwrap_result!(cred_management.set_pin_uv_auth_param(Some(t)), callback); } _ => {} } let mut pin = None; while alive() { if !skip_puap { pin_uv_auth_result = unwrap_result!( determine_puap_if_needed( &mut cred_management, dev, skip_uv, PinUvAuthTokenPermission::CredentialManagement, UserVerificationRequirement::Preferred, &status, alive, &mut pin, ), callback ); } debug!("------------------------------------------------------------------"); debug!("{cred_management:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); let resp = dev.send_cbor_cancellable(&cred_management, alive); match resp { Ok(result) => { skip_puap = true; match cred_management.subcommand { CredManagementCommand::GetCredsMetadata => { let existing_resident_credentials_count = unwrap_option!(result.existing_resident_credentials_count, callback); let max_possible_remaining_resident_credentials_count = unwrap_option!( result.max_possible_remaining_resident_credentials_count, callback ); credential_result.existing_resident_credentials_count = existing_resident_credentials_count; credential_result.max_possible_remaining_resident_credentials_count = max_possible_remaining_resident_credentials_count; if existing_resident_credentials_count > 0 { cred_management.subcommand = CredManagementCommand::EnumerateRPsBegin; // We have to regenerate PUAP here. PUAT hasn't changed, but the content // of the command has changed, and that is part of the PUAP-calculation unwrap_result!( cred_management.set_pin_uv_auth_param( pin_uv_auth_result.get_pin_uv_auth_token() ), callback ); continue; } else { // This token doesn't have any resident keys, but its not an error, // so we send an empty list. send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::CredentialManagementUpdate(( CredentialManagementResult::CredentialList( credential_result, ), Some(pin_uv_auth_result), )), ), ); return true; } } CredManagementCommand::EnumerateRPsBegin | CredManagementCommand::EnumerateRPsGetNextRP => { if matches!( cred_management.subcommand, CredManagementCommand::EnumerateRPsBegin ) { let total_rps = unwrap_option!(result.total_rps, callback); if total_rps == 0 { // This token doesn't have any RPs, but its not an error, // so we return an Ok with an empty list. send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::CredentialManagementUpdate(( CredentialManagementResult::CredentialList( credential_result, ), Some(pin_uv_auth_result), )), ), ); return true; } remaining_rps = total_rps - 1; } else { remaining_rps -= 1; } let rp = unwrap_option!(result.rp, callback); let rp_id_hash = unwrap_option!(result.rp_id_hash, callback); let rp_res = CredentialRpListEntry { rp, rp_id_hash, credentials: vec![], }; credential_result.credential_list.push(rp_res); if remaining_rps > 0 { cred_management.subcommand = CredManagementCommand::EnumerateRPsGetNextRP; } else { // We have queried all RPs, now start querying the corresponding credentials for each RP cred_management.subcommand = CredManagementCommand::EnumerateCredentialsBegin( credential_result.credential_list[0].rp_id_hash.clone(), ); } // We have to regenerate PUAP here. PUAT hasn't changed, but the content // of the command has changed, and that is part of the PUAP-calculation unwrap_result!( cred_management .set_pin_uv_auth_param(pin_uv_auth_result.get_pin_uv_auth_token()), callback ); continue; } CredManagementCommand::EnumerateCredentialsBegin(..) | CredManagementCommand::EnumerateCredentialsGetNextCredential => { let user = unwrap_option!(result.user, callback); let credential_id = unwrap_option!(result.credential_id, callback); let public_key = unwrap_option!(result.public_key, callback); let cred_protect = unwrap_option!(result.cred_protect, callback); let large_blob_key = result.large_blob_key; if matches!( cred_management.subcommand, CredManagementCommand::EnumerateCredentialsBegin(..) ) { remaining_cred_ids = unwrap_option!(result.total_credentials, callback) - 1; } else { remaining_cred_ids -= 1; } // We might have to change the global variable, but need the unmodified below let current_rp_backup = current_rp; let mut we_are_done = false; if remaining_cred_ids > 0 { cred_management.subcommand = CredManagementCommand::EnumerateCredentialsGetNextCredential; } else { current_rp += 1; // We have all credentials from this RP. Starting with the next RP. if current_rp < credential_result.credential_list.len() { cred_management.subcommand = CredManagementCommand::EnumerateCredentialsBegin( credential_result.credential_list[current_rp] .rp_id_hash .clone(), ); // We have to regenerate PUAP here. PUAT hasn't changed, but the content // of the command has changed, and that is part of the PUAP-calculation unwrap_result!( cred_management.set_pin_uv_auth_param( pin_uv_auth_result.get_pin_uv_auth_token() ), callback ); } else { // Finally done iterating over all RPs and their Credentials we_are_done = true; } } let key = CredentialListEntry { user, credential_id, public_key, cred_protect, large_blob_key, }; credential_result.credential_list[current_rp_backup] .credentials .push(key); if we_are_done { send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::CredentialManagementUpdate(( CredentialManagementResult::CredentialList( credential_result, ), Some(pin_uv_auth_result), )), ), ); return true; } else { continue; } } CredManagementCommand::DeleteCredential(_) => { send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::CredentialManagementUpdate(( CredentialManagementResult::DeleteSucess, Some(pin_uv_auth_result), )), ), ); return true; } CredManagementCommand::UpdateUserInformation(_) => { send_status( &status, StatusUpdate::InteractiveManagement( InteractiveUpdate::CredentialManagementUpdate(( CredentialManagementResult::UpdateSuccess, Some(pin_uv_auth_result), )), ), ); return true; } }; } Err(e) => { handle_errors!( e, status, callback, pin_uv_auth_result, skip_uv, skip_puap, cached_puat ); } } } false } pub(crate) fn configure_authenticator( dev: &mut Device, puat_result: Option, cfg_subcommand: AuthConfigCommand, status: Sender, callback: StateCallback>, alive: &dyn Fn() -> bool, ) -> bool { let mut authcfg = AuthenticatorConfig::new(cfg_subcommand); let mut skip_uv = false; let authinfo = match dev.get_authenticator_info() { Some(i) => i.clone(), None => { callback.call(Err(HIDError::DeviceNotInitialized.into())); return false; } }; if authinfo.options.authnr_cfg != Some(true) { callback.call(Err(AuthenticatorError::HIDError( HIDError::UnsupportedCommand, ))); return false; } let mut skip_puap = false; let mut cached_puat = false; // If we were provided with a cached puat from the outside let mut pin_uv_auth_result = puat_result .clone() .unwrap_or(PinUvAuthResult::NoAuthRequired); match puat_result { Some(PinUvAuthResult::SuccessGetPinToken(t)) | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingUvWithPermissions(t)) | Some(PinUvAuthResult::SuccessGetPinUvAuthTokenUsingPinWithPermissions(t)) if t.permissions .contains(PinUvAuthTokenPermission::AuthenticatorConfiguration) => { skip_puap = true; cached_puat = true; unwrap_result!(authcfg.set_pin_uv_auth_param(Some(t)), callback); } _ => {} } let mut pin = None; while alive() { // We can use the AuthenticatorConfiguration-command only in two cases: // 1. The device also supports the uv_acfg-permission (otherwise we can't establish a PUAP) // 2. The device is NOT protected by PIN/UV (yet). This allows organizations to configure // the token, before handing them out. // If authinfo.options.uv_acfg is not supported, this will return UnauthorizedPermission if !skip_puap { pin_uv_auth_result = unwrap_result!( determine_puap_if_needed( &mut authcfg, dev, skip_uv, PinUvAuthTokenPermission::AuthenticatorConfiguration, UserVerificationRequirement::Preferred, &status, alive, &mut pin, ), callback ); } debug!("------------------------------------------------------------------"); debug!("{authcfg:?} using {pin_uv_auth_result:?}"); debug!("------------------------------------------------------------------"); let resp = dev.send_cbor_cancellable(&authcfg, alive); match resp { Ok(()) => { let auth_info = unwrap_option!(dev.refresh_authenticator_info(), callback); send_status( &status, StatusUpdate::InteractiveManagement(InteractiveUpdate::AuthConfigUpdate(( AuthConfigResult::Success(auth_info.clone()), Some(pin_uv_auth_result), ))), ); return true; } Err(e) => { handle_errors!( e, status, callback, pin_uv_auth_result, skip_uv, skip_puap, cached_puat ); } } } false }