Bug 1103196 - Add ability to ignore invalid TLS certificates; r=automatedtester,keeler,mossop draft
authorAndreas Tolfsen <ato@mozilla.com>
Sun, 06 Nov 2016 18:03:31 +0000
changeset 444163 c165681fab068fbadbcbdefe572018101ae169c9
parent 444162 f1adbcdf6e443fdf2beb2a2165fbfc6152477312
child 538244 8d268a1eef06ed0dfcc5a402e3834b522fe764c0
push id37208
push userbmo:ato@mozilla.com
push dateSat, 26 Nov 2016 15:03:07 +0000
reviewersautomatedtester, keeler, mossop
bugs1103196
milestone53.0a1
Bug 1103196 - Add ability to ignore invalid TLS certificates; r=automatedtester,keeler,mossop When the `acceptInsecureCerts` capability is set to true on creating a new Marionette session, a `nsICertOverrideService` override service is installed that causes all invalid TLS certificates to be ignored. This is in line with the expectations of the WebDriver specification. It is worth noting that this is a potential security risk and that this feature is only available in Gecko when the Marionette server is enabled. MozReview-Commit-ID: BXrQw17TgDy
testing/marionette/cert.js
testing/marionette/driver.js
testing/marionette/harness/marionette/tests/unit/test_capabilities.py
testing/marionette/harness/marionette/tests/unit/test_navigation.py
testing/marionette/jar.mn
new file mode 100644
--- /dev/null
+++ b/testing/marionette/cert.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["cert"];
+
+const registrar =
+    Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+const sss = Cc["@mozilla.org/ssservice;1"]
+    .getService(Ci.nsISiteSecurityService);
+
+const CONTRACT_ID = "@mozilla.org/security/certoverride;1";
+const CERT_PINNING_ENFORCEMENT_PREF =
+    "security.cert_pinning.enforcement_level";
+const HSTS_PRELOAD_LIST_PREF =
+    "network.stricttransportsecurity.preloadlist";
+
+/** TLS certificate service override management for Marionette. */
+this.cert = {
+  Error: {
+    Untrusted: 1,
+    Mismatch: 2,
+    Time: 4,
+  },
+
+  currentOverride: null,
+};
+
+/**
+ * Installs a TLS certificate service override.
+ *
+ * The provided |service| must implement the |register| and |unregister|
+ * functions that causes a new |nsICertOverrideService| interface
+ * implementation to be registered with the |nsIComponentRegistrar|.
+ *
+ * After |service| is registered and made the |cert.currentOverride|,
+ * |nsICertOverrideService| is reinitialised to cause all Gecko components
+ * to pick up the new service.
+ *
+ * If an override is already installed, i.e. when |cert.currentOverride|
+ * is not null, this functions acts as a NOOP.
+ *
+ * @param {cert.Override} service
+ *     Service generator that registers and unregisters the XPCOM service.
+ *
+ * @throws {Components.Exception}
+ *     If unable to register or initialise |service|.
+ */
+cert.installOverride = function(service) {
+  if (this.currentOverride) {
+    return;
+  }
+
+  service.register();
+  cert.currentOverride = service;
+};
+
+/**
+ * Uninstall a TLS certificate service override.
+ *
+ * After the service has been unregistered, |cert.currentOverride|
+ * is reset to null.
+ *
+ * If there no current override installed, i.e. if |cert.currentOverride|
+ * is null, this function acts as a NOOP.
+ */
+cert.uninstallOverride = function() {
+  if (!cert.currentOverride) {
+    return;
+  }
+  cert.currentOverride.unregister();
+  this.currentOverride = null;
+};
+
+/**
+ * Certificate override service that acts in an all-inclusive manner
+ * on TLS certificates.
+ *
+ * When an invalid certificate is encountered, it is overriden
+ * with the |matching| bit level, which is typically a combination of
+ * |cert.Error.Untrusted|, |cert.Error.Mismatch|, and |cert.Error.Time|.
+ *
+ * @type cert.Override
+ *
+ * @throws {Components.Exception}
+ *     If there are any problems registering the service.
+ */
+cert.InsecureSweepingOverride = function() {
+  const CID = Components.ID("{4b67cce0-a51c-11e6-9598-0800200c9a66}");
+  const DESC = "All-encompassing cert service that matches on a bitflag";
+
+  // This needs to be an old-style class with a function constructor
+  // and prototype assignment because... XPCOM.  Any attempt at
+  // modernisation will be met with cryptic error messages which will
+  // make your life miserable.
+  let service = function() {};
+  service.prototype = {
+    hasMatchingOverride: function(
+        aHostName, aPort, aCert, aOverrideBits, aIsTemporary) {
+      aIsTemporary.value = false;
+      aOverrideBits.value =
+          cert.Error.Untrusted | cert.Error.Mismatch | cert.Error.Time;
+
+      return true;
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsICertOverrideService]),
+  };
+  let factory = XPCOMUtils.generateSingletonFactory(service);
+
+  return {
+    register: function() {
+      // make it possible to register certificate overrides for domains
+      // that use HSTS or HPKP
+      Preferences.set(HSTS_PRELOAD_LIST_PREF, false);
+      Preferences.set(CERT_PINNING_ENFORCEMENT_PREF, 0);
+
+      registrar.registerFactory(CID, DESC, CONTRACT_ID, factory);
+    },
+
+    unregister: function() {
+      registrar.unregisterFactory(CID, factory);
+
+      Preferences.reset(HSTS_PRELOAD_LIST_PREF);
+      Preferences.reset(CERT_PINNING_ENFORCEMENT_PREF);
+
+      // clear collected HSTS and HPKP state
+      // through the site security service
+      sss.clearAll();
+      sss.clearPreloads();
+    },
+  };
+};
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -18,16 +18,17 @@ XPCOMUtils.defineLazyServiceGetter(
     this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2");
 
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/action.js");
 Cu.import("chrome://marionette/content/addon.js");
 Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/browser.js");
+Cu.import("chrome://marionette/content/cert.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/l10n.js");
 Cu.import("chrome://marionette/content/legacyaction.js");
 Cu.import("chrome://marionette/content/logging.js");
@@ -491,26 +492,34 @@ GeckoDriver.prototype.newSession = funct
     throw new SessionNotCreatedError("Maximum number of active sessions.")
   }
   this.sessionId = cmd.parameters.sessionId ||
       cmd.parameters.session_id ||
       element.generateUUID();
 
   this.newSessionCommandId = cmd.id;
   this.setSessionCapabilities(cmd.parameters.capabilities);
