summaryrefslogtreecommitdiffstats
path: root/security/manager/ssl/tests/unit/test_content_signing.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /security/manager/ssl/tests/unit/test_content_signing.js
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/manager/ssl/tests/unit/test_content_signing.js')
-rw-r--r--security/manager/ssl/tests/unit/test_content_signing.js431
1 files changed, 431 insertions, 0 deletions
diff --git a/security/manager/ssl/tests/unit/test_content_signing.js b/security/manager/ssl/tests/unit/test_content_signing.js
new file mode 100644
index 0000000000..7e96ea2df6
--- /dev/null
+++ b/security/manager/ssl/tests/unit/test_content_signing.js
@@ -0,0 +1,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)"
+ );
+});