diff options
Diffstat (limited to 'dom/webauthn/tests/browser')
-rw-r--r-- | dom/webauthn/tests/browser/browser.ini | 32 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_abort_visibility.js | 276 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_fido_appid_extension.js | 67 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_ipaddress.js | 28 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_prompts.js | 316 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_telemetry.js | 134 | ||||
-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, 1022 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..0c95bc1921 --- /dev/null +++ b/dom/webauthn/tests/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=true + +[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/browser/browser_abort_visibility.js b/dom/webauthn/tests/browser/browser_abort_visibility.js new file mode 100644 index 0000000000..e462c5f1ad --- /dev/null +++ b/dom/webauthn/tests/browser/browser_abort_visibility.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/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/browser/browser_fido_appid_extension.js b/dom/webauthn/tests/browser/browser_fido_appid_extension.js new file mode 100644 index 0000000000..0d45bda434 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_fido_appid_extension.js @@ -0,0 +1,67 @@ +/* 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"); + +add_task(async function test_appid_unused() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let appid = "https://example.com/appId"; + + let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab); + let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj); + + // Make sure the RP ID hash matches what we calculate. + await checkRpIdHash(authDataObj.rpIdHash, "example.com"); + + // Get a new assertion. + let { clientDataJSON, authenticatorData, signature, extensions } = + await promiseWebAuthnGetAssertion(tab, rawId, { appid }); + + ok( + "appid" in extensions, + `appid should be populated in the extensions data, but saw: ` + + `${JSON.stringify(extensions)}` + ); + is(extensions.appid, false, "appid extension should indicate it was unused"); + + // Check auth data. + let attestation = await webAuthnDecodeAuthDataArray( + new Uint8Array(authenticatorData) + ); + is( + "" + attestation.flags, + "" + flag_TUP, + "Assertion's user presence byte set correctly" + ); + + // Verify the signature. + let params = await deriveAppAndChallengeParam( + "example.com", + clientDataJSON, + attestation + ); + let signedData = await assembleSignedData( + params.appParam, + params.attestation.flags, + params.attestation.counter, + params.challengeParam + ); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + signature + ); + ok(valid, "signature is valid"); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js new file mode 100644 index 0000000000..2c3f8ea025 --- /dev/null +++ b/dom/webauthn/tests/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/browser/browser_webauthn_prompts.js b/dom/webauthn/tests/browser/browser_webauthn_prompts.js new file mode 100644 index 0000000000..10835f73b6 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js @@ -0,0 +1,316 @@ +/* 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.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); +add_task(test_register); +add_task(test_register_escape); +add_task(test_sign); +add_task(test_sign_escape); +add_task(test_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.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-presence"); + + // Cancel the request with the button. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_escape() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab, "none", {}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request by hitting escape. + ok(active, "request should still be active"); + EventUtils.synthesizeKey("KEY_Escape"); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_sign() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new assertion and wait for the prompt. + let active = true; + let request = promiseWebAuthnGetAssertion(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request with the button. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_sign_escape() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new assertion and wait for the prompt. + let active = true; + let request = promiseWebAuthnGetAssertion(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request by hitting escape. + ok(active, "request should still be active"); + EventUtils.synthesizeKey("KEY_Escape"); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_cancel() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let active = true; + let promise = promiseWebAuthnMakeCredential(tab, "direct", {}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register-direct"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await promise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +// 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-presence"); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second tab. + let tab_two = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org/" + ); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "closed" + ); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + + // Go back to the first tab + await BrowserTestUtils.removeTab(tab_two); + + await promiseNotification("webauthn-prompt-presence"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab_one); +} + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +async function test_window_switching() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab, "none", {}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second window + let new_window = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(new_window); + + await TestUtils.waitForCondition( + () => new_window.PopupNotifications.panel.state == "closed" + ); + is( + new_window.PopupNotifications.panel.state, + "closed", + "Doorhanger is hidden" + ); + + // Go back to the first tab + await BrowserTestUtils.closeWindow(new_window); + await SimpleTest.promiseFocus(window); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is still visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_proceed() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct", {}); + await promiseNotification("webauthn-prompt-register-direct"); + + // Proceed. + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "direct" attestation. + await request.then(verifyDirectCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_proceed_anon() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct", {}); + await promiseNotification("webauthn-prompt-register-direct"); + + // Check "anonymize anyway" and proceed. + PopupNotifications.panel.firstElementChild.checkbox.checked = true; + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "none" attestation. + await request.then(verifyAnonymizedCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} 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..bb96432a02 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_telemetry.js @@ -0,0 +1,134 @@ +/* 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(); +} + +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.CTAPRegisterFinish, + 1, + "webauthn_used CTAPRegisterFinish scalar should be 1" + ); + is( + webauthn_used.CTAPSignFinish, + 1, + "webauthn_used CTAPSignFinish scalar should be 1" + ); + is( + webauthn_used.CTAPSignAbort, + undefined, + "webauthn_used CTAPSignAbort scalar must be unset" + ); + is( + webauthn_used.CTAPRegisterAbort, + undefined, + "webauthn_used CTAPRegisterAbort scalar must be unset" + ); + + 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..d2c364cecd --- /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> |