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 }