Dr.Who
← blog

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

· dkim · email · deliverability · authentication · canonicalization

dkimemaildeliverabilityauthenticationcanonicalization

A DKIM "body hash did not verify" error means the body of the message that arrived is not the body the signer hashed. DKIM stores a hash of the body in the bh= tag of the signature; the verifier recomputes that hash over the bytes it received, and a mismatch means something rewrote the body in transit. This is almost always caused by a mailing list, forwarder, or appliance that appended a footer or disclaimer after signing — not by a broken key or a spoofer.

What the body hash actually checks

A DKIM-Signature header carries two hashes. The bh= tag is the base64 hash of the message body; the b= tag is the signature over the headers. The verifier does the body hash first: it canonicalizes the received body, hashes it, and compares to bh=. If those do not match you get "body hash did not verify" and it never even reaches the header signature.

A real signature looks like this:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
 d=example.com; s=mail; t=1718900000;
 h=from:to:subject:date;
 bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
 b=AbCd...

That c=relaxed/relaxed is the canonicalization: the first value applies to headers, the second to the body.

The usual causes

Body-hash failures cluster around middleware that touches the body after the signer ran:

  • Mailing lists and forwarders. Mailman, Google Groups, and listservs append an unsubscribe footer or rewrite the subject with a [list-name] tag. The footer alone breaks the body hash.
  • Disclaimer and signature appenders. Corporate gateways that bolt on a legal disclaimer or "Sent from" block change the body bytes.
  • Antivirus and content rewriting. Scanners that rewrite links for click-tracking or "safe links," or normalize HTML — every byte counts.
  • The l= body-length tag. If the signer set l= to cover only the first N bytes, appended content is ignored — but many verifiers treat l= as a red flag, and a partial body is its own risk.
  • Encoding shifts in transit. A relay re-wrapping lines, changing CRLF/LF, or re-encoding quoted-printable moves the hash with no visible change.

Body hash vs header/signature failure

Do not confuse the two. "Body hash did not verify" (bh= mismatch) means the body changed — look at forwarders and appenders. A header or signature failure (the b= check) usually means a signed header was altered, the DNS public key rotated or is missing, or the selector is wrong. Different root causes, so fix the right one. Pull the published key first:

dig +short TXT mail._domainkey.example.com

If the key is present and valid but the body hash still fails, the problem is downstream of your signer.

How to fix it

  • Use relaxed/relaxed canonicalization. Simple canonicalization is intolerant of any whitespace change — a single trailing space breaks it. Relaxed tolerates whitespace normalization and is the practical default.
  • Sign after body-modifying middleware, not before. If a gateway appends disclaimers, it must sign the final outbound bytes. Reorder so the signer is the last hop to touch the body.
  • Drop the l= tag. Signing only part of the body invites both breakage and abuse. Sign the whole thing.
  • Hash exactly the bytes you send. Make sure nothing re-encodes or re-wraps the message between the signer and the SMTP edge.

For mail you forward through lists you do not control, accept that DKIM will break and lean on a sender that re-signs (ARC) plus an aligned SPF path so DMARC can still pass.

Look up your DKIM record and selector →

Further reading