diff options
Diffstat (limited to 'security/manager/ssl/tests/unit/test_intermediate_preloads.js')
-rw-r--r-- | security/manager/ssl/tests/unit/test_intermediate_preloads.js | 645 |
1 files changed, 645 insertions, 0 deletions
diff --git a/security/manager/ssl/tests/unit/test_intermediate_preloads.js b/security/manager/ssl/tests/unit/test_intermediate_preloads.js new file mode 100644 index 0000000000..1f8b289eb3 --- /dev/null +++ b/security/manager/ssl/tests/unit/test_intermediate_preloads.js @@ -0,0 +1,645 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// 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"; +do_get_profile(); // must be called before getting nsIX509CertDB + +const { RemoteSettings } = ChromeUtils.import( + "resource://services-settings/remote-settings.js" +); +const { RemoteSecuritySettings } = ChromeUtils.import( + "resource://gre/modules/psm/RemoteSecuritySettings.jsm" +); +const { TestUtils } = ChromeUtils.import( + "resource://testing-common/TestUtils.jsm" +); +const { TelemetryTestUtils } = ChromeUtils.import( + "resource://testing-common/TelemetryTestUtils.jsm" +); +const { IntermediatePreloadsClient } = RemoteSecuritySettings.init(); + +let server; + +let intermediate1Data; +let intermediate2Data; + +const INTERMEDIATES_DL_PER_POLL_PREF = + "security.remote_settings.intermediates.downloads_per_poll"; +const INTERMEDIATES_ENABLED_PREF = + "security.remote_settings.intermediates.enabled"; + +function getHashCommon(aStr, useBase64) { + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + hasher.init(Ci.nsICryptoHash.SHA256); + let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stringStream.data = aStr; + hasher.updateFromStream(stringStream, -1); + + return hasher.finish(useBase64); +} + +// Get a hexified SHA-256 hash of the given string. +function getHash(aStr) { + return hexify(getHashCommon(aStr, false)); +} + +function countTelemetryReports(histogram) { + let count = 0; + for (let x in histogram.values) { + count += histogram.values[x]; + } + return count; +} + +function clearTelemetry() { + Services.telemetry.getHistogramById("INTERMEDIATE_PRELOADING_ERRORS").clear(); + Services.telemetry + .getHistogramById("INTERMEDIATE_PRELOADING_UPDATE_TIME_MS") + .clear(); + Services.telemetry.clearScalars(); +} + +function getSubjectBytes(certDERString) { + let bytes = stringToArray(certDERString); + let cert = new X509.Certificate(); + cert.parse(bytes); + return arrayToString(cert.tbsCertificate.subject._der._bytes); +} + +function getSPKIBytes(certDERString) { + let bytes = stringToArray(certDERString); + let cert = new X509.Certificate(); + cert.parse(bytes); + return arrayToString(cert.tbsCertificate.subjectPublicKeyInfo._der._bytes); +} + +/** + * Simulate a Remote Settings synchronization by filling up the + * local data with fake records. + * + * @param {*} filenames List of pem files for which we will create + * records. + * @param {*} options Options for records to generate. + */ +async function syncAndDownload(filenames, options = {}) { + const { + hashFunc = getHash, + lengthFunc = arr => arr.length, + clear = true, + } = options; + + const localDB = await IntermediatePreloadsClient.client.db; + if (clear) { + await localDB.clear(); + } + + let count = 1; + for (const filename of filenames) { + const file = do_get_file(`test_intermediate_preloads/${filename}`); + const certBytes = readFile(file); + const certDERBytes = atob(pemToBase64(certBytes)); + + const record = { + details: { + who: "", + why: "", + name: "", + created: "", + }, + derHash: getHashCommon(certDERBytes, true), + subject: "", + subjectDN: btoa(getSubjectBytes(certDERBytes)), + attachment: { + hash: hashFunc(certBytes), + size: lengthFunc(certBytes), + filename: `intermediate certificate #${count}.pem`, + location: `security-state-workspace/intermediates/${filename}`, + mimetype: "application/x-pem-file", + }, + whitelist: false, + pubKeyHash: getHashCommon(getSPKIBytes(certDERBytes), true), + crlite_enrolled: true, + }; + + await localDB.create(record); + count++; + } + // This promise will wait for the end of downloading. + const updatedPromise = TestUtils.topicObserved( + "remote-security-settings:intermediates-updated" + ); + // Simulate polling for changes, trigger the download of attachments. + Services.obs.notifyObservers(null, "remote-settings:changes-poll-end"); + const results = await updatedPromise; + return results[1]; // topicObserved gives back a 2-array +} + +/** + * Return the list of records whose attachment was downloaded. + */ +async function locallyDownloaded() { + return IntermediatePreloadsClient.client.get({ + filters: { cert_import_complete: true }, + syncIfEmpty: false, + }); +} + +add_task(async function test_preload_empty() { + Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); + + let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + + // load the first root and end entity, ignore the initial intermediate + addCertFromFile(certDB, "test_intermediate_preloads/ca.pem", "CTu,,"); + + let ee_cert = constructCertFromFile( + "test_intermediate_preloads/default-ee.pem" + ); + notEqual(ee_cert, null, "EE cert should have successfully loaded"); + + equal( + await syncAndDownload([]), + "success", + "Preloading update should have run" + ); + + equal( + (await locallyDownloaded()).length, + 0, + "There should have been no downloads" + ); + + // check that ee cert 1 is unknown + await checkCertErrorGeneric( + certDB, + ee_cert, + SEC_ERROR_UNKNOWN_ISSUER, + certificateUsageSSLServer + ); +}); + +add_task(async function test_preload_disabled() { + Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, false); + + equal( + await syncAndDownload(["int.pem"]), + "disabled", + "Preloading update should not have run" + ); + + equal( + (await locallyDownloaded()).length, + 0, + "There should have been no downloads" + ); +}); + +add_task(async function test_preload_invalid_hash() { + // Enable the collection (during test) for all products so even products + // that don't collect the data will be able to run the test without failure. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); + const invalidHash = + "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d"; + + clearTelemetry(); + + const result = await syncAndDownload(["int.pem"], { + hashFunc: () => invalidHash, + }); + equal(result, "success", "Preloading update should have run"); + + let errors_histogram = Services.telemetry + .getHistogramById("INTERMEDIATE_PRELOADING_ERRORS") + .snapshot(); + + equal( + countTelemetryReports(errors_histogram), + 1, + "There should be one error report" + ); + equal( + errors_histogram.values[7], + 1, + "There should be one invalid hash error" + ); + + equal( + (await locallyDownloaded()).length, + 0, + "There should be no local entry" + ); + + let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + + // load the first root and end entity, ignore the initial intermediate + addCertFromFile(certDB, "test_intermediate_preloads/ca.pem", "CTu,,"); + + let ee_cert = constructCertFromFile( + "test_intermediate_preloads/default-ee.pem" + ); + notEqual(ee_cert, null, "EE cert should have successfully loaded"); + + // We should still have a missing intermediate. + await checkCertErrorGeneric( + certDB, + ee_cert, + SEC_ERROR_UNKNOWN_ISSUER, + certificateUsageSSLServer + ); +}); + +add_task(async function test_preload_invalid_length() { + Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); + + clearTelemetry(); + + const result = await syncAndDownload(["int.pem"], { + lengthFunc: () => 42, + }); + equal(result, "success", "Preloading update should have run"); + + let errors_histogram = Services.telemetry + .getHistogramById("INTERMEDIATE_PRELOADING_ERRORS") + .snapshot(); + + equal( + countTelemetryReports(errors_histogram), + 1, + "There should be only one error report" + ); + equal( + errors_histogram.values[7], + 1, + "There should be one invalid content hash error" + ); + + equal( + (await locallyDownloaded()).length, + 0, + "There should be no local entry" + ); + + let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + + // load the first root and end entity, ignore the initial intermediate + addCertFromFile(certDB, "test_intermediate_preloads/ca.pem", "CTu,,"); + + let ee_cert = constructCertFromFile( + "test_intermediate_preloads/default-ee.pem" + ); + notEqual(ee_cert, null, "EE cert should have successfully loaded"); + + // We should still have a missing intermediate. + await checkCertErrorGeneric( + certDB, + ee_cert, + SEC_ERROR_UNKNOWN_ISSUER, + certificateUsageSSLServer + ); +}); + +add_task(async function test_preload_basic() { + Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); + Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 100); + + let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + + // load the first root and end entity, ignore the initial intermediate + addCertFromFile(certDB, "test_intermediate_preloads/ca.pem", "CTu,,"); + + let ee_cert = constructCertFromFile( + "test_intermediate_preloads/default-ee.pem" + ); + notEqual(ee_cert, null, "EE cert should have successfully loaded"); + + // load the second end entity, ignore both intermediate and root + let ee_cert_2 = constructCertFromFile("test_intermediate_preloads/ee2.pem"); + notEqual(ee_cert_2, null, "EE cert 2 should have successfully loaded"); + + // check that the missing intermediate causes an unknown issuer error, as + // expected, in both cases + await checkCertErrorGeneric( + certDB, + ee_cert, + SEC_ERROR_UNKNOWN_ISSUER, + certificateUsageSSLServer + ); + await checkCertErrorGeneric( + certDB, + ee_cert_2, + SEC_ERROR_UNKNOWN_ISSUER, + certificateUsageSSLServer + ); + + let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService( + Ci.nsICertStorage + ); + let intermediateBytes = readFile( + do_get_file("test_intermediate_preloads/int.pem") + ); + let intermediateDERBytes = atob(pemToBase64(intermediateBytes)); + let intermediateCert = new X509.Certificate(); + intermediateCert.parse(stringToArray(intermediateDERBytes)); + let crliteStateBefore = certStorage.getCRLiteState( + intermediateCert.tbsCertificate.subject._der._bytes, + intermediateCert.tbsCertificate.subjectPublicKeyInfo._der._bytes + ); + equal( + crliteStateBefore, + Ci.nsICertStorage.STATE_UNSET, + "crlite state should be unset before" + ); + + const result = await syncAndDownload(["int.pem", "int2.pem"]); + equal(result, "success", "Preloading update should have run"); + + equal( + (await locallyDownloaded()).length, + 2, + "There should have been 2 downloads" + ); + + // check that ee cert 1 verifies now the update has happened and there is + // an intermediate + + // First verify by connecting to a server that uses that end-entity + // certificate but doesn't send the intermediate. + await asyncStartTLSTestServer( + "BadCertAndPinningServer", + "test_intermediate_preloads" + ); + // This ensures the test server doesn't include the intermediate in the + // handshake. + let certDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + certDir.append("test_intermediate_preloads"); + Assert.ok(certDir.exists(), "test_intermediate_preloads should exist"); + let args = ["-D", "-n", "int"]; + // If the certdb is cached from a previous run, the intermediate will have + // already been deleted, so this may "fail". + run_certutil_on_directory(certDir.path, args, false); + let certsCachedPromise = TestUtils.topicObserved( + "psm:intermediate-certs-cached" + ); + await asyncConnectTo("ee.example.com", PRErrorCodeSuccess); + let subjectAndData = await certsCachedPromise; + Assert.equal(subjectAndData.length, 2, "expecting [subject, data]"); + // Since the intermediate is preloaded, we don't save it to the profile's + // certdb. + Assert.equal(subjectAndData[1], "0", `expecting "0" certs imported`); + + await checkCertErrorGeneric( + certDB, + ee_cert, + PRErrorCodeSuccess, + certificateUsageSSLServer + ); + + let localDB = await IntermediatePreloadsClient.client.db; + let data = await localDB.list(); + ok(data.length > 0, "should have some entries"); + // simulate a sync (syncAndDownload doesn't actually... sync.) + await IntermediatePreloadsClient.client.emit("sync", { + data: { + current: data, + created: data, + deleted: [], + updated: [], + }, + }); + + let crliteStateAfter = certStorage.getCRLiteState( + intermediateCert.tbsCertificate.subject._der._bytes, + intermediateCert.tbsCertificate.subjectPublicKeyInfo._der._bytes + ); + equal( + crliteStateAfter, + Ci.nsICertStorage.STATE_ENFORCE, + "crlite state should be set after" + ); + + // check that ee cert 2 does not verify - since we don't know the issuer of + // this certificate + await checkCertErrorGeneric( + certDB, + ee_cert_2, + SEC_ERROR_UNKNOWN_ISSUER, + certificateUsageSSLServer + ); +}); + +add_task(async function test_preload_200() { + Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); + Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 100); + + const files = []; + for (let i = 0; i < 200; i++) { + files.push(["int.pem", "int2.pem"][i % 2]); + } + + clearTelemetry(); + + let result = await syncAndDownload(files); + equal(result, "success", "Preloading update should have run"); + + equal( + (await locallyDownloaded()).length, + 100, + "There should have been only 100 downloaded" + ); + + const scalars = TelemetryTestUtils.getProcessScalars("parent"); + TelemetryTestUtils.assertScalar( + scalars, + "security.intermediate_preloading_num_preloaded", + 100, + "Should have preloaded 100 certs" + ); + TelemetryTestUtils.assertScalar( + scalars, + "security.intermediate_preloading_num_pending", + 100, + "Should report 100 pending" + ); + + let time_histogram = Services.telemetry + .getHistogramById("INTERMEDIATE_PRELOADING_UPDATE_TIME_MS") + .snapshot(); + let errors_histogram = Services.telemetry + .getHistogramById("INTERMEDIATE_PRELOADING_ERRORS") + .snapshot(); + equal(countTelemetryReports(time_histogram), 1, "Should report time once"); + equal( + countTelemetryReports(errors_histogram), + 0, + "There should be no error reports" + ); + + // Re-run + result = await syncAndDownload([], { clear: false }); + equal(result, "success", "Preloading update should have run"); + + equal( + (await locallyDownloaded()).length, + 200, + "There should have been 200 downloaded" + ); +}); + +add_task(async function test_delete() { + Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); + Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 100); + + let syncResult = await syncAndDownload(["int.pem", "int2.pem"]); + equal(syncResult, "success", "Preloading update should have run"); + + equal( + (await locallyDownloaded()).length, + 2, + "There should have been 2 downloads" + ); + + let localDB = await IntermediatePreloadsClient.client.db; + let data = await localDB.list(); + ok(data.length > 0, "should have some entries"); + let subject = data[0].subjectDN; + let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService( + Ci.nsICertStorage + ); + let resultsBefore = certStorage.findCertsBySubject( + stringToArray(atob(subject)) + ); + equal( + resultsBefore.length, + 1, + "should find the intermediate in cert storage before" + ); + // simulate a sync where we deleted the entry + await IntermediatePreloadsClient.client.emit("sync", { + data: { + current: [], + created: [], + deleted: [data[0]], + updated: [], + }, + }); + let resultsAfter = certStorage.findCertsBySubject( + stringToArray(atob(subject)) + ); + equal( + resultsAfter.length, + 0, + "shouldn't find intermediate in cert storage now" + ); +}); + +function findCertByCommonName(certDB, commonName) { + for (let cert of certDB.getCerts()) { + if (cert.commonName == commonName) { + return cert; + } + } + return null; +} + +add_task(async function test_healer() { + Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true); + Services.prefs.setIntPref(INTERMEDIATES_DL_PER_POLL_PREF, 100); + + let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService( + Ci.nsIX509CertDB + ); + // Add an intermediate as if it had previously been cached. + addCertFromFile(certDB, "test_intermediate_preloads/int.pem", ",,"); + // Add an intermediate with non-default trust settings as if it had been added by the user. + addCertFromFile(certDB, "test_intermediate_preloads/int2.pem", "CTu,,"); + + let syncResult = await syncAndDownload(["int.pem", "int2.pem"]); + equal(syncResult, "success", "Preloading update should have run"); + + equal( + (await locallyDownloaded()).length, + 2, + "There should have been 2 downloads" + ); + + let healerRanPromise = TestUtils.topicObserved( + "psm:intermediate-preloading-healer-ran" + ); + Services.prefs.setIntPref( + "security.intermediate_preloading_healer.timer_interval_ms", + 500 + ); + Services.prefs.setBoolPref( + "security.intermediate_preloading_healer.enabled", + true + ); + await healerRanPromise; + Services.prefs.setBoolPref( + "security.intermediate_preloading_healer.enabled", + false + ); + + let intermediate = findCertByCommonName( + certDB, + "intermediate-preloading-intermediate" + ); + equal(intermediate, null, "should not find intermediate in NSS"); + let intermediate2 = findCertByCommonName( + certDB, + "intermediate-preloading-intermediate2" + ); + notEqual(intermediate2, null, "should find second intermediate in NSS"); +}); + +function run_test() { + server = new HttpServer(); + server.start(-1); + registerCleanupFunction(() => server.stop(() => {})); + + server.registerDirectory( + "/cdn/security-state-workspace/intermediates/", + do_get_file("test_intermediate_preloads") + ); + + server.registerPathHandler("/v1/", (request, response) => { + response.write( + JSON.stringify({ + capabilities: { + attachments: { + base_url: `http://localhost:${server.identity.primaryPort}/cdn/`, + }, + }, + }) + ); + response.setHeader("Content-Type", "application/json; charset=UTF-8"); + response.setStatusLine(null, 200, "OK"); + }); + + Services.prefs.setCharPref( + "services.settings.server", + `http://localhost:${server.identity.primaryPort}/v1` + ); + + Services.prefs.setCharPref("browser.policies.loglevel", "debug"); + + run_next_test(); +} |