From c089d9f739cd05d93613d59880c5b79ac819b459 Mon Sep 17 00:00:00 2001 From: sukhman Date: Mon, 17 Feb 2025 01:30:48 +0530 Subject: [PATCH 1/3] Add skipBodyHash check flag in verifyDkimSignature function --- packages/helpers/src/dkim/index.ts | 8 +++++-- .../helpers/src/lib/mailauth/dkim-verifier.ts | 9 +++++--- packages/helpers/tests/dkim.test.ts | 22 +++++++++++++++++++ .../tests/test-data/email-bodyless.eml | 15 +++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 packages/helpers/tests/test-data/email-bodyless.eml diff --git a/packages/helpers/src/dkim/index.ts b/packages/helpers/src/dkim/index.ts index be55336..7a15ecd 100644 --- a/packages/helpers/src/dkim/index.ts +++ b/packages/helpers/src/dkim/index.ts @@ -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 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 + * @param skipBodyHash If true, it bypass the dkim body hash check * @returns */ export async function verifyDKIMSignature( @@ -37,17 +38,18 @@ export async function verifyDKIMSignature( domain: string = '', enableSanitization: boolean = true, fallbackToZKEmailDNSArchive: boolean = false, + skipBodyHash = false, ): Promise { 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 let appliedSanitization; if (dkimResult.status.comment === 'bad signature' && enableSanitization) { const results = await Promise.all( sanitizers.map((sanitize) => - tryVerifyDKIM(sanitize(emailStr), domain, fallbackToZKEmailDNSArchive).then((result) => ({ + tryVerifyDKIM(sanitize(emailStr), domain, fallbackToZKEmailDNSArchive, skipBodyHash).then((result) => ({ result, sanitizer: sanitize.name, })), @@ -98,6 +100,7 @@ async function tryVerifyDKIM( email: Buffer | string, domain: string = '', fallbackToZKEmailDNSArchive: boolean = false, + skipBodyHash = false, ) { const resolver = async (name: string, type: string) => { try { @@ -115,6 +118,7 @@ async function tryVerifyDKIM( const dkimVerifier = new DkimVerifier({ resolver, + skipBodyHash, }); await writeToStream(dkimVerifier, email as any); diff --git a/packages/helpers/src/lib/mailauth/dkim-verifier.ts b/packages/helpers/src/lib/mailauth/dkim-verifier.ts index 779aec6..79402ae 100644 --- a/packages/helpers/src/lib/mailauth/dkim-verifier.ts +++ b/packages/helpers/src/lib/mailauth/dkim-verifier.ts @@ -50,12 +50,14 @@ export class DkimVerifier extends MessageParser { private arc: { chain: false }; private seal: { bodyHash: string }; private sealBodyHashKey: string = ''; + private skipBodyHash: boolean = false; constructor(options: Record) { super(); this.options = options || {}; this.resolver = this.options.resolver; this.minBitLength = this.options.minBitLength; + this.skipBodyHash = this.options.skipBodyHash this.results = []; @@ -180,7 +182,7 @@ export class DkimVerifier extends MessageParser { async finalChunk() { try { - if (!this.headers || !this.bodyHashes.size) { + if ((!this.headers || !this.bodyHashes.size) && !this.skipBodyHash) { return; } @@ -242,7 +244,7 @@ export class DkimVerifier extends MessageParser { let bodyHashObj = this.bodyHashes.get(signatureHeader.bodyHashKey); let bodyHash = bodyHashObj?.hash; - if (signatureHeader.parsed?.bh?.value !== bodyHash) { + if ((signatureHeader.parsed?.bh?.value !== bodyHash) && !this.skipBodyHash) { status.result = 'neutral'; status.comment = `body hash did not verify`; } else { @@ -332,7 +334,8 @@ export class DkimVerifier extends MessageParser { if ( typeof signatureHeader.maxBodyLength === 'number' && - signatureHeader.maxBodyLength !== signatureHeader.bodyHashedBytes + signatureHeader.maxBodyLength !== signatureHeader.bodyHashedBytes && + !this.skipBodyHash ) { status.result = 'fail'; status.comment = `invalid body length ${signatureHeader.bodyHashedBytes}`; diff --git a/packages/helpers/tests/dkim.test.ts b/packages/helpers/tests/dkim.test.ts index e98fa02..2f56e2b 100644 --- a/packages/helpers/tests/dkim.test.ts +++ b/packages/helpers/tests/dkim.test.ts @@ -72,6 +72,28 @@ describe('DKIM signature verification', () => { 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 () => { diff --git a/packages/helpers/tests/test-data/email-bodyless.eml b/packages/helpers/tests/test-data/email-bodyless.eml new file mode 100644 index 0000000..295ab71 --- /dev/null +++ b/packages/helpers/tests/test-data/email-bodyless.eml @@ -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 + From 69965a4d35a2a490b3702a0182a02998b128492e Mon Sep 17 00:00:00 2001 From: sukhman Date: Tue, 18 Feb 2025 03:35:37 +0530 Subject: [PATCH 2/3] Bump version by minor amount --- packages/helpers/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 246c8d2..a9f1754 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zk-email/helpers", - "version": "6.3.2", + "version": "6.4.2", "license": "MIT", "main": "dist", "scripts": { From e5b47e899f4acfd03abf2ba6da9e591586dfb88e Mon Sep 17 00:00:00 2001 From: sukhman Date: Wed, 19 Feb 2025 00:14:54 +0530 Subject: [PATCH 3/3] refactor condition check --- packages/helpers/src/lib/mailauth/dkim-verifier.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/helpers/src/lib/mailauth/dkim-verifier.ts b/packages/helpers/src/lib/mailauth/dkim-verifier.ts index 79402ae..04a8132 100644 --- a/packages/helpers/src/lib/mailauth/dkim-verifier.ts +++ b/packages/helpers/src/lib/mailauth/dkim-verifier.ts @@ -182,7 +182,7 @@ export class DkimVerifier extends MessageParser { async finalChunk() { try { - if ((!this.headers || !this.bodyHashes.size) && !this.skipBodyHash) { + if (!this.headers || (!this.skipBodyHash && !this.bodyHashes.size)) { return; }