476 lines
14 KiB
JavaScript
476 lines
14 KiB
JavaScript
// -*- 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 { RemoteSecuritySettings } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs"
|
|
);
|
|
const { TestUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/TestUtils.sys.mjs"
|
|
);
|
|
const { IntermediatePreloadsClient } = RemoteSecuritySettings.init();
|
|
|
|
let server;
|
|
|
|
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.setByteStringData(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 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,
|
|
Ci.nsIX509CertDB.verifyUsageTLSServer
|
|
);
|
|
});
|
|
|
|
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() {
|
|
Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
|
|
const invalidHash =
|
|
"6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d";
|
|
|
|
const result = await syncAndDownload(["int.pem"], {
|
|
hashFunc: () => invalidHash,
|
|
});
|
|
equal(result, "success", "Preloading update should have run");
|
|
|
|
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,
|
|
Ci.nsIX509CertDB.verifyUsageTLSServer
|
|
);
|
|
});
|
|
|
|
add_task(async function test_preload_invalid_length() {
|
|
Services.prefs.setBoolPref(INTERMEDIATES_ENABLED_PREF, true);
|
|
|
|
const result = await syncAndDownload(["int.pem"], {
|
|
lengthFunc: () => 42,
|
|
});
|
|
equal(result, "success", "Preloading update should have run");
|
|
|
|
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,
|
|
Ci.nsIX509CertDB.verifyUsageTLSServer
|
|
);
|
|
});
|
|
|
|
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,
|
|
Ci.nsIX509CertDB.verifyUsageTLSServer
|
|
);
|
|
await checkCertErrorGeneric(
|
|
certDB,
|
|
ee_cert_2,
|
|
SEC_ERROR_UNKNOWN_ISSUER,
|
|
Ci.nsIX509CertDB.verifyUsageTLSServer
|
|
);
|
|
|
|
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));
|
|
|
|
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);
|
|
|
|
await checkCertErrorGeneric(
|
|
certDB,
|
|
ee_cert,
|
|
PRErrorCodeSuccess,
|
|
Ci.nsIX509CertDB.verifyUsageTLSServer
|
|
);
|
|
|
|
let localDB = await IntermediatePreloadsClient.client.db;
|
|
let data = await localDB.list();
|
|
ok(!!data.length, "should have some entries");
|
|
// simulate a sync (syncAndDownload doesn't actually... sync.)
|
|
await IntermediatePreloadsClient.client.emit("sync", {
|
|
data: {
|
|
current: data,
|
|
created: data,
|
|
deleted: [],
|
|
updated: [],
|
|
},
|
|
});
|
|
|
|
// 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,
|
|
Ci.nsIX509CertDB.verifyUsageTLSServer
|
|
);
|
|
});
|
|
|
|
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]);
|
|
}
|
|
|
|
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"
|
|
);
|
|
|
|
// 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, "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"
|
|
);
|
|
});
|
|
|
|
add_task(async function test_bug1966632() {
|
|
let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
|
|
Ci.nsIX509CertDB
|
|
);
|
|
|
|
constructCertFromFile("test_intermediate_preloads/bug1966632-int1.pem", ",,");
|
|
await checkRootOfBuiltChain(
|
|
certDB,
|
|
constructCertFromFile("test_intermediate_preloads/bug1966632-ee.pem", ",,"),
|
|
"G/ANXI8TwJTdF+AFBM8IiIUPEv0Gf6H5LA/b9guG4yE=",
|
|
new Date("2025-05-21T00:00:00Z").getTime() / 1000,
|
|
undefined,
|
|
Ci.nsIX509CertDB.FLAG_LOCAL_ONLY
|
|
);
|
|
});
|
|
|
|
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();
|
|
}
|