Bug 1207146 - Add a link to expert technical information in the cert error page. r=Gijs,keeler
authorPanos Astithas <past@mozilla.com>
Wed, 25 Nov 2015 17:30:27 +0200
changeset 309772 de962dcbc06c89cfb889426b0a6d7d56a4deffe4
parent 309771 2489a4b3a2a5fc17675499e56072ff4ef1a76d32
child 309773 8ddda69469f1f07e33db46f895ebffec55950dbb
push id5513
push userraliiev@mozilla.com
push dateMon, 25 Jan 2016 13:55:34 +0000
treeherdermozilla-beta@5ee97dd05b5c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs, keeler
bugs1207146
milestone45.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 1207146 - Add a link to expert technical information in the cert error page. r=Gijs,keeler
browser/base/content/aboutNetError.xhtml
browser/base/content/aboutcerterror/aboutCertError.xhtml
browser/base/content/browser.js
browser/base/content/content.js
browser/base/content/test/general/browser_aboutCertError.js
browser/base/content/test/general/browser_blockHPKP.js
browser/locales/en-US/chrome/browser/aboutCertError.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/locales/en-US/chrome/overrides/netError.dtd
browser/themes/shared/aboutCertError.css
browser/themes/shared/aboutNetError.css
security/manager/locales/en-US/chrome/pipnss/pipnss.properties
security/manager/ssl/TransportSecurityInfo.cpp
security/manager/ssl/nsNSSErrors.cpp
--- a/browser/base/content/aboutNetError.xhtml
+++ b/browser/base/content/aboutNetError.xhtml
@@ -280,70 +280,82 @@
         var event = new CustomEvent("AboutNetErrorLoad", {bubbles:true});
         document.dispatchEvent(event);
 
         if (err == "nssBadCert") {
           // Remove the "Try again" button for security exceptions, since it's
           // almost certainly useless.
           document.getElementById("errorTryAgain").style.display = "none";
           document.getElementById("errorPageContainer").setAttribute("class", "certerror");
-          addDomainErrorLink();
         }
         else {
           // Remove the override block for non-certificate errors.  CSS-hiding
           // isn't good enough here, because of bug 39098
           var secOverride = document.getElementById("securityOverrideDiv");
           secOverride.parentNode.removeChild(secOverride);
         }
+        addDomainErrorLinks();
       }
 
       function showSecuritySection() {
         // Swap link out, content in
         document.getElementById('securityOverrideContent').style.display = '';
         document.getElementById('securityOverrideLink').style.display = 'none';
       }
 
-      /* In the case of SSL error pages about domain mismatch, see if
+      /* Try to preserve the links contained in the error description, like
+         the error code.
+
+         Also, in the case of SSL error pages about domain mismatch, see if
          we can hyperlink the user to the correct site.  We don't want
          to do this generically since it allows MitM attacks to redirect
          users to a site under attacker control, but in certain cases
          it is safe (and helpful!) to do so.  Bug 402210
       */
-      function addDomainErrorLink() {
+      function addDomainErrorLinks() {
         // Rather than textContent, we need to treat description as HTML
         var sd = document.getElementById("errorShortDescText");
         if (sd) {
           var desc = getDescription();
 
           // sanitize description text - see bug 441169
 
-          // First, find the index of the <a> tag we care about, being careful not to
-          // use an over-greedy regex
-          var re = /<a id="cert_domain_link" title="([^"]+)">/;
-          var result = re.exec(desc);
-          if(!result)
+          // First, find the index of the <a> tags we care about, being
+          // careful not to use an over-greedy regex.
+          var codeRe = /<a id="errorCode" title="([^"]+)">/;
+          var codeResult = codeRe.exec(desc);
+          var domainRe = /<a id="cert_domain_link" title="([^"]+)">/;
+          var domainResult = domainRe.exec(desc);
+
+          // The order of these links in the description is fixed in
+          // TransportSecurityInfo.cpp:formatOverridableCertErrorMessage.
+          var firstResult = domainResult;
+          if(!domainResult)
+            firstResult = codeResult;
+          if (!firstResult)
             return;
-
           // Remove sd's existing children
           sd.textContent = "";
 
-          // Everything up to the link should be text content
-          sd.appendChild(document.createTextNode(desc.slice(0, result.index)));
+          // Everything up to the first link should be text content.
+          sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index)));
 
-          // Now create the link itself
-          var anchorEl = document.createElement("a");
-          anchorEl.setAttribute("id", "cert_domain_link");
-          anchorEl.setAttribute("title", result[1]);
-          anchorEl.appendChild(document.createTextNode(result[1]));
-          sd.appendChild(anchorEl);
+          // Now create the actual links.
+          if (domainResult) {
+            createLink(sd, "cert_domain_link", domainResult[1])
+            // Append text for anything between the two links.
+            sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index)));
+          }
+          createLink(sd, "errorCode", codeResult[1])
 
-          // Finally, append text for anything after the closing </a>
-          sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length)));
+          // Finally, append text for anything after the last closing </a>.
+          sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length)));
         }
 
+        // Initialize the cert domain link.
         var link = document.getElementById('cert_domain_link');
         if (!link)
           return;
 
         var okHost = link.getAttribute("title");
         var thisHost = document.location.hostname;
         var proto = document.location.protocol;
 
@@ -360,30 +372,39 @@
          * Make sure to include the "." ahead of thisHost so that
          * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com"
          *
          * We'd normally just use a RegExp here except that we lack a
          * library function to escape them properly (bug 248062), and
          * domain names are famous for having '.' characters in them,
          * which would allow spurious and possibly hostile matches.
          */
