Vulnerability analysis · DKIM
Rspamd's DKIM result is the same whether or not the key record's h= list covers the signature's hash algorithm, so the downgrade protection the tag exists to provide is absent.
| Software | Rspamd |
|---|---|
| Vendor | Rspamd project (Vsevolod Stakhov) |
| Source | https://github.com/rspamd/rspamd |
| Affected | Rspamd 4.1.2. Originally isolated on 3.10.2 and re-confirmed on 4.1.2 on 2026-07-28. OpenDKIM 2.11.0 enforces the restriction correctly. Newest published release checked on 2026-07-30. |
| Where | DKIM public-key record handling, the h= acceptable-hash list of RFC 6376 section 3.6.1, which section 6.1.2 requires the verifier to check |
| Weakness | CWE-347 (Improper Verification of Cryptographic Signature) / CWE-757 (Selection of Less-Secure Algorithm During Negotiation) |
| Reachable by | Remote. The victim key record must publish h=, and the attacker must present a signature whose hash algorithm that list excludes. |
| 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. Isolated from a DKIM key-record differential sweep and re-confirmed with a direct OpenDKIM-versus-Rspamd probe in which only the h= tag varies. |
A DKIM key record may carry an h= tag: a colon-separated list naming every hash algorithm that key is allowed to be used with. RFC 6376 section 3.6.1 defines the tag, and section 6.1.2 step 1 states the obligation as a MUST: “If the h= tag exists in the public-key record, the hash algorithm implied by the a= tag in the DKIM-Signature header field MUST be included in the contents of the h= tag; if not, the verifier MUST ignore the key record”. A verifier left with no key record has nothing to verify against, so the signature fails.
Rspamd skips that step. Whatever the h= list holds, the DKIM result comes out the same. Hand it a key record naming a single hash together with a signature computed under a different one, and it still emits R_DKIM_ALLOW. OpenDKIM, on the same input, discards the key and returns dkim=fail with the reason signature-key hash mismatch.
Only one variable moves across the test matrix. The message, the key pair and the signature stay put, a single valid rsa-sha256 signature reused for every row, while the h= tag of the published key record changes:
Key record h= tag | OpenDKIM | Rspamd | Required |
|---|---|---|---|
h=sha256 (the signature's algorithm is listed) | pass | R_DKIM_ALLOW | pass |
h=sha1:sha256 (listed among several) | pass | R_DKIM_ALLOW | pass |
h=sha1 (the signature's algorithm is not listed) | fail, reason="signature-key hash mismatch" | R_DKIM_ALLOW | fail |
(no h= tag, control) | pass | R_DKIM_ALLOW | pass |
Rows one and two carry as much weight as row three. A verifier that passed everything unconditionally would produce the same table, so those rows are what separate under-enforcement from a blanket “Rspamd always passes”. Rspamd does pass the two combinations the specification permits; the verdict is therefore not pinned to a constant. The single case it is obliged to reject is the case it lets through. Row four, a record carrying no h= at all, fixes the baseline, and the restricted record lands on that same baseline, so the tag contributes nothing to the outcome.
The pairing actually exercised is the reverse of the dangerous one: a key limited to SHA-1, presented with a SHA-256 signature. That pairing can be assembled from one valid signature, with no need to weaken the signature to make the point. The dangerous pairing is a key limited to h=sha256 presented with an rsa-sha1 signature. It follows from the same invariance, since the verifier reads the tag in neither direction.
a=rsa-sha256.h=sha256 in place of h=sha1, then with h=sha1:sha256. Both verifierspass in both runs, which rules out a malformed record as the explanation.
The key record:
sel._domainkey.signer.example. IN TXT "v=DKIM1; k=rsa; h=sha1; p=<base64 RSA public key>"
The signature:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=signer.example; s=sel;
h=from:to:subject:date; bh=<body hash>; b=<signature>
Expected per RFC 6376 section 6.1.2 step 1. a=rsa-sha256 implies SHA-256, the record's h=sha1 list omits it, and the prescribed response is to throw the key record away, which leaves the signature failing.
Observed. Rspamd reports R_DKIM_ALLOW. The same message and the same record put through OpenDKIM produce dkim=fail with reason="signature-key hash mismatch".
h= is policy attached to one key, and what it buys the publisher is downgrade protection. A domain that issues a key for SHA-256 work alone writes h=sha256 into the record, so that no verifier will honor the key behind a weaker rsa-sha1 signature. RFC 8301 deprecates rsa-sha1, yet legacy verifiers go on accepting it, which is the reason the restriction is worth publishing at all. A receiver running Rspamd delivers none of that. The record sits in DNS where anyone can read it, and the verifier never looks.
Exposure is narrow: only domains that publish h=. An attacker gains nothing against a domain they do not control, since h= comes from the key owner and forging that owner's signature still takes the owner's private key. The loss falls on the owner, who asked the receiver for a check and does not get it. A signature made under an algorithm the owner excluded, and therefore weaker, is accepted by Rspamd, while a conforming verifier discards the key instead. That is exactly the case h= exists to prevent. Neither algorithm agility nor a staged hash migration can rest on h= for any receiver running Rspamd.
Put the check where the key record is loaded. If h= is present, derive the hash from the signature's a= tag and search the list for it. A miss means the record cannot serve this signature, which is what RFC 6376 section 6.1.2 step 1 calls for. A list that is empty, or that fails to parse, should also mark the record unusable. Passing over such a list in silence reproduces the same defect.
Few domains publish h= today. That keeps small the number of messages on which the missing check would have mattered, but it is a fact about deployment, not about the code. The specification writes the check as a MUST, and of the two implementations compared here Rspamd is the one that departs from it.
Everything above rests on black-box results. Nobody has pointed to the line in Rspamd's DKIM verifier where h= is read and then dropped, and checking upstream rspamd_dkim is still to be done. The report documents observed behavior; it does not name a code path.
Two sibling findings cover the same ground for other key-record constraints: rspamd-auid-subtree-unchecked for the i= AUID subtree check, and rspamd-key-service-tag-ignored for the s= service type. One pattern repeats in all three. Rspamd takes in a constraint the specification tells it to enforce, and the constraint never reaches the verdict.