Bug 1151511: Implement the periodic check for unsigned add-ons. r=dveditz
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 07 Apr 2015 11:08:34 -0700
changeset 273225 002bcc21b3543cae4a993292a019f393b0a3db86
parent 273224 e9ddb507f07236f8bfdd6fe355808228f8fa570d
child 273226 65afb97fc3990a0f54e1d4a5265a0ec09d1e07e6
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdveditz
bugs1151511
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1151511: Implement the periodic check for unsigned add-ons. r=dveditz
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -124,16 +124,18 @@ const KEY_APP_SYSTEM_USER             = 
 const NOTIFICATION_FLUSH_PERMISSIONS  = "flush-pending-permissions";
 const XPI_PERMISSION                  = "install";
 
 const RDFURI_INSTALL_MANIFEST_ROOT    = "urn:mozilla:install-manifest";
 const PREFIX_NS_EM                    = "http://www.mozilla.org/2004/em-rdf#";
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
+const XPI_SIGNATURE_CHECK_PERIOD      = 24 * 60 * 60;
+
 // The value for this is in Makefile.in
 #expand const DB_SCHEMA                       = __MOZ_EXTENSIONS_DB_SCHEMA__;
 const NOTIFICATION_TOOLBOXPROCESS_LOADED      = "ToolboxProcessLoaded";
 
 // Properties that exist in the install manifest
 const PROP_METADATA      = ["id", "version", "type", "internalName", "updateURL",
                             "updateKey", "optionsURL", "optionsType", "aboutURL",
                             "iconURL", "icon64URL"];
