chore: add dkim verification with arc handling

This commit is contained in:
Saleel
2024-01-17 00:15:51 +05:30
parent c708a2d8df
commit 2955d935e8
6 changed files with 585 additions and 20 deletions

View File

@@ -16,8 +16,10 @@
"libmime": "^5.2.1",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"mailauth": "^4.6.0",
"node-forge": "^1.3.1",
"pako": "^2.1.0",
"pki": "^1.1.0",
"psl": "^1.9.0",
"snarkjs": "https://github.com/sampritipanda/snarkjs.git#fef81fc51d17a734637555c6edbd585ecda02d9e"
},

View File

@@ -0,0 +1,103 @@
import { authenticate } from "mailauth";
import { pki } from "node-forge";
interface DKIMVerificationResult {
publicKey: bigint;
signature: bigint;
message: Buffer;
bodyHash: string;
signingDomain: string;
selector: string;
algo: string;
format: string;
modulusLength: number;
}
export async function verifyDKIMSignature(
email: string,
domain: string = "",
tryRevertForwarderChanges: boolean = true
): Promise<DKIMVerificationResult> {
let dkimResult = await tryVerifyDKIM(email, domain);
if (dkimResult.status.result !== "pass" && tryRevertForwarderChanges) {
const modified = await revertForwarderChanges(email);
dkimResult = await tryVerifyDKIM(modified, domain);
}
if (dkimResult.status.result !== "pass") {
throw new Error(
`DKIM signature verification failed for domain ${dkimResult.signingDomain}`
);
}
return {
signature: BigInt("0x" + Buffer.from(dkimResult.signature, "base64").toString("hex")),
message: Buffer.from(dkimResult.signingHeaders.canonicalizedHeader, "base64"),
bodyHash: dkimResult.bodyHash,
signingDomain: dkimResult.signingDomain,
publicKey: BigInt(pki.publicKeyFromPem(dkimResult.publicKey).n.toString()),
selector: dkimResult.selector,
algo: dkimResult.algo,
format: dkimResult.format,
modulusLength: dkimResult.modulusLength,
};
}
async function tryVerifyDKIM(email: string, domain: string = "") {
const authResult = await authenticate(email, {
disableArc: true,
disableDmarc: true,
disableBimi: true,
});
const { dkim } = authResult;
let domainToVerifyDKIM = domain;
if (!domainToVerifyDKIM) {
if (dkim.headerFrom.length > 1) {
throw new Error(
"Multiple From header in email and domain for verification not specified"
);
}
domainToVerifyDKIM = dkim.headerFrom[0].split("@")[1];
}
const dkimResult = dkim.results.find(
(d: any) => d.signingDomain === domainToVerifyDKIM
);
if (!dkimResult) {
throw new Error(
`DKIM signature not found for domain ${domainToVerifyDKIM}`
);
}
return dkimResult;
}
function getHeaderValue(email: string, header: string) {
const headerStartIndex = email.indexOf(`${header}: `) + header.length + 2;
const headerEndIndex = email.indexOf("\n", headerStartIndex);
const headerValue = email.substring(headerStartIndex, headerEndIndex);
return headerValue;
}
function setHeaderValue(email: string, header: string, value: string) {
return email.replace(getHeaderValue(email, header), value);
}
async function revertForwarderChanges(email: string) {
// Google sets their own Message-ID and put the original one in X-Google-Original-Message-ID when forwarding
const googleReplacedMessageId = getHeaderValue(
email,
"X-Google-Original-Message-ID"
);
if (googleReplacedMessageId) {
email = setHeaderValue(email, "Message-ID", googleReplacedMessageId);
}
return email;
}

View File

@@ -1,3 +1,4 @@
export * from "./dkim";
export * from "./binaryFormat";
export * from "./constants";
export * from "./fast-sha256";

View File

@@ -13,6 +13,10 @@
"module": "commonjs",
"declaration": true,
"baseUrl": "./src",
"outDir": "dist"
}
"outDir": "dist",
"typeRoots": [
"./types",
"../../node_modules/@types"
],
},
}

257
packages/helpers/types/mailauth.d.ts vendored Normal file
View File

