DKIM Signature Did Not Verify: Causes and How to Fix It

What DKIM signature did not verify means and how it differs from body hash did not verify: signed headers changed in transit, the h= list and header canonicalization, key mismatch after rotation, rsa-sha1 and short keys, and how to fix it.

Home › Guides › DKIM signature did not verify

Last updated 18 October 2026

Quick answer

Signature did not verify means the receiver checked the b= signature in the DKIM-Signature header against the public key and they didn't match. Unlike body hash did not verify, the body usually isn't the problem. Unlike no key for signature, a key was found. That leaves two kinds of cause:

  1. A signed header changed after signing. A gateway tagged the Subject, a mailing list rewrote From, a relay re-folded a header under simple header canonicalization, or a header the signer had locked (oversigned) was added.
  2. The key doesn't match. The signer uses a private key whose public half isn't the one published at the selector, for example after a key rotation, a server rebuild, or while resolvers still hold an old record.

To tell them apart, check whether every message from that signer fails (usually the key) or only mail that went through a particular hop (usually a header change).

What the error means

RFC 6376 section 6.1.3 describes verification in steps. The verifier canonicalizes the message using c=, l= and the header names in h=, computes the hashes, then:

  1. compares the body hash with bh=; if it doesn't match, the verifier SHOULD ignore the signature and return PERMFAIL (body hash did not verify);
  2. checks the b= signature against the header hash using the public key and the algorithm in a=; if it doesn't validate, the verifier SHOULD ignore the signature and return PERMFAIL (signature did not verify).

In that order, reaching the second check means the body hash matched, so the fault is in what the header hash covers or in the key. The RFC lets implementations verify the header signature before the body hash, so treat this as a strong hint rather than proof, and compare the body as well if a footer is possible. The words in brackets are explanations, not fixed strings, and receivers word their comments differently.

RFC 6376 status (explanation)Where it happensWhat it points to
PERMFAIL (body hash did not verify)Section 6.1.3, step 3The body changed after signing. See body hash did not verify.
PERMFAIL (signature did not verify)Section 6.1.3, step 4A signed header (or the DKIM-Signature header itself) changed, or the key doesn't match the signer's private key. This guide.
PERMFAIL (no key for signature), (key revoked), (key syntax error)Section 6.1.2The key record is missing, empty or broken. See no key for signature.
PERMFAIL (inappropriate key algorithm) or (inappropriate hash algorithm)Section 6.1.2, steps 6 and 8The signature's a= doesn't suit the key's k=, or the key record's h= excludes the hash algorithm.
PERMFAIL (From field not signed), (signature syntax error)Section 6.1.1The DKIM-Signature header itself is unacceptable before any crypto is done.

What the header hash covers (h=)

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

RFC 6376 section 3.7 says the header hash is computed over:

  1. the header fields named in h=, in the order listed, each canonicalized with the header algorithm from c=; then
  2. the DKIM-Signature header itself, with every tag included (even unknown ones) and only the value of b= treated as empty.

The private key signs that hash into b=. Rules that matter when you debug it (RFC 6376 sections 3.5, 5.4 and 5.4.2):

Header canonicalization: simple vs relaxed

The first half of c= is the header algorithm. If c= is missing the default is simple/simple (RFC 6376 section 3.5).

Header algorithmWhat it tolerates (RFC 6376 sections 3.4.1 and 3.4.2)
simpleNothing. Header fields must be presented exactly as they are, with no case folding of names and no whitespace changes.
relaxedHeader names in a different case (they are lowercased), unfolded continuation lines, runs of spaces or tabs (reduced to one space), whitespace at the end of a value and around the colon.

Neither algorithm tolerates a changed value: a word added to the Subject, a different address in From, a rewritten Date. With simple, a relay that re-wraps a long Subject or To header, or changes Subject: to SUBJECT:, is enough to break the signature.

Common causes: headers changed in transit

Common causes: the key doesn't match

Common causes: algorithm and key length

Step 1: Read Authentication-Results and find the hop

