mailauth 7.2.0 → 7.3.1

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.
@@ -0,0 +1,572 @@
1
+ 'use strict';
2
+
3
+ // DKIM2 verifier (draft-ietf-dkim-dkim2-spec-06 sections 10 and 11)
4
+
5
+ const { Buffer } = require('node:buffer');
6
+ const crypto = require('node:crypto');
7
+ const { formatAuthHeaderRow, getCurTime, isSameOrSubdomain } = require('../tools');
8
+ const { MI_KEY, SIG_KEY, HASH_ALGORITHMS, parseMessageInstance, parseSignature, parsePath, continuesCustody, buildSignatureInput } = require('./fields');
9
+ const { Dkim2Parser } = require('./dkim2-parser');
10
+ const { createKeyLookup, KeyError, ALGORITHM_KEY_TYPES, signatureData } = require('./key');
11
+
12
+ // draft-ietf-dkim-dkim2-bcp-01 section 7.5 recommends a local ceiling on the number of
13
+ // Message-Instance and DKIM2-Signature header fields, with a PERMERROR above it
14
+ const DEFAULT_MAX_INSTANCES = 20;
15
+ // section 11.3: a failure when a signature is more than 14 days old
16
+ const DEFAULT_MAX_SIGNATURE_AGE = 14 * 24 * 3600;
17
+ // section 8.4: signatures from the future MAY be ignored. Five minutes allows for clock skew
18
+ const DEFAULT_MAX_FUTURE_TIME = 5 * 60;
19
+
20
+ // the order in which a result wins over the others, FAIL first so that a cryptographic failure is
21
+ // never reported as a TEMPERROR (section 10.4)
22
+ const RESULT_ORDER = ['fail', 'permerror', 'temperror', 'pass'];
23
+
24
+ const pickResult = results => RESULT_ORDER.find(result => results.includes(result)) || 'pass';
25
+
26
+ // a number from a header field for an error string, "?" when it could not be read
27
+ const ordinal = value => (Number.isSafeInteger(value) ? value : '?');
28
+
29
+ const verifySignature = (algorithm, input, publicKey, signature) => {
30
+ try {
31
+ return crypto.verify(...signatureData(algorithm, input), publicKey, signature);
32
+ } catch (err) {
33
+ return false;
34
+ }
35
+ };
36
+
37
+ // the first index from 1 that is missing from a map keyed by numbers
38
+ const firstGap = map => {
39
+ let pos = 1;
40
+ while (map.has(pos)) {
41
+ pos++;
42
+ }
43
+ return pos;
44
+ };
45
+
46
+ class Dkim2Verifier extends Dkim2Parser {
47
+ /**
48
+ * @param {Object} [options]
49
+ * @param {Function} [options.resolver] DNS resolver
50
+ * @param {String} [options.mailFrom] SMTP MAIL FROM of the delivery, "" for the null path. When
51
+ * set, it is checked against the mf= of the highest DKIM2-Signature (section 11.4)
52
+ * @param {String|String[]} [options.rcptTo] SMTP RCPT TO addresses of the delivery, each of
53
+ * which has to be listed in rt= of the highest DKIM2-Signature
54
+ * @param {Number|Boolean} [options.maxSignatureAge=1209600] Seconds after t= when a signature
55
+ * expires (section 11.3), false to not check
56
+ * @param {Number|Boolean} [options.maxFutureTime=300] Seconds a t= may be ahead of the current
57
+ * time (section 8.4), false to not check
58
+ * @param {Date|Number|String} [options.curTime] Time to verify against, defaults to now
59
+ * @param {Number} [options.maxInstances=20] Most Message-Instance, and most DKIM2-Signature,
60
+ * header fields processed for one message
61
+ * @param {Function} [options.checkReplay] async ({ key, exploded }) => true when a message with
62
+ * the same m=1 hashes was seen before (section 11.9)
63
+ */
64
+ constructor(options) {
65
+ super();
66
+ this.options = options || {};
67
+ this.result = null;
68
+
69
+ this.maxInstances = this.options.maxInstances || DEFAULT_MAX_INSTANCES;
70
+ this.instanceRows = [];
71
+ this.signatureRows = [];
72
+ }
73
+
74
+ // too many DKIM2 header fields to process, which verify() reports
75
+ get overLimit() {
76
+ return this.instanceRows.length > this.maxInstances || this.signatureRows.length > this.maxInstances;
77
+ }
78
+
79
+ async messageHeaders(headers) {
80
+ let rows = headers.parsed;
81
+ this.instanceRows = rows.filter(row => row.key === MI_KEY).map(row => ({ line: row.line }));
82
+ this.signatureRows = rows.filter(row => row.key === SIG_KEY).map(row => ({ line: row.line }));
83
+
84
+ if ((!this.instanceRows.length && !this.signatureRows.length) || this.overLimit) {
85
+ // nothing to verify, or nothing that will be verified, so the body is not hashed at all
86
+ return;
87
+ }
88
+
89
+ for (let row of this.signatureRows) {
90
+ row.parsed = parseSignature(row.line);
91
+ }
92
+
93
+ for (let row of this.instanceRows) {
94
+ row.parsed = parseMessageInstance(row.line);
95
+ // hash the body with every algorithm a Message-Instance uses
96
+ for (let { algorithm } of row.parsed.hashes) {
97
+ if (HASH_ALGORITHMS.has(algorithm)) {
98
+ this.hashBodyWith(algorithm);
99
+ }
100
+ }
101
+ if (Array.isArray(row.parsed.recipe?.b)) {
102
+ // a body Recipe copies lines from the body, so the body has to be kept
103
+ this.keepBody();
104
+ }
105
+ }
106
+ }
107
+
108
+ async finalChunk() {
109
+ this.result = await this.verify();
110
+ }
111
+
112
+ async verify() {
113
+ let errors = [];
114
+ let instances = new Map();
115
+ let signatures = new Map();
116
+ let instanceResults = [];
117
+ let signatureResults = new Map();
118
+
119
+ // the DKIM2-Signature a failure of a Message-Instance is reported against, the one with
120
+ // the lowest i= that covers it (draft-gondwana-dkim2-authres-00 section 3.2.2)
121
+ const signatureFor = m => {
122
+ for (let i = 1; signatures.has(i); i++) {
123
+ if (signatures.get(i).m === m) {
124
+ return i;
125
+ }
126
+ }
127
+ return undefined;
128
+ };
129
+
130
+ const addError = (result, message, ref) => {
131
+ let entry = { result, message };
132
+ if (Number.isSafeInteger(ref?.i)) {
133
+ entry.i = ref.i;
134
+ }
135
+ if (Number.isSafeInteger(ref?.m)) {
136
+ entry.m = ref.m;
137
+ let i = signatureFor(ref.m);
138
+ if (i) {
139
+ entry.i = i;
140
+ }
141
+ }
142
+ errors.push(entry);
143
+ };
144
+
145
+ const finish = replay => this.formatResult({ errors, signatures, signatureResults, instanceResults, replay });
146
+
147
+ if (!this.instanceRows.length && !this.signatureRows.length) {
148
+ return this.formatResult({ none: true });
149
+ }
150
+
151
+ if (this.overLimit) {
152
+ addError('permerror', `Message has more than ${this.maxInstances} Message-Instance or DKIM2-Signature header fields`);
153
+ return finish();
154
+ }
155
+
156
+ // section 11.2: every field is valid, numbered without gaps, and every instance is signed
157
+ for (let { line, parsed } of this.instanceRows) {
158
+ if (parsed.error) {
159
+ addError('permerror', `Message-Instance m=${ordinal(parsed.m)} ${parsed.error}`);
160
+ } else if (instances.has(parsed.m)) {
161
+ addError('permerror', `Message-Instance m=${parsed.m} appears more than once`);
162
+ } else {
163
+ instances.set(parsed.m, Object.assign({ line }, parsed));
164
+ }
165
+ }
166
+
167
+ for (let { line, parsed } of this.signatureRows) {
168
+ if (parsed.error) {
169
+ addError('permerror', `DKIM2-Signature i=${ordinal(parsed.i)} ${parsed.error}`, { i: parsed.i });
170
+ } else if (signatures.has(parsed.i)) {
171
+ addError('permerror', `DKIM2-Signature i=${parsed.i} appears more than once`, { i: parsed.i });
172
+ } else {
173
+ signatures.set(parsed.i, Object.assign({ line }, parsed));
174
+ }
175
+ }
176
+
177
+ if (errors.length) {
178
+ return finish();
179
+ }
180
+
181
+ let maxM = Math.max(0, ...instances.keys());
182
+ let maxI = Math.max(0, ...signatures.keys());
183
+
184
+ let missingM = firstGap(instances);
185
+ if (missingM <= maxM || !maxM) {
186
+ addError('permerror', `Message-Instance m=${missingM} missing`);
187
+ }
188
+
189
+ let missingI = firstGap(signatures);
190
+ if (missingI <= maxI || !maxI) {
191
+ addError('permerror', `DKIM2-Signature i=${missingI} missing`);
192
+ }
193
+
194
+ if (errors.length) {
195
+ return finish();
196
+ }
197
+
198
+ for (let [i, signature] of signatures) {
199
+ if (signature.m > maxM) {
200
+ addError('permerror', `Message-Instance m=${signature.m} missing`, { i });
201
+ return finish();
202
+ }
203
+ }
204
+
205
+ let maxSignedM = Math.max(...Array.from(signatures.values()).map(signature => signature.m));
206
+ if (maxM > maxSignedM) {
207
+ addError('permerror', `Message-Instance m=${maxM} is not signed`);
208
+ return finish();
209
+ }
210
+
211
+ // recreate every instance of the message by applying the Recipes from the top down
212
+ let current = this.currentState();
213
+
214
+ let states = new Map([[maxM, current]]);
215
+ for (let m = maxM; m > 1; m--) {
216
+ let state = states.get(m);
217
+ let recipe = instances.get(m).recipe;
218
+ if (recipe) {
219
+ try {
220
+ state = state.previous(recipe);
221
+ } catch (err) {
222
+ addError('permerror', `Message-Instance m=${m} contains invalid JSON: ${err.message}`, { m });
223
+ return finish();
224
+ }
225
+ }
226
+ states.set(m - 1, state);
227
+ }
228
+
229
+ // section 11.7: the hashes of every instance
230
+ for (let m = 1; m <= maxM; m++) {
231
+ let instance = instances.get(m);
232
+ let state = states.get(m);
233
+ let hashResults = [];
234
+
235
+ for (let { algorithm, headerHash, bodyHash } of instance.hashes) {
236
+ if (!HASH_ALGORITHMS.has(algorithm)) {
237
+ // section 3.4: hashes with an unknown algorithm are ignored
238
+ continue;
239
+ }
240
+
241
+ let headerResult = state.headerHash(algorithm) === headerHash ? 'pass' : 'fail';
242
+ if (headerResult === 'fail') {
243
+ addError('fail', `Message Instance m=${m} header hash ${algorithm} mismatch`, { m });
244
+ }
245
+
246
+ // a body before a null body Recipe can not be checked
247
+ let computedBodyHash = state.bodyHash(algorithm);
248
+ let bodyResult = computedBodyHash === null ? 'unknown' : computedBodyHash === bodyHash ? 'pass' : 'fail';
249
+ if (bodyResult === 'fail') {
250
+ addError('fail', `Message Instance m=${m} body hash ${algorithm} mismatch`, { m });
251
+ }
252
+
253
+ hashResults.push({ algorithm, header: headerResult, body: bodyResult });
254
+ }
255
+
256
+ if (!hashResults.length) {
257
+ addError('permerror', `Message-Instance m=${m} has no supported hash algorithm`, { m });
258
+ }
259
+
260
+ let entry = { m, hashes: hashResults };
261
+ if (instance.recipe) {
262
+ entry.recipe = {
263
+ headers: Array.from(instance.recipe.h.keys()),
264
+ body: instance.recipe.b === undefined ? 'unchanged' : instance.recipe.b === null ? 'unrecoverable' : 'recipe'
265
+ };
266
+ }
267
+ instanceResults.push(entry);
268
+ }
269
+
270
+ // section 11.3
271
+ let maxAge = this.options.maxSignatureAge === undefined ? DEFAULT_MAX_SIGNATURE_AGE : this.options.maxSignatureAge;
272
+ let now = Math.floor(getCurTime(this.options.curTime).getTime() / 1000);
273
+ let maxFuture = this.options.maxFutureTime === undefined ? DEFAULT_MAX_FUTURE_TIME : this.options.maxFutureTime;
274
+ for (let [i, signature] of signatures) {
275
+ if (maxAge && now - signature.t > maxAge) {
276
+ addError('permerror', `DKIM2-Signature i=${i} signature expired`, { i });
277
+ }
278
+ // section 8.4: signatures from the future MAY be ignored, and draft-ietf-dkim-dkim2-bcp-01
279
+ // section 3.6 lets verifiers treat them with suspicion
280
+ if (typeof maxFuture === 'number' && signature.t - now > maxFuture) {
281
+ addError('permerror', `DKIM2-Signature i=${i} signature timestamp is in the future`, { i });
282
+ }
283
+ }
284
+
285
+ this.checkCustody(signatures, maxI, addError);
286
+
287
+ // sections 11.5 and 11.6, from the most recent signature down (section 10.1)
288
+ // the key lookups of every hop run at the same time, the results are recorded in order
289
+ let keyLookup = createKeyLookup(this.options.resolver);
290
+ let hopOrder = Array.from({ length: maxI }, (v, index) => maxI - index);
291
+ let hopResults = await Promise.all(hopOrder.map(i => this.verifyHop(signatures.get(i), instances, signatures, keyLookup)));
292
+ hopOrder.forEach((i, index) => {
293
+ let result = hopResults[index];
294
+ signatureResults.set(i, result);
295
+ if (result.status.result !== 'pass') {
296
+ addError(result.status.result, result.status.comment, { i });
297
+ }
298
+ });
299
+
300
+ // section 11.8
301
+ let compareAlgorithm = this.bodyHashers.keys().next().value;
302
+ let lastExploded = 0;
303
+ for (let [i, signature] of signatures) {
304
+ if (signature.flags.includes('exploded')) {
305
+ lastExploded = Math.max(lastExploded, i);
306
+ }
307
+ }
308
+
309
+ let donotmodify = Array.from(signatures.values()).filter(signature => signature.flags.includes('donotmodify'));
310
+ if (compareAlgorithm && donotmodify.some(signature => !states.get(signature.m).isPreservedIn(current, compareAlgorithm))) {
311
+ addError('fail', 'Message has been modified despite a donotmodify request');
312
+ }
313
+
314
+ if (Array.from(signatures.values()).some(signature => signature.flags.includes('donotexplode') && signature.i < lastExploded)) {
315
+ addError('fail', 'Message has been exploded despite a donotexplode request');
316
+ }
317
+
318
+ // section 11.9: the m=1 hashes identify the message, the exploded flag allows copies
319
+ let firstHashes = instances.get(1).hashes;
320
+ let replayHash = firstHashes.find(hash => HASH_ALGORITHMS.has(hash.algorithm)) || firstHashes[0];
321
+ let replay = {
322
+ key: `${replayHash.algorithm}:${replayHash.headerHash}:${replayHash.bodyHash}`,
323
+ exploded: lastExploded > 0
324
+ };
325
+
326
+ if (typeof this.options.checkReplay === 'function') {
327
+ let duplicate = await this.options.checkReplay(Object.assign({}, replay));
328
+ if (duplicate && !replay.exploded) {
329
+ addError('fail', 'Duplicate message with no exploded flag');
330
+ }
331
+ }
332
+
333
+ return finish(replay);
334
+ }
335
+
336
+ // Section 11.4 and the links between the signatures described in section 9.4
337
+ checkCustody(signatures, maxI, addError) {
338
+ for (let i = 1; i <= maxI; i++) {
339
+ let signature = signatures.get(i);
340
+
341
+ // section 8.8: the d= domain is the mf= domain or a parent of it, unless mf= is null
342
+ if (signature.mailFrom && signature.mailFrom !== '<>' && !isSameOrSubdomain(parsePath(signature.mailFrom).domain, signature.signingDomain)) {
343
+ addError('permerror', `DKIM2-Signature i=${i} MAIL FROM and d= do not match`, { i });
344
+ }
345
+
346
+ // section 8.7: nd= names the d= of the next signature exactly
347
+ let next = signatures.get(i + 1);
348
+ if (signature.nextDomain && next && next.signingDomain !== signature.nextDomain) {
349
+ addError('permerror', `DKIM2-Signature i=${i} MAIL nd= does not match`, { i });
350
+ }
351
+
352
+ // section 9.4: the MAIL FROM of every hop matches a RCPT TO of the hop before it
353
+ if (!continuesCustody(signatures.get(i - 1), signature.mailFrom, signature.signingDomain)) {
354
+ addError(
355
+ 'permerror',
356
+ `DKIM2-Signature i=${i} MAIL FROM ${signature.mailFrom && signature.mailFrom !== '<>' ? signature.mailFrom : signature.signingDomain} did not match`,
357
+ { i }
358
+ );
359
+ }
360
+ }
361
+
362
+ let { mailFrom, rcptList, hasMailFrom } = this.envelope();
363
+ if (!hasMailFrom && !rcptList.length) {
364
+ // the delivery is not known, so it can not be compared
365
+ return;
366
+ }
367
+
368
+ let top = signatures.get(maxI);
369
+ if (top.nextDomain) {
370
+ // section 9.3: without mf= and rt= the delivery can only be accepted on out-of-band information
371
+ addError('permerror', `DKIM2-Signature i=${maxI} unexpected nd= tag`, { i: maxI });
372
+ return;
373
+ }
374
+
375
+ // section 11.4: an exact match, domains in lower case, local parts as they are
376
+ if (hasMailFrom && parsePath(mailFrom).address !== parsePath(top.mailFrom).address) {
377
+ addError('permerror', `DKIM2-Signature i=${maxI} MAIL FROM ${mailFrom} did not match`, { i: maxI });
378
+ }
379
+
380
+ let listed = new Set(top.rcptTo.map(path => parsePath(path).address));
381
+ for (let rcpt of rcptList) {
382
+ if (!listed.has(parsePath(rcpt).address)) {
383
+ addError('permerror', `DKIM2-Signature i=${maxI} RCPT TO ${rcpt} did not match`, { i: maxI });
384
+ }
385
+ }
386
+ }
387
+
388
+ // the SMTP envelope of the delivery, as far as the caller gave it
389
+ envelope() {
390
+ let { mailFrom, rcptTo } = this.options;
391
+ return {
392
+ mailFrom,
393
+ hasMailFrom: typeof mailFrom === 'string',
394
+ rcptList: [].concat(rcptTo || []).filter(value => typeof value === 'string')
395
+ };
396
+ }
397
+
398
+ // Without the envelope the highest DKIM2-Signature can not be compared with the delivery
399
+ // (section 11.4), and an unchanged copy of the message sent to anyone else passes as well
400
+ envelopeWarnings() {
401
+ let { hasMailFrom, rcptList } = this.envelope();
402
+ let warnings = [];
403
+ if (!hasMailFrom) {
404
+ warnings.push('mail-from-not-checked');
405
+ }
406
+ if (!rcptList.length) {
407
+ warnings.push('rcpt-to-not-checked');
408
+ }
409
+ return warnings;
410
+ }
411
+
412
+ // Sections 9.6, 11.5 and 11.6 for one DKIM2-Signature
413
+ async verifyHop(signature, instances, signatures, keyLookup) {
414
+ let i = signature.i;
415
+
416
+ // the instances it covers, the signatures before it, and itself with the values emptied
417
+ let input = buildSignatureInput(
418
+ Array.from({ length: signature.m }, (v, index) => instances.get(index + 1).line),
419
+ Array.from({ length: i - 1 }, (v, index) => signatures.get(index + 1).line),
420
+ signature.line
421
+ );
422
+
423
+ // start every key lookup before waiting for any of them
424
+ let lookups = signature.signatures.map(({ selector, algorithm }) =>
425
+ ALGORITHM_KEY_TYPES.has(algorithm) ? keyLookup(signature.signingDomain, selector, algorithm) : null
426
+ );
427
+ lookups.forEach(lookup => lookup?.catch(() => false));
428
+
429
+ let values = [];
430
+ for (let [index, { selector, algorithm, signature: value }] of signature.signatures.entries()) {
431
+ let entry = { selector, algorithm };
432
+ values.push(entry);
433
+
434
+ if (!lookups[index]) {
435
+ // section 3.4: unknown algorithms are ignored
436
+ entry.result = 'none';
437
+ continue;
438
+ }
439
+
440
+ let key;
441
+ try {
442
+ key = await lookups[index];
443
+ } catch (err) {
444
+ if (!(err instanceof KeyError)) {
445
+ throw err;
446
+ }
447
+ entry.result = err.result;
448
+ entry.comment = `public key ${selector} ${err.message}`;
449
+ if (err.rr) {
450
+ entry.rr = err.rr;
451
+ }
452
+ continue;
453
+ }
454
+
455
+ entry.result = verifySignature(algorithm, input, key.publicKey, Buffer.from(value, 'base64')) ? 'pass' : 'fail';
456
+ entry.rr = key.rr;
457
+ if (key.modulusLength) {
458
+ entry.modulusLength = key.modulusLength;
459
+ }
460
+ if (key.testing) {
461
+ // draft-ietf-dkim-dkim2-dns-00 section 3.4.1 t=y: the signer is testing DKIM2
462
+ entry.testing = true;
463
+ }
464
+ }
465
+
466
+ let selectorsWith = result =>
467
+ values
468
+ .filter(entry => entry.result === result)
469
+ .map(entry => entry.selector)
470
+ .join(', ');
471
+
472
+ let status;
473
+ let passed = selectorsWith('pass');
474
+ let failed = selectorsWith('fail');
475
+ let keyErrors = values.filter(entry => ['permerror', 'temperror'].includes(entry.result));
476
+
477
+ if (failed) {
478
+ // section 11.6: every signature that can be checked is checked, and any failure fails
479
+ status = {
480
+ result: 'fail',
481
+ comment: passed
482
+ ? `DKIM2-Signature i=${i} ${passed} signature passed, ${failed} signature failed`
483
+ : `DKIM2-Signature i=${i} ${failed} incorrect signature`
484
+ };
485
+ } else if (passed) {
486
+ status = { result: 'pass' };
487
+ } else if (keyErrors.length) {
488
+ // a TEMPERROR may pass on a later attempt, so it is reported before a PERMERROR
489
+ let keyError = keyErrors.find(entry => entry.result === 'temperror') || keyErrors[0];
490
+ status = { result: keyError.result, comment: `DKIM2-Signature i=${i} ${keyError.comment}` };
491
+ } else {
492
+ // Section 3.4 has the unknown algorithms ignored, so nothing could be verified and nothing
493
+ // was found incorrect: a PERMERROR by the definitions of section 11.1. The FAIL of section
494
+ // 11.6 step 3 is about the signatures that can be checked
495
+ status = { result: 'permerror', comment: `DKIM2-Signature i=${i} has no supported signature algorithm` };
496
+ }
497
+
498
+ return { status, values };
499
+ }
500
+
501
+ formatResult({ none, errors, signatures, signatureResults, instanceResults, replay }) {
502
+ if (none) {
503
+ let status = { result: 'none' };
504
+ return { status, info: formatAuthHeaderRow('dkim2', status), errors: [], instances: [], signatures: [] };
505
+ }
506
+
507
+ let result = pickResult(errors.map(error => error.result));
508
+ let reported = errors.find(error => error.result === result);
509
+
510
+ let signatureList = [];
511
+ let hops = [];
512
+ for (let i = 1; signatures.has(i); i++) {
513
+ let signature = signatures.get(i);
514
+ let hop = signatureResults.get(i);
515
+
516
+ // the outcome of a hop includes the errors that are reported against it
517
+ let hopResult = hop ? pickResult([hop.status.result, ...errors.filter(error => error.i === i).map(error => error.result)]) : 'skipped';
518
+ hops.push(`i=${i} ${signature.signingDomain} ${hopResult}`);
519
+
520
+ let entry = {
521
+ i,
522
+ m: signature.m,
523
+ signingDomain: signature.signingDomain,
524
+ timestamp: signature.t,
525
+ signTime: new Date(signature.t * 1000).toISOString(),
526
+ flags: signature.flags,
527
+ status: hop ? { result: hopResult, comment: hop.status.comment } : { result: 'skipped' },
528
+ values: hop ? hop.values : signature.signatures.map(({ selector, algorithm }) => ({ selector, algorithm }))
529
+ };
530
+ for (let key of ['mailFrom', 'rcptTo', 'nextDomain', 'nonce']) {
531
+ if (signature[key] !== undefined) {
532
+ entry[key] = signature[key];
533
+ }
534
+ }
535
+ signatureList.push(entry);
536
+ }
537
+
538
+ let status = { result };
539
+ if (reported) {
540
+ status.comment = reported.message;
541
+ }
542
+
543
+ let warnings = this.envelopeWarnings();
544
+ if (warnings.length) {
545
+ // like the DKIM1 warnings, only present when there is something to report and never
546
+ // written into the Authentication-Results text
547
+ status.warnings = warnings;
548
+ }
549
+
550
+ // draft-gondwana-dkim2-authres-00 section 3: one resinfo for the message, the hops and the
551
+ // diagnostic in a comment, header.d of the originator, header.i of the failing signature
552
+ let header = {};
553
+ if (signatures.has(1)) {
554
+ header.d = signatures.get(1).signingDomain;
555
+ }
556
+ if (result !== 'pass' && typeof reported?.i === 'number') {
557
+ header.i = reported.i;
558
+ }
559
+ status.header = header;
560
+
561
+ let comment = [hops.join(', '), reported?.message].filter(value => value).join('; ');
562
+ let info = formatAuthHeaderRow('dkim2', { result, comment, header });
563
+
564
+ let output = { status, info, errors, instances: instanceResults, signatures: signatureList };
565
+ if (replay) {
566
+ output.replay = replay;
567
+ }
568
+ return output;
569
+ }
570
+ }
571
+
572
+ module.exports = { Dkim2Verifier };