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.ini22
-rw-r--r--dom/webauthn/tests/browser/browser_abort_visibility.js277
-rw-r--r--dom/webauthn/tests/browser/browser_fido_appid_extension.js183
-rw-r--r--dom/webauthn/tests/browser/browser_webauthn_ipaddress.js51
-rw-r--r--dom/webauthn/tests/browser/browser_webauthn_prompts.js271
-rw-r--r--dom/webauthn/tests/browser/browser_webauthn_telemetry.js144
-rw-r--r--dom/webauthn/tests/browser/head.js155
-rw-r--r--dom/webauthn/tests/browser/tab_webauthn_result.html14
8 files changed, 1117 insertions, 0 deletions
diff --git a/dom/webauthn/tests/browser/browser.ini b/dom/webauthn/tests/browser/browser.ini
new file mode 100644
index 0000000000..e16a1e7723
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+support-files =
+ head.js
+ tab_webauthn_result.html
+ ../pkijs/*
+ ../cbor.js
+ ../u2futil.js
+skip-if = !e10s
+
+[browser_abort_visibility.js]
+skip-if =
+ os == 'win' && version == '18362' # win10-1903+ - test not relevant on 1903
+[browser_fido_appid_extension.js]
+skip-if =
+ os == 'win' && version == '18362' # win10-1903+ - test not relevant on 1903
+[browser_webauthn_prompts.js]
+skip-if =
+ os == 'win' && version == '18362' # win10-1903+ - test not relevant on 1903
+[browser_webauthn_telemetry.js]
+skip-if =
+ os == 'win' && version == '18362' # win10-1903+ - test not relevant on 1903
+[browser_webauthn_ipaddress.js]
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..4cfb37136e
--- /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";
+
+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", icon: "none" },
+ user: {
+ id: new Uint8Array(),
+ name: "none",
+ icon: "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";
+ });
+}
+
+add_task(async function test_setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.webauth.webauthn", true],
+ ["security.webauth.webauthn_enable_softtoken", false],
+ ["security.webauth.webauthn_enable_android_fido2", false],
+ ["security.webauth.webauthn_enable_usbtoken", true],
+ ],
+ });
+});
+
+// Test that MakeCredential() and GetAssertion() requests
+// are aborted when the current tab loses its focus.
+add_task(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"),
+ ]);
+}
+
+add_task(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);
+});
+
+add_task(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);
+});
+
+add_task(async function test_minimize_make() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ // Minimizing windows doesn't supported in headless mode.
+ if (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.closeWindow(win);
+ await windowBackPromise;
+});
+
+add_task(async function test_minimize_get() {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ // Minimizing windows doesn't supported in headless mode.
+ if (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.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..1b574f7064
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_fido_appid_extension.js
@@ -0,0 +1,183 @@
+/* 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 expectInvalidStateError = expectError("InvalidState");
+let expectSecurityError = expectError("Security");
+
+function promiseU2FRegister(tab, app_id_) {
+ let challenge_ = crypto.getRandomValues(new Uint8Array(16));
+ challenge_ = bytesToBase64UrlSafe(challenge_);
+
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [[app_id_, challenge_]],
+ function([app_id, challenge]) {
+ return new Promise(resolve => {
+ content.u2f.register(
+ app_id,
+ [{ version: "U2F_V2", challenge }],
+ [],
+ resolve
+ );
+ });
+ }
+ ).then(res => {
+ is(res.errorCode, 0, "u2f.register() succeeded");
+ let data = base64ToBytesUrlSafe(res.registrationData);
+ is(data[0], 0x05, "Reserved byte is correct");
+ return data.slice(67, 67 + data[66]);
+ });
+}
+
+add_task(function test_setup() {
+ Services.prefs.setBoolPref("security.webauth.u2f", true);
+ Services.prefs.setBoolPref("security.webauth.webauthn", true);
+ Services.prefs.setBoolPref(
+ "security.webauth.webauthn_enable_android_fido2",
+ false
+ );
+ Services.prefs.setBoolPref(
+ "security.webauth.webauthn_enable_softtoken",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "security.webauth.webauthn_enable_usbtoken",
+ false
+ );
+});
+
+add_task(async function test_appid() {
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ // Get a keyHandle for a FIDO AppId.
+ let appid = "https://example.com/appId";
+ let keyHandle = await promiseU2FRegister(tab, appid);
+
+ // The FIDO AppId extension can't be used for MakeCredential.
+ await promiseWebAuthnMakeCredential(tab, "none", { appid })
+ .then(arrivingHereIsBad)
+ .catch(expectNotSupportedError);
+
+ // Using the keyHandle shouldn't work without the FIDO AppId extension.
+ // This will be an invalid state, because the softtoken will consent without
+ // having the correct "RP ID" via the FIDO extension.
+ await promiseWebAuthnGetAssertion(tab, keyHandle)
+ .then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+
+ // Invalid app IDs (for the current origin) must be rejected.
+ await promiseWebAuthnGetAssertion(tab, keyHandle, {
+ appid: "https://bogus.com/appId",
+ })
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+
+ // Non-matching app IDs must be rejected. Even when the user/softtoken
+ // consents, leading to an invalid state.
+ await promiseWebAuthnGetAssertion(tab, keyHandle, { appid: appid + "2" })
+ .then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+
+ let rpId = new TextEncoder("utf-8").encode(appid);
+ let rpIdHash = await crypto.subtle.digest("SHA-256", rpId);
+
+ // Succeed with the right fallback rpId.
+ await promiseWebAuthnGetAssertion(tab, keyHandle, { appid }).then(
+ ({ authenticatorData, clientDataJSON, extensions }) => {
+ is(extensions.appid, true, "appid extension was acted upon");
+
+ // Check that the correct rpIdHash is returned.
+ let rpIdHashSign = authenticatorData.slice(0, 32);
+ ok(memcmp(rpIdHash, rpIdHashSign), "rpIdHash is correct");
+
+ let clientData = JSON.parse(buffer2string(clientDataJSON));
+ is(clientData.clientExtensions.appid, appid, "appid extension sent");
+ }
+ );
+
+ // Close tab.
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_appid_unused() {
+ // Open a new tab.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ // Get a keyHandle for a FIDO AppId.
+ 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 });
+
+ // Check the we can parse clientDataJSON.
+ let clientData = JSON.parse(buffer2string(clientDataJSON));
+ ok(
+ "appid" in clientData.clientExtensions,
+ `since it was passed, appid field should appear in the client data, but ` +
+ `saw: ${JSON.stringify(clientData.clientExtensions)}`
+ );
+
+ 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,
+ "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);
+});
+
+add_task(function test_cleanup() {
+ Services.prefs.clearUserPref("security.webauth.u2f");
+ Services.prefs.clearUserPref("security.webauth.webauthn");
+ Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken");
+ Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken");
+});
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..ec4f39e02f
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js
@@ -0,0 +1,51 @@
+/* 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 expectSecurityError = expectError("Security");
+
+add_task(function test_setup() {
+ Services.prefs.setBoolPref("security.webauth.u2f", true);
+ Services.prefs.setBoolPref("security.webauth.webauthn", true);
+ Services.prefs.setBoolPref(
+ "security.webauth.webauthn_enable_android_fido2",
+ false
+ );
+ Services.prefs.setBoolPref(
+ "security.webauth.webauthn_enable_softtoken",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "security.webauth.webauthn_enable_usbtoken",
+ false
+ );
+ Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
+});
+
+registerCleanupFunction(async function() {
+ Services.prefs.clearUserPref("security.webauth.u2f");
+ Services.prefs.clearUserPref("security.webauth.webauthn");
+ Services.prefs.clearUserPref(
+ "security.webauth.webauthn_enable_android_fido2"
+ );
+ Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken");
+ Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken");
+ Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+});
+
+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, "none", {})
+ .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..3726453074
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js
@@ -0,0 +1,271 @@
+/* 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/";
+
+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();
+ }
+ });
+ });
+}
+
+function triggerMainPopupCommand(popup) {
+ info("triggering main command");
+ let notifications = popup.childNodes;
+ ok(notifications.length > 0, "at least one notification displayed");
+ let notification = notifications[0];
+ info("triggering command: " + notification.getAttribute("buttonlabel"));
+
+ return EventUtils.synthesizeMouseAtCenter(notification.button, {});
+}
+
+let expectAbortError = expectError("Abort");
+
+function verifyAnonymizedCertificate(result) {
+ let { attObj, rawId } = result;
+ return webAuthnDecodeCBORAttestation(attObj).then(({ fmt, attStmt }) => {
+ is("none", fmt, "Is a None Attestation");
+ is("object", typeof attStmt, "attStmt is a map");
+ is(0, Object.keys(attStmt).length, "attStmt is empty");
+ });
+}
+
+function verifyDirectCertificate(result) {
+ let { attObj, rawId } = result;
+ return webAuthnDecodeCBORAttestation(attObj).then(({ fmt, attStmt }) => {
+ is("fido-u2f", fmt, "Is a FIDO U2F Attestation");
+ is("object", typeof attStmt, "attStmt is a map");
+ ok(attStmt.hasOwnProperty("x5c"), "attStmt.x5c exists");
+ ok(attStmt.hasOwnProperty("sig"), "attStmt.sig exists");
+ });
+}
+
+add_task(async function test_setup_usbtoken() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.webauth.u2f", false],
+ ["security.webauth.webauthn", true],
+ ["security.webauth.webauthn_enable_softtoken", false],
+ ["security.webauth.webauthn_enable_android_fido2", false],
+ ["security.webauth.webauthn_enable_usbtoken", true],
+ ],
+ });
+});
+
+add_task(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, "indirect", {})
+ .then(arrivingHereIsBad)
+ .catch(expectAbortError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-register");
+
+ // Cancel the request.
+ ok(active, "request should still be active");
+ PopupNotifications.panel.firstElementChild.button.click();
+ await request;
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(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(expectAbortError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-sign");
+
+ // Cancel the request.
+ ok(active, "request should still be active");
+ PopupNotifications.panel.firstElementChild.button.click();
+ await request;
+
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(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(expectAbortError)
+ .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);
+});
+
+// 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.
+add_task(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, "indirect", {})
+ .then(arrivingHereIsBad)
+ .catch(expectAbortError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-register");
+ 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-register");
+
+ 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.
+add_task(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, "indirect", {})
+ .then(arrivingHereIsBad)
+ .catch(expectAbortError)
+ .then(() => (active = false));
+ await promiseNotification("webauthn-prompt-register");
+
+ 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);
+});
+
+add_task(async function test_setup_softtoken() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.webauth.u2f", false],
+ ["security.webauth.webauthn", true],
+ ["security.webauth.webauthn_enable_softtoken", true],
+ ["security.webauth.webauthn_enable_usbtoken", false],
+ ],
+ });
+});
+
+add_task(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);
+});
+
+add_task(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);
+});
diff --git a/dom/webauthn/tests/browser/browser_webauthn_telemetry.js b/dom/webauthn/tests/browser/browser_webauthn_telemetry.js
new file mode 100644
index 0000000000..09b8c54e99
--- /dev/null
+++ b/dom/webauthn/tests/browser/browser_webauthn_telemetry.js
@@ -0,0 +1,144 @@
+/* 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";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "TelemetryTestUtils",
+ "resource://testing-common/TelemetryTestUtils.jsm"
+);
+
+const TEST_URL = "https://example.com/";
+
+function getTelemetryForScalar(aName) {
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ return scalars[aName] || 0;
+}
+
+function cleanupTelemetry() {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ Services.telemetry.getHistogramById("WEBAUTHN_CREATE_CREDENTIAL_MS").clear();
+ Services.telemetry.getHistogramById("WEBAUTHN_GET_ASSERTION_MS").clear();
+}
+
+function validateHistogramEntryCount(aHistogramName, aExpectedCount) {
+ let hist = Services.telemetry.getHistogramById(aHistogramName);
+ let resultIndexes = hist.snapshot();
+
+ let entriesSeen = Object.values(resultIndexes.values).reduce(
+ (a, b) => a + b,
+ 0
+ );
+
+ is(
+ entriesSeen,
+ aExpectedCount,
+ "Expecting " + aExpectedCount + " histogram entries in " + aHistogramName
+ );
+}
+
+add_task(async function test_setup() {
+ cleanupTelemetry();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.webauth.webauthn", true],
+ ["security.webauth.webauthn_enable_softtoken", true],
+ ["security.webauth.webauthn_enable_usbtoken", false],
+ ["security.webauth.webauthn_enable_android_fido2", false],
+ ["security.webauth.webauthn_testing_allow_direct_attestation", true],
+ ],
+ });
+});
+
+add_task(async function test() {
+ // These tests can't run simultaneously as the preference changes will race.
+ // So let's run them sequentially here, but in an async function so we can
+ // use await.
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+
+ // Create a new credential.
+ 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,
+ } = await promiseWebAuthnGetAssertion(tab, rawId);
+
+ // Check the we can parse clientDataJSON.
+ JSON.parse(buffer2string(clientDataJSON));
+
+ // Check auth data.
+ let attestation = await webAuthnDecodeAuthDataArray(
+ new Uint8Array(authenticatorData)
+ );
+ is(
+ "" + attestation.flags,
+ "" + 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");
+
+ // Check telemetry data.
+ let webauthn_used = getTelemetryForScalar("security.webauthn_used");
+ ok(
+ webauthn_used,
+ "Scalar keys are set: " + Object.keys(webauthn_used).join(", ")
+ );
+ is(
+ webauthn_used.U2FRegisterFinish,
+ 1,
+ "webauthn_used U2FRegisterFinish scalar should be 1"
+ );
+ is(
+ webauthn_used.U2FSignFinish,
+ 1,
+ "webauthn_used U2FSignFinish scalar should be 1"
+ );
+ is(
+ webauthn_used.U2FSignAbort,
+ undefined,
+ "webauthn_used U2FSignAbort scalar must be unset"
+ );
+ is(
+ webauthn_used.U2FRegisterAbort,
+ undefined,
+ "webauthn_used U2FRegisterAbort scalar must be unset"
+ );
+
+ validateHistogramEntryCount("WEBAUTHN_CREATE_CREDENTIAL_MS", 1);
+ validateHistogramEntryCount("WEBAUTHN_GET_ASSERTION_MS", 1);
+
+ BrowserTestUtils.removeTab(tab);
+
+ // There aren't tests for register succeeding and sign failing, as I don't see an easy way to prompt
+ // the soft token to fail that way _and_ trigger the Abort telemetry.
+});
diff --git a/dom/webauthn/tests/browser/head.js b/dom/webauthn/tests/browser/head.js
new file mode 100644
index 0000000000..70ea10c526
--- /dev/null
+++ b/dom/webauthn/tests/browser/head.js
@@ -0,0 +1,155 @@
+/* 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 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",
+ extensions = {}
+) {
+ return ContentTask.spawn(
+ tab.linkedBrowser,
+ [attestation, extensions],
+ ([attestation, 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", icon: "none" },
+ user: {
+ id: new Uint8Array(),
+ name: "none",
+ icon: "none",
+ displayName: "none",
+ },
+ pubKeyCredParams,
+ extensions,
+ attestation,
+ challenge,
+ };
+
+ return content.navigator.credentials
+ .create({ publicKey })
+ .then(credential => {
+ return {
+ 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 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.");
+ }
+ });
+}
+/* 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>