Bug 704988: Check the add-on hotfix is signed by a specific certificate. r=robstrong, r=Unfocused
authorDave Townsend <dtownsend@oxymoronical.com>
Fri, 16 Dec 2011 12:04:28 -0800
changeset 83608 e5d8d2fb987db970e2176c7f857cdc14f349f062
parent 83607 e7cd9a56b5dadd17cea77d831e29ad1998ad449d
child 83609 ba447ace2594aa48b6dff05fc479728bd6c6aff1
push idunknown
push userunknown
push dateunknown
reviewersrobstrong, Unfocused
bugs704988
milestone11.0a1
Bug 704988: Check the add-on hotfix is signed by a specific certificate. r=robstrong, r=Unfocused
browser/app/profile/firefox.js
build/automation.py.in
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/test/browser/Makefile.in
toolkit/mozapps/extensions/test/browser/browser_hotfix.js
toolkit/mozapps/extensions/test/browser/signed_hotfix.rdf
toolkit/mozapps/extensions/test/browser/signed_hotfix.xpi
toolkit/mozapps/extensions/test/browser/unsigned_hotfix.rdf
toolkit/mozapps/extensions/test/browser/unsigned_hotfix.xpi
toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js
toolkit/mozapps/shared/CertUtils.jsm
toolkit/mozapps/shared/test/unit/test_readCertPrefs.js
toolkit/mozapps/shared/test/unit/xpcshell.ini
toolkit/mozapps/update/nsUpdateService.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -77,16 +77,18 @@ pref("extensions.blocklist.interval", 86
 pref("extensions.blocklist.level", 2);
 pref("extensions.blocklist.url", "https://addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
 pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/");
 pref("extensions.blocklist.itemURL", "https://addons.mozilla.org/%LOCALE%/%APP%/blocked/%blockID%");
 
 pref("extensions.update.autoUpdateDefault", true);
 
 pref("extensions.hotfix.id", "firefox-hotfix@mozilla.org");
+pref("extensions.hotfix.cert.checkAttributes", true);
+pref("extensions.hotfix.certs.1.sha1Fingerprint", "foo");
 
 // Disable add-ons installed into the shared user and shared system areas by
 // default. This does not include the application directory. See the SCOPE
 // constants in AddonManager.jsm for values to use here
 pref("extensions.autoDisableScopes", 15);
 
 // Dictionary download preference
 pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/");
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -391,16 +391,17 @@ user_pref("camino.warn_when_closing", fa
 user_pref("urlclassifier.updateinterval", 172800);
 // Point the url-classifier to the local testing server for fast failures
 user_pref("browser.safebrowsing.provider.0.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
 user_pref("browser.safebrowsing.provider.0.keyURL", "http://%(server)s/safebrowsing-dummy/newkey");
 user_pref("browser.safebrowsing.provider.0.updateURL", "http://%(server)s/safebrowsing-dummy/update");
 // Point update checks to the local testing server for fast failures
 user_pref("extensions.update.url", "http://%(server)s/extensions-dummy/updateURL");
 user_pref("extensions.blocklist.url", "http://%(server)s/extensions-dummy/blocklistURL");
+user_pref("extensions.hotfix.url", "http://%(server)s/extensions-dummy/hotfixURL");
 // Make sure opening about:addons won't hit the network
 user_pref("extensions.webservice.discoverURL", "http://%(server)s/extensions-dummy/discoveryURL");
 // Make sure AddonRepository won't hit the network
 user_pref("extensions.getAddons.maxResults", 0);
 user_pref("extensions.getAddons.get.url", "http://%(server)s/extensions-dummy/repositoryGetURL");
 user_pref("extensions.getAddons.search.browseURL", "http://%(server)s/extensions-dummy/repositoryBrowseURL");
 user_pref("extensions.getAddons.search.url", "http://%(server)s/extensions-dummy/repositorySearchURL");
 """ % { "server" : self.webServer + ":" + str(self.httpPort) }
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -51,30 +51,34 @@ const PREF_EM_LAST_PLATFORM_VERSION   = 
 const PREF_EM_AUTOUPDATE_DEFAULT      = "extensions.update.autoUpdateDefault";
 const PREF_EM_STRICT_COMPATIBILITY    = "extensions.strictCompatibility";
 const PREF_EM_UPDATE_URL              = "extensions.update.url";
 const PREF_APP_UPDATE_ENABLED         = "app.update.enabled";
 const PREF_APP_UPDATE_AUTO            = "app.update.auto";
 const PREF_EM_HOTFIX_ID               = "extensions.hotfix.id";
 const PREF_EM_HOTFIX_LASTVERSION      = "extensions.hotfix.lastVersion";
 const PREF_EM_HOTFIX_URL              = "extensions.hotfix.url";
+const PREF_EM_CERT_CHECKATTRIBUTES    = "extensions.hotfix.cert.checkAttributes";
+const PREF_EM_HOTFIX_CERTS            = "extensions.hotfix.certs.";
 const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
 const PREF_SELECTED_LOCALE            = "general.useragent.locale";
 
 const UPDATE_REQUEST_VERSION          = 2;
 const CATEGORY_UPDATE_PARAMS          = "extension-update-params";
 
 // Note: This has to be kept in sync with the same constant in AddonRepository.jsm
 const STRICT_COMPATIBILITY_DEFAULT    = true;
 
 const TOOLKIT_ID                      = "toolkit@mozilla.org";
 
 const VALID_TYPES_REGEXP = /^[\w\-]+$/;
 
 Components.utils.import("resource://gre/modules/Services.jsm");
+var CertUtils = {};
+Components.utils.import("resource://gre/modules/CertUtils.jsm", CertUtils);
 
 var EXPORTED_SYMBOLS = [ "AddonManager", "AddonManagerPrivate" ];
 
 const CATEGORY_PROVIDER_MODULE = "addon-provider-module";
 
 // A list of providers to load by default
 const DEFAULT_PROVIDERS = [
   "resource://gre/modules/XPIProvider.jsm",
@@ -826,16 +830,37 @@ var AddonManagerInternal = {
           if (Services.vc.compare(hotfixVersion, update.version) >= 0) {
             notifyComplete();
             return;
           }
 
           LOG("Downloading hotfix version " + update.version);
           AddonManager.getInstallForURL(update.updateURL, function(aInstall) {
             aInstall.addListener({
+              onDownloadEnded: function(aInstall) {
+                try {
+                  if (!Services.prefs.getBoolPref(PREF_EM_CERT_CHECKATTRIBUTES))
+                    return;
+                }
+                catch (e) {
+                  // By default don't do certificate checks.
+                  return;
+                }
+
+                try {
+                  CertUtils.validateCert(aInstall.certificate,
+                                         CertUtils.readCertPrefs(PREF_EM_HOTFIX_CERTS));
+                }
+                catch (e) {
+                  WARN("The hotfix add-on was not signed by the expected " +
+                       "certificate and so will not be installed.");
+                  aInstall.cancel();
+                }
+              },
+
               onInstallEnded: function(aInstall) {
                 // Remember the last successfully installed version.
                 Services.prefs.setCharPref(PREF_EM_HOTFIX_LASTVERSION,
                                            aInstall.version);
               },
 
               onInstallCancelled: function(aInstall) {
                 // Revert to the previous version if the installation was
--- a/toolkit/mozapps/extensions/test/browser/Makefile.in
+++ b/toolkit/mozapps/extensions/test/browser/Makefile.in
@@ -92,16 +92,17 @@ include $(DEPTH)/config/autoconf.mk
   browser_inlinesettings.js \
   browser_tabsettings.js \
   $(NULL)
 
 _TEST_FILES = \
   head.js \
   browser_bug557956.js \
   browser_bug616841.js \
+  browser_hotfix.js \
   browser_updatessl.js \
   browser_installssl.js \
   browser_newaddon.js \
   browser_select_selection.js \
   browser_select_compatoverrides.js \
   browser_select_confirm.js \
   browser_select_update.js \
   $(NULL)
@@ -122,16 +123,20 @@ include $(DEPTH)/config/autoconf.mk
   browser_updatessl.rdf^headers^ \
   browser_install.rdf \
   browser_install.rdf^headers^ \
   browser_install.xml \
   browser_install1_3.xpi \
   browser_eula.xml \
   browser_purchase.xml \
   discovery.html \
+  signed_hotfix.rdf \
+  signed_hotfix.xpi \
+  unsigned_hotfix.rdf \
+  unsigned_hotfix.xpi \
   more_options.xul \
   options.xul \
   redirect.sjs \
   releaseNotes.xhtml \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_hotfix.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const PREF_EM_HOTFIX_ID                = "extensions.hotfix.id";
+const PREF_EM_HOTFIX_LASTVERSION       = "extensions.hotfix.lastVersion";
+const PREF_EM_HOTFIX_URL               = "extensions.hotfix.url";
+const PREF_EM_HOTFIX_CERTS             = "extensions.hotfix.certs.";
+const PREF_EM_CERT_CHECKATTRIBUTES     = "extensions.hotfix.cert.checkAttributes";
+
+const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+const PREF_UPDATE_REQUIREBUILTINCERTS  = "extensions.update.requireBuiltInCerts";
+
+const PREF_APP_UPDATE_ENABLED          = "app.update.enabled";
+
+const HOTFIX_ID = "hotfix@tests.mozilla.org";
+
+var gNextTest;
+
+var SuccessfulInstallListener = {
+  onDownloadCancelled: function(aInstall) {
+    ok(false, "Should not have seen the download cancelled");
+    is(aInstall.addon.id, HOTFIX_ID, "Should have seen the right add-on");
+
+    AddonManager.removeInstallListener(this);
+    gNextTest();
+  },
+
+  onInstallEnded: function(aInstall) {
+    ok(true, "Should have seen the install complete");
+    is(aInstall.addon.id, HOTFIX_ID, "Should have installed the right add-on");
+
+    AddonManager.removeInstallListener(this);
+    aInstall.addon.uninstall();
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_LASTVERSION);
+    gNextTest();
+  }
+}
+
+var FailedInstallListener = {
+  onDownloadCancelled: function(aInstall) {
+    ok(true, "Should have seen the download cancelled");
+    is(aInstall.addon.id, HOTFIX_ID, "Should have seen the right add-on");
+
+    AddonManager.removeInstallListener(this);
+    gNextTest();
+  },
+
+  onInstallEnded: function(aInstall) {
+    ok(false, "Should not have seen the install complete");
+    is(aInstall.addon.id, HOTFIX_ID, "Should have installed the right add-on");
+
+    AddonManager.removeInstallListener(this);
+    aInstall.addon.uninstall();
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_LASTVERSION);
+    gNextTest();
+  }
+}
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true);
+  Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+  Services.prefs.setBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, false);
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_ID, HOTFIX_ID);
+  var oldURL = Services.prefs.getCharPref(PREF_EM_HOTFIX_URL);
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, TESTROOT + "signed_hotfix.rdf");
+
+  registerCleanupFunction(function() {
+    Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_ID);
+    Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, oldURL);
+    Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+    Services.prefs.clearUserPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+
+    Services.prefs.clearUserPref(PREF_EM_CERT_CHECKATTRIBUTES);
+    var prefs = Services.prefs.getChildList(PREF_EM_HOTFIX_CERTS);
+    prefs.forEach(Services.prefs.clearUserPref);
+  });
+
+  run_next_test();
+}
+
+function end_test() {
+  finish();
+}
+
+add_test(function check_no_cert_checks() {
+  Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, false);
+  AddonManager.addInstallListener(SuccessfulInstallListener);
+
+  gNextTest = run_next_test;
+
+  AddonManagerPrivate.backgroundUpdateCheck();
+});
+
+add_test(function check_wrong_cert_fingerprint() {
+  Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "foo");
+
+  AddonManager.addInstallListener(FailedInstallListener);
+
+  gNextTest = function() {
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+
+    run_next_test();
+  };
+
+  AddonManagerPrivate.backgroundUpdateCheck();
+});
+
+add_test(function check_right_cert_fingerprint() {
+  Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
+
+  AddonManager.addInstallListener(SuccessfulInstallListener);
+
+  gNextTest = function() {
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+
+    run_next_test();
+  };
+
+  AddonManagerPrivate.backgroundUpdateCheck();
+});
+
+add_test(function check_multi_cert_fingerprint_1() {
+  Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint", "foo");
+
+  AddonManager.addInstallListener(SuccessfulInstallListener);
+
+  gNextTest = function() {
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint");
+
+    run_next_test();
+  };
+
+  AddonManagerPrivate.backgroundUpdateCheck();
+});
+
+add_test(function check_multi_cert_fingerprint_2() {
+  Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "foo");
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
+
+  AddonManager.addInstallListener(SuccessfulInstallListener);
+
+  gNextTest = function() {
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint");
+
+    run_next_test();
+  };
+
+  AddonManagerPrivate.backgroundUpdateCheck();
+});
+
+add_test(function check_no_cert_no_checks() {
+  Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, false);
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, TESTROOT + "unsigned_hotfix.rdf");
+
+  AddonManager.addInstallListener(SuccessfulInstallListener);
+
+  gNextTest = run_next_test;
+
+  AddonManagerPrivate.backgroundUpdateCheck();
+});
+
+add_test(function check_no_cert_cert_fingerprint_check() {
+  Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+  Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
+
+  AddonManager.addInstallListener(FailedInstallListener);
+
+  gNextTest = function() {
+    Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+
+    run_next_test();
+  };
+
+  AddonManagerPrivate.backgroundUpdateCheck();
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/signed_hotfix.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+
+<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:extension:hotfix@tests.mozilla.org">
+    <em:updates>
+      <Seq>
+        <li>
+          <Description>
+            <em:version>1.0</em:version>
+            <em:targetApplication>
+              <Description>
+                <em:id>toolkit@mozilla.org</em:id>
+                <em:minVersion>0</em:minVersion>
+                <em:maxVersion>*</em:maxVersion>
+                <em:updateLink>https://example.com/browser/toolkit/mozapps/extensions/test/browser/signed_hotfix.xpi</em:updateLink>
+              </Description>
+            </em:targetApplication>
+          </Description>
+        </li>
+      </Seq>
+    </em:updates>
+  </Description>
+
+</RDF>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bd1890573800d2213c44ac63b261f5077993aa6e
GIT binary patch
literal 2745
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
Qt07B!)>49o?EABS1F?!pQvd(}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+
+<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:extension:hotfix@tests.mozilla.org">
+    <em:updates>
+      <Seq>
+        <li>
+          <Description>
+            <em:version>1.0</em:version>
+            <em:targetApplication>
+              <Description>
+                <em:id>toolkit@mozilla.org</em:id>
+                <em:minVersion>0</em:minVersion>
+                <em:maxVersion>*</em:maxVersion>
+                <em:updateLink>https://example.com/browser/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.xpi</em:updateLink>
+              </Description>
+            </em:targetApplication>
+          </Description>
+        </li>
+      </Seq>
+    </em:updates>
+  </Description>
+
+</RDF>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f2d475bd2339df39954a6ddc7b1ca9aed49abed6
GIT binary patch
literal 560
zc$^FHW@Zs#U}E54sAz1oFZv>2X3ogK(7?pNz|By=keOFpl9-dDSCo<#>=MnegaHUR
z7}_Tv`kS1RkSUOukYJF|z#y-XmcnWr>DqYY`(4v(^Y5&F?U%WB#-}F9?J~;3%*?{h
zEX>9cQ7sSc7uzr^E4!=}m@%oL!_e`FNJ9(LjI~UH$CK3=LPK~N*bj@}@S6_Br9syD
zhYSSnP5i;1cSB3EeG7LWN64ZT-qTmTs%v0eUFY)c;*!5#OXoN+ZHd#V=f7W`p8na(
z^V89_!fxjmowVqC_Orl1CMEy0=*Kx_n_o0UEa{n=YN;oG?AW`TDOPhgStNQoy)ARU
zyWMTYin7HS77Aav&P`d(z1hg#$&15WrSQ**9jo3h)(BBii7wi{agoFP)&sX>3dLmC
z`bEcUZE>D3N$Wz=aoKD8c10Wu)!O#+-~+!9?k_#Bub+xIT>fa={X5&GPh8&Fc`-xa
z&Ai9KfgT$fb8cL?<$A`r!mshz3IE3KdgeJ>yk7Kg%bUE2Jw|h(mEiZ@P0Z&O-pF0G
z;P2}GY5NvWS+nM%$Gor8_7t_KDhPO0FTS*(;-8S3_r1R<hvzcBG09H8u&`Chz50pc
zMaQ?7<Bp!5@jt%qKWl(DBa=8ct{4>rMIZwsg95|0Mi2`nep$fr8ydn2iHHDiRyK%g
NMg}h+eSs0g0|0cF<c9zN
--- a/toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js
@@ -1,16 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 // This verifies that hotfix installation works
 
 // The test extension uses an insecure update url.
 Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+// Ignore any certificate requirements the app has set
+Services.prefs.setBoolPref("extensions.hotfix.cert.checkAttributes", false);
 
 do_load_httpd_js();
 var testserver;
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
 function run_test() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
--- a/toolkit/mozapps/shared/CertUtils.jsm
+++ b/toolkit/mozapps/shared/CertUtils.jsm
@@ -34,23 +34,126 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 #endif
-EXPORTED_SYMBOLS = [ "BadCertHandler", "checkCert" ];
+EXPORTED_SYMBOLS = [ "BadCertHandler", "checkCert", "readCertPrefs", "validateCert" ];
 
 const Ce = Components.Exception;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Reads a set of expected certificate attributes from preferences. The returned
+ * array can be passed to validateCert or checkCert to validate that a
+ * certificate matches the expected attributes. The preferences should look like
+ * this:
+ *   prefix.1.attribute1
+ *   prefix.1.attribute2
+ *   prefix.2.attribute1
+ *   etc.
+ * Each numeric branch contains a set of required attributes for a single
+ * certificate. Having multiple numeric branches means that multiple
+ * certificates would be accepted by validateCert.
+ *
+ * @param  aPrefBranch
+ *         The prefix for all preferences, should end with a ".".
+ * @return An array of JS objects with names / values corresponding to the
+ *         expected certificate's attribute names / values.
+ */
+function readCertPrefs(aPrefBranch) {
+  if (Services.prefs.getBranch(aPrefBranch).getChildList("").length == 0)
+    return null;
+
+  let certs = [];
+  let counter = 1;
+  while (true) {
+    let prefBranchCert = Services.prefs.getBranch(aPrefBranch + counter + ".");
+    let prefCertAttrs = prefBranchCert.getChildList("");
+    if (prefCertAttrs.length == 0)
+      break;
+
+    let certAttrs = {};
+    for each (let prefCertAttr in prefCertAttrs)
+      certAttrs[prefCertAttr] = prefBranchCert.getCharPref(prefCertAttr);
+
+    certs.push(certAttrs);
+    counter++;
+  }
+
+  return certs;
+}
+
+/**
+ * Verifies that an nsIX509Cert matches the expected certificate attribute
+ * values.
+ *
+ * @param  aCertificate
+ *         The nsIX509Cert to compare to the expected attributes.
+ * @param  aCerts
+ *         An array of JS objects with names / values corresponding to the
+ *         expected certificate's attribute names / values. If this is null or
+ *         an empty array then no checks are performed.
+ * @throws NS_ERROR_ILLEGAL_VALUE if a certificate attribute name from the
+ *         aCerts param does not exist or the value for a certificate attribute
+ *         from the aCerts param is different than the expected value or
+ *         aCertificate wasn't specified and aCerts is not null or an empty
+ *         array.
+ */
+function validateCert(aCertificate, aCerts) {
+  // If there are no certificate requirements then just exit
+  if (!aCerts || aCerts.length == 0)
+    return;
+
+  if (!aCertificate) {
+    const missingCertErr = "A required certificate was not present.";
+    Cu.reportError(missingCertErr);
+    throw new Ce(missingCertErr, Cr.NS_ERROR_ILLEGAL_VALUE);
+  }
+
+  var errors = [];
+  for (var i = 0; i < aCerts.length; ++i) {
+    var error = false;
+    var certAttrs = aCerts[i];
+    for (var name in certAttrs) {
+      if (!(name in aCertificate)) {
+        error = true;
+        errors.push("Expected attribute '" + name + "' not present in " +
+                    "certificate.");
+        break;
+      }
+      if (aCertificate[name] != certAttrs[name]) {
+        error = true;
+        errors.push("Expected certificate attribute '" + name + "' " +
+                    "value incorrect, expected: '" + certAttrs[name] +
+                    "', got: '" + aCertificate[name] + "'.");
+        break;
+      }
+    }
+
+    if (!error)
+      break;
+  }
+
+  if (error) {
+    errors.forEach(Cu.reportError);
+    const certCheckErr = "Certificate checks failed. See previous errors " +
+                         "for details.";
+    Cu.reportError(certCheckErr);
+    throw new Ce(certCheckErr, Cr.NS_ERROR_ILLEGAL_VALUE);
+  }
+}
+
 /**
  * Checks if the connection must be HTTPS and if so, only allows built-in
  * certificates and validates application specified certificate attribute
  * values.
  * See bug 340198 and bug 544442.
  *
  * @param  aChannel
  *         The nsIChannel that will have its certificate checked.
@@ -60,17 +163,17 @@ const Cu = Components.utils;
  * @param  aCerts (optional)
  *         An array of JS objects with names / values corresponding to the
  *         channel's expected certificate's attribute names / values. If it
  *         isn't null or not specified the the scheme for the channel's
  *         originalURI must be https.
  * @throws NS_ERROR_UNEXPECTED if a certificate is expected and the URI scheme
  *         is not https.
  *         NS_ERROR_ILLEGAL_VALUE if a certificate attribute name from the
- *         cert param does not exist or the value for a certificate attribute
+ *         aCerts param does not exist or the value for a certificate attribute
  *         from the aCerts  param is different than the expected value.
  *         NS_ERROR_ABORT if the certificate issuer is not built-in.
  */
 function checkCert(aChannel, aAllowNonBuiltInCerts, aCerts) {
   if (!aChannel.originalURI.schemeIs("https")) {
     // Require https if there are certificate values to verify
     if (aCerts) {
       throw new Ce("SSL is required and URI scheme is not https.",
@@ -78,47 +181,17 @@ function checkCert(aChannel, aAllowNonBu
     }
     return;
   }
 
   var cert =
       aChannel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider).
       SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
 
-  if (aCerts) {
-    for (var i = 0; i < aCerts.length; ++i) {
-      var error = false;
-      var certAttrs = aCerts[i];
-      for (var name in certAttrs) {
-        if (!(name in cert)) {
-          error = true;
-          Cu.reportError("Expected attribute '" + name + "' not present in " +
-                         "certificate.");
-          break;
-        }
-        if (cert[name] != certAttrs[name]) {
-          error = true;
-          Cu.reportError("Expected certificate attribute '" + name + "' " +
-                         "value incorrect, expected: '" + certAttrs[name] +
-                         "', got: '" + cert[name] + "'.");
-          break;
-        }
-      }
-
-      if (!error)
-        break;
-    }
-
-    if (error) {
-      const certCheckErr = "Certificate checks failed. See previous errors " +
-                           "for details.";
-      Cu.reportError(certCheckErr);
-      throw new Ce(certCheckErr, Cr.NS_ERROR_ILLEGAL_VALUE);
-    }
-  }
+  validateCert(cert, aCerts);
 
   if (aAllowNonBuiltInCerts ===  true)
     return;
 
   var issuerCert = cert;
   while (issuerCert.issuer && !issuerCert.issuer.equals(issuerCert))
     issuerCert = issuerCert.issuer;
 
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/shared/test/unit/test_readCertPrefs.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/CertUtils.jsm");
+
+const PREF_PREFIX = "certutils.certs.";
+
+function run_test() {
+  run_next_test();
+}
+
+function resetPrefs() {
+  var prefs = Services.prefs.getChildList(PREF_PREFIX);
+  prefs.forEach(Services.prefs.clearUserPref);
+}
+
+function attributes_match(aCert, aExpected) {
+  if (Object.keys(aCert).length != Object.keys(aExpected).length)
+    return false;
+
+  for (var attribute in aCert) {
+    if (!(attribute in aExpected))
+      return false;
+    if (aCert[attribute] != aExpected[attribute])
+      return false;
+  }
+
+  return true;
+}
+
+function test_results(aCerts, aExpected) {
+  do_check_eq(aCerts.length, aExpected.length);
+
+  for (var i = 0; i < aCerts.length; i++) {
+    if (!attributes_match(aCerts[i], aExpected[i])) {
+      dump("Attributes for certificate " + (i + 1) + " did not match expected attributes\n");
+      dump("Saw: " + aCerts[i].toSource() + "\n");
+      dump("Expected: " + aExpected[i].toSource() + "\n");
+      do_check_true(false);
+    }
+  }
+}
+
+add_test(function test_singleCert() {
+  Services.prefs.setCharPref(PREF_PREFIX + "1.attribute1", "foo");
+  Services.prefs.setCharPref(PREF_PREFIX + "1.attribute2", "bar");
+
+  var certs = readCertPrefs(PREF_PREFIX);
+  test_results(certs, [{
+    attribute1: "foo",
+    attribute2: "bar"
+  }]);
+
+  resetPrefs();
+  run_next_test();
+});
+
+add_test(function test_multipleCert() {
+  Services.prefs.setCharPref(PREF_PREFIX + "1.md5Fingerprint", "cf84a9a2a804e021f27cb5128fe151f4");
+  Services.prefs.setCharPref(PREF_PREFIX + "1.nickname", "1st cert");
+  Services.prefs.setCharPref(PREF_PREFIX + "2.md5Fingerprint", "9441051b7eb50e5ca2226095af710c1a");
+  Services.prefs.setCharPref(PREF_PREFIX + "2.nickname", "2nd cert");
+
+  var certs = readCertPrefs(PREF_PREFIX);
+  test_results(certs, [{
+    md5Fingerprint: "cf84a9a2a804e021f27cb5128fe151f4",
+    nickname: "1st cert"
+  }, {
+    md5Fingerprint: "9441051b7eb50e5ca2226095af710c1a",
+    nickname: "2nd cert"
+  }]);
+
+  resetPrefs();
+  run_next_test();
+});
+
+add_test(function test_skippedCert() {
+  Services.prefs.setCharPref(PREF_PREFIX + "1.issuerName", "Mozilla");
+  Services.prefs.setCharPref(PREF_PREFIX + "1.nickname", "1st cert");
+  Services.prefs.setCharPref(PREF_PREFIX + "2.issuerName", "Top CA");
+  Services.prefs.setCharPref(PREF_PREFIX + "2.nickname", "2nd cert");
+  Services.prefs.setCharPref(PREF_PREFIX + "4.issuerName", "Unknown CA");
+  Services.prefs.setCharPref(PREF_PREFIX + "4.nickname", "Ignored cert");
+
+  var certs = readCertPrefs(PREF_PREFIX);
+  test_results(certs, [{
+    issuerName: "Mozilla",
+    nickname: "1st cert"
+  }, {
+    issuerName: "Top CA",
+    nickname: "2nd cert"
+  }]);
+
+  resetPrefs();
+  run_next_test();
+});
--- a/toolkit/mozapps/shared/test/unit/xpcshell.ini
+++ b/toolkit/mozapps/shared/test/unit/xpcshell.ini
@@ -1,5 +1,7 @@
 [DEFAULT]
 head = 
 tail = 
 
 [test_FileUtils.js]
+
+[test_readCertPrefs.js]
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -2389,34 +2389,18 @@ Checker.prototype = {
    *          The nsIDOMEvent for the load
    */
   onLoad: function UC_onLoad(event) {
     LOG("Checker:onLoad - request completed downloading document");
 
     var prefs = Services.prefs;
     var certs = null;
     if (!prefs.prefHasUserValue(PREF_APP_UPDATE_URL_OVERRIDE) &&
-        getPref("getBoolPref", PREF_APP_UPDATE_CERT_CHECKATTRS, true) &&
-        prefs.getBranch(PREF_APP_UPDATE_CERTS_BRANCH).getChildList("").length) {
-      certs = [];
-      let counter = 1;
-      while (true) {
-        let prefBranchCert = prefs.getBranch(PREF_APP_UPDATE_CERTS_BRANCH +
-                                             counter + ".");
-        let prefCertAttrs = prefBranchCert.getChildList("");
-        if (prefCertAttrs.length == 0)
-          break;
-
-        let certAttrs = {};
-        for each (let prefCertAttr in prefCertAttrs)
-          certAttrs[prefCertAttr] = prefBranchCert.getCharPref(prefCertAttr);
-
-        certs.push(certAttrs);
-        counter++;
-      }
+        getPref("getBoolPref", PREF_APP_UPDATE_CERT_CHECKATTRS, true)) {
+      certs = gCertUtils.readCertPrefs(PREF_APP_UPDATE_CERTS_BRANCH);
     }
 
     try {
       // Analyze the resulting DOM and determine the set of updates.
       var updates = this._updates;
       LOG("Checker:onLoad - number of updates available: " + updates.length);
       var allowNonBuiltIn = !getPref("getBoolPref",
                                      PREF_APP_UPDATE_CERT_REQUIREBUILTIN, true);