DKIM body hash did not verify: the usual suspects
When DKIM fails with a body hash mismatch, something changed the message after signing. Find the culprit: footers, rewriters, line endings, encoding.

On this page(7 sections)
- What the body hash is
- Canonicalization sets how strict the comparison is
- The usual suspects
- 1. Footers and disclaimers added after signing
- 2. Link or tracking rewriting
- 3. Line ending and line length changes
- 4. Transfer encoding conversion
- 5. Character set or MIME restructuring
- 6. The message is being forwarded or sent through a list
- 7. A bug in your own signing code
- How to find the culprit
- When a body hash failure is not your problem
- A short diagnostic checklist
- Bottom line
"Body hash did not verify" is DKIM's way of saying the message that arrived is not the message that was signed. The signature is fine, the key is fine, and DNS is fine.
Something between your signer and the receiver changed the body.
What the body hash is
A DKIM signature contains two hashes. The bh= tag holds a hash of the canonicalized message body. The b= tag holds the cryptographic signature over a set of headers, including the DKIM-Signature header itself with its bh= value.
The verifier recomputes the body hash from the message it received. If the result differs from bh=, verification stops with a body hash failure before the signature is even checked. That tells you precisely where to look: the body changed after signing.
Canonicalization sets how strict the comparison is
The c= tag in the signature selects canonicalization for headers and body, written as header/body:
- simple body canonicalization tolerates almost nothing. Only trailing empty lines at the end of the body are ignored.
- relaxed body canonicalization also ignores trailing whitespace on lines and collapses runs of spaces and tabs within a line into a single space.
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=k1; ...
If your signer uses c=simple/simple or c=relaxed/simple, switching the body to relaxed removes a whole class of whitespace-related failures. Most modern signers default to relaxed/relaxed, but check.
Neither mode tolerates changes to the actual content, line breaks, encoding or MIME structure.
The usual suspects
1. Footers and disclaimers added after signing
The most common cause by far. A mail server, security gateway or mailing list appends "This message is confidential..." or an unsubscribe footer after the DKIM signature was applied. Every message from that path fails.
Fix: make sure signing happens at the last hop before the message leaves your infrastructure, after any footer or disclaimer is added. If a third-party gateway adds footers, sign after the gateway or configure the gateway to sign.
2. Link or tracking rewriting
Click tracking, URL defense products and some security appliances rewrite links in the body. If that rewrite happens after signing, the body hash breaks. Sending services that do click tracking rewrite before they sign; a separate appliance in your outbound path may not.
3. Line ending and line length changes
SMTP requires lines to end with CRLF. A signer that hashes LF-only content while the transport converts it to CRLF (or the reverse) produces a mismatch. Similarly, RFC 5322 limits lines to 998 characters, and a relay that wraps longer lines changes the body.
Fix: generate messages with CRLF line endings and keep lines under 998 characters. Use quoted-printable or base64 transfer encoding for HTML with very long lines.
4. Transfer encoding conversion
A relay that converts 8bit content to quoted-printable or base64 (because the next hop did not advertise 8BITMIME) re-encodes the body. The decoded content is identical, but DKIM hashes the encoded form, so the hash changes.
Fix: encode non-ASCII content as quoted-printable or base64 before signing, so no downstream server needs to convert it.
5. Character set or MIME restructuring
Some gateways rewrite MIME structure: stripping attachments, converting HTML, normalizing charset declarations, or wrapping the original message in a new multipart. All of these change the body.
6. The message is being forwarded or sent through a list
Mailing list software commonly adds footers and subject tags. Forwarding services sometimes modify content. These are third-party changes you cannot prevent; ARC exists to help receivers evaluate such mail.
7. A bug in your own signing code
If you sign in application code, a classic mistake is signing one serialization of the message and sending another: for example, signing before your MIME library normalizes headers or line endings, or computing the body hash over a string that gets re-encoded when written to the socket.
How to find the culprit
Work from what you can observe:
- Get the received message source. Send to a mailbox you control and view the original, including all headers.
- Read Authentication-Results. Confirm the failure is a body hash failure (often shown as
dkim=fail (body hash did not verify)) rather than a signature or key failure. - Check which signature failed. A message may carry several DKIM signatures. Note the
d=ands=of the failing one, which identifies the signer. - Compare with what you sent. Capture the message as it left your signer (from logs or a BCC to a local file) and diff it against what arrived. The diff shows exactly what changed.
- Test paths separately. Send the same message directly from the signer to the test mailbox, bypassing gateways. If it passes, a downstream hop is modifying it.
A quick local check with a DKIM verification tool can confirm whether the message as stored verifies:
# Using the dkimpy package
pip install dkimpy
dkimverify < received-message.eml
When a body hash failure is not your problem
If the failures appear only for mail that passed through mailing lists or forwarding, and direct delivery passes, the modification is happening on a path you do not control. That is expected behavior. With DMARC, those messages may fail unless the forwarder implements ARC and the receiver trusts it, or the list rewrites the From address. It is a reason to monitor, not necessarily to change your setup.
A short diagnostic checklist
- Confirm the error is specifically a body hash mismatch.
- Use
relaxedbody canonicalization. - Sign at the final outbound hop, after footers, disclaimers and link rewriting.
- Use CRLF line endings and keep lines under 998 characters.
- Pre-encode non-ASCII content so relays do not convert it.
- Diff the sent and received message to find the exact change.
- Separate failures on direct mail from failures on forwarded or list mail.
Bottom line
A body hash failure always means the body changed after signing. Footers, link rewriters, encoding conversions and line ending differences cause nearly all of them. Sign last, sign with relaxed canonicalization, send clean CRLF and pre-encoded content, and diff the before-and-after message when something still breaks.
Start with Koltrix
Your domain, one inbox, and an API that sends.
A team inbox where AI sorts and drafts (nothing is sent without your click), plus the transactional API and SMTP relay your product sends with. 7 days free, no card.