-        if (endsWith(okHost, "." + thisHost))
+        if (okHost.endsWith("." + thisHost))
           link.href = proto + okHost;
 
         /* case #2:
          * browser.garage.maemo.org uses an invalid security certificate.
          *
          * The certificate is only valid for garage.maemo.org
          */
-        if (endsWith(thisHost, "." + okHost))
+        if (thisHost.endsWith("." + okHost))
           link.href = proto + okHost;
+
+        // If we set a link, meaning there's something helpful for
+        // the user here, expand the section by default
+        if (link.href && getCSSClass() != "expertBadCert")
+          toggleVisibility("advancedPanel");
       }
 
-      function endsWith(haystack, needle) {
-        return haystack.slice(-needle.length) == needle;
+      function createLink(el, id, text) {
+        var anchorEl = document.createElement("a");
+        anchorEl.setAttribute("id", id);
+        anchorEl.setAttribute("title", text);
+        anchorEl.appendChild(document.createTextNode(text));
+        el.appendChild(anchorEl);
       }
     ]]></script>
   </head>
 
   <body dir="&locale.dir;">
 
     <!-- ERROR ITEM CONTAINER (removed during loading to avoid bug 39098) -->
     <div id="errorContainer">
@@ -439,18 +460,18 @@
         <div id="ed_nssFailure2">&nssFailure2.longDesc2;</div>
         <div id="ed_nssBadCert">&nssBadCert.longDesc2;</div>
         <div id="ed_malwareBlocked">&malwareBlocked.longDesc;</div>
         <div id="ed_unwantedBlocked">&unwantedBlocked.longDesc;</div>
         <div id="ed_forbiddenBlocked">&forbiddenBlocked.longDesc;</div>
         <div id="ed_cspBlocked">&cspBlocked.longDesc;</div>
         <div id="ed_remoteXUL">&remoteXUL.longDesc;</div>
         <div id="ed_corruptedContentError">&corruptedContentError.longDesc;</div>
-        <div id="ed_sslv3Used">&sslv3Used.longDesc;</div>
-        <div id="ed_weakCryptoUsed">&weakCryptoUsed.longDesc;</div>
+        <div id="ed_sslv3Used">&sslv3Used.longDesc2;</div>
+        <div id="ed_weakCryptoUsed">&weakCryptoUsed.longDesc2;</div>
       </div>
     </div>
 
     <!-- PAGE CONTAINER (for styling purposes only) -->
     <div id="errorPageContainer">
 
       <!-- Error Title -->
       <div id="errorTitle">
--- a/browser/base/content/aboutcerterror/aboutCertError.xhtml
+++ b/browser/base/content/aboutcerterror/aboutCertError.xhtml
@@ -50,16 +50,23 @@
         // parenthetical match is the second entry
         return decodeURIComponent(matches[1]);
       }
 
       function toggleVisibility(id)
       {
         var node = document.getElementById(id);
         node.style.visibility = node.style.visibility == "" ? "hidden" : "";
+        // Toggling the advanced panel must ensure that the debugging
+        // information panel is hidden as well, since it's opened by the
+        // error code link in the advanced panel.
+        if (id == "advancedPanel") {
+          var div = document.getElementById("certificateErrorDebugInformation");
+          div.style.display = "none";
+        }
       }
 
       function getDescription()
       {
         var url = document.documentURI;
         var desc = url.search(/d\=/);
 
         // desc == -1 if not found; if so, return an empty string
@@ -90,57 +97,89 @@
         if (cssClass != "badStsCert") {
           document.getElementById("badStsCertExplanation").setAttribute("hidden", "true");
         }
 
         var tech = document.getElementById("technicalContentText");
         if (tech)
           tech.textContent = getDescription();
 
-        addDomainErrorLink();
+        var event = new CustomEvent("AboutCertErrorLoad", {bubbles:true});
+        document.getElementById("advancedButton").dispatchEvent(event);
+
+        addDomainErrorLinks();
       }
 
-      /* In the case of SSL error pages about domain mismatch, see if
+      /* Try to preserve the links contained in the error description, like
+         the error code.
+
+         Also, in the case of SSL error pages about domain mismatch, see if
          we can hyperlink the user to the correct site.  We don't want
          to do this generically since it allows MitM attacks to redirect
          users to a site under attacker control, but in certain cases
          it is safe (and helpful!) to do so.  Bug 402210
       */
-      function addDomainErrorLink() {
+      function addDomainErrorLinks() {
         // Rather than textContent, we need to treat description as HTML
         var sd = document.getElementById("technicalContentText");
         if (sd) {
           var desc = getDescription();
 
           // sanitize description text - see bug 441169
 
-          // First, find the index of the <a> tag we care about, being careful not to
-          // use an over-greedy regex
-          var re = /<a id="cert_domain_link" title="([^"]+)">/;
-          var result = re.exec(desc);
-          if(!result)
+          // First, find the index of the <a> tags we care about, being
+          // careful not to use an over-greedy regex.
+          var codeRe = /<a id="errorCode" title="([^"]+)">/;
+          var codeResult = codeRe.exec(desc);
+          var domainRe = /<a id="cert_domain_link" title="([^"]+)">/;
+          var domainResult = domainRe.exec(desc);
+
+          // The order of these links in the description is fixed in
+          // TransportSecurityInfo.cpp:formatOverridableCertErrorMessage.
+          var firstResult = domainResult;
+          if(!domainResult)
+            firstResult = codeResult;
+          if (!firstResult)
             return;
 
           // Remove sd's existing children
           sd.textContent = "";
 
-          // Everything up to the link should be text content
-          sd.appendChild(document.createTextNode(desc.slice(0, result.index)));
+          // Everything up to the first link should be text content.
+          sd.appendChild(document.createTextNode(desc.slice(0, firstResult.index)));
 