Open the received copy (view the original or the message source) and paste the full headers into the Email Header Analyzer. An illustrative example of what to look for (your receiver's wording will differ):

Authentication-Results: mx.receiver.example;
  dkim=fail (signature did not verify) header.d=example.com
    header.s=s2024 header.a=rsa-sha256 header.b=AbCdEf12;
  spf=pass smtp.mailfrom=example.com;
  dmarc=pass header.from=example.com

Step 2: Compare the signed headers with what was sent

  1. Copy the h= list from the failing signature and, for each name, compare the received header with your sent copy (or a copy taken before the suspect hop). Look for a Subject tag, a changed From or Reply-To, rewritten addresses, a different Date or Message-ID.
  2. Count instances: if a header listed in h= now appears more often than when it was signed, or a name in h= was absent when signed but is present now, a hop added it.
  3. Check the first half of c=. With simple, look for re-wrapped lines or changed case in the signed headers too.
  4. Isolate the path: send a test straight from the signing platform to an outside mailbox, skipping the list, forwarder or gateway. If that copy passes, the skipped hop is the cause; if it fails too, check the key (Step 3).

Step 3: Check that the published key matches the signer

# The key the receiver sees (compare several resolvers)
dig +short TXT s2024._domainkey.example.com
dig @1.1.1.1 +short TXT s2024._domainkey.example.com
dig @8.8.8.8 +short TXT s2024._domainkey.example.com
dig TXT s2024._domainkey.example.com +noall +answer

# The public half of the RSA private key your own signer uses
openssl rsa -in /path/to/dkim-private.pem -pubout -outform DER | openssl base64 -A

# Windows
Resolve-DnsName -Name s2024._domainkey.example.com -Type TXT

Fixes

Verify the fix

  1. Confirm the selector's key with the DKIM Checker and decode it with the DKIM Record Explainer: one record, k=rsa, 2048 bits, no h= tag that excludes sha256.
  2. If you changed or added a key record, check it from many public resolvers with the DNS Propagation Checker before relying on it.
  3. Send a new message through each path (direct, through the gateway, through the list or forwarder) to an outside mailbox, paste its headers into the Email Header Analyzer, and look for dkim=pass with header.d set to your domain and dmarc=pass. A message that already arrived keeps its old result.
  4. Watch a week of aggregate reports in the DMARC Report Analyzer and check the policy with the DMARC Record Checker; every source should show DKIM 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 d=, s=, h= and each DKIM result with the Email Header Analyzer, check published selectors and key length with the DKIM Checker, decode a key record with the DKIM Record Explainer, confirm a key change across resolvers with the DNS Propagation Checker or query it with DNS Lookup. Related guides: DKIM body hash did not verify, DKIM no key for signature, DKIM for Microsoft 365, SPF, DKIM and DMARC for Google Workspace and DMARC fail with SPF pass (alignment).

FAQ

What does DKIM signature did not verify mean?

The receiver found the public key and checked the b= signature against the hash of the signed headers, and they did not match. RFC 6376 calls this PERMFAIL (signature did not verify). Usually a header listed in h= changed after signing, or the published key does not match the private key the signer used.

How is signature did not verify different from body hash did not verify?

Body hash did not verify means the message body no longer matches the bh= hash, so something changed the body. Signature did not verify is the next check in the RFC order: the body hash matched, but the b= signature over the signed headers and the DKIM-Signature header failed, which points to changed headers or a key mismatch.

Can a Subject tag like [EXTERNAL] break DKIM?

Yes, if Subject is in the h= list, which it almost always is. Any change to the value of a signed header breaks the signature, whatever the canonicalization. Relaxed header canonicalization only tolerates whitespace, line wrapping and header name case changes.

Why did DKIM start failing right after I rotated the key?

The signer and DNS disagree. Either the signer switched to the new private key before the new public key was visible, or the key was replaced at the same selector and resolvers still cache the old record. RFC 6376 recommends a new selector for each new key and keeping the old key published during the transition.

Is rsa-sha1 or a 1024-bit DKIM key still acceptable?

rsa-sha1 is not: RFC 8301 says it must not be used for signing or verifying. RSA keys of 1024 bits are still the minimum signers must use and verifiers must accept, but RFC 8301 says signers should use at least 2048 bits. Keys under 1024 bits must not be considered valid.

Want the full kit?

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