tor-browser

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

test_intermediate_preloads.js (13850B)


      1 // -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
      2 // This Source Code Form is subject to the terms of the Mozilla Public
      3 // License, v. 2.0. If a copy of the MPL was not distributed with this
      4 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
      5 
      6 "use strict";
      7 do_get_profile(); // must be called before getting nsIX509CertDB
      8 
      9 const { RemoteSecuritySettings } = ChromeUtils.importESModule(
     10  "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs"
     11 );
     12 const { TestUtils } = ChromeUtils.importESModule(
     13  "resource://testing-common/TestUtils.sys.mjs"
     14 );
     15 const { IntermediatePreloadsClient } = RemoteSecuritySettings.init();
     16 
     17 let server;
     18 
     19 const INTERMEDIATES_DL_PER_POLL_PREF =
     20  "security.remote_settings.intermediates.downloads_per_poll";
     21 const INTERMEDIATES_ENABLED_PREF =
     22  "security.remote_settings.intermediates.enabled";
     23 
     24 function getHashCommon(aStr, useBase64) {
     25  let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
     26    Ci.nsICryptoHash
     27  );
     28  hasher.init(Ci.nsICryptoHash.SHA256);
     29  let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
     30    Ci.nsIStringInputStream
     31  );
     32  stringStream.setByteStringData(aStr);
     33  hasher.updateFromStream(stringStream, -1);
     34 
     35  return hasher.finish(useBase64);
     36 }
     37 
     38 // Get a hexified SHA-256 hash of the given string.
     39 function getHash(aStr) {
     40  return hexify(getHashCommon(aStr, false));
     41 }
     42 
     43 function getSubjectBytes(certDERString) {
     44  let bytes = stringToArray(certDERString);
     45  let cert = new X509.Certificate();
     46  cert.parse(bytes);
     47  return arrayToString(cert.tbsCertificate.subject._der._bytes);
     48 }
     49 
     50 function getSPKIBytes(certDERString) {
     51  let bytes = stringToArray(certDERString);
     52  let cert = new X509.Certificate();
     53  cert.parse(bytes);
     54  return arrayToString(cert.tbsCertificate.subjectPublicKeyInfo._der._bytes);
     55 }
     56 
     57 /**
     58 * Simulate a Remote Settings synchronization by filling up the
     59 * local data with fake records.
     60 *
     61 * @param {*} filenames List of pem files for which we will create
     62 *                      records.
     63 * @param {*} options Options for records to generate.
     64 */
     65 async function syncAndDownload(filenames, options = {}) {
     66  const {
     67    hashFunc = getHash,
     68    lengthFunc = arr => arr.length,
     69    clear = true,
     70  } = options;
     71 
     72  const localDB = await IntermediatePreloadsClient.client.db;
     73  if (clear) {
     74    await localDB.clear();
     75  }
     76 
     77  let count = 1;
     78  for (const filename of filenames) {
     79    const file = do_get_file(`test_intermediate_preloads/${filename}`);
     80    const certBytes = readFile(file);
     81    const certDERBytes = atob(pemToBase64(certBytes));
     82 
     83    const record = {
     84      details: {
     85        who: "",
     86        why: "",
     87        name: "",
     88        created: "",
     89      },
     90      derHash: getHashCommon(certDERBytes, true),
     91      subject: "",
     92      subjectDN: btoa(getSubjectBytes(certDERBytes)),
     93      attachment: {
     94        hash: hashFunc(certBytes),
     95        size: lengthFunc(certBytes),
     96        filename: `intermediate certificate #${count}.pem`,
     97        location: `security-state-workspace/intermediates/${filename}`,
     98        mimetype: "application/x-pem-file",
     99      },
    100      whitelist: false,
    101      pubKeyHash: getHashCommon(getSPKIBytes(certDERBytes), true),
    102      crlite_enrolled: true,
    103    };
    104 
    105    await localDB.create(record);
    106    count++;
    107  }
    108  // This promise will wait for the end of downloading.
    109  const updatedPromise = TestUtils.topicObserved(
    110    "remote-security-settings:intermediates-updated"
    111  );
    112  // Simulate polling for changes, trigger the download of attachments.
    113  Services.obs.notifyObservers(null, "remote-settings:changes-poll-end");
    114  const results = await updatedPromise;
    115  return results[1]; // topicObserved gives back a 2-array
    116 }
    117 
    118 /**
    119 * Return the list of records whose attachment was downloaded.
    120 */
    121 async function locallyDownloaded() {
    122  return IntermediatePreloadsClient.client.get({
    123    filters: { cert_import_complete: true },
    124    syncIfEmpty: false,
    125  });
    126 }
    127 
    128 add_task(async function test_preload_empty() {
    129  Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
    130 
    131  let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
    132    Ci.nsIX509CertDB
    133  );
    134 
    135  // load the first root and end entity, ignore the initial intermediate
    136  addCertFromFile(certDB, "test_intermediate_preloads/ca.pem", "CTu,,");
    137 
    138  let ee_cert = constructCertFromFile(
    139    "test_intermediate_preloads/default-ee.pem"
    140  );
    141  notEqual(ee_cert, null, "EE cert should have successfully loaded");
    142 
    143  equal(
    144    await syncAndDownload([]),
    145    "success",
    146    "Preloading update should have run"
    147  );
    148 
    149  equal(
    150    (await locallyDownloaded()).length,
    151    0,
    152    "There should have been no downloads"
    153  );
    154 
    155  // check that ee cert 1 is unknown
    156  await checkCertErrorGeneric(
    157    certDB,
    158    ee_cert,
    159    SEC_ERROR_UNKNOWN_ISSUER,
    160    Ci.nsIX509CertDB.verifyUsageTLSServer
    161  );
    162 });
    163 
    164 add_task(async function test_preload_disabled() {
    165  Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, false);
    166 
    167  equal(
    168    await syncAndDownload(["int.pem"]),
    169    "disabled",
    170    "Preloading update should not have run"
    171  );
    172 
    173  equal(
    174    (await locallyDownloaded()).length,
    175    0,
    176    "There should have been no downloads"
    177  );
    178 });
    179 
    180 add_task(async function test_preload_invalid_hash() {
    181  Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
    182  const invalidHash =
    183    "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d";
    184 
    185  const result = await syncAndDownload(["int.pem"], {
    186    hashFunc: () => invalidHash,
    187  });
    188  equal(result, "success", "Preloading update should have run");
    189 
    190  equal(
    191    (await locallyDownloaded()).length,
    192    0,
    193    "There should be no local entry"
    194  );
    195 
    196  let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
    197    Ci.nsIX509CertDB
    198  );
    199 
    200  // load the first root and end entity, ignore the initial intermediate
    201  addCertFromFile(certDB, "test_intermediate_preloads/ca.pem", "CTu,,");
    202 
    203  let ee_cert = constructCertFromFile(
    204    "test_intermediate_preloads/default-ee.pem"
    205  );
    206  notEqual(ee_cert, null, "EE cert should have successfully loaded");
    207 
    208  // We should still have a missing intermediate.
    209  await checkCertErrorGeneric(
    210    certDB,
    211    ee_cert,
    212    SEC_ERROR_UNKNOWN_ISSUER,
    213    Ci.nsIX509CertDB.verifyUsageTLSServer
    214  );
    215 });
    216 
    217 add_task(async function test_preload_invalid_length() {
    218  Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
    219 
    220  const result = await syncAndDownload(["int.pem"], {
    221    lengthFunc: () => 42,
    222  });
    223  equal(result, "success", "Preloading update should have run");
    224 
    225  equal(
    226    (await locallyDownloaded()).length,
    227    0,
    228    "There should be no local entry"
    229  );
    230 
    231  let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
    232    Ci.nsIX509CertDB
    233  );
    234 
    235  // load the first root and end entity, ignore the initial intermediate
    236  addCertFromFile(certDB, "test_intermediate_preloads/ca.pem", "CTu,,");
    237 
    238  let ee_cert = constructCertFromFile(
    239    "test_intermediate_preloads/default-ee.pem"
    240  );
    241  notEqual(ee_cert, null, "EE cert should have successfully loaded");
    242 
    243  // We should still have a missing intermediate.
    244  await checkCertErrorGeneric(
    245    certDB,
    246    ee_cert,
    247    SEC_ERROR_UNKNOWN_ISSUER,
    248    Ci.nsIX509CertDB.verifyUsageTLSServer
    249  );
    250 });
    251 
    252 add_task(async function test_preload_basic() {
    253  Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
    254  Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 100);
    255 
    256  let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
    257    Ci.nsIX509CertDB
    258  );
    259 
    260  // load the first root and end entity, ignore the initial intermediate
    261  addCertFromFile(certDB, "test_intermediate_preloads/ca.pem", "CTu,,");
    262 
    263  let ee_cert = constructCertFromFile(
    264    "test_intermediate_preloads/default-ee.pem"
    265  );
    266  notEqual(ee_cert, null, "EE cert should have successfully loaded");
    267 
    268  // load the second end entity, ignore both intermediate and root
    269  let ee_cert_2 = constructCertFromFile("test_intermediate_preloads/ee2.pem");
    270  notEqual(ee_cert_2, null, "EE cert 2 should have successfully loaded");
    271 
    272  // check that the missing intermediate causes an unknown issuer error, as
    273  // expected, in both cases
    274  await checkCertErrorGeneric(
    275    certDB,
    276    ee_cert,
    277    SEC_ERROR_UNKNOWN_ISSUER,
    278    Ci.nsIX509CertDB.verifyUsageTLSServer
    279  );
    280  await checkCertErrorGeneric(
    281    certDB,
    282    ee_cert_2,
    283    SEC_ERROR_UNKNOWN_ISSUER,
    284    Ci.nsIX509CertDB.verifyUsageTLSServer
    285  );
    286 
    287  let intermediateBytes = readFile(
    288    do_get_file("test_intermediate_preloads/int.pem")
    289  );
    290  let intermediateDERBytes = atob(pemToBase64(intermediateBytes));
    291  let intermediateCert = new X509.Certificate();
    292  intermediateCert.parse(stringToArray(intermediateDERBytes));
    293 
    294  const result = await syncAndDownload(["int.pem", "int2.pem"]);
    295  equal(result, "success", "Preloading update should have run");
    296 
    297  equal(
    298    (await locallyDownloaded()).length,
    299    2,
    300    "There should have been 2 downloads"
    301  );
    302 
    303  // check that ee cert 1 verifies now the update has happened and there is
    304  // an intermediate
    305 
    306  // First verify by connecting to a server that uses that end-entity
    307  // certificate but doesn't send the intermediate.
    308  await asyncStartTLSTestServer(
    309    "BadCertAndPinningServer",
    310    "test_intermediate_preloads"
    311  );
    312  // This ensures the test server doesn't include the intermediate in the
    313  // handshake.
    314  let certDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
    315  certDir.append("test_intermediate_preloads");
    316  Assert.ok(certDir.exists(), "test_intermediate_preloads should exist");
    317  let args = ["-D", "-n", "int"];
    318  // If the certdb is cached from a previous run, the intermediate will have
    319  // already been deleted, so this may "fail".
    320  run_certutil_on_directory(certDir.path, args, false);
    321 
    322  await checkCertErrorGeneric(
    323    certDB,
    324    ee_cert,
    325    PRErrorCodeSuccess,
    326    Ci.nsIX509CertDB.verifyUsageTLSServer
    327  );
    328 
    329  let localDB = await IntermediatePreloadsClient.client.db;
    330  let data = await localDB.list();
    331  ok(!!data.length, "should have some entries");
    332  // simulate a sync (syncAndDownload doesn't actually... sync.)
    333  await IntermediatePreloadsClient.client.emit("sync", {
    334    data: {
    335      current: data,
    336      created: data,
    337      deleted: [],
    338      updated: [],
    339    },
    340  });
    341 
    342  // check that ee cert 2 does not verify - since we don't know the issuer of
    343  // this certificate
    344  await checkCertErrorGeneric(
    345    certDB,
    346    ee_cert_2,
    347    SEC_ERROR_UNKNOWN_ISSUER,
    348    Ci.nsIX509CertDB.verifyUsageTLSServer
    349  );
    350 });
    351 
    352 add_task(async function test_preload_200() {
    353  Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
    354  Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 100);
    355 
    356  const files = [];
    357  for (let i = 0; i < 200; i++) {
    358    files.push(["int.pem", "int2.pem"][i % 2]);
    359  }
    360 
    361  let result = await syncAndDownload(files);
    362  equal(result, "success", "Preloading update should have run");
    363 
    364  equal(
    365    (await locallyDownloaded()).length,
    366    100,
    367    "There should have been only 100 downloaded"
    368  );
    369 
    370  // Re-run
    371  result = await syncAndDownload([], { clear: false });
    372  equal(result, "success", "Preloading update should have run");
    373 
    374  equal(
    375    (await locallyDownloaded()).length,
    376    200,
    377    "There should have been 200 downloaded"
    378  );
    379 });
    380 
    381 add_task(async function test_delete() {
    382  Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
    383  Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 100);
    384 
    385  let syncResult = await syncAndDownload(["int.pem", "int2.pem"]);
    386  equal(syncResult, "success", "Preloading update should have run");
    387 
    388  equal(
    389    (await locallyDownloaded()).length,
    390    2,
    391    "There should have been 2 downloads"
    392  );
    393 
    394  let localDB = await IntermediatePreloadsClient.client.db;
    395  let data = await localDB.list();
    396  ok(!!data.length, "should have some entries");
    397  let subject = data[0].subjectDN;
    398  let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
    399    Ci.nsICertStorage
    400  );
    401  let resultsBefore = certStorage.findCertsBySubject(
    402    stringToArray(atob(subject))
    403  );
    404  equal(
    405    resultsBefore.length,
    406    1,
    407    "should find the intermediate in cert storage before"
    408  );
    409  // simulate a sync where we deleted the entry
    410  await IntermediatePreloadsClient.client.emit("sync", {
    411    data: {
    412      current: [],
    413      created: [],
    414      deleted: [data[0]],
    415      updated: [],
    416    },
    417  });
    418  let resultsAfter = certStorage.findCertsBySubject(
    419    stringToArray(atob(subject))
    420  );
    421  equal(
    422    resultsAfter.length,
    423    0,
    424    "shouldn't find intermediate in cert storage now"
    425  );
    426 });
    427 
    428 add_task(async function test_bug1966632() {
    429  let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
    430    Ci.nsIX509CertDB
    431  );
    432 
    433  constructCertFromFile("test_intermediate_preloads/bug1966632-int1.pem", ",,");
    434  await checkRootOfBuiltChain(
    435    certDB,
    436    constructCertFromFile("test_intermediate_preloads/bug1966632-ee.pem", ",,"),
    437    "G/ANXI8TwJTdF+AFBM8IiIUPEv0Gf6H5LA/b9guG4yE=",
    438    new Date("2025-05-21T00:00:00Z").getTime() / 1000,
    439    undefined,
    440    Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
    441  );
    442 });
    443 
    444 function run_test() {
    445  server = new HttpServer();
    446  server.start(-1);
    447  registerCleanupFunction(() => server.stop(() => {}));
    448 
    449  server.registerDirectory(
    450    "/cdn/security-state-workspace/intermediates/",
    451    do_get_file("test_intermediate_preloads")
    452  );
    453 
    454  server.registerPathHandler("/v1/", (request, response) => {
    455    response.write(
    456      JSON.stringify({
    457        capabilities: {
    458          attachments: {
    459            base_url: `http://localhost:${server.identity.primaryPort}/cdn/`,
    460          },
    461        },
    462      })
    463    );
    464    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    465    response.setStatusLine(null, 200, "OK");
    466  });
    467 
    468  Services.prefs.setCharPref(
    469    "services.settings.server",
    470    `http://localhost:${server.identity.primaryPort}/v1`
    471  );
    472 
    473  Services.prefs.setCharPref("browser.policies.loglevel", "debug");
    474 
    475  run_next_test();
    476 }