Vulnerability analysis · DKIM
An ed25519-sha256 signature passes against a record that declares rsa, and against one that declares nothing and so defaults to rsa. Rspamd and dkimpy reject both.
| Software | OpenDKIM |
|---|---|
| Vendor | The Trusted Domain Project |
| Source | https://github.com/trusteddomainproject/OpenDKIM |
| Affected | OpenDKIM 2.11.0 (Debian package 2.11.0~beta2-9.1+b1). Rspamd 4.1.2 and dkimpy 1.1.8 both reject the same records correctly. Newest published release checked on 2026-07-30. |
| Where | DKIM public-key record handling, the k= key-type tag of RFC 6376 section 3.6.1 and its binding to the signature algorithm |
| Weakness | CWE-347 (Improper Verification of Cryptographic Signature) / CWE-757 (Selection of Less-Secure Algorithm During Negotiation) |
| Reachable by | Remote. The signature algorithm is chosen by whoever signs; the key record is published by the key owner. No interaction with a third party is required. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N (3.7 (Low)) |
| Verification | Reproduced on 2026-07-28 against the current release (run 20260728-120957) with a scripted reproducer, negative controls, and a second independent implementation as the differential oracle. Confirmed three ways, with Rspamd and dkimpy as independent oracles, three stable repetitions, and a negative control in which the key bytes themselves are wrong. |
A DKIM key record states which kind of key it publishes. RFC 6376 section 3.6.1 defines the k= tag for that purpose, gives it a default of rsa, and makes it govern how the p= public-key data is to be read. RFC 8463 section 3 ties ed25519-sha256 signatures to records that declare k=ed25519. A verifier is therefore supposed to compare the algorithm implied by the signature's a= tag against the type the record declares, and to reject the pair when the two disagree. RFC 6376 section 6.1.2 requires that rejection when the key data is unsuitable for the algorithm.
OpenDKIM never reads k=. It picks the algorithm from the a= tag of the signature and then reads the p= bytes as a key for that algorithm. A record declaring k=rsa will verify an ed25519 signature. So will a record that declares no key type at all, which under the RFC is an RSA record. Both return dkim=pass.
The message and the p= key data are the same in every row below: one correct ed25519 public key. The record's k= declaration and the signature's a= tag are the only things that change.
| Key record | Signature a= | OpenDKIM | Rspamd | dkimpy | Required |
|---|---|---|---|---|---|
k=ed25519 (control) | ed25519-sha256 | pass | pass | pass | pass |
k=rsa | ed25519-sha256 | dkim=pass (header.a=ed25519-sha256) | R_DKIM_PERMFAIL | fail | fail |
no k= (RFC default rsa) | ed25519-sha256 | dkim=pass | R_DKIM_PERMFAIL | fail | fail |
k=ed25519 with RSA key data in p= | rsa-sha256 | pass | fail | fail | fail |
Row three states the defect most plainly. That record names ed25519 nowhere, so under section 3.6.1 it declares an RSA key. OpenDKIM still verifies an ed25519 signature against it and reports the message as DKIM-authenticated. Row four shows the same failure running the other way: a record declaring k=ed25519 whose p= holds RSA key data verifies an RSA signature.
The cryptography itself still runs. A k=rsa record carrying a different ed25519 public key in p= fails on OpenDKIM, which is the negative control. What OpenDKIM drops is the k= declaration alone; the signature is still checked against the key bytes that were published. Among the three implementations tested, OpenDKIM is the only one that accepts the mismatched records. Rspamd and the dkimpy reference implementation both reject them, as section 6.1.2 requires.
that the RFC default of rsa applies.
sel._domainkey.signer.example. IN TXT "v=DKIM1; p=<base64 ed25519 public key>"
DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed; d=signer.example; s=sel;
h=from:to:subject:date; bh=<body hash>; b=<signature>
declares an RSA key by default, the key data is unsuitable for ed25519-sha256, and the verifier fails the signature.
dkim=pass with header.a=ed25519-sha256. Rspamdreturns R_DKIM_PERMFAIL. dkimpy fails verification.
Writing the type out as "v=DKIM1; k=rsa; p=<base64 ed25519 public key>" gives the same result. Putting a different ed25519 key in p= makes OpenDKIM fail, which is the control that proves the signature check itself still runs.
This is a conformance and robustness fail-open. It is not a victim-spoofing bypass and must not be reported as one. The k= and p= record is published by the owner of the signing domain, and that same party produces the signature. Ignoring k= therefore hands an attacker no way to sign for a domain they do not control. A dmarc=pass that follows is for the signer's own aligned domain, and the signer could have obtained it by publishing a correct k=ed25519 record.
Two effects do follow. The first is a split in how receivers see the same mail. A domain whose record still says k=rsa after an ed25519 migration, or whose k= and p= disagree for any other reason, is DKIM-authenticated on OpenDKIM receivers and unauthenticated on Rspamd, dkimpy, Gmail, and every other conforming verifier. The operator gets no signal that the two populations disagree.
The second is the loss of a defense-in-depth control. A key record is supposed to be able to pin a key to one algorithm. On OpenDKIM that restriction does nothing. The moment it matters most is an algorithm-agility transition, which is exactly when a stale declaration is most likely to be sitting in DNS.
Take the key type from the record's k= tag. Apply the RFC 6376 section 3.6.1 default of rsa when the tag is absent. Reject the signature when the algorithm implied by a= does not match that type, per section 6.1.2 and RFC 8463 section 3. The key record decides the algorithm; the signature only confirms it.
The framing here is narrower than the initial triage on purpose. An early note called this a full DKIM and DMARC bypass. That framing is withdrawn. The dmarc=pass observed is for the signer's own aligned domain, and ignoring k= yields no victim-spoofing scenario.
An earlier laboratory lead treated a k=ed25519 versus RSA mismatch as ambiguous under the RFC and did not promote it. The no-k= case in the table settles the question, because the default of rsa is written into section 3.6.1 and needs no interpretation.
Two harness details matter to anyone reproducing this. OpenDKIM's Authentication-Results value has to be parsed after unfolding, since the dkim= token can land on a folded continuation line. Rspamd's R_DKIM_PERMFAIL symbol has to be read directly, not through a client wrapper that collapses it. Both were fixed in the reproducer before the result was recorded.
Two steps remain open: confirming the behavior against OpenDKIM HEAD, and locating the point in the code where a= is used without consulting the record's k=.