Aller au contenu
ecdsa.com
← Tous les articles

Échec DKIM : condensat du corps, signature, clés et DNS

DKIM casse lorsqu’un message est modifié après la signature ou lorsque le vérificateur ne parvient pas à obtenir une clé utilisable. Chaque cas d’échec ci-dessous a été reproduit avec dkimpy et mailauth, avec le verdict exact qu’il produit, pour que vous puissiez le rapprocher de ce que vous voyez dans Authentication-Results.

En bref. Une signature DKIM couvre deux choses : un condensat du corps du message (bh=) et une signature portant sur une sélection d’en-têtes et sur ce condensat (b=). body hash did not verify signifie que le corps a changé après la signature, par exemple à cause d’un pied de page, d’un avertissement juridique, d’une bannière d’antivirus ou d’un changement d’encodage. bad signature / signature verification failed signifie qu’un en-tête signé a changé, par exemple une étiquette ajoutée au Subject ou un From réécrit, ou bien que la clé ne correspond pas. no key / key not found et permerror renvoient au DNS : mauvais sélecteur, enregistrement révoqué ou mal formé. La canonicalisation relaxed (c=relaxed/relaxed) tolère les modifications d’espaces et les changements de casse des noms d’en-tête, et rien d’autre. Pour diagnostiquer, comparez l’en-tête Authentication-Results aux balises de l’en-tête DKIM-Signature, puis corrigez le composant qui modifie les e-mails après la signature, ou signez plus tard dans le pipeline.

Ce que DKIM signe réellement

DKIM (RFC 6376) ajoute un en-tête DKIM-Signature. Celui-ci a été produit par dkimpy 1.1.8 pour un message de test :

En-tête DKIM-Signature (dkimpy 1.1.8, replié tel que généré)
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

La valeur de la signature est abrégée ici. Les balises utiles au débogage :

BaliseSignificationÉchec typique
d=, s=Domaine de signature et sélecteur. La clé est récupérée sous s._domainkey.dMauvais sélecteur, enregistrement absent ou révoqué
c=Canonicalisation des en-têtes/du corps, simple ou relaxed. Par défaut simple/simpleLes modifications d’espaces cassent simple
h=Champs d’en-tête signés, dans l’ordreToute modification de l’un d’eux
bh=Condensat Base64 du corps canonicaliséCorps modifié
b=Signature portant sur les en-têtes signés et sur l’en-tête DKIM-Signature lui-même (avec b= vide)En-tête modifié, ou mauvaise clé
l=Facultatif : seuls les l premiers octets du corps sont hachésPermet à quiconque d’ajouter du contenu (voir plus bas)
a=Algorithme : rsa-sha256 ou ed25519-sha256rsa-sha1 n’est plus valide (RFC 8301)

Le vérificateur procède dans un ordre fixe. Il récupère d’abord la clé (s._domainkey.d, TXT). Il canonicalise ensuite le corps et compare son condensat à bh=. Ce n’est qu’après qu’il vérifie b= sur les en-têtes. Cet ordre explique les verdicts : une modification du corps est signalée comme un problème de condensat du corps, même si un en-tête a également changé.

Canonicalisation : ce que « relaxed » tolère

La canonicalisation ramène le message à une forme standard avant le hachage, afin que des modifications anodines en transit ne cassent pas la signature. La RFC 6376 §3.4 définit deux formes :

  • simple pour les en-têtes : aucune modification. Même la casse du nom d’en-tête ne doit pas changer.
  • relaxed pour les en-têtes : les noms passent en minuscules, les lignes de continuation sont dépliées, les suites d’espaces sont réduites à un seul espace, et les espaces en fin de valeur et autour du deux-points sont supprimés.
  • simple pour le corps : seules les lignes vides finales sont ignorées.
  • relaxed pour le corps : en outre, les espaces en fin de ligne sont ignorés et les suites d’espaces et de tabulations au sein d’une ligne deviennent un seul espace.

Aucune des deux formes ne tolère de mots modifiés, de lignes ajoutées, de parties MIME réencodées ni de fins de ligne différentes. relaxed ne tolère que les espaces et la casse des noms d’en-tête. C’est pourquoi c=relaxed/relaxed est le choix habituel. La valeur par défaut de la RFC est simple/simple, si bien qu’un en-tête DKIM-Signature sans c= utilise la forme stricte.

Les échecs, reproduits

Le test signe un message court avec une clé RSA de 2048 bits (c=relaxed/relaxed, h=from:to:subject:date:message-id), le modifie comme le font de vrais intermédiaires, puis le vérifie avec dkimpy 1.1.8. La requête DNS est remplacée par une fonction interne au processus, ce qui permet de modifier l’enregistrement de clé d’un cas à l’autre :

dkimpy 1.1.8, Python 3.13.5 (lignes de journal de dkimpy, valeurs base64 complètes)
--- 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

Cas par cas :

  1. Message inchangé. Le condensat du corps correspond, donc la vérification de la signature réussit.
  2. Pied de page ajouté. Une liste de diffusion ajoute un pied de page de désabonnement. Le condensat du corps calculé (got) diffère de bh= (expected), si bien que la vérification s’arrête avant d’atteindre la signature.
  3. Étiquette dans le Subject. La liste ajoute le préfixe [team]. Le condensat du corps correspond toujours, le corps est donc intact, mais la signature des en-têtes échoue. « Condensat du corps OK, signature pas OK » désigne un en-tête modifié ou une mauvaise clé.
  4. Espaces. Le test double un espace dans le Subject, réduit un double espace dans le corps et ajoute un espace final. Avec relaxed/relaxed, la signature reste valide. Avec simple/simple, la modification du corps fait échouer le condensat du corps (4b), et la modification de l’en-tête suffit à elle seule à faire échouer la signature (4c).

Les serveurs destinataires inscrivent le résultat dans Authentication-Results. Voici les mêmes cas vérifiés par mailauth 8.0.1 (Node.js 24.22), au format que vous verrez dans un message reçu :

mailauth 8.0.1 : Authentication-Results, partie dkim
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;

Les vérificateurs formulent les résultats différemment. Par défaut, mailauth signale une non-concordance du condensat du corps par neutral, et par dkim=fail (body hash did not verify) en mode strict. OpenDKIM écrit dkim=fail reason="signature verification failed" aussi bien pour les modifications du corps que pour celles des en-têtes. La cause, elle, est la même. Les pages d’erreur body hash did not verify, signature verification failed, key not found in DNS et permerror recensent les variantes.

Qui modifie les messages en transit

Du symptôme à sa cause :

SymptômeCoupable habituel
Non-concordance du condensat du corps sur chaque message d’un même expéditeurUn filtre de contenu ou un outil qui ajoute un avertissement juridique ou un bloc de signature, exécuté après le signataire DKIM, souvent chez l’expéditeur lui-même
Non-concordance du condensat du corps uniquement pour les messages passés par une liste de diffusionLe logiciel de liste ajoute un pied de page ou réécrit des parties MIME
Non-concordance du condensat du corps uniquement pour les messages contenant du texte non ASCIIContenu 8 bits converti en quoted-printable par un relais. La RFC 6376 §5.3 demande aux signataires de convertir en MIME 7 bits avant de signer
Non-concordance du condensat du corps pour les messages générés par un scriptFins de ligne LF seules à la signature, CRLF sur le réseau, ou l’inverse
Signature invalide, condensat du corps OKÉtiquette dans le Subject, réécriture du From (la parade DMARC d’une liste), passerelle qui replie un en-tête ou en change l’encodage, Reply-To ou To modifié par un transfert
Signature invalide sur chaque messageLa clé privée et l’enregistrement DNS ne correspondent pas : mauvais sélecteur, clé ayant fait l’objet d’une rotation, ou clé collée avec une erreur

Un transfert par simple .forward ou par alias conserve généralement le corps et les en-têtes signés, si bien que DKIM survit là où SPF casse. Les listes de diffusion sont la cause classique de ces échecs, parce qu’elles veulent modifier le message. Un logiciel de liste moderne peut l’éviter en ne modifiant ni le corps ni le Subject, ou en réécrivant From avec le domaine de la liste et en signant avec sa propre clé. Les serveurs destinataires qui prennent en charge ARC (RFC 8617, statut expérimental) peuvent aussi faire confiance à la chaîne ARC d’une liste, qui enregistre le résultat DKIM d’origine avant la modification.

La balise l= : une robustesse qui invite à la falsification

l= indique aux vérificateurs de ne hacher que les l premiers octets du corps canonicalisé. Cette balise devait permettre aux signatures de survivre aux pieds de page des listes de diffusion. Le test en montre le revers. Le même message signé avec l=35 reste valide après l’ajout de contenu, qu’il s’agisse d’un pied de page ou d’un paragraphe entièrement nouveau écrit par quelqu’un d’autre :

dkimpy 1.1.8 : une signature avec 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

Le texte ajouté en 5b était Please pay invoice 4711 to IBAN XX00 0000 0000., et la signature indique toujours pass. La RFC 6376 §8.2 avertit que l= « permet des attaques dans lesquelles un intermédiaire malveillant peut modifier un message », que « les signataires devraient se montrer extrêmement prudents dans l’usage de cette balise » et que « les évaluateurs pourraient souhaiter ignorer les signatures qui utilisent cette balise ». Avec MIME et HTML, le contenu ajouté peut remplacer entièrement ce que voit le lecteur. Ne signez pas avec l=. Si vous la trouvez dans vos e-mails sortants, retirez-la de la configuration du signataire. Dans OpenDKIM, l= provient des paramètres BodyLengthDB et MaximumSignedBytes.

Clés et DNS

Le vérificateur recherche selector._domainkey.domain sous forme d’enregistrement TXT. D’abord les cas de la démonstration, puis le DNS réel :

dkimpy 1.1.8 : problèmes de clé
--- 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

C’est par un p= vide qu’un domaine révoque un sélecteur. Le cas 8 montre que l’arithmétique RSA d’une signature de 512 bits est correcte, mais que le vérificateur rejette quand même la clé. La RFC 8301 exige que les vérificateurs « ne considèrent pas (MUST NOT) comme valides les signatures utilisant des clés RSA de moins de 1024 bits ». Les signataires « doivent (MUST) utiliser des clés RSA d’au moins 1024 bits » et « devraient (SHOULD) utiliser des clés RSA d’au moins 2048 bits », et rsa-sha1 « ne doit pas (MUST NOT) être utilisé pour signer ni pour vérifier ». Ed25519 (a=ed25519-sha256, RFC 8463) est également défini. Le message d’exemple de cette RFC porte à la fois une signature RSA et une signature Ed25519, et c’est ainsi qu’Ed25519 est déployé, la signature RSA servant aux vérificateurs qui ne prennent pas en charge Ed25519.

Dans le DNS réel, l’outil opendkim-testkey d’OpenDKIM vérifie un sélecteur comme le ferait un vérificateur :

opendkim-testkey (OpenDKIM 2.11.0~beta2, Debian 13), 20 octobre 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 » signifie seulement que la réponse n’a pas été validée par DNSSEC. Le sélecteur 20230601 de gmail.com est une vraie clé retirée, avec un p= vide. Les messages signés avec elle échouent désormais, c’est pourquoi d’anciens messages peuvent « perdre » leur verdict DKIM pass après une rotation de clé.

Une clé de 2048 bits ne tient pas dans une seule chaîne TXT de 255 octets, elle est donc publiée en plusieurs chaînes que les vérificateurs concatènent sans séparateur :

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

Erreurs DNS typiques : des guillemets ou un espace collés dans la clé lors d’un découpage à la main. Un fournisseur DNS qui transforme une longue chaîne en deux enregistrements au lieu d’un seul enregistrement à deux chaînes. La clé publiée sous un mauvais nom, par exemple sel._domainkey.example.com.example.com lorsque la zone ajoute l’origine. Et un nouveau sélecteur utilisé avant que l’enregistrement se soit propagé. Le générateur d’enregistrements DKIM produit un enregistrement correctement découpé à partir d’une clé publique.

Diagnostic pas à pas

  1. Récupérez le message brut tel qu’il a été reçu, avec tous ses en-têtes : « Afficher l’original » dans Gmail, « Afficher la source » dans Outlook, ou le fichier .eml. Le fait de vous transférer le message le modifie.
  2. Lisez Authentication-Results de votre serveur destinataire, celui qui figure le plus haut. Notez le verdict DKIM et sa raison : condensat du corps, signature invalide, clé absente, permerror.
  3. Lisez l’en-tête DKIM-Signature : d= et s= (quelle clé), c= (quel degré de rigueur), h= (quels en-têtes), l= (devrait être absente).
  4. Vérifiez la clé : dig +short TXT s._domainkey.d ou opendkim-testkey -d d -s s -vvv. Est-elle présente, non révoquée, d’au moins 1024 bits, et complète ?
  5. Non-concordance du condensat du corps : cherchez ce qui a été ajouté ou réencodé (pieds de page, bannières, changements de Content-Transfer-Encoding) entre ce qui a été envoyé et ce qui a été reçu. Si vous contrôlez l’expéditeur, comparez la copie du dossier Envoyés ou du journal du MTA avec la copie reçue.
  6. Signature invalide avec un condensat du corps correct : comparez chaque en-tête listé dans h= entre la copie envoyée et la copie reçue. Si aucun n’a changé, la clé ne correspond pas à la clé privée utilisée. Faites le test avec un message signé et vérifié localement.
  7. Échecs intermittents : cherchez plusieurs chemins sortants (deux MTA, un relais cloud) dont un seul signe correctement, ou un sélecteur ayant fait l’objet d’une rotation mais encore utilisé par l’un d’eux.

Pour faire tout cela sur un message collé plutôt qu’à la main, le débogueur DKIM montre chaque étape de canonicalisation, le bh= calculé face à la valeur de l’en-tête, la clé issue du DNS et la modification la plus probable. Le message reste dans votre navigateur. Pour une vue d’ensemble, SPF, DMARC et ARC compris, utilisez l’analyseur d’authentification des e-mails.

Des correctifs durables

  • Signez en dernier. Sur votre propre chemin sortant, faites passer les avertissements juridiques, les pieds de page, la réécriture des liens et l’antivirus avant le signataire DKIM. Dans Postfix, c’est l’ordre de smtpd_milters ou de content_filter.
  • Utilisez c=relaxed/relaxed. Cela ne coûte rien et résiste au repliage des en-têtes et aux modifications d’espaces.
  • Convertissez en MIME 7 bits avant de signer (quoted-printable ou base64), comme le recommande la RFC 6376 §5.3, afin que les relais sans prise en charge de 8BITMIME ne réencodent pas le corps.
  • N’utilisez pas l=, et ne placez pas dans h= les en-têtes que les intermédiaires modifient légitimement, sauf si vous avez besoin qu’ils soient signés.
  • Effectuez la rotation des clés avec chevauchement. Publiez le nouveau sélecteur, attendez l’expiration des caches DNS, basculez la signature, conservez l’ancien enregistrement quelques jours pour que les e-mails en transit soient encore vérifiés avec succès, puis révoquez-le avec un p= vide.
  • Des clés RSA de 2048 bits. Le minimum est 1024. Ajoutez en parallèle une signature Ed25519 si votre signataire la prend en charge.
  • Pour les listes de diffusion que vous gérez : ne modifiez ni le corps ni le Subject, ou bien réécrivez From et signez avec le domaine de la liste, et ajoutez des signatures ARC.

Sources

  • RFC 6376 : DKIM Signatures, §3.4 (canonicalisation), §3.5 (balises), §5.3 (conversions de transport), §8.2 (l=).
  • RFC 8301 : mise à jour sur les algorithmes cryptographiques et l’utilisation des clés (plus de SHA-1, minimum de 1024 bits).
  • RFC 8463 : Ed25519 pour DKIM.
  • RFC 8617 : Authenticated Received Chain (statut expérimental).
  • dkimpy 1.1.8, mailauth 8.0.1, opendkim-testkey d’OpenDKIM.

Les cas dkimpy ont été exécutés dans node:24.22.0-trixie (le Python 3.13.5 de Debian avec un venv), mailauth dans node:24.22.0-bookworm-slim, opendkim-testkey dans debian:trixie-20261005-slim et dig dans nicolaka/netshoot:v0.14, tous le 20 octobre 2026.