CertUtils.jsm patch for Bug 544442 - Add support for signed AUS update snippets. r=dveditz, r=dtownsend, a=blocking2.0-Beta3
authorRobert Strong <robert.bugzilla@gmail.com>
Sun, 01 Aug 2010 19:01:43 -0700
changeset 48686 c262b545756f46d468d351725c6628ee00f369a9
parent 48685 9eb1da47fc94426352071248d35f9712dc8f183e
child 48687 14bbdcaf695f89121c81d7475ffd73c3a4180013
push idunknown
push userunknown
push dateunknown
reviewersdveditz, dtownsend, blocking2.0-Beta3
bugs544442
milestone2.0b3pre
CertUtils.jsm patch for Bug 544442 - Add support for signed AUS update snippets. r=dveditz, r=dtownsend, a=blocking2.0-Beta3
toolkit/mozapps/shared/CertUtils.jsm
toolkit/mozapps/shared/Makefile.in
toolkit/mozapps/shared/test/chrome/Makefile.in
toolkit/mozapps/shared/test/chrome/test_bug544442_checkCert.xul
--- a/toolkit/mozapps/shared/CertUtils.jsm
+++ b/toolkit/mozapps/shared/CertUtils.jsm
@@ -18,16 +18,17 @@
  * The Initial Developer of the Original Code is Ben Goodger.
  * Portions created by the Initial Developer are Copyright (C) 2004
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *  Darin Fisher <darin@meer.net>
  *  Daniel Veditz <dveditz@mozilla.com>
  *  Jesper Kristensen <mail@jesperkristensen.dk>
+ *  Robert Strong <robert.bugzilla@gmail.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -35,43 +36,101 @@
  * 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" ];
 
+const Ce = Components.Exception;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
 /**
- * Only allow built-in certs for HTTPS connections.  See bug 340198.
+ * 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.
+ * @param  aCerts
+ *         An array of JS objects with names / values corresponding to the
+ *         channel's expected certificate's attribute names / values. This can
+ *         be null or an empty array. If it isn't null 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
+ *         from the aCerts  param is different than the expected value.
+ *         NS_ERROR_ABORT if the certificate issuer is not built-in.
  */
-function checkCert(channel) {
-  if (!channel.originalURI.schemeIs("https"))  // bypass
+function checkCert(aChannel, 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.",
+                   Cr.NS_ERROR_UNEXPECTED);
+    }
     return;
+  }
 
-  const Ci = Components.interfaces;  
   var cert =
-      channel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider).
+      aChannel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider).
       SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
 
-  var issuer = cert.issuer;
-  while (issuer && !cert.equals(issuer)) {
-    cert = issuer;
-    issuer = cert.issuer;
+  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);
+    }
   }
 
-  var errorstring = "cert issuer is not built-in";
-  if (!issuer)
-    throw errorstring;
+
+  var issuerCert = cert;
+  while (issuerCert.issuer && !issuerCert.issuer.equals(issuerCert))
+    issuerCert = issuerCert.issuer;
 
-  issuer = issuer.QueryInterface(Ci.nsIX509Cert3);
-  var tokenNames = issuer.getAllTokenNames({});
+  const certNotBuiltInErr = "Certificate issuer is not built-in.";
+  if (!issuerCert)
+    throw new Ce(certNotBuiltInErr, Cr.NS_ERROR_ABORT);
 
