Les dix principales erreurs commises par les développeurs en cryptographie

Tutoriel sur les erreurs à éviter en cryptographie

Scott Contini

Traducteur

Laurent Rosenfeld

Après avoir effectué des centaines de revues de sécurité de code auprès de sociétés allant de la petite start-up à la grande banque ou à l'opérateur majeur de télécommunications, et après avoir lu des centaines de questions ou de billets sur la sécurité, j'ai établi une liste des dix principaux problèmes de cryptographie que j'ai observés.

Commentez

- Titre : Les dix principales erreurs commises par les développeurs en cryptographie

- Auteur : Scott Contini

- Traducteur : Laurent Rosenfeld

- Parution : 13 août 2017

- Mise à jour : 23 avril 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2017 Scott Contini. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Introduction à la cryptographie, Utiliser et implémenter correctement des primitives cryptographiques avec TMS Cryptography Pack

- Introduction à la cryptographie

Sommaire

- I. Introduction

- II. Clefs codées en dur

- III. Choix incorrect du vecteur d'initialisation

- IV. Utilisation de dictionnaires de codes (ECB)

- V. Utilisation fautive ou inadaptée de primitives cryptographiques pour stocker des mots de passe

- VI. MD5 ne veut pas mourir, et SHA1 doit aussi disparaître

- VII. Les mots de passe ne sont pas des clefs cryptographiques

- VIII. Supposer que le chiffrement assure l'intégrité

- IX. Clefs asymétriques trop petites

- X. Générateurs de nombres aléatoires non sûrs

- XI. La soupe cryptographique

- XII. Pour conclure

- XIII. Remerciements

I - Introduction

La mauvaise cryptographie est malheureusement omniprésente. Les chances d'avoir une mise en œuvre cryptographique correctement faite sont très inférieures à celles d'en avoir une mal faite. De nombreux problèmes sont dus à des API cryptographiques qui ne sont pas sûres par défaut et insuffisamment documentées. Java en est le premier responsable, mais ce n'est pas le seul. Java devrait peut-être prendre des leçons de son principal rival, .Net, sur la façon de construire une API (Interface de Programmation Applicative) plus facile à utiliser et moins susceptible de poser des problèmes de sécurité.

Une autre raison majeure est qu'une analyse manuelle du code par un expert chevronné semble également indispensable pour détecter les éventuels problèmes. Selon mon expérience, les outils courants d'analyse statique ne détectent pas efficacement les problèmes cryptographiques. De même, les tests d'intrusion par boîte noire ne découvrent presque jamais ce genre de problèmes. Mon espoir est que publier la présente liste à l'usage des développeurs et des personnes chargées des revues de code aidera à améliorer la situation de la cryptographie logicielle.

Voici la liste :

- Clefs codées en dur ;

- Choix incorrect de vecteur d'initialisation ;

- Mode d'opération par dictionnaires de codes ;

- Utilisation fautive ou inadaptée de primitives cryptographique pour stocker des mots de passe ;

- MD5 ne veut pas mourir, et SHA1 doit aussi disparaître ;

- Les mots de passe ne sont pas des clefs cryptographiques ;

- Supposition que le chiffrement assure l'intégrité ;

- Clefs asymétriques trop petites ;

- Générateurs de nombres aléatoires non sûrs ;

- La « soupe cryptographique ».

Passons chacun de ces points en revue.

II - Clefs codées en dur

J'ai très souvent vu cela. Avec les clefs codées en dur, quiconque a accès au logiciel connaît les clefs pour déchiffrer les données (et que les développeurs qui se mettent à parler de rendre le code opaque consultent le dernier point de ma liste). Idéalement, nous ne voulons jamais que les clefs cryptographiques soient accessibles (voir par exemple ce qui est arrivé à la société RSA en 2011). Mais beaucoup d'entreprises sont très loin de cet idéal, si bien qu'à défaut, nous devons au moins obtenir que l'accès aux clefs soit restreint à la seule équipe de sécurité des opérations. Les développeurs ne doivent pas avoir accès aux clefs de production, et, en particulier, ces clefs ne doivent pas se retrouver dans des outils de configuration logicielle.

