mailauth 2.3.4 → 3.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/README.md CHANGED
@@ -327,15 +327,6 @@ Some example authority evidence documents:
327
327
  - [from default.\_bimi.cnn.com](https://amplify.valimail.com/bimi/time-warner/LysAFUdG-Hw-cnn_vmc.pem)
328
328
  - [from default.\_bimi.entrustdatacard.com](https://www.entrustdatacard.com/-/media/certificate/Entrust%20VMC%20July%2014%202020.pem)
329
329
 
330
- You can parse logos from these certificate files using the `parseLogoFromX509` function.
331
-
332
- ```js
333
- const { parseLogoFromX509 } = require('mailauth/lib/tools');
334
- let { altnNames, svg } = await parseLogoFromX509(fs.readFileSync('vmc.pem'));
335
- ```
336
-
337
- > **NB!** `parseLogoFromX509` does not verify the validity of the VMC certificate. It could be self-signed or expired and still be processed.
338
-
339
330
  ## MTA-STS
340
331
 
341
332
  `mailauth` allows you to fetch MTA-STS information for a domain name.
package/bin/mailauth.js CHANGED
@@ -6,10 +6,13 @@ const yargs = require('yargs/yargs');
6
6
  const { hideBin } = require('yargs/helpers');
7
7
  const os = require('os');
8
8
  const assert = require('assert');
9
+
9
10
  const commandReport = require('../lib/commands/report');
10
11
  const commandSign = require('../lib/commands/sign');
11
12
  const commandSeal = require('../lib/commands/seal');
12
13
  const commandSpf = require('../lib/commands/spf');
14
+ const commandVmc = require('../lib/commands/vmc');
15
+
13
16
  const fs = require('fs');
14
17
  const pathlib = require('path');
15
18
 
@@ -287,6 +290,35 @@ const argv = yargs(hideBin(process.argv))
287
290
  });
288
291
  }
289
292
  )