-  if (!tokenNames.some(isBuiltinToken))
-    throw errorstring;
+  issuerCert = issuerCert.QueryInterface(Ci.nsIX509Cert3);
+  var tokenNames = issuerCert.getAllTokenNames({});
+
+  if (!tokenNames || !tokenNames.some(isBuiltinToken))
+    throw new Ce(certNotBuiltInErr, Cr.NS_ERROR_ABORT);
 }
 
 function isBuiltinToken(tokenName) {
   return tokenName == "Builtin Object Token";
 }
 
 /**
  * This class implements nsIBadCertListener.  Its job is to prevent "bad cert"
@@ -86,17 +145,17 @@ BadCertHandler.prototype = {
   // nsIChannelEventSink
   onChannelRedirect: function(oldChannel, newChannel, flags) {
     if (this.allowNonBuiltInCerts)
       return;
 
     // make sure the certificate of the old channel checks out before we follow
     // a redirect from it.  See bug 340198.
     // Don't call checkCert for internal redirects. See bug 569648.
-    if (!(flags & Components.interfaces.nsIChannelEventSink.REDIRECT_INTERNAL))
+    if (!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL))
       checkCert(oldChannel);
   },
 
   // Suppress any certificate errors
   notifyCertProblem: function(socketInfo, status, targetSite) {
     return true;
   },
 
@@ -107,17 +166,17 @@ BadCertHandler.prototype = {
 
   // nsIInterfaceRequestor
   getInterface: function(iid) {
     return this.QueryInterface(iid);
   },
 
   // nsISupports
   QueryInterface: function(iid) {
-    if (!iid.equals(Components.interfaces.nsIChannelEventSink) &&
-        !iid.equals(Components.interfaces.nsIBadCertListener2) &&
-        !iid.equals(Components.interfaces.nsISSLErrorListener) &&
-        !iid.equals(Components.interfaces.nsIInterfaceRequestor) &&
-        !iid.equals(Components.interfaces.nsISupports))
-      throw Components.results.NS_ERROR_NO_INTERFACE;
+    if (!iid.equals(Ci.nsIChannelEventSink) &&
+        !iid.equals(Ci.nsIBadCertListener2) &&
+        !iid.equals(Ci.nsISSLErrorListener) &&
+        !iid.equals(Ci.nsIInterfaceRequestor) &&
+        !iid.equals(Ci.nsISupports))
+      throw Cr.NS_ERROR_NO_INTERFACE;
     return this;
   }
 };
--- a/toolkit/mozapps/shared/Makefile.in
+++ b/toolkit/mozapps/shared/Makefile.in
@@ -44,9 +44,13 @@ include $(DEPTH)/config/autoconf.mk
 
 MODULE = toolkitShared
 
 EXTRA_PP_JS_MODULES = \
   CertUtils.jsm \
   FileUtils.jsm \
   $(NULL)
 
+ifdef ENABLE_TESTS
+DIRS += test/chrome
+endif
+
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/shared/test/chrome/Makefile.in
@@ -0,0 +1,53 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Robert Strong <robert.bugzilla@gmail.com> (Original Author)
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# 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 *****
+
+DEPTH     = ../../../../..
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+relativesrcdir = toolkit/mozapps/shared/test/chrome
+
+include $(DEPTH)/config/autoconf.mk
+
+_CHROME_FILES = \
+  test_bug544442_checkCert.xul \
+  $(NULL)
+
+include $(topsrcdir)/config/rules.mk
+
+libs:: $(_CHROME_FILES)
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/chrome/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/shared/test/chrome/test_bug544442_checkCert.xul
@@ -0,0 +1,142 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Test CertUtils.jsm checkCert - bug 340198 and bug 544442"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        onload="testStart();">
+<script type="application/javascript" 
+        src="chrome://mochikit/content/MochiKit/packed.js"/>
+<script type="application/javascript"
+        src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+SimpleTest.waitForExplicitFinish();
+
+Components.utils.import("resource://gre/modules/CertUtils.jsm");
+
+function testStart() {
+  ok(true, "Entering testStart");
+
+  var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+                createInstance(Ci.nsIXMLHttpRequest);
+  request.open("GET", "https://example.com/", true);
+  request.channel.notificationCallbacks = new BadCertHandler(true);
+  request.onerror = function(event) { testXHRError(event); };
+  request.onload = function(event) { testXHRLoad(event); };
+  request.send(null);
+}
+
+function testXHRError(aEvent) {
+  ok(true, "Entering testXHRError - something went wrong");
+
+  var request = aEvent.target;
+  var status = 0;
+  try {
+    status = request.status;
+  }
+  catch (e) {
+  }
+
+  if (status == 0)
+    status = request.channel.QueryInterface(Ci.nsIRequest).status;
+
+  ok(false, "XHR onerror called: " + status);
+
+  SimpleTest.finish();
+}
+
+function getCheckCertResult(aChannel, aCerts) {
+  try {
+    checkCert(aChannel, aCerts);
+  }
+  catch (e) {
+    return e.result;
+  }
+  return Cr.NS_OK;
+}
+
+function testXHRLoad(aEvent) {
+  ok(true, "Entering testXHRLoad");
+
+  var channel = aEvent.target.channel;
+
+  var certs = null;
+  is(getCheckCertResult(channel, certs), Cr.NS_ERROR_ABORT,
+     "checkCert should throw NS_ERROR_ABORT when the certificate attributes " +
+     "array passed to checkCert is null and the certificate is not builtin");
+
+  certs = [ { invalidAttribute: "Invalid attribute" } ];
+  is(getCheckCertResult(channel, certs), Cr.NS_ERROR_ILLEGAL_VALUE,
+     "checkCert should throw NS_ERROR_ILLEGAL_VALUE when the certificate " +
+     "attributes array passed to checkCert has an element that has an " +
+     "attribute that does not exist on the certificate");
+
+  certs = [ { issuerName: "Incorrect issuerName" } ];
+  is(getCheckCertResult(channel, certs), Cr.NS_ERROR_ILLEGAL_VALUE,
+     "checkCert should throw NS_ERROR_ILLEGAL_VALUE when the certificate " +
+     "attributes array passed to checkCert has an element that has an " +
+     "issuerName that is not the same as the certificate's");
+
+  var cert = channel.securityInfo.QueryInterface(Ci.nsISSLStatusProvider).
+             SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
+
+  certs = [ { issuerName: cert.issuerName,
+              commonName: cert.commonName } ];
+  is(getCheckCertResult(channel, certs), Cr.NS_ERROR_ABORT,
+     "checkCert should throw NS_ERROR_ABORT when the certificate attributes " +
+     "array passed to checkCert has a single element that has the same " +
+     "issuerName and commonName as the certificate's and the certificate is " +
+     "not builtin");
+
+  certs = [ { issuerName: "Incorrect issuerName",
+              invalidAttribute: "Invalid attribute" },
+            { issuerName: cert.issuerName,
+              commonName: "Invalid Common Name" },
+            { issuerName: cert.issuerName,
+              commonName: cert.commonName } ];
+  is(getCheckCertResult(channel, certs), Cr.NS_ERROR_ABORT,
+     "checkCert should throw NS_ERROR_ABORT when the certificate attributes " +
+     "array passed to checkCert has an element that has the same issuerName " +
+     "and commonName as the certificate's and the certificate is not builtin");
+
+  var mockChannel = { originalURI: Cc["@mozilla.org/network/io-service;1"].
+                                   getService(Ci.nsIIOService).
+                                   newURI("http://example.com/", null, null) };
+
+  certs = [ ];
+  is(getCheckCertResult(mockChannel, certs), Cr.NS_ERROR_UNEXPECTED,
+     "checkCert should throw NS_ERROR_UNEXPECTED when the certificate " +
+     "attributes array passed to checkCert is not null and the channel's " +
+     "originalURI is not https");
+
+  certs = null;
+  is(getCheckCertResult(mockChannel, certs), Cr.NS_OK,
+     "checkCert should not throw when the certificate attributes object " +
+     "passed to checkCert is null and the the channel's originalURI is not " +
+     "https");
+
+  SimpleTest.finish();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test"></pre>
+</body>
+</window>