RemoteSecuritySettings.jsm 27 KB
Newer Older
1
2
3
4
5
/* 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";

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const EXPORTED_SYMBOLS = ["RemoteSecuritySettings"];

const { RemoteSettings } = ChromeUtils.import(
  "resource://services-settings/remote-settings.js"
);

const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);
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";

40
41
42
43
44
45
46
47
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";

48
const ONECRL_BUCKET_PREF = "services.settings.security.onecrl.bucket";
49
const ONECRL_COLLECTION_PREF = "services.settings.security.onecrl.collection";
50
51
const ONECRL_SIGNER_PREF = "services.settings.security.onecrl.signer";
const ONECRL_CHECKED_PREF = "services.settings.security.onecrl.checked";
52

53
54
55
const PINNING_ENABLED_PREF = "services.blocklist.pinning.enabled";
const PINNING_BUCKET_PREF = "services.blocklist.pinning.bucket";
const PINNING_COLLECTION_PREF = "services.blocklist.pinning.collection";
56
const PINNING_CHECKED_SECONDS_PREF = "services.blocklist.pinning.checked";
57
const PINNING_SIGNER_PREF = "services.blocklist.pinning.signer";
58

59
60
61
62
63
64
65
66
67
68
69
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";

70
71
72
73
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);

XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());

74
75
76
77
78
79
80
81
82
83
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,
  });
});
84

85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// 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);
101
102
}

103
104
105
106
107
108
109
class CRLiteState {
  constructor(subject, spkiHash, state) {
    this.subject = subject;
    this.spkiHash = spkiHash;
    this.state = state;
  }
}
110
CRLiteState.prototype.QueryInterface = ChromeUtils.generateQI([
111
  "nsICRLiteState",
112
]);
113

114
115
116
117
118
119
120
class CertInfo {
  constructor(cert, subject) {
    this.cert = cert;
    this.subject = subject;
    this.trust = Ci.nsICertStorage.TRUST_INHERIT;
  }
}
121
CertInfo.prototype.QueryInterface = ChromeUtils.generateQI(["nsICertInfo"]);
122

123
124
125
126
127
class RevocationState {
  constructor(state) {
    this.state = state;
  }
}
128

129
130
131
132
133
134
135
class IssuerAndSerialRevocationState extends RevocationState {
  constructor(issuer, serial, state) {
    super(state);
    this.issuer = issuer;
    this.serial = serial;
  }
}
136
IssuerAndSerialRevocationState.prototype.QueryInterface = ChromeUtils.generateQI(
137
  ["nsIIssuerAndSerialRevocationState"]
138
);
139

140
141
142
143
144
145
146
class SubjectAndPubKeyRevocationState extends RevocationState {
  constructor(subject, pubKey, state) {
    super(state);
    this.subject = subject;
    this.pubKey = pubKey;
  }
}
147
SubjectAndPubKeyRevocationState.prototype.QueryInterface = ChromeUtils.generateQI(
148
  ["nsISubjectAndPubKeyRevocationState"]
149
);
150
151

function setRevocations(certStorage, revocations) {
152
  return new Promise(resolve =>
153
154
155
156
157
158
159
160
161
    certStorage.setRevocations(revocations, resolve)
  );
}

/**
 * Revoke the appropriate certificates based on the records from the blocklist.
 *
 * @param {Object} data   Current records in the local db.
 */
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
const updateCertBlocklist = AppConstants.MOZ_NEW_CERT_STORAGE
  ? async function({ data: { current, created, updated, deleted } }) {
      const certList = Cc["@mozilla.org/security/certstorage;1"].getService(
        Ci.nsICertStorage
      );
      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 new Promise(resolve => {
        certList.hasPriorData(
          Ci.nsICertStorage.DATA_TYPE_REVOCATION,
          (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);
            }
          }
        );
184
      });
185

186
187
188
189
190
      // If we don't have prior data, make it so we re-load everything.
      if (!hasPriorRevocationData) {
        deleted = [];
        updated = [];
        created = current;
191
      }
192

193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
      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
            )
          );
        }
211
      }
212

213
214
215
      const toAdd = created.concat(updated.map(u => u.new));

      for (let item of toAdd) {
216
        if (item.issuerName && item.serialNumber) {
217
218
219
220
221
222
223
          items.push(
            new IssuerAndSerialRevocationState(
              item.issuerName,
              item.serialNumber,
              Ci.nsICertStorage.STATE_ENFORCE
            )
          );
224
        } else if (item.subject && item.pubKeyHash) {
225
226
227
228
229
230
231
          items.push(
            new SubjectAndPubKeyRevocationState(
              item.subject,
              item.pubKeyHash,
              Ci.nsICertStorage.STATE_ENFORCE
            )
          );
232
        }
233
234
235
236
      }

      try {
        await setRevocations(certList, items);
237
238
239
240
      } catch (e) {
        Cu.reportError(e);
      }
    }
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
  : async function({ data: { current: records } }) {
      const certList = Cc["@mozilla.org/security/certblocklist;1"].getService(
        Ci.nsICertBlocklist
      );
      for (let item of records) {
        try {
          if (item.issuerName && item.serialNumber) {
            certList.revokeCertByIssuerAndSerial(
              item.issuerName,
              item.serialNumber
            );
          } else if (item.subject && item.pubKeyHash) {
            certList.revokeCertBySubjectAndPubKey(
              item.subject,
              item.pubKeyHash
            );
          }
        } catch (e) {
          // Prevent errors relating to individual blocklist entries from causing sync to fail.
          Cu.reportError(e);
        }
      }
      certList.saveEntries();
    };
265
266
267
268
269
270
271
272
273
274
275
276

