From 5319e76bba8bb64a9a30e89479228176a7166cd1 Mon Sep 17 00:00:00 2001 From: Niklas Baumgardner Date: Thu, 24 Oct 2024 01:19:45 +0000 Subject: [PATCH] Bug 1873514 - Update about:neterror appearance and strings. r=Gijs,fluent-reviewers,desktop-theme-reviewers,bolsson,sfoster Differential Revision: https://phabricator.services.mozilla.com/D217621 --- browser/app/profile/firefox.js | 1 + .../test/general/browser_addCertException.js | 126 +++-- .../content/test/general/browser_bug431826.js | 126 +++-- browser/base/content/test/general/head.js | 26 +- .../browser/browser_badCertDomainFixup.js | 110 ++-- toolkit/content/aboutNetError.mjs | 197 ++----- toolkit/content/aboutNetErrorHelpers.mjs | 156 ++++++ toolkit/content/jar.mn | 2 + toolkit/content/net-error-card.mjs | 523 ++++++++++++++++++ .../en-US/toolkit/neterror/certError.ftl | 71 +++ .../modules/RemotePageAccessManager.sys.mjs | 2 + toolkit/themes/shared/aboutNetError.css | 29 + toolkit/themes/shared/desktop-jar.inc.mn | 1 + .../shared/illustrations/security-error.svg | 4 + 14 files changed, 1078 insertions(+), 296 deletions(-) create mode 100644 toolkit/content/aboutNetErrorHelpers.mjs create mode 100644 toolkit/content/net-error-card.mjs create mode 100644 toolkit/themes/shared/illustrations/security-error.svg 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`
+

+ ${content} +
`; + } + + advancedSectionTemplate(params) { + let { + whyDangerousL10nId, + whyDangerousL10nArgs, + whatCanYouDoL10nId, + whatCanYouDoL10nArgs, + importantNote, + learnMoreL10nId, + learnMoreSupportPage, + viewCert, + viewDateTime, + } = params; + return html`

+ ${whyDangerousL10nId + ? html` + ` + : null} +

+ ${whatCanYouDoL10nId + ? html`

+ + +

` + : null} + ${importantNote ? html`

` : null} + ${learnMoreL10nId + ? html`

+ +

` + : null} + ${viewCert + ? html`

+ +

` + : null} +

+ +

