Pular para o conteúdo
ecdsa.com
← Todos os artigos

Falhas de DKIM: hash do corpo, assinatura, chaves e DNS

O DKIM quebra quando a mensagem muda depois de assinada ou quando o verificador não consegue obter uma chave utilizável. Cada modo de falha abaixo foi reproduzido com o dkimpy e o mailauth, com o veredicto exato que cada falha produz, para que você possa comparar com o que aparece no Authentication-Results.

Resposta curta. Uma assinatura DKIM cobre duas coisas: um hash do corpo da mensagem (bh=) e uma assinatura sobre cabeçalhos selecionados mais esse hash (b=). body hash did not verify significa que o corpo mudou depois da assinatura, por exemplo por causa de um rodapé, um aviso legal, um banner de antivírus ou uma mudança de codificação. bad signature / signature verification failed significa que um cabeçalho assinado mudou, como uma tag no Subject ou um From reescrito, ou que a chave não corresponde. no key / key not found e permerror apontam para o DNS: seletor errado, registro revogado ou malformado. A canonicalização relaxed (c=relaxed/relaxed) tolera mudanças nos espaços em branco e nas maiúsculas e minúsculas dos nomes de cabeçalho, e nada mais. Para diagnosticar, compare o cabeçalho Authentication-Results com as tags do DKIM-Signature e então corrija o componente que modifica o e-mail depois da assinatura, ou assine mais adiante no pipeline.

O que o DKIM realmente assina

O DKIM (RFC 6376) adiciona um cabeçalho DKIM-Signature. Este foi gerado pelo dkimpy 1.1.8 para uma mensagem de teste:

Cabeçalho DKIM-Signature (dkimpy 1.1.8, quebrado em linhas como foi gerado)
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

O valor da assinatura foi encurtado aqui. As tags que importam para a depuração:

TagSignificadoFalha típica
d=, s=Domínio de assinatura e seletor. A chave é buscada em s._domainkey.dSeletor errado, registro ausente ou revogado
c=Canonicalização do cabeçalho/corpo, simple ou relaxed. Padrão: simple/simpleMudanças de espaços em branco quebram simple
h=Campos de cabeçalho assinados, em ordemQualquer mudança em um deles
bh=Hash em Base64 do corpo canonicalizadoO corpo mudou
b=Assinatura sobre os cabeçalhos assinados e o próprio DKIM-Signature (com b= vazio)Um cabeçalho mudou, ou a chave está errada
l=Opcional: só os primeiros l bytes do corpo entram no hashPermite que qualquer pessoa acrescente conteúdo (veja abaixo)
a=Algoritmo: rsa-sha256 ou ed25519-sha256rsa-sha1 não é mais válido (RFC 8301)

O verificador segue uma ordem fixa. Primeiro, busca a chave (s._domainkey.d, TXT). Depois, canonicaliza o corpo e compara o hash dele com bh=. Só então verifica b= sobre os cabeçalhos. Essa ordem explica os veredictos: uma mudança no corpo é relatada como problema no hash do corpo, mesmo que um cabeçalho também tenha mudado.

Canonicalização: o que o “relaxed” perdoa

A canonicalização converte a mensagem em uma forma padrão antes do cálculo do hash, para que mudanças inofensivas no caminho não quebrem a assinatura. A RFC 6376 §3.4 define duas formas:

  • simple nos cabeçalhos: nenhuma alteração. Nem a caixa (maiúsculas e minúsculas) do nome do cabeçalho pode mudar.
  • relaxed nos cabeçalhos: os nomes passam para minúsculas, as linhas de continuação são juntadas, cada sequência de espaços em branco vira um único espaço e os espaços em branco no fim do valor e em volta dos dois-pontos são removidos.
  • simple no corpo: só as linhas vazias no final são ignoradas.
  • relaxed no corpo: além disso, os espaços em branco no fim das linhas são ignorados, e cada sequência de espaços e tabulações dentro de uma linha vira um único espaço.

