Août 262014
 

Le but de cet article est de présenter en détail les mécanismes cryptographiques mis en œuvre par DKIM (DomainKeys Identified Mail).

Référence : rfc4871

Présentation

DKIM (DomainKeys Identified Mail) est une norme d’authentification fiable du nom de domaine de l’expéditeur d’un courrier électronique. Elle constitue une protection efficace contre le spam et l’hameçonnage. En effet, DKIM fonctionne par signature cryptographique du corps du message et d’une partie de ses en-têtes. Une signature DKIM vérifie donc l’authenticité du domaine expéditeur et garantit l’intégrité du message.

Principes généraux

Génération de la clef privée et publique (RSA).

Dépot de la clef privée sur le MTA (serveur de mail) émetteur.
Dépot de la clef publique dans la zone DNS du domaine de l’expéditeur du mail.

  • Elle est déclarée sous la forme d’un enregistrement de type TXT.

Le MTA émetteur signe des éléments d’entêtes et le corps du message avec la clef privée et insère la signature dans l’entête du message.

La signature consiste :

  • à concaténer des éléments d’entêtes et le corps du message
  • à calculer le hash
  • et à chiffer le hash avec la clef privée

Le MTA destinataire, à la réception du mail, interroge l’enregistrement TXT de la zone DNS du domaine de l’expéditeur pour obtenir la clef publique.

  • Il déchiffre la signature avec la clef publique pour obtenir le hash.
  • Il concatène des éléments d’entêtes et le corps du message et en calcule le hash
  • Il compare les deux hashs :
    • S’ils sont identiques, cela valide l’authenticité du domaine expéditeur et garantit l’intégrité du message.

Algorithme en détail

Génération des clefs RSA

Le domaine est : cybercod.com

Les clefs peuvent être générées avec la commande suivante

opendkim-genkey -a -b 1024 -h sha256 -D /etc/opendkim/cybercod.com -d cybercod.com -s mail

-a : ajout de cybercod.com aprés mail._domainkey
-b : taille de la clef
-h : type de hashage
-D : dossier où seront générées les clefs
-d : nom du domaine
-s : sélecteur

La clef privée est générée dans /etc/opendkim/cybercod.com/mail.private
extrait :

-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDGQHEC1dOS9ORvySp992sLpFx0rEkgz/XrQlpZvdo69Gqz7Qq5
... ...
GqPNt5YkvhwVMbvSISiCd6Jszj5d2R8EUk77MZWZFH1y
-----END RSA PRIVATE KEY-----

la clef publique est générée dans /etc/opendkim/cybercod.com/mail.txt


mail._domainkey.cybercod.com. IN TXT "v=DKIM1; h=sha256; k=rsa; p=MIGfMA0G.......cQIDAQAB"

Le champ p contient la clef publique encodée en base64

On peut également générer les clef avec openssl.

Clef privée :

openssl genrsa -out mail.private 1024

Clef publique:

openssl rsa -in mail.private -out mail.txt -pubout -outform PEM

Affichage détaillé de la clef privée

openssl.exe rsa -noout -text -in <chemin de mail.private>

extrait :

Private-Key: (1024 bit)
modulus:
00:c6:40:71:02:d5:d3:92:f4:e4:6f:c9:2a:7d:f7:
6b:0b:a4:5c:74:ac:49:20:cf:f5:eb:42:5a:59:bd:
da:3a:f4:6a:b3:ed:0a:b9:36:63:3e:aa:2d:70:4c:
83:c9:86:b9:14:ad:94:aa:ee:ce:85:47:51:fc:86:
2f:ab:f1:37:64:23:73:47:5f:ab:4d:92:ae:ab:ca:
28:b4:cf:0f:d3:b7:a7:b9:8d:dd:71:aa:bd:ba:fc:
88:f0:ca:e9:cf:2f:2f:2e:55:76:3a:99:2c:2c:eb:
07:80:7c:65:cf:2f:79:e8:dc:6e:78:a3:c6:6b:59:
2e:a4:d6:60:5f:47:38:43:71
publicExponent: 65537 (0x10001)
privateExponent:
00:96:9f:5f:3d:48:37:f6:ef:18:9f:d5:b6:f2:fd:
87:d6:d0:89:6e:1b:77:73:f6:8c:60:b1:88:f3:a5:
ca:a8:00:0b:11:a8:86:fd:30:d5:36:47:15:3e:bc:
e3:63:b9:77:e4:bd:fc:b5:e1:ba:06:88:a9:41:b2:
b1:85:71:3f:22:ff:24:21:78:08:bc:22:43:ec:87:
e3:7a:1c:67:62:ab:56:0b:89:60:cf:5e:59:f6:79:
c0:f3:18:06:51:00:bf:d8:66:39:79:68:7c:2b:8a:
2d:61:14:b5:29:3c:0a:3c:79:ed:4b:87:48:c1:a6:
03:10:53:94:88:9b:a3:58:01

