mailauth 4.6.3 → 4.6.5
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/CHANGELOG.md +14 -0
- package/lib/arc/index.js +1 -1
- package/lib/dkim/body/relaxed.js +16 -0
- package/lib/dkim/body/simple.js +16 -0
- package/lib/dkim/dkim-signer.js +24 -4
- package/lib/dkim/dkim-verifier.js +27 -2
- package/lib/dkim/mime-structure-start-finder.js +85 -0
- package/lib/tools.js +24 -1
- package/man/mailauth.1 +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [4.6.5](https://github.com/postalsys/mailauth/compare/v4.6.4...v4.6.5) (2024-02-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **dkim:** Added new output property mimeStructureStart ([8f25353](https://github.com/postalsys/mailauth/commit/8f25353fa6a67ba3e1f0c5091325007b2434a29d))
|
|
9
|
+
|
|
10
|
+
## [4.6.4](https://github.com/postalsys/mailauth/compare/v4.6.3...v4.6.4) (2024-02-05)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **ed25519:** Fixed ed25519 signing and verification ([40f1245](https://github.com/postalsys/mailauth/commit/40f12457d8f49f0ea21015fe4203b4de746ab7b8))
|
|
16
|
+
|
|
3
17
|
## [4.6.3](https://github.com/postalsys/mailauth/compare/v4.6.2...v4.6.3) (2024-01-26)
|
|
4
18
|
|
|
5
19
|
|
package/lib/arc/index.js
CHANGED
|
@@ -146,7 +146,7 @@ const signAS = async (chain, entry, signatureData) => {
|
|
|
146
146
|
.sign(
|
|
147
147
|
// use `null` as algorithm to detect it from the key file
|
|
148
148
|
signAlgo === 'rsa' ? algorithm : null,
|
|
149
|
-
canonicalizedHeader,
|
|
149
|
+
signAlgo === 'rsa' ? canonicalizedHeader : crypto.createHash('sha256').update(canonicalizedHeader).digest(),
|
|
150
150
|
privateKey
|
|
151
151
|
)
|
|
152
152
|
.toString('base64');
|
package/lib/dkim/body/relaxed.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
const crypto = require('crypto');
|
|
6
|
+
const { MimeStructureStartFinder } = require('../mime-structure-start-finder');
|
|
6
7
|
|
|
7
8
|
const CHAR_CR = 0x0d;
|
|
8
9
|
const CHAR_LF = 0x0a;
|
|
@@ -39,9 +40,20 @@ class RelaxedHash {
|
|
|
39
40
|
this.maxSizeReached = maxBodyLength === 0;
|
|
40
41
|
|
|
41
42
|
this.emptyLinesQueue = [];
|
|
43
|
+
|
|
44
|
+
this.mimeStructureStartFinder = new MimeStructureStartFinder();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setContentType(contentTypeObj) {
|
|
48
|
+
if (/^multipart\//i.test(contentTypeObj.value) && contentTypeObj.params.boundary) {
|
|
49
|
+
this.mimeStructureStartFinder.setBoundary(contentTypeObj.params.boundary);
|
|
50
|
+
}
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
_updateBodyHash(chunk) {
|
|
54
|
+
// serach through the entire document, not just signed part
|
|
55
|
+
this.mimeStructureStartFinder.update(chunk);
|
|
56
|
+
|
|
45
57
|
this.canonicalizedLength += chunk.length;
|
|
46
58
|
|
|
47
59
|
if (this.maxSizeReached) {
|
|
@@ -270,6 +282,10 @@ class RelaxedHash {
|
|
|
270
282
|
// finalize
|
|
271
283
|
return this.bodyHash.digest(encoding);
|
|
272
284
|
}
|
|
285
|
+
|
|
286
|
+
getMimeStructureStart() {
|
|
287
|
+
return this.mimeStructureStartFinder.getMimeStructureStart();
|
|
288
|
+
}
|
|
273
289
|
}
|
|
274
290
|
|
|
275
291
|
module.exports = { RelaxedHash };
|
package/lib/dkim/body/simple.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { MimeStructureStartFinder } = require('../mime-structure-start-finder');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Class for calculating body hash of an email message body stream
|
|
@@ -30,9 +31,20 @@ class SimpleHash {
|
|
|
30
31
|
this.maxSizeReached = maxBodyLength === 0;
|
|
31
32
|
|
|
32
33
|
this.lastNewline = false;
|
|
34
|
+
|
|
35
|
+
this.mimeStructureStartFinder = new MimeStructureStartFinder();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setContentType(contentTypeObj) {
|
|
39
|
+
if (/^multipart\//i.test(contentTypeObj.value) && contentTypeObj.params.boundary) {
|
|
40
|
+
this.mimeStructureStartFinder.setBoundary(contentTypeObj.params.boundary);
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
_updateBodyHash(chunk) {
|
|
45
|
+
// serach through the entire document, not just signed part
|
|
46
|
+
this.mimeStructureStartFinder.update(chunk);
|
|
47
|
+
|
|
36
48
|
this.canonicalizedLength += chunk.length;
|
|
37
49
|
|
|
38
50
|
if (this.maxSizeReached) {
|
|
@@ -115,6 +127,10 @@ class SimpleHash {
|
|
|
115
127
|
|
|
116
128
|
return this.bodyHash.digest(encoding);
|
|
117
129
|
}
|
|
130
|
+
|
|
131
|
+
getMimeStructureStart() {
|
|
132
|
+
return this.mimeStructureStartFinder.getMimeStructureStart();
|
|
133
|
+
}
|
|
118
134
|
}
|
|
119
135
|
|
|
120
136
|
module.exports = { SimpleHash };
|
package/lib/dkim/dkim-signer.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
getSigningHeaderLines,
|
|
5
|
+
formatSignatureHeaderLine,
|
|
6
|
+
defaultDKIMFieldNames,
|
|
7
|
+
defaultARCFieldNames,
|
|
8
|
+
validateAlgorithm,
|
|
9
|
+
getPrivateKey
|
|
10
|
+
} = require('../../lib/tools');
|
|
4
11
|
const { MessageParser } = require('./message-parser');
|
|
5
12
|
const { dkimBody } = require('./body');
|
|
6
13
|
const { generateCanonicalizedHeader } = require('./header');
|
|
@@ -194,10 +201,23 @@ class DkimSigner extends MessageParser {
|
|
|
194
201
|
continue;
|
|
195
202
|
}
|
|
196
203
|
|
|
204
|
+
let privateKeyObj;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
privateKeyObj = getPrivateKey(signatureData.privateKey);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
this.errors.push({
|
|
210
|
+
selector: signatureData.selector,
|
|
211
|
+
signingDomain: signatureData.signingDomain,
|
|
212
|
+
err
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
197
217
|
let hashKey = `${bodyCanon}:${hashAlgo}:${signatureData.maxBodyLength}`;
|
|
198
218
|
|
|
199
219
|
try {
|
|
200
|
-
let keyType =
|
|
220
|
+
let keyType = privateKeyObj.asymmetricKeyType;
|
|
201
221
|
if (signAlgo && keyType !== signAlgo) {
|
|
202
222
|
// invalid key type
|
|
203
223
|
let err = new Error(`Invalid key type: "${keyType}" (expecting "${signAlgo}")`);
|
|
@@ -272,8 +292,8 @@ class DkimSigner extends MessageParser {
|
|
|
272
292
|
.sign(
|
|
273
293
|
// use `null` as algorithm to detect it from the key file
|
|
274
294
|
signAlgo === 'rsa' ? algorithm : null,
|
|
275
|
-
canonicalizedHeader,
|
|
276
|
-
|
|
295
|
+
signAlgo === 'rsa' ? canonicalizedHeader : crypto.createHash('sha256').update(canonicalizedHeader).digest(),
|
|
296
|
+
privateKeyObj
|
|
277
297
|
)
|
|
278
298
|
.toString('base64');
|
|
279
299
|
|
|
@@ -8,6 +8,7 @@ const { getARChain } = require('../arc');
|
|
|
8
8
|
const addressparser = require('nodemailer/lib/addressparser');
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const { v4: uuidv4 } = require('uuid');
|
|
11
|
+
const libmime = require('libmime');
|
|
11
12
|
|
|
12
13
|
class DkimVerifier extends MessageParser {
|
|
13
14
|
constructor(options) {
|
|
@@ -147,6 +148,23 @@ class DkimVerifier extends MessageParser {
|
|
|
147
148
|
if (!this.bodyHashes.has(signatureHeader.bodyHashKey)) {
|
|
148
149
|
this.bodyHashes.set(signatureHeader.bodyHashKey, dkimBody(signatureHeader.bodyCanon, signatureHeader.hashAlgo, signatureHeader.maxBodyLength));
|
|
149
150
|
}
|
|
151
|
+
|
|
152
|
+
let headersArray = this.headers.parsed;
|
|
153
|
+
const findLastMethod = typeof headersArray.findLast === 'function' ? headersArray.findLast : headersArray.find;
|
|
154
|
+
if (typeof headersArray.findLast !== 'function') {
|
|
155
|
+
headersArray = [].concat(headersArray).reverse();
|
|
156
|
+
}
|
|
157
|
+
const contentTypeHeader = findLastMethod.call(headersArray, header => header.key === 'content-type');
|
|
158
|
+
if (contentTypeHeader) {
|
|
159
|
+
let line = contentTypeHeader.line.toString();
|
|
160
|
+
if (line.indexOf(':') >= 0) {
|
|
161
|
+
line = line.substring(line.indexOf(':') + 1).trim();
|
|
162
|
+
}
|
|
163
|
+
const parsedContentType = libmime.parseHeaderValue(line);
|
|
164
|
+
for (let hasher of this.bodyHashes.values()) {
|
|
165
|
+
hasher.setContentType(parsedContentType);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
150
168
|
}
|
|
151
169
|
}
|
|
152
170
|
|
|
@@ -165,6 +183,7 @@ class DkimVerifier extends MessageParser {
|
|
|
165
183
|
// convert bodyHashes from hash objects to base64 strings
|
|
166
184
|
for (let [key, bodyHash] of this.bodyHashes.entries()) {
|
|
167
185
|
this.bodyHashes.get(key).hash = bodyHash.digest('base64');
|
|
186
|
+
this.bodyHashes.get(key).mimeStructureStart = bodyHash.getMimeStructureStart();
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
for (let signatureHeader of this.signatureHeaders) {
|
|
@@ -210,7 +229,9 @@ class DkimVerifier extends MessageParser {
|
|
|
210
229
|
: false;
|
|
211
230
|
}
|
|
212
231
|
|
|
213
|
-
|
|
232
|
+
const bodyHash = this.bodyHashes.get(signatureHeader.bodyHashKey)?.hash;
|
|
233
|
+
const mimeStructureStart = this.bodyHashes.get(signatureHeader.bodyHashKey)?.mimeStructureStart;
|
|
234
|
+
|
|
214
235
|
if (signatureHeader.parsed?.bh?.value !== bodyHash) {
|
|
215
236
|
status.result = 'neutral';
|
|
216
237
|
status.comment = `body hash did not verify`;
|
|
@@ -230,7 +251,7 @@ class DkimVerifier extends MessageParser {
|
|
|
230
251
|
try {
|
|
231
252
|
status.result = crypto.verify(
|
|
232
253
|
signatureHeader.signAlgo === 'rsa' ? signatureHeader.algorithm : null,
|
|
233
|
-
canonicalizedHeader,
|
|
254
|
+
signatureHeader.signAlgo === 'rsa' ? canonicalizedHeader : crypto.createHash('sha256').update(canonicalizedHeader).digest(),
|
|
234
255
|
publicKey,
|
|
235
256
|
Buffer.from(signatureHeader.parsed?.b?.value, 'base64')
|
|
236
257
|
)
|
|
@@ -344,6 +365,10 @@ class DkimVerifier extends MessageParser {
|
|
|
344
365
|
result.canonBodyLengthLimited = false;
|
|
345
366
|
}
|
|
346
367
|
|
|
368
|
+
if (typeof mimeStructureStart === 'number') {
|
|
369
|
+
result.mimeStructureStart = mimeStructureStart;
|
|
370
|
+
}
|
|
371
|
+
|
|
347
372
|
if (publicKey) {
|
|
348
373
|
result.publicKey = publicKey.toString();
|
|
349
374
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class MimeStructureStartFinder {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.byteCache = [];
|
|
6
|
+
|
|
7
|
+
this.matchFound = false;
|
|
8
|
+
this.noMatch = false;
|
|
9
|
+
this.lineStart = -1;
|
|
10
|
+
|
|
11
|
+
this.prevChunks = 0;
|
|
12
|
+
|
|
13
|
+
this.mimeStructureStart = -1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
setBoundary(boundary) {
|
|
17
|
+
this.boundary = (boundary || '').toString().trim();
|
|
18
|
+
|
|
19
|
+
this.boundaryBuf = Array.from(Buffer.from(`--${this.boundary}`));
|
|
20
|
+
this.boundaryBufLen = this.boundaryBuf.length;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
update(chunk) {
|
|
24
|
+
if (this.matchFound || !this.boundary) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (let i = 0, bufLen = chunk.length; i < bufLen; i++) {
|
|
29
|
+
let c = chunk[i];
|
|
30
|
+
|
|
31
|
+
// check ending
|
|
32
|
+
if (c === 0x0a || c === 0x0d) {
|
|
33
|
+
if (!this.noMatch && this.byteCache.length === this.boundaryBufLen) {
|
|
34
|
+
// match found
|
|
35
|
+
this.matchFound = true;
|
|
36
|
+
this.mimeStructureStart = this.lineStart;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
// reset counter
|
|
40
|
+
this.lineStart = -1;
|
|
41
|
+
this.noMatch = false;
|
|
42
|
+
this.byteCache = [];
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.noMatch) {
|
|
47
|
+
// no need to look
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.lineStart < 0) {
|
|
52
|
+
this.lineStart = this.prevChunks + i;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (this.byteCache.length >= this.boundaryBufLen) {
|
|
56
|
+
this.noMatch = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const expectingByte = this.boundaryBuf[this.byteCache.length];
|
|
61
|
+
if (expectingByte !== c) {
|
|
62
|
+
this.noMatch = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
this.byteCache[this.byteCache.length] = c;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.prevChunks += chunk.length;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getMimeStructureStart() {
|
|
72
|
+
if (!this.boundary) {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!this.matchFound && !this.noMatch && this.byteCache.length === this.boundaryBufLen) {
|
|
77
|
+
this.matchFound = true;
|
|
78
|
+
this.mimeStructureStart = this.lineStart;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return this.mimeStructureStart;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { MimeStructureStartFinder };
|
package/lib/tools.js
CHANGED
|
@@ -333,6 +333,28 @@ const getPublicKey = async (type, name, minBitLength, resolver) => {
|
|
|
333
333
|
throw err;
|
|
334
334
|
};
|
|
335
335
|
|
|
336
|
+
const getPrivateKey = privateKeyBuf => {
|
|
337
|
+
let privateKeyOpts;
|
|
338
|
+
|
|
339
|
+
if (typeof privateKeyBuf === 'string') {
|
|
340
|
+
privateKeyBuf = Buffer.from(privateKeyBuf);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (privateKeyBuf.length === 32) {
|
|
344
|
+
// seems like a raw ed25519 key
|
|
345
|
+
privateKeyBuf = Buffer.concat([Buffer.from('MC4CAQAwBQYDK2VwBCIEIA==', 'base64'), privateKeyBuf]);
|
|
346
|
+
privateKeyOpts = {
|
|
347
|
+
key: privateKeyBuf,
|
|
348
|
+
format: 'der',
|
|
349
|
+
type: 'pkcs8'
|
|
350
|
+
};
|
|
351
|
+
} else {
|
|
352
|
+
privateKeyOpts = { key: privateKeyBuf, format: 'pem' };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return crypto.createPrivateKey(privateKeyOpts);
|
|
356
|
+
};
|
|
357
|
+
|
|
336
358
|
const fetch = url =>
|
|
337
359
|
new Promise((resolve, reject) => {
|
|
338
360
|
https
|
|
@@ -399,7 +421,7 @@ const formatAuthHeaderRow = (method, status) => {
|
|
|
399
421
|
parts.push(`${method}=${status.result || 'none'}`);
|
|
400
422
|
|
|
401
423
|
if (status.underSized) {
|
|
402
|
-
parts.push(`(${escapeCommentValue(`undersized signature: ${status.underSized}`)})`);
|
|
424
|
+
parts.push(`(${escapeCommentValue(`undersized signature: ${status.underSized} bytes unsigned`)})`);
|
|
403
425
|
}
|
|
404
426
|
|
|
405
427
|
if (status.comment) {
|
|
@@ -554,6 +576,7 @@ module.exports = {
|
|
|
554
576
|
formatSignatureHeaderLine,
|
|
555
577
|
parseDkimHeaders,
|
|
556
578
|
getPublicKey,
|
|
579
|
+
getPrivateKey,
|
|
557
580
|
formatAuthHeaderRow,
|
|
558
581
|
escapeCommentValue,
|
|
559
582
|
fetch,
|
package/man/mailauth.1
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailauth",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.5",
|
|
4
4
|
"description": "Email authentication library for Node.js",
|
|
5
5
|
"main": "lib/mailauth.js",
|
|
6
6
|
"scripts": {
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
"marked-man": "0.7.0",
|
|
44
44
|
"mbox-reader": "1.1.5",
|
|
45
45
|
"mocha": "10.2.0",
|
|
46
|
-
"npm-check-updates": "16.14.
|
|
46
|
+
"npm-check-updates": "16.14.14",
|
|
47
47
|
"pkg": "5.8.1"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@postalsys/vmc": "1.0.
|
|
51
|
-
"fast-xml-parser": "4.3.
|
|
50
|
+
"@postalsys/vmc": "1.0.8",
|
|
51
|
+
"fast-xml-parser": "4.3.4",
|
|
52
52
|
"ipaddr.js": "2.1.0",
|
|
53
|
-
"joi": "17.12.
|
|
53
|
+
"joi": "17.12.1",
|
|
54
54
|
"libmime": "5.2.1",
|
|
55
|
-
"nodemailer": "6.9.
|
|
55
|
+
"nodemailer": "6.9.9",
|
|
56
56
|
"psl": "1.9.0",
|
|
57
57
|
"punycode": "2.3.1",
|
|
58
58
|
"undici": "5.28.2",
|