Vulnerability analysis · DKIM

A DKIM expiry of x=0 escapes Rspamd's expiration check, so a signature whose validity ended at the Unix epoch is still accepted as current [Rspamd ≤ 4.1.2, latest release checked 2026-07-30]

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

An x= value of 0 puts the expiration on 1 January 1970. Rspamd hands such a signature R_DKIM_ALLOW, as if no expiry tag had been written. dkimpy compares the same value against the clock and calls the signature past.

SoftwareRspamd
VendorRspamd project (Vsevolod Stakhov)
Sourcehttps://github.com/rspamd/rspamd
AffectedRspamd 4.1.2. Originally isolated on 3.10.2 and re-confirmed on 4.1.2 on 2026-07-28. dkimpy 1.1.8 handles the same signature correctly. Newest published release checked on 2026-07-30.
WhereDKIM signature expiration, the x= tag of RFC 6376 section 3.5 and the expiry comparison performed at verification
WeaknessCWE-347 (Improper Verification of Cryptographic Signature) / CWE-20 (Improper Input Validation)
Reachable byRemote, but only in the direction the signed scope permits: x= is covered by b=, so the value must have been chosen by the original signer.
CVSS v3.1CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N (3.7 (Low))
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. Confirmed against dkimpy as the differential oracle, with the neighbouring past values, the future values, and the t=-present, t=-absent and x<t variants all exercised.

1. Overview

x= in RFC 6376 section 3.5 carries an absolute Unix timestamp: the instant after which the signature no longer holds. Verification at any later instant has to fail. The same section imposes a second condition, that an x= below the signature's own t= invalidates it whatever the clock says. Nothing in the section carves out a reserved value. x= is an ordinary integer, and zero is an ordinary integer.

Rspamd reaches the expiry comparison only when the value is non-zero. x=0 therefore behaves as if no expiry tag had been written, and a signature whose life ended at the Unix epoch comes back as R_DKIM_ALLOW. Handed the identical signature, dkimpy returns ValidationError: x= value is past (b'0').

2. Analysis

Only the x= value moves across the sweep:

Signature x=RspamddkimpyRequired
0 (the Unix epoch, long past)R_DKIM_ALLOW, treated as unsetValidationError: x= value is past (b'0')fail
1 through now − 60 (any other past value)suppressed as expiredexpiredfail
now + 60 and later (future)passpasspass

Row two is what makes this a bug claim rather than a report of absent expiry checking. The comparison exists in Rspamd and it does its job on every other past timestamp, including 1, a single second above the value that slips through. Zero is the one exemption. A guard on zero wrapped around the comparison fits that pattern. A comparison that was never written does not.

The variations around the case leave it undisturbed. It holds with t= present, with t= absent, and in the x < t arrangement, where the test signature carried x=0 next to t=1781531772 and fails on both counts of section 3.5. Rspamd shows no Y2038-style wrap. x=2^31 and x=2^32 come back as far-future values, which is correct, and anything at or above 2^63 is dropped. dkimpy works in arbitrary-precision integers and shows no boundary effect anywhere.

3. Reproduction

  1. Publish an ordinary key record for the signing domain.
  2. Sign the message with the matching private key and set the expiry to the Unix epoch.

t= and x= both live inside the DKIM-Signature header field, so b= covers them and both have to be final before the signature is computed.

  1. Hand the message to Rspamd and to dkimpy, and record each verdict.
  2. Re-sign with x=1 and repeat. Rspamd suppresses that signature as expired, which

pins the divergence on the single value zero.

The key record:

sel._domainkey.signer.example.  IN TXT "v=DKIM1; k=rsa; p=<base64 RSA public key>"

The signature:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=signer.example; s=sel;
    t=1781531772; x=0; h=from:to:subject:date; bh=<body hash>; b=<signature>

Expected under RFC 6376 section 3.5. Verification happens long after x=0, and x also falls below t, so the signature fails on two counts.

Observed. Rspamd returns R_DKIM_ALLOW. dkimpy returns ValidationError: x= value is past (b'0').

4. Assessment

A signature carrying x=0 has no closing edge on Rspamd. The same bytes on the wire go on clearing Rspamd-based filtering with no end date, while a conforming verifier wrote the signature off as expired long ago. Two consequences follow. Whether a message counts as DKIM-authenticated turns on which verifier looked at it, and nothing in the output flags the split. Beyond that, x= stops bounding replay, which is the one job the tag has.

The reach of this is bounded by the signed scope, and that bound is structural rather than incidental. b= covers x=, so an attacker cannot drop a zero into somebody else's signature and leave the signature valid. The value has to come from the original signer: expiry arithmetic off by one, a field left uninitialized, or a deliberate choice of zero to mean “no expiry”. Once a signer emits one, Rspamd alone honors that signature with no end, including on a message captured and replayed long after the signer expected it to lapse.

5. Remediation

x=0 is a timestamp in the past and belongs in the comparison like any other. Whether an x= tag is present or absent is a property of the tag list, not of the integer it holds, so a verifier can draw that distinction without inspecting the value at all. Take the zero guard off the expiry comparison. Then enforce the other section 3.5 rule separately: x below t invalidates the signature. That rule on its own would catch the epoch case too.

6. Additional notes

dkimpy is the differential oracle for this entry. The x= vein was never run against OpenDKIM, so nothing here says how OpenDKIM treats an epoch expiry.

The zero guard is an inference drawn from the observed pattern, in which every past value except zero is suppressed. Nobody located the responsible code in Rspamd's DKIM module, and confirming it upstream is still open. No function name and no line number is asserted.

The neighboring DKIM entries in this group deal with key-record tags. This one deals with a signature tag, x= from section 3.5. Signature expiration appears in no other finding in the set.

7. References