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",
"version": "6.3.2",
"version": "6.4.2",
"license": "MIT",
"main": "dist",
"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 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<DKIMVerificationResult> {
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);

View File

@@ -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<string, any>) {
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.skipBodyHash && !this.bodyHashes.size)) {
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}`;

View File

@@ -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 () => {

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