mirror of
https://github.com/zkemail/zk-email-verify.git
synced 2026-01-09 21:48:14 -05:00
Merge pull request #251 from zkemail/optional_bh_check
Add skipBodyHash check flag in verifyDkimSignature function
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
15
packages/helpers/tests/test-data/email-bodyless.eml
Normal file
15
packages/helpers/tests/test-data/email-bodyless.eml
Normal 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
|
||||||
|
|
||||||
Reference in New Issue
Block a user