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.
Files changed (58) hide show
  1. package/.c8rc.json +11 -0
  2. package/.prettierignore +3 -0
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +68 -0
  5. package/README.md +131 -31
  6. package/bin/mailauth.js +62 -0
  7. package/cli.md +150 -8
  8. package/docs/arc.md +62 -54
  9. package/docs/bimi.md +153 -54
  10. package/docs/dkim.md +37 -36
  11. package/docs/dkim2.md +154 -0
  12. package/docs/dmarc.md +4 -1
  13. package/docs/mta-sts.md +12 -9
  14. package/docs/spf.md +14 -13
  15. package/index.d.ts +583 -42
  16. package/lib/arc/index.d.ts +6 -2
  17. package/lib/arc/index.js +45 -15
  18. package/lib/bimi/index.js +362 -102
  19. package/lib/bimi/validate-svg.js +493 -60
  20. package/lib/commands/dkim2-hash.js +25 -0
  21. package/lib/commands/dkim2-sign.js +77 -0
  22. package/lib/commands/dkim2-verify.js +59 -0
  23. package/lib/commands/report.js +10 -35
  24. package/lib/commands/resolver.js +51 -0
  25. package/lib/commands/seal.js +10 -38
  26. package/lib/commands/spf.js +4 -35
  27. package/lib/dkim/dkim-signer.js +50 -30
  28. package/lib/dkim/dkim-verifier.js +79 -27
  29. package/lib/dkim/header/simple.js +2 -2
  30. package/lib/dkim/sign.js +7 -2
  31. package/lib/dkim/syntax.js +11 -0
  32. package/lib/dkim/verify.js +1 -0
  33. package/lib/dkim2/dkim2-parser.js +52 -0
  34. package/lib/dkim2/dkim2-signer.js +334 -0
  35. package/lib/dkim2/dkim2-verifier.js +572 -0
  36. package/lib/dkim2/fields.js +570 -0
  37. package/lib/dkim2/hash.d.ts +3 -0
  38. package/lib/dkim2/hash.js +51 -0
  39. package/lib/dkim2/key.js +203 -0
  40. package/lib/dkim2/message-state.js +181 -0
  41. package/lib/dkim2/recipe.js +134 -0
  42. package/lib/dkim2/sign.d.ts +3 -0
  43. package/lib/dkim2/sign.js +38 -0
  44. package/lib/dkim2/verify.d.ts +3 -0
  45. package/lib/dkim2/verify.js +19 -0
  46. package/lib/dmarc/author-domain.js +270 -5
  47. package/lib/dmarc/get-dmarc-record.js +24 -11
  48. package/lib/dmarc/verify.js +7 -3
  49. package/lib/mailauth.js +57 -14
  50. package/lib/mta-sts.js +37 -11
  51. package/lib/parse-received.js +214 -25
  52. package/lib/spf/index.js +21 -4
  53. package/lib/spf/macro.js +5 -3
  54. package/lib/spf/spf-verify.js +14 -5
  55. package/lib/spf/syntax.js +5 -0
  56. package/lib/tools.js +160 -21
  57. package/package.json +15 -6
  58. package/stryker.config.json +16 -0
@@ -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, and any errors
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(input: MessageInput | false, data: ARCCreateSealData): Promise<{ headers: string[]; errors: ARCSealError[]; warnings: string[] }>;
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
- // if last entry was listed as passing then add our seal even if the validation failed
609
- status.shouldSeal = ['pass', 'none'].includes(getCv(data?.lastEntry));
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?.selector,
703
- signingDomain: seal?.signingDomain,
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 created,
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, seal } = data;
721
- let bodyHash = seal?.bodyHash;
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?.strict);
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 authResults = `ARC-Authentication-Results: i=${seal.i}; ${seal.authResults}`;
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': authResults,
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, authResults],
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 };