+
+  this.scriptTimeout = 10000;
+
+  this.secureTLS = !this.sessionCapabilities.acceptInsecureCerts;
+  if (!this.secureTLS) {
+    logger.warn("TLS certificate errors will be ignored for this session");
+    let acceptAllCerts = new cert.InsecureSweepingOverride();
+    cert.installOverride(acceptAllCerts);
+  }
+
   // If we are testing accessibility with marionette, start a11y service in
   // chrome first. This will ensure that we do not have any content-only
   // services hanging around.
   if (this.sessionCapabilities.raisesAccessibilityExceptions &&
       accessibility.service) {
     logger.info("Preemptively starting accessibility service in Chrome");
   }
 
-  this.scriptTimeout = 10000;
-
   let registerBrowsers = this.registerPromise();
   let browserListening = this.listeningPromise();
 
   let waitForWindow = function() {
     let win = this.getCurrentWindow();
     if (!win) {
       // if the window isn't even created, just poll wait for it
       let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
@@ -649,23 +658,16 @@ GeckoDriver.prototype.setSessionCapabili
         `Not all requiredCapabilities could be met: ${JSON.stringify(errors)}`);
   };
 
   // clone, overwrite, and set
   let caps = copy(this.sessionCapabilities);
   caps = copy(newCaps, caps);
   logger.config("Changing capabilities: " + JSON.stringify(caps));
 
