DKIM "body hash did not verify"
“body hash did not verify” means your DKIM signature was correct when the message left your server, and something rewrote the body before the receiver checked it. Your key in DNS is fine, and so is the signer. Re-publishing or rotating the key won’t change the verdict, because the key wasn’t the problem. You’re hunting for the hop that edits your mail after it has been signed, and there are four usual suspects.
The line in the headers
Open the full headers of a bounced or junked message and find the receiver’s verdict. It looks like this:
Authentication-Results: ...; dkim=fail (body hash did not verify)
You may also see it as dkim=neutral (body hash did not verify). Either way the text in brackets is the part that matters, because it tells you which of the two hashes in your signature failed.
A DKIM signature (RFC 6376) carries two of them. bh= is a hash of the message body exactly as it stood at signing time. b= is the signature proper, computed over the selected headers plus that body hash. The verifier fetches your public key, recomputes the body hash from the message it received and compares the result with bh=. If the two don’t match, verification stops there, and the receiver writes this string without checking b= at all.
What the verdict rules out
This narrow a failure helps you, because it means the verifier found your key, parsed your signature and got as far as hashing the body. DNS, the selector and the signing configuration are all sound, or the receiver would have said something else.
Different strings point at different problems. signature did not verify means the headers or the b= value are wrong, which usually is a key or signer fault. no key for signature means the receiver couldn’t fetch a key at the selector the signature named, and finding your DKIM selector is where to start for that one.
In your case the body changed between signing and verification. One appended line is enough, because a hash function has no notion of a small change.
The four things that rewrite a body
Something on the delivery path edited the message after your signer sealed it. In order of how often it turns out to be the cause:
| Modifier | What it changes | The tell |
|---|---|---|
| Outbound gateway or smart host | Appends a legal disclaimer, marketing footer or EXTERNAL banner after the mail server has already signed | All mail on that route fails; a direct send that bypasses the gateway passes |
| Security appliance or link protection | Rewrites URLs into scanning redirects or tracking wrappers | Only messages containing links fail |
| Mailing list | Adds a subject tag and a list footer, sometimes reflows the body | Only mail sent through the list fails |
| Forwarder or relay | Re-encodes MIME, transcodes the charset, re-wraps long lines | Failures cluster on one recipient or one forwarding address |
Start at the top. The most common shape is a mail server that signs and an edge device that edits the message after it has been signed. Nobody configured that on purpose; the gateway was added later, the disclaimer rule was added later still, and no one went back to check where signing happened.
Two test messages find the hop
You’re comparing a path that works against a path that fails, so send two messages and read both sets of full headers.
- Send a direct test to a mailbox you control at a major receiver, bypassing as much of your outbound chain as you can. Sending from the mail server itself, or from a client that submits straight to it, is ideal.
- Send a second message along the failing path: through the gateway, through the list, to the affected recipient’s domain.
- Compare the
Authentication-Resultslines. If the direct send showsdkim=passand the failing path showsdkim=fail (body hash did not verify), the modifier lives between those two routes. - Read the
Received:chain in the failing message. The relay that appears there and nowhere in the direct send is your hop. - Look at the delivered body. A footer you didn’t type, or a link rewritten to a scanning domain, names the culprit outright.
Your DMARC aggregate reports show the same pattern at scale. A source that consistently reports DKIM fail alongside SPF pass is usually your own route modifying mail in transit, so look for your own gateway’s address in that row before you go looking for an attacker.
Fix it in this order
Sign at the outermost device that touches the message, and disable signing upstream. If a gateway is the last thing your mail passes through before the internet, the gateway should hold the private key and add the signature, and the mail server behind it should stop signing. If a hosted mail platform is your last hop, sign there and let nothing edit mail after it. This is the correct fix because it puts the seal on after the last edit, whatever that edit is.
If you can’t move the signing point, take the edit out of the transport path. Move the disclaimer into the client signature or the message template, where it’s part of the body before signing. Scope the EXTERNAL banner to inbound mail only; your own outbound mail isn’t external, and the banner is what’s breaking the hash. Exempt authenticated outbound mail from link rewriting, or apply the rewriting upstream of the signer.
Then send the two test messages again. Both should show dkim=pass with your domain in d=.
Why relaxed canonicalisation and l= won’t save you
Relaxed body canonicalisation (c=relaxed/relaxed) tolerates whitespace and line-ending changes, so a relay that re-wraps a long line stops breaking your hash, and that’s worth having. An appended disclaimer is a content change, so the hash still fails on it, as it should. If switching to relaxed made your failures disappear, the modifier was only reflowing lines and you’ve got off lightly.
The l= tag will tempt you. It tells the verifier to hash only the first N bytes of the body, so an appended footer no longer breaks the signature. That works by leaving the end of your message unsigned. Anyone holding a legitimate signed message from you can append whatever they like, and your valid signature still verifies over the result. Leave it out and fix the modification.
Mailing lists and forwarders
A list that adds a subject tag or a footer breaks your body hash by design, and you don’t control the list. There’s no sender-side setting that makes a list stop editing your mail. ARC, the Authenticated Received Chain, is the list’s and the receiver’s mechanism, so there’s nothing on your side to set: the list attests to what authentication looked like on arrival, and the receiver decides whether to trust that attestation.
What you do control is how hard DMARC punishes the residue. A staged rollout, from p=none through p=quarantine with a pct= ramp while you read the aggregate reports, keeps list-mangled mail in the quarantine folder while you work out how much of your traffic goes through lists at all.
Forwarding is the neighbouring case. It reliably breaks SPF and only sometimes breaks DKIM, which is why an aligned DKIM pass is the leg that survives forwarding when the body does. DMARC needs one aligned pass, and an SPF pass that isn’t aligned to the From domain never counted for DMARC in the first place. If the failure only happens to one customer’s domain, their side is almost certainly modifying inbound mail before their verifier runs, and the fix sits on their gateway. The email delivery piece covers the other reasons a message goes missing once authentication is sound.
How rare a working chain is
DKIM has to survive the trip; SPF and DMARC are read from DNS and don’t. The September 2026 census can only see part of the picture here, since a scanner can’t know your selector unless it observes one, so its DKIM figure is a floor. Of the 75,823,110 domains where the DKIM check could run, a selector was found on 37,999,462 (50.1%); for 240,777,792 domains the check couldn’t determine anything. How many domains sign what they send is a different question, and one a scanner can’t answer.
The other figure worth knowing is how many domains have all three legs in place at once. Of the 316,600,902 domains carrying a grade in the September 2026 census, 7,862,126 hold SPF, a DKIM key and an enforcing DMARC policy at once. Getting a body hash failure fixed is the hard part of joining them, because the record side, covered on the DKIM page, is a DNS edit and this part is an audit of your own transport path.
Send the two test messages today. Read the Received: chain for the relay that only appears in the failing one, then move the signing point past it. Once both tests pass, run the free scan again to confirm the record side still reads clean.
Figures as of 5 September 2026, from the September 2026 edition of the defaults.exposed census. Census numbers move every month; the current values are on the census data page.