mailauth 7.1.1 → 7.3.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/.c8rc.json +11 -0
- package/.prettierignore +3 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +68 -0
- package/README.md +131 -31
- package/bin/mailauth.js +62 -0
- package/cli.md +150 -8
- package/docs/arc.md +62 -54
- package/docs/bimi.md +153 -54
- package/docs/dkim.md +37 -36
- package/docs/dkim2.md +154 -0
- package/docs/dmarc.md +4 -1
- package/docs/mta-sts.md +12 -9
- package/docs/spf.md +14 -13
- package/index.d.ts +583 -42
- package/lib/arc/index.d.ts +6 -2
- package/lib/arc/index.js +45 -15
- package/lib/bimi/index.js +362 -102
- package/lib/bimi/validate-svg.js +493 -60
- package/lib/commands/dkim2-hash.js +25 -0
- package/lib/commands/dkim2-sign.js +77 -0
- package/lib/commands/dkim2-verify.js +59 -0
- package/lib/commands/report.js +10 -35
- package/lib/commands/resolver.js +51 -0
- package/lib/commands/seal.js +10 -38
- package/lib/commands/spf.js +4 -35
- package/lib/dkim/dkim-signer.js +50 -30
- package/lib/dkim/dkim-verifier.js +79 -27
- package/lib/dkim/header/simple.js +2 -2
- package/lib/dkim/sign.js +7 -2
- package/lib/dkim/syntax.js +11 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dkim2/dkim2-parser.js +52 -0
- package/lib/dkim2/dkim2-signer.js +334 -0
- package/lib/dkim2/dkim2-verifier.js +572 -0
- package/lib/dkim2/fields.js +570 -0
- package/lib/dkim2/hash.d.ts +3 -0
- package/lib/dkim2/hash.js +51 -0
- package/lib/dkim2/key.js +203 -0
- package/lib/dkim2/message-state.js +181 -0
- package/lib/dkim2/recipe.js +134 -0
- package/lib/dkim2/sign.d.ts +3 -0
- package/lib/dkim2/sign.js +38 -0
- package/lib/dkim2/verify.d.ts +3 -0
- package/lib/dkim2/verify.js +19 -0
- package/lib/dmarc/author-domain.js +270 -5
- package/lib/dmarc/get-dmarc-record.js +24 -11
- package/lib/dmarc/verify.js +7 -3
- package/lib/mailauth.js +57 -14
- package/lib/mta-sts.js +37 -11
- package/lib/parse-received.js +214 -25
- package/lib/spf/index.js +21 -4
- package/lib/spf/macro.js +5 -3
- package/lib/spf/spf-verify.js +14 -5
- package/lib/spf/syntax.js +5 -0
- package/lib/tools.js +160 -21
- package/package.json +15 -6
- package/stryker.config.json +16 -0
package/lib/arc/index.d.ts
CHANGED
|
@@ -44,6 +44,10 @@ export function verifyASChain(data: ARCData, opts: ARCOptions & { warnings?: str
|
|
|
44
44
|
*
|
|
45
45
|
* @param input - RFC822 formatted message or false for pre-calculated data
|
|
46
46
|
* @param data - Seal creation data
|
|
47
|
-
* @returns Seal headers, empty if the ARC-Message-Signature could not be signed,
|
|
47
|
+
* @returns Seal headers, empty if the ARC-Message-Signature could not be signed, any errors, and
|
|
48
|
+
* the instance (i=) of a created set
|
|
48
49
|
*/
|
|
49
|
-
export function createSeal(
|
|
50
|
+
export function createSeal(
|
|
51
|
+
input: MessageInput | false,
|
|
52
|
+
data: ARCCreateSealData
|
|
53
|
+
): Promise<{ headers: string[]; errors: ARCSealError[]; warnings: string[]; instance?: number }>;
|
package/lib/arc/index.js
CHANGED
|
@@ -14,7 +14,8 @@ const {
|
|
|
14
14
|
addWarning,
|
|
15
15
|
KEY_ERROR_COMMENTS,
|
|
16
16
|
splitAlgorithm,
|
|
17
|
-
createKeyCache
|
|
17
|
+
createKeyCache,
|
|
18
|
+
isHostNameDomain
|
|
18
19
|
} = require('../../lib/tools');
|
|
19
20
|
const { TIMESTAMP, BODY_LENGTH, MAX_ARC_INSTANCE, ARC_HEADER_KEYS } = require('../dkim/syntax');
|
|
20
21
|
const crypto = require('node:crypto');
|
|
@@ -472,6 +473,15 @@ const getARChain = (headers, opts) => {
|
|
|
472
473
|
checkTagList(arcInstance, 'arc-message-signature', strict, warn);
|
|
473
474
|
}
|
|
474
475
|
|
|
476
|
+
// A d= that is not a host name does not name the zone of its key record (RFC 6376
|
|
477
|
+
// section 3.5), so its key is never looked up and the chain fails in every mode
|
|
478
|
+
for (let headerKey of isNewest ? ['arc-seal', 'arc-message-signature'] : ['arc-seal']) {
|
|
479
|
+
let signingDomain = (arcInstance[headerKey].parsed?.d?.value || '').toString();
|
|
480
|
+
if (signingDomain && !isHostNameDomain(signingDomain)) {
|
|
481
|
+
throw createError(`i=${arcInstance.i} ${SHORT_NAMES[headerKey]} invalid d=`, 'invalid_arc_tags');
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
475
485
|
let cv = getCv(arcInstance);
|
|
476
486
|
|
|
477
487
|
if (i === 0 && cv !== 'none') {
|
|
@@ -605,8 +615,11 @@ const arc = async (data, opts) => {
|
|
|
605
615
|
// all failures are permanent in the scope of ARC
|
|
606
616
|
result.i = data?.lastEntry?.i || false;
|
|
607
617
|
status.result = 'fail';
|
|
608
|
-
//
|
|
609
|
-
|
|
618
|
+
// RFC 8617 section 5.1 step 2: a failed chain is sealed with cv=fail unless its newest
|
|
619
|
+
// seal already says cv=fail. The DKIM verifier reads the newest seal from the header
|
|
620
|
+
// fields (latestCv), so a chain that failed the structure checks and could not be
|
|
621
|
+
// parsed is sealed as well. Data from elsewhere only has the parsed chain
|
|
622
|
+
status.shouldSeal = data?.latestCv !== undefined ? data.latestCv !== 'fail' : ['pass', 'none'].includes(getCv(data?.lastEntry));
|
|
610
623
|
|
|
611
624
|
switch (err.code) {
|
|
612
625
|
case 'invalid_arc_seal':
|
|
@@ -699,8 +712,8 @@ const arc = async (data, opts) => {
|
|
|
699
712
|
|
|
700
713
|
const sealError = (seal, err) => ({
|
|
701
714
|
type: 'ARC',
|
|
702
|
-
selector: seal
|
|
703
|
-
signingDomain: seal
|
|
715
|
+
selector: seal.selector,
|
|
716
|
+
signingDomain: seal.signingDomain,
|
|
704
717
|
err
|
|
705
718
|
});
|
|
706
719
|
|
|
@@ -713,14 +726,17 @@ const isSet = value => typeof value !== 'undefined' && value !== null && value !
|
|
|
713
726
|
* @param {ReadableStream|Buffer|String|false} input The message, or false to use `data.headers`
|
|
714
727
|
* and `seal.bodyHash` instead
|
|
715
728
|
* @param {Object} data { headers, arc, seal, strict }
|
|
716
|
-
* @returns {Object} { headers, errors, warnings }. `headers` is empty when no set was
|
|
717
|
-
* and `errors` says why
|
|
729
|
+
* @returns {Object} { headers, errors, warnings, instance }. `headers` is empty when no set was
|
|
730
|
+
* created, and `errors` says why. `instance` is the i= of a created set
|
|
718
731
|
*/
|
|
719
732
|
const createSeal = async (input, data) => {
|
|
720
|
-
let { headers, arc
|
|
721
|
-
|
|
733
|
+
let { headers, arc } = data;
|
|
734
|
+
// a copy, the instance chosen below is stored on it and the caller's object, which may be
|
|
735
|
+
// reused for the next message, stays unchanged
|
|
736
|
+
let seal = Object.assign({}, data.seal);
|
|
737
|
+
let bodyHash = seal.bodyHash;
|
|
722
738
|
|
|
723
|
-
const strict = !!(data.strict || seal
|
|
739
|
+
const strict = !!(data.strict || seal.strict);
|
|
724
740
|
const warnings = [];
|
|
725
741
|
const warn = warning => addWarning(warnings, warning);
|
|
726
742
|
|
|
@@ -761,6 +777,19 @@ const createSeal = async (input, data) => {
|
|
|
761
777
|
}
|
|
762
778
|
}
|
|
763
779
|
|
|
780
|
+
// RFC 8617 section 4.1.1, the payload of the ARC-Authentication-Results header. A line break
|
|
781
|
+
// is only allowed as folding (CRLF followed by whitespace and more content), anything else
|
|
782
|
+
// would start a new header field or end the header block
|
|
783
|
+
let authResults = isSet(seal.authResults) ? String(seal.authResults) : '';
|
|
784
|
+
if (!/\S/.test(authResults)) {
|
|
785
|
+
return refuse('The authResults value for the ARC-Authentication-Results header is missing', 'EINVALIDAUTHRESULTS');
|
|
786
|
+
}
|
|
787
|
+
// a CR that does not start folding, or an LF without a CR. A search for the bad line break,
|
|
788
|
+
// a pattern that matches the whole value runs out of stack on a large one
|
|
789
|
+
if (/\r(?!\n[ \t]+[^ \t\r\n])|(?<!\r)\n/.test(authResults)) {
|
|
790
|
+
return refuse('The authResults value contains a line break that is not header folding', 'EINVALIDAUTHRESULTS');
|
|
791
|
+
}
|
|
792
|
+
|
|
764
793
|
// Step 1. Calculate ARC-Message-Signature
|
|
765
794
|
dkimSigner = new DkimSigner({
|
|
766
795
|
// headers and bodyHash are prepared values if we do not have the source message anymore
|
|
@@ -869,7 +898,7 @@ const createSeal = async (input, data) => {
|
|
|
869
898
|
return { headers: [], errors: dkimSigner.errors, warnings: collectWarnings() };
|
|
870
899
|
}
|
|
871
900
|
|
|
872
|
-
const
|
|
901
|
+
const authResultsHeader = `ARC-Authentication-Results: i=${seal.i}; ${authResults}`;
|
|
873
902
|
|
|
874
903
|
// the ARC-Seal uses the algorithm of the ARC-Message-Signature, which follows the key type
|
|
875
904
|
// when no algorithm was given
|
|
@@ -881,7 +910,7 @@ const createSeal = async (input, data) => {
|
|
|
881
910
|
arcSeal = await signAS(
|
|
882
911
|
arc?.chain || [],
|
|
883
912
|
{
|
|
884
|
-
'arc-authentication-results':
|
|
913
|
+
'arc-authentication-results': authResultsHeader,
|
|
885
914
|
'arc-message-signature': messageSignature
|
|
886
915
|
},
|
|
887
916
|
{
|
|
@@ -900,9 +929,10 @@ const createSeal = async (input, data) => {
|
|
|
900
929
|
}
|
|
901
930
|
|
|
902
931
|
return {
|
|
903
|
-
headers: [arcSeal, messageSignature,
|
|
932
|
+
headers: [arcSeal, messageSignature, authResultsHeader],
|
|
904
933
|
errors: dkimSigner.errors,
|
|
905
|
-
warnings: collectWarnings()
|
|
934
|
+
warnings: collectWarnings(),
|
|
935
|
+
instance: seal.i
|
|
906
936
|
};
|
|
907
937
|
};
|
|
908
938
|
|
|
@@ -911,4 +941,4 @@ const sealMessage = async (input, seal) => {
|
|
|
911
941
|
return headers.length ? Buffer.from(headers.join('\r\n') + '\r\n') : Buffer.from('');
|
|
912
942
|
};
|
|
913
943
|
|
|
914
|
-
module.exports = { getARChain, verifyASChain, arc, createSeal, sealMessage };
|
|
944
|
+
module.exports = { getARChain, getHighestArcInstance, verifyASChain, arc, createSeal, sealMessage };
|