mailauth 6.0.0 → 7.0.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +111 -39
- package/bin/mailauth.js +14 -6
- package/cli.md +8 -3
- package/docs/arc.md +126 -43
- package/docs/bimi.md +54 -37
- package/docs/dkim.md +79 -40
- package/docs/dmarc.md +80 -33
- package/docs/mta-sts.md +140 -42
- package/docs/spf.md +69 -4
- package/index.d.ts +495 -43
- package/lib/arc/index.d.ts +10 -8
- package/lib/arc/index.js +505 -124
- package/lib/bimi/index.js +162 -81
- package/lib/commands/report.js +2 -1
- package/lib/commands/seal.js +35 -5
- package/lib/commands/sign.js +8 -1
- package/lib/commands/spf.js +8 -0
- package/lib/dkim/body/relaxed.js +6 -5
- package/lib/dkim/body/simple.js +1 -3
- package/lib/dkim/dkim-signer.js +219 -33
- package/lib/dkim/dkim-verifier.js +574 -231
- package/lib/dkim/header/relaxed.js +6 -43
- package/lib/dkim/header/signature-opts.js +87 -0
- package/lib/dkim/header/simple.js +6 -43
- package/lib/dkim/message-parser.js +18 -4
- package/lib/dkim/sign.d.ts +13 -7
- package/lib/dkim/sign.js +18 -2
- package/lib/dkim/syntax.js +31 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dmarc/author-domain.js +181 -0
- package/lib/dmarc/get-dmarc-record.js +140 -47
- package/lib/dmarc/index.d.ts +29 -2
- package/lib/dmarc/verify.js +120 -44
- package/lib/mailauth.js +75 -36
- package/lib/mta-sts.d.ts +77 -11
- package/lib/mta-sts.js +497 -182
- package/lib/parse-dkim-headers.js +376 -191
- package/lib/parse-received.js +104 -32
- package/lib/spf/index.d.ts +37 -1
- package/lib/spf/index.js +260 -28
- package/lib/spf/macro.js +50 -13
- package/lib/spf/spf-verify.js +440 -204
- package/lib/spf/syntax.js +366 -0
- package/lib/tools.js +522 -116
- package/package.json +1 -1
package/lib/arc/index.js
CHANGED
|
@@ -1,21 +1,110 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { Buffer } = require('node:buffer');
|
|
4
|
+
const { isIP } = require('node:net');
|
|
4
5
|
const {
|
|
5
6
|
parseDkimHeaders,
|
|
6
7
|
formatRelaxedLine,
|
|
7
8
|
stripSignatureValue,
|
|
8
|
-
getPublicKey,
|
|
9
9
|
formatAuthHeaderRow,
|
|
10
10
|
formatSignatureHeaderLine,
|
|
11
11
|
writeToStream,
|
|
12
|
-
validateAlgorithm
|
|
12
|
+
validateAlgorithm,
|
|
13
|
+
createError,
|
|
14
|
+
addWarning,
|
|
15
|
+
KEY_ERROR_COMMENTS,
|
|
16
|
+
splitAlgorithm,
|
|
17
|
+
createKeyCache
|
|
13
18
|
} = require('../../lib/tools');
|
|
19
|
+
const { TIMESTAMP, BODY_LENGTH, MAX_ARC_INSTANCE, ARC_HEADER_KEYS } = require('../dkim/syntax');
|
|
14
20
|
const crypto = require('node:crypto');
|
|
15
21
|
const { DkimSigner } = require('../dkim/dkim-signer');
|
|
16
22
|
|
|
23
|
+
// RFC 8617 section 3.9: chain-status = ("none" / "fail" / "pass"). ABNF strings are case-insensitive
|
|
24
|
+
const CHAIN_STATUS = ['none', 'pass', 'fail'];
|
|
25
|
+
|
|
26
|
+
// RFC 8617 section 3.9: position = 1*2DIGIT
|
|
27
|
+
const INSTANCE_VALUE = /^[0-9]{1,2}$/;
|
|
28
|
+
|
|
29
|
+
// RFC 8617 section 4.1.1: arc-info = instance [CFWS] ";" authres-payload, so the instance is the
|
|
30
|
+
// first thing in an ARC-Authentication-Results value. Comments (without nesting) are allowed as CFWS
|
|
31
|
+
const CFWS = '(?:[ \\t\\r\\n]|\\([^()\\\\]*\\))*';
|
|
32
|
+
const AAR_INSTANCE = new RegExp(`^${CFWS}i${CFWS}=${CFWS}([0-9]{1,2})${CFWS};`);
|
|
33
|
+
|
|
34
|
+
// Tags an ARC-Seal (RFC 8617 section 4.1.3) and an ARC-Message-Signature (section 4.1.2, with
|
|
35
|
+
// the DKIM-Signature tags of RFC 6376 section 3.5 except v= and i=) can not do without
|
|
36
|
+
const REQUIRED_TAGS = {
|
|
37
|
+
'arc-seal': ['a', 'b', 'cv', 'd', 's'],
|
|
38
|
+
'arc-message-signature': ['a', 'b', 'bh', 'd', 'h', 's']
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const NUMERIC_TAGS = {
|
|
42
|
+
'arc-seal': [['t', TIMESTAMP]],
|
|
43
|
+
'arc-message-signature': [
|
|
44
|
+
['t', TIMESTAMP],
|
|
45
|
+
['x', TIMESTAMP],
|
|
46
|
+
['l', BODY_LENGTH]
|
|
47
|
+
]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const SHORT_NAMES = {
|
|
51
|
+
'arc-seal': 'as',
|
|
52
|
+
'arc-message-signature': 'ams',
|
|
53
|
+
'arc-authentication-results': 'aar'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// the cv= value of an ARC set in lower case, or an empty string
|
|
57
|
+
const getCv = arcInstance => (arcInstance?.['arc-seal']?.parsed?.cv?.value || '').toString().toLowerCase().trim();
|
|
58
|
+
|
|
59
|
+
// The highest i= found on the ARC header fields of a message, or 0, and the cv= of the seal
|
|
60
|
+
// with that instance. A new set follows it (RFC 8617 section 5.1 step 3) even when the chain
|
|
61
|
+
// can not be parsed, so that it never reuses an instance that already exists
|
|
62
|
+
const getHighestArcInstance = headers => {
|
|
63
|
+
let highest = 0;
|
|
64
|
+
let latestCv = false;
|
|
65
|
+
for (let row of headers?.parsed || []) {
|
|
66
|
+
if (!ARC_HEADER_KEYS.includes(row.key)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = parseDkimHeaders(row.line)?.parsed;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
let instance = parsed?.i?.value;
|
|
76
|
+
if (typeof instance !== 'number' || !Number.isInteger(instance) || instance < 1) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (instance > highest) {
|
|
80
|
+
highest = instance;
|
|
81
|
+
latestCv = false;
|
|
82
|
+
}
|
|
83
|
+
if (instance === highest && row.key === 'arc-seal' && typeof parsed?.cv?.value === 'string') {
|
|
84
|
+
latestCv = parsed.cv.value.toLowerCase().trim();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { highest, latestCv };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Where a new ARC set goes (RFC 8617 section 5.1 steps 2 and 3): the highest existing instance
|
|
91
|
+
// and the cv= of its seal, from the header fields and from the parsed chain, for callers that
|
|
92
|
+
// pass the chain without the complete header list
|
|
93
|
+
const getArcPosition = (headers, chain) => {
|
|
94
|
+
let { highest, latestCv } = getHighestArcInstance(headers);
|
|
95
|
+
let lastEntry = chain?.length ? chain[chain.length - 1] : false;
|
|
96
|
+
if (lastEntry && lastEntry.i >= highest) {
|
|
97
|
+
highest = lastEntry.i;
|
|
98
|
+
latestCv = getCv(lastEntry) || latestCv;
|
|
99
|
+
}
|
|
100
|
+
return { highest, latestCv };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// the DNS name of the key record of an ARC-Seal
|
|
104
|
+
const sealKeyName = seal => `${seal.parsed?.s?.value}._domainkey.${seal.parsed?.d?.value}`;
|
|
105
|
+
|
|
17
106
|
const verifyAS = async (chain, opts) => {
|
|
18
|
-
const {
|
|
107
|
+
const { strict, warnings, keyCache } = opts || {};
|
|
19
108
|
|
|
20
109
|
let chunks = [];
|
|
21
110
|
let signatureHeader;
|
|
@@ -32,39 +121,63 @@ const verifyAS = async (chain, opts) => {
|
|
|
32
121
|
} else {
|
|
33
122
|
signatureHeader = link['arc-seal'];
|
|
34
123
|
if (!signatureHeader.parsed?.s?.value || !signatureHeader.parsed?.d?.value) {
|
|
35
|
-
|
|
36
|
-
err.code = 'invalid_arc_seal';
|
|
37
|
-
throw err;
|
|
124
|
+
throw createError(`Invalid ARC-Seal header`, 'invalid_arc_seal');
|
|
38
125
|
}
|
|
39
126
|
|
|
40
|
-
chunks.push(stripSignatureValue(formatRelaxedLine(link['arc-seal'].original)));
|
|
127
|
+
chunks.push(stripSignatureValue(formatRelaxedLine(link['arc-seal'].original), strict));
|
|
41
128
|
}
|
|
42
129
|
}
|
|
43
130
|
|
|
131
|
+
// The algorithm is read from the seal itself. Every seal of the chain is verified here, not
|
|
132
|
+
// only the newest one, so nothing set up elsewhere for the newest seal can be relied on
|
|
133
|
+
let algorithm = (signatureHeader.parsed?.a?.value || '').toString().toLowerCase().trim();
|
|
134
|
+
try {
|
|
135
|
+
// rsa-sha256 and ed25519-sha256 only, ARC has no rsa-sha1
|
|
136
|
+
validateAlgorithm(algorithm, true);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
throw createError(`i=${chain.length} invalid seal algorithm`, 'invalid_arc_seal');
|
|
139
|
+
}
|
|
140
|
+
let { signAlgo } = splitAlgorithm(algorithm);
|
|
141
|
+
|
|
44
142
|
let canonicalizedHeader = Buffer.concat(chunks);
|
|
45
143
|
|
|
46
144
|
let publicKey;
|
|
47
|
-
let queryDomain =
|
|
145
|
+
let queryDomain = sealKeyName(signatureHeader);
|
|
48
146
|
try {
|
|
49
|
-
|
|
147
|
+
// the key record's own h= and s= restrictions apply to ARC keys as well (RFC 6376
|
|
148
|
+
// section 3.6.1), and every ARC signature uses sha256
|
|
149
|
+
let res = await keyCache.get('AS', queryDomain, opts?.minBitLength, { strict, hashAlgo: 'sha256' });
|
|
50
150
|
publicKey = res?.publicKey;
|
|
151
|
+
|
|
152
|
+
if (res?.keyType && res.keyType !== signAlgo) {
|
|
153
|
+
// RFC 6376 section 6.1.2 step 8: the key has to suit the a= algorithm
|
|
154
|
+
throw createError('Key type does not match the seal algorithm', 'EINVALIDKEYALGO');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (Array.isArray(warnings)) {
|
|
158
|
+
addWarning(warnings, ...(res?.warnings || []));
|
|
159
|
+
}
|
|
51
160
|
} catch (err) {
|
|
52
161
|
err.queryDomain = queryDomain;
|
|
53
162
|
// the error code is what the ARC result reports as a comment
|
|
54
163
|
throw err;
|
|
55
164
|
}
|
|
56
165
|
|
|
57
|
-
let pass =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
166
|
+
let pass = false;
|
|
167
|
+
try {
|
|
168
|
+
pass = crypto.verify(
|
|
169
|
+
signAlgo === 'rsa' ? 'sha256' : null,
|
|
170
|
+
// RFC 8463 section 3: Ed25519 signs the SHA-256 hash of the canonicalized data
|
|
171
|
+
signAlgo === 'rsa' ? canonicalizedHeader : crypto.createHash('sha256').update(canonicalizedHeader).digest(),
|
|
172
|
+
publicKey,
|
|
173
|
+
Buffer.from(signatureHeader.parsed?.b?.value || '', 'base64')
|
|
174
|
+
);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
pass = false;
|
|
177
|
+
}
|
|
63
178
|
|
|
64
179
|
if (!pass) {
|
|
65
|
-
|
|
66
|
-
err.code = 'failing_arc_seal';
|
|
67
|
-
throw err;
|
|
180
|
+
throw createError(`i=${chain.length} seal signature validation failed`, 'failing_arc_seal');
|
|
68
181
|
}
|
|
69
182
|
|
|
70
183
|
return true;
|
|
@@ -75,13 +188,13 @@ const signAS = async (chain, entry, signatureData) => {
|
|
|
75
188
|
|
|
76
189
|
instance = instance || 1;
|
|
77
190
|
|
|
78
|
-
const signAlgo = algorithm
|
|
191
|
+
const { signAlgo } = splitAlgorithm(algorithm);
|
|
79
192
|
|
|
80
193
|
signTime = signTime || new Date();
|
|
81
194
|
|
|
82
195
|
let chunks = [];
|
|
83
196
|
|
|
84
|
-
if (
|
|
197
|
+
if (cv === 'pass') {
|
|
85
198
|
// sign existing only chain for passing validation
|
|
86
199
|
for (let i = 0; i < chain.length; i++) {
|
|
87
200
|
let link = chain[i];
|
|
@@ -146,35 +259,172 @@ const signAS = async (chain, entry, signatureData) => {
|
|
|
146
259
|
return formatSignatureHeaderLine('AS', headerOpts, true);
|
|
147
260
|
};
|
|
148
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Validates the ARC-Seal signatures of a chain (RFC 8617 section 5.2 steps 2, 3.C and 6).
|
|
264
|
+
* Throws if the chain is not valid, so it never returns true for a chain with an invalid seal.
|
|
265
|
+
*
|
|
266
|
+
* @param {Object} data { chain } as returned by getARChain()
|
|
267
|
+
* @param {Object} [opts] { resolver, minBitLength, strict, warnings, keyCache }, where keyCache
|
|
268
|
+
* is a createKeyCache() of the same resolver, shared with other key lookups of the message
|
|
269
|
+
* @returns {Boolean} false if there is no chain, true if every seal validates
|
|
270
|
+
*/
|
|
149
271
|
const verifyASChain = async (data, opts) => {
|
|
150
272
|
if (!data?.chain?.length) {
|
|
151
273
|
return false;
|
|
152
274
|
}
|
|
153
275
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
276
|
+
const chain = data.chain;
|
|
277
|
+
opts = Object.assign({}, opts);
|
|
278
|
+
if (!opts.keyCache) {
|
|
279
|
+
opts.keyCache = createKeyCache(opts.resolver);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < chain.length; i++) {
|
|
283
|
+
let arcInstance = chain[i];
|
|
284
|
+
|
|
285
|
+
if (arcInstance?.i !== i + 1) {
|
|
286
|
+
throw createError(`i=${arcInstance?.i} expected=${i + 1}`, 'invalid_arc_instance');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (let headerKey of ARC_HEADER_KEYS) {
|
|
290
|
+
if (!arcInstance[headerKey]) {
|
|
291
|
+
throw createError(`i=${arcInstance.i} no ${headerKey} set`, 'missing_arc_header');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Step 2 (a newest seal with cv=fail fails the chain) and step 3.C: cv=none for i=1 and
|
|
296
|
+
// cv=pass for every later set. The values are compared case-insensitively, as an ABNF
|
|
297
|
+
// string is. A seal with cv=fail only signs its own set (section 5.1.2), so it can
|
|
298
|
+
// never vouch for the sets before it
|
|
299
|
+
let cv = getCv(arcInstance);
|
|
300
|
+
let expected = i === 0 ? 'none' : 'pass';
|
|
301
|
+
if (cv !== expected) {
|
|
302
|
+
throw createError(`i=${arcInstance.i} cv=${cv}`, 'invalid_cv_value');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// The keys of all the seals are fetched at once. A failed lookup is only reported when its
|
|
307
|
+
// seal is validated, so the errors come in the same order as without the prefetch
|
|
308
|
+
for (let link of chain) {
|
|
309
|
+
let seal = link['arc-seal'];
|
|
310
|
+
if (seal.parsed?.s?.value && seal.parsed?.d?.value) {
|
|
311
|
+
opts.keyCache.get('AS', sealKeyName(seal), opts.minBitLength, { strict: opts.strict, hashAlgo: 'sha256' });
|
|
161
312
|
}
|
|
313
|
+
}
|
|
162
314
|
|
|
163
|
-
|
|
164
|
-
|
|
315
|
+
// Step 6: every seal, from the newest to the oldest, throws if validation fails
|
|
316
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
317
|
+
await verifyAS(chain.slice(0, i + 1), opts);
|
|
165
318
|
}
|
|
166
319
|
|
|
167
320
|
return true;
|
|
168
321
|
};
|
|
169
322
|
|
|
170
|
-
|
|
323
|
+
// The instance of an ARC header field exactly as written, or null. For the
|
|
324
|
+
// ARC-Authentication-Results it is the leading "i=N;" (RFC 8617 section 4.1.1), for the other
|
|
325
|
+
// two it is the value of the (case-sensitive) i tag
|
|
326
|
+
const getRawInstance = (headerKey, value, line) => {
|
|
327
|
+
if (headerKey === 'arc-authentication-results') {
|
|
328
|
+
let str = (line || '').toString('binary');
|
|
329
|
+
str = str.substring(str.indexOf(':') + 1);
|
|
330
|
+
let match = AAR_INSTANCE.exec(str);
|
|
331
|
+
return match ? match[1] : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let tag = (value?.tags || []).find(tag => tag.name === 'i');
|
|
335
|
+
return tag ? tag.value : null;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// Checks the tag-list of an ARC-Seal or ARC-Message-Signature (RFC 6376 section 3.2 and 3.5,
|
|
339
|
+
// RFC 8617 section 4.1). The strict mode, where getARChain has read the tags case-sensitively,
|
|
340
|
+
// rejects the set for any syntax error, the lenient mode keeps reading them the way it always
|
|
341
|
+
// has and only notes what the strict mode would have rejected
|
|
342
|
+
const checkTagList = (arcInstance, headerKey, strict, warn) => {
|
|
343
|
+
let header = arcInstance[headerKey];
|
|
344
|
+
let tags = header.tags || [];
|
|
345
|
+
|
|
346
|
+
let problems = [].concat(header.syntaxErrors || []);
|
|
347
|
+
|
|
348
|
+
let rawTags = new Map();
|
|
349
|
+
for (let tag of tags) {
|
|
350
|
+
rawTags.set(tag.name, tag.value);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (let tag of REQUIRED_TAGS[headerKey]) {
|
|
354
|
+
if (!rawTags.has(tag)) {
|
|
355
|
+
problems.push(`missing ${tag}=`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (let [tag, format] of NUMERIC_TAGS[headerKey]) {
|
|
360
|
+
if (rawTags.has(tag) && !format.test(rawTags.get(tag))) {
|
|
361
|
+
problems.push(`invalid ${tag}=`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (strict) {
|
|
366
|
+
if (problems.length) {
|
|
367
|
+
throw createError(`i=${arcInstance.i} ${SHORT_NAMES[headerKey]} ${problems[0]}`, 'invalid_arc_tags');
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (problems.length || tags.some(tag => tag.name !== tag.name.toLowerCase())) {
|
|
373
|
+
warn('arc-tag-syntax');
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Collects the ARC sets of a message and validates the structure of the chain (RFC 8617
|
|
379
|
+
* section 5.2 steps 1 to 3).
|
|
380
|
+
*
|
|
381
|
+
* @param {Object} headers Parsed message headers
|
|
382
|
+
* @param {Object} [opts]
|
|
383
|
+
* @param {Boolean} [opts.strict=false] Apply the RFC 8617 and RFC 6376 syntax rules exactly
|
|
384
|
+
* @param {Array} [opts.warnings] Lax acceptance markers are added here
|
|
385
|
+
* @returns {Array|false} The ARC sets ordered by instance, or false if there are none. Throws
|
|
386
|
+
* for a chain that is not valid
|
|
387
|
+
*/
|
|
388
|
+
const getARChain = (headers, opts) => {
|
|
389
|
+
opts = opts || {};
|
|
390
|
+
const strict = !!opts.strict;
|
|
391
|
+
const warn = warning => {
|
|
392
|
+
if (Array.isArray(opts.warnings)) {
|
|
393
|
+
addWarning(opts.warnings, warning);
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
171
397
|
let headerRows = (headers && headers.parsed) || [];
|
|
172
398
|
|
|
173
399
|
let arcChain = new Map();
|
|
174
400
|
for (let row of headerRows) {
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
401
|
+
if (ARC_HEADER_KEYS.includes(row.key)) {
|
|
402
|
+
// RFC 6376 section 3.2: "Tags MUST be interpreted in a case-sensitive manner", so
|
|
403
|
+
// the strict mode keys the tags of the ARC-Seal and ARC-Message-Signature by name
|
|
404
|
+
// as written
|
|
405
|
+
let value = parseDkimHeaders(row.line, { strict });
|
|
406
|
+
|
|
407
|
+
let rawInstance = getRawInstance(row.key, value, row.line);
|
|
408
|
+
let validInstance =
|
|
409
|
+
typeof rawInstance === 'string' && INSTANCE_VALUE.test(rawInstance) && Number(rawInstance) >= 1 && Number(rawInstance) <= MAX_ARC_INSTANCE;
|
|
410
|
+
|
|
411
|
+
let instance;
|
|
412
|
+
if (strict) {
|
|
413
|
+
if (!validInstance) {
|
|
414
|
+
// RFC 8617 section 3.9, instance = "i=" 1*2DIGIT, 1 to 50. A header field
|
|
415
|
+
// with any other instance is not part of a valid chain
|
|
416
|
+
throw createError(`invalid ${SHORT_NAMES[row.key]} instance`, 'invalid_arc_instance');
|
|
417
|
+
}
|
|
418
|
+
instance = Number(rawInstance);
|
|
419
|
+
} else {
|
|
420
|
+
// the lenient mode reads the instance as a number, and ignores ARC header
|
|
421
|
+
// fields that have none
|
|
422
|
+
instance = value?.parsed?.i?.value;
|
|
423
|
+
if (!validInstance) {
|
|
424
|
+
warn('arc-instance-syntax');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
178
428
|
if (instance) {
|
|
179
429
|
if (!arcChain.has(instance)) {
|
|
180
430
|
arcChain.set(instance, {
|
|
@@ -182,9 +432,7 @@ const getARChain = headers => {
|
|
|
182
432
|
});
|
|
183
433
|
} else if (arcChain.get(instance)[row.key]) {
|
|
184
434
|
// value for this header is already set
|
|
185
|
-
|
|
186
|
-
err.code = 'multiple_arc_keys';
|
|
187
|
-
throw err;
|
|
435
|
+
throw createError(`i=${instance} multiple ${row.key} values`, 'multiple_arc_keys');
|
|
188
436
|
}
|
|
189
437
|
arcChain.get(instance)[row.key] = value;
|
|
190
438
|
}
|
|
@@ -197,88 +445,75 @@ const getARChain = headers => {
|
|
|
197
445
|
return false;
|
|
198
446
|
}
|
|
199
447
|
|
|
200
|
-
if (arcChain.length >
|
|
201
|
-
|
|
202
|
-
err.code = 'invalid_arc_count';
|
|
203
|
-
throw err;
|
|
448
|
+
if (arcChain.length > MAX_ARC_INSTANCE) {
|
|
449
|
+
throw createError(`chain-length=${arcChain.length}`, 'invalid_arc_count');
|
|
204
450
|
}
|
|
205
451
|
|
|
206
452
|
for (let i = 0; i < arcChain.length; i++) {
|
|
207
453
|
const arcInstance = arcChain[i];
|
|
454
|
+
const isNewest = i === arcChain.length - 1;
|
|
208
455
|
|
|
209
456
|
if (arcInstance.i !== i + 1) {
|
|
210
457
|
// not a complete sequence
|
|
211
|
-
|
|
212
|
-
err.code = 'invalid_arc_instance';
|
|
213
|
-
throw err;
|
|
458
|
+
throw createError(`i=${arcInstance.i} expected=${i + 1}`, 'invalid_arc_instance');
|
|
214
459
|
}
|
|
215
460
|
|
|
216
|
-
for (let headerKey of
|
|
461
|
+
for (let headerKey of ARC_HEADER_KEYS) {
|
|
217
462
|
if (!arcInstance[headerKey]) {
|
|
218
463
|
// missing required header
|
|
219
|
-
|
|
220
|
-
err.code = 'missing_arc_header';
|
|
221
|
-
throw err;
|
|
464
|
+
throw createError(`i=${arcInstance.i} no ${headerKey} set`, 'missing_arc_header');
|
|
222
465
|
}
|
|
223
466
|
}
|
|
224
467
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
468
|
+
// Every seal is validated, so the syntax of every seal counts. Only the newest
|
|
469
|
+
// ARC-Message-Signature is validated (step 4), the older ones are just sealed data
|
|
470
|
+
checkTagList(arcInstance, 'arc-seal', strict, warn);
|
|
471
|
+
if (isNewest) {
|
|
472
|
+
checkTagList(arcInstance, 'arc-message-signature', strict, warn);
|
|
229
473
|
}
|
|
230
474
|
|
|
231
|
-
let
|
|
232
|
-
if (asC && asC !== 'relaxed/relaxed') {
|
|
233
|
-
let err = new Error(`i=${arcInstance.i} invalid as c`);
|
|
234
|
-
err.code = 'unexpected_as_c_value';
|
|
235
|
-
throw err;
|
|
236
|
-
}
|
|
475
|
+
let cv = getCv(arcInstance);
|
|
237
476
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
arcInstance['arc-message-signature'].parsed.c = { value: 'relaxed/relaxed' };
|
|
477
|
+
if (i === 0 && cv !== 'none') {
|
|
478
|
+
throw createError(`i=1 cv="${arcInstance['arc-seal']?.parsed?.cv?.value}`, 'invalid_cv_value');
|
|
241
479
|
}
|
|
242
480
|
|
|
481
|
+
// c= is not checked here. An ARC-Seal has no c= tag (RFC 8617 section 4.1.3, it always
|
|
482
|
+
// uses relaxed header canonicalization), so a c= tag on it is an unknown tag, and
|
|
483
|
+
// unknown tags are ignored (RFC 6376 section 3.2). The newest ARC-Message-Signature is
|
|
484
|
+
// verified by the DKIM verifier, where a missing c= means simple/simple (RFC 6376
|
|
485
|
+
// section 3.5) and the lenient mode also tries relaxed/relaxed
|
|
486
|
+
|
|
243
487
|
if (arcInstance['arc-seal']?.parsed?.a && !arcInstance['arc-seal']?.parsed?.a?.value) {
|
|
244
|
-
|
|
245
|
-
err.code = 'invalid_a_value';
|
|
246
|
-
throw err;
|
|
488
|
+
throw createError(`i=${arcInstance.i} empty a`, 'invalid_a_value');
|
|
247
489
|
}
|
|
248
490
|
|
|
249
491
|
if (!arcInstance['arc-seal']?.parsed?.a?.value) {
|
|
250
|
-
|
|
251
|
-
err.code = 'missing_a_value';
|
|
252
|
-
throw err;
|
|
492
|
+
throw createError(`i=${arcInstance.i} missing a`, 'missing_a_value');
|
|
253
493
|
}
|
|
254
494
|
|
|
255
495
|
// throws if using non-supported algorithm
|
|
256
496
|
validateAlgorithm(arcInstance['arc-seal']?.parsed?.a?.value, true);
|
|
257
497
|
validateAlgorithm(arcInstance['arc-message-signature']?.parsed?.a?.value, true);
|
|
258
498
|
|
|
259
|
-
if (i > 0 &&
|
|
260
|
-
|
|
261
|
-
err.code = 'invalid_cv_value';
|
|
262
|
-
throw err;
|
|
499
|
+
if (i > 0 && cv !== 'pass') {
|
|
500
|
+
throw createError(`i=${arcInstance.i} cv=${arcInstance['arc-seal']?.parsed?.cv?.value}`, 'invalid_cv_value');
|
|
263
501
|
}
|
|
264
502
|
|
|
265
503
|
if (arcInstance['arc-seal']?.parsed?.h) {
|
|
266
|
-
|
|
267
|
-
err.code = 'unexpected_as_h_value';
|
|
268
|
-
throw err;
|
|
504
|
+
throw createError(`i=${arcInstance.i} unexpected as h`, 'unexpected_as_h_value');
|
|
269
505
|
}
|
|
270
506
|
|
|
271
507
|
let amsH = arcInstance['arc-message-signature']?.parsed?.h?.value
|
|
272
|
-
?.
|
|
508
|
+
?.toString()
|
|
509
|
+
.trim()
|
|
273
510
|
.toLowerCase()
|
|
274
511
|
.split(':')
|
|
275
512
|
.map(v => v.trim())
|
|
276
513
|
.filter(v => v);
|
|
277
514
|
|
|
278
515
|
if (amsH?.some(v => v === 'arc-seal')) {
|
|
279
|
-
|
|
280
|
-
err.code = 'invalid_ams_h_value';
|
|
281
|
-
throw err;
|
|
516
|
+
throw createError(`i=${arcInstance.i} invalid ams h`, 'invalid_ams_h_value');
|
|
282
517
|
}
|
|
283
518
|
}
|
|
284
519
|
|
|
@@ -287,6 +522,9 @@ const getARChain = headers => {
|
|
|
287
522
|
|
|
288
523
|
// {chain, last}
|
|
289
524
|
const arc = async (data, opts) => {
|
|
525
|
+
opts = opts || {};
|
|
526
|
+
const strict = !!opts.strict;
|
|
527
|
+
|
|
290
528
|
const status = {
|
|
291
529
|
result: 'none'
|
|
292
530
|
};
|
|
@@ -300,24 +538,35 @@ const arc = async (data, opts) => {
|
|
|
300
538
|
value: data.chain
|
|
301
539
|
});
|
|
302
540
|
|
|
541
|
+
// what the lenient mode accepted that the strict mode would have rejected
|
|
542
|
+
const warnings = [].concat(data?.warnings || []);
|
|
543
|
+
|
|
303
544
|
try {
|
|
304
545
|
if (data.error) {
|
|
305
546
|
// raise error from `getARChain`
|
|
306
547
|
throw data.error;
|
|
307
548
|
}
|
|
308
549
|
|
|
309
|
-
let hasChain = await verifyASChain(data,
|
|
550
|
+
let hasChain = await verifyASChain(data, {
|
|
551
|
+
resolver: opts.resolver,
|
|
552
|
+
minBitLength: opts.minBitLength,
|
|
553
|
+
strict,
|
|
554
|
+
warnings,
|
|
555
|
+
// the key lookups of the DKIM verifier, when they used the same resolver
|
|
556
|
+
keyCache: data.keyCache?.resolver === opts.resolver ? data.keyCache : undefined
|
|
557
|
+
});
|
|
558
|
+
|
|
310
559
|
if (hasChain) {
|
|
311
560
|
result.i = data?.lastEntry?.i || false;
|
|
312
561
|
result.signature = data?.lastEntry?.messageSignature || false;
|
|
313
562
|
|
|
314
563
|
if (result?.signature?.status?.result !== 'pass') {
|
|
315
564
|
// no valid ARC-Message-Signature found
|
|
316
|
-
|
|
317
|
-
err.code = 'missing_valid_ams';
|
|
318
|
-
throw err;
|
|
565
|
+
throw createError(`i=${result.i} no valid signature`, 'missing_valid_ams');
|
|
319
566
|
}
|
|
320
567
|
|
|
568
|
+
addWarning(warnings, ...(result.signature.status.warnings || []));
|
|
569
|
+
|
|
321
570
|
result.authenticationResults = data?.lastEntry?.['arc-authentication-results']?.parsed;
|
|
322
571
|
|
|
323
572
|
if (result.authenticationResults) {
|
|
@@ -357,7 +606,7 @@ const arc = async (data, opts) => {
|
|
|
357
606
|
result.i = data?.lastEntry?.i || false;
|
|
358
607
|
status.result = 'fail';
|
|
359
608
|
// if last entry was listed as passing then add our seal even if the validation failed
|
|
360
|
-
status.shouldSeal = ['pass', 'none'].includes(data?.lastEntry
|
|
609
|
+
status.shouldSeal = ['pass', 'none'].includes(getCv(data?.lastEntry));
|
|
361
610
|
|
|
362
611
|
switch (err.code) {
|
|
363
612
|
case 'invalid_arc_seal':
|
|
@@ -370,8 +619,7 @@ const arc = async (data, opts) => {
|
|
|
370
619
|
case 'unexpected_as_h_value':
|
|
371
620
|
case 'invalid_ams_h_value':
|
|
372
621
|
case 'missing_valid_ams':
|
|
373
|
-
case '
|
|
374
|
-
case 'unexpected_ams_c_value':
|
|
622
|
+
case 'invalid_arc_tags':
|
|
375
623
|
status.comment = err.message
|
|
376
624
|
.toLowerCase()
|
|
377
625
|
.replace(/["'()]/g, ' ')
|
|
@@ -382,26 +630,19 @@ const arc = async (data, opts) => {
|
|
|
382
630
|
|
|
383
631
|
case 'ENOTFOUND':
|
|
384
632
|
case 'ENODATA':
|
|
385
|
-
if (err.queryDomain) {
|
|
386
|
-
status.comment = `no key for ${err.queryDomain}`;
|
|
387
|
-
}
|
|
388
|
-
break;
|
|
389
|
-
|
|
390
633
|
case 'EINVALIDVER':
|
|
391
|
-
if (err.queryDomain) {
|
|
392
|
-
status.comment = `unknown key version for ${err.queryDomain}`;
|
|
393
|
-
}
|
|
394
|
-
break;
|
|
395
|
-
|
|
396
634
|
case 'EINVALIDTYPE':
|
|
635
|
+
case 'EINVALIDVAL':
|
|
636
|
+
case 'EINVALIDHASH':
|
|
637
|
+
case 'EINVALIDSERVICE':
|
|
397
638
|
if (err.queryDomain) {
|
|
398
|
-
status.comment =
|
|
639
|
+
status.comment = `${KEY_ERROR_COMMENTS.get(err.code)} for ${err.queryDomain}`;
|
|
399
640
|
}
|
|
400
641
|
break;
|
|
401
642
|
|
|
402
|
-
case '
|
|
643
|
+
case 'EINVALIDKEYALGO':
|
|
403
644
|
if (err.queryDomain) {
|
|
404
|
-
status.comment = `
|
|
645
|
+
status.comment = `inappropriate key algorithm for ${err.queryDomain}`;
|
|
405
646
|
}
|
|
406
647
|
break;
|
|
407
648
|
|
|
@@ -414,7 +655,14 @@ const arc = async (data, opts) => {
|
|
|
414
655
|
}
|
|
415
656
|
}
|
|
416
657
|
|
|
417
|
-
if (
|
|
658
|
+
if (strict && opts.ip && isIP(opts.ip.toString())) {
|
|
659
|
+
// RFC 8617 section 6: smtp.remote-ip SHOULD be recorded
|
|
660
|
+
status.smtp = { 'remote-ip': opts.ip.toString() };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// The strict mode reports arc=none as well (RFC 8617 section 5.2 step 1 and section 6), the
|
|
664
|
+
// lenient mode leaves it out of the Authentication-Results header as it always has
|
|
665
|
+
if (status.result !== 'none' || strict) {
|
|
418
666
|
if (status.result === 'pass' && result.authenticationResults) {
|
|
419
667
|
let comment = [`i=${result.i}`, result.authenticationResults.spf ? `spf=${result.authenticationResults.spf.result}` : false];
|
|
420
668
|
|
|
@@ -438,18 +686,83 @@ const arc = async (data, opts) => {
|
|
|
438
686
|
status.comment = comment.filter(v => v).join(' ');
|
|
439
687
|
}
|
|
440
688
|
|
|
441
|
-
result.info = formatAuthHeaderRow('arc', status);
|
|
689
|
+
result.info = formatAuthHeaderRow('arc', status, { strict });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (warnings.length && status.result !== 'fail') {
|
|
693
|
+
// never rendered into the Authentication-Results header
|
|
694
|
+
result.warnings = warnings;
|
|
442
695
|
}
|
|
443
696
|
|
|
444
697
|
return result;
|
|
445
698
|
};
|
|
446
699
|
|
|
700
|
+
const sealError = (seal, err) => ({
|
|
701
|
+
type: 'ARC',
|
|
702
|
+
selector: seal?.selector,
|
|
703
|
+
signingDomain: seal?.signingDomain,
|
|
704
|
+
err
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// whether an option has a value, false and an empty string mean that it is not set
|
|
708
|
+
const isSet = value => typeof value !== 'undefined' && value !== null && value !== false && value !== '';
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Creates an ARC set (RFC 8617 section 5.1) for a message
|
|
712
|
+
*
|
|
713
|
+
* @param {ReadableStream|Buffer|String|false} input The message, or false to use `data.headers`
|
|
714
|
+
* and `seal.bodyHash` instead
|
|
715
|
+
* @param {Object} data { headers, arc, seal, strict }
|
|
716
|
+
* @returns {Object} { headers, errors, warnings }. `headers` is empty when no set was created,
|
|
717
|
+
* and `errors` says why
|
|
718
|
+
*/
|
|
447
719
|
const createSeal = async (input, data) => {
|
|
448
720
|
let { headers, arc, seal } = data;
|
|
449
721
|
let bodyHash = seal?.bodyHash;
|
|
450
722
|
|
|
723
|
+
const strict = !!(data.strict || seal?.strict);
|
|
724
|
+
const warnings = [];
|
|
725
|
+
const warn = warning => addWarning(warnings, warning);
|
|
726
|
+
|
|
727
|
+
let dkimSigner;
|
|
728
|
+
// the signer's warnings first, then the ones of the seal, each listed once
|
|
729
|
+
const collectWarnings = () => addWarning([].concat(dkimSigner?.warnings || []), ...warnings);
|
|
730
|
+
|
|
731
|
+
const refuse = (message, code) => ({ headers: [], errors: [sealError(seal, createError(message, code))], warnings: collectWarnings() });
|
|
732
|
+
|
|
733
|
+
// Caller input that can only produce an ARC set every validator rejects is refused in
|
|
734
|
+
// every mode
|
|
735
|
+
let explicitInstance = isSet(seal.i);
|
|
736
|
+
if (explicitInstance) {
|
|
737
|
+
let instance = Number(seal.i);
|
|
738
|
+
if (!Number.isInteger(instance) || instance < 1 || instance > MAX_ARC_INSTANCE) {
|
|
739
|
+
// RFC 8617 section 4.2.1, instance values range from 1 to 50
|
|
740
|
+
return refuse(`ARC instance ${JSON.stringify(seal.i)} is out of range (1-${MAX_ARC_INSTANCE})`, 'EINVALIDINSTANCE');
|
|
741
|
+
}
|
|
742
|
+
seal.i = instance;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
let cv = false;
|
|
746
|
+
if (isSet(seal.cv)) {
|
|
747
|
+
cv = seal.cv.toString().toLowerCase().trim();
|
|
748
|
+
if (!CHAIN_STATUS.includes(cv)) {
|
|
749
|
+
return refuse(`Invalid cv value ${JSON.stringify(seal.cv)} (expecting "none", "pass" or "fail")`, 'EINVALIDCV');
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
let algorithm = false;
|
|
754
|
+
if (seal.algorithm) {
|
|
755
|
+
algorithm = seal.algorithm.toString().toLowerCase().trim();
|
|
756
|
+
try {
|
|
757
|
+
// rsa-sha256 or ed25519-sha256, no validator accepts an ARC signature made with sha1
|
|
758
|
+
validateAlgorithm(algorithm, true);
|
|
759
|
+
} catch (err) {
|
|
760
|
+
return refuse(`Unsupported ARC algorithm ${JSON.stringify(seal.algorithm)}`, 'EINVALIDALGO');
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
451
764
|
// Step 1. Calculate ARC-Message-Signature
|
|
452
|
-
|
|
765
|
+
dkimSigner = new DkimSigner({
|
|
453
766
|
// headers and bodyHash are prepared values if we do not have the source message anymore
|
|
454
767
|
headers,
|
|
455
768
|
bodyHash: seal.bodyHash,
|
|
@@ -459,15 +772,22 @@ const createSeal = async (input, data) => {
|
|
|
459
772
|
// which headers to sign
|
|
460
773
|
headerList: seal.headerList,
|
|
461
774
|
|
|
775
|
+
strict,
|
|
776
|
+
|
|
462
777
|
arc: {
|
|
463
|
-
instance: seal.i,
|
|
464
|
-
|
|
778
|
+
instance: seal.i,
|
|
779
|
+
// follows the key type when not set
|
|
780
|
+
algorithm,
|
|
465
781
|
signingDomain: seal.signingDomain,
|
|
466
782
|
selector: seal.selector,
|
|
467
783
|
privateKey: seal.privateKey
|
|
468
784
|
},
|
|
469
785
|
|
|
470
|
-
getARChain // pass as a property so we do not have to use circular require()
|
|
786
|
+
getARChain, // pass as a property so we do not have to use circular require()
|
|
787
|
+
|
|
788
|
+
// a set without an explicit instance follows the highest existing one, the same
|
|
789
|
+
// value is used for the ARC-Seal below
|
|
790
|
+
getArcInstance: hdrs => getArcPosition(hdrs, (arc || dkimSigner.arc)?.chain).highest + 1
|
|
471
791
|
});
|
|
472
792
|
|
|
473
793
|
if (input) {
|
|
@@ -480,14 +800,65 @@ const createSeal = async (input, data) => {
|
|
|
480
800
|
|
|
481
801
|
bodyHash = dkimSigner.bodyHashes.get(hashKey)?.hash;
|
|
482
802
|
|
|
803
|
+
headers = dkimSigner.headers;
|
|
483
804
|
arc = arc || dkimSigner.arc;
|
|
484
|
-
seal.i = arc.instance;
|
|
485
805
|
} else {
|
|
486
806
|
// this gives us dkimSigner.arc.messageSignature
|
|
487
807
|
await dkimSigner.finalize();
|
|
488
808
|
}
|
|
489
809
|
|
|
490
|
-
|
|
810
|
+
let { highest, latestCv } = getArcPosition(headers, arc?.chain);
|
|
811
|
+
|
|
812
|
+
if (!explicitInstance) {
|
|
813
|
+
if (highest >= MAX_ARC_INSTANCE) {
|
|
814
|
+
// RFC 8617 section 4.2.1, instance values range from 1 to 50. A 51st set would make
|
|
815
|
+
// the whole chain invalid for every validator, so the message is not sealed
|
|
816
|
+
return refuse(`Can not add ARC set i=${highest + 1}, the chain already has ${highest} sets`, 'EINVALIDINSTANCE');
|
|
817
|
+
}
|
|
818
|
+
seal.i = highest + 1;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// RFC 8617 section 5.1 step 2: a chain whose newest seal says cv=fail is not sealed again
|
|
822
|
+
if (latestCv === 'fail') {
|
|
823
|
+
return refuse(`The newest ARC-Seal (i=${highest}) has cv=fail, the chain can not be sealed again`, 'EARCCHAINFAILED');
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// RFC 8617 section 5.1 step 3: the next instance follows the highest existing one
|
|
827
|
+
if (seal.i <= highest) {
|
|
828
|
+
// a second set with the same instance makes the whole chain invalid
|
|
829
|
+
return refuse(`ARC instance ${seal.i} already exists on the message (highest instance ${highest})`, 'EINVALIDINSTANCE');
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (seal.i !== highest + 1) {
|
|
833
|
+
// an explicit instance that leaves a gap, which no validator accepts
|
|
834
|
+
if (strict) {
|
|
835
|
+
return refuse(`ARC instance ${seal.i} does not follow the highest existing instance ${highest}`, 'EINVALIDINSTANCE');
|
|
836
|
+
}
|
|
837
|
+
warn('arc-instance-gap');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (!cv) {
|
|
841
|
+
if (seal.i !== 1) {
|
|
842
|
+
// there is no way to tell here whether the chain validated
|
|
843
|
+
return refuse(`A cv value is required for ARC instance ${seal.i}`, 'EINVALIDCV');
|
|
844
|
+
}
|
|
845
|
+
// RFC 8617 section 4.4, there was no chain
|
|
846
|
+
cv = 'none';
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if ((seal.i === 1) !== (cv === 'none')) {
|
|
850
|
+
// RFC 8617 section 5.2 step 3.C: cv=none for i=1, pass or fail for every later set
|
|
851
|
+
if (strict) {
|
|
852
|
+
return refuse(`cv=${cv} is not valid for ARC instance ${seal.i}`, 'EINVALIDCV');
|
|
853
|
+
}
|
|
854
|
+
warn('arc-cv-instance');
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (cv === 'pass' && seal.i > 1 && !arc?.chain?.length) {
|
|
858
|
+
// a cv=pass seal signs the whole chain (RFC 8617 section 5.1.1), and there is no valid
|
|
859
|
+
// chain on the message to sign
|
|
860
|
+
return refuse(`cv=pass needs a valid ARC chain to seal`, 'EINVALIDCV');
|
|
861
|
+
}
|
|
491
862
|
|
|
492
863
|
const messageSignature = dkimSigner.arc?.messageSignature;
|
|
493
864
|
if (!messageSignature) {
|
|
@@ -495,33 +866,43 @@ const createSeal = async (input, data) => {
|
|
|
495
866
|
// being the usual reason. There is no ARC set to seal then: sealing anyway hashes
|
|
496
867
|
// a header that does not exist and puts an empty line into the header block, which
|
|
497
868
|
// ends it early for everything downstream
|
|
498
|
-
return { headers: [], errors: dkimSigner.errors };
|
|
869
|
+
return { headers: [], errors: dkimSigner.errors, warnings: collectWarnings() };
|
|
499
870
|
}
|
|
500
871
|
|
|
501
872
|
const authResults = `ARC-Authentication-Results: i=${seal.i}; ${seal.authResults}`;
|
|
502
873
|
|
|
874
|
+
// the ARC-Seal uses the algorithm of the ARC-Message-Signature, which follows the key type
|
|
875
|
+
// when no algorithm was given
|
|
876
|
+
let sealAlgorithm = (parseDkimHeaders(messageSignature)?.parsed?.a?.value || algorithm || 'rsa-sha256').toString().toLowerCase();
|
|
877
|
+
|
|
503
878
|
// Step 2. Calculate ARC-Seal
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
879
|
+
let arcSeal;
|
|
880
|
+
try {
|
|
881
|
+
arcSeal = await signAS(
|
|
882
|
+
arc?.chain || [],
|
|
883
|
+
{
|
|
884
|
+
'arc-authentication-results': authResults,
|
|
885
|
+
'arc-message-signature': messageSignature
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
instance: seal.i,
|
|
889
|
+
algorithm: sealAlgorithm,
|
|
890
|
+
signingDomain: seal.signingDomain,
|
|
891
|
+
selector: seal.selector,
|
|
892
|
+
bodyHash,
|
|
893
|
+
cv,
|
|
894
|
+
signTime: seal.signTime,
|
|
895
|
+
privateKey: seal.privateKey
|
|
896
|
+
}
|
|
897
|
+
);
|
|
898
|
+
} catch (err) {
|
|
899
|
+
return { headers: [], errors: dkimSigner.errors.concat(sealError(seal, err)), warnings: collectWarnings() };
|
|
900
|
+
}
|
|
521
901
|
|
|
522
902
|
return {
|
|
523
903
|
headers: [arcSeal, messageSignature, authResults],
|
|
524
|
-
errors: dkimSigner.errors
|
|
904
|
+
errors: dkimSigner.errors,
|
|
905
|
+
warnings: collectWarnings()
|
|
525
906
|
};
|
|
526
907
|
};
|
|
527
908
|
|