-          // Now create the link itself
-          var anchorEl = document.createElement("a");
-          anchorEl.setAttribute("id", "cert_domain_link");
-          anchorEl.setAttribute("title", result[1]);
-          anchorEl.appendChild(document.createTextNode(result[1]));
-          sd.appendChild(anchorEl);
+          // Now create the actual links.
+          if (domainResult) {
+            createLink(sd, "cert_domain_link", domainResult[1])
+            // Append text for anything between the two links.
+            sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length, codeResult.index)));
+          }
+          createLink(sd, "errorCode", codeResult[1])
 
-          // Finally, append text for anything after the closing </a>
-          sd.appendChild(document.createTextNode(desc.slice(desc.indexOf("</a>") + "</a>".length)));
+          // Finally, append text for anything after the last closing </a>.
+          sd.appendChild(document.createTextNode(desc.slice(desc.lastIndexOf("</a>") + "</a>".length)));
         }
 
+        // Initialize the error code link embedded in the error message to
+        // display debug information about the cert error.
+        var errorCode = document.getElementById("errorCode");
+        if (errorCode) {
+          errorCode.href = "#technicalInformation";
+          errorCode.addEventListener("click", () => {
+            var div = document.getElementById("certificateErrorDebugInformation");
+            if (div.style.display == "block") {
+              div.style.display = "none";
+            } else {
+              div.style.display = "block";
+              div.scrollIntoView(true);
+            }
+          }, false);
+        }
+
+        // Then initialize the cert domain link.
         var link = document.getElementById('cert_domain_link');
         if (!link)
           return;
 
         var okHost = link.getAttribute("title");
         var thisHost = document.location.hostname;
         var proto = document.location.protocol;
 
@@ -173,16 +212,24 @@
         if (thisHost.endsWith("." + okHost))
           link.href = proto + okHost;
 
         // If we set a link, meaning there's something helpful for
         // the user here, expand the section by default
         if (link.href && getCSSClass() != "expertBadCert")
           toggleVisibility("advancedPanel");
       }
+
+      function createLink(el, id, text) {
+        var anchorEl = document.createElement("a");
+        anchorEl.setAttribute("id", id);
+        anchorEl.setAttribute("title", text);
+        anchorEl.appendChild(document.createTextNode(text));
+        el.appendChild(anchorEl);
+      }
     ]]></script>
   </head>
 
   <body dir="&locale.dir;">
     <!-- PAGE CONTAINER (for styling purposes only) -->
     <div id="errorPageContainer">
 
       <!-- Error Title -->
@@ -205,21 +252,28 @@
         <div id="buttonContainer">
           <button id="returnButton" autocomplete="off" autofocus="true">&certerror.returnToPreviousPage.label;</button>
           <div id="buttonSpacer"></div>
           <button id="advancedButton" autocomplete="off" onclick="toggleVisibility('advancedPanel');" autofocus="true">&certerror.advanced.label;</button>
         </div>
         <!-- Advanced panel, which is hidden by default -->
         <div id="advancedPanel" style="visibility: hidden;">
           <p id="technicalContentText"/>
-          <button id='exceptionDialogButton'>&certerror.addException.label;</button>
+          <button id="exceptionDialogButton">&certerror.addException.label;</button>
         </div>
       </div>
     </div>
 
