Skip to content

Commit

Permalink
Merge pull request #24 from postalsys/3.0.2
Browse files Browse the repository at this point in the history
3.0.2
  • Loading branch information
andris9 authored Jul 14, 2022
2 parents 7060c64 + ecb9af4 commit 590a6b1
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 13 deletions.
49 changes: 40 additions & 9 deletions cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,6 @@ $ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.
"logoFile": "<2300B base64 encoded file>",
"validHash": true,
"certificate": {
"subjectAltName": [
"cnn.com"
],
"subject": {
"businessCategory": "Private Organization",
"jurisdictionCountryName": "US",
Expand All @@ -255,8 +252,13 @@ $ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.
"trademarkCountryOrRegionName": "US",
"trademarkRegistration": "5817930"
},
"subjectAltName": [
"cnn.com"
],
"fingerprint": "17:B3:94:97:E6:6B:C8:6B:33:B8:0A:D2:F0:79:6B:08:A2:A6:84:BD",
"serialNumber": "0821B8FE0A9CBC3BAC10DA08C088EEF4",
"validFrom": "2021-08-12T00:00:00.000Z",
"validTo": "2022-08-12T23:59:59.000Z",
"issuer": {
"countryName": "US",
"organizationName": "DigiCert, Inc.",
Expand All @@ -267,7 +269,7 @@ $ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn.
}
```

If the certificate verification fails, then the contents are not returned.
If the certificate verification fails, then the logo contents are not returned.

```
$ mailauth vmc -p /path/to/random/cert-bundle.pem
Expand All @@ -276,17 +278,46 @@ $ mailauth vmc -p /path/to/random/cert-bundle.pem
"error": {
"message": "Self signed certificate in certificate chain",
"details": {
"subject": "CN=postal.vmc.local\nO=Postal Systems OU.\nC=EE",
"fingerprint": "CC:49:83:ED:3F:6B:77:45:5B:A5:3B:9E:EC:99:0E:A1:EF:D7:FF:97",
"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",
"validFrom": "2022-07-10T06:28:06.482Z",
"validTo": "2022-07-10T06:28:06.482Z"
"certificate": {
"subject": {
"commonName": "postal.vmc.local",
"organizationName": "Postal Systems OU.",
"countryName": "EE"
},
"subjectAltName": [],
"fingerprint": "CC:49:83:ED:3F:6B:77:45:5B:A5:3B:9E:EC:99:0E:A1:EF:D7:FF:97",
"serialNumber": "B61FBFBA917B15D9",
"validFrom": "2022-07-09T06:13:33.000Z",
"validTo": "2023-07-09T06:13:33.000Z",
"issuer": {
"commonName": "postal.vmc.local",
"organizationName": "Postal Systems OU.",
"countryName": "EE"
}
}
},
"code": "SELF_SIGNED_CERT_IN_CHAIN"
}
}
```

The embedded SVG file is also validated.

```
$ mailauth vmc -p /path/to/vmc-with-invalid-svg.pem
{
"success": false,
"error": {
"message": "VMC logo SVG validation failed",
"details": {
"message": "Not a Tiny PS profile",
"code": "INVALID_BASE_PROFILE"
},
"code": "SVG_VALIDATION_FAILED"
}
}
```

### license

Display licenses for `mailauth` and included modules.
Expand Down
34 changes: 34 additions & 0 deletions lib/bimi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const httpsSchema = Joi.string().uri({
const https = require('https');
const http = require('http');
const { vmc } = require('@postalsys/vmc');
const { validateSvg } = require('./validate-svg');

const lookup = async data => {
let { dmarc, headers, resolver } = data;
Expand Down Expand Up @@ -303,6 +304,12 @@ const validateVMC = async bimiData => {
try {
let vmcData = await vmc(authorityValue);

if (!vmcData.logoFile) {
let error = new Error('VMC does not contain a log file');
error.code = 'MISSING_VMC_LOGO';
throw error;
}

if (vmcData?.mediaType?.toLowerCase() !== 'image/svg+xml') {
let error = new Error('Invalid media type for the logo file');
error.details = {
Expand All @@ -312,6 +319,33 @@ const validateVMC = async bimiData => {
throw error;
}

if (!vmcData.validHash) {
let error = new Error('VMC hash does not match logo file');
error.details = {
hashAlgo: vmcData.hashAlgo,
hashValue: vmcData.hashValue,
logoFile: vmcData.logoFile
};
error.code = 'INVALID_LOGO_HASH';
throw error;
}

// throws on invalid logo file
try {
validateSvg(Buffer.from(vmcData.logoFile, 'base64'));
} catch (err) {
let error = new Error('VMC logo SVG validation failed');
error.details = Object.assign(
{
message: err.message
},
error.details || {},
err.code ? { code: err.code } : {}
);
error.code = 'SVG_VALIDATION_FAILED';
throw error;
}

if (d) {
// validate domain
let selectorSet = [];
Expand Down
96 changes: 96 additions & 0 deletions lib/bimi/validate-svg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use strict';

const { XMLParser } = require('fast-xml-parser');

function validateSvg(logo) {
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '@_'
});

let logoObj;
try {
logoObj = parser.parse(logo);
if (!logoObj) {
throw new Error('Emtpy file');
}
} catch (err) {
let error = new Error('Invalid SVG file');
error._err = err;
error.code = 'INVALID_XML_FILE';
throw error;
}

if (!logoObj.svg) {
let error = new Error('Invalid SVG file');
error.code = 'INVALID_SVG_FILE';
throw error;
}

if (logoObj.svg['@_baseProfile'] !== 'tiny-ps') {
let error = new Error('Not a Tiny PS profile');
error.code = 'INVALID_BASE_PROFILE';
throw error;
}

if (!logoObj.svg.title) {
let error = new Error('Logo file is missing title');
error.code = 'LOGO_MISSING_TITLE';
throw error;
}

if ('@_x' in logoObj.svg || '@_y' in logoObj.svg) {
let error = new Error('Logo root includes x/y attributes');
error.code = 'LOGO_INVALID_ROOT_ATTRS';
throw error;
}

let walkElm = (node, name, path) => {
if (!node) {
return;
}
if (Array.isArray(node)) {
for (let entry of node) {
walkElm(entry, name, path + '.' + name + '[]');
}
} else if (typeof node === 'object') {
if (node['@_xlink:href'] && !/^#/.test(node['@_xlink:href'])) {
let error = new Error('External reference found from file');
error.details = {
element: name,
link: node['@_xlink:href'],
path
};
error.code = 'LOGO_INCLUDES_REFERENCE';
throw error;
}

for (let key of Object.keys(node)) {
if (['script', 'animate', 'animatemotion', 'animatetransform', 'discard', 'set'].includes(key.toLowerCase())) {
let error = new Error('Unallowed element found from file');
error.details = {
element: key,
path: path + '.' + key
};
error.code = 'LOGO_INVALID_ELEMENT';
throw error;
}

if (Array.isArray(node[key])) {
for (let entry of node[key]) {
walkElm(entry, key, path + '.' + key + '[]');
}
} else if (node[key] && typeof node[key] === 'object') {
walkElm(node[key], key, path + '.' + key);
}
}
}
};

walkElm(logoObj, 'root', '');

// all validations passed
return true;
}

module.exports = { validateSvg };
8 changes: 7 additions & 1 deletion lib/mailauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { spf } = require('./spf');
const { dmarc } = require('./dmarc');
const { arc, createSeal } = require('./arc');
const { bimi, validateVMC: validateBimiVmc } = require('./bimi');
const { validateSvg: validateBimiSvg } = require('./bimi/validate-svg');
const { parseReceived } = require('./parse-received');
const { sealMessage } = require('./arc');
const libmime = require('libmime');
Expand Down Expand Up @@ -180,4 +181,9 @@ const authenticate = async (input, opts) => {
};
};

module.exports = { authenticate, sealMessage, validateBimiVmc };
module.exports = {
authenticate,
sealMessage,
validateBimiVmc,
validateBimiSvg
};
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mailauth",
"version": "3.0.1",
"version": "3.0.2",
"description": "Email authentication library for Node.js",
"main": "lib/mailauth.js",
"scripts": {
Expand Down Expand Up @@ -42,10 +42,11 @@
"marked-man": "0.7.0",
"mbox-reader": "1.1.5",
"mocha": "10.0.0",
"pkg": "5.7.0"
"pkg": "5.8.0"
},
"dependencies": {
"@postalsys/vmc": "1.0.3",
"@postalsys/vmc": "1.0.4",
"fast-xml-parser": "4.0.9",
"ipaddr.js": "2.0.1",
"joi": "17.6.0",
"libmime": "5.1.0",
Expand Down

0 comments on commit 590a6b1

Please sign in to comment.