Des clefs codées en dur sont aussi le symptôme d'une réflexion insuffisante sur la gestion des clefs. La gestion des clefs est un sujet complexe qui va bien au-delà de l'objet de ce tutoriel. Je me bornerai à dire que si une clef est compromise, alors le remplacement d'une clef codée en dur nécessite l'installation de nouvelles versions logicielles qui doivent être testées avant d'aller en production. Cela prend du temps, mais ce n'est pas un luxe quand on veut éviter ce genre d'incident.

Il est facile pour des experts en sécurité de dire aux développeurs ce qu'ils ne doivent pas faire, mais la réalité est malheureusement que ce que nous voudrions qu'ils fassent n'est souvent pas possible pour diverses raisons. Les développeurs ont donc besoin de conseils proportionnés.

Une mise en garde importante s'impose ici : je ne suis pas un spécialiste des opérations de sécurité ni un expert de la gestion des clefs. Je peux cependant témoigner de ce que j'ai pu observer depuis un certain temps en certains endroits. Même si c'est loin d'être idéal, il vaut mieux stocker les clefs dans un fichier de configuration que les coder en dur. Bien que certains frameworks autorisent des sections de configuration chiffrées (voir aussi ces conseils concernant .Net), il est vraiment nécessaire que les développeurs aient des clefs de test pour leurs tests et leurs environnements de développement, et que ces clefs soient remplacées par des clefs réelles par l'équipe des opérations de sécurité lors du déploiement dans les environnements de production.

Dans la pratique, j'ai souvent vu ces règles appliquées de façon bâclée. Par exemple, l'équipe chargée du déploiement a installé de façon incorrecte une clef RSA publique et s'est vue incapable de déchiffrer les cryptogrammes parce qu'elle n'avait pas la clef privée correspondant à cette clef publique erronée. Mon conseil est que le logiciel a besoin d'un moyen de s'autotester pour s'assurer qu'il est capable de chiffrer et de déchiffrer (ou de faire quoi que ce soit d'autre) correctement. Ou alors il faut mettre en place, dans le cadre du déploiement, une procédure permettant d'assurer que les choses fonctionnent correctement.

III - Choix incorrect du vecteur d'initialisation

Ce problème a lieu généralement avec les méthodes CBC (Cipher Block Chaining ou enchaînement de blocs) de chiffrement (1). Très souvent c'est un vecteur d'initialisation codé en dur, souvent même par une simple suite de 0. Dans d'autres cas, on utilise de la magie sur un code secret (souvent la clef elle-même) et/ou un salage, mais le résultat final est que c'est le même vecteur d'initialisation qui est utilisé à chaque fois. Le pire que j'aie vu - à trois reprises ! - est d'avoir utilisé la clef elle-même comme vecteur d'initialisation. Voir la section 7.6 du document Crypto 101 pour comprendre pourquoi c'est dangereux.

Quand vous utilisez une méthode CBC, il faut que le vecteur d'initialisation soit choisi aléatoirement et soit imprévisible. En Java, utilisez SecureRandom. En .Net, utilisez simplement GenerateIV. À noter également que vous ne pouvez pas choisir un vecteur d'initialisation de cette façon et réutiliser le même pour un autre chiffrement. Il faut générer un nouveau vecteur d'initialisation pour chaque nouveau chiffrement. Le vecteur d'initialisation n'a pas besoin d'être secret, et il est souvent inclus en clair au début des données chiffrées.

Si vous ne choisissez pas correctement votre vecteur d'initialisation, alors la sécurité est compromise. Un exemple de choix inadapté de vecteur d'initialisation dont les conséquences ont été catastrophiques est SSL/TLS.

