Merge pull request #251 from zkemail/optional_bh_check

Add skipBodyHash check flag in verifyDkimSignature function
This commit is contained in:
Yush G
2025-02-18 17:47:11 -08:00
committed by GitHub
5 changed files with 50 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@zk-email/helpers", "name": "@zk-email/helpers",
"version": "6.3.2", "version": "6.4.2",
"license": "MIT", "license": "MIT",
"main": "dist", "main": "dist",
"scripts": { "scripts": {

View File

@@ -30,6 +30,7 @@ export interface DKIMVerificationResult {
* @param enableSanitization If true, email will be applied with various sanitization to try and pass DKIM verification * @param enableSanitization If true, email will be applied with various sanitization to try and pass DKIM verification
* @param fallbackToZKEmailDNSArchive If true, ZK Email DNS Archive (https://archive.prove.email/api-explorer) will * @param fallbackToZKEmailDNSArchive If true, ZK Email DNS Archive (https://archive.prove.email/api-explorer) will
* be used to resolve DKIM public keys if we cannot resolve from HTTP DNS * be used to resolve DKIM public keys if we cannot resolve from HTTP DNS
* @param skipBodyHash If true, it bypass the dkim body hash check
* @returns * @returns
*/ */
export async function verifyDKIMSignature( export async function verifyDKIMSignature(
@@ -37,17 +38,18 @@ export async function verifyDKIMSignature(
domain: string = '', domain: string = '',
enableSanitization: boolean = true, enableSanitization: boolean = true,
fallbackToZKEmailDNSArchive: boolean = false, fallbackToZKEmailDNSArchive: boolean = false,
skipBodyHash = false,
): Promise<DKIMVerificationResult> { ): Promise<DKIMVerificationResult> {
const emailStr = email.toString(); const emailStr = email.toString();
let dkimResult = await tryVerifyDKIM(email, domain, fallbackToZKEmailDNSArchive); let dkimResult = await tryVerifyDKIM(email, domain, fallbackToZKEmailDNSArchive, skipBodyHash);
// If DKIM verification fails, try again after sanitizing email // If DKIM verification fails, try again after sanitizing email
let appliedSanitization; let appliedSanitization;
if (dkimResult.status.comment === 'bad signature' && enableSanitization) { if (dkimResult.status.comment === 'bad signature' && enableSanitization) {
const results = await Promise.all( const results = await Promise.all(
sanitizers.map((sanitize) => sanitizers.map((sanitize) =>
tryVerifyDKIM(sanitize(emailStr), domain, fallbackToZKEmailDNSArchive).then((result) => ({ tryVerifyDKIM(sanitize(emailStr), domain, fallbackToZKEmailDNSArchive, skipBodyHash).then((result) => ({
result, result,
sanitizer: sanitize.name, sanitizer: sanitize.name,
})), })),
@@ -98,6 +100,7 @@ async function tryVerifyDKIM(
email: Buffer | string, email: Buffer | string,
domain: string = '', domain: string = '',
fallbackToZKEmailDNSArchive: boolean = false, fallbackToZKEmailDNSArchive: boolean = false,
skipBodyHash = false,
) { ) {
const resolver = async (name: string, type: string) => { const resolver = async (name: string, type: string) => {
try { try {
@@ -115,6 +118,7 @@ async function tryVerifyDKIM(
const dkimVerifier = new DkimVerifier({ const dkimVerifier = new DkimVerifier({
resolver, resolver,
skipBodyHash,
}); });
await writeToStream(dkimVerifier, email as any); await writeToStream(dkimVerifier, email as any);

View File

@@ -50,12 +50,14 @@ export class DkimVerifier extends MessageParser {
private arc: { chain: false }; private arc: { chain: false };
private seal: { bodyHash: string }; private seal: { bodyHash: string };
private sealBodyHashKey: string = ''; private sealBodyHashKey: string = '';
private skipBodyHash: boolean = false;
constructor(options: Record<string, any>) { constructor(options: Record<string, any>) {
super(); super();
this.options = options || {}; this.options = options || {};
this.resolver = this.options.resolver; this.resolver = this.options.resolver;
this.minBitLength = this.options.minBitLength; this.minBitLength = this.options.minBitLength;
this.skipBodyHash = this.options.skipBodyHash
this.results = []; this.results = [];
@@ -180,7 +182,7 @@ export class DkimVerifier extends MessageParser {
async finalChunk() { async finalChunk() {
try { try {
if (!this.headers || !this.bodyHashes.size) { if (!this.headers || (!this.skipBodyHash && !this.bodyHashes.size)) {
return; return;
} }
@@ -242,7 +244,7 @@ export class DkimVerifier extends MessageParser {
let bodyHashObj = this.bodyHashes.get(signatureHeader.bodyHashKey); let bodyHashObj = this.bodyHashes.get(signatureHeader.bodyHashKey);
let bodyHash = bodyHashObj?.hash; let bodyHash = bodyHashObj?.hash;
if (signatureHeader.parsed?.bh?.value !== bodyHash) { if ((signatureHeader.parsed?.bh?.value !== bodyHash) && !this.skipBodyHash) {
status.result = 'neutral'; status.result = 'neutral';
status.comment = `body hash did not verify`; status.comment = `body hash did not verify`;
} else { } else {
@@ -332,7 +334,8 @@ export class DkimVerifier extends MessageParser {
if ( if (
typeof signatureHeader.maxBodyLength === 'number' && typeof signatureHeader.maxBodyLength === 'number' &&
signatureHeader.maxBodyLength !== signatureHeader.bodyHashedBytes signatureHeader.maxBodyLength !== signatureHeader.bodyHashedBytes &&
!this.skipBodyHash
) { ) {
status.result = 'fail'; status.result = 'fail';
status.comment = `invalid body length ${signatureHeader.bodyHashedBytes}`; status.comment = `invalid body length ${signatureHeader.bodyHashedBytes}`;

View File

@@ -72,6 +72,28 @@ describe('DKIM signature verification', () => {
expect(e.message).toBe('DKIM signature not found for domain domain.com'); expect(e.message).toBe('DKIM signature not found for domain domain.com');
} }
}); });
it('should skip body-hash verification for body-less emails', async () => {
// From address domain is icloud.com
const email = fs.readFileSync(path.join(__dirname, 'test-data/email-bodyless.eml'));
// Should pass with default domain
const result = await verifyDKIMSignature(email, "", true, false, true);
expect.assertions(1);
expect(result.signingDomain).toBe('icloud.com');
});
it('should pass for tampered body if skipBodyHash=true', async () => {
const email = fs.readFileSync(path.join(__dirname, 'test-data/email-body-tampered.eml'));
try {
await verifyDKIMSignature(email, '', true, false, true);
} catch (e) {
expect(e.message).toBe(
'DKIM signature verification failed for domain icloud.com. Reason: body hash did not verify',
);
}
});
}); });
it('should fallback to ZK Email Archive if DNS over HTTP fails', async () => { it('should fallback to ZK Email Archive if DNS over HTTP fails', async () => {

View File

@@ -0,0 +1,15 @@
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=icloud.com; s=1a1hai; t=1693038337; bh=7xQMDuoVVU4m0W0WRVSrVXMeGSIASsnucK9dJsrc+vU=; h=from:Content-Type:Mime-Version:Subject:Message-Id:Date:to; b=EhLyVPpKD7d2/+h1nrnu+iEEBDfh6UWiAf9Y5UK+aPNLt3fAyEKw6Ic46v32NOcZD
M/zhXWucN0FXNiS0pz/QVIEy8Bcdy7eBZA0QA1fp8x5x5SugDELSRobQNbkOjBg7Mx
VXy7h4pKZMm/hKyhvMZXK4AX9fSoXZt4VGlAFymFNavfdAeKgg/SHXLds4lOPJV1wR
2E21g853iz5m/INq3uK6SQKzTnz/wDkdyiq90gC0tHQe8HpDRhPIqgL5KSEpuvUYmJ
wjEOwwHqP6L3JfEeROOt6wyuB1ah7wgRvoABOJ81+qLYRn3bxF+y1BC+PwFd5yFWH5
Ry43lwp1/3+sA==
from: runnier.leagues.0j@icloud.com
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: 7bit
Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3731.500.231\))
Subject: Hello
Message-Id: <8F819D32-B6AC-489D-977F-438BBC4CAB27@me.com>
Date: Sat, 26 Aug 2023 12:25:22 +0400
to: zkewtest@gmail.com