Revision control

Copy as Markdown

Other Tools

/* 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/. */
/**
* Tests for remote content in encrypted OpenPGP messages (bug 1994709).
*
* Remote content is permitted only for OpenPGP messages that are integrity
* protected (MDC/SEIPD v1 or AEAD) and decrypted as the top-level part; it stays
* blocked for unprotected/tampered messages and for encrypted sub-parts inside
* attacker-controlled MIME.
*/
"use strict";
requestLongerTimeout(2);
const { get_about_message, open_message_from_file } =
ChromeUtils.importESModule(
);
const {
get_notification,
get_notification_button,
wait_for_notification_to_show,
} = ChromeUtils.importESModule(
);
const { OpenPGPTestUtils } = ChromeUtils.importESModule(
);
const { MailServices } = ChromeUtils.importESModule(
);
const NOTIFICATION_BOX = "mail-notification-top";
const NOTIFICATION_VALUE = "remoteContent";
const DISABLE_PREF = "mailnews.message_display.disable_remote_image";
add_setup(async function () {
const account = MailServices.accounts.createAccount();
account.incomingServer = MailServices.accounts.createIncomingServer(
"bob",
"openpgp.example",
"pop3"
);
const identity = MailServices.accounts.createIdentity();
identity.email = "bob@openpgp.example";
account.addIdentity(identity);
const [id] = await OpenPGPTestUtils.importPrivateKey(
window,
new FileUtils.File(
getTestFilePath(
"data/keys/bob@openpgp.example-0xfbfcc82a015e7330-secret.asc"
)
)
);
identity.setUnicharAttribute("openpgp_key_id", id);
registerCleanupFunction(async () => {
await OpenPGPTestUtils.removeKeyById("0xFBFCC82A015E7330", true);
MailServices.accounts.removeAccount(account, true);
});
});
// Open a raw .eml from data/eml in its own message window. Opening from a file
// registers the message with the encrypted-URI service exactly as a folder view
// does (bug 2052070), so the content-policy gate sees the same integrity state.
async function openEml(eml) {
return open_message_from_file(
new FileUtils.File(getTestFilePath(`data/eml/${eml}`))
);
}
function remoteImage(msgc) {
return get_about_message(msgc)
.getMessagePaneBrowser()
.contentDocument?.getElementById("testelement");
}
function bodyText(msgc) {
return (
get_about_message(msgc).getMessagePaneBrowser().contentDocument
?.documentElement?.textContent ?? ""
);
}
// With remote content globally allowed, an allowed message loads the image; a
// blocked one does not. Exercises the nsMsgContentPolicy gate directly (the gate
// runs before the disable_remote_image check).
async function assertImageLoads(eml, shouldLoad) {
await SpecialPowers.pushPrefEnv({
set: [[DISABLE_PREF, false]],
});
const msgc = await openEml(eml);
try {
if (shouldLoad) {
await TestUtils.waitForCondition(() => {
const img = remoteImage(msgc);
return img && img.complete && img.naturalWidth > 0;
}, `remote image should load for ${eml}`);
Assert.greater(
remoteImage(msgc).naturalWidth,
0,
`${eml}: image should load`
);
} else {
// Wait for the (blocked) image to appear; a wrongly-permitted load would
// happen once decryption completes, so poll until it does or we time out.
const loaded = await TestUtils.waitForCondition(() => {
const img = remoteImage(msgc);
return img && img.complete && img.naturalWidth > 0;
}, `remote image for ${eml}`).catch(() => false);
Assert.ok(!loaded, `${eml}: remote image should NOT load`);
}
} finally {
await BrowserTestUtils.closeWindow(msgc);
}
}
// With remote content blocked by default, an integrity-protected message shows
// the remote-content notification WITH the override button.
async function assertOverrideOffered(eml, offered) {
await SpecialPowers.pushPrefEnv({
set: [[DISABLE_PREF, true]],
});
const msgc = await openEml(eml);
try {
const aboutMessage = get_about_message(msgc);
await wait_for_notification_to_show(
aboutMessage,
NOTIFICATION_BOX,
NOTIFICATION_VALUE
);
if (offered) {
const button = get_notification_button(
aboutMessage,
NOTIFICATION_BOX,
NOTIFICATION_VALUE,
{ popup: "remoteContentOptions" }
);
Assert.ok(button, `${eml}: override should be offered`);
} else {
const notification = get_notification(
aboutMessage,
NOTIFICATION_BOX,
NOTIFICATION_VALUE
);
const buttons = notification.buttonContainer.querySelectorAll(
"button, toolbarbutton"
);
Assert.equal(buttons.length, 0, `${eml}: no override should be offered`);
}
} finally {
await BrowserTestUtils.closeWindow(msgc);
}
}
add_task(async function test_mdc_allowed() {
await assertImageLoads("rc-openpgp-mdc.eml", true);
await assertOverrideOffered("rc-openpgp-mdc.eml", true);
});
add_task(async function test_aead_allowed() {
await assertImageLoads("rc-openpgp-aead.eml", true);
await assertOverrideOffered("rc-openpgp-aead.eml", true);
});
add_task(async function test_nomdc_blocked() {
// No valid integrity -> RNP returns no plaintext -> nothing to load.
await assertImageLoads("rc-openpgp-nomdc.eml", false);
});
add_task(async function test_tampered_blocked() {
await assertImageLoads("rc-openpgp-mdc-tampered.eml", false);
});
add_task(async function test_sigwrap_mdc_allowed() {
// Gateway signing: a top-level OpenPGP signature wrapping an integrity-
// protected encrypted child must apply the same gate -> allowed (invariant #4).
await assertImageLoads("rc-openpgp-sigwrap-mdc.eml", true);
await assertOverrideOffered("rc-openpgp-sigwrap-mdc.eml", true);
});
add_task(async function test_sigwrap_nomdc_blocked() {
// Same wrapper, but an unprotected child must stay blocked (invariant #4).
await assertImageLoads("rc-openpgp-sigwrap-nomdc.eml", false);
});
add_task(async function test_mixed_subpart_blocked() {
// The integrity-protected part is a non-top-level sub-part inside attacker
// MIME; it must not be decrypted into this document, so neither the secret nor
// its remote content appears (invariant #3).
await SpecialPowers.pushPrefEnv({
set: [[DISABLE_PREF, false]],
});
const msgc = await openEml("rc-openpgp-mixed-subpart.eml");
try {
const loaded = await TestUtils.waitForCondition(() => {
const img = remoteImage(msgc);
return img && img.complete && img.naturalWidth > 0;
}, "remote image for rc-openpgp-mixed-subpart.eml").catch(() => false);
Assert.ok(!loaded, "mixed sub-part: remote image did NOT load");
Assert.ok(
!bodyText(msgc).includes("Integrity-protected secret body"),
"encrypted sub-part is not decrypted into the top-level document"
);
} finally {
await BrowserTestUtils.closeWindow(msgc);
}
});