/**
 * 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;
  }

277
278
279
  const siteSecurityService = Cc["@mozilla.org/ssservice;1"].getService(
    Ci.nsISiteSecurityService
  );
280
281
282
283
284
285
286

  // clear the current preload list
  siteSecurityService.clearPreloads();

  // write each KeyPin entry to the preload list
  for (let item of records) {
    try {
287
288
289
290
291
292
293
      const { pinType, versions } = item;
      if (versions.includes(Services.appinfo.version) && pinType == "STSPin") {
        siteSecurityService.setHSTSPreload(
          item.hostName,
          item.includeSubdomains,
          item.expires
        );
294
295
      }
    } catch (e) {
296
      // Prevent errors relating to individual preload entries from causing sync to fail.
297
      Cu.reportError(e);
298
    }
299
300
  }
}
301

302
303
304
305
306
307
308
309
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() {
310
311
312
313
314
315
316
317
    const OneCRLBlocklistClient = RemoteSettings(
      Services.prefs.getCharPref(ONECRL_COLLECTION_PREF),
      {
        bucketNamePref: ONECRL_BUCKET_PREF,
        lastCheckTimePref: ONECRL_CHECKED_PREF,
        signerName: Services.prefs.getCharPref(ONECRL_SIGNER_PREF),
      }
    );
318
319
    OneCRLBlocklistClient.on("sync", updateCertBlocklist);

320
321
322
323
324
325
326
327
    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),
      }
    );
328
329
330
    PinningBlocklistClient.on("sync", updatePinningList);

    let IntermediatePreloadsClient;
331
    let CRLiteFiltersClient;
332
333
    if (AppConstants.MOZ_NEW_CERT_STORAGE) {
      IntermediatePreloadsClient = new IntermediatePreloads();
334
      CRLiteFiltersClient = new CRLiteFilters();
335
    }
336

337
338
339
340
341
    this.OneCRLBlocklistClient = OneCRLBlocklistClient;
    this.PinningBlocklistClient = PinningBlocklistClient;
    this.IntermediatePreloadsClient = IntermediatePreloadsClient;
    this.CRLiteFiltersClient = CRLiteFiltersClient;

342
343
344
345
    return {
      OneCRLBlocklistClient,
      PinningBlocklistClient,
      IntermediatePreloadsClient,
346
      CRLiteFiltersClient,
347
348
349
    };
  },
};
350

351
352
class IntermediatePreloads {
  constructor() {
353
354
355
356
357
358
359
360
361
362
    this.maybeInit();
  }

  maybeInit() {
    if (
      this.client ||
      !Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)
    ) {
      return;
    }
363
364
365
366
367
368
369
370
371
    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"],
      }
    );
372
373

    this.client.on("sync", this.onSync.bind(this));
374
375
376
377
    Services.obs.addObserver(
      this.onObservePollEnd.bind(this),
      "remote-settings:changes-poll-end"
    );
378
379
380
381
382
383
384

    log.debug("Intermediate Preloading: constructor");
  }

  async updatePreloadedIntermediates() {
    if (!Services.prefs.getBoolPref(INTERMEDIATES_ENABLED_PREF, true)) {
      log.debug("Intermediate Preloading is disabled");
385
386
387
388
389
      Services.obs.notifyObservers(
        null,
        "remote-security-settings:intermediates-updated",
        "disabled"
      );
390
      return;
391
    }
392
    this.maybeInit();
393

394
    // Download attachments that are awaiting download, up to a max.
395
396
397
398
399
400
401
402
    const maxDownloadsPerRun = Services.prefs.getIntPref(
      INTERMEDIATES_DL_PER_POLL_PREF,
      100
    );
    const parallelDownloads = Services.prefs.getIntPref(
      INTERMEDIATES_DL_PARALLEL_REQUESTS,
      8
    );
403
404
405
406
407
408
409

    // 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)).
410
411
412
413
414
415
416
417
418
419
420
421
422
423
    const certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
      Ci.nsICertStorage
    );
    let hasPriorCertData = await new Promise(resolve => {
      certStorage.hasPriorData(
        Ci.nsICertStorage.DATA_TYPE_CERTIFICATE,
        (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);
          }
424
        }
425
      );
426
427
428
    });
    // If we don't have prior data, make it so we re-load everything.
    if (!hasPriorCertData) {
429
430
      let current;
      try {
431
        current = await this.client.db.list();
432
433
434
435
436
437
438
439
      } 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;
      }
440
      const toReset = current.filter(record => record.cert_import_complete);
441
      try {
442
443
444
        await this.client.db.importChanges(
          undefined, // do not touch metadata.
          undefined, // do not touch collection timestamp.
445
446
          toReset.map(r => ({ ...r, cert_import_complete: false }))
        );
447
448
449
450
451
452
453
454
455
456
457
      } 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 {
458
      current = await this.client.db.list();
459
460
461
462
463
464
465
    } 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;
466
467
468
469
    }
    const waiting = current.filter(record => !record.cert_import_complete);

    log.debug(`There are ${waiting.length} intermediates awaiting download.`);
470
471
    if (waiting.length == 0) {
      // Nothing to do.
472
473
474
475
476
      Services.obs.notifyObservers(
        null,
        "remote-security-settings:intermediates-updated",
        "success"
      );
477
478
      return;
    }
479

480
481
    TelemetryStopwatch.start(INTERMEDIATES_UPDATE_MS_TELEMETRY);

482
    let toDownload = waiting.slice(0, maxDownloadsPerRun);
483
484
485
    let recordsCertsAndSubjects = [];
    for (let i = 0; i < toDownload.length; i += parallelDownloads) {
      const chunk = toDownload.slice(i, i + parallelDownloads);
486
487
488
      const downloaded = await Promise.all(
        chunk.map(record => this.maybeDownloadAttachment(record))
      );
489
490
491
      recordsCertsAndSubjects = recordsCertsAndSubjects.concat(downloaded);
    }

492
493
    let certInfos = [];
    let recordsToUpdate = [];
494
    for (let { record, cert, subject } of recordsCertsAndSubjects) {
495
496
497
498
499
      if (cert && subject) {
        certInfos.push(new CertInfo(cert, subject));
        recordsToUpdate.push(record);
      }
    }
500
    let result = await new Promise(resolve => {
501
      certStorage.addCerts(certInfos, resolve);
502
    }).catch(err => err);
503
504
    if (result != Cr.NS_OK) {
      Cu.reportError(`certStorage.addCerts failed: ${result}`);
505
506
507
      Services.telemetry
        .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY)
        .add("failedToUpdateDB");
508
509
      return;
    }
510
    try {
511
512
513
      await this.client.db.importChanges(
        undefined, // do not touch metadata.
        undefined, // do not touch collection timestamp.
514
515
        recordsToUpdate.map(r => ({ ...r, cert_import_complete: true }))
      );
516
517
518
519
520
521
522
523
    } 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;
    }
524

525
526
    let finalCurrent;
    try {
527
      finalCurrent = await this.client.db.list();
528
529
530
531
532
533
534
535
    } 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;
    }
536
537
538
    const finalWaiting = finalCurrent.filter(
      record => !record.cert_import_complete
    );
539

540
541
542
543
544
545
546
547
548
549
550
551
    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
    );

552
553
554
555
556
    Services.obs.notifyObservers(
      null,
      "remote-security-settings:intermediates-updated",
      "success"
    );
557
  }
558

559
560
561
562
563
564
565
  async onObservePollEnd(subject, topic, data) {
    log.debug(`onObservePollEnd ${subject} ${topic}`);

    try {
      await this.updatePreloadedIntermediates();
    } catch (err) {
      log.warn(`Unable to update intermediate preloads: ${err}`);
566
567
568
569

      Services.telemetry
        .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY)
        .add("failedToObserve");
570
    }
571
  }
572

573
574
575
576
577
  // 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;
578
579
    }

580
581
    log.debug(`Removing ${deleted.length} Intermediate certificates`);
    await this.removeCerts(deleted);
582
583
584
585
586
587
588
589
590
591
592
593
    let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
      Ci.nsICertStorage
    );
    let hasPriorCRLiteData = await new Promise(resolve => {
      certStorage.hasPriorData(
        Ci.nsICertStorage.DATA_TYPE_CRLITE,
        (rv, hasPriorData) => {
          if (rv == Cr.NS_OK) {
            resolve(hasPriorData);
          } else {
            resolve(false);
          }
594
        }
595
      );
596
597
598
599
600
601
602
603
604
    });
    if (!hasPriorCRLiteData) {
      deleted = [];
      updated = [];
      created = current;
    }
    const toAdd = created.concat(updated.map(u => u.new));
    let entries = [];
    for (let entry of deleted) {
605
606
607
608
609
610
611
      entries.push(
        new CRLiteState(
          entry.subjectDN,
          entry.pubKeyHash,
          Ci.nsICertStorage.STATE_UNSET
        )
      );
612
613
    }
    for (let entry of toAdd) {
614
615
616
617
618
619
620
621
622
      entries.push(
        new CRLiteState(
          entry.subjectDN,
          entry.pubKeyHash,
          entry.crlite_enrolled
            ? Ci.nsICertStorage.STATE_ENFORCE
            : Ci.nsICertStorage.STATE_UNSET
        )
      );
623
    }
624
    await new Promise(resolve => certStorage.setCRLiteState(entries, resolve));
625
  }
626

627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
  /**
   * 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 };

642
    let dataAsString = null;
643
    try {
644
645
646
647
      let buffer = await this.client.attachments.downloadAsBytes(record, {
        retries: 0,
      });
      dataAsString = gTextDecoder.decode(new Uint8Array(buffer));
648
649
    } catch (err) {
      // Bug 1519273 - Log telemetry for these rejections
650
651
652
653
654
655
656
657
658
659
660
      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");
      }
661
      return result;
662
    }
663
664
665
666
667
668
669
670
671
672
673
674

    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
675
676
677
      subjectBase64 = btoa(
        bytesToString(cert.tbsCertificate.subject._der._bytes)
      );
678
679
    } catch (err) {
      Cu.reportError(`Failed to decode cert: ${err}`);
680
681
682
683
684
685
686

      // Re-purpose the "failedToUpdateNSS" telemetry tag as "failed to
      // decode preloaded intermediate certificate"
      Services.telemetry
        .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY)
        .add("failedToUpdateNSS");

687
      return result;
688
    }
689
690
691
692
    result.cert = certBase64;
    result.subject = subjectBase64;
    return result;
  }
693

694
695
696
697
698
  async maybeSync(expectedTimestamp, options) {
    return this.client.maybeSync(expectedTimestamp, options);
  }

  async removeCerts(recordsToRemove) {
699
700
701
    let certStorage = Cc["@mozilla.org/security/certstorage;1"].getService(
      Ci.nsICertStorage
    );
702
    let hashes = recordsToRemove.map(record => record.derHash);
703
704
705
    let result = await new Promise(resolve => {
      certStorage.removeCertsByHashes(hashes, resolve);
    }).catch(err => err);
706
707
    if (result != Cr.NS_OK) {
      Cu.reportError(`Failed to remove some intermediate certificates`);
708
709
710
      Services.telemetry
        .getHistogramById(INTERMEDIATES_ERRORS_TELEMETRY)
        .add("failedToRemove");
711
    }
712
713
  }
}
714
715

// Helper function to compare filters. One filter is "less than" another filter (i.e. it sorts
716
// earlier) if its timestamp is farther in the past than the other.
717
function compareFilters(filterA, filterB) {
718
  return filterA.effectiveTimestamp - filterB.effectiveTimestamp;
719
720
721
722
}

class CRLiteFilters {
  constructor() {
723
724
725
726
727
728
729
730
731
732
    this.maybeInit();
  }

  maybeInit() {
    if (
      this.client ||
      !Services.prefs.getBoolPref(CRLITE_FILTERS_ENABLED_PREF, true)
    ) {
      return;
    }
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
    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),
      }
    );

    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;
    }
758
    this.maybeInit();
759
    let current = await this.client.db.list();
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
    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);
    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);
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
    // 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];
    }
795
    let filtersDownloaded = [];
796
797
798
    const certList = Cc["@mozilla.org/security/certstorage;1"].getService(
      Ci.nsICertStorage
    );
799
    for (let filter of filtersToDownload) {
800
801
802
803
804
805
806
      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`);
        filtersDownloaded.push(filter.details.name);
807
808
        if (!filter.incremental) {
          let timestamp = Math.floor(filter.effectiveTimestamp / 1000);
809
810
811
812
813
814
815
816
          log.debug(`setting CRLite filter timestamp to ${timestamp}`);
          await new Promise(resolve => {
            certList.setFullCRLiteFilter(bytes, timestamp, rv => {
              log.debug(`setFullCRLiteFilter: ${rv}`);
              resolve();
            });
          });
        } else {
817
818
819
820
821
822
823
          log.debug("adding incremental update");
          await new Promise(resolve => {
            certList.addCRLiteStash(bytes, rv => {
              log.debug(`addCRLiteStash: ${rv}`);
              resolve();
            });
          });
824
        }
825
      } catch (e) {
826
        log.debug(e);
827
828
829
830
831
832
833
834
835
836
        Cu.reportError("failed to download CRLite filter", e);
      }
    }
    Services.obs.notifyObservers(
      null,
      "remote-security-settings:crlite-filters-downloaded",
      `finished;${filtersDownloaded.join(",")}`
    );
  }
}