+ ${viewDateTime + ? html`

` + : null} + `; + } + + async getDomainMismatchNames() { + if (this.domainMismatchNamesPromise) { + return; + } + + this.domainMismatchNamesPromise = getSubjectAltNames(this.failedCertInfo); + let subjectAltNames = await this.domainMismatchNamesPromise; + this.domainMismatchNames = subjectAltNames.join(", "); + } + + async getCertificateErrorText() { + if (this.certificateErrorTextPromise) { + return; + } + + this.certificateErrorTextPromise = getFailedCertificatesAsPEMString(); + this.certificateErrorText = await this.certificateErrorTextPromise; + } + + certErrorDebugInfoTemplate() { + if (!this.certErrorDebugInfoShowing) { + return null; + } + + if (!this.certificateErrorText) { + this.getCertificateErrorText(); + return null; + } + + return html`
+ +
${this.certificateErrorText}
+ +
`; + } + + handleGoBackClick(e) { + this.handleTelemetryClick(e); + RPMSendAsyncMessage("Browser:SSLErrorGoBack"); + } + + handleProceedToUrlClick(e) { + this.handleTelemetryClick(e); + const isPermanent = + !RPMIsWindowPrivate() && + RPMGetBoolPref("security.certerrors.permanentOverride"); + document.addCertException(!isPermanent).then( + () => { + location.reload(); + }, + () => {} + ); + } + + toggleAdvancedShowing(e) { + if (e) { + this.handleTelemetryClick(e); + } + + this.advancedShowing = !this.advancedShowing; + + if (!this.advancedShowing) { + return; + } + + this.revealAdvancedContainer(); + } + + async revealAdvancedContainer() { + await this.getUpdateComplete(); + + // 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. + this.certErrorDebugInfoShowing = false; + + // Reveal, but disabled (and grayed-out) for 3.0s. + this.exceptionButton.disabled = true; + + // - + + if (this.resetReveal) { + this.resetReveal(); // Reset if previous is pending. + } + let wasReset = false; + this.resetReveal = () => { + wasReset = true; + }; + + // Wait for 10 frames to ensure that the warning text is rendered + // and gets all the way to the screen for the user to read it. + // This is only ~0.160s at 60Hz, so it's not too much extra time that we're + // taking to ensure that we're caught up with rendering, on top of the + // (by default) whole second(s) we're going to wait based on the + // security.dialog_enable_delay pref. + // The catching-up to rendering is the important part, not the + // N-frame-delay here. + for (let i = 0; i < 10; i++) { + await new Promise(requestAnimationFrame); + } + + // Wait another Nms (default: 1000) for the user to be very sure. (Sorry speed readers!) + const securityDelayMs = RPMGetIntPref("security.dialog_enable_delay", 1000); + await new Promise(go => setTimeout(go, securityDelayMs)); + + if (wasReset || !this.advancedShowing) { + this.resetReveal = null; + return; + } + + // Enable and un-gray-out. + this.exceptionButton.disabled = false; + } + + async toggleCertErrorDebugInfoShowing(event) { + this.handleTelemetryClick(event); + event.preventDefault(); + + this.certErrorDebugInfoShowing = !this.certErrorDebugInfoShowing; + + if (this.certErrorDebugInfoShowing) { + await this.getUpdateComplete(); + this.copyButtonTop.scrollIntoView({ + block: "start", + behavior: "smooth", + }); + this.copyButtonTop.focus(); + } + } + + copyCertErrorTextToClipboard(e) { + this.handleTelemetryClick(e); + navigator.clipboard.writeText(this.certificateErrorText); + } + + handleTelemetryClick(event) { + let target = event.originalTarget; + if (!target.hasAttribute("data-telemetry-id")) { + target = target.getRootNode().host; + } + let telemetryId = target.dataset.telemetryId; + void recordSecurityUITelemetry( + "securityUiCerterror", + "click" + + telemetryId + .split("_") + .map(word => word[0].toUpperCase() + word.slice(1)) + .join(""), + this.failedCertInfo + ); + } + + render() { + if (!this.failedCertInfo) { + return null; + } + + return html` +
+
+ +
+
+

