diff options
Diffstat (limited to 'browser/base/content/aboutNetError.js')
-rw-r--r-- | browser/base/content/aboutNetError.js | 1265 |
1 files changed, 1265 insertions, 0 deletions
diff --git a/browser/base/content/aboutNetError.js b/browser/base/content/aboutNetError.js new file mode 100644 index 0000000000..376d9d9dea --- /dev/null +++ b/browser/base/content/aboutNetError.js @@ -0,0 +1,1265 @@ +/* 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/. */ + +/* eslint-env mozilla/frame-script */ + +import { parse } from "chrome://global/content/certviewer/certDecoder.js"; +import { pemToDER } from "chrome://global/content/certviewer/utils.js"; + +const formatter = new Intl.DateTimeFormat("default"); + +const HOST_NAME = new URL(RPMGetInnerMostURI(document.location.href)).hostname; + +// Used to check if we have a specific localized message for an error. +const KNOWN_ERROR_TITLE_IDS = new Set([ + // Error titles: + "connectionFailure-title", + "deniedPortAccess-title", + "dnsNotFound-title", + "fileNotFound-title", + "fileAccessDenied-title", + "generic-title", + "captivePortal-title", + "malformedURI-title", + "netInterrupt-title", + "notCached-title", + "netOffline-title", + "contentEncodingError-title", + "unsafeContentType-title", + "netReset-title", + "netTimeout-title", + "unknownProtocolFound-title", + "proxyConnectFailure-title", + "proxyResolveFailure-title", + "redirectLoop-title", + "unknownSocketType-title", + "nssFailure2-title", + "csp-xfo-error-title", + "corruptedContentError-title", + "remoteXUL-title", + "sslv3Used-title", + "inadequateSecurityError-title", + "blockedByPolicy-title", + "clockSkewError-title", + "networkProtocolError-title", + "nssBadCert-title", + "nssBadCert-sts-title", + "certerror-mitm-title", +]); + +/* The error message IDs from nsserror.ftl get processed into + * aboutNetErrorCodes.js which is loaded before we are: */ +/* global KNOWN_ERROR_MESSAGE_IDS */ + +// The following parameters are parsed from the error URL: +// e - the error code +// s - custom CSS class to allow alternate styling/favicons +// d - error description +// captive - "true" to indicate we're behind a captive portal. +// Any other value is ignored. + +// Note that this file uses document.documentURI to get +// the URL (with the format from above). This is because +// document.location.href gets the current URI off the docshell, +// which is the URL displayed in the location bar, i.e. +// the URI that the user attempted to load. + +let searchParams = new URLSearchParams(document.documentURI.split("?")[1]); + +// Set to true on init if the error code is nssBadCert. +let gIsCertError; + +function getErrorCode() { + return searchParams.get("e"); +} + +function getCSSClass() { + return searchParams.get("s"); +} + +function getDescription() { + return searchParams.get("d"); +} + +function isCaptive() { + return searchParams.get("captive") == "true"; +} + +function retryThis(buttonEl) { + RPMSendAsyncMessage("Browser:EnableOnlineMode"); + buttonEl.disabled = true; +} + +function toggleDisplay(node) { + const toggle = { + "": "block", + none: "block", + block: "none", + }; + return (node.style.display = toggle[node.style.display]); +} + +function showBlockingErrorReporting() { + // Display blocking error reporting UI for XFO error and CSP error. + document.getElementById("blockingErrorReporting").style.display = "block"; +} + +function showPrefChangeContainer() { + const panel = document.getElementById("prefChangeContainer"); + panel.style.display = "block"; + document.getElementById("netErrorButtonContainer").style.display = "none"; + document + .getElementById("prefResetButton") + .addEventListener("click", function resetPreferences() { + RPMSendAsyncMessage("Browser:ResetSSLPreferences"); + }); + addAutofocus("#prefResetButton", "beforeend"); +} + +function showTls10Container() { + const panel = document.getElementById("enableTls10Container"); + panel.style.display = "block"; + document.getElementById("netErrorButtonContainer").style.display = "none"; + const button = document.getElementById("enableTls10Button"); + button.addEventListener("click", function enableTls10(e) { + RPMSetBoolPref("security.tls.version.enable-deprecated", true); + retryThis(button); + }); + addAutofocus("#enableTls10Button", "beforeend"); +} + +function setupAdvancedButton() { + // Get the hostname and add it to the panel + var panel = document.getElementById("badCertAdvancedPanel"); + for (var span of panel.querySelectorAll("span.hostname")) { + span.textContent = HOST_NAME; + } + + // Register click handler for the weakCryptoAdvancedPanel + document + .getElementById("advancedButton") + .addEventListener("click", togglePanelVisibility); + + function togglePanelVisibility() { + toggleDisplay(panel); + if (gIsCertError) { + // Toggling the advanced panel must ensure that the debugging + // information panel is hidden as well, since it's opened by the + // error code link in the advanced panel. + var div = document.getElementById("certificateErrorDebugInformation"); + div.style.display = "none"; + } + + if (panel.style.display == "block") { + // send event to trigger telemetry ping + var event = new CustomEvent("AboutNetErrorUIExpanded", { bubbles: true }); + document.dispatchEvent(event); + } + } + + if (!gIsCertError) { + return; + } + + if (getCSSClass() == "expertBadCert") { + toggleDisplay(document.getElementById("badCertAdvancedPanel")); + // Toggling the advanced panel must ensure that the debugging + // information panel is hidden as well, since it's opened by the + // error code link in the advanced panel. + var div = document.getElementById("certificateErrorDebugInformation"); + div.style.display = "none"; + } + + disallowCertOverridesIfNeeded(); +} + +function disallowCertOverridesIfNeeded() { + var cssClass = getCSSClass(); + // Disallow overrides if this is a Strict-Transport-Security + // host and the cert is bad (STS Spec section 7.3) or if the + // certerror is in a frame (bug 633691). + if (cssClass == "badStsCert" || window != top) { + document + .getElementById("exceptionDialogButton") + .setAttribute("hidden", "true"); + } + if (cssClass == "badStsCert") { + document.getElementById("badStsCertExplanation").removeAttribute("hidden"); + + let stsReturnButtonText = document.getElementById("stsReturnButtonText") + .textContent; + document.getElementById("returnButton").textContent = stsReturnButtonText; + document.getElementById( + "advancedPanelReturnButton" + ).textContent = stsReturnButtonText; + + let stsMitmWhatCanYouDoAboutIt3 = document.getElementById( + "stsMitmWhatCanYouDoAboutIt3" + ).innerHTML; + // eslint-disable-next-line no-unsanitized/property + document.getElementById( + "mitmWhatCanYouDoAboutIt3" + ).innerHTML = stsMitmWhatCanYouDoAboutIt3; + } +} + +function setErrorPageStrings(err) { + let title = err + "-title"; + + let isCertError = err == "nssBadCert"; + let className = getCSSClass(); + if (isCertError && (window !== window.top || className == "badStsCert")) { + title = err + "-sts-title"; + } + + let cspXfoError = err === "cspBlocked" || err === "xfoBlocked"; + if (cspXfoError) { + title = "csp-xfo-error-title"; + } + + let titleElement = document.querySelector(".title-text"); + + if (!KNOWN_ERROR_TITLE_IDS.has(title)) { + console.error("No strings exist for error:", title); + title = "generic-title"; + } + document.l10n.setAttributes(titleElement, title); +} + +function initPage() { + var err = getErrorCode(); + // List of error pages with an illustration. + let illustratedErrors = [ + "malformedURI", + "dnsNotFound", + "connectionFailure", + "netInterrupt", + "netTimeout", + "netReset", + "netOffline", + ]; + if (illustratedErrors.includes(err)) { + document.body.classList.add("illustrated", err); + } + if (err == "blockedByPolicy") { + document.body.classList.add("blocked"); + } + + gIsCertError = err == "nssBadCert"; + // Only worry about captive portals if this is a cert error. + let showCaptivePortalUI = isCaptive() && gIsCertError; + if (showCaptivePortalUI) { + err = "captivePortal"; + } + + let l10nErrId = err; + let className = getCSSClass(); + if (className) { + document.body.classList.add(className); + } + + if (gIsCertError && (window !== window.top || className == "badStsCert")) { + l10nErrId += "_sts"; + } + + let pageTitle = document.getElementById("ept_" + l10nErrId); + if (pageTitle) { + document.title = pageTitle.textContent; + } + + // if it's an unknown error or there's no title or description + // defined, get the generic message + var errDesc = document.getElementById("ed_" + l10nErrId); + if (!errDesc) { + errDesc = document.getElementById("ed_generic"); + } + + setErrorPageStrings(err); + + var sd = document.getElementById("errorShortDescText"); + if (sd) { + if (gIsCertError) { + // eslint-disable-next-line no-unsanitized/property + sd.innerHTML = errDesc.innerHTML; + } else { + sd.textContent = getDescription(); + } + } + + if (gIsCertError) { + if (showCaptivePortalUI) { + initPageCaptivePortal(); + } else { + initPageCertError(); + updateContainerPosition(); + } + + initCertErrorPageActions(); + setTechnicalDetailsOnCertError(); + return; + } + + addAutofocus("#netErrorButtonContainer > .try-again"); + + document.body.classList.add("neterror"); + + var ld = document.getElementById("errorLongDesc"); + if (ld) { + // eslint-disable-next-line no-unsanitized/property + ld.innerHTML = errDesc.innerHTML; + } + + if (err == "sslv3Used") { + document.getElementById("learnMoreContainer").style.display = "block"; + document.body.className = "certerror"; + } + + // remove undisplayed errors to avoid bug 39098 + var errContainer = document.getElementById("errorContainer"); + errContainer.remove(); + + if (err == "remoteXUL") { + // Remove the "Try again" button for remote XUL errors given that + // it is useless. + document.getElementById("netErrorButtonContainer").style.display = "none"; + } + + let learnMoreLink = document.getElementById("learnMoreLink"); + let baseURL = RPMGetFormatURLPref("app.support.baseURL"); + learnMoreLink.setAttribute("href", baseURL + "connection-not-secure"); + + if (err == "cspBlocked" || err == "xfoBlocked") { + // Remove the "Try again" button for XFO and CSP violations, + // since it's almost certainly useless. (Bug 553180) + document.getElementById("netErrorButtonContainer").style.display = "none"; + + // Adding a button for opening websites blocked for CSP and XFO violations + // in a new window. (Bug 1461195) + document.getElementById("errorShortDesc").style.display = "none"; + + let hostString = document.location.hostname; + let longDescription = document.getElementById("errorLongDesc"); + document.l10n.setAttributes(longDescription, "csp-xfo-blocked-long-desc", { + hostname: hostString, + }); + + document.getElementById("openInNewWindowContainer").style.display = "block"; + + let openInNewWindowButton = document.getElementById( + "openInNewWindowButton" + ); + openInNewWindowButton.href = document.location.href; + + // Add a learn more link + document.getElementById("learnMoreContainer").style.display = "block"; + learnMoreLink.setAttribute("href", baseURL + "xframe-neterror-page"); + + setupBlockingReportingUI(); + } + + setNetErrorMessageFromCode(); + + // Pinning errors are of type nssFailure2 + if (err == "nssFailure2") { + document.getElementById("learnMoreContainer").style.display = "block"; + + const errorCode = document.getNetErrorInfo().errorCodeString; + const isTlsVersionError = + errorCode == "SSL_ERROR_UNSUPPORTED_VERSION" || + errorCode == "SSL_ERROR_PROTOCOL_VERSION_ALERT"; + const tls10OverrideEnabled = RPMGetBoolPref( + "security.tls.version.enable-deprecated" + ); + + if ( + isTlsVersionError && + !tls10OverrideEnabled && + !RPMPrefIsLocked("security.tls.version.min") + ) { + // security.tls.* prefs may be reset by the user when they + // encounter an error, so it's important that this has a + // different pref branch. + const showOverride = RPMGetBoolPref( + "security.certerrors.tls.version.show-override", + true + ); + + // This is probably a TLS 1.0 server; offer to re-enable. + if (showOverride) { + showTls10Container(); + } + } else { + const hasPrefStyleError = [ + "interrupted", // This happens with subresources that are above the max tls + "SSL_ERROR_NO_CIPHERS_SUPPORTED", + "SSL_ERROR_NO_CYPHER_OVERLAP", + "SSL_ERROR_PROTOCOL_VERSION_ALERT", + "SSL_ERROR_SSL_DISABLED", + "SSL_ERROR_UNSUPPORTED_VERSION", + ].some(substring => { + return substring == errorCode; + }); + + if (hasPrefStyleError) { + RPMAddMessageListener("HasChangedCertPrefs", msg => { + if (msg.data.hasChangedCertPrefs) { + // Configuration overrides might have caused this; offer to reset. + showPrefChangeContainer(); + } + }); + RPMSendAsyncMessage("GetChangedCertPrefs"); + } + } + } + + if (err == "sslv3Used") { + document.getElementById("advancedButton").style.display = "none"; + } + + if (err == "inadequateSecurityError" || err == "blockedByPolicy") { + // Remove the "Try again" button from pages that don't need it. + // For HTTP/2 inadequate security or pages blocked by policy, trying + // again won't help. + document.getElementById("netErrorButtonContainer").style.display = "none"; + + var container = document.getElementById("errorLongDesc"); + for (var span of container.querySelectorAll("span.hostname")) { + span.textContent = HOST_NAME; + } + } +} + +function setupBlockingReportingUI() { + let checkbox = document.getElementById("automaticallyReportBlockingInFuture"); + + let reportingAutomatic = RPMGetBoolPref( + "security.xfocsp.errorReporting.automatic" + ); + checkbox.checked = !!reportingAutomatic; + + checkbox.addEventListener("change", function({ target: { checked } }) { + onSetBlockingReportAutomatic(checked); + }); + + let reportingEnabled = RPMGetBoolPref( + "security.xfocsp.errorReporting.enabled" + ); + + if (!reportingEnabled) { + return; + } + + showBlockingErrorReporting(); + + if (reportingAutomatic) { + reportBlockingError(); + } +} + +function reportBlockingError() { + // We only report if we are in a frame. + if (window === window.top) { + return; + } + + let err = getErrorCode(); + // Ensure we only deal with XFO and CSP here. + if (!["xfoBlocked", "cspBlocked"].includes(err)) { + return; + } + + let xfo_header = RPMGetHttpResponseHeader("X-Frame-Options"); + let csp_header = RPMGetHttpResponseHeader("Content-Security-Policy"); + + // Extract the 'CSP: frame-ancestors' from the CSP header. + let reg = /(?:^|\s)frame-ancestors\s([^;]*)[$]*/i; + let match = reg.exec(csp_header); + csp_header = match ? match[1] : ""; + + // If it's the csp error page without the CSP: frame-ancestors, this means + // this error page is not triggered by CSP: frame-ancestors. So, we bail out + // early. + if (err === "cspBlocked" && !csp_header) { + return; + } + + let xfoAndCspInfo = { + error_type: err === "xfoBlocked" ? "xfo" : "csp", + xfo_header, + csp_header, + }; + + RPMSendAsyncMessage("ReportBlockingError", { + scheme: document.location.protocol, + host: document.location.host, + port: parseInt(document.location.port) || -1, + path: document.location.pathname, + xfoAndCspInfo, + }); +} + +function onSetBlockingReportAutomatic(checked) { + RPMSetBoolPref("security.xfocsp.errorReporting.automatic", checked); + + // If we're enabling reports, send a report for this failure. + if (checked) { + reportBlockingError(); + } +} + +async function setNetErrorMessageFromCode() { + let hostString = HOST_NAME; + + let port = document.location.port; + if (port && port != 443) { + hostString += ":" + port; + } + + let securityInfo; + try { + securityInfo = document.getNetErrorInfo(); + } catch (ex) { + // We don't have a securityInfo when this is for example a DNS error. + return; + } + + let desc = document.getElementById("errorShortDescText"); + let errorCodeStr = securityInfo.errorCodeString || ""; + let errorCodeStrId = errorCodeStr + .split("_") + .join("-") + .toLowerCase(); + let errorCodeMsg = ""; + + if (KNOWN_ERROR_MESSAGE_IDS.has(errorCodeStrId)) { + [errorCodeMsg] = await document.l10n.formatValues([errorCodeStrId]); + } + + if (!errorCodeMsg) { + console.warn("This error page has no error code in its security info"); + document.l10n.setAttributes(desc, "ssl-connection-error", { + errorMessage: errorCodeStr, + hostname: hostString, + }); + return; + } + + document.l10n.setAttributes(desc, "ssl-connection-error", { + errorMessage: errorCodeMsg, + hostname: hostString, + }); + + let desc2 = document.getElementById("errorShortDescText2"); + document.l10n.setAttributes(desc2, "cert-error-code-prefix", { + error: errorCodeStr, + }); +} + +// This function centers the error container after its content updates. +// It is currently duplicated in NetErrorChild.jsm to avoid having to do +// async communication to the page that would result in flicker. +// TODO(johannh): Get rid of this duplication. +function updateContainerPosition() { + let textContainer = document.getElementById("text-container"); + // Using the vh CSS property our margin adapts nicely to window size changes. + // Unfortunately, this doesn't work correctly in iframes, which is why we need + // to manually compute the height there. + if (window.parent == window) { + textContainer.style.marginTop = `calc(50vh - ${textContainer.clientHeight / + 2}px)`; + } else { + let offset = + document.documentElement.clientHeight / 2 - + textContainer.clientHeight / 2; + if (offset > 0) { + textContainer.style.marginTop = `${offset}px`; + } + } +} + +function initPageCaptivePortal() { + document.body.className = "captiveportal"; + document + .getElementById("openPortalLoginPageButton") + .addEventListener("click", () => { + RPMSendAsyncMessage("Browser:OpenCaptivePortalPage"); + }); + + addAutofocus("#openPortalLoginPageButton"); + setupAdvancedButton(); + + // When the portal is freed, an event is sent by the parent process + // that we can pick up and attempt to reload the original page. + RPMAddMessageListener("AboutNetErrorCaptivePortalFreed", () => { + document.location.reload(); + }); +} + +function initPageCertError() { + document.body.classList.add("certerror"); + for (let host of document.querySelectorAll(".hostname")) { + host.textContent = HOST_NAME; + } + + addAutofocus("#returnButton"); + setupAdvancedButton(); + document.getElementById("learnMoreContainer").style.display = "block"; + + let hideAddExceptionButton = RPMGetBoolPref( + "security.certerror.hideAddException", + false + ); + if (hideAddExceptionButton) { + document.querySelector(".exceptionDialogButtonContainer").hidden = true; + } + + let els = document.querySelectorAll("[data-telemetry-id]"); + for (let el of els) { + el.addEventListener("click", recordClickTelemetry); + } + + let failedCertInfo = document.getFailedCertSecurityInfo(); + // Truncate the error code to avoid going over the allowed + // string size limit for telemetry events. + let errorCode = failedCertInfo.errorCodeString.substring(0, 40); + RPMRecordTelemetryEvent( + "security.ui.certerror", + "load", + "aboutcerterror", + errorCode, + { + has_sts: (getCSSClass() == "badStsCert").toString(), + is_frame: (window.parent != window).toString(), + } + ); + + setCertErrorDetails(); +} + +function recordClickTelemetry(e) { + let target = e.originalTarget; + let telemetryId = target.dataset.telemetryId; + let failedCertInfo = document.getFailedCertSecurityInfo(); + // Truncate the error code to avoid going over the allowed + // string size limit for telemetry events. + let errorCode = failedCertInfo.errorCodeString.substring(0, 40); + RPMRecordTelemetryEvent( + "security.ui.certerror", + "click", + telemetryId, + errorCode, + { + has_sts: (getCSSClass() == "badStsCert").toString(), + is_frame: (window.parent != window).toString(), + } + ); +} + +function initCertErrorPageActions() { + document + .getElementById("returnButton") + .addEventListener("click", onReturnButtonClick); + document + .getElementById("advancedPanelReturnButton") + .addEventListener("click", onReturnButtonClick); + document + .getElementById("copyToClipboardTop") + .addEventListener("click", copyPEMToClipboard); + document + .getElementById("copyToClipboardBottom") + .addEventListener("click", copyPEMToClipboard); + document + .getElementById("exceptionDialogButton") + .addEventListener("click", addCertException); +} + +function addCertException() { + const isPermanent = + !RPMIsWindowPrivate() && + RPMGetBoolPref("security.certerrors.permanentOverride"); + document.addCertException(!isPermanent).then( + () => { + location.reload(); + }, + err => {} + ); +} + +function onReturnButtonClick(e) { + RPMSendAsyncMessage("Browser:SSLErrorGoBack"); +} + +async function copyPEMToClipboard(e) { + let details = await getFailedCertificatesAsPEMString(); + navigator.clipboard.writeText(details); +} + +async function getFailedCertificatesAsPEMString() { + let location = document.location.href; + let failedCertInfo = document.getFailedCertSecurityInfo(); + let errorMessage = failedCertInfo.errorMessage; + let hasHSTS = failedCertInfo.hasHSTS.toString(); + let hasHPKP = failedCertInfo.hasHPKP.toString(); + let [ + hstsLabel, + hpkpLabel, + failedChainLabel, + ] = await document.l10n.formatValues([ + { id: "cert-error-details-hsts-label", args: { hasHSTS } }, + { id: "cert-error-details-key-pinning-label", args: { hasHPKP } }, + { id: "cert-error-details-cert-chain-label" }, + ]); + + let certStrings = failedCertInfo.certChainStrings; + let failedChainCertificates = ""; + for (let der64 of certStrings) { + let wrapped = der64.replace(/(\S{64}(?!$))/g, "$1\r\n"); + failedChainCertificates += + "-----BEGIN CERTIFICATE-----\r\n" + + wrapped + + "\r\n-----END CERTIFICATE-----\r\n"; + } + + let details = + location + + "\r\n\r\n" + + errorMessage + + "\r\n\r\n" + + hstsLabel + + "\r\n" + + hpkpLabel + + "\r\n\r\n" + + failedChainLabel + + "\r\n\r\n" + + failedChainCertificates; + return details; +} + +function setCertErrorDetails(event) { + // 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). + let failedCertInfo = document.getFailedCertSecurityInfo(); + let mitmPrimingEnabled = RPMGetBoolPref( + "security.certerrors.mitm.priming.enabled" + ); + if ( + mitmPrimingEnabled && + failedCertInfo.errorCodeString == "SEC_ERROR_UNKNOWN_ISSUER" && + // Only do this check for top-level failures. + window.parent == window + ) { + RPMSendAsyncMessage("Browser:PrimeMitm"); + } + + let learnMoreLink = document.getElementById("learnMoreLink"); + let baseURL = RPMGetFormatURLPref("app.support.baseURL"); + learnMoreLink.setAttribute("href", baseURL + "connection-not-secure"); + let errWhatToDo = document.getElementById( + "es_nssBadCert_" + failedCertInfo.errorCodeString + ); + let es = document.getElementById("errorWhatToDoText"); + let errWhatToDoTitle = document.getElementById("edd_nssBadCert"); + let est = document.getElementById("errorWhatToDoTitleText"); + let error = getErrorCode(); + + if (error == "sslv3Used") { + learnMoreLink.setAttribute("href", baseURL + "sslv3-error-messages"); + } + + if (error == "nssFailure2") { + let shortDesc = document.getElementById("errorShortDescText").textContent; + // nssFailure2 also gets us other non-overrideable errors. Choose + // a "learn more" link based on description: + if (shortDesc.includes("MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE")) { + learnMoreLink.setAttribute( + "href", + baseURL + "certificate-pinning-reports" + ); + } + } + + // This is set to true later if the user's system clock is at fault for this error. + let clockSkew = false; + document.body.setAttribute("code", failedCertInfo.errorCodeString); + + let titleElement = document.querySelector(".title-text"); + let desc; + switch (failedCertInfo.errorCodeString) { + case "SSL_ERROR_BAD_CERT_DOMAIN": + case "SEC_ERROR_OCSP_INVALID_SIGNING_CERT": + case "SEC_ERROR_UNKNOWN_ISSUER": + if (es) { + // eslint-disable-next-line no-unsanitized/property + es.innerHTML = errWhatToDo.innerHTML; + } + if (est) { + // eslint-disable-next-line no-unsanitized/property + est.innerHTML = errWhatToDoTitle.innerHTML; + } + updateContainerPosition(); + break; + + // This error code currently only exists for the Symantec distrust + // in Firefox 63, so we add copy explaining that to the user. + // In case of future distrusts of that scale we might need to add + // additional parameters that allow us to identify the affected party + // without replicating the complex logic from certverifier code. + case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED": + desc = document.getElementById("errorShortDescText2"); + document.l10n.setAttributes( + desc, + "cert-error-symantec-distrust-description", + { + HOST_NAME, + } + ); + + let adminDesc = document.createElement("p"); + document.l10n.setAttributes( + adminDesc, + "cert-error-symantec-distrust-admin" + ); + + learnMoreLink.href = baseURL + "symantec-warning"; + updateContainerPosition(); + break; + + case "MOZILLA_PKIX_ERROR_MITM_DETECTED": + let autoEnabledEnterpriseRoots = RPMGetBoolPref( + "security.enterprise_roots.auto-enabled", + false + ); + if (mitmPrimingEnabled && autoEnabledEnterpriseRoots) { + RPMSendAsyncMessage("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 names = document.querySelectorAll(".mitm-name"); + for (let span of names) { + span.textContent = failedCertInfo.issuerCommonName; + } + + learnMoreLink.href = baseURL + "security-error"; + + document.l10n.setAttributes(titleElement, "certerror-mitm-title"); + desc = document.getElementById("ed_mitm"); + // eslint-disable-next-line no-unsanitized/property + document.getElementById("errorShortDescText").innerHTML = desc.innerHTML; + + // eslint-disable-next-line no-unsanitized/property + es.innerHTML = errWhatToDo.innerHTML; + // eslint-disable-next-line no-unsanitized/property + est.innerHTML = errWhatToDoTitle.innerHTML; + + updateContainerPosition(); + break; + + case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT": + learnMoreLink.href = baseURL + "security-error"; + break; + + // In case the certificate expired we make sure the system clock + // matches the remote-settings service (blocklist via Kinto) ping time + // and is not before the build date. + case "SEC_ERROR_EXPIRED_CERTIFICATE": + case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE": + case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_CERTIFICATE": + case "MOZILLA_PKIX_ERROR_NOT_YET_VALID_ISSUER_CERTIFICATE": + learnMoreLink.href = baseURL + "time-errors"; + // We check against the remote-settings server time first if available, because that allows us + // to give the user an approximation of what the correct time is. + let difference = RPMGetIntPref("services.settings.clock_skew_seconds", 0); + let lastFetched = + RPMGetIntPref("services.settings.last_update_seconds", 0) * 1000; + + let now = Date.now(); + let certRange = { + notBefore: failedCertInfo.certValidityRangeNotBefore, + notAfter: failedCertInfo.certValidityRangeNotAfter, + }; + let approximateDate = now - difference * 1000; + // If the difference is more than a day, we last fetched the date in the last 5 days, + // and adjusting the date per the interval would make the cert valid, warn the user: + if ( + Math.abs(difference) > 60 * 60 * 24 && + now - lastFetched <= 60 * 60 * 24 * 5 * 1000 && + certRange.notBefore < approximateDate && + certRange.notAfter > approximateDate + ) { + clockSkew = true; + // If there is no clock skew with Kinto servers, check against the build date. + // (The Kinto ping could have happened when the time was still right, or not at all) + } else { + let appBuildID = RPMGetAppBuildID(); + let year = parseInt(appBuildID.substr(0, 4), 10); + let month = parseInt(appBuildID.substr(4, 2), 10) - 1; + let day = parseInt(appBuildID.substr(6, 2), 10); + + let buildDate = new Date(year, month, day); + let systemDate = new Date(); + + // We don't check the notBefore of the cert with the build date, + // as it is of course almost certain that it is now later than the build date, + // so we shouldn't exclude the possibility that the cert has become valid + // since the build date. + if ( + buildDate > systemDate && + new Date(certRange.notAfter) > buildDate + ) { + clockSkew = true; + } + } + + let systemDate = formatter.format(new Date()); + document.getElementById( + "wrongSystemTime_systemDate1" + ).textContent = systemDate; + if (clockSkew) { + document.body.classList.add("illustrated", "clockSkewError"); + document.l10n.setAttributes(titleElement, "clockSkewError-title"); + let clockErrDesc = document.getElementById("ed_clockSkewError"); + desc = document.getElementById("errorShortDescText"); + document.getElementById("errorShortDesc").style.display = "block"; + if (desc) { + // eslint-disable-next-line no-unsanitized/property + desc.innerHTML = clockErrDesc.innerHTML; + } + let errorPageContainer = document.getElementById("errorPageContainer"); + let textContainer = document.getElementById("text-container"); + errorPageContainer.style.backgroundPosition = `left top calc(50vh - ${textContainer.clientHeight / + 2}px)`; + } else { + let targetElems = document.querySelectorAll( + "#wrongSystemTime_systemDate2" + ); + for (let elem of targetElems) { + elem.textContent = systemDate; + } + + let errDesc = document.getElementById( + "ed_nssBadCert_SEC_ERROR_EXPIRED_CERTIFICATE" + ); + let sd = document.getElementById("errorShortDescText"); + // eslint-disable-next-line no-unsanitized/property + sd.innerHTML = errDesc.innerHTML; + + let span = sd.querySelector(".hostname"); + span.textContent = HOST_NAME; + + // The secondary description mentions expired certificates explicitly + // and should only be shown if the certificate has actually expired + // instead of being not yet valid. + if (failedCertInfo.errorCodeString == "SEC_ERROR_EXPIRED_CERTIFICATE") { + let cssClass = getCSSClass(); + let stsSuffix = cssClass == "badStsCert" ? "_sts" : ""; + let errDesc2 = document.getElementById( + `ed2_nssBadCert_SEC_ERROR_EXPIRED_CERTIFICATE${stsSuffix}` + ); + let sd2 = document.getElementById("errorShortDescText2"); + // eslint-disable-next-line no-unsanitized/property + sd2.innerHTML = errDesc2.innerHTML; + if ( + Math.abs(difference) <= 60 * 60 * 24 && + now - lastFetched <= 60 * 60 * 24 * 5 * 1000 + ) { + errWhatToDo = document.getElementById( + "es_nssBadCert_SSL_ERROR_BAD_CERT_DOMAIN" + ); + } + } + + if (es) { + // eslint-disable-next-line no-unsanitized/property + es.innerHTML = errWhatToDo.innerHTML; + } + if (est) { + // eslint-disable-next-line no-unsanitized/property + est.textContent = errWhatToDoTitle.textContent; + est.style.fontWeight = "bold"; + } + updateContainerPosition(); + } + break; + } + + // Add slightly more alarming UI unless there are indicators that + // show that the error is harmless or can not be skipped anyway. + let cssClass = getCSSClass(); + // Don't alarm users when they can't continue to the website anyway... + if ( + cssClass != "badStsCert" && + // Errors in iframes can't be skipped either... + window.parent == window && + // Also don't bother if it's just the user's clock being off... + !clockSkew && + // Symantec distrust is likely harmless as well. + failedCertInfo.errorCodeString != + "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED" + ) { + document.body.classList.add("caution"); + } +} + +// The optional argument is only here for testing purposes. +async function setTechnicalDetailsOnCertError( + failedCertInfo = document.getFailedCertSecurityInfo() +) { + let technicalInfo = document.getElementById("badCertTechnicalInfo"); + + function setL10NLabel(l10nId, args = {}, attrs = {}, rewrite = true) { + let elem = document.createElement("label"); + if (rewrite) { + technicalInfo.textContent = ""; + } + technicalInfo.appendChild(elem); + + let newLines = document.createTextNode("\n \n"); + technicalInfo.appendChild(newLines); + + if (attrs) { + let link = document.createElement("a"); + for (let attr of Object.keys(attrs)) { + link.setAttribute(attr, attrs[attr]); + } + elem.appendChild(link); + } + + if (args) { + document.l10n.setAttributes(elem, l10nId, args); + } else { + document.l10n.setAttributes(elem, l10nId); + } + } + + let cssClass = getCSSClass(); + let error = getErrorCode(); + + let hostString = HOST_NAME; + let port = document.location.port; + if (port && port != 443) { + hostString += ":" + port; + } + + let l10nId; + let args = { + hostname: hostString, + }; + if (failedCertInfo.isUntrusted) { + switch (failedCertInfo.errorCodeString) { + case "MOZILLA_PKIX_ERROR_MITM_DETECTED": + setL10NLabel("cert-error-mitm-intro"); + setL10NLabel("cert-error-mitm-mozilla", {}, {}, false); + setL10NLabel("cert-error-mitm-connection", {}, {}, false); + break; + case "SEC_ERROR_UNKNOWN_ISSUER": + setL10NLabel("cert-error-trust-unknown-issuer-intro"); + setL10NLabel("cert-error-trust-unknown-issuer", args, {}, false); + break; + case "SEC_ERROR_CA_CERT_INVALID": + setL10NLabel("cert-error-intro", args); + setL10NLabel("cert-error-trust-cert-invalid", {}, {}, false); + break; + case "SEC_ERROR_UNTRUSTED_ISSUER": + setL10NLabel("cert-error-intro", args); + setL10NLabel("cert-error-trust-untrusted-issuer", {}, {}, false); + break; + case "SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED": + setL10NLabel("cert-error-intro", args); + setL10NLabel( + "cert-error-trust-signature-algorithm-disabled", + {}, + {}, + false + ); + break; + case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE": + setL10NLabel("cert-error-intro", args); + setL10NLabel("cert-error-trust-expired-issuer", {}, {}, false); + break; + case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT": + setL10NLabel("cert-error-intro", args); + setL10NLabel("cert-error-trust-self-signed", {}, {}, false); + break; + case "MOZILLA_PKIX_ERROR_ADDITIONAL_POLICY_CONSTRAINT_FAILED": + setL10NLabel("cert-error-intro", args); + setL10NLabel("cert-error-trust-symantec", {}, {}, false); + break; + default: + setL10NLabel("cert-error-intro", args); + setL10NLabel("cert-error-untrusted-default", {}, {}, false); + } + } else if (failedCertInfo.isDomainMismatch) { + let serverCertBase64 = failedCertInfo.certChainStrings[0]; + let parsed = await parse(pemToDER(serverCertBase64)); + let subjectAltNamesExtension = parsed.ext.san; + let subjectAltNames = []; + if (subjectAltNamesExtension) { + for (let name of subjectAltNamesExtension.altNames) { + if (name[0] == "DNS Name" && name[1].length) { + subjectAltNames.push(name[1]); + } + } + } + let numSubjectAltNames = subjectAltNames.length; + if (numSubjectAltNames != 0) { + if (numSubjectAltNames == 1) { + args["alt-name"] = subjectAltNames[0]; + + // Let's check if we want to make this a link. + let okHost = subjectAltNames[0]; + let href = ""; + let thisHost = HOST_NAME; + let proto = document.location.protocol + "//"; + // If okHost is a wildcard domain ("*.example.com") let's + // use "www" instead. "*.example.com" isn't going to + // get anyone anywhere useful. bug 432491 + okHost = okHost.replace(/^\*\./, "www."); + /* case #1: + * example.com uses an invalid security certificate. + * + * The certificate is only valid for www.example.com + * + * Make sure to include the "." ahead of thisHost so that + * a MitM attack on paypal.com doesn't hyperlink to "notpaypal.com" + * + * We'd normally just use a RegExp here except that we lack a + * library function to escape them properly (bug 248062), and + * domain names are famous for having '.' characters in them, + * which would allow spurious and possibly hostile matches. + */ + + if (okHost.endsWith("." + thisHost)) { + href = proto + okHost; + } + /* case #2: + * browser.garage.maemo.org uses an invalid security certificate. + * + * The certificate is only valid for garage.maemo.org + */ + if (thisHost.endsWith("." + okHost)) { + href = proto + okHost; + } + + // If we set a link, meaning there's something helpful for + // the user here, expand the section by default + if (href && cssClass != "expertBadCert") { + document.getElementById("badCertAdvancedPanel").style.display = + "block"; + if (error == "nssBadCert") { + // Toggling the advanced panel must ensure that the debugging + // information panel is hidden as well, since it's opened by the + // error code link in the advanced panel. + let div = document.getElementById( + "certificateErrorDebugInformation" + ); + div.style.display = "none"; + } + } + + // Set the link if we want it. + if (href) { + setL10NLabel("cert-error-domain-mismatch-single", args, { + href, + "data-l10n-name": "domain-mismatch-link", + id: "cert_domain_link", + }); + } else { + setL10NLabel("cert-error-domain-mismatch-single-nolink", args); + } + } else { + let names = subjectAltNames.join(", "); + args["subject-alt-names"] = names; + setL10NLabel("cert-error-domain-mismatch-multiple", args); + } + } else { + setL10NLabel("cert-error-domain-mismatch", { hostname: hostString }); + } + } else if (failedCertInfo.isNotValidAtThisTime) { + let notBefore = failedCertInfo.validNotBefore; + let notAfter = failedCertInfo.validNotAfter; + args = { + hostname: hostString, + }; + if (notBefore && Date.now() < notAfter) { + let notBeforeLocalTime = formatter.format(new Date(notBefore)); + l10nId = "cert-error-not-yet-valid-now"; + args["not-before-local-time"] = notBeforeLocalTime; + } else { + let notAfterLocalTime = formatter.format(new Date(notAfter)); + l10nId = "cert-error-expired-now"; + args["not-after-local-time"] = notAfterLocalTime; + } + setL10NLabel(l10nId, args); + } + + setL10NLabel( + "cert-error-code-prefix-link", + { error: failedCertInfo.errorCodeString }, + { + title: failedCertInfo.errorCodeString, + id: "errorCode", + "data-l10n-name": "error-code-link", + "data-telemetry-id": "error_code_link", + }, + false + ); + let errorCodeLink = document.getElementById("errorCode"); + if (errorCodeLink) { + // We're attaching the event listener to the parent element and not on + // the errorCodeLink itself because event listeners cannot be attached + // to fluent DOM overlays. + technicalInfo.addEventListener("click", handleErrorCodeClick); + } + + let div = document.getElementById("certificateErrorText"); + div.textContent = await getFailedCertificatesAsPEMString(); +} + +function handleErrorCodeClick(event) { + if (event.target.id !== "errorCode") { + return; + } + + let debugInfo = document.getElementById("certificateErrorDebugInformation"); + debugInfo.style.display = "block"; + debugInfo.scrollIntoView({ block: "start", behavior: "smooth" }); + recordClickTelemetry(event); +} + +/* Only do autofocus if we're the toplevel frame; otherwise we + don't want to call attention to ourselves! The key part is + that autofocus happens on insertion into the tree, so we + can remove the button, add @autofocus, and reinsert the + button. +*/ +function addAutofocus(selector, position = "afterbegin") { + if (window.top == window) { + var button = document.querySelector(selector); + var parent = button.parentNode; + button.remove(); + button.setAttribute("autofocus", "true"); + parent.insertAdjacentElement(position, button); + } +} + +for (let button of document.querySelectorAll(".try-again")) { + button.addEventListener("click", function() { + retryThis(this); + }); +} + +window.addEventListener("DOMContentLoaded", () => { + // Expose this so tests can call it. + window.setTechnicalDetailsOnCertError = setTechnicalDetailsOnCertError; + + initPage(); + // Dispatch this event so tests can detect that we finished loading the error page. + let event = new CustomEvent("AboutNetErrorLoad", { bubbles: true }); + document.dispatchEvent(event); +}); |