+    <div id="certificateErrorDebugInformation">
+      <a name="technicalInformation"></a>
+      <button id="copyToClipboard">&certerror.copyToClipboard.label;</button>
+      <div id="certificateErrorText"/>
+      <button id="copyToClipboard">&certerror.copyToClipboard.label;</button>
+    </div>
+
     <!--
     - Note: It is important to run the script this way, instead of using
     - an onload handler. This is because error pages are loaded as
     - LOAD_BACKGROUND, which means that onload handlers will not be executed.
     -->
     <script type="application/javascript">initPage();</script>
 
   </body>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2696,17 +2696,17 @@ var BrowserOnClick = {
     }
   },
 
   receiveMessage: function (msg) {
     switch (msg.name) {
       case "Browser:CertExceptionError":
         this.onAboutCertError(msg.target, msg.data.elementId,
                               msg.data.isTopFrame, msg.data.location,
-                              msg.data.sslStatusAsString);
+                              msg.data.securityInfoAsString);
       break;
       case "Browser:SiteBlockedError":
         this.onAboutBlocked(msg.data.elementId, msg.data.reason,
                             msg.data.isTopFrame, msg.data.location);
       break;
       case "Browser:EnableOnlineMode":
         if (Services.io.offline) {
           // Reset network state and refresh the page.
@@ -2782,29 +2782,16 @@ var BrowserOnClick = {
      * - Error type (e.g. Pinning, domain mismatch, etc)
      * - Cert chain (at minimum, same data to distrust each cert in the
      *   chain)
      * - Request data (e.g. User Agent, IP, Timestamp)
      *
      * The request data should be added to the report by the receiving server.
      */
 
-    // TODO: can we pull this in from pippki.js isntead of duplicating it
-    // here?
-    function getDERString(cert)
-    {
-      var length = {};
-      var derArray = cert.getRawDER(length);
-      var derString = '';
-      for (var i = 0; i < derArray.length; i++) {
-        derString += String.fromCharCode(derArray[i]);
-      }
-      return derString;
-    }
-
     // Convert the nsIX509CertList into a format that can be parsed into
     // JSON
     let asciiCertChain = [];
 
     if (transportSecurityInfo.failedCertChain) {
       let certs = transportSecurityInfo.failedCertChain.getEnumerator();
       while (certs.hasMoreElements()) {
         let cert = certs.getNext();
@@ -2851,29 +2838,30 @@ var BrowserOnClick = {
       } else {
         showReportStatus("complete");
       }
     };
 
     xhr.send(JSON.stringify(report));
   },
 
-  onAboutCertError: function (browser, elementId, isTopFrame, location, sslStatusAsString) {
+  onAboutCertError: function (browser, elementId, isTopFrame, location, securityInfoAsString) {
     let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
 
     switch (elementId) {
       case "exceptionDialogButton":
         if (isTopFrame) {
           secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_CLICK_ADD_EXCEPTION);
         }
 
         let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
                            .getService(Ci.nsISerializationHelper);
-        let sslStatus = serhelper.deserializeObject(sslStatusAsString);
-        sslStatus.QueryInterface(Components.interfaces.nsISSLStatus);
+        let securityInfo = serhelper.deserializeObject(securityInfoAsString);
+        let sslStatus = securityInfo.QueryInterface(Ci.nsISSLStatusProvider)
+                                    .SSLStatus;
         let params = { exceptionAdded : false,
                        sslStatus : sslStatus };
 
         try {
           switch (Services.prefs.getIntPref("browser.ssl_override_behavior")) {
             case 2 : // Pre-fetch & pre-populate
               params.prefetchCert = true;
             case 1 : // Pre-populate
@@ -2898,16 +2886,29 @@ var BrowserOnClick = {
         }
         goBackFromErrorPage();
         break;
 
       case "advancedButton":
         if (isTopFrame) {
           secHistogram.add(Ci.nsISecurityUITelemetry.WARNING_BAD_CERT_TOP_UNDERSTAND_RISKS);
         }
+
+        let errorInfo = getDetailedCertErrorInfo(location,
+                                                 securityInfoAsString);
+        browser.messageManager.sendAsyncMessage("AboutCertErrorDetails",
+                                                { info: errorInfo });
+        break;
+
+      case "copyToClipboard":
+        const gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
+                                    .getService(Ci.nsIClipboardHelper);
+        let detailedInfo = getDetailedCertErrorInfo(location,
+                                                    securityInfoAsString);
+        gClipboardHelper.copyString(detailedInfo);
         break;
 
     }
   },
 
   onAboutBlocked: function (elementId, reason, isTopFrame, location) {
     // Depending on what page we are displaying here (malware/phishing/unwanted)
     // use the right strings and links for each.
@@ -3152,16 +3153,86 @@ function BrowserReloadWithFlags(reloadFl
 
   gBrowser.selectedBrowser
           .messageManager
           .sendAsyncMessage("Browser:Reload",
                             { flags: reloadFlags,
                               handlingUserInput: windowUtils.isHandlingUserInput });
 }
 
+/**
+ * Returns a string with detailed information about the certificate validation
+ * failure from the specified URI that can be used to send a report.
+ */
+function getDetailedCertErrorInfo(location, securityInfoAsString) {
+  if (!securityInfoAsString)
+    return "";
+
+  let details = [];
+  details.push(location);
+
+  const serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+                       .getService(Ci.nsISerializationHelper);
+  let securityInfo = serhelper.deserializeObject(securityInfoAsString);
+  securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+
+  let errors = Cc["@mozilla.org/nss_errors_service;1"]
+                  .getService(Ci.nsINSSErrorsService);
+  let code = securityInfo.errorCode;
+  details.push(errors.getErrorMessage(errors.getXPCOMFromNSSError(code)));
+
+  const sss = Cc["@mozilla.org/ssservice;1"]
+                 .getService(Ci.nsISiteSecurityService);
+  // SiteSecurityService uses different storage if the channel is
+  // private. Thus we must give isSecureHost correct flags or we
+  // might get incorrect results.
+  let flags = PrivateBrowsingUtils.isWindowPrivate(window) ?
+              Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0;
+
+  let uri = Services.io.newURI(location, null, null);
+  details.push(sss.isSecureHost(sss.HEADER_HSTS, uri.host, flags));
+  details.push(sss.isSecureHost(sss.HEADER_HPKP, uri.host, flags));
+
+  let certChain = "";
+  if (securityInfo.failedCertChain) {
+    let certs = securityInfo.failedCertChain.getEnumerator();
+    while (certs.hasMoreElements()) {
+      let cert = certs.getNext();
+      cert.QueryInterface(Ci.nsIX509Cert);
+      certChain += getPEMString(cert);
+    }
+  }
+  details.push(certChain);
+  return gNavigatorBundle.getFormattedString("certErrorDetails.label", details, 5);
+}
+
+// TODO: can we pull getDERString and getPEMString in from pippki.js instead of
+// duplicating them here?
+function getDERString(cert)
+{
+  var length = {};
+  var derArray = cert.getRawDER(length);
+  var derString = '';
+  for (var i = 0; i < derArray.length; i++) {
+    derString += String.fromCharCode(derArray[i]);
+  }
+  return derString;
+}
+
+function getPEMString(cert)
+{
+  var derb64 = btoa(getDERString(cert));
+  // Wrap the Base64 string into lines of 64 characters,
+  // with CRLF line breaks (as specified in RFC 1421).
+  var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n");
+  return "-----BEGIN CERTIFICATE-----\r\n"
+         + wrapped
+         + "\r\n-----END CERTIFICATE-----\r\n";
+}
+
 var PrintPreviewListener = {
   _printPreviewTab: null,
   _tabBeforePrintPreview: null,
 
   getPrintPreviewBrowser: function () {
     if (!this._printPreviewTab) {
       let browser = gBrowser.selectedTab.linkedBrowser;
       let forceNotRemote = gMultiProcessBrowser && !browser.isRemoteBrowser;
@@ -7940,9 +8011,8 @@ TabModalPromptBox.prototype = {
   get browser() {
     let browser = this._weakBrowserRef.get();
     if (!browser) {
       throw "Stale promptbox! The associated browser is gone.";
     }
     return browser;
   },
 };
-
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -311,17 +311,16 @@ var AboutNetErrorListener = {
           reportSendingMsg.style.display = "none";
           sendAsyncMessage("Browser:SSLErrorReportTelemetry",
                            {reportStatus: TLS_ERROR_REPORT_TELEMETRY_SUCCESS});
           break;
         }
       }
     });
 
-    let failedChannel = docShell.failedChannel;
     let location = contentDoc.location.href;
 
     let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
                      .getService(Ci.nsISerializationHelper);
 
     let serializable =  docShell.failedChannel.securityInfo
                                 .QueryInterface(Ci.nsITransportSecurityInfo)
                                 .QueryInterface(Ci.nsISerializable);
@@ -431,31 +430,31 @@ var ClickEventHandler = {
   },
 
   onAboutCertError: function (targetElement, ownerDoc) {
     let docshell = ownerDoc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
                                        .getInterface(Ci.nsIWebNavigation)
                                        .QueryInterface(Ci.nsIDocShell);
     let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
                      .getService(Ci.nsISerializationHelper);
-    let serializedSSLStatus = "";
+    let serializedSecurityInfo = "";
 
     try {
       let serializable =  docShell.failedChannel.securityInfo
-                                  .QueryInterface(Ci.nsISSLStatusProvider)
-                                  .SSLStatus
+                                  .QueryInterface(Ci.nsITransportSecurityInfo)
                                   .QueryInterface(Ci.nsISerializable);
-      serializedSSLStatus = serhelper.serializeToString(serializable);
+
+      serializedSecurityInfo = serhelper.serializeToString(serializable);
     } catch (e) { }
 
     sendAsyncMessage("Browser:CertExceptionError", {
       location: ownerDoc.location.href,
       elementId: targetElement.getAttribute("id"),
       isTopFrame: (ownerDoc.defaultView.parent === ownerDoc.defaultView),
-      sslStatusAsString: serializedSSLStatus
+      securityInfoAsString: serializedSecurityInfo
     });
   },
 
   onAboutBlocked: function (targetElement, ownerDoc) {
     var reason = 'phishing';
     if (/e=malwareBlocked/.test(ownerDoc.documentURI)) {
       reason = 'malware';
     } else if (/e=unwantedBlocked/.test(ownerDoc.documentURI)) {
@@ -549,16 +548,27 @@ var pluginContent = new PluginContent(gl
 addEventListener("DOMWebNotificationClicked", function(event) {
   sendAsyncMessage("DOMWebNotificationClicked", {});
 }, false);
 
 addEventListener("DOMServiceWorkerFocusClient", function(event) {
   sendAsyncMessage("DOMServiceWorkerFocusClient", {});
 }, false);
 
+addEventListener("AboutCertErrorLoad", function(event) {
+  let originalTarget = event.originalTarget;
+  let ownerDoc = originalTarget.ownerDocument;
+  ClickEventHandler.onAboutCertError(originalTarget, ownerDoc);
+}, false, true);
+
+addMessageListener("AboutCertErrorDetails", function(message) {
+  let div = content.document.getElementById("certificateErrorText");
+  div.textContent = message.data.info;
+});
+
 ContentWebRTC.init();
 addMessageListener("rtcpeer:Allow", ContentWebRTC);
 addMessageListener("rtcpeer:Deny", ContentWebRTC);
 addMessageListener("webrtc:Allow", ContentWebRTC);
 addMessageListener("webrtc:Deny", ContentWebRTC);
 addMessageListener("webrtc:StopSharing", ContentWebRTC);
 addMessageListener("webrtc:StartBrowserSharing", () => {
   let windowID = content.QueryInterface(Ci.nsIInterfaceRequestor)
--- a/browser/base/content/test/general/browser_aboutCertError.js
+++ b/browser/base/content/test/general/browser_aboutCertError.js
@@ -1,13 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-// This is testing the aboutCertError page (Bug 1207107).  It's a start,
-// but should be expanded to include cert_domain_link
+// This is testing the aboutCertError page (Bug 1207107).
 
 const GOOD_PAGE = "https://example.com/";
 const BAD_CERT = "https://expired.example.com/";
 const BAD_STS_CERT = "https://badchain.include-subdomains.pinning.example.com:443";
 const {TabStateFlusher} = Cu.import("resource:///modules/sessionstore/TabStateFlusher.jsm", {});
 const ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
 
 add_task(function* checkReturnToAboutHome() {
@@ -98,17 +97,186 @@ add_task(function* checkBadStsCert() {
     let exceptionButton = doc.getElementById("exceptionDialogButton");
     return exceptionButton.hidden;
   });
   ok(exceptionButtonHidden, "Exception button is hidden");
 
   gBrowser.removeCurrentTab();
 });
 
+add_task(function* checkAdvancedDetails() {
+  info("Loading a bad cert page and verifying the advanced details section");
+  let browser;
+  let certErrorLoaded;
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+    gBrowser.selectedTab = gBrowser.addTab(BAD_CERT);
+    browser = gBrowser.selectedBrowser;
+    certErrorLoaded = waitForCertErrorLoad(browser);
+  }, false);
+
+  info("Loading and waiting for the cert error");
+  yield certErrorLoaded;
+
+  let message = yield ContentTask.spawn(browser, null, function* () {
+    let doc = content.document;
+    let advancedButton = doc.getElementById("advancedButton");
+    advancedButton.click();
+    let el = doc.getElementById("errorCode");
+    return { textContent: el.textContent, tagName: el.tagName };
+  });
+  is(message.textContent, "SEC_ERROR_EXPIRED_CERTIFICATE",
+     "Correct error message found");
+  is(message.tagName, "a", "Error message is a link");
+
+  message = yield ContentTask.spawn(browser, null, function* () {
+    let doc = content.document;
+    let errorCode = doc.getElementById("errorCode");
+    errorCode.click();
+    let div = doc.getElementById("certificateErrorDebugInformation");
+    let text = doc.getElementById("certificateErrorText");
+
+    let docshell = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIWebNavigation)
+                          .QueryInterface(Ci.nsIDocShell);
+    let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+                     .getService(Ci.nsISerializationHelper);
+    let serializable =  docShell.failedChannel.securityInfo
+                                .QueryInterface(Ci.nsITransportSecurityInfo)
+                                .QueryInterface(Ci.nsISerializable);
+    let serializedSecurityInfo = serhelper.serializeToString(serializable);
+    return {
+      divDisplay: div.style.display,
+      text: text.textContent,
+      securityInfoAsString: serializedSecurityInfo
+    };
+  });
+  is(message.divDisplay, "block", "Debug information is visible");
+  ok(message.text.contains(BAD_CERT), "Correct URL found");
+  ok(message.text.contains("Certificate has expired"),
+     "Correct error message found");
+  ok(message.text.contains("HTTP Strict Transport Security: false"),
+     "Correct HSTS value found");
+  ok(message.text.contains("HTTP Public Key Pinning: false"),
+     "Correct HPKP value found");
+  let certChain = getCertChain(message.securityInfoAsString);
+  ok(message.text.contains(certChain), "Found certificate chain");
+
+  gBrowser.removeCurrentTab();
+});
+
+add_task(function* checkAdvancedDetailsForHSTS() {
+  info("Loading a bad STS cert page and verifying the advanced details section");
+  let browser;
+  let certErrorLoaded;
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+    gBrowser.selectedTab = gBrowser.addTab(BAD_STS_CERT);
+    browser = gBrowser.selectedBrowser;
+    certErrorLoaded = waitForCertErrorLoad(browser);
+  }, false);
+
+  info("Loading and waiting for the cert error");
+  yield certErrorLoaded;
+
+  let message = yield ContentTask.spawn(browser, null, function* () {
+    let doc = content.document;
+    let advancedButton = doc.getElementById("advancedButton");
+    advancedButton.click();
+    let ec = doc.getElementById("errorCode");
+    let cdl = doc.getElementById("cert_domain_link");
+    return {
+      ecTextContent: ec.textContent,
+      ecTagName: ec.tagName,
+      cdlTextContent: cdl.textContent,
+      cdlTagName: cdl.tagName
+    };
+  });
+
+  const badStsUri = Services.io.newURI(BAD_STS_CERT, null, null);
+  is(message.ecTextContent, "SSL_ERROR_BAD_CERT_DOMAIN",
+     "Correct error message found");
+  is(message.ecTagName, "a", "Error message is a link");
+  const url = badStsUri.prePath.slice(badStsUri.prePath.indexOf(".") + 1);
+  is(message.cdlTextContent, url,
+     "Correct cert_domain_link contents found");
+  is(message.cdlTagName, "a", "cert_domain_link is a link");
+
+  message = yield ContentTask.spawn(browser, null, function* () {
+    let doc = content.document;
+    let errorCode = doc.getElementById("errorCode");
+    errorCode.click();
+    let div = doc.getElementById("certificateErrorDebugInformation");
+    let text = doc.getElementById("certificateErrorText");
+
+    let docshell = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                          .getInterface(Ci.nsIWebNavigation)
+                          .QueryInterface(Ci.nsIDocShell);
+    let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+                     .getService(Ci.nsISerializationHelper);
+    let serializable =  docShell.failedChannel.securityInfo
+                                .QueryInterface(Ci.nsITransportSecurityInfo)
+                                .QueryInterface(Ci.nsISerializable);
+    let serializedSecurityInfo = serhelper.serializeToString(serializable);
+    return {
+      divDisplay: div.style.display,
+      text: text.textContent,
+      securityInfoAsString: serializedSecurityInfo
+    };
+  });
+  is(message.divDisplay, "block", "Debug information is visible");
+  ok(message.text.contains(badStsUri.spec), "Correct URL found");
+  ok(message.text.contains("requested domain name does not match the server's certificate"),
+     "Correct error message found");
+  ok(message.text.contains("HTTP Strict Transport Security: false"),
+     "Correct HSTS value found");
+  ok(message.text.contains("HTTP Public Key Pinning: true"),
+     "Correct HPKP value found");
+  let certChain = getCertChain(message.securityInfoAsString);
+  ok(message.text.contains(certChain), "Found certificate chain");
+
+  gBrowser.removeCurrentTab();
+});
+
 function waitForCertErrorLoad(browser) {
   return new Promise(resolve => {
     info("Waiting for DOMContentLoaded event");
     browser.addEventListener("DOMContentLoaded", function load() {
       browser.removeEventListener("DOMContentLoaded", load, false, true);
       resolve();
     }, false, true);
   });
 }
+
+function getCertChain(securityInfoAsString) {
+  let certChain = "";
+  const serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+                       .getService(Ci.nsISerializationHelper);
+  let securityInfo = serhelper.deserializeObject(securityInfoAsString);
+  securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+  let certs = securityInfo.failedCertChain.getEnumerator();
+  while (certs.hasMoreElements()) {
+    let cert = certs.getNext();
+    cert.QueryInterface(Ci.nsIX509Cert);
+    certChain += getPEMString(cert);
+  }
+  return certChain;
+}
+
+function getDERString(cert)
+{
+  var length = {};
+  var derArray = cert.getRawDER(length);
+  var derString = '';
+  for (var i = 0; i < derArray.length; i++) {
+    derString += String.fromCharCode(derArray[i]);
+  }
+  return derString;
+}
+
+function getPEMString(cert)
+{
+  var derb64 = btoa(getDERString(cert));
+  // Wrap the Base64 string into lines of 64 characters,
+  // with CRLF line breaks (as specified in RFC 1421).
+  var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n");
+  return "-----BEGIN CERTIFICATE-----\r\n"
+         + wrapped
+         + "\r\n-----END CERTIFICATE-----\r\n";
+}
--- a/browser/base/content/test/general/browser_blockHPKP.js
+++ b/browser/base/content/test/general/browser_blockHPKP.js
@@ -68,17 +68,17 @@ var successfulPinningPageListener = {
 
 // The browser should load about:neterror, when this happens, proceed
 // to load the pinning domain again, this time removing the pinning information
 var certErrorProgressListener = {
   onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
     if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
       let textElement = content.document.getElementById("errorShortDescText");
       let text = textElement.innerHTML;
-      ok(text.indexOf("mozilla_pkix_error_key_pinning_failure") > 0,
+      ok(text.indexOf("MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE") > 0,
          "Got a pinning error page");
       gBrowser.removeProgressListener(this);
       gBrowser.selectedBrowser.addEventListener("load",
                                                 successfulPinningRemovalPageListener,
                                                 true);
       gBrowser.selectedBrowser.loadURI("https://" + kPinningDomain + kURLPath + "zeromaxagevalid");
     }
   }
--- a/browser/locales/en-US/chrome/browser/aboutCertError.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutCertError.dtd
@@ -28,8 +28,9 @@ certificate.">
 
 <!ENTITY certerror.expert.content "If you understand what's going on, you
 can tell &brandShortName; to start trusting this site's identification.
 <b>Even if you trust the site, this error could mean that someone is
 tampering with your connection.</b>">
 <!ENTITY certerror.expert.contentPara2 "Don't add an exception unless
 you know there's a good reason why this site doesn't use trusted identification.">
 <!ENTITY certerror.addException.label "Add Exception…">
+<!ENTITY certerror.copyToClipboard.label "Copy text to clipboard">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -796,14 +796,21 @@ muteTab.label = Mute Tab
 muteTab.accesskey = M
 unmuteTab.label = Unmute Tab
 unmuteTab.accesskey = M
 
 # LOCALIZATION NOTE (weakCryptoOverriding.message): %S is brandShortName
 weakCryptoOverriding.message = %S recommends that you don't enter your password, credit card and other personal information on this website.
 revokeOverride.label = Don't Trust This Website
 revokeOverride.accesskey = D
+# LOCALIZATION NOTE (certErrorDetails.label): This is a text string that
+# appears in the about:certerror page, so that the user can copy and send it to
+# the server administrators for troubleshooting. %1$S is the visited URL, %2$S
+# is the error message, %3$S is true or false, depending on whether the server
+# supports HSTS, %4$S is true or false, depending on whether the server
+# supports HPKP, %5$S is the certificate chain in PEM format.
+certErrorDetails.label = %1$S\r\n\r\n%2$S\r\n\r\nHTTP Strict Transport Security: %3$S\r\nHTTP Public Key Pinning: %4$S\r\n\r\nCertificate chain:\r\n\r\n%5$S
 
 # LOCALIZATION NOTE (tabgroups.migration.anonGroup):
 # %S is the group number/ID
 tabgroups.migration.anonGroup = Group %S
 tabgroups.migration.tabGroupBookmarkFolderName = Bookmarked Tab Groups
 
--- a/browser/locales/en-US/chrome/overrides/netError.dtd
+++ b/browser/locales/en-US/chrome/overrides/netError.dtd
@@ -210,19 +210,19 @@ functionality specific to firefox. -->
 <!ENTITY errorReporting.sent "Report sent">
 <!ENTITY errorReporting.report "Report">
 <!ENTITY errorReporting.tryAgain "Try again">
 
 <!ENTITY remoteXUL.title "Remote XUL">
 <!ENTITY remoteXUL.longDesc "<p><ul><li>Please contact the website owners to inform them of this problem.</li></ul></p>">
 
 <!ENTITY sslv3Used.title "Unable to Connect Securely">
-<!-- LOCALIZATION NOTE (sslv3Used.longDesc) - Do not translate
-     "ssl_error_unsupported_version". -->
-<!ENTITY sslv3Used.longDesc "Advanced info: ssl_error_unsupported_version">
+<!-- LOCALIZATION NOTE (sslv3Used.longDesc2) - Do not translate
+     "SSL_ERROR_UNSUPPORTED_VERSION". -->
+<!ENTITY sslv3Used.longDesc2 "Advanced info: SSL_ERROR_UNSUPPORTED_VERSION">
 
 <!ENTITY weakCryptoUsed.title "Your connection is not secure">
-<!-- LOCALIZATION NOTE (weakCryptoUsed.longDesc) - Do not translate
-     "ssl_error_no_cypher_overlap". -->
-<!ENTITY weakCryptoUsed.longDesc "Advanced info: ssl_error_no_cypher_overlap">
+<!-- LOCALIZATION NOTE (weakCryptoUsed.longDesc2) - Do not translate
+     "SSL_ERROR_NO_CYPHER_OVERLAP". -->
+<!ENTITY weakCryptoUsed.longDesc2 "Advanced info: SSL_ERROR_NO_CYPHER_OVERLAP">
 <!ENTITY weakCryptoAdvanced.title "Advanced">
 <!ENTITY weakCryptoAdvanced.longDesc "<span class='hostname'></span> uses security technology that is outdated and vulnerable to attack. An attacker could easily reveal information which you thought to be safe.">
 <!ENTITY weakCryptoAdvanced.override "(Not secure) Try loading <span class='hostname'></span> using outdated security">
--- a/browser/themes/shared/aboutCertError.css
+++ b/browser/themes/shared/aboutCertError.css
@@ -48,16 +48,37 @@ body {
   display: flex;
   flex-flow: row wrap;
 }
 
 #buttonSpacer {
   flex: 1;
 }
 
+#certificateErrorDebugInformation {
+  display: none;
+  background-color: var(--in-content-box-background-hover) !important;
+  border-top: 1px solid var(--in-content-border-color);
+  position: absolute;
+  left: 0%;
+  top: 100%;
+  width: 65%;
+  padding: 1em 17.5%;
+}
+
+#certificateErrorText {
+  font-family: monospace;
+  white-space: pre-wrap;
+  padding: 1em 0;
+}
+
+#errorCode {
+  white-space: nowrap;
+}
+
 #returnButton {
   background-color: var(--in-content-primary-button-background);
   border: none;
   color: var(--in-content-selected-text);
   min-width: 250px;
   margin-inline-start: 0;
 }
 
--- a/browser/themes/shared/aboutNetError.css
+++ b/browser/themes/shared/aboutNetError.css
@@ -168,8 +168,14 @@ div#weakCryptoAdvancedPanel {
 
 span#hostname {
   font-weight: bold;
 }
 
 #automaticallyReportInFuture {
   cursor: pointer;
 }
