Bug 932179 - Part 1: Expose security information in the WebConsoleActor. r=past
authorSami Jaktholm <sjakthol@outlook.com>
Tue, 06 Jan 2015 02:58:00 +0200
changeset 223083 c4d908fa4442a6132527bd49bbc9bc1186c91ee7
parent 223082 2c8cdce4fed4dadf98f1ef32ced6f6518a400478
child 223084 7f0b79f1904f19d0fd666d7eb0c5f188bee3a5ae
push id10756
push userpastithas@mozilla.com
push dateSun, 11 Jan 2015 09:33:32 +0000
treeherderfx-team@4e343b8494f9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs932179
milestone37.0a1
Bug 932179 - Part 1: Expose security information in the WebConsoleActor. r=past
toolkit/devtools/server/actors/webconsole.js
toolkit/devtools/webconsole/client.js
toolkit/devtools/webconsole/network-helper.js
toolkit/devtools/webconsole/network-monitor.js
toolkit/devtools/webconsole/test/chrome.ini
toolkit/devtools/webconsole/test/test_network_get.html
toolkit/devtools/webconsole/test/test_network_longstring.html
toolkit/devtools/webconsole/test/test_network_post.html
toolkit/devtools/webconsole/test/test_network_security-hpkp.html
toolkit/devtools/webconsole/test/test_network_security-hsts.html
toolkit/devtools/webconsole/test/unit/test_security-info-certificate.js
toolkit/devtools/webconsole/test/unit/test_security-info-parser.js
toolkit/devtools/webconsole/test/unit/test_security-info-protocol-version.js
toolkit/devtools/webconsole/test/unit/test_security-info-state.js
toolkit/devtools/webconsole/test/unit/xpcshell.ini
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -1750,16 +1750,30 @@ NetworkEventActor.prototype =
     return {
       from: this.actorID,
       postData: this._request.postData,
       postDataDiscarded: this._discardRequestBody,
     };
   },
 
   /**
+   * The "getSecurityInfo" packet type handler.
+   *
+   * @return object
+   *         The response packet - connection security information.
+   */
+  onGetSecurityInfo: function NEA_onGetSecurityInfo()
+  {
+    return {
+      from: this.actorID,
+      securityInfo: this._securityInfo,
+    };
+  },
+
+  /**
    * The "getResponseHeaders" packet type handler.
    *
    * @return object
    *         The response packet - network response headers.
    */
   onGetResponseHeaders: function NEA_onGetResponseHeaders()
   {
     return {
@@ -1905,16 +1919,36 @@ NetworkEventActor.prototype =
       updateType: "responseStart",
       response: aInfo,
     };
 
     this.conn.send(packet);
   },
 
   /**
+   * Add connection security information.
+   *
+   * @param object info
+   *        The object containing security information.
+   */
+  addSecurityInfo: function NEA_addSecurityInfo(info)
+  {
+    this._securityInfo = info;
+
+    let packet = {
+      from: this.actorID,
+      type: "networkEventUpdate",
+      updateType: "securityInfo",
+      state: info.state,
+    };
+
+    this.conn.send(packet);
+  },
+
+  /**
    * Add network response headers.
    *
    * @param array aHeaders
    *        The response headers array.
    */
   addResponseHeaders: function NEA_addResponseHeaders(aHeaders)
   {
     this._response.headers = aHeaders;
@@ -2027,9 +2061,10 @@ NetworkEventActor.prototype.requestTypes
   "release": NetworkEventActor.prototype.onRelease,
   "getRequestHeaders": NetworkEventActor.prototype.onGetRequestHeaders,
   "getRequestCookies": NetworkEventActor.prototype.onGetRequestCookies,
   "getRequestPostData": NetworkEventActor.prototype.onGetRequestPostData,
   "getResponseHeaders": NetworkEventActor.prototype.onGetResponseHeaders,
   "getResponseCookies": NetworkEventActor.prototype.onGetResponseCookies,
   "getResponseContent": NetworkEventActor.prototype.onGetResponseContent,
   "getEventTimings": NetworkEventActor.prototype.onGetEventTimings,
+  "getSecurityInfo": NetworkEventActor.prototype.onGetSecurityInfo,
 };
