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