La commande suivante affiche également le détail de la clef privée.

openssl asn1parse -in <chemin de mail.private>

0:d=0 hl=4 l= 605 cons: SEQUENCE
4:d=1 hl=2 l= 1 prim: INTEGER :00
7:d=1 hl=3 l= 129 prim: INTEGER :C6407102D5D392F4E46FC92A7DF76B0B
A45C74AC4920CFF5EB425A59BDDA3AF46AB3ED0AB936633EAA2D704C83C986B914AD94AAEECE8547
51FC862FABF137642373475FAB4D92AEABCA28B4CF0FD3B7A7B98DDD71AABDBAFC88F0CAE9CF2F2F
2E55763A992C2CEB07807C65CF2F79E8DC6E78A3C66B592EA4D6605F47384371
139:d=1 hl=2 l= 3 prim: INTEGER :010001
144:d=1 hl=3 l= 129 prim: INTEGER :969F5F3D4837F6EF189FD5B6F2FD87D6
D0896E1B7773F68C60B188F3A5CAA8000B11A886FD30D53647153EBCE363B977E4BDFCB5E1BA0688
A941B2B185713F22FF24217808BC2243EC87E37A1C6762AB560B8960CF5E59F679C0F318065100BF
D8663979687C2B8A2D6114B5293C0A3C79ED4B8748C1A603105394889BA35801

Le modulus et l’exposant public seront utilisés dans les programmes python ci-dessous pour déchiffrer la signature.

Mécanismes cryptographiques

Prenons comme exemple le message suivant :

J’ai colorisé les champs qui vont être utiles.


# Début du message
From – Fri Aug 22 13:36:26 2014
X-Account-Key: account24
X-UIDL: 0000008153eb5b16
X-Mozilla-Status: 0001
X-Mozilla-Status2: 00000000
X-Mozilla-Keys:
Return-Path: <philippe@cybercod.com>
Delivered-To: <philippe@cybercod.com>
Received: from mail.cybercod.com
by ct-106.debian.local (Dovecot) with LMTP id tJCjFzIr91MeFgAARLzBNg
for <philippe@cybercod.com>; Fri, 22 Aug 2014 13:36:18 +0200
Received: from [192.168.0.9] (poo40-1-78-231-184-55.fbx.proxad.net [78.231.184.55])
by mail.cybercod.com (Postfix) with ESMTPSA id 39FC83826043
for <philippe@cybercod.com>; Fri, 22 Aug 2014 13:36:18 +0200 (CEST)
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=cybercod.com; s=mail;
t=1408707378; bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=;
h=Date:From:To:Subject:From;
b=P3zAiFT2nqXinY15ieBkFCNUgzEM0YtchzbCFFxJ4hIKYaKJg9GzFtdvg4Y2bbD2I
3BILrBtXRuodCICl+PLp4ssqA9uzqHo0qjHGpe+RZ0FwsS+ddA0FZ0xzRaZGoEvWu+
i58kfsYJy5pLQr/ygrGdzm464J4ODBkc/hb2NRr0=

Message-ID: <53F72B2E.2080609@cybercod.com>
Date: Fri, 22 Aug 2014 13:36:14 +0200
From: Philippe <philippe@cybercod.com>
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:31.0) Gecko/20100101 Thunderbird/31.0
MIME-Version: 1.0

To: philippe@cybercod.com
Subject: test

Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: 7bit
0
test

# Fin du message

On extrait la signature DKIM.


DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=cybercod.com; s=mail;
t=1408707378;bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=;
h=Date:From:To:Subject:From;
b=P3zAiFT2nqXinY15ieBkFCNUgzEM0YtchzbCFFxJ4hIKYaKJg9GzFtdvg4Y2bbD2I3BILrBtXRuodCICl+PLp
4ssqA9uzqHo0qjHGpe+RZ0FwsS+ddA0FZ0xzRaZGoEvWu+i58kfsYJy5pLQr/ygrGdzm464J4ODBkc/hb2NRr0=

pour information, le champ « t » contient la date de création de la signature au format epoch unix (nombre de secondes depuis le 01 janvier 1970).
Pour la conversion aller sur le site suivant : epochconverter.com
Dans cette exemple la signature a été créée le : 22/8/2014 13:36:18 GMT+2

