-
Notifications
You must be signed in to change notification settings - Fork 80
/
index.ts
140 lines (120 loc) · 4.32 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { pki } from 'node-forge';
import { DkimVerifier } from '../lib/mailauth/dkim-verifier';
import { writeToStream } from '../lib/mailauth/tools';
import sanitizers from './sanitizers';
import { resolveDNSHTTP } from './dns-over-http';
import { resolveDNSFromZKEmailArchive } from './dns-archive';
// `./mailauth` is modified version of https://github.com/postalsys/mailauth
// Main modification are including emailHeaders in the DKIM result, making it work in the browser, add types
// TODO: Fork the repo and make the changes; consider upstream to original repo
export interface DKIMVerificationResult {
publicKey: bigint;
signature: bigint;
headers: Buffer;
body: Buffer;
bodyHash: string;
signingDomain: string;
selector: string;
algo: string;
format: string;
modulusLength: number;
appliedSanitization?: string;
}
/**
*
* @param email Entire email data as a string or buffer
* @param domain Domain to verify DKIM signature for. If not provided, the domain is extracted from the `From` header
* @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
* be used to resolve DKIM public keys if we cannot resolve from HTTP DNS
* @returns
*/
export async function verifyDKIMSignature(
email: Buffer | string,
domain: string = '',
enableSanitization: boolean = true,
fallbackToZKEmailDNSArchive: boolean = false,
): Promise<DKIMVerificationResult> {
const emailStr = email.toString();
let dkimResult = await tryVerifyDKIM(email, domain, fallbackToZKEmailDNSArchive);
// If DKIM verification fails, try again after sanitizing email
let appliedSanitization;
if (dkimResult.status.comment === 'bad signature' && enableSanitization) {
const results = await Promise.all(
sanitizers.map((sanitize) =>
tryVerifyDKIM(sanitize(emailStr), domain, fallbackToZKEmailDNSArchive).then((result) => ({
result,
sanitizer: sanitize.name,
})),
),
);
const passed = results.find((r) => r.result.status.result === 'pass');
if (passed) {
console.log(`DKIM: Verification passed after applying sanitization "${passed.sanitizer}"`);
dkimResult = passed.result;
appliedSanitization = passed.sanitizer;
}
}
const {
status: { result, comment },
signingDomain,
publicKey,
signature,
status,
body,
bodyHash,
} = dkimResult;
if (result !== 'pass') {
throw new Error(`DKIM signature verification failed for domain ${signingDomain}. Reason: ${comment}`);
}
const pubKeyData = pki.publicKeyFromPem(publicKey.toString());
return {
signature: BigInt(`0x${Buffer.from(signature, 'base64').toString('hex')}`),
headers: status.signedHeaders,
body,
bodyHash,
signingDomain: dkimResult.signingDomain,
publicKey: BigInt(pubKeyData.n.toString()),
selector: dkimResult.selector,
algo: dkimResult.algo,
format: dkimResult.format,
modulusLength: dkimResult.modulusLength,
appliedSanitization,
};
}
async function tryVerifyDKIM(
email: Buffer | string,
domain: string = '',
fallbackToZKEmailDNSArchive: boolean = false,
) {
const resolver = async (name: string, type: string) => {
try {
const result = await resolveDNSHTTP(name, type);
return result;
} catch (e) {
if (fallbackToZKEmailDNSArchive) {
console.log('DNS over HTTP failed, falling back to ZK Email Archive');
const result = await resolveDNSFromZKEmailArchive(name, type);
return result;
}
throw e;
}
};
const dkimVerifier = new DkimVerifier({
resolver,
});
await writeToStream(dkimVerifier, email as any);
let domainToVerifyDKIM = domain;
if (!domainToVerifyDKIM) {
if (dkimVerifier.headerFrom.length > 1) {
throw new Error('Multiple From header in email and domain for verification not specified');
}
domainToVerifyDKIM = dkimVerifier.headerFrom[0].split('@')[1];
}
const dkimResult = dkimVerifier.results.find((d: any) => d.signingDomain === domainToVerifyDKIM);
if (!dkimResult) {
throw new Error(`DKIM signature not found for domain ${domainToVerifyDKIM}`);
}
dkimResult.headers = dkimVerifier.headers;
return dkimResult;
}