diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/webauthn/tests/u2f | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webauthn/tests/u2f')
25 files changed, 3353 insertions, 0 deletions
diff --git a/dom/webauthn/tests/u2f/browser/browser.ini b/dom/webauthn/tests/u2f/browser/browser.ini new file mode 100644 index 0000000000..2145f61ecc --- /dev/null +++ b/dom/webauthn/tests/u2f/browser/browser.ini @@ -0,0 +1,32 @@ +[DEFAULT] +support-files = + head.js + tab_webauthn_result.html + ../../pkijs/* + ../../cbor.js + ../../u2futil.js +prefs = + security.webauth.webauthn=true + security.webauth.webauthn_enable_softtoken=true + security.webauth.webauthn_enable_android_fido2=false + security.webauth.webauthn_enable_usbtoken=false + security.webauthn.ctap2=false + +[browser_abort_visibility.js] +skip-if = + win10_2004 # Test not relevant on 1903+ + win11_2009 # Test not relevant on 1903+ +[browser_fido_appid_extension.js] +skip-if = + win10_2004 # Test not relevant on 1903+ + win11_2009 # Test not relevant on 1903+ +[browser_webauthn_prompts.js] +skip-if = + win10_2004 # Test not relevant on 1903+ + win11_2009 # Test not relevant on 1903+ +[browser_webauthn_telemetry.js] +skip-if = + win10_2004 # Test not relevant on 1903+ + win11_2009 # Test not relevant on 1903+ + fission && os == "linux" && asan # Bug 1713907 - new Fission platform triage +[browser_webauthn_ipaddress.js] diff --git a/dom/webauthn/tests/u2f/browser/browser_abort_visibility.js b/dom/webauthn/tests/u2f/browser/browser_abort_visibility.js new file mode 100644 index 0000000000..059a9de0b3 --- /dev/null +++ b/dom/webauthn/tests/u2f/browser/browser_abort_visibility.js @@ -0,0 +1,274 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = + "https://example.com/browser/dom/webauthn/tests/browser/tab_webauthn_result.html"; + +add_task(async function test_setup() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); +add_task(test_switch_tab); +add_task(test_new_window_make); +add_task(test_new_window_get); +add_task(test_minimize_make); +add_task(test_minimize_get); + +async function assertStatus(tab, expected) { + let actual = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function() { + info("visbility state: " + content.document.visibilityState); + info("active: " + content.browsingContext.isActive); + return content.document.getElementById("status").value; + } + ); + is(actual, expected, "webauthn request " + expected); +} + +async function waitForStatus(tab, expected) { + /* eslint-disable no-shadow */ + await SpecialPowers.spawn(tab.linkedBrowser, [[expected]], async function( + expected + ) { + return ContentTaskUtils.waitForCondition(() => { + info( + "expecting " + + expected + + ", visbility state: " + + content.document.visibilityState + ); + info( + "expecting " + + expected + + ", active: " + + content.browsingContext.isActive + ); + return content.document.getElementById("status").value == expected; + }); + }); + /* eslint-enable no-shadow */ + + await assertStatus(tab, expected); +} + +function startMakeCredentialRequest(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + const cose_alg_ECDSA_w_SHA256 = -7; + + let publicKey = { + rp: { id: content.document.domain, name: "none", icon: "none" }, + user: { + id: new Uint8Array(), + name: "none", + icon: "none", + displayName: "none", + }, + challenge: content.crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{ type: "public-key", alg: cose_alg_ECDSA_w_SHA256 }], + }; + + let status = content.document.getElementById("status"); + + info( + "Attempting to create credential for origin: " + + content.document.nodePrincipal.origin + ); + content.navigator.credentials + .create({ publicKey }) + .then(() => { + status.value = "completed"; + }) + .catch(() => { + status.value = "aborted"; + }); + + status.value = "pending"; + }); +} + +function startGetAssertionRequest(tab) { + return SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let newCredential = { + type: "public-key", + id: content.crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }; + + let publicKey = { + challenge: content.crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: content.document.domain, + allowCredentials: [newCredential], + }; + + let status = content.document.getElementById("status"); + + info( + "Attempting to get credential for origin: " + + content.document.nodePrincipal.origin + ); + content.navigator.credentials + .get({ publicKey }) + .then(() => { + status.value = "completed"; + }) + .catch(ex => { + info("aborted: " + ex); + status.value = "aborted"; + }); + + status.value = "pending"; + }); +} + +// Test that MakeCredential() and GetAssertion() requests +// are aborted when the current tab loses its focus. +async function test_switch_tab() { + // Create a new tab for the MakeCredential() request. + let tab_create = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TEST_URL + ); + + // Start the request. + await startMakeCredentialRequest(tab_create); + await assertStatus(tab_create, "pending"); + + // Open another tab and switch to it. The first will lose focus. + let tab_get = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + await assertStatus(tab_create, "pending"); + + // Start a GetAssertion() request in the second tab, the first is aborted + await startGetAssertionRequest(tab_get); + await waitForStatus(tab_create, "aborted"); + await assertStatus(tab_get, "pending"); + + // Start a second request in the second tab. It should abort. + await startGetAssertionRequest(tab_get); + await waitForStatus(tab_get, "aborted"); + + // Close tabs. + BrowserTestUtils.removeTab(tab_create); + BrowserTestUtils.removeTab(tab_get); +} + +function waitForWindowActive(win, active) { + return Promise.all([ + BrowserTestUtils.waitForEvent(win, active ? "focus" : "blur"), + BrowserTestUtils.waitForEvent(win, active ? "activate" : "deactivate"), + ]); +} + +async function test_new_window_make() { + // Create a new tab for the MakeCredential() request. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start a MakeCredential request. + await startMakeCredentialRequest(tab); + await assertStatus(tab, "pending"); + + let windowGonePromise = waitForWindowActive(window, false); + // Open a new window. The tab will lose focus. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await windowGonePromise; + await assertStatus(tab, "pending"); + + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_new_window_get() { + // Create a new tab for the GetAssertion() request. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Start a GetAssertion request. + await startGetAssertionRequest(tab); + await assertStatus(tab, "pending"); + + let windowGonePromise = waitForWindowActive(window, false); + // Open a new window. The tab will lose focus. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await windowGonePromise; + await assertStatus(tab, "pending"); + + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; + + // Close tab. + BrowserTestUtils.removeTab(tab); +} + +async function test_minimize_make() { + // Minimizing windows doesn't supported in headless mode. + if (Services.env.get("MOZ_HEADLESS")) { + return; + } + + // Create a new window for the MakeCredential() request. + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + // Start a MakeCredential request. + await startMakeCredentialRequest(tab); + await assertStatus(tab, "pending"); + + // Minimize the window. + let windowGonePromise = waitForWindowActive(win, false); + win.minimize(); + await assertStatus(tab, "pending"); + await windowGonePromise; + + // Restore the window. + await new Promise(resolve => SimpleTest.waitForFocus(resolve, win)); + await assertStatus(tab, "pending"); + + // Close window and wait for main window to be focused again. + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; +} + +async function test_minimize_get() { + // Minimizing windows doesn't supported in headless mode. + if (Services.env.get("MOZ_HEADLESS")) { + return; + } + + // Create a new window for the GetAssertion() request. + let win = await BrowserTestUtils.openNewBrowserWindow(); + let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL); + + // Start a GetAssertion request. + await startGetAssertionRequest(tab); + await assertStatus(tab, "pending"); + + // Minimize the window. + let windowGonePromise = waitForWindowActive(win, false); + win.minimize(); + await assertStatus(tab, "pending"); + await windowGonePromise; + + // Restore the window. + await new Promise(resolve => SimpleTest.waitForFocus(resolve, win)); + await assertStatus(tab, "pending"); + + // Close window and wait for main window to be focused again. + let windowBackPromise = waitForWindowActive(window, true); + await BrowserTestUtils.closeWindow(win); + await windowBackPromise; +} diff --git a/dom/webauthn/tests/u2f/browser/browser_fido_appid_extension.js b/dom/webauthn/tests/u2f/browser/browser_fido_appid_extension.js new file mode 100644 index 0000000000..3988e01b87 --- /dev/null +++ b/dom/webauthn/tests/u2f/browser/browser_fido_appid_extension.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com/"; + +let expectNotSupportedError = expectError("NotSupported"); +let expectInvalidStateError = expectError("InvalidState"); +let expectSecurityError = expectError("Security"); + +function promiseU2FRegister(tab, app_id_) { + let challenge_ = crypto.getRandomValues(new Uint8Array(16)); + challenge_ = bytesToBase64UrlSafe(challenge_); + + return SpecialPowers.spawn( + tab.linkedBrowser, + [[app_id_, challenge_]], + function([app_id, challenge]) { + return new Promise(resolve => { + content.u2f.register( + app_id, + [{ version: "U2F_V2", challenge }], + [], + resolve + ); + }); + } + ).then(res => { + is(res.errorCode, 0, "u2f.register() succeeded"); + let data = base64ToBytesUrlSafe(res.registrationData); + is(data[0], 0x05, "Reserved byte is correct"); + return data.slice(67, 67 + data[66]); + }); +} + +add_task(async function test_setup_u2f() { + return SpecialPowers.pushPrefEnv({ + set: [["security.webauth.u2f", true]], + }); +}); +add_task(async function test_appid() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Get a keyHandle for a FIDO AppId. + let appid = "https://example.com/appId"; + let keyHandle = await promiseU2FRegister(tab, appid); + + // The FIDO AppId extension can't be used for MakeCredential. + await promiseWebAuthnMakeCredential(tab, "none", { appid }) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + + // Using the keyHandle shouldn't work without the FIDO AppId extension. + // This will be an invalid state, because the softtoken will consent without + // having the correct "RP ID" via the FIDO extension. + await promiseWebAuthnGetAssertion(tab, keyHandle) + .then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + // Invalid app IDs (for the current origin) must be rejected. + await promiseWebAuthnGetAssertion(tab, keyHandle, { + appid: "https://bogus.com/appId", + }) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // Non-matching app IDs must be rejected. Even when the user/softtoken + // consents, leading to an invalid state. + await promiseWebAuthnGetAssertion(tab, keyHandle, { appid: appid + "2" }) + .then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + let rpId = new TextEncoder("utf-8").encode(appid); + let rpIdHash = await crypto.subtle.digest("SHA-256", rpId); + + // Succeed with the right fallback rpId. + await promiseWebAuthnGetAssertion(tab, keyHandle, { appid }).then( + ({ authenticatorData, clientDataJSON, extensions }) => { + is(extensions.appid, true, "appid extension was acted upon"); + + // Check that the correct rpIdHash is returned. + let rpIdHashSign = authenticatorData.slice(0, 32); + ok(memcmp(rpIdHash, rpIdHashSign), "rpIdHash is correct"); + } + ); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_appid_unused() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Get a keyHandle for a FIDO AppId. + let appid = "https://example.com/appId"; + + let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab); + let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj); + + // Make sure the RP ID hash matches what we calculate. + await checkRpIdHash(authDataObj.rpIdHash, "example.com"); + + // Get a new assertion. + let { + clientDataJSON, + authenticatorData, + signature, + extensions, + } = await promiseWebAuthnGetAssertion(tab, rawId, { appid }); + + ok( + "appid" in extensions, + `appid should be populated in the extensions data, but saw: ` + + `${JSON.stringify(extensions)}` + ); + is(extensions.appid, false, "appid extension should indicate it was unused"); + + // Check auth data. + let attestation = await webAuthnDecodeAuthDataArray( + new Uint8Array(authenticatorData) + ); + is( + "" + attestation.flags, + "" + flag_TUP, + "Assertion's user presence byte set correctly" + ); + + // Verify the signature. + let params = await deriveAppAndChallengeParam( + "example.com", + clientDataJSON, + attestation + ); + let signedData = await assembleSignedData( + params.appParam, + params.attestation.flags, + params.attestation.counter, + params.challengeParam + ); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + signature + ); + ok(valid, "signature is valid"); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/u2f/browser/browser_webauthn_ipaddress.js b/dom/webauthn/tests/u2f/browser/browser_webauthn_ipaddress.js new file mode 100644 index 0000000000..2c3f8ea025 --- /dev/null +++ b/dom/webauthn/tests/u2f/browser/browser_webauthn_ipaddress.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let expectSecurityError = expectError("Security"); + +add_task(async function test_setup() { + return SpecialPowers.pushPrefEnv({ + set: [["network.proxy.allow_hijacking_localhost", true]], + }); +}); + +add_task(async function test_appid() { + // 127.0.0.1 triggers special cases in ssltunnel, so let's use .2! + const TEST_URL = "https://127.0.0.2/"; + + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + await promiseWebAuthnMakeCredential(tab, "none", {}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + + // Close tab. + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/webauthn/tests/u2f/browser/browser_webauthn_prompts.js b/dom/webauthn/tests/u2f/browser/browser_webauthn_prompts.js new file mode 100644 index 0000000000..fbf7585896 --- /dev/null +++ b/dom/webauthn/tests/u2f/browser/browser_webauthn_prompts.js @@ -0,0 +1,276 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const TEST_URL = "https://example.com/"; + +add_task(async function test_setup_usbtoken() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.u2f", false], + ["security.webauth.webauthn", true], + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ], + }); +}); +add_task(test_register); +add_task(test_sign); +add_task(test_register_direct_cancel); +add_task(test_tab_switching); +add_task(test_window_switching); +add_task(async function test_setup_softtoken() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.u2f", false], + ["security.webauth.webauthn", true], + ["security.webauth.webauthn_enable_softtoken", true], + ["security.webauth.webauthn_enable_usbtoken", false], + ], + }); +}); +add_task(test_register_direct_proceed); +add_task(test_register_direct_proceed_anon); + +function promiseNotification(id) { + return new Promise(resolve => { + PopupNotifications.panel.addEventListener("popupshown", function shown() { + let notification = PopupNotifications.getNotification(id); + if (notification) { + ok(true, `${id} prompt visible`); + PopupNotifications.panel.removeEventListener("popupshown", shown); + resolve(); + } + }); + }); +} + +function triggerMainPopupCommand(popup) { + info("triggering main command"); + let notifications = popup.childNodes; + ok(notifications.length, "at least one notification displayed"); + let notification = notifications[0]; + info("triggering command: " + notification.getAttribute("buttonlabel")); + + return EventUtils.synthesizeMouseAtCenter(notification.button, {}); +} + +let expectNotAllowedError = expectError("NotAllowed"); + +function verifyAnonymizedCertificate(result) { + let { attObj, rawId } = result; + return webAuthnDecodeCBORAttestation(attObj).then(({ fmt, attStmt }) => { + is("none", fmt, "Is a None Attestation"); + is("object", typeof attStmt, "attStmt is a map"); + is(0, Object.keys(attStmt).length, "attStmt is empty"); + }); +} + +function verifyDirectCertificate(result) { + let { attObj, rawId } = result; + return webAuthnDecodeCBORAttestation(attObj).then(({ fmt, attStmt }) => { + is("fido-u2f", fmt, "Is a FIDO U2F Attestation"); + is("object", typeof attStmt, "attStmt is a map"); + ok(attStmt.hasOwnProperty("x5c"), "attStmt.x5c exists"); + ok(attStmt.hasOwnProperty("sig"), "attStmt.sig exists"); + }); +} + +async function test_register() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab, "none", {}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_sign() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new assertion and wait for the prompt. + let active = true; + let request = promiseWebAuthnGetAssertion(tab) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-sign"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.button.click(); + await request; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_cancel() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let active = true; + let promise = promiseWebAuthnMakeCredential(tab, "direct", {}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register-direct"); + + // Cancel the request. + ok(active, "request should still be active"); + PopupNotifications.panel.firstElementChild.secondaryButton.click(); + await promise; + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +async function test_tab_switching() { + // Open a new tab. + let tab_one = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab_one, "none", {}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register"); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second tab. + let tab_two = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "https://example.org/" + ); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "closed" + ); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + + // Go back to the first tab + await BrowserTestUtils.removeTab(tab_two); + + await promiseNotification("webauthn-prompt-register"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab_one); +} + +// Add two tabs, open WebAuthn in the first, switch, assert the prompt is +// not visible, switch back, assert the prompt is there and cancel it. +async function test_window_switching() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential and wait for the prompt. + let active = true; + let request = promiseWebAuthnMakeCredential(tab, "none", {}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError) + .then(() => (active = false)); + await promiseNotification("webauthn-prompt-register"); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is visible"); + + // Open and switch to a second window + let new_window = await BrowserTestUtils.openNewBrowserWindow(); + await SimpleTest.promiseFocus(new_window); + + await TestUtils.waitForCondition( + () => new_window.PopupNotifications.panel.state == "closed" + ); + is( + new_window.PopupNotifications.panel.state, + "closed", + "Doorhanger is hidden" + ); + + // Go back to the first tab + await BrowserTestUtils.closeWindow(new_window); + await SimpleTest.promiseFocus(window); + + await TestUtils.waitForCondition( + () => PopupNotifications.panel.state == "open" + ); + is(PopupNotifications.panel.state, "open", "Doorhanger is still visible"); + + // Cancel the request. + ok(active, "request should still be active"); + await triggerMainPopupCommand(PopupNotifications.panel); + await request; + ok(!active, "request should be stopped"); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_proceed() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct", {}); + await promiseNotification("webauthn-prompt-register-direct"); + + // Proceed. + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "direct" attestation. + await request.then(verifyDirectCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} + +async function test_register_direct_proceed_anon() { + // Open a new tab. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Request a new credential with direct attestation and wait for the prompt. + let request = promiseWebAuthnMakeCredential(tab, "direct", {}); + await promiseNotification("webauthn-prompt-register-direct"); + + // Check "anonymize anyway" and proceed. + PopupNotifications.panel.firstElementChild.checkbox.checked = true; + PopupNotifications.panel.firstElementChild.button.click(); + + // Ensure we got "none" attestation. + await request.then(verifyAnonymizedCertificate); + + // Close tab. + await BrowserTestUtils.removeTab(tab); +} diff --git a/dom/webauthn/tests/u2f/browser/browser_webauthn_telemetry.js b/dom/webauthn/tests/u2f/browser/browser_webauthn_telemetry.js new file mode 100644 index 0000000000..92af3c8a50 --- /dev/null +++ b/dom/webauthn/tests/u2f/browser/browser_webauthn_telemetry.js @@ -0,0 +1,142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", +}); + +const TEST_URL = "https://example.com/"; + +function getTelemetryForScalar(aName) { + let scalars = TelemetryTestUtils.getProcessScalars("parent", true); + return scalars[aName] || 0; +} + +function cleanupTelemetry() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + Services.telemetry.getHistogramById("WEBAUTHN_CREATE_CREDENTIAL_MS").clear(); + Services.telemetry.getHistogramById("WEBAUTHN_GET_ASSERTION_MS").clear(); +} + +function validateHistogramEntryCount(aHistogramName, aExpectedCount) { + let hist = Services.telemetry.getHistogramById(aHistogramName); + let resultIndexes = hist.snapshot(); + + let entriesSeen = Object.values(resultIndexes.values).reduce( + (a, b) => a + b, + 0 + ); + + is( + entriesSeen, + aExpectedCount, + "Expecting " + aExpectedCount + " histogram entries in " + aHistogramName + ); +} + +add_task(async function test_setup() { + cleanupTelemetry(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.webauth.webauthn", true], + ["security.webauth.webauthn_enable_softtoken", true], + ["security.webauth.webauthn_enable_usbtoken", false], + ["security.webauth.webauthn_enable_android_fido2", false], + ["security.webauth.webauthn_testing_allow_direct_attestation", true], + ], + }); +}); + +add_task(async function test() { + // These tests can't run simultaneously as the preference changes will race. + // So let's run them sequentially here, but in an async function so we can + // use await. + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + // Create a new credential. + let { attObj, rawId } = await promiseWebAuthnMakeCredential(tab); + let { authDataObj } = await webAuthnDecodeCBORAttestation(attObj); + + // Make sure the RP ID hash matches what we calculate. + await checkRpIdHash(authDataObj.rpIdHash, "example.com"); + + // Get a new assertion. + let { + clientDataJSON, + authenticatorData, + signature, + } = await promiseWebAuthnGetAssertion(tab, rawId); + + // Check the we can parse clientDataJSON. + JSON.parse(buffer2string(clientDataJSON)); + + // Check auth data. + let attestation = await webAuthnDecodeAuthDataArray( + new Uint8Array(authenticatorData) + ); + is( + "" + attestation.flags, + "" + flag_TUP, + "Assertion's user presence byte set correctly" + ); + + // Verify the signature. + let params = await deriveAppAndChallengeParam( + "example.com", + clientDataJSON, + attestation + ); + let signedData = await assembleSignedData( + params.appParam, + params.attestation.flags, + params.attestation.counter, + params.challengeParam + ); + let valid = await verifySignature( + authDataObj.publicKeyHandle, + signedData, + signature + ); + ok(valid, "signature is valid"); + + // Check telemetry data. + let webauthn_used = getTelemetryForScalar("security.webauthn_used"); + ok( + webauthn_used, + "Scalar keys are set: " + Object.keys(webauthn_used).join(", ") + ); + is( + webauthn_used.U2FRegisterFinish, + 1, + "webauthn_used U2FRegisterFinish scalar should be 1" + ); + is( + webauthn_used.U2FSignFinish, + 1, + "webauthn_used U2FSignFinish scalar should be 1" + ); + is( + webauthn_used.U2FSignAbort, + undefined, + "webauthn_used U2FSignAbort scalar must be unset" + ); + is( + webauthn_used.U2FRegisterAbort, + undefined, + "webauthn_used U2FRegisterAbort scalar must be unset" + ); + + validateHistogramEntryCount("WEBAUTHN_CREATE_CREDENTIAL_MS", 1); + validateHistogramEntryCount("WEBAUTHN_GET_ASSERTION_MS", 1); + + BrowserTestUtils.removeTab(tab); + + // There aren't tests for register succeeding and sign failing, as I don't see an easy way to prompt + // the soft token to fail that way _and_ trigger the Abort telemetry. +}); diff --git a/dom/webauthn/tests/u2f/browser/head.js b/dom/webauthn/tests/u2f/browser/head.js new file mode 100644 index 0000000000..70ea10c526 --- /dev/null +++ b/dom/webauthn/tests/u2f/browser/head.js @@ -0,0 +1,155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let exports = this; + +const scripts = [ + "pkijs/common.js", + "pkijs/asn1.js", + "pkijs/x509_schema.js", + "pkijs/x509_simpl.js", + "browser/cbor.js", + "browser/u2futil.js", +]; + +for (let script of scripts) { + Services.scriptloader.loadSubScript( + `chrome://mochitests/content/browser/dom/webauthn/tests/${script}`, + this + ); +} + +function memcmp(x, y) { + let xb = new Uint8Array(x); + let yb = new Uint8Array(y); + + if (x.byteLength != y.byteLength) { + return false; + } + + for (let i = 0; i < xb.byteLength; ++i) { + if (xb[i] != yb[i]) { + return false; + } + } + + return true; +} + +function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); +} + +function expectError(aType) { + let expected = `${aType}Error`; + return function(aResult) { + is( + aResult.slice(0, expected.length), + expected, + `Expecting a ${aType}Error` + ); + }; +} + +/* eslint-disable no-shadow */ +function promiseWebAuthnMakeCredential( + tab, + attestation = "none", + extensions = {} +) { + return ContentTask.spawn( + tab.linkedBrowser, + [attestation, extensions], + ([attestation, extensions]) => { + const cose_alg_ECDSA_w_SHA256 = -7; + + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + + let pubKeyCredParams = [ + { + type: "public-key", + alg: cose_alg_ECDSA_w_SHA256, + }, + ]; + + let publicKey = { + rp: { id: content.document.domain, name: "none", icon: "none" }, + user: { + id: new Uint8Array(), + name: "none", + icon: "none", + displayName: "none", + }, + pubKeyCredParams, + extensions, + attestation, + challenge, + }; + + return content.navigator.credentials + .create({ publicKey }) + .then(credential => { + return { + attObj: credential.response.attestationObject, + rawId: credential.rawId, + }; + }); + } + ); +} + +function promiseWebAuthnGetAssertion(tab, key_handle = null, extensions = {}) { + return ContentTask.spawn( + tab.linkedBrowser, + [key_handle, extensions], + ([key_handle, extensions]) => { + let challenge = content.crypto.getRandomValues(new Uint8Array(16)); + if (key_handle == null) { + key_handle = content.crypto.getRandomValues(new Uint8Array(16)); + } + + let credential = { + id: key_handle, + type: "public-key", + transports: ["usb"], + }; + + let publicKey = { + challenge, + extensions, + rpId: content.document.domain, + allowCredentials: [credential], + }; + + return content.navigator.credentials + .get({ publicKey }) + .then(assertion => { + return { + authenticatorData: assertion.response.authenticatorData, + clientDataJSON: assertion.response.clientDataJSON, + extensions: assertion.getClientExtensionResults(), + signature: assertion.response.signature, + }; + }); + } + ); +} + +function checkRpIdHash(rpIdHash, hostname) { + return crypto.subtle + .digest("SHA-256", string2buffer(hostname)) + .then(calculatedRpIdHash => { + let calcHashStr = bytesToBase64UrlSafe( + new Uint8Array(calculatedRpIdHash) + ); + let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(rpIdHash)); + + if (calcHashStr != providedHashStr) { + throw new Error("Calculated RP ID hash doesn't match."); + } + }); +} +/* eslint-enable no-shadow */ diff --git a/dom/webauthn/tests/u2f/browser/tab_webauthn_result.html b/dom/webauthn/tests/u2f/browser/tab_webauthn_result.html new file mode 100644 index 0000000000..8e8b9f82cd --- /dev/null +++ b/dom/webauthn/tests/u2f/browser/tab_webauthn_result.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Generic W3C Web Authentication Test Result Page</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Generic W3C Web Authentication Test Result Page</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a> +<input type="text" id="status" value="init" /> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/get_assertion_dead_object.html b/dom/webauthn/tests/u2f/get_assertion_dead_object.html new file mode 100644 index 0000000000..e7de9d3deb --- /dev/null +++ b/dom/webauthn/tests/u2f/get_assertion_dead_object.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset=utf-8> +</head> +<body> +<script type="text/javascript"> + window.addEventListener('load', function() { + let o = []; + o[0] = window.navigator; + document.writeln(''); + // Since the USB token is enabled by default, this will pop up a notification that the + // user can insert/interact with it. Since this is just a test, this won't happen. The + // request will eventually time out. + // Unfortunately the minimum timeout is 15 seconds. + o[0].credentials.get({ publicKey: { challenge: new Uint8Array(128), timeout: 15000 } }); + o.forEach((n, i) => o[i] = null); + }); +</script> +</body> +</html> diff --git a/dom/webauthn/tests/u2f/mochitest.ini b/dom/webauthn/tests/u2f/mochitest.ini new file mode 100644 index 0000000000..add3bde37c --- /dev/null +++ b/dom/webauthn/tests/u2f/mochitest.ini @@ -0,0 +1,76 @@ +[DEFAULT] +support-files = + ../cbor.js + ../u2futil.js + ../pkijs/* + get_assertion_dead_object.html +scheme = https +prefs = + security.webauth.webauthn=true + security.webauth.webauthn_enable_softtoken=true + security.webauth.webauthn_enable_android_fido2=false + security.webauth.webauthn_enable_usbtoken=false + security.webauthn.ctap2=false + +[test_webauthn_abort_signal.html] +fail-if = xorigin +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_attestation_conveyance.html] +fail-if = xorigin # NotAllowedError +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_authenticator_selection.html] +fail-if = xorigin # NotAllowedError +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_authenticator_transports.html] +fail-if = xorigin # NotAllowedError +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_loopback.html] +skip-if = + xorigin # Hangs, JavaScript error: https://example.org/tests/SimpleTest/SimpleTest.js, line 76: DataCloneError: The object could not be cloned. + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_no_token.html] +skip-if = + xorigin # JavaScript error: https://example.org/tests/SimpleTest/SimpleTest.js, line 76: DataCloneError: The object could not be cloned. + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_make_credential.html] +fail-if = xorigin # NotAllowedError +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_get_assertion.html] +fail-if = xorigin # NotAllowedError +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_get_assertion_dead_object.html] +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_override_request.html] +[test_webauthn_store_credential.html] +fail-if = xorigin # NotAllowedError +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_sameorigin.html] +fail-if = xorigin # NotAllowedError +skip-if = + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_sameoriginwithancestors.html] +skip-if = + xorigin # this test has its own cross-origin setup + win10_2004 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) + win11_2009 # Bug 1718296 (Windows 10 1903+ has its own window and U2F that we cannot control with tests.) +[test_webauthn_isplatformauthenticatoravailable.html] +[test_webauthn_isexternalctap2securitykeysupported.html] diff --git a/dom/webauthn/tests/u2f/test_webauthn_abort_signal.html b/dom/webauthn/tests/u2f/test_webauthn_abort_signal.html new file mode 100644 index 0000000000..95e347ae61 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_abort_signal.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for aborting W3C Web Authentication request</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test for aborting W3C Web Authentication request</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectAbortError(aResult) { + is(aResult.code, DOMException.ABORT_ERR, "Expecting an AbortError"); + } + + add_task(() => { + // Enable USB tokens. + return SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ]}); + }); + + // Start a new MakeCredential() request. + function requestMakeCredential(signal) { + let publicKey = { + rp: {id: document.domain, name: "none", icon: "none"}, + user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + }; + + return navigator.credentials.create({publicKey, signal}); + } + + // Start a new GetAssertion() request. + async function requestGetAssertion(signal) { + let newCredential = { + type: "public-key", + id: crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }; + + let publicKey = { + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + + // Start the request, handle failures only. + return navigator.credentials.get({publicKey, signal}); + } + + // Create an AbortController and abort immediately. + add_task(async function test_create_abortcontroller_and_abort() { + let ctrl = new AbortController(); + ctrl.abort(); + + // The event shouldn't fire. + ctrl.signal.onabort = arrivingHereIsBad; + + // MakeCredential() should abort immediately. + await requestMakeCredential(ctrl.signal) + .then(arrivingHereIsBad) + .catch(expectAbortError); + + // GetAssertion() should abort immediately. + await requestGetAssertion(ctrl.signal) + .then(arrivingHereIsBad) + .catch(expectAbortError); + }); + + // Request a new credential and abort the request. + add_task(async function test_request_credential_and_abort() { + let aborted = false; + let ctrl = new AbortController(); + + ctrl.signal.onabort = () => { + ok(!aborted, "abort event fired once"); + aborted = true; + }; + + // Request a new credential. + let request = requestMakeCredential(ctrl.signal) + .then(arrivingHereIsBad) + .catch(err => { + ok(aborted, "abort event was fired"); + expectAbortError(err); + }); + + // Wait a tick for the statemachine to start. + await Promise.resolve(); + + // Abort the request. + ok(!aborted, "request still pending"); + ctrl.abort(); + ok(aborted, "request aborted"); + + // Wait for the request to terminate. + await request; + }); + + // Request a new assertion and abort the request. + add_task(async function test_request_assertion_and_abort() { + let aborted = false; + let ctrl = new AbortController(); + + ctrl.signal.onabort = () => { + ok(!aborted, "abort event fired once"); + aborted = true; + }; + + // Request a new assertion. + let request = requestGetAssertion(ctrl.signal) + .then(arrivingHereIsBad) + .catch(err => { + ok(aborted, "abort event was fired"); + expectAbortError(err); + }); + + // Wait a tick for the statemachine to start. + await Promise.resolve(); + + // Abort the request. + ok(!aborted, "request still pending"); + ctrl.abort(); + ok(aborted, "request aborted"); + + // Wait for the request to terminate. + await request; + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_attestation_conveyance.html b/dom/webauthn/tests/u2f/test_webauthn_attestation_conveyance.html new file mode 100644 index 0000000000..d173e6db90 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_attestation_conveyance.html @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>W3C Web Authentication - Attestation Conveyance</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <script type="text/javascript" src="../cbor.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>W3C Web Authentication - Attestation Conveyance</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1428916">Mozilla Bug 1428916</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1416056">Mozilla Bug 1416056</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + add_task(() => { + return SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_testing_allow_direct_attestation", true], + ]}); + }); + + function getAttestationCertFromAttestationBuffer(aAttestationBuffer) { + return webAuthnDecodeCBORAttestation(aAttestationBuffer) + .then((aAttestationObj) => { + is(aAttestationObj.fmt, "fido-u2f", "Is a FIDO U2F Attestation"); + let attestationCertDER = aAttestationObj.attStmt.x5c[0]; + let certDERBuffer = attestationCertDER.slice(0, attestationCertDER.byteLength).buffer; + let certAsn1 = org.pkijs.fromBER(certDERBuffer); + return new org.pkijs.simpl.CERT({ schema: certAsn1.result }); + }); + } + + function verifyAnonymizedCertificate(aResult) { + return webAuthnDecodeCBORAttestation(aResult.response.attestationObject) + .then(({fmt, attStmt}) => { + is(fmt, "none", "Is a None Attestation"); + is(typeof(attStmt), "object", "attStmt is a map"); + is(Object.keys(attStmt).length, 0, "attStmt is empty"); + }); + } + + function verifyDirectCertificate(aResult) { + return getAttestationCertFromAttestationBuffer(aResult.response.attestationObject) + .then((attestationCert) => { + let subject = attestationCert.subject.types_and_values[0].value.value_block.value; + is(subject, "Firefox U2F Soft Token", "Subject name matches the direct Soft Token") + }); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectTypeError(aResult) { + ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError, got " + aResult); + } + + // Start a new MakeCredential() request. + function requestMakeCredential(attestation) { + let publicKey = { + rp: {id: document.domain, name: "none", icon: "none"}, + user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + attestation, + }; + + return navigator.credentials.create({publicKey}); + } + + // Test success cases for make credential. + add_task(async function test_make_credential_success () { + // No selection criteria should be equal to none, which means anonymized + await requestMakeCredential() + .then(verifyAnonymizedCertificate) + .catch(arrivingHereIsBad); + + // Request no attestation. + await requestMakeCredential("none") + .then(verifyAnonymizedCertificate) + .catch(arrivingHereIsBad); + + // Request indirect attestation, which is the same as direct. + await requestMakeCredential("indirect") + .then((x) => { + if (AppConstants.platform === "android") { + // If this is Android, the result will be anonymized (Bug 1551229) + return verifyAnonymizedCertificate(x); + } else { + return verifyDirectCertificate(x); + } + }) + .catch(arrivingHereIsBad); + + // Request direct attestation, which will prompt for user intervention. + await requestMakeCredential("direct") + .then((x) => { + if (AppConstants.platform === "android") { + // If this is Android, the result will be anonymized (Bug 1551229) + return verifyAnonymizedCertificate(x); + } else { + return verifyDirectCertificate(x); + } + }) + .catch(arrivingHereIsBad); + }); + + // Test failure cases for make credential. + add_task(async function test_make_credential_failures() { + // Request a platform authenticator. + await requestMakeCredential("unknown") + .then(arrivingHereIsBad) + .catch(expectTypeError); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_authenticator_selection.html b/dom/webauthn/tests/u2f/test_webauthn_authenticator_selection.html new file mode 100644 index 0000000000..cd07df3e01 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_authenticator_selection.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>W3C Web Authentication - Authenticator Selection Criteria</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>W3C Web Authentication - Authenticator Selection Criteria</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406462">Mozilla Bug 1406462</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406467">Mozilla Bug 1406467</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult); + } + + // We store the credential of the first successful make credential + // operation so we can use it for get assertion tests later. + let gCredential; + + // Start a new MakeCredential() request. + function requestMakeCredential(authenticatorSelection) { + let publicKey = { + rp: {id: document.domain, name: "none", icon: "none"}, + user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + authenticatorSelection, + }; + + return navigator.credentials.create({publicKey}); + } + + // Start a new GetAssertion() request. + function requestGetAssertion(userVerification) { + let newCredential = { + type: "public-key", + id: gCredential, + transports: ["usb"], + }; + + let publicKey = { + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + + if (userVerification) { + publicKey.userVerification = userVerification; + } + + return navigator.credentials.get({publicKey}); + } + + // Test success cases for make credential. + add_task(async function test_make_credential_successes() { + // No selection criteria. + await requestMakeCredential({}) + // Save the credential so we can use it for sign success tests. + .then(res => gCredential = res.rawId) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Request a cross-platform authenticator. + await requestMakeCredential({authenticatorAttachment: "cross-platform"}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Don't require a resident key. + await requestMakeCredential({requireResidentKey: false}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Prefer user verification. + await requestMakeCredential({userVerification: "preferred"}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Discourage user verification. + await requestMakeCredential({userVerification: "discouraged"}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + }); + + // Test success cases for get assertion. + add_task(async function test_get_assertion_successes() { + // No selection criteria. + await requestGetAssertion() + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Prefer user verification. + await requestGetAssertion("preferred") + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Discourage user verification. + await requestGetAssertion("discouraged") + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + }); + + // Test failure cases for make credential. + add_task(async function test_make_credential_failures() { + // Request a platform authenticator. + await requestMakeCredential({authenticatorAttachment: "platform"}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // Require a resident key. + await requestMakeCredential({requireResidentKey: true}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + + // Require user verification. + await requestMakeCredential({userVerification: "required"}) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + }); + + // Test failure cases for get assertion. + add_task(async function test_get_assertion_failures() { + // Require user verification. + await requestGetAssertion("required") + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_authenticator_transports.html b/dom/webauthn/tests/u2f/test_webauthn_authenticator_transports.html new file mode 100644 index 0000000000..ffd74ebab3 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_authenticator_transports.html @@ -0,0 +1,150 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>W3C Web Authentication - Authenticator Transports</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>W3C Web Authentication - Authenticator Transports</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1406467">Mozilla Bug 1406467</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult); + } + + function expectInvalidStateError(aResult) { + ok(aResult.toString().startsWith("InvalidStateError"), "Expecting a InvalidStateError, got " + aResult); + } + + // Store the credential of the first successful make credential + // operation so we can use it to get assertions later. + let gCredential; + + // Start a new MakeCredential() request. + function requestMakeCredential(excludeCredentials) { + let publicKey = { + rp: {id: document.domain, name: "none", icon: "none"}, + user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + excludeCredentials + }; + + return navigator.credentials.create({publicKey}); + } + + // Start a new GetAssertion() request. + function requestGetAssertion(allowCredentials) { + let publicKey = { + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials + }; + + return navigator.credentials.get({publicKey}); + } + + // Test make credential behavior. + add_task(async function test_make_credential() { + // Make a credential. + await requestMakeCredential([]) + // Save the credential for later. + .then(res => gCredential = res.rawId) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Pass a random credential to exclude. + await requestMakeCredential([{ + type: "public-key", + id: crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }]).then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Pass gCredential with transport=usb. + // The credential already exists, and the softoken consents to create, + // so the error is InvalidState and not NotAllowed. + await requestMakeCredential([{ + type: "public-key", + id: gCredential, + transports: ["usb"], + }]).then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + // Pass gCredential with transport=nfc. + // The softoken pretends to support all transports. + // Also, as above, the credential exists and the token indicates consent. + await requestMakeCredential([{ + type: "public-key", + id: gCredential, + transports: ["nfc"], + }]).then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + // Pass gCredential with an empty transports list. + // As above, the token indicates consent, so expect InvalidStateError. + await requestMakeCredential([{ + type: "public-key", + id: gCredential, + transports: [], + }]).then(arrivingHereIsBad) + .catch(expectInvalidStateError); + }); + + // Test get assertion behavior. + add_task(async function test_get_assertion() { + // Request an assertion for gCredential. + await requestGetAssertion([{ + type: "public-key", + id: gCredential, + transports: ["usb"], + }]).then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Request an assertion for a random credential. The token indicates + // consent even though this credential doesn't exist, so expect an + // InvalidStateError. + await requestGetAssertion([{ + type: "public-key", + id: crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }]).then(arrivingHereIsBad) + .catch(expectInvalidStateError); + + // Request an assertion for gCredential with transport=nfc. + // The softoken pretends to support all transports. + await requestGetAssertion([{ + type: "public-key", + id: gCredential, + transports: ["nfc"], + }]).then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + + // Request an assertion for gCredential with an empty transports list. + await requestGetAssertion([{ + type: "public-key", + id: gCredential, + transports: [], + }]).then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_get_assertion.html b/dom/webauthn/tests/u2f/test_webauthn_get_assertion.html new file mode 100644 index 0000000000..b595b402ae --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_get_assertion.html @@ -0,0 +1,253 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Tests for GetAssertion for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Tests for GetAssertion for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let gAssertionChallenge = new Uint8Array(16); + window.crypto.getRandomValues(gAssertionChallenge); + + let invalidCred = {type: "Magic", id: base64ToBytes("AAA=")}; + let unknownCred = {type: "public-key", id: base64ToBytes("AAA=")}; + let validCred = null; + + add_task(test_setup_valid_credential); + add_task(test_without_credential); + add_task(test_with_credential); + add_task(test_unexpected_option); + add_task(test_unexpected_option_with_credential); + add_task(test_unexpected_transport); + add_task(test_invalid_credential); + add_task(test_unknown_credential); + add_task(test_too_many_credentials); + add_task(test_unexpected_option_invalid_credential); + add_task(test_empty_credential_list); + add_task(() => { + // Enable USB tokens. + return SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ]}); + }); + add_task(test_usb_empty_credential_list); + + function requestGetAssertion(params) { + return navigator.credentials.get(params); + } + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError, got " + aResult); + } + + function expectInvalidStateError(aResult) { + ok(aResult.toString().startsWith("InvalidStateError"), "Expecting a InvalidStateError, got " + aResult); + } + + function expectTypeError(aResult) { + ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError, got " + aResult); + } + + function expectSecurityError(aResult) { + ok(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError, got " + aResult); + } + + function expectAbortError(aResult) { + is(aResult.code, DOMException.ABORT_ERR, "Expecting an AbortError"); + } + + // Set up a valid credential + async function test_setup_valid_credential() { + let publicKey = { + rp: {id: document.domain, name: "none", icon: "none"}, + user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + }; + + return navigator.credentials.create({publicKey}) + .then(res => validCred = {type: "public-key", id: res.rawId} ); + } + + // Test basic good call, but without giving a credential so expect failures + // this is OK by the standard, but not supported by U2F-backed authenticators + // like the soft token in use here. + async function test_without_credential() { + let publicKey = { + challenge: gAssertionChallenge + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectInvalidStateError); + } + + // Test with a valid credential + async function test_with_credential() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [validCred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with an unexpected option. That won't stop anything, and we'll + // fail with InvalidState just as if we had no valid credentials -- which + // we don't. + async function test_unexpected_option() { + let publicKey = { + challenge: gAssertionChallenge, + unknownValue: "hi" + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectInvalidStateError); + } + + // Test with an unexpected option but a valid credential + async function test_unexpected_option_with_credential() { + let publicKey = { + challenge: gAssertionChallenge, + unknownValue: "hi", + allowCredentials: [validCred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with an unexpected transport on a valid credential + async function test_unexpected_transport() { + let cred = validCred; + cred.transports = ["unknown", "usb"]; + + let publicKey = { + challenge: gAssertionChallenge, + unknownValue: "hi", + allowCredentials: [cred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with an invalid credential + async function test_invalid_credential() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [invalidCred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with an unknown credential + async function test_unknown_credential() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [unknownCred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectInvalidStateError); + } + + // Test with too many credentials + async function test_too_many_credentials() { + let tooManyCredentials = Array(21).fill(validCred); + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: tooManyCredentials, + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + + // Test with an unexpected option and an invalid credential + async function test_unexpected_option_invalid_credential() { + let publicKey = { + challenge: gAssertionChallenge, + unknownValue: "hi", + allowCredentials: [invalidCred] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with an empty credential list + // This will return InvalidStateError since the softotken consents, but + // there are no valid credentials. + async function test_empty_credential_list() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [] + }; + + await requestGetAssertion({publicKey}) + .then(arrivingHereIsBad) + .catch(expectInvalidStateError); + } + + // Test with an empty credential list + async function test_usb_empty_credential_list() { + let publicKey = { + challenge: gAssertionChallenge, + allowCredentials: [] + }; + + let ctrl = new AbortController(); + let request = requestGetAssertion({publicKey, signal: ctrl.signal}) + .then(arrivingHereIsBad) + .catch(expectAbortError); + + // Wait a tick for the statemachine to start. + await Promise.resolve(); + + // The request should time out. We'll abort it here and will fail or + // succeed upon resolution, when the error code is checked. + ctrl.abort(); + await request; + } + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_get_assertion_dead_object.html b/dom/webauthn/tests/u2f/test_webauthn_get_assertion_dead_object.html new file mode 100644 index 0000000000..18a4f512f0 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_get_assertion_dead_object.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for GetAssertion on dead object</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test for GetAssertion on dead object</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1483905">Mozilla Bug 1483905</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout( + "Due to the nature of this test, there's no way for the window we're opening to signal " + + "that it's done (the `document.writeln('')` is essential and basically clears any state " + + "we could use). So, we have to wait at least 15 seconds for the webauthn call to time out."); + let win = window.open("https://example.com/tests/dom/webauthn/tests/get_assertion_dead_object.html"); + setTimeout(() => { + win.close(); + SimpleTest.finish(); + }, 20000); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_isexternalctap2securitykeysupported.html b/dom/webauthn/tests/u2f/test_webauthn_isexternalctap2securitykeysupported.html new file mode 100644 index 0000000000..e25225a123 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_isexternalctap2securitykeysupported.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for W3C Web Authentication isExternalCTAP2SecurityKeySupported</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Test for W3C Web Authentication isExternalCTAP2SecurityKeySupported</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1526023">Mozilla Bug 1526023</a> + +<script class="testbody" type="text/javascript"> +"use strict"; + +// Execute the full-scope test +SimpleTest.waitForExplicitFinish(); + +add_task(async function test_external_key_support() { + PublicKeyCredential.isExternalCTAP2SecurityKeySupported() + .then(aResult => ok(true, `Should always return either true or false: ${aResult}`)) + .catch(aProblem => ok(false, `We shouldn't get here: ${aProblem}`)) +}); + +</script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_isplatformauthenticatoravailable.html b/dom/webauthn/tests/u2f/test_webauthn_isplatformauthenticatoravailable.html new file mode 100644 index 0000000000..dba00df656 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_isplatformauthenticatoravailable.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for W3C Web Authentication isUserVerifyingPlatformAuthenticatorAvailable</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Test for W3C Web Authentication isUserVerifyingPlatformAuthenticatorAvailable</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + +<script class="testbody" type="text/javascript"> +"use strict"; + +// Execute the full-scope test +SimpleTest.waitForExplicitFinish(); + +add_task(async function test_is_platform_available() { + // This test ensures that isUserVerifyingPlatformAuthenticatorAvailable() + // is a callable method, but with the softtoken enabled, it's not useful to + // figure out what it actually returns, so we'll just make sure it runs. + await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() + .then(function(aResult) { + ok(true, "Resolved: " + aResult); + }) + .catch(function(aProblem) { + ok(false, "Problem encountered: " + aProblem); + }); +}); + +</script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_loopback.html b/dom/webauthn/tests/u2f/test_webauthn_loopback.html new file mode 100644 index 0000000000..a5c0ca097d --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_loopback.html @@ -0,0 +1,213 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <script type="text/javascript" src="../cbor.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Full-run test for MakeCredential/GetAssertion for W3C Web Authentication</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + +<script class="testbody" type="text/javascript"> +"use strict"; + +// Execute the full-scope test +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({"set": [["security.webauth.webauthn_testing_allow_direct_attestation", true]]}, +function() { +is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); +isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); +isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); +isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let credm = navigator.credentials; + + let gCredentialChallenge = new Uint8Array(16); + window.crypto.getRandomValues(gCredentialChallenge); + let gAssertionChallenge = new Uint8Array(16); + window.crypto.getRandomValues(gAssertionChallenge); + + testMakeCredential(); + + function decodeCreatedCredential(aCredInfo) { + /* PublicKeyCredential : Credential + - rawId: Key Handle buffer pulled from U2F Register() Response + - id: Key Handle buffer in base64url form, should == rawId + - type: Literal 'public-key' + - response : AuthenticatorAttestationResponse : AuthenticatorResponse + - attestationObject: CBOR object + - clientDataJSON: serialized JSON + */ + + is(aCredInfo.type, "public-key", "Credential type must be public-key") + + ok(aCredInfo.rawId.byteLength > 0, "Key ID exists"); + is(aCredInfo.id, bytesToBase64UrlSafe(aCredInfo.rawId), "Encoded Key ID and Raw Key ID match"); + + ok(aCredInfo.rawId === aCredInfo.rawId, "PublicKeyCredential.RawID is SameObject"); + ok(aCredInfo.response === aCredInfo.response, "PublicKeyCredential.Response is SameObject"); + ok(aCredInfo.response.clientDataJSON === aCredInfo.response.clientDataJSON, "PublicKeyCredential.Response.ClientDataJSON is SameObject"); + ok(aCredInfo.response.attestationObject === aCredInfo.response.attestationObject, "PublicKeyCredential.Response.AttestationObject is SameObject"); + + let clientData = JSON.parse(buffer2string(aCredInfo.response.clientDataJSON)); + is(clientData.challenge, bytesToBase64UrlSafe(gCredentialChallenge), "Challenge is correct"); + is(clientData.origin, window.location.origin, "Origin is correct"); + is(clientData.type, "webauthn.create", "Type is correct"); + + return webAuthnDecodeCBORAttestation(aCredInfo.response.attestationObject) + .then(function(aAttestationObj) { + // Make sure the RP ID hash matches what we calculate. + return crypto.subtle.digest("SHA-256", string2buffer(document.domain)) + .then(function(calculatedRpIdHash) { + let calcHashStr = bytesToBase64UrlSafe(new Uint8Array(calculatedRpIdHash)); + let providedHashStr = bytesToBase64UrlSafe(new Uint8Array(aAttestationObj.authDataObj.rpIdHash)); + + is(calcHashStr, providedHashStr, + "Calculated RP ID hash must match what the browser derived."); + return Promise.resolve(aAttestationObj); + }); + }) + .then(function(aAttestationObj) { + ok(aAttestationObj.authDataObj.flags == (flag_TUP | flag_AT), + "User presence and Attestation Object must be the only flags set"); + + aCredInfo.clientDataObj = clientData; + aCredInfo.publicKeyHandle = aAttestationObj.authDataObj.publicKeyHandle; + aCredInfo.attestationObject = aAttestationObj.authDataObj.attestationAuthData; + return aCredInfo; + }); +} + + function checkAssertionAndSigValid(aPublicKey, aAssertion) { + /* PublicKeyCredential : Credential + - rawId: ID of Credential from AllowList that succeeded + - id: Key Handle buffer in base64url form, should == rawId + - type: Literal 'public-key' + - response : AuthenticatorAssertionResponse : AuthenticatorResponse + - clientDataJSON: serialized JSON + - authenticatorData: RP ID Hash || U2F Sign() Response + - signature: U2F Sign() Response + */ + + is(aAssertion.type, "public-key", "Credential type must be public-key") + + ok(aAssertion.rawId.byteLength > 0, "Key ID exists"); + is(aAssertion.id, bytesToBase64UrlSafe(new Uint8Array(aAssertion.rawId)), "Encoded Key ID and Raw Key ID match"); + + ok(aAssertion.response.authenticatorData === aAssertion.response.authenticatorData, "AuthenticatorAssertionResponse.AuthenticatorData is SameObject"); + ok(aAssertion.response.authenticatorData instanceof ArrayBuffer, "AuthenticatorAssertionResponse.AuthenticatorData is an ArrayBuffer"); + ok(aAssertion.response.signature === aAssertion.response.signature, "AuthenticatorAssertionResponse.Signature is SameObject"); + ok(aAssertion.response.signature instanceof ArrayBuffer, "AuthenticatorAssertionResponse.Signature is an ArrayBuffer"); + ok(aAssertion.response.userHandle === null, "AuthenticatorAssertionResponse.UserHandle is null for u2f authenticators"); + + ok(aAssertion.response.authenticatorData.byteLength > 0, "Authenticator data exists"); + let clientData = JSON.parse(buffer2string(aAssertion.response.clientDataJSON)); + is(clientData.challenge, bytesToBase64UrlSafe(gAssertionChallenge), "Challenge is correct"); + is(clientData.origin, window.location.origin, "Origin is correct"); + is(clientData.type, "webauthn.get", "Type is correct"); + + return webAuthnDecodeAuthDataArray(aAssertion.response.authenticatorData) + .then(function(aAttestation) { + ok(new Uint8Array(aAttestation.flags) == flag_TUP, "User presence must be the only flag set"); + is(aAttestation.counter.byteLength, 4, "Counter must be 4 bytes"); + return deriveAppAndChallengeParam(window.location.host, aAssertion.response.clientDataJSON, aAttestation) + }) + .then(function(aParams) { + console.log(aParams); + console.log("ClientData buffer: ", hexEncode(aAssertion.response.clientDataJSON)); + console.log("ClientDataHash: ", hexEncode(aParams.challengeParam)); + return assembleSignedData(aParams.appParam, aParams.attestation.flags, + aParams.attestation.counter, aParams.challengeParam); + }) + .then(function(aSignedData) { + console.log(aPublicKey, aSignedData, aAssertion.response.signature); + return verifySignature(aPublicKey, aSignedData, aAssertion.response.signature); + }) +} + + function testMakeCredential() { + let rp = {id: document.domain, name: "none", icon: "none"}; + let user = {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}; + let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + let makeCredentialOptions = { + rp, + user, + challenge: gCredentialChallenge, + pubKeyCredParams: [param], + attestation: "direct" + }; + credm.create({publicKey: makeCredentialOptions}) + .then(decodeCreatedCredential) + .then(testMakeDuplicate) + .catch(function(aReason) { + ok(false, aReason); + SimpleTest.finish(); + }); +} + + function testMakeDuplicate(aCredInfo) { + let rp = {id: document.domain, name: "none", icon: "none"}; + let user = {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}; + let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + let makeCredentialOptions = { + rp, + user, + challenge: gCredentialChallenge, + pubKeyCredParams: [param], + excludeCredentials: [{type: "public-key", id: new Uint8Array(aCredInfo.rawId), + transports: ["usb"]}] + }; + credm.create({publicKey: makeCredentialOptions}) + .then(function() { + // We should have errored here! + ok(false, "The excludeList didn't stop a duplicate being created!"); + SimpleTest.finish(); + }) + .catch(function(aReason) { + ok(aReason.toString().startsWith("InvalidStateError"), "Expect InvalidStateError, got " + aReason); + testAssertion(aCredInfo); + }); +} + + function testAssertion(aCredInfo) { + let newCredential = { + type: "public-key", + id: new Uint8Array(aCredInfo.rawId), + transports: ["usb"], + } + + let publicKeyCredentialRequestOptions = { + challenge: gAssertionChallenge, + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(function(aAssertion) { + /* Pass along the pubKey. */ + return checkAssertionAndSigValid(aCredInfo.publicKeyHandle, aAssertion); + }) + .then(function(aSigVerifyResult) { + ok(aSigVerifyResult, "Signing signature verified"); + SimpleTest.finish(); + }) + .catch(function(reason) { + ok(false, "Signing signature invalid: " + reason); + SimpleTest.finish(); + }); +} +}); + +</script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_make_credential.html b/dom/webauthn/tests/u2f/test_webauthn_make_credential.html new file mode 100644 index 0000000000..9c5e5ec457 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_make_credential.html @@ -0,0 +1,387 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for MakeCredential for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test for MakeCredential for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let credm; + let gCredentialChallenge; + let rp; + let user; + let param; + let unsupportedParam; + let badParam; + + // Setup test env + add_task(() => { + gCredentialChallenge = new Uint8Array(16); + window.crypto.getRandomValues(gCredentialChallenge); + + rp = {id: document.domain, name: "none", icon: "none"}; + user = {id: new Uint8Array(64), name: "none", icon: "none", displayName: "none"}; + param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + unsupportedParam = {type: "public-key", alg: cose_alg_ECDSA_w_SHA512}; + badParam = {type: "SimplePassword", alg: "MaxLength=2"}; + credm = navigator.credentials; + }); + // Add tests + add_task(test_good_call); + add_task(test_empty_account); + add_task(test_without_rp_name); + add_task(test_without_user_id); + add_task(test_without_user_name); + add_task(test_without_user_displayname); + add_task(test_user_too_large); + add_task(test_empty_parameters); + add_task(test_without_parameters); + add_task(test_unsupported_parameter); + add_task(test_unsupported_but_one_param); + add_task(test_one_bad_parameter); + add_task(test_one_bad_one_unsupported_param); + add_task(test_one_of_each_parameters); + add_task(test_without_challenge); + add_task(test_invalid_challenge); + add_task(test_duplicate_pub_key); + add_task(test_invalid_rp_id); + add_task(test_invalid_rp_id_2); + add_task(test_missing_rp); + add_task(test_incorrect_user_id_type); + add_task(test_missing_user); + add_task(test_complete_account); + add_task(test_too_large_user_id); + add_task(test_excluding_unknown_transports); + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + return Promise.resolve(); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + return Promise.resolve(); + } + + function expectNotAllowedError(aResult) { + ok(aResult.toString().startsWith("NotAllowedError"), "Expecting a NotAllowedError"); + return Promise.resolve(); + } + + function expectTypeError(aResult) { + ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError"); + return Promise.resolve(); + } + + function expectNotSupportedError(aResult) { + ok(aResult.toString().startsWith("NotSupportedError"), "Expecting a NotSupportedError"); + return Promise.resolve(); + } + + // Test basic good call + async function test_good_call() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test empty account + async function test_empty_account() { + let makeCredentialOptions = { + challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without rp.name + async function test_without_rp_name() { + let rp1 = {id: document.domain, icon: "none"}; + let makeCredentialOptions = { + rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without user.id + async function test_without_user_id() { + let user1 = {name: "none", icon: "none", displayName: "none"}; + let makeCredentialOptions = { + rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without user.name + async function test_without_user_name() { + let user1 = {id: new Uint8Array(64), icon: "none", displayName: "none"}; + let makeCredentialOptions = { + rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without user.displayName + async function test_without_user_displayname() { + let user1 = {id: new Uint8Array(64), name: "none", icon: "none"}; + let makeCredentialOptions = { + rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with a user handle that exceeds the max length + async function test_user_too_large() { + let user1 = {id: new Uint8Array(65), name: "none", icon: "none", displayName: "none"}; + let makeCredentialOptions = { + rp, user: user1, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without any parameters; this is acceptable meaning the RP ID is + // happy to accept either ECDSA-SHA256 or RSA-SHA256 + async function test_empty_parameters() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test without a parameter array at all + async function test_without_parameters() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with an unsupported parameter + async function test_unsupported_parameter() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [unsupportedParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + } + + // Test with an unsupported parameter and a good one + async function test_unsupported_but_one_param() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, + pubKeyCredParams: [param, unsupportedParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with a bad parameter + async function test_one_bad_parameter() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, pubKeyCredParams: [badParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with an unsupported parameter, and a bad one + async function test_one_bad_one_unsupported_param() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, + pubKeyCredParams: [unsupportedParam, badParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with an unsupported parameter, a bad one, and a good one. This + // should still fail, as anything with a badParam should fail. + async function test_one_of_each_parameters() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, + pubKeyCredParams: [param, unsupportedParam, badParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test without a challenge + async function test_without_challenge() { + let makeCredentialOptions = { + rp, user, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with an invalid challenge + async function test_invalid_challenge() { + let makeCredentialOptions = { + rp, user, challenge: "begone, thou ill-fitting moist glove!", + pubKeyCredParams: [unsupportedParam] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with duplicate pubKeyCredParams + async function test_duplicate_pub_key() { + let makeCredentialOptions = { + rp, user, challenge: gCredentialChallenge, + pubKeyCredParams: [param, param, param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with an RP ID that is not a valid domain string + async function test_invalid_rp_id() { + let rp1 = { id: document.domain + ":somejunk", name: "none", icon: "none" }; + let makeCredentialOptions = { + rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(arrivingHereIsGood); + } + + // Test with another RP ID that is not a valid domain string + async function test_invalid_rp_id_2() { + let rp1 = { id: document.domain + ":8888", name: "none", icon: "none" }; + let makeCredentialOptions = { + rp: rp1, user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(arrivingHereIsGood); + } + + // Test with missing rp + async function test_missing_rp() { + let makeCredentialOptions = { + user, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with incorrect user ID type + async function test_incorrect_user_id_type() { + let invalidType = {id: "a string, which is not a buffer", name: "none", icon: "none", displayName: "none"}; + let makeCredentialOptions = { + user: invalidType, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with missing user + async function test_missing_user() { + let makeCredentialOptions = { + rp, challenge: gCredentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test a complete account + async function test_complete_account() { + let completeRP = {id: document.domain, name: "Foxxy Name", + icon: "https://example.com/fox.svg"}; + let completeUser = {id: string2buffer("foxes_are_the_best@example.com"), + name: "Fox F. Foxington", + icon: "https://example.com/fox.svg", + displayName: "Foxxy V"}; + let makeCredentialOptions = { + rp: completeRP, user: completeUser, challenge: gCredentialChallenge, + pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + + // Test with too-large user ID buffer + async function test_too_large_user_id() { + let hugeUser = {id: new Uint8Array(65), + name: "Fox F. Foxington", + icon: "https://example.com/fox.svg", + displayName: "Foxxy V"}; + let makeCredentialOptions = { + rp, user: hugeUser, challenge: gCredentialChallenge, + pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + + // Test with excluding unknown transports + async function test_excluding_unknown_transports() { + let completeRP = {id: document.domain, name: "Foxxy Name", + icon: "https://example.com/fox.svg"}; + let completeUser = {id: string2buffer("foxes_are_the_best@example.com"), + name: "Fox F. Foxington", + icon: "https://example.com/fox.svg", + displayName: "Foxxy V"}; + let excludedUnknownTransport = {type: "public-key", + id: string2buffer("123"), + transports: ["unknown", "usb"]}; + let makeCredentialOptions = { + rp: completeRP, user: completeUser, challenge: gCredentialChallenge, + pubKeyCredParams: [param], excludeCredentials: [excludedUnknownTransport] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + }; + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_no_token.html b/dom/webauthn/tests/u2f/test_webauthn_no_token.html new file mode 100644 index 0000000000..b79851ff7a --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_no_token.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for W3C Web Authentication with no token</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<h1>Test for W3C Web Authentication with no token</h1> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + +<script class="testbody" type="text/javascript"> +"use strict"; + +is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); +isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); +isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); +isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + +let credm; +let credentialChallenge; +let assertionChallenge; +let credentialId; + +// Setup test env +add_task(() => { + credentialChallenge = new Uint8Array(16); + window.crypto.getRandomValues(credentialChallenge); + assertionChallenge = new Uint8Array(16); + window.crypto.getRandomValues(assertionChallenge); + credentialId = new Uint8Array(128); + window.crypto.getRandomValues(credentialId); + credm = navigator.credentials; + // Turn off all tokens. This should result in "not allowed" failures + return SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", false], + ]}); +}); + +add_task(async function test_no_token_make_credential() { + let rp = {id: document.domain, name: "none", icon: "none"}; + let user = {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}; + let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + let makeCredentialOptions = { + rp, user, challenge: credentialChallenge, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(function(aResult) { + ok(false, "Should have failed."); + }) + .catch(function(aReason) { + ok(aReason.toString().startsWith("NotAllowedError"), aReason); + }); +}); + +add_task(async function test_no_token_get_assertion() { + let newCredential = { + type: "public-key", + id: credentialId, + transports: ["usb"], + } + let publicKeyCredentialRequestOptions = { + challenge: assertionChallenge, + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(function(aResult) { + ok(false, "Should have failed."); + }) + .catch(function(aReason) { + ok(aReason.toString().startsWith("NotAllowedError"), aReason); + }) +}); + +</script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_override_request.html b/dom/webauthn/tests/u2f/test_webauthn_override_request.html new file mode 100644 index 0000000000..bf6b31ac8b --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_override_request.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for overriding W3C Web Authentication request</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test for overriding W3C Web Authentication request</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1415675">Mozilla Bug 1415675</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + // Last request status. + let status = ""; + + add_task(() => { + return SpecialPowers.pushPrefEnv({"set": [ + ["security.webauth.webauthn_enable_softtoken", false], + ["security.webauth.webauthn_enable_usbtoken", true], + ]}); + }); + + // Start a new MakeCredential() request. + async function requestMakeCredential(status_value) { + let publicKey = { + rp: {id: document.domain, name: "none", icon: "none"}, + user: {id: new Uint8Array(), name: "none", icon: "none", displayName: "none"}, + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + pubKeyCredParams: [{type: "public-key", alg: cose_alg_ECDSA_w_SHA256}], + }; + + // Start the request, handle failures only. + navigator.credentials.create({publicKey}).catch(() => { + status = status_value; + }); + + // Wait a tick to let the statemachine start. + await Promise.resolve(); + } + + // Start a new GetAssertion() request. + async function requestGetAssertion(status_value) { + let newCredential = { + type: "public-key", + id: crypto.getRandomValues(new Uint8Array(16)), + transports: ["usb"], + }; + + let publicKey = { + challenge: crypto.getRandomValues(new Uint8Array(16)), + timeout: 5000, // the minimum timeout is actually 15 seconds + rpId: document.domain, + allowCredentials: [newCredential] + }; + + // Start the request, handle failures only. + navigator.credentials.get({publicKey}).catch(() => { + status = status_value; + }); + + // Wait a tick to let the statemachine start. + await Promise.resolve(); + } + + // Test that .create() and .get() requests override any pending requests. + add_task(async function test_override_pending_requests() { + // Request a new credential. + await requestMakeCredential("aborted1"); + + // Request another credential, the new request will abort. + await requestMakeCredential("aborted2"); + is(status, "aborted2", "second request aborted"); + + // Request an assertion, the new request will still abort. + await requestGetAssertion("aborted3"); + is(status, "aborted3", "third request aborted"); + + // Request another assertion, this fourth request will abort. + await requestGetAssertion("aborted4"); + is(status, "aborted4", "fourth request aborted"); + + // Request another credential, the fifth request will still abort. Why + // do we keep trying? Well, the test originally looked like this, and + // let's face it, it's kinda funny. + await requestMakeCredential("aborted5"); + is(status, "aborted5", "fifth request aborted"); + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_sameorigin.html b/dom/webauthn/tests/u2f/test_webauthn_sameorigin.html new file mode 100644 index 0000000000..8442d0f62b --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_sameorigin.html @@ -0,0 +1,316 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for MakeCredential for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test Same Origin Policy for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + // Execute the full-scope test + SimpleTest.waitForExplicitFinish(); + + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let credm; + let chall; + let user; + let param; + let gTrackedCredential; + add_task(() => { + credm = navigator.credentials; + + chall = new Uint8Array(16); + window.crypto.getRandomValues(chall); + + user = {id: new Uint8Array(16), name: "none", icon: "none", displayName: "none"}; + param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + gTrackedCredential = {}; + }); + + add_task(test_basic_good); + add_task(test_rp_id_unset); + add_task(test_rp_name_unset); + add_task(test_origin_with_optional_fields); + add_task(test_blank_rp_id); + add_task(test_subdomain); + add_task(test_same_origin); + add_task(test_etld); + add_task(test_different_domain_same_tld); + add_task(test_assertion_basic_good); + add_task(test_assertion_rp_id_unset); + add_task(test_assertion_origin_with_optional_fields); + add_task(test_assertion_blank_rp_id); + add_task(test_assertion_subdomain); + add_task(test_assertion_same_origin); + add_task(test_assertion_etld); + add_task(test_assertion_different_domain_same_tld); + add_task(test_basic_good_with_origin); + add_task(test_assertion_basic_good_with_origin); + add_task(test_assertion_invalid_rp_id); + add_task(test_assertion_another_invalid_rp_id); + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectSecurityError(aResult) { + ok(aResult.toString().startsWith("SecurityError"), "Expecting a SecurityError"); + } + + function expectTypeError(aResult) { + ok(aResult.toString().startsWith("TypeError"), "Expecting a TypeError"); + } + + function keepThisPublicKeyCredential(aIdentifier) { + return function(aPublicKeyCredential) { + gTrackedCredential[aIdentifier] = { + type: "public-key", + id: new Uint8Array(aPublicKeyCredential.rawId), + transports: [ "usb" ], + } + return Promise.resolve(aPublicKeyCredential); + } + } + + function test_basic_good() { + // Test basic good call + let rp = {id: document.domain, name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(keepThisPublicKeyCredential("basic")) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_rp_id_unset() { + // Test rp.id being unset + let makeCredentialOptions = { + rp: {name: "none"}, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_rp_name_unset() { + // Test rp.name being unset + let makeCredentialOptions = { + rp: {id: document.domain}, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectTypeError); + } + function test_origin_with_optional_fields() { + // Test this origin with optional fields + let rp = {id: "user:pass@" + document.domain + ":8888", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_blank_rp_id() { + // Test blank rp.id + let rp = {id: "", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_subdomain() { + // Test subdomain of this origin + let rp = {id: "subdomain." + document.domain, name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_same_origin() { + // Test the same origin + let rp = {id: "example.com", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_etld() { + // Test the eTLD + let rp = {id: "com", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_different_domain_same_tld() { + // Test a different domain within the same TLD + let rp = {id: "alt.test", name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_basic_good() { + // Test basic good call + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: document.domain, + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_assertion_rp_id_unset() { + // Test rpId being unset + let publicKeyCredentialRequestOptions = { + challenge: chall, + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_assertion_origin_with_optional_fields() { + // Test this origin with optional fields + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "user:pass@" + document.origin + ":8888", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_blank_rp_id() { + // Test blank rpId + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_subdomain() { + // Test subdomain of this origin + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "subdomain." + document.domain, + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_same_origin() { + // Test the same origin + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "example.com", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsGood) + .catch(arrivingHereIsBad); + } + function test_assertion_etld() { + // Test the eTLD + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "com", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_different_domain_same_tld() { + // Test a different domain within the same TLD + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: "alt.test", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_basic_good_with_origin() { + // Test basic good Create call but using an origin (Bug 1380421) + let rp = {id: window.origin, name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + return credm.create({publicKey: makeCredentialOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_basic_good_with_origin() { + // Test basic good Get call but using an origin (Bug 1380421) + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: window.origin, + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(expectSecurityError); + } + function test_assertion_invalid_rp_id() { + // Test with an rpId that is not a valid domain string + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: document.domain + ":somejunk", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(arrivingHereIsGood); + } + function test_assertion_another_invalid_rp_id() { + // Test with another rpId that is not a valid domain string + let publicKeyCredentialRequestOptions = { + challenge: chall, + rpId: document.domain + ":8888", + allowCredentials: [gTrackedCredential.basic] + }; + return credm.get({publicKey: publicKeyCredentialRequestOptions}) + .then(arrivingHereIsBad) + .catch(arrivingHereIsGood); + } + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_sameoriginwithancestors.html b/dom/webauthn/tests/u2f/test_webauthn_sameoriginwithancestors.html new file mode 100644 index 0000000000..9b94a2cc47 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_sameoriginwithancestors.html @@ -0,0 +1,107 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Test for MakeCredential for W3C Web Authentication (sameOriginWithAncestors = false)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="../u2futil.js"></script> + <script type="text/javascript" src="../pkijs/common.js"></script> + <script type="text/javascript" src="../pkijs/asn1.js"></script> + <script type="text/javascript" src="../pkijs/x509_schema.js"></script> + <script type="text/javascript" src="../pkijs/x509_simpl.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Test Same Origin Policy for W3C Web Authentication (sameOriginWithAncestors = false)</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1694639">Mozilla Bug 1694639</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + // Execute the full-scope test + SimpleTest.waitForExplicitFinish(); + + var gTrackedCredential = {}; + + function arrivingHereIsGood(aResult) { + ok(true, "Good result! Received a: " + aResult); + } + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + } + + function expectNotAllowedError(aResult) { + ok(aResult == "NotAllowedError", "Expecting a NotAllowedError, got " + aResult); + } + + function keepThisPublicKeyCredential(aIdentifier) { + return function(aPublicKeyCredential) { + gTrackedCredential[aIdentifier] = { + type: "public-key", + id: new Uint8Array(aPublicKeyCredential.rawId), + transports: [ "usb" ], + } + return Promise.resolve(aPublicKeyCredential); + } + } + + add_task(async function runTests() { + let iframe = document.createElement("iframe"); + iframe.src = "https://example.org"; + document.body.appendChild(iframe); + await new Promise(resolve => iframe.addEventListener("load", resolve, {once: true})); + + is(navigator.authentication, undefined, "navigator.authentication does not exist any longer"); + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + + let credm = navigator.credentials; + + let chall = new Uint8Array(16); + window.crypto.getRandomValues(chall); + + let user = {id: new Uint8Array(16), name: "none", icon: "none", displayName: "none"}; + let param = {type: "public-key", alg: cose_alg_ECDSA_w_SHA256}; + + let rp = {id: document.domain, name: "none"}; + let makeCredentialOptions = { + rp, user, challenge: chall, pubKeyCredParams: [param] + }; + await credm.create({publicKey: makeCredentialOptions}) + .then(keepThisPublicKeyCredential("basic")) + .catch(arrivingHereIsBad); + + var testFuncs = [ + function (args) { + // Test create when sameOriginWithAncestors = false + let credentialOptions = { + rp: args.rp, user: args.user, challenge: args.challenge, pubKeyCredParams: [args.param] + }; + return this.content.window.navigator.credentials.create({publicKey: credentialOptions}) + .catch(e => Promise.reject(e.name)); + }, + function (args) { + // Test get when sameOriginWithAncestors = false + let publicKeyCredentialRequestOptions = { + challenge: args.challenge, + rpId: args.rp.id, + allowCredentials: [args.trackedCredential.basic] + }; + return this.content.window.navigator.credentials.get({publicKey: publicKeyCredentialRequestOptions}) + .catch(e => Promise.reject(e.name)); + }, + ]; + + let args = { user, param, rp, challenge: chall, trackedCredential: gTrackedCredential } + for(let func of testFuncs) { + await SpecialPowers.spawn(iframe, [args], func) + .then(arrivingHereIsBad) + .catch(expectNotAllowedError); + } + }); + </script> + +</body> +</html> diff --git a/dom/webauthn/tests/u2f/test_webauthn_store_credential.html b/dom/webauthn/tests/u2f/test_webauthn_store_credential.html new file mode 100644 index 0000000000..f19d1d7fa0 --- /dev/null +++ b/dom/webauthn/tests/u2f/test_webauthn_store_credential.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<head> + <title>Tests for Store for W3C Web Authentication</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + + <h1>Tests for Store for W3C Web Authentication</h1> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1309284">Mozilla Bug 1309284</a> + + <script class="testbody" type="text/javascript"> + "use strict"; + + isnot(navigator.credentials, undefined, "Credential Management API endpoint must exist"); + isnot(navigator.credentials.create, undefined, "CredentialManagement create API endpoint must exist"); + isnot(navigator.credentials.get, undefined, "CredentialManagement get API endpoint must exist"); + isnot(navigator.credentials.store, undefined, "CredentialManagement store API endpoint must exist"); + + function arrivingHereIsBad(aResult) { + ok(false, "Bad result! Received a: " + aResult); + return Promise.resolve(); + } + + function expectNotSupportedError(aResult) { + ok(aResult.toString().startsWith("NotSupportedError"), "Expecting a NotSupportedError, received: " + aResult); + return Promise.resolve(); + } + + add_task(async function test_store_credential() { + let credentialChallenge = new Uint8Array(16); + window.crypto.getRandomValues(credentialChallenge); + + let rp = {id: document.domain, name: "none", icon: "none"}; + let user = {id: new Uint8Array(64), name: "none", icon: "none", displayName: "none"}; + let params = [ {type: "public-key", alg: "es256"}, {type: "public-key", alg: -7} ] + + let makeCredentialOptions = { + rp, user, challenge: credentialChallenge, pubKeyCredParams: params + }; + + let credential = await navigator.credentials.create({publicKey: makeCredentialOptions}) + .catch(arrivingHereIsBad); + + await navigator.credentials.store(credential) + .then(arrivingHereIsBad) + .catch(expectNotSupportedError); + }); + </script> + +</body> +</html> |