Bug 1529643 - Implement MitM priming on certificate error pages. r=keeler,mconley
Differential Revision: https://phabricator.services.mozilla.com/D22406
This commit is contained in:
@@ -28,6 +28,8 @@ XPCOMUtils.defineLazyPreferenceGetter(this, "newErrorPagesEnabled",
|
|||||||
"browser.security.newcerterrorpage.enabled");
|
"browser.security.newcerterrorpage.enabled");
|
||||||
XPCOMUtils.defineLazyPreferenceGetter(this, "mitmErrorPageEnabled",
|
XPCOMUtils.defineLazyPreferenceGetter(this, "mitmErrorPageEnabled",
|
||||||
"browser.security.newcerterrorpage.mitm.enabled");
|
"browser.security.newcerterrorpage.mitm.enabled");
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(this, "mitmPrimingEnabled",
|
||||||
|
"security.certerrors.mitm.priming.enabled");
|
||||||
XPCOMUtils.defineLazyGetter(this, "gNSSErrorsBundle", function() {
|
XPCOMUtils.defineLazyGetter(this, "gNSSErrorsBundle", function() {
|
||||||
return Services.strings.createBundle("chrome://pipnss/locale/nsserrors.properties");
|
return Services.strings.createBundle("chrome://pipnss/locale/nsserrors.properties");
|
||||||
});
|
});
|
||||||
@@ -356,6 +358,7 @@ class NetErrorChild extends ActorChild {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line complexity
|
||||||
onCertErrorDetails(msg, docShell) {
|
onCertErrorDetails(msg, docShell) {
|
||||||
let doc = docShell.document;
|
let doc = docShell.document;
|
||||||
|
|
||||||
@@ -378,6 +381,16 @@ class NetErrorChild extends ActorChild {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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");
|
let div = doc.getElementById("certificateErrorText");
|
||||||
div.textContent = msg.data.info;
|
div.textContent = msg.data.info;
|
||||||
this._setTechDetails(msg, doc);
|
this._setTechDetails(msg, doc);
|
||||||
@@ -453,6 +466,14 @@ class NetErrorChild extends ActorChild {
|
|||||||
break;
|
break;
|
||||||
case MOZILLA_PKIX_ERROR_MITM_DETECTED:
|
case MOZILLA_PKIX_ERROR_MITM_DETECTED:
|
||||||
if (newErrorPagesEnabled && mitmErrorPageEnabled) {
|
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
|
// 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
|
// 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
|
// root issuer to the user. In the worst case they are as clueless as
|
||||||
|
|||||||
@@ -971,6 +971,9 @@ pref("browser.security.newcerterrorpage.enabled", true);
|
|||||||
pref("browser.security.newcerterrorpage.mitm.enabled", true);
|
pref("browser.security.newcerterrorpage.mitm.enabled", true);
|
||||||
pref("security.certerrors.recordEventTelemetry", true);
|
pref("security.certerrors.recordEventTelemetry", true);
|
||||||
pref("security.certerrors.permanentOverride", 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
|
// Whether to start the private browsing mode at application startup
|
||||||
pref("browser.privatebrowsing.autostart", false);
|
pref("browser.privatebrowsing.autostart", false);
|
||||||
|
|||||||
@@ -2935,6 +2935,9 @@ function PageProxyClickHandler(aEvent) {
|
|||||||
const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED = 2;
|
const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED = 2;
|
||||||
const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3;
|
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."];
|
const PREF_SSL_IMPACT_ROOTS = ["security.tls.version.", "security.ssl3."];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2953,6 +2956,8 @@ var BrowserOnClick = {
|
|||||||
mm.addMessageListener("Browser:ResetSSLPreferences", this);
|
mm.addMessageListener("Browser:ResetSSLPreferences", this);
|
||||||
mm.addMessageListener("Browser:SSLErrorReportTelemetry", this);
|
mm.addMessageListener("Browser:SSLErrorReportTelemetry", this);
|
||||||
mm.addMessageListener("Browser:SSLErrorGoBack", 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-abort");
|
||||||
Services.obs.addObserver(this, "captive-portal-login-success");
|
Services.obs.addObserver(this, "captive-portal-login-success");
|
||||||
@@ -2967,6 +2972,8 @@ var BrowserOnClick = {
|
|||||||
mm.removeMessageListener("Browser:ResetSSLPreferences", this);
|
mm.removeMessageListener("Browser:ResetSSLPreferences", this);
|
||||||
mm.removeMessageListener("Browser:SSLErrorReportTelemetry", this);
|
mm.removeMessageListener("Browser:SSLErrorReportTelemetry", this);
|
||||||
mm.removeMessageListener("Browser:SSLErrorGoBack", 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-abort");
|
||||||
Services.obs.removeObserver(this, "captive-portal-login-success");
|
Services.obs.removeObserver(this, "captive-portal-login-success");
|
||||||
@@ -3031,9 +3038,76 @@ var BrowserOnClick = {
|
|||||||
case "Browser:SSLErrorGoBack":
|
case "Browser:SSLErrorGoBack":
|
||||||
goBackFromErrorPage();
|
goBackFromErrorPage();
|
||||||
break;
|
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) {
|
onCertError(browser, elementId, isTopFrame, location, securityInfoAsString, frameId) {
|
||||||
let securityInfo;
|
let securityInfo;
|
||||||
let cert;
|
let cert;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ prefs =
|
|||||||
[browser_aboutCertError.js]
|
[browser_aboutCertError.js]
|
||||||
[browser_aboutCertError_clockSkew.js]
|
[browser_aboutCertError_clockSkew.js]
|
||||||
[browser_aboutCertError_exception.js]
|
[browser_aboutCertError_exception.js]
|
||||||
|
[browser_aboutCertError_mitm.js]
|
||||||
[browser_aboutCertError_telemetry.js]
|
[browser_aboutCertError_telemetry.js]
|
||||||
[browser_aboutHome_search_POST.js]
|
[browser_aboutHome_search_POST.js]
|
||||||
[browser_aboutHome_search_composing.js]
|
[browser_aboutHome_search_composing.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);
|
||||||
|
});
|
||||||
@@ -69,6 +69,53 @@ var TestUtils = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Takes a screenshot of an area and returns it as a data URL.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -687,4 +687,28 @@ var BrowserUtils = {
|
|||||||
}
|
}
|
||||||
return fragment;
|
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);
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user