460 lines
14 KiB
JavaScript
460 lines
14 KiB
JavaScript
/* 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);
|
|
}
|