DKIM Fail: Body Hash Did Not Verify (How to Fix)

What dkim=fail (body hash did not verify) means, how bh=, l= and c= canonicalization work, the footers, gateways, re-encoding and forwarding that break DKIM after signing, and how to find the hop and fix it.

Home › Guides › DKIM body hash did not verify

Last updated 18 October 2026

Quick answer

dkim=fail (body hash did not verify) means the message body that arrived is not the body that was signed. The receiver hashed the body again and got a different value from the one stored in the signature's bh= tag. Your DNS key record isn't the problem: the body hash check doesn't use the key at all. Something changed the message after it was signed.

The usual suspects, in rough order:

  1. A footer, disclaimer, banner or email signature added by a server, gateway or signature service that handles the mail after the signing step.
  2. A security gateway that rewrites links or inserts warning text.
  3. A relay that re-encodes the message (line endings, 8-bit to 7-bit, base64 or quoted-printable) or a mailing list or forwarder that edits it.

The fix is to sign last: add every footer and rewrite before the message is signed, or move DKIM signing to the last server you control. Then use c=relaxed/relaxed, add ARC for forwarding paths, and keep an aligned SPF pass as the second route to DMARC.

What the DKIM results mean

Receivers report DKIM in the Authentication-Results header with one of the result values defined in RFC 8601 (section 2.7.1). The text in parentheses is a comment from the receiver; it carries the reason.

ResultMeaning (RFC 8601)What to look at
passSigned, and the signature passed verification.For DMARC, also check that header.d aligns with the From domain.
failSigned, but the signature failed verification.The comment. body hash did not verify: the body changed. signature did not verify: a signed header changed, or the key doesn't match.
neutralSigned, but the signature had syntax errors or couldn't be processed; also used for other failures not covered elsewhere.The comment. A receiver can report a broken signature as neutral so it is treated as if unsigned.
noneNot signed.Whether a DKIM-Signature header is there at all, or was stripped on the way.
policySigned, but some aspect of the signature wasn't acceptable to the receiver.The receiver's own rules, for example about algorithms.
temperrorA temporary error, such as being unable to fetch the public key right now.DNS for <selector>._domainkey.<domain>; a later try may work.
permerrorAn error that won't fix itself, such as a required header field being absent.The DKIM-Signature syntax and the key record.

RFC 6376 (section 6.1.3) defines the two failure reasons you see most. The verifier first compares the body hash with bh=; on a mismatch it SHOULD ignore the signature and return PERMFAIL (body hash did not verify). Then it checks the b= signature with the public key; on a mismatch it returns PERMFAIL (signature did not verify). Receivers differ in which of fail, neutral or permerror they put on the result line for a given problem, so always read the comment.

How bh=, l= and c= work

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
  s=s1; h=from:to:subject:date:message-id;
  bh=<base64 hash of the body>; b=<base64 signature>

RFC 6376 section 3.7 says signer and verifier compute two hashes:

  1. Body hash. The body is canonicalized with the body algorithm from c=, cut to the length in l= if there is one, hashed, and stored in base64 as bh=.
  2. Header hash. The header fields listed in h=, plus the DKIM-Signature header itself (including bh=, with b= left empty), are canonicalized and hashed. That hash is what the private key signs into b=.

So a changed body breaks bh=, and a changed signed header (a [EXTERNAL] tag added to the Subject, say) breaks b=. Two details catch people out:

Canonicalization: simple vs relaxed

The c= tag names the header and body algorithms, separated by a slash. If it is missing, the default is simple/simple. If only one name is given, it applies to the header and the body stays simple, so c=relaxed means relaxed/simple.

AlgorithmWhat it tolerates (RFC 6376 section 3.4)
Header simpleNothing. Header fields must arrive exactly as signed, including case and whitespace.
Header relaxedHeader names in a different case, re-folded (rewrapped) header lines, runs of spaces or tabs, and whitespace at the end of a value or around the colon.
Body simpleOnly extra empty lines at the end of the body.
Body relaxedWhitespace at the end of lines, runs of spaces or tabs inside a line (reduced to one space), and extra empty lines at the end.

Neither body algorithm tolerates added or removed text, lines broken in different places, changed characters or a different transfer encoding. Relaxed canonicalization protects against whitespace changes, not against footers.

Canonicalization also assumes the message is already in standard form, with CRLF line endings. RFC 6376 section 5.3 says bare CR or LF must be converted to CRLF before signing, and that conversion of 8-bit text to 7-bit in transit breaks signatures, so signers SHOULD encode 8-bit content as quoted-printable or base64 first.

Common causes

Step 1: Read the failing message's headers

Take the copy that arrived at the receiving mailbox (open the original or view the message source), and paste the headers into the Email Header Analyzer. Find the receiver's own Authentication-Results line:

Authentication-Results: mx.receiver.example;
  dkim=fail (body hash did not verify) header.d=example.com header.s=s1;
  spf=pass smtp.mailfrom=example.com;
  dmarc=pass header.from=example.com

