Comparing version 1.0.16 to 1.0.17
@@ -55,3 +55,4 @@ 'use strict'; | ||
try { | ||
publicKey = await getPublicKey('AS', queryDomain, resolver); | ||
let res = await getPublicKey('AS', queryDomain, resolver); | ||
publicKey = res?.publicKey; | ||
} catch (err) { | ||
@@ -58,0 +59,0 @@ err.queryDomain = queryDomain; |
@@ -177,3 +177,3 @@ 'use strict'; | ||
let publicKey; | ||
let publicKey, rr; | ||
let status = { | ||
@@ -206,3 +206,3 @@ result: 'neutral', | ||
try { | ||
publicKey = await getPublicKey( | ||
let res = await getPublicKey( | ||
signatureHeader.type, | ||
@@ -213,2 +213,5 @@ `${signatureHeader.selector}._domainkey.${signatureHeader.signingDomain}`, | ||
publicKey = res?.publicKey; | ||
rr = res?.rr; | ||
try { | ||
@@ -231,2 +234,6 @@ status.result = crypto.verify( | ||
} catch (err) { | ||
if (err.rr) { | ||
rr = err.rr; | ||
} | ||
switch (err.code) { | ||
@@ -281,2 +288,6 @@ case 'ENOTFOUND': | ||
if (rr) { | ||
result.rr = rr; | ||
} | ||
switch (signatureHeader.type) { | ||
@@ -283,0 +294,0 @@ case 'ARC': |
@@ -69,2 +69,3 @@ 'use strict'; | ||
parsed.rr = txt; | ||
parsed.isOrgRecord = isOrgRecord; | ||
@@ -158,3 +159,4 @@ | ||
sp: dmarcRecord.sp || dmarcRecord.p, | ||
pct: dmarcRecord.pct | ||
pct: dmarcRecord.pct, | ||
rr: dmarcRecord.rr | ||
}); | ||
@@ -161,0 +163,0 @@ }; |
@@ -217,2 +217,6 @@ 'use strict'; | ||
if (result.rr) { | ||
response.rr = result.rr; | ||
} | ||
response.status = status; | ||
@@ -219,0 +223,0 @@ response.header = formatHeaders(response); |
@@ -78,2 +78,3 @@ 'use strict'; | ||
let spfRecord; | ||
let spfRr; | ||
@@ -91,2 +92,3 @@ for (let row of responses) { | ||
} | ||
spfRr = row; | ||
spfRecord = parts.slice(1); | ||
@@ -102,251 +104,253 @@ } | ||
// this check is only for passing test suite | ||
for (let i = spfRecord.length - 1; i >= 0; i--) { | ||
let part = spfRecord[i]; | ||
if (/^[^:/]+=/.test(part)) { | ||
//modifier, not mechanism | ||
let getResult = async () => { | ||
// this check is only for passing test suite | ||
for (let i = spfRecord.length - 1; i >= 0; i--) { | ||
let part = spfRecord[i]; | ||
if (/^[^:/]+=/.test(part)) { | ||
//modifier, not mechanism | ||
if (!/^[a-z](a-z0-9-_\.)*/i.test(part)) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `invalid modifier ${part}` }; | ||
throw err; | ||
if (!/^[a-z](a-z0-9-_\.)*/i.test(part)) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `invalid modifier ${part}` }; | ||
throw err; | ||
} | ||
let splitPos = part.indexOf('='); | ||
let modifier = part.substr(0, splitPos).toLowerCase(); | ||
let value = part.substr(splitPos + 1); | ||
value = macro(value, opts) | ||
// remove trailing dot | ||
.replace(/\.$/, ''); | ||
if (!value) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `Empty modifier value for ${modifier}` }; | ||
throw err; | ||
} else if (modifier === 'redirect' && !/^([\x21-\x2D\x2f-\x7e]+\.)+[a-z]+[a-z\-0-9]*$/.test(value)) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `Invalid redirect target ${value}` }; | ||
throw err; | ||
} | ||
spfRecord.splice(i, 1); | ||
spfRecord.push({ modifier, value }); | ||
continue; | ||
} | ||
let splitPos = part.indexOf('='); | ||
let modifier = part.substr(0, splitPos).toLowerCase(); | ||
let value = part.substr(splitPos + 1); | ||
let mechanism = part | ||
.split(/[:/=]/) | ||
.shift() | ||
.toLowerCase() | ||
.replace(/^[?\-~+]/, ''); | ||
value = macro(value, opts) | ||
// remove trailing dot | ||
.replace(/\.$/, ''); | ||
if (!value) { | ||
if (!['all', 'include', 'a', 'mx', 'ip4', 'ip6', 'exists', 'ptr'].includes(mechanism)) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `Empty modifier value for ${modifier}` }; | ||
err.spfResult = { error: 'permerror', text: `Unknown mechanism ${mechanism}` }; | ||
throw err; | ||
} else if (modifier === 'redirect' && !/^([\x21-\x2D\x2f-\x7e]+\.)+[a-z]+[a-z\-0-9]*$/.test(value)) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `Invalid redirect target ${value}` }; | ||
throw err; | ||
} | ||
spfRecord.splice(i, 1); | ||
spfRecord.push({ modifier, value }); | ||
continue; | ||
} | ||
let mechanism = part | ||
.split(/[:/=]/) | ||
.shift() | ||
.toLowerCase() | ||
.replace(/^[?\-~+]/, ''); | ||
if (!['all', 'include', 'a', 'mx', 'ip4', 'ip6', 'exists', 'ptr'].includes(mechanism)) { | ||
if (spfRecord.filter(p => p && p.modifier === 'redirect').length > 1) { | ||
// too many redirects | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `Unknown mechanism ${mechanism}` }; | ||
err.spfResult = { error: 'permerror', text: `more than 1 redirect found` }; | ||
throw err; | ||
} | ||
} | ||
if (spfRecord.filter(p => p && p.modifier === 'redirect').length > 1) { | ||
// too many redirects | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `more than 1 redirect found` }; | ||
throw err; | ||
} | ||
for (let i = 0; i < spfRecord.length; i++) { | ||
let part = spfRecord[i]; | ||
for (let i = 0; i < spfRecord.length; i++) { | ||
let part = spfRecord[i]; | ||
if (typeof part === 'object' && part.modifier) { | ||
let { modifier, value } = part; | ||
if (typeof part === 'object' && part.modifier) { | ||
let { modifier, value } = part; | ||
switch (modifier) { | ||
case 'redirect': | ||
{ | ||
if (spfRecord.some(p => /^[?\-~+]?all$/i.test(p))) { | ||
// ignore redirect if "all" condition is set | ||
continue; | ||
} | ||
switch (modifier) { | ||
case 'redirect': | ||
{ | ||
if (spfRecord.some(p => /^[?\-~+]?all$/i.test(p))) { | ||
// ignore redirect if "all" condition is set | ||
continue; | ||
} | ||
try { | ||
let subResult = await spfVerify(value, opts); | ||
if (subResult) { | ||
return subResult; | ||
} | ||
} catch (err) { | ||
// kind of ignore | ||
if (err.spfResult) { | ||
if (err.spfResult.error === 'none') { | ||
err.spfResult.error = 'permerror'; | ||
try { | ||
let subResult = await spfVerify(value, opts); | ||
if (subResult) { | ||
return subResult; | ||
} | ||
throw err; | ||
} catch (err) { | ||
// kind of ignore | ||
if (err.spfResult) { | ||
if (err.spfResult.error === 'none') { | ||
err.spfResult.error = 'permerror'; | ||
} | ||
throw err; | ||
} | ||
} | ||
} | ||
} | ||
break; | ||
break; | ||
case 'exp': | ||
default: | ||
// do nothing | ||
case 'exp': | ||
default: | ||
// do nothing | ||
} | ||
continue; | ||
} | ||
continue; | ||
} | ||
let key = ''; | ||
let val = ''; | ||
let qualifier = '+'; // default is pass | ||
let key = ''; | ||
let val = ''; | ||
let qualifier = '+'; // default is pass | ||
let splitterPos = part.indexOf(':'); | ||
if (splitterPos === part.length - 1) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `unexpected empty value` }; | ||
throw err; | ||
} | ||
if (splitterPos >= 0) { | ||
key = part.substr(0, splitterPos); | ||
val = part.substr(splitterPos + 1); | ||
} else { | ||
let splitterPos = part.indexOf('/'); | ||
let splitterPos = part.indexOf(':'); | ||
if (splitterPos === part.length - 1) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `unexpected empty value` }; | ||
throw err; | ||
} | ||
if (splitterPos >= 0) { | ||
key = part.substr(0, splitterPos); | ||
val = part.substr(splitterPos); // keep the / for CIDR | ||
val = part.substr(splitterPos + 1); | ||
} else { | ||
key = part; | ||
let splitterPos = part.indexOf('/'); | ||
if (splitterPos >= 0) { | ||
key = part.substr(0, splitterPos); | ||
val = part.substr(splitterPos); // keep the / for CIDR | ||
} else { | ||
key = part; | ||
} | ||
} | ||
} | ||
if (/^[?\-~+]/.test(key)) { | ||
qualifier = key.charAt(0); | ||
key = key.substr(1); | ||
} | ||
if (/^[?\-~+]/.test(key)) { | ||
qualifier = key.charAt(0); | ||
key = key.substr(1); | ||
} | ||
let type = key.toLowerCase(); | ||
switch (type) { | ||
case 'all': | ||
if (val) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `unexpected extension for all` }; | ||
throw err; | ||
} | ||
return { type, qualifier }; | ||
case 'include': | ||
{ | ||
try { | ||
let redirect = macro(val, opts) | ||
// remove trailing dot | ||
.replace(/\.$/, ''); | ||
let sub = await spfVerify(redirect, opts); | ||
if (sub && sub.qualifier === '+') { | ||
// ignore other valid responses | ||
return { type, val, include: sub, qualifier }; | ||
} | ||
if (sub && sub.error) { | ||
return sub; | ||
} | ||
} catch (err) { | ||
// kind of ignore | ||
if (err.spfResult) { | ||
if (err.spfResult.error === 'none') { | ||
err.spfResult.error = 'permerror'; | ||
} | ||
return err.spfResult; | ||
} | ||
} | ||
} | ||
break; | ||
case 'ip4': | ||
case 'ip6': | ||
{ | ||
let { domain: range, cidr4, cidr6 } = parseCidrValue(val); | ||
if (!range) { | ||
let type = key.toLowerCase(); | ||
switch (type) { | ||
case 'all': | ||
if (val) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `bare IP address` }; | ||
err.spfResult = { error: 'permerror', text: `unexpected extension for all` }; | ||
throw err; | ||
} | ||
return { type, qualifier }; | ||
let originalRange = range; | ||
let mappingMatch = (range || '').toString().match(/^[:A-F]+:((\d+\.){3}\d+)$/i); | ||
if (mappingMatch) { | ||
range = mappingMatch[1]; | ||
case 'include': | ||
{ | ||
try { | ||
let redirect = macro(val, opts) | ||
// remove trailing dot | ||
.replace(/\.$/, ''); | ||
let sub = await spfVerify(redirect, opts); | ||
if (sub && sub.qualifier === '+') { | ||
// ignore other valid responses | ||
return { type, val, include: sub, qualifier }; | ||
} | ||
if (sub && sub.error) { | ||
return sub; | ||
} | ||
} catch (err) { | ||
// kind of ignore | ||
if (err.spfResult) { | ||
if (err.spfResult.error === 'none') { | ||
err.spfResult.error = 'permerror'; | ||
} | ||
return err.spfResult; | ||
} | ||
} | ||
} | ||
break; | ||
if (net.isIP(range)) { | ||
if (type === 'ip6' && net.isIPv6(opts.ip) && net.isIPv6(originalRange) && net.isIPv4(range) && cidr4 === '/0') { | ||
// map all IPv6 addresses | ||
return { type, val, qualifier }; | ||
} | ||
// validate ipv4 range only, skip ipv6 | ||
if (cidr6 && net.isIPv4(range)) { | ||
case 'ip4': | ||
case 'ip6': | ||
{ | ||
let { domain: range, cidr4, cidr6 } = parseCidrValue(val); | ||
if (!range) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `invalid CIDR for IP` }; | ||
err.spfResult = { error: 'permerror', text: `bare IP address` }; | ||
throw err; | ||
} | ||
if (net.isIP(range) !== net.isIP(opts.ip) || net.isIP(range) !== Number(type.charAt(2))) { | ||
// nothing to do here | ||
break; | ||
let originalRange = range; | ||
let mappingMatch = (range || '').toString().match(/^[:A-F]+:((\d+\.){3}\d+)$/i); | ||
if (mappingMatch) { | ||
range = mappingMatch[1]; | ||
} | ||
let cidr = net.isIPv6(range) ? cidr6 : cidr4; | ||
if (matchIp(addr, range + cidr)) { | ||
return { type, val, qualifier }; | ||
if (net.isIP(range)) { | ||
if (type === 'ip6' && net.isIPv6(opts.ip) && net.isIPv6(originalRange) && net.isIPv4(range) && cidr4 === '/0') { | ||
// map all IPv6 addresses | ||
return { type, val, qualifier }; | ||
} | ||
// validate ipv4 range only, skip ipv6 | ||
if (cidr6 && net.isIPv4(range)) { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `invalid CIDR for IP` }; | ||
throw err; | ||
} | ||
if (net.isIP(range) !== net.isIP(opts.ip) || net.isIP(range) !== Number(type.charAt(2))) { | ||
// nothing to do here | ||
break; | ||
} | ||
let cidr = net.isIPv6(range) ? cidr6 : cidr4; | ||
if (matchIp(addr, range + cidr)) { | ||
return { type, val, qualifier }; | ||
} | ||
} else { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `invalid IP address` }; | ||
throw err; | ||
} | ||
} else { | ||
let err = new Error('SPF failure'); | ||
err.spfResult = { error: 'permerror', text: `invalid IP address` }; | ||
throw err; | ||
} | ||
} | ||
break; | ||
break; | ||
case 'a': | ||
{ | ||
let { domain: a, cidr4, cidr6 } = parseCidrValue(val, domain); | ||
let cidr = net.isIPv6(opts.ip) ? cidr6 : cidr4; | ||
case 'a': | ||
{ | ||
let { domain: a, cidr4, cidr6 } = parseCidrValue(val, domain); | ||
let cidr = net.isIPv6(opts.ip) ? cidr6 : cidr4; | ||
a = macro(a, opts); | ||
a = macro(a, opts); | ||
try { | ||
a = punycode.toASCII(a); | ||
} catch (err) { | ||
// ignore punycode conversion errors | ||
} | ||
try { | ||
a = punycode.toASCII(a); | ||
} catch (err) { | ||
// ignore punycode conversion errors | ||
} | ||
let responses = await resolver(a, net.isIPv6(opts.ip) ? 'AAAA' : 'A'); | ||
if (responses) { | ||
for (let ip of responses) { | ||
if (matchIp(addr, ip + cidr)) { | ||
return { type, val: domain, qualifier }; | ||
let responses = await resolver(a, net.isIPv6(opts.ip) ? 'AAAA' : 'A'); | ||
if (responses) { | ||
for (let ip of responses) { | ||
if (matchIp(addr, ip + cidr)) { | ||
return { type, val: domain, qualifier }; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
break; | ||
break; | ||
case 'mx': | ||
{ | ||
let { domain: mxDomain, cidr4, cidr6 } = parseCidrValue(val, domain); | ||
let cidr = net.isIPv6(opts.ip) ? cidr6 : cidr4; | ||
case 'mx': | ||
{ | ||
let { domain: mxDomain, cidr4, cidr6 } = parseCidrValue(val, domain); | ||
let cidr = net.isIPv6(opts.ip) ? cidr6 : cidr4; | ||
try { | ||
mxDomain = punycode.toASCII(mxDomain); | ||
} catch (err) { | ||
// ignore punycode conversion errors | ||
} | ||
try { | ||
mxDomain = punycode.toASCII(mxDomain); | ||
} catch (err) { | ||
// ignore punycode conversion errors | ||
} | ||
let mxList = await resolver(mxDomain, 'MX'); | ||
if (mxList) { | ||
mxList = mxList.sort((a, b) => a.priority - b.priority); | ||
for (let mx of mxList) { | ||
if (mx.exchange) { | ||
let responses = await resolver(mx.exchange, net.isIPv6(opts.ip) ? 'AAAA' : 'A'); | ||
if (responses) { | ||
for (let a of responses) { | ||
if (matchIp(addr, a + cidr)) { | ||
return { type, val: mx.exchange, qualifier }; | ||
let mxList = await resolver(mxDomain, 'MX'); | ||
if (mxList) { | ||
mxList = mxList.sort((a, b) => a.priority - b.priority); | ||
for (let mx of mxList) { | ||
if (mx.exchange) { | ||
let responses = await resolver(mx.exchange, net.isIPv6(opts.ip) ? 'AAAA' : 'A'); | ||
if (responses) { | ||
for (let a of responses) { | ||
if (matchIp(addr, a + cidr)) { | ||
return { type, val: mx.exchange, qualifier }; | ||
} | ||
} | ||
@@ -358,30 +362,43 @@ } | ||
} | ||
} | ||
break; | ||
break; | ||
case 'exists': | ||
{ | ||
let existDomain = macro(val, opts); | ||
try { | ||
existDomain = punycode.toASCII(existDomain); | ||
} catch (err) { | ||
// ignore punycode conversion errors | ||
} | ||
case 'exists': | ||
{ | ||
let existDomain = macro(val, opts); | ||
try { | ||
existDomain = punycode.toASCII(existDomain); | ||
} catch (err) { | ||
// ignore punycode conversion errors | ||
} | ||
let responses = await resolver(existDomain, 'A'); | ||
if (responses && responses.length) { | ||
return { type, val: existDomain, qualifier }; | ||
let responses = await resolver(existDomain, 'A'); | ||
if (responses && responses.length) { | ||
return { type, val: existDomain, qualifier }; | ||
} | ||
} | ||
} | ||
break; | ||
break; | ||
case 'ptr': | ||
// ignore, not supported | ||
break; | ||
case 'ptr': | ||
// ignore, not supported | ||
break; | ||
} | ||
} | ||
return false; | ||
}; | ||
try { | ||
let res = await getResult(); | ||
if (res && spfRr) { | ||
res.rr = spfRr; | ||
} | ||
return res; | ||
} catch (err) { | ||
if (spfRr && err.spfResult) { | ||
err.spfResult.rr = spfRr; | ||
} | ||
throw err; | ||
} | ||
return false; | ||
}; | ||
module.exports = { spfVerify }; |
@@ -234,7 +234,7 @@ /* eslint no-control-regex: 0 */ | ||
const getPublicKey = async (type, rr, resolver) => { | ||
const getPublicKey = async (type, name, resolver) => { | ||
resolver = resolver || dns.resolve; | ||
let list = await resolver(rr, 'TXT'); | ||
let row = | ||
let list = await resolver(name, 'TXT'); | ||
let rr = | ||
list && | ||
@@ -246,10 +246,11 @@ [] | ||
if (row) { | ||
if (rr) { | ||
// prefix value for parsing as there is no default value | ||
let entry = parseDkimHeaders(`DNS: TXT;${row}`); | ||
let entry = parseDkimHeaders(`DNS: TXT;${rr}`); | ||
let pubKey = entry?.parsed?.p?.value; | ||
if (!pubKey) { | ||
let publicKey = entry?.parsed?.p?.value; | ||
if (!publicKey) { | ||
let err = new Error('Missing key value'); | ||
err.code = 'EINVALIDVAL'; | ||
err.rr = rr; | ||
throw err; | ||
@@ -261,7 +262,8 @@ } | ||
err.code = 'EINVALIDVER'; | ||
err.rr = rr; | ||
throw err; | ||
} | ||
pubKey = Buffer.from(`-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`); | ||
let keyType = crypto.createPublicKey({ key: pubKey, format: 'pem' }).asymmetricKeyType; | ||
publicKey = Buffer.from(`-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`); | ||
let keyType = crypto.createPublicKey({ key: publicKey, format: 'pem' }).asymmetricKeyType; | ||
@@ -271,2 +273,3 @@ if (!['rsa', 'ed25519'].includes(keyType) || (entry?.parsed?.k && entry?.parsed?.k?.value?.toLowerCase() !== keyType)) { | ||
err.code = 'EINVALIDTYPE'; | ||
err.rr = rr; | ||
throw err; | ||
@@ -277,6 +280,7 @@ } | ||
// check key length | ||
const pubKeyData = pki.publicKeyFromPem(pubKey.toString()); | ||
const pubKeyData = pki.publicKeyFromPem(publicKey.toString()); | ||
if (pubKeyData.n.bitLength() < 1024) { | ||
let err = new Error('Key too short'); | ||
err.code = 'ESHORTKEY'; | ||
err.rr = rr; | ||
throw err; | ||
@@ -286,3 +290,3 @@ } | ||
return pubKey; | ||
return { publicKey, rr }; | ||
} | ||
@@ -289,0 +293,0 @@ |
{ | ||
"name": "mailauth", | ||
"version": "1.0.16", | ||
"version": "1.0.17", | ||
"description": "Email authentication library for Node.js", | ||
@@ -5,0 +5,0 @@ "main": "lib/mailauth.js", |
228762
3677