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 249084 c4d908fa4442a6132527bd49bbc9bc1186c91ee7
parent 249083 2c8cdce4fed4dadf98f1ef32ced6f6518a400478
child 249085 7f0b79f1904f19d0fd666d7eb0c5f188bee3a5ae
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspast
bugs932179
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 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]