Bug 846498 - create NetError UI, implement report send functionality. r=felipc@gmail.com

This commit is contained in:
Mark Goodwin 2014-10-30 12:52:00 +01:00
parent a0f34fc2ad
commit 91d8cf8984
10 changed files with 486 additions and 0 deletions

View File

@ -91,6 +91,37 @@
buttonEl.disabled = true;
}
function toggleDisplay(node) {
toggle = {
'': 'block',
'none': 'block',
'block': 'none'
};
node.style.display = toggle[node.style.display];
}
function showCertificateErrorReporting() {
// Display error reporting UI
document.getElementById('certificateErrorReporting').style.display = 'block';
// Get the hostname and add it to the panel
document.getElementById('hostname').textContent = document.location.hostname;
// Register click handler for the certificateErrorReportingPanel
document.getElementById('showCertificateErrorReportingPanel')
.addEventListener('click', function togglePanelVisibility() {
var panel = document.getElementById('certificateErrorReportingPanel');
toggleDisplay(panel);
});
}
function sendErrorReport() {
var event = new CustomEvent("AboutNetErrorSendReport", {bubbles:true});
document.dispatchEvent(event);
}
function initPage()
{
var err = getErrorCode();
@ -161,6 +192,37 @@
document.getElementById("errorTryAgain").style.display = "none";
}
window.addEventListener("AboutNetErrorOptions", function(evt) {
// Pinning errors are of type nssFailure2 (don't ask me why)
if (getErrorCode() == "nssFailure2") {
// TODO: and the pref is set...
var options = JSON.parse(evt.detail);
if (options && options.enabled) {
var checkbox = document.getElementById('automaticallyReportInFuture');
showCertificateErrorReporting();
if (options.automatic) {
// set the checkbox
checkbox.checked = true;
}
checkbox.addEventListener('change', function(evt) {
var event = new CustomEvent("AboutNetErrorSetAutomatic",
{bubbles:true, detail:evt.target.checked});
document.dispatchEvent(event);
}, false);
var reportBtn = document.getElementById('reportCertificateError');
var retryBtn = document.getElementById('reportCertificateErrorRetry');
reportBtn.addEventListener('click', sendErrorReport, false);
retryBtn.addEventListener('click', sendErrorReport, false);
}
}
}.bind(this), true, true);
var event = new CustomEvent("AboutNetErrorLoad", {bubbles:true});
document.dispatchEvent(event);
if (err == "nssBadCert") {
// Remove the "Try again" button for security exceptions, since it's
// almost certainly useless.
@ -348,6 +410,7 @@
<a id="securityOverrideLink" href="javascript:showSecuritySection();" >&securityOverride.linkText;</a>
<div id="securityOverrideContent" style="display: none;">&securityOverride.warningContent;</div>
</div>
</div>
<!-- Retry Button -->
@ -368,6 +431,28 @@
}
</script>
<!-- UI for option to report certificate errors to Mozilla. Removed on
init for other error types .-->
<div id="certificateErrorReporting">
<a id="showCertificateErrorReportingPanel" href="#">&errorReporting.title;<span class="downArrow"></span></a>
</div>
<div id="certificateErrorReportingPanel">
<p>&errorReporting.longDesc;</p>
<p>
<input type="checkbox" id="automaticallyReportInFuture" />
<label for="automaticallyReportInFuture" id="automaticallyReportInFuture">&errorReporting.automatic;</label>
</p>
<!-- TODO add link to relevant page on sumo -->
<a href="https://support.mozilla.org/kb/certificate-pinning-reports" target="new">&errorReporting.learnMore;</a>
<span id="reportingState">
<button id="reportCertificateError">&errorReporting.report;</button>
<button id="reportCertificateErrorRetry">&errorReporting.tryAgain;</button>
<span id="reportSendingMessage">&errorReporting.sending;</span>
<span id="reportSentMessage">&errorReporting.sent;</span>
</span>
</div>
</div>
<!--

View File

