mailauth 7.1.0 → 7.2.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +60 -0
- package/README.md +32 -31
- package/cli.md +1 -1
- package/docs/arc.md +62 -54
- package/docs/bimi.md +153 -54
- package/docs/dkim.md +37 -36
- package/docs/dmarc.md +4 -1
- package/docs/mta-sts.md +12 -9
- package/docs/spf.md +14 -13
- package/index.d.ts +224 -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/report.js +2 -2
- package/lib/commands/seal.js +8 -6
- package/lib/commands/spf.js +2 -2
- 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/syntax.js +11 -0
- package/lib/dkim/verify.js +1 -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 +17 -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 +159 -21
- package/package.json +18 -9
- package/stryker.config.json +16 -0
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,
|
|
@@ -8,28 +8,34 @@ const {
|
|
|
8
8
|
getAlignment,
|
|
9
9
|
getCurTime,
|
|
10
10
|
toALabel,
|
|
11
|
+
isHostNameDomain,
|
|
11
12
|
normalizeDomainName,
|
|
12
13
|
isSameOrSubdomain,
|
|
13
14
|
addWarning,
|
|
14
15
|
KEY_ERROR_COMMENTS,
|
|
16
|
+
isValidDnsName,
|
|
15
17
|
splitAlgorithm,
|
|
16
18
|
isSupportedAlgorithm,
|
|
17
19
|
createKeyCache
|
|
18
20
|
} = require('../../lib/tools');
|
|
19
|
-
const { DOMAIN_NAME, SELECTOR, TIMESTAMP, BODY_LENGTH } = require('./syntax');
|
|
21
|
+
const { DOMAIN_NAME, SELECTOR, LOCAL_PART, TIMESTAMP, BODY_LENGTH } = require('./syntax');
|
|
20
22
|
const { MessageParser } = require('./message-parser');
|
|
21
23
|
const { dkimBody } = require('./body');
|
|
22
24
|
const { generateCanonicalizedHeader } = require('./header');
|
|
23
|
-
const { getARChain } = require('../arc');
|
|
25
|
+
const { getARChain, getHighestArcInstance } = require('../arc');
|
|
24
26
|
const addressparser = require('nodemailer/lib/addressparser');
|
|
27
|
+
const { parseFromHeader, getAuthorDomain } = require('../dmarc/author-domain');
|
|
25
28
|
const crypto = require('node:crypto');
|
|
26
29
|
const libmime = require('libmime');
|
|
27
30
|
|
|
28
31
|
// RFC 6376 section 3.5 syntax, used by the strict mode
|
|
29
32
|
// sig-a-tag-alg = sig-a-tag-k "-" sig-a-tag-h, both ALPHA *(ALPHA / DIGIT)
|
|
30
33
|
const ALGORITHM = /^[A-Za-z][A-Za-z0-9]*-[A-Za-z][A-Za-z0-9]*$/;
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
|
|
35
|
+
// sig-c-tag-alg is "simple", "relaxed" or a hyphenated-word, sig-c-tag one or two of them
|
|
36
|
+
// separated by "/" (RFC 6376 section 3.5). An empty c= is not an omitted one (section 3.2)
|
|
37
|
+
const C_TAG_ALG = '[A-Za-z](?:[A-Za-z0-9-]*[A-Za-z0-9])?';
|
|
38
|
+
const CANONICALIZATION = new RegExp(`^${C_TAG_ALG}(?:/${C_TAG_ALG})?$`);
|
|
33
39
|
|
|
34
40
|
// Required tags of a DKIM-Signature (RFC 6376 section 3.5)
|
|
35
41
|
const REQUIRED_DKIM_TAGS = ['v', 'a', 'b', 'bh', 'd', 'h', 's'];
|
|
@@ -73,6 +79,10 @@ class DkimVerifier extends MessageParser {
|
|
|
73
79
|
|
|
74
80
|
this.headerFrom = [];
|
|
75
81
|
this.fromFields = 0;
|
|
82
|
+
// "valid", "lax" or "invalid", see parseFromHeader
|
|
83
|
+
this.fromSyntax = 'valid';
|
|
84
|
+
// the Author Domain result of getAuthorDomain, set when there is a From address
|
|
85
|
+
this.author = false;
|
|
76
86
|
this.envelopeFrom = false;
|
|
77
87
|
|
|
78
88
|
// ARC verification info, `warnings` lists what the lenient mode accepted in the chain
|
|
@@ -83,8 +93,9 @@ class DkimVerifier extends MessageParser {
|
|
|
83
93
|
this.keyCache = createKeyCache(this.resolver);
|
|
84
94
|
Object.defineProperty(this.arc, 'keyCache', { value: this.keyCache, enumerable: false, configurable: true, writable: true });
|
|
85
95
|
|
|
86
|
-
// should we also seal this message using ARC
|
|
87
|
-
|
|
96
|
+
// should we also seal this message using ARC. A copy, so the body hash stored on it
|
|
97
|
+
// below never ends up on the caller's object, which may be shared by concurrent calls
|
|
98
|
+
this.seal = this.options.seal ? Object.assign({}, this.options.seal) : false;
|
|
88
99
|
|
|
89
100
|
if (this.seal) {
|
|
90
101
|
// calculate body hash for the seal
|
|
@@ -106,6 +117,9 @@ class DkimVerifier extends MessageParser {
|
|
|
106
117
|
} catch (err) {
|
|
107
118
|
this.arc.error = err;
|
|
108
119
|
}
|
|
120
|
+
// the cv= of the newest ARC-Seal in the header fields, also when the chain could not be
|
|
121
|
+
// parsed. arc() uses it to decide whether a failed chain is sealed with cv=fail
|
|
122
|
+
this.arc.latestCv = getHighestArcInstance(headers).latestCv;
|
|
109
123
|
|
|
110
124
|
this.signatureHeaders = headers.parsed
|
|
111
125
|
.filter(h => h.key === 'dkim-signature')
|
|
@@ -125,19 +139,22 @@ class DkimVerifier extends MessageParser {
|
|
|
125
139
|
if (splitterPos >= 0) {
|
|
126
140
|
fromHeader = fromHeader.substr(splitterPos + 1);
|
|
127
141
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this.headerFrom.push(mailbox.address);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
142
|
+
// the full addr-specs, including the mailboxes of a group (RFC 6854). A general
|
|
143
|
+
// purpose address parser guesses at malformed input, which could hand DMARC a domain
|
|
144
|
+
// that is not the one a reader sees, so the RFC 5322 grammar is followed instead
|
|
145
|
+
let { addresses, syntax } = parseFromHeader(fromHeader);
|
|
146
|
+
this.headerFrom.push(...addresses);
|
|
147
|
+
if (syntax === 'invalid' || this.fromSyntax === 'valid') {
|
|
148
|
+
this.fromSyntax = syntax;
|
|
138
149
|
}
|
|
139
150
|
}
|
|
140
151
|
|
|
152
|
+
if (this.headerFrom.length) {
|
|
153
|
+
// the same Author Domain rules as the DMARC check: a From header field that does
|
|
154
|
+
// not yield a single Author Domain aligns with no signature
|
|
155
|
+
this.author = getAuthorDomain(this.headerFrom, this.fromFields, { fromSyntax: this.fromSyntax, strict: this.strict });
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
if (this.options.sender) {
|
|
142
159
|
let returnPath = addressparser(this.options.sender);
|
|
143
160
|
this.envelopeFrom = returnPath.length && returnPath[0].address ? returnPath[0].address : false;
|
|
@@ -258,9 +275,12 @@ class DkimVerifier extends MessageParser {
|
|
|
258
275
|
signatureHeader.algorithm = (parsed.a?.value || '').toString();
|
|
259
276
|
Object.assign(signatureHeader, splitAlgorithm(signatureHeader.algorithm));
|
|
260
277
|
|
|
261
|
-
|
|
262
|
-
signatureHeader.headerCanon =
|
|
263
|
-
signatureHeader.bodyCanon = getBodyCanon(
|
|
278
|
+
let canonicalization = (parsed.c?.value || '').toString();
|
|
279
|
+
signatureHeader.headerCanon = canonicalization.split('/').shift().toLowerCase().trim() || 'simple';
|
|
280
|
+
signatureHeader.bodyCanon = getBodyCanon(canonicalization);
|
|
281
|
+
// the canonicalization as read here, for the header canonicalizer. The c= text itself
|
|
282
|
+
// can be malformed, "/relaxed" has no header part
|
|
283
|
+
signatureHeader.canonicalization = `${signatureHeader.headerCanon}/${signatureHeader.bodyCanon}`;
|
|
264
284
|
|
|
265
285
|
signatureHeader.signingDomain = (parsed.d?.value || '').toString();
|
|
266
286
|
signatureHeader.selector = (parsed.s?.value || '').toString();
|
|
@@ -331,12 +351,31 @@ class DkimVerifier extends MessageParser {
|
|
|
331
351
|
return;
|
|
332
352
|
}
|
|
333
353
|
|
|
354
|
+
// the lenient mode reads a missing part as "simple" and ignores a third one
|
|
355
|
+
let canonError = parsed.c && !CANONICALIZATION.test((rawTags.get('c') || '').replace(/[ \t\r\n]+/g, ''));
|
|
356
|
+
if (canonError && violation('signature syntax error', 'tag-syntax')) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
334
360
|
// "This list MUST NOT be empty", the From check below covers the lenient mode
|
|
335
361
|
if (parsed.h && !normalizeFieldList(parsed.h.value).length && violation('signature syntax error')) {
|
|
336
362
|
return;
|
|
337
363
|
}
|
|
338
364
|
|
|
339
|
-
|
|
365
|
+
// toALabel converts each label on its own, so these also make up the key record name
|
|
366
|
+
let signingDomainALabel = toALabel(signatureHeader.signingDomain);
|
|
367
|
+
let selectorALabel = toALabel(signatureHeader.selector);
|
|
368
|
+
if (!isHostNameDomain(signingDomainALabel)) {
|
|
369
|
+
signatureHeader.invalid = 'signature syntax error';
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// DOMAIN_NAME limits the length of each label, the key record name has to fit in
|
|
374
|
+
// the 253 octets of a DNS name as well
|
|
375
|
+
let invalidNames =
|
|
376
|
+
!DOMAIN_NAME.test(signingDomainALabel) ||
|
|
377
|
+
!SELECTOR.test(selectorALabel) ||
|
|
378
|
+
!isValidDnsName(`${selectorALabel}._domainkey.${signingDomainALabel}`);
|
|
340
379
|
if (invalidNames && violation('signature syntax error', 'tag-syntax')) {
|
|
341
380
|
return;
|
|
342
381
|
}
|
|
@@ -467,6 +506,7 @@ class DkimVerifier extends MessageParser {
|
|
|
467
506
|
* @returns {Object} Signature result
|
|
468
507
|
*/
|
|
469
508
|
async verifySignature(signatureHeader, fallback) {
|
|
509
|
+
// the canonicalization as prepareSignature() read it
|
|
470
510
|
const canonicalization = fallback ? signatureHeader.fallbackCanonicalization : signatureHeader.canonicalization;
|
|
471
511
|
const bodyHashKey = fallback ? signatureHeader.fallbackBodyHashKey : signatureHeader.bodyHashKey;
|
|
472
512
|
|
|
@@ -493,8 +533,8 @@ class DkimVerifier extends MessageParser {
|
|
|
493
533
|
header: this.getStatusHeader(signatureHeader)
|
|
494
534
|
};
|
|
495
535
|
|
|
496
|
-
if (signatureHeader.type === 'DKIM' && this.
|
|
497
|
-
status.aligned =
|
|
536
|
+
if (signatureHeader.type === 'DKIM' && this.author) {
|
|
537
|
+
status.aligned = (this.author.authorDomain && getAlignment(this.author.authorDomain, [signatureHeader.signingDomain])?.domain) || false;
|
|
498
538
|
}
|
|
499
539
|
|
|
500
540
|
const bodyHasher = this.bodyHashes.get(bodyHashKey);
|
|
@@ -503,11 +543,22 @@ class DkimVerifier extends MessageParser {
|
|
|
503
543
|
|
|
504
544
|
const warnings = [].concat(signatureHeader.warnings || []);
|
|
505
545
|
|
|
506
|
-
|
|
546
|
+
// RFC 6376 section 3.5 l=: "This value MUST NOT be larger than the actual number of
|
|
547
|
+
// octets in the canonicalized message body"
|
|
548
|
+
let invalidBodyLength =
|
|
549
|
+
signatureHeader.type === 'DKIM' &&
|
|
550
|
+
typeof signatureHeader.maxBodyLength === 'number' &&
|
|
551
|
+
typeof bodyHasher?.canonicalizedLength === 'number' &&
|
|
552
|
+
signatureHeader.maxBodyLength > bodyHasher.canonicalizedLength;
|
|
553
|
+
if (invalidBodyLength && !this.strict) {
|
|
554
|
+
addWarning(warnings, 'tag-syntax');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (signatureHeader.invalid || (invalidBodyLength && this.strict)) {
|
|
507
558
|
// RFC 6376 section 6.1.1 PERMFAIL, reported as neutral, which RFC 8601 section
|
|
508
559
|
// 2.7.1 uses for a signature with syntax errors. The key is not looked up
|
|
509
560
|
status.result = 'neutral';
|
|
510
|
-
status.comment = signatureHeader.invalid;
|
|
561
|
+
status.comment = signatureHeader.invalid || 'signature syntax error';
|
|
511
562
|
} else if (signatureHeader.parsed?.bh?.value !== bodyHash) {
|
|
512
563
|
// RFC 6376 section 6.1.3 step 3 is a PERMFAIL, which RFC 8601 reports as "fail".
|
|
513
564
|
// The lenient mode keeps reporting neutral, as it always has
|
|
@@ -542,9 +593,10 @@ class DkimVerifier extends MessageParser {
|
|
|
542
593
|
}
|
|
543
594
|
|
|
544
595
|
let keyError = false;
|
|
545
|
-
if (
|
|
546
|
-
// RFC 6376 section 6.1.2 step 8
|
|
547
|
-
//
|
|
596
|
+
if (res?.keyType && res.keyType !== signatureHeader.signAlgo) {
|
|
597
|
+
// RFC 6376 section 6.1.2 step 8, in every mode. The crypto library does not
|
|
598
|
+
// catch every mismatch: an RSA key verifies a PKCS#1 v1.5 signature over the
|
|
599
|
+
// digest that an ed25519-sha256 signature is made over
|
|
548
600
|
keyError = 'inappropriate key algorithm';
|
|
549
601
|
} else if (
|
|
550
602
|
res?.flags?.includes('s') &&
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { Buffer } = require('node:buffer');
|
|
4
|
-
const { stripSignatureValue } = require('../../../lib/tools');
|
|
4
|
+
const { stripSignatureValue, toLineBytes } = require('../../../lib/tools');
|
|
5
5
|
const { buildSignatureOpts } = require('./signature-opts');
|
|
6
6
|
|
|
7
|
-
const formatSimpleLine = (line, suffix) => Buffer.
|
|
7
|
+
const formatSimpleLine = (line, suffix) => Buffer.concat([toLineBytes(line), Buffer.from(suffix ? suffix : '', 'binary')]);
|
|
8
8
|
|
|
9
9
|
// generate headers for signing
|
|
10
10
|
const simpleHeaders = (type, signingHeaderLines, options) => {
|
package/lib/dkim/syntax.js
CHANGED
|
@@ -13,6 +13,15 @@ const SELECTOR = new RegExp(`^${DOMAIN_LABEL}(?:\\.${DOMAIN_LABEL})*$`);
|
|
|
13
13
|
// sig-t-tag and sig-x-tag are 1*12DIGIT, sig-l-tag is 1*76DIGIT
|
|
14
14
|
const TIMESTAMP = /^[0-9]{1,12}$/;
|
|
15
15
|
const BODY_LENGTH = /^[0-9]{1,76}$/;
|
|
16
|
+
// the Local-part of an i= value (RFC 6376 section 3.5): RFC 5322 dot-atom-text with the UTF-8
|
|
17
|
+
// of RFC 6531, or a quoted-string
|
|
18
|
+
const LOCAL_PART = /^(?:(?:[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~]|[^\x00-\x7f])+(?:\.(?:[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~]|[^\x00-\x7f])+)*|"(?:[^"\\\r\n]|\\.)*")$/;
|
|
19
|
+
|
|
20
|
+
// The characters of a host name, after U-labels are converted to A-labels. A d= with anything
|
|
21
|
+
// else (such as "/", "?", "#", "@" or ":") can not name the zone of its key record, so a
|
|
22
|
+
// DKIM-Signature, ARC-Seal or ARC-Message-Signature with it is a syntax error in every mode
|
|
23
|
+
// (RFC 6376 section 3.5)
|
|
24
|
+
const HOST_NAME_CHARS = /^[A-Za-z0-9_.-]+$/;
|
|
16
25
|
|
|
17
26
|
// RFC 8617 section 4.2.1: instance values range from 1 to 50
|
|
18
27
|
const MAX_ARC_INSTANCE = 50;
|
|
@@ -24,6 +33,8 @@ module.exports = {
|
|
|
24
33
|
DOMAIN_LABEL,
|
|
25
34
|
DOMAIN_NAME,
|
|
26
35
|
SELECTOR,
|
|
36
|
+
LOCAL_PART,
|
|
37
|
+
HOST_NAME_CHARS,
|
|
27
38
|
TIMESTAMP,
|
|
28
39
|
BODY_LENGTH,
|
|
29
40
|
MAX_ARC_INSTANCE,
|
package/lib/dkim/verify.js
CHANGED
|
@@ -11,6 +11,7 @@ const dkimVerify = async (input, options) => {
|
|
|
11
11
|
//headers: dkimVerifier.headers,
|
|
12
12
|
headerFrom: dkimVerifier.headerFrom,
|
|
13
13
|
fromFields: dkimVerifier.fromFields,
|
|
14
|
+
fromSyntax: dkimVerifier.fromSyntax,
|
|
14
15
|
envelopeFrom: dkimVerifier.envelopeFrom,
|
|
15
16
|
results: dkimVerifier.results
|
|
16
17
|
};
|