@@ -0,0 +1,110 @@
|
||||
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 <ada@example.test>",
|
||||
"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",
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user