@ -5,6 +5,7 @@
let Ci = Components.interfaces;
let Cu = Components.utils;
let Cc = Components.classes;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NotificationDB.jsm");
@ -47,6 +48,9 @@ var gMultiProcessBrowser =
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsILoadContext)
.useRemoteTabs;
var gAppInfo = Cc["@mozilla.org/xre/app-info;1"]
.getService(Ci.nsIXULAppInfo)
.QueryInterface(Ci.nsIXULRuntime);
#ifndef XP_MACOSX
var gEditUIVisible = true;
@ -2417,6 +2421,8 @@ let BrowserOnClick = {
mm.addMessageListener("Browser:CertExceptionError", this);
mm.addMessageListener("Browser:SiteBlockedError", this);
mm.addMessageListener("Browser:NetworkError", this);
mm.addMessageListener("Browser:SendSSLErrorReport", this);
mm.addMessageListener("Browser:SetSSLErrorReportAuto", this);
},
uninit: function () {
@ -2424,6 +2430,8 @@ let BrowserOnClick = {
mm.removeMessageListener("Browser:CertExceptionError", this);
mm.removeMessageListener("Browser:SiteBlockedError", this);
mm.removeMessageListener("Browser:NetworkError", this);
mm.removeMessageListener("Browser:SendSSLErrorReport", this);
mm.removeMessageListener("Browser:SetSSLErrorReportAuto", this);
},
handleEvent: function (event) {
@ -2462,9 +2470,124 @@ let BrowserOnClick = {
// Reset network state, the error page will refresh on its own.
Services.io.offline = false;
break;
case "Browser:SendSSLErrorReport":
this.onSSLErrorReport(msg.target, msg.data.elementId,
msg.data.documentURI,
msg.data.location,
msg.data.securityInfo);
break;
case "Browser:SetSSLErrorReportAuto":
Services.prefs.setBoolPref("security.ssl.errorReporting.automatic", msg.json.automatic);
break;
}
},
onSSLErrorReport: function(browser, elementId, documentURI, location, securityInfo) {
function showReportStatus(reportStatus) {
gBrowser.selectedBrowser
.messageManager
.sendAsyncMessage("Browser:SSLErrorReportStatus",
{
reportStatus: reportStatus,
documentURI: documentURI
});
}
if (!Services.prefs.getBoolPref("security.ssl.errorReporting.enabled")) {
showReportStatus("error");
Cu.reportError("User requested certificate error report sending, but certificate error reporting is disabled");
return;
}
let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
.getService(Ci.nsISerializationHelper);
let transportSecurityInfo = serhelper.deserializeObject(securityInfo);
transportSecurityInfo.QueryInterface(Ci.nsITransportSecurityInfo)
if (transportSecurityInfo.failedCertChain == null) {
Cu.reportError("transportSecurityInfo didn't have a failedCertChain for a failedChannel");
return;
}
showReportStatus("activity");
/*
* Requested info for the report:
* - Domain of bad connection
* - Error type (e.g. Pinning, domain mismatch, etc)
* - Cert chain (at minimum, same data to distrust each cert in the
* chain)
* - Request data (e.g. User Agent, IP, Timestamp)
*
* The request data should be added to the report by the receiving server.
*/
// TODO: can we pull this in from pippki.js isntead of duplicating it
// here?
function getDERString(cert)
{
var length = {};
var derArray = cert.getRawDER(length);
var derString = '';
for (var i = 0; i < derArray.length; i++) {
derString += String.fromCharCode(derArray[i]);
}
return derString;
}
// Convert the nsIX509CertList into a format that can be parsed into
// JSON
let asciiCertChain = [];
let certs = transportSecurityInfo.failedCertChain.getEnumerator();
while (certs.hasMoreElements()) {
let cert = certs.getNext();
cert.QueryInterface(Ci.nsIX509Cert);
asciiCertChain.push(btoa(getDERString(cert)));
}
let report = {
hostname: location.hostname,
port: location.port,
timestamp: Math.round(Date.now() / 1000),
errorCode: transportSecurityInfo.errorCode,
failedCertChain: asciiCertChain,
userAgent: window.navigator.userAgent,
version: 1,
build: gAppInfo.appBuildID,
product: gAppInfo.name,
channel: Services.prefs.getCharPref("app.update.channel")
}
let reportURL = Services.prefs.getCharPref("security.ssl.errorReporting.url");
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
try {
xhr.open("POST", reportURL);
} catch (e) {
Cu.reportError("xhr.open exception", e);
showReportStatus("error");
}
xhr.onerror = function (e) {
// error making request to reportURL
Cu.reportError("xhr onerror", e);
showReportStatus("error");
};
xhr.onload = function (event) {
if (xhr.status !== 201 && xhr.status !== 0) {
// request returned non-success status
Cu.reportError("xhr returned failure code", xhr.status);
showReportStatus("error");
} else {
showReportStatus("complete");
}
};
xhr.send(JSON.stringify(report));
},
onAboutCertError: function (browser, elementId, isTopFrame, location, sslStatusAsString) {
let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");

View File

@ -122,6 +122,113 @@ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
}
let AboutNetErrorListener = {
init: function(chromeGlobal) {
chromeGlobal.addEventListener('AboutNetErrorLoad', this, false, true);
chromeGlobal.addEventListener('AboutNetErrorSetAutomatic', this, false, true);
chromeGlobal.addEventListener('AboutNetErrorSendReport', this, false, true);
},
get isAboutNetError() {
return content.document.documentURI.startsWith("about:neterror");
},
handleEvent: function(aEvent) {
if (!this.isAboutNetError) {
return;
}
switch (aEvent.type) {
case "AboutNetErrorLoad":
this.onPageLoad(aEvent);
break;
case "AboutNetErrorSetAutomatic":
this.onSetAutomatic(aEvent);
break;
case "AboutNetErrorSendReport":
this.onSendReport(aEvent);
break;
}
},
onPageLoad: function(evt) {
let automatic = Services.prefs.getBoolPref("security.ssl.errorReporting.automatic");
content.dispatchEvent(new content.CustomEvent("AboutNetErrorOptions", {
detail: JSON.stringify({
enabled: Services.prefs.getBoolPref("security.ssl.errorReporting.enabled"),
automatic: automatic
})
}
));
if (automatic) {
this.onSendReport(evt);
}
},
onSetAutomatic: function(evt) {
sendAsyncMessage("Browser:SetSSLErrorReportAuto", {
automatic: evt.detail
});
},
onSendReport: function(evt) {
let contentDoc = content.document;
let reportSendingMsg = contentDoc.getElementById("reportSendingMessage");
let reportSentMsg = contentDoc.getElementById("reportSentMessage");
let reportBtn = contentDoc.getElementById("reportCertificateError");
let retryBtn = contentDoc.getElementById("reportCertificateErrorRetry");
addMessageListener("Browser:SSLErrorReportStatus", function(message) {
// show and hide bits - but only if this is a message for the right
// document - we'll compare on document URI
if (contentDoc.documentURI === message.data.documentURI) {
switch(message.data.reportStatus) {
case "activity":
// Hide the button that was just clicked
reportBtn.style.display = "none";
retryBtn.style.display = "none";
reportSentMsg.style.display = "none";
reportSendingMsg.style.display = "inline";
break;
case "error":
// show the retry button
retryBtn.style.display = "inline";
reportSendingMsg.style.display = "none";
break;
case "complete":
// Show a success indicator
reportSentMsg.style.display = "inline";
reportSendingMsg.style.display = "none";
break;
}
}
});
let failedChannel = docShell.failedChannel;
let location = contentDoc.location.href;
let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
.getService(Ci.nsISerializationHelper);
let serializable = docShell.failedChannel.securityInfo
.QueryInterface(Ci.nsITransportSecurityInfo)
.QueryInterface(Ci.nsISerializable);
let serializedSecurityInfo = serhelper.serializeToString(serializable);
sendAsyncMessage("Browser:SendSSLErrorReport", {
elementId: evt.target.id,
documentURI: contentDoc.documentURI,
location: contentDoc.location,
securityInfo: serializedSecurityInfo
});
}
}
AboutNetErrorListener.init(this);
let AboutHomeListener = {
init: function(chromeGlobal) {
chromeGlobal.addEventListener('AboutHomeLoad', this, false, true);

View File

@ -0,0 +1,46 @@
.. _healthreport_dataformat:
==============
Payload Format
==============
An example report::
{
"timestamp":1413490449,
"errorCode":-16384,
"failedCertChain":[
],
"userAgent":"Mozilla/5.0 (X11; Linux x86_64; rv:36.0) Gecko/20100101 Firefox/36.0",
"version":1,
"build":"20141022164419",
"product":"Firefox",
"channel":"default"
}
Where the data represents the following:
"timestamp"
The (local) time at which the report was generated. Seconds since 1 Jan 1970,
UTC.
"errorCode"
The error code. This is the error code from certificate verification. Here's a small list of the most commonly-encountered errors:
https://wiki.mozilla.org/SecurityEngineering/x509Certs#Error_Codes_in_Firefox
In theory many of the errors from sslerr.h, secerr.h, and pkixnss.h could be encountered. We're starting with just MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE, which means that key pinning failed (i.e. there wasn't an intersection between the keys in any computed trusted certificate chain and the expected list of keys for the domain the user is attempting to connect to).
"failedCertChain"
The certificate chain which caused the pinning violation (array of base64
encoded PEM)
"user agent"
The user agent string of the browser sending the report
"build"
The build ID
"product"
The product name
"channel"
The user's release channel

View File

@ -0,0 +1,15 @@
.. _sslerrorreport
===================
SSL Error Reporting
===================
With the introduction of HPKP, it becomes useful to be able to capture data
on pin violations. SSL Error Reporting is an opt-in mechanism to allow users
to send data on such violations to mozilla.
.. toctree::
:maxdepth: 1
dataformat
preferences

View File

@ -0,0 +1,23 @@
.. _healthreport_preferences:
===========
Preferences
===========
The following preferences are used by SSL Error reporting:
"security.ssl.errorReporting.enabled"
Should the SSL Error Reporting UI be shown on pin violations? Default
value: ``true``
"security.ssl.errorReporting.url"
Where should SSL error reports be sent? Default value:
``https://data.mozilla.com/submit/sslreports-stg``
"security.ssl.errorReporting.automatic"
Should error reports be sent without user interaction. Default value:
``false``. Note: this pref is overridden by the value of
``security.ssl.errorReporting.enabled``
This is only set when specifically requested by the user. The user can set
this value (or unset it) by checking the "Automatically report errors in the
future" checkbox when about:neterror is displayed for SSL Errors.

View File

@ -4,6 +4,8 @@
# 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/.
SPHINX_TREES['sslerrorreport'] = 'content/docs/sslerrorreport'
MOCHITEST_MANIFESTS += [
'content/test/general/mochitest.ini',
]

View File

@ -193,5 +193,14 @@ functionality specific to firefox. -->
<button id='exceptionDialogButton'>&securityOverride.exceptionButtonLabel;</button>
">
<!ENTITY errorReporting.title "Report this error">
<!ENTITY errorReporting.longDesc "Reporting the address and certificate information for <span id='hostname'></span> will help us identify and block malicious sites. Thanks for helping create a safer web!">
<!ENTITY errorReporting.automatic "Automatically report errors in the future">
<!ENTITY errorReporting.learnMore "Learn more…">
<!ENTITY errorReporting.sending "Sending report">
<!ENTITY errorReporting.sent "Report sent">
<!ENTITY errorReporting.report "Report">
<!ENTITY errorReporting.tryAgain "Try again">
<!ENTITY remoteXUL.title "Remote XUL">
<!ENTITY remoteXUL.longDesc "<p><ul><li>Please contact the website owners to inform them of this problem.</li></ul></p>">

View File

@ -67,3 +67,75 @@ ul {
button:disabled {
cursor: pointer;
}
div#certificateErrorReporting {
display: none;
float:right;
/* Align with the "Try Again" button */
margin-top:24px;
margin-right:24px;
}
div#certificateErrorReporting a,
div#certificateErrorReportingPanel a {
color: #0095DD;
}
div#certificateErrorReporting a {
text-decoration: none;
}
div#certificateErrorReporting a:hover {
text-decoration: underline;
}
span.downArrow {
font-size: 0.9em;
}
div#certificateErrorReportingPanel {
/* Hidden until the link is clicked */
display: none;
background-color: white;
border: 1px lightgray solid;
/* Don't use top padding because the default p style has top padding, and it
* makes the overall div look uneven */
padding: 0 12px 12px 12px;
box-shadow: 0 0 4px #ddd;
position: relative;
width: 75%;
left: 34%;
font-size: 0.9em;
top: 8px;
}
span#hostname {
font-weight: bold;
}
#automaticallyReportInFuture {
cursor: pointer;
}
#reportingState {
padding-left: 150px;
}
#reportSendingMessage {
position: relative;
display: none;
}
#reportSentMessage {
position: relative;
display: none;
}
button#reportCertificateError {
position: relative;
}
button#reportCertificateErrorRetry {
position: relative;
display: none;
}

View File

@ -50,3 +50,7 @@ pref("security.password_lifetime", 30);
pref("security.OCSP.enabled", 1);
pref("security.OCSP.require", false);
pref("security.OCSP.GET.enabled", false);
pref("security.ssl.errorReporting.enabled", true);
pref("security.ssl.errorReporting.url", "https://data.mozilla.com/submit/sslreports-stg");
pref("security.ssl.errorReporting.automatic", false);