Skip to main content
Relaymetry

DKIM body hash did not verify: what causes it and how to fix it

DKIM stores a hash of the message body in the bh= tag of the signature. A body hash did not verify failure means the verifier recomputed that hash from the body it received and it did not match — the body changed after signing. The usual cause is a forwarder or mailing-list footer, a gateway rewriting content, or an encoding change in transit; the other is a signing misconfiguration. It is not a missing-key or DNS problem, so fix the modification or your canonicalization, not the record.

DKIM selector identifies a specific signing key. Common values: google, default, s1, selector1, mailchimp, sendgrid. Find yours in the DKIM-Signature header (d=domain s=selector).

Quick answer

A DKIM signature carries a hash of the message body in its bh= tag. "Body hash did not verify" means the receiving server recomputed the hash of the body it got and it did not match bh=, so the body changed after signing. The cause is almost always something altering the body in transit (a mailing-list or forwarder footer, a gateway rewriting content, or an encoding change), or a signing misconfiguration. It is not a missing-key or DNS problem.

What the body hash is

DKIM does not sign the whole message in one step. It produces two separate hashes, defined in RFC 6376 §3.7. One is the hash of the message body, stored in the signature's bh= tag. The other is a hash over the selected header fields, and that second hash includes the bh= value. The b= tag is the cryptographic signature over those headers, so b= indirectly covers the body through bh=.

That split is why the failure is specific. When verification reports "body hash did not verify", it means the body hash check failed before the header signature was even considered. The b= signature over the headers can be sound and the key in DNS can be correct, yet the body no longer produces the bh= the signer recorded.

How the body is turned into bytes before hashing is set by the canonicalization tag c=, which takes the form header/body. RFC 6376 §3.4 defines two body canonicalization algorithms. simple tolerates almost no change: only empty lines at the very end of the body are ignored, and any other difference breaks the hash. relaxed is more forgiving: it ignores trailing whitespace on each line, collapses runs of whitespace within a line to a single space, and ignores trailing empty lines. Both signer and verifier must canonicalize the same way, because the c= value travels in the signature and the verifier follows it.

Why the body hash fails

The common cause is modification in transit. A mailing list appends a footer or rewrites the subject with a [list] tag and adds an unsubscribe block to the body. A forwarder or a security gateway inserts a banner, rewrites links for click-tracking, or strips and re-adds an attachment. A relay changes the Content-Transfer-Encoding, re-wrapping or re-encoding a part. Every one of these changes the bytes the verifier hashes, so the recomputed body hash no longer matches bh=. With simple body canonicalization even a single re-wrapped line is enough; relaxed survives whitespace noise but not added or removed content.

The other cause is on the signing side. If the body is normalized differently after signing than the c= you chose anticipates, the hash drifts. The sharpest signer-side trap is the l= body-length tag from RFC 6376 §3.5. l= tells the verifier to hash only the first N octets of the body, so anything appended past that length is not covered by the signature. RFC 6376 §8.2 warns that this lets an attacker append arbitrary content while the signature still verifies, and a mismatch between the signed length and the actual body can also produce verification failures. The tag is optional and the safe choice is to omit it so the whole body is signed.

How to diagnose it

Start from a received copy. The Authentication-Results header records the verdict, and a body hash failure shows as dkim=fail with the reason body hash did not verify. That string is the signal that distinguishes this class from a key or selector problem. Read the header on the actual message that failed, not a fresh test, so you see the result for the path that broke.

Then isolate the hop. Send or collect one copy received directly from the sender and one received through the path that fails: the mailing list, a forwarder, a gateway. Compare the bodies. Whatever differs is the modifier: an appended footer, a rewritten URL, a re-encoded part. The change you can see is the change that broke the hash.

Two tools help. The email header analyzer renders the Received chain and the authentication results so you can read where the message traveled and what each hop reported. The DKIM checker confirms the selector side, that the public key exists and resolves, which is worth ruling out so you know you are looking at a body problem and not a key problem.

How to fix it

