tor-browser

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

browser_clientAuth_connection.js (13143B)


      1 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 // Any copyright is dedicated to the Public Domain.
      3 // http://creativecommons.org/publicdomain/zero/1.0/
      4 "use strict";
      5 
      6 // Tests various scenarios connecting to a server that requires client cert
      7 // authentication. Also tests that nsIClientAuthDialogService.chooseCertificate
      8 // is called at the appropriate times and with the correct arguments.
      9 
     10 const { MockRegistrar } = ChromeUtils.importESModule(
     11  "resource://testing-common/MockRegistrar.sys.mjs"
     12 );
     13 
     14 const DialogState = {
     15  // Assert that chooseCertificate() is never called.
     16  ASSERT_NOT_CALLED: "ASSERT_NOT_CALLED",
     17  // Return that the user selected the first given cert.
     18  RETURN_CERT_SELECTED: "RETURN_CERT_SELECTED",
     19  // Return that the user canceled.
     20  RETURN_CERT_NOT_SELECTED: "RETURN_CERT_NOT_SELECTED",
     21 };
     22 
     23 var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
     24 let cars = Cc["@mozilla.org/security/clientAuthRememberService;1"].getService(
     25  Ci.nsIClientAuthRememberService
     26 );
     27 
     28 var gExpectedClientCertificateChoices;
     29 
     30 // Mock implementation of nsIClientAuthDialogService.
     31 const gClientAuthDialogService = {
     32  _state: DialogState.ASSERT_NOT_CALLED,
     33  _rememberClientAuthCertificate: false,
     34  _chooseCertificateCalled: false,
     35 
     36  set state(newState) {
     37    info(`old state: ${this._state}`);
     38    this._state = newState;
     39    info(`new state: ${this._state}`);
     40  },
     41 
     42  get state() {
     43    return this._state;
     44  },
     45 
     46  set rememberClientAuthCertificate(value) {
     47    this._rememberClientAuthCertificate = value;
     48  },
     49 
     50  get rememberClientAuthCertificate() {
     51    return this._rememberClientAuthCertificate;
     52  },
     53 
     54  get chooseCertificateCalled() {
     55    return this._chooseCertificateCalled;
     56  },
     57 
     58  set chooseCertificateCalled(value) {
     59    this._chooseCertificateCalled = value;
     60  },
     61 
     62  chooseCertificate(hostname, certArray, loadContext, caNames, callback) {
     63    this.chooseCertificateCalled = true;
     64    Assert.notEqual(
     65      this.state,
     66      DialogState.ASSERT_NOT_CALLED,
     67      "chooseCertificate() should be called only when expected"
     68    );
     69    Assert.equal(
     70      hostname,
     71      "requireclientcert.example.com",
     72      "Hostname should be 'requireclientcert.example.com'"
     73    );
     74 
     75    // For mochitests, the cert at build/pgo/certs/mochitest.client should be
     76    // selectable as well as one of the PGO certs we loaded in `setup`, so we do
     77    // some brief checks to confirm this.
     78    Assert.notEqual(certArray, null, "Cert list should not be null");
     79    Assert.equal(
     80      certArray.length,
     81      gExpectedClientCertificateChoices,
     82      `${gExpectedClientCertificateChoices} certificates should be available`
     83    );
     84 
     85    for (let cert of certArray) {
     86      Assert.notEqual(cert, null, "Cert list should contain nsIX509Certs");
     87      Assert.equal(
     88        cert.issuerCommonName,
     89        "Temporary Certificate Authority",
     90        "cert should have expected issuer CN"
     91      );
     92    }
     93 
     94    if (this.state == DialogState.RETURN_CERT_SELECTED) {
     95      callback.certificateChosen(
     96        certArray[0],
     97        this.rememberClientAuthCertificate
     98      );
     99    } else {
    100      callback.certificateChosen(null, this.rememberClientAuthCertificate);
    101    }
    102  },
    103 
    104  QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogService"]),
    105 };
    106 
    107 add_setup(async function () {
    108  let clientAuthDialogServiceCID = MockRegistrar.register(
    109    "@mozilla.org/security/ClientAuthDialogService;1",
    110    gClientAuthDialogService
    111  );
    112  registerCleanupFunction(() => {
    113    MockRegistrar.unregister(clientAuthDialogServiceCID);
    114  });
    115 
    116  // This CA has the expected keyCertSign and cRLSign usages. It should not be
    117  // presented for use as a client certificate.
    118  await readCertificate("pgo-ca-regular-usages.pem", "CTu,CTu,CTu");
    119  // This CA has all keyUsages. For compatibility with preexisting behavior, it
    120  // will be presented for use as a client certificate.
    121  await readCertificate("pgo-ca-all-usages.pem", "CTu,CTu,CTu");
    122  // This client certificate was issued by an intermediate that was issued by
    123  // the test CA. The server only lists the test CA's subject distinguished name
    124  // as an acceptible issuer name for client certificates. If the implementation
    125  // can determine that the test CA is a root CA for the client certificate and
    126  // thus is acceptible to use, it should be included in the chooseCertificate
    127  // callback. At the beginning of this test (speaking of this file as a whole),
    128  // the client is not aware of the intermediate, and so it is not available in
    129  // the callback.
    130  await readCertificate("client-cert-via-intermediate.pem", ",,");
    131  // This certificate has an id-kp-OCSPSigning EKU. Client certificates
    132  // shouldn't have this EKU, but there is at least one private PKI where they
    133  // do. For interoperability, such certificates will be presented for use.
    134  await readCertificate("client-cert-with-ocsp-signing.pem", ",,");
    135  gExpectedClientCertificateChoices = 3;
    136 });
    137 
    138 /**
    139 * Test helper for the tests below.
    140 *
    141 * @param {string} prefValue
    142 *        Value to set the "security.default_personal_cert" pref to.
    143 * @param {string} urlToNavigate
    144 *        The URL to navigate to.
    145 * @param {string} expectedURL
    146 *        If the connection is expected to load successfully, the URL that
    147 *        should load. If the connection is expected to fail and result in an
    148 *        error page, |undefined|.
    149 * @param {boolean} expectCallingChooseCertificate
    150 *        Determines whether we expect chooseCertificate to be called.
    151 * @param {object} options
    152 *        Optional options object to pass on to the window that gets opened.
    153 * @param {string} expectStringInPage
    154 *        Optional string that is expected to be in the content of the page
    155 *        once it loads.
    156 */
    157 async function testHelper(
    158  prefValue,
    159  urlToNavigate,
    160  expectedURL,
    161  expectCallingChooseCertificate,
    162  options = undefined,
    163  expectStringInPage = undefined
    164 ) {
    165  gClientAuthDialogService.chooseCertificateCalled = false;
    166  await SpecialPowers.pushPrefEnv({
    167    set: [["security.default_personal_cert", prefValue]],
    168  });
    169 
    170  let win = await BrowserTestUtils.openNewBrowserWindow(options);
    171 
    172  BrowserTestUtils.startLoadingURIString(
    173    win.gBrowser.selectedBrowser,
    174    urlToNavigate
    175  );
    176  if (expectedURL) {
    177    await BrowserTestUtils.browserLoaded(
    178      win.gBrowser.selectedBrowser,
    179      false,
    180      "https://requireclientcert.example.com/",
    181      true
    182    );
    183    let loadedURL = win.gBrowser.selectedBrowser.documentURI.spec;
    184    Assert.ok(
    185      loadedURL.startsWith(expectedURL),
    186      `Expected and actual URLs should match (got '${loadedURL}', expected '${expectedURL}')`
    187    );
    188  } else {
    189    await new Promise(resolve => {
    190      let removeEventListener = BrowserTestUtils.addContentEventListener(
    191        win.gBrowser.selectedBrowser,
    192        "AboutNetErrorLoad",
    193        () => {
    194          removeEventListener();
    195          resolve();
    196        },
    197        { capture: false, wantUntrusted: true }
    198      );
    199    });
    200  }
    201 
    202  Assert.equal(
    203    gClientAuthDialogService.chooseCertificateCalled,
    204    expectCallingChooseCertificate,
    205    "chooseCertificate should have been called if we were expecting it to be called"
    206  );
    207 
    208  if (expectStringInPage) {
    209    let pageContent = await SpecialPowers.spawn(
    210      win.gBrowser.selectedBrowser,
    211      [],
    212      async function () {
    213        return content.document.body.textContent;
    214      }
    215    );
    216    Assert.ok(
    217      pageContent.includes(expectStringInPage),
    218      `page should contain the string '${expectStringInPage}' (was '${pageContent}')`
    219    );
    220  }
    221 
    222  await win.close();
    223 
    224  // This clears the TLS session cache so we don't use a previously-established
    225  // ticket to connect and bypass selecting a client auth certificate in
    226  // subsequent tests.
    227  sdr.logout();
    228 }
    229 
    230 // Test that if a certificate is chosen automatically the connection succeeds,
    231 // and that nsIClientAuthDialogService.chooseCertificate() is never called.
    232 add_task(async function testCertChosenAutomatically() {
    233  gClientAuthDialogService.state = DialogState.ASSERT_NOT_CALLED;
    234  await testHelper(
    235    "Select Automatically",
    236    "https://requireclientcert.example.com/",
    237    "https://requireclientcert.example.com/",
    238    false
    239  );
    240  // This clears all saved client auth certificate state so we don't influence
    241  // subsequent tests.
    242  cars.clearRememberedDecisions();
    243 });
    244 
    245 // Test that if the user doesn't choose a certificate, the connection fails and
    246 // an error page is displayed.
    247 add_task(async function testCertNotChosenByUser() {
    248  gClientAuthDialogService.state = DialogState.RETURN_CERT_NOT_SELECTED;
    249  await testHelper(
    250    "Ask Every Time",
    251    "https://requireclientcert.example.com/",
    252    undefined,
    253    true,
    254    undefined,
    255    // bug 1818556: ssltunnel doesn't behave as expected here on Windows
    256    AppConstants.platform != "win"
    257      ? "SSL_ERROR_RX_CERTIFICATE_REQUIRED_ALERT"
    258      : undefined
    259  );
    260  cars.clearRememberedDecisions();
    261 });
    262 
    263 // Test that if the user chooses a certificate the connection suceeeds.
    264 add_task(async function testCertChosenByUser() {
    265  gClientAuthDialogService.state = DialogState.RETURN_CERT_SELECTED;
    266  await testHelper(
    267    "Ask Every Time",
    268    "https://requireclientcert.example.com/",
    269    "https://requireclientcert.example.com/",
    270    true
    271  );
    272  cars.clearRememberedDecisions();
    273 });
    274 
    275 // Test that the cancel decision is remembered correctly
    276 add_task(async function testEmptyCertChosenByUser() {
    277  gClientAuthDialogService.state = DialogState.RETURN_CERT_NOT_SELECTED;
    278  gClientAuthDialogService.rememberClientAuthCertificate = true;
    279  await testHelper(
    280    "Ask Every Time",
    281    "https://requireclientcert.example.com/",
    282    undefined,
    283    true
    284  );
    285  await testHelper(
    286    "Ask Every Time",
    287    "https://requireclientcert.example.com/",
    288    undefined,
    289    false
    290  );
    291  cars.clearRememberedDecisions();
    292 });
    293 
    294 // Test that if the user chooses a certificate in a private browsing window,
    295 // configures Firefox to remember this certificate for the duration of the
    296 // session, closes that window (and thus all private windows), reopens a private
    297 // window, and visits that site again, they are re-asked for a certificate (i.e.
    298 // any state from the previous private session should be gone). Similarly, after
    299 // closing that private window, if the user opens a non-private window, they
    300 // again should be asked to choose a certificate (i.e. private state should not
    301 // be remembered/used in non-private contexts).
    302 add_task(async function testClearPrivateBrowsingState() {
    303  gClientAuthDialogService.rememberClientAuthCertificate = true;
    304  gClientAuthDialogService.state = DialogState.RETURN_CERT_SELECTED;
    305  await testHelper(
    306    "Ask Every Time",
    307    "https://requireclientcert.example.com/",
    308    "https://requireclientcert.example.com/",
    309    true,
    310    {
    311      private: true,
    312    }
    313  );
    314  await testHelper(
    315    "Ask Every Time",
    316    "https://requireclientcert.example.com/",
    317    "https://requireclientcert.example.com/",
    318    true,
    319    {
    320      private: true,
    321    }
    322  );
    323  await testHelper(
    324    "Ask Every Time",
    325    "https://requireclientcert.example.com/",
    326    "https://requireclientcert.example.com/",
    327    true
    328  );
    329  // NB: we don't `cars.clearRememberedDecisions()` in between the two calls to
    330  // `testHelper` because that would clear all client auth certificate state and
    331  // obscure what we're testing (that Firefox properly clears the relevant state
    332  // when the last private window closes).
    333  cars.clearRememberedDecisions();
    334 });
    335 
    336 // Test that 3rd party certificates are taken into account when filtering client
    337 // certificates based on the acceptible CA list sent by the server.
    338 add_task(async function testCertFilteringWithIntermediate() {
    339  let intermediateBytes = await IOUtils.readUTF8(
    340    getTestFilePath("intermediate.pem")
    341  ).then(
    342    pem => {
    343      let base64 = pemToBase64(pem);
    344      let bin = atob(base64);
    345      let bytes = [];
    346      for (let i = 0; i < bin.length; i++) {
    347        bytes.push(bin.charCodeAt(i));
    348      }
    349      return bytes;
    350    },
    351    error => {
    352      throw error;
    353    }
    354  );
    355  let nssComponent = Cc["@mozilla.org/psm;1"].getService(Ci.nsINSSComponent);
    356  nssComponent.addEnterpriseIntermediate(intermediateBytes);
    357  gExpectedClientCertificateChoices = 4;
    358  gClientAuthDialogService.state = DialogState.RETURN_CERT_SELECTED;
    359  await testHelper(
    360    "Ask Every Time",
    361    "https://requireclientcert.example.com/",
    362    "https://requireclientcert.example.com/",
    363    true
    364  );
    365  cars.clearRememberedDecisions();
    366  // This will reset the added intermediate.
    367  await SpecialPowers.pushPrefEnv({
    368    set: [["security.enterprise_roots.enabled", true]],
    369  });
    370 });
    371 
    372 // Test that if the server certificate does not validate successfully,
    373 // nsIClientAuthDialogService.chooseCertificate() is never called.
    374 add_task(async function testNoDialogForUntrustedServerCertificate() {
    375  gClientAuthDialogService.state = DialogState.ASSERT_NOT_CALLED;
    376  await testHelper(
    377    "Ask Every Time",
    378    "https://requireclientcert-untrusted.example.com/",
    379    undefined,
    380    false
    381  );
    382  // This clears all saved client auth certificate state so we don't influence
    383  // subsequent tests.
    384  cars.clearRememberedDecisions();
    385 });