Nenhuma das duas formas tolera palavras alteradas, linhas adicionadas, partes MIME recodificadas ou terminações de linha diferentes. O relaxed só perdoa espaços em branco e a caixa dos nomes de cabeçalho. É por isso que c=relaxed/relaxed é a escolha habitual. O padrão da RFC é simple/simple, então um DKIM-Signature sem c= usa a forma estrita.

Reprodução das falhas

O teste assina uma mensagem curta com uma chave RSA de 2048 bits (c=relaxed/relaxed, h=from:to:subject:date:message-id), altera a mensagem como fazem os intermediários reais e a verifica com o dkimpy 1.1.8. A consulta ao DNS é substituída por uma função no próprio processo, para que o registro da chave possa ser alterado entre um caso e outro:

dkimpy 1.1.8, Python 3.13.5 (linhas de log do dkimpy, valores base64 completos)
--- 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

Caso a caso:

  1. Mensagem inalterada. O hash do corpo confere, então a assinatura passa na verificação.
  2. Rodapé acrescentado. Uma lista de discussão adiciona um rodapé para cancelar a inscrição. O hash do corpo calculado (got) difere de bh= (expected), então a verificação para antes de chegar à assinatura.
  3. Tag no Subject. A lista coloca [team] no início do Subject. O hash do corpo continua conferindo, então o corpo está intacto, mas a assinatura dos cabeçalhos falha. “Hash do corpo OK, assinatura não OK” aponta para um cabeçalho alterado ou para uma chave errada.
  4. Espaços em branco. O teste duplica um espaço no Subject, reduz um espaço duplo do corpo a um só e acrescenta um espaço no fim de uma linha. Com relaxed/relaxed, a assinatura continua passando na verificação. Com simple/simple, a mudança no corpo faz o hash do corpo divergir (4b), e a mudança no cabeçalho, sozinha, já faz a assinatura falhar (4c).

Os servidores receptores gravam o resultado em Authentication-Results. Aqui estão os mesmos casos verificados pelo mailauth 8.0.1 (Node.js 24.22), no formato que você verá em uma mensagem recebida:

mailauth 8.0.1: Authentication-Results, parte do 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;

Cada verificador descreve os resultados de um jeito. O mailauth relata a divergência do hash do corpo como neutral por padrão e como dkim=fail (body hash did not verify) no modo estrito. O OpenDKIM escreve dkim=fail reason="signature verification failed" tanto para mudanças no corpo quanto para mudanças nos cabeçalhos. A causa é a mesma. As páginas de erro de body hash did not verify, signature verification failed, key not found in DNS e permerror listam as variantes.

Quem altera as mensagens no caminho

Como relacionar o sintoma à causa:

SintomaCulpado habitual
Hash do corpo divergente em todas as mensagens de um remetenteUm filtro de conteúdo, um aviso legal ou uma ferramenta que adiciona assinaturas de e-mail rodando depois do signatário DKIM, muitas vezes no próprio lado do remetente
Hash do corpo divergente só em mensagens que passam por uma lista de discussãoO software da lista adiciona um rodapé ou reescreve partes MIME
Hash do corpo divergente só em mensagens com texto não ASCIIConteúdo de 8 bits convertido em quoted-printable por um servidor de retransmissão. A RFC 6376 §5.3 orienta os signatários a converter para MIME de 7 bits antes de assinar
Hash do corpo divergente em mensagens geradas por um scriptTerminações de linha LF isoladas na assinatura e CRLF na transmissão, ou o contrário
Assinatura inválida, hash do corpo OKTag no Subject, reescrita do From (a mitigação de DMARC de uma lista), um gateway que quebra de novo as linhas de um cabeçalho ou o recodifica, Reply-To ou To alterado pelo encaminhamento
Assinatura inválida em todas as mensagensA chave privada e o registro DNS não correspondem: seletor errado, chave rotacionada ou chave colada com erro

