diff --git a/content/momentum/4/dkim2.md b/content/momentum/4/dkim2.md index 78eb4963..fd6e2eb6 100644 --- a/content/momentum/4/dkim2.md +++ b/content/momentum/4/dkim2.md @@ -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." --- @@ -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 `._domainkey.` as a TXT record with the standard -RFC 6376 §3.6.1 format (`v=DKIM1; k=rsa; p=`). +raw PEM bytes in memory (`keybuf`). The matching public key is +published in DNS at `._domainkey.` as a TXT record — +`v=DKIM1; k=rsa; p=` for RSA, `v=DKIM1; k=ed25519; +p=` 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 diff --git a/content/momentum/4/dkim2/debug.md b/content/momentum/4/dkim2/debug.md index 91256a11..2271fb01 100644 --- a/content/momentum/4/dkim2/debug.md +++ b/content/momentum/4/dkim2/debug.md @@ -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." --- @@ -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. | diff --git a/content/momentum/4/dkim2/sign.md b/content/momentum/4/dkim2/sign.md index 8811978a..78a1a4ca 100644 --- a/content/momentum/4/dkim2/sign.md +++ b/content/momentum/4/dkim2/sign.md @@ -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." --- @@ -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 `._domainkey.` — 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, diff --git a/content/momentum/4/dkim2/verify.md b/content/momentum/4/dkim2/verify.md index 68fd6cc1..e5d6e28d 100644 --- a/content/momentum/4/dkim2/verify.md +++ b/content/momentum/4/dkim2/verify.md @@ -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." --- @@ -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`. | @@ -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`