mailauth 3.0.0 → 3.0.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.
- package/bin/mailauth.js +19 -12
- package/cli.md +15 -9
- package/lib/bimi/index.js +68 -16
- package/lib/commands/vmc.js +7 -2
- package/lib/tools.js +2 -1
- package/licenses.txt +12 -11
- package/man/mailauth.1 +1 -1
- package/package.json +2 -3
package/bin/mailauth.js
CHANGED
|
@@ -294,18 +294,25 @@ const argv = yargs(hideBin(process.argv))
|
|
|
294
294
|
['vmc'],
|
|
295
295
|
'Validate VMC logo',
|
|
296
296
|
yargs => {
|
|
297
|
-
yargs
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
297
|
+
yargs
|
|
298
|
+
.option('authorityPath', {
|
|
299
|
+
alias: 'p',
|
|
300
|
+
type: 'string',
|
|
301
|
+
description: 'Path to a VMC file',
|
|
302
|
+
demandOption: false
|
|
303
|
+
})
|
|
304
|
+
.option('authority', {
|
|
305
|
+
alias: 'a',
|
|
306
|
+
type: 'string',
|
|
307
|
+
description: 'URL to a VMC file',
|
|
308
|
+
demandOption: false
|
|
309
|
+
})
|
|
310
|
+
.option('domain', {
|
|
311
|
+
alias: 'd',
|
|
312
|
+
type: 'string',
|
|
313
|
+
description: 'Sending domain to validate',
|
|
314
|
+
demandOption: false
|
|
315
|
+
});
|
|
309
316
|
},
|
|
310
317
|
argv => {
|
|
311
318
|
commandVmc(argv)
|
package/cli.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
Command line utility and a [Node.js library](README.md) for email authentication.
|
|
4
|
+
|
|
1
5
|
# CLI USAGE
|
|
2
6
|
|
|
3
7
|
## TOC
|
|
@@ -224,20 +228,22 @@ Where
|
|
|
224
228
|
**Options**
|
|
225
229
|
|
|
226
230
|
- `--authority <url>` or `-a <url>` is the URL for the VMC resource
|
|
227
|
-
- `--
|
|
231
|
+
- `--authorityPath <path>` or `-p <path>` is the cached file for the authority URL to avoid network requests
|
|
232
|
+
- `--domain <domain>` or `-d <domain>` is the sender domain to compare the certificate against
|
|
228
233
|
|
|
229
234
|
**Example**
|
|
230
235
|
|
|
231
236
|
```
|
|
232
|
-
$ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem
|
|
237
|
+
$ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem -d cnn.com
|
|
233
238
|
{
|
|
234
239
|
"url": "https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem",
|
|
235
240
|
"success": true,
|
|
241
|
+
"domainVerified": true,
|
|
236
242
|
"vmc": {
|
|
237
243
|
"mediaType": "image/svg+xml",
|
|
238
244
|
"hashAlgo": "sha1",
|
|
239
245
|
"hashValue": "ea8c81da633c66a16262134a78576cdf067638e9",
|
|
240
|
-
"logoFile": "
|
|
246
|
+
"logoFile": "<2300B base64 encoded file>",
|
|
241
247
|
"validHash": true,
|
|
242
248
|
"certificate": {
|
|
243
249
|
"subjectAltName": [
|
|
@@ -272,17 +278,17 @@ $ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.
|
|
|
272
278
|
If the certificate verification fails, then the contents are not returned.
|
|
273
279
|
|
|
274
280
|
```
|
|
275
|
-
$ mailauth vmc -
|
|
281
|
+
$ mailauth vmc -p /path/to/random/cert-bundle.pem
|
|
276
282
|
{
|
|
277
283
|
"success": false,
|
|
278
284
|
"error": {
|
|
279
285
|
"message": "Self signed certificate in certificate chain",
|
|
280
286
|
"details": {
|
|
281
|
-
"subject": "CN=
|
|
282
|
-
"fingerprint": "
|
|
283
|
-
"fingerprint235": "
|
|
284
|
-
"validFrom": "Jul
|
|
285
|
-
"validTo": "
|
|
287
|
+
"subject": "CN=postal.vmc.local\nO=Postal Systems OU.\nC=EE",
|
|
288
|
+
"fingerprint": "CC:49:83:ED:3F:6B:77:45:5B:A5:3B:9E:EC:99:0E:A1:EF:D7:FF:97",
|
|
289
|
+
"fingerprint235": "D4:36:6F:B4:EF:2B:4F:9E:84:23:3D:F2:3A:F7:13:21:C6:C3:CF:CB:03:5F:BB:54:5B:69:A4:AC:6A:43:61:7D",
|
|
290
|
+
"validFrom": "Jul 9 06:13:33 2022 GMT",
|
|
291
|
+
"validTo": "Jul 9 06:13:33 2023 GMT"
|
|
286
292
|
},
|
|
287
293
|
"code": "SELF_SIGNED_CERT_IN_CHAIN"
|
|
288
294
|
}
|
package/lib/bimi/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const dns = require('dns');
|
|
5
|
-
const { formatAuthHeaderRow, parseDkimHeaders } = require('../tools');
|
|
5
|
+
const { formatAuthHeaderRow, parseDkimHeaders, formatDomain, getAlignment } = require('../tools');
|
|
6
6
|
const Joi = require('joi');
|
|
7
7
|
const packageData = require('../../package.json');
|
|
8
8
|
const httpsSchema = Joi.string().uri({
|
|
@@ -244,26 +244,30 @@ const validateVMC = async bimiData => {
|
|
|
244
244
|
return false;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
let selector = bimiData?.status?.header?.selector;
|
|
248
|
+
let d = bimiData?.status?.header?.d;
|
|
249
|
+
|
|
247
250
|
let promises = [];
|
|
248
251
|
|
|
249
|
-
promises.push(downloadPromise(bimiData.location, bimiData.
|
|
250
|
-
promises.push(downloadPromise(bimiData.authority, bimiData.
|
|
252
|
+
promises.push(downloadPromise(bimiData.location, bimiData.locationPath));
|
|
253
|
+
promises.push(downloadPromise(bimiData.authority, bimiData.authorityPath));
|
|
251
254
|
|
|
252
255
|
if (!promises.length) {
|
|
253
256
|
return false;
|
|
254
257
|
}
|
|
255
258
|
|
|
256
|
-
let
|
|
259
|
+
let [{ reason: locationError, value: locationValue, status: locationStatus }, { reason: authorityError, value: authorityValue, status: authorityStatus }] =
|
|
260
|
+
await Promise.allSettled(promises);
|
|
257
261
|
|
|
258
262
|
let result = {};
|
|
259
|
-
if (
|
|
263
|
+
if (locationValue || locationError) {
|
|
260
264
|
result.location = {
|
|
261
265
|
url: bimiData.location,
|
|
262
|
-
success:
|
|
266
|
+
success: locationStatus === 'fulfilled'
|
|
263
267
|
};
|
|
264
268
|
|
|
265
|
-
if (
|
|
266
|
-
let err =
|
|
269
|
+
if (locationError) {
|
|
270
|
+
let err = locationError;
|
|
267
271
|
result.location.error = { message: err.message };
|
|
268
272
|
if (err.redirect) {
|
|
269
273
|
result.location.error.redirect = err.redirect;
|
|
@@ -274,18 +278,18 @@ const validateVMC = async bimiData => {
|
|
|
274
278
|
}
|
|
275
279
|
|
|
276
280
|
if (result.location.success) {
|
|
277
|
-
result.location.logoFile =
|
|
281
|
+
result.location.logoFile = locationValue.toString('base64');
|
|
278
282
|
}
|
|
279
283
|
}
|
|
280
284
|
|
|
281
|
-
if (
|
|
285
|
+
if (authorityValue || authorityError) {
|
|
282
286
|
result.authority = {
|
|
283
287
|
url: bimiData.authority,
|
|
284
|
-
success:
|
|
288
|
+
success: authorityStatus === 'fulfilled'
|
|
285
289
|
};
|
|
286
290
|
|
|
287
|
-
if (
|
|
288
|
-
let err =
|
|
291
|
+
if (authorityError) {
|
|
292
|
+
let err = authorityError;
|
|
289
293
|
result.authority.error = { message: err.message };
|
|
290
294
|
if (err.redirect) {
|
|
291
295
|
result.authority.error.redirect = err.redirect;
|
|
@@ -295,9 +299,57 @@ const validateVMC = async bimiData => {
|
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
301
|
|
|
298
|
-
if (
|
|
302
|
+
if (authorityValue) {
|
|
299
303
|
try {
|
|
300
|
-
|
|
304
|
+
let vmcData = await vmc(authorityValue);
|
|
305
|
+
|
|
306
|
+
if (vmcData?.mediaType?.toLowerCase() !== 'image/svg+xml') {
|
|
307
|
+
let error = new Error('Invalid media type for the logo file');
|
|
308
|
+
error.details = {
|
|
309
|
+
mediaType: vmcData.mediaType
|
|
310
|
+
};
|
|
311
|
+
error.code = 'INVALID_MEDIATYPE';
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (d) {
|
|
316
|
+
// validate domain
|
|
317
|
+
let selectorSet = [];
|
|
318
|
+
let domainSet = [];
|
|
319
|
+
vmcData?.certificate?.subjectAltName?.map(formatDomain)?.forEach(domain => {
|
|
320
|
+
if (/\b_bimi\./.test(domain)) {
|
|
321
|
+
selectorSet.push(domain);
|
|
322
|
+
} else {
|
|
323
|
+
domainSet.push(domain);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
let domainVerified = false;
|
|
328
|
+
|
|
329
|
+
if (selector && selectorSet.includes(formatDomain(`${selector}._bimi.${d}`))) {
|
|
330
|
+
domainVerified = true;
|
|
331
|
+
} else {
|
|
332
|
+
let alignedDomain = getAlignment(d, domainSet, false);
|
|
333
|
+
if (alignedDomain) {
|
|
334
|
+
domainVerified = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!domainVerified) {
|
|
339
|
+
let error = new Error('Domain can not be verified');
|
|
340
|
+
error.details = {
|
|
341
|
+
subjectAltName: vmcData?.certificate?.subjectAltName,
|
|
342
|
+
selector,
|
|
343
|
+
d
|
|
344
|
+
};
|
|
345
|
+
error.code = 'VMC_DOMAIN_MISMATCH';
|
|
346
|
+
throw error;
|
|
347
|
+
} else {
|
|
348
|
+
result.authority.domainVerified = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
result.authority.vmc = vmcData;
|
|
301
353
|
} catch (err) {
|
|
302
354
|
result.authority.success = false;
|
|
303
355
|
result.authority.error = { message: err.message };
|
|
@@ -313,7 +365,7 @@ const validateVMC = async bimiData => {
|
|
|
313
365
|
if (result.location && result.location.success && result.authority.success) {
|
|
314
366
|
try {
|
|
315
367
|
if (result.location.success && result.authority.vmc.hashAlgo && result.authority.vmc.validHash) {
|
|
316
|
-
let hash = crypto.createHash(result.authority.vmc.hashAlgo).update(
|
|
368
|
+
let hash = crypto.createHash(result.authority.vmc.hashAlgo).update(locationValue).digest('hex');
|
|
317
369
|
result.location.hashAlgo = result.authority.vmc.hashAlgo;
|
|
318
370
|
result.location.hashValue = hash;
|
|
319
371
|
result.authority.hashMatch = hash === result.authority.vmc.hashValue;
|
package/lib/commands/vmc.js
CHANGED
|
@@ -6,13 +6,18 @@ const fs = require('fs').promises;
|
|
|
6
6
|
|
|
7
7
|
const cmd = async argv => {
|
|
8
8
|
let bimiData = {};
|
|
9
|
-
if (argv.
|
|
10
|
-
bimiData.
|
|
9
|
+
if (argv.authorityPath) {
|
|
10
|
+
bimiData.authorityPath = await fs.readFile(argv.authorityPath);
|
|
11
11
|
}
|
|
12
|
+
|
|
12
13
|
if (argv.authority) {
|
|
13
14
|
bimiData.authority = argv.authority;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
if (argv.domain) {
|
|
18
|
+
bimiData.status = { header: { d: argv.domain } };
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
const result = await validateVMC(bimiData);
|
|
17
22
|
process.stdout.write(JSON.stringify(result.authority, false, 2) + '\n');
|
|
18
23
|
};
|
package/lib/tools.js
CHANGED
package/licenses.txt
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
name
|
|
2
|
-
----
|
|
3
|
-
@fidm/x509
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
name license type link installed version author
|
|
2
|
+
---- ------------ ---- ----------------- ------
|
|
3
|
+
@fidm/x509 MIT git+ssh://git@github.com/fidm/x509.git 1.2.1
|
|
4
|
+
@postalsys/vmc MIT https://registry.npmjs.org/@postalsys/vmc/-/vmc-1.0.1.tgz 1.0.1 Postal Systems OÜ
|
|
5
|
+
ipaddr.js MIT git://github.com/whitequark/ipaddr.js.git 2.0.1 whitequark whitequark@whitequark.org
|
|
6
|
+
joi BSD-3-Clause git://github.com/sideway/joi.git 17.6.0
|
|
7
|
+
libmime MIT git://github.com/andris9/libmime.git 5.1.0 Andris Reinman andris@kreata.ee
|
|
8
|
+
node-forge (BSD-3-Clause OR GPL-2.0) git+https://github.com/digitalbazaar/forge.git 1.3.1 Digital Bazaar, Inc. support@digitalbazaar.com http://digitalbazaar.com/
|
|
9
|
+
nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.7 Andris Reinman
|
|
10
|
+
psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.9.0 Lupo Montero lupomontero@gmail.com https://lupomontero.com/
|
|
11
|
+
punycode MIT git+https://github.com/bestiejs/punycode.js.git 2.1.1 Mathias Bynens https://mathiasbynens.be/
|
|
12
|
+
yargs MIT git+https://github.com/yargs/yargs.git 17.5.1
|
package/man/mailauth.1
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mailauth",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Email authentication library for Node.js",
|
|
5
5
|
"main": "lib/mailauth.js",
|
|
6
6
|
"scripts": {
|
|
@@ -45,8 +45,7 @@
|
|
|
45
45
|
"pkg": "5.7.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@
|
|
49
|
-
"@postalsys/vmc": "1.0.1",
|
|
48
|
+
"@postalsys/vmc": "1.0.3",
|
|
50
49
|
"ipaddr.js": "2.0.1",
|
|
51
50
|
"joi": "17.6.0",
|
|
52
51
|
"libmime": "5.1.0",
|