import { bytesToBase64 } from "@add-ideas/toolbox-helpers"; import { describe, expect, it } from "vitest"; import { inspectDkimSignatures, prepareDkimVerification, verifyDkimSignature, } from "../../src/core/dkim"; import { parseMessage } from "../../src/core/mime"; async function signedMessage() { const keyPair = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, true, ["sign", "verify"], ); const body = "Hello DKIM!\r\n"; const bodyHash = bytesToBase64( new Uint8Array( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body)), ), ); const unsigned = [ "From: Ada ", "Subject: Local verification", `DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.test; s=mail; h=from:subject; bh=${bodyHash}; b=`, "", body, ].join("\r\n"); const prepared = await prepareDkimVerification(parseMessage(unsigned)); const signature = bytesToBase64( new Uint8Array( await crypto.subtle.sign( "RSASSA-PKCS1-v1_5", keyPair.privateKey, prepared.headerBytes, ), ), ); const publicKey = bytesToBase64( new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey)), ); return { source: unsigned.replace(/; b=(?=\r\n)/u, `; b=${signature}`), keyRecord: `v=DKIM1; k=rsa; h=sha256; s=email; p=${publicKey}`, }; } describe("DKIM laboratory", () => { it("inspects signatures without network access", () => { const message = parseMessage( "From: a@example.test\r\nDKIM-Signature: v=1; a=rsa-sha256; d=example.test; s=mail; c=relaxed/relaxed; h=from; bh=YQ==; b=Yg==\r\n\r\na", ); expect(inspectDkimSignatures(message)[0]).toMatchObject({ queryName: "mail._domainkey.example.test", algorithm: "rsa-sha256", headerCanonicalization: "relaxed", bodyCanonicalization: "relaxed", supported: true, }); }); it("verifies body and header signatures against a pasted key record", async () => { const fixture = await signedMessage(); const result = await verifyDkimSignature( parseMessage(fixture.source), 0, fixture.keyRecord, ); expect(result).toMatchObject({ bodyHash: "pass", signature: "pass", status: "pass", }); }); it("rejects a changed body before checking the signature", async () => { const fixture = await signedMessage(); const changed = fixture.source.replace("Hello DKIM!", "Hello altered!"); const result = await verifyDkimSignature( parseMessage(changed), 0, fixture.keyRecord, ); expect(result).toMatchObject({ bodyHash: "fail", signature: "not-checked", status: "fail", }); }); it("fails closed for malformed key records and unsupported signatures", async () => { const fixture = await signedMessage(); expect( await verifyDkimSignature(parseMessage(fixture.source), 0, "v=DKIM1; p="), ).toMatchObject({ signature: "error", status: "permerror" }); const obsolete = parseMessage( fixture.source.replace("a=rsa-sha256", "a=rsa-sha1"), ); expect( await verifyDkimSignature(obsolete, 0, fixture.keyRecord), ).toMatchObject({ status: "permerror", }); }); });