Skip to content

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.

Koltrix Team4 min read
A magnifying glass on a white table
Photo by Markus Winkler on Unsplash
On this page(7 sections)
  1. What the body hash is
  2. Canonicalization sets how strict the comparison is
  3. The usual suspects
  4. 1. Footers and disclaimers added after signing
  5. 2. Link or tracking rewriting
  6. 3. Line ending and line length changes
  7. 4. Transfer encoding conversion
  8. 5. Character set or MIME restructuring
  9. 6. The message is being forwarded or sent through a list
  10. 7. A bug in your own signing code
  11. How to find the culprit
  12. When a body hash failure is not your problem
  13. A short diagnostic checklist
  14. 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.

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:

  1. Get the received message source. Send to a mailbox you control and view the original, including all headers.
  2. 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.
  3. Check which signature failed. A message may carry several DKIM signatures. Note the d= and s= of the failing one, which identifies the signer.
  4. 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.
  5. 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 relaxed body 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.

SharePost on XLinkedIn
  • A magnifying glass on a white table
    Deliverability

    Why two SPF records are worse than none

    Publishing a second SPF TXT record makes every check a permerror. How it happens, how to detect it with dig, and how to merge records safely.

    4 min read

  • A row of six numbered mailboxes on a wooden rail in front of dense green plants
    Deliverability

    SPF, DKIM and DMARC in plain English

    Three DNS records decide whether your email arrives. What each one actually does, what to publish, and the four mistakes that cause most of the support tickets.

    4 min read