SHA-1, Hachage et Sécurité

Google : “We have broken SHA-1 in practice.”

L’annonce a fait l’effet d’une bombe. Google explique qu’avec l’aide de l’institut de recherche CWI aux Pays Bas (celui-là même qui a vu la naissance de Python), ils ont généré deux fichiers PDF contenant une image JPG légèrement différente et qui produisent la même clé SHA-1. L’utilisation de SHA-1 était déjà dépréciée depuis 2011. Mais la démonstration de Google a fini par convaincre d’autant plus les éditeurs de logiciels.

Dans cet article, nous vous proposons de revenir sur ce qu’est une fonction de hachage, avec quelques exemples. Nous verrons ensuite quelques limitations, puis une implémentation en Java, avant de conclure.

Fonction de hachage

SHA-1 est une fonction de hachage. Rappelons ici ce à quoi ça correspond.

Une fonction de hachage est une fonction qui prend en entrée des données (ou messages) de taille quelconque et produit une “empreinte” ou un “hash”. En règle générale, l’ensemble en sortie de la fonction de hachage est plus petit que celui en entrée et de taille généralement fixe. Par exemple, la fonction SHA-256 ne produit que des valeurs sur 256 bits quelle que soit la taille du message.

Les propriétés d’une fonction de hachage sont les suivantes :

  • Déterministe : la même entrée fournit toujours le même hash.
  • Uniformité : toutes les valeurs de hash ont (à peu près) la même probabilité d’être générées.

Dans le cadre de la cryptographie, nous devons avoir les propriétés suivantes :

  • Rapidité : la valeur de hash se calcule “très rapidement”.
  • Non-inversable : la valeur de hash ne permet de reconstruire le message en entrée que très difficilement (ie. impossible en l’état actuel).
  • Non-continuité : un changement léger dans le message en entrée de la fonction de hachage doit produire une sortie complètement différente.

En particulier, en cryptographie, les algorithmes de hachage doivent montrer certaines résistances :

  • Résistance à la préimage : pour une valeur h de hash donnée, il est très difficile de trouver un message m tel que h = hash(m).
  • Résistance à la seconde préimage : pour un message m donné, il est très difficile de trouver un message m’ tel que hash(m) = hash(m’).
  • Résistance à la collision : il est très difficile que deux messages différents produisent la même sortie.

Les fonctions de hachage ont une utilité variée. Dans sa forme la plus simple, ces fonctions permettent de calculer un index sur une donnée, afin d’enregistrer une valeur dans une table de hachage. La fonction de hachage est utilisée par certains SCM (Source Control Manager) comme Git pour indexer les modifications. On peut s’en servir pour générer des séquences de bits pseudo-aléatoires. Dans le domaine de la sécurité, on se sert des fonctions de hachage pour vérifier l’intégrité d’un fichier, vérifier un mot de passe ou sceller un ensemble de transactions dans le cadre de blockchain.

Quelques algorithmes connus

Voici les algorithmes les plus souvent utilisés.

MD5

MD5 calcule un hash sur 128 bits. Il est utilisé dans le calcul d’empreinte de fichier à transférer. Du fait de sa faiblesse reconnue depuis 1996, MD5 disparaît de plus en plus du champ de vision.

SHA-1

SHA-1 calcule un hash sur 160 bits. Il est utilisé notamment dans le cadre de protocoles Internet et applications sécurisés comme TLS et SSL, PGP ou SSH. Il apparaît dans le cadre de Git et Mercurial pour identifier les commits.

Mais la force de SHA-1 est petit à petit mise à mal depuis 2005. En 2011, le NIST (National Institute of Standards and Technology) le déprécie. Récemment, des chercheurs de Google et du CWI ont réussi à produire une collision SHA-1 à partir de deux PDF différents (article). Si le temps de calcul nécessaire reste considérable (110 GPU sur un an !), il est quand même 100 000 fois inférieur à une attaque force brute grâce à l’exploitation de failles dans l’algorithme.

Si la plupart des applications utilisant SHA-1 ont proposé une mise à jour assez rapidement avec un algorithme plus sûr, Linus Torvalds indique dans un message  qu’il prévoit de faire de même avec Git. Toutefois, celui-ci indique qu’il le fera sans empressement, l’utilisation de SHA-1 dans Git étant liée plus à une problématique d’indexation que de sécurité.

SHA-256 et SHA-512

SHA-256 et SHA-512 sont des algorithmes de type SHA-2 qui calcule un hash de 256 bits et 512 bits respectivement. SHA-256 est conseillé en premier lieu dans le domaine de la cryptographie. Néanmoins, SHA-512 est plus performant sur un processeur 64 bits.

Autres

Il existe d’autres algorithmes de type SHA-2 avec des longueurs de clé plus importantes. Il y a aussi les algorithmes de type SHA-3, qui doivent être vus plus comme une alternative que comme une évolution à SHA-2. On peut enfin citer BLAKE2, dont certaines versions s’adaptent mieux à un environnement multi-processeurs.

