Vulnerability analysis · DKIM
The signature is valid and says rsa-sha256. OpenDKIM reports permerror and stamps rsa-sha1 into a header that carries the receiver's own authentication service identifier.
| 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 are the differential oracles and both handle the same signatures correctly. Newest published release checked on 2026-07-30. |
| Where | DKIM-Signature tag-value parsing (RFC 6376 section 3.5) and the algorithm value written into the Authentication-Results header |
| Weakness | CWE-1286 (Improper Validation of Syntactic Correctness of Input) leading to CWE-393 (Return of Wrong Status Code) |
| Reachable by | Remote, unauthenticated. One message carrying a cryptographically valid signature made with the sender's own key, using an RFC-valid but unusual i= or d= value. No key material or DNS control of any victim is needed. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L (6.5 (Medium)) |
| 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. Deterministic across five repetitions, with syntactic controls that pass and report the correct algorithm, three verifiers on the identical message, and a directed accept-direction sweep that never produced a pass. |
A quoted string is one of the forms RFC 6376 section 3.5 permits for the local part of an i= identity, and section 3.2 sets out the tag-value list that identity is written into. Nothing in either section rules out i="x@y"@attacker.net, and nothing rules out a d= ending in a dot. Both are legal signatures.
Hand either one to OpenDKIM and its tag tokenizer loses track of where it is. The verdict on a cryptographically valid rsa-sha256 signature comes back dkim=permerror, and the Authentication-Results header OpenDKIM writes claims header.a=rsa-sha1. That is not the algorithm the message used, and it is weaker than the algorithm the message used. Rspamd and dkimpy take the same signature without complaint.
Four forms trigger it. All four ran against a message carrying a cryptographically valid signature, with From: a@attacker.net and the key for d=attacker.net published.
i="x@y"@attacker.net quoted-string local part containing an interior '@'
i=x@y@attacker.net two unescaped '@'
i=x\@y@attacker.net backslash-escaped '@'
d=attacker.net. trailing dot
One message, byte for byte, went to three verifiers, and five repetitions returned the same answers every time.
| Verifier | Verdict | Identity and algorithm reported |
|---|---|---|
| OpenDKIM | dkim=permerror | stamps header.a=rsa-sha1, which is false: the signature is rsa-sha256 |
| Rspamd | R_DKIM_ALLOW, dkim=pass | identity attacker.net, taken from d= |
| dkimpy | verify() returns true | AUID domain by the last-@ split is attacker.net, equal to d=, so valid |
The next set of rows separates the cause from two nearby suspects. Quoting on its own is not enough to trigger the fault, and using i= at all is not enough either. A second structural character inside the value is what does it.
| Signature tag under test | OpenDKIM verdict | Reported algorithm |
|---|---|---|
i=x@attacker.net (one @, no quoting) | dkim=pass | rsa-sha256, correct |
i="xy"@attacker.net (quoted, no interior @) | dkim=pass | rsa-sha256, correct |
i="x@y"@attacker.net (quoted, interior @) | dkim=permerror | rsa-sha1, false |
header.a=rsa-sha1 is itself the proof of what went wrong. No reading of the message yields that value, since the message carries a=rsa-sha256. What OpenDKIM printed is a default or a leftover, which is what a parser produces once it no longer knows which tag it is holding.
Whether the desync can be pushed toward acceptance was tested head-on. A sweep put the offending i= immediately ahead of each tag that matters, one at a time: a=, d=, s=, h=, bh=, b=. Beyond that it tried a=rsa-sha1;a=rsa-sha256 duplicated, a d= swallowed, a d= duplicated, an a= dropped, a quote left unterminated, and a quote-swallow injection built to replace d=. Every case ended in permerror, or in no Authentication-Results header at all. dkim=pass never appeared. One injection did get the parser to put header.d=victim.net on the line, and even there the verdict stayed permerror.
sel._domainkey.attacker.net. IN TXT "v=DKIM1; k=rsa; p=<public key>"
in i=.
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=attacker.net; s=sel;
i="x@y"@attacker.net; h=from:to:subject; bh=...; b=...
From: a@attacker.net
To: user@receiver.test
Subject: hello
...
Those exact bytes carry a cryptographically valid signature, and both dkimpy and Rspamd confirm it.
the AUID domain matches d=, which leaves dkim=pass as the correct verdict.
Authentication-Results: receiver.test; dkim=permerror header.d=attacker.net header.a=rsa-sha1
Neither part of that line is true: the signature verifies, and it was made with rsa-sha256. Drop the interior at-sign, leaving i="xy"@attacker.net and altering nothing else, and OpenDKIM returns dkim=pass with header.a=rsa-sha256.
Two things follow from this, and spoofing is not among them.
Interoperability. Mail that conforms to the RFC gets turned away. Any signer whose i= uses a quoted-string local part, or whose d= ends in a dot, has its signatures come back permerror at OpenDKIM receivers while Rspamd and dkimpy accept the very same ones. Should the sending domain lean on DKIM to align for DMARC, that permerror carries through and legitimate mail fails DMARC.
Authentication-Results integrity. The receiver's own trusted authentication service identifier sits on the same line as the false header.a=rsa-sha1. RFC 8601 section 2.7.1 says header.a reports the algorithm actually used, and RFC 8301 has deprecated rsa-sha1 for DKIM, so anything downstream that pulls algorithm data out of Authentication-Results takes in a deprecated algorithm for a message signed under SHA-256. Policy engines that refuse deprecated algorithms read that field, as do inventories of algorithm use and security dashboards. Nobody measured whether a given consumer then acts on it. The false value is deterministic regardless.
Two fixes are needed, one in the parser and one in the reporting path. In the DKIM-Signature tag tokenizer, a quoted string has to be taken in one piece, an escaped @ has to stay inside the value it appears in, and a d= ending in a dot has to be read as a fully qualified name instead of a structural break. Section 3.2 places the delimiters at the semicolon and equals structure; a character that can legally appear within a value must never end one.
The reporting path should print no algorithm it did not actually read out of the signature. Where the algorithm is unknown, leaving header.a off is the honest option. Defaulting it to rsa-sha1 states a falsehood, and the falsehood points toward the weaker algorithm.
Read as a spoofing report, this entry would be wrong. Finding a steerable variant is exactly what the sweep above set out to do, and none turned up. Under every placement and every injection attempted OpenDKIM stayed away from dkim=pass, the case that put a victim domain into header.d included. An identity that ought to be rejected cannot be authenticated through this desync. Interoperability plus the integrity of the Authentication-Results value is the whole of the claim.
One sweep coming up empty is strong evidence, not a proof. A steerable variant may still exist; what is stated here is only what has been demonstrated.
Other DKIM tag findings in this series deal with an i= outside d=, with the key record's k= tag, and with the t=s flag. None of them is this. Here the tokenizer itself loses a tag, and the rsa-sha1 misreport puts that on display.