[tor-commits] [tor-browser/tor-browser-60.3.0esr-8.5-1] Bug 1322748 add securityInfo to webRequest listeners, r=keeler, rpl

gk at torproject.org gk at torproject.org
Mon Dec 3 16:18:46 UTC 2018


commit 1935dcf38ca112f9fbc9fe42c2289d77e4f95932
Author: Shane Caraveo <scaraveo at mozilla.com>
Date:   Wed May 23 14:36:19 2018 -0400

    Bug 1322748 add securityInfo to webRequest listeners, r=keeler,rpl
    
    MozReview-Commit-ID: Hen1tl1RWTC
    
    --HG--
    extra : rebase_source : e5dae021438ece0477d89e1d4e91eaaf2ebfd06e
---
 toolkit/components/extensions/ext-webRequest.js    |   8 +
 .../components/extensions/schemas/web_request.json | 171 ++++++++++++
 .../test/mochitest/test_ext_webrequest_hsts.html   |  28 +-
 toolkit/modules/addons/SecurityInfo.jsm            | 297 +++++++++++++++++++++
 toolkit/modules/addons/WebRequest.jsm              |  21 +-
 toolkit/modules/moz.build                          |   1 +
 6 files changed, 516 insertions(+), 10 deletions(-)

diff --git a/toolkit/components/extensions/ext-webRequest.js b/toolkit/components/extensions/ext-webRequest.js
index f953be4a6e40..19306816adc3 100644
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -102,6 +102,14 @@ this.webRequest = class extends ExtensionAPI {
         onResponseStarted: new WebRequestEventManager(context, "onResponseStarted").api(),
         onErrorOccurred: new WebRequestEventManager(context, "onErrorOccurred").api(),
         onCompleted: new WebRequestEventManager(context, "onCompleted").api(),
+        getSecurityInfo: function(requestId, options = {}) {
+          return WebRequest.getSecurityInfo({
+            id: requestId,
+            extension: context.extension.policy,
+            tabParent: context.xulBrowser.frameLoader.tabParent,
+            options,
+          });
+        },
         handlerBehaviorChanged: function() {
           // TODO: Flush all caches.
         },
diff --git a/toolkit/components/extensions/schemas/web_request.json b/toolkit/components/extensions/schemas/web_request.json
index 97badfc797b9..ed1840cabe2a 100644
--- a/toolkit/components/extensions/schemas/web_request.json
+++ b/toolkit/components/extensions/schemas/web_request.json
@@ -177,6 +177,148 @@
         }
       },
       {
+        "id": "CertificateInfo",
+        "type": "object",
+        "description": "Contains the certificate properties of the request if it is a secure request.",
+        "properties": {
+          "subject": {
+            "type": "string"
+          },
+          "issuer": {
+            "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"
+          },
+          "isBuiltInRoot": {
+            "type": "boolean"
+          },
+          "subjectPublicKeyInfoDigest": {
+            "type": "object",
+            "properties": {
+              "sha256": { "type": "string" }
+            }
+          },
+          "keyUsages": {
+            "type": "string"
+          },
+          "rawDER": {
+            "optional": true,
+            "type": "array",
+            "items": {
+              "type": "integer"
+            }
+          }
+        }
+      },
+      {
+        "id": "CertificateTransparencyStatus",
+        "type": "string",
+        "enum": ["not_applicable", "policy_compliant", "policy_not_enough_scts", "policy_not_diverse_scts"]
+      },
+      {
+        "id": "TransportWeaknessReasons",
+        "type": "string",
+        "enum": ["cipher"]
+      },
+      {
+        "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
+          },
+          "certificates": {
+            "description": "Certificate data if state is \"secure\".  Will only contain one entry unless <code>certificateChain</code> is passed as an option.",
+            "type": "array",
+            "items": { "$ref": "CertificateInfo" }
+          },
+          "isDomainMismatch": {
+            "description": "The domain name does not match the certificate domain.",
+            "type": "boolean",
+            "optional": true
+          },
+          "isExtendedValidation": {
+            "type": "boolean",
+            "optional": true
+          },
+          "isNotValidAtThisTime": {
+            "description": "The certificate is either expired or is not yet valid.  See <code>CertificateInfo.validity</code> for start and end dates.",
+            "type": "boolean",
+            "optional": true
+          },
+          "isUntrusted": {
+            "type": "boolean",
+            "optional": true
+          },
+          "certificateTransparencyStatus": {
+            "description": "Certificate transparency compliance per RFC 6962.  See <code>https://www.certificate-transparency.org/what-is-ct</code> for more information.",
+            "$ref": "CertificateTransparencyStatus",
+            "optional": true
+          },
+          "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": { "$ref": "TransportWeaknessReasons" },
+            "description": "list of reasons that cause the request to be considered weak, if state is \"weak\"",
+            "optional": true
+          }
+        }
+      },
+      {
         "id": "UploadData",
         "type": "object",
         "properties": {
@@ -225,6 +367,35 @@
           "additionalProperties": {"type": "any"},
           "isInstanceOf": "StreamFilter"
         }
+      },
+      {
+        "name": "getSecurityInfo",
+        "type": "function",
+        "async": true,
+        "description": "Retrieves the security information for the request.  Returns a promise that will resolve to a SecurityInfo object.",
+        "parameters": [
+          {
+            "name": "requestId",
+            "type": "string"
+          },
+          {
+            "name": "options",
+            "optional": true,
+            "type": "object",
+            "properties": {
+              "certificateChain": {
+                "type": "boolean",
+                "description": "Include the entire certificate chain.",
+                "optional": true
+              },
+              "rawDER": {
+                "type": "boolean",
+                "description": "Include raw certificate data for processing by the extension.",
+                "optional": true
+              }
+            }
+          }
+        ]
       }
     ],
     "events": [
diff --git a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html
index 4dce90cd377e..ad4d4f32a657 100644
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_hsts.html
@@ -25,9 +25,35 @@ function getExtension() {
     browser.webRequest.onSendHeaders.addListener(details => {
       browser.test.assertEq(expect.shift(), "onSendHeaders");
     }, {urls}, ["requestHeaders"]);
-    browser.webRequest.onHeadersReceived.addListener(details => {
+
+    async function testSecurityInfo(details, options) {
+      let securityInfo = await browser.webRequest.getSecurityInfo(details.requestId, options);
+      browser.test.assertTrue(securityInfo && securityInfo.state == "secure",
+                              "security info reflects https");
+
+      if (options.certificateChain) {
+        // Some of the tests here only produce a single cert in the chain.
+        browser.test.assertTrue(securityInfo.certificates.length >= 1, "have certificate chain");
+      } else {
+        browser.test.assertTrue(securityInfo.certificates.length == 1, "no certificate chain");
+      }
+      if (options.rawDER) {
+        for (let cert of securityInfo.certificates) {
+          browser.test.assertTrue(cert.rawDER.length > 0, "have rawDER");
+        }
+      }
+    }
+
+    browser.webRequest.onHeadersReceived.addListener(async (details) => {
       browser.test.assertEq(expect.shift(), "onHeadersReceived");
 
+      // We exepect all requests to have been upgraded at this point.
+      browser.test.assertTrue(details.url.startsWith("https"), "connection is https");
+      await testSecurityInfo(details, {});
+      await testSecurityInfo(details, {certificateChain: true});
+      await testSecurityInfo(details, {rawDER: true});
+      await testSecurityInfo(details, {certificateChain: true, rawDER: true});
+
       let headers = details.responseHeaders || [];
       for (let header of headers) {
         if (header.name.toLowerCase() === "strict-transport-security") {
diff --git a/toolkit/modules/addons/SecurityInfo.jsm b/toolkit/modules/addons/SecurityInfo.jsm
new file mode 100644
index 000000000000..4984f76dd463
--- /dev/null
+++ b/toolkit/modules/addons/SecurityInfo.jsm
@@ -0,0 +1,297 @@
+/* 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"];
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const wpl = Ci.nsIWebProgressListener;
+XPCOMUtils.defineLazyServiceGetter(this, "NSSErrorsService",
+                                   "@mozilla.org/nss_errors_service;1",
+                                   "nsINSSErrorsService");
+XPCOMUtils.defineLazyServiceGetter(this, "sss",
+                                   "@mozilla.org/ssservice;1",
+                                   "nsISiteSecurityService");
+
+// NOTE: SecurityInfo is largely reworked from the devtools NetworkHelper with changes
+// to better support the WebRequest api.  The objects returned are formatted specifically
+// to pass through as part of a response to webRequest listeners.
+
+const SecurityInfo = {
+  /**
+   * Extracts security information from nsIChannel.securityInfo.
+   *
+   * @param {nsIChannel} channel
+   *        If null channel is assumed to be insecure.
+   * @param {Object} options
+   *
+   * @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.
+   */
+  getSecurityInfo(channel, options = {}) {
+    const info = {
+      state: "insecure",
+    };
+
+    /**
+     * 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 SSLStatus = securityInfo.SSLStatus;
+    if (NSSErrorsService.isNSSErrorCode(securityInfo.errorCode)) {
+      // The connection failed.
+      info.state = "broken";
+      info.errorMessage = securityInfo.errorMessage;
+      if (options.certificateChain && SSLStatus.failedCertChain) {
+        info.certificates = this.getCertificateChain(SSLStatus.failedCertChain, options);
+      }
+      return info;
+    }
+
+    const state = securityInfo.securityState;
+
+    let 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.
+      // Leave 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);
+
+    if (options.certificateChain && SSLStatus.succeededCertChain) {
+      info.certificates = this.getCertificateChain(SSLStatus.succeededCertChain, options);
+    } else {
+      info.certificates = [this.parseCertificateInfo(SSLStatus.serverCert, options)];
+    }
+
+    // HSTS and HPKP if available.
+    if (uri && uri.host) {
+      // SiteSecurityService uses different storage if the channel is
+      // private. Thus we must give isSecureURI correct flags or we
+      // might get incorrect results.
+      let flags = 0;
+      if (channel instanceof Ci.nsIPrivateBrowsingChannel && channel.isChannelPrivate) {
+        flags = Ci.nsISocketProvider.NO_PERMANENT_STORAGE;
+      }
+
+      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;
+    }
+
+    return info;
+  },
+
+  getCertificateChain(certChain, options = {}) {
+    let certificates = [];
+    for (let cert of XPCOMUtils.IterSimpleEnumerator(certChain.getEnumerator(), Ci.nsIX509Cert)) {
+      certificates.push(this.parseCertificateInfo(cert, options));
+    }
+    return certificates;
+  },
+
+  /**
+   * Takes an nsIX509Cert and returns an object with certificate information.
+   *
+   * @param {nsIX509Cert} cert
+   *        The certificate to extract the information from.
+   * @param {Object} options
+   * @returns {Object}
+   *         An object with following format:
+   *           {
+   *             subject: subjectName,
+   *             issuer: issuerName,
+   *             validity: { start, end },
+   *             fingerprint: { sha1, sha256 }
+   *           }
+   */
+  parseCertificateInfo(cert, options = {}) {
+    if (!cert) {
+      return {};
+    }
+
+    let certData = {
+      subject: cert.subjectName,
+      issuer: cert.issuerName,
+      validity: {
+        startGMT: cert.validity.notBeforeGMT,
+        endGMT: cert.validity.notAfterGMT,
+      },
+      fingerprint: {
+        sha1: cert.sha1Fingerprint,
+        sha256: cert.sha256Fingerprint,
+      },
+      serialNumber: cert.serialNumber,
+      isBuiltInRoot: cert.isBuiltInRoot,
+      subjectPublicKeyInfoDigest: {
+        sha256: cert.sha256SubjectPublicKeyInfoDigest,
+      },
+      keyUsages: cert.keyUsages,
+    };
+    if (options.rawDER) {
+      certData.rawDER = cert.getRawDER({});
+    }
+    return certData;
+  },
+
+  // Bug 1355903 Transparency is currently disabled using security.pki.certificate_transparency.mode
+  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";
+  },
+
+  /**
+   * 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";
+    }
+    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) {
+    // 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) {
+      if (state & wpl.STATE_USES_WEAK_CRYPTO) {
+        reasons.push("cipher");
+      }
+    }
+
+    return reasons;
+  },
+};
diff --git a/toolkit/modules/addons/WebRequest.jsm b/toolkit/modules/addons/WebRequest.jsm
index 786a2d5dbe91..a4c9e9859a21 100644
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -15,14 +15,12 @@ const {nsIHttpActivityObserver, nsISocketTransport} = Ci;
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "ExtensionUtils",
-                               "resource://gre/modules/ExtensionUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "WebRequestCommon",
-                               "resource://gre/modules/WebRequestCommon.jsm");
-ChromeUtils.defineModuleGetter(this, "WebRequestUpload",
-                               "resource://gre/modules/WebRequestUpload.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "ExtensionError", () => ExtensionUtils.ExtensionError);
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm",
+  WebRequestCommon: "resource://gre/modules/WebRequestCommon.jsm",
+  WebRequestUpload: "resource://gre/modules/WebRequestUpload.jsm",
+  SecurityInfo: "resource://gre/modules/SecurityInfo.jsm",
+});
 
 function runLater(job) {
   Services.tm.dispatchToMainThread(job);
@@ -41,7 +39,7 @@ function parseExtra(extra, allowed = [], optionsObj = {}) {
   if (extra) {
     for (let ex of extra) {
       if (!allowed.includes(ex)) {
-        throw new ExtensionError(`Invalid option ${ex}`);
+        throw new ExtensionUtils.ExtensionError(`Invalid option ${ex}`);
       }
     }
   }
@@ -1011,6 +1009,11 @@ var WebRequest = {
 
   // nsIHttpActivityObserver.
   onErrorOccurred: onErrorOccurred,
+
+  getSecurityInfo: (details) => {
+    let channel = ChannelWrapper.getRegisteredChannel(details.id, details.extension, details.tabParent);
+    return SecurityInfo.getSecurityInfo(channel.channel, details.options);
+  },
 };
 
 Services.ppmm.loadProcessScript("resource://gre/modules/WebRequestContent.js", true);
diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build
index 110fff7737f2..0a1e48b964c1 100644
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -167,6 +167,7 @@ with Files('docs/**'):
 
 EXTRA_JS_MODULES += [
     'addons/MatchURLFilters.jsm',
+    'addons/SecurityInfo.jsm',
     'addons/WebNavigation.jsm',
     'addons/WebNavigationContent.js',
     'addons/WebNavigationFrames.jsm',





More information about the tor-commits mailing list