Bug 1096197 - Ensure SSL Error reports work when there is no failed certificate chain. r=keeler
authorMark Goodwin <mgoodwin@mozilla.com>
Wed, 07 Jan 2015 02:28:00 -0500
changeset 222465 4f547cd4ce3da6147e8f78be0e4da2e64136e440
parent 222464 6461b8a32d0eb94340b023587e910a1d1da6a708
child 222466 adbabd8de78c56a3db8f54a3be87d28d848ad790
push id53631
push userryanvm@gmail.com
push dateWed, 07 Jan 2015 18:00:19 +0000
treeherdermozilla-inbound@cb37bece36f3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskeeler
bugs1096197
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1096197 - Ensure SSL Error reports work when there is no failed certificate chain. r=keeler
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
@@ -2638,21 +2638,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)
@@ -2672,21 +2667,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
@@ -148,17 +148,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]
@@ -394,16 +393,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 || os == 'linux' # 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 = [
   "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"
   ];
 
-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.
 #
@@ -222,17 +226,18 @@ 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
 
 # Hosts for ssl3/rc4 console warning tests
 https://ssl3.example.com:443        privileged,ssl3
 https://rc4.example.com:443         privileged,rc4
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -909,17 +909,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 self.useSSLTunnelExts and option in ('ssl3', 'rc4'):
+      if self.useSSLTunnelExts and 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;
 }