diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 657a8d719c82..bb8e30cf5321 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -2654,6 +2654,7 @@ pref("browser.toolbars.bookmarks.showOtherBookmarks", true); // Felt Privacy pref to control simplified private browsing UI pref("browser.privatebrowsing.felt-privacy-v1", false); +pref("security.certerrors.felt-privacy-v1", false); // Prefs to control the Firefox Account toolbar menu. // This pref will surface existing Firefox Account information diff --git a/browser/base/content/test/general/browser_addCertException.js b/browser/base/content/test/general/browser_addCertException.js index d3d1ac1ce4d4..99766b305082 100644 --- a/browser/base/content/test/general/browser_addCertException.js +++ b/browser/base/content/test/general/browser_addCertException.js @@ -10,68 +10,76 @@ // dialog, using that to add an exception, and finally successfully visiting // the site, including showing the right identity box and control center icons. add_task(async function () { - await BrowserTestUtils.openNewForegroundTab(gBrowser); - await loadBadCertPage("https://expired.example.com"); + for (let feltPrivacyEnabled of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["security.certerrors.felt-privacy-v1", feltPrivacyEnabled]], + }); - let { gIdentityHandler } = gBrowser.ownerGlobal; - let promisePanelOpen = BrowserTestUtils.waitForEvent( - gBrowser.ownerGlobal, - "popupshown", - true, - event => event.target == gIdentityHandler._identityPopup - ); - gIdentityHandler._identityIconBox.click(); - await promisePanelOpen; + await BrowserTestUtils.openNewForegroundTab(gBrowser); + await loadBadCertPage("https://expired.example.com", feltPrivacyEnabled); - let promiseViewShown = BrowserTestUtils.waitForEvent( - gIdentityHandler._identityPopup, - "ViewShown" - ); - document.getElementById("identity-popup-security-button").click(); - await promiseViewShown; + let { gIdentityHandler } = gBrowser.ownerGlobal; + let promisePanelOpen = BrowserTestUtils.waitForEvent( + gBrowser.ownerGlobal, + "popupshown", + true, + event => event.target == gIdentityHandler._identityPopup + ); + gIdentityHandler._identityIconBox.click(); + await promisePanelOpen; - is_element_visible( - document.getElementById("identity-icon"), - "Should see identity icon" - ); - let identityIconImage = gBrowser.ownerGlobal - .getComputedStyle(document.getElementById("identity-icon")) - .getPropertyValue("list-style-image"); - let securityViewBG = gBrowser.ownerGlobal - .getComputedStyle( - document - .getElementById("identity-popup-securityView") - .getElementsByClassName("identity-popup-security-connection")[0] - ) - .getPropertyValue("list-style-image"); - let securityContentBG = gBrowser.ownerGlobal - .getComputedStyle( - document - .getElementById("identity-popup-mainView") - .getElementsByClassName("identity-popup-security-connection")[0] - ) - .getPropertyValue("list-style-image"); - is( - identityIconImage, - 'url("chrome://global/skin/icons/security-warning.svg")', - "Using expected icon image in the identity block" - ); - is( - securityViewBG, - 'url("chrome://global/skin/icons/security-warning.svg")', - "Using expected icon image in the Control Center main view" - ); - is( - securityContentBG, - 'url("chrome://global/skin/icons/security-warning.svg")', - "Using expected icon image in the Control Center subview" - ); + let promiseViewShown = BrowserTestUtils.waitForEvent( + gIdentityHandler._identityPopup, + "ViewShown" + ); + document.getElementById("identity-popup-security-button").click(); + await promiseViewShown; - gIdentityHandler._identityPopup.hidePopup(); + is_element_visible( + document.getElementById("identity-icon"), + "Should see identity icon" + ); + let identityIconImage = gBrowser.ownerGlobal + .getComputedStyle(document.getElementById("identity-icon")) + .getPropertyValue("list-style-image"); + let securityViewBG = gBrowser.ownerGlobal + .getComputedStyle( + document + .getElementById("identity-popup-securityView") + .getElementsByClassName("identity-popup-security-connection")[0] + ) + .getPropertyValue("list-style-image"); + let securityContentBG = gBrowser.ownerGlobal + .getComputedStyle( + document + .getElementById("identity-popup-mainView") + .getElementsByClassName("identity-popup-security-connection")[0] + ) + .getPropertyValue("list-style-image"); + is( + identityIconImage, + 'url("chrome://global/skin/icons/security-warning.svg")', + "Using expected icon image in the identity block" + ); + is( + securityViewBG, + 'url("chrome://global/skin/icons/security-warning.svg")', + "Using expected icon image in the Control Center main view" + ); + is( + securityContentBG, + 'url("chrome://global/skin/icons/security-warning.svg")', + "Using expected icon image in the Control Center subview" + ); - let certOverrideService = Cc[ - "@mozilla.org/security/certoverride;1" - ].getService(Ci.nsICertOverrideService); - certOverrideService.clearValidityOverride("expired.example.com", -1, {}); - BrowserTestUtils.removeTab(gBrowser.selectedTab); + gIdentityHandler._identityPopup.hidePopup(); + + let certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + certOverrideService.clearValidityOverride("expired.example.com", -1, {}); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + await SpecialPowers.popPrefEnv(); + } }); diff --git a/browser/base/content/test/general/browser_bug431826.js b/browser/base/content/test/general/browser_bug431826.js index f90de6c53084..507d365490f4 100644 --- a/browser/base/content/test/general/browser_bug431826.js +++ b/browser/base/content/test/general/browser_bug431826.js @@ -1,59 +1,89 @@ -function remote(task) { - return SpecialPowers.spawn(gBrowser.selectedBrowser, [], task); -} - add_task(async function () { - gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + for (let feltPrivacyEnabled of [false, true]) { + await SpecialPowers.pushPrefEnv({ + set: [["security.certerrors.felt-privacy-v1", feltPrivacyEnabled]], + }); - let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); - BrowserTestUtils.startLoadingURIString( - gBrowser, - "https://nocert.example.com/" - ); - await promise; + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); - await remote(() => { - // Confirm that we are displaying the contributed error page, not the default - let uri = content.document.documentURI; - Assert.ok( - uri.startsWith("about:certerror"), - "Broken page should go to about:certerror, not about:neterror" + let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser, + "https://nocert.example.com/" ); - }); + await promise; - await remote(() => { - let div = content.document.getElementById("badCertAdvancedPanel"); - // Confirm that the expert section is collapsed - Assert.ok(div, "Advanced content div should exist"); - Assert.equal( - div.ownerGlobal.getComputedStyle(div).display, - "none", - "Advanced content should not be visible by default" - ); - }); + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + // Confirm that we are displaying the contributed error page, not the default + let uri = content.document.documentURI; + Assert.ok( + uri.startsWith("about:certerror"), + "Broken page should go to about:certerror, not about:neterror" + ); + }); - // Tweak the expert mode pref - Services.prefs.setBoolPref("browser.xul.error_pages.expert_bad_cert", true); + if (feltPrivacyEnabled) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + let netErrorCard = + content.document.querySelector("net-error-card").wrappedJSObject; + await netErrorCard.getUpdateComplete(); + Assert.ok( + !netErrorCard.advancedShowing, + "Advanced showing attribute should be true" + ); + Assert.ok(!netErrorCard.advancedContainer); + }); + } else { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + let div = content.document.getElementById("badCertAdvancedPanel"); + // Confirm that the expert section is collapsed + Assert.ok(div, "Advanced content div should exist"); + Assert.equal( + div.ownerGlobal.getComputedStyle(div).display, + "none", + "Advanced content should not be visible by default" + ); + }); + } - promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); - gBrowser.reload(); - await promise; + // Tweak the expert mode pref + Services.prefs.setBoolPref("browser.xul.error_pages.expert_bad_cert", true); - await remote(() => { - let div = content.document.getElementById("badCertAdvancedPanel"); - Assert.ok(div, "Advanced content div should exist"); - Assert.equal( - div.ownerGlobal.getComputedStyle(div).display, - "block", - "Advanced content should be visible by default" - ); - }); + promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + gBrowser.reload(); + await promise; - // Clean up - gBrowser.removeCurrentTab(); - if ( - Services.prefs.prefHasUserValue("browser.xul.error_pages.expert_bad_cert") - ) { - Services.prefs.clearUserPref("browser.xul.error_pages.expert_bad_cert"); + if (feltPrivacyEnabled) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + let netErrorCard = + content.document.querySelector("net-error-card").wrappedJSObject; + await netErrorCard.getUpdateComplete(); + Assert.ok( + netErrorCard.advancedShowing, + "Advanced showing attribute should be true" + ); + Assert.ok(ContentTaskUtils.isVisible(netErrorCard.advancedContainer)); + }); + } else { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + let div = content.document.getElementById("badCertAdvancedPanel"); + Assert.ok(div, "Advanced content div should exist"); + Assert.equal( + div.ownerGlobal.getComputedStyle(div).display, + "block", + "Advanced content should be visible by default" + ); + }); + } + + // Clean up + gBrowser.removeCurrentTab(); + if ( + Services.prefs.prefHasUserValue("browser.xul.error_pages.expert_bad_cert") + ) { + Services.prefs.clearUserPref("browser.xul.error_pages.expert_bad_cert"); + } + + await SpecialPowers.popPrefEnv(); } }); diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js index 7b13f4c30df7..a91355a4034f 100644 --- a/browser/base/content/test/general/head.js +++ b/browser/base/content/test/general/head.js @@ -289,13 +289,31 @@ function promiseOnBookmarkItemAdded(aExpectedURI) { }); } -async function loadBadCertPage(url) { +async function loadBadCertPage(url, feltPrivacy = false) { let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url); await loaded; - await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { - content.document.getElementById("exceptionDialogButton").click(); - }); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [feltPrivacy], + async isFeltPrivacy => { + if (isFeltPrivacy) { + let netErrorCard = + content.document.querySelector("net-error-card").wrappedJSObject; + await netErrorCard.getUpdateComplete(); + netErrorCard.advancedButton.click(); + await ContentTaskUtils.waitForCondition(() => { + return ( + netErrorCard.exceptionButton && + !netErrorCard.exceptionButton.disabled + ); + }, "Waiting for exception button"); + netErrorCard.exceptionButton.click(); + } else { + content.document.getElementById("exceptionDialogButton").click(); + } + } + ); await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); } diff --git a/docshell/test/browser/browser_badCertDomainFixup.js b/docshell/test/browser/browser_badCertDomainFixup.js index 1bc8cfa47717..8cec9fc7d24a 100644 --- a/docshell/test/browser/browser_badCertDomainFixup.js +++ b/docshell/test/browser/browser_badCertDomainFixup.js @@ -7,39 +7,70 @@ // with www. when we encounter a SSL_ERROR_BAD_CERT_DOMAIN error. // For example, https://example.com -> https://www.example.com. -async function verifyErrorPage(errorPageURL) { +async function verifyErrorPage(errorPageURL, feltPrivacy = false) { let certErrorLoaded = BrowserTestUtils.waitForErrorPage( gBrowser.selectedBrowser ); BrowserTestUtils.startLoadingURIString(gBrowser, errorPageURL); await certErrorLoaded; - await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { - let ec; - await ContentTaskUtils.waitForCondition(() => { - ec = content.document.getElementById("errorCode"); - return ec.textContent; - }, "Error code has been set inside the advanced button panel"); - is( - ec.textContent, - "SSL_ERROR_BAD_CERT_DOMAIN", - "Correct error code is shown" - ); - }); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [feltPrivacy], + async isFeltPrivacy => { + let ec; + if (isFeltPrivacy) { + let netErrorCard = + content.document.querySelector("net-error-card").wrappedJSObject; + await netErrorCard.getUpdateComplete(); + netErrorCard.advancedButton.click(); + await ContentTaskUtils.waitForCondition(() => { + return (ec = netErrorCard.errorCode); + }, "Error code has been set inside the net-error-card advanced panel"); + + is( + ec.textContent.split(" ").at(-1), + "SSL_ERROR_BAD_CERT_DOMAIN", + "Correct error code is shown" + ); + } else { + await ContentTaskUtils.waitForCondition(() => { + ec = content.document.getElementById("errorCode"); + return ec.textContent; + }, "Error code has been set inside the advanced button panel"); + is( + ec.textContent, + "SSL_ERROR_BAD_CERT_DOMAIN", + "Correct error code is shown" + ); + } + } + ); } // Turn off the pref and ensure that we show the error page as expected. add_task(async function testNoFixupDisabledByPref() { - await SpecialPowers.pushPrefEnv({ - set: [["security.bad_cert_domain_error.url_fix_enabled", false]], - }); - gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + for (let feltPrivacyEnabled of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.bad_cert_domain_error.url_fix_enabled", false], + ["security.certerrors.felt-privacy-v1", feltPrivacyEnabled], + ], + }); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); - await verifyErrorPage("https://badcertdomain.example.com"); - await verifyErrorPage("https://www.badcertdomain2.example.com"); + await verifyErrorPage( + "https://badcertdomain.example.com", + feltPrivacyEnabled + ); + await verifyErrorPage( + "https://www.badcertdomain2.example.com", + feltPrivacyEnabled + ); - BrowserTestUtils.removeTab(gBrowser.selectedTab); - await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + await SpecialPowers.popPrefEnv(); + } }); // Test that "www." is prefixed to a https url when we encounter a bad cert domain @@ -63,22 +94,35 @@ add_task(async function testAddPrefixForBadCertDomain() { // Test that we don't prefix "www." to a https url when we encounter a bad cert domain // error under certain conditions. add_task(async function testNoFixupCases() { - gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + for (let feltPrivacyEnabled of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["security.certerrors.felt-privacy-v1", feltPrivacyEnabled]], + }); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); - // Test for when "www." form is not present in the certificate. - await verifyErrorPage("https://mismatch.badcertdomain.example.com"); + // Test for when "www." form is not present in the certificate. + await verifyErrorPage( + "https://mismatch.badcertdomain.example.com", + feltPrivacyEnabled + ); - // Test that urls with IP addresses are not fixed. - await SpecialPowers.pushPrefEnv({ - set: [["network.proxy.allow_hijacking_localhost", true]], - }); - await verifyErrorPage("https://127.0.0.3:433"); - await SpecialPowers.popPrefEnv(); + // Test that urls with IP addresses are not fixed. + await SpecialPowers.pushPrefEnv({ + set: [["network.proxy.allow_hijacking_localhost", true]], + }); + await verifyErrorPage("https://127.0.0.3:433", feltPrivacyEnabled); + await SpecialPowers.popPrefEnv(); - // Test that urls with ports are not fixed. - await verifyErrorPage("https://badcertdomain.example.com:82"); + // Test that urls with ports are not fixed. + await verifyErrorPage( + "https://badcertdomain.example.com:82", + feltPrivacyEnabled + ); - BrowserTestUtils.removeTab(gBrowser.selectedTab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + + await SpecialPowers.popPrefEnv(); + } }); // Test removing "www." prefix if the "www."-less form is included in the diff --git a/toolkit/content/aboutNetError.mjs b/toolkit/content/aboutNetError.mjs index 4c68b84b123b..fcb2f3a1427d 100644 --- a/toolkit/content/aboutNetError.mjs +++ b/toolkit/content/aboutNetError.mjs @@ -5,23 +5,27 @@ /* eslint-env mozilla/remote-page */ /* eslint-disable import/no-unassigned-import */ +import { NetErrorCard } from "chrome://global/content/net-error-card.mjs"; import { - parse, - pemToDER, -} from "chrome://global/content/certviewer/certDecoder.mjs"; + gIsCertError, + gErrorCode, + gHasSts, + searchParams, + getHostName, + getSubjectAltNames, + getFailedCertificatesAsPEMString, + recordSecurityUITelemetry, + getCSSClass, +} from "chrome://global/content/aboutNetErrorHelpers.mjs"; const formatter = new Intl.DateTimeFormat(); const HOST_NAME = getHostName(); -function getHostName() { - try { - return new URL(RPMGetInnerMostURI(document.location.href)).hostname; - } catch (error) { - console.error("Could not parse URL", error); - } - return ""; -} +const FELT_PRIVACY_REFRESH = RPMGetBoolPref( + "security.certerrors.felt-privacy-v1", + false +); // Used to check if we have a specific localized message for an error. const KNOWN_ERROR_TITLE_IDS = new Set([ @@ -66,25 +70,6 @@ const KNOWN_ERROR_TITLE_IDS = new Set([ /* global KNOWN_ERROR_MESSAGE_IDS */ const ERROR_MESSAGES_FTL = "toolkit/neterror/nsserrors.ftl"; -// 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]); - -let gErrorCode = searchParams.get("e"); -let gIsCertError = gErrorCode == "nssBadCert"; -let gHasSts = gIsCertError && getCSSClass() === "badStsCert"; - // If the location of the favicon changes, FAVICON_CERTERRORPAGE_URL and/or // FAVICON_ERRORPAGE_URL in toolkit/components/places/nsFaviconService.idl // should also be updated. @@ -93,10 +78,6 @@ document.getElementById("favicon").href = ? "chrome://global/skin/icons/warning.svg" : "chrome://global/skin/icons/info.svg"; -function getCSSClass() { - return searchParams.get("s"); -} - function getDescription() { return searchParams.get("d"); } @@ -994,64 +975,6 @@ function initPageCertError() { setCertErrorDetails(); } -async function recordSecurityUITelemetry(category, name, errorInfo) { - // Truncate the error code to avoid going over the allowed - // string size limit for telemetry events. - let errorCode = errorInfo.errorCodeString.substring(0, 40); - let extraKeys = { - value: errorCode, - is_frame: window.parent != window, - }; - if (category == "securityUiCerterror") { - extraKeys.has_sts = gHasSts; - } - if (name.startsWith("load")) { - extraKeys.channel_status = errorInfo.channelStatus; - } - if (category == "securityUiCerterror" && name.startsWith("load")) { - extraKeys.issued_by_cca = false; - extraKeys.hyphen_compat = false; - // This issue only applies to certificate domain name mismatch errors where - // the first label in the domain name starts or ends with a hyphen. - let label = HOST_NAME.substring(0, HOST_NAME.indexOf(".")); - if ( - errorCode == "SSL_ERROR_BAD_CERT_DOMAIN" && - (label.startsWith("-") || label.endsWith("-")) - ) { - try { - let subjectAltNames = await getSubjectAltNames(errorInfo); - for (let subjectAltName of subjectAltNames) { - // If the certificate has a wildcard entry that matches the domain - // name (e.g. '*.example.com' matches 'foo-.example.com'), then - // this error is probably due to Firefox disallowing hyphens in - // domain names when matching wildcard entries. - if ( - subjectAltName.startsWith("*.") && - subjectAltName.substring(1) == HOST_NAME.substring(label.length) - ) { - extraKeys.hyphen_compat = true; - break; - } - } - } catch (e) { - console.error("error parsing certificate:", e); - } - } - let issuer = errorInfo.certChainStrings.at(-1); - if (issuer && errorCode == "SEC_ERROR_UNKNOWN_ISSUER") { - try { - let parsed = await parse(pemToDER(issuer)); - extraKeys.issued_by_cca = - parsed.issuer.dn == "c=IN, o=India PKI, cn=CCA India 2022 SPL" || - parsed.issuer.dn == "c=IN, o=India PKI, cn=CCA India 2015 SPL"; - } catch (e) { - console.error("error parsing issuer certificate:", e); - } - } - } - RPMRecordGleanEvent(category, name, extraKeys); -} - function recordClickTelemetry(e) { let target = e.originalTarget; let telemetryId = target.dataset.telemetryId; @@ -1109,44 +1032,6 @@ function copyPEMToClipboard() { navigator.clipboard.writeText(errorText.textContent); } -async function getFailedCertificatesAsPEMString() { - let locationUrl = 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 = - locationUrl + - "\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() { // 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 @@ -1380,21 +1265,6 @@ function setCertErrorDetails() { } } -async function getSubjectAltNames(failedCertInfo) { - const serverCertBase64 = failedCertInfo.certChainStrings[0]; - const parsed = await parse(pemToDER(serverCertBase64)); - const subjectAltNamesExtension = parsed.ext.san; - const subjectAltNames = []; - if (subjectAltNamesExtension) { - for (let [key, value] of subjectAltNamesExtension.altNames) { - if (key === "DNS Name" && value.length) { - subjectAltNames.push(value); - } - } - } - return subjectAltNames; -} - // The optional argument is only here for testing purposes. function setTechnicalDetailsOnCertError( failedCertInfo = document.getFailedCertSecurityInfo() @@ -1603,13 +1473,36 @@ function setFocus(selector, position = "afterbegin") { } } -for (let button of document.querySelectorAll(".try-again")) { - button.addEventListener("click", function () { - retryThis(this); - }); +function shouldUseFeltPrivacyRefresh() { + if (!FELT_PRIVACY_REFRESH) { + return false; + } + + let failedCertInfo; + try { + failedCertInfo = document.getFailedCertSecurityInfo(); + } catch { + return false; + } + + return NetErrorCard.ERROR_CODES.has(failedCertInfo.errorCodeString); } -initPage(); +if (!shouldUseFeltPrivacyRefresh()) { + for (let button of document.querySelectorAll(".try-again")) { + button.addEventListener("click", function () { + retryThis(this); + }); + } -// Dispatch this event so tests can detect that we finished loading the error page. -document.dispatchEvent(new CustomEvent("AboutNetErrorLoad", { bubbles: true })); + initPage(); + + // Dispatch this event so tests can detect that we finished loading the error page. + document.dispatchEvent( + new CustomEvent("AboutNetErrorLoad", { bubbles: true }) + ); +} else { + customElements.define("net-error-card", NetErrorCard); + document.body.classList.add("felt-privacy-body"); + document.body.replaceChildren(document.createElement("net-error-card")); +} diff --git a/toolkit/content/aboutNetErrorHelpers.mjs b/toolkit/content/aboutNetErrorHelpers.mjs new file mode 100644 index 000000000000..1a33d30e8331 --- /dev/null +++ b/toolkit/content/aboutNetErrorHelpers.mjs @@ -0,0 +1,156 @@ +/* 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/remote-page */ + +import { + parse, + pemToDER, +} from "chrome://global/content/certviewer/certDecoder.mjs"; + +// 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. + +export let searchParams = new URLSearchParams( + document.documentURI.split("?")[1] +); + +export let gErrorCode = searchParams.get("e"); +export let gIsCertError = gErrorCode == "nssBadCert"; +export let gHasSts = gIsCertError && getCSSClass() === "badStsCert"; +const HOST_NAME = getHostName(); + +export function getCSSClass() { + return searchParams.get("s"); +} + +export function getHostName() { + try { + return new URL(RPMGetInnerMostURI(document.location.href)).hostname; + } catch (error) { + console.error("Could not parse URL", error); + } + return ""; +} + +export async function getFailedCertificatesAsPEMString() { + let locationUrl = 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 = + locationUrl + + "\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; +} + +export async function getSubjectAltNames(failedCertInfo) { + const serverCertBase64 = failedCertInfo.certChainStrings[0]; + const parsed = await parse(pemToDER(serverCertBase64)); + const subjectAltNamesExtension = parsed.ext.san; + const subjectAltNames = []; + if (subjectAltNamesExtension) { + for (let [key, value] of subjectAltNamesExtension.altNames) { + if (key === "DNS Name" && value.length) { + subjectAltNames.push(value); + } + } + } + return subjectAltNames; +} + +export async function recordSecurityUITelemetry(category, name, errorInfo) { + // Truncate the error code to avoid going over the allowed + // string size limit for telemetry events. + let errorCode = errorInfo.errorCodeString.substring(0, 40); + let extraKeys = { + value: errorCode, + is_frame: window.parent != window, + }; + if (category == "securityUiCerterror") { + extraKeys.has_sts = gHasSts; + } + if (name.startsWith("load")) { + extraKeys.channel_status = errorInfo.channelStatus; + } + if (category == "securityUiCerterror" && name.startsWith("load")) { + extraKeys.issued_by_cca = false; + extraKeys.hyphen_compat = false; + // This issue only applies to certificate domain name mismatch errors where + // the first label in the domain name starts or ends with a hyphen. + let label = HOST_NAME.substring(0, HOST_NAME.indexOf(".")); + if ( + errorCode == "SSL_ERROR_BAD_CERT_DOMAIN" && + (label.startsWith("-") || label.endsWith("-")) + ) { + try { + let subjectAltNames = await getSubjectAltNames(errorInfo); + for (let subjectAltName of subjectAltNames) { + // If the certificate has a wildcard entry that matches the domain + // name (e.g. '*.example.com' matches 'foo-.example.com'), then + // this error is probably due to Firefox disallowing hyphens in + // domain names when matching wildcard entries. + if ( + subjectAltName.startsWith("*.") && + subjectAltName.substring(1) == HOST_NAME.substring(label.length) + ) { + extraKeys.hyphen_compat = true; + break; + } + } + } catch (e) { + console.error("error parsing certificate:", e); + } + } + let issuer = errorInfo.certChainStrings.at(-1); + if (issuer && errorCode == "SEC_ERROR_UNKNOWN_ISSUER") { + try { + let parsed = await parse(pemToDER(issuer)); + extraKeys.issued_by_cca = + parsed.issuer.dn == "c=IN, o=India PKI, cn=CCA India 2022 SPL" || + parsed.issuer.dn == "c=IN, o=India PKI, cn=CCA India 2015 SPL"; + } catch (e) { + console.error("error parsing issuer certificate:", e); + } + } + } + RPMRecordGleanEvent(category, name, extraKeys); +} diff --git a/toolkit/content/jar.mn b/toolkit/content/jar.mn index d48c177a6465..d24f8aa820dc 100644 --- a/toolkit/content/jar.mn +++ b/toolkit/content/jar.mn @@ -9,6 +9,8 @@ toolkit.jar: content/global/aboutLogging.html content/global/aboutNetError.mjs content/global/aboutNetError.html + content/global/aboutNetErrorHelpers.mjs + content/global/net-error-card.mjs content/global/aboutNetworking.js content/global/aboutNetworking.html #ifndef ANDROID diff --git a/toolkit/content/net-error-card.mjs b/toolkit/content/net-error-card.mjs new file mode 100644 index 000000000000..0400eeb79d92 --- /dev/null +++ b/toolkit/content/net-error-card.mjs @@ -0,0 +1,523 @@ +/* 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-disable import/no-unassigned-import */ +/* eslint-env mozilla/remote-page */ + +import { + getCSSClass, + getHostName, + getSubjectAltNames, + getFailedCertificatesAsPEMString, + recordSecurityUITelemetry, +} from "chrome://global/content/aboutNetErrorHelpers.mjs"; +import { html } from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; +import "chrome://global/content/elements/moz-button-group.mjs"; +import "chrome://global/content/elements/moz-button.mjs"; +import "chrome://global/content/elements/moz-support-link.mjs"; + +const HOST_NAME = getHostName(); + +export class NetErrorCard extends MozLitElement { + static properties = { + hostname: { type: String }, + domainMismatchNames: { type: String }, + advancedShowing: { type: Boolean, reflect: true }, + certErrorDebugInfoShowing: { type: Boolean, reflect: true }, + certificateErrorText: { type: String }, + }; + + static queries = { + copyButtonTop: "#copyToClipboardTop", + exceptionButton: "#exception-button", + errorCode: "#errorCode", + advancedContainer: ".advanced-container", + advancedButton: "#advanced-button", + }; + + static ERROR_CODES = new Set([ + "SEC_ERROR_UNKNOWN_ISSUER", + "SSL_ERROR_BAD_CERT_DOMAIN", + "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT", + "SEC_ERROR_EXPIRED_CERTIFICATE", + "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE", + ]); + + constructor() { + super(); + + this.domainMismatchNames = null; + this.advancedShowing = false; + this.certErrorDebugInfoShowing = false; + this.certificateErrorText = null; + this.domainMismatchNamesPromise = null; + this.certificateErrorTextPromise = null; + } + + async getUpdateComplete() { + const result = await super.getUpdateComplete(); + + if (this.domainMismatchNames && this.certificateErrorText) { + return result; + } + + await Promise.all([ + this.getDomainMismatchNames(), + this.getCertificateErrorText(), + ]); + + await Promise.all([ + this.domainMismatchNamesPromise, + this.certificateErrorTextPromise, + ]); + + return result; + } + + connectedCallback() { + super.connectedCallback(); + + this.init(); + } + + firstUpdated() { + // Dispatch this event so tests can detect that we finished loading the error page. + document.dispatchEvent( + new CustomEvent("AboutNetErrorLoad", { bubbles: true }) + ); + } + + init() { + document.l10n.setAttributes( + document.querySelector("title"), + "fp-certerror-page-title" + ); + + this.failedCertInfo = document.getFailedCertSecurityInfo(); + + this.hostname = HOST_NAME; + const { port } = document.location; + if (port && port != 443) { + this.hostname += ":" + port; + } + + if (getCSSClass() == "expertBadCert") { + this.toggleAdvancedShowing(); + } + } + + introContentTemplate() { + switch (this.failedCertInfo.errorCodeString) { + case "SEC_ERROR_UNKNOWN_ISSUER": + case "SSL_ERROR_BAD_CERT_DOMAIN": + case "SEC_ERROR_EXPIRED_CERTIFICATE": + case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT": + return html`
`; + case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE": + return html``; + } + + return null; + } + + advancedContainerTemplate() { + if (!this.advancedShowing) { + return null; + } + + let content; + + switch (this.failedCertInfo.errorCodeString) { + case "SEC_ERROR_UNKNOWN_ISSUER": { + content = this.advancedSectionTemplate({ + whyDangerousL10nId: "fp-certerror-unknown-issuer-why-dangerous-body", + whatCanYouDoL10nId: + "fp-certerror-unknown-issuer-what-can-you-do-body", + learnMoreL10nId: "fp-learn-more-about-cert-issues", + learnMoreSupportPage: "connection-not-secure", + viewCert: true, + viewDateTime: true, + }); + break; + } + case "SSL_ERROR_BAD_CERT_DOMAIN": { + if (!this.domainMismatchNames) { + this.getDomainMismatchNames(); + return null; + } + + content = this.advancedSectionTemplate({ + whyDangerousL10nId: "fp-certerror-bad-domain-why-dangerous-body", + whyDangerousL10nArgs: { + hostname: this.hostname, + validHosts: this.domainMismatchNames ?? "", + }, + whatCanYouDoL10nId: "fp-certerror-bad-domain-what-can-you-do-body", + learnMoreL10nId: "fp-learn-more-about-secure-connection-failures", + learnMoreSupportPage: "connection-not-secure", + viewCert: true, + viewDateTime: true, + }); + break; + } + case "SEC_ERROR_EXPIRED_CERTIFICATE": { + const notBefore = this.failedCertInfo.validNotBefore; + const notAfter = this.failedCertInfo.validNotAfter; + if (notBefore && Date.now() < notAfter) { + content = this.advancedSectionTemplate({ + whyDangerousL10nId: "fp-certerror-not-yet-valid-why-dangerous-body", + whyDangerousL10nArgs: { + date: notBefore, + }, + whatCanYouDoL10nId: "fp-certerror-expired-what-can-you-do-body", + whatCanYouDoL10nArgs: { + date: Date.now(), + }, + learnMoreL10nId: "fp-learn-more-about-time-related-errors", + learnMoreSupportPage: "time-errors", + viewCert: true, + viewDateTime: true, + }); + } else { + content = this.advancedSectionTemplate({ + whyDangerousL10nId: "fp-certerror-expired-why-dangerous-body", + whyDangerousL10nArgs: { + date: notAfter, + }, + whatCanYouDoL10nId: "fp-certerror-expired-what-can-you-do-body", + whatCanYouDoL10nArgs: { + date: Date.now(), + }, + learnMoreL10nId: "fp-learn-more-about-time-related-errors", + learnMoreSupportPage: "time-errors", + viewCert: true, + viewDateTime: true, + }); + } + break; + } + case "MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT": { + content = this.advancedSectionTemplate({ + whyDangerousL10nId: "fp-certerror-self-signed-why-dangerous-body", + whatCanYouDoL10nId: "fp-certerror-self-signed-what-can-you-do-body", + importantNote: "fp-certerror-self-signed-important-note", + viewCert: true, + viewDateTime: true, + }); + break; + } + case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE": { + const notAfter = this.failedCertInfo.validNotAfter; + content = this.advancedSectionTemplate({ + whyDangerousL10nId: "fp-certerror-expired-why-dangerous-body", + whyDangerousL10nArgs: { + date: notAfter, + }, + whatCanYouDoL10nId: "fp-certerror-expired-what-can-you-do-body", + whatCanYouDoL10nArgs: { + date: Date.now(), + }, + learnMoreL10nId: "fp-learn-more-about-time-related-errors", + learnMoreSupportPage: "time-errors", + viewCert: true, + viewDateTime: true, + }); + break; + } + } + + return html`+ ${whyDangerousL10nId + ? html` + ` + : null} +
+ ${whatCanYouDoL10nId + ? html`+ + +
` + : null} + ${importantNote ? html`` : null} + ${learnMoreL10nId + ? html`` + : null} + ${viewCert + ? html`` + : null} + + ${viewDateTime + ? html`` + : null} +