summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/tests/unit/test_cert_blocklist.js
blob: 75688b58aa73c21a28bad81823ae0e0b51f82426 (plain)
1
2
3
4
5
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

// This test checks a number of things:
// * it ensures that data loaded from revocations.txt on startup is present
// * it ensures that certItems in blocklist.xml are persisted correctly
// * it ensures that items in the CertBlocklist are seen as revoked by the
//   cert verifier
// * it does a sanity check to ensure other cert verifier behavior is
//   unmodified

// First, we need to setup appInfo for the blocklist service to work
var id = "xpcshell@tests.mozilla.org";
var appName = "XPCShell";
var version = "1";
var platformVersion = "1.9.2";
Cu.import("resource://testing-common/AppInfo.jsm", this);
/*global updateAppInfo:false*/ // Imported via AppInfo.jsm.
updateAppInfo({
  name: appName,
  ID: id,
  version: version,
  platformVersion: platformVersion ? platformVersion : "1.0",
  crashReporter: true,
});

// we need to ensure we setup revocation data before certDB, or we'll start with
// no revocation.txt in the profile
var gProfile = do_get_profile();

// Write out an empty blocklist.xml file to the profile to ensure nothing
// is blocklisted by default
var blockFile = gProfile.clone();
blockFile.append("blocklist.xml");
var stream = Cc["@mozilla.org/network/file-output-stream;1"]
               .createInstance(Ci.nsIFileOutputStream);
stream.init(blockFile,
  FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
  FileUtils.PERMS_FILE, 0);

var data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
           "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">\n" +
           "</blocklist>\n";
stream.write(data, data.length);
stream.close();

const PREF_BLOCKLIST_UPDATE_ENABLED = "services.blocklist.update_enabled";
const PREF_ONECRL_VIA_AMO = "security.onecrl.via.amo";

var gRevocations = gProfile.clone();
gRevocations.append("revocations.txt");
if (!gRevocations.exists()) {
  let existing = do_get_file("test_onecrl/sample_revocations.txt", false);
  existing.copyTo(gProfile, "revocations.txt");
}

var certDB = Cc["@mozilla.org/security/x509certdb;1"]
               .getService(Ci.nsIX509CertDB);

// set up a test server to serve the blocklist.xml
var testserver = new HttpServer();

var initialBlocklist = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
    "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">" +
    // test with some bad data ...
    "<certItems><certItem issuerName='Some nonsense in issuer'>" +
    "<serialNumber>AkHVNA==</serialNumber>" +
    "</certItem><certItem issuerName='MA0xCzAJBgNVBAMMAmNh'>" +
    "<serialNumber>some nonsense in serial</serialNumber>" +
    "</certItem><certItem issuerName='some nonsense in both issuer'>" +
    "<serialNumber>and serial</serialNumber></certItem>" +
    // some mixed
    // In this case, the issuer name and the valid serialNumber correspond
    // to test-int.pem in bad_certs/
    "<certItem issuerName='MBIxEDAOBgNVBAMMB1Rlc3QgQ0E='>" +
    "<serialNumber>oops! more nonsense.</serialNumber>" +
    "<serialNumber>BVio/iQ21GCi2iUven8oJ/gae74=</serialNumber></certItem>" +
    // ... and some good
    // In this case, the issuer name and the valid serialNumber correspond
    // to other-test-ca.pem in bad_certs/ (for testing root revocation)
    "<certItem issuerName='MBgxFjAUBgNVBAMMDU90aGVyIHRlc3QgQ0E='>" +
    "<serialNumber>exJUIJpq50jgqOwQluhVrAzTF74=</serialNumber></certItem>" +
    // This item corresponds to an entry in sample_revocations.txt where:
    // isser name is "another imaginary issuer" base-64 encoded, and
    // serialNumbers are:
    // "serial2." base-64 encoded, and
    // "another serial." base-64 encoded
    // We need this to ensure that existing items are retained if they're
    // also in the blocklist
    "<certItem issuerName='YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy'>" +
    "<serialNumber>c2VyaWFsMi4=</serialNumber>" +
    "<serialNumber>YW5vdGhlciBzZXJpYWwu</serialNumber></certItem>" +
    // This item revokes same-issuer-ee.pem by subject and pubKeyHash.
    "<certItem subject='MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5'" +
    " pubKeyHash='VCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8='>" +
    "</certItem></certItems></blocklist>";

var updatedBlocklist = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
    "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">" +
    "<certItems>" +
    "<certItem issuerName='something new in both the issuer'>" +
    "<serialNumber>and the serial number</serialNumber></certItem>" +
    "</certItems></blocklist>";


var blocklists = {
  "/initialBlocklist/": initialBlocklist,
  "/updatedBlocklist/": updatedBlocklist
};

function serveResponse(request, response) {
  do_print("Serving for path " + request.path + "\n");
  response.write(blocklists[request.path]);
}

