Hello,
We are writing an independent DKIM2 verifier in Rust (OxiMail), against
-06. Implementing the Chain of Custody checks, we found a gap around the
nd= tag that lets anyone replay a signed message they merely received.
What the text says
- 9.4: "the MAIL FROM value recorded in every DKIM2-Signature header
field (except of course the i=1 instance) can be matched with a RCPT
TO
value of the next lower numbered DKIM2-Signature header field."
- 8.7: "when an nd= tag is present both mf= and rt= MUST be omitted."
- 9.3: "the creation of an extra DKIM2-Signature header field (whether
the nd= scheme is used or not) will require the Signer to have access
to a DKIM2 private key associated with a domain in the RCPT TO entry
of
the message as it arrived."
The nd= signature carries no mf=, so 9.4 never checks it against the rt=
below it. The 9.3 sentence describes what an honest signer needs; no
verifier check enforces it.
The replay
1. A sender signs i=1 for a message to [email protected]
(d=sender.example, [email protected]). The attacker only
reads that mailbox; they hold no key for mailbox.example.
2. The attacker adds i=2 with d=evil.example and nd=evil.example, and
no mf= or rt=. This is allowed by 8.7, and no written rule relates
evil.example to the rt= of i=1.
3. The attacker adds i=3 with d=evil.example, [email protected] and
[email protected], and sends it to target.example. Here:
- d= matches the nd= of i=2 (8.7);
- the envelope matches the mf= and rt= of i=3 exactly (9.2, 11.4);
- under 9.4, the mf= of i=3 has no rt= to be compared with at i=2.
Either a verifier skips the check (the chain passes) or it fails it
(no forwarder can ever use nd=). The text says neither.
With the chain passing, target.example sees sender.example's signed
message delivered to a recipient it was never sent to. This is the
replay DKIM2 sets out to close.
A suggested fix
For a DKIM2-Signature carrying nd=:
A Verifier MUST check that there is a relaxed domain match (9.4)
between the d= of the signature carrying nd= and a domain of the rt=
of the next lower numbered DKIM2-Signature header field; and the
signature that follows it is exempt from the 9.4 check against the
signature carrying nd=, since its d= is bound by nd= instead.
This turns the 9.3 sentence into a check. Our verifier applies this
rule. So does the Python verifier of the dkim2wg interop repository: it
already refuses an nd= signer that holds no domain of the previous rt=.
This suggests the intent was always there; only the text is missing.
Two smaller points, in case they are useful:
- 8.1 and 11.2 disagree on whether gaps in the i= numbering are
acceptable.
- The error templates are written both as "PERMERROR " and as
"PERMERROR:".
Best regards,
Julien Frère
https://oximail.ch
_______________________________________________
Ietf-dkim mailing list -- [email protected]
To unsubscribe send an email to [email protected]