+ ${this.introContentTemplate()} + + ${this.advancedContainerTemplate()} + ${this.certErrorDebugInfoTemplate()} +
+
`; + } +} diff --git a/toolkit/locales/en-US/toolkit/neterror/certError.ftl b/toolkit/locales/en-US/toolkit/neterror/certError.ftl index a3f68d800bd7..612d18ac3f79 100644 --- a/toolkit/locales/en-US/toolkit/neterror/certError.ftl +++ b/toolkit/locales/en-US/toolkit/neterror/certError.ftl @@ -146,3 +146,74 @@ networkProtocolError-title = Network Protocol Error nssBadCert-title = Warning: Potential Security Risk Ahead nssBadCert-sts-title = Did Not Connect: Potential Security Issue certerror-mitm-title = Software is Preventing { -brand-short-name } From Safely Connecting to This Site + +## Felt Privacy V1 Strings + +fp-certerror-page-title = Warning: Security Risk +fp-certerror-body-title = Be careful. Something doesn’t look right. + +fp-certerror-why-site-dangerous = What makes the site look dangerous? +fp-certerror-what-can-you-do = What can you do about it? + +fp-certerror-advanced-title = Advanced + +fp-certerror-advanced-button = Advanced +fp-certerror-hide-advanced-button = Hide advanced + +## Variables: +## $hostname (String) - Hostname of the website to which the user was trying to connect. + +fp-certerror-override-exception-button = Proceed to { $hostname } (Risky) +fp-certerror-intro = { -brand-short-name } spotted a potentially serious security issue with { $hostname }. Someone pretending to be the site could try to steal things like credit card info, passwords, or emails. +fp-certerror-expired-into = { -brand-short-name } spotted a security issue with { $hostname }. Either the site isn’t set up right or your device’s clock is set to the wrong date/time. + +## + +fp-certerror-view-certificate-link = View the site’s certificate +fp-certerror-return-to-previous-page-recommended-button = Go back (Recommended) + +# This string appears after the following string: "What makes the site look dangerous?" (fp-certerror-why-site-dangerous) +# Variables: +# $hostname (String) - Hostname of the website to which the user was trying to connect. +# $validHosts (String) - Valid hostnames. +fp-certerror-bad-domain-why-dangerous-body = The site is set up to allow only secure connections, but there’s a problem with the site’s certificate. It’s possible that a bad actor is trying to impersonate the site. Sites use certificates issued by a certificate authority to prove they’re really who they say they are. { -brand-short-name } doesn’t trust this site because its certificate isn’t valid for { $hostname }. The certificate is only valid for: { $validHosts }. +# This string appears after the following string: "What can you do about it?" (fp-certerror-what-can-you-do) +fp-certerror-bad-domain-what-can-you-do-body = Probably nothing, since it’s likely there’s a problem with the site itself. Sites use certificates issued by a certificate authority to prove they’re really who they say they are. But if you’re on a corporate network, your support team may have more info. If you’re using antivirus software, try searching for potential conflicts or known issues. + +# This string appears after the following string: "What makes the site look dangerous?" (fp-certerror-why-site-dangerous) +fp-certerror-unknown-issuer-why-dangerous-body = There’s an issue with the site’s certificate. It’s possible that a bad actor is trying to impersonate the site. Sites use certificates issued by a certificate authority to prove they’re really who they say they are. { -brand-short-name } doesn’t trust this site because we can’t tell who issued the certificate, it’s self-signed, or the site isn’t sending intermediate certificates we trust. +# This string appears after the following string: "What can you do about it?" (fp-certerror-what-can-you-do) +fp-certerror-unknown-issuer-what-can-you-do-body = Probably nothing, since it’s likely there’s a problem with the site itself. But if you’re on a corporate network, your support team may have more info. If you’re using antivirus software, it may need to be configured to work with { -brand-short-name }. + +# This string appears after the following string: "What makes the site look dangerous?" (fp-certerror-why-site-dangerous) +fp-certerror-self-signed-why-dangerous-body = Because there’s an issue with the site’s certificate. Sites use certificates issued by a certificate authority to prove they’re really who they say they are. This site’s certificate is self-signed. It wasn’t issued by a recognized certificate authority – so we don’t trust it by default. +# This string appears after the following string: "What can you do about it?" (fp-certerror-what-can-you-do) +fp-certerror-self-signed-what-can-you-do-body = Not much. It’s likely there’s a problem with the site itself. +fp-certerror-self-signed-important-note = IMPORTANT NOTE: If you are trying to visit this site on a corporate intranet, your IT staff may use self-signed certificates. They can help you check their authenticity. + +# This string appears after the following string: "What makes the site look dangerous?" (fp-certerror-why-site-dangerous) +# Variables: +# $date (Date) - Certificate expiration date. +fp-certerror-expired-why-dangerous-body = Sites use certificates issued by a certificate authority to prove they’re really who they say they are. { -brand-short-name } doesn’t trust this site because it looks like the certificate expired on { DATETIME($date, month: "numeric", day: "numeric", year: "numeric") }. + +# This string appears after the following string: "What makes the site look dangerous?" (fp-certerror-why-site-dangerous) +# Variables: +# $date (Date) - Certificate start date. +fp-certerror-not-yet-valid-why-dangerous-body = Sites use certificates issued by a certificate authority to prove they’re really who they say they are. { -brand-short-name } doesn’t trust this site because it looks like the certificate will not be valid until { DATETIME($date, month: "numeric", day: "numeric", year: "numeric") }. + +# This string appears after the following string: "What can you do about it?" (fp-certerror-what-can-you-do) +# Variables: +# $date (Date) - Clock date. +fp-certerror-expired-what-can-you-do-body = Your device’s clock is set to { DATETIME($date, month: "numeric", day: "numeric", year: "numeric") }. If this is correct, the security issue is probably with the site itself. If it’s wrong, you can change it in your device’s system settings. + +# Variables: +# $error (string) - NSS error code string that specifies type of cert error. e.g. unknown issuer, invalid cert, etc. +fp-cert-error-code = Error Code: { $error } + +# Variables: +# $datetime (Date) - Current datetime. +fp-datetime = { DATETIME($datetime, month: "short", year: "numeric", day: "numeric") } { DATETIME($datetime, timeStyle: "long") } + +fp-learn-more-about-secure-connection-failures = Learn more about secure connection failures +fp-learn-more-about-cert-issues = Learn more about these kinds of certificate issues +fp-learn-more-about-time-related-errors = Learn more about troubleshooting time-related errors diff --git a/toolkit/modules/RemotePageAccessManager.sys.mjs b/toolkit/modules/RemotePageAccessManager.sys.mjs index 3432930f9d7f..e556ce50b5a2 100644 --- a/toolkit/modules/RemotePageAccessManager.sys.mjs +++ b/toolkit/modules/RemotePageAccessManager.sys.mjs @@ -49,6 +49,7 @@ export let RemotePageAccessManager = { "security.enterprise_roots.auto-enabled", "security.certerror.hideAddException", "network.trr.display_fallback_warning", + "security.certerrors.felt-privacy-v1", ], RPMGetIntPref: [ "security.dialog_enable_delay", @@ -102,6 +103,7 @@ export let RemotePageAccessManager = { "security.xfocsp.errorReporting.enabled", "security.xfocsp.hideOpenInNewWindow", "network.trr.display_fallback_warning", + "security.certerrors.felt-privacy-v1", ], RPMSetPref: [ "security.xfocsp.errorReporting.automatic", diff --git a/toolkit/themes/shared/aboutNetError.css b/toolkit/themes/shared/aboutNetError.css index a3dc3394da72..5fe3a64ede5f 100644 --- a/toolkit/themes/shared/aboutNetError.css +++ b/toolkit/themes/shared/aboutNetError.css @@ -165,6 +165,35 @@ button:disabled { margin-top: 10px; } +/* Felt Privacy v1 LARGER SCREEN! */ +/* stylelint-disable-next-line media-query-no-invalid */ +@media (-moz-bool-pref: "security.certerrors.felt-privacy-v1") { + .felt-privacy-body { + justify-content: start; + } + + net-error-card { + width: min(100%, 720px); + min-width: min-content; + margin-top: 150px; + } + + .felt-privacy-container { + display: flex; + flex-direction: row; + } + + .img-container > img { + width: 8.75em; + margin-block-start: 2em; + margin-inline-end: 2.5em; + } + + #viewCertificate { + margin: 0; + } +} + @media only screen and (max-width: 959px) { #certificateErrorText { /* The encoded certificate chain looks better when we're not diff --git a/toolkit/themes/shared/desktop-jar.inc.mn b/toolkit/themes/shared/desktop-jar.inc.mn index 4b30af216bcb..b3ad3f493b09 100644 --- a/toolkit/themes/shared/desktop-jar.inc.mn +++ b/toolkit/themes/shared/desktop-jar.inc.mn @@ -132,6 +132,7 @@ skin/classic/global/illustrations/about-rights.svg (../../shared/illustrations/about-rights.svg) skin/classic/global/illustrations/about-license.svg (../../shared/illustrations/about-license.svg) skin/classic/global/illustrations/error-malformed-url.svg (../../shared/illustrations/error-malformed-url.svg) + skin/classic/global/illustrations/security-error.svg (../../shared/illustrations/security-error.svg) skin/classic/global/media/picture-in-picture-open.svg (../../shared/media/picture-in-picture-open.svg) skin/classic/global/media/picture-in-picture-closed.svg (../../shared/media/picture-in-picture-closed.svg) skin/classic/global/icons/minus.svg (../../shared/icons/minus.svg) diff --git a/toolkit/themes/shared/illustrations/security-error.svg b/toolkit/themes/shared/illustrations/security-error.svg new file mode 100644 index 000000000000..a1de97df28b5 --- /dev/null +++ b/toolkit/themes/shared/illustrations/security-error.svg @@ -0,0 +1,4 @@ + +