summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/tests/u2f
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webauthn/tests/u2f')
-rw-r--r--dom/webauthn/tests/u2f/browser/browser.ini32
-rw-r--r--dom/webauthn/tests/u2f/browser/browser_abort_visibility.js274
-rw-r--r--dom/webauthn/tests/u2f/browser/browser_fido_appid_extension.js153
-rw-r--r--dom/webauthn/tests/u2f/browser/browser_webauthn_ipaddress.js28
-rw-r--r--dom/webauthn/tests/u2f/browser/browser_webauthn_prompts.js276
-rw-r--r--dom/webauthn/tests/u2f/browser/browser_webauthn_telemetry.js142
-rw-r--r--dom/webauthn/tests/u2f/browser/head.js155
-rw-r--r--dom/webauthn/tests/u2f/browser/tab_webauthn_result.html14
-rw-r--r--dom/webauthn/tests/u2f/get_assertion_dead_object.html21
-rw-r--r--dom/webauthn/tests/u2f/mochitest.ini76
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_abort_signal.html146
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_attestation_conveyance.html126
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_authenticator_selection.html146
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_authenticator_transports.html150
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_get_assertion.html253
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_get_assertion_dead_object.html29
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_isexternalctap2securitykeysupported.html33
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_isplatformauthenticatoravailable.html40
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_loopback.html213
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_make_credential.html387
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_no_token.html87
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_override_request.html96
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_sameorigin.html316
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_sameoriginwithancestors.html107
-rw-r--r--dom/webauthn/tests/u2f/test_webauthn_store_credential.html53
25 files changed, 3353 insertions, 0 deletions
diff --git a/dom/webauthn/tests/u2f/browser/browser.ini b/dom/webauthn/tests/u2f/browser/browser.ini
new file mode 100644
index 0000000000..2145f61ecc
--- /dev/null
+++ b/dom/webauthn/tests/u2f/browser/browser.ini
@@ -0,0 +1,32 @@
+[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_android_fido2=false
+ security.webauth.webauthn_enable_usbtoken=false
+ security.webauthn.ctap2=false
+
+[browser_abort_visibility.js]
+skip-if =
+ win10_2004 # Test not relevant on 1903+
+ win11_2009 # Test not relevant on 1903+
+[browser_fido_appid_extension.js]
+skip-if =
+ win10_2004 # Test not relevant on 1903+
+ win11_2009 # Test not relevant on 1903+
+[browser_webauthn_prompts.js]
+skip-if =
+ win10_2004 # Test not relevant on 1903+
+ win11_2009 # Test not relevant on 1903+
+[browser_webauthn_telemetry.js]
+skip-if =
+ win10_2004 # Test not relevant on 1903+
+ win11_2009 # Test not relevant on 1903+
+ fission && os == "linux" && asan # Bug 1713907 - new Fission platform triage
+[browser_webauthn_ipaddress.js]
diff --git a/dom/webauthn/tests/u2f/browser/browser_abort_visibility.js b/dom/webauthn/tests/u2f/browser/browser_abort_visibility.js
new file mode 100644
index 0000000000..059a9de0b3
--- /dev/null
+++ b/dom/webauthn/tests/u2f/browser/browser_abort_visibility.js
@@ -0,0 +1,274 @@
+/* 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", 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";
+ });
+}
+
+// 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.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.closeWindow(win);
+ await windowBackPromise;
+}
diff --git a/dom/webauthn/tests/u2f/browser/browser_fido_appid_extension.js b/dom/webauthn/tests/u2f/browser/browser_fido_appid_extension.js
new file mode 100644
index 0000000000..3988e01b87
--- /dev/null
+++ b/dom/webauthn/tests/u2f/browser/browser_fido_appid_extension.js
@@ -0,0 +1,153 @@
+/* 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(async function test_setup_u2f() {
+ return SpecialPowers.pushPrefEnv({
+ set: [["security.webauth.u2f", true]],
+ });
+});
+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");
+ }
+ );
+
+ // 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 });
+
+ 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);
+});
diff --git a/dom/webauthn/tests/u2f/browser/browser_webauthn_ipaddress.js b/dom/webauthn/tests/u2f/browser/browser_webauthn_ipaddress.js
new file mode 100644
index 0000000000..2c3f8ea025
--- /dev/null
+++ b/dom/webauthn/tests/u2f/browser/browser_webauthn_ipaddress.js
@@ -0,0 +1,28 @@
+/* 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(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, "none", {})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+
+ // Close tab.
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/webauthn/tests/u2f/browser/browser_webauthn_prompts.js b/dom/webauthn/tests/u2f/browser/browser_webauthn_prompts.js
new file mode 100644
index 0000000000..fbf7585896
--- /dev/null
+++ b/dom/webauthn/tests/u2f/browser/browser_webauthn_prompts.js
@@ -0,0 +1,276 @@
+/* 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/";
+
+add_task(async function test_setup_usbtoken() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["security.webauth.u2f", false],
+ ["security.webauth.webauthn", true],
+ ["security.webauth.webauthn_enable_softtoken", false],
+ ["security.webauth.webauthn_enable_usbtoken", true],
+ ],
+ });
+});
+add_task(test_register);
+add_task(test_sign);
+add_task(test_register_direct_cancel);
+add_task(test_tab_switching);
+add_task(test_window_switching);
+add_task(async function test_setup_softtoken() {
+ return 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(test_register_direct_proceed);
+add_task(test_register_direct_proceed_anon);
+
+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, "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(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");
+ });
+}
+
+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, "none", {})
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .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);
+}
+
+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-sign");
+
+ // Cancel the request.
+ ok(active, "request should still be active");
+ PopupNotifications.panel.firstElementChild.button.click();
+ 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);
+}
+
+// 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, "none", {})
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .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.
+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, "none", {})
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError)
+ .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);
+}
+
+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);
+}
diff --git a/dom/webauthn/tests/u2f/browser/browser_webauthn_telemetry.js b/dom/webauthn/tests/u2f/browser/browser_webauthn_telemetry.js
new file mode 100644
index 0000000000..92af3c8a50
--- /dev/null
+++ b/dom/webauthn/tests/u2f/browser/browser_webauthn_telemetry.js
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+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/u2f/browser/head.js b/dom/webauthn/tests/u2f/browser/head.js
new file mode 100644
index 0000000000..70ea10c526
--- /dev/null
+++ b/dom/webauthn/tests/u2f/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/u2f/browser/tab_webauthn_result.html b/dom/webauthn/tests/u2f/browser/tab_webauthn_result.html
new file mode 100644
index 0000000000..8e8b9f82cd
--- /dev/null
+++ b/dom/webauthn/tests/u2f/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>
diff --git a/dom/webauthn/tests/u2f/get_assertion_dead_object.html b/dom/webauthn/tests/u2f/get_assertion_dead_object.html
new file mode 100644
index 0000000000..e7de9d3deb
--- /dev/null
+++ b/dom/webauthn/tests/u2f/get_assertion_dead_object.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+</head>
+<body>
+<script type="text/javascript">
+ window.addEventListener('load', function() {
+ let o = [];
+ o[0] = window.navigator;
+ document.writeln('');
+ // Since the USB token is enabled by default, this will pop up a notification that the
+ // user can insert/interact with it. Since this is just a test, this won't happen. The
+ // request will eventually time out.
+ // Unfortunately the minimum timeout is 15 seconds.
+ o[0].credentials.get({ publicKey: { challenge: new Uint8Array(128), timeout: 15000 } });
+ o.forEach((n, i) => o[i] = null);
+ });
+</script>
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/mochitest.ini b/dom/webauthn/tests/u2f/mochitest.ini
new file mode 100644
index 0000000000..add3bde37c
--- /dev/null
+++ b/dom/webauthn/tests/u2f/mochitest.ini
@@ -0,0 +1,76 @@
+[DEFAULT]
+support-files =
+ ../cbor.js
+ ../u2futil.js
+ ../pkijs/*
+ get_assertion_dead_object.html
+scheme = https
+prefs =
+ security.webauth.webauthn=true
+ security.webauth.webauthn_enable_softtoken=true
+ security.webauth.webauthn_enable_android_fido2=false
+ security.webauth.webauthn_enable_usbtoken=false
+ security.webauthn.ctap2=false
+
+[test_webauthn_abort_signal.html]
+fail-if = xorigin
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_attestation_conveyance.html]
+fail-if = xorigin # NotAllowedError
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_authenticator_selection.html]
+fail-if = xorigin # NotAllowedError
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_authenticator_transports.html]
+fail-if = xorigin # NotAllowedError
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_loopback.html]
+skip-if =
+ xorigin # Hangs, JavaScript error: https://example.org/tests/SimpleTest/SimpleTest.js, line 76: DataCloneError: The object could not be cloned.
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_no_token.html]
+skip-if =
+ xorigin # JavaScript error: https://example.org/tests/SimpleTest/SimpleTest.js, line 76: DataCloneError: The object could not be cloned.
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_make_credential.html]
+fail-if = xorigin # NotAllowedError
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_get_assertion.html]
+fail-if = xorigin # NotAllowedError
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_get_assertion_dead_object.html]
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_override_request.html]
+[test_webauthn_store_credential.html]
+fail-if = xorigin # NotAllowedError
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_sameorigin.html]
+fail-if = xorigin # NotAllowedError
+skip-if =
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_sameoriginwithancestors.html]
+skip-if =
+ xorigin # this test has its own cross-origin setup
+ win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+ win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.)
+[test_webauthn_isplatformauthenticatoravailable.html]
+[test_webauthn_isexternalctap2securitykeysupported.html]
diff --git a/dom/webauthn/tests/u2f/test_webauthn_abort_signal.html b/dom/webauthn/tests/u2f/test_webauthn_abort_signal.html
new file mode 100644
index 0000000000..95e347ae61
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_abort_signal.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for aborting W3C Web Authentication request</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>Test for aborting W3C Web Authentication request</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ }
+
+ function expectAbortError(aResult) {
+ is(aResult.code, DOMException.ABORT_ERR, "Expecting an AbortError");
+ }
+
+ add_task(() => {
+ // Enable USB tokens.
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["security.webauth.webauthn_enable_softtoken", false],
+ ["security.webauth.webauthn_enable_usbtoken", true],
+ ]});
+ });
+
+ // Start a new MakeCredential() request.
+ function requestMakeCredential(signal) {
+ let publicKey = {
+ rp: {id: document.domain, name: "none", icon: "none"},
+ user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
+ };
+
+ return navigator.credentials.create({publicKey, signal});
+ }
+
+ // Start a new GetAssertion() request.
+ async function requestGetAssertion(signal) {
+ let newCredential = {
+ type: "public-key",
+ id: crypto.getRandomValues(new Uint8Array(16)),
+ transports: ["usb"],
+ };
+
+ let publicKey = {
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ rpId: document.domain,
+ allowCredentials: [newCredential]
+ };
+
+ // Start the request, handle failures only.
+ return navigator.credentials.get({publicKey, signal});
+ }
+
+ // Create an AbortController and abort immediately.
+ add_task(async function test_create_abortcontroller_and_abort() {
+ let ctrl = new AbortController();
+ ctrl.abort();
+
+ // The event shouldn't fire.
+ ctrl.signal.onabort = arrivingHereIsBad;
+
+ // MakeCredential() should abort immediately.
+ await requestMakeCredential(ctrl.signal)
+ .then(arrivingHereIsBad)
+ .catch(expectAbortError);
+
+ // GetAssertion() should abort immediately.
+ await requestGetAssertion(ctrl.signal)
+ .then(arrivingHereIsBad)
+ .catch(expectAbortError);
+ });
+
+ // Request a new credential and abort the request.
+ add_task(async function test_request_credential_and_abort() {
+ let aborted = false;
+ let ctrl = new AbortController();
+
+ ctrl.signal.onabort = () => {
+ ok(!aborted, "abort event fired once");
+ aborted = true;
+ };
+
+ // Request a new credential.
+ let request = requestMakeCredential(ctrl.signal)
+ .then(arrivingHereIsBad)
+ .catch(err => {
+ ok(aborted, "abort event was fired");
+ expectAbortError(err);
+ });
+
+ // Wait a tick for the statemachine to start.
+ await Promise.resolve();
+
+ // Abort the request.
+ ok(!aborted, "request still pending");
+ ctrl.abort();
+ ok(aborted, "request aborted");
+
+ // Wait for the request to terminate.
+ await request;
+ });
+
+ // Request a new assertion and abort the request.
+ add_task(async function test_request_assertion_and_abort() {
+ let aborted = false;
+ let ctrl = new AbortController();
+
+ ctrl.signal.onabort = () => {
+ ok(!aborted, "abort event fired once");
+ aborted = true;
+ };
+
+ // Request a new assertion.
+ let request = requestGetAssertion(ctrl.signal)
+ .then(arrivingHereIsBad)
+ .catch(err => {
+ ok(aborted, "abort event was fired");
+ expectAbortError(err);
+ });
+
+ // Wait a tick for the statemachine to start.
+ await Promise.resolve();
+
+ // Abort the request.
+ ok(!aborted, "request still pending");
+ ctrl.abort();
+ ok(aborted, "request aborted");
+
+ // Wait for the request to terminate.
+ await request;
+ });
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_attestation_conveyance.html b/dom/webauthn/tests/u2f/test_webauthn_attestation_conveyance.html
new file mode 100644
index 0000000000..d173e6db90
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_attestation_conveyance.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>W3C Web Authentication - Attestation Conveyance</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <script type="text/javascript" src="../cbor.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>W3C Web Authentication - Attestation Conveyance</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1428916">Mozilla Bug 1428916</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1416056">Mozilla Bug 1416056</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ add_task(() => {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["security.webauth.webauthn_testing_allow_direct_attestation", true],
+ ]});
+ });
+
+ function getAttestationCertFromAttestationBuffer(aAttestationBuffer) {
+ return webAuthnDecodeCBORAttestation(aAttestationBuffer)
+ .then((aAttestationObj) => {
+ is(aAttestationObj.fmt, "fido-u2f", "Is a FIDO U2F Attestation");
+ let attestationCertDER = aAttestationObj.attStmt.x5c[0];
+ let certDERBuffer = attestationCertDER.slice(0, attestationCertDER.byteLength).buffer;
+ let certAsn1 = org.pkijs.fromBER(certDERBuffer);
+ return new org.pkijs.simpl.CERT({ schema: certAsn1.result });
+ });
+ }
+
+ function verifyAnonymizedCertificate(aResult) {
+ return webAuthnDecodeCBORAttestation(aResult.response.attestationObject)
+ .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");
+ });
+ }
+
+ function verifyDirectCertificate(aResult) {
+ return getAttestationCertFromAttestationBuffer(aResult.response.attestationObject)
+ .then((attestationCert) => {
+ let subject = attestationCert.subject.types_and_values[0].value.value_block.value;
+ is(subject, "Firefox U2F Soft Token", "Subject name matches the direct Soft Token")
+ });
+ }
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ }
+
+ function expectTypeError(aResult) {
+ ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError, got " + aResult);
+ }
+
+ // Start a new MakeCredential() request.
+ function requestMakeCredential(attestation) {
+ let publicKey = {
+ rp: {id: document.domain, name: "none", icon: "none"},
+ user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
+ attestation,
+ };
+
+ return navigator.credentials.create({publicKey});
+ }
+
+ // Test success cases for make credential.
+ add_task(async function test_make_credential_success () {
+ // No selection criteria should be equal to none, which means anonymized
+ await requestMakeCredential()
+ .then(verifyAnonymizedCertificate)
+ .catch(arrivingHereIsBad);
+
+ // Request no attestation.
+ await requestMakeCredential("none")
+ .then(verifyAnonymizedCertificate)
+ .catch(arrivingHereIsBad);
+
+ // Request indirect attestation, which is the same as direct.
+ await requestMakeCredential("indirect")
+ .then((x) => {
+ if (AppConstants.platform === "android") {
+ // If this is Android, the result will be anonymized (Bug 1551229)
+ return verifyAnonymizedCertificate(x);
+ } else {
+ return verifyDirectCertificate(x);
+ }
+ })
+ .catch(arrivingHereIsBad);
+
+ // Request direct attestation, which will prompt for user intervention.
+ await requestMakeCredential("direct")
+ .then((x) => {
+ if (AppConstants.platform === "android") {
+ // If this is Android, the result will be anonymized (Bug 1551229)
+ return verifyAnonymizedCertificate(x);
+ } else {
+ return verifyDirectCertificate(x);
+ }
+ })
+ .catch(arrivingHereIsBad);
+ });
+
+ // Test failure cases for make credential.
+ add_task(async function test_make_credential_failures() {
+ // Request a platform authenticator.
+ await requestMakeCredential("unknown")
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ });
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_authenticator_selection.html b/dom/webauthn/tests/u2f/test_webauthn_authenticator_selection.html
new file mode 100644
index 0000000000..cd07df3e01
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_authenticator_selection.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>W3C Web Authentication - Authenticator Selection Criteria</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>W3C Web Authentication - Authenticator Selection Criteria</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406462">Mozilla Bug 1406462</a>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406467">Mozilla Bug 1406467</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ function arrivingHereIsGood(aResult) {
+ ok(true, "Good result! Received a: " + aResult);
+ }
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ }
+
+ function expectNotAllowedError(aResult) {
+ ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult);
+ }
+
+ // We store the credential of the first successful make credential
+ // operation so we can use it for get assertion tests later.
+ let gCredential;
+
+ // Start a new MakeCredential() request.
+ function requestMakeCredential(authenticatorSelection) {
+ let publicKey = {
+ rp: {id: document.domain, name: "none", icon: "none"},
+ user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
+ authenticatorSelection,
+ };
+
+ return navigator.credentials.create({publicKey});
+ }
+
+ // Start a new GetAssertion() request.
+ function requestGetAssertion(userVerification) {
+ let newCredential = {
+ type: "public-key",
+ id: gCredential,
+ transports: ["usb"],
+ };
+
+ let publicKey = {
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ rpId: document.domain,
+ allowCredentials: [newCredential]
+ };
+
+ if (userVerification) {
+ publicKey.userVerification = userVerification;
+ }
+
+ return navigator.credentials.get({publicKey});
+ }
+
+ // Test success cases for make credential.
+ add_task(async function test_make_credential_successes() {
+ // No selection criteria.
+ await requestMakeCredential({})
+ // Save the credential so we can use it for sign success tests.
+ .then(res => gCredential = res.rawId)
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Request a cross-platform authenticator.
+ await requestMakeCredential({authenticatorAttachment: "cross-platform"})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Don't require a resident key.
+ await requestMakeCredential({requireResidentKey: false})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Prefer user verification.
+ await requestMakeCredential({userVerification: "preferred"})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Discourage user verification.
+ await requestMakeCredential({userVerification: "discouraged"})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ });
+
+ // Test success cases for get assertion.
+ add_task(async function test_get_assertion_successes() {
+ // No selection criteria.
+ await requestGetAssertion()
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Prefer user verification.
+ await requestGetAssertion("preferred")
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Discourage user verification.
+ await requestGetAssertion("discouraged")
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ });
+
+ // Test failure cases for make credential.
+ add_task(async function test_make_credential_failures() {
+ // Request a platform authenticator.
+ await requestMakeCredential({authenticatorAttachment: "platform"})
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError);
+
+ // Require a resident key.
+ await requestMakeCredential({requireResidentKey: true})
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError);
+
+ // Require user verification.
+ await requestMakeCredential({userVerification: "required"})
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError);
+ });
+
+ // Test failure cases for get assertion.
+ add_task(async function test_get_assertion_failures() {
+ // Require user verification.
+ await requestGetAssertion("required")
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError);
+ });
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_authenticator_transports.html b/dom/webauthn/tests/u2f/test_webauthn_authenticator_transports.html
new file mode 100644
index 0000000000..ffd74ebab3
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_authenticator_transports.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>W3C Web Authentication - Authenticator Transports</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>W3C Web Authentication - Authenticator Transports</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406467">Mozilla Bug 1406467</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ function arrivingHereIsGood(aResult) {
+ ok(true, "Good result! Received a: " + aResult);
+ }
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ }
+
+ function expectNotAllowedError(aResult) {
+ ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult);
+ }
+
+ function expectInvalidStateError(aResult) {
+ ok(aResult.toString().startsWith("InvalidStateError"), "Expecting a InvalidStateError, got " + aResult);
+ }
+
+ // Store the credential of the first successful make credential
+ // operation so we can use it to get assertions later.
+ let gCredential;
+
+ // Start a new MakeCredential() request.
+ function requestMakeCredential(excludeCredentials) {
+ let publicKey = {
+ rp: {id: document.domain, name: "none", icon: "none"},
+ user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
+ excludeCredentials
+ };
+
+ return navigator.credentials.create({publicKey});
+ }
+
+ // Start a new GetAssertion() request.
+ function requestGetAssertion(allowCredentials) {
+ let publicKey = {
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ rpId: document.domain,
+ allowCredentials
+ };
+
+ return navigator.credentials.get({publicKey});
+ }
+
+ // Test make credential behavior.
+ add_task(async function test_make_credential() {
+ // Make a credential.
+ await requestMakeCredential([])
+ // Save the credential for later.
+ .then(res => gCredential = res.rawId)
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Pass a random credential to exclude.
+ await requestMakeCredential([{
+ type: "public-key",
+ id: crypto.getRandomValues(new Uint8Array(16)),
+ transports: ["usb"],
+ }]).then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Pass gCredential with transport=usb.
+ // The credential already exists, and the softoken consents to create,
+ // so the error is InvalidState and not NotAllowed.
+ await requestMakeCredential([{
+ type: "public-key",
+ id: gCredential,
+ transports: ["usb"],
+ }]).then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+
+ // Pass gCredential with transport=nfc.
+ // The softoken pretends to support all transports.
+ // Also, as above, the credential exists and the token indicates consent.
+ await requestMakeCredential([{
+ type: "public-key",
+ id: gCredential,
+ transports: ["nfc"],
+ }]).then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+
+ // Pass gCredential with an empty transports list.
+ // As above, the token indicates consent, so expect InvalidStateError.
+ await requestMakeCredential([{
+ type: "public-key",
+ id: gCredential,
+ transports: [],
+ }]).then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+ });
+
+ // Test get assertion behavior.
+ add_task(async function test_get_assertion() {
+ // Request an assertion for gCredential.
+ await requestGetAssertion([{
+ type: "public-key",
+ id: gCredential,
+ transports: ["usb"],
+ }]).then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Request an assertion for a random credential. The token indicates
+ // consent even though this credential doesn't exist, so expect an
+ // InvalidStateError.
+ await requestGetAssertion([{
+ type: "public-key",
+ id: crypto.getRandomValues(new Uint8Array(16)),
+ transports: ["usb"],
+ }]).then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+
+ // Request an assertion for gCredential with transport=nfc.
+ // The softoken pretends to support all transports.
+ await requestGetAssertion([{
+ type: "public-key",
+ id: gCredential,
+ transports: ["nfc"],
+ }]).then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+
+ // Request an assertion for gCredential with an empty transports list.
+ await requestGetAssertion([{
+ type: "public-key",
+ id: gCredential,
+ transports: [],
+ }]).then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ });
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_get_assertion.html b/dom/webauthn/tests/u2f/test_webauthn_get_assertion.html
new file mode 100644
index 0000000000..b595b402ae
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_get_assertion.html
@@ -0,0 +1,253 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Tests for GetAssertion for W3C Web Authentication</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>Tests for GetAssertion for W3C Web Authentication</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ is(navigator.authentication, undefined, "navigator.authentication does not exist any longer");
+ isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist");
+ isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist");
+ isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
+
+ let gAssertionChallenge = new Uint8Array(16);
+ window.crypto.getRandomValues(gAssertionChallenge);
+
+ let invalidCred = {type: "Magic", id: base64ToBytes("AAA=")};
+ let unknownCred = {type: "public-key", id: base64ToBytes("AAA=")};
+ let validCred = null;
+
+ add_task(test_setup_valid_credential);
+ add_task(test_without_credential);
+ add_task(test_with_credential);
+ add_task(test_unexpected_option);
+ add_task(test_unexpected_option_with_credential);
+ add_task(test_unexpected_transport);
+ add_task(test_invalid_credential);
+ add_task(test_unknown_credential);
+ add_task(test_too_many_credentials);
+ add_task(test_unexpected_option_invalid_credential);
+ add_task(test_empty_credential_list);
+ add_task(() => {
+ // Enable USB tokens.
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["security.webauth.webauthn_enable_softtoken", false],
+ ["security.webauth.webauthn_enable_usbtoken", true],
+ ]});
+ });
+ add_task(test_usb_empty_credential_list);
+
+ function requestGetAssertion(params) {
+ return navigator.credentials.get(params);
+ }
+
+ function arrivingHereIsGood(aResult) {
+ ok(true, "Good result! Received a: " + aResult);
+ }
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ }
+
+ function expectNotAllowedError(aResult) {
+ ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult);
+ }
+
+ function expectInvalidStateError(aResult) {
+ ok(aResult.toString().startsWith("InvalidStateError"), "Expecting a InvalidStateError, got " + aResult);
+ }
+
+ function expectTypeError(aResult) {
+ ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError, got " + aResult);
+ }
+
+ function expectSecurityError(aResult) {
+ ok(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError, got " + aResult);
+ }
+
+ function expectAbortError(aResult) {
+ is(aResult.code, DOMException.ABORT_ERR, "Expecting an AbortError");
+ }
+
+ // Set up a valid credential
+ async function test_setup_valid_credential() {
+ let publicKey = {
+ rp: {id: document.domain, name: "none", icon: "none"},
+ user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
+ };
+
+ return navigator.credentials.create({publicKey})
+ .then(res => validCred = {type: "public-key", id: res.rawId} );
+ }
+
+ // Test basic good call, but without giving a credential so expect failures
+ // this is OK by the standard, but not supported by U2F-backed authenticators
+ // like the soft token in use here.
+ async function test_without_credential() {
+ let publicKey = {
+ challenge: gAssertionChallenge
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+ }
+
+ // Test with a valid credential
+ async function test_with_credential() {
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ allowCredentials: [validCred]
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+
+ // Test with an unexpected option. That won't stop anything, and we'll
+ // fail with InvalidState just as if we had no valid credentials -- which
+ // we don't.
+ async function test_unexpected_option() {
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ unknownValue: "hi"
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+ }
+
+ // Test with an unexpected option but a valid credential
+ async function test_unexpected_option_with_credential() {
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ unknownValue: "hi",
+ allowCredentials: [validCred]
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+
+ // Test with an unexpected transport on a valid credential
+ async function test_unexpected_transport() {
+ let cred = validCred;
+ cred.transports = ["unknown", "usb"];
+
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ unknownValue: "hi",
+ allowCredentials: [cred]
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+
+ // Test with an invalid credential
+ async function test_invalid_credential() {
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ allowCredentials: [invalidCred]
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with an unknown credential
+ async function test_unknown_credential() {
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ allowCredentials: [unknownCred]
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+ }
+
+ // Test with too many credentials
+ async function test_too_many_credentials() {
+ let tooManyCredentials = Array(21).fill(validCred);
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ allowCredentials: tooManyCredentials,
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+
+ // Test with an unexpected option and an invalid credential
+ async function test_unexpected_option_invalid_credential() {
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ unknownValue: "hi",
+ allowCredentials: [invalidCred]
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with an empty credential list
+ // This will return InvalidStateError since the softotken consents, but
+ // there are no valid credentials.
+ async function test_empty_credential_list() {
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ allowCredentials: []
+ };
+
+ await requestGetAssertion({publicKey})
+ .then(arrivingHereIsBad)
+ .catch(expectInvalidStateError);
+ }
+
+ // Test with an empty credential list
+ async function test_usb_empty_credential_list() {
+ let publicKey = {
+ challenge: gAssertionChallenge,
+ allowCredentials: []
+ };
+
+ let ctrl = new AbortController();
+ let request = requestGetAssertion({publicKey, signal: ctrl.signal})
+ .then(arrivingHereIsBad)
+ .catch(expectAbortError);
+
+ // Wait a tick for the statemachine to start.
+ await Promise.resolve();
+
+ // The request should time out. We'll abort it here and will fail or
+ // succeed upon resolution, when the error code is checked.
+ ctrl.abort();
+ await request;
+ }
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_get_assertion_dead_object.html b/dom/webauthn/tests/u2f/test_webauthn_get_assertion_dead_object.html
new file mode 100644
index 0000000000..18a4f512f0
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_get_assertion_dead_object.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for GetAssertion on dead object</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>Test for GetAssertion on dead object</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1483905">Mozilla Bug 1483905</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout(
+ "Due to the nature of this test, there's no way for the window we're opening to signal " +
+ "that it's done (the `document.writeln('')` is essential and basically clears any state " +
+ "we could use). So, we have to wait at least 15 seconds for the webauthn call to time out.");
+ let win = window.open("https://example.com/tests/dom/webauthn/tests/get_assertion_dead_object.html");
+ setTimeout(() => {
+ win.close();
+ SimpleTest.finish();
+ }, 20000);
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_isexternalctap2securitykeysupported.html b/dom/webauthn/tests/u2f/test_webauthn_isexternalctap2securitykeysupported.html
new file mode 100644
index 0000000000..e25225a123
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_isexternalctap2securitykeysupported.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for W3C Web Authentication isExternalCTAP2SecurityKeySupported</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Test for W3C Web Authentication isExternalCTAP2SecurityKeySupported</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1526023">Mozilla Bug 1526023</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Execute the full-scope test
+SimpleTest.waitForExplicitFinish();
+
+add_task(async function test_external_key_support() {
+ PublicKeyCredential.isExternalCTAP2SecurityKeySupported()
+ .then(aResult => ok(true, `Should always return either true or false: ${aResult}`))
+ .catch(aProblem => ok(false, `We shouldn't get here: ${aProblem}`))
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_isplatformauthenticatoravailable.html b/dom/webauthn/tests/u2f/test_webauthn_isplatformauthenticatoravailable.html
new file mode 100644
index 0000000000..dba00df656
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_isplatformauthenticatoravailable.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for W3C Web Authentication isUserVerifyingPlatformAuthenticatorAvailable</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Test for W3C Web Authentication isUserVerifyingPlatformAuthenticatorAvailable</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Execute the full-scope test
+SimpleTest.waitForExplicitFinish();
+
+add_task(async function test_is_platform_available() {
+ // This test ensures that isUserVerifyingPlatformAuthenticatorAvailable()
+ // is a callable method, but with the softtoken enabled, it's not useful to
+ // figure out what it actually returns, so we'll just make sure it runs.
+ await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
+ .then(function(aResult) {
+ ok(true, "Resolved: " + aResult);
+ })
+ .catch(function(aProblem) {
+ ok(false, "Problem encountered: " + aProblem);
+ });
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_loopback.html b/dom/webauthn/tests/u2f/test_webauthn_loopback.html
new file mode 100644
index 0000000000..a5c0ca097d
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_loopback.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <script type="text/javascript" src="../cbor.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// Execute the full-scope test
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["security.webauth.webauthn_testing_allow_direct_attestation", true]]},
+function() {
+is(navigator.authentication, undefined, "navigator.authentication does not exist any longer");
+isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist");
+isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist");
+isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
+
+ let credm = navigator.credentials;
+
+ let gCredentialChallenge = new Uint8Array(16);
+ window.crypto.getRandomValues(gCredentialChallenge);
+ let gAssertionChallenge = new Uint8Array(16);
+ window.crypto.getRandomValues(gAssertionChallenge);
+
+ testMakeCredential();
+
+ function decodeCreatedCredential(aCredInfo) {
+ /* PublicKeyCredential : Credential
+ - rawId: Key Handle buffer pulled from U2F Register() Response
+ - id: Key Handle buffer in base64url form, should == rawId
+ - type: Literal 'public-key'
+ - response : AuthenticatorAttestationResponse : AuthenticatorResponse
+ - attestationObject: CBOR object
+ - clientDataJSON: serialized JSON
+ */
+
+ is(aCredInfo.type, "public-key", "Credential type must be public-key")
+
+ ok(aCredInfo.rawId.byteLength > 0, "Key ID exists");
+ is(aCredInfo.id, bytesToBase64UrlSafe(aCredInfo.rawId), "Encoded Key ID and Raw Key ID match");
+
+ ok(aCredInfo.rawId === aCredInfo.rawId, "PublicKeyCredential.RawID is SameObject");
+ ok(aCredInfo.response === aCredInfo.response, "PublicKeyCredential.Response is SameObject");
+ ok(aCredInfo.response.clientDataJSON === aCredInfo.response.clientDataJSON, "PublicKeyCredential.Response.ClientDataJSON is SameObject");
+ ok(aCredInfo.response.attestationObject === aCredInfo.response.attestationObject, "PublicKeyCredential.Response.AttestationObject is SameObject");
+
+ let clientData = JSON.parse(buffer2string(aCredInfo.response.clientDataJSON));
+ is(clientData.challenge, bytesToBase64UrlSafe(gCredentialChallenge), "Challenge is correct");
+ is(clientData.origin, window.location.origin, "Origin is correct");
+ is(clientData.type, "webauthn.create", "Type is correct");
+
+ return webAuthnDecodeCBORAttestation(aCredInfo.response.attestationObject)
+ .then(function(aAttestationObj) {
+ // Make sure the RP ID hash matches what we calculate.
+ return crypto.subtle.digest("SHA-256", string2buffer(document.domain))
+ .then(function(calculatedRpIdHash) {
+ let calcHashStr = bytesToBase64UrlSafe(new Uint8Array(calculatedRpIdHash));
+ let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(aAttestationObj.authDataObj.rpIdHash));
+
+ is(calcHashStr, providedHashStr,
+ "Calculated RP ID hash must match what the browser derived.");
+ return Promise.resolve(aAttestationObj);
+ });
+ })
+ .then(function(aAttestationObj) {
+ ok(aAttestationObj.authDataObj.flags == (flag_TUP | flag_AT),
+ "User presence and Attestation Object must be the only flags set");
+
+ aCredInfo.clientDataObj = clientData;
+ aCredInfo.publicKeyHandle = aAttestationObj.authDataObj.publicKeyHandle;
+ aCredInfo.attestationObject = aAttestationObj.authDataObj.attestationAuthData;
+ return aCredInfo;
+ });
+}
+
+ function checkAssertionAndSigValid(aPublicKey, aAssertion) {
+ /* PublicKeyCredential : Credential
+ - rawId: ID of Credential from AllowList that succeeded
+ - id: Key Handle buffer in base64url form, should == rawId
+ - type: Literal 'public-key'
+ - response : AuthenticatorAssertionResponse : AuthenticatorResponse
+ - clientDataJSON: serialized JSON
+ - authenticatorData: RP ID Hash || U2F Sign() Response
+ - signature: U2F Sign() Response
+ */
+
+ is(aAssertion.type, "public-key", "Credential type must be public-key")
+
+ ok(aAssertion.rawId.byteLength > 0, "Key ID exists");
+ is(aAssertion.id, bytesToBase64UrlSafe(new Uint8Array(aAssertion.rawId)), "Encoded Key ID and Raw Key ID match");
+
+ ok(aAssertion.response.authenticatorData === aAssertion.response.authenticatorData, "AuthenticatorAssertionResponse.AuthenticatorData is SameObject");
+ ok(aAssertion.response.authenticatorData instanceof ArrayBuffer, "AuthenticatorAssertionResponse.AuthenticatorData is an ArrayBuffer");
+ ok(aAssertion.response.signature === aAssertion.response.signature, "AuthenticatorAssertionResponse.Signature is SameObject");
+ ok(aAssertion.response.signature instanceof ArrayBuffer, "AuthenticatorAssertionResponse.Signature is an ArrayBuffer");
+ ok(aAssertion.response.userHandle === null, "AuthenticatorAssertionResponse.UserHandle is null for u2f authenticators");
+
+ ok(aAssertion.response.authenticatorData.byteLength > 0, "Authenticator data exists");
+ let clientData = JSON.parse(buffer2string(aAssertion.response.clientDataJSON));
+ is(clientData.challenge, bytesToBase64UrlSafe(gAssertionChallenge), "Challenge is correct");
+ is(clientData.origin, window.location.origin, "Origin is correct");
+ is(clientData.type, "webauthn.get", "Type is correct");
+
+ return webAuthnDecodeAuthDataArray(aAssertion.response.authenticatorData)
+ .then(function(aAttestation) {
+ ok(new Uint8Array(aAttestation.flags) == flag_TUP, "User presence must be the only flag set");
+ is(aAttestation.counter.byteLength, 4, "Counter must be 4 bytes");
+ return deriveAppAndChallengeParam(window.location.host, aAssertion.response.clientDataJSON, aAttestation)
+ })
+ .then(function(aParams) {
+ console.log(aParams);
+ console.log("ClientData buffer: ", hexEncode(aAssertion.response.clientDataJSON));
+ console.log("ClientDataHash: ", hexEncode(aParams.challengeParam));
+ return assembleSignedData(aParams.appParam, aParams.attestation.flags,
+ aParams.attestation.counter, aParams.challengeParam);
+ })
+ .then(function(aSignedData) {
+ console.log(aPublicKey, aSignedData, aAssertion.response.signature);
+ return verifySignature(aPublicKey, aSignedData, aAssertion.response.signature);
+ })
+}
+
+ function testMakeCredential() {
+ let rp = {id: document.domain, name: "none", icon: "none"};
+ let user = {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"};
+ let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
+ let makeCredentialOptions = {
+ rp,
+ user,
+ challenge: gCredentialChallenge,
+ pubKeyCredParams: [param],
+ attestation: "direct"
+ };
+ credm.create({publicKey: makeCredentialOptions})
+ .then(decodeCreatedCredential)
+ .then(testMakeDuplicate)
+ .catch(function(aReason) {
+ ok(false, aReason);
+ SimpleTest.finish();
+ });
+}
+
+ function testMakeDuplicate(aCredInfo) {
+ let rp = {id: document.domain, name: "none", icon: "none"};
+ let user = {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"};
+ let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
+ let makeCredentialOptions = {
+ rp,
+ user,
+ challenge: gCredentialChallenge,
+ pubKeyCredParams: [param],
+ excludeCredentials: [{type: "public-key", id: new Uint8Array(aCredInfo.rawId),
+ transports: ["usb"]}]
+ };
+ credm.create({publicKey: makeCredentialOptions})
+ .then(function() {
+ // We should have errored here!
+ ok(false, "The excludeList didn't stop a duplicate being created!");
+ SimpleTest.finish();
+ })
+ .catch(function(aReason) {
+ ok(aReason.toString().startsWith("InvalidStateError"), "Expect InvalidStateError, got " + aReason);
+ testAssertion(aCredInfo);
+ });
+}
+
+ function testAssertion(aCredInfo) {
+ let newCredential = {
+ type: "public-key",
+ id: new Uint8Array(aCredInfo.rawId),
+ transports: ["usb"],
+ }
+
+ let publicKeyCredentialRequestOptions = {
+ challenge: gAssertionChallenge,
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ rpId: document.domain,
+ allowCredentials: [newCredential]
+ };
+ credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(function(aAssertion) {
+ /* Pass along the pubKey. */
+ return checkAssertionAndSigValid(aCredInfo.publicKeyHandle, aAssertion);
+ })
+ .then(function(aSigVerifyResult) {
+ ok(aSigVerifyResult, "Signing signature verified");
+ SimpleTest.finish();
+ })
+ .catch(function(reason) {
+ ok(false, "Signing signature invalid: " + reason);
+ SimpleTest.finish();
+ });
+}
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_make_credential.html b/dom/webauthn/tests/u2f/test_webauthn_make_credential.html
new file mode 100644
index 0000000000..9c5e5ec457
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_make_credential.html
@@ -0,0 +1,387 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for MakeCredential for W3C Web Authentication</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>Test for MakeCredential for W3C Web Authentication</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ is(navigator.authentication, undefined, "navigator.authentication does not exist any longer");
+ isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist");
+ isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist");
+ isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
+
+ let credm;
+ let gCredentialChallenge;
+ let rp;
+ let user;
+ let param;
+ let unsupportedParam;
+ let badParam;
+
+ // Setup test env
+ add_task(() => {
+ gCredentialChallenge = new Uint8Array(16);
+ window.crypto.getRandomValues(gCredentialChallenge);
+
+ rp = {id: document.domain, name: "none", icon: "none"};
+ user = {id: new Uint8Array(64), name: "none", icon: "none", displayName: "none"};
+ param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
+ unsupportedParam = {type: "public-key", alg: cose_alg_ECDSA_w_SHA512};
+ badParam = {type: "SimplePassword", alg: "MaxLength=2"};
+ credm = navigator.credentials;
+ });
+ // Add tests
+ add_task(test_good_call);
+ add_task(test_empty_account);
+ add_task(test_without_rp_name);
+ add_task(test_without_user_id);
+ add_task(test_without_user_name);
+ add_task(test_without_user_displayname);
+ add_task(test_user_too_large);
+ add_task(test_empty_parameters);
+ add_task(test_without_parameters);
+ add_task(test_unsupported_parameter);
+ add_task(test_unsupported_but_one_param);
+ add_task(test_one_bad_parameter);
+ add_task(test_one_bad_one_unsupported_param);
+ add_task(test_one_of_each_parameters);
+ add_task(test_without_challenge);
+ add_task(test_invalid_challenge);
+ add_task(test_duplicate_pub_key);
+ add_task(test_invalid_rp_id);
+ add_task(test_invalid_rp_id_2);
+ add_task(test_missing_rp);
+ add_task(test_incorrect_user_id_type);
+ add_task(test_missing_user);
+ add_task(test_complete_account);
+ add_task(test_too_large_user_id);
+ add_task(test_excluding_unknown_transports);
+
+ function arrivingHereIsGood(aResult) {
+ ok(true, "Good result! Received a: " + aResult);
+ return Promise.resolve();
+ }
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ return Promise.resolve();
+ }
+
+ function expectNotAllowedError(aResult) {
+ ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError");
+ return Promise.resolve();
+ }
+
+ function expectTypeError(aResult) {
+ ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError");
+ return Promise.resolve();
+ }
+
+ function expectNotSupportedError(aResult) {
+ ok(aResult.toString().startsWith("NotSupportedError"), "Expecting a NotSupportedError");
+ return Promise.resolve();
+ }
+
+ // Test basic good call
+ async function test_good_call() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+
+ // Test empty account
+ async function test_empty_account() {
+ let makeCredentialOptions = {
+ challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test without rp.name
+ async function test_without_rp_name() {
+ let rp1 = {id: document.domain, icon: "none"};
+ let makeCredentialOptions = {
+ rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test without user.id
+ async function test_without_user_id() {
+ let user1 = {name: "none", icon: "none", displayName: "none"};
+ let makeCredentialOptions = {
+ rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test without user.name
+ async function test_without_user_name() {
+ let user1 = {id: new Uint8Array(64), icon: "none", displayName: "none"};
+ let makeCredentialOptions = {
+ rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test without user.displayName
+ async function test_without_user_displayname() {
+ let user1 = {id: new Uint8Array(64), name: "none", icon: "none"};
+ let makeCredentialOptions = {
+ rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with a user handle that exceeds the max length
+ async function test_user_too_large() {
+ let user1 = {id: new Uint8Array(65), name: "none", icon: "none", displayName: "none"};
+ let makeCredentialOptions = {
+ rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test without any parameters; this is acceptable meaning the RP ID is
+ // happy to accept either ECDSA-SHA256 or RSA-SHA256
+ async function test_empty_parameters() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge, pubKeyCredParams: []
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+
+ // Test without a parameter array at all
+ async function test_without_parameters() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with an unsupported parameter
+ async function test_unsupported_parameter() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [unsupportedParam]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectNotSupportedError);
+ }
+
+ // Test with an unsupported parameter and a good one
+ async function test_unsupported_but_one_param() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge,
+ pubKeyCredParams: [param, unsupportedParam]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+
+ // Test with a bad parameter
+ async function test_one_bad_parameter() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [badParam]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with an unsupported parameter, and a bad one
+ async function test_one_bad_one_unsupported_param() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge,
+ pubKeyCredParams: [unsupportedParam, badParam]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with an unsupported parameter, a bad one, and a good one. This
+ // should still fail, as anything with a badParam should fail.
+ async function test_one_of_each_parameters() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge,
+ pubKeyCredParams: [param, unsupportedParam, badParam]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test without a challenge
+ async function test_without_challenge() {
+ let makeCredentialOptions = {
+ rp, user, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with an invalid challenge
+ async function test_invalid_challenge() {
+ let makeCredentialOptions = {
+ rp, user, challenge: "begone, thou ill-fitting moist glove!",
+ pubKeyCredParams: [unsupportedParam]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with duplicate pubKeyCredParams
+ async function test_duplicate_pub_key() {
+ let makeCredentialOptions = {
+ rp, user, challenge: gCredentialChallenge,
+ pubKeyCredParams: [param, param, param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+
+ // Test with an RP ID that is not a valid domain string
+ async function test_invalid_rp_id() {
+ let rp1 = { id: document.domain + ":somejunk", name: "none", icon: "none" };
+ let makeCredentialOptions = {
+ rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(arrivingHereIsGood);
+ }
+
+ // Test with another RP ID that is not a valid domain string
+ async function test_invalid_rp_id_2() {
+ let rp1 = { id: document.domain + ":8888", name: "none", icon: "none" };
+ let makeCredentialOptions = {
+ rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(arrivingHereIsGood);
+ }
+
+ // Test with missing rp
+ async function test_missing_rp() {
+ let makeCredentialOptions = {
+ user, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with incorrect user ID type
+ async function test_incorrect_user_id_type() {
+ let invalidType = {id: "a string, which is not a buffer", name: "none", icon: "none", displayName: "none"};
+ let makeCredentialOptions = {
+ user: invalidType, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with missing user
+ async function test_missing_user() {
+ let makeCredentialOptions = {
+ rp, challenge: gCredentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test a complete account
+ async function test_complete_account() {
+ let completeRP = {id: document.domain, name: "Foxxy Name",
+ icon: "https://example.com/fox.svg"};
+ let completeUser = {id: string2buffer("foxes_are_the_best@example.com"),
+ name: "Fox F. Foxington",
+ icon: "https://example.com/fox.svg",
+ displayName: "Foxxy V"};
+ let makeCredentialOptions = {
+ rp: completeRP, user: completeUser, challenge: gCredentialChallenge,
+ pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+
+ // Test with too-large user ID buffer
+ async function test_too_large_user_id() {
+ let hugeUser = {id: new Uint8Array(65),
+ name: "Fox F. Foxington",
+ icon: "https://example.com/fox.svg",
+ displayName: "Foxxy V"};
+ let makeCredentialOptions = {
+ rp, user: hugeUser, challenge: gCredentialChallenge,
+ pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+
+ // Test with excluding unknown transports
+ async function test_excluding_unknown_transports() {
+ let completeRP = {id: document.domain, name: "Foxxy Name",
+ icon: "https://example.com/fox.svg"};
+ let completeUser = {id: string2buffer("foxes_are_the_best@example.com"),
+ name: "Fox F. Foxington",
+ icon: "https://example.com/fox.svg",
+ displayName: "Foxxy V"};
+ let excludedUnknownTransport = {type: "public-key",
+ id: string2buffer("123"),
+ transports: ["unknown", "usb"]};
+ let makeCredentialOptions = {
+ rp: completeRP, user: completeUser, challenge: gCredentialChallenge,
+ pubKeyCredParams: [param], excludeCredentials: [excludedUnknownTransport]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ };
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_no_token.html b/dom/webauthn/tests/u2f/test_webauthn_no_token.html
new file mode 100644
index 0000000000..b79851ff7a
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_no_token.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for W3C Web Authentication with no token</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<h1>Test for W3C Web Authentication with no token</h1>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+<script class="testbody" type="text/javascript">
+"use strict";
+
+is(navigator.authentication, undefined, "navigator.authentication does not exist any longer");
+isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist");
+isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist");
+isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
+
+let credm;
+let credentialChallenge;
+let assertionChallenge;
+let credentialId;
+
+// Setup test env
+add_task(() => {
+ credentialChallenge = new Uint8Array(16);
+ window.crypto.getRandomValues(credentialChallenge);
+ assertionChallenge = new Uint8Array(16);
+ window.crypto.getRandomValues(assertionChallenge);
+ credentialId = new Uint8Array(128);
+ window.crypto.getRandomValues(credentialId);
+ credm = navigator.credentials;
+ // Turn off all tokens. This should result in "not allowed" failures
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["security.webauth.webauthn_enable_softtoken", false],
+ ["security.webauth.webauthn_enable_usbtoken", false],
+ ]});
+});
+
+add_task(async function test_no_token_make_credential() {
+ let rp = {id: document.domain, name: "none", icon: "none"};
+ let user = {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"};
+ let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
+ let makeCredentialOptions = {
+ rp, user, challenge: credentialChallenge, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(function(aResult) {
+ ok(false, "Should have failed.");
+ })
+ .catch(function(aReason) {
+ ok(aReason.toString().startsWith("NotAllowedError"), aReason);
+ });
+});
+
+add_task(async function test_no_token_get_assertion() {
+ let newCredential = {
+ type: "public-key",
+ id: credentialId,
+ transports: ["usb"],
+ }
+ let publicKeyCredentialRequestOptions = {
+ challenge: assertionChallenge,
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ rpId: document.domain,
+ allowCredentials: [newCredential]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(function(aResult) {
+ ok(false, "Should have failed.");
+ })
+ .catch(function(aReason) {
+ ok(aReason.toString().startsWith("NotAllowedError"), aReason);
+ })
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_override_request.html b/dom/webauthn/tests/u2f/test_webauthn_override_request.html
new file mode 100644
index 0000000000..bf6b31ac8b
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_override_request.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for overriding W3C Web Authentication request</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>Test for overriding W3C Web Authentication request</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ // Last request status.
+ let status = "";
+
+ add_task(() => {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["security.webauth.webauthn_enable_softtoken", false],
+ ["security.webauth.webauthn_enable_usbtoken", true],
+ ]});
+ });
+
+ // Start a new MakeCredential() request.
+ async function requestMakeCredential(status_value) {
+ let publicKey = {
+ rp: {id: document.domain, name: "none", icon: "none"},
+ user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"},
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}],
+ };
+
+ // Start the request, handle failures only.
+ navigator.credentials.create({publicKey}).catch(() => {
+ status = status_value;
+ });
+
+ // Wait a tick to let the statemachine start.
+ await Promise.resolve();
+ }
+
+ // Start a new GetAssertion() request.
+ async function requestGetAssertion(status_value) {
+ let newCredential = {
+ type: "public-key",
+ id: crypto.getRandomValues(new Uint8Array(16)),
+ transports: ["usb"],
+ };
+
+ let publicKey = {
+ challenge: crypto.getRandomValues(new Uint8Array(16)),
+ timeout: 5000, // the minimum timeout is actually 15 seconds
+ rpId: document.domain,
+ allowCredentials: [newCredential]
+ };
+
+ // Start the request, handle failures only.
+ navigator.credentials.get({publicKey}).catch(() => {
+ status = status_value;
+ });
+
+ // Wait a tick to let the statemachine start.
+ await Promise.resolve();
+ }
+
+ // Test that .create() and .get() requests override any pending requests.
+ add_task(async function test_override_pending_requests() {
+ // Request a new credential.
+ await requestMakeCredential("aborted1");
+
+ // Request another credential, the new request will abort.
+ await requestMakeCredential("aborted2");
+ is(status, "aborted2", "second request aborted");
+
+ // Request an assertion, the new request will still abort.
+ await requestGetAssertion("aborted3");
+ is(status, "aborted3", "third request aborted");
+
+ // Request another assertion, this fourth request will abort.
+ await requestGetAssertion("aborted4");
+ is(status, "aborted4", "fourth request aborted");
+
+ // Request another credential, the fifth request will still abort. Why
+ // do we keep trying? Well, the test originally looked like this, and
+ // let's face it, it's kinda funny.
+ await requestMakeCredential("aborted5");
+ is(status, "aborted5", "fifth request aborted");
+ });
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_sameorigin.html b/dom/webauthn/tests/u2f/test_webauthn_sameorigin.html
new file mode 100644
index 0000000000..8442d0f62b
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_sameorigin.html
@@ -0,0 +1,316 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for MakeCredential for W3C Web Authentication</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>Test Same Origin Policy for W3C Web Authentication</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ // Execute the full-scope test
+ SimpleTest.waitForExplicitFinish();
+
+ is(navigator.authentication, undefined, "navigator.authentication does not exist any longer");
+ isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist");
+ isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist");
+ isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
+
+ let credm;
+ let chall;
+ let user;
+ let param;
+ let gTrackedCredential;
+ add_task(() => {
+ credm = navigator.credentials;
+
+ chall = new Uint8Array(16);
+ window.crypto.getRandomValues(chall);
+
+ user = {id: new Uint8Array(16), name: "none", icon: "none", displayName: "none"};
+ param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
+ gTrackedCredential = {};
+ });
+
+ add_task(test_basic_good);
+ add_task(test_rp_id_unset);
+ add_task(test_rp_name_unset);
+ add_task(test_origin_with_optional_fields);
+ add_task(test_blank_rp_id);
+ add_task(test_subdomain);
+ add_task(test_same_origin);
+ add_task(test_etld);
+ add_task(test_different_domain_same_tld);
+ add_task(test_assertion_basic_good);
+ add_task(test_assertion_rp_id_unset);
+ add_task(test_assertion_origin_with_optional_fields);
+ add_task(test_assertion_blank_rp_id);
+ add_task(test_assertion_subdomain);
+ add_task(test_assertion_same_origin);
+ add_task(test_assertion_etld);
+ add_task(test_assertion_different_domain_same_tld);
+ add_task(test_basic_good_with_origin);
+ add_task(test_assertion_basic_good_with_origin);
+ add_task(test_assertion_invalid_rp_id);
+ add_task(test_assertion_another_invalid_rp_id);
+
+ function arrivingHereIsGood(aResult) {
+ ok(true, "Good result! Received a: " + aResult);
+ }
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ }
+
+ function expectSecurityError(aResult) {
+ ok(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError");
+ }
+
+ function expectTypeError(aResult) {
+ ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError");
+ }
+
+ function keepThisPublicKeyCredential(aIdentifier) {
+ return function(aPublicKeyCredential) {
+ gTrackedCredential[aIdentifier] = {
+ type: "public-key",
+ id: new Uint8Array(aPublicKeyCredential.rawId),
+ transports: [ "usb" ],
+ }
+ return Promise.resolve(aPublicKeyCredential);
+ }
+ }
+
+ function test_basic_good() {
+ // Test basic good call
+ let rp = {id: document.domain, name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(keepThisPublicKeyCredential("basic"))
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+ function test_rp_id_unset() {
+ // Test rp.id being unset
+ let makeCredentialOptions = {
+ rp: {name: "none"}, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+ function test_rp_name_unset() {
+ // Test rp.name being unset
+ let makeCredentialOptions = {
+ rp: {id: document.domain}, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectTypeError);
+ }
+ function test_origin_with_optional_fields() {
+ // Test this origin with optional fields
+ let rp = {id: "user:pass@" + document.domain + ":8888", name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_blank_rp_id() {
+ // Test blank rp.id
+ let rp = {id: "", name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_subdomain() {
+ // Test subdomain of this origin
+ let rp = {id: "subdomain." + document.domain, name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_same_origin() {
+ // Test the same origin
+ let rp = {id: "example.com", name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+ function test_etld() {
+ // Test the eTLD
+ let rp = {id: "com", name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_different_domain_same_tld() {
+ // Test a different domain within the same TLD
+ let rp = {id: "alt.test", name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_assertion_basic_good() {
+ // Test basic good call
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: document.domain,
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+ function test_assertion_rp_id_unset() {
+ // Test rpId being unset
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+ function test_assertion_origin_with_optional_fields() {
+ // Test this origin with optional fields
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: "user:pass@" + document.origin + ":8888",
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_assertion_blank_rp_id() {
+ // Test blank rpId
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: "",
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_assertion_subdomain() {
+ // Test subdomain of this origin
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: "subdomain." + document.domain,
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_assertion_same_origin() {
+ // Test the same origin
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: "example.com",
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsGood)
+ .catch(arrivingHereIsBad);
+ }
+ function test_assertion_etld() {
+ // Test the eTLD
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: "com",
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_assertion_different_domain_same_tld() {
+ // Test a different domain within the same TLD
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: "alt.test",
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_basic_good_with_origin() {
+ // Test basic good Create call but using an origin (Bug 1380421)
+ let rp = {id: window.origin, name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ return credm.create({publicKey: makeCredentialOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_assertion_basic_good_with_origin() {
+ // Test basic good Get call but using an origin (Bug 1380421)
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: window.origin,
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsBad)
+ .catch(expectSecurityError);
+ }
+ function test_assertion_invalid_rp_id() {
+ // Test with an rpId that is not a valid domain string
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: document.domain + ":somejunk",
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsBad)
+ .catch(arrivingHereIsGood);
+ }
+ function test_assertion_another_invalid_rp_id() {
+ // Test with another rpId that is not a valid domain string
+ let publicKeyCredentialRequestOptions = {
+ challenge: chall,
+ rpId: document.domain + ":8888",
+ allowCredentials: [gTrackedCredential.basic]
+ };
+ return credm.get({publicKey: publicKeyCredentialRequestOptions})
+ .then(arrivingHereIsBad)
+ .catch(arrivingHereIsGood);
+ }
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_sameoriginwithancestors.html b/dom/webauthn/tests/u2f/test_webauthn_sameoriginwithancestors.html
new file mode 100644
index 0000000000..9b94a2cc47
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_sameoriginwithancestors.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Test for MakeCredential for W3C Web Authentication (sameOriginWithAncestors = false)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="../u2futil.js"></script>
+ <script type="text/javascript" src="../pkijs/common.js"></script>
+ <script type="text/javascript" src="../pkijs/asn1.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_schema.js"></script>
+ <script type="text/javascript" src="../pkijs/x509_simpl.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>Test Same Origin Policy for W3C Web Authentication (sameOriginWithAncestors = false)</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1694639">Mozilla Bug 1694639</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ // Execute the full-scope test
+ SimpleTest.waitForExplicitFinish();
+
+ var gTrackedCredential = {};
+
+ function arrivingHereIsGood(aResult) {
+ ok(true, "Good result! Received a: " + aResult);
+ }
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ }
+
+ function expectNotAllowedError(aResult) {
+ ok(aResult == "NotAllowedError", "Expecting a NotAllowedError, got " + aResult);
+ }
+
+ function keepThisPublicKeyCredential(aIdentifier) {
+ return function(aPublicKeyCredential) {
+ gTrackedCredential[aIdentifier] = {
+ type: "public-key",
+ id: new Uint8Array(aPublicKeyCredential.rawId),
+ transports: [ "usb" ],
+ }
+ return Promise.resolve(aPublicKeyCredential);
+ }
+ }
+
+ add_task(async function runTests() {
+ let iframe = document.createElement("iframe");
+ iframe.src = "https://example.org";
+ document.body.appendChild(iframe);
+ await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true}));
+
+ is(navigator.authentication, undefined, "navigator.authentication does not exist any longer");
+ isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist");
+ isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist");
+ isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
+
+ let credm = navigator.credentials;
+
+ let chall = new Uint8Array(16);
+ window.crypto.getRandomValues(chall);
+
+ let user = {id: new Uint8Array(16), name: "none", icon: "none", displayName: "none"};
+ let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256};
+
+ let rp = {id: document.domain, name: "none"};
+ let makeCredentialOptions = {
+ rp, user, challenge: chall, pubKeyCredParams: [param]
+ };
+ await credm.create({publicKey: makeCredentialOptions})
+ .then(keepThisPublicKeyCredential("basic"))
+ .catch(arrivingHereIsBad);
+
+ var testFuncs = [
+ function (args) {
+ // Test create when sameOriginWithAncestors = false
+ let credentialOptions = {
+ rp: args.rp, user: args.user, challenge: args.challenge, pubKeyCredParams: [args.param]
+ };
+ return this.content.window.navigator.credentials.create({publicKey: credentialOptions})
+ .catch(e => Promise.reject(e.name));
+ },
+ function (args) {
+ // Test get when sameOriginWithAncestors = false
+ let publicKeyCredentialRequestOptions = {
+ challenge: args.challenge,
+ rpId: args.rp.id,
+ allowCredentials: [args.trackedCredential.basic]
+ };
+ return this.content.window.navigator.credentials.get({publicKey: publicKeyCredentialRequestOptions})
+ .catch(e => Promise.reject(e.name));
+ },
+ ];
+
+ let args = { user, param, rp, challenge: chall, trackedCredential: gTrackedCredential }
+ for(let func of testFuncs) {
+ await SpecialPowers.spawn(iframe, [args], func)
+ .then(arrivingHereIsBad)
+ .catch(expectNotAllowedError);
+ }
+ });
+ </script>
+
+</body>
+</html>
diff --git a/dom/webauthn/tests/u2f/test_webauthn_store_credential.html b/dom/webauthn/tests/u2f/test_webauthn_store_credential.html
new file mode 100644
index 0000000000..f19d1d7fa0
--- /dev/null
+++ b/dom/webauthn/tests/u2f/test_webauthn_store_credential.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<head>
+ <title>Tests for Store for W3C Web Authentication</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+ <h1>Tests for Store for W3C Web Authentication</h1>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a>
+
+ <script class="testbody" type="text/javascript">
+ "use strict";
+
+ isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist");
+ isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist");
+ isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist");
+ isnot(navigator.credentials.store, undefined, "CredentialManagement store API endpoint must exist");
+
+ function arrivingHereIsBad(aResult) {
+ ok(false, "Bad result! Received a: " + aResult);
+ return Promise.resolve();
+ }
+
+ function expectNotSupportedError(aResult) {
+ ok(aResult.toString().startsWith("NotSupportedError"), "Expecting a NotSupportedError, received: " + aResult);
+ return Promise.resolve();
+ }
+
+ add_task(async function test_store_credential() {
+ let credentialChallenge = new Uint8Array(16);
+ window.crypto.getRandomValues(credentialChallenge);
+
+ let rp = {id: document.domain, name: "none", icon: "none"};
+ let user = {id: new Uint8Array(64), name: "none", icon: "none", displayName: "none"};
+ let params = [ {type: "public-key", alg: "es256"}, {type: "public-key", alg: -7} ]
+
+ let makeCredentialOptions = {
+ rp, user, challenge: credentialChallenge, pubKeyCredParams: params
+ };
+
+ let credential = await navigator.credentials.create({publicKey: makeCredentialOptions})
+ .catch(arrivingHereIsBad);
+
+ await navigator.credentials.store(credential)
+ .then(arrivingHereIsBad)
+ .catch(expectNotSupportedError);
+ });
+ </script>
+
+</body>
+</html>