@@ -1355,16 +1357,32 @@ function verifyZipSignedState(aFile, aEx
  * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
  */
 function verifyDirSignedState(aDir, aExpectedID) {
   // TODO: Get the certificate for an unpacked add-on (bug 1038072)
   return Promise.resolve(AddonManager.SIGNEDSTATE_MISSING);
 }
 
 /**
+ * Verifies that a bundle's contents are all correctly signed by an
+ * AMO-issued certificate
+ *
+ * @param  aBundle
+ *         the nsIFile for the bundle to check, either a directory or zip file
+ * @param  aExpectedID
+ *         the expected ID of the signature
+ * @return a Promise that resolves to an AddonManager.SIGNEDSTATE_* constant.
+ */
+function verifyBundleSignedState(aBundle, aExpectedID) {
+  if (aBundle.isFile())
+    return verifyZipSignedState(aBundle, aExpectedID);
+  return verifyDirSignedState(aBundle, aExpectedID);
+}
+
+/**
  * Replaces %...% strings in an addon url (update and updateInfo) with
  * appropriate values.
  *
  * @param  aAddon
  *         The AddonInternal representing the add-on
  * @param  aUri
  *         The uri to escape
  * @param  aUpdateType
@@ -2339,16 +2357,22 @@ this.XPIProvider = {
           Services.obs.removeObserver(this, "final-ui-startup");
         }
       }, "final-ui-startup", false);
 
       AddonManagerPrivate.recordTimestamp("XPI_startup_end");
 
       this.extensionsActive = true;
       this.runPhase = XPI_BEFORE_UI_STARTUP;
+
+      let timerManager = Cc["@mozilla.org/updates/timer-manager;1"].
+                         getService(Ci.nsIUpdateTimerManager);
+      timerManager.registerTimer("xpi-signature-verification", () => {
+        this.verifySignatures();
+      }, XPI_SIGNATURE_CHECK_PERIOD);
     }
     catch (e) {
       logger.error("startup failed", e);
       AddonManagerPrivate.recordException("XPI", "startup failed", e);
     }
   },
 
   /**
@@ -2490,16 +2514,55 @@ this.XPIProvider = {
     ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
 
     // Ensure any changes to the add-ons list are flushed to disk
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS,
                                !XPIDatabase.writeAddonsList());
   },
 
   /**
+   * Verifies that all installed add-ons are still correctly signed.
+   */
+  verifySignatures: function XPI_verifySignatures() {
+    XPIDatabase.getAddonList(addon => SIGNED_TYPES.has(addon.type), (addons) => {
+      Task.spawn(function*() {
+        let changes = {
+          enabled: [],
+          disabled: []
+        };
+
+        for (let addon of addons) {
+          // The add-on might have vanished, we'll catch that on the next startup
+          if (!addon._sourceBundle.exists())
+            continue;
+
+          let signedState = yield verifyBundleSignedState(addon._sourceBundle, addon.id);
+          if (signedState == addon.signedState)
+            continue;
+
+          addon.signedState = signedState;
+          AddonManagerPrivate.callAddonListeners("onPropertyChanged",
+                                                 createWrapper(addon),
+                                                 ["signedState"]);
+
+          let disabled = XPIProvider.updateAddonDisabledState(addon);
+          if (disabled !== undefined)
+            changes[disabled ? "disabled" : "enabled"].push(addon.id);
+        }
+
+        XPIDatabase.saveChanges();
+
+        Services.obs.notifyObservers(null, "xpi-signature-changed", JSON.stringify(changes));
+      }).then(null, err => {
+        logger.error("XPI_verifySignature: " + err);
+      })
+    });
+  },
+
+  /**
    * Persists changes to XPIProvider.bootstrappedAddons to its store (a pref).
    */
   persistBootstrappedAddons: function XPI_persistBootstrappedAddons() {
     // Experiments are disabled upon app load, so don't persist references.
     let filtered = {};
     for (let id in this.bootstrappedAddons) {
       let entry = this.bootstrappedAddons[id];
       if (entry.type == "experiment") {
@@ -4552,16 +4615,20 @@ this.XPIProvider = {
    * @param  aAddon
    *         The DBAddonInternal to update
    * @param  aUserDisabled
    *         Value for the userDisabled property. If undefined the value will
    *         not change
    * @param  aSoftDisabled
    *         Value for the softDisabled property. If undefined the value will
    *         not change. If true this will force userDisabled to be true
+   * @return a tri-state indicating the action taken for the add-on:
+   *           - undefined: The add-on did not change state
+   *           - true: The add-on because disabled
+   *           - false: The add-on became enabled
    * @throws if addon is not a DBAddonInternal
    */
   updateAddonDisabledState: function XPI_updateAddonDisabledState(aAddon,
                                                                   aUserDisabled,
                                                                   aSoftDisabled) {
     if (!(aAddon.inDatabase))
       throw new Error("Can only update addon states for installed addons.");
     if (aUserDisabled !== undefined && aSoftDisabled !== undefined) {
@@ -4582,17 +4649,17 @@ this.XPIProvider = {
     if (aSoftDisabled === undefined || aUserDisabled)
       aSoftDisabled = aAddon.softDisabled;
 
     let appDisabled = !isUsableAddon(aAddon);
     // No change means nothing to do here
     if (aAddon.userDisabled == aUserDisabled &&
         aAddon.appDisabled == appDisabled &&
         aAddon.softDisabled == aSoftDisabled)
-      return;
+      return undefined;
 
     let wasDisabled = aAddon.disabled;
     let isDisabled = aUserDisabled || aSoftDisabled || appDisabled;
 
     // If appDisabled changes but addon.disabled doesn't,
     // no onDisabling/onEnabling is sent - so send a onPropertyChanged.
     let appDisabledChanged = aAddon.appDisabled != appDisabled;
 
@@ -4602,31 +4669,32 @@ this.XPIProvider = {
     if (aAddon.type != "experiment") {
       XPIDatabase.setAddonProperties(aAddon, {
         userDisabled: aUserDisabled,
         appDisabled: appDisabled,
         softDisabled: aSoftDisabled
       });
     }
 
+    let wrapper = createWrapper(aAddon);
+
     if (appDisabledChanged) {
       AddonManagerPrivate.callAddonListeners("onPropertyChanged",
-                                            aAddon,
-                                            ["appDisabled"]);
+                                             wrapper,
+                                             ["appDisabled"]);
     }
 
     // If the add-on is not visible or the add-on is not changing state then
     // there is no need to do anything else
     if (!aAddon.visible || (wasDisabled == isDisabled))
-      return;
+      return undefined;
 
     // Flag that active states in the database need to be updated on shutdown
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
 
-    let wrapper = createWrapper(aAddon);
     // Have we just gone back to the current state?
     if (isDisabled != aAddon.active) {
       AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
     }
     else {
       if (isDisabled) {
         var needsRestart = this.disableRequiresRestart(aAddon);
         AddonManagerPrivate.callAddonListeners("onDisabling", wrapper,
@@ -4668,16 +4736,18 @@ this.XPIProvider = {
     } else {
       // There should always be an xpiState
       logger.warn("No XPIState for ${id} in ${location}", aAddon);
     }
 
     // Notify any other providers that a new theme has been enabled
     if (aAddon.type == "theme" && !isDisabled)
       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart);
+
+    return isDisabled;
   },
 
   /**
    * Uninstalls an add-on, immediately if possible or marks it as pending
    * uninstall if not.
    *
    * @param  aAddon
    *         The DBAddonInternal to uninstall
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js
@@ -0,0 +1,234 @@
+// Enable signature checks for these tests
+Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const DATA = "data/signing_checks/";
+const GOOD = [
+  ["signed_bootstrap_2.xpi", AddonManager.SIGNEDSTATE_SIGNED],
+  ["signed_nonbootstrap_2.xpi", AddonManager.SIGNEDSTATE_SIGNED]
+];
+const BAD = [
+  ["unsigned_bootstrap_2.xpi", AddonManager.SIGNEDSTATE_MISSING],
+  ["signed_bootstrap_badid_2.xpi", AddonManager.SIGNEDSTATE_BROKEN],
+  ["unsigned_nonbootstrap_2.xpi", AddonManager.SIGNEDSTATE_MISSING],
+  ["signed_nonbootstrap_badid_2.xpi", AddonManager.SIGNEDSTATE_BROKEN],
+];
+const ID = "test@tests.mozilla.org";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function verifySignatures() {
+  return new Promise(resolve => {
+    let observer = (subject, topic, data) => {
+      Services.obs.removeObserver(observer, "xpi-signature-changed");
+      resolve(JSON.parse(data));
+    }
+    Services.obs.addObserver(observer, "xpi-signature-changed", false);
+
+    do_print("Verifying signatures");
+    let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+    XPIscope.XPIProvider.verifySignatures();
+  });
+}
+
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+
+  run_next_test();
+}
+
+function verify_no_change([startFile, startState], [endFile, endState]) {
+  add_task(function*() {
+    do_print("A switch from " + startFile + " to " + endFile + " should cause no change.");
+
+    // Install the first add-on
+    manuallyInstall(do_get_file(DATA + startFile), profileDir, ID);
+    startupManager();
+
+    let addon = yield promiseAddonByID(ID);
+    do_check_neq(addon, null);
+    let wasAppDisabled = addon.appDisabled;
+    do_check_neq(addon.appDisabled, addon.isActive);
+    do_check_eq(addon.pendingOperations, AddonManager.PENDING_NONE);
+    do_check_eq(addon.signedState, startState);
+
+    // Swap in the files from the next add-on
+    manuallyUninstall(profileDir, ID);
+    manuallyInstall(do_get_file(DATA + endFile), profileDir, ID);
+
+    let events = {
+      [ID]: []
+    };
+
+    if (startState != endState)
+      events[ID].unshift(["onPropertyChanged", ["signedState"]]);
+
+    prepare_test(events);
+
+    // Trigger the check
+    let changes = yield verifySignatures();
+    do_check_eq(changes.enabled.length, 0);
+    do_check_eq(changes.disabled.length, 0);
+
+    do_check_eq(addon.appDisabled, wasAppDisabled);
+    do_check_neq(addon.appDisabled, addon.isActive);
+    do_check_eq(addon.pendingOperations, AddonManager.PENDING_NONE);
+    do_check_eq(addon.signedState, endState);
+
+    // Remove the add-on and restart to let it go away
+    manuallyUninstall(profileDir, ID);
+    yield promiseRestartManager();
+    yield promiseShutdownManager();
+  });
+}
+
+function verify_enables([startFile, startState], [endFile, endState]) {
+  add_task(function*() {
+    do_print("A switch from " + startFile + " to " + endFile + " should enable the add-on.");
+
+    // Install the first add-on
+    manuallyInstall(do_get_file(DATA + startFile), profileDir, ID);
+    startupManager();
+
+    let addon = yield promiseAddonByID(ID);
+    do_check_neq(addon, null);
+    do_check_false(addon.isActive);
+    do_check_eq(addon.pendingOperations, AddonManager.PENDING_NONE);
+    do_check_eq(addon.signedState, startState);
+
+    // Swap in the files from the next add-on
+    manuallyUninstall(profileDir, ID);
+    manuallyInstall(do_get_file(DATA + endFile), profileDir, ID);
+
+    let needsRestart = hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE);
+    do_print(needsRestart);
+
+    let events = {};
+    if (!needsRestart) {
+      events[ID] = [
+        ["onPropertyChanged", ["appDisabled"]],
+        ["onEnabling", false],
+        "onEnabled"
+      ];
+    }
+    else {
+      events[ID] = [
+        ["onPropertyChanged", ["appDisabled"]],
+        "onEnabling"
+      ];
+    }
+
+    if (startState != endState)
+      events[ID].unshift(["onPropertyChanged", ["signedState"]]);
+
+    prepare_test(events);
+
+    // Trigger the check
+    let changes = yield verifySignatures();
+    do_check_eq(changes.enabled.length, 1);
+    do_check_eq(changes.enabled[0], ID);
+    do_check_eq(changes.disabled.length, 0);
+
+    do_check_false(addon.appDisabled);
+    if (needsRestart)
+      do_check_neq(addon.pendingOperations, AddonManager.PENDING_NONE);
+    else
+      do_check_true(addon.isActive);
+    do_check_eq(addon.signedState, endState);
+
+    ensure_test_completed();
+
+    // Remove the add-on and restart to let it go away
+    manuallyUninstall(profileDir, ID);
+    yield promiseRestartManager();
+    yield promiseShutdownManager();
+  });
+}
+
+function verify_disables([startFile, startState], [endFile, endState]) {
+  add_task(function*() {
+    do_print("A switch from " + startFile + " to " + endFile + " should disable the add-on.");
+
+    // Install the first add-on
+    manuallyInstall(do_get_file(DATA + startFile), profileDir, ID);
+    startupManager();
+
+    let addon = yield promiseAddonByID(ID);
+    do_check_neq(addon, null);
+    do_check_true(addon.isActive);
+    do_check_eq(addon.pendingOperations, AddonManager.PENDING_NONE);
+    do_check_eq(addon.signedState, startState);
+
+    let needsRestart = hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE);
+
+    // Swap in the files from the next add-on
+    manuallyUninstall(profileDir, ID);
+    manuallyInstall(do_get_file(DATA + endFile), profileDir, ID);
+
+    let events = {};
+    if (!needsRestart) {
+      events[ID] = [
+        ["onPropertyChanged", ["appDisabled"]],
+        ["onDisabling", false],
+        "onDisabled"
+      ];
+    }
+    else {
+      events[ID] = [
+        ["onPropertyChanged", ["appDisabled"]],
+        "onDisabling"
+      ];
+    }
+
+    if (startState != endState)
+      events[ID].unshift(["onPropertyChanged", ["signedState"]]);
+
+    prepare_test(events);
+
+    // Trigger the check
+    let changes = yield verifySignatures();
+    do_check_eq(changes.enabled.length, 0);
+    do_check_eq(changes.disabled.length, 1);
+    do_check_eq(changes.disabled[0], ID);
+
+    do_check_true(addon.appDisabled);
+    if (needsRestart)
+      do_check_neq(addon.pendingOperations, AddonManager.PENDING_NONE);
+    else
+      do_check_false(addon.isActive);
+    do_check_eq(addon.signedState, endState);
+
+    ensure_test_completed();
+
+    // Remove the add-on and restart to let it go away
+    manuallyUninstall(profileDir, ID);
+    yield promiseRestartManager();
+    yield promiseShutdownManager();
+  });
+}
+
+for (let start of GOOD) {
+  for (let end of BAD) {
+    verify_disables(start, end);
+  }
+}
+
+for (let start of BAD) {
+  for (let end of GOOD) {
+    verify_enables(start, end);
+  }
+}
+
+for (let start of GOOD) {
+  for (let end of GOOD.filter(f => f != start)) {
+    verify_no_change(start, end);
+  }
+}
+
+for (let start of BAD) {
+  for (let end of BAD.filter(f => f != start)) {
+    verify_no_change(start, end);
+  }
+}
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -17,16 +17,17 @@ skip-if = appname != "firefox"
 [test_isReady.js]
 [test_metadata_update.js]
 [test_pluginInfoURL.js]
 [test_provider_markSafe.js]
 [test_provider_shutdown.js]
 [test_provider_unsafe_access_shutdown.js]
 [test_provider_unsafe_access_startup.js]
 [test_shutdown.js]
+[test_signed_verify.js]
 [test_signed_inject.js]
 skip-if = true
 [test_signed_install.js]
 run-sequentially = Uses hardcoded ports in xpi files.
 [test_signed_migrate.js]
 [test_XPIcancel.js]
 [test_XPIStates.js]