mailauth 6.0.0 → 7.0.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 +12 -0
- package/README.md +111 -39
- package/bin/mailauth.js +14 -6
- package/cli.md +8 -3
- package/docs/arc.md +126 -43
- package/docs/bimi.md +54 -37
- package/docs/dkim.md +79 -40
- package/docs/dmarc.md +80 -33
- package/docs/mta-sts.md +140 -42
- package/docs/spf.md +69 -4
- package/index.d.ts +495 -43
- package/lib/arc/index.d.ts +10 -8
- package/lib/arc/index.js +505 -124
- package/lib/bimi/index.js +162 -81
- package/lib/commands/report.js +2 -1
- package/lib/commands/seal.js +35 -5
- package/lib/commands/sign.js +8 -1
- package/lib/commands/spf.js +8 -0
- package/lib/dkim/body/relaxed.js +6 -5
- package/lib/dkim/body/simple.js +1 -3
- package/lib/dkim/dkim-signer.js +219 -33
- package/lib/dkim/dkim-verifier.js +574 -231
- package/lib/dkim/header/relaxed.js +6 -43
- package/lib/dkim/header/signature-opts.js +87 -0
- package/lib/dkim/header/simple.js +6 -43
- package/lib/dkim/message-parser.js +18 -4
- package/lib/dkim/sign.d.ts +13 -7
- package/lib/dkim/sign.js +18 -2
- package/lib/dkim/syntax.js +31 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dmarc/author-domain.js +181 -0
- package/lib/dmarc/get-dmarc-record.js +140 -47
- package/lib/dmarc/index.d.ts +29 -2
- package/lib/dmarc/verify.js +120 -44
- package/lib/mailauth.js +75 -36
- package/lib/mta-sts.d.ts +77 -11
- package/lib/mta-sts.js +497 -182
- package/lib/parse-dkim-headers.js +376 -191
- package/lib/parse-received.js +104 -32
- package/lib/spf/index.d.ts +37 -1
- package/lib/spf/index.js +260 -28
- package/lib/spf/macro.js +50 -13
- package/lib/spf/spf-verify.js +440 -204
- package/lib/spf/syntax.js +366 -0
- package/lib/tools.js +522 -116
- package/package.json +1 -1
package/docs/arc.md
CHANGED
|
@@ -14,25 +14,30 @@ const { arc } = await authenticate(message, {
|
|
|
14
14
|
});
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
With `strict: true` the validation and sealing follow RFC 8617 exactly, see [Strict Mode](#strict-mode). The default mode is lenient about tag syntax, and marks what it accepted in the `warnings` array of the result (see [Warnings](#warnings)).
|
|
18
|
+
|
|
17
19
|
## Validation Result Object Fields
|
|
18
20
|
|
|
19
|
-
| Field | Type | Presence | Description
|
|
20
|
-
| ----------------------- | --------------- | ----------------- |
|
|
21
|
-
| `status` | `object` | Always | Validation status object (see below)
|
|
22
|
-
| `chain` | `array` | Non-enumerable | Array of ARC chain entries (hidden from JSON serialization)
|
|
23
|
-
| `i` | `number\|false` | Always | Last instance number in chain, or `false` if no chain
|
|
24
|
-
| `signature` | `object` | Chain exists | Verification result for last ARC-Message-Signature
|
|
25
|
-
| `authenticationResults` | `object` | Chain exists | Parsed last ARC-Authentication-Results
|
|
26
|
-
| `info` | `string` | Result not "none" | Formatted Authentication-Results header value
|
|
21
|
+
| Field | Type | Presence | Description |
|
|
22
|
+
| ----------------------- | --------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `status` | `object` | Always | Validation status object (see below) |
|
|
24
|
+
| `chain` | `array` | Non-enumerable | Array of ARC chain entries (hidden from JSON serialization) |
|
|
25
|
+
| `i` | `number\|false` | Always | Last instance number in chain, or `false` if no chain |
|
|
26
|
+
| `signature` | `object` | Chain exists | Verification result for last ARC-Message-Signature |
|
|
27
|
+
| `authenticationResults` | `object` | Chain exists | Parsed last ARC-Authentication-Results |
|
|
28
|
+
| `info` | `string` | Result not "none" | Formatted Authentication-Results header value. In strict mode also for "none" |
|
|
29
|
+
| `warnings` | `string[]` | Default mode | What was accepted that strict mode would reject, see [Warnings](#warnings). Not set for a failing chain, never written into `info` |
|
|
30
|
+
| `sealErrors` | `array` | Sealing failed | Why the message was not sealed when `authenticate()` was asked to seal it (see [Sealing](#sealing)) |
|
|
27
31
|
|
|
28
32
|
## status Object
|
|
29
33
|
|
|
30
|
-
| Field | Type | Presence | Description
|
|
31
|
-
| ------------ | --------- | ------------ |
|
|
32
|
-
| `result` | `string` | Always | ARC result code (see below)
|
|
33
|
-
| `comment` | `string` | On pass/fail | Description or error message
|
|
34
|
-
| `shouldSeal` | `boolean` | On fail | Whether to continue sealing despite failure
|
|
35
|
-
| `policy` | `object` | Policy issue | Policy violation details (e.g., `{"dkim-rules": "weak-key"}`)
|
|
34
|
+
| Field | Type | Presence | Description |
|
|
35
|
+
| ------------ | --------- | ------------ | ---------------------------------------------------------------- |
|
|
36
|
+
| `result` | `string` | Always | ARC result code (see below) |
|
|
37
|
+
| `comment` | `string` | On pass/fail | Description or error message |
|
|
38
|
+
| `shouldSeal` | `boolean` | On fail | Whether to continue sealing despite failure |
|
|
39
|
+
| `policy` | `object` | Policy issue | Policy violation details (e.g., `{"dkim-rules": "weak-key"}`) |
|
|
40
|
+
| `smtp` | `object` | Strict mode | `{"remote-ip": "192.0.2.1"}`, the client IP (RFC 8617 section 6) |
|
|
36
41
|
|
|
37
42
|
## authenticationResults Object
|
|
38
43
|
|
|
@@ -56,31 +61,109 @@ When ARC validation passes, the `authenticationResults` object contains parsed r
|
|
|
56
61
|
|
|
57
62
|
## Comment Values (on failure)
|
|
58
63
|
|
|
59
|
-
| Comment Pattern
|
|
60
|
-
|
|
|
61
|
-
| `"i={n} seal signature validation failed"`
|
|
62
|
-
| `"i={n} no valid signature"`
|
|
63
|
-
| `"i={n} multiple {header} values"`
|
|
64
|
-
| `"chain-length={n}"`
|
|
65
|
-
| `"i={n} expected={m}"`
|
|
66
|
-
| `"i={n} no {header} set"`
|
|
67
|
-
| `"i=1 cv={value}"`
|
|
68
|
-
| `"i={n} cv={value}"`
|
|
69
|
-
| `"i={n}
|
|
70
|
-
| `"i={n} invalid ams h"`
|
|
71
|
-
| `"
|
|
72
|
-
| `"
|
|
73
|
-
| `"
|
|
74
|
-
| `"
|
|
75
|
-
| `"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
64
|
+
| Comment Pattern | Description |
|
|
65
|
+
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| `"i={n} seal signature validation failed"` | ARC-Seal cryptographic verification failed |
|
|
67
|
+
| `"i={n} no valid signature"` | ARC-Message-Signature verification failed |
|
|
68
|
+
| `"i={n} multiple {header} values"` | Duplicate ARC headers for same instance |
|
|
69
|
+
| `"chain-length={n}"` | Chain exceeds 50 instances |
|
|
70
|
+
| `"i={n} expected={m}"` | Missing or out-of-order instance numbers |
|
|
71
|
+
| `"i={n} no {header} set"` | Missing required ARC header |
|
|
72
|
+
| `"i=1 cv={value}"` | First instance must have `cv=none` |
|
|
73
|
+
| `"i={n} cv={value}"` | Non-first instance must have `cv=pass`, and a newest seal with `cv=fail` fails the chain |
|
|
74
|
+
| `"i={n} unexpected as h"` | ARC-Seal has an `h=` tag |
|
|
75
|
+
| `"i={n} invalid ams h"` | ARC-Message-Signature signed arc-seal (forbidden) |
|
|
76
|
+
| `"i={n} as {problem}"`, `"i={n} ams {problem}"` | Strict mode: a tag-list syntax error, a missing required tag or a malformed `t=`/`x=`/`l=` |
|
|
77
|
+
| `"invalid as instance"`, `"invalid ams instance"`, `"invalid aar instance"` | Strict mode: an ARC header field whose `i=` is not 1 to 50 written as 1*2DIGIT (first in ARC-Authentication-Results) |
|
|
78
|
+
| `"no key for {domain}"` | DNS key not found |
|
|
79
|
+
| `"unknown key version for {domain}"` | Key record `v=` is not `DKIM1` |
|
|
80
|
+
| `"unknown key type for {domain}"` | Unsupported key type |
|
|
81
|
+
| `"invalid public key for {domain}"` | Malformed public key |
|
|
82
|
+
| `"inappropriate hash algorithm for {domain}"` | Key record `h=` does not list sha256 |
|
|
83
|
+
| `"key not for email for {domain}"` | Key record `s=` does not list email |
|
|
84
|
+
| `"inappropriate key algorithm for {domain}"` | The key type does not match the seal's `a=` algorithm |
|
|
85
|
+
| `"weak key for {domain}"` | RSA key too short |
|
|
86
|
+
|
|
87
|
+
An ARC-Message-Signature that does not sign the From header field is not valid (RFC 6376 section 6.1.1), which fails the chain with `"i={n} no valid signature"` in both modes. An ARC-Seal `c=` tag is not defined by RFC 8617, so it is ignored like any other unknown tag.
|
|
88
|
+
|
|
89
|
+
## Strict Mode
|
|
90
|
+
|
|
91
|
+
These rules apply in both modes:
|
|
92
|
+
|
|
93
|
+
- `cv` values are compared case-insensitively (`cv=Pass` is `pass`), and every seal of the chain is validated.
|
|
94
|
+
- An Ed25519 seal signs the SHA-256 hash of the canonicalized data (RFC 8463).
|
|
95
|
+
- The seal's `a=` algorithm has to match the key type, and the key record's `v=`, `h=` and `s=` tags are honored.
|
|
96
|
+
- An ARC-Message-Signature without `c=` uses `simple/simple`, the DKIM-Signature default.
|
|
97
|
+
|
|
98
|
+
Differences between the modes:
|
|
99
|
+
|
|
100
|
+
| Input | Default | `strict: true` |
|
|
101
|
+
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------- |
|
|
102
|
+
| Duplicate tags, empty tag-specs, invalid tag names in ARC-Seal or the newest ARC-Message-Signature | accepted, warning `arc-tag-syntax` | `fail` |
|
|
103
|
+
| Upper case tag names (`S=` for `s=`) | read as lower case, warning `arc-tag-syntax` | unknown tag, so usually `fail` for the missing tag |
|
|
104
|
+
| Missing required tags, malformed `t=`, `x=` or `l=` | warning `arc-tag-syntax` (a missing tag usually fails anyway) | `fail` |
|
|
105
|
+
| `i=` written as `0x1`, `+1`, `1e0` or `1.0` | read as a number, warning `arc-instance-syntax` | `fail` |
|
|
106
|
+
| ARC header field with an `i=` that is missing or not a number from 1 to 50 | ignored, warning `arc-instance-syntax` | `fail` |
|
|
107
|
+
| ARC-Authentication-Results whose `i=` is not first | accepted, warning `arc-instance-syntax` | `fail` |
|
|
108
|
+
| ARC-Message-Signature without `c=` that only verifies as `relaxed/relaxed` (the ARC drafts' default) | accepted, warning `ams-c-default` | `fail` |
|
|
109
|
+
| No ARC chain | no `arc=` entry in Authentication-Results | `arc=none` |
|
|
110
|
+
| Client IP | not reported | `smtp.remote-ip` |
|
|
111
|
+
|
|
112
|
+
The position of `i=` in an ARC-Seal or ARC-Message-Signature is not checked in either mode. The RFC 8617 ABNF puts it first, but a tag-list is unordered (RFC 6376 section 3.2) and the ARC test suite places it elsewhere in all of its valid chains.
|
|
113
|
+
|
|
114
|
+
## Warnings
|
|
115
|
+
|
|
116
|
+
In the default mode `warnings` lists what was accepted although strict mode would have rejected it. It is only present when there is something to report and the chain did not fail, and it is never written into the Authentication-Results text.
|
|
117
|
+
|
|
118
|
+
| Warning | Meaning |
|
|
119
|
+
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `arc-tag-syntax` | An ARC-Seal or the newest ARC-Message-Signature has a tag-list syntax error or an upper case tag name |
|
|
121
|
+
| `arc-instance-syntax` | An `i=` value is not valid RFC 8617 syntax, or an ARC header field was ignored because of its `i=` |
|
|
122
|
+
| `ams-c-default` | The ARC-Message-Signature has no `c=` and only verified with `relaxed/relaxed` |
|
|
123
|
+
| `key-syntax`, `key-v-syntax`, `key-type-inferred` | Key record leniencies, see the [DKIM result reference](dkim.md#warnings) |
|
|
124
|
+
|
|
125
|
+
## Sealing
|
|
126
|
+
|
|
127
|
+
`sealMessage(message, seal)` returns the ARC headers to prepend as a Buffer, and an empty Buffer when no ARC set was created. `createSeal(message, { seal, strict })` returns the reason as well:
|
|
128
|
+
|
|
129
|
+
| Field | Type | Description |
|
|
130
|
+
| ---------- | ---------- | ------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `headers` | `string[]` | ARC headers to prepend: `[ARC-Seal, ARC-Message-Signature, ARC-Authentication-Results]`, or empty |
|
|
132
|
+
| `errors` | `array` | `{ err, type, selector, signingDomain }` entries saying why no set was created |
|
|
133
|
+
| `warnings` | `string[]` | What the default mode sealed that strict mode would have refused |
|
|
134
|
+
|
|
135
|
+
Seal options:
|
|
136
|
+
|
|
137
|
+
| Option | Type | Description |
|
|
138
|
+
| --------------- | ------------------ | ---------------------------------------------------------------------------------------------------------- |
|
|
139
|
+
| `signingDomain` | `string` | `d=` value |
|
|
140
|
+
| `selector` | `string` | `s=` value |
|
|
141
|
+
| `privateKey` | `string\|Buffer` | RSA or Ed25519 private key |
|
|
142
|
+
| `algorithm` | `string` | `rsa-sha256` or `ed25519-sha256`, follows the key type when not set |
|
|
143
|
+
| `authResults` | `string` | The Authentication-Results payload for the ARC-Authentication-Results header |
|
|
144
|
+
| `cv` | `string` | `none`, `pass` or `fail`. Defaults to `none` for the first set, required for later ones |
|
|
145
|
+
| `i` | `number` | Instance, defaults to one more than the highest instance on the message |
|
|
146
|
+
| `headerList` | `string\|string[]` | Header fields for the ARC-Message-Signature. ARC header fields and Authentication-Results are never signed |
|
|
147
|
+
| `signTime` | `Date` | Signing time |
|
|
148
|
+
| `strict` | `boolean` | Follow the RFCs exactly |
|
|
149
|
+
|
|
150
|
+
No ARC set is created (in either mode) when:
|
|
151
|
+
|
|
152
|
+
| Error code | Reason |
|
|
153
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
|
154
|
+
| `EARCCHAINFAILED` | The newest ARC-Seal on the message already has `cv=fail` (RFC 8617 section 5.1 step 2) |
|
|
155
|
+
| `EINVALIDINSTANCE` | The instance is not 1 to 50, already exists on the message, or the chain already has 50 sets |
|
|
156
|
+
| `EINVALIDCV` | `cv` is not `none`, `pass` or `fail`, is missing for an instance above 1, or is `pass` while the chain on the message could not be parsed |
|
|
157
|
+
| `EINVALIDALGO` | The algorithm is not `rsa-sha256` or `ed25519-sha256` |
|
|
158
|
+
| `EINVALIDTYPE` | The algorithm does not match the key type |
|
|
159
|
+
|
|
160
|
+
What only strict mode refuses (the default mode seals and adds the warning):
|
|
161
|
+
|
|
162
|
+
| Input | Warning |
|
|
163
|
+
| ------------------------------------------------------------ | ------------------ |
|
|
164
|
+
| An explicit `i` that leaves a gap after the highest instance | `arc-instance-gap` |
|
|
165
|
+
| `cv=none` for an instance above 1, or not `none` for `i=1` | `arc-cv-instance` |
|
|
166
|
+
| An RSA key shorter than 1024 bits | `weak-key` |
|
|
84
167
|
|
|
85
168
|
## Example Output
|
|
86
169
|
|
|
@@ -169,8 +252,8 @@ const sealHeaders = await sealMessage(message, {
|
|
|
169
252
|
cv: 'pass'
|
|
170
253
|
});
|
|
171
254
|
|
|
172
|
-
// sealHeaders is a Buffer containing:
|
|
173
|
-
// ARC-Seal: i=
|
|
174
|
-
// ARC-Message-Signature: i=
|
|
175
|
-
// ARC-Authentication-Results: i=
|
|
255
|
+
// sealHeaders is a Buffer containing, for a message that already had one ARC set:
|
|
256
|
+
// ARC-Seal: i=2; a=rsa-sha256; cv=pass; d=example.com; s=arc; ...
|
|
257
|
+
// ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=example.com; ...
|
|
258
|
+
// ARC-Authentication-Results: i=2; mx.example.com; spf=pass; dkim=pass
|
|
176
259
|
```
|
package/docs/bimi.md
CHANGED
|
@@ -4,7 +4,15 @@ This document describes the result object returned by BIMI (Brand Indicators for
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
BIMI allows organizations to display brand logos in email clients. BIMI information is resolved during the authentication step, provided the message passes DMARC validation with
|
|
7
|
+
BIMI allows organizations to display brand logos in email clients. BIMI information is resolved during the authentication step, provided the message passes DMARC validation with an enforcing policy. Record discovery and the result keywords follow [draft-brand-indicators-for-message-identification-14](https://datatracker.ietf.org/doc/html/draft-brand-indicators-for-message-identification). Local-part selectors (`lps=`) are not supported.
|
|
8
|
+
|
|
9
|
+
Before looking up a BIMI record, mailauth checks the requirements of section 7.1 of the draft:
|
|
10
|
+
|
|
11
|
+
- The message has one From header field with one address. DMARC evaluates several addresses that share a domain, BIMI does not.
|
|
12
|
+
- DMARC passed, with an effective policy other than `none` and without `t=y`.
|
|
13
|
+
- Neither the DMARC record of the author domain nor the one of its Organizational Domain has `p=none` or `sp=none`, or `p=quarantine` with a `pct` other than 100. A record found at the author domain itself does not hide a lax record of the Organizational Domain. `dmarc()` passes both records to `bimi()`, for a DMARC result built elsewhere `bimi()` looks up the Organizational Domain's record itself.
|
|
14
|
+
|
|
15
|
+
The record is looked up at `<selector>._bimi.<author domain>` and then at `<selector>._bimi.<organizational domain>`, where the selector comes from the `BIMI-Selector` header and defaults to `default`. A custom selector does not fall back to `default`. TXT records that do not start with `v=BIMI1` are ignored, several BIMI records are a failure.
|
|
8
16
|
|
|
9
17
|
```javascript
|
|
10
18
|
const { authenticate } = require('mailauth');
|
|
@@ -29,12 +37,12 @@ const { bimi } = await authenticate(message, {
|
|
|
29
37
|
|
|
30
38
|
## status Object
|
|
31
39
|
|
|
32
|
-
| Field | Type | Presence
|
|
33
|
-
| --------- | -------- |
|
|
34
|
-
| `result` | `string` | Always
|
|
35
|
-
| `comment` | `string` | On skip/fail | Reason for skip or failure |
|
|
36
|
-
| `header` | `object` | Always
|
|
37
|
-
| `policy` | `object` | VMC found
|
|
40
|
+
| Field | Type | Presence | Description |
|
|
41
|
+
| --------- | -------- | ---------------------- | ---------------------------- |
|
|
42
|
+
| `result` | `string` | Always | BIMI result code (see below) |
|
|
43
|
+
| `comment` | `string` | On skip/fail/temperror | Reason for skip or failure |
|
|
44
|
+
| `header` | `object` | Always | Header information |
|
|
45
|
+
| `policy` | `object` | VMC found | Authority policy details |
|
|
38
46
|
|
|
39
47
|
### status.header Object
|
|
40
48
|
|
|
@@ -52,39 +60,51 @@ const { bimi } = await authenticate(message, {
|
|
|
52
60
|
|
|
53
61
|
## Result Values
|
|
54
62
|
|
|
55
|
-
| Result
|
|
56
|
-
|
|
|
57
|
-
| `pass`
|
|
58
|
-
| `skipped`
|
|
59
|
-
| `fail`
|
|
60
|
-
| `none`
|
|
61
|
-
| `
|
|
63
|
+
| Result | Description |
|
|
64
|
+
| ----------- | -------------------------------------------------------------- |
|
|
65
|
+
| `pass` | BIMI record found and valid |
|
|
66
|
+
| `skipped` | BIMI lookup skipped (see skip reasons below) |
|
|
67
|
+
| `fail` | BIMI record found but invalid |
|
|
68
|
+
| `none` | No BIMI record found |
|
|
69
|
+
| `declined` | The domain published a Declination to Publish (`v=BIMI1; l=;`) |
|
|
70
|
+
| `temperror` | Temporary error during a DNS lookup |
|
|
71
|
+
|
|
72
|
+
Earlier versions reported a DNS error as `temperr`, a keyword the draft does not define. It is now `temperror`, the keyword of the draft and of the other methods in RFC 8601.
|
|
62
73
|
|
|
63
74
|
## Skip Reasons
|
|
64
75
|
|
|
65
76
|
The `status.comment` field explains why BIMI was skipped:
|
|
66
77
|
|
|
67
|
-
| Comment
|
|
68
|
-
|
|
|
69
|
-
| `"DMARC not enabled"`
|
|
70
|
-
| `"message failed DMARC"`
|
|
71
|
-
| `"
|
|
72
|
-
| `"
|
|
73
|
-
| `"
|
|
74
|
-
| `"
|
|
78
|
+
| Comment | Description |
|
|
79
|
+
| ---------------------------------------------------- | -------------------------------------------------------- |
|
|
80
|
+
| `"DMARC not enabled"` | DMARC result was `none` |
|
|
81
|
+
| `"message failed DMARC"` | DMARC result was not `pass` |
|
|
82
|
+
| `"multiple From addresses"` | More than one From header field or address |
|
|
83
|
+
| `"too lax DMARC policy"` | DMARC policy is `none`, or the record has `t=y` |
|
|
84
|
+
| `"too lax DMARC subdomain policy"` | The author or Organizational Domain record has `sp=none` |
|
|
85
|
+
| `"DMARC policy applied to a percentage of messages"` | `p=quarantine` with a `pct` other than 100 |
|
|
86
|
+
| `"Aligned DKIM signature required"` | `bimiWithAlignedDkim` option set but no aligned DKIM |
|
|
87
|
+
| `"undersized DKIM signature"` | DKIM signature has unsigned body bytes (due to `l=` tag) |
|
|
88
|
+
| `"could not determine domain"` | Unable to extract domain from headers |
|
|
75
89
|
|
|
76
90
|
## Fail Reasons
|
|
77
91
|
|
|
78
|
-
| Comment | Description
|
|
79
|
-
| ------------------------------------------- |
|
|
80
|
-
| `"multiple BIMI-Selector headers"` | Message has more than one BIMI-Selector header
|
|
81
|
-
| `"missing bimi version in selector header"` | BIMI-Selector header missing `v=BIMI1`
|
|
82
|
-
| `"missing bimi version in dns record"` | DNS record missing `v=BIMI1`
|
|
83
|
-
| `"missing location value in dns record"` | Record has
|
|
84
|
-
| `"invalid location value in dns record"` | `l=` value is not a valid HTTPS URL
|
|
85
|
-
| `"invalid authority value in dns record"` | `a=` value is not a valid HTTPS URL
|
|
86
|
-
| `"
|
|
87
|
-
|
|
92
|
+
| Comment | Description |
|
|
93
|
+
| ------------------------------------------- | ------------------------------------------------------------------------ |
|
|
94
|
+
| `"multiple BIMI-Selector headers"` | Message has more than one BIMI-Selector header |
|
|
95
|
+
| `"missing bimi version in selector header"` | BIMI-Selector header missing `v=BIMI1` |
|
|
96
|
+
| `"missing bimi version in dns record"` | DNS record missing `v=BIMI1` |
|
|
97
|
+
| `"missing location value in dns record"` | Record has no `l=` value (and is not a declination), even if it has `a=` |
|
|
98
|
+
| `"invalid location value in dns record"` | `l=` value is not a valid HTTPS URL |
|
|
99
|
+
| `"invalid authority value in dns record"` | `a=` value is not a valid HTTPS URL |
|
|
100
|
+
| `"multiple BIMI records for {name}"` | More than one `v=BIMI1` record at the name |
|
|
101
|
+
|
|
102
|
+
## Temperror Reasons
|
|
103
|
+
|
|
104
|
+
| Comment | Description |
|
|
105
|
+
| ------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
106
|
+
| `"failed to resolve {name}"` | DNS lookup error for a BIMI record |
|
|
107
|
+
| `"failed to resolve the DMARC policy of the organizational domain"` | The DMARC record of the Organizational Domain could not be looked up |
|
|
88
108
|
|
|
89
109
|
## VMC Validation Result
|
|
90
110
|
|
|
@@ -210,12 +230,9 @@ These headers should be added to messages after successful BIMI validation. The
|
|
|
210
230
|
{
|
|
211
231
|
"status": {
|
|
212
232
|
"result": "none",
|
|
213
|
-
"header": {
|
|
214
|
-
"selector": "default",
|
|
215
|
-
"d": "example.com"
|
|
216
|
-
}
|
|
233
|
+
"header": {}
|
|
217
234
|
},
|
|
218
|
-
"info": "bimi=none
|
|
235
|
+
"info": "bimi=none"
|
|
219
236
|
}
|
|
220
237
|
```
|
|
221
238
|
|
package/docs/dkim.md
CHANGED
|
@@ -9,17 +9,19 @@ The `dkimVerify` function verifies all DKIM-Signature headers in an email messag
|
|
|
9
9
|
```javascript
|
|
10
10
|
const { dkimVerify } = require('mailauth/lib/dkim/verify');
|
|
11
11
|
|
|
12
|
-
const result = await dkimVerify(message);
|
|
12
|
+
const result = await dkimVerify(message, { strict: false });
|
|
13
13
|
// result.results is an array of signature verification results
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
With `strict: true` the verification follows RFC 6376, RFC 8301 and RFC 8463 exactly. The default mode is lenient about weak and non-conforming signatures, and marks what it accepted in `status.warnings` (see [Warnings](#warnings)).
|
|
17
|
+
|
|
16
18
|
## Top-Level Result Object
|
|
17
19
|
|
|
18
|
-
| Field | Type | Description
|
|
19
|
-
| -------------- | --------------- |
|
|
20
|
-
| `headerFrom` | `string[]` |
|
|
21
|
-
| `envelopeFrom` | `string\|false` | Email address from Return-Path header or sender option
|
|
22
|
-
| `results` | `object[]` | Array of verification results for each DKIM signature
|
|
20
|
+
| Field | Type | Description |
|
|
21
|
+
| -------------- | --------------- | ---------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| `headerFrom` | `string[]` | Addresses from the From header, as full addr-specs. The mailboxes of a group (RFC 6854) are included |
|
|
23
|
+
| `envelopeFrom` | `string\|false` | Email address from Return-Path header or sender option |
|
|
24
|
+
| `results` | `object[]` | Array of verification results for each DKIM signature |
|
|
23
25
|
|
|
24
26
|
## Signature Result Object
|
|
25
27
|
|
|
@@ -61,51 +63,88 @@ Each entry in the `results` array has the following structure:
|
|
|
61
63
|
|
|
62
64
|
### status Object
|
|
63
65
|
|
|
64
|
-
| Field | Type | Presence | Description
|
|
65
|
-
| ------------ | --------------- | --------------- |
|
|
66
|
-
| `result` | `string` | Always | Verification result code (see below)
|
|
67
|
-
| `comment` | `string` | On error/info | Human-readable explanation
|
|
68
|
-
| `aligned` | `string\|false` | DKIM signatures | DMARC-aligned domain, or false
|
|
69
|
-
| `header` | `object` | Always | Signature header info
|
|
70
|
-
| `policy` | `object` | Policy result | Policy violation details
|
|
71
|
-
| `underSized` | `number` | Body limited | Number of unsigned bytes
|
|
66
|
+
| Field | Type | Presence | Description |
|
|
67
|
+
| ------------ | --------------- | --------------- | ----------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `result` | `string` | Always | Verification result code (see below) |
|
|
69
|
+
| `comment` | `string` | On error/info | Human-readable explanation |
|
|
70
|
+
| `aligned` | `string\|false` | DKIM signatures | DMARC-aligned domain, or false |
|
|
71
|
+
| `header` | `object` | Always | Signature header info |
|
|
72
|
+
| `policy` | `object` | Policy result | Policy violation details |
|
|
73
|
+
| `underSized` | `number` | Body limited | Number of unsigned bytes |
|
|
74
|
+
| `testing` | `boolean` | Key has `t=y` | The signing domain is testing DKIM (RFC 6376 section 3.6.1) |
|
|
75
|
+
| `warnings` | `string[]` | Default mode | What was accepted that strict mode would reject, see [Warnings](#warnings). Never written into `info` |
|
|
72
76
|
|
|
73
77
|
#### status.header Object
|
|
74
78
|
|
|
75
|
-
| Field | Type | Description
|
|
76
|
-
| ----- | --------------- |
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
79
|
+
| Field | Type | Description |
|
|
80
|
+
| ----- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `d` | `string` | Strict mode only: the signing domain (`d=` tag) |
|
|
82
|
+
| `i` | `string\|false` | Signing domain with @ prefix (e.g., `"@example.com"`). In strict mode the AUID: the `i=` value, or `@` and the signing domain when there is no `i=` (RFC 8601 section 2.7.1) |
|
|
83
|
+
| `s` | `string` | DKIM selector |
|
|
84
|
+
| `a` | `string` | Algorithm |
|
|
85
|
+
| `b` | `string` | First 8 characters of signature value |
|
|
81
86
|
|
|
82
87
|
## Result Values
|
|
83
88
|
|
|
84
|
-
| Result | Description
|
|
85
|
-
| ----------- |
|
|
86
|
-
| `pass` | Signature verified successfully
|
|
87
|
-
| `fail` | Signature verification failed (bad signature)
|
|
88
|
-
| `neutral` | Signature could not be verified (missing key, expired, body hash mismatch)
|
|
89
|
-
| `policy` | Signature failed policy check (
|
|
90
|
-
| `temperror` | Temporary error (DNS failure)
|
|
91
|
-
| `none` | Message not signed
|
|
89
|
+
| Result | Description |
|
|
90
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
91
|
+
| `pass` | Signature verified successfully |
|
|
92
|
+
| `fail` | Signature verification failed (bad signature). In strict mode also a body hash mismatch |
|
|
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) |
|
|
95
|
+
| `temperror` | Temporary error (DNS failure) |
|
|
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` |
|
|
92
97
|
|
|
93
98
|
## Comment Values
|
|
94
99
|
|
|
95
100
|
Common values for `status.comment`:
|
|
96
101
|
|
|
97
|
-
| Comment
|
|
98
|
-
|
|
|
99
|
-
| `"body hash did not verify"`
|
|
100
|
-
| `"bad signature"`
|
|
101
|
-
| `"invalid expiration"`
|
|
102
|
-
| `"expired"`
|
|
103
|
-
| `"no key"`
|
|
104
|
-
| `"unknown key version"`
|
|
105
|
-
| `"unknown key type"`
|
|
106
|
-
| `"invalid public key"`
|
|
107
|
-
| `"
|
|
108
|
-
| `"
|
|
102
|
+
| Comment | Description |
|
|
103
|
+
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
104
|
+
| `"body hash did not verify"` | Calculated body hash does not match `bh=` tag |
|
|
105
|
+
| `"bad signature"` | Cryptographic signature verification failed |
|
|
106
|
+
| `"invalid expiration"` | Expiration timestamp is before signing timestamp |
|
|
107
|
+
| `"expired"` | Signature has expired (past `x=` timestamp) |
|
|
108
|
+
| `"no key"` | No DKIM key found in DNS |
|
|
109
|
+
| `"unknown key version"` | Unsupported key version in DNS record |
|
|
110
|
+
| `"unknown key type"` | Unsupported key type in DNS record |
|
|
111
|
+
| `"invalid public key"` | Public key in DNS record is malformed, or revoked (empty `p=`) |
|
|
112
|
+
| `"inappropriate hash algorithm"` | The key record `h=` tag does not list the hash algorithm of the signature (both modes) |
|
|
113
|
+
| `"key not for email"` | The key record `s=` tag does not list `email` or `*` (both modes) |
|
|
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: rsa-sha1 signature (RFC 8301) |
|
|
116
|
+
| `"signature syntax error"` | Strict mode: the signature is not valid RFC 6376 syntax |
|
|
117
|
+
| `"signature missing required tag"` | Strict mode: `v=`, `a=`, `b=`, `bh=`, `d=`, `h=` or `s=` is missing |
|
|
118
|
+
| `"incompatible version"` | Strict mode: `v=` is not 1 |
|
|
119
|
+
| `"domain mismatch"` | Strict mode: the `i=` domain is not `d=` or its subdomain, or is a subdomain while the key has `t=s` |
|
|
120
|
+
| `"unsupported query method"` | Strict mode: `q=` does not list `dns/txt` |
|
|
121
|
+
| `"inappropriate key algorithm"` | Strict mode: the key type does not match the signature algorithm |
|
|
122
|
+
| `"unknown algorithm"`, `"unknown canonicalization"` | Strict mode: the signature can not be processed |
|
|
123
|
+
| `"DNS failure: {code}"` | DNS lookup failed with error code |
|
|
124
|
+
| `"message not signed"` | No DKIM-Signature headers found |
|
|
125
|
+
|
|
126
|
+
## Warnings
|
|
127
|
+
|
|
128
|
+
In the default mode `status.warnings` lists what was accepted although strict mode would have rejected it. The array is only present when there is something to report, and it is never written into the Authentication-Results text.
|
|
129
|
+
|
|
130
|
+
| Warning | Meaning |
|
|
131
|
+
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| `rsa-sha1` | The signature uses rsa-sha1, which RFC 8301 section 3.1 does not allow. A domain can refuse it with `h=sha256` in its key record, which applies in both modes |
|
|
133
|
+
| `tag-syntax` | Duplicate tags, upper case tag names, a malformed `t=`, `x=` or `l=` value, or a `d=` or `s=` that is not a domain name |
|
|
134
|
+
| `missing-v`, `missing-h` (and the other `missing-*`) | A required tag is missing. Without `h=` the default header list is used, which includes From |
|
|
135
|
+
| `invalid-v` | `v=` is not `1` |
|
|
136
|
+
| `identity-domain` | The `i=` domain is not `d=` or its subdomain, or it is a subdomain while the key record has `t=s` |
|
|
137
|
+
| `invalid-expiration` | `x=` is equal to `t=`. An `x=` before `t=` is `neutral (invalid expiration)` in both modes |
|
|
138
|
+
| `query-method` | `q=` does not list `dns/txt` |
|
|
139
|
+
| `key-syntax` | The key record is not valid tag-list syntax |
|
|
140
|
+
| `key-v-syntax` | The key record `v=` is not the first tag, or not exactly `DKIM1` |
|
|
141
|
+
| `key-type-inferred` | An Ed25519 key was recognized from its length, the key record has no `k=ed25519` |
|
|
142
|
+
|
|
143
|
+
A negative `l=` value is read as no limit in the default mode, so the whole body is covered and nothing is left unsigned. Strict mode reports it as a syntax error.
|
|
144
|
+
|
|
145
|
+
## Tag parsing
|
|
146
|
+
|
|
147
|
+
DKIM-Signature headers and key records are tag-lists (RFC 6376 section 3.2): semicolons separate the tags, and quotes, parentheses and backslashes are ordinary value characters. A note such as `n=it's` or a copied header in `z=` with a parenthesis in it does not affect the other tags. When the signature is hashed, only the value of its own `b=` tag is emptied, even when another tag contains the text `b=`.
|
|
109
148
|
|
|
110
149
|
## Example Output
|
|
111
150
|
|