/* 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";
XPCOMUtils.defineLazyScriptGetter(
this,
[
"FullScreen"],
"chrome://browser/content/browser-fullScreenAndPointerLock.js"
);
const TEST_URL =
"https://example.com/";
var gAuthenticatorId;
/**
* Waits for the PopupNotifications button enable delay to expire so the
* Notification can be interacted with using the buttons.
*/
async
function waitForPopupNotificationSecurityDelay() {
let notification = PopupNotifications.panel.firstChild.notification;
let notificationEnableDelayMS = Services.prefs.getIntPref(
"security.notification_enable_delay"
);
await TestUtils.waitForCondition(
() => {
let timeSinceShown = Cu.now() - notification.timeShown;
return timeSinceShown > notificationEnableDelayMS;
},
"Wait for security delay to expire",
500,
50
);
}
add_task(async
function test_setup_usbtoken() {
return SpecialPowers.pushPrefEnv({
set: [
[
"security.webauth.webauthn_enable_softtoken",
false],
[
"security.webauth.webauthn_enable_usbtoken",
true],
],
});
});
add_task(test_register);
add_task(test_register_escape);
add_task(test_sign);
add_task(test_sign_escape);
add_task(test_tab_switching);
add_task(test_window_switching);
add_task(async
function test_setup_softtoken() {
gAuthenticatorId = add_virtual_authenticator();
return SpecialPowers.pushPrefEnv({
set: [
[
"browser.fullscreen.autohide",
true],
[
"full-screen-api.enabled",
true],
[
"full-screen-api.allow-trusted-requests-only",
false],
[
"security.webauth.webauthn_enable_softtoken",
true],
[
"security.webauth.webauthn_enable_usbtoken",
false],
],
});
});
add_task(test_fullscreen_show_nav_toolbar);
add_task(test_no_fullscreen_dom);
add_task(test_register_direct_with_consent);
add_task(test_register_direct_without_consent);
add_task(test_select_sign_result);
function promiseNavToolboxStatus(aExpectedStatus) {
let navToolboxStatus;
return TestUtils.topicObserved(
"fullscreen-nav-toolbox", (subject, data) => {
navToolboxStatus = data;
return data == aExpectedStatus;
}).then(() =>
Assert.equal(
navToolboxStatus,
aExpectedStatus,
"nav toolbox is " + aExpectedStatus
)
);
}
function promiseFullScreenPaint(aExpectedStatus) {
return TestUtils.topicObserved(
"fullscreen-painted");
}
function triggerMainPopupCommand(popup) {
info(
"triggering main command");
let notifications = popup.childNodes;
ok(notifications.length,
"at least one notification displayed");
let notification = notifications[0];
info(
"triggering command: " + notification.getAttribute(
"buttonlabel"));
return EventUtils.synthesizeMouseAtCenter(notification.button, {});
}
let expectNotAllowedError = expectError(
"NotAllowed");
function verifyAnonymizedCertificate(aResult) {
return webAuthnDecodeCBORAttestation(aResult.attObj).then(
({ fmt, attStmt }) => {
is(fmt,
"none",
"Is a None Attestation");
is(
typeof attStmt,
"object",
"attStmt is a map");
is(Object.keys(attStmt).length, 0,
"attStmt is empty");
}
);
}
async
function verifyDirectCertificate(aResult) {
let clientDataHash = await crypto.subtle
.digest(
"SHA-256", aResult.clientDataJSON)
.then(digest =>
new Uint8Array(digest));
let { fmt, attStmt, authData, authDataObj } =
await webAuthnDecodeCBORAttestation(aResult.attObj);
is(fmt,
"packed",
"Is a Packed Attestation");
let signedData =
new Uint8Array(authData.length + clientDataHash.length);
signedData.set(authData);
signedData.set(clientDataHash, authData.length);
let valid = await verifySignature(
authDataObj.publicKeyHandle,
signedData,
new Uint8Array(attStmt.sig)
);
ok(valid,
"Signature is valid.");
}
async
function test_register() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential and wait for the prompt.
let notificationPromise = promiseNotification(
"webauthn-prompt-presence");
let active =
true;
let request = promiseWebAuthnMakeCredential(tab)
.then(arrivingHereIsBad)
.
catch(expectNotAllowedError)
.then(() => (active =
false));
await notificationPromise;
// Cancel the request with the button.
ok(active,
"request should still be active");
PopupNotifications.panel.firstElementChild.button.click();
await request;
// Close tab.
await BrowserTestUtils.removeTab(tab);
}
async
function test_register_escape() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential and wait for the prompt.
let notificationPromise = promiseNotification(
"webauthn-prompt-presence");
let active =
true;
let request = promiseWebAuthnMakeCredential(tab)
.then(arrivingHereIsBad)
.
catch(expectNotAllowedError)
.then(() => (active =
false));
await notificationPromise;
// Cancel the request by hitting escape.
ok(active,
"request should still be active");
EventUtils.synthesizeKey(
"KEY_Escape");
await request;
// Close tab.
await BrowserTestUtils.removeTab(tab);
}
async
function test_sign() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new assertion and wait for the prompt.
let notificationPromise = promiseNotification(
"webauthn-prompt-presence");
let active =
true;
let request = promiseWebAuthnGetAssertion(tab)
.then(arrivingHereIsBad)
.
catch(expectNotAllowedError)
.then(() => (active =
false));
await notificationPromise;
// Cancel the request with the button.
ok(active,
"request should still be active");
PopupNotifications.panel.firstElementChild.button.click();
await request;
// Close tab.
await BrowserTestUtils.removeTab(tab);
}
async
function test_sign_escape() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new assertion and wait for the prompt.
let notificationPromise = promiseNotification(
"webauthn-prompt-presence");
let active =
true;
let request = promiseWebAuthnGetAssertion(tab)
.then(arrivingHereIsBad)
.
catch(expectNotAllowedError)
.then(() => (active =
false));
await notificationPromise;
// Cancel the request by hitting escape.
ok(active,
"request should still be active");
EventUtils.synthesizeKey(
"KEY_Escape");
await request;
// Close tab.
await BrowserTestUtils.removeTab(tab);
}
// Add two tabs, open WebAuthn in the first, switch, assert the prompt is
// not visible, switch back, assert the prompt is there and cancel it.
async
function test_tab_switching() {
// Open a new tab.
let tab_one = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential and wait for the prompt.
let notificationPromise = promiseNotification(
"webauthn-prompt-presence");
let active =
true;
let request = promiseWebAuthnMakeCredential(tab_one)
.then(arrivingHereIsBad)
.
catch(expectNotAllowedError)
.then(() => (active =
false));
await notificationPromise;
is(PopupNotifications.panel.state,
"open",
"Doorhanger is visible");
// Open and switch to a second tab.
let tab_two = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://example.org/"
);
await TestUtils.waitForCondition(
() => PopupNotifications.panel.state ==
"closed"
);
is(PopupNotifications.panel.state,
"closed",
"Doorhanger is hidden");
let notificationPromise2 = promiseNotification(
"webauthn-prompt-presence");
// Go back to the first tab
await BrowserTestUtils.removeTab(tab_two);
await notificationPromise2;
await TestUtils.waitForCondition(
() => PopupNotifications.panel.state ==
"open"
);
is(PopupNotifications.panel.state,
"open",
"Doorhanger is visible");
// Cancel the request.
ok(active,
"request should still be active");
await triggerMainPopupCommand(PopupNotifications.panel);
await request;
ok(!active,
"request should be stopped");
// Close tab.
await BrowserTestUtils.removeTab(tab_one);
}
// Add two tabs, open WebAuthn in the first, switch, assert the prompt is
// not visible, switch back, assert the prompt is there and cancel it.
async
function test_window_switching() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential and wait for the prompt.
let notificationPromise = promiseNotification(
"webauthn-prompt-presence");
let active =
true;
let request = promiseWebAuthnMakeCredential(tab)
.then(arrivingHereIsBad)
.
catch(expectNotAllowedError)
.then(() => (active =
false));
await notificationPromise;
await TestUtils.waitForCondition(
() => PopupNotifications.panel.state ==
"open"
);
is(PopupNotifications.panel.state,
"open",
"Doorhanger is visible");
// Open and switch to a second window
let new_window = await BrowserTestUtils.openNewBrowserWindow();
await SimpleTest.promiseFocus(new_window);
await TestUtils.waitForCondition(
() => new_window.PopupNotifications.panel.state ==
"closed"
);
is(
new_window.PopupNotifications.panel.state,
"closed",
"Doorhanger is hidden"
);
// Go back to the first tab
await BrowserTestUtils.closeWindow(new_window);
await SimpleTest.promiseFocus(window);
await TestUtils.waitForCondition(
() => PopupNotifications.panel.state ==
"open"
);
is(PopupNotifications.panel.state,
"open",
"Doorhanger is still visible");
// Cancel the request.
ok(active,
"request should still be active");
await triggerMainPopupCommand(PopupNotifications.panel);
await request;
ok(!active,
"request should be stopped");
// Close tab.
await BrowserTestUtils.removeTab(tab);
}
async
function test_register_direct_with_consent() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential with direct attestation and wait for the prompt.
let notificationPromise = promiseNotification(
"webauthn-prompt-register-direct"
);
let request = promiseWebAuthnMakeCredential(tab,
"direct");
await notificationPromise;
// Click "Allow".
PopupNotifications.panel.firstElementChild.button.click();
// Ensure we got "direct" attestation.
await request.then(verifyDirectCertificate);
// Close tab.
await BrowserTestUtils.removeTab(tab);
}
async
function test_register_direct_without_consent() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Request a new credential with direct attestation and wait for the prompt.
let notificationPromise = promiseNotification(
"webauthn-prompt-register-direct"
);
let request = promiseWebAuthnMakeCredential(tab,
"direct");
await notificationPromise;
// Click "Block".
PopupNotifications.panel.firstElementChild.secondaryButton.click();
// Ensure we got "none" attestation.
await request.then(verifyAnonymizedCertificate);
// Close tab.
await BrowserTestUtils.removeTab(tab);
}
async
function test_select_sign_result() {
// Open a new tab.
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Make two discoverable credentials for the same RP ID so that
// the user has to select one to return.
let cred1 = await addCredential(gAuthenticatorId,
"example.com");
let cred2 = await addCredential(gAuthenticatorId,
"example.com");
let notificationPromise = promiseNotification(
"webauthn-prompt-select-sign-result"
);
let active =
true;
let request = promiseWebAuthnGetAssertionDiscoverable(tab)
.then(arrivingHereIsBad)
.
catch(expectNotAllowedError)
.then(() => (active =
false));
// Ensure the selection prompt is shown
await notificationPromise;
ok(active,
"request is active");
// Cancel the request
PopupNotifications.panel.firstElementChild.button.click();
await request;
await removeCredential(gAuthenticatorId, cred1);
await removeCredential(gAuthenticatorId, cred2);
await BrowserTestUtils.removeTab(tab);
}
async
function test_fullscreen_show_nav_toolbar() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
// Start with the window fullscreen and the nav toolbox hidden
let fullscreenState = window.fullScreen;
let navToolboxHiddenPromise = promiseNavToolboxStatus(
"hidden");
window.fullScreen =
true;
FullScreen.hideNavToolbox(
false);
await navToolboxHiddenPromise;
// Request a new credential with direct attestation. The consent prompt will
// keep the request active until we can verify that the nav toolbar is shown.
let promptPromise = promiseNotification(
"webauthn-prompt-register-direct");
let navToolboxShownPromise = promiseNavToolboxStatus(
"shown");
let active =
true;
let requestPromise = promiseWebAuthnMakeCredential(tab,
"direct").then(
() => (active =
false)
);
await Promise.all([promptPromise, navToolboxShownPromise]);
ok(active,
"request is active");
ok(window.fullScreen,
"window is fullscreen");
// Proceed through the consent prompt.
PopupNotifications.panel.firstElementChild.secondaryButton.click();
await requestPromise;
window.fullScreen = fullscreenState;
// Close tab.
await BrowserTestUtils.removeTab(tab);
}
async
function test_no_fullscreen_dom() {
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
let fullScreenPaintPromise = promiseFullScreenPaint();
// Make a DOM element fullscreen
await ContentTask.spawn(tab.linkedBrowser, [], () => {
return content.document.body.requestFullscreen();
});
await fullScreenPaintPromise;
ok(!!document.fullscreenElement,
"a DOM element is fullscreen");
// Request a new credential with direct attestation. The consent prompt will
// keep the request active until we can verify that we've left fullscreen.
let promptPromise = promiseNotification(
"webauthn-prompt-register-direct");
fullScreenPaintPromise = promiseFullScreenPaint();
let active =
true;
let requestPromise = promiseWebAuthnMakeCredential(tab,
"direct").then(
() => (active =
false)
);
await Promise.all([promptPromise, fullScreenPaintPromise]);
ok(active,
"request is active");
ok(!document.fullscreenElement,
"no DOM element is fullscreen");
// Proceed through the consent prompt.
await waitForPopupNotificationSecurityDelay();
PopupNotifications.panel.firstElementChild.secondaryButton.click();
await requestPromise;
// Close tab.
await BrowserTestUtils.removeTab(tab);
}