Bug 1322748 add securityInfo to webRequest listeners draft
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 25 Jan 2018 17:11:48 -0700
changeset 747392 688f1e13e74968448c6152b130eb876ed6717797
parent 724080 0e62eb7804c00c0996a9bdde5350328a384fb7af
push id96898
push usermixedpuppy@gmail.com
push dateFri, 26 Jan 2018 00:13:12 +0000
bugs1322748
milestone60.0a1
Bug 1322748 add securityInfo to webRequest listeners MozReview-Commit-ID: IDTqF6MoK3l
toolkit/components/extensions/schemas/web_request.json
toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html
toolkit/modules/addons/SecurityInfo.jsm
toolkit/modules/addons/WebRequest.jsm
toolkit/modules/moz.build
--- a/toolkit/components/extensions/schemas/web_request.json
+++ b/toolkit/components/extensions/schemas/web_request.json
@@ -68,37 +68,37 @@
       {
         "id": "OnSendHeadersOptions",
         "type": "string",
         "enum": ["requestHeaders"]
       },
       {
         "id": "OnHeadersReceivedOptions",
         "type": "string",
-        "enum": ["blocking", "responseHeaders"]
+        "enum": ["blocking", "responseHeaders", "securityInfo"]
       },
       {
         "id": "OnAuthRequiredOptions",
         "type": "string",
-        "enum": ["responseHeaders", "blocking", "asyncBlocking"]
+        "enum": ["responseHeaders", "blocking", "asyncBlocking", "securityInfo"]
       },
       {
         "id": "OnResponseStartedOptions",
         "type": "string",
-        "enum": ["responseHeaders"]
+        "enum": ["responseHeaders", "securityInfo"]
       },
       {
         "id": "OnBeforeRedirectOptions",
         "type": "string",
-        "enum": ["responseHeaders"]
+        "enum": ["responseHeaders", "securityInfo"]
       },
       {
         "id": "OnCompletedOptions",
         "type": "string",
-        "enum": ["responseHeaders"]
+        "enum": ["responseHeaders", "securityInfo"]
       },
       {
         "id": "RequestFilter",
         "type": "object",
         "description": "An object describing filters to apply to webRequest events.",
         "properties": {
           "urls": {
             "type": "array",
@@ -172,16 +172,157 @@
             "properties": {
               "username": {"type": "string"},
               "password": {"type": "string"}
             }
           }
         }
       },
       {
+        "id": "CertificateInfo",
+        "type": "object",
+        "description": "Contains the certificate properties of the request if it is a secure request.",
+        "properties": {
+          "subject": {
+            "type": "object",
+            "properties": {
+              "commonName": { "type": "string" },
+              "organization": { "type": "string" },
+              "organizationalUnit": { "type": "string" }
+            }
+          },
+          "issuer": {
+            "type": "object",
+            "properties": {
+              "commonName": { "type": "string" },
+              "organization": { "type": "string" },
+              "organizationalUnit": { "type": "string" }
+            }
+          },
+          "validity": {
+            "type": "object",
+            "description": "Contains start and end dates in GMT.",
+            "properties": {
+              "startGMT": { "type": "string" },
+              "endGMT": { "type": "string" }
+            }
+          },
+          "fingerprint": {
+            "type": "object",
+            "properties": {
+              "sha1": { "type": "string" },
+              "sha256": { "type": "string" }
+            }
+          },
+          "serialNumber": {
+            "type": "string"
+          },
+          "subjectName": {
+            "type": "string"
+          },
+          "isBuiltInRoot": {
+            "type": "boolean"
+          },
+          "isSelfSigned": {
+            "type": "boolean"
+          },
+          "serialNumber": {
+            "type": "string"
+          },
+          "certType": {
+            "type": "string",
+            "enum": [ "unknown", "ca", "user", "email", "server", "any" ]
+          }
+        }
+      },
+      {
+        "id": "SecurityInfo",
+        "type": "object",
+        "description": "Contains the security properties of the request (ie. SSL/TLS information).",
+        "properties": {
+          "state": {
+            "type": "string",
+            "enum": [
+              "insecure",
+              "weak",
+              "broken",
+              "secure"
+            ]
+          },
+          "errorMessage": {
+            "type": "string",
+            "description": "Error message if state is \"broken\"",
+            "optional": true
+          },
+          "protocolVersion": {
+            "type": "string",
+            "description": "Protocol version if state is \"secure\"",
+            "enum": [
+              "TLSv1",
+              "TLSv1.1",
+              "TLSv1.2",
+              "TLSv1.3",
+              "unknown"
+            ],
+            "optional": true
+          },
+          "cipherSuite": {
+            "type": "string",
+            "description": "The cipher suite used in this request if state is \"secure\".",
+            "optional": true
+          },
+          "cert": {
+            "description": "Certificate data if state is \"secure\".",
+            "$ref": "CertificateInfo",
+            "optional": true
+          },
+          "isDomainMismatch": {
+            "type": "boolean",
+            "optional": true
+          },
+          "isExtendedValidation": {
+            "type": "boolean",
+            "optional": true
+          },
+          "isNotValidAtThisTime": {
+            "type": "boolean",
+            "optional": true
+          },
+          "isUntrusted": {
+            "type": "boolean",
+            "optional": true
+          },
+          "certificateTransparencyStatus": {
+            "type": "string",
+            "enum": [
+              "not_applicable",
+              "policy_compliant",
+              "policy_not_enough_scts",
+              "policy_not_diverse_scts"
+            ]
+          },
+          "hsts": {
+            "type": "boolean",
+            "description": "True if host uses Strict Transport Security and state is \"secure\".",
+            "optional": true
+          },
+          "hpkp": {
+            "type": "string",
+            "description": "True if host uses Public Key Pinning and state is \"secure\".",
+            "optional": true
+          },
+          "weaknessReasons": {
+            "type": "array",
+            "items": { "type": "string" },
+            "description": "list of reasons that cause the request to be considered weak, if state is \"weak\"",
+            "optional": true
+          }
+        }
+      },
+      {
         "id": "UploadData",
         "type": "object",
         "properties": {
           "bytes": {
             "type": "any",
             "optional": true,
             "description": "An ArrayBuffer with a copy of the data."
           },
@@ -397,16 +538,17 @@
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "originUrl": {"type": "string", "optional": true, "description": "URL of the resource that triggered this request."},
               "documentUrl": {"type": "string", "optional": true, "description": "URL of the page into which the requested resource will be loaded."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line)."},
               "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that have been received with this response."},
+              "securityInfo": {"$ref": "SecurityInfo", "optional": true, "description": "Security information for this request."},
               "statusCode": {"type": "integer", "description": "Standard HTTP status code returned by the server."}
              }
           }
         ],
         "extraParameters": [
           {
             "$ref": "RequestFilter",
             "name": "filter",
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html
@@ -21,31 +21,35 @@ function getExtension() {
     }, {urls}, ["blocking"]);
     browser.webRequest.onBeforeSendHeaders.addListener(details => {
       browser.test.assertEq(expect.shift(), "onBeforeSendHeaders");
     }, {urls}, ["blocking", "requestHeaders"]);
     browser.webRequest.onSendHeaders.addListener(details => {
       browser.test.assertEq(expect.shift(), "onSendHeaders");
     }, {urls}, ["requestHeaders"]);
     browser.webRequest.onHeadersReceived.addListener(details => {
+      browser.test.log(`**** securityInfo ${details.securityInfo && JSON.stringify(details.securityInfo)}`);
       browser.test.assertEq(expect.shift(), "onHeadersReceived");
+      browser.test.assertEq(details.url.startsWith("https"),
+                            details.securityInfo && details.securityInfo.state == "secure",
+                            "security info reflects https");
 
       let headers = details.responseHeaders || [];
       for (let header of headers) {
         if (header.name.toLowerCase() === "strict-transport-security") {
           return;
         }
       }
 
       headers.push({
         name: "Strict-Transport-Security",
         value: "max-age=31536000000",
       });
       return {responseHeaders: headers};
-    }, {urls}, ["blocking", "responseHeaders"]);
+    }, {urls}, ["blocking", "responseHeaders", "securityInfo"]);
     browser.webRequest.onBeforeRedirect.addListener(details => {
       browser.test.assertEq(expect.shift(), "onBeforeRedirect");
     }, {urls});
     browser.webRequest.onResponseStarted.addListener(details => {
       browser.test.assertEq(expect.shift(), "onResponseStarted");
     }, {urls});
     browser.webRequest.onCompleted.addListener(details => {
       browser.test.assertEq(expect.shift(), "onCompleted");
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/addons/SecurityInfo.jsm
@@ -0,0 +1,299 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["SecurityInfo"];
+
+const {interfaces: Ci, classes: Cc} = Components;
+
+// NOTE: SecurityInfo is largly copied from the devtools NetworkHelper with some
+// minor differences.
+
+const SecurityInfo = {
+  /**
+   * Extracts security information from nsIChannel.securityInfo.
+   *
+   * @param {nsIChannel} channel
+   *        If null channel is assumed to be insecure.
+   *
+   * @returns {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)
+   *                    * "weak": the connection has minor security issues
+   *                    * "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 TLSv1, TLSv1.1, TLSv1.2, TLSv1.3.
+   *            - 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
+   *          If state == weak: Same as state == secure and
+   *            - weaknessReasons: list of reasons that cause the request to be
+   *                               considered weak. See getReasonsForWeakness.
+   */
+  parseSecurityInfo(channel) {
+    const info = {
+      state: "insecure",
+    };
+
+    if (!channel) {
+      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
+     *   https://hg.mozilla.org/mozilla-central/annotate/def6ed9d1c1a/
+     *   security/manager/ssl/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 === "weak"
+     */
+
+
+    let securityInfo = channel.securityInfo;
+    if (!securityInfo) {
+      return info;
+    }
+
+    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 (!NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
+      const state = securityInfo.securityState;
+
+      let uri = null;
+      if (channel.URI) {
+        uri = channel.URI;
+      }
+      if (uri && !uri.schemeIs("https") && !uri.schemeIs("wss")) {
+        // it is not enough to look at the transport security info -
+        // schemes other than https and wss are subject to
+        // downgrade/etc at the scheme level and should always be
+        // considered insecure
+        info.state = "insecure";
+      } else if (state & wpl.STATE_IS_SECURE) {
+        // The connection is secure if the scheme is sufficient
+        info.state = "secure";
+      } else if (state & wpl.STATE_IS_BROKEN) {
+        // The connection is not secure, there was no error but there's some
+        // minor security issues.
+        info.state = "weak";
+        info.weaknessReasons = this.getReasonsForWeakness(state);
+      } else if (state & wpl.STATE_IS_INSECURE) {
+        // This was most likely an https request that was aborted before
+        // validation. Return info as info.state = insecure.
+        return info;
+      } else {
+        // No known STATE_IS_* flags.
+        return info;
+      }
+
+      // Cipher suite.
+      info.cipherSuite = SSLStatus.cipherName;
+
+      // Key exchange group name.
+      info.keaGroupName = SSLStatus.keaGroupName;
+
+      // Certificate signature scheme.
+      info.signatureSchemeName = SSLStatus.signatureSchemeName;
+
+      info.isDomainMismatch = SSLStatus.isDomainMismatch;
+      info.isExtendedValidation = SSLStatus.isExtendedValidation;
+      info.isNotValidAtThisTime = SSLStatus.isNotValidAtThisTime;
+      info.isUntrusted = SSLStatus.isUntrusted;
+
+      info.certificateTransparencyStatus = this.getTransparencyStatus(SSLStatus.certificateTransparencyStatus);
+
+      // Protocol version.
+      info.protocolVersion =
+        this.formatSecurityProtocol(SSLStatus.protocolVersion);
+
+      // Certificate.
+      info.cert = this.parseCertificateInfo(SSLStatus.serverCert);
+
+      // HSTS and HPKP if available.
+      if (uri && uri.host) {
+        const sss = Cc["@mozilla.org/ssservice;1"]
+                      .getService(Ci.nsISiteSecurityService);
+
+        // SiteSecurityService uses different storage if the channel is
+        // private. Thus we must give isSecureURI correct flags or we
+        // might get incorrect results.
+        channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
+        let flags = (channel.isChannelPrivate) ?
+                      Ci.nsISocketProvider.NO_PERMANENT_STORAGE : 0;
+
+        info.hsts = sss.isSecureURI(sss.HEADER_HSTS, uri, flags);
+        info.hpkp = sss.isSecureURI(sss.HEADER_HPKP, uri, flags);
+      } else {
+        info.hsts = false;
+        info.hpkp = false;
+      }
+    } else {
+      // The connection failed.
+      info.state = "broken";
+      info.errorMessage = securityInfo.errorMessage;
+    }
+
+    return info;
+  },
+
+  /**
+   * Takes an nsIX509Cert and returns an object with certificate information.
+   *
+   * @param {nsIX509Cert} cert
+   *        The certificate to extract the information from.
+   * @returns {Object}
+   *         An object with following format:
+   *           {
+   *             subject: { commonName, organization, organizationalUnit },
+   *             issuer: { commonName, organization, organizationUnit },
+   *             validity: { start, end },
+   *             fingerprint: { sha1, sha256 }
+   *           }
+   */
+  parseCertificateInfo(cert) {
+    if (!cert) {
+      return;
+    }
+
+    return {
+      subject: {
+        commonName: cert.commonName,
+        organization: cert.organization,
+        organizationalUnit: cert.organizationalUnit,
+      },
+      issuer: {
+        commonName: cert.issuerCommonName,
+        organization: cert.issuerOrganization,
+        organizationUnit: cert.issuerOrganizationUnit,
+      },
+      validity: {
+        startGMT: cert.validity.notBeforeGMT,
+        endGMT: cert.validity.notAfterGMT,
+      },
+      fingerprint: {
+        sha1: cert.sha1Fingerprint,
+        sha256: cert.sha256Fingerprint,
+      },
+      serialNumber: cert.serialNumber,
+      subjectName: cert.subjectName,
+      isBuiltInRoot: cert.isBuiltInRoot,
+      certType: this.getCertType(cert.certType),
+      isSelfSigned: cert.isSelfSigned,
+    };
+  },
+
+  getTransparencyStatus(status) {
+    switch (status) {
+      case Ci.nsISSLStatus.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE: return "not_applicable";
+      case Ci.nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT: return "policy_compliant";
+      case Ci.nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS: return "policy_not_enough_scts";
+      case Ci.nsISSLStatus.CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS: return "policy_not_diverse_scts";
+    }
+    return "unknown";
+  },
+
+  getCertType(type) {
+    switch (type) {
+      case Ci.nsIX509Cert.CA_CERT: return "ca";
+      case Ci.nsIX509Cert.USER_CERT: return "user";
+      case Ci.nsIX509Cert.EMAIL_CERT: return "email";
+      case Ci.nsIX509Cert.SERVER_CERT: return "server";
+      case Ci.nsIX509Cert.ANY_CERT: return "any";
+    }
+    return "unknown";
+  },
+
+  /**
+   * Takes protocolVersion of SSLStatus object and returns human readable
+   * description.
+   *
+   * @param {number} version
+   *        One of nsISSLStatus version constants.
+   * @returns {string}
+   *         One of TLSv1, TLSv1.1, TLSv1.2, TLSv1.3 if version
+   *         is valid, Unknown otherwise.
+   */
+  formatSecurityProtocol(version) {
+    switch (version) {
+      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";
+      case Ci.nsISSLStatus.TLS_VERSION_1_3:
+        return "TLSv1.3";
+      default:
+        return "unknown";
+    }
+  },
+
+  /**
+   * Takes the securityState bitfield and returns reasons for weak connection
+   * as an array of strings.
+   *
+   * @param {number} state
+   *        nsITransportSecurityInfo.securityState.
+   *
+   * @returns {array<string>}
+   *         List of weakness reasons. A subset of { cipher } where
+   *         * cipher: The cipher suite is consireded to be weak (RC4).
+   */
+  getReasonsForWeakness(state) {
+    const wpl = Ci.nsIWebProgressListener;
+
+    // If there's non-fatal security issues the request has STATE_IS_BROKEN
+    // flag set. See https://hg.mozilla.org/mozilla-central/file/44344099d119
+    // /security/manager/ssl/nsNSSCallbacks.cpp#l1233
+    let reasons = [];
+
+    if (state & wpl.STATE_IS_BROKEN) {
+      let isCipher = state & wpl.STATE_USES_WEAK_CRYPTO;
+
+      if (isCipher) {
+        reasons.push("cipher");
+      }
+    }
+
+    return reasons;
+  },
+};
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -21,16 +21,18 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils",
                                   "resource://gre/modules/ExtensionUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestCommon",
                                   "resource://gre/modules/WebRequestCommon.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebRequestUpload",
                                   "resource://gre/modules/WebRequestUpload.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SecurityInfo",
+                                  "resource://gre/modules/SecurityInfo.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "ExtensionError", () => ExtensionUtils.ExtensionError);
 
 function runLater(job) {
   Services.tm.dispatchToMainThread(job);
 }
 
 function parseFilter(filter) {
@@ -180,18 +182,19 @@ class ResponseHeaderChanger extends Head
   }
 }
 
 const MAYBE_CACHED_EVENTS = new Set([
   "onResponseStarted", "onHeadersReceived", "onBeforeRedirect", "onCompleted", "onErrorOccurred",
 ]);
 
 const OPTIONAL_PROPERTIES = [
-  "requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl",
-  "requestBody", "scheme", "realm", "isProxy", "challenger", "proxyInfo", "ip", "frameAncestors",
+  "requestHeaders", "responseHeaders", "statusCode", "statusLine", "error",
+  "redirectUrl", "requestBody", "scheme", "securityInfo", "realm", "isProxy",
+  "challenger", "proxyInfo", "ip", "frameAncestors",
 ];
 
 function serializeRequestData(eventName) {
   let data = {
     requestId: this.requestId,
     url: this.url,
     originUrl: this.originUrl,
     documentUrl: this.documentUrl,
@@ -746,16 +749,20 @@ HttpObserverManager = {
           return;
         }
 
         if (!commonData) {
           commonData = this.getRequestData(channel, extraData);
           if (this.STATUS_TYPES.has(kind)) {
             commonData.statusCode = channel.statusCode;
             commonData.statusLine = channel.statusLine;
+            // TODO: Should we require a opts.securityInfo before we retreive it?
+            if (opts.securityInfo) {
+              commonData.securityInfo = SecurityInfo.parseSecurityInfo(channel.channel);
+            }
           }
         }
         let data = Object.create(commonData);
 
         if (registerFilter && opts.blocking && opts.extension) {
           channel.registerTraceableChannel(opts.extension, opts.tabParent);
         }
 
@@ -950,21 +957,21 @@ HttpEvent.prototype = {
 
   removeListener(callback) {
     HttpObserverManager.removeListener(this.internalEvent, callback);
   },
 };
 
 var onBeforeSendHeaders = new HttpEvent("modify", ["requestHeaders", "blocking"]);
 var onSendHeaders = new HttpEvent("afterModify", ["requestHeaders"]);
-var onHeadersReceived = new HttpEvent("headersReceived", ["blocking", "responseHeaders"]);
-var onAuthRequired = new HttpEvent("authRequired", ["blocking", "responseHeaders"]);
-var onBeforeRedirect = new HttpEvent("onRedirect", ["responseHeaders"]);
-var onResponseStarted = new HttpEvent("onStart", ["responseHeaders"]);
-var onCompleted = new HttpEvent("onStop", ["responseHeaders"]);
+var onHeadersReceived = new HttpEvent("headersReceived", ["blocking", "responseHeaders", "securityInfo"]);
+var onAuthRequired = new HttpEvent("authRequired", ["blocking", "responseHeaders", "securityInfo"]);
+var onBeforeRedirect = new HttpEvent("onRedirect", ["responseHeaders", "securityInfo"]);
+var onResponseStarted = new HttpEvent("onStart", ["responseHeaders", "securityInfo"]);
+var onCompleted = new HttpEvent("onStop", ["responseHeaders", "securityInfo"]);
 var onErrorOccurred = new HttpEvent("onError");
 
 var WebRequest = {
   // http-on-modify observer for HTTP(S), content policy for the other protocols (notably, data:)
   onBeforeRequest: onBeforeRequest,
 
   // http-on-modify observer.
   onBeforeSendHeaders: onBeforeSendHeaders,
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -165,16 +165,17 @@ TESTING_JS_MODULES += [
 
 SPHINX_TREES['toolkit_modules'] = 'docs'
 
 with Files('docs/**'):
     SCHEDULES.exclusive = ['docs']
 
 EXTRA_JS_MODULES += [
     'addons/MatchURLFilters.jsm',
+    'addons/SecurityInfo.jsm',
     'addons/WebNavigation.jsm',
     'addons/WebNavigationContent.js',
     'addons/WebNavigationFrames.jsm',
     'addons/WebRequest.jsm',
     'addons/WebRequestCommon.jsm',
     'addons/WebRequestContent.js',
     'addons/WebRequestUpload.jsm',
     'AppMenuNotifications.jsm',