When a receiving mail server reports DKIM signature verification failed, it means one thing precisely: the cryptographic math didn't check out. The server re-ran the signature algorithm using the public key it fetched from DNS, and the result didn't match the b= value the signer embedded in the DKIM-Signature header. Understanding why that mismatch happened, and it's rarely a single cause, is the job of this article.

This breakdown covers every practical failure mode, with raw header evidence for each, and a command-line playbook for verifying signatures yourself. It's the technically grounded companion to how SPF, DKIM, and DMARC interact as a layered authentication system, read that first if you need to understand where DKIM sits in the broader pipeline.

What 'DKIM Signature Verification Failed' Actually Means

The Verification Pipeline in Plain Terms

DKIM works by having the sending server sign a message at dispatch time and publish the corresponding public key in DNS. The receiving MTA, on arrival, fetches that key, reconstructs the canonical form of the signed headers and body, and runs the same hash-then-verify operation the signer ran. If the output matches b=, the signature is valid.

Here's a real-world-style DKIM-Signature header with every tag annotated:

DKIM-Signature: v=1;               # version, always 1
                a=rsa-sha256;      # signing algorithm
                c=relaxed/simple;  # header/body canonicalization
                d=example.com;     # signing domain
                s=mail2026;        # selector
                h=from:to:subject:date:mime-version:content-type;  # signed headers
                bh=47DEQpj8HB...;  # body hash (base64)
                b=dGhpcyBpcyBu...  # signature over the header block (base64)
                x=1752364800;      # optional expiry (Unix timestamp)

Every tag participates in verification. A problem with any one of them produces a failure. The s= and d= tags determine the DNS lookup. The c= tag defines canonicalization. The h= tag lists which headers were signed. The bh= tag holds the body hash. The b= tag is the signature itself.

permfail vs. tempfail: Why the Distinction Matters

The Authentication-Results header surfaces two categories of DKIM failure. The difference determines what you do next.

DKIM permfail, a permanent, structural failure. The signature is broken in a way that retrying won't fix: the DNS record doesn't exist, the signature is cryptographically invalid, or the key has been revoked. No amount of redelivery recovers this.

DKIM tempfail, a transient failure. The DNS lookup timed out or returned SERVFAIL. The message isn't necessarily broken; the lookup infrastructure had a momentary problem. Receiving MTAs typically defer (not reject) on tempfail and retry later.

Authentication-Results: mx.receiver.example;
  dkim=permerror (no key for signature)
    header.d=example.com header.s=mail2026;
  dkim=temperror (DNS query failed)
    header.d=example.com header.s=backup

If you're diagnosing a permfail, work through the failure modes below in order. A tempfail sends you to your DNS infrastructure instead.

Failure Mode 1, DKIM Selector Not Found (DNS Lookup Failure)

Reading the Selector and Domain from the DKIM-Signature Header

The receiving MTA constructs the DNS query from two tags: s= (selector) and d= (signing domain). The TXT record name it queries is always:

<selector>._domainkey.<d=value>

For the header above, that's mail2026._domainkey.example.com. If that name returns NXDOMAIN, the record doesn't exist, you get a permfail. If it returns SERVFAIL, the DNS resolver encountered an error, you get a DKIM tempfail.

Common causes of DKIM selector not found:

  • The selector in the header doesn't match the name published in DNS (a typo, a rotation that wasn't completed, or a deleted record).
  • The signing domain uses a CNAME that the receiving resolver can't follow through to a TXT record.
  • The DNS zone was recently migrated and the old selector wasn't carried over.

What a DKIM DNS Lookup Failure Looks Like in Authentication-Results

Authentication-Results: mx.receiver.example;
  dkim=permerror (no key for signature)
    header.i=@example.com header.s=mail2026 header.b="dGhpcyBpcyBu"

permerror with no key for signature is unambiguous: the TXT record for that selector-domain pair returned nothing. Verify with:

dig TXT mail2026._domainkey.example.com +short

A healthy response returns the key record starting with v=DKIM1; k=rsa; p=.... An empty response confirms NXDOMAIN. A SERVFAIL alongside tempfail in Authentication-Results points to your DNS resolver, not the record itself.

Failure Mode 2, DKIM Body Hash Mismatch

How DKIM Canonicalization Shapes the Body Hash

RFC 6376 defines exactly two canonicalization algorithms, simple and relaxed, and the choice between them is the single most common source of body hash mismatches when messages pass through mailing list managers or security gateways.

The c= tag specifies header/body canonicalization as a pair. c=relaxed/simple means relaxed header canonicalization and simple body canonicalization.

Simple body canonicalization is strict: the body must be byte-for-byte identical to what the signer hashed, except for the trailing CRLF. Any change, a rewritten line ending, an added footer, a normalized whitespace sequence, invalidates the bh= value.

Relaxed body canonicalization tolerates whitespace changes within lines (multiple spaces collapse to one, trailing whitespace per line is stripped) but still fails on injected content like mailing list footers.

The bh= tag in the DKIM-Signature header is the base64-encoded SHA-256 hash of the canonicalized body. When it doesn't match what the verifier recomputes, you get:

Authentication-Results: mx.receiver.example;
  dkim=fail (body hash did not verify)
    header.d=example.com header.s=mail2026

Spotting Body Mutation in Transit

The most common transit agents that cause a DKIM body hash mismatch:

  • Mailing list managers (Mailman, Listserv): they inject a footer into the body, which changes every byte of content the signer hashed.
  • Security gateways (antivirus scanners, DLP appliances): some rewrite MIME boundaries or normalize line endings from CRLF to LF mid-stream.
  • Content rewriting proxies: click-tracking systems that rewrite URLs inside the body.