for (var path in blocklists) {
  testserver.registerPathHandler(path, serveResponse);
}

// start the test server
testserver.start(-1);
var port = testserver.identity.primaryPort;

// Setup the addonManager
var addonManager = Cc["@mozilla.org/addons/integration;1"]
                     .getService(Ci.nsIObserver)
                     .QueryInterface(Ci.nsITimerCallback);
addonManager.observe(null, "addons-startup", null);

var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
                  .createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";

function verify_cert(file, expectedError) {
  let ee = constructCertFromFile(file);
  checkCertErrorGeneric(certDB, ee, expectedError, certificateUsageSSLServer);
}

// The certificate blocklist currently only applies to TLS server certificates.
function verify_non_tls_usage_succeeds(file) {
  let ee = constructCertFromFile(file);
  checkCertErrorGeneric(certDB, ee, PRErrorCodeSuccess,
                        certificateUsageSSLClient);
  checkCertErrorGeneric(certDB, ee, PRErrorCodeSuccess,
                        certificateUsageEmailSigner);
  checkCertErrorGeneric(certDB, ee, PRErrorCodeSuccess,
                        certificateUsageEmailRecipient);
}

function load_cert(cert, trust) {
  let file = "bad_certs/" + cert + ".pem";
  addCertFromFile(certDB, file, trust);
}

function test_is_revoked(certList, issuerString, serialString, subjectString,
                         pubKeyString) {
  let issuer = converter.convertToByteArray(issuerString ? issuerString : '',
                                            {});

  let serial = converter.convertToByteArray(serialString ? serialString : '',
                                            {});

  let subject = converter.convertToByteArray(subjectString ? subjectString : '',
                                             {});

  let pubKey = converter.convertToByteArray(pubKeyString ? pubKeyString : '',
                                            {});

  return certList.isCertRevoked(issuer,
                                issuerString ? issuerString.length : 0,
                                serial,
                                serialString ? serialString.length : 0,
                                subject,
                                subjectString ? subjectString.length : 0,
                                pubKey,
                                pubKeyString ? pubKeyString.length : 0);
}

function fetch_blocklist(blocklistPath) {
  do_print("path is " + blocklistPath + "\n");
  let certblockObserver = {
    observe: function(aSubject, aTopic, aData) {
      Services.obs.removeObserver(this, "blocklist-updated");
      run_next_test();
    }
  };

  Services.obs.addObserver(certblockObserver, "blocklist-updated", false);
  Services.prefs.setCharPref("extensions.blocklist.url",
                              `http://localhost:${port}/${blocklistPath}`);
  let blocklist = Cc["@mozilla.org/extensions/blocklist;1"]
                    .getService(Ci.nsITimerCallback);
  blocklist.notify(null);
}

function check_revocations_txt_contents(expected) {
  let profile = do_get_profile();
  let revocations = profile.clone();
  revocations.append("revocations.txt");
  ok(revocations.exists(), "the revocations file should exist");
  let inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
                      .createInstance(Ci.nsIFileInputStream);
  inputStream.init(revocations, -1, -1, 0);
  inputStream.QueryInterface(Ci.nsILineInputStream);
  let contents = "";
  let hasmore = false;
  do {
    let line = {};
    hasmore = inputStream.readLine(line);
    contents += (contents.length == 0 ? "" : "\n") + line.value;
  } while (hasmore);
  equal(contents, expected, "revocations.txt should be as expected");
}

