summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/authrs_bridge/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/webauthn/authrs_bridge/src
parentInitial commit. (diff)
downloadfirefox-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.rs166
-rw-r--r--dom/webauthn/authrs_bridge/src/lib.rs1534
-rw-r--r--dom/webauthn/authrs_bridge/src/test_token.rs974
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)
+ }
+}