To isolate the mutation, compare the bh= the signer produced against the hash you compute locally over the raw received body. If they differ, trace the Received: chain, the mutation happened between the hop where the signature was added and the hop where it failed. Forensic email header analysis, reading the full delivery path explains how to read that chain precisely.

Failure Mode 3, Header Field Mutation and the DKIM Header Field List

The h= tag is a colon-separated list of header field names the signer included in the signature. If any of those fields change between signing and verification, the b= value won't match, even if the body is completely intact and the DNS record resolves perfectly. This is the most treacherous failure mode, because every visible signal says the message is fine.

The verifier takes the header fields named in h=, applies the canonicalization algorithm specified in c=, concatenates them in the order listed (bottom-up for duplicate fields), and hashes the result together with the DKIM-Signature header itself (with b= zeroed out).

Under relaxed header canonicalization, header field names are lowercased and leading/trailing whitespace is stripped from values. This tolerates minor reformatting. Under simple, the headers must match byte-for-byte. A gateway that capitalizes MIME-Version differently, adds a space after the colon in Subject:, or folds a long header line differently will break a simple-canonicalized signature.

The fields most frequently mutated in transit:

  • Subject:, mailing list managers prepend [List-Name] tags.
  • Content-Type:, gateways sometimes rewrite boundary parameters or add ; charset= qualifiers.
  • MIME-Version:, some forwarders re-emit this with different whitespace.
  • Reply-To:, rewritten by mailing list managers to point to the list address.

If Subject appears in h= and a mailing list prepends its tag, the signature fails immediately. The fix from the operator side is either to exclude mutable headers from h= at signing time, or to use relaxed header canonicalization. The DKIM header field list guidance in RFC 6376 recommends signing only the headers you control and expect to remain stable across all forwarding paths.

Failure Mode 4, DKIM Signature Expired

The x= tag sets an expiry timestamp (Unix epoch seconds). Most senders omit it entirely, the majority of DKIM implementations don't set x= by default, because a valid key rotation policy makes per-signature expiry redundant.

When x= is present and the verification time is past that timestamp, the receiving MTA reports:

Authentication-Results: mx.receiver.example;
  dkim=fail (signature has expired)
    header.d=example.com header.s=mail2026

This is a DKIM permfail, retrying doesn't help, because the expiry is baked into the signed header.

A DKIM signature expired failure appears in two scenarios:

  1. Long-delayed delivery. A message queued for days (due to a receiving server outage, a routing loop, or a spam hold) arrives after the signer's short x= window.
  2. Replay attack mitigation. Some high-security senders deliberately set short expiry windows (hours, not days) to prevent captured messages from being replayed later. A verification failure on a legitimately delivered message in this context means the delivery path is unusually slow.

The diagnostic clue: if you see x= in the DKIM-Signature header of a failing message, check the gap between the Date: header and the last Received: timestamp. A gap longer than the x= window explains the failure completely.

How to Verify a DKIM Signature Manually, A Step-by-Step Playbook

This is the sequence practitioners use when the Authentication-Results header isn't enough, when you need to prove to yourself exactly where the math broke. For the broader skill of diagnosing delivery failures by reading email headers, the same raw-header discipline applies throughout.

Step 1: Extract and Decode the DKIM-Signature Header

Save the raw message as a .eml file. Extract the DKIM-Signature header, the full, unfolded value including all tags. Note these values:

  • d=, the signing domain
  • s=, the selector
  • c=, the canonicalization pair (header/body)
  • bh=, the claimed body hash (base64)
  • b=, the claimed signature (base64)

Decode b= from base64 to raw bytes:

echo "dGhpcyBpcyBu..." | base64 -d > signature.bin

Step 2: Reconstruct the Canonicalized Body and Header Block

Apply the body canonicalization algorithm specified in c= to the raw message body (everything after the blank line separating headers from body). For relaxed, collapse internal whitespace within each line, strip trailing whitespace per line, and ensure the body ends with exactly one CRLF. For simple, strip trailing blank lines and ensure the body ends with one CRLF.

Hash the canonicalized body with SHA-256:

openssl dgst -sha256 -binary canonical_body.txt | base64

Compare this output to the bh= tag value. If they differ, you have a body hash mismatch, the body was mutated in transit, and no amount of signature checking will recover it.

If bh= matches, the body is intact. The problem is in the headers or the key. Reconstruct the signed header block: take each header named in h=, in listed order (for duplicates, take the bottommost instance first), apply header canonicalization, and concatenate. Append the DKIM-Signature header itself with the b= value replaced by an empty string.

Write the result to signed_headers.txt.

Step 3: Fetch the Public Key and Run the Signature Math

Fetch the public key via DNS:

dig TXT mail2026._domainkey.example.com +short

The response includes p=<base64-encoded public key>. Extract just the p= value and decode it:

echo "<p= value>" | base64 -d > pubkey.der
openssl pkey -inform DER -pubin -in pubkey.der -out pubkey.pem

Now verify the signature:

openssl dgst -sha256 -verify pubkey.pem -signature signature.bin signed_headers.txt

Verified OK means the signature is cryptographically valid and the problem is elsewhere (expired, policy, etc.). Verification Failure means the signed header block you reconstructed doesn't match what the signer computed, check your canonicalization logic step by step.


For practitioners who need the complete picture, how key rotation interacts with selector lifecycle, how DMARC policy interprets DKIM results, and what the full signing and verification lifecycle looks like end to end, Chapter 6 covers SPF, DKIM, DMARC, ARC, and BIMI as one connected authentication system in full technical depth. That's the reference to keep open alongside the raw headers when a failure mode doesn't fit neatly into any of the categories above.