Bug 1096197 - Ensure SSL Error reports work when there is no failed certificate chain. r=keeler, a=sledru
authorMark Goodwin <mgoodwin@mozilla.com>
Tue, 20 Jan 2015 11:58:00 -0500
changeset 242966 a7f164f7c32d
parent 242965 430bff48811d
child 242967 d00b4a85897c
push id4350
push userryanvm@gmail.com
push date2015-01-21 19:56 +0000
treeherdermozilla-beta@4501fcac9e0b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskeeler, sledru
bugs1096197
milestone36.0
Bug 1096197 - Ensure SSL Error reports work when there is no failed certificate chain. r=keeler, a=sledru
browser/base/content/browser.js
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_bug846489.js
browser/base/content/test/general/browser_bug846489_content.js
browser/base/content/test/general/browser_ssl_error_reports.js
browser/base/content/test/general/browser_ssl_error_reports_content.js
browser/base/content/test/general/pinning_reports.sjs
build/pgo/server-locations.txt
testing/mochitest/runtests.py
testing/mochitest/ssltunnel/ssltunnel.cpp
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2647,21 +2647,16 @@ let BrowserOnClick = {
       return;
     }
 
     let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
                            .getService(Ci.nsISerializationHelper);
     let transportSecurityInfo = serhelper.deserializeObject(securityInfo);
     transportSecurityInfo.QueryInterface(Ci.nsITransportSecurityInfo)
 
