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

> On 4 Jul 2026, at 13:56, Edmund Lodewijks <[email protected]> wrote:
> 
> Hi Mauro,
> 
> Nice work — and thanks for signing the message too, I used it as a test.
> 
> I ran it through PhoenixDKIM (a C implementation based off OpenDKIM
> I've been working on) and it doesn't pass, though that was to be
> expected: The signature itself verifies fine, but the IETF list rewrote the
> envelope sender and the From/Subject without adding a DKIM2 hop,
> so the chain is broken by the time it reaches me.
> 
> On the folding side, PhoenixDKIM seems to cope with the cases you hit —
> it ignores whitespace inside the base64, and it lands on lowercase recipe
> keys like Steve's does, so +1 for the spec just saying lowercase.
> 
> Is Stalwart enabling DKIM2 by default already?
> 
> If you'd like to poke at it:
> 
>  https://github.com/edmundlod/PhoenixDKIM/tree/feature/dkim2
> 
> Kind regards,
>    Edmund
> 
> 
> On 2026/07/04 09:25, Mauro De Gennaro wrote:
>> Hi all,
>> I wanted to share some news from implementing DKIM2, along with a few 
>> findings that came out of interop testing. Incidentally, this message is 
>> itself DKIM2 signed, so if your tooling supports it, feel free to use it as 
>> a small real-world sample.
>> I have added DKIM2 support (draft-ietf-dkim-dkim2-spec-03) to mail-auth, the 
>> Rust library for email authentication, which lives at 
>> https://github.com/stalwartlabs/mail-auth. One thing worth mentioning is 
>> that mail-auth also compiles to WebAssembly, so it can be used from 
>> JavaScript applications and other WASM environments. To make it easy to try, 
>> I put together a playground at https://mail-auth.stalw.art/ that signs and 
>> validates DKIM2, as well as DMARC, entirely from the browser, resolving the 
>> records using DNS-over-HTTP.
>> DKIM2 is now also supported in Stalwart mail server 
>> (https://github.com/stalwartlabs/stalwart/), which uses mail-auth 
>> internally. Beyond signing and verification, I have implemented inbound DSN 
>> validation for DKIM2 as described in the draft, so that delivery status 
>> notifications coming back along the chain can themselves be authenticated.
>> On interoperability, I tested mail-auth against Bron's Python implementation 
>> (https://github.com/dkim2wg/interop.git) and Steve's Go implementation 
>> (https://forge.turscar.ie/turscar/dkim2.git). The integration tests are at 
>> https://github.com/stalwartlabs/mail-auth/blob/main/src/dkim2/interop_test.rs
>>  and they exercise signing, verification and recipe generation across all 
>> three implementations, in both directions, including multi-hop chains. The 
>> good news is that with header folding disabled, every combination passes: 
>> signing, verifying and recipe based reconstruction all agree across the 
>> three implementations.
>> I did hit one issue, when mail-auth folds the DKIM2-Signature and 
>> Message-Instance header fields, which is expected behaviour for long fields 
>> such as the base64 signature, both the Python and the Go implementations 
>> fail to parse them. The draft describes in section 2.12 that folding 
>> whitespace is allowed inside base64 strings and must be ignored when the 
>> value is used, so I believe mail-auth's output is conformant here, and it 
>> round-trips cleanly through my own parser. In Steve's Go library the mf= and 
>> rt= values are base64 decoded in strict mode without first removing the 
>> folding whitespace, which trips on the embedded space or tab. In Bron's 
>> Python library the s= value is split on the colon without unfolding first, 
>> so the algorithm token ends up carrying the leading CRLF and tab, and the 
>> algorithm comparison then fails. Neither looks hard to fix, and as noted, 
>> disabling folding on my side makes all tests pass.
>> Finally, a couple of small notes on the spec itself. Overall I found it 
>> clear, but a few of the sections that describe algorithms in prose would 
>> benefit from some accompanying pseudocode, in particular the parts covering 
>> recipe application and the chain walk during verification. It would remove a 
>> little ambiguity for implementers. The other point concerns header field 
>> names in recipes. It is not entirely clear from the text whether the JSON 
>> keys must be lowercased or may retain their original case. Steve's 
>> implementation expects lowercase and rejects mixed case, whereas I initially 
>> generated recipes preserving the original header case, which the current 
>> wording about matching without regard to case seemed to permit. A sentence 
>> stating the expected behaviour would avoid this divergence.
>> Thanks to everyone working on this. It has been a pleasure to implement, and 
>> I am happy to help with test vectors or further interop testing.
>> Best,
>> Mauro
>> _______________________________________________
>> Ietf-dkim mailing list -- [email protected]
>> To unsubscribe send an email to [email protected]
> 
> 
> -- 
> Edmund Lodewijks <[email protected]>
> TZ: UTC+2 / GMT+2
> 
> _______________________________________________
> Ietf-dkim mailing list -- [email protected]
> To unsubscribe send an email to [email protected]

_______________________________________________
Ietf-dkim mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to