DKIM suele tratarse como una caja negra: el correo sale, el receptor dice dkim=pass o dkim=fail y nadie sabe bien qué pasó en medio. Steve Atkins, de Word to the Wise, publicó el 27 de octubre un ejercicio corto para abrir esa caja con una terminal y openssl: calcular a mano el hash del cuerpo que va en la etiqueta bh= de la firma. Vale la pena porque ese hash es justo el paso que más se descompone cuando un servidor o una librería firma mal, y comprobarlo toma un minuto. Aquí va el ejercicio, una trampa de Bash que no viene en el original y una prueba que sí corrí.
Lo que pasa antes de firmar
El RFC 6376, que define DKIM, dice en su sección 3.7 que firmar y verificar empiezan igual: con dos hashes. El primero es del cuerpo del mensaje, ya canonizado; ese resultado, en base64, es lo que aparece en bh=. El segundo cubre los encabezados que lista la etiqueta h=, más el propio encabezado DKIM-Signature con la etiqueta b= vacía. Solo ese segundo hash se firma con la llave privada, y el resultado va en b=.
La parte de RSA no se puede hacer a mano, como reconoce el propio Atkins. Pero el hash del cuerpo sí, y ese ya te dice mucho.

Canonizar: simple contra relaxed
Antes del hash, el cuerpo se "canoniza", o sea, se normaliza para que cambios triviales en el camino no rompan la firma. La etiqueta c= lo declara con dos palabras separadas por una diagonal: la primera es para los encabezados y la segunda para el cuerpo. En c=relaxed/simple, el cuerpo va en simple. Si la etiqueta no aparece, el RFC dice que el valor por omisión es simple/simple.
- Simple (sección 3.4.3): quita las líneas vacías del final y, si no queda un CRLF al final, lo agrega. Nada más.
- Relaxed (sección 3.4.4): además quita los espacios y tabuladores al final de cada línea y junta cualquier racha de espacios dentro de una línea en uno solo. También quita las líneas vacías del final.
CRLF es el fin de línea del correo: los bytes 13 y 10, que en la terminal se escriben \r\n.
El caso interesante es el cuerpo vacío. Con simple queda convertido en un CRLF de dos bytes; con relaxed queda en cero bytes. Y el RFC trae los dos hashes SHA-256 esperados escritos en el texto, así que tienes contra qué comparar.
El ejercicio, en tu terminal
Para el cuerpo vacío con canonización simple:
printf '\r\n' | openssl sha256 -binary | base64
Para relaxed:
printf '' | openssl sha256 -binary | base64
Los resultados que obtuve son exactamente los que trae el RFC:
| Canonización del cuerpo | Bytes que se hashean | bh= (SHA-256) |
|---|---|---|
| simple | CRLF (2 bytes) | frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY= |
| relaxed | nada (0 bytes) | 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= |
La propuesta de Atkins para comprobarlo con correo real: mándate un mensaje sin cuerpo, abre el original y busca el encabezado DKIM-Signature. Si dice c=…/simple, el bh= debe ser el primero; si dice c=…/relaxed, el segundo. Si no coincide, quien firma está canonizando mal. Atkins lo propone como prueba rápida para código de firma recién escrito o recién configurado.
La trampa de Bash
El artículo original usa echo -n "\r\n". Ojo: en Bash, echo no interpreta \r\n por omisión, así que manda cuatro caracteres literales (una diagonal, una r, otra diagonal, una n). Lo comprobé: en Bash ese comando da zjGKtEkm8ss54KepwId5Q7EXM6uOYRy7Fw/u8aPVxUk=, que no es el hash correcto. En zsh, el shell por omisión de macOS, echo sí interpreta las secuencias, y por eso el ejemplo funciona ahí. printf se porta igual en los dos, así que es la opción segura. Es el tipo de error que te hace creer que tu firma está mal cuando lo que está mal es tu prueba.
Un paso más: un cuerpo que no está vacío
Para ver la diferencia entre las dos canonizaciones, armé un cuerpo con "Hola", dos espacios, "mundo", un espacio, un tabulador y dos líneas vacías al final. Lo firmé con dkimpy 1.1.8, una librería de Python, usando una llave RSA de 2048 bits de prueba y una consulta DNS simulada en el mismo script. No usé un dominio real ni lo mandé a ningún buzón.

Esto salió:
c=relaxed/simple: elbh=fuejuK/TBgWpMJK3vDkztZJleJYe6pij7a9l+Llw26v110=, el mismo que daopensslsobre la línea original sin las líneas vacías. Al agregar espacios al final de la línea, la verificación falló.c=relaxed/relaxed: elbh=fuexmFamFbJ3x0s4j/SB7mPftiyd8AMni6zF0oiedfSu6c=, el hash deHola mundomás un CRLF. Con los mismos espacios agregados, la verificación pasó.- En los dos casos, cambiar "Hola" por "HOLA" rompió la firma. Ninguna canonización perdona cambios de contenido.
Para reproducirlo sin librerías, el cuerpo relaxed es printf 'Hola mundo\r\n' y el simple es printf 'Hola mundo \t\r\n', cada uno pasado por openssl sha256 -binary | base64.
Cómo usarlo para diagnosticar
Mi lectura práctica, para quien opera el correo de sus sistemas:
- Compara
bh=primero. Si el receptor dice que el hash del cuerpo no coincide, el problema está en el cuerpo: un relay que reescribe, un pie de página agregado por una lista o un antivirus, o una librería que canoniza mal. Sibh=cuadra pero la firma no, busca en los encabezados o en la llave publicada en DNS. - Prefiere
relaxed/relaxedsi tus correos pasan por intermediarios que tocan espacios o fines de línea. Simple es más estricto y se rompe con menos. - No uses
l=. Esa etiqueta firma solo los primeros bytes del cuerpo. El RFC 6376, en su sección 8.2, advierte que permite que un intermediario agregue contenido sin romper la firma. - Usa
a=rsa-sha256. El RFC 8301 prohibió firmar y verificar conrsa-sha1, y pide llaves RSA de al menos 1024 bits, recomendando 2048.
Esto no reemplaza a un validador completo, pero convierte un "dkim=fail" opaco en una pregunta concreta: ¿falló el cuerpo o fallaron los encabezados?
Fuentes
- RFC 6376: DomainKeys Identified Mail (DKIM) Signatures
- RFC 6376, sección 3.4.3: canonización simple del cuerpo
- RFC 6376, sección 3.4.4: canonización relaxed del cuerpo
- RFC 6376, sección 3.7: cálculo de los hashes del mensaje
- RFC 8301: actualización de algoritmos y tamaños de llave en DKIM
- Fun with DKIM, Word to the Wise
- dkimpy en PyPI