Vulnerability analysis · DKIM
A key record carrying t=s must reject any signature whose i= names a subdomain. Rspamd returns R_DKIM_ALLOW and dkimpy reports success. Of the three implementations tested, only OpenDKIM applies the flag.
| Software | Rspamd and dkimpy |
|---|---|
| Vendor | Rspamd project (Vsevolod Stakhov) and the dkimpy maintainers (Scott Kitterman et al.) |
| 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. dkimpy 1.1.8. OpenDKIM 2.11.0 enforces the flag correctly. Newest published release checked on 2026-07-30. |
| Where | DKIM public-key record handling, the s (strict) value of the t= flags tag in RFC 6376 section 3.6.1 |
| Weakness | CWE-347 (Improper Verification of Cryptographic Signature) / CWE-20 (Improper Input Validation) |
| Reachable by | Remote. The victim key record must publish t=s, and the attacker must be able to produce a signature under that key carrying a subdomain AUID. |
| 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. Three deterministic repetitions with both isolating controls, and dkimpy cross-checked as a second non-enforcing implementation. |
A DKIM key record can carry a t= tag, and RFC 6376 section 3.6.1 gives the value s a specific meaning: “Any DKIM-Signature header fields using the i= tag MUST have the same domain value on the right-hand side of the @ in the i= tag and the value of the d= tag. That is, the i= domain MUST NOT be a subdomain of d=.” By default, under section 3.5, a subdomain AUID is legal. The s flag is how a key owner turns that default off and demands exact equality.
Rspamd and dkimpy never apply the flag. Present a signature whose AUID is a subdomain of d=, under a key published with t=s, and both accept it. OpenDKIM fails it, as required. Two independent implementations miss the same MUST, and one enforces it.
The signature itself is valid in every case below. The i= tag sits inside the signed tag set, so b= covers it and no later hop could have inserted it. Two things vary: the t= tag in the key record, and whether the AUID is a subdomain or an exact match.
| Key record | Signature i= | OpenDKIM | Rspamd | dkimpy | Required |
|---|---|---|---|---|---|
t=s | @sub.d (subdomain) | fail | R_DKIM_ALLOW | pass | fail |
t=s:y (list form) | @sub.d | fail | pass | pass | fail |
(no t=, control) | @sub.d | pass | pass | pass | pass |
t=s (control) | @d (exact) | pass | pass | pass | pass |
The bottom two rows are controls, and together they pin the cause to the flag. Drop t= and the subdomain AUID becomes legal; all three implementations pass it, correctly. Keep t=s but make the AUID equal d= and all three pass again, also correctly. OpenDKIM fails only when the flag and the subdomain form occur together, which is the one case section 3.6.1 moves from legal to invalid. Rspamd and dkimpy give the identical verdict in all four rows. A reader of the flag cannot produce that pattern.
The t=s:y row rules out a parsing problem with the list form. Neither implementation applies the flag whether it stands alone or shares the tag with another flag.
sel._domainkey.signer.example. IN TXT "v=DKIM1; k=rsa; t=s; p=<base64 RSA public key>"
signing domain, and include i= in the signed tag set so b= covers it:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=signer.example; s=sel;
i=user@sub.signer.example; h=from:to:subject:date; bh=<body hash>; b=<signature>
Expected, per RFC 6376 section 3.6.1: with t=s published, the AUID domain must equal d= and must not be a subdomain of it, so the signature is invalid and verification fails.
Observed: Rspamd reports R_DKIM_ALLOW. dkimpy returns true from verification. OpenDKIM fails the signature.
Two controls finish the reproduction. Remove t=s from the record and all three pass, which the section 3.5 default makes correct. Restore t=s and set i=user@signer.example and all three pass again. The implementations diverge only when both conditions hold.
A key owner publishes t=s to bind a key to one exact domain. The typical reason is a key handed to someone else: a delegated sub-signer, a departmental mail system, a tenant of a shared signing service. The flag says that holder may sign as d, and not as sub.d. On Rspamd and dkimpy the flag does nothing, and the subdomain AUID comes back marked authenticated.
The attacker here holds the delegated key already. Nobody gains access to a domain they do not control, because the AUID is chosen by whoever signs and the flag is published by the key owner. What the key owner loses is the one mechanism DKIM gives them for forbidding subdomain identities on a key they handed out.
The exposure is narrow, and it should be described that way. Few deployments publish t=s, so few are positioned to be affected. The finding is worth recording because it is an unambiguous MUST violation shared by two implementations that were written independently.
Parse the t= flags from the key record, list form included. When the s flag is present, require the AUID domain to equal d= exactly. Membership in the d= subtree is not sufficient. Section 3.6.1 also requires unknown flags in the list to be ignored; keep that tolerance for flags you do not recognize, and do not extend it to the ones you do.
This is not the same finding as rspamd-auid-subtree-unchecked. That one concerns the section 3.5 default, under which a parent, sibling, or unrelated AUID domain is invalid. This one concerns the section 3.6.1 strict flag, which additionally forbids the subdomain form that the default treats as a correct pass. Flipping that baseline is the entire purpose of t=s.
An earlier laboratory run recorded t=s as a consensus with no divergence. That run tried only the benign direction, t=s with an AUID equal to d=. It never tried the failing direction with a subdomain AUID. The two results do not contradict each other; the earlier one was incomplete, not wrong.
Confirmation against upstream Rspamd and upstream dkimpy is still an open step. Disclosure priority is low to match: the flag is obscure, even though the MUST is not ambiguous.