@@ -0,0 +1,257 @@
declare module "mailauth" {
export interface MailAuthResult {
dkim: Dkim;
spf: Spf;
dmarc: Dmarc;
arc: Arc;
bimi: Bimi;
receivedChain: ReceivedChain[];
headers: string;
}
export interface Dkim {
headerFrom: string[];
envelopeFrom: string;
results: Result[];
}
export interface Result {
signingDomain: string;
selector: string;
signature: string;
algo: string;
format: string;
bodyHash: string;
bodyHashExpecting: string;
signingHeaders: SigningHeaders;
status: Status;
canonBodyLength: number;
publicKey: string;
modulusLength: number;
rr: string;
info: string;
}
export interface SigningHeaders {
keys: string;
headers: string[];
canonicalizedHeader: string;
}
export interface Status {
result: string;
comment: string;
header: Header;
aligned: any;
}
export interface Header {
i: string;
s: string;
a: string;
b: string;
}
export interface Spf {
domain: string;
"envelope-from": string;
status: Status2;
header: string;
info: string;
lookups: Lookups;
}
export interface Status2 {
result: string;
comment: string;
smtp: Smtp;
}
export interface Smtp {
mailfrom: string;
}
export interface Lookups {
limit: number;
count: number;
void: number;
subqueries: Subqueries;
}
export interface Subqueries {}
export interface Dmarc {
status: Status3;
domain: string;
policy: string;
p: string;
sp: string;
rr: string;
alignment: Alignment;
info: string;
}
export interface Status3 {
result: string;
comment: string;
header: Header2;
}
export interface Header2 {
from: string;
d: string;
}
export interface Alignment {
spf: Spf2;
dkim: Dkim2;
}
export interface Spf2 {
result: boolean;
strict: boolean;
}
export interface Dkim2 {
result: boolean;
strict: boolean;
}
export interface Arc {
status: Status4;
i: number;
signature: Signature;
authenticationResults: AuthenticationResults;
info: string;
authResults: string;
}
export interface Status4 {
result: string;
comment: string;
}
export interface Signature {
signingDomain: string;
selector: string;
signature: string;
algo: string;
format: string;
bodyHash: string;
bodyHashExpecting: string;
signingHeaders: SigningHeaders2;
status: Status5;
canonBodyLength: number;
publicKey: string;
modulusLength: number;
rr: string;
}
export interface SigningHeaders2 {
keys: string;
headers: string[];
canonicalizedHeader: string;
}
export interface Status5 {
result: string;
header: Header3;
}
export interface Header3 {
i: string;
s: string;
a: string;
b: string;
}
export interface AuthenticationResults {
mta: string;
dkim: Dkim3[];
spf: Spf3;
dmarc: Dmarc2;
}
export interface Dkim3 {
result: string;
header: Header4;
}
export interface Header4 {
i: string;
s: string;
b: string;
}
export interface Spf3 {
result: string;
smtp: Smtp2;
comment: string;
}
export interface Smtp2 {
mailfrom: string;
}
export interface Dmarc2 {
result: string;
header: Header5;
comment: string;
}
export interface Header5 {
from: string;
}
export interface Bimi {
status: Status6;
info: string;
}
export interface Status6 {
header: Header6;
result: string;
comment: string;
}
export interface Header6 {}
export interface ReceivedChain {
by: By;
with: With;
id: Id;
timestamp: string;
full: string;
from?: From;
tls?: Tls;
for?: For;
}
export interface By {
value: string;
}
export interface With {
value: string;
}
export interface Id {
value: string;
}
export interface From {
value: string;
comment: string;
}
export interface Tls {
value: string;
comment: string;
}
export interface For {
value: string;
}
export function authenticate(email: string, options: any) : Promise<MailAuthResult>;
}

234
yarn.lock
View File

