diff options
Diffstat (limited to 'security/manager/ssl/RemoteSecuritySettings.jsm')
-rw-r--r-- | security/manager/ssl/RemoteSecuritySettings.jsm | 850 |
1 files changed, 850 insertions, 0 deletions
diff --git a/security/manager/ssl/RemoteSecuritySettings.jsm b/security/manager/ssl/RemoteSecuritySettings.jsm new file mode 100644 index 0000000000..0beca14247 --- /dev/null +++ b/security/manager/ssl/RemoteSecuritySettings.jsm @@ -0,0 +1,850 @@ +/* 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"; + +const EXPORTED_SYMBOLS = ["RemoteSecuritySettings"]; + +const { RemoteSettings } = ChromeUtils.import( + "resource://services-settings/remote-settings.js" +); + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { X509 } = ChromeUtils.import( + "resource://gre/modules/psm/X509.jsm", + null +); + +const INTERMEDIATES_BUCKET_PREF = + "security.remote_settings.intermediates.bucket"; +const INTERMEDIATES_CHECKED_SECONDS_PREF = + "security.remote_settings.intermediates.checked"; +const INTERMEDIATES_COLLECTION_PREF = + "security.remote_settings.intermediates.collection"; +const INTERMEDIATES_DL_PER_POLL_PREF = + "security.remote_settings.intermediates.downloads_per_poll"; +const INTERMEDIATES_DL_PARALLEL_REQUESTS = + "security.remote_settings.intermediates.parallel_downloads"; +const INTERMEDIATES_ENABLED_PREF = + "security.remote_settings.intermediates.enabled"; +const INTERMEDIATES_SIGNER_PREF = + "security.remote_settings.intermediates.signer"; +const LOGLEVEL_PREF = "browser.policies.loglevel"; + +const INTERMEDIATES_ERRORS_TELEMETRY = "INTERMEDIATE_PRELOADING_ERRORS"; +const INTERMEDIATES_PENDING_TELEMETRY = + "security.intermediate_preloading_num_pending"; +const INTERMEDIATES_PRELOADED_TELEMETRY = + "security.intermediate_preloading_num_preloaded"; +const INTERMEDIATES_UPDATE_MS_TELEMETRY = + "INTERMEDIATE_PRELOADING_UPDATE_TIME_MS"; + +const ONECRL_BUCKET_PREF = "services.settings.security.onecrl.bucket"; +const ONECRL_COLLECTION_PREF = "services.settings.security.onecrl.collection"; +const ONECRL_SIGNER_PREF = "services.settings.security.onecrl.signer"; +const ONECRL_CHECKED_PREF = "services.settings.security.onecrl.checked"; + +const PINNING_ENABLED_PREF = "services.blocklist.pinning.enabled"; +const PINNING_BUCKET_PREF = "services.blocklist.pinning.bucket"; +const PINNING_COLLECTION_PREF = "services.blocklist.pinning.collection"; +const PINNING_CHECKED_SECONDS_PREF = "services.blocklist.pinning.checked"; +const PINNING_SIGNER_PREF = "services.blocklist.pinning.signer"; + +const CRLITE_FILTERS_BUCKET_PREF = + "security.remote_settings.crlite_filters.bucket"; +const CRLITE_FILTERS_CHECKED_SECONDS_PREF = + "security.remote_settings.crlite_filters.checked"; +const CRLITE_FILTERS_COLLECTION_PREF = + "security.remote_settings.crlite_filters.collection"; +const CRLITE_FILTERS_ENABLED_PREF = + "security.remote_settings.crlite_filters.enabled"; +const CRLITE_FILTERS_SIGNER_PREF = + "security.remote_settings.crlite_filters.signer"; + +XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]); + +XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder()); + +XPCOMUtils.defineLazyGetter(this, "log", () => { + let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm"); + return new ConsoleAPI({ + prefix: "RemoteSecuritySettings.jsm", + // tip: set maxLogLevel to "debug" and use log.debug() to create detailed + // messages during development. See LOG_LEVELS in Console.jsm for details. + maxLogLevel: "error", + maxLogLevelPref: LOGLEVEL_PREF, + }); +}); + +// Converts a JS string to an array of bytes consisting of the char code at each +// index in the string. +function stringToBytes(s) { + let b = []; + for (let i = 0; i < s.length; i++) { + b.push(s.charCodeAt(i)); + } + return b; +} + +// Converts an array of bytes to a JS string using fromCharCode on each byte. +function bytesToString(bytes) { + if (bytes.length > 65535) { + throw new Error("input too long for bytesToString"); + } + return String.fromCharCode.apply(null, bytes); +} + +class CRLiteState { + constructor(subject, spkiHash, state) { + this.subject = subject; + this.spkiHash = spkiHash; + this.state = state; + } +} +CRLiteState.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsICRLiteState", +]); + +class CertInfo { + constructor(cert, subject) { + this.cert = cert; + this.subject = subject; + this.trust = Ci.nsICertStorage.TRUST_INHERIT; + } +} +CertInfo.prototype.QueryInterface = ChromeUtils.generateQI(["nsICertInfo"]); + +class RevocationState { + constructor(state) { + this.state = state; + } +} + +class IssuerAndSerialRevocationState extends RevocationState { + constructor(issuer, serial, state) { + super(state); + this.issuer = issuer; + this.serial = serial; + } +} +IssuerAndSerialRevocationState.prototype.QueryInterface = ChromeUtils.generateQI( + ["nsIIssuerAndSerialRevocationState"] +); + +class SubjectAndPubKeyRevocationState extends RevocationState { + constructor(subject, pubKey, state) { + super(state); + this.subject = subject; + this.pubKey = pubKey; + } +} +SubjectAndPubKeyRevocationState.prototype.QueryInterface = ChromeUtils.generateQI( + ["nsISubjectAndPubKeyRevocationState"] +); + +function setRevocations(certStorage, revocations) { + return new Promise(resolve => + certStorage.setRevocations(revocations, resolve) + ); +} + +/** + * Helper function that returns a promise that will resolve with whether or not + * the nsICertStorage implementation has prior data of the given type. + * + * @param {Integer} dataType a Ci.nsICertStorage.DATA_TYPE_* constant + * indicating the type of data + + * @return {Promise} a promise that will resolve with true if the data type is + * present + */ +function hasPriorData(dataType) { + let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService( + Ci.nsICertStorage + ); + return new Promise(resolve => { + certStorage.hasPriorData(dataType, (rv, hasPriorData) => { + if (rv == Cr.NS_OK) { + resolve(hasPriorData); + } else { + // If calling hasPriorData failed, assume we need to reload everything + // (even though it's unlikely doing so will succeed). + resolve(false); + } + }); + }); +} + +/** + * Revoke the appropriate certificates based on the records from the blocklist. + * + * @param {Object} data Current records in the local db. + */ +const updateCertBlocklist = async function({ + data: { current, created, updated, deleted }, +}) { + let items = []; + + // See if we have prior revocation data (this can happen when we can't open + // the database and we have to re-create it (see bug 1546361)). + let hasPriorRevocationData = await hasPriorData( + Ci.nsICertStorage.DATA_TYPE_REVOCATION + ); + + // If we don't have prior data, make it so we re-load everything. + if (!hasPriorRevocationData) { + deleted = []; + updated = []; + created = current; + } + + for (let item of deleted) { + if (item.issuerName && item.serialNumber) { + items.push( + new IssuerAndSerialRevocationState( + item.issuerName, + item.serialNumber, + Ci.nsICertStorage.STATE_UNSET + ) + ); + } else if (item.subject && item.pubKeyHash) { + items.push( + new SubjectAndPubKeyRevocationState( + item.subject, + item.pubKeyHash, + Ci.nsICertStorage.STATE_UNSET + ) + ); + } + } + + const toAdd = created.concat(updated.map(u => u.new)); + + for (let item of toAdd) { + if (item.issuerName && item.serialNumber) { + items.push( + new IssuerAndSerialRevocationState( + item.issuerName, + item.serialNumber, + Ci.nsICertStorage.STATE_ENFORCE + ) + ); + } else if (item.subject && item.pubKeyHash) { + items.push( + new SubjectAndPubKeyRevocationState( + item.subject, + item.pubKeyHash, + Ci.nsICertStorage.STATE_ENFORCE + ) + ); + } + } + + try { + const certList = Cc["@mozilla.org/security/certstorage;1"].getService( + Ci.nsICertStorage + ); + await setRevocations(certList, items); + } catch (e) { + Cu.reportError(e); + } +}; + +/** + * Modify the appropriate security pins based on records from the remote + * collection. + * + * @param {Object} data Current records in the local db. + */ +async function updatePinningList({ data: { current: records } }) { + if (!Services.prefs.getBoolPref(PINNING_ENABLED_PREF)) { + return; + } + + const siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService + ); + + // clear the current preload list + siteSecurityService.clearPreloads(); + + // write each KeyPin entry to the preload list + for (let item of records) { + try { + const { pinType, versions } = item; + if (versions.includes(Services.appinfo.version) && pinType == "STSPin") { + siteSecurityService.setHSTSPreload( + item.hostName, + item.includeSubdomains, + item.expires + ); + } + } catch (e) { + // Prevent errors relating to individual preload entries from causing sync to fail. + Cu.reportError(e); + } + } +} + +var RemoteSecuritySettings = { + /** + * Initialize the clients (cheap instantiation) and setup their sync event. + * This static method is called from BrowserGlue.jsm soon after startup. + * + * @returns {Object} intantiated clients for security remote settings. + */ + init() { + const OneCRLBlocklistClient = RemoteSettings( + Services.prefs.getCharPref(ONECRL_COLLECTION_PREF), + { + bucketNamePref: ONECRL_BUCKET_PREF, + lastCheckTimePref: ONECRL_CHECKED_PREF, + signerName: Services.prefs.getCharPref(ONECRL_SIGNER_PREF), + } + ); + OneCRLBlocklistClient.on("sync", updateCertBlocklist); + + const PinningBlocklistClient = RemoteSettings( + Services.prefs.getCharPref(PINNING_COLLECTION_PREF), + { + bucketNamePref: PINNING_BUCKET_PREF, + lastCheckTimePref: PINNING_CHECKED_SECONDS_PREF, + signerName: Services.prefs.getCharPref(PINNING_SIGNER_PREF), + } + ); + PinningBlocklistClient.on("sync", updatePinningList); + + let IntermediatePreloadsClient = new IntermediatePreloads(); + let CRLiteFiltersClient = new CRLiteFilters(); + + this.OneCRLBlocklistClient = OneCRLBlocklistClient; + this.PinningBlocklistClient = PinningBlocklistClient; + this.IntermediatePreloadsClient = IntermediatePreloadsClient; + this.CRLiteFiltersClient = CRLiteFiltersClient; + + return { + OneCRLBlocklistClient, + PinningBlocklistClient, + IntermediatePreloadsClient, + CRLiteFiltersClient, + }; + }, +}; + +class IntermediatePreloads { + constructor() { + this.client = RemoteSettings( + Services.prefs.getCharPref(INTERMEDIATES_COLLECTION_PREF), + { + bucketNamePref: INTERMEDIATES_BUCKET_PREF, + lastCheckTimePref: INTERMEDIATES_CHECKED_SECONDS_PREF, + signerName: Services.prefs.getCharPref(INTERMEDIATES_SIGNER_PREF), + localFields: ["cert_import_complete"], + } + ); + + this.client.on("sync", this.onSync.bind(this)); + Services.obs.addObserver( + this.onObservePollEnd.bind(this), + "remote-settings:changes-poll-end" + ); + + log.debug("Intermediate Preloading: constructor"); + } + + async updatePreloadedIntermediates() { + if (!Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)) { + log.debug("Intermediate Preloading is disabled"); + Services.obs.notifyObservers( + null, + "remote-security-settings:intermediates-updated", + "disabled" + ); + return; + } + + // Download attachments that are awaiting download, up to a max. + const maxDownloadsPerRun = Services.prefs.getIntPref( + INTERMEDIATES_DL_PER_POLL_PREF, + 100 + ); + const parallelDownloads = Services.prefs.getIntPref( + INTERMEDIATES_DL_PARALLEL_REQUESTS, + 8 + ); + + // Bug 1519256: Move this to a separate method that's on a separate timer + // with a higher frequency (so we can attempt to download outstanding + // certs more than once daily) + + // See if we have prior cert data (this can happen when we can't open the database and we + // have to re-create it (see bug 1546361)). + let hasPriorCertData = await hasPriorData( + Ci.nsICertStorage.DATA_TYPE_CERTIFICATE + ); + // If we don't have prior data, make it so we re-load everything. + if (!hasPriorCertData) { + let current; + try { + current = await this.client.db.list(); + } catch (err) { + log.warn(`Unable to list intermediate preloading collection: ${err}`); + // Re-purpose the "failedToFetch" category to indicate listing the collection failed. + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("failedToFetch"); + return; + } + const toReset = current.filter(record => record.cert_import_complete); + try { + await this.client.db.importChanges( + undefined, // do not touch metadata. + undefined, // do not touch collection timestamp. + toReset.map(r => ({ ...r, cert_import_complete: false })) + ); + } catch (err) { + log.warn(`Unable to update intermediate preloading collection: ${err}`); + // Re-purpose the "unexpectedLength" category to indicate updating the collection failed. + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("unexpectedLength"); + return; + } + } + let current; + try { + current = await this.client.db.list(); + } catch (err) { + log.warn(`Unable to list intermediate preloading collection: ${err}`); + // Re-purpose the "failedToFetch" category to indicate listing the collection failed. + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("failedToFetch"); + return; + } + const waiting = current.filter(record => !record.cert_import_complete); + + log.debug(`There are ${waiting.length} intermediates awaiting download.`); + if (waiting.length == 0) { + // Nothing to do. + Services.obs.notifyObservers( + null, + "remote-security-settings:intermediates-updated", + "success" + ); + return; + } + + TelemetryStopwatch.start(INTERMEDIATES_UPDATE_MS_TELEMETRY); + + let toDownload = waiting.slice(0, maxDownloadsPerRun); + let recordsCertsAndSubjects = []; + for (let i = 0; i < toDownload.length; i += parallelDownloads) { + const chunk = toDownload.slice(i, i + parallelDownloads); + const downloaded = await Promise.all( + chunk.map(record => this.maybeDownloadAttachment(record)) + ); + recordsCertsAndSubjects = recordsCertsAndSubjects.concat(downloaded); + } + + let certInfos = []; + let recordsToUpdate = []; + for (let { record, cert, subject } of recordsCertsAndSubjects) { + if (cert && subject) { + certInfos.push(new CertInfo(cert, subject)); + recordsToUpdate.push(record); + } + } + const certStorage = Cc["@mozilla.org/security/certstorage;1"].getService( + Ci.nsICertStorage + ); + let result = await new Promise(resolve => { + certStorage.addCerts(certInfos, resolve); + }).catch(err => err); + if (result != Cr.NS_OK) { + Cu.reportError(`certStorage.addCerts failed: ${result}`); + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("failedToUpdateDB"); + return; + } + try { + await this.client.db.importChanges( + undefined, // do not touch metadata. + undefined, // do not touch collection timestamp. + recordsToUpdate.map(r => ({ ...r, cert_import_complete: true })) + ); + } catch (err) { + log.warn(`Unable to update intermediate preloading collection: ${err}`); + // Re-purpose the "unexpectedLength" category to indicate updating the collection failed. + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("unexpectedLength"); + return; + } + + let finalCurrent; + try { + finalCurrent = await this.client.db.list(); + } catch (err) { + log.warn(`Unable to list intermediate preloading collection: ${err}`); + // Re-purpose the "failedToFetch" category to indicate listing the collection failed. + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("failedToFetch"); + return; + } + const finalWaiting = finalCurrent.filter( + record => !record.cert_import_complete + ); + + const countPreloaded = finalCurrent.length - finalWaiting.length; + + TelemetryStopwatch.finish(INTERMEDIATES_UPDATE_MS_TELEMETRY); + Services.telemetry.scalarSet( + INTERMEDIATES_PRELOADED_TELEMETRY, + countPreloaded + ); + Services.telemetry.scalarSet( + INTERMEDIATES_PENDING_TELEMETRY, + finalWaiting.length + ); + + Services.obs.notifyObservers( + null, + "remote-security-settings:intermediates-updated", + "success" + ); + } + + async onObservePollEnd(subject, topic, data) { + log.debug(`onObservePollEnd ${subject} ${topic}`); + + try { + await this.updatePreloadedIntermediates(); + } catch (err) { + log.warn(`Unable to update intermediate preloads: ${err}`); + + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("failedToObserve"); + } + } + + // This method returns a promise to RemoteSettingsClient.maybeSync method. + async onSync({ data: { current, created, updated, deleted } }) { + if (!Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)) { + log.debug("Intermediate Preloading is disabled"); + return; + } + + log.debug(`Removing ${deleted.length} Intermediate certificates`); + await this.removeCerts(deleted); + let hasPriorCRLiteData = await hasPriorData( + Ci.nsICertStorage.DATA_TYPE_CRLITE + ); + if (!hasPriorCRLiteData) { + deleted = []; + updated = []; + created = current; + } + const toAdd = created.concat(updated.map(u => u.new)); + let entries = []; + for (let entry of deleted) { + entries.push( + new CRLiteState( + entry.subjectDN, + entry.pubKeyHash, + Ci.nsICertStorage.STATE_UNSET + ) + ); + } + for (let entry of toAdd) { + entries.push( + new CRLiteState( + entry.subjectDN, + entry.pubKeyHash, + entry.crlite_enrolled + ? Ci.nsICertStorage.STATE_ENFORCE + : Ci.nsICertStorage.STATE_UNSET + ) + ); + } + let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService( + Ci.nsICertStorage + ); + await new Promise(resolve => certStorage.setCRLiteState(entries, resolve)); + } + + /** + * Attempts to download the attachment, assuming it's not been processed + * already. Does not retry, and always resolves (e.g., does not reject upon + * failure.) Errors are reported via Cu.reportError. + * @param {AttachmentRecord} record defines which data to obtain + * @return {Promise} a Promise that will resolve to an object with the properties + * record, cert, and subject. record is the original record. + * cert is the base64-encoded bytes of the downloaded certificate (if + * downloading was successful), and null otherwise. + * subject is the base64-encoded bytes of the subject distinguished + * name of the same. + */ + async maybeDownloadAttachment(record) { + let result = { record, cert: null, subject: null }; + + let dataAsString = null; + try { + let buffer = await this.client.attachments.downloadAsBytes(record, { + retries: 0, + }); + dataAsString = gTextDecoder.decode(new Uint8Array(buffer)); + } catch (err) { + // Bug 1519273 - Log telemetry for these rejections + if (err.name == "BadContentError") { + log.debug(`Bad attachment content.`); + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("unexpectedHash"); + } else { + Cu.reportError(`Failed to download attachment: ${err}`); + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("failedToDownloadMisc"); + } + return result; + } + + let certBase64; + let subjectBase64; + try { + // split off the header and footer + certBase64 = dataAsString.split("-----")[2].replace(/\s/g, ""); + // get an array of bytes so we can use X509.jsm + let certBytes = stringToBytes(atob(certBase64)); + let cert = new X509.Certificate(); + cert.parse(certBytes); + // get the DER-encoded subject and get a base64-encoded string from it + // TODO(bug 1542028): add getters for _der and _bytes + subjectBase64 = btoa( + bytesToString(cert.tbsCertificate.subject._der._bytes) + ); + } catch (err) { + Cu.reportError(`Failed to decode cert: ${err}`); + + // Re-purpose the "failedToUpdateNSS" telemetry tag as "failed to + // decode preloaded intermediate certificate" + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("failedToUpdateNSS"); + + return result; + } + result.cert = certBase64; + result.subject = subjectBase64; + return result; + } + + async maybeSync(expectedTimestamp, options) { + return this.client.maybeSync(expectedTimestamp, options); + } + + async removeCerts(recordsToRemove) { + let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService( + Ci.nsICertStorage + ); + let hashes = recordsToRemove.map(record => record.derHash); + let result = await new Promise(resolve => { + certStorage.removeCertsByHashes(hashes, resolve); + }).catch(err => err); + if (result != Cr.NS_OK) { + Cu.reportError(`Failed to remove some intermediate certificates`); + Services.telemetry + .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY) + .add("failedToRemove"); + } + } +} + +// Helper function to compare filters. One filter is "less than" another filter (i.e. it sorts +// earlier) if its timestamp is farther in the past than the other. +function compareFilters(filterA, filterB) { + return filterA.effectiveTimestamp - filterB.effectiveTimestamp; +} + +class CRLiteFilters { + constructor() { + this.client = RemoteSettings( + Services.prefs.getCharPref(CRLITE_FILTERS_COLLECTION_PREF), + { + bucketNamePref: CRLITE_FILTERS_BUCKET_PREF, + lastCheckTimePref: CRLITE_FILTERS_CHECKED_SECONDS_PREF, + signerName: Services.prefs.getCharPref(CRLITE_FILTERS_SIGNER_PREF), + localFields: ["loaded_into_cert_storage"], + } + ); + + Services.obs.addObserver( + this.onObservePollEnd.bind(this), + "remote-settings:changes-poll-end" + ); + } + + async onObservePollEnd(subject, topic, data) { + if (!Services.prefs.getBoolPref(CRLITE_FILTERS_ENABLED_PREF, true)) { + log.debug("CRLite filter downloading is disabled"); + Services.obs.notifyObservers( + null, + "remote-security-settings:crlite-filters-downloaded", + "disabled" + ); + return; + } + + let hasPriorFilter = await hasPriorData( + Ci.nsICertStorage.DATA_TYPE_CRLITE_FILTER_FULL + ); + if (!hasPriorFilter) { + let current = await this.client.db.list(); + let toReset = current.filter( + record => !record.incremental && record.loaded_into_cert_storage + ); + await this.client.db.importChanges( + undefined, // do not touch metadata. + undefined, // do not touch collection timestamp. + toReset.map(r => ({ ...r, loaded_into_cert_storage: false })) + ); + } + let hasPriorStash = await hasPriorData( + Ci.nsICertStorage.DATA_TYPE_CRLITE_FILTER_INCREMENTAL + ); + if (!hasPriorStash) { + let current = await this.client.db.list(); + let toReset = current.filter( + record => record.incremental && record.loaded_into_cert_storage + ); + await this.client.db.importChanges( + undefined, // do not touch metadata. + undefined, // do not touch collection timestamp. + toReset.map(r => ({ ...r, loaded_into_cert_storage: false })) + ); + } + + let current = await this.client.db.list(); + let fullFilters = current.filter(filter => !filter.incremental); + if (fullFilters.length < 1) { + log.debug("no full CRLite filters to download?"); + Services.obs.notifyObservers( + null, + "remote-security-settings:crlite-filters-downloaded", + "unavailable" + ); + return; + } + fullFilters.sort(compareFilters); + log.debug("fullFilters:", fullFilters); + let fullFilter = fullFilters.pop(); // the most recent filter sorts last + let incrementalFilters = current.filter( + filter => + // Return incremental filters that are more recent than (i.e. sort later than) the full + // filter. + filter.incremental && compareFilters(filter, fullFilter) > 0 + ); + incrementalFilters.sort(compareFilters); + // Map of id to filter where that filter's parent has the given id. + let parentIdMap = {}; + for (let filter of incrementalFilters) { + if (filter.parent in parentIdMap) { + log.debug(`filter with parent id ${filter.parent} already seen?`); + } else { + parentIdMap[filter.parent] = filter; + } + } + let filtersToDownload = []; + let nextFilter = fullFilter; + while (nextFilter) { + filtersToDownload.push(nextFilter); + nextFilter = parentIdMap[nextFilter.id]; + } + const certList = Cc["@mozilla.org/security/certstorage;1"].getService( + Ci.nsICertStorage + ); + filtersToDownload = filtersToDownload.filter( + filter => !filter.loaded_into_cert_storage + ); + log.debug("filtersToDownload:", filtersToDownload); + let filtersDownloaded = []; + for (let filter of filtersToDownload) { + try { + // If we've already downloaded this, the backend should just grab it from its cache. + let localURI = await this.client.attachments.download(filter); + let buffer = await (await fetch(localURI)).arrayBuffer(); + let bytes = new Uint8Array(buffer); + log.debug(`Downloaded ${filter.details.name}: ${bytes.length} bytes`); + filter.bytes = bytes; + filtersDownloaded.push(filter); + } catch (e) { + log.debug(e); + Cu.reportError("failed to download CRLite filter", e); + } + } + let fullFiltersDownloaded = filtersDownloaded.filter( + filter => !filter.incremental + ); + if (fullFiltersDownloaded.length > 0) { + if (fullFiltersDownloaded.length > 1) { + log.warn("trying to install more than one full CRLite filter?"); + } + let filter = fullFiltersDownloaded[0]; + let timestamp = Math.floor(filter.effectiveTimestamp / 1000); + log.debug(`setting CRLite filter timestamp to ${timestamp}`); + await new Promise(resolve => { + certList.setFullCRLiteFilter(filter.bytes, timestamp, rv => { + log.debug(`setFullCRLiteFilter: ${rv}`); + resolve(); + }); + }); + } + let stashes = filtersDownloaded.filter(filter => filter.incremental); + let totalLength = stashes.reduce( + (sum, filter) => sum + filter.bytes.length, + 0 + ); + let concatenatedStashes = new Uint8Array(totalLength); + let offset = 0; + for (let filter of stashes) { + concatenatedStashes.set(filter.bytes, offset); + offset += filter.bytes.length; + } + if (concatenatedStashes.length > 0) { + log.debug( + `adding concatenated incremental updates of total length ${concatenatedStashes.length}` + ); + await new Promise(resolve => { + certList.addCRLiteStash(concatenatedStashes, rv => { + log.debug(`addCRLiteStash: ${rv}`); + resolve(); + }); + }); + } + + for (let filter of filtersDownloaded) { + delete filter.bytes; + } + + await this.client.db.importChanges( + undefined, // do not touch metadata. + undefined, // do not touch collection timestamp. + filtersDownloaded.map(r => ({ ...r, loaded_into_cert_storage: true })) + ); + + Services.obs.notifyObservers( + null, + "remote-security-settings:crlite-filters-downloaded", + `finished;${filtersDownloaded + .map(filter => filter.details.name) + .join(",")}` + ); + } +} |