mailauth 7.0.0 → 7.1.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +3 -1
- package/bin/mailauth.js +1 -0
- package/cli.md +1 -0
- package/docs/dkim.md +2 -2
- package/docs/dmarc.md +1 -1
- package/index.d.ts +13 -0
- package/lib/commands/report.js +2 -1
- package/lib/dkim/dkim-verifier.js +6 -3
- package/lib/dmarc/get-dmarc-record.js +16 -2
- package/lib/mailauth.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [7.1.0](https://github.com/postalsys/mailauth/compare/v7.0.0...v7.1.0) (2026-09-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **cli:** add --reject-rsa-sha1 to the report command ([#140](https://github.com/postalsys/mailauth/issues/140)) ([fc8e965](https://github.com/postalsys/mailauth/commit/fc8e9651e304ff6d9f3249e2f3c1db7e481c7d8c))
|
|
9
|
+
* **dkim:** add rejectRsaSha1 to reject rsa-sha1 signatures without the strict mode ([#138](https://github.com/postalsys/mailauth/issues/138)) ([92d2c23](https://github.com/postalsys/mailauth/commit/92d2c23027d443dc62dc93a49b54d561beb3f2ed))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **dmarc:** count a subdomain with a dangling CNAME as existing for np ([#137](https://github.com/postalsys/mailauth/issues/137)) ([b38ecef](https://github.com/postalsys/mailauth/commit/b38ecef53b56e479863dd96cae1a6fe24c970353))
|
|
15
|
+
|
|
3
16
|
## [7.0.0](https://github.com/postalsys/mailauth/compare/v6.0.0...v7.0.0) (2026-09-27)
|
|
4
17
|
|
|
5
18
|
|
package/README.md
CHANGED
|
@@ -85,6 +85,7 @@ await authenticate(message [, options])
|
|
|
85
85
|
- **trustReceived** (`boolean`): If `true`, parses `ip` and `helo` from the latest `Received` header if not provided. The IP address is the connecting address from the TCP-info comment of the `from` clause (RFC 5321 section 4.4), never an address literal the client sent in its HELO. Postfix, Sendmail, Exim (including its `from [ip] (helo=...)` form) and similar formats are understood. Defaults to `false`.
|
|
86
86
|
- **mta** (`string`): Hostname of the server performing the authentication. Defaults to `os.hostname()`. Used as the authserv-id of the Authentication headers, so it should be a host name: an internationalized name is converted to A-labels, and a value that is not a valid token (for example one with a space or a semicolon) is written as a quoted string. With `strict` such a value throws an error with the code `EINVALIDAUTHSERVID` instead.
|
|
87
87
|
- **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. Keys with fewer bits will fail validation.
|
|
88
|
+
- **rejectRsaSha1** (`boolean`): If `true`, an rsa-sha1 DKIM signature gives `dkim=policy` with `policy.dkim-rules=weak-algorithm`, as in strict mode, and is not counted for DMARC. Every other check keeps the lenient default. It only affects verification. Defaults to `false`, and `strict` implies it.
|
|
88
89
|
- **disableArc** (`boolean`): If `true`, skips ARC checks.
|
|
89
90
|
- **disableDmarc** (`boolean`): If `true`, skips DMARC checks, also disabling dependent checks like BIMI.
|
|
90
91
|
- **disableBimi** (`boolean`): If `true`, skips BIMI checks.
|
|
@@ -151,7 +152,7 @@ You can see the full output, including structured data for DKIM, SPF, DMARC, and
|
|
|
151
152
|
|
|
152
153
|
By default mailauth is lenient where accepting non-conforming input costs little security, mostly weak signatures that still bind the signing domain to the content. With `strict: true` every check follows its RFC exactly.
|
|
153
154
|
|
|
154
|
-
Whatever the default mode accepts that strict mode would reject is marked in a `warnings` array on the affected result (for example `dkim.results[0].status.warnings` is `['rsa-sha1']`), so that you can apply your own policy without switching modes. The markers are never written into the generated headers.
|
|
155
|
+
Whatever the default mode accepts that strict mode would reject is marked in a `warnings` array on the affected result (for example `dkim.results[0].status.warnings` is `['rsa-sha1']`), so that you can apply your own policy without switching modes. The markers are never written into the generated headers. For rsa-sha1 verification the `rejectRsaSha1` option applies the strict rule on its own, which also keeps such a signature out of DMARC in `authenticate()`.
|
|
155
156
|
|
|
156
157
|
Some rules apply in both modes, because breaking them is a defect and not leniency. For DKIM these are: a signature must sign the From header, the key record's `h=` (hash algorithms) and `s=` (service types) restrictions are honored, `ed25519-sha1` is not an algorithm, and signing refuses a domain, selector or identity that would break out of its tag.
|
|
157
158
|
|
|
@@ -296,6 +297,7 @@ const result = await dkimVerify(message [, options]);
|
|
|
296
297
|
- **options** (optional):
|
|
297
298
|
- **resolver** (`async function`): Custom DNS resolver function. Defaults to `dns.promises.resolve`. A U-label signing domain is converted to A-labels before it is looked up (RFC 8616).
|
|
298
299
|
- **minBitLength** (`number`): Minimum allowed bits for RSA public keys. Defaults to `1024`. A shorter key gives `dkim=policy` with `policy.dkim-rules=weak-key`.
|
|
300
|
+
- **rejectRsaSha1** (`boolean`): If `true`, an rsa-sha1 signature gives `dkim=policy` with `policy.dkim-rules=weak-algorithm`, as in strict mode, while every other check keeps the lenient default. Signing is not affected. Defaults to `false`, and `strict` implies it.
|
|
299
301
|
- **sender** (`string`): Envelope sender. Defaults to the `Return-Path` header.
|
|
300
302
|
- **curTime** (`Date`): The time to check `t=` and `x=` against. Defaults to now.
|
|
301
303
|
- **strict** (`boolean`): If `true`, follows RFC 6376, RFC 8301 and RFC 8463 exactly. Defaults to `false`. See [Strict mode](#strict-mode).
|
package/bin/mailauth.js
CHANGED
|
@@ -104,6 +104,7 @@ program
|
|
|
104
104
|
.option('-x, --max-lookups <number>', 'Maximum allowed DNS lookups during SPF checks. Defaults to 10.', numberArg, 10)
|
|
105
105
|
.option('-z, --max-void-lookups <number>', 'Maximum allowed DNS lookups that return no data (void lookups) during SPF checks. Defaults to 2.', numberArg, 2)
|
|
106
106
|
.option('--strict', 'Follow the RFCs exactly instead of the lenient defaults (for example, reject rsa-sha1 DKIM signatures).')
|
|
107
|
+
.option('--reject-rsa-sha1', 'Report rsa-sha1 DKIM signatures as dkim=policy and do not count them for DMARC, while keeping the other lenient defaults.')
|
|
107
108
|
.action(runCommand(commandReport, 'Failed to generate report for the input message.'));
|
|
108
109
|
|
|
109
110
|
program
|
package/cli.md
CHANGED
|
@@ -87,6 +87,7 @@ mailauth report [options] [email]
|
|
|
87
87
|
- `--max-lookups number`, `-x number`: Sets the maximum number of DNS lookups for SPF checks. Defaults to `10`.
|
|
88
88
|
- `--max-void-lookups number`, `-z number`: Sets the maximum number of void DNS lookups for SPF checks. Defaults to `2`.
|
|
89
89
|
- `--strict`: Follows the RFCs exactly instead of the lenient defaults, for example an `rsa-sha1` DKIM signature is reported as `dkim=policy`. See [Strict mode](README.md#strict-mode).
|
|
90
|
+
- `--reject-rsa-sha1`: Reports an `rsa-sha1` DKIM signature as `dkim=policy` and does not count it for DMARC, as `--strict` does, while every other check keeps the lenient default.
|
|
90
91
|
|
|
91
92
|
#### Example
|
|
92
93
|
|
package/docs/dkim.md
CHANGED
|
@@ -91,7 +91,7 @@ Each entry in the `results` array has the following structure:
|
|
|
91
91
|
| `pass` | Signature verified successfully |
|
|
92
92
|
| `fail` | Signature verification failed (bad signature). In strict mode also a body hash mismatch |
|
|
93
93
|
| `neutral` | Signature could not be verified (missing key, expired, body hash mismatch in the default mode, invalid signature) |
|
|
94
|
-
| `policy` | Signature failed policy check (a key shorter than `minBitLength`, or rsa-sha1 in strict mode)
|
|
94
|
+
| `policy` | Signature failed policy check (a key shorter than `minBitLength`, or rsa-sha1 in strict mode or with `rejectRsaSha1`) |
|
|
95
95
|
| `temperror` | Temporary error (DNS failure) |
|
|
96
96
|
| `none` | Message not signed. In the default mode also a message whose only signatures use an unknown algorithm or canonicalization, or have no `d=` or `s=`. Strict mode reports those as `neutral` |
|
|
97
97
|
|
|
@@ -112,7 +112,7 @@ Common values for `status.comment`:
|
|
|
112
112
|
| `"inappropriate hash algorithm"` | The key record `h=` tag does not list the hash algorithm of the signature (both modes) |
|
|
113
113
|
| `"key not for email"` | The key record `s=` tag does not list `email` or `*` (both modes) |
|
|
114
114
|
| `"From field not signed"` | The `h=` tag does not include From (both modes, RFC 6376 section 6.1.1) |
|
|
115
|
-
| `"weak algorithm"` | Strict mode
|
|
115
|
+
| `"weak algorithm"` | Strict mode or `rejectRsaSha1`: rsa-sha1 signature (RFC 8301) |
|
|
116
116
|
| `"signature syntax error"` | Strict mode: the signature is not valid RFC 6376 syntax |
|
|
117
117
|
| `"signature missing required tag"` | Strict mode: `v=`, `a=`, `b=`, `bh=`, `d=`, `h=` or `s=` is missing |
|
|
118
118
|
| `"incompatible version"` | Strict mode: `v=` is not 1 |
|
package/docs/dmarc.md
CHANGED
|
@@ -61,7 +61,7 @@ On a `temperror` that happens after a record was found, `policy`, `p`, `sp`, `rr
|
|
|
61
61
|
|
|
62
62
|
`policyDomain` is the domain an RFC 9990 aggregate report lists as the domain of the published policy. By default it is also in `status.header.d`. `domain` is the Organizational Domain, which is a different domain when the policy came from the author domain's own record below it, or from a PSD above it.
|
|
63
63
|
|
|
64
|
-
`policy` is `p` for the author domain's own record. For a record inherited from the Organizational Domain or a PSD it is `np` when the author domain does not exist (NXDOMAIN, RFC 9989 A.4), otherwise `sp`, both falling back to `p`. The existence query is only made when the record has `np`. When it fails and `np` differs from the other policy, a message that passes still passes, `policy` is the weaker of the two and `policy.dmarc` is left out, and a message that does not pass gets `temperror`. With `t=y` the policy is one level below that, so `reject` becomes `quarantine` and `quarantine` becomes `none`, while `p`, `sp` and `np` stay as published.
|
|
64
|
+
`policy` is `p` for the author domain's own record. For a record inherited from the Organizational Domain or a PSD it is `np` when the author domain does not exist (NXDOMAIN, RFC 9989 A.4), otherwise `sp`, both falling back to `p`. The existence query is only made when the record has `np`. It asks for A, and after an NXDOMAIN for the name's own CNAME, because a name that is a CNAME exists even when its target does not. When it fails and `np` differs from the other policy, a message that passes still passes, `policy` is the weaker of the two and `policy.dmarc` is left out, and a message that does not pass gets `temperror`. With `t=y` the policy is one level below that, so `reject` becomes `quarantine` and `quarantine` becomes `none`, while `p`, `sp` and `np` stay as published.
|
|
65
65
|
|
|
66
66
|
A record without a valid `p`, or with an invalid `sp` or `np`, is applied as `p=none` when its `rua` lists a syntactically valid URI (RFC 3986, so `mailto:` counts and a bare address does not), and otherwise gets no DMARC processing (result `none`). `pct` is historic in RFC 9989 and is ignored.
|
|
67
67
|
|
package/index.d.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface AuthenticateOptions {
|
|
|
55
55
|
*/
|
|
56
56
|
minBitLength?: number;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* If true, rsa-sha1 DKIM signatures get `policy` (weak-algorithm) as in strict mode and are
|
|
60
|
+
* not counted for DMARC, while every other check keeps the lenient default. Verification
|
|
61
|
+
* only (default: false)
|
|
62
|
+
*/
|
|
63
|
+
rejectRsaSha1?: boolean;
|
|
64
|
+
|
|
58
65
|
/**
|
|
59
66
|
* Custom DNS resolver function
|
|
60
67
|
*/
|
|
@@ -1216,6 +1223,12 @@ export interface DKIMVerifyOptions {
|
|
|
1216
1223
|
*/
|
|
1217
1224
|
minBitLength?: number;
|
|
1218
1225
|
|
|
1226
|
+
/**
|
|
1227
|
+
* If true, rsa-sha1 signatures get `policy` (weak-algorithm) as in strict mode, while every
|
|
1228
|
+
* other check keeps the lenient default (default: false)
|
|
1229
|
+
*/
|
|
1230
|
+
rejectRsaSha1?: boolean;
|
|
1231
|
+
|
|
1219
1232
|
/**
|
|
1220
1233
|
* Current time for signature expiration checks
|
|
1221
1234
|
*/
|
package/lib/commands/report.js
CHANGED
|
@@ -61,6 +61,9 @@ class DkimVerifier extends MessageParser {
|
|
|
61
61
|
// follow RFC 6376, RFC 8301 and RFC 8463 exactly instead of the lenient defaults
|
|
62
62
|
this.strict = !!this.options.strict;
|
|
63
63
|
|
|
64
|
+
// RFC 8301 section 3.1 on its own, without the rest of the strict mode
|
|
65
|
+
this.rejectRsaSha1 = this.strict || !!this.options.rejectRsaSha1;
|
|
66
|
+
|
|
64
67
|
this.curTime = getCurTime(this.options.curTime);
|
|
65
68
|
|
|
66
69
|
this.results = [];
|
|
@@ -378,8 +381,8 @@ class DkimVerifier extends MessageParser {
|
|
|
378
381
|
|
|
379
382
|
if (signatureHeader.hashAlgo === 'sha1') {
|
|
380
383
|
// RFC 8301 section 3.1: rsa-sha1 MUST NOT be used for verifying. Only the
|
|
381
|
-
// strict mode
|
|
382
|
-
// with the h= tag of the key record
|
|
384
|
+
// strict mode and rejectRsaSha1 reject it, the lenient mode lets the key owner
|
|
385
|
+
// opt out of it with the h= tag of the key record
|
|
383
386
|
warn('rsa-sha1');
|
|
384
387
|
}
|
|
385
388
|
}
|
|
@@ -510,7 +513,7 @@ class DkimVerifier extends MessageParser {
|
|
|
510
513
|
// The lenient mode keeps reporting neutral, as it always has
|
|
511
514
|
status.result = this.strict ? 'fail' : 'neutral';
|
|
512
515
|
status.comment = `body hash did not verify`;
|
|
513
|
-
} else if (this.
|
|
516
|
+
} else if (this.rejectRsaSha1 && signatureHeader.type === 'DKIM' && signatureHeader.hashAlgo === 'sha1') {
|
|
514
517
|
// RFC 8301 section 3.1, "rsa-sha1 MUST NOT be used for signing or verifying"
|
|
515
518
|
status.result = 'policy';
|
|
516
519
|
status.comment = 'weak algorithm';
|
|
@@ -54,18 +54,32 @@ const getPolicyTags = record => {
|
|
|
54
54
|
|
|
55
55
|
// Domain existence test (RFC 9989 A.4): any RR makes a name exist, only NXDOMAIN means it
|
|
56
56
|
// does not. A query for A tells the two apart, NODATA is an existing name without A records.
|
|
57
|
+
// A query for A follows a CNAME and gets the target's NXDOMAIN when the target is gone (RFC 6604 3),
|
|
58
|
+
// so after an NXDOMAIN the name's own CNAME, which is not followed, is looked up as well.
|
|
57
59
|
const domainExists = async (domain, resolver) => {
|
|
58
60
|
if (!isValidName(domain)) {
|
|
59
61
|
return false;
|
|
60
62
|
}
|
|
63
|
+
resolver = resolver || dns.resolve;
|
|
61
64
|
try {
|
|
62
|
-
await
|
|
65
|
+
await resolver(domain, 'A');
|
|
63
66
|
return true;
|
|
64
67
|
} catch (err) {
|
|
65
68
|
if (err.code === 'ENODATA') {
|
|
66
69
|
return true;
|
|
67
70
|
}
|
|
68
|
-
if (err.code === '
|
|
71
|
+
if (err.code === 'EBADNAME') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (err.code !== 'ENOTFOUND') {
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return ((await resolver(domain, 'CNAME')) || []).length > 0;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (NO_RECORD_CODES.has(err.code)) {
|
|
82
|
+
// no CNAME, so the NXDOMAIN for A was the name's own
|
|
69
83
|
return false;
|
|
70
84
|
}
|
|
71
85
|
throw err;
|
package/lib/mailauth.js
CHANGED
|
@@ -24,6 +24,7 @@ const { isIP } = require('net');
|
|
|
24
24
|
* @param {String} [opts.helo] Hostname from EHLO/HELO
|
|
25
25
|
* @param {String} [opts.mta] MTA/MX hostname (defaults to os.hostname)
|
|
26
26
|
* @param {Number} [opts.minBitLength=1024] Minimal allowed length of public keys. If DKIM/ARC key is smaller, then verification fails
|
|
27
|
+
* @param {Boolean} [opts.rejectRsaSha1=false] If true then rsa-sha1 DKIM signatures get dkim=policy (weak-algorithm) as in strict mode (RFC 8301), the other checks stay lenient
|
|
27
28
|
* @param {Object} [opts.seal] ARC sealing options
|
|
28
29
|
* @param {String} [opts.seal.signingDomain] ARC key domain name
|
|
29
30
|
* @param {String} [opts.seal.selector] ARC key selector
|
|
@@ -49,6 +50,7 @@ const authenticate = async (input, opts) => {
|
|
|
49
50
|
sender: opts.sender, // defaults to Return-Path header
|
|
50
51
|
seal: opts.seal,
|
|
51
52
|
minBitLength: opts.minBitLength,
|
|
53
|
+
rejectRsaSha1: opts.rejectRsaSha1,
|
|
52
54
|
strict: opts.strict
|
|
53
55
|
});
|
|
54
56
|
|