From 7d61dacd6f91c3fbfbb2151dc14c50d3315dbb40 Mon Sep 17 00:00:00 2001 From: dodmi <4572946+dodmi@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:51:29 +0100 Subject: [PATCH] Backported simple support for BIMI indicators --- modules/AuthVerifier.jsm.js | 31 ++++++++---- modules/bimi.jsm.js | 96 +++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 10 deletions(-) create mode 100644 modules/bimi.jsm.js diff --git a/modules/AuthVerifier.jsm.js b/modules/AuthVerifier.jsm.js index 7758f10..8e01a56 100644 --- a/modules/AuthVerifier.jsm.js +++ b/modules/AuthVerifier.jsm.js @@ -15,8 +15,8 @@ // options for ESLint /* global Components, Services, MailServices */ -/* global Logging, ARHParser */ -/* global PREF, dkimStrings, domainIsInDomain, getDomainFromAddr, tryGetFormattedString, DKIM_InternalError */ +/* global Logging, ARHParser, BIMI */ +/* global PREF, dkimStrings, domainIsInDomain, getDomainFromAddr, tryGetFormattedString, DKIM_InternalError, addrIsInDomain */ /* exported EXPORTED_SYMBOLS, AuthVerifier */ "use strict"; @@ -43,6 +43,7 @@ Cu.import("resource://dkim_verifier/logging.jsm.js"); Cu.import("resource://dkim_verifier/helper.jsm.js"); Cu.import("resource://dkim_verifier/MsgReader.jsm.js"); Cu.import("resource://dkim_verifier/ARHParser.jsm.js"); +Cu.import("resource://dkim_verifier/bimi.jsm.js"); // @ts-ignore let DKIM = {}; Cu.import("resource://dkim_verifier/dkimPolicy.jsm.js", DKIM); @@ -70,12 +71,13 @@ let prefs = Services.prefs.getBranch(PREF_BRANCH); /** * @typedef {Object} SavedAuthResult|SavedAuthResultV3 * @property {String} version - * result version ("3.0") + * result version ("3.1") * @property {dkimSigResultV2[]} dkim * @property {ARHResinfo[]} [spf] * @property {ARHResinfo[]} [dmarc] * @property {Object} [arh] * @property {dkimSigResultV2[]} [arh.dkim] + * @property {string|undefined} [bimiIndicator] Since version 3.1 */ /** @@ -149,13 +151,14 @@ var AuthVerifier = { savedAuthResult = arhResult; } else { savedAuthResult = { - version: "3.0", + version: "3.1", dkim: [], spf: arhResult.spf, dmarc: arhResult.dmarc, arh: { dkim: arhResult.dkim }, + bimiIndicator: arhResult.bimiIndicator, }; } } else { @@ -260,6 +263,7 @@ function getARHResult(msgHdr, msg) { let arhDKIM = []; let arhSPF = []; let arhDMARC = []; + let arhBIMI = []; for (let i = 0; i < msg.headerFields.get("authentication-results").length; i++) { let arh; try { @@ -286,6 +290,7 @@ function getARHResult(msgHdr, msg) { arhDKIM = arhDKIM.concat(arh.resinfo.filter(e => e.method === "dkim")); arhSPF = arhSPF.concat(arh.resinfo.filter(e => e.method === "spf")); arhDMARC = arhDMARC.concat(arh.resinfo.filter(e => e.method === "dmarc")); + arhBIMI = arhBIMI.concat(arh.resinfo.filter(e => e.method === "bimi")); } // convert DKIM results @@ -345,12 +350,13 @@ function getARHResult(msgHdr, msg) { // sort signatures DKIM.Verifier.sortSignatures(msg, dkimSigResults); - + let savedAuthResult = { - version: "3.0", + version: "3.1", dkim: dkimSigResults, spf: arhSPF, dmarc: arhDMARC, + bimiIndicator: BIMI.getBimiIndicator(msg.headerFields, arhBIMI) || undefined, }; log.debug("ARH result:", savedAuthResult); return savedAuthResult; @@ -701,7 +707,7 @@ async function SavedAuthResult_to_AuthResult(savedAuthResult, from) { // eslint- authResult.arh.dkim = authResult.arh.dkim.map( dkimSigResultV2_to_AuthResultDKIM); } - return addFavicons(authResult, from); + return addFavicons(authResult, from, savedAuthResult.bimiIndicator); } /** @@ -724,16 +730,21 @@ function AuthResultDKIMV2_to_dkimSigResultV2(authResultDKIM) { * * @param {AuthResult} authResult * @param {String|undefined} from + * @param {String|undefined} bimiIndicator * @return {Promise} authResult */ -async function addFavicons(authResult, from) { +async function addFavicons(authResult, from, bimiIndicator) { if (!prefs.getBoolPref("display.favicon.show")) { return authResult; } for (let i = 0; i < authResult.dkim.length; i++) { if (authResult.dkim[i].sdid) { - authResult.dkim[i].favicon = - await DKIM.Policy.getFavicon(authResult.dkim[i].sdid, authResult.dkim[i].auid, from); + if (bimiIndicator && from && addrIsInDomain(from, authResult.dkim[i].sdid)) { + authResult.dkim[i].favicon = `data:image/svg+xml;base64,${bimiIndicator}`; + } else { + authResult.dkim[i].favicon = + await DKIM.Policy.getFavicon(authResult.dkim[i].sdid, authResult.dkim[i].auid, from); + } } } return authResult; diff --git a/modules/bimi.jsm.js b/modules/bimi.jsm.js new file mode 100644 index 0000000..11c1932 --- /dev/null +++ b/modules/bimi.jsm.js @@ -0,0 +1,96 @@ +/** + * Brand Indicators for Message Identification (BIMI). + * https://datatracker.ietf.org/doc/draft-brand-indicators-for-message-identification/04/ + * + * BIMI implementation for a Mail User Agent (MUA). + * Gets the BIMI Indicator based on the information the receiving + * Mail Transfer Agent (MTA) writes into the headers of the message. + * + * This is not a complete implementation of BIMI. + * + * Copyright (c) 2023 Philippe Lieser + * + * This software is licensed under the terms of the MIT License. + * + * The above copyright and license notice shall be + * included in all copies or substantial portions of the Software. + */ + +// options for ESLint +/* global Components */ +/* global Logging */ +/* exported EXPORTED_SYMBOLS, BIMI */ + +"use strict"; + +var EXPORTED_SYMBOLS = [ + "BIMI" +]; + +// @ts-ignore +const Cu = Components.utils; + +Cu.import("resource://dkim_verifier/logging.jsm.js"); +Cu.import("resource://dkim_verifier/ARHParser.jsm.js"); + +let BIMI = (function() { + + const log = Logging.getLogger("BIMI"); + + // WSP as specified in Appendix B.1 of RFC 5234 + const WSP_p = "[ \t]"; + // obs-FWS as specified in Section 4.2 of RFC 5322 + const obs_FWS_p = `(?:${WSP_p}+(?:\r\n${WSP_p}+)*)`; + // FWS as specified in Section 3.2.2 of RFC 5322 + const FWS_p = `(?:(?:(?:${WSP_p}*\r\n)?${WSP_p}+)|${obs_FWS_p})`; + + let that = { + /** + * Try to get the BIMI Indicator if available. + * + * @param {Map} headers + * @param {ARHResinfo[]} arhBIMI - Trusted ARHs containing a BIMI result. + * @returns {string|null} + */ + getBimiIndicator: function(headers, arhBIMI) { + // Assuming: + // 1. We only get ARHs that can be trusted (i.e. from the receiving MTA). + // 2. If the receiving MTA does not supports BIMI, + // we will not see an ARH with a BIMI result (because of 1) + // 3. If the receiving MTA supports BIMI, + // it will make sure we only see his BIMI-Indicator headers (as required by the RFC). + // + // Given the above, it should be safe to trust the BIMI indicator from the BIMI-Indicator header + // if we have a passing BIMI result there the MTA claims to have checked the Authority Evidence. + const hasAuthorityPassBIMI = arhBIMI.some( + arh => arh.method === "bimi" && + arh.result === "pass" && + arh.propertys.policy.authority === "pass" + ); + if (!hasAuthorityPassBIMI) { + return null; + } + + const bimiIndicators = headers.get("bimi-indicator") || []; + if (bimiIndicators.length > 1) { + log.warn("Message contains more than one BIMI-Indicator header"); + return null; + } + let bimiIndicator = bimiIndicators[0]; + if (!bimiIndicator) { + log.warn("Message contains an ARH with passing BIMI but does not have a BIMI-Indicator header"); + return null; + } + + // TODO: If in the future we support ARC we might want to check the policy.indicator-hash + + // Remove header name and new line at end + bimiIndicator = bimiIndicator.slice("bimi-indicator:".length, -"\r\n".length); + // Remove all whitespace + bimiIndicator = bimiIndicator.replace(new RegExp(`${FWS_p}`, "g"), ""); + + return bimiIndicator; + } + }; + return that; +}()); \ No newline at end of file