O encaminhamento simples, por um .forward ou por um alias, costuma preservar o corpo e os cabeçalhos assinados, então o DKIM sobrevive onde o SPF quebra. As listas de discussão são as clássicas culpadas, porque querem alterar a mensagem. Um software de lista moderno pode evitar isso não modificando o corpo nem o Subject, ou reescrevendo o From com o domínio da lista e assinando com a própria chave. Os receptores que oferecem suporte ao ARC (RFC 8617, experimental) também podem confiar na cadeia ARC da lista, que registra o resultado DKIM original antes da modificação.

A tag l=: uma robustez que convida à falsificação

l= instrui os verificadores a calcular o hash só dos primeiros l bytes do corpo canonicalizado. A ideia era permitir que as assinaturas sobrevivessem aos rodapés das listas de discussão. O teste mostra o lado ruim. A mesma mensagem assinada com l=35 continua passando na verificação depois que se acrescenta conteúdo, seja um rodapé, seja um parágrafo inteiro novo escrito por outra pessoa:

dkimpy 1.1.8: uma assinatura com 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

O texto acrescentado em 5b foi Please pay invoice 4711 to IBAN XX00 0000 0000., e a assinatura continua dizendo pass. A RFC 6376, na §8.2, alerta que l= “permite ataques em que um intermediário mal-intencionado pode modificar uma mensagem”, que “os signatários deveriam ser extremamente cautelosos ao usar essa tag” e que “os avaliadores podem querer ignorar as assinaturas que usam essa tag”. Com MIME e HTML, o conteúdo acrescentado pode substituir por completo o que o leitor vê. Não assine com l=. Se encontrar essa tag nos seus e-mails de saída, remova-a da configuração do signatário. No OpenDKIM, a tag l= vem das configurações BodyLengthDB e MaximumSignedBytes.

Chaves e DNS

O verificador consulta selector._domainkey.domain como registro TXT. Primeiro os casos da demonstração, depois o DNS real:

dkimpy 1.1.8: problemas de chave
--- 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

Um p= vazio é a forma de um domínio revogar um seletor. O caso 8 mostra que a aritmética RSA de uma assinatura de 512 bits confere, mas o verificador rejeita a chave mesmo assim. A RFC 8301 estabelece que os verificadores “NÃO DEVEM considerar válidas as assinaturas que usam chaves RSA de menos de 1024 bits”. Os signatários “DEVEM usar chaves RSA de pelo menos 1024 bits” e “DEVERIAM usar chaves RSA de pelo menos 2048 bits”, e o rsa-sha1 “NÃO DEVE ser usado para assinar nem para verificar”. O Ed25519 (a=ed25519-sha256, RFC 8463) também está definido. A própria mensagem de exemplo da RFC traz uma assinatura RSA e uma Ed25519, e é assim que ele é implantado: com a assinatura RSA para os verificadores que não oferecem suporte a Ed25519.

No DNS real, o opendkim-testkey do OpenDKIM verifica um seletor como um verificador faria:

opendkim-testkey (OpenDKIM 2.11.0~beta2, Debian 13), 20 de outubro de 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” só significa que a resposta não foi validada com DNSSEC. O seletor 20230601 em gmail.com é uma chave real aposentada, com p= vazio. As mensagens assinadas com ela falham agora, e é por isso que mensagens antigas podem “perder” o pass do DKIM depois de uma rotação de chaves.

Uma chave de 2048 bits não cabe em uma única string TXT de 255 bytes, então ela é publicada como várias strings, que os verificadores juntam sem separadores:

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

Erros típicos de DNS: aspas ou um espaço colados dentro da chave ao dividi-la à mão; um provedor de DNS que transforma uma string longa em dois registros, em vez de um registro com duas strings; a chave publicada com o nome errado, como sel._domainkey.example.com.example.com quando a zona acrescenta a origem; e um seletor novo usado antes de o registro se propagar. O gerador de registros DKIM produz, a partir de uma chave pública, um registro dividido corretamente.

