From f4b1edc284d93b625f9fde9ab9b5975fb950dc56 Mon Sep 17 00:00:00 2001 From: "M. Sirringhaus" Date: Tue, 31 Oct 2023 21:34:34 +0000 Subject: [PATCH] Bug 1854618 - Implement about:webauthn page to display authenticator info (r=jschanck,desktop-theme-reviewers,bolsson) Differential Revision: https://phabricator.services.mozilla.com/D188973 --- .../static/browser_all_files_referenced.js | 5 + docshell/base/nsAboutRedirector.cpp | 4 + docshell/build/components.conf | 2 + dom/webauthn/AndroidWebAuthnService.cpp | 3 + dom/webauthn/WebAuthnService.cpp | 8 + .../src/about_webauthn_controller.rs | 49 ++++ dom/webauthn/authrs_bridge/src/lib.rs | 134 +++++++++-- dom/webauthn/nsIWebAuthnService.idl | 3 + .../aboutwebauthn/content/aboutWebauthn.css | 61 +++++ .../aboutwebauthn/content/aboutWebauthn.html | 56 +++++ .../aboutwebauthn/content/aboutWebauthn.js | 142 ++++++++++++ toolkit/components/aboutwebauthn/jar.mn | 8 + toolkit/components/aboutwebauthn/moz.build | 13 ++ .../aboutwebauthn/tests/browser/browser.ini | 11 + .../browser/browser_aboutwebauthn_info.js | 218 ++++++++++++++++++ .../browser/browser_aboutwebauthn_no_token.js | 49 ++++ .../aboutwebauthn/tests/browser/head.js | 11 + toolkit/components/moz.build | 3 + .../en-US/toolkit/about/aboutWebauthn.ftl | 87 +++++++ 19 files changed, 852 insertions(+), 15 deletions(-) create mode 100644 dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs create mode 100644 toolkit/components/aboutwebauthn/content/aboutWebauthn.css create mode 100644 toolkit/components/aboutwebauthn/content/aboutWebauthn.html create mode 100644 toolkit/components/aboutwebauthn/content/aboutWebauthn.js create mode 100644 toolkit/components/aboutwebauthn/jar.mn create mode 100644 toolkit/components/aboutwebauthn/moz.build create mode 100644 toolkit/components/aboutwebauthn/tests/browser/browser.ini create mode 100644 toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js create mode 100644 toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js create mode 100644 toolkit/components/aboutwebauthn/tests/browser/head.js create mode 100644 toolkit/locales/en-US/toolkit/about/aboutWebauthn.ftl diff --git a/browser/base/content/test/static/browser_all_files_referenced.js b/browser/base/content/test/static/browser_all_files_referenced.js index 08aca68e0156..103220fb09d9 100644 --- a/browser/base/content/test/static/browser_all_files_referenced.js +++ b/browser/base/content/test/static/browser_all_files_referenced.js @@ -263,6 +263,11 @@ var allowlist = [ file: "resource://gre/localization/en-US/toolkit/about/aboutThirdParty.ftl", platforms: ["linux", "macosx"], }, + // Bug 1854618 - referenced by aboutWebauthn.html which is only for Linux and Mac + { + file: "resource://gre/localization/en-US/toolkit/about/aboutWebauthn.ftl", + platforms: ["win", "android"], + }, // Bug 1973834 - referenced by aboutWindowsMessages.html which is only for Windows { file: "resource://gre/localization/en-US/toolkit/about/aboutWindowsMessages.ftl", diff --git a/docshell/base/nsAboutRedirector.cpp b/docshell/base/nsAboutRedirector.cpp index 435c9f6275e6..2ca6b97142eb 100644 --- a/docshell/base/nsAboutRedirector.cpp +++ b/docshell/base/nsAboutRedirector.cpp @@ -131,6 +131,10 @@ static const RedirEntry kRedirMap[] = { nsIAboutModule::IS_SECURE_CHROME_UI}, {"mozilla", "chrome://global/content/mozilla.html", nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT}, +#if !defined(ANDROID) && !defined(XP_WIN) + {"webauthn", "chrome://global/content/aboutWebauthn.html", + nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::IS_SECURE_CHROME_UI}, +#endif {"neterror", "chrome://global/content/aboutNetError.xhtml", nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT | nsIAboutModule::URI_CAN_LOAD_IN_CHILD | nsIAboutModule::ALLOW_SCRIPT | diff --git a/docshell/build/components.conf b/docshell/build/components.conf index 7f7e2c243440..691277a0bb5d 100644 --- a/docshell/build/components.conf +++ b/docshell/build/components.conf @@ -44,6 +44,8 @@ if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] == 'windows': about_pages.append('windows-messages') if not defined('MOZ_GLEAN_ANDROID'): about_pages.append('glean') +if buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'android' and buildconfig.substs['MOZ_WIDGET_TOOLKIT'] != 'windows': + about_pages.append('webauthn') Headers = ['/docshell/build/nsDocShellModule.h'] diff --git a/dom/webauthn/AndroidWebAuthnService.cpp b/dom/webauthn/AndroidWebAuthnService.cpp index deec2d5afbc0..a76bdbc859f4 100644 --- a/dom/webauthn/AndroidWebAuthnService.cpp +++ b/dom/webauthn/AndroidWebAuthnService.cpp @@ -369,5 +369,8 @@ AndroidWebAuthnService::SetUserVerified(uint64_t authenticatorId, return NS_ERROR_NOT_IMPLEMENTED; } +NS_IMETHODIMP +AndroidWebAuthnService::Listen() { return NS_ERROR_NOT_IMPLEMENTED; } + } // namespace dom } // namespace mozilla diff --git a/dom/webauthn/WebAuthnService.cpp b/dom/webauthn/WebAuthnService.cpp index de0a9556d058..115907d8dec0 100644 --- a/dom/webauthn/WebAuthnService.cpp +++ b/dom/webauthn/WebAuthnService.cpp @@ -160,4 +160,12 @@ WebAuthnService::SetUserVerified(uint64_t authenticatorId, return mPlatformService->SetUserVerified(authenticatorId, isUserVerified); } +NS_IMETHODIMP +WebAuthnService::Listen() { + if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) { + return mTestService->Listen(); + } + return mPlatformService->Listen(); +} + } // namespace mozilla::dom 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 000000000000..1c31c2114e77 --- /dev/null +++ b/dom/webauthn/authrs_bridge/src/about_webauthn_controller.rs @@ -0,0 +1,49 @@ +/* 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::{InteractiveRequest, InteractiveUpdate}; + +pub(crate) type InteractiveManagementReceiver = Option>; +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) +} + +pub(crate) fn interactive_status_callback( + status_rx: Receiver, + transaction: Arc>>, /* Shared with an AuthrsTransport */ +) -> 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::SelectDeviceNotice) => { + info!("STATUS: Please select a device by touching one of them."); + let prompt = BrowserPromptType::SelectDevice; + send_about_prompt(&prompt)?; + } + 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 index ed2afcb6ae81..98d59c387994 100644 --- a/dom/webauthn/authrs_bridge/src/lib.rs +++ b/dom/webauthn/authrs_bridge/src/lib.rs @@ -20,7 +20,8 @@ use authenticator::{ }, errors::AuthenticatorError, statecallback::StateCallback, - Pin, RegisterResult, SignResult, StateMachine, StatusPinUv, StatusUpdate, + AuthenticatorInfo, ManageResult, Pin, RegisterResult, SignResult, StateMachine, StatusPinUv, + StatusUpdate, }; use base64::Engine; use cstr::cstr; @@ -44,7 +45,8 @@ use xpcom::interfaces::{ nsIWebAuthnSignPromise, nsIWebAuthnSignResult, }; use xpcom::{xpcom_method, RefPtr}; - +mod about_webauthn_controller; +use about_webauthn_controller::*; mod test_token; use test_token::TestTokenManager; @@ -77,6 +79,9 @@ enum BrowserPromptType<'a> { SelectDevice, UvBlocked, PinRequired, + SelectedDevice { + auth_info: Option, + }, PinInvalid { retries: Option, }, @@ -87,6 +92,14 @@ enum BrowserPromptType<'a> { SelectSignResult { entities: &'a [PublicKeyCredentialUserEntity], }, + ListenSuccess, + ListenError, +} + +#[derive(Debug)] +enum PromptTarget { + Browser, + AboutPage, } #[derive(Serialize)] @@ -98,13 +111,29 @@ struct BrowserPromptMessage<'a> { browsing_context_id: Option, } +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::() { + 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, ) -> Result<(), nsresult> { - let main_thread = get_main_thread()?; let mut json = nsString::new(); write!( json, @@ -117,18 +146,7 @@ fn send_prompt( }) ) .or(Err(NS_ERROR_FAILURE))?; - RunnableBuilder::new("AuthrsService::send_prompt", move || { - if let Ok(obs_svc) = xpcom::components::Observer::service::() { - unsafe { - obs_svc.NotifyObservers( - std::ptr::null(), - cstr!("webauthn-prompt").as_ptr(), - json.as_ptr(), - ); - } - } - }) - .dispatch(main_thread.coerce()) + notify_observers(PromptTarget::Browser, json) } fn cancel_prompts(tid: u64) -> Result<(), nsresult> { @@ -499,6 +517,7 @@ impl SignPromise { #[derive(Clone)] enum TransactionPromise { + Listen, Register(RegisterPromise), Sign(SignPromise), } @@ -506,6 +525,7 @@ enum TransactionPromise { 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)), } @@ -525,6 +545,7 @@ struct TransactionState { promise: TransactionPromise, pin_receiver: PinReceiver, selection_receiver: SelectionReceiver, + interactive_receiver: InteractiveManagementReceiver, } // AuthrsService provides an nsIWebAuthnService built on top of authenticator-rs. @@ -717,6 +738,7 @@ impl AuthrsService { browsing_context_id, pending_args: Some(TransactionArgs::Register(timeout_ms as u64, info)), promise: TransactionPromise::Register(promise), + interactive_receiver: None, pin_receiver: None, selection_receiver: None, }); @@ -752,6 +774,10 @@ impl AuthrsService { 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::(); let status_transaction = self.transaction.clone(); @@ -803,6 +829,7 @@ impl AuthrsService { let _ = cancel_prompts(tid); } let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror)); + *guard = None; }), ); @@ -935,6 +962,7 @@ impl AuthrsService { let _ = cancel_prompts(tid); } let _ = promise.resolve_or_reject(result.map_err(authrs_to_nserror)); + *guard = None; }), ); @@ -966,6 +994,7 @@ impl AuthrsService { browsing_context_id, pending_args: None, promise: TransactionPromise::Sign(promise), + interactive_receiver: None, pin_receiver: None, selection_receiver: None, }); @@ -1143,6 +1172,81 @@ impl AuthrsService { 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, + interactive_receiver: None, + pin_receiver: None, + selection_receiver: None, + }); + } + + let callback_transaction = self.transaction.clone(); + let state_callback = StateCallback::>::new( + Box::new(move |result| { + let mut guard = callback_transaction.lock().unwrap(); + let Some(state) = guard.as_mut() else { + return; + }; + match state.promise { + TransactionPromise::Listen => (), + _ => return, + } + *guard = None; + let msg = match result { + Ok(_) => BrowserPromptType::ListenSuccess, + Err(_) => BrowserPromptType::ListenError, + }; + 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::(); + let status_transaction = self.transaction.clone(); + RunnableBuilder::new( + "AuthrsTransport::AboutWebauthn::StatusReceiver", + move || { + let _ = interactive_status_callback(status_rx, status_transaction); + }, + ) + .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(()) + } } #[no_mangle] diff --git a/dom/webauthn/nsIWebAuthnService.idl b/dom/webauthn/nsIWebAuthnService.idl index 65f4a6ddd18b..dba4aad9258f 100644 --- a/dom/webauthn/nsIWebAuthnService.idl +++ b/dom/webauthn/nsIWebAuthnService.idl @@ -90,4 +90,7 @@ interface nsIWebAuthnService : nsISupports // Sets the "isUserVerified" bit on a virtual authenticator. See // https://w3c.github.io/webauthn/#sctn-automation-set-user-verified void setUserVerified(in uint64_t authenticatorId, in bool isUserVerified); + + // about:webauthn-specific functions + void listen(); }; diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.css b/toolkit/components/aboutwebauthn/content/aboutWebauthn.css new file mode 100644 index 000000000000..ea5bca974972 --- /dev/null +++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.css @@ -0,0 +1,61 @@ +/* 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/. */ + +@import url("chrome://global/skin/in-content/common.css"); + +html { + height: 100%; +} + +body { + display: flex; + align-items: stretch; + height: 100%; +} + +label { + display: block; +} + +#info-text-div { + padding: 20px; +} + +#ctap-listen-div { + padding-top: 15px; +} + +.category { + cursor: pointer; + /* Center category names */ + display: flex; + align-items: center; +} + +.disabled-category { + pointer-events: none; +} + +@media (max-width: 830px){ + #categories > .category { + padding-inline-start: 5px; + margin-inline-start: 0; + } +} + +#main-content { + flex: 1; +} + +.token-info-flex-box { + display: flex; +} + +.token-info-flex-child { + flex: 1; +} + +.token-info-flex-child#authenticator-options { + margin-inline-end: 2px; +} diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.html b/toolkit/components/aboutwebauthn/content/aboutWebauthn.html new file mode 100644 index 000000000000..84de6f414781 --- /dev/null +++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + +
+ +
+ + + + + + diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.js b/toolkit/components/aboutwebauthn/content/aboutWebauthn.js new file mode 100644 index 000000000000..7a318d75b488 --- /dev/null +++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.js @@ -0,0 +1,142 @@ +/* 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 strict"; + +let AboutWebauthnService = null; + +var AboutWebauthnManagerJS = { + _topic: "about-webauthn-prompt", + _initialized: false, + _l10n: null, + + init() { + if (this._initialized) { + return; + } + this._l10n = new Localization(["toolkit/about/aboutWebauthn.ftl"], true); + Services.obs.addObserver(this, this._topic); + this._initialized = true; + reset_page(); + }, + + uninit() { + Services.obs.removeObserver(this, this._topic); + }, + + observe(aSubject, aTopic, aData) { + let data = JSON.parse(aData); + + // We have token + if (data.type == "selected-device") { + this._curr_data = data.auth_info; + document.getElementById("token-info-section").style.display = "block"; + this.show_ui_based_on_authenticator_info(data); + } else if (data.type == "select-device") { + set_info_text("about-webauthn-text-select-device"); + } else if (data.type == "listen-success" || data.type == "listen-error") { + reset_page(); + AboutWebauthnService.listen(); + } + }, + + show_authenticator_options(options, element, l10n_base) { + let table = document.getElementById(element); + var empty_table = document.createElement("table"); + empty_table.id = element; + table.parentNode.replaceChild(empty_table, table); + table = document.getElementById(element); + for (let key in options) { + if (key == "options") { + continue; + } + // Create an empty element and add it to the 1st position of the table: + var row = table.insertRow(0); + + // Insert new cells ( elements) at the 1st and 2nd position of the "new" element: + var cell1 = row.insertCell(0); + var cell2 = row.insertCell(1); + + // Add some text to the new cells: + let key_text = this._l10n.formatValueSync( + l10n_base + "-" + key.toLowerCase().replace(/_/g, "-") + ); + var key_node = document.createTextNode(key_text); + cell1.appendChild(key_node); + var raw_value = JSON.stringify(options[key]); + var value = raw_value; + if (["true", "false", "null"].includes(raw_value)) { + value = this._l10n.formatValueSync(l10n_base + "-" + raw_value); + } + var value_node = document.createTextNode(value); + cell2.appendChild(value_node); + } + }, + + show_ui_based_on_authenticator_info(data) { + // Hide the "Please plug in a token"-message + document.getElementById("info-text-div").hidden = true; + // Show options, based on what the token supports + if (data.auth_info) { + document.getElementById("main-content").hidden = false; + document.getElementById("categories").hidden = false; + this.show_authenticator_options( + data.auth_info.options, + "authenticator-options", + "about-webauthn-auth-option" + ); + this.show_authenticator_options( + data.auth_info, + "authenticator-info", + "about-webauthn-auth-info" + ); + } else { + // Currently auth-rs doesn't send this, because it filters out ctap2-devices. + // U2F / CTAP1 tokens can't be managed + set_info_text("about-webauthn-text-non-ctap2-device"); + } + }, +}; + +function set_info_text(l10nId) { + document.getElementById("info-text-div").hidden = false; + let field = document.getElementById("info-text-field"); + field.setAttribute("data-l10n-id", l10nId); +} + +function reset_page() { + // Hide all main sections + document.getElementById("main-content").hidden = true; + document.getElementById("categories").hidden = true; + + // Only display the "please connect a device" - text + set_info_text("about-webauthn-text-connect-device"); +} + +async function onLoad() { + AboutWebauthnManagerJS.init(); + try { + AboutWebauthnService.listen(); + } catch (ex) { + set_info_text("about-webauthn-text-not-available"); + AboutWebauthnManagerJS.uninit(); + } +} + +try { + AboutWebauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + document.addEventListener("DOMContentLoaded", onLoad); + window.addEventListener("beforeunload", event => { + AboutWebauthnManagerJS.uninit(); + if (AboutWebauthnService) { + AboutWebauthnService.cancel(0); + } + }); +} catch (ex) { + // Do nothing if we fail to create a singleton instance, + // showing the default no-module message. + console.error(ex); +} diff --git a/toolkit/components/aboutwebauthn/jar.mn b/toolkit/components/aboutwebauthn/jar.mn new file mode 100644 index 000000000000..7478168bbd2c --- /dev/null +++ b/toolkit/components/aboutwebauthn/jar.mn @@ -0,0 +1,8 @@ +# 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/. + +toolkit.jar: + content/global/aboutWebauthn.css (content/aboutWebauthn.css) + content/global/aboutWebauthn.html (content/aboutWebauthn.html) + content/global/aboutWebauthn.js (content/aboutWebauthn.js) \ No newline at end of file diff --git a/toolkit/components/aboutwebauthn/moz.build b/toolkit/components/aboutwebauthn/moz.build new file mode 100644 index 000000000000..9ce8ce8e22d0 --- /dev/null +++ b/toolkit/components/aboutwebauthn/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Web Authentication") + +if CONFIG["MOZ_WIDGET_TOOLKIT"] not in ("windows", "android"): + BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser.ini b/toolkit/components/aboutwebauthn/tests/browser/browser.ini new file mode 100644 index 000000000000..cbc37a3b09f9 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +head = head.js +prefs = + security.webauth.webauthn=true + security.webauth.webauthn_enable_softtoken=false + security.webauth.webauthn_enable_android_fido2=false + security.webauth.webauthn_enable_usbtoken=false + security.webauthn.ctap2=true + +[browser_aboutwebauthn_info.js] +[browser_aboutwebauthn_no_token.js] diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js new file mode 100644 index 000000000000..f66f5929fc74 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js @@ -0,0 +1,218 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var doc, tab; + +add_setup(async function () { + info("Starting about:webauthn"); + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:webauthn", + waitForLoad: true, + }); + + doc = tab.linkedBrowser.contentDocument; +}); + +registerCleanupFunction(async function () { + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +function send_auth_data_and_check(auth_data) { + Services.obs.notifyObservers( + null, + "about-webauthn-prompt", + JSON.stringify({ type: "selected-device", auth_info: auth_data }) + ); + + let info_text = doc.getElementById("info-text-div"); + is(info_text.hidden, true, "Start prompt not hidden"); + + let info_section = doc.getElementById("token-info-section"); + isnot(info_section.style.display, "none", "Info section hidden"); +} + +add_task(async function multiple_devices() { + Services.obs.notifyObservers( + null, + "about-webauthn-prompt", + JSON.stringify({ type: "select-device" }) + ); + + let info_text = doc.getElementById("info-text-div"); + is(info_text.hidden, false, "Start prompt hidden"); + let field = doc.getElementById("info-text-field"); + is( + field.getAttribute("data-l10n-id"), + "about-webauthn-text-select-device", + "Field does not prompt user to touch device for selection" + ); +}); + +add_task(async function multiple_devices() { + send_auth_data_and_check(REAL_AUTH_INFO_1); + reset_about_page(doc); + send_auth_data_and_check(REAL_AUTH_INFO_2); + reset_about_page(doc); + send_auth_data_and_check(REAL_AUTH_INFO_3); + reset_about_page(doc); +}); + +// Yubikey BIO +const REAL_AUTH_INFO_1 = { + versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE", "FIDO_2_1"], + extensions: [ + "credProtect", + "hmac-secret", + "largeBlobKey", + "credBlob", + "minPinLength", + ], + aaguid: [ + 216, 82, 45, 159, 87, 91, 72, 102, 136, 169, 186, 153, 250, 2, 243, 91, + ], + options: { + plat: false, + rk: true, + clientPin: true, + up: true, + uv: true, + pinUvAuthToken: true, + noMcGaPermissionsWithClientPin: null, + largeBlobs: true, + ep: null, + bioEnroll: true, + userVerificationMgmtPreview: true, + uvBioEnroll: null, + authnrCfg: true, + uvAcfg: null, + credMgmt: true, + credentialMgmtPreview: true, + setMinPINLength: true, + makeCredUvNotRqd: true, + alwaysUv: false, + }, + max_msg_size: 1200, + pin_protocols: [2, 1], + max_credential_count_in_list: 8, + max_credential_id_length: 128, + transports: ["usb"], + algorithms: [ + { alg: -7, type: "public-key" }, + { alg: -8, type: "public-key" }, + ], + max_ser_large_blob_array: 1024, + force_pin_change: false, + min_pin_length: 4, + firmware_version: 328966, + max_cred_blob_length: 32, + max_rpids_for_set_min_pin_length: 1, + preferred_platform_uv_attempts: 3, + uv_modality: 2, + certifications: null, + remaining_discoverable_credentials: 20, + vendor_prototype_config_commands: null, +}; + +// Yubikey 5 +const REAL_AUTH_INFO_2 = { + versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"], + extensions: ["credProtect", "hmac-secret"], + aaguid: [ + 47, 192, 87, 159, 129, 19, 71, 234, 177, 22, 187, 90, 141, 185, 32, 42, + ], + options: { + plat: false, + rk: true, + clientPin: true, + up: true, + uv: null, + pinUvAuthToken: null, + noMcGaPermissionsWithClientPin: null, + largeBlobs: null, + ep: null, + bioEnroll: null, + userVerificationMgmtPreview: null, + uvBioEnroll: null, + authnrCfg: null, + uvAcfg: null, + credMgmt: null, + credentialMgmtPreview: true, + setMinPINLength: null, + makeCredUvNotRqd: null, + alwaysUv: null, + }, + max_msg_size: 1200, + pin_protocols: [1], + max_credential_count_in_list: 8, + max_credential_id_length: 128, + transports: ["nfc", "usb"], + algorithms: [ + { alg: -7, type: "public-key" }, + { alg: -8, type: "public-key" }, + ], + max_ser_large_blob_array: null, + force_pin_change: null, + min_pin_length: null, + firmware_version: null, + max_cred_blob_length: null, + max_rpids_for_set_min_pin_length: null, + preferred_platform_uv_attempts: null, + uv_modality: null, + certifications: null, + remaining_discoverable_credentials: null, + vendor_prototype_config_commands: null, +}; + +// Nitrokey 3 +const REAL_AUTH_INFO_3 = { + versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"], + extensions: ["credProtect", "hmac-secret"], + aaguid: [ + 47, 192, 87, 159, 129, 19, 71, 234, 177, 22, 187, 90, 141, 185, 32, 42, + ], + options: { + plat: false, + rk: true, + clientPin: true, + up: true, + uv: null, + pinUvAuthToken: null, + noMcGaPermissionsWithClientPin: null, + largeBlobs: null, + ep: null, + bioEnroll: null, + userVerificationMgmtPreview: null, + uvBioEnroll: null, + authnrCfg: null, + uvAcfg: null, + credMgmt: null, + credentialMgmtPreview: true, + setMinPINLength: null, + makeCredUvNotRqd: null, + alwaysUv: null, + }, + max_msg_size: 1200, + pin_protocols: [1], + max_credential_count_in_list: 8, + max_credential_id_length: 128, + transports: ["nfc", "usb"], + algorithms: [ + { alg: -7, type: "public-key" }, + { alg: -8, type: "public-key" }, + ], + max_ser_large_blob_array: null, + force_pin_change: null, + min_pin_length: null, + firmware_version: null, + max_cred_blob_length: null, + max_rpids_for_set_min_pin_length: null, + preferred_platform_uv_attempts: null, + uv_modality: null, + certifications: null, + remaining_discoverable_credentials: null, + vendor_prototype_config_commands: null, +}; diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js new file mode 100644 index 000000000000..3d6359f91216 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var doc, tab; + +add_setup(async function () { + info("Starting about:webauthn"); + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:webauthn", + waitForLoad: true, + }); + + doc = tab.linkedBrowser.contentDocument; +}); + +registerCleanupFunction(async function () { + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function verify_page_no_token() { + let info_text = doc.getElementById("info-text-div"); + is(info_text.hidden, false, "info-text-div should be visible"); + let categories = doc.getElementById("categories"); + is(categories.hidden, true, "categories-sidebar should be invisible"); +}); + +add_task(async function verify_no_auth_info() { + let field = doc.getElementById("info-text-field"); + let promise = BrowserTestUtils.waitForMutationCondition( + field, + { attributes: true, attributeFilter: ["data-l10n-id"] }, + () => + field.getAttribute("data-l10n-id") === + "about-webauthn-text-non-ctap2-device" + ); + Services.obs.notifyObservers( + null, + "about-webauthn-prompt", + JSON.stringify({ type: "selected-device", auth_info: null }) + ); + await promise; + + let info_text = doc.getElementById("info-text-div"); + is(info_text.hidden, false); +}); diff --git a/toolkit/components/aboutwebauthn/tests/browser/head.js b/toolkit/components/aboutwebauthn/tests/browser/head.js new file mode 100644 index 000000000000..640252291d37 --- /dev/null +++ b/toolkit/components/aboutwebauthn/tests/browser/head.js @@ -0,0 +1,11 @@ +async function reset_about_page(doc) { + let info_text = doc.getElementById("info-text-div"); + let msg = JSON.stringify({ action: "listen-finished-success" }); + let promise = BrowserTestUtils.waitForMutationCondition( + info_text, + { attributes: true, attributeFilter: ["hidden"] }, + () => info_text.hidden !== false + ); + Services.obs.notifyObservers(null, "about-webauthn-prompt", msg); + await promise; +} diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build index ca8742d393f0..5dc461d3af39 100644 --- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -124,6 +124,9 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": "components.conf", ] +if CONFIG["MOZ_WIDGET_TOOLKIT"] not in ("android", "windows"): + DIRS += ["aboutwebauthn"] + if CONFIG["MOZ_BUILD_APP"] == "browser": DIRS += ["normandy", "messaging-system"] diff --git a/toolkit/locales/en-US/toolkit/about/aboutWebauthn.ftl b/toolkit/locales/en-US/toolkit/about/aboutWebauthn.ftl new file mode 100644 index 000000000000..822d66d2505e --- /dev/null +++ b/toolkit/locales/en-US/toolkit/about/aboutWebauthn.ftl @@ -0,0 +1,87 @@ +# 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/. + +### Localization for about:webauthn, a security token management page + +# Page title +# 'WebAuthn' is a protocol name and should not be translated +about-webauthn-page-title = About WebAuthn + +## Section titles + +about-webauthn-info-section-title = Device info +about-webauthn-info-subsection-title = Authenticator info +about-webauthn-options-subsection-title = Authenticator options + +## Info field texts + +about-webauthn-text-connect-device = Please connect a security token. +# If multiple devices are plugged in, they will blink and we are asking the user to select one by touching the device they want. +about-webauthn-text-select-device = Please select your desired security token by touching the device. +# CTAP2 refers to Client to Authenticator Protocol version 2 +about-webauthn-text-non-ctap2-device = Unable to manage options because your security token does not support CTAP2. +about-webauthn-text-not-available = Not available on this platform. + +## Authenticator options fields +## Option fields correspond to the CTAP2 option IDs and definitions found in https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#option-id + +about-webauthn-auth-option-uv = User verification +about-webauthn-auth-option-up = User presence +about-webauthn-auth-option-clientpin = Client PIN +about-webauthn-auth-option-rk = Resident key +about-webauthn-auth-option-plat = Platform device +# pinUvAuthToken should not be translated. +about-webauthn-auth-option-pinuvauthtoken = Command permissions (pinUvAuthToken) +# MakeCredential and GetAssertion should not be translated. +about-webauthn-auth-option-nomcgapermissionswithclientpin = No MakeCredential / GetAssertion permissions with client PIN +about-webauthn-auth-option-largeblobs = Large blobs +about-webauthn-auth-option-ep = Enterprise attestation +about-webauthn-auth-option-bioenroll = Biometric enrollment +# FIDO_2_1_PRE should not be translated. +about-webauthn-auth-option-userverificationmgmtpreview = Prototype of biometric enrollment (FIDO_2_1_PRE) +about-webauthn-auth-option-uvbioenroll = Biometric enrollment permission +about-webauthn-auth-option-authnrcfg = Authenticator config +about-webauthn-auth-option-uvacfg = Authenticator config permission +about-webauthn-auth-option-credmgmt = Credential management +about-webauthn-auth-option-credentialmgmtpreview = Prototype credential management +about-webauthn-auth-option-setminpinlength = Set minimum PIN length +# MakeCredential should not be translated. +about-webauthn-auth-option-makecreduvnotrqd = MakeCredential without user verification +about-webauthn-auth-option-alwaysuv = Always require user verification +# Shows when boolean value for an option is True. True should not be translated. +about-webauthn-auth-option-true = True +# Shows when boolean value of an option is False. False should not be translated. +about-webauthn-auth-option-false = False +# If the value is missing (null), it means a certain feature is not supported. +about-webauthn-auth-option-null = Not supported + +## Authenticator info fields +## Info fields correspond to the CTAP2 authenticatorGetInfo field member name and definitions found in https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#authenticatorGetInfo + +about-webauthn-auth-info-vendor-prototype-config-commands = Vendor prototype config commands +about-webauthn-auth-info-remaining-discoverable-credentials = Remaining discoverable credentials +about-webauthn-auth-info-certifications = Certifications +about-webauthn-auth-info-uv-modality = User verification modality +about-webauthn-auth-info-preferred-platform-uv-attempts = Preferred platform user verification attempts +about-webauthn-auth-info-max-rpids-for-set-min-pin-length = Max relying party IDs for set minimum PIN length +about-webauthn-auth-info-max-cred-blob-length = Max credential blob length +about-webauthn-auth-info-firmware-version = Firmware version +about-webauthn-auth-info-min-pin-length = Minimum PIN length +about-webauthn-auth-info-force-pin-change = Force PIN change +about-webauthn-auth-info-max-ser-large-blob-array = Max size of large blob array +about-webauthn-auth-info-algorithms = Algorithms +about-webauthn-auth-info-transports = Transports +about-webauthn-auth-info-max-credential-id-length = Max credential ID length +about-webauthn-auth-info-max-credential-count-in-list = Max credential count in list +about-webauthn-auth-info-pin-protocols = PIN protocols +about-webauthn-auth-info-max-msg-size = Max message size +# AAGUID should not be translated. +about-webauthn-auth-info-aaguid = AAGUID +about-webauthn-auth-info-extensions = Extensions +about-webauthn-auth-info-versions = Versions +# Shows when boolean value for an info field is True. True should not be translated. +about-webauthn-auth-info-true = True +# Shows when boolean value for an info field is False. False should not be translated. +about-webauthn-auth-info-false = False +about-webauthn-auth-info-null = Not supported