☠☠ backed out by 99581ff1fb9d ☠ ☠ | |
author | Johann Hofmann <jhofmann@mozilla.com> |
Wed, 13 Mar 2019 21:04:02 +0000 | |
changeset 463907 | e4718a35d70b174f7acf7528c3de81b567a87b66 |
parent 463906 | a8f1fd35587fa7df233e2fced117f22926b7cabd |
child 463908 | 000dfd4caca0183893f821c4856d2a97c043bf5c |
push id | 80442 |
push user | jhofmann@mozilla.com |
push date | Wed, 13 Mar 2019 22:31:13 +0000 |
treeherder | autoland@000dfd4caca0 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | keeler, mconley |
bugs | 1529643 |
milestone | 67.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
|
--- a/browser/actors/NetErrorChild.jsm +++ b/browser/actors/NetErrorChild.jsm @@ -23,16 +23,18 @@ XPCOMUtils.defineLazyGetter(this, "gPipN }); XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() { return Services.strings.createBundle("chrome://branding/locale/brand.properties"); }); XPCOMUtils.defineLazyPreferenceGetter(this, "newErrorPagesEnabled", "browser.security.newcerterrorpage.enabled"); XPCOMUtils.defineLazyPreferenceGetter(this, "mitmErrorPageEnabled", "browser.security.newcerterrorpage.mitm.enabled"); +XPCOMUtils.defineLazyPreferenceGetter(this, "mitmPrimingEnabled", + "security.certerrors.mitm.priming.enabled"); XPCOMUtils.defineLazyGetter(this, "gNSSErrorsBundle", function() { return Services.strings.createBundle("chrome://pipnss/locale/nsserrors.properties"); }); const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; const MOZILLA_PKIX_ERROR_BASE = Ci.nsINSSErrorsService.MOZILLA_PKIX_ERROR_BASE; @@ -373,16 +375,26 @@ class NetErrorChild extends ActorChild { } else { let offset = (doc.documentElement.clientHeight / 2) - (textContainer.clientHeight / 2); if (offset > 0) { textContainer.style.marginTop = `${offset}px`; } } } + // Check if the connection is being man-in-the-middled. When the parent + // detects an intercepted connection, the page may be reloaded with a new + // error code (MOZILLA_PKIX_ERROR_MITM_DETECTED). + if (newErrorPagesEnabled && mitmPrimingEnabled && + msg.data.code == SEC_ERROR_UNKNOWN_ISSUER && + // Only do this check for top-level failures. + doc.ownerGlobal.top === doc.ownerGlobal) { + this.mm.sendAsyncMessage("Browser:PrimeMitm"); + } + let div = doc.getElementById("certificateErrorText"); div.textContent = msg.data.info; this._setTechDetails(msg, doc); let learnMoreLink = doc.getElementById("learnMoreLink"); let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); learnMoreLink.setAttribute("href", baseURL + "connection-not-secure"); let errWhatToDo = doc.getElementById("es_nssBadCert_" + msg.data.codeString); let es = doc.getElementById("errorWhatToDoText"); @@ -448,16 +460,24 @@ class NetErrorChild extends ActorChild { descriptionContainer.append(adminDescription); learnMoreLink.href = baseURL + "symantec-warning"; updateContainerPosition(); break; case MOZILLA_PKIX_ERROR_MITM_DETECTED: if (newErrorPagesEnabled && mitmErrorPageEnabled) { + let autoEnabledEnterpriseRoots = + Services.prefs.getBoolPref("security.enterprise_roots.auto-enabled", false); + if (mitmPrimingEnabled && autoEnabledEnterpriseRoots) { + // If we automatically tried to import enterprise root certs but it didn't + // fix the MITM, reset the pref. + this.mm.sendAsyncMessage("Browser:ResetEnterpriseRootsPref"); + } + // We don't actually know what the MitM is called (since we don't // maintain a list), so we'll try and display the common name of the // root issuer to the user. In the worst case they are as clueless as // before, in the best case this gives them an actionable hint. // This may be revised in the future. let {securityInfo} = docShell.failedChannel; securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); let mitmName = null;
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -966,16 +966,19 @@ pref("app.productInfo.baseURL", "https:/ pref("security.alternate_certificate_error_page", "certerror"); // Enable the new certificate error pages. pref("browser.security.newcerterrorpage.enabled", true); pref("browser.security.newcerterrorpage.mitm.enabled", true); pref("security.certerrors.recordEventTelemetry", true); pref("security.certerrors.permanentOverride", true); +pref("security.certerrors.mitm.priming.enabled", true); +pref("security.certerrors.mitm.priming.endpoint", "https://mitmdetection.services.mozilla.com/"); +pref("security.certerrors.mitm.auto_enable_enterprise_roots", false); // Whether to start the private browsing mode at application startup pref("browser.privatebrowsing.autostart", false); // Whether to show the new private browsing UI with in-content search box. pref("browser.privatebrowsing.searchUI", true); // Whether the bookmark panel should be shown when bookmarking a page.
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2930,16 +2930,19 @@ function PageProxyClickHandler(aEvent) { if (aEvent.button == 1 && Services.prefs.getBoolPref("middlemouse.paste")) middleMousePaste(aEvent); } // Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED = 2; const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3; +const SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; +const SEC_ERROR_UNKNOWN_ISSUER = SEC_ERROR_BASE + 13; + const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."]; /** * Handle command events bubbling up from error page content * or from about:newtab or from remote error pages that invoke * us via async messaging. */ var BrowserOnClick = { @@ -2948,30 +2951,34 @@ var BrowserOnClick = { mm.addMessageListener("Browser:CertExceptionError", this); mm.addMessageListener("Browser:OpenCaptivePortalPage", this); mm.addMessageListener("Browser:SiteBlockedError", this); mm.addMessageListener("Browser:EnableOnlineMode", this); mm.addMessageListener("Browser:SetSSLErrorReportAuto", this); mm.addMessageListener("Browser:ResetSSLPreferences", this); mm.addMessageListener("Browser:SSLErrorReportTelemetry", this); mm.addMessageListener("Browser:SSLErrorGoBack", this); + mm.addMessageListener("Browser:PrimeMitm", this); + mm.addMessageListener("Browser:ResetEnterpriseRootsPref", this); Services.obs.addObserver(this, "captive-portal-login-abort"); Services.obs.addObserver(this, "captive-portal-login-success"); }, uninit() { let mm = window.messageManager; mm.removeMessageListener("Browser:CertExceptionError", this); mm.removeMessageListener("Browser:SiteBlockedError", this); mm.removeMessageListener("Browser:EnableOnlineMode", this); mm.removeMessageListener("Browser:SetSSLErrorReportAuto", this); mm.removeMessageListener("Browser:ResetSSLPreferences", this); mm.removeMessageListener("Browser:SSLErrorReportTelemetry", this); mm.removeMessageListener("Browser:SSLErrorGoBack", this); + mm.removeMessageListener("Browser:PrimeMitm", this); + mm.removeMessageListener("Browser:ResetEnterpriseRootsPref", this); Services.obs.removeObserver(this, "captive-portal-login-abort"); Services.obs.removeObserver(this, "captive-portal-login-success"); }, observe(aSubject, aTopic, aData) { switch (aTopic) { case "captive-portal-login-abort": @@ -3026,17 +3033,84 @@ var BrowserOnClick = { case "Browser:SSLErrorReportTelemetry": let reportStatus = msg.data.reportStatus; Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI") .add(reportStatus); break; case "Browser:SSLErrorGoBack": goBackFromErrorPage(); break; - } + case "Browser:PrimeMitm": + this.primeMitm(msg.target); + break; + case "Browser:ResetEnterpriseRootsPref": + Services.prefs.clearUserPref("security.enterprise_roots.enabled"); + Services.prefs.clearUserPref("security.enterprise_roots.auto-enabled"); + break; + } + }, + + /** + * This function does a canary request to a reliable, maintained endpoint, in + * order to help network code detect a system-wide man-in-the-middle. + */ + primeMitm(browser) { + // If we already have a mitm canary issuer stored, then don't bother with the + // extra request. This will be cleared on every update ping. + if (Services.prefs.getStringPref("security.pki.mitm_canary_issuer", null)) { + return; + } + + let url = Services.prefs.getStringPref("security.certerrors.mitm.priming.endpoint"); + let request = new XMLHttpRequest({mozAnon: true}); + request.open("HEAD", url); + request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; + + request.addEventListener("error", event => { + // Make sure the user is still on the cert error page. + if (!browser.documentURI.spec.startsWith("about:certerror")) { + return; + } + + let secInfo = request.channel.securityInfo.QueryInterface(Ci.nsITransportSecurityInfo); + if (secInfo.errorCode != SEC_ERROR_UNKNOWN_ISSUER) { + return; + } + + // When we get to this point there's already something deeply wrong, it's very likely + // that there is indeed a system-wide MitM. + if (secInfo.serverCert && secInfo.serverCert.issuerName) { + // Grab the issuer of the certificate used in the exchange and store it so that our + // network-level MitM detection code has a comparison baseline. + Services.prefs.setStringPref("security.pki.mitm_canary_issuer", secInfo.serverCert.issuerName); + + // MitM issues are sometimes caused by software not registering their root certs in the + // Firefox root store. We might opt for using third party roots from the system root store. + if (Services.prefs.getBoolPref("security.certerrors.mitm.auto_enable_enterprise_roots")) { + if (!Services.prefs.getBoolPref("security.enterprise_roots.enabled")) { + // Loading enterprise roots happens on a background thread, so wait for import to finish. + BrowserUtils.promiseObserved("psm:enterprise-certs-imported").then(() => { + if (browser.documentURI.spec.startsWith("about:certerror")) { + browser.reload(); + } + }); + + Services.prefs.setBoolPref("security.enterprise_roots.enabled", true); + // Record that this pref was automatically set. + Services.prefs.setBoolPref("security.enterprise_roots.auto-enabled", true); + } + } else { + // Need to reload the page to make sure network code picks up the canary issuer pref. + browser.reload(); + } + } + }); + + request.send(null); }, onCertError(browser, elementId, isTopFrame, location, securityInfoAsString, frameId) { let securityInfo; let cert; switch (elementId) { case "viewCertificate":
--- a/browser/base/content/test/about/browser.ini +++ b/browser/base/content/test/about/browser.ini @@ -7,16 +7,17 @@ support-files = POSTSearchEngine.xml dummy_page.html prefs = browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false [browser_aboutCertError.js] [browser_aboutCertError_clockSkew.js] [browser_aboutCertError_exception.js] +[browser_aboutCertError_mitm.js] [browser_aboutCertError_telemetry.js] [browser_aboutHome_search_POST.js] [browser_aboutHome_search_composing.js] [browser_aboutHome_search_searchbar.js] [browser_aboutHome_search_suggestion.js] skip-if = os == "mac" || (os == "linux" && (!debug || bits == 64)) || (os == 'win' && os_version == '10.0' && bits == 64 && !debug) # Bug 1399648, bug 1402502 [browser_aboutHome_search_telemetry.js] [browser_aboutNetError.js]
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/about/browser_aboutCertError_mitm.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const PREF_NEW_CERT_ERRORS = "browser.security.newcerterrorpage.enabled"; +const PREF_MITM_PRIMING = "security.certerrors.mitm.priming.enabled"; +const PREF_MITM_PRIMING_ENDPOINT = "security.certerrors.mitm.priming.endpoint"; +const PREF_MITM_CANARY_ISSUER = "security.pki.mitm_canary_issuer"; +const PREF_MITM_AUTO_ENABLE_ENTERPRISE_ROOTS = "security.certerrors.mitm.auto_enable_enterprise_roots"; +const PREF_ENTERPRISE_ROOTS = "security.enterprise_roots.enabled"; + +const UNKNOWN_ISSUER = "https://untrusted.example.com"; + +// Check that basic MitM priming works and the MitM error page is displayed successfully. +add_task(async function checkMitmPriming() { + await SpecialPowers.pushPrefEnv({"set": [ + [PREF_NEW_CERT_ERRORS, true], + [PREF_MITM_PRIMING, true], + [PREF_MITM_PRIMING_ENDPOINT, UNKNOWN_ISSUER], + ]}); + + let browser; + let certErrorLoaded; + await BrowserTestUtils.openNewForegroundTab(gBrowser, () => { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, UNKNOWN_ISSUER); + browser = gBrowser.selectedBrowser; + certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser); + }, false); + + await certErrorLoaded; + + // The page will reload after the initial canary request, so we'll just + // wait until we're seeing the dedicated MitM page. + await TestUtils.waitForCondition(function() { + return ContentTask.spawn(browser, {}, () => { + return content.document.body.getAttribute("code") == "MOZILLA_PKIX_ERROR_MITM_DETECTED"; + }); + }, "Loads the MitM error page."); + + ok(true, "Successfully loaded the MitM error page."); + + is(Services.prefs.getStringPref(PREF_MITM_CANARY_ISSUER), "CN=Unknown CA", "Stored the correct issuer"); + + await ContentTask.spawn(browser, {}, () => { + let mitmName1 = content.document.querySelector("#errorShortDescText .mitm-name"); + ok(ContentTaskUtils.is_visible(mitmName1), "Potential man in the middle is displayed"); + is(mitmName1.textContent, "Unknown CA", "Shows the name of the issuer."); + + let mitmName2 = content.document.querySelector("#errorWhatToDoText .mitm-name"); + ok(ContentTaskUtils.is_visible(mitmName2), "Potential man in the middle is displayed"); + is(mitmName2.textContent, "Unknown CA", "Shows the name of the issuer."); + }); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + Services.prefs.clearUserPref(PREF_MITM_CANARY_ISSUER); +}); + +// Check that we set the enterprise roots pref correctly on MitM +add_task(async function checkMitmAutoEnableEnterpriseRoots() { + await SpecialPowers.pushPrefEnv({"set": [ + [PREF_NEW_CERT_ERRORS, true], + [PREF_MITM_PRIMING, true], + [PREF_MITM_PRIMING_ENDPOINT, UNKNOWN_ISSUER], + [PREF_MITM_AUTO_ENABLE_ENTERPRISE_ROOTS, true], + ]}); + + let browser; + let certErrorLoaded; + + let prefChanged = TestUtils.waitForPrefChange(PREF_ENTERPRISE_ROOTS, value => value === true); + await BrowserTestUtils.openNewForegroundTab(gBrowser, () => { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, UNKNOWN_ISSUER); + browser = gBrowser.selectedBrowser; + certErrorLoaded = BrowserTestUtils.waitForErrorPage(browser); + }, false); + + await certErrorLoaded; + await prefChanged; + + // The page will reload after the initial canary request, so we'll just + // wait until we're seeing the dedicated MitM page. + await TestUtils.waitForCondition(function() { + return ContentTask.spawn(browser, {}, () => { + return content.document.body.getAttribute("code") == "MOZILLA_PKIX_ERROR_MITM_DETECTED"; + }); + }, "Loads the MitM error page."); + + ok(true, "Successfully loaded the MitM error page."); + + ok(!Services.prefs.prefHasUserValue(PREF_ENTERPRISE_ROOTS), "Flipped the enterprise roots pref back"); + + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + Services.prefs.clearUserPref(PREF_MITM_CANARY_ISSUER); +});
--- a/testing/modules/TestUtils.jsm +++ b/testing/modules/TestUtils.jsm @@ -65,16 +65,63 @@ var TestUtils = { Services.obs.removeObserver(observer, topic); reject(ex); } }, topic); }); }, /** + * Waits for the specified preference to be change. + * + * @param {string} prefName + * The pref to observe. + * @param {function} checkFn [optional] + * Called with the new preference value as argument, should return true if the + * notification is the expected one, or false if it should be ignored + * and listening should continue. If not specified, the first + * notification for the specified topic resolves the returned promise. + * + * @note Because this function is intended for testing, any error in checkFn + * will cause the returned promise to be rejected instead of waiting for + * the next notification, since this is probably a bug in the test. + * + * @return {Promise} + * @resolves The value of the preference. + */ + waitForPrefChange(prefName, checkFn) { + return new Promise((resolve, reject) => { + Services.prefs.addObserver(prefName, function observer(subject, topic, data) { + try { + let prefValue = null; + switch (Services.prefs.getPrefType(prefName)) { + case Services.prefs.PREF_STRING: + prefValue = Services.prefs.getStringPref(prefName); + break; + case Services.prefs.PREF_INT: + prefValue = Services.prefs.getIntPref(prefName); + break; + case Services.prefs.PREF_BOOL: + prefValue = Services.prefs.getBoolPref(prefName); + break; + } + if (checkFn && !checkFn(prefValue)) { + return; + } + Services.prefs.removeObserver(prefName, observer); + resolve(prefValue); + } catch (ex) { + Services.prefs.removeObserver(prefName, observer); + reject(ex); + } + }); + }); + }, + + /** * Takes a screenshot of an area and returns it as a data URL. * * @param eltOrRect * The DOM node or rect ({left, top, width, height}) to screenshot. * @param win * The current window. */ screenshotArea(eltOrRect, win) {
--- a/toolkit/modules/BrowserUtils.jsm +++ b/toolkit/modules/BrowserUtils.jsm @@ -682,9 +682,33 @@ var BrowserUtils = { fragment.appendChild(doc.createTextNode(part)); } } else { fragment.appendChild(part); } } return fragment; }, + + /** + * Returns a Promise which resolves when the given observer topic has been + * observed. + * + * @param {string} topic + * The topic to observe. + * @param {function(nsISupports, string)} [test] + * An optional test function which, when called with the + * observer's subject and data, should return true if this is the + * expected notification, false otherwise. + * @returns {Promise<object>} + */ + promiseObserved(topic, test = () => true) { + return new Promise(resolve => { + let observer = (subject, topic, data) => { + if (test(subject, data)) { + Services.obs.removeObserver(observer, topic); + resolve({subject, data}); + } + }; + Services.obs.addObserver(observer, topic); + }); + }, };