Zum Inhalt springen
ecdsa.com
← Alle Artikel

Warum DKIM scheitert: Body-Hash, Signatur, Schlüssel, DNS

DKIM scheitert, wenn sich eine Nachricht nach dem Signieren ändert oder der Verifizierer keinen brauchbaren Schlüssel bekommt. Jeder der folgenden Fehlerfälle wurde mit dkimpy und mailauth reproduziert, samt dem genauen Urteil, das er auslöst – so kannst du ihn mit dem abgleichen, was du in Authentication-Results siehst.

Kurz gesagt. Eine DKIM-Signatur deckt zwei Dinge ab: einen Hash des Nachrichtentexts (bh=) und eine Signatur über ausgewählte Header plus diesen Hash (b=). body hash did not verify bedeutet, dass sich der Nachrichtentext nach dem Signieren geändert hat, etwa durch einen Footer, einen Disclaimer, ein Virenscanner-Banner oder eine Umkodierung. bad signature / signature verification failed bedeutet, dass sich ein signierter Header geändert hat, etwa durch ein Präfix im Subject oder ein umgeschriebenes From, oder dass der Schlüssel nicht passt. no key / key not found und permerror deuten auf das DNS hin: falscher Selektor, widerrufener oder fehlerhafter Eintrag. Die Kanonisierung „relaxed“ (c=relaxed/relaxed) toleriert Änderungen am Whitespace und an der Groß- und Kleinschreibung von Header-Namen, sonst nichts. Für die Diagnose vergleichst du den Header Authentication-Results mit den Tags im Header DKIM-Signature; dann behebst du den Fehler an der Komponente, die Mails nach dem Signieren verändert, oder du signierst später in der Pipeline.

Was DKIM tatsächlich signiert

DKIM (RFC 6376) fügt einen Header DKIM-Signature hinzu. Diesen hier hat dkimpy 1.1.8 für eine Testnachricht erzeugt:

Header DKIM-Signature (dkimpy 1.1.8, gefaltet wie erzeugt)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com;
 i=@example.com; q=dns/txt; s=sel; t=1792500885; h=from : to : subject
 : date : message-id; bh=eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE=;
 b=FVDVEZSc7Whs7waF1W5ImDSxpa/czkhdpeUBefQ3/daFclzCdP0x9EQh6NOxBCa6oWph4
 WtntuKENSuqQGtjzcVlLdT+itgDCj5PaKMHI6S6JmQPBo2yy2EzPpdcDPeuiORtNbJMRHdZ
 0MhMgg7k11sPD9ts0pE6i+nzWmUXyJ2jEKWw68oG59kSO3B9jns

Der Signaturwert ist hier gekürzt. Die Tags, auf die es beim Debuggen ankommt:

TagBedeutungTypischer Fehler
d=, s=Signierende Domain und Selektor. Der Schlüssel wird unter s._domainkey.d abgerufenFalscher Selektor, fehlender oder widerrufener Eintrag
c=Kanonisierung von Header und Nachrichtentext, simple oder relaxed. Standard: simple/simpleUnter simple machen Whitespace-Änderungen die Signatur ungültig
h=Signierte Header-Felder, in ihrer ReihenfolgeJede Änderung an einem davon
bh=Base64-Hash des kanonisierten NachrichtentextsNachrichtentext geändert
b=Signatur über die signierten Header und den Header DKIM-Signature selbst (mit leerem b=)Header geändert oder falscher Schlüssel
l=Optional: Nur die ersten l Bytes des Nachrichtentexts werden gehashtErlaubt jedem, Inhalt anzuhängen (siehe unten)
a=Algorithmus: rsa-sha256 oder ed25519-sha256rsa-sha1 ist nicht mehr gültig (RFC 8301)