Sign with relaxed body canonicalization, c=relaxed/relaxed. Relaxed body canonicalization absorbs the whitespace and line-wrapping churn that intermediaries routinely introduce, so tolerable changes no longer break the hash. It will not save you from added or removed content, but it removes a large class of false failures.

Drop the l= tag. Sign the entire body rather than a prefix, so appended footers are detected as the modifications they are and the signature covers everything it claims to cover.

For a modifier you operate (your own list manager, forwarder, or outbound gateway), re-sign the message after it makes its changes, or route the message through a path that does not touch the body. The signature has to be applied as the last step that alters the content.

For a third party you do not control, you cannot keep DKIM intact. A list that rewrites the body and does not re-sign will always break your signature. This is where DMARC helps: DMARC passes on either aligned DKIM or aligned SPF, so a message that travels an aligned SPF path can still satisfy DMARC even when the forwarded DKIM signature is broken. You lean on the alignment either-or instead of trying to preserve a signature the intermediary destroys.

How this differs from "DKIM record not found"

These are two different failures with two different fixes. "DKIM record not found" means the verifier looked up the public key at the selector's DNS name and found nothing, so there was no key to verify against, a DNS or selector problem, covered in DKIM record not found. "Body hash did not verify" means the key was found and the signature was processed, but the body no longer matches what was signed, an in-transit-modification or signing-configuration problem. Confirming which one you have is the first step, because the remedies do not overlap.

Gmail returns the SMTP code 5.7.30 when it rejects a message for a DKIM authentication failure, and a broken body hash is one way to land there. If you are chasing a 5.7.30 bounce, the diagnosis is the same: read the authentication result and find the hop that altered the body. The dedicated guide to Gmail 5.7.30 DKIM failures walks through the bounce end to end.

Why the DKIM body hash fails: signer hashes the body, a forwarder appends a footer, the verifier recomputes a different hash, mismatch

Frequently asked questions

What does 'body hash did not verify' mean in DKIM?

DKIM signs two things: a hash of selected headers (the b= signature) and a separate hash of the message body (the bh= value). "Body hash did not verify" means the verifier recomputed the hash of the body it received and it did not match the bh= the signer recorded. In other words the body changed between signing and verification, or the body hash was computed differently on each side. The DNS key and selector can be perfectly fine and you will still see this failure.

What causes a DKIM body hash mismatch?

Almost always something altered the body in transit: a mailing list or forwarder appending a footer or [tag] to the subject and body, a security gateway rewriting links or adding a banner, or a hop changing the Content-Transfer-Encoding. The other cause is signer-side: the wrong canonicalization (c=) for how the body is later normalized, or an l= body-length tag that does not cover the whole body. It is not a missing-record or wrong-key problem — that is a different failure.

How do I diagnose which hop broke the body hash?

Read the Authentication-Results header on a received copy: dkim=fail with "body hash did not verify" confirms the class. Then compare a copy received directly with one received through the path that fails (a list, a forwarder). Whatever differs in the body — an appended footer, a rewritten link, a re-encoded part — is the modifier. An email header analyzer makes the Received chain and the auth results readable.

How do I fix DKIM body hash failures?

Sign with relaxed body canonicalization (c=relaxed/relaxed) so tolerable whitespace changes do not break the hash, and do not use the l= length tag, which invites tampering and partial coverage. If a forwarder or list you control modifies messages, have it re-sign after modification, or send through a path that does not alter the body. You cannot fix a third-party list that rewrites mail and does not re-sign — DMARC handles that case through the DKIM-or-SPF alignment either-or.

Is 'body hash did not verify' the same as 'DKIM record not found'?

No. "DKIM record not found" means the verifier could not retrieve a public key at the selector DNS name, so there is nothing to verify against. "Body hash did not verify" means the key was found and the signature was checked, but the body no longer matches what was signed. They have different causes and different fixes — the first is a DNS/selector problem, the second is an in-transit-modification or signing-configuration problem.

Other email authentication problems

References

Browse all guides →