summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/tests/unit/test_content_signing.js
blob: 7e96ea2df6d7e78e5bafbcba3f9b6085c7989b08 (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
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
/* -*- 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";

// These tests ensure content signatures are working correctly.

const TEST_DATA_DIR = "test_content_signing/";

const ONECRL_NAME = "oneCRL-signer.mozilla.org";
const ABOUT_NEWTAB_NAME = "remotenewtab.content-signature.mozilla.org";
var VERIFICATION_HISTOGRAM = Services.telemetry.getHistogramById(
  "CONTENT_SIGNATURE_VERIFICATION_STATUS"
);
var ERROR_HISTOGRAM = Services.telemetry.getKeyedHistogramById(
  "CONTENT_SIGNATURE_VERIFICATION_ERRORS"
);

function getSignatureVerifier() {
  return Cc["@mozilla.org/security/contentsignatureverifier;1"].getService(
    Ci.nsIContentSignatureVerifier
  );
}

function getCertHash(name) {
  let cert = constructCertFromFile(`test_content_signing/${name}.pem`);
  return cert.sha256Fingerprint.replace(/:/g, "");
}

function loadChain(prefix, names) {
  let chain = [];
  for (let name of names) {
    let filename = `${prefix}_${name}.pem`;
    chain.push(readFile(do_get_file(filename)));
  }
  return chain;
}

function check_telemetry(expected_index, expected, expectedId) {
  for (let i = 0; i < 10; i++) {
    let expected_value = 0;
    if (i == expected_index) {
      expected_value = expected;
    }
    let errorSnapshot = ERROR_HISTOGRAM.snapshot();
    for (let k in errorSnapshot) {
      // We clear the histogram every time so there should be only this one
      // category.
      equal(k, expectedId);
      equal(errorSnapshot[k].values[i] || 0, expected_value);
    }
    equal(
      VERIFICATION_HISTOGRAM.snapshot().values[i] || 0,
      expected_value,
      "count " +
        i +
        ": " +
        VERIFICATION_HISTOGRAM.snapshot().values[i] +
        " expected " +
        expected_value
    );
  }
  VERIFICATION_HISTOGRAM.clear();
  ERROR_HISTOGRAM.clear();
}

add_task(async function run_test() {
  // set up some data
  const DATA = readFile(do_get_file(TEST_DATA_DIR + "test.txt"));
  const GOOD_SIGNATURE =
    "p384ecdsa=" +
    readFile(do_get_file(TEST_DATA_DIR + "test.txt.signature")).trim();

  const BAD_SIGNATURE =
    "p384ecdsa=WqRXFQ7tnlVufpg7A-ZavXvWd2Zln0o4woHBy26C2r" +
    "UWM4GJke4pE8ecHiXoi-7KnZXty6Pe3s4o3yAIyKDP9jUC52Ek1G" +
    "q25j_X703nP5rk5gM1qz5Fe-qCWakPPl6L";

  let remoteNewTabChain = loadChain(TEST_DATA_DIR + "content_signing", [
    "remote_newtab_ee",
    "int",
  ]);

  let oneCRLChain = loadChain(TEST_DATA_DIR + "content_signing", [
    "onecrl_ee",
    "int",
  ]);

  let oneCRLBadKeyChain = loadChain(TEST_DATA_DIR + "content_signing", [
    "onecrl_wrong_key_ee",
    "int",
  ]);

  let noSANChain = loadChain(TEST_DATA_DIR + "content_signing", [
    "onecrl_no_SAN_ee",
    "int",
  ]);

  let expiredOneCRLChain = loadChain(TEST_DATA_DIR + "content_signing", [
    "onecrl_ee_expired",
    "int",
  ]);

  let notValidYetOneCRLChain = loadChain(TEST_DATA_DIR + "content_signing", [
    "onecrl_ee_not_valid_yet",
    "int",
  ]);

  // Check signature verification works without throwing when using the wrong
  // root
  VERIFICATION_HISTOGRAM.clear();
  let chain1 = oneCRLChain.join("\n");
  let verifier = getSignatureVerifier();
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain1,
      ONECRL_NAME,
      Ci.nsIContentSignatureVerifier.ContentSignatureProdRoot
    )),
    "using the wrong root, signatures should fail to verify but not throw."
  );
  // Check for generic chain building error.
  check_telemetry(6, 1, getCertHash("content_signing_onecrl_ee"));

  // Check good signatures from good certificates with the correct SAN
  ok(
    await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain1,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    ),
    "A OneCRL signature should verify with the OneCRL chain"
  );
  let chain2 = remoteNewTabChain.join("\n");
  ok(
    await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain2,
      ABOUT_NEWTAB_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    ),
    "A newtab signature should verify with the newtab chain"
  );
  // Check for valid signature
  check_telemetry(0, 2, getCertHash("content_signing_remote_newtab_ee"));

  // Check a bad signature when a good chain is provided
  chain1 = oneCRLChain.join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      BAD_SIGNATURE,
      chain1,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A bad signature should not verify"
  );
  // Check for invalid signature
  check_telemetry(1, 1, getCertHash("content_signing_onecrl_ee"));

  // Check a good signature from cert with good SAN but a different key than the
  // one used to create the signature
  let badKeyChain = oneCRLBadKeyChain.join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      badKeyChain,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the signing key is wrong"
  );
  // Check for wrong key in cert.
  check_telemetry(9, 1, getCertHash("content_signing_onecrl_wrong_key_ee"));

  // Check a good signature from cert with good SAN but a different key than the
  // one used to create the signature (this time, an RSA key)
  let rsaKeyChain = oneCRLBadKeyChain.join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      rsaKeyChain,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the signing key is wrong (RSA)"
  );
  // Check for wrong key in cert.
  check_telemetry(9, 1, getCertHash("content_signing_onecrl_wrong_key_ee"));

  // Check a good signature from cert with good SAN but with no path to root
  let missingInt = [oneCRLChain[0], oneCRLChain[2]].join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      missingInt,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the chain is incomplete (missing int)"
  );
  // Check for generic chain building error.
  check_telemetry(6, 1, getCertHash("content_signing_onecrl_ee"));

  // Check good signatures from good certificates with the wrong SANs
  chain1 = oneCRLChain.join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain1,
      ABOUT_NEWTAB_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A OneCRL signature should not verify if we require the newtab SAN"
  );
  // Check for invalid EE cert.
  check_telemetry(7, 1, getCertHash("content_signing_onecrl_ee"));

  chain2 = remoteNewTabChain.join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain2,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A newtab signature should not verify if we require the OneCRL SAN"
  );
  // Check for invalid EE cert.
  check_telemetry(7, 1, getCertHash("content_signing_remote_newtab_ee"));

  // Check good signatures with good chains with some other invalid names
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain1,
      "",
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the SANs do not match an empty name"
  );
  // Check for invalid EE cert.
  check_telemetry(7, 1, getCertHash("content_signing_onecrl_ee"));

  // Test expired certificate.
  let chainExpired = expiredOneCRLChain.join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chainExpired,
      "",
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the signing certificate is expired"
  );
  // Check for expired cert.
  check_telemetry(4, 1, getCertHash("content_signing_onecrl_ee_expired"));

  // Test not valid yet certificate.
  let chainNotValidYet = notValidYetOneCRLChain.join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chainNotValidYet,
      "",
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the signing certificate is not valid yet"
  );
  // Check for not yet valid cert.
  check_telemetry(5, 1, getCertHash("content_signing_onecrl_ee_not_valid_yet"));

  let relatedName = "subdomain." + ONECRL_NAME;
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain1,
      relatedName,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the SANs do not match a related name"
  );

  let randomName =
    "\xb1\x9bU\x1c\xae\xaa3\x19H\xdb\xed\xa1\xa1\xe0\x81\xfb" +
    "\xb2\x8f\x1cP\xe5\x8b\x9c\xc2s\xd3\x1f\x8e\xbbN";
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain1,
      randomName,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the SANs do not match a random name"
  );

  // check good signatures with chains that have strange or missing SANs
  chain1 = noSANChain.join("\n");
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA,
      GOOD_SIGNATURE,
      chain1,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A signature should not verify if the SANs do not match a supplied name"
  );

  // Check malformed signature data
  chain1 = oneCRLChain.join("\n");
  let bad_signatures = [
    // wrong length
    "p384ecdsa=WqRXFQ7tnlVufpg7A-ZavXvWd2Zln0o4woHBy26C2rUWM4GJke4pE8ecHiXoi-" +
      "7KnZXty6Pe3s4o3yAIyKDP9jUC52Ek1Gq25j_X703nP5rk5gM1qz5Fe-qCWakPPl6L==",
    // incorrectly encoded
    "p384ecdsa='WqRXFQ7tnlVufpg7A-ZavXvWd2Zln0o4woHBy26C2rUWM4GJke4pE8ecHiXoi" +
      "-7KnZXty6Pe3s4o3yAIyKDP9jUC52Ek1Gq25j_X703nP5rk5gM1qz5Fe-qCWakPPl6L=",
    // missing directive
    "other_directive=WqRXFQ7tnlVufpg7A-ZavXvWd2Zln0o4woHBy26C2rUWM4GJke4pE8ec" +
      "HiXoi-7KnZXty6Pe3s4o3yAIyKDP9jUC52Ek1Gq25j_X703nP5rk5gM1qz5Fe-qCWakPPl6L",
    // actually sha256 with RSA
    "p384ecdsa=XS_jiQsS5qlzQyUKaA1nAnQn_OvxhvDfKybflB8Xe5gNH1wNmPGK1qN-jpeTfK" +
      "6ob3l3gCTXrsMnOXMeht0kPP3wLfVgXbuuO135pQnsv0c-ltRMWLe56Cm4S4Z6E7WWKLPWaj" +
      "jhAcG5dZxjffP9g7tuPP4lTUJztyc4d1z_zQZakEG7R0vN7P5_CaX9MiMzP4R7nC3H4Ba6yi" +
      "yjlGvsZwJ_C5zDQzWWs95czUbMzbDScEZ_7AWnidw91jZn-fUK3xLb6m-Zb_b4GAqZ-vnXIf" +
      "LpLB1Nzal42BQZn7i4rhAldYdcVvy7rOMlsTUb5Zz6vpVW9LCT9lMJ7Sq1xbU-0g==",
  ];
  for (let badSig of bad_signatures) {
    await Assert.rejects(
      verifier.asyncVerifyContentSignature(
        DATA,
        badSig,
        chain1,
        ONECRL_NAME,
        Ci.nsIX509CertDB.AppXPCShellRoot
      ),
      /NS_ERROR/,
      `Bad or malformed signature "${badSig}" should be rejected`
    );
  }

  // Check malformed and missing certificate chain data
  let chainSuffix = [oneCRLChain[1], oneCRLChain[2]].join("\n");
  let badChains = [
    // no data
    "",
    // completely wrong data
    "blah blah \n blah",
  ];

  let badSections = [
    // data that looks like PEM but isn't
    "-----BEGIN CERTIFICATE-----\nBSsPRlYp5+gaFMRIczwUzaioRfteCjr94xyz0g==\n",
    // data that will start to parse but won't base64decode
    "-----BEGIN CERTIFICATE-----\nnon-base64-stuff\n-----END CERTIFICATE-----",
    // data with garbage outside of PEM sections
    "this data is garbage\n-----BEGIN CERTIFICATE-----\nnon-base64-stuff\n" +
      "-----END CERTIFICATE-----",
  ];

  for (let badSection of badSections) {
    // ensure we test each bad section on its own...
    badChains.push(badSection);
    // ... and as part of a chain with good certificates
    badChains.push(badSection + "\n" + chainSuffix);
  }

  for (let badChain of badChains) {
    await Assert.rejects(
      verifier.asyncVerifyContentSignature(
        DATA,
        GOOD_SIGNATURE,
        badChain,
        ONECRL_NAME,
        Ci.nsIX509CertDB.AppXPCShellRoot
      ),
      /NS_ERROR/,
      `Bad chain data starting "${badChain.substring(0, 80)}" ` +
        "should be rejected"
    );
  }

  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA + "appended data",
      GOOD_SIGNATURE,
      chain1,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A good signature should not verify if the data is tampered with (append)"
  );
  ok(
    !(await verifier.asyncVerifyContentSignature(
      "prefixed data" + DATA,
      GOOD_SIGNATURE,
      chain1,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A good signature should not verify if the data is tampered with (prefix)"
  );
  ok(
    !(await verifier.asyncVerifyContentSignature(
      DATA.replace(/e/g, "i"),
      GOOD_SIGNATURE,
      chain1,
      ONECRL_NAME,
      Ci.nsIX509CertDB.AppXPCShellRoot
    )),
    "A good signature should not verify if the data is tampered with (modify)"
  );
});