Skip to content

dkim=fail "Body Hash Did Not Verify" — Causes and Fixes

Short answer

"Body hash did not verify" means the message body changed between signing and verification. The bh= value in the DKIM-Signature no longer matches a fresh hash of the body. The key and the signature are fine — something in transit edited the message, usually a mailing list, a security appliance, or a relay.

Check it yourself
DKIM Checker
Find and validate DKIM keys — selector discovery included

DKIM signs two things separately. The bh= tag is a hash of the canonicalized message body. The b= tag is a signature over selected headers *plus* the bh= value. A verifier recomputes the body hash first; if that does not match, it stops and reports dkim=fail (body hash did not verify) without even checking the header signature.

That error is therefore a very specific diagnosis, and a useful one: the key resolved, the syntax was fine, and the body was modified after signing. It is not a DNS problem and not a key problem. Contrast it with dkim=fail (no key for signature) or dkim=permerror, which are DNS-side and are covered in how to find your DKIM selector.

Email Header Analyzer
Decode Received chains, SPF/DKIM/DMARC results, and delays

How do I prove it from the headers?

View the raw source of the failing message. You want the Authentication-Results line added by the receiving server and the DKIM-Signature header it was judging.

Authentication-Results: mx.example.net;
       dkim=fail reason="body hash did not verify"
            header.d=example.com header.s=selector1 header.b=Xk9pQ2mR;
       spf=pass [email protected];
       dmarc=fail (p=NONE) header.from=example.com

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
        s=selector1; t=1756684800;
        h=from:to:subject:date:message-id;
        bh=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=;
        b=Xk9pQ2mR7vN4tY8wL3cE5aB1dF6gH0jK2lM4nO7pQ9rS...
The receiver names the exact reason and the selector it used

Three details make the diagnosis:

  1. reason="body hash did not verify" — the body, not the headers, is the problem. Not every receiver includes a reason string, but Google, Microsoft, and most open-source MTAs do.
  2. header.d= and header.s= tell you which signature failed. If the message carries several DKIM-Signature headers, only one may have failed.
  3. The Received: chain above the failing Authentication-Results shows every hop the message took. Whatever modified the body is one of those hops.

Then run the control experiment: send the same message content directly from the same system to the same recipient domain, bypassing whatever intermediary is in the chain. If the direct send passes DKIM and the routed one fails, the intermediary is doing it. If both fail, the signing side is producing a hash that never matched — look at the sending path first. Paste both sets of headers into the email header analyzer to line up the hops.

Confirm the key itself is healthy while you are there, so you can rule it out entirely:

dig +short TXT selector1._domainkey.example.com
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
A resolvable key with a non-empty p= rules out the DNS side
DKIM Checker
Find and validate DKIM keys — selector discovery included

What actually modifies a body in transit?

Mailing lists

The single most common cause. Traditional list managers append an unsubscribe footer, prepend a [list-name] tag to the subject, or convert the message to plain text. Any of those breaks the body hash — or the header signature, if subject was in the signed h= list.

The correct fix is on the list side, not yours: the list should either leave the body alone, or use From: rewriting so it takes responsibility for the message under its own domain. ARC (Authenticated Received Chain) lets a downstream receiver see that the message authenticated before the list touched it, but it only helps if the receiver evaluates ARC and trusts that list — treat it as mitigation, not a guarantee. Practically, mailing lists are the reason a DMARC rollout leaves a residue of failures that never goes to zero; see p=none vs quarantine vs reject.

Security appliances and gateways

  • Antivirus or DLP gateways that append a "scanned by" banner or a confidentiality notice.
  • URL-rewriting link protection that replaces every href with a click-tracking wrapper — this rewrites the HTML part and always breaks the hash.
  • Attachment sandboxing that strips or replaces an attachment with a stub, changing MIME part boundaries.
  • Disclaimer-stamping appliances that add a legal footer on egress. If this sits *after* your signer, every outbound message is broken before it leaves.
Ordering matters more than anything else here. Any device that modifies the body must sit before the DKIM signer on the way out. A disclaimer appliance placed after the signer breaks 100 percent of your outbound DKIM, and it will look like an intermittent receiver problem because you only notice at receivers that report it.

Encoding and line-ending changes

