summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/tests/browser
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webauthn/tests/browser')
-rw-r--r--dom/webauthn/tests/browser/browser.toml35
-rw-r--r--dom/webauthn/tests/browser/browser_abort_visibility.js277
-rw-r--r--dom/webauthn/tests/browser/browser_fido_appid_extension.js131
-rw-r--r--dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js177
-rw-r--r--dom/webauthn/tests/browser/browser_webauthn_ipaddress.js30
-rw-r--r--dom/webauthn/tests/browser/browser_webauthn_prompts.js501
-rw-r--r--dom/webauthn/tests/browser/head.js259
-rw-r--r--dom/webauthn/tests/browser/tab_webauthn_result.html14
8 files changed, 1424 insertions, 0 deletions
diff --git a/dom/webauthn/tests/browser/browser.toml b/dom/webauthn/tests/browser/browser.toml
new file mode 100644
index 0000000000..41dc82d6ee
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser.toml
@@ -0,0 +1,35 @@
+[DEFAULT]
+support-files = [
+ "head.js",
+ "tab_webauthn_result.html",
+ "../pkijs/*",
+ "../cbor.js",
+ "../u2futil.js",
+]
+prefs = [
+ "security.webauth.webauthn=true",
+ "security.webauth.webauthn_enable_softtoken=true",
+ "security.webauth.webauthn_enable_usbtoken=false",
+ "security.webauthn.ctap2=true",
+ "security.webauthn.enable_conditional_mediation=true",
+ "security.webauthn.enable_macos_passkeys=false",
+]
+
+["browser_abort_visibility.js"]
+skip-if = [
+ "win11_2009", # Test not relevant on 1903+
+]
+
+["browser_fido_appid_extension.js"]
+skip-if = [
+ "win11_2009", # Test not relevant on 1903+
+]
+
+["browser_webauthn_conditional_mediation.js"]
+
+["browser_webauthn_ipaddress.js"]
+
+["browser_webauthn_prompts.js"]
+skip-if = [
+ "win11_2009", # Test not relevant on 1903+
+]
diff --git a/dom/webauthn/tests/browser/browser_abort_visibility.js b/dom/webauthn/tests/browser/browser_abort_visibility.js
new file mode 100644
index 0000000000..d8b2b5edcc
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_abort_visibility.js
@@ -0,0 +1,277 @@
+/* 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";
+
+const TEST_URL =
+ "https://example.com/browser/dom/webauthn/tests/browser/tab_webauthn_result.html";
+
+add_task(async function test_setup() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.webauth.webauthn_enable_softtoken", false],
+ ["security.webauth.webauthn_enable_usbtoken", true],
+ ],
+ });
+});
+add_task(test_switch_tab);
+add_task(test_new_window_make);
+add_task(test_new_window_get);
+add_task(test_minimize_make);
+add_task(test_minimize_get);
+
+async function assertStatus(tab, expected) {
+ let actual = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ async function () {
+ info("visbility state: " + content.document.visibilityState);
+ info("active: " + content.browsingContext.isActive);
+ return content.document.getElementById("status").value;
+ }
+ );
+ is(actual, expected, "webauthn request " + expected);
+}
+
+async function waitForStatus(tab, expected) {
+ /* eslint-disable no-shadow */
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [[expected]],
+ async function (expected) {
+ return ContentTaskUtils.waitForCondition(() => {
+ info(
+ "expecting " +
+ expected +
+ ", visbility state: " +
+ content.document.visibilityState
+ );
+ info(
+ "expecting " +
+ expected +
+ ", active: " +
+ content.browsingContext.isActive
+ );
+ return content.document.getElementById("status").value == expected;
+ });
+ }
+ );
+ /* eslint-enable no-shadow */
+
+ await assertStatus(tab, expected);
+}
+
+function startMakeCredentialRequest(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ const cose_alg_ECDSA_w_SHA256 = -7;
+
+ let publicKey = {
+ rp: { id: content.document.domain, name: "none" },
+ user: {
+ id: new Uint8Array(),
+ name: "none",
+ displayName: "none",
+ },
+ challenge: content.crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ pubKeyCredParams: [{ type: "public-key", alg: cose_alg_ECDSA_w_SHA256 }],
+ };
+
+ let status = content.document.getElementById("status");
+
+ info(
+ "Attempting to create credential for origin: " +
+ content.document.nodePrincipal.origin
+ );
+ content.navigator.credentials
+ .create({ publicKey })
+ .then(() => {
+ status.value = "completed";
+ })
+ .catch(() => {
+ status.value = "aborted";
+ });
+
+ status.value = "pending";
+ });
+}
+
+function startGetAssertionRequest(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ let newCredential = {
+ type: "public-key",
+ id: content.crypto.getRandomValues(new Uint8Array(16)),
+ transports: ["usb"],
+ };
+
+ let publicKey = {
+ challenge: content.crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ rpId: content.document.domain,
+ allowCredentials: [newCredential],
+ };
+
+ let status = content.document.getElementById("status");
+
+ info(
+ "Attempting to get credential for origin: " +
+ content.document.nodePrincipal.origin
+ );
+ content.navigator.credentials
+ .get({ publicKey })
+ .then(() => {
+ status.value = "completed";
+ })
+ .catch(ex => {
+ info("aborted: " + ex);
+ status.value = "aborted";
+ });
+
+ status.value = "pending";
+ });
+}
+
+// Test that MakeCredential() and GetAssertion() requests
+// are aborted when the current tab loses its focus.
+async function test_switch_tab() {
+ // Create a new tab for the MakeCredential() request.
+ let tab_create = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_URL
+ );
+
+ // Start the request.
+ await startMakeCredentialRequest(tab_create);
+ await assertStatus(tab_create, "pending");
+
+ // Open another tab and switch to it. The first will lose focus.
+ let tab_get = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+ await assertStatus(tab_create, "pending");
+
+ // Start a GetAssertion() request in the second tab, the first is aborted
+ await startGetAssertionRequest(tab_get);
+ await waitForStatus(tab_create, "aborted");
+ await assertStatus(tab_get, "pending");
+
+ // Start a second request in the second tab. It should abort.
+ await startGetAssertionRequest(tab_get);
+ await waitForStatus(tab_get, "aborted");
+
+ // Close tabs.
+ BrowserTestUtils.removeTab(tab_create);
+ BrowserTestUtils.removeTab(tab_get);
+}
+
+function waitForWindowActive(win, active) {
+ return Promise.all([
+ BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"),
+ BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"),
+ ]);
+}
+
+async function test_new_window_make() {
+ // Create a new tab for the MakeCredential() request.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ // Start a MakeCredential request.
+ await startMakeCredentialRequest(tab);
+ await assertStatus(tab, "pending");
+
+ let windowGonePromise = waitForWindowActive(window, false);
+ // Open a new window. The tab will lose focus.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await windowGonePromise;
+ await assertStatus(tab, "pending");
+
+ let windowBackPromise = waitForWindowActive(window, true);
+ await BrowserTestUtils.closeWindow(win);
+ await windowBackPromise;
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+}
+
+async function test_new_window_get() {
+ // Create a new tab for the GetAssertion() request.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ // Start a GetAssertion request.
+ await startGetAssertionRequest(tab);
+ await assertStatus(tab, "pending");
+
+ let windowGonePromise = waitForWindowActive(window, false);
+ // Open a new window. The tab will lose focus.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await windowGonePromise;
+ await assertStatus(tab, "pending");
+
+ let windowBackPromise = waitForWindowActive(window, true);
+ await BrowserTestUtils.closeWindow(win);
+ await windowBackPromise;
+
+ // Close tab.
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function test_minimize_make() {
+ // Minimizing windows doesn't supported in headless mode.
+ if (Services.env.get("MOZ_HEADLESS")) {
+ return;
+ }
+
+ // Create a new window for the MakeCredential() request.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL);
+
+ // Start a MakeCredential request.
+ await startMakeCredentialRequest(tab);
+ await assertStatus(tab, "pending");
+
+ // Minimize the window.
+ let windowGonePromise = waitForWindowActive(win, false);
+ win.minimize();
+ await assertStatus(tab, "pending");
+ await windowGonePromise;
+
+ // Restore the window.
+ await new Promise(resolve => SimpleTest.waitForFocus(resolve, win));
+ await assertStatus(tab, "pending");
+
+ // Close window and wait for main window to be focused again.
+ let windowBackPromise = waitForWindowActive(window, true);
+ await BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+ await windowBackPromise;
+}
+
+async function test_minimize_get() {
+ // Minimizing windows doesn't supported in headless mode.
+ if (Services.env.get("MOZ_HEADLESS")) {
+ return;
+ }
+
+ // Create a new window for the GetAssertion() request.
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL);
+
+ // Start a GetAssertion request.
+ await startGetAssertionRequest(tab);
+ await assertStatus(tab, "pending");
+
+ // Minimize the window.
+ let windowGonePromise = waitForWindowActive(win, false);
+ win.minimize();
+ await assertStatus(tab, "pending");
+ await windowGonePromise;
+
+ // Restore the window.
+ await new Promise(resolve => SimpleTest.waitForFocus(resolve, win));
+ await assertStatus(tab, "pending");
+
+ // Close window and wait for main window to be focused again.
+ let windowBackPromise = waitForWindowActive(window, true);
+ await BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+ await windowBackPromise;
+}
diff --git a/dom/webauthn/tests/browser/browser_fido_appid_extension.js b/dom/webauthn/tests/browser/browser_fido_appid_extension.js
new file mode 100644
index 0000000000..fca4443074
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_fido_appid_extension.js
@@ -0,0 +1,131 @@
+/* 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";
+
+const TEST_URL = "https://example.com/";
+
+let expectNotSupportedError = expectError("NotSupported");
+let expectNotAllowedError = expectError("NotAllowed");
+let expectSecurityError = expectError("Security");
+
+let gAppId = "https://example.com/appId";
+let gCrossOriginAppId = "https://example.org/appId";
+let gAuthenticatorId = add_virtual_authenticator();
+
+add_task(async function test_appid() {
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ // The FIDO AppId extension can't be used for MakeCredential.
+ await promiseWebAuthnMakeCredential(tab, "none", "discouraged", {
+ appid: gAppId,
+ })
+ .then(arrivingHereIsBad)
+ .catch(expectNotSupportedError);
+
+ // Side-load a credential with an RP ID matching the App ID.
+ let credIdB64 = await addCredential(gAuthenticatorId, gAppId);
+ let credId = base64ToBytesUrlSafe(credIdB64);
+
+ // And another for a different origin
+ let crossOriginCredIdB64 = await addCredential(
+ gAuthenticatorId,
+ gCrossOriginAppId
+ );
+ let crossOriginCredId = base64ToBytesUrlSafe(crossOriginCredIdB64);
+
+ // The App ID extension is required
+ await promiseWebAuthnGetAssertion(tab, credId)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError);
+
+ // The value in the App ID extension must match the origin.
+ await promiseWebAuthnGetAssertion(tab, crossOriginCredId, {
+ appid: gCrossOriginAppId,
+ })
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+
+ // The value in the App ID extension must match the credential's RP ID.
+ await promiseWebAuthnGetAssertion(tab, credId, { appid: gAppId + "2" })
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError);
+
+ // Succeed with the right App ID.
+ let rpIdHash = await promiseWebAuthnGetAssertion(tab, credId, {
+ appid: gAppId,
+ })
+ .then(({ authenticatorData, extensions }) => {
+ is(extensions.appid, true, "appid extension was acted upon");
+ return authenticatorData.slice(0, 32);
+ })
+ .then(rpIdHash => {
+ // Make sure the returned RP ID hash matches the hash of the App ID.
+ checkRpIdHash(rpIdHash, gAppId);
+ })
+ .catch(arrivingHereIsBad);
+
+ removeCredential(gAuthenticatorId, credIdB64);
+ removeCredential(gAuthenticatorId, crossOriginCredIdB64);
+
+ // Close tab.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_appid_unused() {
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let appid = "https://example.com/appId";
+
+ let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab);
+ let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj);
+
+ // Make sure the RP ID hash matches what we calculate.
+ await checkRpIdHash(authDataObj.rpIdHash, "example.com");
+
+ // Get a new assertion.
+ let { clientDataJSON, authenticatorData, signature, extensions } =
+ await promiseWebAuthnGetAssertion(tab, rawId, { appid });
+
+ ok(
+ "appid" in extensions,
+ `appid should be populated in the extensions data, but saw: ` +
+ `${JSON.stringify(extensions)}`
+ );
+ is(extensions.appid, false, "appid extension should indicate it was unused");
+
+ // Check auth data.
+ let attestation = await webAuthnDecodeAuthDataArray(
+ new Uint8Array(authenticatorData)
+ );
+ is(
+ "" + (attestation.flags & flag_TUP),
+ "" + flag_TUP,
+ "Assertion's user presence byte set correctly"
+ );
+
+ // Verify the signature.
+ let params = await deriveAppAndChallengeParam(
+ "example.com",
+ clientDataJSON,
+ attestation
+ );
+ let signedData = await assembleSignedData(
+ params.appParam,
+ params.attestation.flags,
+ params.attestation.counter,
+ params.challengeParam
+ );
+ let valid = await verifySignature(
+ authDataObj.publicKeyHandle,
+ signedData,
+ signature
+ );
+ ok(valid, "signature is valid");
+
+ // Close tab.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js b/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js
new file mode 100644
index 0000000000..fff1ec5dab
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js
@@ -0,0 +1,177 @@
+/* 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";
+
+const TEST_URL = "https://example.com";
+
+let gAuthenticatorId = add_virtual_authenticator();
+let gExpectNotAllowedError = expectError("NotAllowed");
+let gExpectAbortError = expectError("Abort");
+let gPendingConditionalGetSubject = "webauthn:conditional-get-pending";
+let gWebAuthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+);
+
+add_task(async function test_webauthn_modal_request_cancels_conditional_get() {
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let browser = tab.linkedBrowser.browsingContext.embedderElement;
+ let browsingContextId = browser.browsingContext.id;
+
+ let transactionId = gWebAuthnService.hasPendingConditionalGet(
+ browsingContextId,
+ TEST_URL
+ );
+ Assert.equal(transactionId, 0, "should not have a pending conditional get");
+
+ let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject);
+
+ let active = true;
+ let condPromise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional")
+ .then(arrivingHereIsBad)
+ .catch(gExpectAbortError)
+ .then(() => (active = false));
+
+ await requestStarted;
+
+ transactionId = gWebAuthnService.hasPendingConditionalGet(
+ browsingContextId,
+ TEST_URL
+ );
+ Assert.notEqual(transactionId, 0, "should have a pending conditional get");
+
+ ok(active, "conditional request should still be active");
+
+ let promptPromise = promiseNotification("webauthn-prompt-register-direct");
+ let modalPromise = promiseWebAuthnMakeCredential(tab, "direct")
+ .then(arrivingHereIsBad)
+ .catch(gExpectNotAllowedError);
+
+ await condPromise;
+
+ ok(!active, "conditional request should not be active");
+
+ // Cancel the modal request with the button.
+ await promptPromise;
+ PopupNotifications.panel.firstElementChild.secondaryButton.click();
+ await modalPromise;
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_webauthn_resume_conditional_get() {
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ let browser = tab.linkedBrowser.browsingContext.embedderElement;
+ let browsingContextId = browser.browsingContext.id;
+
+ let transactionId = gWebAuthnService.hasPendingConditionalGet(
+ browsingContextId,
+ TEST_URL
+ );
+ Assert.equal(transactionId, 0, "should not have a pending conditional get");
+
+ let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject);
+
+ let active = true;
+ let promise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional")
+ .then(arrivingHereIsBad)
+ .catch(gExpectNotAllowedError)
+ .then(() => (active = false));
+
+ await requestStarted;
+
+ transactionId = gWebAuthnService.hasPendingConditionalGet(0, TEST_URL);
+ Assert.equal(
+ transactionId,
+ 0,
+ "hasPendingConditionalGet should check the browsing context id"
+ );
+
+ transactionId = gWebAuthnService.hasPendingConditionalGet(
+ browsingContextId,
+ "https://example.org"
+ );
+ Assert.equal(
+ transactionId,
+ 0,
+ "hasPendingConditionalGet should check the origin"
+ );
+
+ transactionId = gWebAuthnService.hasPendingConditionalGet(
+ browsingContextId,
+ TEST_URL
+ );
+ Assert.notEqual(transactionId, 0, "should have a pending conditional get");
+
+ ok(active, "request should still be active");
+
+ gWebAuthnService.resumeConditionalGet(transactionId);
+ await promise;
+
+ ok(!active, "request should not be active");
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_webauthn_select_autofill_entry() {
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ // Add credentials
+ let cred1 = await addCredential(gAuthenticatorId, "example.com");
+ let cred2 = await addCredential(gAuthenticatorId, "example.com");
+
+ let browser = tab.linkedBrowser.browsingContext.embedderElement;
+ let browsingContextId = browser.browsingContext.id;
+
+ let transactionId = gWebAuthnService.hasPendingConditionalGet(
+ browsingContextId,
+ TEST_URL
+ );
+ Assert.equal(transactionId, 0, "should not have a pending conditional get");
+
+ let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject);
+
+ let active = true;
+ let promise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional")
+ .catch(arrivingHereIsBad)
+ .then(() => (active = false));
+
+ await requestStarted;
+
+ transactionId = gWebAuthnService.hasPendingConditionalGet(
+ browsingContextId,
+ TEST_URL
+ );
+ Assert.notEqual(transactionId, 0, "should have a pending conditional get");
+
+ let autoFillEntries = gWebAuthnService.getAutoFillEntries(transactionId);
+ ok(
+ autoFillEntries.length == 2 &&
+ autoFillEntries[0].rpId == "example.com" &&
+ autoFillEntries[1].rpId == "example.com",
+ "should have two autofill entries for example.com"
+ );
+
+ gWebAuthnService.selectAutoFillEntry(
+ transactionId,
+ autoFillEntries[0].credentialId
+ );
+ let result = await promise;
+
+ ok(!active, "request should not be active");
+
+ // Remove credentials
+ gWebAuthnService.removeCredential(gAuthenticatorId, cred1);
+ gWebAuthnService.removeCredential(gAuthenticatorId, cred2);
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js
new file mode 100644
index 0000000000..6658ae5f02
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js
@@ -0,0 +1,30 @@
+/* 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";
+
+add_virtual_authenticator();
+
+let expectSecurityError = expectError("Security");
+
+add_task(async function test_setup() {
+ return SpecialPowers.pushPrefEnv({
+ set: [["network.proxy.allow_hijacking_localhost", true]],
+ });
+});
+
+add_task(async function test_appid() {
+ // 127.0.0.1 triggers special cases in ssltunnel, so let's use .2!
+ const TEST_URL = "https://127.0.0.2/";
+
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ await promiseWebAuthnMakeCredential(tab)
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+
+ // Close tab.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/webauthn/tests/browser/browser_webauthn_prompts.js b/dom/webauthn/tests/browser/browser_webauthn_prompts.js
new file mode 100644
index 0000000000..05c77271d5
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js
@@ -0,0 +1,501 @@
+/* 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 = performance.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_register_direct_cancel);
+add_task(test_register_direct_presence);
+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_fullscreen() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.fullscreen.autohide", true],
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ],
+ });
+});
+add_task(test_fullscreen_show_nav_toolbar);
+add_task(test_no_fullscreen_dom);
+add_task(async function test_setup_softtoken() {
+ gAuthenticatorId = add_virtual_authenticator();
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.webauth.webauthn_enable_softtoken", true],
+ ["security.webauth.webauthn_enable_usbtoken", false],
+ ],
+ });
+});
+add_task(test_register_direct_proceed);
+add_task(test_register_direct_proceed_anon);
+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 active = true;
+ let request = promiseWebAuthnMakeCredential(tab)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-presence");
+
+ // 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 active = true;
+ let request = promiseWebAuthnMakeCredential(tab)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-presence");
+
+ // 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 active = true;
+ let request = promiseWebAuthnGetAssertion(tab)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-presence");
+
+ // 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 active = true;
+ let request = promiseWebAuthnGetAssertion(tab)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-presence");
+
+ // 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_register_direct_cancel() {
+ // 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 active = true;
+ let promise = promiseWebAuthnMakeCredential(tab, "direct")
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-register-direct");
+
+ // Cancel the request.
+ ok(active, "request should still be active");
+ PopupNotifications.panel.firstElementChild.secondaryButton.click();
+ await promise;
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+}
+
+async function test_register_direct_presence() {
+ // 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 active = true;
+ let promise = promiseWebAuthnMakeCredential(tab, "direct")
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-register-direct");
+
+ // Click "proceed" and wait for presence prompt
+ let presence = promiseNotification("webauthn-prompt-presence");
+ PopupNotifications.panel.firstElementChild.button.click();
+ await presence;
+
+ // Cancel the request.
+ ok(active, "request should still be active");
+ PopupNotifications.panel.firstElementChild.button.click();
+ await promise;
+
+ // 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 active = true;
+ let request = promiseWebAuthnMakeCredential(tab_one)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-presence");
+ 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");
+
+ // Go back to the first tab
+ await BrowserTestUtils.removeTab(tab_two);
+
+ await promiseNotification("webauthn-prompt-presence");
+
+ 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 active = true;
+ let request = promiseWebAuthnMakeCredential(tab)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-presence");
+
+ 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_proceed() {
+ // 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 request = promiseWebAuthnMakeCredential(tab, "direct");
+ await promiseNotification("webauthn-prompt-register-direct");
+
+ // Proceed.
+ 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_proceed_anon() {
+ // 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 request = promiseWebAuthnMakeCredential(tab, "direct");
+ await promiseNotification("webauthn-prompt-register-direct");
+
+ // Check "anonymize anyway" and proceed.
+ PopupNotifications.panel.firstElementChild.checkbox.checked = true;
+ PopupNotifications.panel.firstElementChild.button.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 active = true;
+ let request = promiseWebAuthnGetAssertionDiscoverable(tab)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+
+ // Ensure the selection prompt is shown
+ await promiseNotification("webauthn-prompt-select-sign-result");
+
+ 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 and wait for the direct attestation consent
+ // prompt.
+ let promptPromise = promiseNotification("webauthn-prompt-register-direct");
+ let navToolboxShownPromise = promiseNavToolboxStatus("shown");
+
+ let active = true;
+ let requestPromise = promiseWebAuthnMakeCredential(tab, "direct")
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+
+ await Promise.all([promptPromise, navToolboxShownPromise]);
+
+ ok(active, "request is active");
+ ok(window.fullScreen, "window is fullscreen");
+
+ // Cancel the request.
+ 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 and wait for the direct attestation consent
+ // prompt.
+ let promptPromise = promiseNotification("webauthn-prompt-register-direct");
+ fullScreenPaintPromise = promiseFullScreenPaint();
+
+ let active = true;
+ let requestPromise = promiseWebAuthnMakeCredential(tab, "direct")
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .then(() => (active = false));
+
+ await Promise.all([promptPromise, fullScreenPaintPromise]);
+
+ ok(active, "request is active");
+ ok(!document.fullscreenElement, "no DOM element is fullscreen");
+
+ // Cancel the request.
+ await waitForPopupNotificationSecurityDelay();
+ PopupNotifications.panel.firstElementChild.secondaryButton.click();
+ await requestPromise;
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+}
diff --git a/dom/webauthn/tests/browser/head.js b/dom/webauthn/tests/browser/head.js
new file mode 100644
index 0000000000..d6cbd56133
--- /dev/null
+++ b/dom/webauthn/tests/browser/head.js
@@ -0,0 +1,259 @@
+/* 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 exports = this;
+
+const scripts = [
+ "pkijs/common.js",
+ "pkijs/asn1.js",
+ "pkijs/x509_schema.js",
+ "pkijs/x509_simpl.js",
+ "browser/cbor.js",
+ "browser/u2futil.js",
+];
+
+for (let script of scripts) {
+ Services.scriptloader.loadSubScript(
+ `chrome://mochitests/content/browser/dom/webauthn/tests/${script}`,
+ this
+ );
+}
+
+function add_virtual_authenticator(autoremove = true) {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+ let id = webauthnService.addVirtualAuthenticator(
+ "ctap2_1",
+ "internal",
+ true,
+ true,
+ true,
+ true
+ );
+ if (autoremove) {
+ registerCleanupFunction(() => {
+ webauthnService.removeVirtualAuthenticator(id);
+ });
+ }
+ return id;
+}
+
+async function addCredential(authenticatorId, rpId) {
+ let keyPair = await crypto.subtle.generateKey(
+ {
+ name: "ECDSA",
+ namedCurve: "P-256",
+ },
+ true,
+ ["sign"]
+ );
+
+ let credId = new Uint8Array(32);
+ crypto.getRandomValues(credId);
+ credId = bytesToBase64UrlSafe(credId);
+
+ let privateKey = await crypto.subtle
+ .exportKey("pkcs8", keyPair.privateKey)
+ .then(privateKey => bytesToBase64UrlSafe(privateKey));
+
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+
+ webauthnService.addCredential(
+ authenticatorId,
+ credId,
+ true, // resident key
+ rpId,
+ privateKey,
+ "VGVzdCBVc2Vy", // "Test User"
+ 0 // sign count
+ );
+
+ return credId;
+}
+
+async function removeCredential(authenticatorId, credId) {
+ let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+
+ webauthnService.removeCredential(authenticatorId, credId);
+}
+
+function memcmp(x, y) {
+ let xb = new Uint8Array(x);
+ let yb = new Uint8Array(y);
+
+ if (x.byteLength != y.byteLength) {
+ return false;
+ }
+
+ for (let i = 0; i < xb.byteLength; ++i) {
+ if (xb[i] != yb[i]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+}
+
+function expectError(aType) {
+ let expected = `${aType}Error`;
+ return function (aResult) {
+ is(
+ aResult.slice(0, expected.length),
+ expected,
+ `Expecting a ${aType}Error`
+ );
+ };
+}
+
+/* eslint-disable no-shadow */
+function promiseWebAuthnMakeCredential(
+ tab,
+ attestation = "none",
+ residentKey = "discouraged",
+ extensions = {}
+) {
+ return ContentTask.spawn(
+ tab.linkedBrowser,
+ [attestation, residentKey, extensions],
+ ([attestation, residentKey, extensions]) => {
+ const cose_alg_ECDSA_w_SHA256 = -7;
+
+ let challenge = content.crypto.getRandomValues(new Uint8Array(16));
+
+ let pubKeyCredParams = [
+ {
+ type: "public-key",
+ alg: cose_alg_ECDSA_w_SHA256,
+ },
+ ];
+
+ let publicKey = {
+ rp: { id: content.document.domain, name: "none" },
+ user: {
+ id: new Uint8Array(),
+ name: "none",
+ displayName: "none",
+ },
+ pubKeyCredParams,
+ authenticatorSelection: {
+ authenticatorAttachment: "cross-platform",
+ residentKey,
+ },
+ extensions,
+ attestation,
+ challenge,
+ };
+
+ return content.navigator.credentials
+ .create({ publicKey })
+ .then(credential => {
+ return {
+ clientDataJSON: credential.response.clientDataJSON,
+ attObj: credential.response.attestationObject,
+ rawId: credential.rawId,
+ };
+ });
+ }
+ );
+}
+
+function promiseWebAuthnGetAssertion(tab, key_handle = null, extensions = {}) {
+ return ContentTask.spawn(
+ tab.linkedBrowser,
+ [key_handle, extensions],
+ ([key_handle, extensions]) => {
+ let challenge = content.crypto.getRandomValues(new Uint8Array(16));
+ if (key_handle == null) {
+ key_handle = content.crypto.getRandomValues(new Uint8Array(16));
+ }
+
+ let credential = {
+ id: key_handle,
+ type: "public-key",
+ transports: ["usb"],
+ };
+
+ let publicKey = {
+ challenge,
+ extensions,
+ rpId: content.document.domain,
+ allowCredentials: [credential],
+ };
+
+ return content.navigator.credentials
+ .get({ publicKey })
+ .then(assertion => {
+ return {
+ authenticatorData: assertion.response.authenticatorData,
+ clientDataJSON: assertion.response.clientDataJSON,
+ extensions: assertion.getClientExtensionResults(),
+ signature: assertion.response.signature,
+ };
+ });
+ }
+ );
+}
+
+function promiseWebAuthnGetAssertionDiscoverable(
+ tab,
+ mediation = "optional",
+ extensions = {}
+) {
+ return ContentTask.spawn(
+ tab.linkedBrowser,
+ [extensions, mediation],
+ ([extensions, mediation]) => {
+ let challenge = content.crypto.getRandomValues(new Uint8Array(16));
+
+ let publicKey = {
+ challenge,
+ extensions,
+ rpId: content.document.domain,
+ allowCredentials: [],
+ };
+
+ return content.navigator.credentials.get({ publicKey, mediation });
+ }
+ );
+}
+
+function checkRpIdHash(rpIdHash, hostname) {
+ return crypto.subtle
+ .digest("SHA-256", string2buffer(hostname))
+ .then(calculatedRpIdHash => {
+ let calcHashStr = bytesToBase64UrlSafe(
+ new Uint8Array(calculatedRpIdHash)
+ );
+ let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(rpIdHash));
+
+ if (calcHashStr != providedHashStr) {
+ throw new Error("Calculated RP ID hash doesn't match.");
+ }
+ });
+}
+
+function promiseNotification(id) {
+ return new Promise(resolve => {
+ PopupNotifications.panel.addEventListener("popupshown", function shown() {
+ let notification = PopupNotifications.getNotification(id);
+ if (notification) {
+ ok(true, `${id} prompt visible`);
+ PopupNotifications.panel.removeEventListener("popupshown", shown);
+ resolve();
+ }
+ });
+ });
+}
+/* eslint-enable no-shadow */
diff --git a/dom/webauthn/tests/browser/tab_webauthn_result.html b/dom/webauthn/tests/browser/tab_webauthn_result.html
new file mode 100644
index 0000000000..8e8b9f82cd
--- /dev/null
+++ b/dom/webauthn/tests/browser/tab_webauthn_result.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Generic W3C Web Authentication Test Result Page</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Generic W3C Web Authentication Test Result Page</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a>
+<input type="text" id="status" value="init" />
+
+</body>
+</html>