Bali, Indonesia
Insights

DKIM Fails With body hash did not verify? Find What Changed the Message

October 30, 2026
0:00
Your domain has a DKIM key published, your server signs outgoing mail, and the DNS check tools say everything looks fine. Yet the recipient's headers show dkim=fail (body hash did not verify) and the message sits in spam. That specific wording is useful, because it rules out a lot. It almost always means the signature was created correctly, and then something changed the message body before it reached the recipient. The job is to find what touched the body and where it happened.
A DKIM signature is a header added by the sending server. Two parts of it matter here. The bh= tag holds a hash of the message body, computed at the moment of signing. The b= tag is the actual cryptographic signature, and it covers the selected headers plus that bh= value. When a receiver checks DKIM, it first recomputes the body hash from the body it received and compares it to bh=. If those differ, verification stops right there with a body hash failure. The receiver never even gets far enough to tell you whether the key in DNS was right. That is why this error is different from a generic signature failure. If the public key was missing, you would usually see something like a key-not-found or permerror result. If a signed header such as Subject had been rewritten, you would usually see the signature itself fail. A body hash mismatch points at the body: added text, removed text, re-encoded text, or changed line endings and whitespace. The c= tag in the signature also matters. It sets canonicalization, which is the set of rules for normalizing the message before hashing. With relaxed body canonicalization, trailing whitespace and runs of spaces are tolerated, along with empty lines at the end. With simple, almost any byte change breaks it. Neither mode survives an added paragraph or a change in transfer encoding.
In support, the first instinct on any DKIM failure is to re-check the DNS record. That is fine, but for this error it rarely helps. A broken or mismatched key produces a different result. If your DNS TXT record for the selector looks correct and other recipients report dkim=pass, the key is not your problem. SPF is another distraction. People see spf=pass next to dkim=fail and assume the two are fighting. They are independent checks. SPF looks at the connecting IP and the envelope sender. DKIM looks at the message content. A pass on one tells you nothing about the other. The question that actually narrows things down is: does it fail everywhere, or only on certain paths? If mail to one recipient fails but mail to another provider passes, the change probably happened on the recipient side or on a path specific to that recipient, such as a mailing list or a forwarding rule. If every outbound message fails at every receiver, the change is on your side, somewhere between the signing step and the final outbound hop.
Ask for the full original headers of a failed message ("Show original" or "View source" in most webmail). Pasted screenshots of the visible message are not enough. You want the raw text. Illustrative output format (values shortened, layout varies by receiver):
TEXT
Authentication-Results: mx.receiver.example;
       dkim=fail (body hash did not verify) header.d=example.com header.s=default;
       spf=pass smtp.mailfrom=bounces.lists.example.org;
       dmarc=fail (p=quarantine) header.from=example.com
List-Id: <team.lists.example.org>
Received: from lists.example.org (lists.example.org [192.0.2.10]) ...
Received: from mail.example.com (mail.example.com [198.51.100.20]) ...
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
       s=default; h=from:to:subject:date:message-id; bh=...; b=...
Read the Received headers from the bottom up. Each hop adds one at the top, so the lowest one is closest to the original sender. Find where your DKIM-Signature was added relative to those hops. Every server listed above that point had a chance to modify the body. In this illustrative example the clues line up. There is a List-Id header, an extra hop through a list server, and SPF passing for the list's bounce domain rather than yours. The message went through a mailing list after signing. Also check the Content-Transfer-Encoding header of the body parts in the received copy. If your server sent 8bit and the recipient sees quoted-printable or base64, something re-encoded the body in transit. That alone is enough to break the body hash. One more tag to look for is l= in the DKIM signature. It limits the body hash to a number of bytes. If it is present, appended content will not break the hash, but it also means anyone can append content under your signature. Do not add l= as a fix.
Most body hash failures trace back to one of three things. Mailing list footers. List software often appends an unsubscribe link or a footer to every message, and sometimes adds a tag like [team] to the subject. The footer breaks the body hash, the subject tag breaks the header signature, or both. You will see List-Id or List-Unsubscribe headers and an extra list hop in Received. This is not something you fix on your server. List operators handle it by rewriting the From address to the list domain, by not modifying messages, or by adding ARC headers, which let receivers see the authentication results from before the list touched the message. Whether a receiver trusts those ARC results is up to the receiver. Antivirus or disclaimer appliances. An outbound gateway or a content filter on your own server may append a legal disclaimer or a "scanned by" line. If that step runs after DKIM signing, every outbound message fails. The giveaway is text at the end of the body that the sender never typed, and failure at every receiver. Compare a copy from your Sent folder with the copy the recipient got. If the received copy has extra lines at the bottom, you have found it. Outbound relays that re-encode. If your server signs and then hands mail to a smarthost, or if the MTA itself converts encoding during delivery, the body changes. A common case is 8-bit content being converted to quoted-printable when the next hop does not advertise 8BITMIME support. Another is lines longer than the SMTP limit being wrapped. Both change bytes inside the body even though the text looks identical to a human. On Postfix with a DKIM milter, you can check whether output MIME conversion is disabled. This is a read-only command:
BASH
# Print the current value; 'yes' means Postfix will not re-encode 8bit bodies on output
postconf disable_mime_output_conversion
The bigger question on any stack is ordering. On Postfix, check whether a content_filter reinjects mail after the milter has already signed it. On Exim, signing usually happens in the remote SMTP transport, so look for any transport filter or system filter that modifies the body around that point. The exact layout depends on your configuration and control panel, so trace it in your own config rather than assuming a default.
Once you have a suspect, confirm it rather than guessing. Save the raw received message as a .eml file and run it through an offline verifier. The Python dkimpy package ships a small tool for this:
BASH
# Verify DKIM on a saved raw message; it fetches the public key from DNS
dkimverify < failed-message.eml
It should report a failure on the received copy. Then do a controlled comparison. Send one test message directly to a mailbox you control on a different provider, bypassing the list or forwarder. If that copy passes, your signing is fine and the change happens on the intermediate path. If it also fails, the change is on your side, and you can remove one hop at a time (disclaimer off, relay bypassed) until a test message passes. When the fix is on your side, it usually comes down to one rule: signing must be the last step that touches the body. Move disclaimers and content filters before the DKIM signer, or have the final outbound relay do the signing instead of an inner server. Switching to relaxed/relaxed canonicalization is a reasonable default and absorbs harmless whitespace changes, but it will not rescue a message that had a footer added or was re-encoded. The practical takeaway: treat body hash did not verify as a pointer, not a DNS problem. Pull the raw headers, find where the signature sits in the Received chain, look at what each later hop could have added or re-encoded, and confirm with an offline verifier and a direct test send. If the culprit is your own pipeline, reorder it so the signature is the last thing applied. If it is a mailing list, the fix belongs to the list operator, and knowing that saves a lot of time spent re-publishing DKIM keys that were never broken.