Bug 1362364: Lazily load the certificate database into the add-ons manager. r?rhelmer
The lazy loading is a little more complex because we want this to be a constant
in the scope so extensions can't trivially replace it. This also changes the
test to be more like the proof of concept from
bug 1244248.
I took the opportunity to promisify a bunch of things which ultimately made the
verifySignatures code nicer.
MozReview-Commit-ID: 2P890uRY1Si
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -80,16 +80,28 @@ XPCOMUtils.defineLazyServiceGetter(this,
XPCOMUtils.defineLazyServiceGetter(this,
"AddonPathService",
"@mozilla.org/addon-path-service;1",
"amIAddonPathService");
XPCOMUtils.defineLazyServiceGetter(this, "aomStartup",
"@mozilla.org/addons/addon-manager-startup;1",
"amIAddonManagerStartup");
+Object.defineProperty(this, "gCertDB", {
+ get() {
+ delete this.gCertDB;
+ XPCOMUtils.defineConstant(this, "gCertDB",
+ Cc["@mozilla.org/security/x509certdb;1"].
+ getService(Ci.nsIX509CertDB))
+ return this.gCertDB;
+ },
+ configurable: true,
+ enumerable: true
+});
+
XPCOMUtils.defineLazyGetter(this, "CertUtils", function() {
let certUtils = {};
Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
return certUtils;
});
XPCOMUtils.defineLazyGetter(this, "IconDetails", () => {
const {ExtensionUtils} = Cu.import("resource://gre/modules/ExtensionUtils.jsm", {});
@@ -1939,19 +1951,16 @@ function shouldVerifySignedState(aAddon)
if (hotfixID && aAddon.id == hotfixID)
return true;
// Otherwise only check signatures if signing is enabled and the add-on is one
// of the signed types.
return ADDON_SIGNING && SIGNED_TYPES.has(aAddon.type);
}
-let gCertDB = Cc["@mozilla.org/security/x509certdb;1"]
- .getService(Ci.nsIX509CertDB);
-
/**
* Verifies that a zip file's contents are all correctly signed by an
* AMO-issued certificate
*
* @param aFile
* the xpi file to check
* @param aAddon
* the add-on object to verify
@@ -3653,50 +3662,50 @@ this.XPIProvider = {
logger.info("Installing new system add-on set");
await systemAddonLocation.installAddonSet(Array.from(addonList.values())
.map(a => a.addon));
},
/**
* Verifies that all installed add-ons are still correctly signed.
*/
- verifySignatures() {
- XPIDatabase.getAddonList(a => true, (addons) => {
- (async 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 = await verifyBundleSignedState(addon._sourceBundle, addon);
-
- if (signedState != addon.signedState) {
- addon.signedState = signedState;
- AddonManagerPrivate.callAddonListeners("onPropertyChanged",
- addon.wrapper,
- ["signedState"]);
- }
-
- let disabled = XPIProvider.updateAddonDisabledState(addon);
- if (disabled !== undefined)
- changes[disabled ? "disabled" : "enabled"].push(addon.id);
+ async verifySignatures() {
+ try {
+ let addons = await XPIDatabase.getAddonList(a => true);
+
+ 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 = await verifyBundleSignedState(addon._sourceBundle, addon);
+
+ if (signedState != addon.signedState) {
+ addon.signedState = signedState;
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged",
+ addon.wrapper,
+ ["signedState"]);
}
- XPIDatabase.saveChanges();
-
- Services.obs.notifyObservers(null, "xpi-signature-changed", JSON.stringify(changes));
- })().then(null, err => {
- logger.error("XPI_verifySignature: " + err);
- })
- });
+ 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));
+ } catch (err) {
+ logger.error("XPI_verifySignature: " + err);
+ }
},
/**
* Adds a list of currently active add-ons to the next crash report.
*/
addAddonsToCrashReporter() {
if (!("nsICrashReporter" in Ci) ||
!(Services.appinfo instanceof Ci.nsICrashReporter))
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -110,56 +110,40 @@ function makeSafe(aCallback) {
aCallback(...aArgs);
} catch (ex) {
logger.warn("XPI Database callback failed", ex);
}
}
}
/**
- * A helper method to asynchronously call a function on an array
- * of objects, calling a callback when function(x) has been gathered
- * for every element of the array.
- * WARNING: not currently error-safe; if the async function does not call
- * our internal callback for any of the array elements, asyncMap will not
- * call the callback parameter.
+ * A helper method to asynchronously call a function on an array of objects.
+ * Returns a promise that resolves with the results for each function call in
+ * the same order as the aObjects array.
+ * WARNING: not currently error-safe; if the async function does not call its
+ * callback for any of the array elements, asyncMap will never resolve.
*
* @param aObjects
* The array of objects to process asynchronously
* @param aMethod
* Function with signature function(object, function(f_of_object))
- * @param aCallback
- * Function with signature f([aMethod(object)]), called when all values
- * are available
*/
-function asyncMap(aObjects, aMethod, aCallback) {
- var resultsPending = aObjects.length;
- var results = []
- if (resultsPending == 0) {
- aCallback(results);
- return;
- }
+function asyncMap(aObjects, aMethod) {
+ let methodCalls = aObjects.map(obj => {
+ return new Promise(resolve => {
+ try {
+ aMethod(obj, resolve);
+ } catch (e) {
+ logger.error("Async map function failed", e);
+ resolve(undefined);
+ }
+ });
+ });
- function asyncMap_gotValue(aIndex, aValue) {
- results[aIndex] = aValue;
- if (--resultsPending == 0) {
- aCallback(results);
- }
- }
-
- aObjects.map(function(aObject, aIndex, aArray) {
- try {
- aMethod(aObject, function(aResult) {
- asyncMap_gotValue(aIndex, aResult);
- });
- } catch (e) {
- logger.warn("Async map function failed", e);
- asyncMap_gotValue(aIndex, undefined);
- }
- });
+ return Promise.all(methodCalls);
}
/**
* Copies properties from one object to another. If no target object is passed
* a new object will be created and returned.
*
* @param aObject
* An object to copy from
@@ -712,30 +696,37 @@ this.XPIDatabase = {
},
/**
* Asynchronously list all addons that match the filter function
* @param aFilter
* Function that takes an addon instance and returns
* true if that addon should be included in the selected array
* @param aCallback
- * Called back with an array of addons matching aFilter
- * or an empty array if none match
+ * Optional and will be called with an array of addons matching
+ * aFilter or an empty array if none match.
+ * @return a Promise that resolves to the list of add-ons matching aFilter or
+ * an empty array if none match
*/
- getAddonList(aFilter, aCallback) {
- this.asyncLoadDB().then(
- addonDB => {
- let addonList = _filterDB(addonDB, aFilter);
- asyncMap(addonList, getRepositoryAddon, makeSafe(aCallback));
- })
- .then(null,
- error => {
- logger.error("getAddonList failed", error);
- makeSafe(aCallback)([]);
- });
+ async getAddonList(aFilter, aCallback) {
+ try {
+ let addonDB = await this.asyncLoadDB();
+ let addonList = _filterDB(addonDB, aFilter);
+ let addons = await asyncMap(addonList, getRepositoryAddon);
+ if (aCallback) {
+ makeSafe(aCallback)(addons);
+ }
+ return addons;
+ } catch (error) {
+ logger.error("getAddonList failed", error);
+ if (aCallback) {
+ makeSafe(aCallback)([]);
+ }
+ return [];
+ }
},
/**
* (Possibly asynchronously) get the first addon that matches the filter function
* @param aFilter
* Function that takes an addon instance and returns
* true if that addon should be selected
* @param aCallback
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_cache_certdb/bootstrap.js
@@ -0,0 +1,76 @@
+var AM_Ci = Components.interfaces;
+
+const CERTDB_CONTRACTID = "@mozilla.org/security/x509certdb;1";
+const CERTDB_CID = Components.ID("{fb0bbc5c-452e-4783-b32c-80124693d871}");
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const CERT = `MIIDITCCAgmgAwIBAgIJALAv8fydd6nBMA0GCSqGSIb3DQEBBQUAMCcxJTAjBgNV
+BAMMHGJvb3RzdHJhcDFAdGVzdHMubW96aWxsYS5vcmcwHhcNMTYwMjAyMjMxNjUy
+WhcNMjYwMTMwMjMxNjUyWjAnMSUwIwYDVQQDDBxib290c3RyYXAxQHRlc3RzLm1v
+emlsbGEub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5caNuLTu
+H8dEqNntLlhKi4y09hrgcF3cb6n5Xx9DIHA8CKiZxt9qGXKeeiDwEiiQ8ibJYzdc
+jLkbzJUyPVUaH9ygrWynSpSTOvv/Ys3+ERrCo9W7Zuzwdmzt6TTEjFMS4lVx06us
+3uUqkdp3JMgCqCEbOFZiztICiSKrp8QFJkAfApZzBqmJOPOWH0yZ2CRRzvbQZ6af
+hqQDUalJQjWfsenyUWphhbREqExetxHJFR3OrmJt/shXVyz6dD7TBuE3PPUh1RpE
+3ejVufcTzjV3XmK79PxsKLM9V2+ww9e9V3OET57kyvn+bpSWdUYm3X4DA8dxNW6+
+kTFWRnQNZ+zQVQIDAQABo1AwTjAdBgNVHQ4EFgQUac36ccv+99N5HxYa8dCDYRaF
+HNQwHwYDVR0jBBgwFoAUac36ccv+99N5HxYa8dCDYRaFHNQwDAYDVR0TBAUwAwEB
+/zANBgkqhkiG9w0BAQUFAAOCAQEAFfu3MN8EtY5wcxOFdGShOmGQPm2MJJVE6MG+
+p4RqHrukHZSgKOyWjkRk7t6NXzNcnHco9HFv7FQRAXSJ5zObmyu+TMZlu4jHHCav
+GMcV3C/4SUGtlipZbgNe00UAIm6tM3Wh8dr38W7VYg4KGAwXou5XhQ9gCAnSn90o
+H/42NqHTjJsR4v18izX2aO25ARQdMby7Lsr5j9RqweHywiSlPusFcKRseqOnIP0d
+JT3+qh78LeMbNBO2mYD3SP/zu0TAmkAVNcj2KPw0+a0kVZ15rvslPC/K3xn9msMk
+fQthv3rDAcsWvi9YO7T+vylgZBgJfn1ZqpQqy58xN96uh6nPOw==`;
+
+function overrideCertDB() {
+ // Unregister the real database.
+ let registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
+ let factory = registrar.getClassObject(CERTDB_CID, AM_Ci.nsIFactory);
+ registrar.unregisterFactory(CERTDB_CID, factory);
+
+ // Get the real DB
+ let realCertDB = factory.createInstance(null, AM_Ci.nsIX509CertDB);
+
+ let fakeCert = realCertDB.constructX509FromBase64(CERT.replace(/\n/g, ""));
+
+ let fakeCertDB = {
+ openSignedAppFileAsync(root, file, callback) {
+ callback.openSignedAppFileFinished(Components.results.NS_OK, null, fakeCert);
+ },
+
+ verifySignedDirectoryAsync(root, dir, callback) {
+ callback.verifySignedDirectoryFinished(Components.results.NS_OK, fakeCert);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIX509CertDB])
+ };
+
+ for (let property of Object.keys(realCertDB)) {
+ if (property in fakeCertDB) {
+ continue;
+ }
+
+ if (typeof realCertDB[property] == "function") {
+ fakeCertDB[property] = realCertDB[property].bind(realCertDB);
+ }
+ }
+
+ let certDBFactory = {
+ createInstance(outer, iid) {
+ if (outer != null) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return fakeCertDB.QueryInterface(iid);
+ }
+ };
+ registrar.registerFactory(CERTDB_CID, "CertDB",
+ CERTDB_CONTRACTID, certDBFactory);
+
+ const scope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
+ scope.gCertDB = fakeCertDB;
+}
+
+function install() { // eslint-disable-line no-unused-vars
+ overrideCertDB();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_cache_certdb/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>bootstrap1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+ <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
--- a/toolkit/mozapps/extensions/test/xpcshell/test_cache_certdb.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_cache_certdb.js
@@ -1,82 +1,29 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
// We require signature checks for this test
Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
gUseRealCertChecks = true;
-const CERT = `MIIDITCCAgmgAwIBAgIJALAv8fydd6nBMA0GCSqGSIb3DQEBBQUAMCcxJTAjBgNV
-BAMMHGJvb3RzdHJhcDFAdGVzdHMubW96aWxsYS5vcmcwHhcNMTYwMjAyMjMxNjUy
-WhcNMjYwMTMwMjMxNjUyWjAnMSUwIwYDVQQDDBxib290c3RyYXAxQHRlc3RzLm1v
-emlsbGEub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5caNuLTu
-H8dEqNntLlhKi4y09hrgcF3cb6n5Xx9DIHA8CKiZxt9qGXKeeiDwEiiQ8ibJYzdc
-jLkbzJUyPVUaH9ygrWynSpSTOvv/Ys3+ERrCo9W7Zuzwdmzt6TTEjFMS4lVx06us
-3uUqkdp3JMgCqCEbOFZiztICiSKrp8QFJkAfApZzBqmJOPOWH0yZ2CRRzvbQZ6af
-hqQDUalJQjWfsenyUWphhbREqExetxHJFR3OrmJt/shXVyz6dD7TBuE3PPUh1RpE
-3ejVufcTzjV3XmK79PxsKLM9V2+ww9e9V3OET57kyvn+bpSWdUYm3X4DA8dxNW6+
-kTFWRnQNZ+zQVQIDAQABo1AwTjAdBgNVHQ4EFgQUac36ccv+99N5HxYa8dCDYRaF
-HNQwHwYDVR0jBBgwFoAUac36ccv+99N5HxYa8dCDYRaFHNQwDAYDVR0TBAUwAwEB
-/zANBgkqhkiG9w0BAQUFAAOCAQEAFfu3MN8EtY5wcxOFdGShOmGQPm2MJJVE6MG+
-p4RqHrukHZSgKOyWjkRk7t6NXzNcnHco9HFv7FQRAXSJ5zObmyu+TMZlu4jHHCav
-GMcV3C/4SUGtlipZbgNe00UAIm6tM3Wh8dr38W7VYg4KGAwXou5XhQ9gCAnSn90o
-H/42NqHTjJsR4v18izX2aO25ARQdMby7Lsr5j9RqweHywiSlPusFcKRseqOnIP0d
-JT3+qh78LeMbNBO2mYD3SP/zu0TAmkAVNcj2KPw0+a0kVZ15rvslPC/K3xn9msMk
-fQthv3rDAcsWvi9YO7T+vylgZBgJfn1ZqpQqy58xN96uh6nPOw==`;
-
-function overrideCertDB() {
- // Unregister the real database.
- let registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
- let factory = registrar.getClassObject(CERTDB_CID, AM_Ci.nsIFactory);
- registrar.unregisterFactory(CERTDB_CID, factory);
-
- // Get the real DB
- let realCertDB = factory.createInstance(null, AM_Ci.nsIX509CertDB);
-
- let fakeCert = realCertDB.constructX509FromBase64(CERT.replace(/\n/g, ""));
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
- let fakeCertDB = {
- openSignedAppFileAsync(root, file, callback) {
- callback.openSignedAppFileFinished(Components.results.NS_OK, null, fakeCert);
- },
-
- verifySignedDirectoryAsync(root, dir, callback) {
- callback.verifySignedDirectoryFinished(Components.results.NS_OK, fakeCert);
- },
-
- QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIX509CertDB])
- };
-
- for (let property of Object.keys(realCertDB)) {
- if (property in fakeCertDB) {
- continue;
- }
-
- if (typeof realCertDB[property] == "function") {
- fakeCertDB[property] = realCertDB[property].bind(realCertDB);
- }
- }
-
- let certDBFactory = {
- createInstance(outer, iid) {
- if (outer != null) {
- throw Components.results.NS_ERROR_NO_AGGREGATION;
- }
- return fakeCertDB.QueryInterface(iid);
- }
- };
- registrar.registerFactory(CERTDB_CID, "CertDB",
- CERTDB_CONTRACTID, certDBFactory);
-}
+const ID = "bootstrap1@tests.mozilla.org";
add_task(async function() {
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ AddonTestUtils.manuallyInstall(do_get_addon("test_cache_certdb"), profileDir, ID);
+
startupManager();
- // Once the application is started we shouldn't be able to replace the
- // certificate database
- overrideCertDB();
+ // Force a rescan of signatures
+ const { XPIProvider } = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
+ await XPIProvider.verifySignatures();
- let install = await AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"));
- do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ let addon = await AddonManager.getAddonByID(ID);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+ do_check_false(addon.isActive);
+ do_check_true(addon.appDisabled);
});