293
+ .command(
294
+ ['vmc'],
295
+ 'Validate VMC logo',
296
+ yargs => {
297
+ yargs.option('authorityFile', {
298
+ alias: 'f',
299
+ type: 'string',
300
+ description: 'Path to a VMC file',
301
+ demandOption: false
302
+ });
303
+ yargs.option('authority', {
304
+ alias: 'a',
305
+ type: 'string',
306
+ description: 'URL to a VMC file',
307
+ demandOption: false
308
+ });
309
+ },
310
+ argv => {
311
+ commandVmc(argv)
312
+ .then(() => {
313
+ process.exit();
314
+ })
315
+ .catch(err => {
316
+ console.error('Failed to verify VMC file');
317
+ console.error(err);
318
+ process.exit(1);
319
+ });
320
+ }
321
+ )
290
322
  .command(
291
323
  ['license'],
292
324
  'Show license information',
package/cli.md CHANGED
@@ -9,6 +9,7 @@
9
9
  - [sign](#sign) - to sign an email with DKIM
10
10
  - [seal](#seal) - to seal an email with ARC
11
11
  - [spf](#spf) - to validate SPF for an IP address and an email address
12
+ - [vmc](#vmc) - to validate BIMI VMC logo files
12
13
  - [license](#license) - display licenses for `mailauth` and included modules
13
14
  - [DNS cache file](#dns-cache-file)
14
15
 
@@ -208,6 +209,86 @@ DNS query for A mail.wildduck.email: ["217.146.76.20"]
208
209
  ...
209
210
  ```
210
211
 
212
+ ### vmc
213
+
214
+ `vmc` command takes either the URL for a VMC file or a file path or both. It then verifies if the VMC resource is a valid file or not and exposes its contents.
215
+
216
+ ```
217
+ $ mailauth vmc [options]
218
+ ```
219
+
220
+ Where
221
+
222
+ - **options** are option flags and arguments
223
+
224
+ **Options**
225
+
226
+ - `--authority <url>` or `-a <url>` is the URL for the VMC resource
227
+ - `--authorityFile <path>` or `-f <path>` is the cached file for the authority URL to avoid network requests
228
+
229
+ **Example**
230
+
231
+ ```
232
+ $ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem
233
+ {
234
+ "url": "https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem",
235
+ "success": true,
236
+ "vmc": {
237
+ "mediaType": "image/svg+xml",
238
+ "hashAlgo": "sha1",
239
+ "hashValue": "ea8c81da633c66a16262134a78576cdf067638e9",
240
+ "logoFile": "PD94bWwgdmVyc...",
241
+ "validHash": true,
242
+ "certificate": {
243
+ "subjectAltName": [
244
+ "cnn.com"
245
+ ],
246
+ "subject": {
247
+ "businessCategory": "Private Organization",
248
+ "jurisdictionCountryName": "US",
249
+ "jurisdictionStateOrProvinceName": "Delaware",
250
+ "serialNumber": "2976730",
251
+ "countryName": "US",
252
+ "stateOrProvinceName": "Georgia",
253
+ "localityName": "Atlanta",
254
+ "street": "190 Marietta St NW",
255
+ "organizationName": "Cable News Network, Inc.",
256
+ "commonName": "Cable News Network, Inc.",
257
+ "trademarkCountryOrRegionName": "US",
258
+ "trademarkRegistration": "5817930"
259
+ },
260
+ "fingerprint": "17:B3:94:97:E6:6B:C8:6B:33:B8:0A:D2:F0:79:6B:08:A2:A6:84:BD",
261
+ "serialNumber": "0821B8FE0A9CBC3BAC10DA08C088EEF4",
262
+ "issuer": {
263
+ "countryName": "US",
264
+ "organizationName": "DigiCert, Inc.",
265
+ "commonName": "DigiCert Verified Mark RSA4096 SHA256 2021 CA1"
266
+ }
267
+ }
268
+ }
269
+ }
270
+ ```
271
+
272
+ If the certificate verification fails, then the contents are not returned.
273
+
274
+ ```
275
+ $ mailauth vmc -f /path/to/random/cert-bundle.pem
276
+ {
277
+ "success": false,
278
+ "error": {
279
+ "message": "Self signed certificate in certificate chain",
280
+ "details": {
281
+ "subject": "CN=catchall.delivery",
282
+ "fingerprint": "35:EF:C9:9A:52:D5:A9:94:00:68:C6:D4:17:F1:26:61:01:0F:70:6D",
283
+ "fingerprint235": "09:AB:0F:6B:F5:4F:16:58:F8:94:80:DE:E2:1A:D1:47:CC:64:F2:BF:63:E7:73:E4:02:F9:D3:C3:F6:9E:CC:86",
284
+ "validFrom": "Jul 6 23:10:49 2022 GMT",
285
+ "validTo": "Oct 4 23:10:48 2022 GMT"
286
+ },
287
+ "code": "SELF_SIGNED_CERT_IN_CHAIN"
288
+ }
289
+ }
290
+ ```
291
+
211
292
  ### license
212
293
 
213
294
  Display licenses for `mailauth` and included modules.
package/lib/bimi/index.js CHANGED
@@ -1,12 +1,18 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('crypto');
3
4
  const dns = require('dns');
4
5
  const { formatAuthHeaderRow, parseDkimHeaders } = require('../tools');
5
6
  const Joi = require('joi');
7
+ const packageData = require('../../package.json');
6
8
  const httpsSchema = Joi.string().uri({
7
9
  scheme: ['https']
8
10
  });
9
11
 
12
+ const https = require('https');
13
+ const http = require('http');
14
+ const { vmc } = require('@postalsys/vmc');
15
+
10
16
  const lookup = async data => {
11
17
  let { dmarc, headers, resolver } = data;
12
18
  let headerRows = (headers && headers.parsed) || [];
@@ -161,4 +167,171 @@ const lookup = async data => {
161
167
  return response;
162
168
  };
163
169
 
164
- module.exports = { bimi: lookup };
170
+ const downloadPromise = (url, cachedFile) => {
171
+ if (cachedFile) {
172
+ return cachedFile;
173
+ }
174
+
175
+ if (!url) {
176
+ return false;
177
+ }
178
+
179
+ const parsedUrl = new URL(url);
180
+
181
+ const options = {
182
+ protocol: parsedUrl.protocol,
183
+ host: parsedUrl.host,
184
+ headers: {
185
+ host: parsedUrl.host,
186
+ 'User-Agent': `mailauth/${packageData.version} (+${packageData.homepage}`
187
+ },
188
+ servername: parsedUrl.hostname,
189
+ port: 443,
190
+ path: parsedUrl.pathname,
191
+ method: 'GET',
192
+ rejectUnauthorized: true
193
+ };
194
+
195
+ return new Promise((resolve, reject) => {
196
+ let protoHandler;
197
+ switch (parsedUrl.protocol) {
198
+ case 'https:':
199
+ protoHandler = https;
200
+ break;
201
+ case 'http:':
202
+ protoHandler = http;
203
+ break;
204
+ default:
205
+ reject(new Error(`Unknown protocol ${parsedUrl.protocol}`));
206
+ }
207
+ const req = protoHandler.request(options, res => {
208
+ let chunks = [],
209
+ chunklen = 0;
210
+ res.on('readable', () => {
211
+ let chunk;
212
+ while ((chunk = res.read()) !== null) {
213
+ chunks.push(chunk);
214
+ chunklen += chunk.length;
215
+ }
216
+ });
217
+ res.on('end', () => {
218
+ let data = Buffer.concat(chunks, chunklen);
219
+ if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
220
+ let err = new Error(`Invalid response code ${res.statusCode || '-'}`);
221
+ err.code = 'http_status_' + (res.statusCode || 'na');
222
+ if (res.headers.location && res.statusCode >= 300 && res.statusCode < 400) {
223
+ err.redirect = {
224
+ code: res.statusCode,
225
+ location: res.headers.location
226
+ };
227
+ }
228
+ return reject(err);
229
+ }
230
+ resolve(data);
231
+ });
232
+ res.on('error', err => reject(err));
233
+ });
234
+
235
+ req.on('error', err => {
236
+ reject(err);
237
+ });
238
+ req.end();
239
+ });
240
+ };
241
+
242
+ const validateVMC = async bimiData => {
243
+ if (!bimiData) {
244
+ return false;
245
+ }
246
+
247
+ let promises = [];
248
+
249
+ promises.push(downloadPromise(bimiData.location, bimiData.locationFile));
250
+ promises.push(downloadPromise(bimiData.authority, bimiData.authorityFile));
251
+
252
+ if (!promises.length) {
253
+ return false;
254
+ }
255
+
256
+ let results = await Promise.allSettled(promises);
257
+
258
+ let result = {};
259
+ if (results[0].value || results[0].reason) {
260
+ result.location = {
261
+ url: bimiData.location,
262
+ success: results[0].status === 'fulfilled'
263
+ };
264
+
265
+ if (results[0].reason) {
266
+ let err = results[0].reason;
267
+ result.location.error = { message: err.message };
268
+ if (err.redirect) {
269
+ result.location.error.redirect = err.redirect;
270
+ }
271
+ if (err.code) {
272
+ result.location.error.code = err.code;
273
+ }
274
+ }
275
+
276
+ if (result.location.success) {
277
+ result.location.logoFile = results[0].value.toString('base64');
278
+ }
279
+ }
280
+
281
+ if (results[1].value || results[1].reason) {
282
+ result.authority = {
283
+ url: bimiData.authority,
284
+ success: results[1].status === 'fulfilled'
285
+ };
286
+
287
+ if (results[1].reason) {
288
+ let err = results[1].reason;
289
+ result.authority.error = { message: err.message };
290
+ if (err.redirect) {
291
+ result.authority.error.redirect = err.redirect;
292
+ }
293
+ if (err.code) {
294
+ result.authority.error.code = err.code;
295
+ }
296
+ }
297
+
298
+ if (results[1].value) {
299
+ try {
300
+ result.authority.vmc = await vmc(results[1].value);
301
+ } catch (err) {
302
+ result.authority.success = false;
303
+ result.authority.error = { message: err.message };
304
+ if (err.details) {
305
+ result.authority.error.details = err.details;
306
+ }
307
+ if (err.code) {
308
+ result.authority.error.code = err.code;
309
+ }
310
+ }
311
+ }
312
+
313
+ if (result.location && result.location.success && result.authority.success) {
314
+ try {
315
+ if (result.location.success && result.authority.vmc.hashAlgo && result.authority.vmc.validHash) {
316
+ let hash = crypto.createHash(result.authority.vmc.hashAlgo).update(results[0].value).digest('hex');
317
+ result.location.hashAlgo = result.authority.vmc.hashAlgo;
318
+ result.location.hashValue = hash;
319
+ result.authority.hashMatch = hash === result.authority.vmc.hashValue;
320
+ }
321
+ } catch (err) {
322
+ result.authority.success = false;
323
+ result.authority.error = { message: err.message };
324
+ if (err.details) {
325
+ result.authority.error.details = err.details;
326
+ }
327
+ if (err.code) {
328
+ result.authority.error.code = err.code;
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ return result;
335
+ };
336
+
337
+ module.exports = { bimi: lookup, validateVMC };
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const { validateVMC } = require('../bimi');
4
+
5
+ const fs = require('fs').promises;
6
+
7
+ const cmd = async argv => {
8
+ let bimiData = {};
9
+ if (argv.authorityFile) {
10
+ bimiData.authorityFile = await fs.readFile(argv.authorityFile);
11
+ }
12
+ if (argv.authority) {
13
+ bimiData.authority = argv.authority;
14
+ }
15
+
16
+ const result = await validateVMC(bimiData);
17
+ process.stdout.write(JSON.stringify(result.authority, false, 2) + '\n');
18
+ };
19
+
20
+ module.exports = cmd;
package/lib/mailauth.js CHANGED
@@ -4,7 +4,7 @@ const { dkimVerify } = require('./dkim/verify');
4
4
  const { spf } = require('./spf');
5
5
  const { dmarc } = require('./dmarc');
6
6
  const { arc, createSeal } = require('./arc');
7
- const { bimi } = require('./bimi');
7
+ const { bimi, validateVMC: validateBimiVmc } = require('./bimi');
8
8
  const { parseReceived } = require('./parse-received');
9
9
  const { sealMessage } = require('./arc');
10
10
  const libmime = require('libmime');
@@ -180,4 +180,4 @@ const authenticate = async (input, opts) => {
180
180
  };
181
181
  };
182
182
 
183
- module.exports = { authenticate, sealMessage };
183
+ module.exports = { authenticate, sealMessage, validateBimiVmc };
package/lib/tools.js CHANGED
@@ -10,10 +10,6 @@ const https = require('https');
10
10
  const packageData = require('../package');
11
11
  const parseDkimHeaders = require('./parse-dkim-headers');
12
12
  const psl = require('psl');
13
- const { Certificate } = require('@fidm/x509');
14
- const zlib = require('zlib');
15
- const util = require('util');
16
- const gunzip = util.promisify(zlib.gunzip);
17
13
  const pki = require('node-forge').pki;
18
14
  const Joi = require('joi');
19
15
  const base64Schema = Joi.string().base64({ paddingRequired: false });
@@ -474,44 +470,6 @@ const validateAlgorithm = (algorithm, strict) => {
474
470
  }
475
471
  };
476
472
 
477
- /**
478
- * Function takes Verified Mark Certificate file and parses domain names and SVG file
479
- * NB! Certificate is not verified in any way. If there are altNames and SVG content
480
- * available then these are returned even if the certificate is self signed or expired.
481
- * @param {Buffer} pem VMC file
482
- * @returns {Object|Boolean} Either an object with {altNames[], svg} or false if required data was missing from the certificate
483
- */
484
- const parseLogoFromX509 = async pem => {
485
- const cert = Certificate.fromPEM(pem);
486
-
487
- const altNames = cert.extensions
488
- .filter(e => e.oid === '2.5.29.17')
489
- .flatMap(d => d?.altNames?.map(an => an?.dnsName?.trim()))
490
- .filter(an => an);
491
- if (!altNames.length) {
492
- return false;
493
- }
494
-
495
- let logo = cert.extensions.find(e => e.oid === '1.3.6.1.5.5.7.1.12');
496
- if (!logo?.value?.length) {
497
- return false;
498
- }
499
-
500
- let str = logo.value.toString();
501
- // No idea what is that binary stuff before the data uri block
502
- let dataMatch = /\bdata:/.test(str) && str.match(/\bbase64,/);
503
- if (dataMatch) {
504
- let b64 = str.substr(dataMatch.index + dataMatch[0].length);
505
- let svg = await gunzip(Buffer.from(b64, 'base64'));
506
- return {
507
- pem,
508
- altNames,
509
- svg: svg.toString()
510
- };
511
- }
512
- return false;
513
- };
514
-
515
473
  module.exports = {
516
474
  writeToStream,
517
475
  parseHeaders,
@@ -532,7 +490,5 @@ module.exports = {
532
490
 
533
491
  getAlignment,
534
492
 
535
- formatRelaxedLine,
536
-
537
- parseLogoFromX509
493
+ formatRelaxedLine
538
494
  };
package/licenses.txt CHANGED
@@ -8,4 +8,4 @@ node-forge (BSD-3-Clause OR GPL-2.0) git+https://github.com/digitalbazaar/forg
8
8
  nodemailer MIT git+https://github.com/nodemailer/nodemailer.git 6.7.5 Andris Reinman
9
9
  psl MIT git+ssh://git@github.com/lupomontero/psl.git 1.8.0 Lupo Montero lupomontero@gmail.com https://lupomontero.com/
10
10
  punycode MIT git+https://github.com/bestiejs/punycode.js.git 2.1.1 Mathias Bynens https://mathiasbynens.be/
11
- yargs MIT git+https://github.com/yargs/yargs.git 17.5.0
11
+ yargs MIT git+https://github.com/yargs/yargs.git 17.5.1
package/man/mailauth.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "MAILAUTH" "1" "June 2022" "v2.3.3" "Mailauth Help"
1
+ .TH "MAILAUTH" "1" "July 2022" "v2.3.4" "Mailauth Help"
2
2
  .SH "NAME"
3
3
  \fBmailauth\fR
4
4
  .QP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailauth",
3
- "version": "2.3.4",
3
+ "version": "3.0.0",
4
4
  "description": "Email authentication library for Node.js",
5
5
  "main": "lib/mailauth.js",
6
6
  "scripts": {
@@ -33,7 +33,7 @@
33
33
  "homepage": "https://github.com/postalsys/mailauth",
34
34
  "devDependencies": {
35
35
  "chai": "4.3.6",
36
- "eslint": "8.17.0",
36
+ "eslint": "8.19.0",
37
37
  "eslint-config-nodemailer": "1.2.0",
38
38
  "eslint-config-prettier": "8.5.0",
39
39
  "js-yaml": "4.1.0",
@@ -46,17 +46,18 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@fidm/x509": "1.2.1",
49
+ "@postalsys/vmc": "1.0.1",
49
50
  "ipaddr.js": "2.0.1",
50
51
  "joi": "17.6.0",
51
52
  "libmime": "5.1.0",
52
53
  "node-forge": "1.3.1",
53
- "nodemailer": "6.7.5",
54
- "psl": "1.8.0",
54
+ "nodemailer": "6.7.7",
55
+ "psl": "1.9.0",
55
56
  "punycode": "2.1.1",
56
57
  "yargs": "17.5.1"
57
58
  },
58
59
  "engines": {
59
- "node": ">=14.0.0"
60
+ "node": ">=16.0.0"
60
61
  },
61
62
  "bin": {
62
63
  "mailauth": "bin/mailauth.js"