mirror of
https://github.com/zkemail/zk-email-verify.git
synced 2026-01-10 05:58:08 -05:00
chore: add tests for fallbackToZKEmailDNSArchive
This commit is contained in:
@@ -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
|
||||
@@ -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> {
|
||||
@@ -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(
|
||||
@@ -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,
|
||||
@@ -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) => {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -43,15 +45,15 @@ 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 user@gmail.com, 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);
|
||||
@@ -59,16 +61,14 @@ describe('DKIM signature verification', () => {
|
||||
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
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user