Bug 1928997: Update tabs icon in Unified Search popup r=desktop-theme-reviewers,daleh...
[gecko.git] / security / manager / ssl / RemoteSecuritySettings.sys.mjs
blobe810c35ec0e41b4de6ec4eda300a30a5978ded46
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { RemoteSettings } from "resource://services-settings/remote-settings.sys.mjs";
7 import { X509 } from "resource://gre/modules/psm/X509.sys.mjs";
9 const SECURITY_STATE_BUCKET = "security-state";
10 const SECURITY_STATE_SIGNER = "onecrl.content-signature.mozilla.org";
12 const INTERMEDIATES_DL_PER_POLL_PREF =
13   "security.remote_settings.intermediates.downloads_per_poll";
14 const INTERMEDIATES_DL_PARALLEL_REQUESTS =
15   "security.remote_settings.intermediates.parallel_downloads";
16 const INTERMEDIATES_ENABLED_PREF =
17   "security.remote_settings.intermediates.enabled";
18 const LOGLEVEL_PREF = "browser.policies.loglevel";
20 const CRLITE_FILTERS_ENABLED_PREF =
21   "security.remote_settings.crlite_filters.enabled";
23 const CRLITE_FILTER_CHANNEL_PREF = "security.pki.crlite_channel";
25 const lazy = {};
27 ChromeUtils.defineLazyGetter(lazy, "gTextDecoder", () => new TextDecoder());
29 ChromeUtils.defineLazyGetter(lazy, "log", () => {
30   let { ConsoleAPI } = ChromeUtils.importESModule(
31     "resource://gre/modules/Console.sys.mjs"
32   );
33   return new ConsoleAPI({
34     prefix: "RemoteSecuritySettings",
35     // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
36     // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
37     maxLogLevel: "error",
38     maxLogLevelPref: LOGLEVEL_PREF,
39   });
40 });
42 // Converts a JS string to an array of bytes consisting of the char code at each
43 // index in the string.
44 function stringToBytes(s) {
45   let b = [];
46   for (let i = 0; i < s.length; i++) {
47     b.push(s.charCodeAt(i));
48   }
49   return b;
52 // Converts an array of bytes to a JS string using fromCharCode on each byte.
53 function bytesToString(bytes) {
54   if (bytes.length > 65535) {
55     throw new Error("input too long for bytesToString");
56   }
57   return String.fromCharCode.apply(null, bytes);
60 class CRLiteCoverage {
61   constructor(b64LogID, minTimestamp, maxTimestamp) {
62     this.b64LogID = b64LogID;
63     this.minTimestamp = minTimestamp;
64     this.maxTimestamp = maxTimestamp;
65   }
67 CRLiteCoverage.prototype.QueryInterface = ChromeUtils.generateQI([
68   "nsICRLiteCoverage",
69 ]);
71 class CertInfo {
72   constructor(cert, subject) {
73     this.cert = cert;
74     this.subject = subject;
75     this.trust = Ci.nsICertStorage.TRUST_INHERIT;
76   }
78 CertInfo.prototype.QueryInterface = ChromeUtils.generateQI(["nsICertInfo"]);
80 class RevocationState {
81   constructor(state) {
82     this.state = state;
83   }
86 class IssuerAndSerialRevocationState extends RevocationState {
87   constructor(issuer, serial, state) {
88     super(state);
89     this.issuer = issuer;
90     this.serial = serial;
91   }
93 IssuerAndSerialRevocationState.prototype.QueryInterface =
94   ChromeUtils.generateQI(["nsIIssuerAndSerialRevocationState"]);
96 class SubjectAndPubKeyRevocationState extends RevocationState {
97   constructor(subject, pubKey, state) {
98     super(state);
99     this.subject = subject;
100     this.pubKey = pubKey;
101   }
103 SubjectAndPubKeyRevocationState.prototype.QueryInterface =
104   ChromeUtils.generateQI(["nsISubjectAndPubKeyRevocationState"]);
106 function setRevocations(certStorage, revocations) {
107   return new Promise(resolve =>
108     certStorage.setRevocations(revocations, resolve)
109   );
113  * Helper function that returns a promise that will resolve with whether or not
114  * the nsICertStorage implementation has prior data of the given type.
116  * @param {Integer} dataType a Ci.nsICertStorage.DATA_TYPE_* constant
117  *                           indicating the type of data
119  * @returns {Promise} a promise that will resolve with true if the data type is
120  *                   present
121  */
122 function hasPriorData(dataType) {
123   let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
124     Ci.nsICertStorage
125   );
126   return new Promise(resolve => {
127     certStorage.hasPriorData(dataType, (rv, hasPriorData) => {
128       if (rv == Cr.NS_OK) {
129         resolve(hasPriorData);
130       } else {
131         // If calling hasPriorData failed, assume we need to reload everything
132         // (even though it's unlikely doing so will succeed).
133         resolve(false);
134       }
135     });
136   });
140  * Revoke the appropriate certificates based on the records from the blocklist.
142  * @param {object} options
143  * @param {object} options.data Current records in the local db.
144  * @param {Array} options.data.current
145  * @param {Array} options.data.created
146  * @param {Array} options.data.updated
147  * @param {Array} options.data.deleted
148  */
149 const updateCertBlocklist = async function ({
150   data: { current, created, updated, deleted },
151 }) {
152   let items = [];
154   // See if we have prior revocation data (this can happen when we can't open
155   // the database and we have to re-create it (see bug 1546361)).
156   let hasPriorRevocationData = await hasPriorData(
157     Ci.nsICertStorage.DATA_TYPE_REVOCATION
158   );
160   // If we don't have prior data, make it so we re-load everything.
161   if (!hasPriorRevocationData) {
162     deleted = [];
163     updated = [];
164     created = current;
165   }
167   let toDelete = deleted.concat(updated.map(u => u.old));
168   for (let item of toDelete) {
169     if (item.issuerName && item.serialNumber) {
170       items.push(
171         new IssuerAndSerialRevocationState(
172           item.issuerName,
173           item.serialNumber,
174           Ci.nsICertStorage.STATE_UNSET
175         )
176       );
177     } else if (item.subject && item.pubKeyHash) {
178       items.push(
179         new SubjectAndPubKeyRevocationState(
180           item.subject,
181           item.pubKeyHash,
182           Ci.nsICertStorage.STATE_UNSET
183         )
184       );
185     }
186   }
188   const toAdd = created.concat(updated.map(u => u.new));
190   for (let item of toAdd) {
191     if (item.issuerName && item.serialNumber) {
192       items.push(
193         new IssuerAndSerialRevocationState(
194           item.issuerName,
195           item.serialNumber,
196           Ci.nsICertStorage.STATE_ENFORCE
197         )
198       );
199     } else if (item.subject && item.pubKeyHash) {
200       items.push(
201         new SubjectAndPubKeyRevocationState(
202           item.subject,
203           item.pubKeyHash,
204           Ci.nsICertStorage.STATE_ENFORCE
205         )
206       );
207     }
208   }
210   try {
211     const certList = Cc["@mozilla.org/security/certstorage;1"].getService(
212       Ci.nsICertStorage
213     );
214     await setRevocations(certList, items);
215   } catch (e) {
216     lazy.log.error(e);
217   }
220 export var RemoteSecuritySettings = {
221   _initialized: false,
222   OneCRLBlocklistClient: null,
223   IntermediatePreloadsClient: null,
224   CRLiteFiltersClient: null,
226   /**
227    * Initialize the clients (cheap instantiation) and setup their sync event.
228    * This static method is called from BrowserGlue.sys.mjs soon after startup.
229    *
230    * @returns {object} instantiated clients for security remote settings.
231    */
232   init() {
233     // Avoid repeated initialization (work-around for bug 1730026).
234     if (this._initialized) {
235       return this;
236     }
237     this._initialized = true;
239     this.OneCRLBlocklistClient = RemoteSettings("onecrl", {
240       bucketName: SECURITY_STATE_BUCKET,
241       signerName: SECURITY_STATE_SIGNER,
242     });
243     this.OneCRLBlocklistClient.on("sync", updateCertBlocklist);
245     this.IntermediatePreloadsClient = new IntermediatePreloads();
247     this.CRLiteFiltersClient = new CRLiteFilters();
249     return this;
250   },
253 class IntermediatePreloads {
254   constructor() {
255     this.client = RemoteSettings("intermediates", {
256       bucketName: SECURITY_STATE_BUCKET,
257       signerName: SECURITY_STATE_SIGNER,
258       localFields: ["cert_import_complete"],
259     });
261     this.client.on("sync", this.onSync.bind(this));
262     Services.obs.addObserver(
263       this.onObservePollEnd.bind(this),
264       "remote-settings:changes-poll-end"
265     );
267     lazy.log.debug("Intermediate Preloading: constructor");
268   }
270   async updatePreloadedIntermediates() {
271     if (!Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)) {
272       lazy.log.debug("Intermediate Preloading is disabled");
273       Services.obs.notifyObservers(
274         null,
275         "remote-security-settings:intermediates-updated",
276         "disabled"
277       );
278       return;
279     }
281     // Download attachments that are awaiting download, up to a max.
282     const maxDownloadsPerRun = Services.prefs.getIntPref(
283       INTERMEDIATES_DL_PER_POLL_PREF,
284       100
285     );
286     const parallelDownloads = Services.prefs.getIntPref(
287       INTERMEDIATES_DL_PARALLEL_REQUESTS,
288       8
289     );
291     // Bug 1519256: Move this to a separate method that's on a separate timer
292     // with a higher frequency (so we can attempt to download outstanding
293     // certs more than once daily)
295     // See if we have prior cert data (this can happen when we can't open the database and we
296     // have to re-create it (see bug 1546361)).
297     let hasPriorCertData = await hasPriorData(
298       Ci.nsICertStorage.DATA_TYPE_CERTIFICATE
299     );
300     // If we don't have prior data, make it so we re-load everything.
301     if (!hasPriorCertData) {
302       let current;
303       try {
304         current = await this.client.db.list();
305       } catch (err) {
306         lazy.log.warn(
307           `Unable to list intermediate preloading collection: ${err}`
308         );
309         return;
310       }
311       const toReset = current.filter(record => record.cert_import_complete);
312       try {
313         await this.client.db.importChanges(
314           undefined, // do not touch metadata.
315           undefined, // do not touch collection timestamp.
316           toReset.map(r => ({ ...r, cert_import_complete: false }))
317         );
318       } catch (err) {
319         lazy.log.warn(
320           `Unable to update intermediate preloading collection: ${err}`
321         );
322         return;
323       }
324     }
326     try {
327       // fetches a bundle containing all attachments, download() is called further down to force a re-sync on hash mismatches for old data or if the bundle fails to download
328       await this.client.attachments.cacheAll();
329     } catch (err) {
330       lazy.log.warn(
331         `Error fetching/caching attachment bundle in intermediate preloading: ${err}`
332       );
333     }
335     let current;
336     try {
337       current = await this.client.db.list();
338     } catch (err) {
339       lazy.log.warn(
340         `Unable to list intermediate preloading collection: ${err}`
341       );
342       return;
343     }
344     const waiting = current.filter(record => !record.cert_import_complete);
346     lazy.log.debug(
347       `There are ${waiting.length} intermediates awaiting download.`
348     );
349     if (!waiting.length) {
350       // Nothing to do.
351       Services.obs.notifyObservers(
352         null,
353         "remote-security-settings:intermediates-updated",
354         "success"
355       );
356       return;
357     }
359     let toDownload = waiting.slice(0, maxDownloadsPerRun);
360     let recordsCertsAndSubjects = [];
361     for (let i = 0; i < toDownload.length; i += parallelDownloads) {
362       const chunk = toDownload.slice(i, i + parallelDownloads);
363       const downloaded = await Promise.all(
364         chunk.map(record => this.maybeDownloadAttachment(record))
365       );
366       recordsCertsAndSubjects = recordsCertsAndSubjects.concat(downloaded);
367     }
369     let certInfos = [];
370     let recordsToUpdate = [];
371     for (let { record, cert, subject } of recordsCertsAndSubjects) {
372       if (cert && subject) {
373         certInfos.push(new CertInfo(cert, subject));
374         recordsToUpdate.push(record);
375       }
376     }
377     const certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
378       Ci.nsICertStorage
379     );
380     let result = await new Promise(resolve => {
381       certStorage.addCerts(certInfos, resolve);
382     }).catch(err => err);
383     if (result != Cr.NS_OK) {
384       lazy.log.error(`certStorage.addCerts failed: ${result}`);
385       return;
386     }
387     try {
388       await this.client.db.importChanges(
389         undefined, // do not touch metadata.
390         undefined, // do not touch collection timestamp.
391         recordsToUpdate.map(r => ({ ...r, cert_import_complete: true }))
392       );
393     } catch (err) {
394       lazy.log.warn(
395         `Unable to update intermediate preloading collection: ${err}`
396       );
397       return;
398     }
400     Services.obs.notifyObservers(
401       null,
402       "remote-security-settings:intermediates-updated",
403       "success"
404     );
405   }
407   async onObservePollEnd(subject, topic) {
408     lazy.log.debug(`onObservePollEnd ${subject} ${topic}`);
410     try {
411       await this.updatePreloadedIntermediates();
412     } catch (err) {
413       lazy.log.warn(`Unable to update intermediate preloads: ${err}`);
414     }
415   }
417   // This method returns a promise to RemoteSettingsClient.maybeSync method.
418   async onSync({ data: { deleted } }) {
419     if (!Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)) {
420       lazy.log.debug("Intermediate Preloading is disabled");
421       return;
422     }
424     lazy.log.debug(`Removing ${deleted.length} Intermediate certificates`);
425     await this.removeCerts(deleted);
426   }
428   /**
429    * Attempts to download the attachment, assuming it's not been processed
430    * already. Does not retry, and always resolves (e.g., does not reject upon
431    * failure.) Errors are reported via console.error.
432    *
433    * @param  {AttachmentRecord} record defines which data to obtain
434    * @returns {Promise}          a Promise that will resolve to an object with the properties
435    *                            record, cert, and subject. record is the original record.
436    *                            cert is the base64-encoded bytes of the downloaded certificate (if
437    *                            downloading was successful), and null otherwise.
438    *                            subject is the base64-encoded bytes of the subject distinguished
439    *                            name of the same.
440    */
441   async maybeDownloadAttachment(record) {
442     let result = { record, cert: null, subject: null };
444     let dataAsString = null;
445     try {
446       let { buffer } = await this.client.attachments.download(record, {
447         retries: 0,
448         checkHash: true,
449       });
450       dataAsString = lazy.gTextDecoder.decode(new Uint8Array(buffer));
451     } catch (err) {
452       if (err.name == "BadContentError") {
453         lazy.log.debug(`Bad attachment content.`);
454       } else {
455         lazy.log.error(`Failed to download attachment: ${err}`);
456       }
457       return result;
458     }
460     let certBase64;
461     let subjectBase64;
462     try {
463       // split off the header and footer
464       certBase64 = dataAsString.split("-----")[2].replace(/\s/g, "");
465       // get an array of bytes so we can use X509.sys.mjs
466       let certBytes = stringToBytes(atob(certBase64));
467       let cert = new X509.Certificate();
468       cert.parse(certBytes);
469       // get the DER-encoded subject and get a base64-encoded string from it
470       // TODO(bug 1542028): add getters for _der and _bytes
471       subjectBase64 = btoa(
472         bytesToString(cert.tbsCertificate.subject._der._bytes)
473       );
474     } catch (err) {
475       lazy.log.error(`Failed to decode cert: ${err}`);
476       return result;
477     }
478     result.cert = certBase64;
479     result.subject = subjectBase64;
480     return result;
481   }
483   async maybeSync(expectedTimestamp, options) {
484     return this.client.maybeSync(expectedTimestamp, options);
485   }
487   async removeCerts(recordsToRemove) {
488     let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
489       Ci.nsICertStorage
490     );
491     let hashes = recordsToRemove.map(record => record.derHash);
492     let result = await new Promise(resolve => {
493       certStorage.removeCertsByHashes(hashes, resolve);
494     }).catch(err => err);
495     if (result != Cr.NS_OK) {
496       lazy.log.error(`Failed to remove some intermediate certificates`);
497     }
498   }
501 // Helper function to compare filters. One filter is "less than" another filter (i.e. it sorts
502 // earlier) if its timestamp is farther in the past than the other.
503 function compareFilters(filterA, filterB) {
504   return filterA.effectiveTimestamp - filterB.effectiveTimestamp;
507 class CRLiteFilters {
508   constructor() {
509     this.client = RemoteSettings("cert-revocations", {
510       bucketName: SECURITY_STATE_BUCKET,
511       signerName: SECURITY_STATE_SIGNER,
512       localFields: ["loaded_into_cert_storage"],
513     });
515     Services.obs.addObserver(
516       this.onObservePollEnd.bind(this),
517       "remote-settings:changes-poll-end"
518     );
519     Services.prefs.addObserver(CRLITE_FILTER_CHANNEL_PREF, this);
520   }
522   async observe(subject, topic, prefName) {
523     if (topic == "nsPref:changed" && prefName == CRLITE_FILTER_CHANNEL_PREF) {
524       // When the user changes from channel A to channel B, mark the records
525       // for channel A (and all other channels) with loaded_into_cert_storage =
526       // false. If we don't do this, then the user will fail to reinstall the
527       // channel A artifacts if they switch back to channel A.
528       let records = await this.client.db.list();
529       let newChannel = Services.prefs.getStringPref(
530         CRLITE_FILTER_CHANNEL_PREF,
531         "none"
532       );
533       let toReset = records.filter(record => record.channel != newChannel);
534       await this.client.db.importChanges(
535         undefined, // do not touch metadata.
536         undefined, // do not touch collection timestamp.
537         toReset.map(r => ({ ...r, loaded_into_cert_storage: false }))
538       );
539     }
540   }
542   async getFilteredRecords() {
543     let records = await this.client.db.list();
544     records = await this.client._filterEntries(records);
545     return records;
546   }
548   async onObservePollEnd() {
549     if (!Services.prefs.getBoolPref(CRLITE_FILTERS_ENABLED_PREF, true)) {
550       lazy.log.debug("CRLite filter downloading is disabled");
551       Services.obs.notifyObservers(
552         null,
553         "remote-security-settings:crlite-filters-downloaded",
554         "disabled"
555       );
556       return;
557     }
559     let hasPriorFilter = await hasPriorData(
560       Ci.nsICertStorage.DATA_TYPE_CRLITE_FILTER_FULL
561     );
562     if (!hasPriorFilter) {
563       let current = await this.getFilteredRecords();
564       let toReset = current.filter(
565         record => !record.incremental && record.loaded_into_cert_storage
566       );
567       await this.client.db.importChanges(
568         undefined, // do not touch metadata.
569         undefined, // do not touch collection timestamp.
570         toReset.map(r => ({ ...r, loaded_into_cert_storage: false }))
571       );
572     }
573     let hasPriorStash = await hasPriorData(
574       Ci.nsICertStorage.DATA_TYPE_CRLITE_FILTER_INCREMENTAL
575     );
576     if (!hasPriorStash) {
577       let current = await this.getFilteredRecords();
578       let toReset = current.filter(
579         record => record.incremental && record.loaded_into_cert_storage
580       );
581       await this.client.db.importChanges(
582         undefined, // do not touch metadata.
583         undefined, // do not touch collection timestamp.
584         toReset.map(r => ({ ...r, loaded_into_cert_storage: false }))
585       );
586     }
588     let current = await this.getFilteredRecords();
589     let fullFilters = current.filter(filter => !filter.incremental);
590     if (fullFilters.length < 1) {
591       lazy.log.debug("no full CRLite filters to download?");
592       Services.obs.notifyObservers(
593         null,
594         "remote-security-settings:crlite-filters-downloaded",
595         "unavailable"
596       );
597       return;
598     }
599     fullFilters.sort(compareFilters);
600     lazy.log.debug("fullFilters:", fullFilters);
601     let fullFilter = fullFilters.pop(); // the most recent filter sorts last
602     let incrementalFilters = current.filter(
603       filter =>
604         // Return incremental filters that are more recent than (i.e. sort later than) the full
605         // filter.
606         filter.incremental && compareFilters(filter, fullFilter) > 0
607     );
608     incrementalFilters.sort(compareFilters);
609     // Map of id to filter where that filter's parent has the given id.
610     let parentIdMap = {};
611     for (let filter of incrementalFilters) {
612       if (filter.parent in parentIdMap) {
613         lazy.log.debug(`filter with parent id ${filter.parent} already seen?`);
614       } else {
615         parentIdMap[filter.parent] = filter;
616       }
617     }
618     let filtersToDownload = [];
619     let nextFilter = fullFilter;
620     while (nextFilter) {
621       filtersToDownload.push(nextFilter);
622       nextFilter = parentIdMap[nextFilter.id];
623     }
624     const certList = Cc["@mozilla.org/security/certstorage;1"].getService(
625       Ci.nsICertStorage
626     );
627     filtersToDownload = filtersToDownload.filter(
628       filter => !filter.loaded_into_cert_storage
629     );
630     lazy.log.debug("filtersToDownload:", filtersToDownload);
631     let filtersDownloaded = [];
632     for (let filter of filtersToDownload) {
633       try {
634         let attachment = await this.client.attachments.downloadAsBytes(filter);
635         let bytes = new Uint8Array(attachment);
636         lazy.log.debug(
637           `Downloaded ${filter.details.name}: ${bytes.length} bytes`
638         );
639         filter.bytes = bytes;
640         filtersDownloaded.push(filter);
641       } catch (e) {
642         lazy.log.error("failed to download CRLite filter", e);
643       }
644     }
645     let fullFiltersDownloaded = filtersDownloaded.filter(
646       filter => !filter.incremental
647     );
648     if (fullFiltersDownloaded.length) {
649       if (fullFiltersDownloaded.length > 1) {
650         lazy.log.warn("trying to install more than one full CRLite filter?");
651       }
652       let filter = fullFiltersDownloaded[0];
654       let coverage = [];
655       if (filter.coverage) {
656         for (let entry of filter.coverage) {
657           coverage.push(
658             new CRLiteCoverage(
659               entry.logID,
660               entry.minTimestamp,
661               entry.maxTimestamp
662             )
663           );
664         }
665       }
666       let enrollment = filter.enrolledIssuers ? filter.enrolledIssuers : [];
668       await new Promise(resolve => {
669         certList.setFullCRLiteFilter(filter.bytes, enrollment, coverage, rv => {
670           lazy.log.debug(`setFullCRLiteFilter: ${rv}`);
671           resolve();
672         });
673       });
674     }
675     let stashes = filtersDownloaded.filter(
676       filter =>
677         filter.incremental && filter.attachment.filename.endsWith("stash")
678     );
679     let totalLength = stashes.reduce(
680       (sum, filter) => sum + filter.bytes.length,
681       0
682     );
683     let concatenatedStashes = new Uint8Array(totalLength);
684     let offset = 0;
685     for (let filter of stashes) {
686       concatenatedStashes.set(filter.bytes, offset);
687       offset += filter.bytes.length;
688     }
689     if (concatenatedStashes.length) {
690       lazy.log.debug(
691         `adding concatenated incremental updates of total length ${concatenatedStashes.length}`
692       );
693       await new Promise(resolve => {
694         certList.addCRLiteStash(concatenatedStashes, rv => {
695           lazy.log.debug(`addCRLiteStash: ${rv}`);
696           resolve();
697         });
698       });
699     }
700     let deltas = filtersDownloaded.filter(
701       filter =>
702         filter.incremental && filter.attachment.filename.endsWith("delta")
703     );
704     for (let filter of deltas) {
705       lazy.log.debug(`adding delta update of size ${filter.bytes.length}`);
706       await new Promise(resolve => {
707         certList.addCRLiteDelta(
708           filter.bytes,
709           filter.attachment.filename,
710           rv => {
711             lazy.log.debug(`addCRLiteDelta: ${rv}`);
712             resolve();
713           }
714         );
715       });
716     }
718     for (let filter of filtersDownloaded) {
719       delete filter.bytes;
720     }
722     await this.client.db.importChanges(
723       undefined, // do not touch metadata.
724       undefined, // do not touch collection timestamp.
725       filtersDownloaded.map(r => ({ ...r, loaded_into_cert_storage: true }))
726     );
728     Services.obs.notifyObservers(
729       null,
730       "remote-security-settings:crlite-filters-downloaded",
731       `finished;${filtersDownloaded
732         .map(filter => filter.details.name)
733         .join(",")}`
734     );
735   }