Deux champs nous intéressent plus particulièrement.

Le champ bh
Le champ b

Le champ bh contient le hash du corps du message. Le hash est encodé en base64
Vérifions :

Le texte du message est : test
Attention il faut ajouter la fin de ligne (0D et 0A)

Nota : toutes les lignes vides en fin de texte sont supprimées. C’est à dire qu’elles ne sont pas prises en compte pour le calcul du hash.

Dans un éditeur hexadécimal, taper la ligne suivante.

Offset(d) 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
00000000  74 65 73 74 0D 0A                                 test..

Sauvegarder le fichier en le nommant bh.txt

calculons le hash sha256 de bh.txt

Nous obtenons : 837ccb607e312b170fac7383d7ccfd61fa5072793f19a25e75fbacb56539b86b

bh

Attention, il s’ agit d’une string hexa , il faut la convertir en base64

programme python de conversion :

import base64

StringHexa="837ccb607e312b170fac7383d7ccfd61fa5072793f19a25e75fbacb56539b86b"
ChaineBase64guid=base64.encodestring(StringHexa.decode('hex'))
print ChaineBase64

On obtient : g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=

Ce qui correspond bien au champ bh contenu dans la ligne dkim.

On constate donc que la ligne DKIM contient un champ nommé bh qui est tout simplement un hash sha256 (non chiffré) du corps du message

Étudions maintenant le champ b.

Principe de sa génération :

Le champ b va être constitué de la concaténation de certaines lignes du header et de la ligne dkim (avec le champ bh précédemment calculé et le champ b à vide ).

Ensuite on exécute un hash sha256 à partir des lignes concaténées
et on signe avec la clef privée.

Dans la ligne dkim , le champ "h" va nous préciser quelles sont les lignes de l’entête qui vont être concaténées pour calculer le hash sha256
Dans le message "h" est égal à :

h=Date:From:To:Subject:From;

Donc on va concaténer : « Date » || « From » || « To » || « Subject »

(attention : Le champ From n’existant qu’une seule fois dans le message, le deuxième From n’ est pas pris en compte pour le calcul du hash)

Cela donne donc :

Date: Fri, 22 Aug 2014 13:36:14 +0200
From: Philippe <philippe@cybercod.com>
To: philippe@cybercod.com
Subject: test
DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=cybercod.com; s=mail;
t=1408707378; bh=g3zLYH4xKxcPrHOD18z9YfpQcnk/GaJedfustWU5uGs=;
h=Date:From:To:Subject:From;
b= <======= b est vide et pas de retour à la ligne

Normal que b soit vide puisque c’ est justement la valeur que l’on calcule

Affichage dans un éditeur hexadécimal :

Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000 44 61 74 65 3A 20 46 72 69 2C 20 32 32 20 41 75 Date: Fri, 22 Au
00000010 67 20 32 30 31 34 20 31 33 3A 33 36 3A 31 34 20 g 2014 13:36:14
00000020 2B 30 32 30 30 0D 0A 46 72 6F 6D 3A 20 50 68 69 +0200..From: Phi
00000030 6C 69 70 70 65 20 3C 70 68 69 6C 69 70 70 65 40 lippe <philippe@
00000040 63 79 62 65 72 63 6F 64 2E 63 6F 6D 3E 0D 0A 54 cybercod.com>..T
00000050 6F 3A 20 70 68 69 6C 69 70 70 65 40 63 79 62 65 o: philippe@cybe
00000060 72 63 6F 64 2E 63 6F 6D 0D 0A 53 75 62 6A 65 63 rcod.com..Subjec
00000070 74 3A 20 74 65 73 74 0D 0A 44 4B 49 4D 2D 53 69 t: test..DKIM-Si
00000080 67 6E 61 74 75 72 65 3A 20 76 3D 31 3B 20 61 3D gnature: v=1; a=
00000090 72 73 61 2D 73 68 61 32 35 36 3B 20 63 3D 73 69 rsa-sha256; c=si
000000A0 6D 70 6C 65 2F 73 69 6D 70 6C 65 3B 20 64 3D 63 mple/simple; d=c
000000B0 79 62 65 72 63 6F 64 2E 63 6F 6D 3B 20 73 3D 6D ybercod.com; s=m
000000C0 61 69 6C 3B 0D 0A 09 74 3D 31 34 30 38 37 30 37 ail;...t=1408707
000000D0 33 37 38 3B 20 62 68 3D 67 33 7A 4C 59 48 34 78 378; bh=g3zLYH4x
000000E0 4B 78 63 50 72 48 4F 44 31 38 7A 39 59 66 70 51 KxcPrHOD18z9YfpQ
000000F0 63 6E 6B 2F 47 61 4A 65 64 66 75 73 74 57 55 35 cnk/GaJedfustWU5
00000100 75 47 73 3D 3B 0D 0A 09 68 3D 44 61 74 65 3A 46 uGs=;...h=Date:F
00000110 72 6F 6D 3A 54 6F 3A 53 75 62 6A 65 63 74 3A 46 rom:To:Subject:F
00000120 72 6F 6D 3B 0D 0A 09 62 3D                      rom;...b=