Step 2: Find which hop changed the message

  1. In the analyzer's route table (oldest hop first), list every server after the one that signed: outbound gateways, signature services, relays, list servers, forwarders, the receiver's own filters.
  2. Look for ARC-Authentication-Results and ARC-Seal headers. Each ARC set records what that intermediary saw when the message arrived there. If i=1 says dkim=pass for your domain and the final receiver says fail, the change happened at that intermediary or after it.
  3. Compare the received body with your sent copy: an added footer, a banner, rewritten links or a different encoding usually shows up quickly.
  4. Isolate the path. Send a test that skips the suspected hop (straight from the mail platform, without the list or forwarder) to an outside mailbox. If that copy passes, the hop you skipped is the cause.

Seeing signature did not verify, permerror or no key instead? Then check the key: the DKIM Checker finds published selectors and checks key length, and the DKIM Record Explainer decodes the key record tag by tag. For permerror or a missing key, how to fix DKIM no key for signature covers the causes step by step. For signature did not verify, also check the signed headers: a Subject tag or rewritten From breaks it too, as how to fix DKIM signature did not verify explains. For a body hash failure the key is fine, so changing or rotating it won't help.

Fix 1: Sign after the last change

DKIM has to be applied to the message exactly as it will leave your systems. RFC 6376 section 5.3 puts it this way: the signer must sign the message as it is expected to be received by the verifier.

Fix 2: Use relaxed/relaxed canonicalization

If your signer offers it, set c=relaxed/relaxed. It survives header rewrapping and whitespace changes that break simple. It won't save a message that gets a footer, so treat it as a complement to Fix 1. Avoid using l= to let footers through: the appended text is unsigned, many footers are inserted inside the MIME or HTML structure rather than at the very end anyway, and RFC 6376 says verifiers might ignore signatures that use it.

Fix 3: Inbound gateways, forwarders and mailing lists (ARC)

When your own security gateway rewrites inbound mail before the mailbox platform checks DKIM, the mailbox sees a broken signature for mail that was fine on arrival. Check authentication at the first hop, or have the gateway add ARC and configure the mailbox platform to trust that sealer, if both support it.

ARC (RFC 8617, an Experimental RFC) lets each intermediary record the authentication results it saw (ARC-Authentication-Results), sign the message as it passed through (ARC-Message-Signature) and seal the chain (ARC-Seal, with cv=none, pass or fail). A receiver MAY, as local policy, accept those earlier results when deciding whether the message passes DMARC (RFC 8617 section 7.2.1). You can't make other people's forwarders or lists use ARC; that is why the next fix matters. For what forwarding and lists do to DMARC as a whole, see why DMARC fails after forwarding or on mailing lists.

Fix 4: Keep aligned SPF as the other route to DMARC

DMARC passes if either aligned SPF or aligned DKIM passes. For mail delivered directly, a passing SPF check on a MAIL FROM domain under yours keeps DMARC passing even when something breaks the body hash, as in the example above. Set up a custom return-path at each sender that offers one; the DMARC alignment guide explains how, and the SPF Record Checker confirms the record. SPF doesn't survive forwarding, so this is a safety net, not a replacement for working DKIM.

Verify the fix

  1. Send a new test from each path (direct, through the gateway or signature service, through the list or forwarder) to an outside mailbox.
  2. Paste the headers into the Email Header Analyzer and confirm dkim=pass with header.d set to your domain, and dmarc=pass.
  3. Check your published policy with the DMARC Record Checker, then watch a week of aggregate reports in the DMARC Report Analyzer: the DKIM result for each source should be pass for your d= domain.

Audit hundreds of domains at once

Need SPF, DMARC and MX checks across a whole client list? Run the Bulk SPF / DMARC / MX Auditor on Apify ($2 per 1,000 domains).

Related tools and guides

Read DKIM results, signatures and ARC sets with the Email Header Analyzer, find published selectors with the DKIM Checker, decode a key record with the DKIM Record Explainer, review your policy with the DMARC Record Checker and see each source's DKIM result in the DMARC Report Analyzer. Related guides: DMARC fail with SPF pass (alignment), DMARC for Microsoft 365 and DKIM for Microsoft 365.

FAQ

What does dkim=fail (body hash did not verify) mean?

The receiver hashed the message body and the result did not match the bh= value in the DKIM-Signature header, so the body changed after it was signed. Footers, disclaimers, link rewriting, banners, re-encoding and mailing lists are the usual causes.

Can a wrong DNS key cause body hash did not verify?

No. The body hash is compared with bh= without using the public key. A wrong, missing or rotated key shows up as signature did not verify, no key, or permerror instead. Fix a body hash failure on the mail path, not in DNS.

Does relaxed canonicalization fix body hash failures?

Only those caused by whitespace: trailing spaces, runs of spaces or tabs, and extra empty lines at the end. It does not tolerate added text such as a footer, changed line breaks or a different transfer encoding. The default when c= is missing is simple/simple, and c=relaxed alone means relaxed/simple.

Should I use the l= tag so footers do not break DKIM?

It is not recommended. l= leaves everything after the given length unsigned, so anyone along the path can append content, and RFC 6376 says verifiers might ignore signatures that use it. Add footers before signing instead.

Will DMARC pass if DKIM fails with body hash did not verify?

It can, if SPF passes for a MAIL FROM domain aligned with the From address, or if another DKIM signature for an aligned domain still verifies. Forwarded mail usually loses SPF, so fixing the DKIM break is the reliable answer.

Want the full kit?

Runbooks, checklists and helpdesk templates: IT Admin Packs on Gumroad.