Le problème provient souvent de l'API. L'API d'Apple est un exemple parfait de ce qu'il ne faut pas faire (voir ce miroir du code d'Apple pour en trouver la démonstration) : dire au développeur que c'est optionnel et qu'on utilise une suite de 0 si ce n'est pas fourni. Bien sûr, ça va chiffrer quand même, mais le cryptogramme ne sera pas sûr.

IV - Utilisation de dictionnaires de codes (ECB)

Quand vous chiffrez des données avec une méthode de chiffrement par blocs comme AES, vous devez choisir un mode d'opération. Le pire que vous puissiez choisir ou que l'on puisse choisir pour vous est l'utilisation de dictionnaires de codes (ECB, ou electronic code books).

Peu importe la méthode de chiffrement par blocs utilisée sous le capot. Si vous utilisez un mode par dictionnaire de codes, ce n'est pas sûr parce qu'il y a fuite d'information sur les données non chiffrées. En particulier, les messages en doublon donnent des chiffres en doublon. Si vous pensez que ça n'a pas trop d'importance, c'est probablement que vous n'avez pas encore vu le pingouin chiffré. Cette image est sous Copyright Larry Ewing, lewing@isc.tamu.edu, et je suis dans l'obligation de mentionner le logiciel de dessin The Gimp.

De mauvaises API, comme celle de Java, laissent au fournisseur la responsabilité de spécifier le comportement par défaut. Typiquement, le mode ECB est proposé par défaut. Il est regrettable que l'Open Web Application Security Project (OWASP) fasse une telle erreur dans leur exemple de « Bonne pratique : utilisez un algorithme fort », bien qu'il le fasse comme il faut sur cette page, qui est l'un des rares endroits d'Internet où il ne semble pas y avoir de problèmes.

Règle de base minimale : n'utilisez pas le mode ECB. Voir ici des modes d'utilisation qui sont sûrs.

V - Utilisation fautive ou inadaptée de primitives cryptographiques pour stocker des mots de passe

Quand un expert de la cryptographie voit que PBKDF2 est utilisé avec 100 itérations pour stocker un mot de passe, il peut à juste titre se plaindre qu'utiliser 1000 itérations n'est pas suffisant et qu'une fonction comme bcrypt est de toute façon bien meilleure. D'un autre côté, j'ai vu tellement de choses bien pires que je suis ravi de voir le développeur sur la bonne voie.

Le problème est en partie lié à la terminologie, que la communauté cryptographique n'a pas suffisamment essayé de clarifier. Les fonctions de hachage sont des fonctions merveilleuses et magiques. Elles résistent aux collisions, elles résistent aux attaques de préimage et aux attaques de seconde préimage, elles se comportent comme des oracles ou boîtes noires aléatoires et sont à la fois rapides et lentes. Peut-être, et j'insiste sur le peut-être, il est temps de définir des fonctions cryptographiques distinctes pour des utilisations distinctes, plutôt que de dépendre trop d'une seule source de magie.

Pour le traitement des mots de passe, les principales propriétés dont nous avons besoin sont des fonctions lentes et résistantes aux attaques de préimage et de seconde préimage. La raison du besoin de lenteur est très bien expliquée par Troy Hunt.

Il existe des fonctions spécifiques pour satisfaire ces objectifs : pbkdf2, bcrypt, scrypt et argon2. Thomas Pornin a beaucoup fait pour aider les développeurs et les spécialistes de la sécurité à comprendre ceci. Il serait grand temps que nous nous débarrassions de MD5, SHA256 et SHA512 pour le traitement des mots de passe.

Je vois aussi parfois des API qui utilisent PBKDF1, qui ne devrait vraiment plus être utilisé. Comme exemple, on peut citer Microsoft et Java.

En outre, un autre problème que je vois assez communément est l'utilisation du salage codé en dur pour le traitement des mots de passe. L'un des principaux objectifs du salage est que deux mots de passe identiques soient « hachés » vers des valeurs différentes. Si vous utilisez un salage codé en dur, alors vous perdez cette propriété. Dans ce cas, quelqu'un qui obtient un accès à votre base de données peut facilement trouver des cibles faciles en procédant à une analyse fréquentielle des mots de passe hachés. Les efforts de l'attaquant deviennent soudainement mieux focalisés et plus susceptibles de réussir.

Je recommande aux programmeurs de faire ce que propose Thomas Pornin. Il commente régulièrement différents aspects du traitement des mots de passe sur le site https://security.stackexchange.com.

Personnellement, j'opterais pour bcrypt quand c'est possible. Malheureusement, beaucoup de bibliothèques ne vous offrent que PBKDF2. Si vous n'avez pas le choix et devez utiliser PBKDF2, alors faites en sorte d'utiliser au moins 10 000 itérations, et consolez-vous en vous disant que vos mots de passe sont mieux protégés que la plupart.

VI - MD5 ne veut pas mourir, et SHA1 doit aussi disparaître

MD5 est périmé dans la pratique depuis plus de dix ans, et il y avait des mises en garde contre son emploi depuis plus de 20 ans. Pourtant, je trouve encore du MD5 un peu partout. Souvent il est utilisé d'une façon absurde au point qu'on se demande quelles étaient les propriétés de sécurité recherchées.

SHA1 est périmé en théorie depuis presque aussi longtemps que MD5, mais la première vraie attaque n'est venue qu'assez récemment. Google a eu bien raison de le rejeter des années avant la première défaillance pratique, mais SHA1 est encore présent dans le code des développeurs un peu partout.

Dès que je vois le code d'un développeur utiliser une fonction de hachage cryptographique, je m'inquiète. Bien souvent, ils ne savent pas ce qu'ils font. Les fonctions de hachage sont des primitives merveilleuses permettant aux cryptographes de bâtir des outils utiles comme des codes d'authentification de messages, des algorithmes de signature électronique, des générateurs de nombres pseudo-aléatoires et ainsi de suite, mais laisser des développeurs en faire ce qui leur plaît équivaut à donner une mitraillette à un enfant de huit ans. Messieurs les développeurs, êtes-vous sûrs que ces fonctions sont ce dont vous avez besoin ?

VII - Les mots de passe ne sont pas des clefs cryptographiques

Je vois souvent cela : ne pas comprendre la différence entre un mot de passe et une clef cryptographique. Les mots de passe sont des choses dont les utilisateurs se souviennent et qui peuvent avoir une longueur arbitraire. Les clefs en revanche ne se limitent pas à des caractères imprimables et ont une longueur fixe.

Ici, le problème de sécurité est que des clefs doivent avoir une entropie pleine et entière, alors que les mots de passe ont par nature une faible entropie. Parfois, on a besoin de transformer un mot de passe en clef. La façon correcte de le faire est d'utiliser une fonction de dérivation de clef basée sur un mot de passe (pbkdf2, bcrypt, scrypt ou argon2), qui compense la faible entropie de la donnée en entrée en rendant lente la dérivation d'une clef à partir d'un mot de passe. C'est une chose que l'on voit rarement.

Des bibliothèques comme Crypto-js mélangent les concepts de mots de passe et de clefs ; inévitablement, les gens qui s'en servent se demandent pourquoi ils ne peuvent pas chiffrer en JavaScript et déchiffrer en Java, .Net ou tout autre langage ou framework. Pis, cette bibliothèque utilise un algorithme horrible basé sur MD5 pour convertir un mot de passe en clef.

Mon conseil aux développeurs : si vous trouvez une API qui utilise des mots de passe pour chiffrer, évitez-la à moins que vous ne sachiez spécifiquement comment le mot de passe est converti en clef. Il est souhaitable que cette conversion soit faite avec des algorithmes tels que PBKDF2, bcrypt, scrypt ou argon2.

Pour les API qui prennent une clef en entrée, générez les clefs avec un générateur de nombres pseudo-aléatoires de qualité cryptographique, comme SecureRandom.

VIII - Supposer que le chiffrement assure l'intégrité

Le chiffrement cache les données, mais un attaquant peut être en mesure de modifier un texte chiffré, et il se peut que votre logiciel accepte le résultat si vous ne vérifiez pas l'intégrité du message. Un développeur dira : « Mais les données modifiées n'auront aucun sens une fois déchiffrées ». Un bon expert de la sécurité verra les choses autrement et prendra en compte la probabilité que les données mal déchiffrées provoquent un dysfonctionnement du logiciel, et il y verra les risques d'une attaque réelle. J'ai vu beaucoup de cas où l'on utilisait le chiffrement des données, alors qu'assurer l'intégrité du message comptait bien plus que son chiffrement. Comprenez vos besoins.

Il existe des modes d'opération de chiffrement qui garantissent à la fois le secret et l'intégrité du message. Le plus connu est le Galois/Counter Mode (GCM). Mais le mode GCM perd son efficacité si le développeur réutilise un vecteur d'initialisation. Compte tenu de la fréquence du problème de la réutilisation des vecteurs d'initialisation, je ne peux recommander le mode GCM. Les solutions de rechange possibles sont le mode Counter with CBC-MAC de .Net, ou les modes CCMBlockCipher, EAXBlockCipher, OCBBlockCipher, proposés par la bibliothèque BouncyCastle de Java.

Pour assurer uniquement l'intégrité du message, HMAC est un excellent choix. HMAC utilise en interne une fonction de hachage, mais le choix de la fonction de hachage n'est pas particulièrement important. Je recommande d'utiliser une fonction de hachage comme SHA-256 sous le capot, mais, à vrai dire, même HMAC-SHA1 est raisonnablement fiable même si SHA1 n'est pas idéal en termes de résistance aux collisions.

Notons que le chiffrement et HMAC peuvent se combiner pour assurer à la fois la confidentialité et l'intégrité du message, mais à condition que HMAC soit appliqué non pas au texte en clair, mais au texte chiffré avec le vecteur d'initialisation. Je remercie les experts de r/crypto qui ont contribué à corriger des versions antérieures de ce paragraphe.

IX - Clefs asymétriques trop petites

Les développeurs se débrouillent généralement bien dans le choix de leurs clefs symétriques, ils choisissent même souvent des tailles bien plus grandes que nécessaire (128 bits suffisent). Mais ils tombent souvent dans le travers inverse pour la cryptographie asymétrique.

Pour des algorithmes à clefs asymétriques comme RSA, DSA, DH ou autres, une clef de 1024 bits est à la portée d'organismes tels que la NSA, et ne tarderont pas à l'être d'organisations plus petites du fait de la loi de Moore. Une clef de 2048 bits est devenue un minimum de nos jours.

Pour les systèmes fondés sur les courbes elliptiques, on peut utiliser des clefs bien plus petites. Je n'ai pas souvent vu ces algorithmes utilisés par des développeurs, donc j'ai rarement vu des problèmes de clefs avec ce genre d'algorithme.

On trouvera ici des conseils généraux sur la taille des clefs.

X - Générateurs de nombres aléatoires non sûrs

Je suis surpris que ça n'arrive pas plus souvent, mais je rencontre le problème de temps en temps. Le problème général est que le générateur de nombres (pseudo) aléatoires typique peut avoir l'air d'être aléatoire aux yeux d'une personne sans expérience, mais un expert averti voit facilement que ces générateurs de nombres aléatoires ne respectent pas le critère essentiel de non-prévisibilité.

Par exemple, imaginez que vous utilisiez java.util.Random pour générer la clef d'une session d'une application Web. Quand, en tant qu'utilisateur légitime, j'obtiens la clef de ma session, je (en tant qu'expert crypto) peux prédire la prochaine clef qui sera utilisée par le prochain utilisateur, ainsi que celle utilisée par l'utilisateur précédent. Dès lors, je peux pirater leurs sessions.

Ceci ne serait pas possible si la clef avait été générée avec SecureRandom. D'une façon générale, il faut utiliser un générateur de nombres aléatoires offrant la sécurité cryptographique. Par exemple, avec .Net, il faudrait utiliser System.Security.Cryptography.RandomNumberGenerator.

Il faut aussi comprendre que ce n'est pas parce que vous utilisez une bonne source aléatoire que vous ne pouvez pas tout gâcher. Par exemple, j'ai vu une implémentation qui utilisait SecureRandom pour produire un entier de 32 bits, et hachait cet entier pour produire une clef de session. Le développeur ne s'était pas rendu compte que cela impliquait qu'il y avait au maximum 2 ^ 32 sessions possibles, ce qui permet à un attaquant de pirater le tout en énumérant simplement toutes ces valeurs.

XI - La soupe cryptographique

J'emploie le terme « soupe cryptographique » pour désigner un développeur qui mélange des primitives cryptographiques sans objectif clair. Je ne veux pas appeler ça « Mettez en œuvre votre propre crypto », parce que je pense que cette expression désigne l'idée de construire quelque chose dont les objectifs sont clairs, comme le chiffrement par blocs.

La soupe cryptographique utilise souvent des fonctions de hachage, et, dès que vous voyez cela, relisez le dernier paragraphe du chapitre 6 ci-dessus. Quand je vois ce genre de problème, j'ai envie de hurler au développeur : « Ne touche pas à cette fonction de hachage, tu ne comprends pas du tout ce que tu fais. »

Je me souviens d'un exemple de soupe cryptographique dans lequel le développeur utilisait une clef codée en dur. Je lui ai dit que je ne comprenais pas bien ce qu'il cherchait à faire, mais qu'il ne pouvait pas utiliser une clef codée en dur. Cela l'a perturbé, parce qu'il ne savait pas comment faire pour éliminer cette clef codée en dur du code source. Il a fini par m'expliquer qu'il n'avait pas besoin de sécurité pour ce qu'il faisait, mais qu'il cherchait simplement à rendre le code opaque. Voilà exactement le type de conversation que l'on tend à avoir quand on voit de la soupe cryptographique.

XII - Pour conclure

Pour améliorer l'état de la cryptographie dans le code des développeurs, je fais les recommandations suivantes :

- Nous avons besoin d'enseignants, d'éducateurs ! Je veux parler de personnes qui comprennent la cryptographie et aussi le code source des programmes. Je suis très content de trouver des personnes vraiment qualifiées pour cela, mais il n'en reste pas moins que l'on trouve beaucoup de très mauvais conseils un peu partout sur Internet. On a vraiment besoin de plus de bons spécialistes.

- Il faut améliorer les API spécialisées dans la cryptographie. Il faut qu'utiliser correctement une fonction cryptographique soit facile. Une API doit être sûre et fiable par défaut. Et la documentation doit expliquer clairement ce qui se passe. Microsoft va dans la bonne direction, mais pas Java.

- Il faut améliorer les outils d'analyse statique. Il y a, dans les problèmes décrits ci-dessus, des aspects que ce genre d'outils ne peut pas détecter, mais il y en a d'autres qu'ils devraient être en mesure de mettre en évidence. J'ai entendu parler d'un outil nommé Cryptosense, mais je n'ai malheureusement pas encore eu la chance de l'essayer. J'ai beaucoup joué avec les outils produits par les grands noms de la profession, mais j'ai été déçu par le peu de résultats qu'ils obtenaient.

- Les personnes chargées d'effectuer des revues de code doivent rechercher les problèmes cryptographiques manuellement. Franchement, ce n'est pas si difficile. Commencez par faire un grep -Rli crypt (voir ici comment faire une recherche équivalente sous Windows) pour trouver une liste des fichiers contenant le mot « crypt ». Et faites une recherche du même genre pour « MD5 », et ainsi de suite.

- Les spécialistes de la cryptographie doivent s'intéresser davantage aux problèmes de sécurité du monde réel. Si des gens comme Dan Boneh et ses collègues peuvent mener à bien des recherches comme celles-ci, alors d'autres peuvent certainement faire aussi bien. Nous avons besoin de beaucoup plus d'aide pour nettoyer la pagaille cryptographique actuelle.

XIII - Remerciements

Nous remercions Scott Contini qui nous a autorisés à publier son tutoriel.

Nous remercions vivement lolo78 pour la traduction de ce tutoriel en français, ainsi que Laethy pour la relecture technique et Maxy35 pour la correction orthographique.

(1)Les méthodes CBC (cipher block chaining) effectuent un XOR (ou exclusif) entre un bloc de texte à chiffrer et le résultat chiffré du bloc de texte précédent. Le vecteur d'initialisation est utilisé pour le premier bloc (qui n'a par définition pas de bloc précédent). Il s'agit normalement d'un nombre aléatoire. (NDT.)