Your dashboards are green. Your mail queue is empty. Yet Gmail (or Microsoft, or a partner’s gateway that still thinks it’s 2009)
says: “DKIM body hash did not verify”. The message arrives. The recipient can read it. But the signature fails,
DMARC alignment falls over, and suddenly your deliverability graph looks like a ski slope.
The maddening part: “DKIM is just a header, right?” No. DKIM is a bet that nobody will touch the message between signing and verifying.
In the real world, everybody touches it—MTAs, security gateways, archivers, footers, “helpful” proxies, and that one vendor who swears they only add
a tracking pixel “in the headers.”
What “body hash mismatch” actually means
A DKIM signature includes a tag named bh= (body hash). The signer canonicalizes the body (according to the DKIM header),
hashes it, base64-encodes that hash, and stores it in bh=. The receiver repeats that process on the received message body.
If the computed hash doesn’t match bh=, you get a body hash mismatch.
That’s distinct from a header signature failure. Header changes can break DKIM too, but a body hash mismatch points directly at:
the body bytes—after canonicalization—are not identical to what the signer saw.
Canonicalization is the trap door
DKIM has two canonicalization modes for headers and body: simple and relaxed. Many deployments use
relaxed/relaxed because it tolerates some header whitespace changes and some body whitespace differences.
“Tolerates some” is doing a lot of work in that sentence.
- simple body canonicalization: almost no forgiveness. Any byte changes (including line endings) break it.
- relaxed body canonicalization: ignores whitespace at line ends, collapses WSP runs, normalizes line endings, and ignores empty lines at the end.
But even relaxed won’t save you if something inserts a footer, re-encodes quoted-printable, changes a MIME boundary, or “normalizes”
line wrapping inside a base64 block. DKIM is tolerant of sloppy whitespace, not creative editing.
A practical mental model: DKIM body hashing is a checksum of “what a human reads,” plus a lot of stuff a human never sees—MIME structure,
transfer encoding, boundaries, and the exact placement of CRLF.
Interesting facts and history that explain today’s weirdness
- DKIM wasn’t born perfect. It came out of two competing proposals (DomainKeys and Identified Internet Mail) that were merged to stop a standards knife fight.
- The “relaxed” modes exist because MTAs kept “helpfully” reformatting mail. The standards didn’t assume a pristine byte stream would survive reality.
- CRLF is not negotiable in SMTP. A surprising number of systems still leak bare LF into pipelines, and DKIM canonicalization is where that crime gets prosecuted.
- Quoted-printable is a DKIM hazard zone. It’s designed to be re-wrapped, and different components can wrap at different columns.
- Some security products intentionally modify messages. They add banners, rewrite URLs, inject disclaimers, or detonate attachments. DKIM doesn’t “understand” intent.
- ARC exists because forwarding breaks DKIM. When a forwarder modifies mail, DKIM fails; ARC was introduced to preserve authentication results across intermediaries.
- “l=” (body length) is both a feature and a footgun. It can prevent breakage by ignoring appended content, but it also lets attackers append content that isn’t signed.
- Mailing lists were a driving reason DKIM adoption was painful. Lists that add footers or rewrite subject lines invalidate signatures routinely.
- DKIM is applied per-message, not per-connection. Any midstream transformation can produce per-recipient failures that look random.
Fast diagnosis playbook (triage order that saves hours)
When you see a body hash mismatch, the temptation is to stare at DNS and keys. Don’t. Keys and DNS usually break verification entirely.
Body hash mismatch screams “message changed.” Here’s the order that finds the bottleneck fast.
1) Confirm it’s really a body mismatch (not a header mismatch)
- Look for verifier logs that explicitly mention
body hash did not verifyorbh=mismatch. - If you only see “bad signature,” you need more visibility: capture the message and verify locally.
2) Identify where the message was signed and where it was verified
- Find the
DKIM-Signatureheader: it tells youd=(domain),s=(selector),c=(canonicalization), and sometimesl=(body length). - Trace the
Received:headers: your modification likely occurs between two adjacent hops.
3) Compare “pre-change” and “post-change” bodies
- Get the message as seen by the signer (or as close as possible: at the signing MTA). Get the message as seen by the receiver (or at your last outbound hop).
- Diff them with tools that respect CRLF and MIME.
4) Hunt the usual modifiers in the chain
- Content filters (Amavis, Rspamd, antivirus gateways, DLP).
- Outbound “brand compliance” footers and disclaimers.
- URL rewriting and click tracking proxies.
- SMTP proxies that re-chunk or re-wrap.
- Archivers / journaling systems that re-inject mail.
5) Fix the process, not the symptom
- Ensure signing happens after all modifications.
- Or stop modifying signed mail, and sign at the last responsible hop.
- If you must modify after signing, accept DKIM will fail and use ARC for downstream trust (where appropriate).
The sneaky causes nobody tells you (and how to prove each)
1) Footers and disclaimers added “late”
The classic. Legal wants a disclaimer. Marketing wants a banner. Security wants an “External Email” warning.
If it’s injected after DKIM signing, the body hash will mismatch. If it’s injected before signing, you’re fine—until some other system injects another one.
How to prove it: locate the added text in the final body and confirm it wasn’t present at the signing hop. The giveaway is a footer line
that appears after the last MIME boundary or in the text/plain part only.
2) Quoted-printable re-wrapping
Quoted-printable (QP) encodes long lines with soft breaks (= at line end). Some gateways re-wrap lines at a different column,
or “normalize” QP by converting spaces/tabs or changing how they break long lines.
DKIM signs the bytes of the encoded body, not the decoded content. If the QP encoding changes, the hash changes.
You can have the same rendered text and still fail DKIM.
3) MIME boundary or part order changes
MIME is a structured document. If a gateway reorders parts, changes boundaries, or converts a multipart/alternative into
a different structure, DKIM breaks. This can happen with “sanitize HTML” filters or attachment detonators that rebuild the MIME tree.
4) Content-Transfer-Encoding changes (8bit ↔ quoted-printable ↔ base64)
Some MTAs downgrade 8bit to quoted-printable when they think the next hop can’t handle 8bit (or when they’re configured to be conservative).
Others do the reverse: they detect QP and “clean it up.” Either way, DKIM hashes what it sees. Changing CTE changes the body bytes.
5) Line ending normalization and the CRLF/LF war
SMTP uses CRLF. But you’ll still meet components that store messages with LF and reconstitute them later. If they do this incorrectly,
you can end up with mixed endings or canonicalization differences that slip past your eyeballs.
With relaxed canonicalization, many line ending issues are tolerated. With simple, you’re playing with matches in a data center.
6) “Helpful” whitespace trimming inside MIME parts
Some content filters trim trailing whitespace, remove “extra blank lines,” or normalize tabs/spaces. Relaxed canonicalization ignores some end-of-line WSP,
but not arbitrary reformatting throughout the body, and not changes in MIME headers inside parts.
7) SMTP chunking proxies and dot-stuffing edge cases
Normally, dot-stuffing is correct and DKIM-safe. But a buggy SMTP proxy can mishandle lines beginning with a dot, or convert leading dots incorrectly when re-injecting.
That’s rare, but when it happens, the diff is tiny and the pain is enormous.
8) Signing the wrong representation (milter ordering / filter pipeline)
If you sign before a filter modifies the body, DKIM fails. If you sign after, it passes. Many stacks accidentally sign at the wrong point:
a milter signs early, then another milter adds a header, or an after-queue content filter rewrites the body.
This failure mode shows up as “it passes for some recipients but not others,” because only some routes hit the modifying component.
9) Rewriting URLs for click tracking
URL rewriting changes the body. Full stop. Some vendors claim to do it “without touching DKIM.” What they mean is:
they might preserve header DKIM for messages they don’t rewrite, or they add their own DKIM signature after rewriting.
But if you expect your DKIM to survive, don’t let anyone rewrite the body after you sign.
10) Mailing list managers and forwarders
Mailing lists add footers, modify headers, and sometimes reformat content. Forwarders can re-encode content or add list-unsubscribe headers.
DKIM breaks; DMARC breaks; then people blame DNS.
11) The l= tag: “fixes” that create security holes
You’ll see advice: “Just set l= so footer additions won’t matter.” This tells verifiers to hash only the first N bytes of the body.
Yes, it can stop body hash mismatches when content is appended.
It also allows an attacker to append content after the signed portion—content that appears legitimate because the DKIM signature still passes.
Many receivers distrust or ignore l= for good reason. Use it only when you’ve thought through the risk model and you control the entire path.
12) Message reconstitution by journaling/archiving systems
Some compliance archivers capture mail, then re-inject it to other systems (journaling, supervision, downstream scanning).
If they reconstitute the message (even “losslessly”), tiny differences creep in: header folding, MIME boundary regeneration, CTE changes.
Body hash mismatch follows.
One paraphrased idea from Richard Cook (reliability engineering): Success hides risk; failures reveal the real system you’re actually operating.
DKIM body hash mismatches are the failure that reveals your real email pipeline.
Joke #1: DKIM is like a tamper-evident seal—great until your own team keeps “just checking what’s inside.”
Practical tasks: commands, outputs, and decisions (12+)
These are the things you do at 02:10 when a VP says “our invoices are going to spam” and you can’t afford a philosophical debate.
Each task includes a command, example output, what it means, and the decision you make.
Task 1: Pull the raw message from Postfix queue (so you’re not guessing)
cr0x@server:~$ sudo postcat -q 3F2A91C02E
*** ENVELOPE RECORDS ***
message_size: 48231 704 1 0
message_arrival_time: Sat Jan 3 01:12:09 2026
sender: billing@example.com
*** MESSAGE CONTENTS ***
Received: from app01 (app01.internal [10.0.12.34])
by mx-out01.example.com (Postfix) with ESMTP id 3F2A91C02E
for <user@recipient.tld>; Sat, 3 Jan 2026 01:12:08 +0000 (UTC)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=mail;
h=from:to:subject:date:mime-version:content-type;
bh=VQeN9v5kG7WJQm...snip...=;
b=Q0Vn...snip...
Content-Type: multipart/alternative; boundary="=_b4c1a7f1d1"
...snip...
What it means: You now have the exact message body as your outbound MTA saw it. This is your “signed input.”
Decision: Save this to a file and verify DKIM locally. If it verifies here but fails at the recipient, something changes after this hop.
Task 2: Save the message to a file for repeatable testing
cr0x@server:~$ sudo postcat -q 3F2A91C02E > /tmp/msg.eml
cr0x@server:~$ ls -l /tmp/msg.eml
-rw-r--r-- 1 root root 48231 Jan 3 01:14 /tmp/msg.eml
What it means: You’ve frozen the evidence.
Decision: Use the same file to test OpenDKIM, parse MIME, and compute canonicalized body hashes consistently.
Task 3: Extract the DKIM-Signature header and note the canonicalization
cr0x@server:~$ grep -n '^DKIM-Signature:' -A2 /tmp/msg.eml
8:DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=mail;
9- h=from:to:subject:date:mime-version:content-type;
10- bh=VQeN9v5kG7WJQm...snip...=; b=Q0Vn...snip...
What it means: Body canonicalization is relaxed. That narrows likely causes: not a simple CRLF issue, more likely body edits or encoding rewrites.
Decision: Focus on filters that modify content, not just whitespace.
Task 4: Verify DKIM locally with OpenDKIM (baseline)
cr0x@server:~$ opendkim-testmsg -d example.com -s mail -k /etc/opendkim/keys/example.com/mail.private < /tmp/msg.eml
opendkim-testmsg: signature ok
What it means: The message as captured matches the signer’s expectation. Your key and signing config are fine.
Decision: Stop changing DNS. Start looking for downstream modifications after the signing hop.
Task 5: Inspect Received headers to identify the modification window
cr0x@server:~$ grep -n '^Received:' -n /tmp/msg.eml
1:Received: from app01 (app01.internal [10.0.12.34])
3:Received: by mx-out01.example.com (Postfix) with ESMTP id 3F2A91C02E
What it means: In this sample, the chain is short. In real incidents, you’ll see many hops (internal relays, security gateways, cloud MTAs).
Decision: Identify the last hop you control before the recipient. Capture there too for diffing.
Task 6: Capture SMTP traffic on the outbound interface (if you must)
cr0x@server:~$ sudo tcpdump -i eth0 -s 0 -w /tmp/smtp.pcap 'tcp port 25 and host 203.0.113.55'
tcpdump: listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
^C
124 packets captured
What it means: You’ve captured the actual bytes on the wire to the next hop. This is expensive and noisy; use it when logs aren’t enough.
Decision: Reconstruct the DATA portion and compare it to your stored .eml. If it differs, something between queue and wire is rewriting.
Task 7: Check for after-queue content filters in Postfix (common culprit)
cr0x@server:~$ postconf | egrep 'content_filter|smtp_tls_policy_maps|receive_override_options'
content_filter = smtp-amavis:[127.0.0.1]:10024
receive_override_options = no_address_mappings
What it means: A content filter is configured. Amavis (or similar) often re-encodes MIME, especially around attachments and QP.
Decision: Ensure DKIM signing happens after this filter, or configure the filter for minimal rewriting.
Task 8: Check OpenDKIM signing/verification mode and milter placement
cr0x@server:~$ grep -E '^(Mode|Socket|Canonicalization|OversignHeaders|SignHeaders)' /etc/opendkim.conf
Mode sv
Socket inet:8891@localhost
Canonicalization relaxed/relaxed
OversignHeaders From
SignHeaders From,To,Subject,Date,Message-ID,MIME-Version,Content-Type
What it means: OpenDKIM is signing and verifying (sv) and uses relaxed/relaxed. Oversigning From is good practice.
Decision: Confirm this milter runs on the final outbound injection stage, not on an earlier internal hop.
Task 9: Identify whether a gateway is injecting banners/footers
cr0x@server:~$ grep -nE 'External Email|DISCLAIMER|This message originated' /tmp/msg.eml
cr0x@server:~$ echo $?
1
What it means: The message as queued doesn’t contain common injected banner text.
Decision: If the recipient sees a banner, it’s being added after your queue capture—likely at an outbound security gateway or recipient side.
Task 10: Check for quoted-printable soft breaks and line wrapping behavior
cr0x@server:~$ grep -n '^Content-Transfer-Encoding:' -n /tmp/msg.eml | head
22:Content-Transfer-Encoding: quoted-printable
cr0x@server:~$ sed -n '30,60p' /tmp/msg.eml | sed -n '1,10p'
Dear customer,=0D=0A=0D=0APlease remit payment within 30 days.=0D=0A=
If you have questions, reply to this email.=0D=0A
What it means: The message body is QP encoded; the presence of = line breaks and explicit =0D=0A makes it fragile.
Decision: Suspect any gateway that decodes/re-encodes QP, or any system that “normalizes” line length.
Task 11: Detect CRLF vs LF (don’t trust your editor)
cr0x@server:~$ python3 - < /tmp/msg.eml
import sys
data = sys.stdin.buffer.read()
print("CRLF count:", data.count(b"\r\n"))
print("LF count:", data.count(b"\n"))
print("Bare LF count:", data.count(b"\n") - data.count(b"\r\n"))
PY
CRLF count: 812
LF count: 812
Bare LF count: 0
What it means: This file is clean CRLF. If a downstream capture shows bare LFs, you found a body mutation vector.
Decision: If bare LF appears after a hop, fix the offending relay/proxy or ensure DKIM signing occurs after it.
Task 12: Extract and compare MIME boundaries (boundary changes are a smoking gun)
cr0x@server:~$ grep -n '^Content-Type: multipart' -n /tmp/msg.eml
15:Content-Type: multipart/alternative; boundary="=_b4c1a7f1d1"
cr0x@server:~$ grep -n '^--=_b4c1a7f1d1' /tmp/msg.eml | head
27:--=_b4c1a7f1d1
88:--=_b4c1a7f1d1--
What it means: You know the expected boundary token. If a downstream copy has a different boundary, something rebuilt the MIME.
Decision: Treat MIME rebuilders as DKIM-hostile. Either move signing after them or configure them to not reserialize content.
Task 13: Check whether the signer used the risky l= tag
cr0x@server:~$ grep -o ' l=[0-9]\+' -n /tmp/msg.eml
cr0x@server:~$ echo $?
1
What it means: No body length limit is used. DKIM expects the entire body to remain stable.
Decision: Good for security; bad for pipelines with footers. Fix the pipeline, not the signature, unless you explicitly accept the l= risk.
Task 14: Compare two message bodies from two hops (diff that respects bytes)
cr0x@server:~$ python3 - << 'PY'
import email, sys
from email import policy
def body_bytes(path):
with open(path,'rb') as f:
msg = email.message_from_binary_file(f, policy=policy.default)
if msg.is_multipart():
# take full serialized body (post headers) for byte diff
raw = open(path,'rb').read()
return raw.split(b"\r\n\r\n",1)[1]
else:
raw = open(path,'rb').read()
return raw.split(b"\r\n\r\n",1)[1]
a = body_bytes("/tmp/msg.eml")
b = body_bytes("/tmp/msg-from-gateway.eml")
print("body lengths:", len(a), len(b))
print("first differing byte index:", next((i for i in range(min(len(a),len(b))) if a[i]!=b[i]), -1))
PY
body lengths: 41802 42110
first differing byte index: 21794
What it means: The body changed between hops; you even have the approximate position to inspect.
Decision: Open both around that index, identify inserted/rewritten content, and pin it to a specific gateway feature.
Joke #2: Email is the only system where adding a friendly footer can be treated like tampering—because it is.
Three corporate mini-stories from the trench
Mini-story 1: The incident caused by a wrong assumption
A mid-sized company ran a clean, boring outbound Postfix stack with OpenDKIM. Their DMARC policy was quarantine, and they were proud of it.
Then they rolled out a “safe links” product on the outbound path—marketed as “transparent.”
The wrong assumption was simple: “It only touches links in HTML; DKIM is relaxed; it’ll be fine.” The first symptom wasn’t a dramatic outage.
It was a slow bleed: partners using strict DMARC started rejecting purchase orders. Internally, everything looked delivered. SMTP 250 OKs everywhere.
The breakthrough came from comparing the queued message to the message received by a test mailbox on a different domain.
The HTML part had rewritten links, and the text/plain part had gained a tracking parameter on bare URLs. Both parts changed, so the DKIM body hash mismatch was guaranteed.
The product also re-encoded quoted-printable differently, so even messages without links broke sometimes.
Fixing it required an uncomfortable decision: either sign after rewriting (meaning the security gateway must DKIM-sign as the organizational domain),
or stop rewriting outbound mail entirely. They chose to sign after rewriting by moving the “final DKIM signing” to the last hop and ensuring the gateway had
access to the correct selector keys. They also added monitoring for unexpected content mutation by hashing the body at multiple hops.
Mini-story 2: The optimization that backfired
A different org wanted to reduce CPU overhead on their MTAs. Someone noticed that their content filter was decoding and re-encoding everything,
even when no malware was found. The “optimization” was to enable a setting that would “normalize” MIME and consolidate encodings for better compression.
It looked brilliant in a micro-benchmark: lower outbound bandwidth, smaller archived messages, fewer weird encodings. Then DKIM started failing for only
certain message classes: invoices from one app, and support replies from another. Because those apps used different libraries, the MIME differed just enough
to trigger the filter’s rewrite path sometimes.
The failures were intermittent by recipient because not all outbound routes went through the same filter cluster. Some recipients saw DKIM pass. Others saw
body hash mismatch. The org spent a week blaming DNS propagation and “recipient-side caching,” because that’s the story people tell when they don’t have a diff.
The fix was to roll back the “normalize” setting and change the architecture: scan and rewrite content before DKIM signing, and treat the signing hop as sacrosanct.
CPU overhead returned, but deliverability stopped being a dice roll.
Mini-story 3: The boring but correct practice that saved the day
A regulated business had a rule: “No mail modification after signing. Ever.” It was unpopular. People wanted banners, department-specific footers,
and last-minute branding changes. The mail team kept saying no like a hobby.
They did allow content filtering, but only in a defined pre-signing pipeline. Their final outbound relay did exactly three things:
enforce TLS policy, rate-limit abusive clients, and DKIM-sign. No banner injection. No URL rewriting. No archive re-injection.
During a partner ecosystem incident—several recipients tightening DMARC and filtering rules—this org saw almost no disruption.
While others scrambled to explain body hash mismatches, they could quickly show that their message bytes remained stable from signing to delivery.
The boring practice wasn’t glamorous; it was just operational hygiene.
The “saved the day” detail: they had a standard capture point at the final relay that stored the exact post-signing message for 48 hours for forensic comparison.
When a recipient claimed “your DKIM fails,” they could prove whether the recipient’s inbound gateway modified the message after receipt,
or whether the failure was in their own chain. Most disputes ended quickly. Quiet wins are still wins.
Common mistakes: symptom → root cause → fix
1) Symptom: DKIM fails only when emailing certain partner domains
Root cause: Route-dependent rewriting (different outbound relay, smart host, TLS gateway, or DLP appliance for those domains).
Fix: Compare the message captured at final signing hop vs the message observed after the route-specific gateway. Standardize routing or move signing after the last modifier.
2) Symptom: DKIM fails only for HTML emails, not plain text
Root cause: HTML sanitization, click tracking, or HTML-part banner injection modifying one MIME part.
Fix: Disable HTML rewriting for outbound, or ensure the rewriting system DKIM-signs as the organizational domain after modification.
3) Symptom: DKIM body hash mismatch appears after enabling a new antivirus or DLP product
Root cause: The product reserializes MIME or changes Content-Transfer-Encoding even when “no threat found.”
Fix: Configure “pass-through” mode that preserves bytes, or place DKIM signing after the product, or switch to a product that supports “no rewrite unless necessary.”
4) Symptom: DKIM fails intermittently for the same message template
Root cause: Non-deterministic rewriting (random boundary tokens, variable QP wrapping, or per-node behavior differences).
Fix: Make serialization deterministic: pin libraries, disable normalization, and sign at the last hop. Also ensure all nodes have identical configs.
5) Symptom: DKIM passes in internal tests but fails “in the wild”
Root cause: Your test path bypasses a real outbound component (a cloud connector, journaling service, partner relay, or outbound proxy).
Fix: Test using the same outbound route as production recipients. Capture at each hop and diff.
6) Symptom: Messages with attachments fail DKIM more than others
Root cause: Attachment scanning/detonation rebuilds MIME or changes base64 line wrapping.
Fix: Move signing after attachment processing, or configure scanning to avoid re-encoding when possible.
7) Symptom: DKIM breaks after enabling “add disclaimer to outbound mail” on an Exchange or gateway
Root cause: Disclaimer is inserted after signing, or signing happens on a different hop than you think.
Fix: Reorder transport rules so disclaimers happen before DKIM signing. If you can’t, accept that your DKIM will fail and re-architect.
8) Symptom: DKIM failure mentions “body canonicalization simple”
Root cause: You’re using c=simple/simple or simple body canonicalization and something touches whitespace/line endings.
Fix: Use relaxed/relaxed unless you control every hop and can prove byte-for-byte stability.
Checklists / step-by-step plan
Checklist A: First 30 minutes of a DKIM body hash mismatch incident
- Get one failing message with full headers and raw source (from the recipient if possible).
- Pull the same message from your outbound queue or logs around the same time (postcat, journaling copy, or capture point).
- Verify the queued copy locally using OpenDKIM tools; record whether it passes.
- Extract DKIM parameters:
d=,s=,c=, and whetherl=is present. - Compare bodies bytewise and find the first difference.
- Identify the hop where modification occurred by correlating Received headers and routing rules.
- Decide: remove the modifier, move signing after it, or accept failure and implement ARC/alternate trust mechanism.
Checklist B: Make your outbound pipeline DKIM-safe (the boring architecture)
- Define a single “last-mile” outbound relay whose job is to sign and send, not to “improve” content.
- Place content filtering, rewriting, disclaimers, and branding upstream of that relay.
- Ensure the last-mile relay does not run after-queue filters that might rewrite content.
- Standardize canonicalization:
relaxed/relaxedfor most organizations. - Keep MIME generation consistent across applications (library choice matters).
- Deploy a capture point at the last-mile relay for forensic diffs (time-bounded retention).
- Monitor DKIM failures by receiver feedback loops and by sampling verifications on outbound copies.
Checklist C: Vendor and internal change review questions
- Does this component rewrite URLs, HTML, or add banners?
- Does it decode and re-encode MIME? Does it preserve Content-Transfer-Encoding?
- Does it rebuild MIME boundaries or modify multipart structure?
- Where in the mail flow does DKIM signing occur relative to this change?
- Can we prove byte-for-byte stability after signing with a diff?
- Is there per-recipient routing that could cause inconsistent behavior?
FAQ
Why does DKIM fail when the email content “looks the same”?
Because DKIM signs bytes, not your mail client’s rendering. Changing quoted-printable wrapping, MIME boundaries, or transfer encoding can preserve appearance but change bytes.
Is relaxed canonicalization enough to prevent body hash mismatches?
It prevents failures from trivial whitespace differences. It does not protect against content injection, URL rewriting, MIME restructuring, or re-encoding.
Should I use c=simple/simple for better security?
Only if you control the full path and can prove nothing modifies the message. Otherwise it’s self-sabotage: you’ll trade theoretical purity for real-world failure.
Can I “fix” this by rotating DKIM keys or changing DNS?
Not if the error is body hash mismatch. Keys and DNS issues usually produce “no key,” “bad key,” or signature verification errors unrelated to bh.
Your problem is message mutation.
What about signing with l= to ignore appended footers?
It can reduce breakage when footers are appended, but it weakens integrity: attackers can append content outside the signed portion.
Many receivers treat l= skeptically. Fix the pipeline first.
Why does forwarding break DKIM so often?
Forwarders frequently modify messages (re-encoding, adding list headers, rewrapping), and DKIM doesn’t survive changes. ARC was introduced to help preserve authentication across hops.
Why does it fail only for messages with attachments?
Attachments trigger scanning and content handling that may rebuild MIME or change base64 line wrapping. Those are body changes, and they invalidate bh.
How do I decide where to DKIM-sign in a complex environment?
Sign at the last hop you control before the public internet—after all content modifications. Make that hop boring. Everything “clever” happens before it.
Can recipients cause a DKIM body hash mismatch on their side?
Yes. Some inbound gateways modify mail (banners, URL rewriting, detonation). If you can prove your outbound bytes are stable, the failure may be on the receiver’s inbound chain.
Does DKIM fail more with UTF-8 or international characters?
Not inherently. The issues come from encoding conversions (8bit/QP/base64) and transport downgrades triggered by non-ASCII content.
Conclusion: next steps that actually reduce incidents
DKIM body hash mismatch is rarely “a DKIM problem.” It’s your mail pipeline telling you something is rewriting content after signing.
That rewrite might be well-intentioned (security banners) or quietly destructive (QP rewrapping, MIME rebuilds). Either way, the fix is architectural.
- Pick one last-mile relay and make it a sanctified signing-and-sending box. No disclaimers. No URL rewriting. No MIME normalization.
- Move all modifications upstream of signing, and treat any post-sign component as guilty until proven innocent with a bytewise diff.
- Instrument for proof: store a short-retention copy of the post-sign message at the last hop so you can settle disputes with evidence, not feelings.
- Standardize canonicalization (usually relaxed/relaxed) and avoid
l=unless you accept its security tradeoffs. - When a vendor says “transparent,” ask “Does the raw body change?” If they can’t answer, assume yes.
You don’t have to love email to run it well. You just have to stop letting random boxes rewrite your bytes after you’ve promised the world they won’t.