Sauvegarder le fichier en le nommant b.txt

Un sha256 des lignes ci-dessus donne
58632e538ba175cce1cdeda787a57ff16755e7026b1454dc3b4951072538dd08

b

Plutôt que de vouloir signer le hash obtenu, nous allons à l’ inverse partir de la signature
jusqu’à obtenir le hash.

La signature est encodée en base 64 , décodons la en hexstring.

P3zAiFT2nqXinY15ieBkFCN............r/ygrGdzm464J4ODBkc/hb2NRr0=

Programme en python conversion base64 vers hexstring.

import base64
 
ChaineBase64= \
"P3zAiFT2nqXinY15ieBkFCNUgzEM0YtchzbCFFxJ4hIKYaKJg9GzFtdvg4Y2bbD2I" + \
"3BILrBtXRuodCICl+PLp4ssqA9uzqHo0qjHGpe+RZ0FwsS+ddA0FZ0xzRaZGoEvWu" + \
"+i58kfsYJy5pLQr/ygrGdzm464J4ODBkc/hb2NRr0="
StringHexa=base64.decodestring(ChaineBase64).encode('hex')
print StringHexa

On obtient la chaine suivante. Je l’ai découpée en groupe de 32 octets sur 4 lignes.

Signature :
3f7cc08854f69ea5e29d8d7989e06414235483310cd18b5c8736c2145c49e212
0a61a28983d1b316d76f8386366db0f62370482eb06d5d1ba874220297e3cba7
8b2ca80f6ecea1e8d2a8c71a97be459d05c2c4be75d034159d31cd16991a812f
5aefa2e7c91fb18272e692d0affca0ac67739b8eb827838306473f85bd8d46bd

Déchiffrement de la signature :

Programme en python de déchiffrement de la signature

# n=modulus
n= int( \
"C6407102D5D392F4E46FC92A7DF76B0BA45C74AC4920CFF5EB425A59BDDA3AF4" + \
"6AB3ED0AB936633EAA2D704C83C986B914AD94AAEECE854751FC862FABF13764" + \
"2373475FAB4D92AEABCA28B4CF0FD3B7A7B98DDD71AABDBAFC88F0CAE9CF2F2F" + \
"2E55763A992C2CEB07807C65CF2F79E8DC6E78A3C66B592EA4D6605F47384371" \
,16)
# exposant public
e = 0x10001
# signature
signature= int( \
"3f7cc08854f69ea5e29d8d7989e06414235483310cd18b5c8736c2145c49e212" + \
"0a61a28983d1b316d76f8386366db0f62370482eb06d5d1ba874220297e3cba7" + \
"8b2ca80f6ecea1e8d2a8c71a97be459d05c2c4be75d034159d31cd16991a812f" + \
"5aefa2e7c91fb18272e692d0affca0ac67739b8eb827838306473f85bd8d46bd" \
,16)
# fonction pow : (signature ^ e) mod n
hash=pow(signature,e,n)
print hex(hash)

On obtient la chaine suivante

0001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffff003031300d060960864801650304020105000420
58632e538ba175cce1cdeda787a57ff16755e7026b1454dc3b4951072538dd08

ajouter au début de la chaine un octets à 0 (0x00) pour être conforme à la rfc 3447.
Affichage sous forme hexa


