@vlayer/sdk 0.1.0-nightly-20250127-5ee6ca8 → 0.1.0-nightly-20250128-0a32cfc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/package.json +2 -1
  2. package/src/api/email/dnsResolver.test.ts +19 -0
  3. package/src/api/email/dnsResolver.ts +87 -0
  4. package/src/api/email/parseEmail.test.ts +133 -0
  5. package/src/api/email/parseEmail.ts +55 -0
  6. package/src/api/email/preverify.test.ts +201 -0
  7. package/src/api/email/preverify.ts +70 -0
  8. package/src/api/email/testdata/test_email.txt +21 -0
  9. package/src/api/email/testdata/test_email_multiple_dkims.txt +28 -0
  10. package/src/api/email/testdata/test_email_subdomain.txt +21 -0
  11. package/src/api/email/testdata/test_email_unknown_domain.txt +21 -0
  12. package/src/api/lib/client.test.ts +261 -0
  13. package/src/api/lib/client.ts +191 -0
  14. package/src/api/lib/errors.ts +19 -0
  15. package/src/api/lib/types/ethereum.ts +45 -0
  16. package/src/api/lib/types/index.ts +3 -0
  17. package/src/api/lib/types/viem.ts +26 -0
  18. package/src/api/lib/types/vlayer.ts +156 -0
  19. package/src/api/lib/types/webProofProvider.ts +68 -0
  20. package/src/api/prover.ts +120 -0
  21. package/src/api/utils/prefixAllButNthSubstring.test.ts +24 -0
  22. package/src/api/utils/prefixAllButNthSubstring.ts +13 -0
  23. package/src/api/utils/versions.test.ts +52 -0
  24. package/src/api/utils/versions.ts +31 -0
  25. package/src/api/v_call.ts +58 -0
  26. package/src/api/v_getProofReceipt.ts +48 -0
  27. package/src/api/v_versions.ts +68 -0
  28. package/src/api/webProof/createWebProofRequest.ts +15 -0
  29. package/src/api/webProof/index.ts +3 -0
  30. package/src/api/webProof/providers/extension.test.ts +122 -0
  31. package/src/api/webProof/providers/extension.ts +197 -0
  32. package/src/api/webProof/providers/index.ts +1 -0
  33. package/src/api/webProof/steps/expectUrl.ts +12 -0
  34. package/src/api/webProof/steps/index.ts +11 -0
  35. package/src/api/webProof/steps/notarize.ts +20 -0
  36. package/src/api/webProof/steps/startPage.ts +12 -0
  37. package/src/config/createContext.ts +69 -0
  38. package/src/config/deploy.ts +108 -0
  39. package/src/config/getChainConfirmations.ts +6 -0
  40. package/src/config/getConfig.ts +71 -0
  41. package/src/config/index.ts +5 -0
  42. package/src/config/types.ts +26 -0
  43. package/src/config/writeEnvVariables.ts +28 -0
  44. package/src/index.ts +7 -0
  45. package/src/testHelpers/readFile.ts +3 -0
  46. package/src/web-proof-commons/index.ts +3 -0
  47. package/src/web-proof-commons/types/message.ts +176 -0
  48. package/src/web-proof-commons/types/redaction.test.ts +97 -0
  49. package/src/web-proof-commons/types/redaction.ts +201 -0
  50. package/src/web-proof-commons/utils.ts +11 -0
package/package.json CHANGED
@@ -27,7 +27,7 @@
27
27
  "default": "./dist/config/index.js"
28
28
  }
29
29
  },
30
- "version": "0.1.0-nightly-20250127-5ee6ca8",
30
+ "version": "0.1.0-nightly-20250128-0a32cfc",
31
31
  "scripts": {
32
32
  "build": "bun tsc --project tsconfig.build.json && bun tsc-alias",
33
33
  "test:unit": "vitest --run",
@@ -62,6 +62,7 @@
62
62
  },
