Bonjour à tous !
J’ai le plaisir d’être l’instigateur du premier Défi de Clem, qui abordera les rivages de la sécurité informatique. Mais avant toute chose, un peu de contexte.
Fonctions de hachage cryptographiques
Vous connaissez tous MD5 et SHA-1, je suppose ? On s’en sert massivement pour stocker des mots de passe dans une base de données sous une forme cryptée, ainsi que dans le domaine de la signature numérique. Toutes deux sont ce qu’on appelle des fonctions de hachage cryptographiques. Pour reprendre le lien donné à l’instant, elles ont les caractéristiques suivantes.
- À partir d’une entrée de n’importe quelle taille, elles produisent un résultat de taille fixe, souvent 128, 160 ou 256 bits.
- Il est strictement impossible de savoir quelle entrée a produit un résultat donné : contrairement à RSA, il n’y a pas de porte dérobée, la fonction est vraiment à sens unique.
- Deux entrées extrêmement similaires, même si elles ne diffèrent que d’un bit, donneront des résultats radicalement différents. Par corollaire, il est très difficile de produire volontairement un message qui donnera le même résultat qu’un autre message donné, ce que l’on appelle une collision.
Le résultat d’une fonction de hachage est appelé un condensat.
Bien évidemment, comme tout produit de la cryptographie, certains s’attachent à casser ces fonctions de hachage, de manière à pouvoir flouer les systèmes qui l’utilisent pour assurer leur sécurité. L’exemple typique est celui de l’authentification par un site Web : pas besoin de connaître le mot de passe de l’administrateur si l’on peut générer un mot de passe qui a le même condensat que le vrai.
Et justement, cela fait maintenant deux ans que MD5 a cédé aux assauts des cryptanalystes. Donc si par hasard certains d’entre vous l’utilisent encore, c’est mal ! SHA-1, qui a l’âge de ses artères, commence lui aussi à montrer des signes de faiblesse. Pas de souci, vous répondra-t-on : il suffit d’utiliser SHA-2, la nouvelle génération, plus robuste, plus sécurisée.
Mais au final, ce n’est qu’une fuite en avant. En effet, SHA-2 est un renforcement de SHA-1, qui est un renforcement de MD5, qui était lui-même un renforcement de MD4, qui lui-même venait renforcer MD2. Parce que tous ces algorithmes, ainsi que pas mal d’autres fonctions de hachage, utilisent la construction de Merkle-Darmgård, une sorte de patron de conception commun. Et à l’intérieur même de ce moule, les techniques utilisées pour mélanger les données sont à peu près les mêmes d’une génération à l’autre. Et forcément, depuis 30 ou 40 ans que les principales fonctions de hachage massivement utilisées sont toutes construites sur le même modèle, on sait fort bien comment les attaquer, dans les grandes lignes.
SHA-3
C’est pourquoi le NIST, l’organisme américain en charge des algorithmes SHA, a décidé de prendre les devants. Ils ont organisé un concours pour sélectionner une troisième génération de SHA bien avant que la deuxième ne soit sérieusement menacée, et le candidat retenu utilise une architecture nettement différente de la construction de Merkle-Darmgård. Je reviendrai dans un instant sur ce grand gagnant.
En effet, cela fait maintenant 3 ans que les résultats du concours ont été annoncés, mais SHA-3 reste utilisé de manière très minime. Il en existe peu d’implémentations dans l’un ou l’autre langage, et elles reposent souvent sur un appel indirect à l’implémentation de référence en C. Or, le 5 août 2015, après un trèèès long processus de validation officielle, le NIST a publié la version définitive du standard SHA-3. Et c’est là l’objectif de notre défi.
Implémenter l’algorithme SHA-3 dans le plus de langages différents possible.
Alors intéressons-nous d’un peu plus près à cet algorithme. À l’origine de SHA-3, il y a la famille de primitives cryptographiques Keccak (à prononcer ké-tchak). Famille, parce qu’il s’agit en réalité d’un modèle de construction permettant de générer des dizaines de fonctions de hachage similaires, en fonction de trois paramètres :
- la taille de l’état interne de la fonction ;
- la vitesse de production du condensat ;
- la longueur du condensat voulu.
SHA-3 est en réalité composé de 4 variantes de Keccak, qui partagent une même taille d’état interne, mais de longueurs de condensat différentes, la vitesse de production de celui-ci étant calibrée pour être proportionnelle à sa longueur. Il me faut donc commencer par vous expliquer en détail le fonctionnement de Keccak. Suivez bien !
Pour les curieux, voici le site officiel de Keccak. Vous y trouverez la norme complète, ainsi que l’implémentation de référence en C. Attention, cependant : l’une et l’autre sont assez ardues à comprendre, et il est sans doute préférable que vous vous contentiez de mes explications simplifiées dans un premier temps.
Keccak
La fonction de permutation
Au cœur de Keccak se trouve la fonction de permutation, qui va complètement secouer les données qui lui sont fournies en entrée, de manière à les rendre méconnaissables. Celle-ci dépend d’un seul paramètre, appelé $l$, et qui peut prendre une valeur entière comprise entre $0$ et $6$. À partir de celle-ci, on définit la longueur $w = 2^l$, puis la taille de l’état interne dont nous avons parlé, qui vaut $b = 25w$.
La fonction de permutation prend en entrée un tableau de $5 \times 5$ cases de $w$ bits de longueur (attention, ce sont bien des bits et non des octets !) et renvoie un tableau du même format. Entre les deux, elle va faire passer ce tableau successivement par 4 fonctions chargées de le mélanger, et répéter l’opération un certain nombre de fois, nombre qui vaut $12 + 2l$. Voici comment ces 4 fonctions sont définies en pseudo code.
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Fonction 1 (appelée θ dans la publi originelle) C[x] = A[x,0] xor A[x,1] xor A[x,2] xor A[x,3] xor A[x,4], forall x in 0…4 D[x] = C[x-1] xor rotl(C[x+1],1), forall x in 0…4 A[x,y] = A[x,y] xor D[x], forall (x,y) in (0…4,0…4) // Fonction 2 (ρ et π combinées) B[y,2*x+3*y] = rotl(A[x,y], r[x,y]), forall (x,y) in (0…4,0…4) // Fonction 3 (χ) A[x,y] = B[x,y] xor ((not B[x+1,y]) and B[x+2,y]), forall (x,y) in (0…4,0…4) // Fonction 4 (ι) A[0,0] = A[0,0] xor RC |
Le A[x, y]
de la fonction 1 est le tableau donné en entrée, B
, C
et D
étant des variables intermédiaires. Les indices sont à comprendre modulo $5$. Par exemple, dans la fonction 2, si $x = 3$ et $y = 4$, alors $2x+3y = 18 mod 5 = 3$. La fonction rotl(mot, i)
est la fonction « rotation de i bits vers la gauche ». Le RC de la fonction 4 est une constante qui dépend de la longueur des mots et d’à la combientième itération des 4 fonctions on en est. Voici le tableau de la valeur des différents RC quand $w$ vaut $64$ ($l$ vaut $6$, et il y a donc $24$ itérations).
Itération | Valeur | Itération | Valeur |
---|---|---|---|
RC[ 0] | 0x0000000000000001 | RC[12] | 0x000000008000808B |
RC[ 1] | 0x0000000000008082 | RC[13] | 0x800000000000008B |
RC[ 2] | 0x800000000000808A | RC[14] | 0x8000000000008089 |
RC[ 3] | 0x8000000080008000 | RC[15] | 0x8000000000008003 |
RC[ 4] | 0x000000000000808B | RC[16] | 0x8000000000008002 |
RC[ 5] | 0x0000000080000001 | RC[17] | 0x8000000000000080 |
RC[ 6] | 0x8000000080008081 | RC[18] | 0x000000000000800A |
RC[ 7] | 0x8000000000008009 | RC[19] | 0x800000008000000A |
RC[ 8] | 0x000000000000008A | RC[20] | 0x8000000080008081 |
RC[ 9] | 0x0000000000000088 | RC[21] | 0x8000000000008080 |
RC[10] | 0x0000000080008009 | RC[22] | 0x0000000080000001 |
RC[11] | 0x000000008000000A | RC[23] | 0x8000000080008008 |
Pour un $w$ plus petit, il faut naturellement tronquer la partie gauche, et prendre uniquement les premiers RC de la liste, en fonction du nombre d’itérations. Quant au r[x,y]
de la fonction 2, il s’agit là aussi d’une valeur définie en dur par la référence, et que voici (les valeurs sont bien sûr modulo $w$).
. | x = 3 | x = 4 | x = 0 | x = 1 | x = 2 |
---|---|---|---|---|---|
y = 2 | 25 | 39 | 3 | 10 | 43 |
y = 1 | 55 | 20 | 36 | 44 | 6 |
y = 0 | 28 | 27 | 0 | 1 | 62 |
y = 4 | 56 | 14 | 18 | 2 | 61 |
y = 3 | 21 | 8 | 41 | 45 | 15 |
Et voilà tout pour la fonction de permutation. Dans la version utilisée pour SHA-3, $l$ vaut toujours $6$ : les mots font alors 64 bits de longueur, ce qui peut permettre des optimisations sur les processeurs 64 bits.
La fonction d’éponge
Nous entrons là dans le vif du sujet ! C’est cette fonction qui traite les données dont on cherche à obtenir le condensat et qui les incorpore petit à petit dans la pâte. Un nouveau paramètre entre en jeu, et c’est la vitesse de production du condensat : elle est notée $r$ (pour rate), prend n’importe quelle valeur entière inférieure à $b$ la taille de l’état interne (cf. plus haut), et on définit également la « capacité » $c = b - r$ (uniquement pour des raisons pratiques, cf. plus bas). Dans l’idéal, $r$ est un multiple de $w$, mais ce n’est pas obligatoire.
La fonction d’éponge va passer elle aussi par un certain nombre d’itérations, nombre qui dépend de $r$ et de la longueur des données fournies en entrée. En effet, la première étape consiste à découper l’entrée en tronçons de $r$ bits de longueur (que l’on complétera ensuite si besoin avec des 0 à la fin, pour atteindre une longueur de $25w$ bits).
Pour cela, il faut s’assurer que la longueur de l’entrée (en bits) est bien un multiple de $r$, ce qui se fait au moyen du bourrage ou padding. Il existe deux façons différentes de bourrer l’entrée.
- Dans la spécification de départ de Keccak, on concatène à la fin de l’entrée un bit qui vaut $1$, puis autant de $0$ que nécessaire, avant d’ajouter un dernier $1$ qui amène à une longueur multiple de $r$.
- Dans la spécification de SHA-3, on commence par y concaténer les deux bits $01$, avant de faire comme dans la spécification de Keccak.
Chacun des tronçons de longueur $25w$ bits est ensuite lui-même découpé en mots de longueur $w$ bits. À chaque tronçon est associée une itération de la fonction d’éponge, constituée des étapes suivantes (avec A[x, y]
qui constitue le résultat de l’itération précédente, sous forme d’un tableau de $5 \times 5$ mots de longueur $w$, comme vous vous en doutez).
- Le premier mot est
xor
é avecA[0, 0]
, le deuxième avecA[1, 0]
, le troisième avecA[2, 0]
, le sixième avecA[0, 1]
, etc. jusqu’à ce qu’on soit arrivé au bout du tableau. - Le tableau
A
, auquel viennent d’être incorporés les mots du tronçon, est passé à la fonction de permutation.
L’état initial est un tableau de la taille adéquate, intégralement composé de $0$.
À ce stade, l’éponge a absorbé toute l’entrée, et il est maintenant temps pour elle de recracher un condensat. C’est là qu’intervient la longueur du condensat voulu. En effet, immédiatement après la fin des itérations d’absorption, et sans étape intermédiaire, on entame une série d’itérations légèrement différentes, définies ainsi.
- On concatène
A[0, 0]
, puisA[1, 0]
, puisA[2, 0]
, etc. (dans le même ordre que plus haut), puis on en prend les $r$ premiers bits, que l’on concatène à la fin du condensat obtenu jusqu’ici (qui est vide au départ, naturellement). C’est là qu’il est intéressant que $r$ soit un multiple de $w$, car on n’a pas à jouer avec des fractions de mot. - Le tableau
A
est passé à la fonction de permutation.
Et ainsi de suite jusqu’à ce que la longueur du condensat dépasse la longueur voulue. À ce moment-là, il suffit de tronquer les bits les plus à la fin du condensat pour le raboter à la bonne longueur.
Le standard SHA-3 définit 4 longueurs de condensat (224, 256, 384 et 512 bits), chacune associée à une valeur de $c$ (à partir de laquelle vous pouvez donc calculer $r$, qui est un multiple de $w$, donc tout est bien pour vous), qui est tout simplement le double de la longueur du condensat voulu. Les choses étant bien faites, une seule itération est suffisante pour recracher l’intégralité du condensat voulu.
Le défi
Voilà, vous avez toutes les cartes en main pour réaliser une implémentation complète dans votre langage préféré ! Vous pouvez choisir d’implémenter tout Keccak et d’y ajouter une surcouche pour SHA-3, ou alors d’implémenter uniquement les cas qui concernent SHA-3 (ce qui peut simplifier les choses, puisque vous n’avez pas à vous occuper des cas où $l < 3$, qui ne sont pas évidents à gérer sur nos ordinateurs qui fonctionnent par octets).
En outre, l’implémentation complète peut se découper en plus petites étapes, permettant à ceux qui n’ont pas beaucoup de temps ou qui ne sont pas très à l’aise de ne réaliser qu’une partie du défi. En voici la liste.
- Le bourrage ou padding.
- La fonction de permutation, qui elle-même incorpore
- la fonction 1 (θ),
- la fonction 2 (ρ et π),
- la fonction 3 (χ),
- la fonction 4 (ι).
- La fonction d’éponge, qu’il est éventuellement possible de diviser en
- absorption de l’entrée,
- extraction du condensat.
- Le condensat obtenu est une suite de bits, ce qui est assez illisible, et il est d’usage de la convertir en sa représentation hexadécimale.
- Une étape bonus peut être d’essayer d’optimiser votre implémentation, dans les limites de ce que permet votre langage.
Sachez qu’il y a un réel cap de difficulté si vous essayez d’implémenter aussi les cas où $l < 3$, donc n’hésitez pas à vous simplifier la vie en ne les gérant pas. Sachez également que dans la fonction d’éponge, l’état interne est un tableau de 5 sur 5 où les y
sont les lignes et les x
les colonnes, mais que dans la fonction de permutation, il peut être plus pratique de travailler avec un tableau où les x
sont les lignes et les y
les colonnes.
La norme présuppose une machine petit-boutiste !
La norme part du principe que l’on est sur une machine petit-boutiste, ce qui a des conséquences assez sévères sur toutes les opérations bit-à-bit (donc à peu près toute la génératrice Keccak, en fait…).
Pour que vous compreniez bien, cela signifie que les bits sont lus à l’envers dans chaque octet (par exemple, s’il y a 0010 0001
sur la machine, le programme devra l’interpréter comme 1000 0100
. Mais également que les octets au sein d’un mot de plus grande taille sont lus dans l’ordre inverse (par exemple, s’il y a 0x801255aa
sur la machine, le programme devra l’interpréter comme les deux mots de 16 bits 0x1280
et 0xaa55
ou comme le mot de 32 bits 0xaa551280
.
Si vous travaillez avec un langage comme le C qui touche directement à la mémoire, vous n’aurez pas de problème, car ces conversions sont faites automatiquement, mais si vous êtes dans un langage de plus haut niveau, il vous faudra sans doute prêter attention à ce que ce soit correctement géré.
Vous pouvez également faire le choix (si votre langage le permet) d’adopter une représentation gros-boutiste des données, mais il vous faudra alors impérativement prendre garde aux points suivants.
- Le message fourni en entrée doit être interprété comme un espace mémoire contenant des octets petit-boutistes. Il faudra donc « retourner » chaque groupe de 8 bits (ainsi que le dernier groupe de bits si la longueur totale n’est pas un multiple de 8) de l’entrée avant d’appliquer le bourrage.
- Le bourrage décrit (on ajoute un 1, puis des 0, puis à nouveau un 1) doit bien être compris comme tel. C’est si vous adoptez une représentation petit-boutiste que cela s’avère plus compliqué : par exemple, rajouter le 1 tout à la fin revient en fait à rajouter l’octet
0x80
(1000 0000
soit0000 0001
en vrai sur la machine). - Les rotations vers la gauche deviennent des rotations vers la droite.
- Les constantes RC doivent elles aussi être interprétées comme étant petit-boutistes. Cela signifie qu’une fois supprimés les bits de gauche dont vous n’avez pas besoin, il vous faudra « retourner » d’un bloc chaque constante. Par exemple, la constante
0x000000008000808B
est rabotée en0x808B
pour des mots de 16 bits et en0x8B
pour des mots de 8 bits, mais sa représentation est toujours petit-boutiste : en gros-boutiste, elle devient respectivement0xD101
et0xD1
. - Une fois que vous serez arrivé à un condensat sous la forme d’une suite gros-boutiste de bits, il vous faudra « retourner » chaque groupe de 8 bits (ainsi que le dernier groupe de bits si la longueur totale du condensat n’est pas un multiple de 8) avant de transformer le tout en sa représentation hexadécimale.
Enfin, afin de vérifier si votre implémentation fonctionne, voici les résultats attendus si vous fournissez une chaîne d’octets (bytestring) vide aux différentes variantes.
1 2 3 4 5 6 7 8 | Keccak-224("") 0xf71837502ba8e10837bdd8d365adb85591895602fc552b48b7390abd Keccak-256("") 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 Keccak-384("") 0x2c23146a63a29acf99e73b88f8c24eaa7dc60aa771780ccc006afbfa8fe2479b2dd2b21362337441ac12b515911957ff Keccak-512("") 0x0eab42de4c3ceb9235fc91acffe746b29c29a8c366b7c60e4e67c466f36a4304c00fa9caf9d87976ba469bcbe06713b435f091ef2769fb160cdab33d3670680e |
1 2 3 4 5 6 7 8 | SHA3-224("") 0x6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7 SHA3-256("") 0xa7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a SHA3-384("") 0x0c63a75b845e4f7d01107d852e4c2485c51a50aaaa94fc61995e71bbee983a2ac3713831264adb47fb6bd1e058d5f004 SHA3-512("") 0xa69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26 |
Pour ceux qui voudraient pousser un peu plus loin, voici le résultat attendu si vous fournissez la suite de 29 bits 0110 0001 0110 0010 0110 0011 0011 0
aux variantes suivantes de Keccak.
1 2 3 4 5 6 7 8 9 10 11 12 | Keccak (l = 6, r = 1152, longueur du condensat = 224) 0x09edb5114e84f5d9c2e227dbd852666ed615449895384ac671b042c7 Keccak (l = 6, r = 1152, longueur du condensat = 223) 0x09edb5114e84f5d9c2e227dbd852666ed615449895384ac671b042 0b1000111 Keccak (l = 6, r = 1100, longueur du condensat = 224) 0xf4332c585e53a89021577d531cdaa862818752196ff6adb9a3d01873 Keccak (l = 3, r = 200, longueur du condensat = 128) 0x38428c4f002a6e20d576ad6b27942f2c Keccak (l = 3, r = 200, longueur du condensat = 224) 0x38428c4f002a6e20d576ad6b27942f2c4c4b122a4b3c3585a8d7d22c Keccak (l = 2, r = 50, longueur du condensat = 24) 0xa2c154 |
Je rajouterai quelques tests pour les fonctions intermédiaires, afin que vous puissiez en vérifier le bon fonctionnement.
Bon courage, et amusez-vous bien ! Je tiendrai à jour la liste des participations, langage par langage.
Implémentations proposées
C
fromvega (uniquement SHA-3) (version complète)