You published the DKIM record, the key is correct, and yet a receiving server reports dkim=fail (body hash did not verify). That wording is precise. It means the DKIM signature was found, the public key was found and the signature itself is valid, but the hash of the message body that the receiver calculated is different from the body hash the sender recorded when it signed. In plain terms, the message was changed after it was signed. This guide explains how the body hash works, lists the things that modify mail in transit, shows how to find the exact point where the change happens and gives fixes for each case. Because DKIM often carries DMARC for providers whose bounce domains do not align, a body hash failure can turn into a rejected or quarantined message, so it is worth fixing properly. For the basics of DKIM records first, see DKIM records: what they look like and how to test them.
How the DKIM body hash works
A DKIM signature is a header called DKIM-Signature. It contains tags such as d= (the signing domain), s= (the selector), h= (the list of signed headers), bh= (the body hash) and b= (the signature over the headers and the body hash). To create bh=, the signer first normalizes the body using a method called canonicalization, then hashes it with SHA-256 and encodes the result. The receiver repeats the same steps on the body it received and compares. If the two hashes differ, it reports a body hash failure. If the hashes match but the signature check fails, you get a different message such as "signature did not verify", which usually points at changed headers or a key problem instead.
RFC 6376 defines two canonicalization methods for the body. Simple tolerates almost no change: even trailing whitespace or an extra blank line at the end alters the hash. Relaxed ignores differences in whitespace and line-wrapping, so it survives small, harmless changes. The setting appears in the signature as c=relaxed/relaxed (header/body). Using relaxed for both is the common recommendation, because it makes signatures far more robust in transit. If you see c=simple/simple or just c=simple, a signing setting change may be the whole fix.
What changes a message after it is signed
The signature should be the last thing applied to a message. Any system that touches the body later breaks it. These are the usual causes, roughly in order of how often we see them.
| Cause | What it does to the body | Fix |
|---|---|---|
| Mailing list software | Adds a footer, a subject prefix or unsubscribe text | Not under your control. Rely on SPF and ARC for list traffic, or ask recipients to avoid lists for critical mail |
| Outbound gateway or disclaimer service | Appends a legal disclaimer or banner after your mail server signs | Move signing to the last hop, or add the disclaimer before signing |
| Link rewriting and click tracking | Replaces URLs in the HTML after the signature was calculated | Enable tracking before the signing step, or turn the rewriting off for mail that you do not track |
| Security scanners at the sender | Rewrites links or adds markers to outgoing mail | Exclude your own outbound stream from rewriting, or sign after the scanner |
| Line ending or encoding changes | Converts LF to CRLF, re-wraps long lines or changes the transfer encoding | Use relaxed canonicalization and keep one component responsible for encoding |
| 8-bit to quoted-printable conversion | A relay downgrades 8-bit content to 7-bit | Send with a transfer encoding that is safe end to end, such as quoted-printable or base64 |
| Template or personalization after signing | The application fills in variables after the signer ran | Render the final content first, then sign |
| Truncated body with the l= tag | The signature covers only a set length of the body and extra content was added | Do not use l=. It makes signatures easy to abuse and many receivers ignore it |
| Forwarding services | Alter the body, add a footer or change the encoding | Rely on ARC for forwarded mail; see our forwarding guide |
Diagnose it: find where the change happens
The goal is to compare the message at the moment it was signed with the message as it arrived. You do not need special tools to start.
- Send to a mailbox you control at two or three different providers and open the original message. Note whether the failure appears everywhere or only at some. A failure that appears only at a recipient that scans or rewrites mail points to that recipient's gateway, not to your system.
- Check the canonicalization in the
DKIM-Signatureheader. If you seec=simple/simple, switch torelaxed/relaxedand test again. - Read the Received headers from bottom to top to list every hop. If a hop belongs to your own infrastructure after the signing point, such as a disclaimer server or a security gateway, it is a candidate.
- Compare the body as sent and as received. Most sending systems let you save the raw message. Use a diff tool to compare it with the received copy, looking at the end of the message, at links and at whitespace.
- Send a minimal message. Send a plain text one-line message through the same path. If it passes, something in your HTML, links or attachments is being changed. Add parts back one at a time until it fails.
Our guide to reading email headers reads the Authentication-Results and DKIM-Signature headers for you, and the guide reading email headers to debug delivery shows what each line means.
Recompute the hash yourself
For a stubborn case, you can reproduce the receiver's calculation. Tools such as dkimpy let you verify a saved message and print the result. The sketch below checks a message file using Python.
# pip install dkimpy
import dkim
with open("received.eml", "rb") as f:
raw = f.read()
d = dkim.DKIM(raw)
ok = d.verify(logger=__import__("logging").getLogger("dkim"))
print("verified:", ok)Run it once on the message as it left your system and once on the message as it arrived. If the first passes and the second fails, the change happens in transit. If both fail, the change happens inside your own pipeline before or during signing.
Fixes in detail
Switch to relaxed canonicalization
In most mail servers and sending platforms, the canonicalization setting is in the signing configuration. Choose relaxed for both headers and body. This single setting removes a whole class of whitespace and wrapping failures. It does not protect against content changes such as an added footer, but it makes the signature far more tolerant of everyday handling.
Move the signing step to the end
If your mail passes through several components, such as an application, a mail server, a gateway that adds a disclaimer and a tracking layer, sign in the last component that edits the content. A common mistake is signing in the application, then letting a gateway add a footer. Either move the disclaimer into the application template or move the signer to the gateway.
Sign with the provider that rewrites links
If your provider rewrites links for click tracking, it must sign after it rewrites. A good provider does this automatically, but a setup that signs in your own server and then sends through a relay that adds tracking breaks the signature. Choose one place for signing and one place for tracking, and make sure the order is rewrite first, sign last. Our guide to tracking domains and alignment explains the pieces.
Fix encoding problems
Some body hash failures are caused by encoding rather than by edits. A message that contains 8-bit characters and passes through a server that does not support the 8BITMIME extension is converted, and the hash changes. To avoid the risk, encode non-ASCII content as quoted-printable or base64 before signing so that the transfer encoding is already safe. Keep the number of components that re-encode a message to one, and avoid very long lines, because some relays wrap them. See MIME, multipart and content types for how bodies are structured.
Accept that some failures belong to the recipient side
Mailing lists and forwarders change messages by design. A body hash failure at a list is not a defect in your setup. That is why DMARC accepts either SPF or DKIM to pass, and why ARC exists so that intermediaries can vouch for the original result. Your job is to make sure that mail sent directly to a mailbox verifies cleanly, and to keep SPF aligned as a second line of defence.
Confirm the fix
- Send a test to Gmail and to at least one other provider.
- Open the original message and confirm
dkim=passwithheader.dmatching your From domain. - Check that
bh=in the signature is stable when you send the same message twice without personalization. - Review your DMARC aggregate reports over the next few days. Failures that remain should come from lists and forwarders, not from your own streams.
If a message still fails at one recipient only, ask that recipient's administrator whether a gateway edits inbound mail, and share the raw header so they can see the mismatch. Our guide on DMARC aggregate reports shows how to see which sources fail and how often.
Check your sending domain Test the SPF, DKIM and DMARC records for your domain and see which ones pass.
References and further reading
Frequently asked questions
What does "body hash did not verify" mean?
The receiver calculated a different hash of the message body than the one recorded in the DKIM signature. The body was changed after it was signed, so the signature no longer matches.
Is the DKIM DNS record wrong if I see this?
Usually not. The receiver found the key and the signature is structurally valid. The problem is that the content changed after signing.
Should I use relaxed or simple canonicalization?
Use relaxed for both header and body. It tolerates whitespace and line-wrapping changes that often happen in transit, so signatures survive more handling.
Why does it fail only for mail sent through a mailing list?
Lists often add footers or subject prefixes, which changes the body. That is expected, and DMARC still passes if SPF passes and aligns.
Can link tracking cause this?
Yes. If links are rewritten after signing, the body hash no longer matches. The tracking step must run before the signing step.