Hi Mauro,
Thank you very much for your reply and for adding PhoenixDKIM to the
interop test suite. I will definitely have to keep up with the moving
spec now, which is great!
With regards to the failure you noticed: Isn't that exactly what spec-03
requires? §9.1.3 now reads as a MUST — a Message-Instance has to carry
either recipes that recreate the message at the current top hashes, or
an explicit null recipe to say reconstruction isn't possible. A
non-first instance with no r= at all is neither of those, so PhoenixDKIM
treats the chain as broken there rather than guessing at what the hop did.
Those two forms are handled as follows by PhoenixDKIM:
- an explicit null recipe: It stops the backward walk and passes on what
it verified down to that hop;
- an empty/identity recipe: It reconstructs prev == current and
hash-checks, which a genuinely transparent hop passes cleanly.
So, I don't think the Go/Python output is far off — it's just omitting
r= entirely on the transparent second hop, where the spec wants one of
those two.
Alternatively, per §9.2.5, a hop that changes nothing MAY simply not add
an instance at all, which is what PhoenixDKIM's own signer does on a
plain re-sign.
Of course, there is this ambiguity in wording between two paragraphs:
§9.1.3 says MUST (recipes or null recipe), but §9.1.6 still says SHOULD
("all other Message-Instance header fields SHOULD contain at least one
recipe").
I went with the MUST, as that one was introduced in spec-03.
Am I doing this correctly, or am I missing something?
Kind regards,
Edmund
On 2026/07/04 19:31, Mauro De Gennaro wrote:
Hi Edmund,
Thank you for putting the message through PhoenixDKIM.
Regarding the failed DKIM2 signature from my previous email you tried, that was
entirely expected as the IETF DKIM lists do not handle DKIM2 signatures yet. If
you want to exercise real interop, you have two options. One is to install the
Rust toolchain and run mail-auth directly. The other, and probably the quicker
one, is the playground at https://mail-auth.stalw.art. It will sign a message
with any Ed25519 or RSA key you provide, straight from the browser, so you can
feed the output into PhoenixDKIM without building anything.
On folding: I went ahead and added PhoenixDKIM to the mail-auth interop test
suite, and everything passed, including the folded cases. When mail-auth emits
folded DKIM2-Signature and Message-Instance headers, PhoenixDKIM unfolds and
verifies them correctly across single-hop and multi-hop chains, for both
Ed25519 and RSA. So your handling of whitespace in the base64 looks solid from
this side too.
I did find a failure in PhoenixDKIM but not against mail-auth. The interop test
suite builds a single container with four implementations (mail-auth, the Go
and Python references, and now PhoenixDKIM), then runs a matrix: every
implementation signs a message, and every implementation verifies each result,
across single-hop, two-hop and three-hop chains. In the plain matrix the
message is relayed unchanged between hops, so the intermediate hops are
transparent re-signs.
That last detail is what trips PhoenixDKIM when the producer is the Go or Python
tool. On a transparent re-sign, those two add a fresh Message-Instance (m>=2)
with no recipe, and PhoenixDKIM's verifier rejects any non-first Message-Instance
that carries no recipe, with:
PERMERROR: Message-Instance m=N carries no recipe
It failed on all 14 multi-hop combinations where a relaying (non-originator)
hop was produced by Go or Python. For what it is worth, I think PhoenixDKIM's
verifier is on reasonable ground here, and mail-auth agrees with you in spirit.
Section 9.1 says a forwarder that leaves the hashes unchanged SHOULD NOT add a
new Message-Instance at all. mail-auth follows that: on an unmodified hop it
adds only a DKIM2-Signature and reuses the existing m, so there is never a
recipe-less instance to reject. PhoenixDKIM's own signer does the same. It is
really the Go and Python signers that emit the recipe-less instance, and then
your verifier flags it.
To reproduce without the full suite: sign a message as originator with the Go
tool, then run the Go signer again over that output without touching the body
(a transparent second hop), then verify with phoenixdkim2-verify. You will get
the PERMERROR above. The Python signer behaves the same way. If you would
rather see all 14 at once, run the mail-auth interop matrix with the skip guard
I added (skip_phoenix_vs_gopy) disabled:
cargo test --lib dkim2::interop_test::dkim2_interop_matrix -- --ignored
--nocapture
Finally, to answer your question about Stalwart, no, DKIM2 is not enabled by
default, and it will not be until DKIM2 becomes an RFC. Anyone who wants to try
it has to turn it on manually, which is only a few clicks. The reason for the
caution is that the draft is still moving. It changed in ways that broke
signatures in the past, and it may well change again, so I would rather users
opt in knowingly than have signatures silently break under them on an upgrade.
Thanks again for the testing and the thoughtful feedback. The PhoenixDKIM link
is noted, I will keep testing against it.
Best,
Mauro
--
Edmund Lodewijks <[email protected]>
TZ: UTC+2 / GMT+2
_______________________________________________
Ietf-dkim mailing list -- [email protected]
To unsubscribe send an email to [email protected]