зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1854618 - Implement about:webauthn page to display authenticator info (r=jschanck,desktop-theme-reviewers,bolsson)
Differential Revision: https://phabricator.services.mozilla.com/D188973
This commit is contained in:
Родитель
b2c34cc36b
Коммит
f4b1edc284
|
@ -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",
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<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)
|
||||
}
|
||||
|
||||
pub(crate) fn interactive_status_callback(
|
||||
status_rx: Receiver<StatusUpdate>,
|
||||
transaction: Arc<Mutex<Option<TransactionState>>>, /* 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(())
|
||||
}
|
|
@ -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<AuthenticatorInfo>,
|
||||
},
|
||||
PinInvalid {
|
||||
retries: Option<u8>,
|
||||
},
|
||||
|
@ -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<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 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::<nsIObserverService>() {
|
||||
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::<StatusUpdate>();
|
||||
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::<Result<ManageResult, AuthenticatorError>>::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::<StatusUpdate>();
|
||||
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]
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<!DOCTYPE html>
|
||||
|
||||
<!-- 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/. -->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src chrome:; style-src chrome:; object-src 'none'"
|
||||
/>
|
||||
<meta charset="utf-8" />
|
||||
<title data-l10n-id="about-webauthn-page-title"></title>
|
||||
<link rel="stylesheet" href="chrome://global/content/aboutWebauthn.css" />
|
||||
<script src="chrome://global/content/aboutWebauthn.js"></script>
|
||||
<link rel="localization" href="toolkit/about/aboutWebauthn.ftl" />
|
||||
</head>
|
||||
|
||||
<body id="body">
|
||||
<div id="info-text-div">
|
||||
<label
|
||||
id="info-text-field"
|
||||
data-l10n-id="about-webauthn-text-connect-device"
|
||||
></label>
|
||||
</div>
|
||||
|
||||
<div hidden id="categories">
|
||||
<div id="info-tab-button" class="category" selected="true">
|
||||
<span
|
||||
class="tablinks"
|
||||
data-l10n-id="about-webauthn-info-section-title"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden id="main-content">
|
||||
<div hidden class="tabcontent token-info-section" id="token-info-section">
|
||||
<h2
|
||||
class="categoryTitle"
|
||||
data-l10n-id="about-webauthn-info-section-title"
|
||||
></h2>
|
||||
<div id="ctap2-token-info" class="token-info-flex-box" display="flex">
|
||||
<div id="ctap2-token-info-options" class="token-info-flex-child">
|
||||
<h3 data-l10n-id="about-webauthn-options-subsection-title"></h3>
|
||||
<table id="authenticator-options"></table>
|
||||
</div>
|
||||
<div id="ctap2-token-info-info" class="token-info-flex-child">
|
||||
<h3 data-l10n-id="about-webauthn-info-subsection-title"></h3>
|
||||
<table id="authenticator-info"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -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 <tr> element and add it to the 1st position of the table:
|
||||
var row = table.insertRow(0);
|
||||
|
||||
// Insert new cells (<td> elements) at the 1st and 2nd position of the "new" <tr> 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);
|
||||
}
|
|
@ -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)
|
|
@ -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"]
|
|
@ -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]
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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
|
Загрузка…
Ссылка в новой задаче