diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/webauthn/tests/browser | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webauthn/tests/browser')
-rw-r--r-- | dom/webauthn/tests/browser/browser.toml | 35 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_abort_visibility.js | 277 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_fido_appid_extension.js | 131 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js | 177 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_ipaddress.js | 30 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/browser_webauthn_prompts.js | 501 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/head.js | 259 | ||||
-rw-r--r-- | dom/webauthn/tests/browser/tab_webauthn_result.html | 14 |
8 files changed, 1424 insertions, 0 deletions
diff --git a/dom/webauthn/tests/browser/browser.toml b/dom/webauthn/tests/browser/browser.toml new file mode 100644 index 0000000000..41dc82d6ee --- /dev/null +++ b/dom/webauthn/tests/browser/browser.toml @@ -0,0 +1,35 @@ +[DEFAULT] +support-files = [ + "head.js", + "tab_webauthn_result.html", + "../pkijs/*", + "../cbor.js", + "../u2futil.js", +] +prefs = [ + "security.webauth.webauthn=true", + "security.webauth.webauthn_enable_softtoken=true", + "security.webauth.webauthn_enable_usbtoken=false", + "security.webauthn.ctap2=true", + "security.webauthn.enable_conditional_mediation=true", + "security.webauthn.enable_macos_passkeys=false", +] + +["browser_abort_visibility.js"] +skip-if = [ + "win11_2009", # Test not relevant on 1903+ +] + +["browser_fido_appid_extension.js"] +skip-if = [ + "win11_2009", # Test not relevant on 1903+ +] + +["browser_webauthn_conditional_mediation.js"] + +["browser_webauthn_ipaddress.js"] + +["browser_webauthn_prompts.js"] +skip-if = [ + "win11_2009", # Test not relevant on 1903+ +] diff --git a/dom/webauthn/tests/browser/browser_abort_visibility.js b/dom/webauthn/tests/browser/browser_abort_visibility.js new file mode 100644 index 0000000000..d8b2b5edcc --- /dev/null +++ b/dom/webauthn/tests/browser/browser_abort_visibility.js @@ -0,0 +1,277 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/dom/webauthn/tests/browser/tab_webauthn_result.html"; + +add_task(async function test_setup() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); +add_task(test_switch_tab); +add_task(test_new_window_make); +add_task(test_new_window_get); +add_task(test_minimize_make); +add_task(test_minimize_get); + +async function assertStatus(tab, expected) { + let actual = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function () { + info("visbility state: " + content.document.visibilityState); + info("active: " + content.browsingContext.isActive); + return content.document.getElementById("status").value; + } + ); + is(actual, expected, "webauthn request " + expected); +} + +async function waitForStatus(tab, expected) { + /* eslint-disable no-shadow */ + await SpecialPowers.spawn( + tab.linkedBrowser, + [[expected]], + async function (expected) { + return ContentTaskUtils.waitForCondition(() => { + info( + "expecting " + + expected + + ", visbility state: " + + content.document.visibilityState + ); + info( + "expecting " + + expected + + ", active: " + + content.browsingContext.isActive + ); + return content.document.getElementById("status").value == expected; + }); + } + ); + /* eslint-enable no-shadow */ + + await assertStatus(tab, expected); +} + +function startMakeCredentialRequest(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + const cose_alg_ECDSA_w_SHA256 = -7; + + let publicKey = { + rp: { id: content.document.domain, name: "none" }, + user: { + id: new Uint8Array(), + name: "none", + displayName: "none", + }, + challenge: content.crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{ type: "public-key", alg: cose_alg_ECDSA_w_SHA256 }], + }; + + let status = content.document.getElementById("status"); + + info( + "Attempting to create credential for origin: " + + content.document.nodePrincipal.origin + ); + content.navigator.credentials + .create({ publicKey }) + .then(() => { + status.value = "completed"; + }) + .catch(() => { + status.value = "aborted"; + }); + + status.value = "pending"; + }); +} + +function startGetAssertionRequest(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], async function () { + let newCredential = { + type: "public-key", + id: content.crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }; + + let publicKey = { + challenge: content.crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: content.document.domain, + allowCredentials: [newCredential], + }; + + let status = content.document.getElementById("status"); + + info( + "Attempting to get credential for origin: " + + content.document.nodePrincipal.origin + ); + content.navigator.credentials + .get({ publicKey }) + .then(() => { + status.value = "completed"; + }) + .catch(ex => { + info("aborted: " + ex); + status.value = "aborted"; + }); + + status.value = "pending"; + }); +} + +// Test that MakeCredential() and GetAssertion() requests +// are aborted when the current tab loses its focus. +async function test_switch_tab() { + // Create a new tab for the MakeCredential() request. + let tab_create = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_URL + ); + + // Start the request. + await startMakeCredentialRequest(tab_create); + await assertStatus(tab_create, "pending"); + + // Open another tab and switch to it. The first will lose focus. + let tab_get = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + await assertStatus(tab_create, "pending"); + + // Start a GetAssertion() request in the second tab, the first is aborted + await startGetAssertionRequest(tab_get); + await waitForStatus(tab_create, "aborted"); + await assertStatus(tab_get, "pending"); + + // Start a second request in the second tab. It should abort. + await startGetAssertionRequest(tab_get); + await waitForStatus(tab_get, "aborted"); + + // Close tabs. + BrowserTestUtils.removeTab(tab_create); + BrowserTestUtils.removeTab(tab_get); +} + +function waitForWindowActive(win, active) { + return Promise.all([ + BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"), + BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"), + ]); +} + +async function test_new_window_make() { + // Create a new tab for the MakeCredential() request. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start a MakeCredential request. + await startMakeCredentialRequest(tab); + await assertStatus(tab, "pending"); + + let windowGonePromise = waitForWindowActive(window, false); + // Open a new window. The tab will lose focus. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await windowGonePromise; + await assertStatus(tab, "pending"); + + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_new_window_get() { + // Create a new tab for the GetAssertion() request. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start a GetAssertion request. + await startGetAssertionRequest(tab); + await assertStatus(tab, "pending"); + + let windowGonePromise = waitForWindowActive(window, false); + // Open a new window. The tab will lose focus. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await windowGonePromise; + await assertStatus(tab, "pending"); + + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; + + // Close tab. + BrowserTestUtils.removeTab(tab); +} + +async function test_minimize_make() { + // Minimizing windows doesn't supported in headless mode. + if (Services.env.get("MOZ_HEADLESS")) { + return; + } + + // Create a new window for the MakeCredential() request. + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + // Start a MakeCredential request. + await startMakeCredentialRequest(tab); + await assertStatus(tab, "pending"); + + // Minimize the window. + let windowGonePromise = waitForWindowActive(win, false); + win.minimize(); + await assertStatus(tab, "pending"); + await windowGonePromise; + + // Restore the window. + await new Promise(resolve => SimpleTest.waitForFocus(resolve, win)); + await assertStatus(tab, "pending"); + + // Close window and wait for main window to be focused again. + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; +} + +async function test_minimize_get() { + // Minimizing windows doesn't supported in headless mode. + if (Services.env.get("MOZ_HEADLESS")) { + return; + } + + // Create a new window for the GetAssertion() request. + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + // Start a GetAssertion request. + await startGetAssertionRequest(tab); + await assertStatus(tab, "pending"); + + // Minimize the window. + let windowGonePromise = waitForWindowActive(win, false); + win.minimize(); + await assertStatus(tab, "pending"); + await windowGonePromise; + + // Restore the window. + await new Promise(resolve => SimpleTest.waitForFocus(resolve, win)); + await assertStatus(tab, "pending"); + + // Close window and wait for main window to be focused again. + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.removeTab(tab); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; +} diff --git a/dom/webauthn/tests/browser/browser_fido_appid_extension.js b/dom/webauthn/tests/browser/browser_fido_appid_extension.js new file mode 100644 index 0000000000..fca4443074 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_fido_appid_extension.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com/"; + +let expectNotSupportedError = expectError("NotSupported"); +let expectNotAllowedError = expectError("NotAllowed"); +let expectSecurityError = expectError("Security"); + +let gAppId = "https://example.com/appId"; +let gCrossOriginAppId = "https://example.org/appId"; +let gAuthenticatorId = add_virtual_authenticator(); + +add_task(async function test_appid() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // The FIDO AppId extension can't be used for MakeCredential. + await promiseWebAuthnMakeCredential(tab, "none", "discouraged", { + appid: gAppId, + }) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + + // Side-load a credential with an RP ID matching the App ID. + let credIdB64 = await addCredential(gAuthenticatorId, gAppId); + let credId = base64ToBytesUrlSafe(credIdB64); + + // And another for a different origin + let crossOriginCredIdB64 = await addCredential( + gAuthenticatorId, + gCrossOriginAppId + ); + let crossOriginCredId = base64ToBytesUrlSafe(crossOriginCredIdB64); + + // The App ID extension is required + await promiseWebAuthnGetAssertion(tab, credId) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // The value in the App ID extension must match the origin. + await promiseWebAuthnGetAssertion(tab, crossOriginCredId, { + appid: gCrossOriginAppId, + }) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // The value in the App ID extension must match the credential's RP ID. + await promiseWebAuthnGetAssertion(tab, credId, { appid: gAppId + "2" }) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // Succeed with the right App ID. + let rpIdHash = await promiseWebAuthnGetAssertion(tab, credId, { + appid: gAppId, + }) + .then(({ authenticatorData, extensions }) => { + is(extensions.appid, true, "appid extension was acted upon"); + return authenticatorData.slice(0, 32); + }) + .then(rpIdHash => { + // Make sure the returned RP ID hash matches the hash of the App ID. + checkRpIdHash(rpIdHash, gAppId); + }) + .catch(arrivingHereIsBad); + + removeCredential(gAuthenticatorId, credIdB64); + removeCredential(gAuthenticatorId, crossOriginCredIdB64); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_appid_unused() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let appid = "https://example.com/appId"; + + let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab); + let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj); + + // Make sure the RP ID hash matches what we calculate. + await checkRpIdHash(authDataObj.rpIdHash, "example.com"); + + // Get a new assertion. + let { clientDataJSON, authenticatorData, signature, extensions } = + await promiseWebAuthnGetAssertion(tab, rawId, { appid }); + + ok( + "appid" in extensions, + `appid should be populated in the extensions data, but saw: ` + + `${JSON.stringify(extensions)}` + ); + is(extensions.appid, false, "appid extension should indicate it was unused"); + + // Check auth data. + let attestation = await webAuthnDecodeAuthDataArray( + new Uint8Array(authenticatorData) + ); + is( + "" + (attestation.flags & flag_TUP), + "" + flag_TUP, + "Assertion's user presence byte set correctly" + ); + + // Verify the signature. + let params = await deriveAppAndChallengeParam( + "example.com", + clientDataJSON, + attestation + ); + let signedData = await assembleSignedData( + params.appParam, + params.attestation.flags, + params.attestation.counter, + params.challengeParam + ); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + signature + ); + ok(valid, "signature is valid"); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js b/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js new file mode 100644 index 0000000000..fff1ec5dab --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_conditional_mediation.js @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com"; + +let gAuthenticatorId = add_virtual_authenticator(); +let gExpectNotAllowedError = expectError("NotAllowed"); +let gExpectAbortError = expectError("Abort"); +let gPendingConditionalGetSubject = "webauthn:conditional-get-pending"; +let gWebAuthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService +); + +add_task(async function test_webauthn_modal_request_cancels_conditional_get() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let browser = tab.linkedBrowser.browsingContext.embedderElement; + let browsingContextId = browser.browsingContext.id; + + let transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.equal(transactionId, 0, "should not have a pending conditional get"); + + let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject); + + let active = true; + let condPromise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional") + .then(arrivingHereIsBad) + .catch(gExpectAbortError) + .then(() => (active = false)); + + await requestStarted; + + transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.notEqual(transactionId, 0, "should have a pending conditional get"); + + ok(active, "conditional request should still be active"); + + let promptPromise = promiseNotification("webauthn-prompt-register-direct"); + let modalPromise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(gExpectNotAllowedError); + + await condPromise; + + ok(!active, "conditional request should not be active"); + + // Cancel the modal request with the button. + await promptPromise; + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await modalPromise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_webauthn_resume_conditional_get() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let browser = tab.linkedBrowser.browsingContext.embedderElement; + let browsingContextId = browser.browsingContext.id; + + let transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.equal(transactionId, 0, "should not have a pending conditional get"); + + let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject); + + let active = true; + let promise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional") + .then(arrivingHereIsBad) + .catch(gExpectNotAllowedError) + .then(() => (active = false)); + + await requestStarted; + + transactionId = gWebAuthnService.hasPendingConditionalGet(0, TEST_URL); + Assert.equal( + transactionId, + 0, + "hasPendingConditionalGet should check the browsing context id" + ); + + transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + "https://example.org" + ); + Assert.equal( + transactionId, + 0, + "hasPendingConditionalGet should check the origin" + ); + + transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.notEqual(transactionId, 0, "should have a pending conditional get"); + + ok(active, "request should still be active"); + + gWebAuthnService.resumeConditionalGet(transactionId); + await promise; + + ok(!active, "request should not be active"); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_webauthn_select_autofill_entry() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Add credentials + let cred1 = await addCredential(gAuthenticatorId, "example.com"); + let cred2 = await addCredential(gAuthenticatorId, "example.com"); + + let browser = tab.linkedBrowser.browsingContext.embedderElement; + let browsingContextId = browser.browsingContext.id; + + let transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.equal(transactionId, 0, "should not have a pending conditional get"); + + let requestStarted = TestUtils.topicObserved(gPendingConditionalGetSubject); + + let active = true; + let promise = promiseWebAuthnGetAssertionDiscoverable(tab, "conditional") + .catch(arrivingHereIsBad) + .then(() => (active = false)); + + await requestStarted; + + transactionId = gWebAuthnService.hasPendingConditionalGet( + browsingContextId, + TEST_URL + ); + Assert.notEqual(transactionId, 0, "should have a pending conditional get"); + + let autoFillEntries = gWebAuthnService.getAutoFillEntries(transactionId); + ok( + autoFillEntries.length == 2 && + autoFillEntries[0].rpId == "example.com" && + autoFillEntries[1].rpId == "example.com", + "should have two autofill entries for example.com" + ); + + gWebAuthnService.selectAutoFillEntry( + transactionId, + autoFillEntries[0].credentialId + ); + let result = await promise; + + ok(!active, "request should not be active"); + + // Remove credentials + gWebAuthnService.removeCredential(gAuthenticatorId, cred1); + gWebAuthnService.removeCredential(gAuthenticatorId, cred2); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js new file mode 100644 index 0000000000..6658ae5f02 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_ipaddress.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_virtual_authenticator(); + +let expectSecurityError = expectError("Security"); + +add_task(async function test_setup() { + return SpecialPowers.pushPrefEnv({ + set: [["network.proxy.allow_hijacking_localhost", true]], + }); +}); + +add_task(async function test_appid() { + // 127.0.0.1 triggers special cases in ssltunnel, so let's use .2! + const TEST_URL = "https://127.0.0.2/"; + + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + await promiseWebAuthnMakeCredential(tab) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/browser/browser_webauthn_prompts.js b/dom/webauthn/tests/browser/browser_webauthn_prompts.js new file mode 100644 index 0000000000..05c77271d5 --- /dev/null +++ b/dom/webauthn/tests/browser/browser_webauthn_prompts.js @@ -0,0 +1,501 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +XPCOMUtils.defineLazyScriptGetter( + this, + ["FullScreen"], + "chrome://browser/content/browser-fullScreenAndPointerLock.js" +); + +const TEST_URL = "https://example.com/"; +var gAuthenticatorId; + +/** + * Waits for the PopupNotifications button enable delay to expire so the + * Notification can be interacted with using the buttons. + */ +async function waitForPopupNotificationSecurityDelay() { + let notification = PopupNotifications.panel.firstChild.notification; + let notificationEnableDelayMS = Services.prefs.getIntPref( + "security.notification_enable_delay" + ); + await TestUtils.waitForCondition( + () => { + let timeSinceShown = performance.now() - notification.timeShown; + return timeSinceShown > notificationEnableDelayMS; + }, + "Wait for security delay to expire", + 500, + 50 + ); +} + +add_task(async function test_setup_usbtoken() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); +add_task(test_register); +add_task(test_register_escape); +add_task(test_register_direct_cancel); +add_task(test_register_direct_presence); +add_task(test_sign); +add_task(test_sign_escape); +add_task(test_tab_switching); +add_task(test_window_switching); +add_task(async function test_setup_fullscreen() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["browser.fullscreen.autohide", true], + ["full-screen-api.enabled", true], + ["full-screen-api.allow-trusted-requests-only", false], + ], + }); +}); +add_task(test_fullscreen_show_nav_toolbar); +add_task(test_no_fullscreen_dom); +add_task(async function test_setup_softtoken() { + gAuthenticatorId = add_virtual_authenticator(); + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn_enable_softtoken", true], + ["security.webauth.webauthn_enable_usbtoken", false], + ], + }); +}); +add_task(test_register_direct_proceed); +add_task(test_register_direct_proceed_anon); +add_task(test_select_sign_result); + +function promiseNavToolboxStatus(aExpectedStatus) { + let navToolboxStatus; + return TestUtils.topicObserved("fullscreen-nav-toolbox", (subject, data) => { + navToolboxStatus = data; + return data == aExpectedStatus; + }).then(() => + Assert.equal( + navToolboxStatus, + aExpectedStatus, + "nav toolbox is " + aExpectedStatus + ) + ); +} + +function promiseFullScreenPaint(aExpectedStatus) { + return TestUtils.topicObserved("fullscreen-painted"); +} + +function triggerMainPopupCommand(popup) { + info("triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + info("triggering command: " + notification.getAttribute("buttonlabel")); + + return EventUtils.synthesizeMouseAtCenter(notification.button, {}); +} + +let expectNotAllowedError = expectError("NotAllowed"); + +function verifyAnonymizedCertificate(aResult) { + return webAuthnDecodeCBORAttestation(aResult.attObj).then( + ({ fmt, attStmt }) => { + is(fmt, "none", "Is a None Attestation"); + is(typeof attStmt, "object", "attStmt is a map"); + is(Object.keys(attStmt).length, 0, "attStmt is empty"); + } + ); +} + +async function verifyDirectCertificate(aResult) { + let clientDataHash = await crypto.subtle + .digest("SHA-256", aResult.clientDataJSON) + .then(digest => new Uint8Array(digest)); + let { fmt, attStmt, authData, authDataObj } = + await webAuthnDecodeCBORAttestation(aResult.attObj); + is(fmt, "packed", "Is a Packed Attestation"); + let signedData = new Uint8Array(authData.length + clientDataHash.length); + signedData.set(authData); + signedData.set(clientDataHash, authData.length); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + new Uint8Array(attStmt.sig) + ); + ok(valid, "Signature is valid."); +} + +async function test_register() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request with the button. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_escape() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request by hitting escape. + ok(active, "request should still be active"); + EventUtils.synthesizeKey("KEY_Escape"); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_sign() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new assertion and wait for the prompt. + let active = true; + let request = promiseWebAuthnGetAssertion(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request with the button. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_sign_escape() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new assertion and wait for the prompt. + let active = true; + let request = promiseWebAuthnGetAssertion(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + // Cancel the request by hitting escape. + ok(active, "request should still be active"); + EventUtils.synthesizeKey("KEY_Escape"); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_cancel() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let active = true; + let promise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register-direct"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await promise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_presence() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let active = true; + let promise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register-direct"); + + // Click "proceed" and wait for presence prompt + let presence = promiseNotification("webauthn-prompt-presence"); + PopupNotifications.panel.firstElementChild.button.click(); + await presence; + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await promise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +async function test_tab_switching() { + // Open a new tab. + let tab_one = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab_one) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second tab. + let tab_two = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org/" + ); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "closed" + ); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + + // Go back to the first tab + await BrowserTestUtils.removeTab(tab_two); + + await promiseNotification("webauthn-prompt-presence"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab_one); +} + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +async function test_window_switching() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-presence"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second window + let new_window = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(new_window); + + await TestUtils.waitForCondition( + () => new_window.PopupNotifications.panel.state == "closed" + ); + is( + new_window.PopupNotifications.panel.state, + "closed", + "Doorhanger is hidden" + ); + + // Go back to the first tab + await BrowserTestUtils.closeWindow(new_window); + await SimpleTest.promiseFocus(window); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is still visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_proceed() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct"); + await promiseNotification("webauthn-prompt-register-direct"); + + // Proceed. + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "direct" attestation. + await request.then(verifyDirectCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_proceed_anon() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct"); + await promiseNotification("webauthn-prompt-register-direct"); + + // Check "anonymize anyway" and proceed. + PopupNotifications.panel.firstElementChild.checkbox.checked = true; + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "none" attestation. + await request.then(verifyAnonymizedCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_select_sign_result() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Make two discoverable credentials for the same RP ID so that + // the user has to select one to return. + let cred1 = await addCredential(gAuthenticatorId, "example.com"); + let cred2 = await addCredential(gAuthenticatorId, "example.com"); + + let active = true; + let request = promiseWebAuthnGetAssertionDiscoverable(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + + // Ensure the selection prompt is shown + await promiseNotification("webauthn-prompt-select-sign-result"); + + ok(active, "request is active"); + + // Cancel the request + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + await removeCredential(gAuthenticatorId, cred1); + await removeCredential(gAuthenticatorId, cred2); + await BrowserTestUtils.removeTab(tab); +} + +async function test_fullscreen_show_nav_toolbar() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start with the window fullscreen and the nav toolbox hidden + let fullscreenState = window.fullScreen; + + let navToolboxHiddenPromise = promiseNavToolboxStatus("hidden"); + + window.fullScreen = true; + FullScreen.hideNavToolbox(false); + + await navToolboxHiddenPromise; + + // Request a new credential and wait for the direct attestation consent + // prompt. + let promptPromise = promiseNotification("webauthn-prompt-register-direct"); + let navToolboxShownPromise = promiseNavToolboxStatus("shown"); + + let active = true; + let requestPromise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + + await Promise.all([promptPromise, navToolboxShownPromise]); + + ok(active, "request is active"); + ok(window.fullScreen, "window is fullscreen"); + + // Cancel the request. + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await requestPromise; + + window.fullScreen = fullscreenState; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_no_fullscreen_dom() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + let fullScreenPaintPromise = promiseFullScreenPaint(); + // Make a DOM element fullscreen + await ContentTask.spawn(tab.linkedBrowser, [], () => { + return content.document.body.requestFullscreen(); + }); + await fullScreenPaintPromise; + ok(!!document.fullscreenElement, "a DOM element is fullscreen"); + + // Request a new credential and wait for the direct attestation consent + // prompt. + let promptPromise = promiseNotification("webauthn-prompt-register-direct"); + fullScreenPaintPromise = promiseFullScreenPaint(); + + let active = true; + let requestPromise = promiseWebAuthnMakeCredential(tab, "direct") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + + await Promise.all([promptPromise, fullScreenPaintPromise]); + + ok(active, "request is active"); + ok(!document.fullscreenElement, "no DOM element is fullscreen"); + + // Cancel the request. + await waitForPopupNotificationSecurityDelay(); + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await requestPromise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} diff --git a/dom/webauthn/tests/browser/head.js b/dom/webauthn/tests/browser/head.js new file mode 100644 index 0000000000..d6cbd56133 --- /dev/null +++ b/dom/webauthn/tests/browser/head.js @@ -0,0 +1,259 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let exports = this; + +const scripts = [ + "pkijs/common.js", + "pkijs/asn1.js", + "pkijs/x509_schema.js", + "pkijs/x509_simpl.js", + "browser/cbor.js", + "browser/u2futil.js", +]; + +for (let script of scripts) { + Services.scriptloader.loadSubScript( + `chrome://mochitests/content/browser/dom/webauthn/tests/${script}`, + this + ); +} + +function add_virtual_authenticator(autoremove = true) { + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + let id = webauthnService.addVirtualAuthenticator( + "ctap2_1", + "internal", + true, + true, + true, + true + ); + if (autoremove) { + registerCleanupFunction(() => { + webauthnService.removeVirtualAuthenticator(id); + }); + } + return id; +} + +async function addCredential(authenticatorId, rpId) { + let keyPair = await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + }, + true, + ["sign"] + ); + + let credId = new Uint8Array(32); + crypto.getRandomValues(credId); + credId = bytesToBase64UrlSafe(credId); + + let privateKey = await crypto.subtle + .exportKey("pkcs8", keyPair.privateKey) + .then(privateKey => bytesToBase64UrlSafe(privateKey)); + + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + + webauthnService.addCredential( + authenticatorId, + credId, + true, // resident key + rpId, + privateKey, + "VGVzdCBVc2Vy", // "Test User" + 0 // sign count + ); + + return credId; +} + +async function removeCredential(authenticatorId, credId) { + let webauthnService = Cc["@mozilla.org/webauthn/service;1"].getService( + Ci.nsIWebAuthnService + ); + + webauthnService.removeCredential(authenticatorId, credId); +} + +function memcmp(x, y) { + let xb = new Uint8Array(x); + let yb = new Uint8Array(y); + + if (x.byteLength != y.byteLength) { + return false; + } + + for (let i = 0; i < xb.byteLength; ++i) { + if (xb[i] != yb[i]) { + return false; + } + } + + return true; +} + +function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); +} + +function expectError(aType) { + let expected = `${aType}Error`; + return function (aResult) { + is( + aResult.slice(0, expected.length), + expected, + `Expecting a ${aType}Error` + ); + }; +} + +/* eslint-disable no-shadow */ +function promiseWebAuthnMakeCredential( + tab, + attestation = "none", + residentKey = "discouraged", + extensions = {} +) { + return ContentTask.spawn( + tab.linkedBrowser, + [attestation, residentKey, extensions], + ([attestation, residentKey, extensions]) => { + const cose_alg_ECDSA_w_SHA256 = -7; + + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + + let pubKeyCredParams = [ + { + type: "public-key", + alg: cose_alg_ECDSA_w_SHA256, + }, + ]; + + let publicKey = { + rp: { id: content.document.domain, name: "none" }, + user: { + id: new Uint8Array(), + name: "none", + displayName: "none", + }, + pubKeyCredParams, + authenticatorSelection: { + authenticatorAttachment: "cross-platform", + residentKey, + }, + extensions, + attestation, + challenge, + }; + + return content.navigator.credentials + .create({ publicKey }) + .then(credential => { + return { + clientDataJSON: credential.response.clientDataJSON, + attObj: credential.response.attestationObject, + rawId: credential.rawId, + }; + }); + } + ); +} + +function promiseWebAuthnGetAssertion(tab, key_handle = null, extensions = {}) { + return ContentTask.spawn( + tab.linkedBrowser, + [key_handle, extensions], + ([key_handle, extensions]) => { + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + if (key_handle == null) { + key_handle = content.crypto.getRandomValues(new Uint8Array(16)); + } + + let credential = { + id: key_handle, + type: "public-key", + transports: ["usb"], + }; + + let publicKey = { + challenge, + extensions, + rpId: content.document.domain, + allowCredentials: [credential], + }; + + return content.navigator.credentials + .get({ publicKey }) + .then(assertion => { + return { + authenticatorData: assertion.response.authenticatorData, + clientDataJSON: assertion.response.clientDataJSON, + extensions: assertion.getClientExtensionResults(), + signature: assertion.response.signature, + }; + }); + } + ); +} + +function promiseWebAuthnGetAssertionDiscoverable( + tab, + mediation = "optional", + extensions = {} +) { + return ContentTask.spawn( + tab.linkedBrowser, + [extensions, mediation], + ([extensions, mediation]) => { + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + + let publicKey = { + challenge, + extensions, + rpId: content.document.domain, + allowCredentials: [], + }; + + return content.navigator.credentials.get({ publicKey, mediation }); + } + ); +} + +function checkRpIdHash(rpIdHash, hostname) { + return crypto.subtle + .digest("SHA-256", string2buffer(hostname)) + .then(calculatedRpIdHash => { + let calcHashStr = bytesToBase64UrlSafe( + new Uint8Array(calculatedRpIdHash) + ); + let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(rpIdHash)); + + if (calcHashStr != providedHashStr) { + throw new Error("Calculated RP ID hash doesn't match."); + } + }); +} + +function promiseNotification(id) { + return new Promise(resolve => { + PopupNotifications.panel.addEventListener("popupshown", function shown() { + let notification = PopupNotifications.getNotification(id); + if (notification) { + ok(true, `${id} prompt visible`); + PopupNotifications.panel.removeEventListener("popupshown", shown); + resolve(); + } + }); + }); +} +/* eslint-enable no-shadow */ diff --git a/dom/webauthn/tests/browser/tab_webauthn_result.html b/dom/webauthn/tests/browser/tab_webauthn_result.html new file mode 100644 index 0000000000..8e8b9f82cd --- /dev/null +++ b/dom/webauthn/tests/browser/tab_webauthn_result.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Generic W3C Web Authentication Test Result Page</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Generic W3C Web Authentication Test Result Page</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a> +<input type="text" id="status" value="init" /> + +</body> +</html> |