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]

Reply via email to