A relay that re-encodes a body from quoted-printable to base64, or downgrades 8-bit content to 7-bit, changes the bytes being hashed. Older relays that do not support the 8BITMIME extension will do this legitimately. Line endings matter too: SMTP requires CRLF, and a relay that normalizes bare LF to CRLF — or the reverse — will change the hash.

Canonicalization is your defence. The c= tag has two halves, header and body:

CanonicalizationToleratesRecommendation
c=simple/simpleAlmost nothing beyond trailing empty linesAvoid — fragile in real routing
c=relaxed/relaxedWhitespace runs, header case, header unfolding, trailing whitespace on body linesUse this. It is what nearly all modern signers default to
c=relaxed/simpleHeader variation onlyRare; no reason to prefer it

Relaxed body canonicalization ignores whitespace differences at the end of lines and collapses runs of whitespace, which absorbs the most common benign rewrites. It does not save you from an appended footer — nothing does.

The l= tag

The optional l= tag says "only the first N octets of the body are covered by this hash". It was designed exactly for the mailing-list footer problem, and it is a bad trade.

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
        s=selector1; l=1234;
        h=from:to:subject; bh=...; b=...
A length-limited signature — avoid publishing these

With l= present, anyone can append arbitrary content past the signed length and the signature still verifies. In an HTML message that means an attacker can add whole visible sections to a message that authenticates as yours. Some receivers ignore l= outright or treat its presence as a negative signal. If your signer sets it, turn it off. If you inherited a configuration with l=, that is a finding, not a workaround.

Forwarding

Plain forwarding is usually fine for DKIM — it is SPF that breaks, because the forwarder is not in your SPF record. That asymmetry is why aligned DKIM is what keeps forwarded mail delivering under an enforcing DMARC policy. But a forwarder that also filters, rewrites links, or adds a banner breaks DKIM too, and then nothing survives the hop. Check what your SPF says with the SPF checker and confirm the alignment picture with the DMARC checker.

A working checklist

  1. Confirm the reason string really is "body hash did not verify" and not a key or syntax failure.
  2. Check whether the failure is universal or path-specific. Send to two receivers, one direct and one through the suspect route.
  3. Read the Received: chain on the failing message and list every hop that is not you and not the final receiver.
  4. Identify anything in your own outbound path that appends or rewrites content, and confirm it runs before the signer.
  5. Switch the signer to c=relaxed/relaxed if it is not there already.
  6. Remove l= from the signing configuration if present.
  7. If the modifying hop is a mailing list you do not control, accept it, and make sure your DMARC rollout accounts for that residue rather than trying to eliminate it.
  8. Verify the key still resolves with the DKIM checker and the whole picture with the deliverability test.
If DKIM breaks for every recipient rather than on one route, look at your own egress path before blaming anyone else. A disclaimer appliance, a smart host, or a signing configuration change is far more likely than the entire internet rewriting your mail.

Frequently asked questions

Is a body hash failure a security problem?
It means the message you sent is not byte-identical to the one that arrived, which is exactly what DKIM exists to detect. In practice the cause is almost always a benign intermediary rather than an attacker. It is still worth identifying which hop is responsible, because you cannot tell benign from malicious from the failure alone.
Why did the same message pass at one receiver and fail at another?
Different routing. One recipient received the message directly; the other received it via a list, a forwarder, or an inbound gateway that modified it. Compare the Received: chains of both copies and the differing hop will be obvious.
Will switching to relaxed canonicalization fix it?
It fixes whitespace and line-ending differences, which is a real and common category. It cannot fix appended footers, rewritten links, or re-encoded MIME parts, because those change actual content rather than formatting.
Should I use the l= tag so mailing list footers do not break my signature?
No. Anything appended past the signed length verifies as part of your authenticated message, which lets someone add visible content to mail that authenticates as yours. Some receivers ignore l= or penalise it. Accept the list failures instead, or ask the list to stop modifying bodies.
Does DMARC still pass if DKIM fails on body hash?
Only if SPF passes in alignment with the From: domain. When the modifying hop is a mailing list or forwarder, SPF is usually broken too because the relay is not in your SPF record, so DMARC fails overall. That combination is the classic mailing-list DMARC problem.
Can I re-sign the message after modifying it?
Only if you control the modifying system and it is authorized to sign for that domain. That is what mailing lists do when they rewrite From: to their own domain and sign as themselves. You cannot re-sign someone else's domain, which is the whole point of DKIM.

Last updated: 2026-09-01