Hi Edmund, Yes, your implementation is correct, this an issue in the Go and Python libraries which are producing non-conformant output. It’s just that my implementation is more permissive and accepts recipe-less instances, but perhaps it shouldn’t.
Best, Mauro > On 5 Jul 2026, at 16:39, Edmund Lodewijks <[email protected]> wrote: > > 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] _______________________________________________ Ietf-dkim mailing list -- [email protected] To unsubscribe send an email to [email protected]
