diff options
Diffstat (limited to 'toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js')
-rw-r--r-- | toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js | 438 |
1 files changed, 434 insertions, 4 deletions
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js index c17cb941cb..e801485c73 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js @@ -25,9 +25,439 @@ function verifySignatures() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); -add_task(async function test_no_change() { +add_setup(async () => { await promiseStartupManager(); +}); + +add_task(function test_hasStrongSignature_helper() { + const { hasStrongSignature } = ChromeUtils.importESModule( + "resource://gre/modules/addons/crypto-utils.sys.mjs" + ); + const { PKCS7_WITH_SHA1, PKCS7_WITH_SHA256, COSE_WITH_SHA256 } = + Ci.nsIAppSignatureInfo; + const testCases = [ + [false, "SHA1 only", [PKCS7_WITH_SHA1]], + [true, "SHA256 only", [PKCS7_WITH_SHA256]], + [true, "COSE only", [COSE_WITH_SHA256]], + [true, "SHA1 and SHA256", [PKCS7_WITH_SHA1, PKCS7_WITH_SHA256]], + [true, "SHA1 and COSE", [PKCS7_WITH_SHA1, COSE_WITH_SHA256]], + [true, "SHA256 and COSE", [PKCS7_WITH_SHA256, COSE_WITH_SHA256]], + ]; + for (const [expect, msg, signedTypes] of testCases) { + Assert.equal(hasStrongSignature({ signedTypes }), expect, msg); + } +}); + +add_task(async function test_addon_signedTypes() { + // This test is allowing weak signatures to run assertions on the AddonWrapper.signedTypes + // property also for extensions only including SHA1 signatures. + const resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(true); + + const { PKCS7_WITH_SHA1, COSE_WITH_SHA256 } = Ci.nsIAppSignatureInfo; + + const { addon: addonSignedCOSE } = await promiseInstallFile( + do_get_file("amosigned-mv3-cose.xpi") + ); + const { addon: addonSignedSHA1 } = await promiseInstallFile( + do_get_file("amosigned-sha1only.xpi") + ); + + Assert.deepEqual( + addonSignedCOSE.signedTypes.sort(), + [COSE_WITH_SHA256, PKCS7_WITH_SHA1].sort(), + `Expect ${addonSignedCOSE.id} to be signed with both COSE and SHA1` + ); + + Assert.deepEqual( + addonSignedSHA1.signedTypes, + [PKCS7_WITH_SHA1], + `Expect ${addonSignedSHA1.id} to be signed with SHA1 only` + ); + + await addonSignedSHA1.uninstall(); + await addonSignedCOSE.uninstall(); + + resetWeakSignaturePref(); +}); + +add_task( + async function test_install_error_on_new_install_with_weak_signature() { + // Ensure restrictions on weak signatures are enabled (this should be removed when + // the new behavior is riding the train). + const resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(false); + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + let install = await AddonManager.getInstallForFile( + do_get_file("amosigned-sha1only.xpi") + ); + + await Assert.equal( + install.state, + AddonManager.STATE_DOWNLOAD_FAILED, + "Expect install state to be STATE_DOWNLOAD_FAILED" + ); + + await Assert.rejects( + install.install(), + /Install failed: onDownloadFailed/, + "Expected install to fail" + ); + }); + + resetWeakSignaturePref(); + + // Checking the message expected to be logged in the Browser Console. + AddonTestUtils.checkMessages(messages, { + expected: [ + { + message: + /Invalid XPI: install rejected due to the package not including a strong cryptographic signature/, + }, + ], + }); + } +); + +/** + * Test helper used to simulate an update from a given pre-installed add-on xpi to a new xpi file for the same + * add-on and assert the expected result and logged messages. + * + * @param {object} params + * @param {string} params.currentAddonXPI + * The path to the add-on xpi to be pre-installed and then updated to `newAddonXPI`. + * @param {string} params.newAddonXPI + * The path to the add-on xpi to be installed as an update over `currentAddonXPI`. + * @param {string} params.newAddonVersion + * The add-on version expected for `newAddonXPI`. + * @param {boolean} params.expectedInstallOK + * Set to true for an update scenario that is expected to be successful. + * @param {Array<string|RegExp>} params.expectedMessages + * Array of strings or RegExp for console messages expected to be logged. + * @param {Array<string|RegExp>} params.forbiddenMessages + * Array of strings or RegExp for console messages expected to NOT be logged. + */ +async function testWeakSignatureXPIUpdate({ + currentAddonXPI, + newAddonXPI, + newAddonVersion, + expectedInstallOK, + expectedMessages, + forbiddenMessages, +}) { + // Temporarily allow weak signature to install the xpi as a new install. + let resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(true); + + const { addon: addonFirstInstall } = await promiseInstallFile( + currentAddonXPI + ); + const addonId = addonFirstInstall.id; + const initialAddonVersion = addonFirstInstall.version; + + resetWeakSignaturePref(); + + // Make sure the install over is executed while weak signature is not allowed + // for new installs to confirm that installing over is allowed. + resetWeakSignaturePref = AddonTestUtils.setWeakSignatureInstallAllowed(false); + + info("Install over the existing installed addon"); + let addonInstalledOver; + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + const fileURL = Services.io.newFileURI(newAddonXPI).spec; + let install = await AddonManager.getInstallForURL(fileURL, { + existingAddon: addonFirstInstall, + version: newAddonVersion, + }); + + addonInstalledOver = await install.install().catch(err => { + if (expectedInstallOK) { + ok(false, `Unexpected error hit on installing update XPI: ${err}`); + } else { + ok(true, `Install failed as expected: ${err}`); + } + }); + }); + + resetWeakSignaturePref(); + + if (expectedInstallOK) { + Assert.equal( + addonInstalledOver.id, + addonFirstInstall.id, + "Expect addon id to be the same" + ); + Assert.equal( + addonInstalledOver.version, + newAddonVersion, + "Got expected addon version after update xpi install completed" + ); + await addonInstalledOver.uninstall(); + } else { + Assert.equal( + addonInstalledOver?.version, + undefined, + "Expect update addon xpi not installed successfully" + ); + Assert.equal( + (await AddonManager.getAddonByID(addonId)).version, + initialAddonVersion, + "Expect the addon version to match the initial XPI version" + ); + await addonFirstInstall.uninstall(); + } + + Assert.equal( + await AddonManager.getAddonByID(addonId), + undefined, + "Expect the test addon to be fully uninstalled" + ); + + // Checking the message logged in the Browser Console. + AddonTestUtils.checkMessages(messages, { + expected: expectedMessages, + forbidden: forbiddenMessages, + }); +} + +add_task(async function test_weak_install_over_weak_existing() { + const addonId = "amosigned-xpi@tests.mozilla.org"; + await testWeakSignatureXPIUpdate({ + currentAddonXPI: do_get_file("amosigned-sha1only.xpi"), + newAddonXPI: do_get_file("amosigned-sha1only.xpi"), + newAddonVersion: "2.1", + expectedInstallOK: true, + expectedMessages: [ + { + message: new RegExp( + `Allow weak signature install over existing "${addonId}" XPI` + ), + }, + ], + }); +}); + +add_task(async function test_update_weak_to_strong_signature() { + const addonId = "amosigned-xpi@tests.mozilla.org"; + await testWeakSignatureXPIUpdate({ + currentAddonXPI: do_get_file("amosigned-sha1only.xpi"), + newAddonXPI: do_get_file("amosigned.xpi"), + newAddonVersion: "2.2", + expectedInstallOK: true, + forbiddenMessages: [ + { + message: new RegExp( + `Allow weak signature install over existing "${addonId}" XPI` + ), + }, + ], + }); +}); + +add_task(async function test_update_strong_to_weak_signature() { + const addonId = "amosigned-xpi@tests.mozilla.org"; + await testWeakSignatureXPIUpdate({ + currentAddonXPI: do_get_file("amosigned.xpi"), + newAddonXPI: do_get_file("amosigned-sha1only.xpi"), + newAddonVersion: "2.1", + expectedInstallOK: false, + expectedMessages: [ + { + message: new RegExp( + "Invalid XPI: install rejected due to the package not including a strong cryptographic signature" + ), + }, + ], + forbiddenMessages: [ + { + message: new RegExp( + `Allow weak signature install over existing "${addonId}" XPI` + ), + }, + ], + }); +}); + +add_task(async function test_signedTypes_stored_in_addonDB() { + const { addon: addonAfterInstalled } = await promiseInstallFile( + do_get_file("amosigned-mv3-cose.xpi") + ); + const addonId = addonAfterInstalled.id; + + const { PKCS7_WITH_SHA1, COSE_WITH_SHA256 } = Ci.nsIAppSignatureInfo; + const expectedSignedTypes = [COSE_WITH_SHA256, PKCS7_WITH_SHA1].sort(); + + Assert.deepEqual( + addonAfterInstalled.signedTypes.sort(), + expectedSignedTypes, + `Got expected ${addonId} signedTyped after install` + ); + + await promiseRestartManager(); + + const addonAfterAOMRestart = await AddonManager.getAddonByID(addonId); + + Assert.deepEqual( + addonAfterAOMRestart.signedTypes.sort(), + expectedSignedTypes, + `Got expected ${addonId} signedTyped after AOM restart` + ); + + const removeSignedStateFromAddonDB = async () => { + const addon_db_file = Services.dirsvc.get("ProfD", Ci.nsIFile); + addon_db_file.append("extensions.json"); + const addon_db_data = await IOUtils.readJSON(addon_db_file.path); + + const addon_db_data_tampered = { + ...addon_db_data, + addons: addon_db_data.addons.map(addonData => { + // Tamper the data of the test extension to mock the + // scenario. + delete addonData.signedTypes; + return addonData; + }), + }; + await IOUtils.writeJSON(addon_db_file.path, addon_db_data_tampered); + }; + + // Shutdown the AddonManager and tamper the AddonDB to confirm XPIProvider.checkForChanges + // calls originated internally when opening a profile created from a previous Firefox version + // is going to populate the new signedTypes property. + info( + "Check that XPIProvider.checkForChanges(true) will recompute missing signedTypes properties" + ); + await promiseShutdownManager(); + await removeSignedStateFromAddonDB(); + await promiseStartupManager(); + + // Expect the signedTypes property to be undefined because of the + // AddonDB data being tampered earlier in this test. + const addonAfterAppUpgrade = await AddonManager.getAddonByID(addonId); + Assert.deepEqual( + addonAfterAppUpgrade.signedTypes, + undefined, + `Got empty ${addonId} signedTyped set to undefied after AddonDB data tampered` + ); + + // Mock call to XPIDatabase.checkForChanges expected to be hit when the application + // is updated. + AddonTestUtils.getXPIExports().XPIProvider.checkForChanges( + /* aAppChanged */ true + ); + + Assert.deepEqual( + addonAfterAppUpgrade.signedTypes?.sort(), + expectedSignedTypes.sort(), + `Got expected ${addonId} signedTyped after XPIProvider.checkForChanges recomputed it` + ); + + // Shutdown the AddonManager and tamper the AddonDB to confirm XPIDatabase.updateCompatibility + // would populate signedTypes if missing. + info( + "Check that XPIDatabase.updateCompatibility will recompute missing signedTypes properties" + ); + await promiseShutdownManager(); + await removeSignedStateFromAddonDB(); + await promiseStartupManager(); + + // Expect the signedTypes property to be undefined because of the + // AddonDB data being tampered earlier in this test. + const addonAfterUpdateCompatibility = await AddonManager.getAddonByID( + addonId + ); + Assert.deepEqual( + addonAfterUpdateCompatibility.signedTypes, + undefined, + `Got empty ${addonId} signedTyped set to undefied after AddonDB data tampered` + ); + + // Mock call to XPIDatabase.updateCompatibility expected to be originated from + // XPIDatabaseReconcile.processFileChanges and confirm that signedTypes has been + // recomputed as expected. + AddonTestUtils.getXPIExports().XPIDatabaseReconcile.processFileChanges( + {}, + true + ); + Assert.deepEqual( + addonAfterUpdateCompatibility.signedTypes?.sort(), + expectedSignedTypes.sort(), + `Got expected ${addonId} signedTyped after XPIDatabase.updateCompatibility recomputed it` + ); + + // Restart the AddonManager and confirm that XPIDatabase.verifySignatures would not recompute + // the content of the signedTypes array if all values are still the same. + info( + "Check that XPIDatabase.updateCompatibility will recompute missing signedTypes properties" + ); + await promiseRestartManager(); + + let listener = { + onPropertyChanged(_addon, properties) { + Assert.deepEqual( + properties, + [], + `No properties should have been changed for ${_addon.id}` + ); + Assert.ok( + false, + `onPropertyChanged should have not been called for ${_addon.id}` + ); + }, + }; + + AddonManager.addAddonListener(listener); + await verifySignatures(); + AddonManager.removeAddonListener(listener); + + // Shutdown the AddonManager and tamper the AddonDB to set signedTypes to undefined + // then confirm that XPIDatabase.verifySignatures does not hit an exception due to + // signedTypes assumed to always be set to an array. + info( + "Check that XPIDatabase.verifySignatures does not fail when signedTypes is undefined" + ); + await promiseShutdownManager(); + await removeSignedStateFromAddonDB(); + await promiseStartupManager(); + + // Expect the signedTypes property to be undefined because of the + // AddonDB data being tampered earlier in this test. + const addonUndefinedSignedTypes = await AddonManager.getAddonByID(addonId); + Assert.deepEqual( + addonUndefinedSignedTypes.signedTypes, + undefined, + `Got empty ${addonId} signedTyped set to undefied after AddonDB data tampered` + ); + await verifySignatures(); + Assert.deepEqual( + addonUndefinedSignedTypes.signedTypes?.sort(), + expectedSignedTypes.sort(), + `Got expected ${addonId} signedTyped after XPIDatabase.verifySignatures recomputed it` + ); + + await addonUndefinedSignedTypes.uninstall(); +}); + +add_task( + { + pref_set: [["xpinstall.signatures.required", false]], + // Skip this test on builds where disabling xpi signature checks is not supported. + skip_if: () => AppConstants.MOZ_REQUIRE_SIGNING, + }, + async function test_weak_signature_not_restricted_on_disabled_signature_checks() { + // Ensure the restriction on installing xpi using only weak signatures is enabled. + let resetWeakSignaturePref = + AddonTestUtils.setWeakSignatureInstallAllowed(false); + const { addon } = await promiseInstallFile( + do_get_file("amosigned-sha1only.xpi") + ); + Assert.notEqual(addon, null, "Expect addon to be installed"); + resetWeakSignaturePref(); + await addon.uninstall(); + } +); +add_task(useAMOStageCert(), async function test_no_change() { // Install the first add-on await promiseInstallFile(do_get_file(`${DATA}/signed1.xpi`)); @@ -43,7 +473,7 @@ add_task(async function test_no_change() { await manuallyInstall(do_get_file(`${DATA}/signed2.xpi`), profileDir, ID); let listener = { - onPropetyChanged(_addon, properties) { + onPropertyChanged(_addon) { Assert.ok(false, `Got unexpected onPropertyChanged for ${_addon.id}`); }, }; @@ -63,7 +493,7 @@ add_task(async function test_no_change() { AddonManager.removeAddonListener(listener); }); -add_task(async function test_diable() { +add_task(useAMOStageCert(), async function test_disable() { // Install the first add-on await promiseInstallFile(do_get_file(`${DATA}/signed1.xpi`)); @@ -96,7 +526,7 @@ add_task(async function test_diable() { Assert.deepEqual( changedProperties, - ["signedState", "appDisabled"], + ["signedState", "signedTypes", "appDisabled"], "Got onPropertyChanged events for signedState and appDisabled" ); |