tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

browser_webauthn_prompts.js (14781B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 XPCOMUtils.defineLazyScriptGetter(
      8  this,
      9  ["FullScreen"],
     10  "chrome://browser/content/browser-fullScreenAndPointerLock.js"
     11 );
     12 
     13 const TEST_URL = "https://example.com/";
     14 var gAuthenticatorId;
     15 
     16 /**
     17 * Waits for the PopupNotifications button enable delay to expire so the
     18 * Notification can be interacted with using the buttons.
     19 */
     20 async function waitForPopupNotificationSecurityDelay() {
     21  let notification = PopupNotifications.panel.firstChild.notification;
     22  let notificationEnableDelayMS = Services.prefs.getIntPref(
     23    "security.notification_enable_delay"
     24  );
     25  await TestUtils.waitForCondition(
     26    () => {
     27      let timeSinceShown = ChromeUtils.now() - notification.timeShown;
     28      return timeSinceShown > notificationEnableDelayMS;
     29    },
     30    "Wait for security delay to expire",
     31    500,
     32    50
     33  );
     34 }
     35 
     36 add_task(async function test_setup_usbtoken() {
     37  return SpecialPowers.pushPrefEnv({
     38    set: [
     39      ["security.webauth.webauthn_enable_softtoken", false],
     40      ["security.webauth.webauthn_enable_usbtoken", true],
     41    ],
     42  });
     43 });
     44 add_task(test_register);
     45 add_task(test_register_escape);
     46 add_task(test_sign);
     47 add_task(test_sign_escape);
     48 add_task(test_tab_switching);
     49 add_task(test_window_switching);
     50 add_task(async function test_setup_softtoken() {
     51  gAuthenticatorId = add_virtual_authenticator();
     52  return SpecialPowers.pushPrefEnv({
     53    set: [
     54      ["browser.fullscreen.autohide", true],
     55      ["full-screen-api.enabled", true],
     56      ["full-screen-api.allow-trusted-requests-only", false],
     57      ["security.webauth.webauthn_enable_softtoken", true],
     58      ["security.webauth.webauthn_enable_usbtoken", false],
     59    ],
     60  });
     61 });
     62 add_task(test_fullscreen_show_nav_toolbar);
     63 add_task(test_no_fullscreen_dom);
     64 add_task(test_register_direct_with_consent);
     65 add_task(test_register_direct_without_consent);
     66 add_task(test_select_sign_result);
     67 
     68 function promiseNavToolboxStatus(aExpectedStatus) {
     69  let navToolboxStatus;
     70  return TestUtils.topicObserved("fullscreen-nav-toolbox", (subject, data) => {
     71    navToolboxStatus = data;
     72    return data == aExpectedStatus;
     73  }).then(() =>
     74    Assert.equal(
     75      navToolboxStatus,
     76      aExpectedStatus,
     77      "nav toolbox is " + aExpectedStatus
     78    )
     79  );
     80 }
     81 
     82 function promiseFullScreenPaint(aExpectedStatus) {
     83  return TestUtils.topicObserved("fullscreen-painted");
     84 }
     85 
     86 function triggerMainPopupCommand(popup) {
     87  info("triggering main command");
     88  let notifications = popup.childNodes;
     89  ok(notifications.length, "at least one notification displayed");
     90  let notification = notifications[0];
     91  info("triggering command: " + notification.getAttribute("buttonlabel"));
     92 
     93  return EventUtils.synthesizeMouseAtCenter(notification.button, {});
     94 }
     95 
     96 let expectNotAllowedError = expectError("NotAllowed");
     97 
     98 function verifyAnonymizedCertificate(aResult) {
     99  return webAuthnDecodeCBORAttestation(aResult.attObj).then(
    100    ({ fmt, attStmt }) => {
    101      is(fmt, "none", "Is a None Attestation");
    102      is(typeof attStmt, "object", "attStmt is a map");
    103      is(Object.keys(attStmt).length, 0, "attStmt is empty");
    104    }
    105  );
    106 }
    107 
    108 async function verifyDirectCertificate(aResult) {
    109  let clientDataHash = await crypto.subtle
    110    .digest("SHA-256", aResult.clientDataJSON)
    111    .then(digest => new Uint8Array(digest));
    112  let { fmt, attStmt, authData, authDataObj } =
    113    await webAuthnDecodeCBORAttestation(aResult.attObj);
    114  is(fmt, "packed", "Is a Packed Attestation");
    115  let signedData = new Uint8Array(authData.length + clientDataHash.length);
    116  signedData.set(authData);
    117  signedData.set(clientDataHash, authData.length);
    118  let valid = await verifySignature(
    119    authDataObj.publicKeyHandle,
    120    signedData,
    121    new Uint8Array(attStmt.sig)
    122  );
    123  ok(valid, "Signature is valid.");
    124 }
    125 
    126 async function test_register() {
    127  // Open a new tab.
    128  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    129 
    130  // Request a new credential and wait for the prompt.
    131  let notificationPromise = promiseNotification("webauthn-prompt-presence");
    132  let active = true;
    133  let request = promiseWebAuthnMakeCredential(tab)
    134    .then(arrivingHereIsBad)
    135    .catch(expectNotAllowedError)
    136    .then(() => (active = false));
    137  await notificationPromise;
    138 
    139  // Cancel the request with the button.
    140  ok(active, "request should still be active");
    141  PopupNotifications.panel.firstElementChild.button.click();
    142  await request;
    143 
    144  // Close tab.
    145  await BrowserTestUtils.removeTab(tab);
    146 }
    147 
    148 async function test_register_escape() {
    149  // Open a new tab.
    150  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    151 
    152  // Request a new credential and wait for the prompt.
    153  let notificationPromise = promiseNotification("webauthn-prompt-presence");
    154  let active = true;
    155  let request = promiseWebAuthnMakeCredential(tab)
    156    .then(arrivingHereIsBad)
    157    .catch(expectNotAllowedError)
    158    .then(() => (active = false));
    159  await notificationPromise;
    160 
    161  // Cancel the request by hitting escape.
    162  ok(active, "request should still be active");
    163  EventUtils.synthesizeKey("KEY_Escape");
    164  await request;
    165 
    166  // Close tab.
    167  await BrowserTestUtils.removeTab(tab);
    168 }
    169 
    170 async function test_sign() {
    171  // Open a new tab.
    172  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    173 
    174  // Request a new assertion and wait for the prompt.
    175  let notificationPromise = promiseNotification("webauthn-prompt-presence");
    176  let active = true;
    177  let request = promiseWebAuthnGetAssertion(tab)
    178    .then(arrivingHereIsBad)
    179    .catch(expectNotAllowedError)
    180    .then(() => (active = false));
    181  await notificationPromise;
    182 
    183  // Cancel the request with the button.
    184  ok(active, "request should still be active");
    185  PopupNotifications.panel.firstElementChild.button.click();
    186  await request;
    187 
    188  // Close tab.
    189  await BrowserTestUtils.removeTab(tab);
    190 }
    191 
    192 async function test_sign_escape() {
    193  // Open a new tab.
    194  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    195 
    196  // Request a new assertion and wait for the prompt.
    197  let notificationPromise = promiseNotification("webauthn-prompt-presence");
    198  let active = true;
    199  let request = promiseWebAuthnGetAssertion(tab)
    200    .then(arrivingHereIsBad)
    201    .catch(expectNotAllowedError)
    202    .then(() => (active = false));
    203  await notificationPromise;
    204 
    205  // Cancel the request by hitting escape.
    206  ok(active, "request should still be active");
    207  EventUtils.synthesizeKey("KEY_Escape");
    208  await request;
    209 
    210  // Close tab.
    211  await BrowserTestUtils.removeTab(tab);
    212 }
    213 
    214 // Add two tabs, open WebAuthn in the first, switch, assert the prompt is
    215 // not visible, switch back, assert the prompt is there and cancel it.
    216 async function test_tab_switching() {
    217  // Open a new tab.
    218  let tab_one = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    219 
    220  // Request a new credential and wait for the prompt.
    221  let notificationPromise = promiseNotification("webauthn-prompt-presence");
    222  let active = true;
    223  let request = promiseWebAuthnMakeCredential(tab_one)
    224    .then(arrivingHereIsBad)
    225    .catch(expectNotAllowedError)
    226    .then(() => (active = false));
    227  await notificationPromise;
    228  is(PopupNotifications.panel.state, "open", "Doorhanger is visible");
    229 
    230  // Open and switch to a second tab.
    231  let tab_two = await BrowserTestUtils.openNewForegroundTab(
    232    gBrowser,
    233    "https://example.org/"
    234  );
    235 
    236  await TestUtils.waitForCondition(
    237    () => PopupNotifications.panel.state == "closed"
    238  );
    239  is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden");
    240 
    241  let notificationPromise2 = promiseNotification("webauthn-prompt-presence");
    242 
    243  // Go back to the first tab
    244  await BrowserTestUtils.removeTab(tab_two);
    245 
    246  await notificationPromise2;
    247 
    248  await TestUtils.waitForCondition(
    249    () => PopupNotifications.panel.state == "open"
    250  );
    251  is(PopupNotifications.panel.state, "open", "Doorhanger is visible");
    252 
    253  // Cancel the request.
    254  ok(active, "request should still be active");
    255  await triggerMainPopupCommand(PopupNotifications.panel);
    256  await request;
    257  ok(!active, "request should be stopped");
    258 
    259  // Close tab.
    260  await BrowserTestUtils.removeTab(tab_one);
    261 }
    262 
    263 // Add two tabs, open WebAuthn in the first, switch, assert the prompt is
    264 // not visible, switch back, assert the prompt is there and cancel it.
    265 async function test_window_switching() {
    266  // Open a new tab.
    267  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    268 
    269  // Request a new credential and wait for the prompt.
    270  let notificationPromise = promiseNotification("webauthn-prompt-presence");
    271  let active = true;
    272  let request = promiseWebAuthnMakeCredential(tab)
    273    .then(arrivingHereIsBad)
    274    .catch(expectNotAllowedError)
    275    .then(() => (active = false));
    276  await notificationPromise;
    277 
    278  await TestUtils.waitForCondition(
    279    () => PopupNotifications.panel.state == "open"
    280  );
    281  is(PopupNotifications.panel.state, "open", "Doorhanger is visible");
    282 
    283  // Open and switch to a second window
    284  let new_window = await BrowserTestUtils.openNewBrowserWindow();
    285  await SimpleTest.promiseFocus(new_window);
    286 
    287  await TestUtils.waitForCondition(
    288    () => new_window.PopupNotifications.panel.state == "closed"
    289  );
    290  is(
    291    new_window.PopupNotifications.panel.state,
    292    "closed",
    293    "Doorhanger is hidden"
    294  );
    295 
    296  // Go back to the first tab
    297  await BrowserTestUtils.closeWindow(new_window);
    298  await SimpleTest.promiseFocus(window);
    299 
    300  await TestUtils.waitForCondition(
    301    () => PopupNotifications.panel.state == "open"
    302  );
    303  is(PopupNotifications.panel.state, "open", "Doorhanger is still visible");
    304 
    305  // Cancel the request.
    306  ok(active, "request should still be active");
    307  await triggerMainPopupCommand(PopupNotifications.panel);
    308  await request;
    309  ok(!active, "request should be stopped");
    310 
    311  // Close tab.
    312  await BrowserTestUtils.removeTab(tab);
    313 }
    314 
    315 async function test_register_direct_with_consent() {
    316  // Open a new tab.
    317  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    318 
    319  // Request a new credential with direct attestation and wait for the prompt.
    320  let notificationPromise = promiseNotification(
    321    "webauthn-prompt-register-direct"
    322  );
    323  let request = promiseWebAuthnMakeCredential(tab, "direct");
    324  await notificationPromise;
    325 
    326  // Click "Allow".
    327  PopupNotifications.panel.firstElementChild.button.click();
    328 
    329  // Ensure we got "direct" attestation.
    330  await request.then(verifyDirectCertificate);
    331 
    332  // Close tab.
    333  await BrowserTestUtils.removeTab(tab);
    334 }
    335 
    336 async function test_register_direct_without_consent() {
    337  // Open a new tab.
    338  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    339 
    340  // Request a new credential with direct attestation and wait for the prompt.
    341  let notificationPromise = promiseNotification(
    342    "webauthn-prompt-register-direct"
    343  );
    344  let request = promiseWebAuthnMakeCredential(tab, "direct");
    345  await notificationPromise;
    346 
    347  // Click "Block".
    348  PopupNotifications.panel.firstElementChild.secondaryButton.click();
    349 
    350  // Ensure we got "none" attestation.
    351  await request.then(verifyAnonymizedCertificate);
    352 
    353  // Close tab.
    354  await BrowserTestUtils.removeTab(tab);
    355 }
    356 
    357 async function test_select_sign_result() {
    358  // Open a new tab.
    359  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    360 
    361  // Make two discoverable credentials for the same RP ID so that
    362  // the user has to select one to return.
    363  let cred1 = await addCredential(gAuthenticatorId, "example.com");
    364  let cred2 = await addCredential(gAuthenticatorId, "example.com");
    365 
    366  let notificationPromise = promiseNotification(
    367    "webauthn-prompt-select-sign-result"
    368  );
    369  let active = true;
    370  let request = promiseWebAuthnGetAssertionDiscoverable(tab)
    371    .then(arrivingHereIsBad)
    372    .catch(expectNotAllowedError)
    373    .then(() => (active = false));
    374 
    375  // Ensure the selection prompt is shown
    376  await notificationPromise;
    377 
    378  ok(active, "request is active");
    379 
    380  // Cancel the request
    381  PopupNotifications.panel.firstElementChild.button.click();
    382  await request;
    383 
    384  await removeCredential(gAuthenticatorId, cred1);
    385  await removeCredential(gAuthenticatorId, cred2);
    386  await BrowserTestUtils.removeTab(tab);
    387 }
    388 
    389 async function test_fullscreen_show_nav_toolbar() {
    390  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    391 
    392  // Start with the window fullscreen and the nav toolbox hidden
    393  let fullscreenState = window.fullScreen;
    394 
    395  let navToolboxHiddenPromise = promiseNavToolboxStatus("hidden");
    396 
    397  window.fullScreen = true;
    398  FullScreen.hideNavToolbox(false);
    399 
    400  await navToolboxHiddenPromise;
    401 
    402  // Request a new credential with direct attestation. The consent prompt will
    403  // keep the request active until we can verify that the nav toolbar is shown.
    404  let promptPromise = promiseNotification("webauthn-prompt-register-direct");
    405  let navToolboxShownPromise = promiseNavToolboxStatus("shown");
    406 
    407  let active = true;
    408  let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then(
    409    () => (active = false)
    410  );
    411 
    412  await Promise.all([promptPromise, navToolboxShownPromise]);
    413 
    414  ok(active, "request is active");
    415  ok(window.fullScreen, "window is fullscreen");
    416 
    417  // Proceed through the consent prompt.
    418  PopupNotifications.panel.firstElementChild.secondaryButton.click();
    419  await requestPromise;
    420 
    421  window.fullScreen = fullscreenState;
    422 
    423  // Close tab.
    424  await BrowserTestUtils.removeTab(tab);
    425 }
    426 
    427 async function test_no_fullscreen_dom() {
    428  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
    429 
    430  let fullScreenPaintPromise = promiseFullScreenPaint();
    431  // Make a DOM element fullscreen
    432  await ContentTask.spawn(tab.linkedBrowser, [], () => {
    433    return content.document.body.requestFullscreen();
    434  });
    435  await fullScreenPaintPromise;
    436  ok(!!document.fullscreenElement, "a DOM element is fullscreen");
    437 
    438  // Request a new credential with direct attestation. The consent prompt will
    439  // keep the request active until we can verify that we've left fullscreen.
    440  let promptPromise = promiseNotification("webauthn-prompt-register-direct");
    441  fullScreenPaintPromise = promiseFullScreenPaint();
    442 
    443  let active = true;
    444  let requestPromise = promiseWebAuthnMakeCredential(tab, "direct").then(
    445    () => (active = false)
    446  );
    447 
    448  await Promise.all([promptPromise, fullScreenPaintPromise]);
    449 
    450  ok(active, "request is active");
    451  ok(!document.fullscreenElement, "no DOM element is fullscreen");
    452 
    453  // Proceed through the consent prompt.
    454  await waitForPopupNotificationSecurityDelay();
    455  PopupNotifications.panel.firstElementChild.secondaryButton.click();
    456  await requestPromise;
    457 
    458  // Close tab.
    459  await BrowserTestUtils.removeTab(tab);
    460 }