Der Verifizierer prüft in einer festen Reihenfolge. Zuerst ruft er den Schlüssel ab (s._domainkey.d, TXT). Dann kanonisiert er den Nachrichtentext und vergleicht dessen Hash mit bh=. Erst danach prüft er b= über die Header. Diese Reihenfolge erklärt die Urteile: Eine Änderung am Nachrichtentext wird als Body-Hash-Problem gemeldet, auch wenn sich zusätzlich ein Header geändert hat.

Kanonisierung: was „relaxed“ verzeiht

Die Kanonisierung bringt die Nachricht vor dem Hashen in eine Standardform, damit harmlose Änderungen unterwegs die Signatur nicht ungültig machen. RFC 6376 §3.4 definiert zwei Formen:

  • simple für Header: überhaupt keine Änderungen. Nicht einmal die Groß- und Kleinschreibung eines Header-Namens darf sich ändern.
  • relaxed für Header: Namen in Kleinbuchstaben umwandeln, Fortsetzungszeilen entfalten, Folgen von Whitespace auf ein Leerzeichen reduzieren und Whitespace am Ende des Werts und rund um den Doppelpunkt löschen.
  • simple für den Nachrichtentext: Nur leere Zeilen am Ende werden ignoriert.
  • relaxed für den Nachrichtentext: Zusätzlich wird Whitespace am Zeilenende ignoriert, und Folgen von Leerzeichen und Tabs innerhalb einer Zeile werden zu einem Leerzeichen.

Keine der beiden Formen toleriert geänderte Wörter, hinzugefügte Zeilen, neu kodierte MIME-Teile oder andere Zeilenenden. relaxed verzeiht nur Whitespace und die Groß- und Kleinschreibung von Header-Namen. Deshalb ist c=relaxed/relaxed die übliche Wahl. Der Standard laut RFC ist simple/simple, ein Header DKIM-Signature ohne c= verwendet also die strenge Form.

Die Fehler, reproduziert

Der Test signiert eine kurze Nachricht mit einem 2048-Bit-RSA-Schlüssel (c=relaxed/relaxed, h=from:to:subject:date:message-id), verändert sie so, wie es echte Zwischenstationen tun, und prüft sie mit dkimpy 1.1.8. Die DNS-Abfrage ist durch eine Funktion im selben Prozess ersetzt, sodass sich der Schlüsseleintrag zwischen den Fällen ändern lässt:

dkimpy 1.1.8, Python 3.13.5 (Log-Zeilen von dkimpy, base64-Werte vollständig)
--- 1. unchanged message, relaxed/relaxed
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    verify: True
--- 2. list appends a footer to the body
    log: bh: b'cL30gL2flFNCvVcW5T6gPIkNZAhq6yYRaXnavNmMAJ8='
    exception: ValidationError: body hash mismatch (got b'cL30gL2flFNCvVcW5T6gPIkNZAhq6yYRaXnavNmMAJ8=', expected b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE=')
--- 3. list adds a [team] tag to the Subject
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: False
    verify: False
--- 4a. whitespace changed in transit, relaxed/relaxed
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    verify: True
--- 4b. same whitespace change, simple/simple
    log: bh: b'CdTP1x6zZ/fF6DwHq354s/aG/L7f72HAMBorT94XVmk='
    exception: ValidationError: body hash mismatch (got b'CdTP1x6zZ/fF6DwHq354s/aG/L7f72HAMBorT94XVmk=', expected b'bxvAnpxX30DaKPhGagkY2hEaFkHH+Q8kAXms32JaSNY=')
--- 4c. simple/simple, only the Subject whitespace changed
    log: bh: b'bxvAnpxX30DaKPhGagkY2hEaFkHH+Q8kAXms32JaSNY='
    log: b'DKIM-Signature' valid: False
    verify: False

Fall für Fall:

  1. Unveränderte Nachricht. Der Body-Hash stimmt überein, also besteht die Signatur die Prüfung.
  2. Footer angehängt. Eine Mailingliste fügt einen Footer zum Abbestellen hinzu. Der berechnete Body-Hash (got) weicht von bh= (expected) ab, also bricht die Prüfung ab, bevor sie die Signatur erreicht.
  3. Präfix im Subject. Die Liste stellt [team] voran. Der Body-Hash stimmt weiterhin, der Nachrichtentext ist also intakt, aber die Signatur über die Header schlägt fehl. „Body-Hash OK, Signatur nicht OK“ deutet auf einen geänderten Header oder einen falschen Schlüssel hin.
  4. Whitespace. Der Test verdoppelt ein Leerzeichen im Subject, fasst ein doppeltes Leerzeichen im Nachrichtentext zusammen und fügt ein Leerzeichen am Zeilenende hinzu. Mit relaxed/relaxed besteht die Signatur weiterhin die Prüfung. Mit simple/simple lässt die Änderung im Nachrichtentext den Body-Hash scheitern (4b), und die Änderung im Header allein lässt die Signatur scheitern (4c).

Empfangende Server schreiben das Ergebnis in Authentication-Results. Hier dieselben Fälle, geprüft mit mailauth 8.0.1 (Node.js 24.22), in dem Format, das du in einer empfangenen Nachricht siehst:

mailauth 8.0.1: Authentication-Results, DKIM-Teil
unchanged                          dkim=pass header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;
footer appended                    dkim=neutral (body hash did not verify) header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;
[team] added to Subject            dkim=fail (bad signature) header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;
key revoked (empty p=)             dkim=neutral (invalid public key) header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;
no key in DNS                      dkim=neutral (no key) header.i=@example.com header.s=sel header.a=rsa-sha256 header.b=P1HuE+RF;

Verifizierer formulieren Ergebnisse unterschiedlich. mailauth meldet einen abweichenden Body-Hash standardmäßig als neutral und im strikten Modus als dkim=fail (body hash did not verify). OpenDKIM schreibt dkim=fail reason="signature verification failed" sowohl bei Änderungen am Nachrichtentext als auch bei Änderungen an Headern. Die Ursache ist dieselbe. Die Fehlerseiten zu body hash did not verify, signature verification failed, key not found in DNS und permerror führen die Varianten auf.

Wer Nachrichten unterwegs verändert

Vom Symptom zur Ursache:

SymptomÜblicher Verursacher
Abweichender Body-Hash bei jeder Nachricht eines AbsendersEin Content-Filter, Disclaimer oder Tool zum Anhängen von E-Mail-Signaturen läuft nach dem DKIM-Signierer, oft auf der eigenen Seite des Absenders
Abweichender Body-Hash nur bei Nachrichten über eine MailinglisteDie Listensoftware fügt einen Footer hinzu oder schreibt MIME-Teile um
Abweichender Body-Hash nur bei Nachrichten mit Nicht-ASCII-TextEin Relay hat 8-Bit-Inhalt in Quoted-Printable umgewandelt. RFC 6376 §5.3 weist Signierer an, vor dem Signieren in 7-Bit-MIME umzuwandeln
Abweichender Body-Hash bei Nachrichten, die ein Skript erzeugtSigniert wurden reine LF-Zeilenenden, übertragen wird CRLF, oder umgekehrt
Ungültige Signatur, Body-Hash OKPräfix im Subject, umgeschriebenes From (die DMARC-Gegenmaßnahme einer Liste), ein Gateway, das einen Header neu faltet oder neu kodiert, Reply-To oder To bei der Weiterleitung geändert
Ungültige Signatur bei jeder NachrichtPrivater Schlüssel und DNS-Eintrag passen nicht zusammen: falscher Selektor, ein rotierter Schlüssel oder ein fehlerhaft eingefügter Schlüssel

Eine Weiterleitung per einfacher .forward-Datei oder per Alias lässt den Nachrichtentext und die signierten Header meist unverändert; DKIM übersteht sie also, während SPF daran scheitert. Mailinglisten sind der klassische Grund für ungültige Signaturen, weil sie die Nachricht verändern wollen. Moderne Listensoftware kann das vermeiden, indem sie Nachrichtentext und Subject nicht verändert oder From auf die Domain der Liste umschreibt und mit ihrem eigenen Schlüssel signiert. Empfänger, die ARC (RFC 8617, experimentell) unterstützen, können außerdem der ARC-Kette einer Liste vertrauen, die das ursprüngliche DKIM-Ergebnis vor der Änderung festhält.

Das Tag l=: Robustheit, die zur Fälschung einlädt

l= weist Verifizierer an, nur die ersten l Bytes des kanonisierten Nachrichtentexts zu hashen. Gedacht war das, damit Signaturen die Footer von Mailinglisten überstehen. Der Test zeigt die Kehrseite. Dieselbe Nachricht, mit l=35 signiert, besteht die Prüfung auch dann noch, wenn Inhalt angehängt wird – egal, ob es ein Footer ist oder ein ganz neuer Absatz von jemand anderem:

dkimpy 1.1.8: eine Signatur mit l=
--- 5. signed with l= (body length limit)
    l=35
--- 5a. footer appended to an l= signature
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    verify: True
--- 5b. a whole new part appended to an l= signature
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    verify: True

Der in 5b angehängte Text lautete Please pay invoice 4711 to IBAN XX00 0000 0000., und die Signatur sagt weiterhin pass. RFC 6376 §8.2 warnt, dass l= „Angriffe ermöglicht, bei denen eine Zwischenstation in böswilliger Absicht eine Nachricht verändern kann“, dass „Signierer bei der Verwendung dieses Tags äußerst vorsichtig sein sollten“ und dass „auswertende Systeme Signaturen, die das Tag verwenden, möglicherweise ignorieren wollen“. Mit MIME und HTML kann angehängter Inhalt das, was der Leser sieht, vollständig ersetzen. Signiere nicht mit l=. Findest du es in deinen ausgehenden Mails, entferne es aus der Konfiguration des Signierers. Bei OpenDKIM stammt l= aus den Einstellungen BodyLengthDB und MaximumSignedBytes.

Schlüssel und DNS

Der Verifizierer fragt selector._domainkey.domain als TXT-Eintrag ab. Erst die Fälle aus der Demo, dann echtes DNS:

dkimpy 1.1.8: Probleme mit dem Schlüssel
--- 6. key revoked in DNS (empty p=)
    log: could not parse RSA public key (b''): Unparsable public key; SubjectPublicKeyInfo: Data truncated at byte 0; RSAPublicKey: Data truncated at byte 0
    verify: False
--- 7. no key record in DNS
    log: missing public key: b'sel._domainkey.example.com.'
    verify: False
--- 8. 512-bit RSA key
    log: bh: b'eMSXagBrSE6c3/J0njUAW7nGMOlyd4roWBYqgR3grrE='
    log: b'DKIM-Signature' valid: True
    exception: KeyFormatError: public key too small: 512

Mit einem leeren p= widerruft eine Domain einen Selektor. Fall 8 zeigt: Die RSA-Arithmetik einer 512-Bit-Signatur geht auf, der Verifizierer lehnt den Schlüssel trotzdem ab. RFC 8301 schreibt vor, dass Verifizierer „Signaturen mit RSA-Schlüsseln von weniger als 1024 Bit NICHT als gültige Signaturen betrachten DÜRFEN“. Signierer „MÜSSEN RSA-Schlüssel mit mindestens 1024 Bit verwenden“ und „SOLLTEN RSA-Schlüssel mit mindestens 2048 Bit verwenden“, und rsa-sha1 „DARF NICHT zum Signieren oder Prüfen verwendet werden“. Ed25519 (a=ed25519-sha256, RFC 8463) ist ebenfalls definiert. Die Beispielnachricht im RFC selbst trägt sowohl eine RSA- als auch eine Ed25519-Signatur, und so wird es auch eingesetzt: mit der RSA-Signatur für Verifizierer, die Ed25519 nicht beherrschen.

Im echten DNS prüft opendkim-testkey von OpenDKIM einen Selektor so, wie es ein Verifizierer tun würde:

opendkim-testkey (OpenDKIM 2.11.0~beta2, Debian 13), 20. Oktober 2026
$ opendkim-testkey -d github.com -s google -vvv
opendkim-testkey: checking key 'google._domainkey.github.com'
opendkim-testkey: key not secure
opendkim-testkey: key OK
$ opendkim-testkey -d gmail.com -s 20230601 -vvv
opendkim-testkey: checking key '20230601._domainkey.gmail.com'
opendkim-testkey: Revoked key
$ opendkim-testkey -d gmail.com -s nosuchselector -vvv
opendkim-testkey: checking key 'nosuchselector._domainkey.gmail.com'
opendkim-testkey: 'nosuchselector._domainkey.gmail.com' record not found

„key not secure“ bedeutet nur, dass die Antwort nicht per DNSSEC validiert wurde. Der Selektor 20230601 bei gmail.com ist ein echter, ausgemusterter Schlüssel mit leerem p=. Damit signierte Nachrichten scheitern heute an der Prüfung; deshalb können alte Nachrichten nach einer Schlüsselrotation ihr DKIM-Ergebnis pass „verlieren“.

Ein 2048-Bit-Schlüssel passt nicht in einen einzelnen TXT-String von 255 Byte, daher wird er als mehrere Strings veröffentlicht, die Verifizierer ohne Trennzeichen zusammenfügen:

dig +short TXT google._domainkey.github.com (BIND 9.20.10, 20. Oktober 2026)
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj6T5sl/RwdSqGoYWaWaFbS2UAeyPrEmd0gogocmRfS441qwR8/0KB81Hw89P0l4YiFRrXYk7NVIGfyCRHAYYZUzCkGeOysI2EjgzLFhd/NEsbRzOEc/kWkK/RO6JFq/5lOn6M9AZw/ap9tds4JG9ApgNNdSpPxp9DmvpsOSgNMVflRxQFrk3kdS4RNAPKu/" "OPoA7dlR/A/pECryjRoYgENtDXzdnK70HgCekems6UDzxDj61cjyoKoXtEMF/QsaHEQ1Gjfv014rDJBsubk/kT5VqHkWHa/ia68Z5r228Ety/wFfQNjXTx/J7KGZ9GkZlKED659eiJcLnWcKDSiQlhwIDAQAB"

Typische DNS-Fehler: Anführungszeichen oder ein Leerzeichen, die beim Aufteilen von Hand in den Schlüssel geraten. Ein DNS-Anbieter, der aus einem langen String zwei Einträge macht statt eines Eintrags mit zwei Strings. Der Schlüssel, veröffentlicht unter dem falschen Namen, etwa sel._domainkey.example.com.example.com, wenn die Zone ihren Origin anhängt. Und ein neuer Selektor, der benutzt wird, bevor der Eintrag propagiert ist. Der DKIM-Eintragsgenerator erzeugt aus einem öffentlichen Schlüssel einen korrekt aufgeteilten Eintrag.

Diagnose Schritt für Schritt

  1. Hol dir die Rohnachricht, so wie sie angekommen ist, mit allen Headern: „Original anzeigen“ in Gmail, „Nachrichtenquelle anzeigen“ in Outlook oder die .eml-Datei. Wenn du dir die Nachricht selbst weiterleitest, verändert sie sich.
  2. Lies den Header Authentication-Results deines empfangenden Servers, also den obersten. Notiere das DKIM-Urteil und seinen Grund: Body-Hash, ungültige Signatur, kein Schlüssel, permerror.
  3. Lies den Header DKIM-Signature: d= und s= (welcher Schlüssel), c= (wie streng), h= (welche Header), l= (sollte fehlen).
  4. Prüfe den Schlüssel: dig +short TXT s._domainkey.d oder opendkim-testkey -d d -s s -vvv. Ist er vorhanden, nicht widerrufen, mindestens 1024 Bit lang und vollständig?
  5. Body-Hash stimmt nicht: Suche nach dem, was hinzugefügt oder neu kodiert wurde: Footer, Banner, Änderungen an Content-Transfer-Encoding zwischen gesendeter und empfangener Fassung. Betreibst du den Absender selbst, vergleiche die Kopie im Ordner „Gesendet“ oder im MTA-Log mit der empfangenen Kopie.
  6. Ungültige Signatur bei korrektem Body-Hash: Vergleiche jeden in h= aufgeführten Header zwischen gesendeter und empfangener Kopie. Hat sich keiner geändert, passt der Schlüssel nicht zum verwendeten privaten Schlüssel. Teste mit einer Nachricht, die du lokal signierst und prüfst.
  7. Sporadische Fehler: Suche nach mehreren ausgehenden Wegen (zwei MTAs, ein Cloud-Relay), von denen nur einer korrekt signiert, oder nach einem rotierten Selektor, den einer davon noch verwendet.

Statt von Hand geht das auch mit einer eingefügten Nachricht: Der DKIM-Debugger zeigt jeden Kanonisierungsschritt, das berechnete bh= im Vergleich zum Wert im Header, den Schlüssel aus dem DNS und die wahrscheinlichste Änderung. Die Nachricht bleibt in deinem Browser. Für das Gesamtbild einschließlich SPF, DMARC und ARC nutze den E-Mail-Authentifizierungs-Analyzer.

Lösungen, die Bestand haben

  • Signiere zuletzt. Lass auf deinem eigenen ausgehenden Weg Disclaimer, Footer, Link-Umschreibung und Virenscanner vor dem DKIM-Signierer laufen. In Postfix ist das die Reihenfolge von smtpd_milters oder content_filter.
  • Nutze c=relaxed/relaxed. Das kostet nichts und übersteht neu gefaltete Header und Whitespace-Änderungen.
  • Wandle vor dem Signieren in 7-Bit-MIME um (Quoted-Printable oder base64), wie RFC 6376 §5.3 empfiehlt, damit Relays ohne Unterstützung für 8BITMIME den Nachrichtentext nicht neu kodieren.
  • Verzichte auf l=, und nimm Header, die Zwischenstationen legitimerweise ändern, nur dann in h= auf, wenn sie signiert sein müssen.
  • Rotiere Schlüssel mit Überlappung. Veröffentliche den neuen Selektor, warte, bis die DNS-Caches abgelaufen sind, stelle das Signieren um, behalte den alten Eintrag noch einige Tage, damit Mails, die noch unterwegs sind, weiterhin die Prüfung bestehen, und widerrufe ihn dann mit einem leeren p=.
  • 2048-Bit-RSA-Schlüssel. Das Minimum sind 1024 Bit. Füge zusätzlich eine Ed25519-Signatur hinzu, wenn dein Signierer das unterstützt.
  • Für Mailinglisten, die du betreibst: Verändere Nachrichtentext und Subject nicht oder schreibe From um und signiere mit der Domain der Liste; füge außerdem ARC-Signaturen hinzu.

Quellen

  • RFC 6376: DKIM-Signaturen, §3.4 (Kanonisierung), §3.5 (Tags), §5.3 (Konvertierungen beim Transport), §8.2 (l=).
  • RFC 8301: Update zu kryptografischen Algorithmen und Schlüsselverwendung (kein SHA-1, Minimum 1024 Bit).
  • RFC 8463: Ed25519 für DKIM.
  • RFC 8617: Authenticated Received Chain (experimentell).
  • dkimpy 1.1.8, mailauth 8.0.1, opendkim-testkey von OpenDKIM.

Die dkimpy-Fälle liefen in node:24.22.0-trixie (Python 3.13.5 von Debian mit einem venv), mailauth in node:24.22.0-bookworm-slim, opendkim-testkey in debian:trixie-20261005-slim und dig in nicolaka/netshoot:v0.14, alle am 20. Oktober 2026.