+
+#errorCode {
+  color: var(--in-content-page-color);
+  cursor: text;
+  text-decoration: none;
+}
--- a/security/manager/locales/en-US/chrome/pipnss/pipnss.properties
+++ b/security/manager/locales/en-US/chrome/pipnss/pipnss.properties
@@ -287,17 +287,18 @@ certErrorMismatchSinglePlain=The certifi
 certErrorMismatchMultiple=The certificate is only valid for the following names:
 certErrorMismatchNoNames=The certificate is not valid for any server names.
 
 # LOCALIZATION NOTE (certErrorExpiredNow): Do not translate %1$S (date+time of expired certificate) or %2$S (current date+time)
 certErrorExpiredNow=The certificate expired on %1$S. The current time is %2$S.
 # LOCALIZATION NOTE (certErrorNotYetValidNow): Do not translate %1$S (date+time certificate will become valid) or %2$S (current date+time)
 certErrorNotYetValidNow=The certificate will not be valid until %1$S. The current time is %2$S.
 
-certErrorCodePrefix=(Error code: %S)
+# LOCALIZATION NOTE (certErrorCodePrefix2): Do not translate <a id="errorCode" title="%1$S">%1$S</a>
+certErrorCodePrefix2=Error code: <a id="errorCode" title="%1$S">%1$S</a>
 
 CertInfoIssuedFor=Issued to:
 CertInfoIssuedBy=Issued by:
 CertInfoValid=Valid
 CertInfoFrom=from
 CertInfoTo=to
 CertInfoPurposes=Purposes
 CertInfoEmail=Email
