Error message
dkim=fail (body hash did not verify)
Also printed as
- dkim=neutral (body hash did not verify)
mailauth writes `dkim=fail (body hash did not verify)` in strict mode and `dkim=neutral (body hash did not verify)` in its default lenient mode (both reproduced with 8.0.1). OpenDKIM reports the same situation as `dkim=fail reason="signature verification failed"` (reproduced with 2.11.0).
DKIM verifiers (Gmail, mailauth, Rspamd…) — Authentication-Results after the body changed in transit
What it means
The `bh=` tag of the DKIM signature holds a hash of the message body as the sender signed it. The receiver hashed the body it received (after canonicalization) and got a different value, so the body changed in transit. The header signature was not even the problem: RFC 6376 makes a body-hash mismatch a permanent failure. Classic culprits are mailing-list footers, disclaimers and security gateways that rewrite links.
Why it happens
A footer or disclaimer was appended
commonMailing lists (unsubscribe footer), corporate disclaimers added by an outbound gateway, or "Sent from…" lines added by relays change the canonicalized body.
A security gateway rewrote the body
commonLink rewriting (Safe Links, URL defense), attachment stripping or banner insertion by inbound or outbound filters.
The MIME transfer encoding changed
occasionalA relay without 8BITMIME converts 8-bit content to quoted-printable or base64. The decoded text is identical; the signed bytes are not.
The signer hashed different line endings
occasionalSigning code that hashes LF line endings while the message is sent with CRLF (or with trailing whitespace stripped later) produces a body hash that never matches with `simple` canonicalization.
How to fix it
- 1.
Recompute bh= yourself
Comparing a recomputed body hash on the sent copy and on the received copy tells you whether the signer or a relay is at fault.
bash # Recompute bh= for relaxed body canonicalization and compare with the header's bh= value python3 - message.eml <<'EOF' import base64, hashlib, re, sys raw = open(sys.argv[1], "rb").read().replace(b"\r\n", b"\n") body = raw.split(b"\n\n", 1)[1] lines = [re.sub(rb"[ \t]+", b" ", l).rstrip(b" \t") for l in body.split(b"\n")] canon = b"\r\n".join(lines).rstrip(b"\r\n") + b"\r\n" print("computed bh=", base64.b64encode(hashlib.sha256(canon).digest()).decode()) EOF grep -o "bh=[^;]*" message.eml - 2.
Sign after every modification
Make the DKIM signer the last step on your outbound path; for intermediaries you do not control, rely on ARC and DMARC alignment via SPF where possible.
text # Postfix: run content filters (disclaimers, antivirus) BEFORE the DKIM signing milter smtpd_milters = inet:127.0.0.1:10025, inet:127.0.0.1:8891 # ^ disclaimer milter ^ OpenDKIM (signs last)
Related errors
dkim=fail reason="signature verification failed"OpenDKIM (Authentication-Results header added by the opendkim milter); mailauth reports the same as "bad signature"dkim=permerrorOpenDKIM (Authentication-Results); mailauth reports syntax errors as dkim=neutraldkim=fail reason="key not found in DNS"OpenDKIM (Authentication-Results); mailauth reports it as dkim=neutral (no key)