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

--HG--
extra : rebase_source : 023f18b07ffbb53c7dbc588a823c62830f032e3d
This commit is contained in:
Andreas Tolfsen 2016-11-06 18:03:31 +00:00
parent 5c6b5dd771
commit da6234665e
5 changed files with 244 additions and 34 deletions

140
testing/marionette/cert.js Normal file
View File

@ -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();
},
};
};

View File

@ -23,6 +23,7 @@ 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");
@ -496,6 +497,16 @@ GeckoDriver.prototype.newSession = function*(cmd, resp) {
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.
@ -504,8 +515,6 @@ GeckoDriver.prototype.newSession = function*(cmd, resp) {
logger.info("Preemptively starting accessibility service in Chrome");
}
this.scriptTimeout = 10000;
let registerBrowsers = this.registerPromise();
let browserListening = this.listeningPromise();
@ -654,13 +663,6 @@ GeckoDriver.prototype.setSessionCapabilities = function(newCaps) {
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;
};
@ -2312,7 +2314,9 @@ GeckoDriver.prototype.sessionTearDown = function(cmd, resp) {
}
this.observing = null;
}
this.sandboxes.clear();
cert.uninstallOverride();
};
/**

View File

@ -34,9 +34,8 @@ class TestCapabilities(MarionetteTestCase):
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)

View File

@ -4,10 +4,10 @@
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):
@ -15,15 +15,22 @@ def inline(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):
@ -33,7 +40,8 @@ class TestNavigate(MarionetteTestCase):
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):
@ -96,18 +104,9 @@ class TestNavigate(MarionetteTestCase):
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")
@ -118,12 +117,13 @@ class TestNavigate(MarionetteTestCase):
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")
@ -138,8 +138,74 @@ class TestNavigate(MarionetteTestCase):
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))
@property
def location_href(self):
return self.marionette.execute_script("return window.location.href")
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()
@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)

View File

@ -15,6 +15,7 @@ marionette.jar:
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)