-  // update session state
-  this.secureTLS = !caps.acceptInsecureCerts;
-  if (!this.secureTLS) {
-    logger.warn("Invalid or self-signed TLS certificates " +
-        "will be discarded for this session");
-  }
-
   this.sessionCapabilities = caps;
 };
 
 GeckoDriver.prototype.setUpProxy = function(proxy) {
   logger.config("User-provided proxy settings: " + JSON.stringify(proxy));
 
   assert.object(proxy);
   if (!proxy.hasOwnProperty("proxyType")) {
@@ -2307,17 +2309,19 @@ GeckoDriver.prototype.sessionTearDown = 
   this.sessionId = null;
 
   if (this.observing !== null) {
     for (let topic in this.observing) {
       Services.obs.removeObserver(this.observing[topic], topic);
     }
     this.observing = null;
   }
+
   this.sandboxes.clear();
+  cert.uninstallOverride();
 };
 
 /**
  * Processes the "deleteSession" request from the client by tearing down
  * the session and responding "ok".
  */
 GeckoDriver.prototype.deleteSession = function(cmd, resp) {
   this.sessionTearDown();
--- a/testing/marionette/harness/marionette/tests/unit/test_capabilities.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_capabilities.py
@@ -29,19 +29,18 @@ class TestCapabilities(MarionetteTestCas
         self.assertEqual(self.caps["browserName"], self.appinfo["name"].lower())
         self.assertEqual(self.caps["browserVersion"], self.appinfo["version"])
         self.assertEqual(self.caps["platformName"], self.os_name)
         self.assertEqual(self.caps["platformVersion"], self.os_version)
         self.assertEqual(self.caps["specificationLevel"], 0)
 
     def test_supported_features(self):
         self.assertIn("rotatable", self.caps)
-        self.assertIn("acceptSslCerts", self.caps)
-
-        self.assertFalse(self.caps["acceptSslCerts"])
+        self.assertIn("acceptInsecureCerts", self.caps)
+        self.assertFalse(self.caps["acceptInsecureCerts"])
 
     def test_additional_capabilities(self):
         self.assertIn("processId", self.caps)
         self.assertEqual(self.caps["processId"], self.appinfo["processID"])
 
     def test_we_can_pass_in_capabilities_on_session_start(self):
         self.marionette.delete_session()
         capabilities = {"desiredCapabilities": {"somethingAwesome": "cake"}}
--- a/testing/marionette/harness/marionette/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_navigation.py
@@ -1,44 +1,52 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import time
 import urllib
+import contextlib
 
 from marionette import MarionetteTestCase
-from marionette_driver.errors import MarionetteException, TimeoutException
-from marionette_driver import By, Wait
+from marionette_driver import errors, By, Wait
 
 
 def inline(doc):
     return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
 
 
 class TestNavigate(MarionetteTestCase):
+
     def setUp(self):
         MarionetteTestCase.setUp(self)
         self.marionette.navigate("about:")
         self.test_doc = self.marionette.absolute_url("test.html")
         self.iframe_doc = self.marionette.absolute_url("test_iframe.html")
 
+    @property
+    def location_href(self):
+        return self.marionette.execute_script("return window.location.href")
+
     def test_set_location_through_execute_script(self):
-        self.marionette.execute_script("window.location.href = '%s'" % self.test_doc)
-        Wait(self.marionette).until(lambda _: self.test_doc == self.location_href)
+        self.marionette.execute_script(
+            "window.location.href = '%s'" % self.test_doc)
+        Wait(self.marionette).until(
+            lambda _: self.test_doc == self.location_href)
         self.assertEqual("Marionette Test", self.marionette.title)
 
     def test_navigate(self):
         self.marionette.navigate(self.test_doc)
         self.assertNotEqual("about:", self.location_href)
         self.assertEqual("Marionette Test", self.marionette.title)
 
     def test_navigate_chrome_error(self):
         with self.marionette.using_context("chrome"):
-            self.assertRaisesRegexp(MarionetteException, "Cannot navigate in chrome context",
+            self.assertRaisesRegexp(
+                errors.MarionetteException, "Cannot navigate in chrome context",
                                     self.marionette.navigate, "about:blank")
 
     def test_get_current_url_returns_top_level_browsing_context_url(self):
         self.marionette.navigate(self.iframe_doc)
         self.assertEqual(self.iframe_doc, self.location_href)
         frame = self.marionette.find_element(By.CSS_SELECTOR, "#test_iframe")
         self.marionette.switch_to_frame(frame)
         self.assertEqual(self.iframe_doc, self.marionette.get_url())
@@ -91,55 +99,113 @@ class TestNavigate(MarionetteTestCase):
         self.marionette.navigate(self.marionette.absolute_url("test_iframe.html"))
         self.marionette.switch_to_frame(0)
         self.marionette.navigate(self.marionette.absolute_url("empty.html"))
         self.assertTrue('empty.html' in self.marionette.get_url())
         self.marionette.switch_to_frame()
         self.assertTrue('test_iframe.html' in self.marionette.get_url())
     """
 
-    def test_should_not_error_if_nonexistent_url_used(self):
-        try:
+    def test_invalid_protocol(self):
+        with self.assertRaises(errors.MarionetteException):
             self.marionette.navigate("thisprotocoldoesnotexist://")
-            self.fail("Should have thrown a MarionetteException")
-        except TimeoutException:
-            self.fail("The socket shouldn't have timed out when navigating to a non-existent URL")
-        except MarionetteException as e:
-            self.assertIn("Reached error page", str(e))
-        except Exception as e:
-            import traceback
-            print traceback.format_exc()
-            self.fail("Should have thrown a MarionetteException instead of %s" % type(e))
 
     def test_should_navigate_to_requested_about_page(self):
         self.marionette.navigate("about:neterror")
         self.assertEqual(self.marionette.get_url(), "about:neterror")
         self.marionette.navigate(self.marionette.absolute_url("test.html"))
         self.marionette.navigate("about:blocked")
         self.assertEqual(self.marionette.get_url(), "about:blocked")
 
     def test_find_element_state_complete(self):
         self.marionette.navigate(self.test_doc)
-        state = self.marionette.execute_script("return window.document.readyState")
+        state = self.marionette.execute_script(
+            "return window.document.readyState")
         self.assertEqual("complete", state)
         self.assertTrue(self.marionette.find_element(By.ID, "mozLink"))
 
     def test_error_when_exceeding_page_load_timeout(self):
-        with self.assertRaises(TimeoutException):
+        with self.assertRaises(errors.TimeoutException):
             self.marionette.timeout.page_load = 0
             self.marionette.navigate(self.marionette.absolute_url("slow"))
             self.marionette.find_element(By.TAG_NAME, "p")
 
     def test_navigate_iframe(self):
         self.marionette.navigate(self.iframe_doc)
         self.assertTrue('test_iframe.html' in self.marionette.get_url())
         self.assertTrue(self.marionette.find_element(By.ID, "test_iframe"))
 
     def test_fragment(self):
         doc = inline("<p id=foo>")
         self.marionette.navigate(doc)
         self.marionette.execute_script("window.visited = true", sandbox=None)
         self.marionette.navigate("%s#foo" % doc)
-        self.assertTrue(self.marionette.execute_script("return window.visited", sandbox=None))
+        self.assertTrue(self.marionette.execute_script(
+            "return window.visited", sandbox=None))
+
+    def test_error_on_tls_navigation(self):
+        self.assertRaises(errors.InsecureCertificateException,
+                          self.marionette.navigate, self.fixtures.where_is("/test.html", on="https"))
+
+
+class TestTLSNavigation(MarionetteTestCase):
+    insecure_tls = {"acceptInsecureCerts": True}
+    secure_tls = {"acceptInsecureCerts": False}
+
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        self.marionette.delete_session()
+        self.capabilities = self.marionette.start_session(
+            desired_capabilities=self.insecure_tls)
+
+    def tearDown(self):
+        try:
+            self.marionette.delete_session()
+        except:
+            pass
+        MarionetteTestCase.tearDown(self)
+
+    @contextlib.contextmanager
+    def safe_session(self):
+        try:
+            self.capabilities = self.marionette.start_session(
+                desired_capabilities=self.secure_tls)
+            yield self.marionette
+        finally:
+            self.marionette.delete_session()
 
-    @property
-    def location_href(self):
-        return self.marionette.execute_script("return window.location.href")
+    @contextlib.contextmanager
+    def unsafe_session(self):
+        try:
+            self.capabilities = self.marionette.start_session(
+                desired_capabilities=self.insecure_tls)
+            yield self.marionette
+        finally:
+            self.marionette.delete_session()
+
+    def test_navigate_by_command(self):
+        self.marionette.navigate(
+            self.fixtures.where_is("/test.html", on="https"))
+        self.assertIn("https", self.marionette.get_url())
+
+    def test_navigate_by_click(self):
+        link_url = self.fixtures.where_is("/test.html", on="https")
+        self.marionette.navigate(
+            inline("<a href=%s>https is the future</a>" % link_url))
+        self.marionette.find_element(By.TAG_NAME, "a").click()
+        self.assertIn("https", self.marionette.get_url())
+
+    def test_deactivation(self):
+        invalid_cert_url = self.fixtures.where_is("/test.html", on="https")
+
+        print "with safe session"
+        with self.safe_session() as session:
+            with self.assertRaises(errors.InsecureCertificateException):
+                session.navigate(invalid_cert_url)
+
+        print "with unsafe session"
+        with self.unsafe_session() as session:
+            session.navigate(invalid_cert_url)
+
+        print "with safe session again"
+        with self.safe_session() as session:
+            with self.assertRaises(errors.InsecureCertificateException):
+                session.navigate(invalid_cert_url)
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -10,16 +10,17 @@ marionette.jar:
   content/legacyaction.js (legacyaction.js)
   content/browser.js (browser.js)
   content/interaction.js (interaction.js)
   content/accessibility.js (accessibility.js)
   content/listener.js (listener.js)
   content/element.js (element.js)
   content/simpletest.js (simpletest.js)
   content/frame.js (frame.js)
+  content/cert.js (cert.js)
   content/event.js  (event.js)
   content/error.js (error.js)
   content/message.js (message.js)
   content/dispatcher.js (dispatcher.js)
   content/modal.js (modal.js)
   content/proxy.js (proxy.js)
   content/capture.js (capture.js)
   content/cookies.js (cookies.js)