mailauth 7.1.1 → 7.3.0
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.
- package/.c8rc.json +11 -0
- package/.prettierignore +3 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +68 -0
- package/README.md +131 -31
- package/bin/mailauth.js +62 -0
- package/cli.md +150 -8
- package/docs/arc.md +62 -54
- package/docs/bimi.md +153 -54
- package/docs/dkim.md +37 -36
- package/docs/dkim2.md +154 -0
- package/docs/dmarc.md +4 -1
- package/docs/mta-sts.md +12 -9
- package/docs/spf.md +14 -13
- package/index.d.ts +583 -42
- package/lib/arc/index.d.ts +6 -2
- package/lib/arc/index.js +45 -15
- package/lib/bimi/index.js +362 -102
- package/lib/bimi/validate-svg.js +493 -60
- package/lib/commands/dkim2-hash.js +25 -0
- package/lib/commands/dkim2-sign.js +77 -0
- package/lib/commands/dkim2-verify.js +59 -0
- package/lib/commands/report.js +10 -35
- package/lib/commands/resolver.js +51 -0
- package/lib/commands/seal.js +10 -38
- package/lib/commands/spf.js +4 -35
- package/lib/dkim/dkim-signer.js +50 -30
- package/lib/dkim/dkim-verifier.js +79 -27
- package/lib/dkim/header/simple.js +2 -2
- package/lib/dkim/sign.js +7 -2
- package/lib/dkim/syntax.js +11 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dkim2/dkim2-parser.js +52 -0
- package/lib/dkim2/dkim2-signer.js +334 -0
- package/lib/dkim2/dkim2-verifier.js +572 -0
- package/lib/dkim2/fields.js +570 -0
- package/lib/dkim2/hash.d.ts +3 -0
- package/lib/dkim2/hash.js +51 -0
- package/lib/dkim2/key.js +203 -0
- package/lib/dkim2/message-state.js +181 -0
- package/lib/dkim2/recipe.js +134 -0
- package/lib/dkim2/sign.d.ts +3 -0
- package/lib/dkim2/sign.js +38 -0
- package/lib/dkim2/verify.d.ts +3 -0
- package/lib/dkim2/verify.js +19 -0
- package/lib/dmarc/author-domain.js +270 -5
- package/lib/dmarc/get-dmarc-record.js +24 -11
- package/lib/dmarc/verify.js +7 -3
- package/lib/mailauth.js +57 -14
- package/lib/mta-sts.js +37 -11
- package/lib/parse-received.js +214 -25
- package/lib/spf/index.js +21 -4
- package/lib/spf/macro.js +5 -3
- package/lib/spf/spf-verify.js +14 -5
- package/lib/spf/syntax.js +5 -0
- package/lib/tools.js +160 -21
- package/package.json +15 -6
- package/stryker.config.json +16 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { dkim2Sign } = require('../dkim2/sign');
|
|
4
|
+
const { GathererStream } = require('../gatherer-stream');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
|
|
7
|
+
const cmd = async argv => {
|
|
8
|
+
let source = argv.email;
|
|
9
|
+
let stream;
|
|
10
|
+
|
|
11
|
+
if (argv.verbose) {
|
|
12
|
+
console.error(`Reading email message from ${source || 'standard input'}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
stream = source ? fs.createReadStream(source) : process.stdin;
|
|
16
|
+
|
|
17
|
+
let privateKeys = argv.privateKey || [];
|
|
18
|
+
let selectors = argv.selector || [];
|
|
19
|
+
if (privateKeys.length !== selectors.length) {
|
|
20
|
+
let err = new Error('Every --private-key needs a --selector, in the same order');
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let gatherer = new GathererStream({ gather: !argv.headersOnly });
|
|
25
|
+
stream.pipe(gatherer);
|
|
26
|
+
stream.on('error', err => gatherer.emit('error', err));
|
|
27
|
+
|
|
28
|
+
let signatureData = [];
|
|
29
|
+
for (let i = 0; i < privateKeys.length; i++) {
|
|
30
|
+
signatureData.push({ selector: selectors[i], privateKey: await fs.promises.readFile(privateKeys[i], 'utf-8') });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let options = {
|
|
34
|
+
signingDomain: argv.domain,
|
|
35
|
+
signatureData,
|
|
36
|
+
signTime: argv.time ? new Date(argv.time * 1000) : new Date(),
|
|
37
|
+
flags: argv.flag,
|
|
38
|
+
nonce: argv.nonce
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (argv.hash?.length) {
|
|
42
|
+
options.hashAlgorithms = argv.hash;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (argv.nextDomain) {
|
|
46
|
+
options.nextDomain = argv.nextDomain;
|
|
47
|
+
} else {
|
|
48
|
+
options.mailFrom = argv.mailFrom;
|
|
49
|
+
options.rcptTo = argv.rcptTo;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (argv.recipe) {
|
|
53
|
+
options.recipe = JSON.parse(await fs.promises.readFile(argv.recipe, 'utf-8'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (argv.verbose) {
|
|
57
|
+
console.error(`Signing domain: ${options.signingDomain}`);
|
|
58
|
+
console.error(`Selectors: ${selectors.join(', ')}`);
|
|
59
|
+
console.error(`Signing time: ${options.signTime.toISOString()}`);
|
|
60
|
+
console.error('--------');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let signResult = await dkim2Sign(gatherer, options);
|
|
64
|
+
|
|
65
|
+
process.stdout.write(signResult.signatures);
|
|
66
|
+
if (!argv.headersOnly) {
|
|
67
|
+
// print the full message as well
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
let msgStream = gatherer.replay();
|
|
70
|
+
msgStream.pipe(process.stdout, { end: false });
|
|
71
|
+
msgStream.on('end', resolve);
|
|
72
|
+
msgStream.on('error', reject);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
module.exports = cmd;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { dkim2Verify } = require('../dkim2/verify');
|
|
4
|
+
const { createResolver } = require('./resolver');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
|
|
7
|
+
const cmd = async argv => {
|
|
8
|
+
let source = argv.email;
|
|
9
|
+
|
|
10
|
+
if (argv.verbose) {
|
|
11
|
+
console.error(`Reading email message from ${source || 'standard input'}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let stream = source ? fs.createReadStream(source) : process.stdin;
|
|
15
|
+
|
|
16
|
+
let opts = {
|
|
17
|
+
resolver: await createResolver(argv),
|
|
18
|
+
rcptTo: argv.rcptTo
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (typeof argv.mailFrom === 'string') {
|
|
22
|
+
opts.mailFrom = argv.mailFrom;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof argv.time === 'number') {
|
|
26
|
+
opts.curTime = new Date(argv.time * 1000);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof argv.maxAge === 'number') {
|
|
30
|
+
// 0 turns the expiration check off
|
|
31
|
+
opts.maxSignatureAge = argv.maxAge || false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof argv.maxFuture === 'number') {
|
|
35
|
+
// 0 turns the check off, like --max-age
|
|
36
|
+
opts.maxFutureTime = argv.maxFuture || false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof argv.maxInstances === 'number') {
|
|
40
|
+
opts.maxInstances = argv.maxInstances;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let result = await dkim2Verify(stream, opts);
|
|
44
|
+
|
|
45
|
+
if (argv.verbose) {
|
|
46
|
+
for (let warning of result.status.warnings || []) {
|
|
47
|
+
console.error(`Warning: ${warning}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (argv.headersOnly) {
|
|
52
|
+
process.stdout.write(result.info + '\n');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
process.stdout.write(JSON.stringify(result, false, 2) + '\n');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = cmd;
|
package/lib/commands/report.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { authenticate } = require('../mailauth');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
|
-
const {
|
|
5
|
+
const { createResolver } = require('./resolver');
|
|
6
6
|
|
|
7
7
|
const cmd = async argv => {
|
|
8
8
|
let source = argv.email;
|
|
@@ -27,18 +27,23 @@ const cmd = async argv => {
|
|
|
27
27
|
const opts = {
|
|
28
28
|
trustReceived: true,
|
|
29
29
|
strict: !!argv.strict,
|
|
30
|
-
rejectRsaSha1: !!argv.rejectRsaSha1
|
|
30
|
+
rejectRsaSha1: !!argv.rejectRsaSha1,
|
|
31
|
+
dkim2: !!argv.dkim2
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
if (argv.rcptTo) {
|
|
35
|
+
opts.rcptTo = argv.rcptTo;
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
if (argv.clientIp) {
|
|
34
39
|
opts.ip = argv.clientIp;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
if (argv.maxLookups) {
|
|
42
|
+
if (typeof argv.maxLookups === 'number') {
|
|
38
43
|
opts.maxResolveCount = argv.maxLookups;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
if (argv.maxVoidLookups) {
|
|
46
|
+
if (typeof argv.maxVoidLookups === 'number') {
|
|
42
47
|
opts.maxVoidCount = argv.maxVoidLookups;
|
|
43
48
|
}
|
|
44
49
|
|
|
@@ -48,37 +53,7 @@ const cmd = async argv => {
|
|
|
48
53
|
}
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
let dnsCache = JSON.parse(await fs.promises.readFile(argv.dnsCache, 'utf-8'));
|
|
53
|
-
|
|
54
|
-
opts.resolver = async (name, rr) => {
|
|
55
|
-
let match = dnsCache?.[name]?.[rr];
|
|
56
|
-
|
|
57
|
-
if (argv.verbose) {
|
|
58
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!match) {
|
|
62
|
-
let err = new Error('Error');
|
|
63
|
-
err.code = 'ENOTFOUND';
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return match;
|
|
68
|
-
};
|
|
69
|
-
} else if (argv.verbose) {
|
|
70
|
-
opts.resolver = async (name, rr) => {
|
|
71
|
-
let match;
|
|
72
|
-
try {
|
|
73
|
-
match = await resolve(name, rr);
|
|
74
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
|
|
75
|
-
return match;
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
|
|
78
|
-
throw err;
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
}
|
|
56
|
+
opts.resolver = await createResolver(argv);
|
|
82
57
|
|
|
83
58
|
let result = await authenticate(stream, opts);
|
|
84
59
|
process.stdout.write(JSON.stringify(result, false, 2) + '\n');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { resolve } = require('node:dns').promises;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The DNS resolver for a command: answers from the --dns-cache file, or real DNS queries that are
|
|
8
|
+
* logged with --verbose. Undefined when neither is set, so the library uses its default resolver
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} argv Command options
|
|
11
|
+
* @returns {Function|undefined} async (name, rrtype) resolver
|
|
12
|
+
*/
|
|
13
|
+
const createResolver = async argv => {
|
|
14
|
+
if (argv.dnsCache) {
|
|
15
|
+
let dnsCache = JSON.parse(await fs.promises.readFile(argv.dnsCache, 'utf-8'));
|
|
16
|
+
|
|
17
|
+
return async (name, rr) => {
|
|
18
|
+
let match = dnsCache?.[name]?.[rr];
|
|
19
|
+
|
|
20
|
+
if (argv.verbose) {
|
|
21
|
+
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!match) {
|
|
25
|
+
let err = new Error('Error');
|
|
26
|
+
err.code = 'ENOTFOUND';
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return match;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (argv.verbose) {
|
|
35
|
+
return async (name, rr) => {
|
|
36
|
+
let match;
|
|
37
|
+
try {
|
|
38
|
+
match = await resolve(name, rr);
|
|
39
|
+
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
|
|
40
|
+
return match;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
module.exports = { createResolver };
|
package/lib/commands/seal.js
CHANGED
|
@@ -5,7 +5,7 @@ const { authenticate } = require('../mailauth');
|
|
|
5
5
|
const { createSeal } = require('../arc');
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const { GathererStream } = require('../gatherer-stream');
|
|
8
|
-
const {
|
|
8
|
+
const { createResolver } = require('./resolver');
|
|
9
9
|
|
|
10
10
|
const writeMessageBody = async gatherer =>
|
|
11
11
|
new Promise((resolve, reject) => {
|
|
@@ -75,6 +75,9 @@ const cmd = async argv => {
|
|
|
75
75
|
if (argv.authResults || argv.authResultsFile) {
|
|
76
76
|
// Seal-only mode: embed the caller-provided Authentication-Results value without authenticating the message
|
|
77
77
|
let authResults = argv.authResultsFile ? (await fs.promises.readFile(argv.authResultsFile, 'utf-8')).trim() : argv.authResults;
|
|
78
|
+
// a folded value from a file usually has LF line endings, header folding needs CRLF.
|
|
79
|
+
// createSeal refuses any line break that does not continue the header with whitespace
|
|
80
|
+
authResults = authResults.replace(/\r?\n/g, '\r\n');
|
|
78
81
|
|
|
79
82
|
let sealOpts = Object.assign({}, signatureOpts, {
|
|
80
83
|
authResults,
|
|
@@ -95,7 +98,7 @@ const cmd = async argv => {
|
|
|
95
98
|
console.error('--------');
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
let { headers, errors, warnings } = await createSeal(gatherer, { seal: sealOpts });
|
|
101
|
+
let { headers, errors, warnings, instance } = await createSeal(gatherer, { seal: sealOpts });
|
|
99
102
|
|
|
100
103
|
if (!headers.length) {
|
|
101
104
|
// nothing was sealed, say why instead of printing the message as if it had been
|
|
@@ -103,20 +106,19 @@ const cmd = async argv => {
|
|
|
103
106
|
throw err;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
console.error(`warning: sealed with i=${sealOpts.i} and cv=none; a chain with i>1 only validates when cv=pass (use --cv pass)`);
|
|
109
|
+
if (instance > 1 && !argv.cv) {
|
|
110
|
+
console.error(`warning: sealed with i=${instance} and cv=none; a chain with i>1 only validates when cv=pass (use --cv pass)`);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
for (let warning of warnings || []) {
|
|
112
114
|
switch (warning) {
|
|
113
115
|
case 'arc-instance-gap':
|
|
114
|
-
console.error(`warning: sealed with i=${
|
|
116
|
+
console.error(`warning: sealed with i=${instance}, which does not follow the existing ARC chain`);
|
|
115
117
|
break;
|
|
116
118
|
case 'arc-cv-instance':
|
|
117
119
|
// covered by the message above for the default cv
|
|
118
120
|
if (argv.cv) {
|
|
119
|
-
console.error(`warning: cv=${argv.cv} is not valid for i=${
|
|
121
|
+
console.error(`warning: cv=${argv.cv} is not valid for i=${instance} (i=1 takes cv=none, later sets take cv=pass or cv=fail)`);
|
|
120
122
|
}
|
|
121
123
|
break;
|
|
122
124
|
default:
|
|
@@ -159,37 +161,7 @@ const cmd = async argv => {
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
|
|
163
|
-
let dnsCache = JSON.parse(await fs.promises.readFile(argv.dnsCache, 'utf-8'));
|
|
164
|
-
|
|
165
|
-
opts.resolver = async (name, rr) => {
|
|
166
|
-
let match = dnsCache?.[name]?.[rr];
|
|
167
|
-
|
|
168
|
-
if (argv.verbose) {
|
|
169
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!match) {
|
|
173
|
-
let err = new Error('Error');
|
|
174
|
-
err.code = 'ENOTFOUND';
|
|
175
|
-
throw err;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return match;
|
|
179
|
-
};
|
|
180
|
-
} else if (argv.verbose) {
|
|
181
|
-
opts.resolver = async (name, rr) => {
|
|
182
|
-
let match;
|
|
183
|
-
try {
|
|
184
|
-
match = await resolve(name, rr);
|
|
185
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
|
|
186
|
-
return match;
|
|
187
|
-
} catch (err) {
|
|
188
|
-
console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
|
|
189
|
-
throw err;
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
}
|
|
164
|
+
opts.resolver = await createResolver(argv);
|
|
193
165
|
|
|
194
166
|
let result = await authenticate(gatherer, opts);
|
|
195
167
|
|
package/lib/commands/spf.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { spf } = require('../spf');
|
|
4
|
-
const
|
|
5
|
-
const { resolve } = require('node:dns').promises;
|
|
4
|
+
const { createResolver } = require('./resolver');
|
|
6
5
|
|
|
7
6
|
const cmd = async argv => {
|
|
8
7
|
let address = argv.sender;
|
|
@@ -24,11 +23,11 @@ const cmd = async argv => {
|
|
|
24
23
|
opts.ip = argv.clientIp;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
if (argv.maxLookups) {
|
|
26
|
+
if (typeof argv.maxLookups === 'number') {
|
|
28
27
|
opts.maxResolveCount = argv.maxLookups;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
if (argv.maxVoidLookups) {
|
|
30
|
+
if (typeof argv.maxVoidLookups === 'number') {
|
|
32
31
|
opts.maxVoidCount = argv.maxVoidLookups;
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -46,37 +45,7 @@ const cmd = async argv => {
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
let dnsCache = JSON.parse(await fs.promises.readFile(argv.dnsCache, 'utf-8'));
|
|
51
|
-
|
|
52
|
-
opts.resolver = async (name, rr) => {
|
|
53
|
-
let match = dnsCache?.[name]?.[rr];
|
|
54
|
-
|
|
55
|
-
if (argv.verbose) {
|
|
56
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'} (using cache)`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!match) {
|
|
60
|
-
let err = new Error('Error');
|
|
61
|
-
err.code = 'ENOTFOUND';
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return match;
|
|
66
|
-
};
|
|
67
|
-
} else if (argv.verbose) {
|
|
68
|
-
opts.resolver = async (name, rr) => {
|
|
69
|
-
let match;
|
|
70
|
-
try {
|
|
71
|
-
match = await resolve(name, rr);
|
|
72
|
-
console.error(`DNS query for ${rr} ${name}: ${match ? JSON.stringify(match) : 'not found'}`);
|
|
73
|
-
return match;
|
|
74
|
-
} catch (err) {
|
|
75
|
-
console.error(`DNS query for ${rr} ${name}: ${err.message}${err.code ? ` [${err.code}]` : ''}`);
|
|
76
|
-
throw err;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
48
|
+
opts.resolver = await createResolver(argv);
|
|
80
49
|
|
|
81
50
|
let result;
|
|
82
51
|
try {
|
package/lib/dkim/dkim-signer.js
CHANGED
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
createError,
|
|
15
15
|
splitAlgorithm
|
|
16
16
|
} = require('../../lib/tools');
|
|
17
|
-
const { DOMAIN_NAME, SELECTOR, MAX_ARC_INSTANCE, ARC_HEADER_KEYS } = require('./syntax');
|
|
17
|
+
const { DOMAIN_NAME, SELECTOR, LOCAL_PART, MAX_ARC_INSTANCE, ARC_HEADER_KEYS } = require('./syntax');
|
|
18
18
|
const { MessageParser } = require('./message-parser');
|
|
19
19
|
const { dkimBody } = require('./body');
|
|
20
20
|
const { generateCanonicalizedHeader } = require('./header');
|
|
@@ -30,6 +30,9 @@ const MAX_TIMESTAMP = 999999999999;
|
|
|
30
30
|
// RFC 8617 section 4.1.2: header fields an ARC-Message-Signature never covers
|
|
31
31
|
const ARC_EXCLUDED_FIELDS = [...ARC_HEADER_KEYS, 'authentication-results'];
|
|
32
32
|
|
|
33
|
+
// the header and body canonicalization algorithms of RFC 6376 section 3.4
|
|
34
|
+
const CANONICALIZATIONS = ['simple', 'relaxed'];
|
|
35
|
+
|
|
33
36
|
// RFC 8301 section 3.2: signers MUST use RSA keys of at least 1024 bits
|
|
34
37
|
const MIN_RSA_KEY_SIZE = 1024;
|
|
35
38
|
|
|
@@ -78,7 +81,8 @@ class DkimSigner extends MessageParser {
|
|
|
78
81
|
this.getARChain = getARChain;
|
|
79
82
|
// decides the instance of an ARC set that has none set, from the message headers
|
|
80
83
|
this.getArcInstance = getArcInstance;
|
|
81
|
-
if (this.arc.signingDomain
|
|
84
|
+
if (this.arc.signingDomain || this.arc.selector || this.arc.privateKey) {
|
|
85
|
+
// an incomplete set of values is reported by finalize() instead of being skipped
|
|
82
86
|
this.signatureData.push({
|
|
83
87
|
type: 'ARC',
|
|
84
88
|
signingDomain: this.arc.signingDomain, // d=
|
|
@@ -111,11 +115,14 @@ class DkimSigner extends MessageParser {
|
|
|
111
115
|
this.warn(warning);
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
// The header and body canonicalization, and the c= value written for them. A value without
|
|
119
|
+
// a body part uses "simple" for the body (RFC 6376 section 3.5)
|
|
114
120
|
getCanonicalization(signatureData) {
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
let [headerCanon, ...bodyParts] = (signatureData?.canonicalization || this.canonicalization).toString().split('/');
|
|
122
|
+
headerCanon = headerCanon.toLowerCase().trim();
|
|
123
|
+
// anything after a second slash stays in the body part, which then is not valid
|
|
124
|
+
let bodyCanon = (bodyParts.join('/') || 'simple').toLowerCase().trim();
|
|
125
|
+
return { canonicalization: `${headerCanon}/${bodyCanon}`, headerCanon, bodyCanon };
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
getAlgorithm(signatureData) {
|
|
@@ -139,6 +146,11 @@ class DkimSigner extends MessageParser {
|
|
|
139
146
|
let { hashAlgo } = this.getAlgorithm(signatureData);
|
|
140
147
|
let { bodyCanon } = this.getCanonicalization(signatureData);
|
|
141
148
|
|
|
149
|
+
if (!CANONICALIZATIONS.includes(bodyCanon)) {
|
|
150
|
+
// there is no body hash to calculate, finalize() reports the error
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
let hashKey = `${bodyCanon}:${hashAlgo}:${signatureData.maxBodyLength}`;
|
|
143
155
|
|
|
144
156
|
if (!this.bodyHashes.has(hashKey)) {
|
|
@@ -152,24 +164,15 @@ class DkimSigner extends MessageParser {
|
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
166
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
error.canonicalization = header;
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
167
|
+
// Throws for a header or body canonicalization that is not known, takes the values
|
|
168
|
+
// getCanonicalization returns
|
|
169
|
+
validateCanonicalization({ headerCanon, bodyCanon }) {
|
|
170
|
+
if (!CANONICALIZATIONS.includes(headerCanon)) {
|
|
171
|
+
throw createError('Unknown header canonicalization', 'EINVALIDCANON', { canonicalization: headerCanon });
|
|
172
|
+
}
|
|
164
173
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
error.canonicalization = body;
|
|
168
|
-
throw error;
|
|
169
|
-
}
|
|
170
|
-
} catch (err) {
|
|
171
|
-
err.code = 'EINVALIDCANON';
|
|
172
|
-
throw err;
|
|
174
|
+
if (!CANONICALIZATIONS.includes(bodyCanon)) {
|
|
175
|
+
throw createError('Unknown body canonicalization', 'EINVALIDCANON', { canonicalization: bodyCanon });
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
|
|
@@ -213,6 +216,12 @@ class DkimSigner extends MessageParser {
|
|
|
213
216
|
throw err;
|
|
214
217
|
}
|
|
215
218
|
|
|
219
|
+
let localPart = identity.substring(0, atPos);
|
|
220
|
+
if ((localPart && !LOCAL_PART.test(localPart)) || !DOMAIN_NAME.test(domain)) {
|
|
221
|
+
// sig-i-tag = [ Local-part ] "@" domain-name
|
|
222
|
+
this.violation(`Invalid identity ${JSON.stringify(signatureData.identity)}`, 'EINVALIDIDENTITY', 'identity-syntax');
|
|
223
|
+
}
|
|
224
|
+
|
|
216
225
|
if (!isSameOrSubdomain(domain, signatureData.signingDomain)) {
|
|
217
226
|
// "The domain part of the address MUST be the same as, or a subdomain of, the value
|
|
218
227
|
// of the d= tag", or every verifier ignores the signature
|
|
@@ -308,11 +317,15 @@ class DkimSigner extends MessageParser {
|
|
|
308
317
|
}
|
|
309
318
|
|
|
310
319
|
async finalize() {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
if (!this.signatureData.length) {
|
|
321
|
+
// the signing domain, selector and key of a DKIM signature are read from the
|
|
322
|
+
// entries of signatureData only, so there is nothing to sign
|
|
323
|
+
this.errors.push({
|
|
324
|
+
err: createError('No signature configured, set signingDomain, selector and privateKey in signatureData', 'ENOSIGNATURE')
|
|
325
|
+
});
|
|
326
|
+
}
|
|
315
327
|
|
|
328
|
+
for (let signatureData of this.signatureData) {
|
|
316
329
|
const pushError = (err, extra) => {
|
|
317
330
|
this.errors.push(
|
|
318
331
|
Object.assign(
|
|
@@ -327,6 +340,12 @@ class DkimSigner extends MessageParser {
|
|
|
327
340
|
);
|
|
328
341
|
};
|
|
329
342
|
|
|
343
|
+
if (!signatureData.privateKey) {
|
|
344
|
+
// a key that was not loaded, for example from an unset environment variable
|
|
345
|
+
pushError(createError('Missing private key', 'ENOKEY'));
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
330
349
|
let signingHeaderLines = getSigningHeaderLines(this.headers.parsed, this.getFieldNames(signatureData));
|
|
331
350
|
|
|
332
351
|
if (!signingHeaderLines.headers.some(header => header.key === 'from')) {
|
|
@@ -363,11 +382,12 @@ class DkimSigner extends MessageParser {
|
|
|
363
382
|
}
|
|
364
383
|
|
|
365
384
|
let { algorithm, signAlgo, hashAlgo } = this.getAlgorithm(signatureData);
|
|
366
|
-
let
|
|
385
|
+
let canonParts = this.getCanonicalization(signatureData);
|
|
386
|
+
let { canonicalization, bodyCanon } = canonParts;
|
|
367
387
|
|
|
368
388
|
try {
|
|
369
389
|
// throws if invalid
|
|
370
|
-
this.validateCanonicalization(
|
|
390
|
+
this.validateCanonicalization(canonParts);
|
|
371
391
|
} catch (err) {
|
|
372
392
|
this.errors.push({
|
|
373
393
|
algorithm,
|
|
@@ -468,7 +488,7 @@ class DkimSigner extends MessageParser {
|
|
|
468
488
|
instance: signatureData.type === 'ARC' ? instance : false, // ARC only
|
|
469
489
|
identity, // DKIM only
|
|
470
490
|
algorithm,
|
|
471
|
-
canonicalization
|
|
491
|
+
canonicalization,
|
|
472
492
|
|
|
473
493
|
signTime: this.signTime,
|
|
474
494
|
expires: this.expires,
|