diff options
Diffstat (limited to 'dom/webauthn/tests/browser')
-rw-r--r-- | dom/webauthn/tests/browser/browser.ini | 22 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_abort_visibility.js | 277 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_fido_appid_extension.js | 183 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_ipaddress.js | 51 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_prompts.js | 271 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_telemetry.js | 144 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/head.js | 155 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/tab_webauthn_result.html | 14 |
8 files changed, 1117 insertions, 0 deletions
diff --git a/dom/webauthn/tests/browser/browser.ini b/dom/webauthn/tests/browser/browser.ini new file mode 100644 index 0000000000..e16a1e7723 --- /dev/null +++ b/dom/webauthn/tests/browser/browser.ini @@ -0,0 +1,22 @@ +[DEFAULT] +support-files = + head.js + tab_webauthn_result.html + ../pkijs/* + ../cbor.js + ../u2futil.js +skip-if = !e10s + +[browser_abort_visibility.js] +skip-if = + os == 'win' && version == '18362' # win10-1903+ - test not relevant on 1903 +[browser_fido_appid_extension.js] +skip-if = + os == 'win' && version == '18362' # win10-1903+ - test not relevant on 1903 +[browser_webauthn_prompts.js] +skip-if = + os == 'win' && version == '18362' # win10-1903+ - test not relevant on 1903 +[browser_webauthn_telemetry.js] +skip-if = + os == 'win' && version == '18362' # win10-1903+ - test not relevant on 1903 +[browser_webauthn_ipaddress.js] diff --git a/dom/webauthn/tests/browser/browser_abort_visibility.js b/dom/webauthn/tests/browser/browser_abort_visibility.js new file mode 100644 index 0000000000..4cfb37136e --- /dev/null +++ b/dom/webauthn/tests/browser/browser_abort_visibility.js @@ -0,0 +1,277 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/dom/webauthn/tests/browser/tab_webauthn_result.html"; + +async function assertStatus(tab, expected) { + let actual = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function() { + info("visbility state: " + content.document.visibilityState); + info("active: " + content.browsingContext.isActive); + return content.document.getElementById("status").value; + } + ); + is(actual, expected, "webauthn request " + expected); +} + +async function waitForStatus(tab, expected) { + /* eslint-disable no-shadow */ + await SpecialPowers.spawn(tab.linkedBrowser, [[expected]], async function( + expected + ) { + return ContentTaskUtils.waitForCondition(() => { + info( + "expecting " + + expected + + ", visbility state: " + + content.document.visibilityState + ); + info( + "expecting " + + expected + + ", active: " + + content.browsingContext.isActive + ); + return content.document.getElementById("status").value == expected; + }); + }); + /* eslint-enable no-shadow */ + + await assertStatus(tab, expected); +} + +function startMakeCredentialRequest(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + const cose_alg_ECDSA_w_SHA256 = -7; + + let publicKey = { + rp: { id: content.document.domain, name: "none", icon: "none" }, + user: { + id: new Uint8Array(), + name: "none", + icon: "none", + displayName: "none", + }, + challenge: content.crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{ type: "public-key", alg: cose_alg_ECDSA_w_SHA256 }], + }; + + let status = content.document.getElementById("status"); + + info( + "Attempting to create credential for origin: " + + content.document.nodePrincipal.origin + ); + content.navigator.credentials + .create({ publicKey }) + .then(() => { + status.value = "completed"; + }) + .catch(() => { + status.value = "aborted"; + }); + + status.value = "pending"; + }); +} + +function startGetAssertionRequest(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let newCredential = { + type: "public-key", + id: content.crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }; + + let publicKey = { + challenge: content.crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: content.document.domain, + allowCredentials: [newCredential], + }; + + let status = content.document.getElementById("status"); + + info( + "Attempting to get credential for origin: " + + content.document.nodePrincipal.origin + ); + content.navigator.credentials + .get({ publicKey }) + .then(() => { + status.value = "completed"; + }) + .catch(ex => { + info("aborted: " + ex); + status.value = "aborted"; + }); + + status.value = "pending"; + }); +} + +add_task(async function test_setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn", true], + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_android_fido2", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); + +// Test that MakeCredential() and GetAssertion() requests +// are aborted when the current tab loses its focus. +add_task(async function test_switch_tab() { + // Create a new tab for the MakeCredential() request. + let tab_create = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_URL + ); + + // Start the request. + await startMakeCredentialRequest(tab_create); + await assertStatus(tab_create, "pending"); + + // Open another tab and switch to it. The first will lose focus. + let tab_get = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + await assertStatus(tab_create, "pending"); + + // Start a GetAssertion() request in the second tab, the first is aborted + await startGetAssertionRequest(tab_get); + await waitForStatus(tab_create, "aborted"); + await assertStatus(tab_get, "pending"); + + // Start a second request in the second tab. It should abort. + await startGetAssertionRequest(tab_get); + await waitForStatus(tab_get, "aborted"); + + // Close tabs. + BrowserTestUtils.removeTab(tab_create); + BrowserTestUtils.removeTab(tab_get); +}); + +function waitForWindowActive(win, active) { + return Promise.all([ + BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"), + BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"), + ]); +} + +add_task(async function test_new_window_make() { + // Create a new tab for the MakeCredential() request. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start a MakeCredential request. + await startMakeCredentialRequest(tab); + await assertStatus(tab, "pending"); + + let windowGonePromise = waitForWindowActive(window, false); + // Open a new window. The tab will lose focus. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await windowGonePromise; + await assertStatus(tab, "pending"); + + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_new_window_get() { + // Create a new tab for the GetAssertion() request. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start a GetAssertion request. + await startGetAssertionRequest(tab); + await assertStatus(tab, "pending"); + + let windowGonePromise = waitForWindowActive(window, false); + // Open a new window. The tab will lose focus. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await windowGonePromise; + await assertStatus(tab, "pending"); + + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_minimize_make() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + // Minimizing windows doesn't supported in headless mode. + if (env.get("MOZ_HEADLESS")) { + return; + } + + // Create a new window for the MakeCredential() request. + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + // Start a MakeCredential request. + await startMakeCredentialRequest(tab); + await assertStatus(tab, "pending"); + + // Minimize the window. + let windowGonePromise = waitForWindowActive(win, false); + win.minimize(); + await assertStatus(tab, "pending"); + await windowGonePromise; + + // Restore the window. + await new Promise(resolve => SimpleTest.waitForFocus(resolve, win)); + await assertStatus(tab, "pending"); + + // Close window and wait for main window to be focused again. + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; +}); + +add_task(async function test_minimize_get() { + let env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + // Minimizing windows doesn't supported in headless mode. + if (env.get("MOZ_HEADLESS")) { + return; + } + + // Create a new window for the GetAssertion() request. + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + // Start a GetAssertion request. + await startGetAssertionRequest(tab); + await assertStatus(tab, "pending"); + + // Minimize the window. + let windowGonePromise = waitForWindowActive(win, false); + win.minimize(); + await assertStatus(tab, "pending"); + await windowGonePromise; + + // Restore the window. + await new Promise(resolve => SimpleTest.waitForFocus(resolve, win)); + await assertStatus(tab, "pending"); + + // Close window and wait for main window to be focused again. + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; +}); diff --git a/dom/webauthn/tests/browser/browser_fido_appid_extension.js b/dom/webauthn/tests/browser/browser_fido_appid_extension.js new file mode 100644 index 0000000000..1b574f7064 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_fido_appid_extension.js @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com/"; + +let expectNotSupportedError = expectError("NotSupported"); +let expectInvalidStateError = expectError("InvalidState"); +let expectSecurityError = expectError("Security"); + +function promiseU2FRegister(tab, app_id_) { + let challenge_ = crypto.getRandomValues(new Uint8Array(16)); + challenge_ = bytesToBase64UrlSafe(challenge_); + + return SpecialPowers.spawn( + tab.linkedBrowser, + [[app_id_, challenge_]], + function([app_id, challenge]) { + return new Promise(resolve => { + content.u2f.register( + app_id, + [{ version: "U2F_V2", challenge }], + [], + resolve + ); + }); + } + ).then(res => { + is(res.errorCode, 0, "u2f.register() succeeded"); + let data = base64ToBytesUrlSafe(res.registrationData); + is(data[0], 0x05, "Reserved byte is correct"); + return data.slice(67, 67 + data[66]); + }); +} + +add_task(function test_setup() { + Services.prefs.setBoolPref("security.webauth.u2f", true); + Services.prefs.setBoolPref("security.webauth.webauthn", true); + Services.prefs.setBoolPref( + "security.webauth.webauthn_enable_android_fido2", + false + ); + Services.prefs.setBoolPref( + "security.webauth.webauthn_enable_softtoken", + true + ); + Services.prefs.setBoolPref( + "security.webauth.webauthn_enable_usbtoken", + false + ); +}); + +add_task(async function test_appid() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Get a keyHandle for a FIDO AppId. + let appid = "https://example.com/appId"; + let keyHandle = await promiseU2FRegister(tab, appid); + + // The FIDO AppId extension can't be used for MakeCredential. + await promiseWebAuthnMakeCredential(tab, "none", { appid }) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + + // Using the keyHandle shouldn't work without the FIDO AppId extension. + // This will be an invalid state, because the softtoken will consent without + // having the correct "RP ID" via the FIDO extension. + await promiseWebAuthnGetAssertion(tab, keyHandle) + .then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + // Invalid app IDs (for the current origin) must be rejected. + await promiseWebAuthnGetAssertion(tab, keyHandle, { + appid: "https://bogus.com/appId", + }) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // Non-matching app IDs must be rejected. Even when the user/softtoken + // consents, leading to an invalid state. + await promiseWebAuthnGetAssertion(tab, keyHandle, { appid: appid + "2" }) + .then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + let rpId = new TextEncoder("utf-8").encode(appid); + let rpIdHash = await crypto.subtle.digest("SHA-256", rpId); + + // Succeed with the right fallback rpId. + await promiseWebAuthnGetAssertion(tab, keyHandle, { appid }).then( + ({ authenticatorData, clientDataJSON, extensions }) => { + is(extensions.appid, true, "appid extension was acted upon"); + + // Check that the correct rpIdHash is returned. + let rpIdHashSign = authenticatorData.slice(0, 32); + ok(memcmp(rpIdHash, rpIdHashSign), "rpIdHash is correct"); + + let clientData = JSON.parse(buffer2string(clientDataJSON)); + is(clientData.clientExtensions.appid, appid, "appid extension sent"); + } + ); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_appid_unused() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Get a keyHandle for a FIDO AppId. + let appid = "https://example.com/appId"; + + let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab); + let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj); + + // Make sure the RP ID hash matches what we calculate. + await checkRpIdHash(authDataObj.rpIdHash, "example.com"); + + // Get a new assertion. + let { + clientDataJSON, + authenticatorData, + signature, + extensions, + } = await promiseWebAuthnGetAssertion(tab, rawId, { appid }); + + // Check the we can parse clientDataJSON. + let clientData = JSON.parse(buffer2string(clientDataJSON)); + ok( + "appid" in clientData.clientExtensions, + `since it was passed, appid field should appear in the client data, but ` + + `saw: ${JSON.stringify(clientData.clientExtensions)}` + ); + + ok( + "appid" in extensions, + `appid should be populated in the extensions data, but saw: ` + + `${JSON.stringify(extensions)}` + ); + is(extensions.appid, false, "appid extension should indicate it was unused"); + + // Check auth data. + let attestation = await webAuthnDecodeAuthDataArray( + new Uint8Array(authenticatorData) + ); + is( + "" + attestation.flags, + "" + flag_TUP, + "Assertion's user presence byte set correctly" + ); + + // Verify the signature. + let params = await deriveAppAndChallengeParam( + "example.com", + clientDataJSON, + attestation + ); + let signedData = await assembleSignedData( + params.appParam, + params.attestation.flags, + params.attestation.counter, + params.challengeParam + ); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + signature + ); + ok(valid, "signature is valid"); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); + +add_task(function test_cleanup() { + Services.prefs.clearUserPref("security.webauth.u2f"); + Services.prefs.clearUserPref("security.webauth.webauthn"); + Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken"); + Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken"); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js new file mode 100644 index 0000000000..ec4f39e02f --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let expectSecurityError = expectError("Security"); + +add_task(function test_setup() { + Services.prefs.setBoolPref("security.webauth.u2f", true); + Services.prefs.setBoolPref("security.webauth.webauthn", true); + Services.prefs.setBoolPref( + "security.webauth.webauthn_enable_android_fido2", + false + ); + Services.prefs.setBoolPref( + "security.webauth.webauthn_enable_softtoken", + true + ); + Services.prefs.setBoolPref( + "security.webauth.webauthn_enable_usbtoken", + false + ); + Services.prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true); +}); + +registerCleanupFunction(async function() { + Services.prefs.clearUserPref("security.webauth.u2f"); + Services.prefs.clearUserPref("security.webauth.webauthn"); + Services.prefs.clearUserPref( + "security.webauth.webauthn_enable_android_fido2" + ); + Services.prefs.clearUserPref("security.webauth.webauthn_enable_softtoken"); + Services.prefs.clearUserPref("security.webauth.webauthn_enable_usbtoken"); + Services.prefs.clearUserPref("network.proxy.allow_hijacking_localhost"); +}); + +add_task(async function test_appid() { + // 127.0.0.1 triggers special cases in ssltunnel, so let's use .2! + const TEST_URL = "https://127.0.0.2/"; + + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + await promiseWebAuthnMakeCredential(tab, "none", {}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_prompts.js b/dom/webauthn/tests/browser/browser_webauthn_prompts.js new file mode 100644 index 0000000000..3726453074 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js @@ -0,0 +1,271 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com/"; + +function promiseNotification(id) { + return new Promise(resolve => { + PopupNotifications.panel.addEventListener("popupshown", function shown() { + let notification = PopupNotifications.getNotification(id); + if (notification) { + ok(true, `${id} prompt visible`); + PopupNotifications.panel.removeEventListener("popupshown", shown); + resolve(); + } + }); + }); +} + +function triggerMainPopupCommand(popup) { + info("triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length > 0, "at least one notification displayed"); + let notification = notifications[0]; + info("triggering command: " + notification.getAttribute("buttonlabel")); + + return EventUtils.synthesizeMouseAtCenter(notification.button, {}); +} + +let expectAbortError = expectError("Abort"); + +function verifyAnonymizedCertificate(result) { + let { attObj, rawId } = result; + return webAuthnDecodeCBORAttestation(attObj).then(({ fmt, attStmt }) => { + is("none", fmt, "Is a None Attestation"); + is("object", typeof attStmt, "attStmt is a map"); + is(0, Object.keys(attStmt).length, "attStmt is empty"); + }); +} + +function verifyDirectCertificate(result) { + let { attObj, rawId } = result; + return webAuthnDecodeCBORAttestation(attObj).then(({ fmt, attStmt }) => { + is("fido-u2f", fmt, "Is a FIDO U2F Attestation"); + is("object", typeof attStmt, "attStmt is a map"); + ok(attStmt.hasOwnProperty("x5c"), "attStmt.x5c exists"); + ok(attStmt.hasOwnProperty("sig"), "attStmt.sig exists"); + }); +} + +add_task(async function test_setup_usbtoken() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.u2f", false], + ["security.webauth.webauthn", true], + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_android_fido2", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); + +add_task(async function test_register() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab, "indirect", {}) + .then(arrivingHereIsBad) + .catch(expectAbortError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_sign() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new assertion and wait for the prompt. + let active = true; + let request = promiseWebAuthnGetAssertion(tab) + .then(arrivingHereIsBad) + .catch(expectAbortError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-sign"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_register_direct_cancel() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let active = true; + let promise = promiseWebAuthnMakeCredential(tab, "direct", {}) + .then(arrivingHereIsBad) + .catch(expectAbortError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register-direct"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await promise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +add_task(async function test_tab_switching() { + // Open a new tab. + let tab_one = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab_one, "indirect", {}) + .then(arrivingHereIsBad) + .catch(expectAbortError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register"); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second tab. + let tab_two = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org/" + ); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "closed" + ); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + + // Go back to the first tab + await BrowserTestUtils.removeTab(tab_two); + + await promiseNotification("webauthn-prompt-register"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab_one); +}); + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +add_task(async function test_window_switching() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab, "indirect", {}) + .then(arrivingHereIsBad) + .catch(expectAbortError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second window + let new_window = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(new_window); + + await TestUtils.waitForCondition( + () => new_window.PopupNotifications.panel.state == "closed" + ); + is( + new_window.PopupNotifications.panel.state, + "closed", + "Doorhanger is hidden" + ); + + // Go back to the first tab + await BrowserTestUtils.closeWindow(new_window); + await SimpleTest.promiseFocus(window); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is still visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_setup_softtoken() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.u2f", false], + ["security.webauth.webauthn", true], + ["security.webauth.webauthn_enable_softtoken", true], + ["security.webauth.webauthn_enable_usbtoken", false], + ], + }); +}); + +add_task(async function test_register_direct_proceed() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct", {}); + await promiseNotification("webauthn-prompt-register-direct"); + + // Proceed. + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "direct" attestation. + await request.then(verifyDirectCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_register_direct_proceed_anon() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct", {}); + await promiseNotification("webauthn-prompt-register-direct"); + + // Check "anonymize anyway" and proceed. + PopupNotifications.panel.firstElementChild.checkbox.checked = true; + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "none" attestation. + await request.then(verifyAnonymizedCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_telemetry.js b/dom/webauthn/tests/browser/browser_webauthn_telemetry.js new file mode 100644 index 0000000000..09b8c54e99 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_telemetry.js @@ -0,0 +1,144 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.defineModuleGetter( + this, + "TelemetryTestUtils", + "resource://testing-common/TelemetryTestUtils.jsm" +); + +const TEST_URL = "https://example.com/"; + +function getTelemetryForScalar(aName) { + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + return scalars[aName] || 0; +} + +function cleanupTelemetry() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + Services.telemetry.getHistogramById("WEBAUTHN_CREATE_CREDENTIAL_MS").clear(); + Services.telemetry.getHistogramById("WEBAUTHN_GET_ASSERTION_MS").clear(); +} + +function validateHistogramEntryCount(aHistogramName, aExpectedCount) { + let hist = Services.telemetry.getHistogramById(aHistogramName); + let resultIndexes = hist.snapshot(); + + let entriesSeen = Object.values(resultIndexes.values).reduce( + (a, b) => a + b, + 0 + ); + + is( + entriesSeen, + aExpectedCount, + "Expecting " + aExpectedCount + " histogram entries in " + aHistogramName + ); +} + +add_task(async function test_setup() { + cleanupTelemetry(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn", true], + ["security.webauth.webauthn_enable_softtoken", true], + ["security.webauth.webauthn_enable_usbtoken", false], + ["security.webauth.webauthn_enable_android_fido2", false], + ["security.webauth.webauthn_testing_allow_direct_attestation", true], + ], + }); +}); + +add_task(async function test() { + // These tests can't run simultaneously as the preference changes will race. + // So let's run them sequentially here, but in an async function so we can + // use await. + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Create a new credential. + let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab); + let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj); + + // Make sure the RP ID hash matches what we calculate. + await checkRpIdHash(authDataObj.rpIdHash, "example.com"); + + // Get a new assertion. + let { + clientDataJSON, + authenticatorData, + signature, + } = await promiseWebAuthnGetAssertion(tab, rawId); + + // Check the we can parse clientDataJSON. + JSON.parse(buffer2string(clientDataJSON)); + + // Check auth data. + let attestation = await webAuthnDecodeAuthDataArray( + new Uint8Array(authenticatorData) + ); + is( + "" + attestation.flags, + "" + flag_TUP, + "Assertion's user presence byte set correctly" + ); + + // Verify the signature. + let params = await deriveAppAndChallengeParam( + "example.com", + clientDataJSON, + attestation + ); + let signedData = await assembleSignedData( + params.appParam, + params.attestation.flags, + params.attestation.counter, + params.challengeParam + ); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + signature + ); + ok(valid, "signature is valid"); + + // Check telemetry data. + let webauthn_used = getTelemetryForScalar("security.webauthn_used"); + ok( + webauthn_used, + "Scalar keys are set: " + Object.keys(webauthn_used).join(", ") + ); + is( + webauthn_used.U2FRegisterFinish, + 1, + "webauthn_used U2FRegisterFinish scalar should be 1" + ); + is( + webauthn_used.U2FSignFinish, + 1, + "webauthn_used U2FSignFinish scalar should be 1" + ); + is( + webauthn_used.U2FSignAbort, + undefined, + "webauthn_used U2FSignAbort scalar must be unset" + ); + is( + webauthn_used.U2FRegisterAbort, + undefined, + "webauthn_used U2FRegisterAbort scalar must be unset" + ); + + validateHistogramEntryCount("WEBAUTHN_CREATE_CREDENTIAL_MS", 1); + validateHistogramEntryCount("WEBAUTHN_GET_ASSERTION_MS", 1); + + BrowserTestUtils.removeTab(tab); + + // There aren't tests for register succeeding and sign failing, as I don't see an easy way to prompt + // the soft token to fail that way _and_ trigger the Abort telemetry. +}); diff --git a/dom/webauthn/tests/browser/head.js b/dom/webauthn/tests/browser/head.js new file mode 100644 index 0000000000..70ea10c526 --- /dev/null +++ b/dom/webauthn/tests/browser/head.js @@ -0,0 +1,155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let exports = this; + +const scripts = [ + "pkijs/common.js", + "pkijs/asn1.js", + "pkijs/x509_schema.js", + "pkijs/x509_simpl.js", + "browser/cbor.js", + "browser/u2futil.js", +]; + +for (let script of scripts) { + Services.scriptloader.loadSubScript( + `chrome://mochitests/content/browser/dom/webauthn/tests/${script}`, + this + ); +} + +function memcmp(x, y) { + let xb = new Uint8Array(x); + let yb = new Uint8Array(y); + + if (x.byteLength != y.byteLength) { + return false; + } + + for (let i = 0; i < xb.byteLength; ++i) { + if (xb[i] != yb[i]) { + return false; + } + } + + return true; +} + +function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); +} + +function expectError(aType) { + let expected = `${aType}Error`; + return function(aResult) { + is( + aResult.slice(0, expected.length), + expected, + `Expecting a ${aType}Error` + ); + }; +} + +/* eslint-disable no-shadow */ +function promiseWebAuthnMakeCredential( + tab, + attestation = "none", + extensions = {} +) { + return ContentTask.spawn( + tab.linkedBrowser, + [attestation, extensions], + ([attestation, extensions]) => { + const cose_alg_ECDSA_w_SHA256 = -7; + + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + + let pubKeyCredParams = [ + { + type: "public-key", + alg: cose_alg_ECDSA_w_SHA256, + }, + ]; + + let publicKey = { + rp: { id: content.document.domain, name: "none", icon: "none" }, + user: { + id: new Uint8Array(), + name: "none", + icon: "none", + displayName: "none", + }, + pubKeyCredParams, + extensions, + attestation, + challenge, + }; + + return content.navigator.credentials + .create({ publicKey }) + .then(credential => { + return { + attObj: credential.response.attestationObject, + rawId: credential.rawId, + }; + }); + } + ); +} + +function promiseWebAuthnGetAssertion(tab, key_handle = null, extensions = {}) { + return ContentTask.spawn( + tab.linkedBrowser, + [key_handle, extensions], + ([key_handle, extensions]) => { + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + if (key_handle == null) { + key_handle = content.crypto.getRandomValues(new Uint8Array(16)); + } + + let credential = { + id: key_handle, + type: "public-key", + transports: ["usb"], + }; + + let publicKey = { + challenge, + extensions, + rpId: content.document.domain, + allowCredentials: [credential], + }; + + return content.navigator.credentials + .get({ publicKey }) + .then(assertion => { + return { + authenticatorData: assertion.response.authenticatorData, + clientDataJSON: assertion.response.clientDataJSON, + extensions: assertion.getClientExtensionResults(), + signature: assertion.response.signature, + }; + }); + } + ); +} + +function checkRpIdHash(rpIdHash, hostname) { + return crypto.subtle + .digest("SHA-256", string2buffer(hostname)) + .then(calculatedRpIdHash => { + let calcHashStr = bytesToBase64UrlSafe( + new Uint8Array(calculatedRpIdHash) + ); + let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(rpIdHash)); + + if (calcHashStr != providedHashStr) { + throw new Error("Calculated RP ID hash doesn't match."); + } + }); +} +/* eslint-enable no-shadow */ diff --git a/dom/webauthn/tests/browser/tab_webauthn_result.html b/dom/webauthn/tests/browser/tab_webauthn_result.html new file mode 100644 index 0000000000..8e8b9f82cd --- /dev/null +++ b/dom/webauthn/tests/browser/tab_webauthn_result.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Generic W3C Web Authentication Test Result Page</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Generic W3C Web Authentication Test Result Page</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a> +<input type="text" id="status" value="init" /> + +</body> +</html> |