--- a/toolkit/devtools/webconsole/client.js
+++ b/toolkit/devtools/webconsole/client.js
@@ -362,16 +362,33 @@ WebConsoleClient.prototype = {
     let packet = {
       to: aActor,
       type: "getEventTimings",
     };
     this._client.request(packet, aOnResponse);
   },
 
   /**
+   * Retrieve the security information for the given NetworkEventActor.
+   *
+   * @param string aActor
+   *        The NetworkEventActor ID.
+   * @param function aOnResponse
+   *        The function invoked when the response is received.
+   */
+  getSecurityInfo: function WCC_getSecurityInfo(aActor, aOnResponse)
+  {
+    let packet = {
+      to: aActor,
+      type: "getSecurityInfo",
+    };
+    this._client.request(packet, aOnResponse);
+  },
+
+  /**
    * Send a HTTP request with the given data.
    *
    * @param string aData
    *        The details of the HTTP request.
    * @param function aOnResponse
    *        The function invoked when the response is received.
    */
   sendHTTPRequest: function WCC_sendHTTPRequest(aData, aOnResponse) {
--- a/toolkit/devtools/webconsole/network-helper.js
+++ b/toolkit/devtools/webconsole/network-helper.js
@@ -51,16 +51,17 @@
  *  Steven Roussey (AppCenter Inc, Network54)
  *  Mihai Sucan (Mozilla Corp.)
  */
 
 "use strict";
 
 const {components, Cc, Ci, Cu} = require("chrome");
 loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
+loader.lazyImporter(this, "DevToolsUtils", "resource://gre/modules/devtools/DevToolsUtils.jsm");
 
 /**
  * Helper object for networking stuff.
  *
  * Most of the following functions have been taken from the Firebug source. They
  * have been modified to match the Firefox coding rules.
  */
 let NetworkHelper = {
@@ -475,13 +476,214 @@ let NetworkHelper = {
       case "svg":
       case "xml":
         return true;
 
       default:
         return false;
     }
   },
+
+  /**
+   * Takes a securityInfo object of nsIRequest, the nsIRequest itself and
+   * extracts security information from them.
+   *
+   * @param object securityInfo
+   *        The securityInfo object of a request. If null channel is assumed
+   *        to be insecure.
+   * @param nsIRequest request
+   *        The nsIRequest object for the request used to dig more information
+   *        about this request.
+   *
+   * @return object
+   *         Returns an object containing following members:
+   *          - state: The security of the connection used to fetch this
+   *                   request. Has one of following string values:
+   *                    * "insecure": the connection was not secure (only http)
+   *                    * "broken": secure connection failed (e.g. expired cert)
+   *                    * "secure": the connection was properly secured.
+   *          If state == broken:
+   *            - errorMessage: full error message from nsITransportSecurityInfo.
+   *          If state == secure:
+   *            - protocolVersion: one of SSLv3, TLSv1, TLSv1.1, TLSv1.2.
+   *            - cipherSuite: the cipher suite used in this connection.
+   *            - cert: information about certificate used in this connection.
+   *                    See parseCertificateInfo for the contents.
+   *            - hsts: true if host uses Strict Transport Security, false otherwise
+   *            - hpkp: true if host uses Public Key Pinning, false otherwise
+   */
+  parseSecurityInfo: function NH_parseSecurityInfo(securityInfo, request) {
+    const info = {
+      state: "insecure",
+    };
+
+    // The request did not contain any security info.
+    if (!securityInfo) {
+      return info;
+    }
+
+    /**
+     * Different scenarios to consider here and how they are handled:
+     * - request is HTTP, the connection is not secure
+     *   => securityInfo is null
+     *      => state === "insecure"
+     *
+     * - request is HTTPS, the connection is secure
+     *   => .securityState has STATE_IS_SECURE flag
+     *      => state === "secure"
+     *
+     * - request is HTTPS, the connection has security issues
+     *   => .securityState has STATE_IS_INSECURE flag
+     *   => .errorCode is an NSS error code.
+     *      => state === "broken"
+     *
+     * - request is HTTPS, the connection was terminated before the security
+     *   could be validated
+     *   => .securityState has STATE_IS_INSECURE flag
+     *   => .errorCode is NOT an NSS error code.
+     *   => .errorMessage is not available.
+     *      => state === "insecure"
+     *
+     * - request is HTTPS but it uses a weak cipher or old protocol, see
+     *   http://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
+     *   security/manager/ssl/src/nsNSSCallbacks.cpp#l1233
+     * - request is mixed content (which makes no sense whatsoever)
+     *   => .securityState has STATE_IS_BROKEN flag
+     *   => .errorCode is NOT an NSS error code
+     *   => .errorMessage is not available
+     *      => state === "insecure"
+     */
+
+    securityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+    securityInfo.QueryInterface(Ci.nsISSLStatusProvider);
+
+    const wpl = Ci.nsIWebProgressListener;
+    const NSSErrorsService = Cc['@mozilla.org/nss_errors_service;1']
+                               .getService(Ci.nsINSSErrorsService);
+    const SSLStatus = securityInfo.SSLStatus;
+
+    if (securityInfo.securityState & wpl.STATE_IS_SECURE) {
+      // The connection is secure.
+      info.state = "secure";
+
+      // Cipher suite.
+      info.cipherSuite = SSLStatus.cipherName;
+
+      // Protocol version.
+      info.protocolVersion = this.formatSecurityProtocol(SSLStatus.protocolVersion);
+
+      // Certificate.
+      info.cert = this.parseCertificateInfo(SSLStatus.serverCert);
+
+      // HSTS and HPKP if available.
+      if (request.URI) {
+        const sss = Cc["@mozilla.org/ssservice;1"]
+                      .getService(Ci.nsISiteSecurityService);
+
+        request.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+
+        // SiteSecurityService uses different storage if the channel is
+        // private. Thus we must give isSecureHost correct flags or we
+        // might get incorrect results.
+        let flags = (request.isChannelPrivate) ?
+                      Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0;
+
+        let host = request.URI.host;
+
+        info.hsts = sss.isSecureHost(sss.HEADER_HSTS, host, flags);
+        info.hpkp = sss.isSecureHost(sss.HEADER_HPKP, host, flags);
+      } else {
+        DevToolsUtils.reportException("NetworkHelper.parseSecurityInfo",
+          "Could not get HSTS/HPKP status as request.URI not available.");
+        info.hsts = false;
+        info.hpkp = false;
+      }
+
+    } else if (NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
+      // The connection failed.
+      info.state = "broken";
+      info.errorMessage = securityInfo.errorMessage;
+    } else {
+      // Connection has securityInfo, it is not secure and there's no problems
+      // to report. Mark the request as insecure.
+      return info;
+    }
+
+    return info;
+  },
+
+  /**
+   * Takes an nsIX509Cert and returns an object with certificate information.
+   *
+   * @param nsIX509Cert cert
+   *        The certificate to extract the information from.
+   * @return object
+   *         An object with following format:
+   *           {
+   *             subject: { commonName, organization, organizationalUnit },
+   *             issuer: { commonName, organization, organizationUnit },
+   *             validity: { start, end },
+   *             fingerprint: { sha1, sha256 }
+   *           }
+   */
+  parseCertificateInfo: function NH_parseCertifificateInfo(cert) {
+    let info = {};
+    if (cert) {
+      info.subject = {
+        commonName: cert.commonName,
+        organization: cert.organization,
+        organizationalUnit: cert.organizationalUnit,
+      };
+
+      info.issuer = {
+        commonName: cert.issuerCommonName,
+        organization: cert.issuerOrganization,
+        organizationUnit: cert.issuerOrganizationUnit,
+      };
+
+      info.validity = {
+        start: cert.validity.notBeforeLocalDay,
+        end: cert.validity.notAfterLocalDay,
+      };
+
+      info.fingerprint = {
+        sha1: cert.sha1Fingerprint,
+        sha256: cert.sha256Fingerprint,
+      };
+    } else {
+      DevToolsUtils.reportException("NetworkHelper.parseCertificateInfo",
+        "Secure connection established without certificate.");
+    }
+
+    return info;
+  },
+
+  /**
+   * Takes protocolVersion of SSLStatus object and returns human readable
+   * description.
+   *
+   * @param Number version
+   *        One of nsISSLStatus version constants.
+   * @return string
+   *         One of SSLv3, TLSv1, TLSv1.1, TLSv1.2 if @param version is valid,
+   *         Unknown otherwise.
+   */
+  formatSecurityProtocol: function NH_formatSecurityProtocol(version) {
+    switch (version) {
+      case Ci.nsISSLStatus.SSL_VERSION_3:
+        return "SSLv3";
+      case Ci.nsISSLStatus.TLS_VERSION_1:
+        return "TLSv1";
+      case Ci.nsISSLStatus.TLS_VERSION_1_1:
+        return "TLSv1.1";
+      case Ci.nsISSLStatus.TLS_VERSION_1_2:
+        return "TLSv1.2";
+      default:
+        DevToolsUtils.reportException("NetworkHelper.formatSecurityProtocol",
+          "protocolVersion " + version + " is unknown.");
+        return "Unknown";
+    }
+  },
 };
 
 for (let prop of Object.getOwnPropertyNames(NetworkHelper)) {
   exports[prop] = NetworkHelper[prop];
 }
