Vulnerability analysis · DKIM

Rspamd authenticates a revoked DKIM key because it stops at the first p= tag it can parse [OpenDKIM <= 2.11.0 and Rspamd ≤ 4.1.2, latest release checked 2026-07-30]

Yongzhe Xu, Virginia Tech  ·  yongzhe@vt.edu  ·  2026-07-31

Rspamd reads the first usable p= value in a key record and discards the empty p= that revokes the key. Signatures made with the revoked key pass in both tag orders.

SoftwareRspamd (OpenDKIM is also non-conformant, in an order-dependent way)
VendorRspamd project (Vsevolod Stakhov)
Sourcehttps://github.com/rspamd/rspamd
AffectedRspamd 4.1.2, originally isolated on 3.10.2. OpenDKIM 2.11.0 is last-tag-wins; dkimpy is correct. Newest published release checked on 2026-07-30.
WhereDKIM public-key record parsing, the p= field and the empty-value revocation marker
WeaknessCWE-347 (Improper Verification of Cryptographic Signature) / CWE-299 (Improper Check for Certificate Revocation)
Reachable byRemote. Requires the victim's key record to carry both values, through a stale entry, a mistaken revocation, or record injection.
CVSS v3.1CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N (5.9 (Medium))
VerificationReproduced 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. Five repetitions with single-record controls in both directions.

1. Overview

A DKIM key record publishes the public key in a p= tag. RFC 6376 section 3.6.1 gives an empty p= a specific meaning: the key is revoked, and a verifier must fail any signature made with it. Section 3.2 forbids duplicate tag names outright, so a record holding two p= tags is malformed before revocation is even considered.

Rspamd keeps the first p= value it can parse and drops the rest. When a valid key and the empty revocation marker sit in the same record, the valid key wins and the revocation is never seen. The result is dkim=pass regardless of which tag comes first.

2. Analysis

Three implementations were given the same two records. The verdicts are shown with the DMARC result each one produced.

Key record at the selectorOpenDKIMRspamddkimpy
v=DKIM1; k=rsa; p=<valid>; p=dkim=fail, "key revoked", then dmarc=faildkim=pass, then dmarc=passfail
v=DKIM1; k=rsa; p=; p=<valid>dkim=pass, then dmarc=passdkim=pass, then dmarc=passfail

The three parsers resolve the duplicate differently. OpenDKIM keeps the last tag, so it fails on the first record and passes on the second by accident of ordering. Rspamd keeps the first tag it can parse, so it passes on both. dkimpy refuses both records, which is the handling section 3.2 calls for.

Single-record controls confirm that the duplicate is what breaks Rspamd, not revocation handling in general. One valid record yields dkim=pass on all three. One clean empty p= yields a failure on all three: OpenDKIM reports "key revoked", Rspamd reports R_DKIM_PERMFAIL, and dkimpy fails. Every implementation honors a clean revocation. Only the coexistence case diverges.

3. Reproduction

Publish this zone. The selector record carries a valid key and an empty p=, in that order.

sel._domainkey.victim.example.  IN TXT "v=DKIM1; k=rsa; p=<valid base64 key>; p="
_dmarc.victim.example.          IN TXT "v=DMARC1; p=reject; adkim=s; aspf=s"
victim.example.                 IN TXT "v=spf1 -all"

Then run these steps.

  1. Sign a message with the revoked key.
  2. Send it from a host outside the SPF record, so SPF fails and the only path to a DMARC

pass runs through an aligned DKIM signature.

  1. Read the authentication verdict.
  2. Repeat with the tags in the other order, p=; p=<valid base64 key>.

Expected (RFC 6376 sections 3.2 and 3.6.1): the record is rejected as malformed, or the empty p= is honored. Either way DKIM fails and DMARC fails.

Observed (Rspamd 4.1.2): dkim=pass, and therefore dmarc=pass, in both tag orders.

4. Assessment

This is a DKIM revocation bypass. Anyone still holding the revoked private key can sign mail that Rspamd treats as authenticated. Three ways a record reaches the vulnerable state: an operator revokes a key but leaves the stale valid value in place, an operator mistypes the revocation, or an attacker who can write to the zone appends ; p=<old key>.

The test setup pins the impact. SPF was -all and the DMARC policy was p=reject; adkim=s; aspf=s, so the observed dmarc=pass could only have come from the revoked key.

The claim has a boundary. The trigger is a malformed duplicate-tag record, which limits how often the condition arises on its own. When it does arise, a revoked key authenticates mail for a domain publishing p=reject.

5. Remediation

Two independent fixes apply. First, reject any key record with duplicate tag names, as section 3.2 requires. Second, treat an empty p= as a revocation even when a valid p= is present, instead of accepting whichever value parses first. OpenDKIM's last-tag-wins resolution violates section 3.2 for the same reason and needs the same first fix.

6. Additional notes

One negative result is worth recording because it bounds the finding. A clean single empty p= revocation is honored as revoked by all three implementations. That holds for the variants tested: p= with a trailing space, reordered tags, a missing p=, and a record with no v=. The broader hypothesis, that revocation goes unhonored in general, is not supported; all three agree and none of them has a bug there. The divergence belongs to the coexistence case alone.

7. References