Diagnóstico passo a passo

  1. Obtenha a mensagem bruta como foi recebida, com todos os cabeçalhos: “Mostrar original” no Gmail, “Exibir código-fonte” no Outlook ou o arquivo .eml. Encaminhar a mensagem para você mesmo a altera.
  2. Leia o Authentication-Results do seu servidor receptor, o que está mais acima. Anote o veredicto do DKIM e o motivo: hash do corpo, assinatura inválida, chave ausente, permerror.
  3. Leia o DKIM-Signature: d= e s= (qual chave), c= (qual o rigor), h= (quais cabeçalhos), l= (deveria estar ausente).
  4. Verifique a chave: dig +short TXT s._domainkey.d ou opendkim-testkey -d d -s s -vvv. Ela existe, não foi revogada, tem pelo menos 1024 bits e está completa?
  5. Hash do corpo divergente: procure o que foi adicionado ou recodificado: rodapés, banners, mudanças de Content-Transfer-Encoding entre o que foi enviado e o que foi recebido. Se você controla o remetente, compare a cópia da pasta Enviados ou do log do MTA com a cópia recebida.
  6. Assinatura inválida com hash do corpo correto: compare cada cabeçalho listado em h= entre a cópia enviada e a recebida. Se nenhum mudou, a chave do DNS não corresponde à chave privada em uso. Teste com uma mensagem que você assina e verifica localmente.
  7. Falhas intermitentes: procure vários caminhos de saída (dois MTAs, um servidor de retransmissão na nuvem) em que só um assina corretamente, ou um seletor já rotacionado que um deles ainda usa.

Para fazer isso com uma mensagem colada, em vez de à mão, o depurador de DKIM mostra cada etapa da canonicalização, o bh= calculado comparado ao valor do cabeçalho, a chave do DNS e a modificação mais provável. A mensagem fica no seu navegador. Para o quadro completo, incluindo SPF, DMARC e ARC, use o analisador de autenticação de e-mail.

Correções duradouras

  • Assine por último. No seu próprio caminho de saída, rode avisos legais, rodapés, reescrita de links e antivírus antes do signatário DKIM. No Postfix, isso é definido pela ordem em smtpd_milters ou content_filter.
  • Use c=relaxed/relaxed. Não custa nada e resiste a novas quebras de linha nos cabeçalhos e a mudanças de espaços em branco.
  • Converta para MIME de 7 bits antes de assinar (quoted-printable ou base64), como recomenda a RFC 6376 §5.3, para que servidores de retransmissão sem suporte a 8BITMIME não recodifiquem o corpo.
  • Não use l= e não coloque em h= cabeçalhos que os intermediários alteram legitimamente, a menos que você precise deles assinados.
  • Faça a rotação de chaves com sobreposição. Publique o novo seletor, espere os caches de DNS expirarem, passe a assinar com ele, mantenha o registro antigo por alguns dias para que os e-mails em trânsito ainda sejam verificados e depois revogue-o com um p= vazio.
  • Chaves RSA de 2048 bits. O mínimo é 1024. Acrescente também uma assinatura Ed25519 se o seu signatário oferecer suporte.
  • Nas listas de discussão que você mantém: não modifique o corpo nem o Subject, ou reescreva o From e assine com o domínio da lista; e adicione assinaturas ARC.

Fontes

  • RFC 6376: assinaturas DKIM, §3.4 (canonicalização), §3.5 (tags), §5.3 (conversões de transporte), §8.2 (l=).
  • RFC 8301: atualização dos algoritmos criptográficos e do uso da chave (sem SHA-1, mínimo de 1024 bits).
  • RFC 8463: Ed25519 para DKIM.
  • RFC 8617: protocolo Authenticated Received Chain (experimental).
  • dkimpy 1.1.8, mailauth 8.0.1, opendkim-testkey do OpenDKIM.

Os casos do dkimpy rodaram em node:24.22.0-trixie (o Python 3.13.5 do Debian com um venv), o mailauth em node:24.22.0-bookworm-slim, o opendkim-testkey em debian:trixie-20261005-slim e o dig em nicolaka/netshoot:v0.14, todos em 20 de outubro de 2026.