00000000 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 00 01 FF FF FF FF FF FF FF FF FF FF FF FF FF FF ..ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
00000010 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
00000020 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
00000030 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
00000040 FF FF FF FF FF FF FF FF FF FF FF FF 00 30 31 30 ÿÿÿÿÿÿÿÿÿÿÿÿ.010
00000050 0D 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 ...`†H.e........
00000060 0D 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 Xc.S‹¡uÌáÍí§‡¥.ñ
00000070 0D 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 gUç.k.TÜ;IQ.%8Ý.

La RFC 3447 chapitre 9.2 EMSA-PKCS1-v1_5 décompose la chaine selon le format suivant :

EM= 0x00 || 0x01 || PS ||0x00 || T

PS : (Padding String) composé d’une série d’octets à 0xff
T : restant de la chaine

Pour information, voici la formule à appliquer pour connaitre la longueur du padding.
Len(EM) – Len(T) – 3 = 128 – 51 – 3 = 74 (longueur du padding 0xFF)

EM : Nombre d’octets composant le modulus. La taille du modulus est de 1024 bits donc 128 octets.

Le nombre d’octets égal à 0xff sera 74.

Le reste de la chaine T est au format asn.1

On devine le hash à la fin de la a chaine T .
003031300d06096086480165030402010500042058632e538ba175cce1cdeda787a57ff16755e7026b1454dc3b4951072538dd08

Si on parse la chaine dans un éditeur asn.1 on obtient le hash
58632e538ba175cce1cdeda787a57ff16755e7026b1454dc3b4951072538dd08

dkim-asn.1

L’ OID 2.16.840.1.101.3.4.2.1 identifie un hash sha256.
Le hash correspond bien au hash que nous avons calculé en concaténant certaines lignes composant l’entête du message et la ligne dkim (avec "b=" à vide).

Voila , nous avons parcouru les mécanismes cryptographiques mis en œuvre par DKIM.

Annexe
canonicalization (forme canonique)

Définition :
En informatique , le passage à la forme canonique permet de transformer des données dont plusieurs représentations sont possibles vers un format « standard ».

Certains systèmes de mail modifient les mails en transit ce qui potentiellement invalidera la signature.

Le mode « simple » n’autorise qu’une seule modification pendant le transit du mail, l’ajout ou la suppression de fin de ligne dans le corps du message

Si d’autres modifications sont effectuées , la signature ne sera pas correcte

Le mode « relaxed » autorise quelques petits changements pendant le transit du mail.
A l’envoi , les données sont transformées vers un format standard (forme canonique) puis la signature est calculée
A la réception, les données sont de nouveau transformées vers le même format stantard (forme canonique) puis la vérification de la signature est executée

Explication concernant le champ « c » de la ligne dkim

c=([simple ou relaxed]/[relaxed ou simple])

le 1er champ concerne le header (dkim compris)
le 2ème champ concerne le corps du message

Exemple :

  • c=relaxed/simple

Dans cet exemple on utilise

  • relaxed pour le header
  • et simple pour le corps du message

Mode relaxed

entête (header)
Dans l’entête , chaque ligne peut se décomposer en 2 parties
un champ "nom" et un champ "donnée" séparés par ":"
exemple :
Date: Fri, 22 Aug 2014 13:36:14 +0200
Le champ « nom » est : Date
Le champ « donnée » est : Fri, 22 Aug 2014 13:36:14 +0200

Pour chaque champ « nom ».

  • Conversion en minuscules.

Pour chaque champ "donnée"
dans cet ordre

  • supprime tout les retours à la ligne (\r\n) dans le champ donnée
  • remplace tous les caractères [ \t\n\r\f\v] par un espace
    • \t => 0x09
    • \n => 0x0a
    • \r => 0x0d
    • \f => 0x0c
    • \v => 0x0b
  • supprime tous les espaces en début de ligne et en fin de ligne (0x20)
  • rajoute une fin de ligne (\r\n)

Attention le champ DKIM fait partie de l’entête

Expression régulière résultante en python :

  • headers contient l’entête (dictionnaire python).
  • return [(x[0].lower(), re.sub(r"\s+", " ", re.sub("\r\n", "", x[1])).strip()+"\r\n") for x in headers]

Corps (body)

  • Supprime les séries d’espaces (espace et tabulation) en fin de lignes
  • Remplace les séries d’espaces (espace et tabulation) par un seul espace dans une ligne à l’ exception des fins de lignes
  • Ignore toutes les lignes vides à la fin du corps du message.

Expression régulière résultante en python :

  • body : contient le corps du message (chaine de caractères)
  • return re.sub("(\r\n)*$", "\r\n", re.sub(r"[\x09\x20]+", " ", re.sub("[\x09\x20]+\r\n", "\r\n", body)))

Mode simple

Entête (header) du message

  • Pas de changement au niveau du header.

Corps (body) du message

  • Ignore toutes les lignes vides à la fin du corps du message.

Expression régulière résultante en python

  • body : contient le corps du message (chaine de caractères)
  • return re.sub("(\r\n)*$", "\r\n", body)
 Publié par à 16 h 31 min

 Laisser un commentaire

Vous pouvez utiliser ces balises et attributs HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(requis)

(requis)