63
63
  "files": [
64
64
  "dist/",
65
+ "src/",
65
66
  "package.json",
66
67
  "README.md"
67
68
  ]
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { DnsResolver } from "./dnsResolver";
3
+
4
+ const resolver = new DnsResolver("https://dns.google/resolve");
5
+
6
+ describe("resolveDkimDns Integration", () => {
7
+ test("resolves vlayer DNS", async () => {
8
+ const resolved = await resolver.resolveDkimDns("google", "vlayer.xyz");
9
+ const expected =
10
+ "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoDLLSKLb3eyflXzeHwBz8qqg9mfpmMY+f1tp+HpwlEOeN5iHO0s4sCd2QbG2i/DJRzryritRnjnc4i2NJ/IJfU8XZdjthotcFUY6rrlFld20a13q8RYBBsETSJhYnBu+DMdIF9q3YxDtXRFNpFCpI1uIeA/x+4qQJm3KTZQWdqi/BVnbsBA6ZryQCOOJC3Ae0oodvz80yfEJUAi9hAGZWqRn+Mprlyu749uQ91pTOYCDCbAn+cqhw8/mY5WMXFqrw9AdfWrk+MwXHPVDWBs8/Hm8xkWxHOqYs9W51oZ/Je3WWeeggyYCZI9V+Czv7eF8BD/yF9UxU/3ZWZPM8EWKKQIDAQAB";
11
+ expect(resolved.dnsRecord.data).toBe(expected);
12
+ });
13
+
14
+ test("throws error if dns not found", async () => {
15
+ await expect(
16
+ resolver.resolveDkimDns("abcd", "not-a-domain.com"),
17
+ ).rejects.toThrow();
18
+ });
19
+ });
@@ -0,0 +1,87 @@
1
+ import { toByteArray } from "base64-js";
2
+ import { toHex } from "viem";
3
+
4
+ interface DnsResponse {
5
+ Status: number;
6
+ TC: boolean;
7
+ RD: boolean;
8
+ RA: boolean;
9
+ AD: boolean;
10
+ CD: boolean;
11
+ Question: {
12
+ name: string;
13
+ type: number;
14
+ }[];
15
+ Answer:
16
+ | {
17
+ name: string;
18
+ type: number;
19
+ TTL: number;
20
+ data: string;
21
+ }[]
22
+ | undefined;
23
+ VerificationData:
24
+ | {
25
+ valid_until: number;
26
+ signature: string;
27
+ pub_key: string;
28
+ }
29
+ | undefined;
30
+ }
31
+
32
+ function parseBase64(data: string): `0x${string}` {
33
+ return toHex(toByteArray(data));
34
+ }
35
+
36
+ function parseVerificationData(response: DnsResponse) {
37
+ if (!response.VerificationData) {
38
+ console.warn(`No verification data in DNS response`);
39
+ return {
40
+ validUntil: 0n,
41
+ signature: "0x" as const,
42
+ pubKey: "0x" as const,
43
+ };
44
+ }
45
+ return {
46
+ validUntil: BigInt(response.VerificationData.valid_until),
47
+ signature: parseBase64(response.VerificationData.signature),
48
+ pubKey: parseBase64(response.VerificationData.pub_key),
49
+ };
50
+ }
51
+
52
+ function takeLastAnswer(response: DnsResponse) {
53
+ const answer = response.Answer;
54
+ if (!answer || answer?.length == 0) {
55
+ throw new Error(
56
+ `No DNS answer found\n${JSON.stringify(response, null, 2)}`,
57
+ );
58
+ }
59
+ const record = answer.flat().at(-1)!;
60
+ return {
61
+ recordType: record.type,
62
+ ttl: BigInt(record.TTL),
63
+ ...record,
64
+ };
65
+ }
66
+
67
+ export class DnsResolver {
68
+ constructor(private host: string) {}
69
+
70
+ async resolveDkimDns(selector: string, domain: string) {
71
+ const response = (await (
72
+ await fetch(
73
+ `${this.host}?name=${selector}._domainkey.${domain}&type=TXT`,
74
+ {
75
+ headers: {
76
+ accept: "application/dns-json",
77
+ },
78
+ },
79
+ )
80
+ ).json()) as DnsResponse;
81
+
82
+ return {
83
+ dnsRecord: takeLastAnswer(response),
84
+ verificationData: parseVerificationData(response),
85
+ };
86
+ }
87
+ }
@@ -0,0 +1,133 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { getDkimSigners, parseEmail, parseParams } from "./parseEmail";
3
+
4
+ const emailHeaders = `From: "John Doe" <john@d.oe>
5
+ To: "Jane Doe" <jane@d.oe>
6
+ Subject: Hello World
7
+ Date: Thu, 1 Jan 1970 00:00:00 +0000
8
+ `;
9
+
10
+ const dkimHeader =
11
+ "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; h=from:to:subject; s=selector1; b=abcdef;";
12
+
13
+ const body = "Hello, World!";
14
+
15
+ const emailFixture = `${emailHeaders}${dkimHeader}\n\n${body}`;
16
+
17
+ describe("parseEmail", () => {
18
+ test("should get dkim header from email", async () => {});
19
+
20
+ test("correctly parses untrimmed email", async () => {
21
+ const untrimmed = `\n ${emailFixture} \n`;
22
+ const email = await parseEmail(untrimmed);
23
+ expect(email.headers.find((h) => h.key === "dkim-signature")).toBeDefined();
24
+ });
25
+
26
+ test("works well with multiple dkim headers", async () => {
27
+ const dkimHeader2 =
28
+ "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=second.signer; h=from:to:subject; s=selector2; b=abcdef;";
29
+
30
+ const email = await parseEmail(
31
+ `${emailHeaders}${dkimHeader}\n${dkimHeader2}\n\n${body}`,
32
+ );
33
+ const dkim = email.headers.filter((h) => h.key === "dkim-signature");
34
+
35
+ expect(dkim).toHaveLength(2);
36
+ expect(parseParams(dkim[0].value).s).toBe("selector1");
37
+ expect(parseParams(dkim[1].value).s).toBe("selector2");
38
+ });
39
+ });
40
+
41
+ describe("getDkimSigners", () => {
42
+ test("should get dkim signers from email", async () => {
43
+ const email = await parseEmail(emailFixture);
44
+ const dkim = getDkimSigners(email);
45
+ expect(dkim).toHaveLength(1);
46
+ expect(dkim[0].domain).toBe("example.com");
47
+ expect(dkim[0].selector).toBe("selector1");
48
+ });
49
+
50
+ test("should get multiple dkim signers from email", async () => {
51
+ const dkimHeader2 =
52
+ "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=second.signer; h=from:to:subject; s=selector2; b=abcdef;";
53
+ const email = await parseEmail(
54
+ `${emailHeaders}${dkimHeader}\n${dkimHeader2}\n\n${body}`,
55
+ );
56
+
57
+ const dkim = getDkimSigners(email);
58
+ expect(dkim).toHaveLength(2);
59
+ expect(dkim[0].domain).toBe("example.com");
60
+ expect(dkim[0].selector).toBe("selector1");
61
+ expect(dkim[1].domain).toBe("second.signer");
62
+ expect(dkim[1].selector).toBe("selector2");
63
+ });
64
+
65
+ test("should throw if no dkim header found", async () => {
66
+ const email = await parseEmail(emailHeaders);
67
+ expect(() => getDkimSigners(email)).toThrowError("No DKIM header found");
68
+ });
69
+
70
+ test("should throw if dkim header is invalid", async () => {
71
+ const email = await parseEmail(
72
+ `${emailHeaders}DKIM-Signature: invalid\n\n${body}`,
73
+ );
74
+ expect(() => getDkimSigners(email)).toThrowError(
75
+ "DKIM header missing domain",
76
+ );
77
+ });
78
+
79
+ test("should throw if dkim header is missing domain", async () => {
80
+ const email = await parseEmail(
81
+ `${emailHeaders}DKIM-Signature: v=1; s=selector\n\n${body}`,
82
+ );
83
+ expect(() => getDkimSigners(email)).toThrowError(
84
+ "DKIM header missing domain",
85
+ );
86
+ });
87
+
88
+ test("should throw if dkim header is missing selector", async () => {
89
+ const email = await parseEmail(
90
+ `${emailHeaders}DKIM-Signature: v=1; d=example.com\n\n${body}`,
91
+ );
92
+ expect(() => getDkimSigners(email)).toThrowError(
93
+ "DKIM header missing selector",
94
+ );
95
+ });
96
+ });
97
+
98
+ describe("parseParams", () => {
99
+ test("should parse single parameter", () => {
100
+ const params = parseParams("a=b");
101
+ expect(params).toEqual({ a: "b" });
102
+ });
103
+
104
+ test("should parse multiple parameters", () => {
105
+ const params = parseParams("a=b; c=d; e=f");
106
+ expect(params).toEqual({ a: "b", c: "d", e: "f" });
107
+ });
108
+
109
+ test("should trim spaces around parameters", () => {
110
+ const params = parseParams(" a = b ; c = d ; e = f ");
111
+ expect(params).toEqual({ a: "b", c: "d", e: "f" });
112
+ });
113
+
114
+ test("should handle empty values", () => {
115
+ const params = parseParams("a=; b=c");
116
+ expect(params).toEqual({ a: "", b: "c" });
117
+ });
118
+
119
+ test("should handle missing values", () => {
120
+ const params = parseParams("a; b=c");
121
+ expect(params).toEqual({ a: undefined, b: "c" });
122
+ });
123
+
124
+ test("should handle empty string", () => {
125
+ const params = parseParams("");
126
+ expect(params).toEqual({});
127
+ });
128
+
129
+ test("should handle parameters with extra semicolons", () => {
130
+ const params = parseParams("a=b;; c=d;");
131
+ expect(params).toEqual({ a: "b", c: "d" });
132
+ });
133
+ });
@@ -0,0 +1,55 @@
1
+ import PostalMime, { type Email, type Header } from "postal-mime";
2
+
3
+ export class DkimParsingError extends Error {
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = "DkimParsingError";
7
+ }
8
+ }
9
+
10
+ export interface DkimDomainSelector {
11
+ domain: string;
12
+ selector: string;
13
+ }
14
+
15
+ export async function parseEmail(mime: string) {
16
+ return await PostalMime.parse(mime.trim());
17
+ }
18
+
19
+ export function getDkimSigners(mail: Email): DkimDomainSelector[] {
20
+ const dkimHeader = mail.headers.filter((h) => h.key === "dkim-signature");
21
+ if (dkimHeader.length === 0) {
22
+ throw new DkimParsingError("No DKIM header found");
23
+ }
24
+ return dkimHeader.map(parseHeader);
25
+ }
26
+
27
+ export function parseParams(str: string) {
28
+ return Object.fromEntries(
29
+ str.split(";").map((s) =>
30
+ s
31
+ .trim()
32
+ .split("=")
33
+ .map((v) => v && v.trim()),
34
+ ),
35
+ ) as Record<string, string>;
36
+ }
37
+
38
+ function parseHeader(header: Header): DkimDomainSelector {
39
+ const params = parseParams(header.value);
40
+ if (!params) {
41
+ throw new DkimParsingError(`Invalid DKIM header ${header.value}`);
42
+ }
43
+
44
+ if (!params.d) {
45
+ throw new DkimParsingError("DKIM header missing domain");
46
+ }
47
+
48
+ if (!params.s) {
49
+ throw new DkimParsingError("DKIM header missing selector");
50
+ }
51
+ return {
52
+ domain: params.d,
53
+ selector: params.s,
54
+ };
55
+ }
@@ -0,0 +1,201 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { readFile } from "testHelpers/readFile";
3
+ import { findIndicesOfMatchingDomains, preverifyEmail } from "./preverify";
4
+
5
+ const rawEmail = readFile("./src/api/email/testdata/test_email.txt");
6
+
7
+ describe("Preverify email: integration", () => {
8
+ test("adds dns record to email mime", async () => {
9
+ const preverifiedEmail = await preverifyEmail(
10
+ rawEmail,
11
+ "https://dns.google/resolve",
12
+ );
13
+ expect(preverifiedEmail.email).toBe(rawEmail);
14
+ expect(preverifiedEmail.dnsRecord).toMatchObject({
15
+ name: "20230601._domainkey.google.com.",
16
+ recordType: 16,
17
+ ttl: expect.any(BigInt), // eslint-disable-line @typescript-eslint/no-unsafe-assignment
18
+ data: expect.stringContaining("v=DKIM1; k=rsa; p="), // eslint-disable-line @typescript-eslint/no-unsafe-assignment
19
+ });
20
+ expect(preverifiedEmail.dnsRecord.data).toContain("v=DKIM1; k=rsa; p=");
21
+ });
22
+
23
+ test("throws error if DKIM not found", async () => {
24
+ const emailWithNoDkimHeader = 'From: "Alice"\n\nBody';
25
+ await expect(
26
+ preverifyEmail(emailWithNoDkimHeader, "https://dns.google/resolve"),
27
+ ).rejects.toThrow("No DKIM header found");
28
+ });
29
+
30
+ test("throws error if DNS could not be resolved", async () => {
31
+ const emailWithNoDkimHeader = readFile(
32
+ "./src/api/email/testdata/test_email_unknown_domain.txt",
33
+ );
34
+ await expect(
35
+ preverifyEmail(emailWithNoDkimHeader, "https://dns.google/resolve"),
36
+ ).rejects.toThrow();
37
+ });
38
+
39
+ describe("multiple DKIM headers", () => {
40
+ function addDkimWithDomain(domain: string, email: string) {
41
+ return `DKIM-Signature: v=1; a=rsa-sha256; d=${domain};
42
+ s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
43
+ ${email}`;
44
+ }
45
+
46
+ function addFakeDkimWithDomain(domain: string, email: string) {
47
+ return `X-${addDkimWithDomain(domain, email)}`;
48
+ }
49
+
50
+ test("looks for DKIM header with the domain matching the sender and removes all other DKIM headers", async () => {
51
+ const emailWithAddedHeaders = ["example.com", "hello.kitty"].reduce(
52
+ (email, domain) => addDkimWithDomain(domain, email),
53
+ rawEmail,
54
+ );
55
+ const email = await preverifyEmail(
56
+ emailWithAddedHeaders,
57
+ "https://dns.google/resolve",
58
+ );
59
+ expect(
60
+ email.email
61
+ .startsWith(`X-DKIM-Signature: v=1; a=rsa-sha256; d=hello.kitty;
62
+ s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
63
+ X-DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
64
+ s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
65
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
66
+ c=simple/simple; d=google.com;`),
67
+ ).toBeTruthy();
68
+ expect(email.dnsRecord.data).toEqual(
69
+ "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4zd3nfUoLHWFbfoPZzAb8bvjsFIIFsNypweLuPe4M+vAP1YxObFxRnpvLYz7Z+bORKLber5aGmgFF9iaufsH1z0+aw8Qex7uDaafzWoJOM/6lAS5iI0JggZiUkqNpRQLL7H6E7HcvOMC61nJcO4r0PwLDZKwEaCs8gUHiqRn/SS3wqEZX29v/VOUVcI4BjaOzOCLaz7V8Bkwmj4Rqq4kaLQQrbfpjas1naScHTAmzULj0Rdp+L1vVyGitm+dd460PcTIG3Pn+FYrgQQo2fvnTcGiFFuMa8cpxgfH3rJztf1YFehLWwJWgeXTriuIyuxUabGdRQu7vh7GrObTsHmIHwIDAQAB",
70
+ );
71
+ });
72
+
73
+ test("throws error if no DNS record domain matches the sender", async () => {
74
+ const emailWithOneDkimHeader = readFile(
75
+ "./src/api/email/testdata/test_email_unknown_domain.txt",
76
+ );
77
+ const emailWithAddedHeaders = addDkimWithDomain(
78
+ "otherdomain.com",
79
+ emailWithOneDkimHeader,
80
+ );
81
+ await expect(
82
+ preverifyEmail(emailWithAddedHeaders, "https://dns.google/resolve"),
83
+ ).rejects.toThrow("Found 0 DKIM headers matching the sender domain");
84
+ });
85
+
86
+ test("removes signatures from subdomain signers", async () => {
87
+ const email = await preverifyEmail(
88
+ addDkimWithDomain("subdomain.google.com", rawEmail),
89
+ "https://dns.google/resolve",
90
+ );
91
+ expect(
92
+ email.email
93
+ .startsWith(`X-DKIM-Signature: v=1; a=rsa-sha256; d=subdomain.google.com;
94
+ s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
95
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
96
+ c=simple/simple; d=google.com;`),
97
+ ).toBeTruthy();
98
+ });
99
+
100
+ test("removes signatures with mismatching subdomains", async () => {
101
+ const emailWithAddedHeaders = addDkimWithDomain(
102
+ "subdomain.google.com",
103
+ readFile("./src/api/email/testdata/test_email_subdomain.txt"),
104
+ );
105
+ await expect(
106
+ preverifyEmail(emailWithAddedHeaders, "https://dns.google/resolve"),
107
+ ).rejects.toThrow("Found 0 DKIM headers matching the sender domain");
108
+ });
109
+
110
+ test("throws error if multiple DNS record domains match the sender", async () => {
111
+ let emailWithAddedHeaders = addDkimWithDomain("google.com", rawEmail);
112
+ emailWithAddedHeaders = addDkimWithDomain(
113
+ "google.com",
114
+ emailWithAddedHeaders,
115
+ );
116
+ await expect(
117
+ preverifyEmail(emailWithAddedHeaders, "https://dns.google/resolve"),
118
+ ).rejects.toThrow("Found 3 DKIM headers matching the sender domain");
119
+ });
120
+
121
+ test("ignores x-dkim-signature headers", async () => {
122
+ const emailWithPrefixedDkim = addFakeDkimWithDomain(
123
+ "example.com",
124
+ addFakeDkimWithDomain("example.com", rawEmail),
125
+ );
126
+ const email = await preverifyEmail(
127
+ emailWithPrefixedDkim,
128
+ "https://dns.google/resolve",
129
+ );
130
+ expect(
131
+ email.email
132
+ .startsWith(`X-DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
133
+ s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
134
+ X-DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
135
+ s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
136
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
137
+ c=simple/simple; d=google.com;`),
138
+ ).toBeTruthy();
139
+ });
140
+
141
+ test("ignores dkim-signature somewhere inside a header", async () => {
142
+ const headerWithDkim = `WTF-IS-THIS-HEADER: DKIM-SIGNATURE;`;
143
+ const emailWithDkimInHeader = `${headerWithDkim}\n${addDkimWithDomain("example.com", rawEmail)}`;
144
+ const email = await preverifyEmail(
145
+ emailWithDkimInHeader,
146
+ "https://dns.google/resolve",
147
+ );
148
+ expect(
149
+ email.email.startsWith(`WTF-IS-THIS-HEADER: DKIM-SIGNATURE;
150
+ X-DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
151
+ s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
152
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
153
+ c=simple/simple; d=google.com;`),
154
+ ).toBeTruthy();
155
+ });
156
+
157
+ test("ignores dkim-signature somewhere inside a body", async () => {
158
+ const emailWithAddedDkim = addDkimWithDomain("example.com", rawEmail);
159
+ const emailWithDkimsInBody = `${emailWithAddedDkim}\ndkim-signature dkim-signature\r\ndkim-signature`;
160
+ const email = await preverifyEmail(
161
+ emailWithDkimsInBody,
162
+ "https://dns.google/resolve",
163
+ );
164
+ expect(
165
+ email.email
166
+ .startsWith(`X-DKIM-Signature: v=1; a=rsa-sha256; d=example.com;
167
+ s=selector; c=relaxed/relaxed; q=dns/txt; bh=; h=From:Subject:Date:To; b=
168
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r
169
+ c=simple/simple; d=google.com;`),
170
+ ).toBeTruthy();
171
+ expect(
172
+ email.email.endsWith(
173
+ `\ndkim-signature dkim-signature\r\ndkim-signature`,
174
+ ),
175
+ ).toBeTruthy();
176
+ });
177
+ });
178
+ });
179
+
180
+ describe("findIndicesOfMatchingDomains", () => {
181
+ test("returns indices of matching domains", () => {
182
+ const signers = [
183
+ { domain: "example.com", selector: "selector1" },
184
+ { domain: "other.other", selector: "selector2" },
185
+ { domain: "example.com", selector: "selector3" },
186
+ ];
187
+ expect(
188
+ findIndicesOfMatchingDomains(signers, "some@example.com"),
189
+ ).toStrictEqual([0, 2]);
190
+ });
191
+
192
+ test("returns empty array if no matching domains", () => {
193
+ const signers = [
194
+ { domain: "example.com", selector: "selector1" },
195
+ { domain: "example.org", selector: "selector2" },
196
+ ];
197
+ expect(findIndicesOfMatchingDomains(signers, "other.other")).toStrictEqual(
198
+ [],
199
+ );
200
+ });
201
+ });
@@ -0,0 +1,70 @@
1
+ import {
2
+ type DkimDomainSelector,
3
+ getDkimSigners,
4
+ parseEmail,
5
+ } from "./parseEmail";
6
+ import { DnsResolver } from "./dnsResolver";
7
+ import { prefixAllButNthSubstring } from "../utils/prefixAllButNthSubstring";
8
+
9
+ export function findIndicesOfMatchingDomains(
10
+ signers: DkimDomainSelector[],
11
+ expectedOrigin: string,
12
+ ) {
13
+ return signers
14
+ .map(({ domain }) => expectedOrigin.endsWith(`@${domain}`))
15
+ .map((isMatch, index) => (isMatch ? index : -1))
16
+ .filter((index) => index !== -1);
17
+ }
18
+
19
+ function requireSameOrigin(
20
+ mimeEmail: string,
21
+ signers: DkimDomainSelector[],
22
+ fromAddress: string,
23
+ ) {
24
+ const matchingIndices = findIndicesOfMatchingDomains(signers, fromAddress);
25
+
26
+ if (matchingIndices.length != 1) {
27
+ throw new Error(
28
+ `Found ${matchingIndices.length} DKIM headers matching the sender domain`,
29
+ );
30
+ }
31
+
32
+ const [matchingIndex] = matchingIndices;
33
+
34
+ return [
35
+ prefixAllButNthSubstring(
36
+ mimeEmail,
37
+ /^\s*dkim-signature/gim,
38
+ signers.length,
39
+ matchingIndex,
40
+ ),
41
+ [signers[matchingIndex]] as DkimDomainSelector[],
42
+ ] as const;
43
+ }
44
+
45
+ export async function preverifyEmail(
46
+ mimeEmail: string,
47
+ dnsResolverUrl: string,
48
+ ) {
49
+ const parsedEmail = await parseEmail(mimeEmail);
50
+ let signers = getDkimSigners(parsedEmail);
51
+ const fromAddress = parsedEmail.from.address;
52
+
53
+ if (!fromAddress) {
54
+ throw new Error("No from address found");
55
+ }
56
+ if (signers.length === 0) {
57
+ throw new Error("No DKIM header found");
58
+ }
59
+ [mimeEmail, signers] = requireSameOrigin(mimeEmail, signers, fromAddress);
60
+
61
+ const [{ domain, selector }] = signers;
62
+ const resolver = new DnsResolver(dnsResolverUrl);
63
+
64
+ const dnsResponse = await resolver.resolveDkimDns(selector, domain);
65
+
66
+ return {
67
+ email: mimeEmail,
68
+ ...dnsResponse,
69
+ };
70
+ }
@@ -0,0 +1,21 @@
1
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
2
+ c=simple/simple; d=google.com;
3
+ h=Received:From:To:Subject:Date:Message-ID; i=joe@google.com;
4
+ s=20230601; t=1615825284; v=1;
5
+ b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
6
+ k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
7
+ s4wwFRRKz/1bksZGSjD8uuSU=
8
+ Received: from client1.football.example.com [192.0.2.1]
9
+ by submitserver.example.com with SUBMISSION;
10
+ Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
11
+ From: Joe SixPack <joe@google.com>
12
+ To: Suzie Q <suzie@shopping.example.net>
13
+ Subject: Is dinner ready?
14
+ Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
15
+ Message-ID: <20030712040037.46341.5F8J@football.example.com>
16
+
17
+ Hi.
18
+
19
+ We lost the game. Are you hungry yet?
20
+
21
+ Joe.
@@ -0,0 +1,28 @@
1
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
2
+ c=simple/simple; d=google.com;
3
+ h=Received:From:To:Subject:Date:Message-ID; i=joe@football.example.com;
4
+ s=20230601; t=1615825284; v=1;
5
+ b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
6
+ k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
7
+ s4wwFRRKz/1bksZGSjD8uuSU=
8
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
9
+ c=simple/simple; d=google.com;
10
+ h=Received:From:To:Subject:Date:Message-ID; i=joe@football.example.com;
11
+ s=20230601; t=1615825284; v=1;
12
+ b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
13
+ k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
14
+ s4wwFRRKz/1bksZGSjD8uuSU=
15
+ Received: from client1.football.example.com [192.0.2.1]
16
+ by submitserver.example.com with SUBMISSION;
17
+ Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
18
+ From: Joe SixPack <joe@football.example.com>
19
+ To: Suzie Q <suzie@shopping.example.net>
20
+ Subject: Is dinner ready?
21
+ Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
22
+ Message-ID: <20030712040037.46341.5F8J@football.example.com>
23
+
24
+ Hi.
25
+
26
+ We lost the game. Are you hungry yet?
27
+
28
+ Joe.
@@ -0,0 +1,21 @@
1
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
2
+ c=simple/simple; d=subdomain.google.com;
3
+ h=Received:From:To:Subject:Date:Message-ID; i=joe@google.com;
4
+ s=20230601; t=1615825284; v=1;
5
+ b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
6
+ k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
7
+ s4wwFRRKz/1bksZGSjD8uuSU=
8
+ Received: from client1.football.example.com [192.0.2.1]
9
+ by submitserver.example.com with SUBMISSION;
10
+ Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
11
+ From: Joe SixPack <joe@google.com>
12
+ To: Suzie Q <suzie@shopping.example.net>
13
+ Subject: Is dinner ready?
14
+ Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
15
+ Message-ID: <20030712040037.46341.5F8J@football.example.com>
16
+
17
+ Hi.
18
+
19
+ We lost the game. Are you hungry yet?
20
+
21
+ Joe.
@@ -0,0 +1,21 @@
1
+ DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
2
+ c=simple/simple; d=foobar.xyz;
3
+ h=Received:From:To:Subject:Date:Message-ID; i=joe@football.example.com;
4
+ s=wertyu; t=1615825284; v=1;
5
+ b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
6
+ k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
7
+ s4wwFRRKz/1bksZGSjD8uuSU=
8
+ Received: from client1.football.example.com [192.0.2.1]
9
+ by submitserver.example.com with SUBMISSION;
10
+ Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
11
+ From: Joe SixPack <joe@football.example.com>
12
+ To: Suzie Q <suzie@shopping.example.net>
13
+ Subject: Is dinner ready?
14
+ Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
15
+ Message-ID: <20030712040037.46341.5F8J@football.example.com>
16
+
17
+ Hi.
18
+
19
+ We lost the game. Are you hungry yet?
20
+
21
+ Joe.