@@ -2403,6 +2403,13 @@ __metadata:
languageName: node
linkType: hard
"@fastify/busboy@npm:^2.0.0":
version: 2.1.0
resolution: "@fastify/busboy@npm:2.1.0"
checksum: 3233abd10f73e50668cb4bb278a79b7b3fadd30215ac6458299b0e5a09a29c3586ec07597aae6bd93f5cbedfcef43a8aeea51829cd28fc13850cdbcd324c28d5
languageName: node
linkType: hard
"@gar/promisify@npm:^1.1.3":
version: 1.1.3
resolution: "@gar/promisify@npm:1.1.3"
@@ -3052,6 +3059,77 @@ __metadata:
languageName: node
linkType: hard
"@peculiar/asn1-schema@npm:2.3.0":
version: 2.3.0
resolution: "@peculiar/asn1-schema@npm:2.3.0"
dependencies:
asn1js: ^3.0.5
pvtsutils: ^1.3.2
tslib: ^2.4.0
checksum: aa510c68de83be94a8d0e96ca1f7c92d2bd790867eed887c0db32d6b2fc49c5dd9d8269648e9665686ba9b2fd8469324c61e04fed50f7aad3f68a0ca0e1cde4b
languageName: node
linkType: hard
"@peculiar/asn1-schema@npm:^2.3.0, @peculiar/asn1-schema@npm:^2.3.8":
version: 2.3.8
resolution: "@peculiar/asn1-schema@npm:2.3.8"
dependencies:
asn1js: ^3.0.5
pvtsutils: ^1.3.5
tslib: ^2.6.2
checksum: 1f4dd421f1411df8bc52bca12b1cef710434c13ff0a8b5746ede42b10d62b5ad06a3925c4a6db53102aaf1e589947539a6955fa8554a9b8ebb1ffa38b0155a24
languageName: node
linkType: hard
"@peculiar/asn1-x509-logotype@npm:2.3.0":
version: 2.3.0
resolution: "@peculiar/asn1-x509-logotype@npm:2.3.0"
dependencies:
"@peculiar/asn1-schema": ^2.3.0
"@peculiar/asn1-x509": ^2.3.0
asn1js: ^3.0.5
tslib: ^2.4.0
checksum: 657eda43eb264b4593e346d2ae6a0d65969dd44977ef2a92bb4ccbb4c45a0a2c5808e060cc25d5964ab931934cbbeb9303e0d440f5b189c05d6f61be010885e8
languageName: node
linkType: hard
"@peculiar/asn1-x509@npm:2.3.0":
version: 2.3.0
resolution: "@peculiar/asn1-x509@npm:2.3.0"
dependencies:
"@peculiar/asn1-schema": ^2.3.0
asn1js: ^3.0.5
ipaddr.js: ^2.0.1
pvtsutils: ^1.3.2
tslib: ^2.4.0
checksum: 9feb2ed049a8fdf836f14298f0922eb28db8bff99cd3ac08f973883d017f17303c3ae243457c8eaa73795bf05c46b7d3555dcf6729cff2a51b134f3f6e9784ec
languageName: node
linkType: hard
"@peculiar/asn1-x509@npm:^2.3.0":
version: 2.3.8
resolution: "@peculiar/asn1-x509@npm:2.3.8"
dependencies:
"@peculiar/asn1-schema": ^2.3.8
asn1js: ^3.0.5
ipaddr.js: ^2.1.0
pvtsutils: ^1.3.5
tslib: ^2.6.2
checksum: 23856e5d024298447afca55bd68d19a7440c0ae076437aee5ced26a0fa2e4efa3e0e4a354fa6ee9968d62ac21ee1c2186fc427942bacfc824d3a3a4d2e80d14b
languageName: node
linkType: hard
"@postalsys/vmc@npm:1.0.6":
version: 1.0.6
resolution: "@postalsys/vmc@npm:1.0.6"
dependencies:
"@peculiar/asn1-schema": 2.3.0
"@peculiar/asn1-x509": 2.3.0
"@peculiar/asn1-x509-logotype": 2.3.0
checksum: 2ffa498326cdcaf980721daf28246741c049f5855ce20c6150346f593827d5529f611c777818ba9ded326dbe45d7fd74ba9ba84b66f8f0311e7c2c59f341746b
languageName: node
linkType: hard
"@puppeteer/browsers@npm:1.7.1":
version: 1.7.1
resolution: "@puppeteer/browsers@npm:1.7.1"
@@ -4784,9 +4862,11 @@ __metadata:
libmime: ^5.2.1
localforage: ^1.10.0
lodash: ^4.17.21
mailauth: ^4.6.0
msw: ^1.2.2
node-forge: ^1.3.1
pako: ^2.1.0
pki: ^1.1.0
psl: ^1.9.0
snarkjs: "https://github.com/sampritipanda/snarkjs.git#fef81fc51d17a734637555c6edbd585ecda02d9e"
languageName: unknown
@@ -5311,6 +5391,17 @@ __metadata:
languageName: node
linkType: hard
"asn1js@npm:^3.0.5":
version: 3.0.5
resolution: "asn1js@npm:3.0.5"
dependencies:
pvtsutils: ^1.3.2
pvutils: ^1.1.3
tslib: ^2.4.0
checksum: 3b6af1bbadd5762ef8ead5daf2f6bda1bc9e23bc825c4dcc996aa1f9521ad7390a64028565d95d98090d69c8431f004c71cccb866004759169d7c203cf9075eb
languageName: node
linkType: hard
"assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0":
version: 1.0.0
resolution: "assert-plus@npm:1.0.0"
@@ -8597,6 +8688,17 @@ __metadata:
languageName: node
linkType: hard
"fast-xml-parser@npm:4.3.2":
version: 4.3.2
resolution: "fast-xml-parser@npm:4.3.2"
dependencies:
strnum: ^1.0.5
bin:
fxparser: src/cli/cli.js
checksum: d507ce2efa5fd13d0a5ba28bd76dd68f2fc30ad8748357c37b70f360d19417866d79e35a688af067d5bceaaa796033fa985206aef9692f7a421e1326b6e73309
languageName: node
linkType: hard
"fastest-stable-stringify@npm:^2.0.2":
version: 2.0.2
resolution: "fastest-stable-stringify@npm:2.0.2"
@@ -9846,6 +9948,13 @@ __metadata:
languageName: node
linkType: hard
"ipaddr.js@npm:2.1.0, ipaddr.js@npm:^2.0.1, ipaddr.js@npm:^2.1.0":
version: 2.1.0
resolution: "ipaddr.js@npm:2.1.0"
checksum: 807a054f2bd720c4d97ee479d6c9e865c233bea21f139fb8dabd5a35c4226d2621c42e07b4ad94ff3f82add926a607d8d9d37c625ad0319f0e08f9f2bd1968e2
languageName: node
linkType: hard
"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1":
version: 1.1.1
resolution: "is-arguments@npm:1.1.1"
@@ -10994,6 +11103,19 @@ __metadata:
languageName: node
linkType: hard
"joi@npm:17.11.0":
version: 17.11.0
resolution: "joi@npm:17.11.0"
dependencies:
"@hapi/hoek": ^9.0.0
"@hapi/topo": ^5.0.0
"@sideway/address": ^4.1.3
"@sideway/formula": ^3.0.1
"@sideway/pinpoint": ^2.0.0
checksum: 3a4e9ecba345cdafe585e7ed8270a44b39718e11dff3749aa27e0001a63d578b75100c062be28e6f48f960b594864034e7a13833f33fbd7ad56d5ce6b617f9bf
languageName: node
linkType: hard
"joi@npm:^17.7.0":
version: 17.9.2
resolution: "joi@npm:17.9.2"
@@ -11466,7 +11588,7 @@ __metadata:
languageName: node
linkType: hard
"libmime@npm:^5.2.1":
"libmime@npm:5.2.1, libmime@npm:^5.2.1":
version: 5.2.1
resolution: "libmime@npm:5.2.1"
dependencies:
@@ -11757,6 +11879,26 @@ __metadata:
languageName: node
linkType: hard
"mailauth@npm:^4.6.0":
version: 4.6.0
resolution: "mailauth@npm:4.6.0"
dependencies:
"@postalsys/vmc": 1.0.6
fast-xml-parser: 4.3.2
ipaddr.js: 2.1.0
joi: 17.11.0
libmime: 5.2.1
nodemailer: 6.9.7
psl: 1.9.0
punycode: 2.3.1
undici: 5.27.0
yargs: 17.7.2
bin:
mailauth: bin/mailauth.js
checksum: 1c656d4d5237cb6d39a3e8fcd4cd83565da5348fe4dd9709576f3cbc0bce3f91644258b67d55d887563cbc81ff377a3909c9345227979a5e8da71f60e6627449
languageName: node
linkType: hard
"make-dir@npm:^3.0.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
@@ -12519,6 +12661,13 @@ __metadata:
languageName: node
linkType: hard
"nodemailer@npm:6.9.7":
version: 6.9.7
resolution: "nodemailer@npm:6.9.7"
checksum: 0cf66d27aed3bd2cbdff9939402cec3d2119c31b2b9ff4af3bcd59f48287ea75b90c0ce2cd9eb0df838164972cd25581b4b723c91fd673e2608bcb28445ccb1b
languageName: node
linkType: hard
"nopt@npm:^6.0.0":
version: 6.0.0
resolution: "nopt@npm:6.0.0"
@@ -13139,6 +13288,16 @@ __metadata:
languageName: node
linkType: hard
"pki@npm:^1.1.0":
version: 1.1.0
resolution: "pki@npm:1.1.0"
peerDependencies:
"@pulumi/kubernetes": ^0.25.6
"@pulumi/pulumi": ^0.17.28
checksum: 31b4ddc413be8508e816204d0eb52597ad23b02c6c0713b486967de6e7e4c42b560d1a8422702d32374c613c9302e6061a35ffbd1c1c5986d546798d775927a9
languageName: node
linkType: hard
"pluralize@npm:^8.0.0":
version: 8.0.0
resolution: "pluralize@npm:8.0.0"
@@ -13420,7 +13579,7 @@ __metadata:
languageName: node
linkType: hard
"psl@npm:^1.1.28, psl@npm:^1.1.33, psl@npm:^1.9.0":
"psl@npm:1.9.0, psl@npm:^1.1.28, psl@npm:^1.1.33, psl@npm:^1.9.0":
version: 1.9.0
resolution: "psl@npm:1.9.0"
checksum: 20c4277f640c93d393130673f392618e9a8044c6c7bf61c53917a0fddb4952790f5f362c6c730a9c32b124813e173733f9895add8d26f566ed0ea0654b2e711d
@@ -13451,6 +13610,13 @@ __metadata:
languageName: node
linkType: hard
"punycode@npm:2.3.1":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
checksum: bb0a0ceedca4c3c57a9b981b90601579058903c62be23c5e8e843d2c2d4148a3ecf029d5133486fb0e1822b098ba8bba09e89d6b21742d02fa26bda6441a6fb2
languageName: node
linkType: hard
"punycode@npm:^1.3.2, punycode@npm:^1.4.1":
version: 1.4.1
resolution: "punycode@npm:1.4.1"
@@ -13525,6 +13691,22 @@ __metadata:
languageName: node
linkType: hard
"pvtsutils@npm:^1.3.2, pvtsutils@npm:^1.3.5":
version: 1.3.5
resolution: "pvtsutils@npm:1.3.5"
dependencies:
tslib: ^2.6.1
checksum: e734516b3cb26086c18bd9c012fefe818928a5073178842ab7e62885a090f1dd7bda9c7bb8cd317167502cb8ec86c0b1b0ccd71dac7ab469382a4518157b0d12
languageName: node
linkType: hard
"pvutils@npm:^1.1.3":
version: 1.1.3
resolution: "pvutils@npm:1.1.3"
checksum: 2ee26a9e5176c348977d6ec00d8ee80bff62f51743b1c5fe8abeeb4c5d29d9959cdfe0ce146707a9e6801bce88190fed3002d720b072dc87d031c692820b44c9
languageName: node
linkType: hard
"qrcode@npm:1.5.0":
version: 1.5.0
resolution: "qrcode@npm:1.5.0"
@@ -15241,6 +15423,13 @@ __metadata:
languageName: node
linkType: hard
"strnum@npm:^1.0.5":
version: 1.0.5
resolution: "strnum@npm:1.0.5"
checksum: 651b2031db5da1bf4a77fdd2f116a8ac8055157c5420f5569f64879133825915ad461513e7202a16d7fec63c54fd822410d0962f8ca12385c4334891b9ae6dd2
languageName: node
linkType: hard
"styled-components@npm:^5.3.5":
version: 5.3.11
resolution: "styled-components@npm:5.3.11"
@@ -15728,7 +15917,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.1, tslib@npm:^2.3.1":
"tslib@npm:^2.0.1, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.6.1, tslib@npm:^2.6.2":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
@@ -15908,6 +16097,15 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:5.27.0":
version: 5.27.0
resolution: "undici@npm:5.27.0"
dependencies:
"@fastify/busboy": ^2.0.0
checksum: 3acad25bfe5957aa5edc24eb160b5da7a9c67a5061e2e001929bef4bafed07d93a2accb36d407179c35b3ae56adbe89b49e1dd80d8cea9fdc44dca2037174330
languageName: node
linkType: hard
"unicode-canonical-property-names-ecmascript@npm:^2.0.0":
version: 2.0.0
resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0"
@@ -17060,6 +17258,21 @@ __metadata:
languageName: node
linkType: hard
"yargs@npm:17.7.2, yargs@npm:^17.3.1, yargs@npm:^17.5.1":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
cliui: ^8.0.1
escalade: ^3.1.1
get-caller-file: ^2.0.5
require-directory: ^2.1.1
string-width: ^4.2.3
y18n: ^5.0.5
yargs-parser: ^21.1.1
checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a
languageName: node
linkType: hard
"yargs@npm:^15.3.1":
version: 15.4.1
resolution: "yargs@npm:15.4.1"
@@ -17079,21 +17292,6 @@ __metadata:
languageName: node
linkType: hard
"yargs@npm:^17.3.1, yargs@npm:^17.5.1":
version: 17.7.2
resolution: "yargs@npm:17.7.2"
dependencies:
cliui: ^8.0.1
escalade: ^3.1.1
get-caller-file: ^2.0.5
require-directory: ^2.1.1
string-width: ^4.2.3
y18n: ^5.0.5
yargs-parser: ^21.1.1
checksum: 73b572e863aa4a8cbef323dd911d79d193b772defd5a51aab0aca2d446655216f5002c42c5306033968193bdbf892a7a4c110b0d77954a7fdf563e653967b56a
languageName: node
linkType: hard
"yauzl@npm:^2.10.0":
version: 2.10.0
resolution: "yauzl@npm:2.10.0"