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
This commit is contained in:
Niklas Baumgardner 2024-10-21 21:07:01 +00:00
parent 786a1efc72
commit 57431e5ed9
14 changed files with 1005 additions and 228 deletions

View File

@ -2644,6 +2644,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

View File

@ -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();
}
});

View File

@ -1,7 +1,3 @@
function remote(task) {
return SpecialPowers.spawn(gBrowser.selectedBrowser, [], task);
}
add_task(async function () {
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
@ -12,7 +8,7 @@ add_task(async function () {
);
await promise;
await remote(() => {
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(
@ -21,7 +17,7 @@ add_task(async function () {
);
});
await remote(() => {
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");
@ -39,7 +35,7 @@ add_task(async function () {
gBrowser.reload();
await promise;
await remote(() => {
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
let div = content.document.getElementById("badCertAdvancedPanel");
Assert.ok(div, "Advanced content div should exist");
Assert.equal(
@ -57,3 +53,53 @@ add_task(async function () {
Services.prefs.clearUserPref("browser.xul.error_pages.expert_bad_cert");
}
});
add_task(async function testWithFeltPrivacyEnabled() {
await SpecialPowers.pushPrefEnv({
set: [["security.certerrors.felt-privacy-v1", true]],
});
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
BrowserTestUtils.startLoadingURIString(
gBrowser,
"https://nocert.example.com/"
);
await promise;
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"
);
});
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
let netErrorCard =
content.document.querySelector("net-error-card").wrappedJSObject;
await netErrorCard.updateComplete;
Assert.ok(
netErrorCard.advancedShowing,
"Advanced showing attribute should be true"
);
await ContentTaskUtils.waitForCondition(
() =>
netErrorCard.advancedContainer &&
ContentTaskUtils.isVisible(netErrorCard.advancedContainer),
"Wait for advanced content to exist"
);
Assert.ok(
ContentTaskUtils.isVisible(netErrorCard.advancedContainer),
"Advanced content div should be visible"
);
});
// Clean up
gBrowser.removeCurrentTab();
await SpecialPowers.popPrefEnv();
});

View File

@ -289,13 +289,24 @@ 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.updateComplete;
netErrorCard.handleProceedToUrlClick();
} else {
content.document.getElementById("exceptionDialogButton").click();
}
}
);
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
}

View File

@ -7,39 +7,66 @@
// 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();
ec = netErrorCard.errorCode;
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 +90,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

View File

@ -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,37 +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;
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;
@ -1082,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
@ -1353,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()
@ -1576,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"));
}

View File

@ -0,0 +1,128 @@
/* 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";
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;
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);
}

View File

@ -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

View File

@ -0,0 +1,514 @@
/* 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 {
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",
};
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;
}
}
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`<p
data-l10n-id="fp-certerror-intro"
data-l10n-args='{"hostname": "${this.hostname}"}'
></p>`;
case "SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE":
return html`<p
data-l10n-id="fp-certerror-expired-intro"
data-l10n-args='{"hostname": "${this.hostname}"}'
></p>`;
}
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`<div class="advanced-container">
<h2 data-l10n-id="fp-certerror-advanced-title"></h2>
${content}
</div>`;
}
advancedSectionTemplate(params) {
let {
whyDangerousL10nId,
whyDangerousL10nArgs,
whatCanYouDoL10nId,
whatCanYouDoL10nArgs,
importantNote,
learnMoreL10nId,
learnMoreSupportPage,
viewCert,
viewDateTime,
} = params;
return html`<p>
${whyDangerousL10nId
? html`<strong
data-l10n-id="fp-certerror-why-site-dangerous"
></strong>
<span
data-l10n-id="${whyDangerousL10nId}"
data-l10n-args=${JSON.stringify(whyDangerousL10nArgs)}
></span>`
: null}
</p>
${whatCanYouDoL10nId
? html`<p>
<strong data-l10n-id="fp-certerror-what-can-you-do"></strong>
<span
data-l10n-id="${whatCanYouDoL10nId}"
data-l10n-args=${JSON.stringify(whatCanYouDoL10nArgs)}
></span>
</p>`
: null}
${importantNote ? html`<p data-l10n-id="${importantNote}"></p>` : null}
${learnMoreL10nId
? html`<p>
<a
is="moz-support-link"
support-page="${learnMoreSupportPage}"
data-l10n-id="${learnMoreL10nId}"
data-telemetry-id="learn_more_link"
@click=${this.handleTelemetryClick}
></a>
</p>`
: null}
${viewCert
? html`<p>
<a
id="viewCertificate"
data-l10n-id="fp-certerror-view-certificate-link"
href="javascript:void(0)"
></a>
</p>`
: null}
<p>
<a
id="errorCode"
data-l10n-id="fp-cert-error-code"
data-l10n-name="error-code-link"
data-telemetry-id="error_code_link"
data-l10n-args='{"error": "${this.failedCertInfo.errorCodeString}"}'
@click=${this.toggleCertErrorDebugInfoShowing}
href="#certificateErrorDebugInformation"
></a>
</p>
${viewDateTime
? html`<p
data-l10n-id="fp-datetime"
data-l10n-args=${JSON.stringify({ datetime: Date.now() })}
></p>`
: null}
<moz-button
id="exception-button"
data-l10n-id="fp-certerror-override-exception-button"
data-l10n-args=${JSON.stringify({ hostname: this.hostname })}
data-telemetry-id="exception_button"
@click=${this.handleProceedToUrlClick}
></moz-button>`;
}
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`<div
id="certificateErrorDebugInformation"
class="advanced-panel"
>
<moz-button
id="copyToClipboardTop"
data-telemetry-id="clipboard_button_top"
data-l10n-id="neterror-copy-to-clipboard-button"
@click=${this.copyCertErrorTextToClipboard}
></moz-button>
<div id="certificateErrorText">${this.certificateErrorText}</div>
<moz-button
data-telemetry-id="clipboard_button_bot"
data-l10n-id="neterror-copy-to-clipboard-button"
@click=${this.copyCertErrorTextToClipboard}
></moz-button>
</div>`;
}
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) {
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`<link
rel="stylesheet"
href="chrome://global/skin/aboutNetError.css"
/>
<article class="felt-privacy-container">
<div class="img-container">
<img src="chrome://global/skin/illustrations/security-error.svg" />
</div>
<div class="container">
<h1 data-l10n-id="fp-certerror-body-title"></h1>
${this.introContentTemplate()}
<moz-button-group
><moz-button
type="primary"
data-l10n-id="fp-certerror-return-to-previous-page-recommended-button"
data-telemetry-id="return_button_adv"
@click=${this.handleGoBackClick}
></moz-button
><moz-button
data-l10n-id="${this.advancedShowing
? "fp-certerror-hide-advanced-button"
: "fp-certerror-advanced-button"}"
data-telemetry-id="advanced_button"
@click=${this.toggleAdvancedShowing}
></moz-button
></moz-button-group>
${this.advancedContainerTemplate()}
${this.certErrorDebugInfoTemplate()}
</div>
</article>`;
}
}

View File

@ -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 doesnt 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 <strong>{ $hostname }</strong>. 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 <strong>{ $hostname }</strong>. Either the site isnt set up right or your devices clock is set to the wrong date/time.
##
fp-certerror-view-certificate-link = View the sites 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 theres a problem with the sites certificate. Its possible that a bad actor is trying to impersonate the site. Sites use certificates issued by a certificate authority to prove theyre really who they say they are. { -brand-short-name } doesnt trust this site because its certificate isnt 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 its likely theres a problem with the site itself. Sites use certificates issued by a certificate authority to prove theyre really who they say they are. But if youre on a corporate network, your support team may have more info. If youre 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 = Theres an issue with the sites certificate. Its possible that a bad actor is trying to impersonate the site. Sites use certificates issued by a certificate authority to prove theyre really who they say they are. { -brand-short-name } doesnt trust this site because we cant tell who issued the certificate, its self-signed, or the site isnt 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 its likely theres a problem with the site itself. But if youre on a corporate network, your support team may have more info. If youre 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 theres an issue with the sites certificate. Sites use certificates issued by a certificate authority to prove theyre really who they say they are. This sites certificate is self-signed. It wasnt issued by a recognized certificate authority so we dont 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. Its likely theres 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 theyre really who they say they are. { -brand-short-name } doesnt 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 theyre really who they say they are. { -brand-short-name } doesnt 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 devices 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 its wrong, you can change it in your devices 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

View File

@ -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",

View File

@ -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

View File

@ -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)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB