diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/authenticator/src/ctap2/preflight.rs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/authenticator/src/ctap2/preflight.rs')
-rw-r--r-- | third_party/rust/authenticator/src/ctap2/preflight.rs | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/third_party/rust/authenticator/src/ctap2/preflight.rs b/third_party/rust/authenticator/src/ctap2/preflight.rs new file mode 100644 index 0000000000..3adf44176e --- /dev/null +++ b/third_party/rust/authenticator/src/ctap2/preflight.rs @@ -0,0 +1,196 @@ +use super::client_data::ClientDataHash; +use super::commands::get_assertion::{GetAssertion, GetAssertionOptions}; +use super::commands::{CommandError, PinUvAuthCommand, RequestCtap1, Retryable, StatusCode}; +use crate::authenticatorservice::GetAssertionExtensions; +use crate::consts::{PARAMETER_SIZE, U2F_AUTHENTICATE, U2F_CHECK_IS_REGISTERED}; +use crate::crypto::PinUvAuthToken; +use crate::ctap2::server::{PublicKeyCredentialDescriptor, RelyingPartyWrapper}; +use crate::errors::AuthenticatorError; +use crate::transport::errors::{ApduErrorStatus, HIDError}; +use crate::transport::FidoDevice; +use crate::u2ftypes::CTAP1RequestAPDU; +use sha2::{Digest, Sha256}; + +/// This command is used to check which key_handle is valid for this +/// token. This is sent before a GetAssertion command, to determine which +/// is valid for a specific token and which key_handle GetAssertion +/// should send to the token. Or before a MakeCredential command, to determine +/// if this token is already registered or not. +#[derive(Debug)] +pub(crate) struct CheckKeyHandle<'assertion> { + pub(crate) key_handle: &'assertion [u8], + pub(crate) client_data_hash: &'assertion [u8], + pub(crate) rp: &'assertion RelyingPartyWrapper, +} + +impl<'assertion> RequestCtap1 for CheckKeyHandle<'assertion> { + type Output = (); + type AdditionalInfo = (); + + fn ctap1_format(&self) -> Result<(Vec<u8>, Self::AdditionalInfo), HIDError> { + // In theory, we only need to do this for up=true, for up=false, we could + // use U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN instead and use the answer directly. + // But that would involve another major refactoring to implement, and so we accept + // that we will send the final request twice to the authenticator. Once with + // U2F_CHECK_IS_REGISTERED followed by U2F_DONT_ENFORCE_USER_PRESENCE_AND_SIGN. + let flags = U2F_CHECK_IS_REGISTERED; + let mut auth_data = Vec::with_capacity(2 * PARAMETER_SIZE + 1 + self.key_handle.len()); + + auth_data.extend_from_slice(self.client_data_hash); + auth_data.extend_from_slice(self.rp.hash().as_ref()); + auth_data.extend_from_slice(&[self.key_handle.len() as u8]); + auth_data.extend_from_slice(self.key_handle); + let cmd = U2F_AUTHENTICATE; + let apdu = CTAP1RequestAPDU::serialize(cmd, flags, &auth_data)?; + Ok((apdu, ())) + } + + fn handle_response_ctap1( + &self, + status: Result<(), ApduErrorStatus>, + _input: &[u8], + _add_info: &Self::AdditionalInfo, + ) -> Result<Self::Output, Retryable<HIDError>> { + // 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 + // simply check whether the provided key handle was originally created by this token, + // and whether it was created for the provided application parameter. If so, the U2F + // token MUST respond with an authentication response + // message:error:test-of-user-presence-required (note that despite the name this + // signals a success condition). If the key handle was not created by this U2F + // token, or if it was created for a different application parameter, the token MUST + // respond with an authentication response message:error:bad-key-handle. + match status { + Ok(_) | Err(ApduErrorStatus::ConditionsNotSatisfied) => Ok(()), + Err(e) => Err(Retryable::Error(HIDError::ApduStatus(e))), + } + } +} + +/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or +/// authenticatorGetAssertion's allowList contain credential IDs that are already +/// present on an authenticator, a platform typically invokes authenticatorGetAssertion +/// with the "up" option key set to false and optionally pinUvAuthParam one or more times. +/// For CTAP1, the resulting list will always be of length 1. +pub(crate) fn do_credential_list_filtering_ctap1<Dev: FidoDevice>( + dev: &mut Dev, + cred_list: &[PublicKeyCredentialDescriptor], + rp: &RelyingPartyWrapper, + client_data_hash: &ClientDataHash, +) -> Option<PublicKeyCredentialDescriptor> { + let key_handle = cred_list + .iter() + // key-handles in CTAP1 are limited to 255 bytes, but are not limited in CTAP2. + // Filter out key-handles that are too long (can happen if this is a CTAP2-request, + // but the token only speaks CTAP1). + .filter(|key_handle| key_handle.id.len() < 256) + .find_map(|key_handle| { + let check_command = CheckKeyHandle { + key_handle: key_handle.id.as_ref(), + client_data_hash: client_data_hash.as_ref(), + rp, + }; + let res = dev.send_ctap1(&check_command); + match res { + Ok(_) => Some(key_handle.clone()), + _ => None, + } + }); + key_handle +} + +/// "pre-flight": In order to determine whether authenticatorMakeCredential's excludeList or +/// authenticatorGetAssertion's allowList contain credential IDs that are already +/// present on an authenticator, a platform typically invokes authenticatorGetAssertion +/// with the "up" option key set to false and optionally pinUvAuthParam one or more times. +pub(crate) fn do_credential_list_filtering_ctap2<Dev: FidoDevice>( + dev: &mut Dev, + cred_list: &[PublicKeyCredentialDescriptor], + rp: &RelyingPartyWrapper, + pin_uv_auth_token: Option<PinUvAuthToken>, +) -> Result<Vec<PublicKeyCredentialDescriptor>, AuthenticatorError> { + let info = dev + .get_authenticator_info() + .ok_or(HIDError::DeviceNotInitialized)?; + let mut cred_list = cred_list.to_vec(); + // Step 1.0: Find out how long the exclude_list/allow_list is allowed to be + // If the token doesn't tell us, we assume a length of 1 + let mut chunk_size = match info.max_credential_count_in_list { + // Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means + // falling back to a chunk size of 1 as the bare minimum. + None | Some(0) => 1, + Some(x) => x, + }; + + // Step 1.1: The device only supports keys up to a certain length. + // Filter out all keys that are longer, because they can't be + // from this device anyways. + match info.max_credential_id_length { + None => { /* no-op */ } + // Length 0 is not allowed by the spec, so we assume the device can't be trusted, which means + // falling back to a chunk size of 1 as the bare minimum. + Some(0) => { + chunk_size = 1; + } + Some(max_key_length) => { + cred_list.retain(|k| k.id.len() <= max_key_length); + } + } + + // Step 1.2: Return early, if we only have one chunk anyways + if cred_list.len() <= chunk_size { + return Ok(cred_list); + } + + let chunked_list = cred_list.chunks(chunk_size); + + // Step 2: If we have more than one chunk: Loop over all, doing GetAssertion + // and if one of them comes back with a success, use only that chunk. + let mut final_list = Vec::new(); + for chunk in chunked_list { + let mut silent_assert = GetAssertion::new( + ClientDataHash(Sha256::digest("").into()), + rp.clone(), + chunk.to_vec(), + GetAssertionOptions { + user_verification: if pin_uv_auth_token.is_some() { + None + } else { + Some(false) + }, + user_presence: Some(false), + }, + GetAssertionExtensions::default(), + None, + None, + ); + silent_assert.set_pin_uv_auth_param(pin_uv_auth_token.clone())?; + let res = dev.send_msg(&silent_assert); + match res { + Ok(response) => { + // This chunk contains a key_handle that is already known to the device. + // Filter out all credentials the device returned. Those are valid. + let credential_ids = response + .0 + .iter() + .filter_map(|a| a.credentials.clone()) + .collect(); + // Replace credential_id_list with the valid credentials + final_list = credential_ids; + break; + } + Err(HIDError::Command(CommandError::StatusCode(StatusCode::NoCredentials, _))) => { + // No-op: Go to next chunk. + } + Err(e) => { + // Some unexpected error + return Err(e.into()); + } + } + } + + // Step 3: Now ExcludeList/AllowList is either empty or has one batch with a 'known' credential. + // Send it as a normal Request and expect a "CredentialExcluded"-error in case of + // MakeCredential or a Success in case of GetAssertion + Ok(final_list) +} |