--- a/security/manager/ssl/TransportSecurityInfo.cpp
+++ b/security/manager/ssl/TransportSecurityInfo.cpp
@@ -877,25 +877,24 @@ static void
 AppendErrorTextCode(PRErrorCode errorCodeToReport,
                     nsINSSComponent *component,
                     nsString &returnedMessage)
 {
   const char *codeName = nsNSSErrors::getDefaultErrorStringName(errorCodeToReport);
   if (codeName)
   {
     nsCString error_id(codeName);
-    ToLowerCase(error_id);
     NS_ConvertASCIItoUTF16 idU(error_id);
 
     const char16_t *params[1];
     params[0] = idU.get();
 
     nsString formattedString;
     nsresult rv;
-    rv = component->PIPBundleFormatStringFromName("certErrorCodePrefix", 
+    rv = component->PIPBundleFormatStringFromName("certErrorCodePrefix2",
                                                   params, 1, 
                                                   formattedString);
     if (NS_SUCCEEDED(rv)) {
       returnedMessage.Append('\n');
       returnedMessage.Append(formattedString);
       returnedMessage.Append('\n');
     }
     else {
--- a/security/manager/ssl/nsNSSErrors.cpp
+++ b/security/manager/ssl/nsNSSErrors.cpp
@@ -73,24 +73,23 @@ nsNSSErrors::getErrorMessageFromCode(PRE
     returnedMessage.AppendASCII(PR_ErrorToString(err, PR_LANGUAGE_EN));
     returnedMessage.Append('\n');
   }
   
   if (nss_error_id_str)
   {
     nsresult rv;
     nsCString error_id(nss_error_id_str);
-    ToLowerCase(error_id);
     NS_ConvertASCIItoUTF16 idU(error_id);
 
     const char16_t *params[1];
     params[0] = idU.get();
 
     nsString formattedString;
-    rv = component->PIPBundleFormatStringFromName("certErrorCodePrefix", 
+    rv = component->PIPBundleFormatStringFromName("certErrorCodePrefix2",
                                                   params, 1, 
                                                   formattedString);
     if (NS_SUCCEEDED(rv)) {
       returnedMessage.Append('\n');
       returnedMessage.Append(formattedString);
       returnedMessage.Append('\n');
     }
     else {