Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions content/momentum/4/dkim2.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
lastUpdated: "07/07/2026"
lastUpdated: "08/16/2026"
title: "Using DKIM2 — Overview"
description: "DKIM2 is the successor to DKIM that adds replay protection (per-message envelope binding), an explicit chain of custody across forwarders, and a structured way for modifying hops to record what they changed. Momentum implements DKIM2 targeting draft-ietf-dkim-dkim2-spec-04."
---
Expand Down Expand Up @@ -203,14 +203,21 @@ reference and more complete policy examples.

DKIM2 reuses the DKIM1 key infrastructure. Keys are PEM-encoded RSA or
Ed25519 private keys, supplied either as a file path (`keyfile`) or as
raw PEM bytes in memory (`keybuf`). The matching public key is published in DNS
at `<selector>._domainkey.<domain>` as a TXT record with the standard
RFC 6376 §3.6.1 format (`v=DKIM1; k=rsa; p=<base64-SPKI>`).
raw PEM bytes in memory (`keybuf`). The matching public key is
published in DNS at `<selector>._domainkey.<domain>` as a TXT record —
`v=DKIM1; k=rsa; p=<base64-SPKI>` for RSA, `v=DKIM1; k=ed25519;
p=<base64-raw-key>` for Ed25519. The two `p=` encodings are not
interchangeable; see
[Publishing the public key](/momentum/4/dkim2/sign#publishing-the-public-key).

If you already publish DKIM1 keys at a selector, you can reuse the same
selector for DKIM2 without any DNS change. To generate fresh keys for
DKIM2 specifically, follow the standard openssl recipe in
[Generating DKIM Keys](/momentum/4/using-dkim#generating-dkim-keys).
selector for DKIM2 without any DNS change. To generate a fresh RSA key
pair, follow the recipe in
[Generating DKIM Keys](/momentum/4/using-dkim#generating-dkim-keys)
it predates DKIM2's Ed25519 support and does not cover that case. For
Ed25519, generate the key pair with `openssl genpkey -algorithm ED25519
-out /etc/dkim2/ed25519.key`, then publish the public half following
[Publishing the public key](/momentum/4/dkim2/sign#publishing-the-public-key).

### Note

Expand Down
4 changes: 2 additions & 2 deletions content/momentum/4/dkim2/debug.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
lastUpdated: "06/09/2026"
lastUpdated: "08/16/2026"
title: "DKIM2 Debugging Reference"
description: "Per-signature reason codes, recipe_chain detail strings, and ec_message context fields for DKIM2 sign and verify operations."
---
Expand All @@ -18,6 +18,6 @@ dkim2 {
| Level | What surfaces |
|---|---|
| `error` | Failures and resolver problems only. **Default.** |
| `warning` | Adds DNS issues and SHOULD-violation warnings. |
| `warning` | Adds DNS issues (including non-conformant key records Momentum tolerates for compatibility) and SHOULD-violation warnings. |
| `info` | Adds one DNS resolution line per verified signature plus verification failures with their cause (`bh_mismatch` with expected vs. actual hash; `sig_invalid` with selector, algorithm, signed-input length, and OpenSSL detail). |
| `debug` | Adds raw TXT-record bytes from the resolver, a per-crypto-check trace line, and the raw signed-input bytes on failure. Too noisy for steady-state production; useful when chasing a specific sign/verify mismatch. |
97 changes: 96 additions & 1 deletion content/momentum/4/dkim2/sign.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
lastUpdated: "07/27/2026"
lastUpdated: "08/16/2026"
title: "DKIM2 Signing — sign()"
description: "Reference for the msys.validate.dkim2.sign() Lua API: hook selection, sign options, forwarder and modifier signing."
---
Expand Down Expand Up @@ -187,6 +187,101 @@ of the options table.

Always check the first return value. On `nil`, no headers were modified. Recipe validation failure and content-changed-without-recipe also log to paniclog at level `error`.

### Publishing the public key

Publish one TXT record per sig-set. A verifier does one lookup per
sig-set, at `<selector>._domainkey.<domain>` — the selector from that
sig-set's entry in `s=`, the domain from the signature's `d=` — and
reads the public key from the record's `p=` tag. A multi-algorithm
`DKIM2-Signature` therefore needs one record per selector it names.
DKIM2 reuses a subset of the DKIM v1 key-record format (RFC 6376
§3.6.1), so the record's `v=` stays `DKIM1` — for compatibility with
existing key deployment (draft-chuang-dkim2-dns-04 §3.4.1). The
`DKIM2-Signature` header carries no version tag of its own; the
generation is identified by the field name
(draft-ietf-dkim-dkim2-spec-04 §8).

**The `p=` encoding is not the same for both algorithms.** Publish the
form shown below for each. For `k=ed25519` that form is exactly what
the specs require; for `k=rsa` it is deployed practice rather than the
RFC's normative text, as explained below. The one form Momentum accepts
that other verifiers will not is covered under
[Upgrading from an earlier 5.3 build](#upgrading-from-an-earlier-53-build).

For `rsa-sha256`, `p=` is the base64 of a SubjectPublicKeyInfo — the
body of the PEM with the `-----BEGIN/END-----` lines and all whitespace
removed. (RFC 6376 §3.6.1, and draft-chuang-dkim2-dns-04 §3.4.1 to the
same effect, call for a bare `RSAPublicKey` instead — but RFC
6376's own Appendix C recipe emits a SubjectPublicKeyInfo, and so does
every deployed signer; errata 6674 and 7001 record the discrepancy in
RFC 6376. Publish the SPKI form below.)

```bash
openssl rsa -in /etc/dkim2/rsa.key -pubout | sed '/-----/d' | tr -d '\n'
```

```
sel-rsa._domainkey.example.com. IN TXT (
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
"...the rest of the base64, continued in as many further quoted"
"...strings as it takes, none exceeding 255 octets..." )
```

A DNS character-string holds at most 255 octets (RFC 1035 §3.3), and
an RSA `p=` easily runs past that on its own — 392 base64 characters
plus the `v=DKIM1; k=rsa; p=` prefix is 410 octets at 2048 bits, more
at larger key sizes — so it must be split across as many adjacent
quoted strings as needed, none over the limit. Verifiers concatenate
them, Momentum included. Ed25519 records never need this — 44
characters fit in one string.

For `ed25519-sha256`, `p=` is the base64 of the **bare 32-byte public
key** — no ASN.1 wrapper of any kind, so always 44 characters. This
comes from draft-chuang-dkim2-dns-04 §3.4.1, which
draft-ietf-dkim-dkim2-spec-04 §3.6 defers to for the key-record format,
and identically from RFC 8463 §4.2.

`openssl` has no output format for the bare key — `-outform` is
PEM or DER only — but an Ed25519 SubjectPublicKeyInfo is a fixed
12-byte prefix followed by the key, so the last 32 bytes of the DER
form are exactly it:

```bash
openssl pkey -in /etc/dkim2/ed25519.key -pubout -outform DER | tail -c 32 | base64
```

```
sel-ed25519._domainkey.example.com. IN TXT (
"v=DKIM1; k=ed25519; p=11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=" )
```

An empty `p=` signals deliberate key revocation.

#### Upgrading from an earlier 5.3 build

Momentum 5.3 builds before this fix read every `p=` as a
SubjectPublicKeyInfo, including for `k=ed25519`. A deployment that
published an Ed25519 key in that form to make such a build verify its
own mail has a record that conformant verifiers reject. Momentum still
accepts it, but the record should be republished in the raw 32-byte
form above. Nothing needs to change for `k=rsa`.

The quickest check is the record itself: a conformant Ed25519 `p=` is
44 base64 characters, the SubjectPublicKeyInfo form 60.

Momentum can also report it, but only from the **verifying** side —
the warning fires when a key record is resolved during verification,
so a deployment that only signs will never see it. On a Momentum that
verifies mail carrying the selector, set `debug_level = "warning"` on
the `dkim2` stanza and it logs a `DWARNING` naming the selector each
time it resolves one. The shipped default is `error`, at which this
warning does **not** appear — see
[Debugging](/momentum/4/dkim2/debug).

Replace the record; do not publish both forms side by side. Two TXT
records at one selector is a §11.5 PERMERROR, so a verifier that would
have accepted either accepts neither.

### Forwarder and modifier signing

The chain-of-custody link between adjacent signatures is a **relaxed,
Expand Down
19 changes: 17 additions & 2 deletions content/momentum/4/dkim2/verify.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
lastUpdated: "07/27/2026"
lastUpdated: "08/16/2026"
title: "DKIM2 Verifying — verify()"
description: "Reference for the msys.validate.dkim2.verify() Lua API: verify options, result table, and SMTP response codes."
---
Expand Down Expand Up @@ -262,7 +262,7 @@ The full set. Unless otherwise noted, each reason code below pairs with `status=
| `key_multiple_records` | DNS returned more than one TXT record for the selector (§11.5). DNS admin misconfiguration on the sender side — only one TXT record is allowed per selector. |
| `key_service_mismatch` | The DNS TXT record's `s=` service list does not include `email` or `*` (RFC 6376 §3.6.1). The key is published for a different service. |
| `key_invalid` | The DNS TXT record was present but structurally unusable (empty content, internal resolver error, or selector/domain too long to query). |
| `key_der_parse` | The `p=` base64 decoded successfully but the DER structure is not a valid public key. |
| `key_der_parse` | The `p=` base64 decoded successfully but the bytes are not a usable public key for the record's `k=`. For `k=rsa` that means they are not a valid DER SubjectPublicKeyInfo. For `k=ed25519` it means they are neither the bare 32-byte key the spec requires nor a SubjectPublicKeyInfo — see [Publishing the public key](/momentum/4/dkim2/sign#publishing-the-public-key). |
| `key_k_unknown` | The DNS record's `k=` tag names an algorithm Momentum doesn't support. |
| `key_alg_mismatch` | The resolved public key's type doesn't match the signature's named algorithm — e.g. an `rsa-sha256` sig-set verified against a `k=ed25519` key (or vice versa), a DNS record whose `k=` tag disagrees with the actual `p=` key, or an RSA-PSS public key; maps to `dkim2=permerror`. |
| `key_v_mismatch` | The DNS TXT record's `v=` tag does not match the expected value. Malformed or wrong-version key record. Maps to `dkim2=permerror`. |
Expand All @@ -280,6 +280,21 @@ The full set. Unless otherwise noted, each reason code below pairs with `status=

`reason=` is included in all failure clauses (`dkim2=fail`, `dkim2=permerror`, `dkim2=temperror`) and absent from pass clauses (`dkim2=pass`).

**Non-conformant Ed25519 key records**: a `k=ed25519` record must carry
the bare 32-byte public key in `p=` (RFC 8463 §4.2,
draft-chuang-dkim2-dns-04 §3.4.1), not a DER SubjectPublicKeyInfo.
Momentum 5.3 builds before this fix instead expected a
SubjectPublicKeyInfo, so a sender running such a build against its own
mail may have published that form. Momentum accepts both — the
signature still verifies — and at `debug_level = "warning"` or a more
verbose level logs a `DWARNING` naming the selector whenever it
resolves the SubjectPublicKeyInfo form, since that record fails at
other verifiers and only the publishing sender can correct it. The
default level is `error`, at which the warning is not emitted — see
`debug_level` in [Debugging](/momentum/4/dkim2/debug). See
[Publishing the public key](/momentum/4/dkim2/sign#publishing-the-public-key)
for which form to publish.

### recipe_chain detail strings (paniclog only)

When the recipe-chain check fails, the overall verdict is `permerror`
Expand Down
Loading