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
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:
simple header canonicalization, or a header the signer had locked (oversigned) was added.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).
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:
bh=; if it doesn't match, the verifier SHOULD ignore the signature and return PERMFAIL (body hash did not verify);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 happens | What it points to |
|---|---|---|
| PERMFAIL (body hash did not verify) | Section 6.1.3, step 3 | The body changed after signing. See body hash did not verify. |
| PERMFAIL (signature did not verify) | Section 6.1.3, step 4 | A 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.2 | The 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 8 | The 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.1 | The DKIM-Signature header itself is unacceptable before any crypto is done. |
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:
h=, in the order listed, each canonicalized with the header algorithm from c=; thenb= 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):
h= is required, can't be empty, and must include From; a signature without From in h= is ignored with PERMFAIL (From field not signed).h= that isn't in the message counts as an empty input, which lets a signer assert that the header is absent: if it is added later, the signature fails.h=from:...:from with one From header) prevents any further instance from being added. This is often called oversigning; RFC 6376 section 8.15 uses h=from:from:... as its example.h= aren't signed, so adding or changing them (a new X- header, an extra Received line) doesn't break the signature.The first half of c= is the header algorithm. If c= is missing the default is simple/simple (RFC 6376 section 3.5).
| Header algorithm | What it tolerates (RFC 6376 sections 3.4.1 and 3.4.2) |
|---|---|
simple | Nothing. Header fields must be presented exactly as they are, with no case folding of names and no whitespace changes. |
relaxed | Header 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.
[EXTERNAL], [SPAM] or a list name changes a header that signers are advised to include (RFC 6376 section 5.4 highly advises signing Subject), so the signature breaks whenever Subject is in h=.h= (To, Cc, Reply-To, Date, Message-ID, the MIME headers) breaks the signature in the same way. Section 5.4 tells signers not to sign headers likely to be legitimately modified or removed in transit, such as Return-Path.simple header canonicalization.simple) or truncates the DKIM-Signature header breaks it.rsa-sha256, and rsa-sha1 MUST NOT be used for signing or verifying; signatures made with it have permanently failed evaluation. Not every receiver enforces this the same way, but a signature with a=rsa-sha1 should be treated as failing. RFC 8601 registers header.a so a receiver can report the algorithm and make such signatures easy to spot.k= (default rsa) must suit the signature's a=; if not, RFC 6376 section 6.1.2 says PERMFAIL (inappropriate key algorithm). If the key record has an h= tag that doesn't list the signature's hash (for example h=sha1 with a=rsa-sha256), it is PERMFAIL (inappropriate hash algorithm). Receivers may report these as fail, policy or permerror.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
mx.receiver.example) naming who did the check. Trust only the ones added by your own receiving system; earlier ones were added by other hops and could be forged.header.d is the signature's d= and header.s its selector. header.a (registered by RFC 8601) shows the algorithm, and header.b (RFC 6008) gives at least the first eight characters of b=, which tells two signatures from the same domain apart.ARC-Authentication-Results header showing dkim=pass for the same signature, the change happened at that hop or later. Use the analyzer's route table (oldest hop first) to list every server between the signer and the receiver.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.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.c=. With simple, look for re-wrapped lines or changed case in the signed headers too.# 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
openssl output should match the p= value once the quoted strings in the TXT answer are joined with no spaces. If it doesn't, the signer and DNS disagree: publish the matching key under a new selector, or point the signer at the right private key.+noall +answer form shows the remaining TTL. Different answers from different resolvers mean a recent change is still being cached; wait at least that long, or better, use a new selector.k=, any h= tag and the key length, and run the domain through the DKIM Checker with the selector from s=.c=relaxed/relaxed survives header re-wrapping and whitespace or case changes that break simple. It doesn't help with changed values.rsa-sha256 and a 2048-bit RSA key, as RFC 8301 recommends. If your DNS host can't store a 2048-bit key in one string, split it into several quoted strings in the same TXT record; see no key for signature for how that goes wrong.k=rsa, 2048 bits, no h= tag that excludes sha256.dkim=pass with header.d set to your domain and dmarc=pass. A message that already arrived keeps its old result.d= domain.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).
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).
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.
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.
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.
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.
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.
Runbooks, checklists and helpdesk templates: IT Admin Packs on Gumroad.