mailauth 2.3.4 → 3.0.2
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 +2 -11
- package/bin/mailauth.js +39 -0
- package/cli.md +118 -8
- package/lib/bimi/index.js +261 -2
- package/lib/bimi/validate-svg.js +96 -0
- package/lib/commands/vmc.js +25 -0
- package/lib/mailauth.js +8 -2
- package/lib/tools.js +1 -44
- package/licenses.txt +12 -11
- package/man/mailauth.1 +1 -1
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
- ARC sealing
|
|
11
11
|
- Sealing on authentication
|
|
12
12
|
- Sealing after modifications
|
|
13
|
-
- **BIMI** resolving
|
|
13
|
+
- **BIMI** resolving and **VMC** validation
|
|
14
14
|
- **MTA-STS** helpers
|
|
15
15
|
|
|
16
|
-
Pure JavaScript implementation, no external applications or compilation needed. It runs on any server/device that has Node
|
|
16
|
+
Pure JavaScript implementation, no external applications or compilation needed. It runs on any server/device that has Node 16+ installed.
|
|
17
17
|
|
|
18
18
|
## Command line usage
|
|
19
19
|
|
|
@@ -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,42 @@ const argv = yargs(hideBin(process.argv))
|
|
|
287
290
|
});
|
|
288
291
|
}
|
|
289
292
|
)
|
|
293
|
+
.command(
|
|
294
|
+
['vmc'],
|
|
295
|
+
'Validate VMC logo',
|
|
296
|
+
yargs => {
|
|
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
|
+
});
|
|
316
|
+
},
|
|
317
|
+
argv => {
|
|
318
|
+
commandVmc(argv)
|
|
319
|
+
.then(() => {
|
|
320
|
+
process.exit();
|
|
321
|
+
})
|
|
322
|
+
.catch(err => {
|
|
323
|
+
console.error('Failed to verify VMC file');
|
|
324
|
+
console.error(err);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
)
|
|
290
329
|
.command(
|
|
291
330
|
['license'],
|
|
292
331
|
'Show license information',
|
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
|
|
@@ -9,6 +13,7 @@
|
|
|
9
13
|
- [sign](#sign) - to sign an email with DKIM
|
|
10
14
|
- [seal](#seal) - to seal an email with ARC
|
|
11
15
|
- [spf](#spf) - to validate SPF for an IP address and an email address
|
|
16
|
+
- [vmc](#vmc) - to validate BIMI VMC logo files
|
|
12
17
|
- [license](#license) - display licenses for `mailauth` and included modules
|
|
13
18
|
- [DNS cache file](#dns-cache-file)
|
|
14
19
|
|
|
@@ -21,14 +26,6 @@ Download `mailauth` for your platform:
|
|
|
21
26
|
- [Windows](https://github.com/postalsys/mailauth/releases/latest/download/mailauth.exe)
|
|
22
27
|
- Or install from the NPM registry: `npm install -g mailauth`
|
|
23
28
|
|
|
24
|
-
> **NB!** Downloadable files are quite large because these are packaged Node.js applications
|
|
25
|
-
|
|
26
|
-
Alternatively you can install `mailauth` from [npm](https://npmjs.com/package/mailauth).
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
npm install -g mailauth
|
|
30
|
-
```
|
|
31
|
-
|
|
32
29
|
## Help
|
|
33
30
|
|
|
34
31
|
```
|
|
@@ -208,6 +205,119 @@ DNS query for A mail.wildduck.email: ["217.146.76.20"]
|
|
|
208
205
|
...
|
|
209
206
|
```
|
|
210
207
|
|
|
208
|
+
### vmc
|
|
209
|
+
|
|
210
|
+
`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.
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
$ mailauth vmc [options]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Where
|
|
217
|
+
|
|
218
|
+
- **options** are option flags and arguments
|
|
219
|
+
|
|
220
|
+
**Options**
|
|
221
|
+
|
|
222
|
+
- `--authority <url>` or `-a <url>` is the URL for the VMC resource
|
|
223
|
+
- `--authorityPath <path>` or `-p <path>` is the cached file for the authority URL to avoid network requests
|
|
224
|
+
- `--domain <domain>` or `-d <domain>` is the sender domain to compare the certificate against
|
|
225
|
+
|
|
226
|
+
**Example**
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
$ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem -d cnn.com
|
|
230
|
+
{
|
|
231
|
+
"url": "https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.pem",
|
|
232
|
+
"success": true,
|
|
233
|
+
"domainVerified": true,
|
|
234
|
+
"vmc": {
|
|
235
|
+
"mediaType": "image/svg+xml",
|
|
236
|
+
"hashAlgo": "sha1",
|
|
237
|
+
"hashValue": "ea8c81da633c66a16262134a78576cdf067638e9",
|
|
238
|
+
"logoFile": "<2300B base64 encoded file>",
|
|
239
|
+
"validHash": true,
|
|
240
|
+
"certificate": {
|
|
241
|
+
"subject": {
|
|
242
|
+
"businessCategory": "Private Organization",
|
|
243
|
+
"jurisdictionCountryName": "US",
|
|
244
|
+
"jurisdictionStateOrProvinceName": "Delaware",
|
|
245
|
+
"serialNumber": "2976730",
|
|
246
|
+
"countryName": "US",
|
|
247
|
+
"stateOrProvinceName": "Georgia",
|
|
248
|
+
"localityName": "Atlanta",
|
|
249
|
+
"street": "190 Marietta St NW",
|
|
250
|
+
"organizationName": "Cable News Network, Inc.",
|
|
251
|
+
"commonName": "Cable News Network, Inc.",
|
|
252
|
+
"trademarkCountryOrRegionName": "US",
|
|
253
|
+
"trademarkRegistration": "5817930"
|
|
254
|
+
},
|
|
255
|
+
"subjectAltName": [
|
|
256
|
+
"cnn.com"
|
|
257
|
+
],
|
|
258
|
+
"fingerprint": "17:B3:94:97:E6:6B:C8:6B:33:B8:0A:D2:F0:79:6B:08:A2:A6:84:BD",
|
|
259
|
+
"serialNumber": "0821B8FE0A9CBC3BAC10DA08C088EEF4",
|
|
260
|
+
"validFrom": "2021-08-12T00:00:00.000Z",
|
|
261
|
+
"validTo": "2022-08-12T23:59:59.000Z",
|
|
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 logo contents are not returned.
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
$ mailauth vmc -p /path/to/random/cert-bundle.pem
|
|
276
|
+
{
|
|
277
|
+
"success": false,
|
|
278
|
+
"error": {
|
|
279
|
+
"message": "Self signed certificate in certificate chain",
|
|
280
|
+
"details": {
|
|
281
|
+
"certificate": {
|
|
282
|
+
"subject": {
|
|
283
|
+
"commonName": "postal.vmc.local",
|
|
284
|
+
"organizationName": "Postal Systems OU.",
|
|
285
|
+
"countryName": "EE"
|
|
286
|
+
},
|
|
287
|
+
"subjectAltName": [],
|
|
288
|
+
"fingerprint": "CC:49:83:ED:3F:6B:77:45:5B:A5:3B:9E:EC:99:0E:A1:EF:D7:FF:97",
|
|
289
|
+
"serialNumber": "B61FBFBA917B15D9",
|
|
290
|
+
"validFrom": "2022-07-09T06:13:33.000Z",
|
|
291
|
+
"validTo": "2023-07-09T06:13:33.000Z",
|
|
292
|
+
"issuer": {
|
|
293
|
+
"commonName": "postal.vmc.local",
|
|
294
|
+
"organizationName": "Postal Systems OU.",
|
|
295
|
+
"countryName": "EE"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
"code": "SELF_SIGNED_CERT_IN_CHAIN"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
The embedded SVG file is also validated.
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
$ mailauth vmc -p /path/to/vmc-with-invalid-svg.pem
|
|
308
|
+
{
|
|
309
|
+
"success": false,
|
|
310
|
+
"error": {
|
|
311
|
+
"message": "VMC logo SVG validation failed",
|
|
312
|
+
"details": {
|
|
313
|
+
"message": "Not a Tiny PS profile",
|
|
314
|
+
"code": "INVALID_BASE_PROFILE"
|
|
315
|
+
},
|
|
316
|
+
"code": "SVG_VALIDATION_FAILED"
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
211
321
|
### license
|
|
212
322
|
|
|
213
323
|
Display licenses for `mailauth` and included modules.
|
package/lib/bimi/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const dns = require('dns');
|
|
4
|
-
const { formatAuthHeaderRow, parseDkimHeaders } = require('../tools');
|
|
5
|
+
const { formatAuthHeaderRow, parseDkimHeaders, formatDomain, getAlignment } = 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
|
+
const { validateSvg } = require('./validate-svg');
|
|
16
|
+
|
|
10
17
|
const lookup = async data => {
|
|
11
18
|
let { dmarc, headers, resolver } = data;
|
|
12
19
|
let headerRows = (headers && headers.parsed) || [];
|
|
@@ -161,4 +168,256 @@ const lookup = async data => {
|
|
|
161
168
|
return response;
|
|
162
169
|
};
|
|
163
170
|
|
|
164
|
-
|
|
171
|
+
const downloadPromise = (url, cachedFile) => {
|
|
172
|
+
if (cachedFile) {
|
|
173
|
+
return cachedFile;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!url) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const parsedUrl = new URL(url);
|
|
181
|
+
|
|
182
|
+
const options = {
|
|
183
|
+
protocol: parsedUrl.protocol,
|
|
184
|
+
host: parsedUrl.host,
|
|
185
|
+
headers: {
|
|
186
|
+
host: parsedUrl.host,
|
|
187
|
+
'User-Agent': `mailauth/${packageData.version} (+${packageData.homepage}`
|
|
188
|
+
},
|
|
189
|
+
servername: parsedUrl.hostname,
|
|
190
|
+
port: 443,
|
|
191
|
+
path: parsedUrl.pathname,
|
|
192
|
+
method: 'GET',
|
|
193
|
+
rejectUnauthorized: true
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
let protoHandler;
|
|
198
|
+
switch (parsedUrl.protocol) {
|
|
199
|
+
case 'https:':
|
|
200
|
+
protoHandler = https;
|
|
201
|
+
break;
|
|
202
|
+
case 'http:':
|
|
203
|
+
protoHandler = http;
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
reject(new Error(`Unknown protocol ${parsedUrl.protocol}`));
|
|
207
|
+
}
|
|
208
|
+
const req = protoHandler.request(options, res => {
|
|
209
|
+
let chunks = [],
|
|
210
|
+
chunklen = 0;
|
|
211
|
+
res.on('readable', () => {
|
|
212
|
+
let chunk;
|
|
213
|
+
while ((chunk = res.read()) !== null) {
|
|
214
|
+
chunks.push(chunk);
|
|
215
|
+
chunklen += chunk.length;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
res.on('end', () => {
|
|
219
|
+
let data = Buffer.concat(chunks, chunklen);
|
|
220
|
+
if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
|
|
221
|
+
let err = new Error(`Invalid response code ${res.statusCode || '-'}`);
|
|
222
|
+
err.code = 'http_status_' + (res.statusCode || 'na');
|
|
223
|
+
if (res.headers.location && res.statusCode >= 300 && res.statusCode < 400) {
|
|
224
|
+
err.redirect = {
|
|
225
|
+
code: res.statusCode,
|
|
226
|
+
location: res.headers.location
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return reject(err);
|
|
230
|
+
}
|
|
231
|
+
resolve(data);
|
|
232
|
+
});
|
|
233
|
+
res.on('error', err => reject(err));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
req.on('error', err => {
|
|
237
|
+
reject(err);
|
|
238
|
+
});
|
|
239
|
+
req.end();
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const validateVMC = async bimiData => {
|
|
244
|
+
if (!bimiData) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let selector = bimiData?.status?.header?.selector;
|
|
249
|
+
let d = bimiData?.status?.header?.d;
|
|
250
|
+
|
|
251
|
+
let promises = [];
|
|
252
|
+
|
|
253
|
+
promises.push(downloadPromise(bimiData.location, bimiData.locationPath));
|
|
254
|
+
promises.push(downloadPromise(bimiData.authority, bimiData.authorityPath));
|
|
255
|
+
|
|
256
|
+
if (!promises.length) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let [{ reason: locationError, value: locationValue, status: locationStatus }, { reason: authorityError, value: authorityValue, status: authorityStatus }] =
|
|
261
|
+
await Promise.allSettled(promises);
|
|
262
|
+
|
|
263
|
+
let result = {};
|
|
264
|
+
if (locationValue || locationError) {
|
|
265
|
+
result.location = {
|
|
266
|
+
url: bimiData.location,
|
|
267
|
+
success: locationStatus === 'fulfilled'
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (locationError) {
|
|
271
|
+
let err = locationError;
|
|
272
|
+
result.location.error = { message: err.message };
|
|
273
|
+
if (err.redirect) {
|
|
274
|
+
result.location.error.redirect = err.redirect;
|
|
275
|
+
}
|
|
276
|
+
if (err.code) {
|
|
277
|
+
result.location.error.code = err.code;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (result.location.success) {
|
|
282
|
+
result.location.logoFile = locationValue.toString('base64');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (authorityValue || authorityError) {
|
|
287
|
+
result.authority = {
|
|
288
|
+
url: bimiData.authority,
|
|
289
|
+
success: authorityStatus === 'fulfilled'
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
if (authorityError) {
|
|
293
|
+
let err = authorityError;
|
|
294
|
+
result.authority.error = { message: err.message };
|
|
295
|
+
if (err.redirect) {
|
|
296
|
+
result.authority.error.redirect = err.redirect;
|
|
297
|
+
}
|
|
298
|
+
if (err.code) {
|
|
299
|
+
result.authority.error.code = err.code;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (authorityValue) {
|
|
304
|
+
try {
|
|
305
|
+
let vmcData = await vmc(authorityValue);
|
|
306
|
+
|
|
307
|
+
if (!vmcData.logoFile) {
|
|
308
|
+
let error = new Error('VMC does not contain a log file');
|
|
309
|
+
error.code = 'MISSING_VMC_LOGO';
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (vmcData?.mediaType?.toLowerCase() !== 'image/svg+xml') {
|
|
314
|
+
let error = new Error('Invalid media type for the logo file');
|
|
315
|
+
error.details = {
|
|
316
|
+
mediaType: vmcData.mediaType
|
|
317
|
+
};
|
|
318
|
+
error.code = 'INVALID_MEDIATYPE';
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!vmcData.validHash) {
|
|
323
|
+
let error = new Error('VMC hash does not match logo file');
|
|
324
|
+
error.details = {
|
|
325
|
+
hashAlgo: vmcData.hashAlgo,
|
|
326
|
+
hashValue: vmcData.hashValue,
|
|
327
|
+
logoFile: vmcData.logoFile
|
|
328
|
+
};
|
|
329
|
+
error.code = 'INVALID_LOGO_HASH';
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// throws on invalid logo file
|
|
334
|
+
try {
|
|
335
|
+
validateSvg(Buffer.from(vmcData.logoFile, 'base64'));
|
|
336
|
+
} catch (err) {
|
|
337
|
+
let error = new Error('VMC logo SVG validation failed');
|
|
338
|
+
error.details = Object.assign(
|
|
339
|
+
{
|
|
340
|
+
message: err.message
|
|
341
|
+
},
|
|
342
|
+
error.details || {},
|
|
343
|
+
err.code ? { code: err.code } : {}
|
|
344
|
+
);
|
|
345
|
+
error.code = 'SVG_VALIDATION_FAILED';
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (d) {
|
|
350
|
+
// validate domain
|
|
351
|
+
let selectorSet = [];
|
|
352
|
+
let domainSet = [];
|
|
353
|
+
vmcData?.certificate?.subjectAltName?.map(formatDomain)?.forEach(domain => {
|
|
354
|
+
if (/\b_bimi\./.test(domain)) {
|
|
355
|
+
selectorSet.push(domain);
|
|
356
|
+
} else {
|
|
357
|
+
domainSet.push(domain);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
let domainVerified = false;
|
|
362
|
+
|
|
363
|
+
if (selector && selectorSet.includes(formatDomain(`${selector}._bimi.${d}`))) {
|
|
364
|
+
domainVerified = true;
|
|
365
|
+
} else {
|
|
366
|
+
let alignedDomain = getAlignment(d, domainSet, false);
|
|
367
|
+
if (alignedDomain) {
|
|
368
|
+
domainVerified = true;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!domainVerified) {
|
|
373
|
+
let error = new Error('Domain can not be verified');
|
|
374
|
+
error.details = {
|
|
375
|
+
subjectAltName: vmcData?.certificate?.subjectAltName,
|
|
376
|
+
selector,
|
|
377
|
+
d
|
|
378
|
+
};
|
|
379
|
+
error.code = 'VMC_DOMAIN_MISMATCH';
|
|
380
|
+
throw error;
|
|
381
|
+
} else {
|
|
382
|
+
result.authority.domainVerified = true;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
result.authority.vmc = vmcData;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
result.authority.success = false;
|
|
389
|
+
result.authority.error = { message: err.message };
|
|
390
|
+
if (err.details) {
|
|
391
|
+
result.authority.error.details = err.details;
|
|
392
|
+
}
|
|
393
|
+
if (err.code) {
|
|
394
|
+
result.authority.error.code = err.code;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (result.location && result.location.success && result.authority.success) {
|
|
400
|
+
try {
|
|
401
|
+
if (result.location.success && result.authority.vmc.hashAlgo && result.authority.vmc.validHash) {
|
|
402
|
+
let hash = crypto.createHash(result.authority.vmc.hashAlgo).update(locationValue).digest('hex');
|
|
403
|
+
result.location.hashAlgo = result.authority.vmc.hashAlgo;
|
|
404
|
+
result.location.hashValue = hash;
|
|
405
|
+
result.authority.hashMatch = hash === result.authority.vmc.hashValue;
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
result.authority.success = false;
|
|
409
|
+
result.authority.error = { message: err.message };
|
|
410
|
+
if (err.details) {
|
|
411
|
+
result.authority.error.details = err.details;
|
|
412
|
+
}
|
|
413
|
+
if (err.code) {
|
|
414
|
+
result.authority.error.code = err.code;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return result;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
module.exports = { bimi: lookup, validateVMC };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { XMLParser } = require('fast-xml-parser');
|
|
4
|
+
|
|
5
|
+
function validateSvg(logo) {
|
|
6
|
+
const parser = new XMLParser({
|
|
7
|
+
ignoreAttributes: false,
|
|
8
|
+
attributeNamePrefix: '@_'
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
let logoObj;
|
|
12
|
+
try {
|
|
13
|
+
logoObj = parser.parse(logo);
|
|
14
|
+
if (!logoObj) {
|
|
15
|
+
throw new Error('Emtpy file');
|
|
16
|
+
}
|
|
17
|
+
} catch (err) {
|
|
18
|
+
let error = new Error('Invalid SVG file');
|
|
19
|
+
error._err = err;
|
|
20
|
+
error.code = 'INVALID_XML_FILE';
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!logoObj.svg) {
|
|
25
|
+
let error = new Error('Invalid SVG file');
|
|
26
|
+
error.code = 'INVALID_SVG_FILE';
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (logoObj.svg['@_baseProfile'] !== 'tiny-ps') {
|
|
31
|
+
let error = new Error('Not a Tiny PS profile');
|
|
32
|
+
error.code = 'INVALID_BASE_PROFILE';
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!logoObj.svg.title) {
|
|
37
|
+
let error = new Error('Logo file is missing title');
|
|
38
|
+
error.code = 'LOGO_MISSING_TITLE';
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if ('@_x' in logoObj.svg || '@_y' in logoObj.svg) {
|
|
43
|
+
let error = new Error('Logo root includes x/y attributes');
|
|
44
|
+
error.code = 'LOGO_INVALID_ROOT_ATTRS';
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let walkElm = (node, name, path) => {
|
|
49
|
+
if (!node) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(node)) {
|
|
53
|
+
for (let entry of node) {
|
|
54
|
+
walkElm(entry, name, path + '.' + name + '[]');
|
|
55
|
+
}
|
|
56
|
+
} else if (typeof node === 'object') {
|
|
57
|
+
if (node['@_xlink:href'] && !/^#/.test(node['@_xlink:href'])) {
|
|
58
|
+
let error = new Error('External reference found from file');
|
|
59
|
+
error.details = {
|
|
60
|
+
element: name,
|
|
61
|
+
link: node['@_xlink:href'],
|
|
62
|
+
path
|
|
63
|
+
};
|
|
64
|
+
error.code = 'LOGO_INCLUDES_REFERENCE';
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let key of Object.keys(node)) {
|
|
69
|
+
if (['script', 'animate', 'animatemotion', 'animatetransform', 'discard', 'set'].includes(key.toLowerCase())) {
|
|
70
|
+
let error = new Error('Unallowed element found from file');
|
|
71
|
+
error.details = {
|
|
72
|
+
element: key,
|
|
73
|
+
path: path + '.' + key
|
|
74
|
+
};
|
|
75
|
+
error.code = 'LOGO_INVALID_ELEMENT';
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Array.isArray(node[key])) {
|
|
80
|
+
for (let entry of node[key]) {
|
|
81
|
+
walkElm(entry, key, path + '.' + key + '[]');
|
|
82
|
+
}
|
|
83
|
+
} else if (node[key] && typeof node[key] === 'object') {
|
|
84
|
+
walkElm(node[key], key, path + '.' + key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
walkElm(logoObj, 'root', '');
|
|
91
|
+
|
|
92
|
+
// all validations passed
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { validateSvg };
|
|
@@ -0,0 +1,25 @@
|
|
|
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.authorityPath) {
|
|
10
|
+
bimiData.authorityPath = await fs.readFile(argv.authorityPath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (argv.authority) {
|
|
14
|
+
bimiData.authority = argv.authority;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (argv.domain) {
|
|
18
|
+
bimiData.status = { header: { d: argv.domain } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = await validateVMC(bimiData);
|
|
22
|
+
process.stdout.write(JSON.stringify(result.authority, false, 2) + '\n');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
module.exports = cmd;
|
package/lib/mailauth.js
CHANGED
|
@@ -4,7 +4,8 @@ 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
|
+
const { validateSvg: validateBimiSvg } = require('./bimi/validate-svg');
|
|
8
9
|
const { parseReceived } = require('./parse-received');
|
|
9
10
|
const { sealMessage } = require('./arc');
|
|
10
11
|
const libmime = require('libmime');
|
|
@@ -180,4 +181,9 @@ const authenticate = async (input, opts) => {
|
|
|
180
181
|
};
|
|
181
182
|
};
|
|
182
183
|
|
|
183
|
-
module.exports = {
|
|
184
|
+
module.exports = {
|
|
185
|
+
authenticate,
|
|
186
|
+
sealMessage,
|
|
187
|
+
validateBimiVmc,
|
|
188
|
+
validateBimiSvg
|
|
189
|
+
};
|
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,
|
|
@@ -533,6 +491,5 @@ module.exports = {
|
|
|
533
491
|
getAlignment,
|
|
534
492
|
|
|
535
493
|
formatRelaxedLine,
|
|
536
|
-
|
|
537
|
-
parseLogoFromX509
|
|
494
|
+
formatDomain
|
|
538
495
|
};
|
package/licenses.txt
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
name
|
|
2
|
-
----
|
|
3
|
-
@
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
name license type link installed version author
|
|
2
|
+
---- ------------ ---- ----------------- ------
|
|
3
|
+
@postalsys/vmc MIT https://registry.npmjs.org/@postalsys/vmc/-/vmc-1.0.4.tgz 1.0.4 Postal Systems OÜ
|
|
4
|
+
fast-xml-parser MIT git+https://github.com/NaturalIntelligence/fast-xml-parser.git 4.0.9 Amit Gupta https://amitkumargupta.work/
|
|
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
|
+
"version": "3.0.2",
|
|
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.
|
|
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",
|
|
@@ -42,21 +42,22 @@
|
|
|
42
42
|
"marked-man": "0.7.0",
|
|
43
43
|
"mbox-reader": "1.1.5",
|
|
44
44
|
"mocha": "10.0.0",
|
|
45
|
-
"pkg": "5.
|
|
45
|
+
"pkg": "5.8.0"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@
|
|
48
|
+
"@postalsys/vmc": "1.0.4",
|
|
49
|
+
"fast-xml-parser": "4.0.9",
|
|
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.
|
|
54
|
-
"psl": "1.
|
|
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": ">=
|
|
60
|
+
"node": ">=16.0.0"
|
|
60
61
|
},
|
|
61
62
|
"bin": {
|
|
62
63
|
"mailauth": "bin/mailauth.js"
|