-    if (transportSecurityInfo.failedCertChain == null) {
-      Cu.reportError("transportSecurityInfo didn't have a failedCertChain for a failedChannel");
-      return;
-    }
-
     showReportStatus("activity");
 
     /*
      * Requested info for the report:
      * - Domain of bad connection
      * - Error type (e.g. Pinning, domain mismatch, etc)
      * - Cert chain (at minimum, same data to distrust each cert in the
      *   chain)
@@ -2681,21 +2676,24 @@ let BrowserOnClick = {
         derString += String.fromCharCode(derArray[i]);
       }
       return derString;
     }
 
     // Convert the nsIX509CertList into a format that can be parsed into
     // JSON
     let asciiCertChain = [];
-    let certs = transportSecurityInfo.failedCertChain.getEnumerator();
-    while (certs.hasMoreElements()) {
-      let cert = certs.getNext();
-      cert.QueryInterface(Ci.nsIX509Cert);
-      asciiCertChain.push(btoa(getDERString(cert)));
+
+    if (transportSecurityInfo.failedCertChain) {
+      let certs = transportSecurityInfo.failedCertChain.getEnumerator();
+      while (certs.hasMoreElements()) {
+        let cert = certs.getNext();
+        cert.QueryInterface(Ci.nsIX509Cert);
+        asciiCertChain.push(btoa(getDERString(cert)));
+      }
     }
 
     let report = {
       hostname: location.hostname,
       port: location.port,
       timestamp: Math.round(Date.now() / 1000),
       errorCode: transportSecurityInfo.errorCode,
       failedCertChain: asciiCertChain,
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -5,20 +5,20 @@ support-files =
   alltabslistener.html
   app_bug575561.html
   app_subframe_bug575561.html
   authenticate.sjs
   aboutHome_content_script.js
   browser_bug479408_sample.html
   browser_bug678392-1.html
   browser_bug678392-2.html
-  browser_bug846489_content.js
   browser_bug970746.xhtml
   browser_fxa_oauth.html
   browser_registerProtocolHandler_notification.html
+  browser_ssl_error_reports_content.js
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   browser_web_channel.html
   bug592338.html
   bug792517-2.html
   bug792517.html
   bug792517.sjs
   bug839103.css
@@ -153,17 +153,16 @@ skip-if = e10s
 [browser_bug413915.js]
 [browser_bug416661.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug417483.js]
 skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
 [browser_bug419612.js]
 skip-if = e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
 [browser_bug422590.js]
-[browser_bug846489.js]
 [browser_bug423833.js]
 skip-if = true # bug 428712
 [browser_bug424101.js]
 skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
 [browser_bug427559.js]
 skip-if = e10s # Bug 1102015 - "content window is focused - Got [object ChromeWindow], expected [object CPOW [object Window]]"
 [browser_bug431826.js]
 [browser_bug432599.js]
@@ -403,16 +402,17 @@ skip-if = buildapp == 'mulet' || e10s # 
 [browser_searchHighlight.js]
 skip-if = true
 [browser_searchSuggestionUI.js]
 skip-if = e10s
 support-files =
   searchSuggestionUI.html
   searchSuggestionUI.js
 [browser_selectTabAtIndex.js]
+[browser_ssl_error_reports.js]
 [browser_star_hsts.js]
 [browser_subframe_favicons_not_used.js]
 [browser_tabDrop.js]
 skip-if = buildapp == 'mulet' || e10s
 [browser_tabMatchesInAwesomebar.js]
 skip-if = e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s (test calls gBrowser.swapBrowsersAndCloseOther)
 [browser_tabMatchesInAwesomebar_perwindowpb.js]
 skip-if = e10s || true # Bug 1093373, Bug 1104755
rename from browser/base/content/test/general/browser_bug846489.js
rename to browser/base/content/test/general/browser_ssl_error_reports.js
--- a/browser/base/content/test/general/browser_bug846489.js
+++ b/browser/base/content/test/general/browser_ssl_error_reports.js
@@ -1,67 +1,69 @@
-var badPin = "https://include-subdomains.pinning.example.com";
-var enabledPref = false;
-var automaticPref = false;
-var urlPref = "security.ssl.errorReporting.url";
-var enforcement_level = 1;
+let badChainURL = "https://badchain.include-subdomains.pinning.example.com";
+let noCertURL = "https://fail-handshake.example.com";
+let enabledPref = false;
+let automaticPref = false;
+let urlPref = "security.ssl.errorReporting.url";
+let enforcement_level = 1;
 
 function loadFrameScript() {
   let mm = Cc["@mozilla.org/globalmessagemanager;1"]
            .getService(Ci.nsIMessageListenerManager);
   const ROOT = getRootDirectory(gTestPath);
-  mm.loadFrameScript(ROOT+"browser_bug846489_content.js", true);
+  mm.loadFrameScript(ROOT + "browser_ssl_error_reports_content.js", true);
 }
 
 add_task(function*(){
   waitForExplicitFinish();
   loadFrameScript();
   SimpleTest.requestCompleteLog();
   yield testSendReportDisabled();
-  yield testSendReportManual();
+  yield testSendReportManual(badChainURL, "succeed");
+  yield testSendReportManual(noCertURL, "nocert");
   yield testSendReportAuto();
   yield testSendReportError();
   yield testSetAutomatic();
 });
 
 // creates a promise of the message in an error page
 function createNetworkErrorMessagePromise(aBrowser) {
   return new Promise(function(resolve, reject) {
     // Error pages do not fire "load" events, so use a progressListener.
-    var originalDocumentURI = aBrowser.contentDocument.documentURI;
+    let originalDocumentURI = aBrowser.contentDocument.documentURI;
 
-    var progressListener = {
+    let progressListener = {
       onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) {
         // Make sure nothing other than an error page is loaded.
         if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE)) {
           reject("location change was not to an error page");
         }
       },
 
       onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
         let doc = aBrowser.contentDocument;
 
         if (doc.getElementById("reportCertificateError")) {
           // Wait until the documentURI changes (from about:blank) this should
           // be the error page URI.
-          var documentURI = doc.documentURI;
+          let documentURI = doc.documentURI;
           if (documentURI == originalDocumentURI) {
             return;
           }
 
           aWebProgress.removeProgressListener(progressListener,
             Ci.nsIWebProgress.NOTIFY_LOCATION |
             Ci.nsIWebProgress.NOTIFY_STATE_REQUEST);
-          var matchArray = /about:neterror\?.*&d=([^&]*)/.exec(documentURI);
+          let matchArray = /about:neterror\?.*&d=([^&]*)/.exec(documentURI);
           if (!matchArray) {
             reject("no network error message found in URI")
           return;
           }
 
-          var errorMsg = matchArray[1];
+          let errorMsg = matchArray[1];
           resolve(decodeURIComponent(errorMsg));
         }
       },
 
       QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                           Ci.nsISupportsWeakReference])
     };
 
@@ -69,17 +71,17 @@ function createNetworkErrorMessagePromis
             Ci.nsIWebProgress.NOTIFY_LOCATION |
             Ci.nsIWebProgress.NOTIFY_STATE_REQUEST);
   });
 }
 
 // check we can set the 'automatically send' pref
 let testSetAutomatic = Task.async(function*() {
   setup();
-  let tab = gBrowser.addTab(badPin, {skipAnimation: true});
+  let tab = gBrowser.addTab(badChainURL, {skipAnimation: true});
   let browser = tab.linkedBrowser;
   let mm = browser.messageManager;
 
   gBrowser.selectedTab = tab;
 
   // ensure we have the correct error message from about:neterror
   let netError = createNetworkErrorMessagePromise(browser);
   yield netError;
@@ -114,32 +116,34 @@ let testSetAutomatic = Task.async(functi
 
   yield prefDisabled;
 
   gBrowser.removeTab(tab);
   cleanup();
 });
 
 // test that manual report sending (with button clicks) works
-let testSendReportManual = Task.async(function*() {
+let testSendReportManual = Task.async(function*(testURL, suffix) {
   setup();
   Services.prefs.setBoolPref("security.ssl.errorReporting.enabled", true);
-  Services.prefs.setCharPref("security.ssl.errorReporting.url", "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?succeed");
+  Services.prefs.setCharPref("security.ssl.errorReporting.url",
+    "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?" + suffix);
 
-  let tab = gBrowser.addTab(badPin, {skipAnimation: true});
+  let tab = gBrowser.addTab(testURL, {skipAnimation: true});
   let browser = tab.linkedBrowser;
   let mm = browser.messageManager;
 
   gBrowser.selectedTab = tab;
 
   // ensure we have the correct error message from about:neterror
   let netError = createNetworkErrorMessagePromise(browser);
   yield netError;
   netError.then(function(val){
-    is(val.startsWith("An error occurred during a connection to include-subdomains.pinning.example.com"), true ,"ensure the correct error message came from about:neterror");
+    is(val.startsWith("An error occurred during a connection to"), true,
+                      "ensure the correct error message came from about:neterror");
   });
 
   // Check the report starts on click
   let btn = browser.contentDocument.getElementById("reportCertificateError");
 
   // check the content script sends the message to report
   let reportWillStart = new Promise(function(resolve, reject){
     mm.addMessageListener("Browser:SendSSLErrorReport", function() {
@@ -180,17 +184,17 @@ let testSendReportManual = Task.async(fu
 
 // test that automatic sending works
 let testSendReportAuto = Task.async(function*() {
   setup();
   Services.prefs.setBoolPref("security.ssl.errorReporting.enabled", true);
   Services.prefs.setBoolPref("security.ssl.errorReporting.automatic", true);
   Services.prefs.setCharPref("security.ssl.errorReporting.url", "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?succeed");
 
-  let tab = gBrowser.addTab(badPin, {skipAnimation: true});
+  let tab = gBrowser.addTab(badChainURL, {skipAnimation: true});
   let browser = tab.linkedBrowser;
   let mm = browser.messageManager;
 
   gBrowser.selectedTab = tab;
 
   let reportWillStart = Promise.defer();
   mm.addMessageListener("Browser:SendSSLErrorReport", function() {
     reportWillStart.resolve();
@@ -229,17 +233,17 @@ let testSendReportAuto = Task.async(func
 
 // test that an error is shown if there's a problem with the report server
 let testSendReportError = Task.async(function*() {
   setup();
   Services.prefs.setBoolPref("security.ssl.errorReporting.enabled", true);
   Services.prefs.setBoolPref("security.ssl.errorReporting.automatic", true);
   Services.prefs.setCharPref("security.ssl.errorReporting.url", "https://example.com/browser/browser/base/content/test/general/pinning_reports.sjs?error");
 
-  let tab = gBrowser.addTab(badPin, {skipAnimation: true});
+  let tab = gBrowser.addTab(badChainURL, {skipAnimation: true});
   let browser = tab.linkedBrowser;
   let mm = browser.messageManager;
 
   gBrowser.selectedTab = tab;
 
   // check the report send starts....
   let reportWillStart = new Promise(function(resolve, reject){
     mm.addMessageListener("Browser:SendSSLErrorReport", function() {
@@ -250,56 +254,56 @@ let testSendReportError = Task.async(fun
   let netError = createNetworkErrorMessagePromise(browser);
   yield netError;
   yield reportWillStart;
 
   // and that errors are seen
   let reportErrors = new Promise(function(resolve, reject) {
     mm.addMessageListener("ssler-test:SSLErrorReportStatus", function(message) {
       switch(message.data.reportStatus) {
-      case "complete":
-        reject(message.data.reportStatus);
-        break;
-      case "error":
-        resolve(message.data.reportStatus);
-        break;
+        case "complete":
+          reject(message.data.reportStatus);
+          break;
+        case "error":
+          resolve(message.data.reportStatus);
+          break;
       }
     });
   });
 
   yield reportErrors;
 
   gBrowser.removeTab(tab);
   cleanup();
 });
 
 let testSendReportDisabled = Task.async(function*() {
   setup();
   Services.prefs.setBoolPref("security.ssl.errorReporting.enabled", false);
   Services.prefs.setCharPref("security.ssl.errorReporting.url", "https://offdomain.com");
 
-  let tab = gBrowser.addTab(badPin, {skipAnimation: true});
+  let tab = gBrowser.addTab(badChainURL, {skipAnimation: true});
   let browser = tab.linkedBrowser;
   let mm = browser.messageManager;
 
   gBrowser.selectedTab = tab;
 
   // Ensure we have an error page
   let netError = createNetworkErrorMessagePromise(browser);
   yield netError;
 
   let reportErrors = new Promise(function(resolve, reject) {
     mm.addMessageListener("ssler-test:SSLErrorReportStatus", function(message) {
       switch(message.data.reportStatus) {
-      case "complete":
-        reject(message.data.reportStatus);
-        break;
-      case "error":
-        resolve(message.data.reportStatus);
-        break;
+        case "complete":
+          reject(message.data.reportStatus);
+          break;
+        case "error":
+          resolve(message.data.reportStatus);
+          break;
       }
     });
   });
 
   // click the button
   mm.sendAsyncMessage("ssler-test:SendBtnClick",{forceUI:true});
 
   // check we get an error
rename from browser/base/content/test/general/browser_bug846489_content.js
rename to browser/base/content/test/general/browser_ssl_error_reports_content.js
--- a/browser/base/content/test/general/pinning_reports.sjs
+++ b/browser/base/content/test/general/pinning_reports.sjs
@@ -1,41 +1,69 @@
 const EXPECTED_CHAIN = [
   "MIIDCjCCAfKgAwIBAgIENUiGYDANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwHhcNMTQxMDAxMjExNDE5WhcNMjQxMDAxMjExNDE5WjAxMS8wLQYDVQQDEyZpbmNsdWRlLXN1YmRvbWFpbnMucGlubmluZy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALxYrge8C4eVfTb6/lJ4k/+/4J6wlnWpp5Szxy1MHhsLB+LJh/HRHqkO/tsigT204kTeU3dxuAfQHz0g+Td8dr6KICLLNVFUPw+XjhBV4AtxV8wcprs6EmdBhJgAjkFB4M76BL7/Ow0NfH012WNESn8TTbsp3isgkmrXjTZhWR33vIL1eDNimykp/Os/+JO+x9KVfdCtDCrPwO9Yusial5JiaW7qemRtVuUDL87NSJ7xokPEOSc9luv/fBamZ3rgqf3K6epqg+0o3nNCCcNFnfLW52G0t69+dIjr39WISHnqqZj3Sb7JPU6OmxTd13ByoLkoM3ZUQ2Lpas+RJvQyGXkCAwEAAaM1MDMwMQYDVR0RBCowKIImaW5jbHVkZS1zdWJkb21haW5zLnBpbm5pbmcuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAAmzXfeoOS59FkNABRonFPRyFl7BoGpVJENUteFfTa2pdAhGYdo19Y4uILTTj+vtDAa5yryb5Uvd+YuJnExosbMMkzCrmZ9+VJCJdqUTb+idwk9/sgPl2gtGeRmefB0hXSUFHc/p1CDufSpYOmj9NCUZD2JEsybgJQNulkfAsVnS3lzDcxAwcO+RC/1uJDSiUtcBpWS4FW58liuDYE7PD67kLJHZPVUV2WCMuIl4VM2tKPtvShz1JkZ5UytOLs6jPfviNAk/ftXczaE2/RJgM2MnDX9nGzOxG6ONcVNCljL8avhFBCosutE6i5LYSZR6V14YY/xOn15WDSuWdnIsJCo=",
   "MIIC2jCCAcKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwHhcNMTQwOTI1MjEyMTU0WhcNMjQwOTI1MjEyMTU0WjAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBT+BwAhO52IWgSIdZZifU9LHOs3IR/+8DCC0WP5d/OuyKlZ6Rqd0tsd3i7durhQyjHSbLf2lJStcnFjcVEbEnNI76RuvlN8xLLn5eV+2Ayr4cZYKztudwRmw+DV/iYAiMSy0hs7m3ssfX7qpoi1aNRjUanwU0VTCPQhF1bEKAC2du+C5Z8e92zN5t87w7bYr7lt+m8197XliXEu+0s9RgnGwGaZ296BIRz6NOoJYTa43n06LU1I1+Z4d6lPdzUFrSR0GBaMhUSurUBtOin3yWiMhg1VHX/KwqGc4als5GyCVXy8HGrA/0zQPOhetxrlhEVAdK/xBt7CZvByj1Rcc7AgMBAAGjEzARMA8GA1UdEwQIMAYBAf8CAQAwDQYJKoZIhvcNAQELBQADggEBAJq/hogSRqzPWTwX4wTn/DVSNdWwFLv53qep9YrSMJ8ZsfbfK9Es4VP4dBLRQAVMJ0Z5mW1I6d/n0KayTanuUBvemYdxPi/qQNSs8UJcllqdhqWzmzAg6a0LxrMnEeKzPBPD6q8PwQ7tYP+B4sBN9tnnsnyPgti9ZiNZn5FwXZliHXseQ7FE9/SqHlLw5LXW3YtKjuti6RmuV6fq3j+D4oeC5vb1mKgIyoTqGN6ze57v8RHi+pQ8Q+kmoUn/L3Z2YmFe4SKN/4WoyXr8TdejpThGOCGCAd3565s5gOx5QfSQX11P8NZKO8hcN0tme3VzmGpHK0Z/6MTmdpNaTwQ6odk="
   ];
 
-function handleRequest(request, response)
-{
-  if (request.queryString === "succeed") {
-    // read the report from the client
-    let inputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
-    inputStream.init(request.bodyInputStream, 0x01, 0004, 0);
+function parseReport(request) {
+  // read the report from the request
+  let inputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream);
+  inputStream.init(request.bodyInputStream, 0x01, 0004, 0);
+
+  let body = "";
+  if (inputStream) {
+    while (inputStream.available()) {
+      body = body + inputStream.read(inputStream.available());
+    }
+  }
+  // parse the report
+  return JSON.parse(body);
+}
+
+function handleRequest(request, response) {
+  let report = {};
+  let certChain = [];
 
-    let body = "";
-    if (inputStream) {
-      while (inputStream.available()) {
-        body = body + inputStream.read(inputStream.available());
+  switch (request.queryString) {
+    case "succeed":
+      report = parseReport(request);
+      certChain = report.failedCertChain;
+
+      // ensure the cert chain is what we expect
+      for (idx in certChain) {
+        if (certChain[idx] !== EXPECTED_CHAIN[idx]) {
+          // if the chain differs, send an error response to cause test
+          // failure
+          response.setStatusLine("1.1", 500, "Server error");
+          response.write("<html>The report contained an unexpected chain</html>");
+          return;
+        }
       }
-    }
-    // parse the report
-    let report = JSON.parse(body);
-    let certChain = report.failedCertChain;
 
-    // ensure the cert chain is what we expect
-    for (idx in certChain) {
-      if (certChain[idx] !== EXPECTED_CHAIN[idx]) {
-        // if the chain differs, send an error response to cause test
-        // failure
+      // if all is as expected, send the 201 the client expects
+      response.setStatusLine("1.1", 201, "Created");
+      response.write("<html>OK</html>");
+      break;
+    case "nocert":
+      report = parseReport(request);
+      certChain = report.failedCertChain;
+
+      if (certChain && certChain.length > 0) {
+        // We're not expecting a chain; if there is one, send an error
         response.setStatusLine("1.1", 500, "Server error");
         response.write("<html>The report contained an unexpected chain</html>");
         return;
       }
-    }
 
-    // if all is as expected, send the 201 the client expects
-    response.setStatusLine("1.1", 201, "Created");
-    response.write("<html>OK</html>");
-  } else if (request.queryString === "error") {
-    response.setStatusLine("1.1", 500, "Server error");
-    response.write("<html>server error</html>");
+      // if all is as expected, send the 201 the client expects
+      response.setStatusLine("1.1", 201, "Created");
+      response.write("<html>OK</html>");
+      break;
+    case "error":
+      response.setStatusLine("1.1", 500, "Server error");
+      response.write("<html>server error</html>");
+      break;
+    default:
+      response.setStatusLine("1.1", 500, "Server error");
+      response.write("<html>succeed, nocert or error expected</html>");
+      break;
   }
 }
--- a/build/pgo/server-locations.txt
+++ b/build/pgo/server-locations.txt
@@ -15,28 +15,32 @@
 # and a comma-separated list of options (if indeed any options are needed).
 #
 # The format of an origin is, referring to RFC 2396, a scheme (either "http" or
 # "https"), followed by "://", followed by a host, followed by ":", followed by
 # a port number.  The colon and port number must be present even if the port
 # number is the default for the protocol.
 #
 # Unrecognized options are ignored.  Recognized options are "primary" and
-# "privileged", "nocert", "cert=some_cert_nickname", "redir=hostname".
+# "privileged", "nocert", "cert=some_cert_nickname", "redir=hostname" and
+# "failHandshake".
 #
 # "primary" denotes a location which is the canonical location of
 # the server; this location is the one assumed for requests which don't
 # otherwise identify a particular origin (e.g. HTTP/1.0 requests).  
 #
 # "privileged" denotes a location which should have the ability to request 
 # elevated privileges; the default is no privileges.
 #
 # "nocert" makes sense only for https:// hosts and means there is not
 # any certificate automatically generated for this host.
 #
+# "failHandshake" causes the tls handshake to fail (by sending a client hello to
+# the client).
+#
 # "cert=nickname" tells the pgo server to use a particular certificate
 # for this host. The certificate is referenced by its nickname that must
 # not contain any spaces. The certificate  key files (PKCS12 modules)
 # for custom certification are loaded from build/pgo/certs
 # directory. When new certificate is added to this dir pgo/ssltunnel
 # must be built then. This is only necessary for cases where we really do
 # want specific certs.
 #
@@ -223,13 +227,14 @@ https://marketplace.firefox.com:443     
 https://marketplace-dev.allizom.org:443   privileged
 https://marketplace.allizom.org:443       privileged
 
 # Host for HPKP
 https://include-subdomains.pinning-dynamic.example.com:443        privileged,cert=dynamicPinningGood
 https://bad.include-subdomains.pinning-dynamic.example.com:443    privileged,cert=dynamicPinningBad
 
 # Host for static pin tests
-https://include-subdomains.pinning.example.com:443                privileged,cert=staticPinningBad
+https://badchain.include-subdomains.pinning.example.com:443       privileged,cert=staticPinningBad
+https://fail-handshake.example.com:443                            privileged,failHandshake
 
 # Hosts for sha1 console warning tests
 https://sha1ee.example.com:443                                    privileged,cert=sha1_end_entity
 https://sha256ee.example.com:443                                  privileged,cert=sha256_end_entity
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -887,17 +887,17 @@ class SSLTunnel:
                      (loc.host, loc.port, self.sslPort, clientauth))
 
       match = self.redirRE.match(option)
       if match:
         redirhost = match.group("redirhost")
         config.write("redirhost:%s:%s:%s:%s\n" %
                      (loc.host, loc.port, self.sslPort, redirhost))
 
-      if option in ('ssl3', 'rc4'):
+      if option in ('ssl3', 'rc4', 'failHandshake'):
         config.write("%s:%s:%s:%s\n" % (option, loc.host, loc.port, self.sslPort))
 
   def buildConfig(self, locations):
     """Create the ssltunnel configuration file"""
     configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg")
     with os.fdopen(configFd, "w") as config:
       config.write("httpproxy:1\n")
       config.write("certdbdir:%s\n" % self.certPath)
--- a/testing/mochitest/ssltunnel/ssltunnel.cpp
+++ b/testing/mochitest/ssltunnel/ssltunnel.cpp
@@ -150,16 +150,17 @@ enum client_auth_option {
 typedef struct {
   int32_t listen_port;
   string cert_nickname;
   PLHashTable* host_cert_table;
   PLHashTable* host_clientauth_table;
   PLHashTable* host_redir_table;
   PLHashTable* host_ssl3_table;
   PLHashTable* host_rc4_table;
+  PLHashTable* host_failhandshake_table;
 } server_info_t;
 
 typedef struct {
   PRFileDesc* client_sock;
   PRNetAddr client_addr;
   server_info_t* server_info;
   // the original host in the Host: header for this connection is
   // stored here, for proxied connections
@@ -254,20 +255,27 @@ static int match_hostname(PLHashEntry *h
  */
 void SignalShutdown()
 {
   PR_Lock(shutdown_lock);
   PR_NotifyCondVar(shutdown_condvar);
   PR_Unlock(shutdown_lock);
 }
 
