Skip to content

Commit

Permalink
chore: add tests for fallbackToZKEmailDNSArchive
Browse files Browse the repository at this point in the history
  • Loading branch information
saleel committed Oct 10, 2024
1 parent 8c63422 commit 4e692ff
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 51 deletions.
47 changes: 23 additions & 24 deletions packages/helpers/src/dkim/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { pki } from "node-forge";
import { DkimVerifier } from "../lib/mailauth/dkim-verifier";
import { CustomError, writeToStream } from "../lib/mailauth/tools";
import sanitizers from "./sanitizers";
import { pki } from 'node-forge';
import { DkimVerifier } from '../lib/mailauth/dkim-verifier';
import { CustomError, writeToStream } from '../lib/mailauth/tools';
import sanitizers from './sanitizers';
import { DoH, DoHServer, resolveDNSHTTP } from './dns-over-http';
import { resolveDNSFromZKEmailArchive } from "./dns-archive";
import { resolveDNSFromZKEmailArchive } from './dns-archive';

// `./mailauth` is modified version of https://github.com/postalsys/mailauth
// Main modification are including emailHeaders in the DKIM result, making it work in the browser, add types
Expand Down Expand Up @@ -34,7 +34,7 @@ export interface DKIMVerificationResult {
*/
export async function verifyDKIMSignature(
email: Buffer | string,
domain: string = "",
domain: string = '',
enableSanitization: boolean = true,
fallbackToZKEmailDNSArchive: boolean = false
): Promise<DKIMVerificationResult> {
Expand All @@ -44,17 +44,15 @@ export async function verifyDKIMSignature(

// If DKIM verification fails, try again after sanitizing email
let appliedSanitization;
if (dkimResult.status.comment === "bad signature" && enableSanitization) {
if (dkimResult.status.comment === 'bad signature' && enableSanitization) {
const results = await Promise.all(
sanitizers.map((sanitize) =>
tryVerifyDKIM(sanitize(emailStr), domain, fallbackToZKEmailDNSArchive).then((result) => ({
result,
sanitizer: sanitize.name,
}))
)
sanitizers.map((sanitize) => tryVerifyDKIM(sanitize(emailStr), domain, fallbackToZKEmailDNSArchive).then((result) => ({
result,
sanitizer: sanitize.name,
}))),
);

const passed = results.find((r) => r.result.status.result === "pass");
const passed = results.find((r) => r.result.status.result === 'pass');

if (passed) {
console.log(
Expand All @@ -75,16 +73,16 @@ export async function verifyDKIMSignature(
bodyHash,
} = dkimResult;

if (result !== "pass") {
if (result !== 'pass') {
throw new Error(
`DKIM signature verification failed for domain ${signingDomain}. Reason: ${comment}`
`DKIM signature verification failed for domain ${signingDomain}. Reason: ${comment}`,
);
}

const pubKeyData = pki.publicKeyFromPem(publicKey.toString());

return {
signature: BigInt(`0x${Buffer.from(signature, "base64").toString("hex")}`),
signature: BigInt(`0x${Buffer.from(signature, 'base64').toString('hex')}`),
headers: status.signedHeaders,
body,
bodyHash,
Expand All @@ -101,7 +99,7 @@ export async function verifyDKIMSignature(

async function tryVerifyDKIM(
email: Buffer | string,
domain: string = "",
domain: string = '',
fallbackToZKEmailDNSArchive: boolean
) {
const resolver = async (name: string, type: string) => {
Expand All @@ -110,8 +108,9 @@ async function tryVerifyDKIM(
return result;
} catch (e) {
if (fallbackToZKEmailDNSArchive) {
console.log("DNS over HTTP failed, falling back to ZK Email Archive");
return resolveDNSFromZKEmailArchive(name, type);
console.log('DNS over HTTP failed, falling back to ZK Email Archive');
const result = await resolveDNSFromZKEmailArchive(name, type);
return result;
}
throw e;
}
Expand All @@ -127,20 +126,20 @@ async function tryVerifyDKIM(
if (!domainToVerifyDKIM) {
if (dkimVerifier.headerFrom.length > 1) {
throw new Error(
"Multiple From header in email and domain for verification not specified"
'Multiple From header in email and domain for verification not specified',
);
}

domainToVerifyDKIM = dkimVerifier.headerFrom[0].split("@")[1];
domainToVerifyDKIM = dkimVerifier.headerFrom[0].split('@')[1];
}

const dkimResult = dkimVerifier.results.find(
(d: any) => d.signingDomain === domainToVerifyDKIM
(d: any) => d.signingDomain === domainToVerifyDKIM,
);

if (!dkimResult) {
throw new Error(
`DKIM signature not found for domain ${domainToVerifyDKIM}`
`DKIM signature not found for domain ${domainToVerifyDKIM}`,
);
}

Expand Down
122 changes: 95 additions & 27 deletions packages/helpers/tests/dkim.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import fs from 'fs';
import path from 'path';
import { verifyDKIMSignature } from '../src/dkim';
import fs from "fs";
import path from "path";
import { verifyDKIMSignature } from "../src/dkim";
import * as dnsOverHttp from "../src/dkim/dns-over-http";
import * as dnsArchive from "../src/dkim/dns-archive";

jest.setTimeout(10000);

describe('DKIM signature verification', () => {
it('should pass for valid email', async () => {
describe("DKIM signature verification", () => {
it("should pass for valid email", async () => {
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-good.eml'),
path.join(__dirname, "test-data/email-good.eml")
);

const result = await verifyDKIMSignature(email);

expect(result.signingDomain).toBe('icloud.com');
expect(result.signingDomain).toBe("icloud.com");
expect(result.appliedSanitization).toBeFalsy();
});

it('should fail for invalid selector', async () => {
it("should fail for invalid selector", async () => {
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-invalid-selector.eml'),
path.join(__dirname, "test-data/email-invalid-selector.eml")
);

expect.assertions(1);
Expand All @@ -27,14 +29,14 @@ describe('DKIM signature verification', () => {
await verifyDKIMSignature(email);
} catch (e) {
expect(e.message).toBe(
'DKIM signature verification failed for domain icloud.com. Reason: no key',
"DKIM signature verification failed for domain icloud.com. Reason: no key"
);
}
});

it('should fail for tampered body', async () => {
it("should fail for tampered body", async () => {
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-body-tampered.eml'),
path.join(__dirname, "test-data/email-body-tampered.eml")
);

expect.assertions(1);
Expand All @@ -43,32 +45,30 @@ describe('DKIM signature verification', () => {
await verifyDKIMSignature(email);
} catch (e) {
expect(e.message).toBe(
'DKIM signature verification failed for domain icloud.com. Reason: body hash did not verify',
"DKIM signature verification failed for domain icloud.com. Reason: body hash did not verify"
);
}
});

it('should fail for when DKIM signature is not present for domain', async () => {
it("should fail for when DKIM signature is not present for domain", async () => {
// In this email From address is [email protected], but the DKIM signature is only for icloud.com
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-invalid-domain.eml'),
path.join(__dirname, "test-data/email-invalid-domain.eml")
);

expect.assertions(1);

try {
await verifyDKIMSignature(email);
} catch (e) {
expect(e.message).toBe(
'DKIM signature not found for domain gmail.com',
);
expect(e.message).toBe("DKIM signature not found for domain gmail.com");
}
});

it('should be able to override domain', async () => {
it("should be able to override domain", async () => {
// From address domain is icloud.com
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-different-domain.eml'),
path.join(__dirname, "test-data/email-different-domain.eml")
);

// Should pass with default domain
Expand All @@ -79,26 +79,94 @@ describe('DKIM signature verification', () => {
// different from From domain and the below check pass.
expect.assertions(1);
try {
await verifyDKIMSignature(email, 'domain.com');
await verifyDKIMSignature(email, "domain.com");
} catch (e) {
expect(e.message).toBe("DKIM signature not found for domain domain.com");
}
});

it("should fallback to ZK Email Archive if DNS over HTTP fails", async () => {
const email = fs.readFileSync(
path.join(__dirname, "test-data/email-good.eml")
);

// Mock resolveDNSHTTP to throw an error just for this test
const mockResolveDNSHTTP = jest
.spyOn(dnsOverHttp, "resolveDNSHTTP")
.mockRejectedValue(new Error("Failed due to mock"));

const consoleSpy = jest.spyOn(console, "log");
await verifyDKIMSignature(email, "icloud.com", true, true);

// Check if the error was logged to ensure fallback to ZK Email Archive happened
expect(consoleSpy).toHaveBeenCalledWith(
"DNS over HTTP failed, falling back to ZK Email Archive"
);

mockResolveDNSHTTP.mockRestore();
});

it("should fail on DNS over HTTP failure if fallback is not enabled", async () => {
const email = fs.readFileSync(
path.join(__dirname, "test-data/email-good.eml")
);

// Mock resolveDNSHTTP to throw an error just for this test
const mockResolveDNSHTTP = jest
.spyOn(dnsOverHttp, "resolveDNSHTTP")
.mockRejectedValue(new Error("Failed due to mock"));

expect.assertions(1);
try {
await verifyDKIMSignature(email, "icloud.com", true, false);
} catch (e) {
expect(e.message).toBe(
'DKIM signature not found for domain domain.com',
"DKIM signature verification failed for domain icloud.com. Reason: DNS failure: Failed due to mock"
);
}
mockResolveDNSHTTP.mockRestore();
});

it("should fail if both DNS over HTTP and ZK Email Archive fail", async () => {
const email = fs.readFileSync(
path.join(__dirname, "test-data/email-good.eml")
);

const mockResolveDNSHTTP = jest
.spyOn(dnsOverHttp, "resolveDNSHTTP")
.mockRejectedValue(new Error("Failed due to mock"));

const mockResolveDNSFromZKEmailArchive = jest
.spyOn(dnsArchive, "resolveDNSFromZKEmailArchive")
.mockRejectedValue(new Error("Failed due to mock"));

expect.assertions(1);
try {
await verifyDKIMSignature(email, "icloud.com", true, false);
} catch (e) {
expect(e.message).toBe(
"DKIM signature verification failed for domain icloud.com. Reason: DNS failure: Failed due to mock"
);
}

mockResolveDNSHTTP.mockRestore();
mockResolveDNSFromZKEmailArchive.mockRestore();
});
});

describe('DKIM with sanitization', () => {
it('should pass after removing label from Subject', async () => {
describe("DKIM with sanitization", () => {
it("should pass after removing label from Subject", async () => {
const email = fs.readFileSync(
path.join(__dirname, 'test-data/email-good.eml'),
path.join(__dirname, "test-data/email-good.eml")
);

// Add a label to the subject
const tamperedEmail = email.toString().replace('Subject: ', 'Subject: [EmailListABC]');
const tamperedEmail = email
.toString()
.replace("Subject: ", "Subject: [EmailListABC]");

const result = await verifyDKIMSignature(tamperedEmail);

expect(result.appliedSanitization).toBe('removeLabels');
expect(result.appliedSanitization).toBe("removeLabels");
});
});

0 comments on commit 4e692ff

Please sign in to comment.