111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|