--- a/toolkit/devtools/webconsole/network-monitor.js
+++ b/toolkit/devtools/webconsole/network-monitor.js
@@ -155,22 +155,37 @@ NetworkResponseListener.prototype = {
    * https://developer.mozilla.org/En/NsIRequestObserver
    *
    * @param nsIRequest aRequest
    * @param nsISupports aContext
    */
   onStartRequest: function NRL_onStartRequest(aRequest)
   {
     this.request = aRequest;
+    this._getSecurityInfo();
     this._findOpenResponse();
     // Asynchronously wait for the data coming from the request.
     this.setAsyncListener(this.sink.inputStream, this);
   },
 
   /**
+   * Parse security state of this request and report it to the client.
+   */
+  _getSecurityInfo: DevToolsUtils.makeInfallible(function NRL_getSecurityInfo() {
+    // Take the security information from the original nsIHTTPChannel instead of
+    // the nsIRequest received in onStartRequest. If response to this request
+    // was a redirect from http to https, the request object seems to contain
+    // security info for the https request after redirect.
+    let secinfo = this.httpActivity.channel.securityInfo;
+    let info = NetworkHelper.parseSecurityInfo(secinfo, this.request);
+
+    this.httpActivity.owner.addSecurityInfo(info);
+  }),
+
+  /**
    * Handle the onStopRequest by closing the sink output stream.
    *
    * For more documentation about nsIRequestObserver go to:
    * https://developer.mozilla.org/En/NsIRequestObserver
    */
   onStopRequest: function NRL_onStopRequest()
   {
     this._findOpenResponse();
@@ -1155,18 +1170,18 @@ NetworkEventActorProxy.prototype = {
     });
     return this;
   }),
 };
 
 (function() {
   // Listeners for new network event data coming from the NetworkMonitor.
   let methods = ["addRequestHeaders", "addRequestCookies", "addRequestPostData",
-                 "addResponseStart", "addResponseHeaders", "addResponseCookies",
-                 "addResponseContent", "addEventTimings"];
+                 "addResponseStart", "addSecurityInfo", "addResponseHeaders",
+                 "addResponseCookies", "addResponseContent", "addEventTimings"];
   let factory = NetworkEventActorProxy.methodFactory;
   for (let method of methods) {
     NetworkEventActorProxy.prototype[method] = factory(method);
   }
 })();
 
 
 /**
--- a/toolkit/devtools/webconsole/test/chrome.ini
+++ b/toolkit/devtools/webconsole/test/chrome.ini
@@ -12,15 +12,17 @@ support-files =
 [test_consoleapi.html]
 [test_consoleapi_innerID.html]
 [test_file_uri.html]
 [test_reflow.html]
 [test_jsterm.html]
 [test_network_get.html]
 [test_network_longstring.html]
 [test_network_post.html]
+[test_network_security-hpkp.html]
+[test_network_security-hsts.html]
 [test_nsiconsolemessage.html]
 [test_object_actor.html]
 [test_object_actor_native_getters.html]
 [test_object_actor_native_getters_lenient_this.html]
 [test_page_errors.html]
 [test_throw.html]
 [test_jsterm_cd_iframe.html]
--- a/toolkit/devtools/webconsole/test/test_network_get.html
+++ b/toolkit/devtools/webconsole/test/test_network_get.html
@@ -86,16 +86,21 @@ function onNetworkEventUpdate(aState, aT
           httpVersion: /^HTTP\/\d\.\d$/,
           status: 200,
           statusText: "OK",
           headersSize: /^\d+$/,
           discardResponseBody: true,
         },
       };
       break;
+    case "securityInfo":
+      expectedPacket = {
+        state: "insecure",
+      };
+      break;
     case "responseCookies":
       expectedPacket = {
         cookies: 0,
       };
       break;
     case "responseContent":
       expectedPacket = {
         mimeType: "application/json",
--- a/toolkit/devtools/webconsole/test/test_network_longstring.html
+++ b/toolkit/devtools/webconsole/test/test_network_longstring.html
@@ -109,16 +109,21 @@ function onNetworkEventUpdate(aState, aT
           httpVersion: /^HTTP\/\d\.\d$/,
           status: 200,
           statusText: "OK",
           headersSize: /^\d+$/,
           discardResponseBody: false,
         },
       };
       break;
+    case "securityInfo":
+      expectedPacket = {
+        state: "insecure",
+      };
+      break;
     case "responseCookies":
       expectedPacket = {
         cookies: 0,
       };
       break;
     case "responseContent":
       expectedPacket = {
         mimeType: "application/json",
--- a/toolkit/devtools/webconsole/test/test_network_post.html
+++ b/toolkit/devtools/webconsole/test/test_network_post.html
@@ -102,16 +102,21 @@ function onNetworkEventUpdate(aState, aT
           httpVersion: /^HTTP\/\d\.\d$/,
           status: 200,
           statusText: "OK",
           headersSize: /^\d+$/,
           discardResponseBody: false,
         },
       };
       break;
+    case "securityInfo":
+      expectedPacket = {
+        state: "insecure",
+      };
+      break;
     case "responseCookies":
       expectedPacket = {
         cookies: 0,
       };
       break;
     case "responseContent":
       expectedPacket = {
         mimeType: "application/json",
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_network_security-hpkp.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the network actor (HPKP detection)</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the network actor (HPKP detection)</p>
+
+<iframe src="https://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let gCurrentTestCase = -1;
+const HPKP_PREF = "security.cert_pinning.process_headers_from_non_builtin_roots";
+const TEST_CASES = [
+  {
+    desc: "no Public Key Pinning",
+    url: "https://example.com",
+    usesPinning: false,
+  },
+  {
+    desc: "static Public Key Pinning",
+    url: "https://include-subdomains.pinning.example.com/",
+    usesPinning: true,
+  },
+  {
+    desc: "dynamic Public Key Pinning with this request",
+    url: "https://include-subdomains.pinning-dynamic.example.com/" +
+         "browser/browser/base/content/test/general/pinning_headers.sjs",
+    usesPinning: true,
+  },
+  {
+    desc: "dynamic Public Key Pinning with previous request",
+    url: "https://include-subdomains.pinning-dynamic.example.com/",
+    usesPinning: true,
+  }
+];
+
+function startTest()
+{
+  // Need to enable this pref or pinning headers are rejected due test
+  // certificate.
+  Services.prefs.setBoolPref(HPKP_PREF, true);
+  SimpleTest.registerCleanupFunction(() => {
+    Services.prefs.setBoolPref(HPKP_PREF, false);
+
+    // Reset pinning state.
+    let gSSService = Cc["@mozilla.org/ssservice;1"]
+                       .getService(Ci.nsISiteSecurityService);
+
+    let gIOService = Cc["@mozilla.org/network/io-service;1"]
+                       .getService(Ci.nsIIOService);
+    for (let {url} of TEST_CASES) {
+      let uri = gIOService.newURI(url, null, null);
+      gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HPKP, uri, 0);
+    }
+  });
+
+  info("Test detection of Public Key Pinning.");
+  removeEventListener("load", startTest);
+  attachConsole(["NetworkActivity"], onAttach, true);
+}
+
+function onAttach(aState, aResponse)
+{
+  onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
+  aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
+
+  runNextCase(aState);
+}
+
+function runNextCase(aState) {
+  gCurrentTestCase++;
+  if (gCurrentTestCase === TEST_CASES.length) {
+    info("Tests ran. Cleaning up.");
+    closeDebugger(aState, SimpleTest.finish);
+    return;
+  }
+
+  let { desc, url } = TEST_CASES[gCurrentTestCase];
+  info("Testing site with " + desc);
+
+  let iframe = document.querySelector("iframe").contentWindow;
+  iframe.wrappedJSObject.makeXhrCallback("GET", url);
+}
+
+function onNetworkEventUpdate(aState, aType, aPacket)
+{
+  function onSecurityInfo(packet) {
+    let data = TEST_CASES[gCurrentTestCase];
+    is(packet.securityInfo.hpkp, data.usesPinning,
+      "Public Key Pinning detected correctly.");
+
+    runNextCase(aState);
+  }
+
+  if (aPacket.updateType === "securityInfo") {
+    aState.client.getSecurityInfo(aPacket.from, onSecurityInfo);
+  }
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_network_security-hsts.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the network actor (HSTS detection)</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the network actor (HSTS detection)</p>
+
+<iframe src="https://example.com/chrome/toolkit/devtools/webconsole/test/network_requests_iframe.html"></iframe>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let gCurrentTestCase = -1;
+const TEST_CASES = [
+  {
+    desc: "no HSTS",
+    url: "https://example.com",
+    usesHSTS: false,
+  },
+  {
+    desc: "HSTS from this response",
+    url: "https://example.com/"+
+         "browser/browser/base/content/test/general/browser_star_hsts.sjs",
+    usesHSTS: true,
+  },
+  {
+    desc: "stored HSTS from previous response",
+    url: "https://example.com/",
+    usesHSTS: true,
+  }
+];
+
+function startTest()
+{
+
+  SimpleTest.registerCleanupFunction(() => {
+    // Reset HSTS state.
+    let gSSService = Cc["@mozilla.org/ssservice;1"]
+                       .getService(Ci.nsISiteSecurityService);
+
+    let gIOService = Cc["@mozilla.org/network/io-service;1"]
+                       .getService(Ci.nsIIOService);
+
+    let uri = gIOService.newURI(TEST_CASES[0].url, null, null);
+    gSSService.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, 0);
+  });
+
+  info("Test detection of HTTP Strict Transport Security.");
+  removeEventListener("load", startTest);
+  attachConsole(["NetworkActivity"], onAttach, true);
+}
+
+function onAttach(aState, aResponse)
+{
+  onNetworkEventUpdate = onNetworkEventUpdate.bind(null, aState);
+  aState.dbgClient.addListener("networkEventUpdate", onNetworkEventUpdate);
+
+  runNextCase(aState);
+}
+
+function runNextCase(aState) {
+  gCurrentTestCase++;
+  if (gCurrentTestCase === TEST_CASES.length) {
+    info("Tests ran. Cleaning up.");
+    closeDebugger(aState, SimpleTest.finish);
+    return;
+  }
+
+  let { desc, url } = TEST_CASES[gCurrentTestCase];
+  info("Testing site with " + desc);
+
+  let iframe = document.querySelector("iframe").contentWindow;
+  iframe.wrappedJSObject.makeXhrCallback("GET", url);
+}
+
+function onNetworkEventUpdate(aState, aType, aPacket)
+{
+  function onSecurityInfo(packet) {
+    let data = TEST_CASES[gCurrentTestCase];
+    is(packet.securityInfo.hsts, data.usesHSTS,
+      "Strict Transport Security detected correctly.");
+
+    runNextCase(aState);
+  }
+
+  if (aPacket.updateType === "securityInfo") {
+    aState.client.getSecurityInfo(aPacket.from, onSecurityInfo);
+  }
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-certificate.js
@@ -0,0 +1,68 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that NetworkHelper.parseCertificateInfo parses certificate information
+// correctly.
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const DUMMY_CERT = {
+  commonName: "cn",
+  organization: "o",
+  organizationalUnit: "ou",
+  issuerCommonName: "issuerCN",
+  issuerOrganization: "issuerO",
+  issuerOrganizationUnit: "issuerOU",
+  sha256Fingerprint: "qwertyuiopoiuytrewq",
+  sha1Fingerprint: "qwertyuiop",
+  validity: {
+    notBeforeLocalDay: "yesterday",
+    notAfterLocalDay: "tomorrow",
+  }
+};
+
+function run_test() {
+  do_print("Testing NetworkHelper.parseCertificateInfo.");
+
+  let result = NetworkHelper.parseCertificateInfo(DUMMY_CERT);
+
+  // Subject
+  equal(result.subject.commonName, DUMMY_CERT.commonName,
+    "Common name is correct.");
+  equal(result.subject.organization, DUMMY_CERT.organization,
+    "Organization is correct.");
+  equal(result.subject.organizationalUnit, DUMMY_CERT.organizationalUnit,
+    "Organizational unit is correct.");
+
+  // Issuer
+  equal(result.issuer.commonName, DUMMY_CERT.issuerCommonName,
+    "Common name of the issuer is correct.");
+  equal(result.issuer.organization, DUMMY_CERT.issuerOrganization,
+    "Organization of the issuer is correct.");
+  equal(result.issuer.organizationalUnit, DUMMY_CERT.issuerOrganizationalUnit,
+    "Organizational unit of the issuer is correct.");
+
+  // Validity
+  equal(result.validity.start, DUMMY_CERT.validity.notBeforeLocalDay,
+    "Start of the validity period is correct.");
+  equal(result.validity.end, DUMMY_CERT.validity.notAfterLocalDay,
+    "End of the validity period is correct.");
+
+  // Fingerprints
+  equal(result.fingerprint.sha1, DUMMY_CERT.sha1Fingerprint,
+    "Certificate SHA1 fingerprint is correct.");
+  equal(result.fingerprint.sha256, DUMMY_CERT.sha256Fingerprint,
+    "Certificate SHA256 fingerprint is correct.");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-parser.js
@@ -0,0 +1,64 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that NetworkHelper.parseSecurityInfo returns correctly formatted object.
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const wpl = Ci.nsIWebProgressListener;
+const MockCertificate = {
+  commonName: "cn",
+  organization: "o",
+  organizationalUnit: "ou",
+  issuerCommonName: "issuerCN",
+  issuerOrganization: "issuerO",
+  issuerOrganizationUnit: "issuerOU",
+  sha256Fingerprint: "qwertyuiopoiuytrewq",
+  sha1Fingerprint: "qwertyuiop",
+  validity: {
+    notBeforeLocalDay: "yesterday",
+    notAfterLocalDay: "tomorrow",
+  }
+};
+
+const MockSecurityInfo = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo,
+                                         Ci.nsISSLStatusProvider]),
+  securityState: wpl.STATE_IS_SECURE,
+  errorCode: 0,
+  SSLStatus: {
+    cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+    protocolVersion: 3, // TLS_VERSION_1_2
+    serverCert: MockCertificate,
+  }
+};
+
+function run_test() {
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+
+  equal(result.state, "secure", "State is correct.");
+
+  equal(result.cipherSuite, MockSecurityInfo.cipherSuite,
+    "Cipher suite is correct.");
+
+  equal(result.protocolVersion, "TLSv1.2", "Protocol version is correct.");
+
+  deepEqual(result.cert, NetworkHelper.parseCertificateInfo(MockCertificate),
+    "Certificate information is correct.");
+
+  equal(result.hpkp, false, "HPKP is false when URI is not available.");
+  equal(result.hsts, false, "HSTS is false when URI is not available.");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-protocol-version.js
@@ -0,0 +1,54 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that NetworkHelper.formatSecurityProtocol returns correct
+// protocol version strings.
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const TEST_CASES = [
+  {
+    description: "SSL_VERSION_3",
+    input: 0,
+    expected: "SSLv3"
+  }, {
+    description: "TLS_VERSION_1",
+    input: 1,
+    expected: "TLSv1"
+  }, {
+    description: "TLS_VERSION_1.1",
+    input: 2,
+    expected: "TLSv1.1"
+  }, {
+    description: "TLS_VERSION_1.2",
+    input: 3,
+    expected: "TLSv1.2"
+  }, {
+    description: "invalid version",
+    input: -1,
+    expected: "Unknown"
+  },
+];
+
+function run_test() {
+  do_print("Testing NetworkHelper.formatSecurityProtocol.");
+
+  for (let {description, input, expected} of TEST_CASES) {
+    do_print("Testing " + description);
+
+    equal(NetworkHelper.formatSecurityProtocol(input), expected,
+      "Got the expected protocol string.");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/unit/test_security-info-state.js
@@ -0,0 +1,100 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Tests that security info parser gives correct general security state for
+// different cases.
+
+const { devtools } = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Object.defineProperty(this, "NetworkHelper", {
+  get: function() {
+    return devtools.require("devtools/toolkit/webconsole/network-helper");
+  },
+  configurable: true,
+  writeable: false,
+  enumerable: true
+});
+
+const Ci = Components.interfaces;
+const wpl = Ci.nsIWebProgressListener;
+const MockSecurityInfo = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsITransportSecurityInfo,
+                                         Ci.nsISSLStatusProvider]),
+  securityState: wpl.STATE_IS_BROKEN,
+  errorCode: 0,
+  SSLStatus: {
+    protocolVersion: 3, // nsISSLStatus.TLS_VERSION_1_2
+    cipherSuite: "TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+  }
+};
+
+function run_test() {
+  test_nullSecurityInfo();
+  test_insecureSecurityInfoWithNSSError();
+  test_insecureSecurityInfoWithoutNSSError();
+  test_brokenSecurityInfo();
+  test_secureSecurityInfo();
+}
+
+/**
+ * Test that undefined security information is returns "insecure".
+ */
+function test_nullSecurityInfo() {
+  let result = NetworkHelper.parseSecurityInfo(null, {});
+  equal(result.state, "insecure",
+    "state == 'insecure' when securityInfo was undefined");
+}
+
+/**
+ * Test that STATE_IS_INSECURE with NSSError returns "broken"
+ */
+function test_insecureSecurityInfoWithNSSError() {
+  MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
+
+  // Taken from security/manager/ssl/tests/unit/head_psm.js.
+  MockSecurityInfo.errorCode = -8180;
+
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+  equal(result.state, "broken",
+    "state == 'broken' if securityState contains STATE_IS_INSECURE flag AND " +
+    "errorCode is NSS error.");
+
+  MockSecurityInfo.errorCode = 0;
+}
+
+/**
+ * Test that STATE_IS_INSECURE without NSSError returns "insecure"
+ */
+function test_insecureSecurityInfoWithoutNSSError() {
+  MockSecurityInfo.securityState = wpl.STATE_IS_INSECURE;
+
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+  equal(result.state, "insecure",
+    "state == 'insecure' if securityState contains STATE_IS_INSECURE flag BUT " +
+    "errorCode is not NSS error.");
+}
+
+/**
+ * Test that STATE_IS_SECURE returns "secure"
+ */
+function test_secureSecurityInfo() {
+  MockSecurityInfo.securityState = wpl.STATE_IS_SECURE;
+
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+  equal(result.state, "secure",
+    "state == 'secure' if securityState contains STATE_IS_SECURE flag");
+}
+
+/**
+ * Test that STATE_IS_BROKEN returns "insecure"
+ */
+function test_brokenSecurityInfo() {
+  MockSecurityInfo.securityState = wpl.STATE_IS_BROKEN;
+
+  let result = NetworkHelper.parseSecurityInfo(MockSecurityInfo, {});
+  equal(result.state, "insecure",
+    "state == 'insecure' if securityState contains STATE_IS_BROKEN flag");
+}
--- a/toolkit/devtools/webconsole/test/unit/xpcshell.ini
+++ b/toolkit/devtools/webconsole/test/unit/xpcshell.ini
@@ -1,8 +1,12 @@
 [DEFAULT]
 head =
 tail =
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
 
 [test_js_property_provider.js]
 [test_network_helper.js]
+[test_security-info-certificate.js]
+[test_security-info-parser.js]
+[test_security-info-protocol-version.js]
+[test_security-info-state.js]