function run_test() {
  // import the certificates we need
  load_cert("test-ca", "CTu,CTu,CTu");
  load_cert("test-int", ",,");
  load_cert("other-test-ca", "CTu,CTu,CTu");

  let certList = Cc["@mozilla.org/security/certblocklist;1"]
                  .getService(Ci.nsICertBlocklist);

  let expected = "# Auto generated contents. Do not edit.\n" +
                 "MCIxIDAeBgNVBAMMF0Fub3RoZXIgVGVzdCBFbmQtZW50aXR5\n" +
                 "\tVCIlmPM9NkgFQtrs4Oa5TeFcDu6MWRTKSNdePEhOgD8=\n" +
                 "MBIxEDAOBgNVBAMMB1Rlc3QgQ0E=\n" +
                 " BVio/iQ21GCi2iUven8oJ/gae74=\n" +
                 "MBgxFjAUBgNVBAMMDU90aGVyIHRlc3QgQ0E=\n" +
                 " exJUIJpq50jgqOwQluhVrAzTF74=\n" +
                 "YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy\n" +
                 " YW5vdGhlciBzZXJpYWwu\n" +
                 " c2VyaWFsMi4=";

  // This test assumes OneCRL updates via AMO
  Services.prefs.setBoolPref(PREF_BLOCKLIST_UPDATE_ENABLED, false);
  Services.prefs.setBoolPref(PREF_ONECRL_VIA_AMO, true);

  add_test(function () {
    // check some existing items in revocations.txt are blocked. Since the
    // CertBlocklistItems don't know about the data they contain, we can use
    // arbitrary data (not necessarily DER) to test if items are revoked or not.
    // This test corresponds to:
    // issuer: c29tZSBpbWFnaW5hcnkgaXNzdWVy
    // serial: c2VyaWFsLg==
    ok(test_is_revoked(certList, "some imaginary issuer", "serial."),
      "issuer / serial pair should be blocked");

    // This test corresponds to:
    // issuer: YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy
    // serial: c2VyaWFsLg==
    ok(test_is_revoked(certList, "another imaginary issuer", "serial."),
      "issuer / serial pair should be blocked");

    // And this test corresponds to:
    // issuer: YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy
    // serial: c2VyaWFsMi4=
    // (we test this issuer twice to ensure we can read multiple serials)
    ok(test_is_revoked(certList, "another imaginary issuer", "serial2."),
      "issuer / serial pair should be blocked");

    // Soon we'll load a blocklist which revokes test-int.pem, which issued
    // test-int-ee.pem.
    // Check the cert validates before we load the blocklist
    let file = "test_onecrl/test-int-ee.pem";
    verify_cert(file, PRErrorCodeSuccess);

    // The blocklist also revokes other-test-ca.pem, which issued
    // other-ca-ee.pem. Check the cert validates before we load the blocklist
    file = "bad_certs/other-issuer-ee.pem";
    verify_cert(file, PRErrorCodeSuccess);

    // The blocklist will revoke same-issuer-ee.pem via subject / pubKeyHash.
    // Check the cert validates before we load the blocklist
    file = "test_onecrl/same-issuer-ee.pem";
    verify_cert(file, PRErrorCodeSuccess);

    run_next_test();
  });

  // blocklist load is async so we must use add_test from here
  add_test(function() {
    fetch_blocklist("initialBlocklist/");
  });

  add_test(function() {
    // The blocklist will be loaded now. Let's check the data is sane.
    // In particular, we should still have the revoked issuer / serial pair
    // that was in both revocations.txt and the blocklist.xml
    ok(test_is_revoked(certList, "another imaginary issuer", "serial2."),
      "issuer / serial pair should be blocked");

    // Check that both serials in the certItem with multiple serials were read
    // properly
    ok(test_is_revoked(certList, "another imaginary issuer", "serial2."),
       "issuer / serial pair should be blocked");
    ok(test_is_revoked(certList, "another imaginary issuer", "another serial."),
       "issuer / serial pair should be blocked");

    // test a subject / pubKey revocation
    ok(test_is_revoked(certList, "nonsense", "more nonsense",
       "some imaginary subject", "some imaginary pubkey"),
       "issuer / serial pair should be blocked");

    // Check the blocklist entry has been persisted properly to the backing
    // file
    check_revocations_txt_contents(expected);

    // Check the blocklisted intermediate now causes a failure
    let file = "test_onecrl/test-int-ee.pem";
    verify_cert(file, SEC_ERROR_REVOKED_CERTIFICATE);
    verify_non_tls_usage_succeeds(file);

    // Check the ee with the blocklisted root also causes a failure
    file = "bad_certs/other-issuer-ee.pem";
    verify_cert(file, SEC_ERROR_REVOKED_CERTIFICATE);
    verify_non_tls_usage_succeeds(file);

    // Check the ee blocked by subject / pubKey causes a failure
    file = "test_onecrl/same-issuer-ee.pem";
    verify_cert(file, SEC_ERROR_REVOKED_CERTIFICATE);
    verify_non_tls_usage_succeeds(file);

    // Check a non-blocklisted chain still validates OK
    file = "bad_certs/default-ee.pem";
    verify_cert(file, PRErrorCodeSuccess);

    // Check a bad cert is still bad (unknown issuer)
    file = "bad_certs/unknownissuer.pem";
    verify_cert(file, SEC_ERROR_UNKNOWN_ISSUER);

    // check that save with no further update is a no-op
    let lastModified = gRevocations.lastModifiedTime;
    // add an already existing entry
    certList.revokeCertByIssuerAndSerial("YW5vdGhlciBpbWFnaW5hcnkgaXNzdWVy",
                                         "c2VyaWFsMi4=");
    certList.saveEntries();
    let newModified = gRevocations.lastModifiedTime;
    equal(lastModified, newModified,
          "saveEntries with no modifications should not update the backing file");

    run_next_test();
  });

  // disable AMO cert blocklist - and check blocklist.xml changes do not
  // affect the data stored.
  add_test(function() {
    Services.prefs.setBoolPref("security.onecrl.via.amo", false);
    fetch_blocklist("updatedBlocklist/");
  });

  add_test(function() {
    // Check the blocklist entry has not changed
    check_revocations_txt_contents(expected);
    run_next_test();
  });

  run_next_test();
}