diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/webauthn/authrs_bridge/src | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webauthn/authrs_bridge/src')
-rw-r--r-- | dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs | 166 | ||||
-rw-r--r-- | dom/webauthn/authrs_bridge/src/lib.rs | 1534 | ||||
-rw-r--r-- | dom/webauthn/authrs_bridge/src/test_token.rs | 974 |
3 files changed, 2674 insertions, 0 deletions
diff --git a/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs b/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs new file mode 100644 index 0000000000..8d77a62df4 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs @@ -0,0 +1,166 @@ +/* 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 super::*; +use authenticator::{ + ctap2::commands::{PinUvAuthResult, StatusCode}, + errors::{CommandError, HIDError}, + BioEnrollmentCmd, CredManagementCmd, InteractiveRequest, InteractiveUpdate, PinError, +}; +use serde::{Deserialize, Serialize}; + +pub(crate) type InteractiveManagementReceiver = Option<Sender<InteractiveRequest>>; +pub(crate) fn send_about_prompt(prompt: &BrowserPromptType) -> Result<(), nsresult> { + let json = nsString::from(&serde_json::to_string(&prompt).unwrap_or_default()); + notify_observers(PromptTarget::AboutPage, json) +} + +// A wrapper around InteractiveRequest, that leaves out the PUAT +// so that we can easily de/serialize it to/from JSON for the JS-side +// and then add our cached PUAT, if we have one. +#[derive(Debug, Serialize, Deserialize)] +pub enum RequestWrapper { + Quit, + ChangePIN(Pin, Pin), + SetPIN(Pin), + CredentialManagement(CredManagementCmd), + BioEnrollment(BioEnrollmentCmd), +} + +pub(crate) fn authrs_to_prompt<'a>(e: AuthenticatorError) -> BrowserPromptType<'a> { + match e { + AuthenticatorError::PinError(PinError::PinIsTooShort) => BrowserPromptType::PinIsTooShort, + AuthenticatorError::PinError(PinError::PinNotSet) => BrowserPromptType::PinNotSet, + AuthenticatorError::PinError(PinError::PinRequired) => BrowserPromptType::PinRequired, + AuthenticatorError::PinError(PinError::PinIsTooLong(_)) => BrowserPromptType::PinIsTooLong, + AuthenticatorError::PinError(PinError::InvalidPin(r)) => { + BrowserPromptType::PinInvalid { retries: r } + } + AuthenticatorError::PinError(PinError::PinAuthBlocked) => BrowserPromptType::PinAuthBlocked, + AuthenticatorError::PinError(PinError::PinBlocked) => BrowserPromptType::DeviceBlocked, + AuthenticatorError::PinError(PinError::UvBlocked) => BrowserPromptType::UvBlocked, + AuthenticatorError::PinError(PinError::InvalidUv(r)) => { + BrowserPromptType::UvInvalid { retries: r } + } + AuthenticatorError::CancelledByUser + | AuthenticatorError::HIDError(HIDError::Command(CommandError::StatusCode( + StatusCode::KeepaliveCancel, + _, + ))) => BrowserPromptType::Cancel, + _ => BrowserPromptType::UnknownError, + } +} + +pub(crate) fn cache_puat( + transaction: Arc<Mutex<Option<TransactionState>>>, + puat: Option<PinUvAuthResult>, +) { + let mut guard = transaction.lock().unwrap(); + if let Some(transaction) = guard.as_mut() { + transaction.puat_cache = puat; + }; +} + +pub(crate) fn interactive_status_callback( + status_rx: Receiver<StatusUpdate>, + transaction: Arc<Mutex<Option<TransactionState>>>, /* Shared with an AuthrsTransport */ + upcoming_error: Arc<Mutex<Option<AuthenticatorError>>>, +) -> Result<(), nsresult> { + loop { + match status_rx.recv() { + Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::StartManagement(( + tx, + auth_info, + )))) => { + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + transaction.interactive_receiver.replace(tx); + let prompt = BrowserPromptType::SelectedDevice { auth_info }; + send_about_prompt(&prompt)?; + } + Ok(StatusUpdate::InteractiveManagement( + InteractiveUpdate::CredentialManagementUpdate((cfg_result, puat_res)), + )) => { + cache_puat(transaction.clone(), puat_res); // We don't care if we fail here. Worst-case: User has to enter PIN more often. + let prompt = BrowserPromptType::CredentialManagementUpdate { result: cfg_result }; + send_about_prompt(&prompt)?; + continue; + } + Ok(StatusUpdate::InteractiveManagement(InteractiveUpdate::BioEnrollmentUpdate(( + bio_res, + puat_res, + )))) => { + cache_puat(transaction.clone(), puat_res); // We don't care if we fail here. Worst-case: User has to enter PIN more often. + let prompt = BrowserPromptType::BioEnrollmentUpdate { result: bio_res }; + send_about_prompt(&prompt)?; + continue; + } + Ok(StatusUpdate::SelectDeviceNotice) => { + info!("STATUS: Please select a device by touching one of them."); + let prompt = BrowserPromptType::SelectDevice; + send_about_prompt(&prompt)?; + } + Ok(StatusUpdate::PinUvError(e)) => { + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + let autherr = match e { + StatusPinUv::PinRequired(pin_sender) => { + transaction.pin_receiver.replace((0, pin_sender)); + send_about_prompt(&BrowserPromptType::PinRequired)?; + continue; + } + StatusPinUv::InvalidPin(pin_sender, r) => { + transaction.pin_receiver.replace((0, pin_sender)); + send_about_prompt(&BrowserPromptType::PinInvalid { retries: r })?; + continue; + } + StatusPinUv::PinIsTooShort => { + AuthenticatorError::PinError(PinError::PinIsTooShort) + } + StatusPinUv::PinIsTooLong(s) => { + AuthenticatorError::PinError(PinError::PinIsTooLong(s)) + } + StatusPinUv::InvalidUv(r) => { + send_about_prompt(&BrowserPromptType::UvInvalid { retries: r })?; + continue; + } + StatusPinUv::PinAuthBlocked => { + AuthenticatorError::PinError(PinError::PinAuthBlocked) + } + StatusPinUv::PinBlocked => AuthenticatorError::PinError(PinError::PinBlocked), + StatusPinUv::PinNotSet => AuthenticatorError::PinError(PinError::PinNotSet), + StatusPinUv::UvBlocked => AuthenticatorError::PinError(PinError::UvBlocked), + }; + // We will cause auth-rs to return an error, once we leave this block + // due to us 'hanging up'. Before we do that, we will safe the actual + // error that caused this, so our callback-function can return the true + // error to JS, instead of "cancelled by user". + let guard = upcoming_error.lock(); + if let Ok(mut entry) = guard { + entry.replace(autherr); + } else { + return Err(NS_ERROR_DOM_INVALID_STATE_ERR); + } + warn!("STATUS: Pin Error {:?}", e); + break; + } + + Ok(_) => { + // currently not handled + continue; + } + Err(RecvError) => { + info!("STATUS: end"); + break; + } + } + } + Ok(()) +} diff --git a/dom/webauthn/authrs_bridge/src/lib.rs b/dom/webauthn/authrs_bridge/src/lib.rs new file mode 100644 index 0000000000..353e1e89a4 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/lib.rs @@ -0,0 +1,1534 @@ +/* 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/. */ + +#[macro_use] +extern crate log; + +#[macro_use] +extern crate xpcom; + +use authenticator::{ + authenticatorservice::{RegisterArgs, SignArgs}, + ctap2::attestation::AttestationObject, + ctap2::commands::{get_info::AuthenticatorVersion, PinUvAuthResult}, + ctap2::server::{ + AuthenticationExtensionsClientInputs, AuthenticatorAttachment, + PublicKeyCredentialDescriptor, PublicKeyCredentialParameters, + PublicKeyCredentialUserEntity, RelyingParty, ResidentKeyRequirement, + UserVerificationRequirement, + }, + errors::AuthenticatorError, + statecallback::StateCallback, + AuthenticatorInfo, BioEnrollmentResult, CredentialManagementResult, InteractiveRequest, + ManageResult, Pin, RegisterResult, SignResult, StateMachine, StatusPinUv, StatusUpdate, +}; +use base64::Engine; +use cstr::cstr; +use moz_task::{get_main_thread, RunnableBuilder}; +use nserror::{ + nsresult, NS_ERROR_DOM_ABORT_ERR, NS_ERROR_DOM_INVALID_STATE_ERR, NS_ERROR_DOM_NOT_ALLOWED_ERR, + NS_ERROR_DOM_OPERATION_ERR, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_NOT_AVAILABLE, + NS_ERROR_NOT_IMPLEMENTED, NS_ERROR_NULL_POINTER, NS_OK, +}; +use nsstring::{nsACString, nsAString, nsCString, nsString}; +use serde::Serialize; +use serde_cbor; +use serde_json::json; +use std::fmt::Write; +use std::sync::mpsc::{channel, Receiver, RecvError, Sender}; +use std::sync::{Arc, Mutex, MutexGuard}; +use thin_vec::{thin_vec, ThinVec}; +use xpcom::interfaces::{ + nsICredentialParameters, nsIObserverService, nsIWebAuthnAttObj, nsIWebAuthnAutoFillEntry, + nsIWebAuthnRegisterArgs, nsIWebAuthnRegisterPromise, nsIWebAuthnRegisterResult, + nsIWebAuthnService, nsIWebAuthnSignArgs, nsIWebAuthnSignPromise, nsIWebAuthnSignResult, +}; +use xpcom::{xpcom_method, RefPtr}; +mod about_webauthn_controller; +use about_webauthn_controller::*; +mod test_token; +use test_token::TestTokenManager; + +fn authrs_to_nserror(e: AuthenticatorError) -> nsresult { + match e { + AuthenticatorError::CredentialExcluded => NS_ERROR_DOM_INVALID_STATE_ERR, + _ => NS_ERROR_DOM_NOT_ALLOWED_ERR, + } +} + +fn should_cancel_prompts<T>(result: &Result<T, AuthenticatorError>) -> bool { + match result { + Err(AuthenticatorError::CredentialExcluded) | Err(AuthenticatorError::PinError(_)) => false, + _ => true, + } +} + +// Using serde(tag="type") makes it so that, for example, BrowserPromptType::Cancel is serialized +// as '{ type: "cancel" }', and BrowserPromptType::PinInvalid { retries: 5 } is serialized as +// '{type: "pin-invalid", retries: 5}'. +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +enum BrowserPromptType<'a> { + AlreadyRegistered, + Cancel, + DeviceBlocked, + PinAuthBlocked, + PinNotSet, + Presence, + SelectDevice, + UvBlocked, + PinRequired, + SelectedDevice { + auth_info: Option<AuthenticatorInfo>, + }, + PinInvalid { + retries: Option<u8>, + }, + PinIsTooLong, + PinIsTooShort, + RegisterDirect, + UvInvalid { + retries: Option<u8>, + }, + SelectSignResult { + entities: &'a [PublicKeyCredentialUserEntity], + }, + ListenSuccess, + ListenError { + error: Box<BrowserPromptType<'a>>, + }, + CredentialManagementUpdate { + result: CredentialManagementResult, + }, + BioEnrollmentUpdate { + result: BioEnrollmentResult, + }, + UnknownError, +} + +#[derive(Debug)] +enum PromptTarget { + Browser, + AboutPage, +} + +#[derive(Serialize)] +struct BrowserPromptMessage<'a> { + prompt: BrowserPromptType<'a>, + tid: u64, + origin: Option<&'a str>, + #[serde(rename = "browsingContextId")] + browsing_context_id: Option<u64>, +} + +fn notify_observers(prompt_target: PromptTarget, json: nsString) -> Result<(), nsresult> { + let main_thread = get_main_thread()?; + let target = match prompt_target { + PromptTarget::Browser => cstr!("webauthn-prompt"), + PromptTarget::AboutPage => cstr!("about-webauthn-prompt"), + }; + + RunnableBuilder::new("AuthrsService::send_prompt", move || { + if let Ok(obs_svc) = xpcom::components::Observer::service::<nsIObserverService>() { + unsafe { + obs_svc.NotifyObservers(std::ptr::null(), target.as_ptr(), json.as_ptr()); + } + } + }) + .dispatch(main_thread.coerce()) +} + +fn send_prompt( + prompt: BrowserPromptType, + tid: u64, + origin: Option<&str>, + browsing_context_id: Option<u64>, +) -> Result<(), nsresult> { + let mut json = nsString::new(); + write!( + json, + "{}", + json!(&BrowserPromptMessage { + prompt, + tid, + origin, + browsing_context_id + }) + ) + .or(Err(NS_ERROR_FAILURE))?; + notify_observers(PromptTarget::Browser, json) +} + +fn cancel_prompts(tid: u64) -> Result<(), nsresult> { + send_prompt(BrowserPromptType::Cancel, tid, None, None)?; + Ok(()) +} + +#[xpcom(implement(nsIWebAuthnRegisterResult), atomic)] +pub struct WebAuthnRegisterResult { + result: RegisterResult, +} + +impl WebAuthnRegisterResult { + xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString); + fn get_client_data_json(&self) -> Result<nsCString, nsresult> { + Err(NS_ERROR_NOT_AVAILABLE) + } + + xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); + fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + serde_cbor::to_writer(&mut out, &self.result.att_obj).or(Err(NS_ERROR_FAILURE))?; + Ok(out) + } + + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); + fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { + let Some(credential_data) = &self.result.att_obj.auth_data.credential_data else { + return Err(NS_ERROR_FAILURE); + }; + Ok(credential_data.credential_id.as_slice().into()) + } + + xpcom_method!(get_transports => GetTransports() -> ThinVec<nsString>); + fn get_transports(&self) -> Result<ThinVec<nsString>, nsresult> { + // The list that we return here might be included in a future GetAssertion request as a + // hint as to which transports to try. In production, we only support the "usb" transport. + // In tests, the result is not very important, but we can at least return "internal" if + // we're simulating platform attachment. + if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") + && self.result.attachment == AuthenticatorAttachment::Platform + { + Ok(thin_vec![nsString::from("internal")]) + } else { + Ok(thin_vec![nsString::from("usb")]) + } + } + + xpcom_method!(get_hmac_create_secret => GetHmacCreateSecret() -> bool); + fn get_hmac_create_secret(&self) -> Result<bool, nsresult> { + let Some(hmac_create_secret) = self.result.extensions.hmac_create_secret else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + Ok(hmac_create_secret) + } + + xpcom_method!(get_cred_props_rk => GetCredPropsRk() -> bool); + fn get_cred_props_rk(&self) -> Result<bool, nsresult> { + let Some(cred_props) = &self.result.extensions.cred_props else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + Ok(cred_props.rk) + } + + xpcom_method!(set_cred_props_rk => SetCredPropsRk(aCredPropsRk: bool)); + fn set_cred_props_rk(&self, _cred_props_rk: bool) -> Result<(), nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } + + xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString); + fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> { + match self.result.attachment { + AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")), + AuthenticatorAttachment::Platform => Ok(nsString::from("platform")), + AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE), + } + } +} + +#[xpcom(implement(nsIWebAuthnAttObj), atomic)] +pub struct WebAuthnAttObj { + att_obj: AttestationObject, +} + +impl WebAuthnAttObj { + xpcom_method!(get_attestation_object => GetAttestationObject() -> ThinVec<u8>); + fn get_attestation_object(&self) -> Result<ThinVec<u8>, nsresult> { + let mut out = ThinVec::new(); + serde_cbor::to_writer(&mut out, &self.att_obj).or(Err(NS_ERROR_FAILURE))?; + Ok(out) + } + + xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>); + fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> { + // TODO(https://github.com/mozilla/authenticator-rs/issues/302) use to_writer + Ok(self.att_obj.auth_data.to_vec().into()) + } + + xpcom_method!(get_public_key => GetPublicKey() -> ThinVec<u8>); + fn get_public_key(&self) -> Result<ThinVec<u8>, nsresult> { + let Some(credential_data) = &self.att_obj.auth_data.credential_data else { + return Err(NS_ERROR_FAILURE); + }; + Ok(credential_data + .credential_public_key + .der_spki() + .or(Err(NS_ERROR_NOT_AVAILABLE))? + .into()) + } + + xpcom_method!(get_public_key_algorithm => GetPublicKeyAlgorithm() -> i32); + fn get_public_key_algorithm(&self) -> Result<i32, nsresult> { + let Some(credential_data) = &self.att_obj.auth_data.credential_data else { + return Err(NS_ERROR_FAILURE); + }; + // safe to cast to i32 by inspection of defined values + Ok(credential_data.credential_public_key.alg as i32) + } +} + +#[xpcom(implement(nsIWebAuthnSignResult), atomic)] +pub struct WebAuthnSignResult { + result: SignResult, +} + +impl WebAuthnSignResult { + xpcom_method!(get_client_data_json => GetClientDataJSON() -> nsACString); + fn get_client_data_json(&self) -> Result<nsCString, nsresult> { + Err(NS_ERROR_NOT_AVAILABLE) + } + + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); + fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { + let Some(cred) = &self.result.assertion.credentials else { + return Err(NS_ERROR_FAILURE); + }; + Ok(cred.id.as_slice().into()) + } + + xpcom_method!(get_signature => GetSignature() -> ThinVec<u8>); + fn get_signature(&self) -> Result<ThinVec<u8>, nsresult> { + Ok(self.result.assertion.signature.as_slice().into()) + } + + xpcom_method!(get_authenticator_data => GetAuthenticatorData() -> ThinVec<u8>); + fn get_authenticator_data(&self) -> Result<ThinVec<u8>, nsresult> { + Ok(self.result.assertion.auth_data.to_vec().into()) + } + + xpcom_method!(get_user_handle => GetUserHandle() -> ThinVec<u8>); + fn get_user_handle(&self) -> Result<ThinVec<u8>, nsresult> { + let Some(user) = &self.result.assertion.user else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + Ok(user.id.as_slice().into()) + } + + xpcom_method!(get_user_name => GetUserName() -> nsACString); + fn get_user_name(&self) -> Result<nsCString, nsresult> { + let Some(user) = &self.result.assertion.user else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + let Some(name) = &user.name else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + Ok(nsCString::from(name)) + } + + xpcom_method!(get_authenticator_attachment => GetAuthenticatorAttachment() -> nsAString); + fn get_authenticator_attachment(&self) -> Result<nsString, nsresult> { + match self.result.attachment { + AuthenticatorAttachment::CrossPlatform => Ok(nsString::from("cross-platform")), + AuthenticatorAttachment::Platform => Ok(nsString::from("platform")), + AuthenticatorAttachment::Unknown => Err(NS_ERROR_NOT_AVAILABLE), + } + } + + xpcom_method!(get_used_app_id => GetUsedAppId() -> bool); + fn get_used_app_id(&self) -> Result<bool, nsresult> { + self.result.extensions.app_id.ok_or(NS_ERROR_NOT_AVAILABLE) + } + + xpcom_method!(set_used_app_id => SetUsedAppId(aUsedAppId: bool)); + fn set_used_app_id(&self, _used_app_id: bool) -> Result<(), nsresult> { + Err(NS_ERROR_NOT_IMPLEMENTED) + } +} + +// A transaction may create a channel to ask a user for additional input, e.g. a PIN. The Sender +// component of this channel is sent to an AuthrsServide in a StatusUpdate. AuthrsService +// caches the sender along with the expected (u64) transaction ID, which is used as a consistency +// check in callbacks. +type PinReceiver = Option<(u64, Sender<Pin>)>; +type SelectionReceiver = Option<(u64, Sender<Option<usize>>)>; + +fn status_callback( + status_rx: Receiver<StatusUpdate>, + tid: u64, + origin: &String, + browsing_context_id: u64, + transaction: Arc<Mutex<Option<TransactionState>>>, /* Shared with an AuthrsService */ +) -> Result<(), nsresult> { + let origin = Some(origin.as_str()); + let browsing_context_id = Some(browsing_context_id); + loop { + match status_rx.recv() { + Ok(StatusUpdate::SelectDeviceNotice) => { + debug!("STATUS: Please select a device by touching one of them."); + send_prompt( + BrowserPromptType::SelectDevice, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PresenceRequired) => { + debug!("STATUS: Waiting for user presence"); + send_prompt( + BrowserPromptType::Presence, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinRequired(sender))) => { + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + transaction.pin_receiver.replace((tid, sender)); + send_prompt( + BrowserPromptType::PinRequired, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidPin(sender, retries))) => { + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + transaction.pin_receiver.replace((tid, sender)); + send_prompt( + BrowserPromptType::PinInvalid { retries }, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinAuthBlocked)) => { + send_prompt( + BrowserPromptType::PinAuthBlocked, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinBlocked)) => { + send_prompt( + BrowserPromptType::DeviceBlocked, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinNotSet)) => { + send_prompt( + BrowserPromptType::PinNotSet, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::InvalidUv(retries))) => { + send_prompt( + BrowserPromptType::UvInvalid { retries }, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::UvBlocked)) => { + send_prompt( + BrowserPromptType::UvBlocked, + tid, + origin, + browsing_context_id, + )?; + } + Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooShort)) + | Ok(StatusUpdate::PinUvError(StatusPinUv::PinIsTooLong(..))) => { + // These should never happen. + warn!("STATUS: Got unexpected StatusPinUv-error."); + } + Ok(StatusUpdate::InteractiveManagement(_)) => { + debug!("STATUS: interactive management"); + } + Ok(StatusUpdate::SelectResultNotice(sender, entities)) => { + debug!("STATUS: select result notice"); + let mut guard = transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + warn!("STATUS: received status update after end of transaction."); + break; + }; + transaction.selection_receiver.replace((tid, sender)); + send_prompt( + BrowserPromptType::SelectSignResult { + entities: &entities, + }, + tid, + origin, + browsing_context_id, + )?; + } + Err(RecvError) => { + debug!("STATUS: end"); + break; + } + } + } + Ok(()) +} + +#[derive(Clone)] +struct RegisterPromise(RefPtr<nsIWebAuthnRegisterPromise>); + +impl RegisterPromise { + fn resolve_or_reject(&self, result: Result<RegisterResult, nsresult>) -> Result<(), nsresult> { + match result { + Ok(result) => { + let wrapped_result = + WebAuthnRegisterResult::allocate(InitWebAuthnRegisterResult { result }) + .query_interface::<nsIWebAuthnRegisterResult>() + .ok_or(NS_ERROR_FAILURE)?; + unsafe { self.0.Resolve(wrapped_result.coerce()) }; + } + Err(result) => { + unsafe { self.0.Reject(result) }; + } + } + Ok(()) + } +} + +#[derive(Clone)] +struct SignPromise(RefPtr<nsIWebAuthnSignPromise>); + +impl SignPromise { + fn resolve_or_reject(&self, result: Result<SignResult, nsresult>) -> Result<(), nsresult> { + match result { + Ok(result) => { + let wrapped_result = + WebAuthnSignResult::allocate(InitWebAuthnSignResult { result }) + .query_interface::<nsIWebAuthnSignResult>() + .ok_or(NS_ERROR_FAILURE)?; + unsafe { self.0.Resolve(wrapped_result.coerce()) }; + } + Err(result) => { + unsafe { self.0.Reject(result) }; + } + } + Ok(()) + } +} + +#[derive(Clone)] +enum TransactionPromise { + Listen, + Register(RegisterPromise), + Sign(SignPromise), +} + +impl TransactionPromise { + fn reject(&self, err: nsresult) -> Result<(), nsresult> { + match self { + TransactionPromise::Listen => Ok(()), + TransactionPromise::Register(promise) => promise.resolve_or_reject(Err(err)), + TransactionPromise::Sign(promise) => promise.resolve_or_reject(Err(err)), + } + } +} + +enum TransactionArgs { + Register(/* timeout */ u64, RegisterArgs), + Sign(/* timeout */ u64, SignArgs), +} + +struct TransactionState { + tid: u64, + browsing_context_id: u64, + pending_args: Option<TransactionArgs>, + promise: TransactionPromise, + pin_receiver: PinReceiver, + selection_receiver: SelectionReceiver, + interactive_receiver: InteractiveManagementReceiver, + puat_cache: Option<PinUvAuthResult>, // Cached credential to avoid repeated PIN-entries +} + +// AuthrsService provides an nsIWebAuthnService built on top of authenticator-rs. +#[xpcom(implement(nsIWebAuthnService), atomic)] +pub struct AuthrsService { + usb_token_manager: Mutex<StateMachine>, + test_token_manager: TestTokenManager, + transaction: Arc<Mutex<Option<TransactionState>>>, +} + +impl AuthrsService { + xpcom_method!(pin_callback => PinCallback(aTransactionId: u64, aPin: *const nsACString)); + fn pin_callback(&self, transaction_id: u64, pin: &nsACString) -> Result<(), nsresult> { + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + let mut guard = self.transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + // No ongoing transaction + return Err(NS_ERROR_FAILURE); + }; + let Some((tid, channel)) = transaction.pin_receiver.take() else { + // We weren't expecting a pin. + return Err(NS_ERROR_FAILURE); + }; + if tid != transaction_id { + // The browser is confused about which transaction is active. + // This shouldn't happen + return Err(NS_ERROR_FAILURE); + } + channel + .send(Pin::new(&pin.to_string())) + .or(Err(NS_ERROR_FAILURE)) + } else { + // Silently accept request, if all webauthn-options are disabled. + // Used for testing. + Ok(()) + } + } + + xpcom_method!(selection_callback => SelectionCallback(aTransactionId: u64, aSelection: u64)); + fn selection_callback(&self, transaction_id: u64, selection: u64) -> Result<(), nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(transaction) = guard.as_mut() else { + // No ongoing transaction + return Err(NS_ERROR_FAILURE); + }; + let Some((tid, channel)) = transaction.selection_receiver.take() else { + // We weren't expecting a selection. + return Err(NS_ERROR_FAILURE); + }; + if tid != transaction_id { + // The browser is confused about which transaction is active. + // This shouldn't happen + return Err(NS_ERROR_FAILURE); + } + channel + .send(Some(selection as usize)) + .or(Err(NS_ERROR_FAILURE)) + } + + xpcom_method!(get_is_uvpaa => GetIsUVPAA() -> bool); + fn get_is_uvpaa(&self) -> Result<bool, nsresult> { + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + Ok(false) + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + Ok(self.test_token_manager.has_platform_authenticator()) + } else { + Err(NS_ERROR_NOT_AVAILABLE) + } + } + + xpcom_method!(make_credential => MakeCredential(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnRegisterArgs, aPromise: *const nsIWebAuthnRegisterPromise)); + fn make_credential( + &self, + tid: u64, + browsing_context_id: u64, + args: &nsIWebAuthnRegisterArgs, + promise: &nsIWebAuthnRegisterPromise, + ) -> Result<(), nsresult> { + self.reset()?; + + let promise = RegisterPromise(RefPtr::new(promise)); + + let mut origin = nsString::new(); + unsafe { args.GetOrigin(&mut *origin) }.to_result()?; + + let mut relying_party_id = nsString::new(); + unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?; + + let mut client_data_hash = ThinVec::new(); + unsafe { args.GetClientDataHash(&mut client_data_hash) }.to_result()?; + let mut client_data_hash_arr = [0u8; 32]; + client_data_hash_arr.copy_from_slice(&client_data_hash); + + let mut timeout_ms = 0u32; + unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; + + let mut exclude_list = ThinVec::new(); + unsafe { args.GetExcludeList(&mut exclude_list) }.to_result()?; + let exclude_list = exclude_list + .iter_mut() + .map(|id| PublicKeyCredentialDescriptor { + id: id.to_vec(), + transports: vec![], + }) + .collect(); + + let mut relying_party_name = nsString::new(); + unsafe { args.GetRpName(&mut *relying_party_name) }.to_result()?; + + let mut user_id = ThinVec::new(); + unsafe { args.GetUserId(&mut user_id) }.to_result()?; + + let mut user_name = nsString::new(); + unsafe { args.GetUserName(&mut *user_name) }.to_result()?; + + let mut user_display_name = nsString::new(); + unsafe { args.GetUserDisplayName(&mut *user_display_name) }.to_result()?; + + let mut cose_algs = ThinVec::new(); + unsafe { args.GetCoseAlgs(&mut cose_algs) }.to_result()?; + let pub_cred_params = cose_algs + .iter() + .filter_map(|alg| PublicKeyCredentialParameters::try_from(*alg).ok()) + .collect(); + + let mut resident_key = nsString::new(); + unsafe { args.GetResidentKey(&mut *resident_key) }.to_result()?; + let resident_key_req = if resident_key.eq("required") { + ResidentKeyRequirement::Required + } else if resident_key.eq("preferred") { + ResidentKeyRequirement::Preferred + } else if resident_key.eq("discouraged") { + ResidentKeyRequirement::Discouraged + } else { + return Err(NS_ERROR_FAILURE); + }; + + let mut user_verification = nsString::new(); + unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; + let user_verification_req = if user_verification.eq("required") { + UserVerificationRequirement::Required + } else if user_verification.eq("discouraged") { + UserVerificationRequirement::Discouraged + } else { + UserVerificationRequirement::Preferred + }; + + let mut authenticator_attachment = nsString::new(); + if unsafe { args.GetAuthenticatorAttachment(&mut *authenticator_attachment) } + .to_result() + .is_ok() + { + if authenticator_attachment.eq("platform") { + return Err(NS_ERROR_FAILURE); + } + } + + let mut attestation_conveyance_preference = nsString::new(); + unsafe { args.GetAttestationConveyancePreference(&mut *attestation_conveyance_preference) } + .to_result()?; + let none_attestation = !(attestation_conveyance_preference.eq("indirect") + || attestation_conveyance_preference.eq("direct") + || attestation_conveyance_preference.eq("enterprise")); + + let mut cred_props = false; + unsafe { args.GetCredProps(&mut cred_props) }.to_result()?; + + let mut min_pin_length = false; + unsafe { args.GetMinPinLength(&mut min_pin_length) }.to_result()?; + + // TODO(Bug 1593571) - Add this to the extensions + // let mut hmac_create_secret = None; + // let mut maybe_hmac_create_secret = false; + // match unsafe { args.GetHmacCreateSecret(&mut maybe_hmac_create_secret) }.to_result() { + // Ok(_) => hmac_create_secret = Some(maybe_hmac_create_secret), + // _ => (), + // } + + let origin = origin.to_string(); + let info = RegisterArgs { + client_data_hash: client_data_hash_arr, + relying_party: RelyingParty { + id: relying_party_id.to_string(), + name: None, + }, + origin: origin.clone(), + user: PublicKeyCredentialUserEntity { + id: user_id.to_vec(), + name: Some(user_name.to_string()), + display_name: None, + }, + pub_cred_params, + exclude_list, + user_verification_req, + resident_key_req, + extensions: AuthenticationExtensionsClientInputs { + cred_props: cred_props.then_some(true), + min_pin_length: min_pin_length.then_some(true), + ..Default::default() + }, + pin: None, + use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"), + }; + + *self.transaction.lock().unwrap() = Some(TransactionState { + tid, + browsing_context_id, + pending_args: Some(TransactionArgs::Register(timeout_ms as u64, info)), + promise: TransactionPromise::Register(promise), + pin_receiver: None, + selection_receiver: None, + interactive_receiver: None, + puat_cache: None, + }); + + if none_attestation + || static_prefs::pref!("security.webauth.webauthn_testing_allow_direct_attestation") + { + self.resume_make_credential(tid, none_attestation) + } else { + send_prompt( + BrowserPromptType::RegisterDirect, + tid, + Some(&origin), + Some(browsing_context_id), + ) + } + } + + xpcom_method!(resume_make_credential => ResumeMakeCredential(aTid: u64, aForceNoneAttestation: bool)); + fn resume_make_credential( + &self, + tid: u64, + force_none_attestation: bool, + ) -> Result<(), nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_FAILURE); + }; + if state.tid != tid { + return Err(NS_ERROR_FAILURE); + }; + let browsing_context_id = state.browsing_context_id; + let Some(TransactionArgs::Register(timeout_ms, info)) = state.pending_args.take() else { + return Err(NS_ERROR_FAILURE); + }; + // We have to drop the guard here, as there _may_ still be another operation + // ongoing and `register()` below will try to cancel it. This will call the state + // callback of that operation, which in turn may try to access `transaction`, deadlocking. + drop(guard); + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let status_transaction = self.transaction.clone(); + let status_origin = info.origin.clone(); + RunnableBuilder::new("AuthrsService::MakeCredential::StatusReceiver", move || { + let _ = status_callback( + status_rx, + tid, + &status_origin, + browsing_context_id, + status_transaction, + ); + }) + .may_block(true) + .dispatch_background_task()?; + + let callback_transaction = self.transaction.clone(); + let callback_origin = info.origin.clone(); + let state_callback = StateCallback::<Result<RegisterResult, AuthenticatorError>>::new( + Box::new(move |mut result| { + let mut guard = callback_transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return; + }; + if state.tid != tid { + return; + } + let TransactionPromise::Register(ref promise) = state.promise else { + return; + }; + if let Ok(inner) = result.as_mut() { + // Tokens always provide attestation, but the user may have asked we not + // include the attestation statement in the response. + if force_none_attestation { + inner.att_obj.anonymize(); + } + } + if let Err(AuthenticatorError::CredentialExcluded) = result { + let _ = send_prompt( + BrowserPromptType::AlreadyRegistered, + tid, + Some(&callback_origin), + Some(browsing_context_id), + ); + } + if should_cancel_prompts(&result) { + // Some errors are accompanied by prompts that should persist after the + // operation terminates. + let _ = cancel_prompts(tid); + } + let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror)); + *guard = None; + }), + ); + + // The authenticator crate provides an `AuthenticatorService` which can dispatch a request + // in parallel to any number of transports. We only support the USB transport in production + // configurations, so we do not need the full generality of `AuthenticatorService` here. + // We disable the USB transport in tests that use virtual devices. + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + // TODO(Bug 1855290) Remove this presence prompt + send_prompt( + BrowserPromptType::Presence, + tid, + Some(&info.origin), + Some(browsing_context_id), + )?; + self.usb_token_manager.lock().unwrap().register( + timeout_ms, + info, + status_tx, + state_callback, + ); + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + self.test_token_manager + .register(timeout_ms, info, status_tx, state_callback); + } else { + return Err(NS_ERROR_FAILURE); + } + + Ok(()) + } + + xpcom_method!(get_assertion => GetAssertion(aTid: u64, aBrowsingContextId: u64, aArgs: *const nsIWebAuthnSignArgs, aPromise: *const nsIWebAuthnSignPromise)); + fn get_assertion( + &self, + tid: u64, + browsing_context_id: u64, + args: &nsIWebAuthnSignArgs, + promise: &nsIWebAuthnSignPromise, + ) -> Result<(), nsresult> { + self.reset()?; + + let promise = SignPromise(RefPtr::new(promise)); + + let mut origin = nsString::new(); + unsafe { args.GetOrigin(&mut *origin) }.to_result()?; + + let mut relying_party_id = nsString::new(); + unsafe { args.GetRpId(&mut *relying_party_id) }.to_result()?; + + let mut client_data_hash = ThinVec::new(); + unsafe { args.GetClientDataHash(&mut client_data_hash) }.to_result()?; + let mut client_data_hash_arr = [0u8; 32]; + client_data_hash_arr.copy_from_slice(&client_data_hash); + + let mut timeout_ms = 0u32; + unsafe { args.GetTimeoutMS(&mut timeout_ms) }.to_result()?; + + let mut allow_list = ThinVec::new(); + unsafe { args.GetAllowList(&mut allow_list) }.to_result()?; + let allow_list = allow_list + .iter() + .map(|id| PublicKeyCredentialDescriptor { + id: id.to_vec(), + transports: vec![], + }) + .collect(); + + let mut user_verification = nsString::new(); + unsafe { args.GetUserVerification(&mut *user_verification) }.to_result()?; + let user_verification_req = if user_verification.eq("required") { + UserVerificationRequirement::Required + } else if user_verification.eq("discouraged") { + UserVerificationRequirement::Discouraged + } else { + UserVerificationRequirement::Preferred + }; + + let mut app_id = None; + let mut maybe_app_id = nsString::new(); + match unsafe { args.GetAppId(&mut *maybe_app_id) }.to_result() { + Ok(_) => app_id = Some(maybe_app_id.to_string()), + _ => (), + } + + let mut conditionally_mediated = false; + unsafe { args.GetConditionallyMediated(&mut conditionally_mediated) }.to_result()?; + + let info = SignArgs { + client_data_hash: client_data_hash_arr, + relying_party_id: relying_party_id.to_string(), + origin: origin.to_string(), + allow_list, + user_verification_req, + user_presence_req: true, + extensions: AuthenticationExtensionsClientInputs { + app_id, + ..Default::default() + }, + pin: None, + use_ctap1_fallback: !static_prefs::pref!("security.webauthn.ctap2"), + }; + + let mut guard = self.transaction.lock().unwrap(); + *guard = Some(TransactionState { + tid, + browsing_context_id, + pending_args: Some(TransactionArgs::Sign(timeout_ms as u64, info)), + promise: TransactionPromise::Sign(promise), + pin_receiver: None, + selection_receiver: None, + interactive_receiver: None, + puat_cache: None, + }); + + if !conditionally_mediated { + // Immediately proceed to the modal UI flow. + self.do_get_assertion(None, guard) + } else { + // Cache the request and wait for the conditional UI to request autofill entries, etc. + Ok(()) + } + } + + fn do_get_assertion( + &self, + mut selected_credential_id: Option<Vec<u8>>, + mut guard: MutexGuard<Option<TransactionState>>, + ) -> Result<(), nsresult> { + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_FAILURE); + }; + let browsing_context_id = state.browsing_context_id; + let tid = state.tid; + let (timeout_ms, mut info) = match state.pending_args.take() { + Some(TransactionArgs::Sign(timeout_ms, info)) => (timeout_ms, info), + _ => return Err(NS_ERROR_FAILURE), + }; + + if let Some(id) = selected_credential_id.take() { + if info.allow_list.is_empty() { + info.allow_list.push(PublicKeyCredentialDescriptor { + id, + transports: vec![], + }); + } else { + // We need to ensure that the selected credential id + // was in the original allow_list. + info.allow_list.retain(|cred| cred.id == id); + if info.allow_list.is_empty() { + return Err(NS_ERROR_FAILURE); + } + } + } + + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let status_transaction = self.transaction.clone(); + let status_origin = info.origin.to_string(); + RunnableBuilder::new("AuthrsService::GetAssertion::StatusReceiver", move || { + let _ = status_callback( + status_rx, + tid, + &status_origin, + browsing_context_id, + status_transaction, + ); + }) + .may_block(true) + .dispatch_background_task()?; + + let uniq_allowed_cred = if info.allow_list.len() == 1 { + info.allow_list.first().cloned() + } else { + None + }; + + let callback_transaction = self.transaction.clone(); + let state_callback = StateCallback::<Result<SignResult, AuthenticatorError>>::new( + Box::new(move |mut result| { + let mut guard = callback_transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return; + }; + if state.tid != tid { + return; + } + let TransactionPromise::Sign(ref promise) = state.promise else { + return; + }; + if uniq_allowed_cred.is_some() { + // In CTAP 2.0, but not CTAP 2.1, the assertion object's credential field + // "May be omitted if the allowList has exactly one credential." If we had + // a unique allowed credential, then copy its descriptor to the output. + if let Ok(inner) = result.as_mut() { + inner.assertion.credentials = uniq_allowed_cred; + } + } + if should_cancel_prompts(&result) { + // Some errors are accompanied by prompts that should persist after the + // operation terminates. + let _ = cancel_prompts(tid); + } + let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror)); + *guard = None; + }), + ); + + // TODO(Bug 1855290) Remove this presence prompt + send_prompt( + BrowserPromptType::Presence, + tid, + Some(&info.origin), + Some(browsing_context_id), + )?; + + // As in `register`, we are intentionally avoiding `AuthenticatorService` here. + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + self.usb_token_manager.lock().unwrap().sign( + timeout_ms as u64, + info, + status_tx, + state_callback, + ); + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + self.test_token_manager + .sign(timeout_ms as u64, info, status_tx, state_callback); + } else { + return Err(NS_ERROR_FAILURE); + } + + Ok(()) + } + + xpcom_method!(has_pending_conditional_get => HasPendingConditionalGet(aBrowsingContextId: u64, aOrigin: *const nsAString) -> u64); + fn has_pending_conditional_get( + &self, + browsing_context_id: u64, + origin: &nsAString, + ) -> Result<u64, nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Ok(0); + }; + let Some(TransactionArgs::Sign(_, info)) = state.pending_args.as_ref() else { + return Ok(0); + }; + if state.browsing_context_id != browsing_context_id { + return Ok(0); + } + if !info.origin.eq(&origin.to_string()) { + return Ok(0); + } + Ok(state.tid) + } + + xpcom_method!(get_autofill_entries => GetAutoFillEntries(aTransactionId: u64) -> ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>); + fn get_autofill_entries( + &self, + tid: u64, + ) -> Result<ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>, nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + if state.tid != tid { + return Err(NS_ERROR_NOT_AVAILABLE); + } + let Some(TransactionArgs::Sign(_, info)) = state.pending_args.as_ref() else { + return Err(NS_ERROR_NOT_AVAILABLE); + }; + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + // We don't currently support silent discovery for credentials on USB tokens. + return Ok(thin_vec![]); + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return self + .test_token_manager + .get_autofill_entries(&info.relying_party_id, &info.allow_list); + } else { + return Err(NS_ERROR_FAILURE); + } + } + + xpcom_method!(select_autofill_entry => SelectAutoFillEntry(aTid: u64, aCredentialId: *const ThinVec<u8>)); + fn select_autofill_entry(&self, tid: u64, credential_id: &ThinVec<u8>) -> Result<(), nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_FAILURE); + }; + if tid != state.tid { + return Err(NS_ERROR_FAILURE); + } + self.do_get_assertion(Some(credential_id.to_vec()), guard) + } + + xpcom_method!(resume_conditional_get => ResumeConditionalGet(aTid: u64)); + fn resume_conditional_get(&self, tid: u64) -> Result<(), nsresult> { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return Err(NS_ERROR_FAILURE); + }; + if tid != state.tid { + return Err(NS_ERROR_FAILURE); + } + self.do_get_assertion(None, guard) + } + + // Clears the transaction state if tid matches the ongoing transaction ID. + // Returns whether the tid was a match. + fn clear_transaction(&self, tid: u64) -> bool { + let mut guard = self.transaction.lock().unwrap(); + let Some(state) = guard.as_ref() else { + return true; // workaround for Bug 1864526. + }; + if state.tid != tid { + // Ignore the cancellation request if the transaction + // ID does not match. + return false; + } + // It's possible that we haven't dispatched the request to the usb_token_manager yet, + // e.g. if we're waiting for resume_make_credential. So reject the promise and drop the + // state here rather than from the StateCallback + let _ = state.promise.reject(NS_ERROR_DOM_NOT_ALLOWED_ERR); + *guard = None; + true + } + + xpcom_method!(cancel => Cancel(aTransactionId: u64)); + fn cancel(&self, tid: u64) -> Result<(), nsresult> { + if self.clear_transaction(tid) { + self.usb_token_manager.lock().unwrap().cancel(); + } + Ok(()) + } + + xpcom_method!(reset => Reset()); + fn reset(&self) -> Result<(), nsresult> { + { + if let Some(state) = self.transaction.lock().unwrap().take() { + cancel_prompts(state.tid)?; + state.promise.reject(NS_ERROR_DOM_ABORT_ERR)?; + } + } // release the transaction lock so a StateCallback can take it + self.usb_token_manager.lock().unwrap().cancel(); + Ok(()) + } + + xpcom_method!( + add_virtual_authenticator => AddVirtualAuthenticator( + protocol: *const nsACString, + transport: *const nsACString, + hasResidentKey: bool, + hasUserVerification: bool, + isUserConsenting: bool, + isUserVerified: bool) -> u64 + ); + fn add_virtual_authenticator( + &self, + protocol: &nsACString, + transport: &nsACString, + has_resident_key: bool, + has_user_verification: bool, + is_user_consenting: bool, + is_user_verified: bool, + ) -> Result<u64, nsresult> { + let protocol = match protocol.to_string().as_str() { + "ctap1/u2f" => AuthenticatorVersion::U2F_V2, + "ctap2" => AuthenticatorVersion::FIDO_2_0, + "ctap2_1" => AuthenticatorVersion::FIDO_2_1, + _ => return Err(NS_ERROR_INVALID_ARG), + }; + let transport = transport.to_string(); + match transport.as_str() { + "usb" | "nfc" | "ble" | "smart-card" | "hybrid" | "internal" => (), + _ => return Err(NS_ERROR_INVALID_ARG), + }; + self.test_token_manager.add_virtual_authenticator( + protocol, + transport, + has_resident_key, + has_user_verification, + is_user_consenting, + is_user_verified, + ) + } + + xpcom_method!(remove_virtual_authenticator => RemoveVirtualAuthenticator(authenticatorId: u64)); + fn remove_virtual_authenticator(&self, authenticator_id: u64) -> Result<(), nsresult> { + self.test_token_manager + .remove_virtual_authenticator(authenticator_id) + } + + xpcom_method!( + add_credential => AddCredential( + authenticatorId: u64, + credentialId: *const nsACString, + isResidentCredential: bool, + rpId: *const nsACString, + privateKey: *const nsACString, + userHandle: *const nsACString, + signCount: u32) + ); + fn add_credential( + &self, + authenticator_id: u64, + credential_id: &nsACString, + is_resident_credential: bool, + rp_id: &nsACString, + private_key: &nsACString, + user_handle: &nsACString, + sign_count: u32, + ) -> Result<(), nsresult> { + let credential_id = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(credential_id) + .or(Err(NS_ERROR_INVALID_ARG))?; + let private_key = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(private_key) + .or(Err(NS_ERROR_INVALID_ARG))?; + let user_handle = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(user_handle) + .or(Err(NS_ERROR_INVALID_ARG))?; + self.test_token_manager.add_credential( + authenticator_id, + &credential_id, + &private_key, + &user_handle, + sign_count, + rp_id.to_string(), + is_resident_credential, + ) + } + + xpcom_method!(get_credentials => GetCredentials(authenticatorId: u64) -> ThinVec<Option<RefPtr<nsICredentialParameters>>>); + fn get_credentials( + &self, + authenticator_id: u64, + ) -> Result<ThinVec<Option<RefPtr<nsICredentialParameters>>>, nsresult> { + self.test_token_manager.get_credentials(authenticator_id) + } + + xpcom_method!(remove_credential => RemoveCredential(authenticatorId: u64, credentialId: *const nsACString)); + fn remove_credential( + &self, + authenticator_id: u64, + credential_id: &nsACString, + ) -> Result<(), nsresult> { + let credential_id = base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(credential_id) + .or(Err(NS_ERROR_INVALID_ARG))?; + self.test_token_manager + .remove_credential(authenticator_id, credential_id.as_ref()) + } + + xpcom_method!(remove_all_credentials => RemoveAllCredentials(authenticatorId: u64)); + fn remove_all_credentials(&self, authenticator_id: u64) -> Result<(), nsresult> { + self.test_token_manager + .remove_all_credentials(authenticator_id) + } + + xpcom_method!(set_user_verified => SetUserVerified(authenticatorId: u64, isUserVerified: bool)); + fn set_user_verified( + &self, + authenticator_id: u64, + is_user_verified: bool, + ) -> Result<(), nsresult> { + self.test_token_manager + .set_user_verified(authenticator_id, is_user_verified) + } + + xpcom_method!(listen => Listen()); + pub(crate) fn listen(&self) -> Result<(), nsresult> { + // For now, we don't support softtokens + if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return Ok(()); + } + + { + let mut guard = self.transaction.lock().unwrap(); + if guard.as_ref().is_some() { + // ignore listen() and continue with ongoing transaction + return Ok(()); + } + *guard = Some(TransactionState { + tid: 0, + browsing_context_id: 0, + pending_args: None, + promise: TransactionPromise::Listen, + pin_receiver: None, + selection_receiver: None, + interactive_receiver: None, + puat_cache: None, + }); + } + + // We may get from status_updates info about certain errors (e.g. PinErrors) + // which we want to present to the user. We will ignore the following error + // which is caused by us "hanging up" on the StatusUpdate-channel and return + // the PinError instead, via `upcoming_error`. + let upcoming_error = Arc::new(Mutex::new(None)); + let upcoming_error_c = upcoming_error.clone(); + let callback_transaction = self.transaction.clone(); + let state_callback = StateCallback::<Result<ManageResult, AuthenticatorError>>::new( + Box::new(move |result| { + let mut guard = callback_transaction.lock().unwrap(); + match guard.as_mut() { + Some(state) => { + match state.promise { + TransactionPromise::Listen => (), + _ => return, + } + *guard = None; + } + // We have no transaction anymore, this means cancel() was called + None => (), + } + let msg = match result { + Ok(_) => BrowserPromptType::ListenSuccess, + Err(e) => { + // See if we have a cached error that should replace this error + let replacement = if let Ok(mut x) = upcoming_error_c.lock() { + x.take() + } else { + None + }; + let replaced_err = replacement.unwrap_or(e); + let err = authrs_to_prompt(replaced_err); + BrowserPromptType::ListenError { + error: Box::new(err), + } + } + }; + let _ = send_about_prompt(&msg); + }), + ); + + // Calling `manage()` within the lock, to avoid race conditions + // where we might check listen_blocked, see that it's false, + // continue along, but in parallel `make_credential()` aborts the + // interactive process shortly after, setting listen_blocked to true, + // then accessing usb_token_manager afterwards and at the same time + // we do it here, causing a runtime crash for trying to mut-borrow it twice. + let (status_tx, status_rx) = channel::<StatusUpdate>(); + let status_transaction = self.transaction.clone(); + RunnableBuilder::new( + "AuthrsTransport::AboutWebauthn::StatusReceiver", + move || { + let _ = interactive_status_callback(status_rx, status_transaction, upcoming_error); + }, + ) + .may_block(true) + .dispatch_background_task()?; + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + self.usb_token_manager.lock().unwrap().manage( + 60 * 1000 * 1000, + status_tx, + state_callback, + ); + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + // We don't yet support softtoken + } else { + // Silently accept request, if all webauthn-options are disabled. + // Used for testing. + } + Ok(()) + } + + xpcom_method!(run_command => RunCommand(c_cmd: *const nsACString)); + pub fn run_command(&self, c_cmd: &nsACString) -> Result<(), nsresult> { + // Always test if it can be parsed from incoming JSON (even for tests) + let incoming: RequestWrapper = + serde_json::from_str(&c_cmd.to_utf8()).or(Err(NS_ERROR_DOM_OPERATION_ERR))?; + if static_prefs::pref!("security.webauth.webauthn_enable_usbtoken") { + let guard = self.transaction.lock().unwrap(); + let puat = guard.as_ref().and_then(|g| g.puat_cache.clone()); + let command = match incoming { + RequestWrapper::Quit => InteractiveRequest::Quit, + RequestWrapper::ChangePIN(a, b) => InteractiveRequest::ChangePIN(a, b), + RequestWrapper::SetPIN(a) => InteractiveRequest::SetPIN(a), + RequestWrapper::CredentialManagement(c) => { + InteractiveRequest::CredentialManagement(c, puat) + } + RequestWrapper::BioEnrollment(c) => InteractiveRequest::BioEnrollment(c, puat), + }; + match &guard.as_ref().unwrap().interactive_receiver { + Some(channel) => channel.send(command).or(Err(NS_ERROR_FAILURE)), + // Either we weren't expecting a pin, or the controller is confused + // about which transaction is active. Neither is recoverable, so it's + // OK to drop the PinReceiver here. + _ => Err(NS_ERROR_FAILURE), + } + } else if static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + // We don't yet support softtoken + Ok(()) + } else { + // Silently accept request, if all webauthn-options are disabled. + // Used for testing. + Ok(()) + } + } +} + +#[no_mangle] +pub extern "C" fn authrs_service_constructor(result: *mut *const nsIWebAuthnService) -> nsresult { + let wrapper = AuthrsService::allocate(InitAuthrsService { + usb_token_manager: Mutex::new(StateMachine::new()), + test_token_manager: TestTokenManager::new(), + transaction: Arc::new(Mutex::new(None)), + }); + + #[cfg(feature = "fuzzing")] + { + let fuzzing_config = static_prefs::pref!("fuzzing.webauthn.authenticator_config"); + if fuzzing_config != 0 { + let is_user_verified = (fuzzing_config & 0x01) != 0; + let is_user_consenting = (fuzzing_config & 0x02) != 0; + let has_user_verification = (fuzzing_config & 0x04) != 0; + let has_resident_key = (fuzzing_config & 0x08) != 0; + let transport = nsCString::from(match (fuzzing_config & 0x10) >> 4 { + 0 => "usb", + 1 => "internal", + _ => unreachable!(), + }); + let protocol = nsCString::from(match (fuzzing_config & 0x60) >> 5 { + 0 => "", // reserved + 1 => "ctap1/u2f", + 2 => "ctap2", + 3 => "ctap2_1", + _ => unreachable!(), + }); + // If this fails it's probably because the protocol bits were zero, + // we'll just ignore it. + let _ = wrapper.add_virtual_authenticator( + &protocol, + &transport, + has_resident_key, + has_user_verification, + is_user_consenting, + is_user_verified, + ); + } + } + + unsafe { + RefPtr::new(wrapper.coerce::<nsIWebAuthnService>()).forget(&mut *result); + } + NS_OK +} + +#[no_mangle] +pub extern "C" fn authrs_webauthn_att_obj_constructor( + att_obj_bytes: &ThinVec<u8>, + anonymize: bool, + result: *mut *const nsIWebAuthnAttObj, +) -> nsresult { + if result.is_null() { + return NS_ERROR_NULL_POINTER; + } + + let mut att_obj: AttestationObject = match serde_cbor::from_slice(att_obj_bytes) { + Ok(att_obj) => att_obj, + Err(_) => return NS_ERROR_INVALID_ARG, + }; + + if anonymize { + att_obj.anonymize(); + } + + let wrapper = WebAuthnAttObj::allocate(InitWebAuthnAttObj { att_obj }); + + unsafe { + RefPtr::new(wrapper.coerce::<nsIWebAuthnAttObj>()).forget(&mut *result); + } + + NS_OK +} diff --git a/dom/webauthn/authrs_bridge/src/test_token.rs b/dom/webauthn/authrs_bridge/src/test_token.rs new file mode 100644 index 0000000000..afc2ddbc75 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/test_token.rs @@ -0,0 +1,974 @@ +/* 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::{RegisterArgs, SignArgs}; +use authenticator::crypto::{ecdsa_p256_sha256_sign_raw, COSEAlgorithm, COSEKey, SharedSecret}; +use authenticator::ctap2::{ + attestation::{ + AAGuid, AttestationObject, AttestationStatement, AttestationStatementPacked, + AttestedCredentialData, AuthenticatorData, AuthenticatorDataFlags, Extension, + }, + client_data::ClientDataHash, + commands::{ + client_pin::{ClientPIN, ClientPinResponse, PINSubcommand}, + get_assertion::{GetAssertion, GetAssertionResponse, GetAssertionResult}, + get_info::{AuthenticatorInfo, AuthenticatorOptions, AuthenticatorVersion}, + get_version::{GetVersion, U2FInfo}, + make_credentials::{MakeCredentials, MakeCredentialsResult}, + reset::Reset, + selection::Selection, + RequestCtap1, RequestCtap2, StatusCode, + }, + preflight::CheckKeyHandle, + server::{ + AuthenticatorAttachment, PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity, + RelyingParty, + }, +}; +use authenticator::errors::{AuthenticatorError, CommandError, HIDError, U2FTokenError}; +use authenticator::{ctap2, statecallback::StateCallback}; +use authenticator::{FidoDevice, FidoDeviceIO, FidoProtocol, VirtualFidoDevice}; +use authenticator::{RegisterResult, SignResult, StatusUpdate}; +use base64::Engine; +use moz_task::RunnableBuilder; +use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_OK}; +use nsstring::{nsACString, nsAString, nsCString, nsString}; +use rand::{thread_rng, RngCore}; +use std::cell::{Ref, RefCell}; +use std::collections::{hash_map::Entry, HashMap}; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::mpsc::Sender; +use std::sync::{Arc, Mutex}; +use thin_vec::ThinVec; +use xpcom::interfaces::{nsICredentialParameters, nsIWebAuthnAutoFillEntry}; +use xpcom::{xpcom_method, RefPtr}; + +// All TestTokens use this fixed, randomly generated, AAGUID +const VIRTUAL_TOKEN_AAGUID: AAGuid = AAGuid([ + 0x68, 0xe1, 0x00, 0xa5, 0x0b, 0x47, 0x91, 0x04, 0xb8, 0x54, 0x97, 0xa9, 0xba, 0x51, 0x06, 0x38, +]); + +#[derive(Debug)] +struct TestTokenCredential { + id: Vec<u8>, + privkey: Vec<u8>, + user_handle: Vec<u8>, + sign_count: AtomicU32, + is_discoverable_credential: bool, + rp: RelyingParty, +} + +impl TestTokenCredential { + fn assert( + &self, + client_data_hash: &ClientDataHash, + flags: AuthenticatorDataFlags, + ) -> Result<GetAssertionResponse, HIDError> { + let credentials = Some(PublicKeyCredentialDescriptor { + id: self.id.clone(), + transports: vec![], + }); + + let auth_data = AuthenticatorData { + rp_id_hash: self.rp.hash(), + flags, + counter: self.sign_count.fetch_add(1, Ordering::Relaxed), + credential_data: None, + extensions: Extension::default(), + }; + + let user = Some(PublicKeyCredentialUserEntity { + id: self.user_handle.clone(), + ..Default::default() + }); + + let mut data = auth_data.to_vec(); + data.extend_from_slice(client_data_hash.as_ref()); + let signature = + ecdsa_p256_sha256_sign_raw(&self.privkey, &data).or(Err(HIDError::DeviceError))?; + + Ok(GetAssertionResponse { + credentials, + auth_data, + signature, + user, + number_of_credentials: Some(1), + }) + } +} + +#[derive(Debug)] +struct TestToken { + protocol: FidoProtocol, + transport: String, + versions: Vec<AuthenticatorVersion>, + has_resident_key: bool, + has_user_verification: bool, + is_user_consenting: bool, + is_user_verified: bool, + // This is modified in `make_credentials` which takes a &TestToken, but we only allow one transaction at a time. + credentials: RefCell<Vec<TestTokenCredential>>, + pin_token: [u8; 32], + shared_secret: Option<SharedSecret>, + authenticator_info: Option<AuthenticatorInfo>, +} + +impl TestToken { + fn new( + versions: Vec<AuthenticatorVersion>, + transport: String, + has_resident_key: bool, + has_user_verification: bool, + is_user_consenting: bool, + is_user_verified: bool, + ) -> TestToken { + let mut pin_token = [0u8; 32]; + thread_rng().fill_bytes(&mut pin_token); + Self { + protocol: FidoProtocol::CTAP2, + transport, + versions, + has_resident_key, + has_user_verification, + is_user_consenting, + is_user_verified, + credentials: RefCell::new(vec![]), + pin_token, + shared_secret: None, + authenticator_info: None, + } + } + + fn insert_credential( + &self, + id: &[u8], + privkey: &[u8], + rp: &RelyingParty, + is_discoverable_credential: bool, + user_handle: &[u8], + sign_count: u32, + ) { + let c = TestTokenCredential { + id: id.to_vec(), + privkey: privkey.to_vec(), + rp: rp.clone(), + is_discoverable_credential, + user_handle: user_handle.to_vec(), + sign_count: AtomicU32::new(sign_count), + }; + + let mut credlist = self.credentials.borrow_mut(); + + match credlist.binary_search_by_key(&id, |probe| &probe.id) { + Ok(_) => {} + Err(idx) => credlist.insert(idx, c), + } + } + + fn get_credentials(&self) -> Ref<Vec<TestTokenCredential>> { + self.credentials.borrow() + } + + fn delete_credential(&mut self, id: &[u8]) -> bool { + let mut credlist = self.credentials.borrow_mut(); + if let Ok(idx) = credlist.binary_search_by_key(&id, |probe| &probe.id) { + credlist.remove(idx); + return true; + } + + false + } + + fn delete_all_credentials(&mut self) { + self.credentials.borrow_mut().clear(); + } + + fn has_credential(&self, id: &[u8]) -> bool { + self.credentials + .borrow() + .binary_search_by_key(&id, |probe| &probe.id) + .is_ok() + } + + fn max_supported_version(&self) -> AuthenticatorVersion { + self.authenticator_info + .as_ref() + .map_or(AuthenticatorVersion::U2F_V2, |info| { + info.max_supported_version() + }) + } +} + +impl FidoDevice for TestToken { + fn pre_init(&mut self) -> Result<(), HIDError> { + Ok(()) + } + + fn should_try_ctap2(&self) -> bool { + true + } + + fn initialized(&self) -> bool { + true + } + + fn is_u2f(&mut self) -> bool { + true + } + + fn get_shared_secret(&self) -> Option<&SharedSecret> { + self.shared_secret.as_ref() + } + + fn set_shared_secret(&mut self, shared_secret: SharedSecret) { + self.shared_secret = Some(shared_secret); + } + + fn get_authenticator_info(&self) -> Option<&AuthenticatorInfo> { + self.authenticator_info.as_ref() + } + + fn set_authenticator_info(&mut self, authenticator_info: AuthenticatorInfo) { + self.authenticator_info = Some(authenticator_info); + } + + fn get_protocol(&self) -> FidoProtocol { + self.protocol + } + + fn downgrade_to_ctap1(&mut self) { + self.protocol = FidoProtocol::CTAP1 + } +} + +impl FidoDeviceIO for TestToken { + fn send_msg_cancellable<Out, Req: RequestCtap1<Output = Out> + RequestCtap2<Output = Out>>( + &mut self, + msg: &Req, + keep_alive: &dyn Fn() -> bool, + ) -> Result<Out, HIDError> { + 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), + } + } + + fn send_cbor_cancellable<Req: RequestCtap2>( + &mut self, + msg: &Req, + _keep_alive: &dyn Fn() -> bool, + ) -> Result<Req::Output, HIDError> { + msg.send_to_virtual_device(self) + } + + fn send_ctap1_cancellable<Req: RequestCtap1>( + &mut self, + msg: &Req, + _keep_alive: &dyn Fn() -> bool, + ) -> Result<Req::Output, HIDError> { + msg.send_to_virtual_device(self) + } +} + +impl VirtualFidoDevice for TestToken { + fn check_key_handle(&self, req: &CheckKeyHandle) -> Result<(), HIDError> { + let credlist = self.credentials.borrow(); + let req_rp_hash = req.rp.hash(); + let eligible_cred_iter = credlist.iter().filter(|x| x.rp.hash() == req_rp_hash); + for credential in eligible_cred_iter { + if req.key_handle == credential.id { + return Ok(()); + } + } + Err(HIDError::DeviceError) + } + + fn client_pin(&self, req: &ClientPIN) -> Result<ClientPinResponse, HIDError> { + match req.subcommand { + PINSubcommand::GetKeyAgreement => { + // We don't need to save, or even know, the private key for the public key returned + // here because we have access to the shared secret derived on the client side. + let (_private, public) = COSEKey::generate(COSEAlgorithm::ECDH_ES_HKDF256) + .map_err(|_| HIDError::DeviceError)?; + Ok(ClientPinResponse { + key_agreement: Some(public), + ..Default::default() + }) + } + PINSubcommand::GetPinUvAuthTokenUsingUvWithPermissions => { + // TODO: permissions + if !self.is_user_consenting || !self.is_user_verified { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::OperationDenied, + None, + ))); + } + let secret = match self.shared_secret.as_ref() { + Some(secret) => secret, + _ => return Err(HIDError::DeviceError), + }; + let encrypted_pin_token = match secret.encrypt(&self.pin_token) { + Ok(token) => token, + _ => return Err(HIDError::DeviceError), + }; + Ok(ClientPinResponse { + pin_token: Some(encrypted_pin_token), + ..Default::default() + }) + } + _ => Err(HIDError::UnsupportedCommand), + } + } + + fn get_assertion(&self, req: &GetAssertion) -> Result<Vec<GetAssertionResult>, HIDError> { + // Algorithm 6.2.2 from CTAP 2.1 + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-makeCred-authnr-alg + + // 1. zero length pinUvAuthParam + // (not implemented) + + // 2. Validate pinUvAuthParam + // Handled by caller + + // 3. Initialize "uv" and "up" bits to false + let mut flags = AuthenticatorDataFlags::empty(); + + // 4. Handle all options + // 4.1 and 4.2 + let effective_uv_opt = + req.options.user_verification.unwrap_or(false) && req.pin_uv_auth_param.is_none(); + + // 4.3 + if effective_uv_opt && !self.has_user_verification { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::InvalidOption, + None, + ))); + } + + // 4.4 rk + // (not implemented, we don't encode it) + + // 4.5 + let effective_up_opt = req.options.user_presence.unwrap_or(true); + + // 5. alwaysUv + // (not implemented) + + // 6. User verification + // TODO: Permissions, (maybe) validate pinUvAuthParam + if self.is_user_verified && (effective_uv_opt || req.pin_uv_auth_param.is_some()) { + flags |= AuthenticatorDataFlags::USER_VERIFIED; + } + + // 7. Locate credentials + let credlist = self.credentials.borrow(); + let req_rp_hash = req.rp.hash(); + let eligible_cred_iter = credlist.iter().filter(|x| x.rp.hash() == req_rp_hash); + + // 8. Set up=true if evidence of user interaction was provided in step 6. + // (not applicable, we use pinUvAuthParam) + + // 9. User presence test + if effective_up_opt { + if self.is_user_consenting { + flags |= AuthenticatorDataFlags::USER_PRESENT; + } else { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::UpRequired, + None, + ))); + } + } + + // 10. Extensions + // (not implemented) + + let mut assertions: Vec<GetAssertionResult> = vec![]; + if !req.allow_list.is_empty() { + // 11. Non-discoverable credential case + // return at most one assertion matching an allowed credential ID + for credential in eligible_cred_iter { + if req.allow_list.iter().any(|x| x.id == credential.id) { + let mut assertion: GetAssertionResponse = + credential.assert(&req.client_data_hash, flags)?; + if req.allow_list.len() == 1 + && self.max_supported_version() == AuthenticatorVersion::FIDO_2_0 + { + // CTAP 2.0 authenticators are allowed to omit the credential ID in the + // response if the allow list contains exactly one entry. This behavior is + // a common source of bugs, e.g. Bug 1864504, so we'll exercise it here. + assertion.credentials = None; + } + assertions.push(GetAssertionResult { + assertion: assertion.into(), + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }); + break; + } + } + } else { + // 12. Discoverable credential case + // return any number of assertions from credentials bound to this RP ID + for credential in eligible_cred_iter.filter(|x| x.is_discoverable_credential) { + let assertion = credential.assert(&req.client_data_hash, flags)?.into(); + assertions.push(GetAssertionResult { + assertion, + attachment: AuthenticatorAttachment::Unknown, + extensions: Default::default(), + }); + } + } + + if assertions.is_empty() { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::NoCredentials, + None, + ))); + } + + Ok(assertions) + } + + fn get_info(&self) -> Result<AuthenticatorInfo, HIDError> { + // This is a CTAP2.1 device with internal user verification support + Ok(AuthenticatorInfo { + versions: self.versions.clone(), + options: AuthenticatorOptions { + platform_device: self.transport == "internal", + resident_key: self.has_resident_key, + pin_uv_auth_token: Some(self.has_user_verification), + user_verification: Some(self.has_user_verification), + ..Default::default() + }, + ..Default::default() + }) + } + + fn get_version(&self, _req: &GetVersion) -> Result<U2FInfo, HIDError> { + Err(HIDError::UnsupportedCommand) + } + + fn make_credentials(&self, req: &MakeCredentials) -> Result<MakeCredentialsResult, HIDError> { + // Algorithm 6.1.2 from CTAP 2.1 + // https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-makeCred-authnr-alg + + // 1. zero length pinUvAuthParam + // (not implemented) + + // 2. Validate pinUvAuthParam + // Handled by caller + + // 3. Validate pubKeyCredParams + if !req + .pub_cred_params + .iter() + .any(|x| x.alg == COSEAlgorithm::ES256) + { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::UnsupportedAlgorithm, + None, + ))); + } + + // 4. initialize "uv" and "up" bits to false + let mut flags = AuthenticatorDataFlags::empty(); + + // 5. process all options + + // 5.1 and 5.2 + let effective_uv_opt = + req.options.user_verification.unwrap_or(false) && req.pin_uv_auth_param.is_none(); + + // 5.3 + if effective_uv_opt && !self.has_user_verification { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::InvalidOption, + None, + ))); + } + + // 5.4 + if req.options.resident_key.unwrap_or(false) && !self.has_resident_key { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::UnsupportedOption, + None, + ))); + } + + // 5.6 and 5.7 + // Nothing to do. We don't provide a way to set up=false. + + // 6. alwaysUv option ID + // (not implemented) + + // 7. and 8. makeCredUvNotRqd option ID + // (not implemented) + + // 9. enterprise attestation + // (not implemented) + + // 11. User verification + // TODO: Permissions, (maybe) validate pinUvAuthParam + if self.is_user_verified { + flags |= AuthenticatorDataFlags::USER_VERIFIED; + } + + // 12. exclude list + // TODO: credProtect + if req.exclude_list.iter().any(|x| self.has_credential(&x.id)) { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::CredentialExcluded, + None, + ))); + } + + // 13. Set up=true if evidence of user interaction was provided in step 11. + // (not applicable, we use pinUvAuthParam) + + // 14. User presence test + if self.is_user_consenting { + flags |= AuthenticatorDataFlags::USER_PRESENT; + } else { + return Err(HIDError::Command(CommandError::StatusCode( + StatusCode::UpRequired, + None, + ))); + } + + // 15. process extensions + let mut extensions = Extension::default(); + if req.extensions.min_pin_length == Some(true) { + // a real authenticator would + // 1) return an actual minimum pin length, and + // 2) check the RP ID against an allowlist before providing any data + extensions.min_pin_length = Some(4); + } + + if extensions.has_some() { + flags |= AuthenticatorDataFlags::EXTENSION_DATA; + } + + // 16. Generate a new credential. + let (private, public) = + COSEKey::generate(COSEAlgorithm::ES256).map_err(|_| HIDError::DeviceError)?; + let counter = 0; + + // 17. and 18. Store credential + // + // All of the credentials that we create are "resident"---we store the private key locally, + // and use a random value for the credential ID. The `req.options.resident_key` field + // determines whether we make the credential "discoverable". + let mut id = [0u8; 32]; + thread_rng().fill_bytes(&mut id); + self.insert_credential( + &id, + &private, + &req.rp, + req.options.resident_key.unwrap_or(false), + &req.user.clone().unwrap_or_default().id, + counter, + ); + + // 19. Generate attestation statement + flags |= AuthenticatorDataFlags::ATTESTED; + + let auth_data = AuthenticatorData { + rp_id_hash: req.rp.hash(), + flags, + counter, + credential_data: Some(AttestedCredentialData { + aaguid: VIRTUAL_TOKEN_AAGUID, + credential_id: id.to_vec(), + credential_public_key: public, + }), + extensions, + }; + + let mut data = auth_data.to_vec(); + data.extend_from_slice(req.client_data_hash.as_ref()); + + let sig = ecdsa_p256_sha256_sign_raw(&private, &data).or(Err(HIDError::DeviceError))?; + + let att_stmt = AttestationStatement::Packed(AttestationStatementPacked { + alg: COSEAlgorithm::ES256, + sig: sig.as_slice().into(), + attestation_cert: vec![], + }); + + let result = MakeCredentialsResult { + attachment: AuthenticatorAttachment::Unknown, + att_obj: AttestationObject { + att_stmt, + auth_data, + }, + extensions: Default::default(), + }; + Ok(result) + } + + fn reset(&self, _req: &Reset) -> Result<(), HIDError> { + Err(HIDError::UnsupportedCommand) + } + + fn selection(&self, _req: &Selection) -> Result<(), HIDError> { + Err(HIDError::UnsupportedCommand) + } +} + +#[xpcom(implement(nsICredentialParameters), atomic)] +struct CredentialParameters { + credential_id: Vec<u8>, + is_resident_credential: bool, + rp_id: String, + private_key: Vec<u8>, + user_handle: Vec<u8>, + sign_count: u32, +} + +impl CredentialParameters { + xpcom_method!(get_credential_id => GetCredentialId() -> nsACString); + fn get_credential_id(&self) -> Result<nsCString, nsresult> { + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(&self.credential_id) + .into()) + } + + xpcom_method!(get_is_resident_credential => GetIsResidentCredential() -> bool); + fn get_is_resident_credential(&self) -> Result<bool, nsresult> { + Ok(self.is_resident_credential) + } + + xpcom_method!(get_rp_id => GetRpId() -> nsACString); + fn get_rp_id(&self) -> Result<nsCString, nsresult> { + Ok(nsCString::from(&self.rp_id)) + } + + xpcom_method!(get_private_key => GetPrivateKey() -> nsACString); + fn get_private_key(&self) -> Result<nsCString, nsresult> { + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(&self.private_key) + .into()) + } + + xpcom_method!(get_user_handle => GetUserHandle() -> nsACString); + fn get_user_handle(&self) -> Result<nsCString, nsresult> { + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(&self.user_handle) + .into()) + } + + xpcom_method!(get_sign_count => GetSignCount() -> u32); + fn get_sign_count(&self) -> Result<u32, nsresult> { + Ok(self.sign_count) + } +} + +#[xpcom(implement(nsIWebAuthnAutoFillEntry), atomic)] +struct WebAuthnAutoFillEntry { + rp: String, + credential_id: Vec<u8>, +} + +impl WebAuthnAutoFillEntry { + xpcom_method!(get_provider => GetProvider() -> u8); + fn get_provider(&self) -> Result<u8, nsresult> { + Ok(nsIWebAuthnAutoFillEntry::PROVIDER_TEST_TOKEN) + } + + xpcom_method!(get_user_name => GetUserName() -> nsAString); + fn get_user_name(&self) -> Result<nsString, nsresult> { + Ok(nsString::from("Test User")) + } + + xpcom_method!(get_rp_id => GetRpId() -> nsAString); + fn get_rp_id(&self) -> Result<nsString, nsresult> { + Ok(nsString::from(&self.rp)) + } + + xpcom_method!(get_credential_id => GetCredentialId() -> ThinVec<u8>); + fn get_credential_id(&self) -> Result<ThinVec<u8>, nsresult> { + Ok(self.credential_id.as_slice().into()) + } +} + +#[derive(Default)] +pub(crate) struct TestTokenManager { + state: Arc<Mutex<HashMap<u64, TestToken>>>, +} + +impl TestTokenManager { + pub fn new() -> Self { + Default::default() + } + + pub fn add_virtual_authenticator( + &self, + protocol: AuthenticatorVersion, + transport: String, + has_resident_key: bool, + has_user_verification: bool, + is_user_consenting: bool, + is_user_verified: bool, + ) -> Result<u64, nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = TestToken::new( + vec![protocol], + transport, + has_resident_key, + has_user_verification, + is_user_consenting, + is_user_verified, + ); + loop { + let id = rand::random::<u64>() & 0x1f_ffff_ffff_ffffu64; // Make the id safe for JS (53 bits) + match guard.deref_mut().entry(id) { + Entry::Occupied(_) => continue, + Entry::Vacant(v) => { + v.insert(token); + return Ok(id); + } + }; + } + } + + pub fn remove_virtual_authenticator(&self, authenticator_id: u64) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + guard + .deref_mut() + .remove(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + Ok(()) + } + + pub fn add_credential( + &self, + authenticator_id: u64, + id: &[u8], + privkey: &[u8], + user_handle: &[u8], + sign_count: u32, + rp_id: String, + is_resident_credential: bool, + ) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .deref_mut() + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + let rp = RelyingParty::from(rp_id); + token.insert_credential( + id, + privkey, + &rp, + is_resident_credential, + user_handle, + sign_count, + ); + Ok(()) + } + + pub fn get_credentials( + &self, + authenticator_id: u64, + ) -> Result<ThinVec<Option<RefPtr<nsICredentialParameters>>>, nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + let credentials = token.get_credentials(); + let mut credentials_parameters = ThinVec::with_capacity(credentials.len()); + for credential in credentials.deref() { + // CTAP1 credentials are not currently supported here. + let credential_parameters = CredentialParameters::allocate(InitCredentialParameters { + credential_id: credential.id.clone(), + is_resident_credential: credential.is_discoverable_credential, + rp_id: credential.rp.id.clone(), + private_key: credential.privkey.clone(), + user_handle: credential.user_handle.clone(), + sign_count: credential.sign_count.load(Ordering::Relaxed), + }) + .query_interface::<nsICredentialParameters>() + .ok_or(NS_ERROR_FAILURE)?; + credentials_parameters.push(Some(credential_parameters)); + } + Ok(credentials_parameters) + } + + pub fn remove_credential(&self, authenticator_id: u64, id: &[u8]) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .deref_mut() + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + if token.delete_credential(id) { + Ok(()) + } else { + Err(NS_ERROR_INVALID_ARG) + } + } + + pub fn remove_all_credentials(&self, authenticator_id: u64) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .deref_mut() + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + token.delete_all_credentials(); + Ok(()) + } + + pub fn set_user_verified( + &self, + authenticator_id: u64, + is_user_verified: bool, + ) -> Result<(), nsresult> { + let mut guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let token = guard + .deref_mut() + .get_mut(&authenticator_id) + .ok_or(NS_ERROR_INVALID_ARG)?; + token.is_user_verified = is_user_verified; + Ok(()) + } + + pub fn register( + &self, + _timeout_ms: u64, + ctap_args: RegisterArgs, + status: Sender<StatusUpdate>, + callback: StateCallback<Result<RegisterResult, AuthenticatorError>>, + ) { + if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return; + } + + let state_obj = self.state.clone(); + + // Registration doesn't currently block, but it might in a future version, so we run it on + // a background thread. + let _ = RunnableBuilder::new("TestTokenManager::register", move || { + // TODO(Bug 1854278) We should actually run one thread per token here + // and attempt to fulfill this request in parallel. + for token in state_obj.lock().unwrap().values_mut() { + let _ = token.init(); + if ctap2::register( + token, + ctap_args.clone(), + status.clone(), + callback.clone(), + &|| true, + ) { + // callback was called + return; + } + } + + // Send an error, if the callback wasn't called already. + callback.call(Err(AuthenticatorError::U2FToken(U2FTokenError::NotAllowed))); + }) + .may_block(true) + .dispatch_background_task(); + } + + pub fn sign( + &self, + _timeout_ms: u64, + ctap_args: SignArgs, + status: Sender<StatusUpdate>, + callback: StateCallback<Result<SignResult, AuthenticatorError>>, + ) { + if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return; + } + + let state_obj = self.state.clone(); + + // Signing can block during signature selection, so we need to run it on a background thread. + let _ = RunnableBuilder::new("TestTokenManager::sign", move || { + // TODO(Bug 1854278) We should actually run one thread per token here + // and attempt to fulfill this request in parallel. + for token in state_obj.lock().unwrap().values_mut() { + let _ = token.init(); + if ctap2::sign( + token, + ctap_args.clone(), + status.clone(), + callback.clone(), + &|| true, + ) { + // callback was called + return; + } + } + + // Send an error, if the callback wasn't called already. + callback.call(Err(AuthenticatorError::U2FToken(U2FTokenError::NotAllowed))); + }) + .may_block(true) + .dispatch_background_task(); + } + + pub fn has_platform_authenticator(&self) -> bool { + if !static_prefs::pref!("security.webauth.webauthn_enable_softtoken") { + return false; + } + + for token in self.state.lock().unwrap().values_mut() { + let _ = token.init(); + if token.transport.as_str() == "internal" { + return true; + } + } + + false + } + + pub fn get_autofill_entries( + &self, + rp_id: &str, + credential_filter: &Vec<PublicKeyCredentialDescriptor>, + ) -> Result<ThinVec<Option<RefPtr<nsIWebAuthnAutoFillEntry>>>, nsresult> { + let guard = self.state.lock().map_err(|_| NS_ERROR_FAILURE)?; + let mut entries = ThinVec::new(); + + for token in guard.values() { + let credentials = token.get_credentials(); + for credential in credentials.deref() { + // The relying party ID must match. + if !rp_id.eq(&credential.rp.id) { + continue; + } + // Only discoverable credentials are admissible. + if !credential.is_discoverable_credential { + continue; + } + // Only credentials listed in the credential filter (if it is + // non-empty) are admissible. + if credential_filter.len() > 0 + && credential_filter + .iter() + .find(|cred| cred.id == credential.id) + .is_none() + { + continue; + } + let entry = WebAuthnAutoFillEntry::allocate(InitWebAuthnAutoFillEntry { + rp: credential.rp.id.clone(), + credential_id: credential.id.clone(), + }) + .query_interface::<nsIWebAuthnAutoFillEntry>() + .ok_or(NS_ERROR_FAILURE)?; + entries.push(Some(entry)); + } + } + Ok(entries) + } +} |