+// available flags
+enum {
+  USE_SSL3 = 1 << 0,
+  USE_RC4 = 1 << 1,
+  FAIL_HANDSHAKE = 1 << 2
+};
+
 bool ReadConnectRequest(server_info_t* server_info, 
     relayBuffer& buffer, int32_t* result, string& certificate,
     client_auth_option* clientauth, string& host, string& location,
-    bool* ssl3, bool* rc4)
+    int32_t* flags)
 {
   if (buffer.present() < 4) {
     LOG_DEBUG((" !! only %d bytes present in the buffer", (int)buffer.present()));
     return false;
   }
   if (strncmp(buffer.buffertail-4, "\r\n\r\n", 4)) {
     LOG_ERRORD((" !! request is not tailed with CRLFCRLF but with %x %x %x %x", 
                *(buffer.buffertail-4),
@@ -306,32 +314,40 @@ bool ReadConnectRequest(server_info_t* s
     *clientauth = *static_cast<client_auth_option*>(c);
   else
     *clientauth = caNone;
 
   void *redir = PL_HashTableLookup(server_info->host_redir_table, token);
   if (redir)
     location = static_cast<char*>(redir);
 
-  *ssl3 = !!PL_HashTableLookup(server_info->host_ssl3_table, token);
+  if (PL_HashTableLookup(server_info->host_ssl3_table, token)) {
+    *flags |= USE_SSL3;
+  }
 
-  *rc4 = !!PL_HashTableLookup(server_info->host_rc4_table, token);
+  if (PL_HashTableLookup(server_info->host_rc4_table, token)) {
+    *flags |= USE_RC4;
+  }
+
+  if (PL_HashTableLookup(server_info->host_failhandshake_table, token)) {
+    *flags |= FAIL_HANDSHAKE;
+  }
 
   token = strtok2(_caret, "/", &_caret);
   if (strcmp(token, "HTTP")) {  
     LOG_ERRORD((" not tailed with HTTP but with %s", token));
     return true;
   }
 
   *result = (redir) ? 302 : 200;
   return true;
 }
 
 bool ConfigureSSLServerSocket(PRFileDesc* socket, server_info_t* si, const string &certificate,
-                              const client_auth_option clientAuth, bool ssl3, bool rc4)
+                              const client_auth_option clientAuth, int32_t flags)
 {
   const char* certnick = certificate.empty() ?
       si->cert_nickname.c_str() : certificate.c_str();
 
   ScopedCERTCertificate cert(PK11_FindCertFromNickname(certnick, nullptr));
   if (!cert) {
     LOG_ERROR(("Failed to find cert %s\n", certnick));
     return false;
@@ -344,16 +360,22 @@ bool ConfigureSSLServerSocket(PRFileDesc
   }
 
   PRFileDesc* ssl_socket = SSL_ImportFD(nullptr, socket);
   if (!ssl_socket) {
     LOG_ERROR(("Error importing SSL socket\n"));
     return false;
   }
 
+  if (flags & FAIL_HANDSHAKE) {
+    // deliberately cause handshake to fail by sending the client a client hello
+    SSL_ResetHandshake(ssl_socket, false);
+    return true;
+  }
+
   SSLKEAType certKEA = NSS_FindCertKEAType(cert);
   if (SSL_ConfigSecureServer(ssl_socket, cert, privKey, certKEA)
       != SECSuccess) {
     LOG_ERROR(("Error configuring SSL server socket\n"));
     return false;
   }
 
   SSL_OptionSet(ssl_socket, SSL_SECURITY, true);
@@ -361,23 +383,23 @@ bool ConfigureSSLServerSocket(PRFileDesc
   SSL_OptionSet(ssl_socket, SSL_HANDSHAKE_AS_SERVER, true);
 
   if (clientAuth != caNone)
   {
     SSL_OptionSet(ssl_socket, SSL_REQUEST_CERTIFICATE, true);
     SSL_OptionSet(ssl_socket, SSL_REQUIRE_CERTIFICATE, clientAuth == caRequire);
   }
 
-  if (ssl3) {
+  if (flags & USE_SSL3) {
     SSLVersionRange range = { SSL_LIBRARY_VERSION_3_0,
                               SSL_LIBRARY_VERSION_3_0 };
     SSL_VersionRangeSet(ssl_socket, &range);
   }
 
-  if (rc4) {
+  if (flags & USE_RC4) {
     for (uint16_t i = 0; i < SSL_NumImplementedCiphers; ++i) {
       uint16_t cipher_id = SSL_ImplementedCiphers[i];
       switch (cipher_id) {
       case TLS_ECDHE_ECDSA_WITH_RC4_128_SHA:
       case TLS_ECDHE_RSA_WITH_RC4_128_SHA:
       case TLS_RSA_WITH_RC4_128_SHA:
       case TLS_RSA_WITH_RC4_128_MD5:
         SSL_CipherPrefSet(ssl_socket, cipher_id, true);
@@ -575,33 +597,32 @@ void HandleConnection(void* data)
   bool client_error = false;
   bool connect_accepted = !do_http_proxy;
   bool ssl_updated = !do_http_proxy;
   bool expect_request_start = do_http_proxy;
   string certificateToUse;
   string locationHeader;
   client_auth_option clientAuth;
   string fullHost;
-  bool ssl3 = false;
-  bool rc4 = false;
+  int32_t flags = 0;
 
   LOG_DEBUG(("SSLTUNNEL(%p)): incoming connection csock(0)=%p, ssock(1)=%p\n",
          static_cast<void*>(data),
          static_cast<void*>(ci->client_sock),
          static_cast<void*>(other_sock)));
   if (other_sock) 
   {
     int32_t numberOfSockets = 1;
 
     relayBuffer buffers[2];
 
     if (!do_http_proxy)
     {
-      if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, certificateToUse, caNone,
-                                    ssl3, rc4))
+      if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info,
+                                    certificateToUse, caNone, flags))
         client_error = true;
       else if (!ConnectSocket(other_sock, &remote_addr, connect_timeout))
         client_error = true;
       else
         numberOfSockets = 2;
     }
 
     PRPollDesc sockets[2] = 
@@ -710,17 +731,17 @@ void HandleConnection(void* data)
 
             buffers[s].buffertail += bytesRead;
             LOG_DEBUG((", read %d bytes", bytesRead));
 
             // We have to accept and handle the initial CONNECT request here
             int32_t response;
             if (!connect_accepted && ReadConnectRequest(ci->server_info, buffers[s],
                 &response, certificateToUse, &clientAuth, fullHost, locationHeader,
-                &ssl3, &rc4))
+                &flags))
             {
               // Mark this as a proxy-only connection (no SSL) if the CONNECT
               // request didn't come for port 443 or from any of the server's
               // cert or clientauth hostnames.
               if (fullHost.find(":443") == string::npos)
               {
                 server_match_t match;
                 match.fullHost = fullHost;
@@ -732,16 +753,19 @@ void HandleConnection(void* data)
                                              match_hostname, 
                                              &match);
                 PL_HashTableEnumerateEntries(ci->server_info->host_ssl3_table, 
                                              match_hostname, 
                                              &match);
                 PL_HashTableEnumerateEntries(ci->server_info->host_rc4_table, 
                                              match_hostname, 
                                              &match);
+                PL_HashTableEnumerateEntries(ci->server_info->host_failhandshake_table,
+                                             match_hostname,
+                                             &match);
                 ci->http_proxy_only = !match.matched;
               }
               else
               {
                 ci->http_proxy_only = false;
               }
 
               // Clean the request as it would be read
@@ -867,17 +891,17 @@ void HandleConnection(void* data)
                 LOG_DEBUG((" proxy response sent to the client"));
                 // Proxy response has just been writen, update to ssl
                 ssl_updated = true;
                 if (ci->http_proxy_only)
                 {
                   LOG_DEBUG((" not updating to SSL based on http_proxy_only for this socket"));
                 }
                 else if (!ConfigureSSLServerSocket(ci->client_sock, ci->server_info, 
-                                                   certificateToUse, clientAuth, ssl3, rc4))
+                                                   certificateToUse, clientAuth, flags))
                 {
                   LOG_ERRORD((" failed to config server socket\n"));
                   client_error = true;
                   break;
                 }
                 else
                 {
                   LOG_DEBUG((" client socket updated to SSL"));
@@ -999,16 +1023,21 @@ PLHashTable* get_ssl3_table(server_info_
   return server->host_ssl3_table;
 }
 
 PLHashTable* get_rc4_table(server_info_t* server)
 {
   return server->host_rc4_table;
 }
 
+PLHashTable* get_failhandshake_table(server_info_t* server)
+{
+  return server->host_failhandshake_table;
+}
+
 int parseWeakCryptoConfig(char* const& keyword, char*& _caret,
                           PLHashTable* (*get_table)(server_info_t*))
 {
   char* hostname = strtok2(_caret, ":", &_caret);
   char* hostportstring = strtok2(_caret, ":", &_caret);
   char* serverportstring = strtok2(_caret, "\n", &_caret);
 
   int port = atoi(serverportstring);
@@ -1174,16 +1203,24 @@ int processConfigLine(char* configLine)
       server.host_rc4_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings,
                                               PL_CompareStrings, nullptr, nullptr);;
       if (!server.host_rc4_table)
       {
         LOG_ERROR(("Internal, could not create hash table\n"));
         return 1;
       }
 
+      server.host_failhandshake_table = PL_NewHashTable(0, PL_HashString, PL_CompareStrings,
+                                              PL_CompareStrings, nullptr, nullptr);;
+      if (!server.host_failhandshake_table)
+      {
+        LOG_ERROR(("Internal, could not create hash table\n"));
+        return 1;
+      }
+
       servers.push_back(server);
     }
 
     return 0;
   }
   
   if (!strcmp(keyword, "clientauth"))
   {
@@ -1293,16 +1330,20 @@ int processConfigLine(char* configLine)
   if (!strcmp(keyword, "ssl3")) {
     return parseWeakCryptoConfig(keyword, _caret, get_ssl3_table);
   }
 
   if (!strcmp(keyword, "rc4")) {
     return parseWeakCryptoConfig(keyword, _caret, get_rc4_table);
   }
 
+  if (!strcmp(keyword, "failHandshake")) {
+    return parseWeakCryptoConfig(keyword, _caret, get_failhandshake_table);
+  }
+
   // Configure the NSS certificate database directory
   if (!strcmp(keyword, "certdbdir"))
   {
     nssconfigdir = strtok2(_caret, "\n", &_caret);
     return 0;
   }
 
   LOG_ERROR(("Error: keyword \"%s\" unexpected\n", keyword));
@@ -1513,18 +1554,20 @@ int main(int argc, char** argv)
   for (vector<server_info_t>::iterator it = servers.begin();
        it != servers.end(); it++) 
   {
     PL_HashTableEnumerateEntries(it->host_cert_table, freeHostCertHashItems, nullptr);
     PL_HashTableEnumerateEntries(it->host_clientauth_table, freeClientAuthHashItems, nullptr);
     PL_HashTableEnumerateEntries(it->host_redir_table, freeHostRedirHashItems, nullptr);
     PL_HashTableEnumerateEntries(it->host_ssl3_table, freeSSL3HashItems, nullptr);
     PL_HashTableEnumerateEntries(it->host_rc4_table, freeRC4HashItems, nullptr);
+    PL_HashTableEnumerateEntries(it->host_failhandshake_table, freeRC4HashItems, nullptr);
     PL_HashTableDestroy(it->host_cert_table);
     PL_HashTableDestroy(it->host_clientauth_table);
     PL_HashTableDestroy(it->host_redir_table);
     PL_HashTableDestroy(it->host_ssl3_table);
     PL_HashTableDestroy(it->host_rc4_table);
+    PL_HashTableDestroy(it->host_failhandshake_table);
   }
 
   PR_Cleanup();
   return 0;
 }