SHA-3 et BLAKE2 ne sont pas proposés par le JDK, ce qui n’est pas le cas de SHA-2 (et donc de SHA-256), dont les algorithmes sont disponibles depuis Java 8 (une raison de plus d’abandonner les versions antérieures de Java !).

Limitation

Projection

Toute fonction de hachage produisant une clé de taille limitée est une projection. C’est-à-dire que la clé est produite en supprimant de l’information du message initial. Imaginez l’ensemble des clés de 512 bits que nous pouvons produire. À côté de ça imaginez l’ensemble des fichiers de 1 Ko (soit ~8000 bits) qu’il est possible de générer (par exemple avec une armée de singes ^^). Dans l’absolu, une collision est donc inévitable.

Cependant, tous les types de fichiers 1 Ko n’ont pas d’intérêt, n’ont pas forcément de sens. En général, nous ne nous intéressons qu’à un sous-ensemble de ces fichiers. Il faut donc que la fonction de hachage puisse couvrir sans collision ce sous-ensemble. Le type d’algorithme a donc une importance, de même que la taille de la clé produite.

Retrouver un hash grâce à Internet !?

Il faut savoir que dans le cadre de contenu de petite taille, il est facile d’inverser n’importe quelle fonction de hachage aussi sécurisée soit-elle, par une simple recherche sur le Web.

Il existe en effet des sites Web qui ont déjà pré-calculé des hash (MD5 ou SHA-1) de petits messages et qui proposent des tables de correspondance permettant de retrouver un message à partir de sa clé. De ce fait, une attaque préimage est donc simple pour des messages de petite taille, en particulier pour des mots de passe.

sha1("Hello World!") // => 2ef7bde608ce5404e97d5f042f95f89f1c232871

Une recherche de 2ef7bde608ce5404e97d5f042f95f89f1c232871 sur Google m’amène à ce site avec la réponse.

Hachez salé !

Pour se prémunir contre une attaque préimage, il convient d’appliquer un sel, c’est-à-dire une chaîne aléatoire qui sera utilisée dans le processus de hachage. Il faudra dans le cas de mots de passe, conserver le sel.

Néanmoins, comme l’explique la société Sophos dans son article “Serious Security: How to store your users’ passwords safely”, le stockage de mots de passe est un problème bien plus complexe ! Pour une solution plus robuste en Java, nous vous orientons vers cet article de HowToDoInJava “Generate Secure Password Hash : MD5, SHA, PBKDF2, BCrypt Examples”, en remplaçant PBKDF2WithHmacSHA1 par PBKDF2WithHmacSHA256 ou PBKDF2WithHmacSHA512, disponibles en Java 8.

Hacher en Java

Java 8 propose les algorithmes suivants :

  • MD2 (pour processeur 8 bits, peu sûr),
  • MD5 (peu sûr),
  • SHA-1 (algorithme SHA par défaut, peu sûr),
  • SHA-224, SHA-256, SHA-384, SHA-512.

Pour récupérer cette liste, il suffit d’exécuter le code suivant :

import java.security.MessageDigest; 
import java.security.Provider; 
import java.security.Security; 
import java.util.Set; 
publicclassHashAlgorithms { 
     publicstaticvoid main(String[] args) { 
         Stringtype= MessageDigest.class.getSimpleName(); 
         Provider[] providers = Security.getProviders(); 
         for (Provider provider : providers) { 
            System.out.println("provider:"+ provider); 
            Set<Provider.Service> services = provider.getServices(); 
            for (Provider.Service service : services) { 
                if (service.getType().equalsIgnoreCase(type)) 
                    System.out.println(" - service: "+ service); 
             } 
          } 
        } 
    }

En Java, pour utiliser SHA-256 (sans sel) :

import java.security.MessageDigest; 
public byte[] hash(Stringmessage) { 
    MessageDigest digest = MessageDigest.getInstance("SHA-256"); 
    return digest.digest(message.getBytes()); 
}

hash("ceci est un test") donne 41173df05303d5e0aa01f6f722139ff3d2c0810745a7736c483cb9df2f11877d.

Conclusion

Nous avons vu ce qu’est une fonction de hachage avec quelques exemples. Nous avons mentionné quelques limitations de l’utilisation de ce type de fonction, en particulier dans le cadre de mots de passe.

Nous ne pouvons que vous conseiller de migrer vos applications utilisant un algorithme peu robuste (MD5 ou SHA-1) vers une implémentation utilisant un algorithme plus résistant, comme SHA-256 ou SHA-512 par exemple.

Néanmoins, la sécurité reste un problème très complexe. Ce domaine est loin de pouvoir se résumer dans le cadre d’articles. Trouver une solution adaptée à vos besoins et à votre contexte ne peut se faire hors du cadre d’une expertise en sécurité.