commit 1935dcf38ca112f9fbc9fe42c2289d77e4f95932 Author: Shane Caraveo scaraveo@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',
tor-commits@lists.torproject.org