New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

mailauth

Package Overview
Dependencies
Maintainers
1
Versions
80
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mailauth - npm Package Compare versions

Comparing version 1.0.4 to 1.0.5

16

examples/authenticate.js
'use strict';
const { authenticate } = require('../lib/mailauth');
//const { dkimSign } = require('../lib/dkim/sign');
const dns = require('dns');

@@ -14,4 +14,16 @@ const fs = require('fs');

mta: 'mx.ethereal.email',
sender: 'andris@ekiri.ee'
sender: 'andris@ekiri.ee',
// optional. add ARC seal if possible
seal: {
algorithm: 'rsa-sha256',
signingDomain: 'tahvel.info',
selector: 'test.rsa',
privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem')
},
resolver: async (name, rr) => {
console.log('DNS', rr, name);
return await dns.promises.resolve(name, rr);
}
});
console.log(JSON.stringify(res, false, 2));

@@ -18,0 +30,0 @@

@@ -40,2 +40,3 @@ 'use strict';

{
//canonicalization: 'relaxed/relaxed',
algorithm: algo,

@@ -42,0 +43,0 @@ signingDomain: 'tahvel.info',

'use strict';
const { parseDkimHeaders, formatRelaxedLine, getPublicKey, formatAuthHeaderRow } = require('../../lib/tools');
const { parseDkimHeaders, formatRelaxedLine, getPublicKey, formatAuthHeaderRow, formatSignatureHeaderLine } = require('../../lib/tools');
const crypto = require('crypto');
const { DkimSigner } = require('../dkim/dkim-signer');

@@ -67,2 +68,81 @@ const verifyAS = async (chain, opts) => {

const signAS = async (chain, entry, signatureData) => {
let { instance, algorithm, selector, signingDomain, bodyHash, cv, signTime, privateKey } = signatureData;
const signAlgo = algorithm?.split('-').shift();
signTime = signTime || new Date();
let chunks = [];
for (let i = 0; i < chain.length; i++) {
let link = chain[i];
chunks.push(formatRelaxedLine(link['arc-authentication-results'].original, '\r\n'));
chunks.push(formatRelaxedLine(link['arc-message-signature'].original, '\r\n'));
chunks.push(formatRelaxedLine(link['arc-seal'].original, '\r\n'));
}
chunks.push(formatRelaxedLine(entry['arc-authentication-results'], '\r\n'));
chunks.push(formatRelaxedLine(entry['arc-message-signature'], '\r\n'));
let headerOpts = {
i: instance,
a: algorithm,
s: selector,
d: signingDomain,
cv,
bh: bodyHash
};
if (signTime) {
if (typeof signTime === 'string' || typeof signTime === 'number') {
signTime = new Date(signTime);
}
if (Object.prototype.toString.call(signTime) === '[object Date]' && signTime.toString() !== 'Invalid Date') {
// we need a unix timestamp value
signTime = Math.round(signTime.getTime() / 1000);
headerOpts.t = signTime;
}
}
let canonSignatureHeaderLine = formatSignatureHeaderLine(
'AS',
Object.assign(
{
// make sure that b= always has a value, otherwise folding would be different
b: 'a'.repeat(73)
},
headerOpts
),
true
);
chunks.push(
Buffer.from(
formatRelaxedLine(canonSignatureHeaderLine)
.toString('binary')
// remove value from b= key
.replace(/([;:\s]+b=)[^;]+/, '$1'),
'binary'
)
);
let canonicalizedHeader = Buffer.concat(chunks);
let signature = crypto
.sign(
// use `null` as algorithm to detect it from the key file
signAlgo === 'rsa' ? algorithm : null,
canonicalizedHeader,
privateKey
)
.toString('base64');
headerOpts.b = signature;
return formatSignatureHeaderLine('AS', headerOpts, true);
};
const verifyASChain = async (data, opts) => {

@@ -83,3 +163,3 @@ if (!data?.chain?.length) {

// throws if validation fails
await verifyAS(data.chain.slice(0, i + 1));
await verifyAS(data.chain.slice(0, i + 1), opts);
}

@@ -105,3 +185,3 @@

// value for this header is already set
let err = new Error(`Multiple "${row.key}" values for the same instance "${instance}"`);
let err = new Error(`i=${instance} error=multiple ${row.key}`);
err.code = 'multiple_arc_keys';

@@ -122,3 +202,3 @@ throw err;

if (arcChain.length > 50) {
let err = new Error(`Too many ARC instances found: "${arcChain.length}"`);
let err = new Error(`chain-length=${arcChain.length}`);
err.code = 'invalid_arc_count';

@@ -133,3 +213,3 @@ throw err;

// not a complete sequence
let err = new Error(`Invalid instance number "${arcInstance.i}". Expecting "${i + 1}"`);
let err = new Error(`i=${arcInstance.i} expected=${i + 1}`);
err.code = 'invalid_arc_instance';

@@ -142,3 +222,3 @@ throw err;

// missing required header
let err = new Error(`Missing header ${headerKey} from ARC instance ${arcInstance.i}`);
let err = new Error(`i=${arcInstance.i} error=no ${headerKey}`);
err.code = 'missing_arc_header';

@@ -150,3 +230,3 @@ throw err;

if (i === 0 && arcInstance['arc-seal']?.parsed?.cv?.value?.toLowerCase() !== 'none') {
let err = new Error(`Unexpected cv value for first ARC instance: "${arcInstance['arc-seal']?.parsed?.cv?.value}". Expecting "none"`);
let err = new Error(`i=1 cv="${arcInstance['arc-seal']?.parsed?.cv?.value}`);
err.code = 'invalid_cv_value';

@@ -157,3 +237,3 @@ throw err;

if (i > 0 && arcInstance['arc-seal']?.parsed?.cv?.value?.toLowerCase() !== 'pass') {
let err = new Error(`Unexpected cv value ARC instance ${arcInstance.i}: "${arcInstance['arc-seal']?.parsed?.cv?.value}". Expecting "pass"`);
let err = new Error(`i=${arcInstance.i} cv=${arcInstance['arc-seal']?.parsed?.cv?.value}`);
err.code = 'invalid_cv_value';

@@ -164,3 +244,3 @@ throw err;

if (arcInstance['arc-seal']?.parsed?.h) {
let err = new Error(`Unexpected h value found from ARC-Seal i=${arcInstance.i}: "${arcInstance['arc-seal']?.parsed?.h?.value}"`);
let err = new Error(`i=${arcInstance.i} error=unexpected h`);
err.code = 'unexpected_h_value';

@@ -198,3 +278,3 @@ throw err;

result.authenticationResults.host = result.authenticationResults.value;
result.authenticationResults.mta = result.authenticationResults.value;
delete result.authenticationResults.value;

@@ -304,2 +384,44 @@

module.exports = { getARChain, arc };
const createSeal = async data => {
const { headers, arc, seal } = data;
// Step 1. Calculate ARC-Message-Signature
let dkimSigner = new DkimSigner({
headers,
bodyHash: seal.bodyHash,
arc: {
instance: seal.i,
algorithm: seal.algorithm,
signingDomain: seal.signingDomain,
selector: seal.selector,
privateKey: seal.privateKey
}
});
// this gives us dkimSigner.arc.messageSignature
await dkimSigner.finalize();
// Step 2. Calculate ARC-Seal
const arcSeal = await signAS(
arc.chain,
{
'arc-authentication-results': seal?.authResults,
'arc-message-signature': dkimSigner?.arc?.messageSignature
},
{
instance: seal.i,
algorithm: seal.algorithm,
signingDomain: seal.signingDomain,
selector: seal.selector,
bodyHash: seal.bodyHash,
cv: seal.cv,
signTime: new Date(),
privateKey: seal.privateKey
}
);
return {
headers: [arcSeal, dkimSigner?.arc?.messageSignature, seal?.authResults].map(v => v)
};
};
module.exports = { getARChain, arc, createSeal };

89

lib/dkim/dkim-signer.js

@@ -13,8 +13,6 @@ 'use strict';

let { canonicalization, signTime, headerList, signatureData, arc } = options || {};
let { canonicalization, algorithm, signTime, headerList, signatureData, arc, bodyHash, headers } = options || {};
this.algorithm = algorithm || false;
this.canonicalization = canonicalization || 'relaxed/relaxed';
this.headerCanon = this.canonicalization.split('/').shift().toLowerCase().trim();
// if body canonicalization is not set, then defaults to 'simple'
this.bodyCanon = (this.canonicalization.split('/')[1] || 'simple').toLowerCase().trim();

@@ -35,3 +33,2 @@ this.errors = [];

if (this.arc && this.arc.instance && this.arc.signingDomain && this.arc.selector && this.arc.privateKey) {
this.arc.set = this.arc.set || {};
this.signatureData.push({

@@ -42,3 +39,4 @@ type: 'ARC',

privateKey: this.arc.privateKey,
algorithm: 'rsa-sha256', // fixed for now
canonicalization: 'relaxed/relaxed',
algorithm: 'rsa-sha256', // fixed for now, throws if non-rsa key is used
instance: this.arc.instance

@@ -49,5 +47,25 @@ });

this.bodyHashes = new Map();
// precalculated hash and headers
this.bodyHash = bodyHash || null;
this.headers = headers;
this.setupHashes();
}
getCanonicalization(signatureData) {
let canonicalization = signatureData?.canonicalization || this.canonicalization;
let headerCanon = canonicalization.split('/').shift().toLowerCase().trim();
let bodyCanon = (canonicalization.split('/')[1] || 'simple').toLowerCase().trim();
return { canonicalization, headerCanon, bodyCanon };
}
getAlgorithm(signatureData) {
let algorithm = (signatureData?.algorithm || this.algorithm || '').toLowerCase().trim();
let signAlgo = algorithm.split('-').shift().toLowerCase().trim() || false; // default is derived from key
let hashAlgo = algorithm.split('-').pop().toLowerCase().trim() || 'sha256';
return { algorithm, signAlgo, hashAlgo };
}
setupHashes() {

@@ -59,7 +77,14 @@ for (let signatureData of this.signatureData) {

let algorithm = (signatureData.algorithm || '').toLowerCase().trim();
let hashAlgo = algorithm.split('-').pop().toLowerCase().trim() || 'sha256';
let { hashAlgo } = this.getAlgorithm(signatureData);
let { bodyCanon } = this.getCanonicalization(signatureData);
if (!this.bodyHashes.has(hashAlgo)) {
this.bodyHashes.set(hashAlgo, { hasher: null, hash: null });
let hashKey = `${bodyCanon}:${hashAlgo}`;
if (!this.bodyHashes.has(hashKey)) {
this.bodyHashes.set(hashKey, {
bodyCanon,
hashAlgo,
hasher: null,
hash: this.bodyHash
});
}

@@ -75,10 +100,10 @@ }

let [signing, hashing] = algorithm.split('-');
let [signAlgo, hashAlgo] = algorithm.split('-');
if (!['rsa', 'ed25519'].includes(signing)) {
throw new Error('Unknown signing algorithm: ' + signing);
if (!['rsa', 'ed25519'].includes(signAlgo)) {
throw new Error('Unknown signing algorithm: ' + signAlgo);
}
if (!['sha256', 'sha1'].includes(hashing)) {
throw new Error('Unknown hashing algorithm: ' + hashing);
if (!['sha256', 'sha1'].includes(hashAlgo)) {
throw new Error('Unknown hashing algorithm: ' + hashAlgo);
}

@@ -94,4 +119,5 @@ } catch (err) {

for (let hashAlgo of this.bodyHashes.keys()) {
this.bodyHashes.get(hashAlgo).hasher = dkimBody(this.bodyCanon, hashAlgo);
for (let hashKey of this.bodyHashes.keys()) {
let [bodyCanon, hashAlgo] = hashKey.split(':');
this.bodyHashes.get(hashKey).hasher = dkimBody(bodyCanon, hashAlgo);
}

@@ -101,5 +127,5 @@ }

async nextChunk(chunk) {
for (let hashAlgo of this.bodyHashes.keys()) {
if (this.bodyHashes.get(hashAlgo).hasher) {
this.bodyHashes.get(hashAlgo).hasher.update(chunk);
for (let hashKey of this.bodyHashes.keys()) {
if (this.bodyHashes.get(hashKey).hasher) {
this.bodyHashes.get(hashKey).hasher.update(chunk);
}

@@ -114,8 +140,12 @@ }

for (let hashAlgo of this.bodyHashes.keys()) {
if (this.bodyHashes.get(hashAlgo).hasher) {
this.bodyHashes.get(hashAlgo).hash = this.bodyHashes.get(hashAlgo).hasher.digest('base64');
for (let hashKey of this.bodyHashes.keys()) {
if (this.bodyHashes.get(hashKey).hasher) {
this.bodyHashes.get(hashKey).hash = this.bodyHashes.get(hashKey).hasher.digest('base64');
}
}
return this.finalize();
}
async finalize() {
for (let signatureData of this.signatureData || []) {

@@ -142,6 +172,7 @@ if (!signatureData.privateKey) {

let algorithm = (signatureData.algorithm || '').toLowerCase().trim();
let signAlgo = algorithm.split('-').shift().toLowerCase().trim() || null;
let hashAlgo = algorithm.split('-').pop().toLowerCase().trim() || 'sha256';
let { algorithm, signAlgo, hashAlgo } = this.getAlgorithm(signatureData);
let { bodyCanon } = this.getCanonicalization(signatureData);
let hashKey = `${bodyCanon}:${hashAlgo}`;
try {

@@ -195,5 +226,5 @@ let keyType = crypto.createPrivateKey({ key: signatureData.privateKey, format: 'pem' }).asymmetricKeyType;

algorithm,
canonicalization: this.canonicalization,
canonicalization: this.getCanonicalization(signatureData).canonicalization,
signTime: this.signTime,
bodyHash: this.bodyHashes.has(hashAlgo) ? this.bodyHashes.get(hashAlgo).hash : null
bodyHash: this.bodyHashes.has(hashKey) ? this.bodyHashes.get(hashKey).hash : null
})

@@ -218,3 +249,3 @@ );

case 'ARC':
this.arc.set['arc-message-signature'] = signatureHeaderLine;
this.arc.messageSignature = signatureHeaderLine;
break;

@@ -221,0 +252,0 @@

@@ -26,3 +26,15 @@ 'use strict';

// ARC verification info
this.arc = { chain: false };
// should we also seal this message using ARC
this.seal = this.options.seal;
if (this.seal) {
// calculate body hash for the seal
let bodyCanon = 'relaxed';
let hashAlgo = 'sha256';
this.sealBodyHashKey = [bodyCanon, hashAlgo].join(':');
this.bodyHashes.set(this.sealBodyHashKey, dkimBody(bodyCanon, hashAlgo, false));
}
}

@@ -147,2 +159,3 @@

// convert bodyHashes from hash objects to base64 strings
for (let [key, bodyHash] of this.bodyHashes.entries()) {

@@ -291,2 +304,6 @@ this.bodyHashes.set(key, bodyHash.digest('base64'));

}
if (this.seal && this.bodyHashes.has(this.sealBodyHashKey) && typeof this.bodyHashes.get(this.sealBodyHashKey) === 'string') {
this.seal.bodyHash = this.bodyHashes.get(this.sealBodyHashKey);
}
}

@@ -293,0 +310,0 @@ }

@@ -17,16 +17,29 @@ 'use strict';

Object.defineProperty(result, 'headers', {
enumerable: false,
configurable: false,
writable: false,
value: dkimVerifier.headers
});
if (dkimVerifier.headers) {
Object.defineProperty(result, 'headers', {
enumerable: false,
configurable: false,
writable: false,
value: dkimVerifier.headers
});
}
Object.defineProperty(result, 'arc', {
enumerable: false,
configurable: false,
writable: false,
value: dkimVerifier.arc
});
if (dkimVerifier.arc) {
Object.defineProperty(result, 'arc', {
enumerable: false,
configurable: false,
writable: false,
value: dkimVerifier.arc
});
}
if (dkimVerifier.seal) {
Object.defineProperty(result, 'seal', {
enumerable: false,
configurable: false,
writable: false,
value: dkimVerifier.seal
});
}
return result;

@@ -33,0 +46,0 @@ };

@@ -6,3 +6,3 @@ 'use strict';

const { dmarc } = require('./dmarc');
const { arc } = require('./arc');
const { arc, createSeal } = require('./arc');
const libmime = require('libmime');

@@ -28,3 +28,4 @@ const os = require('os');

resolver: opts.resolver,
sender: opts.sender
sender: opts.sender,
seal: opts.seal
}),

@@ -39,10 +40,8 @@ spf(opts)

let headers = [];
let arHeader = [];
if (dkimResult && dkimResult.results) {
dkimResult.results.forEach(row => {
arHeader.push(`${libmime.foldLines(row.info, 160)}`);
});
}
dkimResult?.results?.forEach(row => {
arHeader.push(`${libmime.foldLines(row.info, 160)}`);
});
if (spfResult) {

@@ -53,3 +52,3 @@ arHeader.push(libmime.foldLines(spfResult.info, 160));

if (arcResult && arcResult.info) {
if (arcResult?.info) {
arHeader.push(`${libmime.foldLines(arcResult.info, 160)}`);

@@ -74,2 +73,27 @@ }

// seal only messages with a valid ARC chain
if (dkimResult?.seal && ['none', 'pass'].includes(arcResult?.status?.result)) {
let i = arcResult.i + 1;
let seal = Object.assign(
{
i,
cv: arcResult.status.result,
authResults: `ARC-Authentication-Results: i=${i}; ${opts.mta};\r\n ` + arHeader.join(';\r\n ')
},
dkimResult.seal
);
// get ARC sealing headers to prepend to the message
let sealResult = await createSeal(
{
headers: dkimResult.headers,
arc: dkimResult.arc,
seal
},
opts
);
sealResult?.headers?.reverse().forEach(header => headers.unshift(header));
}
return {

@@ -76,0 +100,0 @@ dkim: dkimResult,

{
"name": "mailauth",
"version": "1.0.4",
"version": "1.0.5",
"description": "Email authentication library for Node.js",

@@ -5,0 +5,0 @@ "main": "lib/mailauth.js",

@@ -11,2 +11,4 @@ # mailauth

- [ ] ARC sealing
- [x] Sealing on authentication
- [ ] Sealing after modifications
- [ ] MTA-STS resolver

@@ -62,7 +64,7 @@

Validate DKIM signatures, SPF, DMARC and ARC for an email.
Validate DKIM signatures, SPF, DMARC and ARC for an email. Also can seal a validated message with ARC.
```js
const { authenticate } = require('mailauth');
const { headers } = await authenticate(
const { dkim, spf, arc, dmarc, headers } = await authenticate(
message, // either a String, a Buffer or a Readable Stream

@@ -75,3 +77,14 @@ {

mta: 'mx.ethereal.email', // server processing this message, defaults to os.hostname()
sender: 'andris@ekiri.ee' // MAIL FROM address
sender: 'andris@ekiri.ee', // MAIL FROM address
// Optional ARC seal settings. If this is set then resulting headers include
// a complete ARC header set (unless the message has a failing ARC chain)
seal: {
signingDomain: 'tahvel.info',
selector: 'test.rsa',
privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem')
},
// Optional DNS resolver function (defaults to `dns.promises.resolve`)
resolver: async (name, rr) => await dns.promises.resolve(name, rr)
}

@@ -97,2 +110,4 @@ );

You can see full output (structured data for DKIM, SPF, DMARC and ARC) from [this example](https://gist.github.com/andris9/6514b5e7c59154a5b08636f99052ce37).
## DKIM

@@ -107,7 +122,9 @@

{
// optional canonicalization, default is "relaxed/relaxed"
// this option applies to all signatures, so you can't create multiple signatures
// that use different canonicalization
// Optional default canonicalization, default is "relaxed/relaxed"
canonicalization: 'relaxed/relaxed', // c=
// Optional default signing and hashing algorithm
// Mostly useful when you want to use rsa-sha1, otherwise no need to set
algorithm: 'rsa-sha256',
// optional, default is current time

@@ -125,5 +142,9 @@ signTime: new Date(), // t=

privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem'),
// Optional algorithm, default is derived from the key.
// Mostly useful when you want to use rsa-sha1, otherwise no need to set
algorithm: 'rsa-sha256'
// Overrides whatever was set in parent object
algorithm: 'rsa-sha256',
// Optional signature specifc canonicalization, overrides whatever was set in parent object
canonicalization: 'relaxed/relaxed' // c=
}

@@ -130,0 +151,0 @@ ]

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc