mailauth 7.1.1 → 7.2.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/index.d.ts CHANGED
@@ -144,20 +144,22 @@ export interface ARCSealOptions {
144
144
  /**
145
145
  * Chain validation status for the ARC-Seal cv= tag: 'none', 'pass' or 'fail' (compared
146
146
  * case-insensitively). Defaults to 'none' for the first set and is required for any later
147
- * set. `authenticate()` sets it from the ARC validation result
147
+ * set. `authenticate()` always sets it from the ARC validation result and ignores this value
148
148
  */
149
149
  cv?: 'none' | 'pass' | 'fail' | string;
150
150
 
151
151
  /**
152
152
  * ARC instance (i= tag), 1 to 50. Defaults to one more than the highest instance on the
153
153
  * message. An instance that already exists is refused, and one that leaves a gap is
154
- * refused in strict mode (the default mode seals it and adds an `arc-instance-gap` warning)
154
+ * refused in strict mode (the default mode seals it and adds an `arc-instance-gap` warning).
155
+ * `authenticate()` ignores it and always uses the next instance
155
156
  */
156
157
  i?: number;
157
158
 
158
159
  /**
159
160
  * Authentication-Results payload for the ARC-Authentication-Results header (the part after
160
- * "i=N;"). `authenticate()` sets it from its own results
161
+ * "i=N;"). Required, and a line break is only allowed as folding (CRLF followed by a space or
162
+ * tab). `authenticate()` always sets it from its own results and ignores this value
161
163
  */
162
164
  authResults?: string;
163
165
 
@@ -218,16 +220,20 @@ export interface AuthStatus {
218
220
  *
219
221
  * - `rsa-sha1`: the signature uses rsa-sha1 (RFC 8301 section 3.1)
220
222
  * - `tag-syntax`: the signature is not valid tag-list syntax (duplicate tags, upper case tag
221
- * names, a malformed t=, x= or l= value, or a d= or s= that is not a domain name)
223
+ * names, a malformed c=, t=, x= or l= value, an l= larger than the canonicalized body, or a d=
224
+ * or s= that is not a domain name)
222
225
  * - `missing-v`, `missing-h` (and the other `missing-*`): a required tag is missing
223
226
  * - `invalid-v`: v= is not "1"
224
227
  * - `identity-domain`: the i= domain is not d= or its subdomain, or it is a subdomain while
225
228
  * the key has t=s
226
229
  * - `invalid-expiration`: x= is equal to t=
227
230
  * - `query-method`: q= does not list dns/txt
228
- * - `key-syntax`: the key record is not valid tag-list syntax
231
+ * - `key-syntax`: the key record is not valid tag-list syntax, or a k=, s= or t= value has
232
+ * whitespace inside it
229
233
  * - `key-v-syntax`: the key record v= is not the first tag, or not exactly "DKIM1"
230
234
  * - `key-type-inferred`: an ed25519 key was found from its length, without k=ed25519
235
+ * - `key-ed25519-spki`: an ed25519 key was published as a SubjectPublicKeyInfo structure, not
236
+ * as the bare 32 octet key of RFC 8463 section 4.2
231
237
  */
232
238
  export type DKIMWarning =
233
239
  | 'rsa-sha1'
@@ -245,7 +251,8 @@ export type DKIMWarning =
245
251
  | 'query-method'
246
252
  | 'key-syntax'
247
253
  | 'key-v-syntax'
248
- | 'key-type-inferred';
254
+ | 'key-type-inferred'
255
+ | 'key-ed25519-spki';
249
256
 
250
257
  /**
251
258
  * DKIM verification result for a single signature
@@ -307,7 +314,8 @@ export interface DKIMResult {
307
314
  /**
308
315
  * Signing domain if it aligns with the From domain, false otherwise. For a passing
309
316
  * signature this follows the DMARC verdict once DMARC found a record, before that it
310
- * only guesses relaxed alignment from the Public Suffix List.
317
+ * only guesses relaxed alignment from the Public Suffix List. Always false when the From
318
+ * header field does not yield a single Author Domain, as DMARC requires.
311
319
  */
312
320
  aligned?: string | false;
313
321
 
@@ -430,6 +438,14 @@ export interface DKIMVerifyResult {
430
438
  */
431
439
  fromFields: number;
432
440
 
441
+ /**
442
+ * Syntax of the From header fields: "valid" (RFC 5322), "lax" (accepted only by the lenient
443
+ * parsing, such as an unquoted comma in a display name) or "invalid" (no reliable addresses,
444
+ * for example text after the angle-addr). DMARC is not evaluated for an "invalid" field, nor
445
+ * for a "lax" one in strict mode
446
+ */
447
+ fromSyntax: 'valid' | 'lax' | 'invalid';
448
+
433
449
  /**
434
450
  * Domain from Return-Path header
435
451
  */
@@ -542,6 +558,10 @@ export interface ARCResult {
542
558
  * Verification status
543
559
  */
544
560
  status: AuthStatus & {
561
+ /**
562
+ * Set for a failed chain: whether it is sealed with cv=fail, false when the newest
563
+ * ARC-Seal on the message already says cv=fail (RFC 8617 section 5.1 step 2)
564
+ */
545
565
  shouldSeal?: boolean;
546
566
  };
547
567
 
@@ -600,9 +620,11 @@ export interface DMARCResult {
600
620
 
601
621
  /**
602
622
  * Effective DMARC policy ('none', 'quarantine', 'reject'): p for the author domain's own
603
- * record, np or sp for an inherited one, one level lower with t=y
623
+ * record, np or sp for an inherited one, one level lower with t=y. Like p, sp, testMode and
624
+ * alignment, only set when a policy record was applied, not for a none result or a
625
+ * temperror before a record was found
604
626
  */
605
- policy: string;
627
+ policy?: string;
606
628
 
607
629
  /**
608
630
  * Domain whose DMARC record the policy was taken from
@@ -612,12 +634,12 @@ export interface DMARCResult {
612
634
  /**
613
635
  * Policy for organizational domain
614
636
  */
615
- p: string;
637
+ p?: string;
616
638
 
617
639
  /**
618
640
  * Policy for subdomains
619
641
  */
620
- sp: string;
642
+ sp?: string;
621
643
 
622
644
  /**
623
645
  * Policy for non-existent subdomains, if published
@@ -627,7 +649,7 @@ export interface DMARCResult {
627
649
  /**
628
650
  * True when the record has t=y, in which case policy is one level below the published one
629
651
  */
630
- testMode: boolean;
652
+ testMode?: boolean;
631
653
 
632
654
  /**
633
655
  * DMARC DNS record
@@ -640,7 +662,7 @@ export interface DMARCResult {
640
662
  status: AuthStatus & {
641
663
  header?: {
642
664
  /**
643
- * Author Domain
665
+ * Author Domain (RFC 9989 section 9.1 header.from)
644
666
  */
645
667
  from?: string;
646
668
 
@@ -652,16 +674,16 @@ export interface DMARCResult {
652
674
 
653
675
  policy?: {
654
676
  /**
655
- * The policy that was applied (RFC 8601 section 2.7.2 policy.dmarc)
677
+ * The policy that was applied (RFC 9989 section 9.1 policy.dmarc)
656
678
  */
657
679
  dmarc?: string;
658
680
  };
659
681
  };
660
682
 
661
683
  /**
662
- * Alignment results
684
+ * Alignment results, set when a policy record was applied
663
685
  */
664
- alignment: {
686
+ alignment?: {
665
687
  spf: {
666
688
  /**
667
689
  * Aligned SPF domain, or undefined if no domain aligned
@@ -729,6 +751,18 @@ export interface BIMIResult {
729
751
  */
730
752
  authority?: string;
731
753
 
754
+ /**
755
+ * Avatar preference from the avp= tag, set only for the valid values
756
+ */
757
+ preference?: 'personal' | 'brand';
758
+
759
+ /**
760
+ * Assertion Record and BIMI-Selector syntax deviations that were accepted because `strict` was not set:
761
+ * 'record-version-case', 'record-tag-case', 'record-syntax', 'uri-comma', 'location-format',
762
+ * 'selector-version-case', 'selector-syntax'
763
+ */
764
+ warnings?: string[];
765
+
732
766
  /**
733
767
  * Authentication-Results formatted info
734
768
  */
@@ -870,9 +904,9 @@ export function authenticate(input: MessageInput, opts?: AuthenticateOptions): P
870
904
  // ============================================================================
871
905
 
872
906
  /**
873
- * DKIM signing options
907
+ * A single DKIM signature to create, an entry of `DKIMSignOptions.signatureData`
874
908
  */
875
- export interface DKIMSignOptions {
909
+ export interface DKIMSignatureData {
876
910
  /**
877
911
  * Signing domain (d= tag)
878
912
  */
@@ -884,10 +918,44 @@ export interface DKIMSignOptions {
884
918
  selector: string;
885
919
 
886
920
  /**
887
- * Private key for signing (PEM format)
921
+ * Private key for signing (PEM format). An entry without a key is not signed and gets an
922
+ * `ENOKEY` error
888
923
  */
889
924
  privateKey: string | Buffer;
890
925
 
926
+ /**
927
+ * Overrides the shared `canonicalization`
928
+ */
929
+ canonicalization?: string;
930
+
931
+ /**
932
+ * Overrides the shared `algorithm`
933
+ */
934
+ algorithm?: string;
935
+
936
+ /**
937
+ * Overrides the shared `headerList`
938
+ */
939
+ headerList?: string[] | string;
940
+
941
+ /**
942
+ * Maximum body length to sign (l= tag)
943
+ */
944
+ maxBodyLength?: number;
945
+
946
+ /**
947
+ * Identity (i= tag), an address whose domain is the signing domain or its subdomain.
948
+ * Given unencoded, it is written as dkim-quoted-printable (`jõgi@example.com` as
949
+ * `i=j=C3=B5gi@example.com`)
950
+ */
951
+ identity?: string;
952
+ }
953
+
954
+ /**
955
+ * DKIM signing options. The values of each signature are set in `signatureData`, the other
956
+ * options apply to all of them
957
+ */
958
+ export interface DKIMSignOptions {
891
959
  /**
892
960
  * Canonicalization algorithm (default: 'relaxed/relaxed')
893
961
  * Format: 'header/body' where each can be 'simple' or 'relaxed'
@@ -903,7 +971,9 @@ export interface DKIMSignOptions {
903
971
 
904
972
  /**
905
973
  * Header fields to sign, as an array or a colon separated string. Default includes
906
- * From, Subject, Date, To, etc. Set per signature in `signatureData` or for all of them
974
+ * From, Subject, Date, To, etc. Set per signature in `signatureData` or for all of them.
975
+ * A name listed more than once is over-signed: it appears in h= as many times as it is
976
+ * listed or as the message has that field, whichever is more (`'From:From'`)
907
977
  */
908
978
  headerList?: string[] | string;
909
979
 
@@ -918,19 +988,10 @@ export interface DKIMSignOptions {
918
988
  expires?: Date | string | number;
919
989
 
920
990
  /**
921
- * Maximum body length to sign (l= tag)
991
+ * The signatures to create. Without any, nothing is signed and `errors` holds an
992
+ * `ENOSIGNATURE` error
922
993
  */
923
- maxBodyLength?: number;
924
-
925
- /**
926
- * Identity (i= tag), an address whose domain is the signing domain or its subdomain
927
- */
928
- identity?: string;
929
-
930
- /**
931
- * Multiple signature configurations
932
- */
933
- signatureData?: DKIMSignOptions[];
994
+ signatureData: DKIMSignatureData[];
934
995
 
935
996
  /**
936
997
  * Follow the RFCs exactly (default: false). Strict mode refuses rsa-sha1, RSA keys
@@ -1119,7 +1180,7 @@ export interface DKIMSignError {
1119
1180
  /**
1120
1181
  * Why the signature was not created. `err.code` is eg. 'ENOFROM', 'EINVALIDALGO',
1121
1182
  * 'EINVALIDTYPE', 'ESHORTKEY', 'EINVALIDDOMAIN', 'EINVALIDSELECTOR', 'EINVALIDIDENTITY',
1122
- * 'EINVALIDTIME', 'EINVALIDCANON' or 'EINVALIDINSTANCE'
1183
+ * 'EINVALIDTIME', 'EINVALIDCANON', 'EINVALIDINSTANCE', 'ENOKEY' or 'ENOSIGNATURE'
1123
1184
  */
1124
1185
  err: Error & { code?: string };
1125
1186
 
@@ -1138,9 +1199,11 @@ export interface DKIMSignError {
1138
1199
  * - `invalid-expiration`: the expiration is not after the signing time, or does not fit into x=
1139
1200
  * - `invalid-signtime`: the signing time does not fit into t=, which was left out
1140
1201
  * - `d-syntax`, `s-syntax`: d= or s= is not valid RFC 6376 syntax
1202
+ * - `identity-syntax`: the identity is not valid RFC 6376 i= syntax
1141
1203
  * - `identity-domain`: the identity domain is not the signing domain or its subdomain
1142
1204
  */
1143
- export type DKIMSignWarning = 'rsa-sha1' | 'weak-key' | 'invalid-expiration' | 'invalid-signtime' | 'd-syntax' | 's-syntax' | 'identity-domain';
1205
+ export type DKIMSignWarning =
1206
+ 'rsa-sha1' | 'weak-key' | 'invalid-expiration' | 'invalid-signtime' | 'd-syntax' | 's-syntax' | 'identity-syntax' | 'identity-domain';
1144
1207
 
1145
1208
  /**
1146
1209
  * DKIM signing result
@@ -1337,6 +1400,13 @@ export interface DMARCOptions {
1337
1400
  */
1338
1401
  fromFields?: number;
1339
1402
 
1403
+ /**
1404
+ * Syntax of the From header field the addresses came from (see DKIMVerifyResult.fromSyntax).
1405
+ * With "invalid", or "lax" in strict mode, DMARC validation is not possible (reason
1406
+ * "invalid-author-domain"). "lax" adds the "from-syntax" warning in the default mode
1407
+ */
1408
+ fromSyntax?: 'valid' | 'lax' | 'invalid';
1409
+
1340
1410
  /**
1341
1411
  * Domains that passed SPF
1342
1412
  */
@@ -1398,6 +1468,13 @@ export interface ARCData {
1398
1468
  * Error encountered during ARC chain parsing
1399
1469
  */
1400
1470
  error?: Error;
1471
+
1472
+ /**
1473
+ * The lower case cv= of the newest ARC-Seal in the header fields, or false if there is
1474
+ * none, set by the DKIM verifier. Read from the header fields even when the chain could not
1475
+ * be parsed, to decide whether a failed chain is sealed with cv=fail
1476
+ */
1477
+ latestCv?: string | false;
1401
1478
  }
1402
1479
 
1403
1480
  /**
@@ -1409,7 +1486,9 @@ export interface ARCSealError {
1409
1486
  * instance out of 1-50, a chain that already has 50 sets, one that already exists, or in
1410
1487
  * strict mode one that leaves a gap),
1411
1488
  * EARCCHAINFAILED (the newest ARC-Seal already has cv=fail, RFC 8617 section 5.1 step 2),
1412
- * EINVALIDALGO and EINVALIDTYPE (an algorithm that is not supported or does not match the key)
1489
+ * EINVALIDALGO and EINVALIDTYPE (an algorithm that is not supported or does not match the key),
1490
+ * EINVALIDAUTHRESULTS (a missing or empty authResults, or one with a line break that is not
1491
+ * CRLF folding)
1413
1492
  */
1414
1493
  err: Error & { code?: string };
1415
1494
  type?: string;
@@ -1516,11 +1595,14 @@ export function verifyASChain(data: ARCData, opts: ARCOptions & { warnings?: str
1516
1595
  *
1517
1596
  * @param input - RFC822 formatted message or false for pre-calculated data
1518
1597
  * @param data - Seal creation data
1519
- * @returns Seal headers (empty when no set was created), the errors that say why, and what the
1598
+ * @returns Seal headers (empty when no set was created), the errors that say why, what the
1520
1599
  * default mode sealed that strict mode would have refused ('arc-instance-gap',
1521
- * 'arc-cv-instance', and the DKIM signing warnings)
1600
+ * 'arc-cv-instance', and the DKIM signing warnings), and the instance (i=) of a created set
1522
1601
  */
1523
- export function createSeal(input: MessageInput | false, data: ARCCreateSealData): Promise<{ headers: string[]; errors: ARCSealError[]; warnings: string[] }>;
1602
+ export function createSeal(
1603
+ input: MessageInput | false,
1604
+ data: ARCCreateSealData
1605
+ ): Promise<{ headers: string[]; errors: ARCSealError[]; warnings: string[]; instance?: number }>;
1524
1606
 
1525
1607
  // ============================================================================
1526
1608
  // BIMI
@@ -1551,8 +1633,10 @@ export interface BIMIOptions {
1551
1633
  resolver?: DNSResolver;
1552
1634
 
1553
1635
  /**
1554
- * Format the Authentication-Results entry in the RFC 8601 section 2.2 form (default: false).
1555
- * BIMI has no other strict-only rules, the draft checks apply in both modes
1636
+ * Parse the Assertion Record exactly (default: false): `v=BIMI1` and the tag names are
1637
+ * case-sensitive, syntax errors such as duplicate tags fail the record, and so do an `l=` URI with
1638
+ * another image format suffix and unencoded commas in URIs. Without it these are accepted and
1639
+ * reported in `warnings`. Also formats the Authentication-Results entry in the RFC 8601 section 2.2 form
1556
1640
  */
1557
1641
  strict?: boolean;
1558
1642
  }
@@ -1589,6 +1673,11 @@ export interface VMCValidationResult {
1589
1673
  */
1590
1674
  code?: string;
1591
1675
 
1676
+ /**
1677
+ * Additional error details, for `SVG_VALIDATION_FAILED` the message and code of the SVG validation error
1678
+ */
1679
+ details?: any;
1680
+
1592
1681
  /**
1593
1682
  * Redirect URL if logo location redirected
1594
1683
  */
@@ -1596,7 +1685,8 @@ export interface VMCValidationResult {
1596
1685
  };
1597
1686
 
1598
1687
  /**
1599
- * Base64-encoded SVG logo file content
1688
+ * Base64-encoded SVG logo file content, uncompressed if the file was SVGZ. Set only when the
1689
+ * file passed SVG validation
1600
1690
  */
1601
1691
  logoFile?: string;
1602
1692
 
@@ -1665,6 +1755,44 @@ export interface VMCValidationResult {
1665
1755
  */
1666
1756
  hashMatch?: boolean;
1667
1757
  };
1758
+
1759
+ /**
1760
+ * The BIMI status of `bimiData` updated with the validation results (7.7): `fail` with a comment
1761
+ * when the indicator or the evidence document failed, `policy.authority` set to `pass` or `fail`
1762
+ * when an evidence document was checked. Set only when `bimiData.status.result` was `pass`
1763
+ */
1764
+ status?: BIMIResult['status'];
1765
+
1766
+ /**
1767
+ * Authentication-Results entry for the updated status, use it instead of the `info` of `bimi()`
1768
+ */
1769
+ info?: string;
1770
+
1771
+ /**
1772
+ * SVG Tiny PS profile deviations of the logo files that were accepted because `strict` was not set,
1773
+ * for example 'svg-version'
1774
+ */
1775
+ warnings?: string[];
1776
+
1777
+ /**
1778
+ * BIMI headers for the message, set only when the logo and the evidence document (if any) validated
1779
+ */
1780
+ headers?: {
1781
+ /**
1782
+ * BIMI-Indicator header with the base64-encoded SVG logo
1783
+ */
1784
+ indicator: string;
1785
+
1786
+ /**
1787
+ * BIMI-Location header, with a= when the evidence document was verified
1788
+ */
1789
+ location: string;
1790
+
1791
+ /**
1792
+ * BIMI-Logo-Preference header
1793
+ */
1794
+ preference?: string;
1795
+ };
1668
1796
  }
1669
1797
 
1670
1798
  /**
@@ -1679,6 +1807,33 @@ export interface BIMIData extends BIMIResult {
1679
1807
  * VMC validation options
1680
1808
  */
1681
1809
  export interface VMCValidationOptions {
1810
+ /**
1811
+ * Validate the logo files exactly against the SVG Tiny PS profile (default: false). Without it,
1812
+ * the deviations that do not make a logo unsafe are accepted and listed in `warnings`. Also
1813
+ * formats `info` in the RFC 8601 section 2.2 form
1814
+ */
1815
+ strict?: boolean;
1816
+
1817
+ /**
1818
+ * Maximum size of the logo file in bytes, also applied to an uncompressed SVGZ file (default: 65536)
1819
+ */
1820
+ maxLogoSize?: number;
1821
+
1822
+ /**
1823
+ * Maximum size of the evidence document in bytes (default: 262144)
1824
+ */
1825
+ maxEvidenceSize?: number;
1826
+
1827
+ /**
1828
+ * Time limit for each download in milliseconds, including redirects and the body (default: 30000)
1829
+ */
1830
+ timeout?: number;
1831
+
1832
+ /**
1833
+ * undici Dispatcher used for the downloads
1834
+ */
1835
+ dispatcher?: any;
1836
+
1682
1837
  /**
1683
1838
  * Custom VMC validation options passed to @postalsys/vmc
1684
1839
  */
@@ -1702,13 +1857,30 @@ export function bimi(opts: BIMIOptions): Promise<BIMIResult | false>;
1702
1857
  */
1703
1858
  export function validateBimiVmc(bimiData: BIMIData | null, opts?: VMCValidationOptions): Promise<VMCValidationResult | false>;
1704
1859
 
1860
+ /**
1861
+ * SVG logo validation options
1862
+ */
1863
+ export interface BIMISvgValidationOptions {
1864
+ /**
1865
+ * Apply the SVG Tiny PS profile exactly (default: false)
1866
+ */
1867
+ strict?: boolean;
1868
+
1869
+ /**
1870
+ * Markers of the accepted profile deviations are added to this array when `strict` is not set
1871
+ */
1872
+ warnings?: string[];
1873
+ }
1874
+
1705
1875
  /**
1706
1876
  * Validates BIMI SVG logo file
1707
1877
  *
1708
1878
  * @param logo - SVG logo file buffer
1879
+ * @param opts - validation options
1880
+ * @returns true
1709
1881
  * @throws Error if validation fails
1710
1882
  */
1711
- export function validateBimiSvg(logo: Buffer): void;
1883
+ export function validateBimiSvg(logo: Buffer | string, opts?: BIMISvgValidationOptions): true;
1712
1884
 
1713
1885
  // ============================================================================
1714
1886
  // MTA-STS
@@ -1752,6 +1924,16 @@ export interface MTASTSPolicy {
1752
1924
  * Error encountered during policy fetch
1753
1925
  */
1754
1926
  error?: Error;
1927
+
1928
+ /**
1929
+ * Policy ID from DNS that could not be fetched, set on a cached policy that is kept in use instead
1930
+ */
1931
+ retryId?: string;
1932
+
1933
+ /**
1934
+ * ISO 8601 timestamp before which the policy for `retryId` is not fetched again (RFC 8461 3.3)
1935
+ */
1936
+ retryAfter?: string;
1755
1937
  }
1756
1938
 
1757
1939
  /**
@@ -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 };