Hachage universel

algorithme cryptographique

En mathématiques et en informatique, le hachage universel, en anglais universal hashing, (dans un algorithme probabiliste ou un bloc de données) est une méthode qui consiste à sélectionner aléatoirement une fonction de hachage dans une famille de fonctions de hachages qui ont certaines propriétés mathématiques. Cela permet de minimiser la probabilité de collision de hachage. Plusieurs familles de fonctions de hachages sont connues (pour hacher des entiers, des chaînes de caractères ou des vecteurs), et leur calcul est souvent très efficace. Le hachage universel a de nombreux usages en informatique, par exemple dans l’implémentation des tables de hachage, les algorithmes probabilistes et le chiffrement de données.

Introduction

modifier

Supposons que nous désirions associer des clés provenant de l'univers   à des fragments binaires   (indexés  ). L'algorithme devra gérer des ensembles de données   à partir des   clés, qui n'est pas connu à l'avance. Habituellement le but du hachage est de minimiser le nombre de collisions (clés de   qui ont la même image par la fonction). Une fonction de hachage déterministe ne peut pas offrir la garantie contre la création intentionnelle de collision si la taille de   est plus grande que la taille de   par l'adversaire pour choisir   de telle sorte qu'il soit l'antécédent d'un fragment. Cela donne un ensemble de clés qui donnent toutes le même hash, rendant le hachage virtuellement inutile. De plus une fonction de hachage déterministe ne se prête pas au rehachage qui peut être nécessaire si un trop grand nombre de collisions a été constaté.

La solution à ces problèmes est de choisir la fonction de hachage de manière aléatoire à partir d'une famille de fonctions de hachage. Une famille de fonctions   est appelée famille universelle si  .

En d'autres mots, deux clés prises au hasard dans l'univers ont une probabilité au plus de   de donner une collision quand la fonction   est tirée aléatoirement de  . C'est la probabilité à laquelle on s'attendrait si la fonction de hachage attribuait un hash de manière aléatoire à chaque clé. Parfois la définition est élargie pour accepter une probabilité de collision en  . Ce concept a été introduit par Larry Carter et Mark N. Wegman (en)[1] en 1977, et a trouvé de nombreuses applications en informatique[2]). Si on a une borne supérieure   sur la probabilité de collision, on parle de fonctions  -presque universelles.

Beaucoup mais pas toutes les fonctions de hachages universelles vérifient la propriété plus forte de différence uniforme :

 , si   est tiré aléatoirement parmi  , la différence   est uniformément distribuée dans  .

Notez que la définition de l'universalité ne s’intéresse qu'aux cas de collisions ( ) alors que la propriété de différence uniforme est plus forte.

De façon similaire, une famille universelle peut être XOR-universelle si  , la valeur de   est uniformément distribué dans   avec   l'opération ou exclusif bit à bit. Ceci n'est possible que dans le cas où   est une puissance de deux.

Une autre condition plus forte que l'universalité est l'indépendance par paires : Cette propriété est obtenue si   on a la probabilité que   donne une paire de hashs   soit la même que si cette paire de hash était calculée d'une manière parfaitement aléatoire :  . L'indépendance par paire est parfois appelée universalité forte.

Une autre propriété est l'uniformité. On dit qu'une famille est uniforme si tous les hashs possibles sont équiprobables en d'autres termes   pour toute valeur de hash  . L'universalité n'implique pas l'uniformité mais l'universalité forte si.

À partir d'une famille vérifiant la propriété de distance uniforme on peut créer une famille de fonctions de hachage fortement universelle de fonction de hachage, c'est-à-dire une famille de fonction de hachage indépendantes deux à deux en ajoutant à chaque fonction de la famille mère une constante aléatoire uniformément distribuée à valeurs dans  . Si   est une puissance de 2, on peut créer cette famille indépendante par une opération XOR avec une constante aléatoire uniformément distribuée. Étant donné que le décalage d'une constante n'a pas forcément d'importance dans certaines applications (par exemple les tables de hachage), la distinction nette entre la propriété de distance uniforme et l'indépendance paire à paire n'est pas toujours effectuée[3].

Pour certaines applications comme les tables de hachage, il est important que les bits les moins significatifs du hash soient eux aussi universels. Quand une famille est fortement universelle, cette propriété est garantie : si   est une famille fortement universelle avec   alors la famille faite avec les fonctions   pour   est également fortement universelle pour  . Cette propriété n'est pas vraie en général pour les familles universelles. Par exemple la famille faites avec les fonctions identité   est clairement universelle mais la famille faite des fonctions   ne l'est pas.

UMAC, Poly1305 (en)-AES et quelques autres algorithmes de codes d'authentification de message se basent sur le hachage universel[4],[5]. Dans ces applications, le logiciel sélectionne une nouvelle fonction de hachage pour chaque message, basé sur un identifiant unique généré pour le hachage du message.

Plusieurs implémentations de tables de hachage utilisent le hachage universel. Dans ces applications, typiquement, le logiciel ne prend une nouvelle fonction de hachage que s'il note que trop de clés ont produit une collision; sinon la même fonction de hachage reste utilisée. Certains schémas de résolution de collision, comme le hachage dynamique parfait, changent de fonction de hachage à la première collision observée. Les autres méthodes de résolution de collision comme la méthode du coucou et le 2-choice hashing, autorisent un certain nombre de collisions avant de changer de fonction de hachage.

Garanties mathématiques

modifier

Pour n'importe quel ensemble   de   clés, utiliser une famille universelle garantit les propriétés suivantes :

  1. Pour tout   de  , le nombre de clés pour le hash   est  . En implémentant des tables de hachage par un chaînage, ce nombre est proportionnel au temps de calcul pour une opération basée sur une clé (recherche, insertion ou suppression).
  2. Le nombre de paires de clé   de   qui produisent des collisions (  et  ) est majoré par   qui est de l'ordre de  . Dans le cas du hachage de   entrées, il y a une probabilité supérieure à 1/2 de n'avoir aucune collision.
  3. Le nombre de clés qui dans les fragments de au moins   clés est majoré par  [6]. Ainsi la capacité de chaque fragment est limitée à trois fois la taille moyenne ( ), le nombre total de clés dans les fragments qui dépassent la moyenne est au plus  . Cette propriété ne tient que pour une famille de fonctions de hachage où la probabilité de collision est majorée par  . Si une définition plus faible est utilisée et qu'on se contente d'une majoration en  , alors ce résultat n'est plus valable[6].

Les garanties ci-dessus sont valables pour n'importe quel ensemble   donné, elles tiennent même si le jeu de données à hacher a été sélectionné par un attaquant. Cependant, comme l'attaquant crée son jeu de données avant que l'algorithme n'ait fait sa sélection de fonction de hachage aléatoire, si l'attaquant peut deviner les fonctions qui seront sélectionnées par l'algorithme, la sélection aléatoire n'apporte rien par rapport au hachage déterministe.

La deuxième et la troisième garantie sont typiquement utilisées en conjonction avec un rehachage. Par exemple, un algorithme probabiliste peut être sélectionné pour un taux de collision attendu de  . Si trop de collisions sont observées, un autre algorithme   de la famille est sélectionné de manière aléatoire et le traitement est répété. L'universalité garantit que le nombre de répétition est une variable aléatoire géométrique.

Constructions

modifier

Étant donné que les informations sont représentées en machine par des mots, les hachages peuvent être réduit à trois situations : un mot (un entier, integer en anglais) ; un vecteur de mots de taille fixée ; et un vecteur de taille variable assimilé à une chaîne de mots informatiques/caractères (string en anglais).

Hachage d'entiers

modifier

Cette section se réfère au cas du hachage d'entiers qui tiennent dans un mot machine. Dans ce cas les opérations addition, multiplication, et division sont exécutées directement sur le processeur et coûtent très peu de ressources. À partir de ce point on considère l'univers à hacher  .

La proposition initiale de Carter et Wegman[1] était de choisir un entier premier   et définir

 

  est un couple d'entiers choisis aléatoirement modulo   avec  . (Cela correspond à un pas d'itération d'un générateur congruentiel linéaire.)

Pour vérifier que   est bien une famille universelle, notez que   ne tient que si

 

pour un certain entier   compris entre   et  . Si leur différence,  , est non nulle et possède un inverse modulo  . Chercher   revient à résoudre :

 .

Il y a   choix possibles pour   (  est exclu) et pour  pris dans l'intervalle autorisé, il y a   valeurs pour les côtés droits et gauche. La probabilité de collision est alors de

 .

Une autre manière de prouver que   est une famille universelle passe par la notion de distance statistique. On écrit la différence   comme :

 .

Étant donné que   est non nul et que   est uniformément distribué dans  , on déduit que   modulo   est aussi uniformément distribué dans  . La distribution de   est ainsi presque uniforme, avec une différence de probabilité au maximum de   entre les échantillons. Il en résulte que la distance statistique à une famille uniforme est  , qui devient négligeable quand  .

La famille la plus simple de fonctions de hachage,  , est seulement approximativement universelle :   pour tout  [1]. De plus, cette analyse est assez fine ; Carter et Wegman[1] ont montré que   pour tout  .

Éviter le calcul modulaire

modifier

La méthode la plus populaire dans le hachage d'entier est la méthode « multiplie et décale » décrit par Dietzfelbinger et al. en 1997[7]. En évitant l'arithmétique modulaire, cette méthode est plus simple à implémenter et est significativement plus rapide en pratique (usuellement plus rapide d'un facteur 4)[8]).

Cette méthode suppose que le nombre de fragments binaires soit une puissance de deux,  . Soit   le nombre de bits dans un mot machine. Les fonctions de hachage sont paramétrées par des entiers positifs impairs   (qui tiennent dans un mot de   bits). Pour calculer  , on multiplie   par   modulo   et ensuite on garde les   bits de poids fort du résultat comme hash. En notation mathématique cela s'écrit :

 

et peut être implémenté dans un langage de la famille du C par le code :

  (unsigned) (a*x) >> (w-M)

Ce schéma ne satisfait pas la propriété de différence uniforme et est seulement  -presque-universel ; pour tout  ,  .

Pour comprendre le comportement de la fonction de hachage, remarquez que si   et   ont les mêmes 'M' bits de poids fort alors   n'a que des 1 ou que des 0 dans ses 'M' bits de poids fort (cela dépend du maximum entre   et  ). Supposons maintenant que les bits de poids faible de   apparaissent en position  . Comme   est un entier impair et que les entiers impairs possèdent un inverse dans l'anneau  , on déduit que   sera uniformément distribué parmi les entiers de   bits avec le bit de poids le plus faible situé à la position  . La probabilité que ces bits soient tous égaux à 0 ou à 1 est donc au plus de  . D'autre part si  , alors les M bits de poids fort de   contiennent des 0 et des 1 et on est certain que  . Finalement, si   alors le bit   de   vaut 1 et   si et seulement si les bits   valent également 1, ce qui arrive avec la probabilité de  .

Cette analyse est difficile, comme on peut le constater avec les exemples   et  . Pour obtenir une fonction de hachage 'vraiment' universelle, on peut utiliser la méthode multiplie et additionne.

 

qui peut être implémenté en langages de la famille du C par

  (unsigned) (a*x+b) >> (w-M)

  est un entier pair positif tiré aléatoirement,  , et   un entier positif tiré aléatoirement  . Avec ce choix de   et  ,   pour tout  [9]. Ceci diffère légèrement, mais sur un point important, de l'erreur de traduction de l'article en anglais[10].

Hachage de vecteurs

modifier

Cette section concerne le hachage de vecteur de mots dont la longueur est fixée à l'avance. Interpréter l'entrée comme un vecteur   de   mots machine (entiers de   bits chacun). Si   est une famille universelle qui vérifie la propriété de différence uniforme, la famille suivante, trouvée par Carter et Wegman[1], possède également la propriété de différence uniforme (et donc est universelle) :

 , où chaque   est choisi de manière aléatoire et indépendante.

Si   est une puissance de deux, la somme peut être remplacée par une opération ou exclusif[11].

En pratique si l'arithmétique en double précision est disponible, on utilise la famille de fonction de hachage « multiplie et décale »[12]. On initialise la fonction avec un vecteur   d'entiers pairs de   bits de long. Ensuite si le nombre de fragments est  , avec  

 .

Il est possible de diviser le nombre de multiplication par 2, ce qui se traduit par un calcul à peu près deux fois plus rapide en pratique[11]. Initialiser la fonction de hachage avec un vecteur   de nombres entiers impairs chacun de   bits de long. La famille de hachage ainsi produite est universelle[13] :

 .

Si les opérations en double précision ne sont pas disponibles, on peut interpréter l'entrée comme un vecteur de demi-mots (des entiers de   bits de longs). L'algorithme va alors utiliser   multiplications, avec k le nombre de demi mots du vecteur. Ainsi l'algorithme tourne à un "taux" d'une multiplicatio par mot d'entrée.

Ce schéma peut également être utilisé pour hacher des entiers en considérant que leurs bits sont un vecteur d'octets. Dans cette variante, la technique est appelée hachage tabulaire et fournit une alternative pratique aux fonctions de hachage universelle basées sur des multiplications[14].

L'universalité forte à grande vitesse est également possible[15]. Initialiser la fonction de hachage avec un vecteur   d'entiers pris au hasard de   bits de long. Calculer :

 .

Le résultat est fortement universel sur   bits. Expérimentalement cette méthode consomme 0.2 cycle CPU par octet sur les dernières puces Intel pour  .

Hachage de chaînes

modifier

Cette section vise le cas de vecteur de mots de taille variable. Si la taille de la chaîne peut être majorée par un petit nombre, il est préférable d'utiliser la méthode avec des vecteurs de taille définie en complétant la chaîne à hacher par des zéros au besoin. L'espace mémoire requis pour le calcul est la longueur maximale de la chaîne, mais le temps de calcul dépend uniquement de la longueur de  . Tant qu'on interdit les zéros dans la chaîne, on peut ignorer le remplissage d'une chaîne par des zéros terminaux sans que la propriété d'universalité en soit affectée[11]. Noter que si on autorise la présence de zéros dans la chaîne alors le problème d'ajout de zéros terminaux se règle en choisissant d'ajouter un caractère non nul, qui balisera la fin de la chaîne à toutes les chaînes en entrée avant le traitement[15].

Maintenant supposons qu'il faille hacher  , sans qu'on puisse avoir d'estimation correcte de  . Une famille de hachage universelle[12] traite la chaîne   comme les coefficients d'un polynôme modulo un grand nombre premier. Si  , soit   un nombre premier, on définit :

 , où   est uniformément aléatoire et   est tiré aléatoirement d'une famille aléatoire universelle qui indexe l'ensemble des entiers  .

En utilisant les propriétés de l'arithmétique modulaire, la formule ci-dessus peut être calculée sans produire de grands nombres par le code suivant[16] :

uint hash(String x, int a, int p)
	uint h = INITIAL_VALUE
	for (uint i=0 ; i < x.length ; ++i)
		h = ((h*a) + x[i]) mod p
	return h

L'algorithme ci-dessus est également connu en tant que fonction de hachage multiplicative[17]. En pratique l'opérateur modulo et le paramètre p peuvent être évités tous les deux simplement en autorisant les entiers à dépasser leur taille maximale (overflow) car cette opération est équivalente à un modulo MAX_INT_VALUE + 1 dans beaucoup de langages de programmation. Ci-dessous, un tableau de valeurs choisies pour initialiser h dans quelques implémentations populaires.

Implementation INITIAL_VALUE a
Bernstein's hash function djb2[18] 5381 33
STLPort 4.6.2 0 5
La fonction de hachage de Kernighan et Ritchie[19] 0 31
java.lang.String.hashCode()[20] 0 31

Soient deux chaînes   et   la taille de la plus longue des deux ; pour les besoins de l'analyse on supposera que la seconde chaîne est complétée par des zéros jusqu'à atteindre la longueur  . Une collision avant d'appliquer   implique que   est une racine du polynôme dont les coefficients sont  . Ce polynôme a au plus   racines modulo  , donc la probabilité de collision est au plus de  . La probabilité de collision à travers l'aléa   descend la probabilité totale de collision à  . Ainsi si le nombre premier   est suffisamment grand comparé à la longueur des chaînes à hacher, la famille est très proche d'une famille universelle (en termes de distance statistique).

Éviter l'arithmétique modulaire

modifier

Pour limiter la consommation de ressources par les calculs d'arithmétique modulaire, deux astuces sont utilisées en pratique[11] :

  1. On peut choisir le nombre premier   proche d'une puissance de 2, un nombre premier de Mersenne par exemple. Cela permet d'implémenter les opérations de modulo sans utiliser de division (en utilisant des opérations plus rapides comme les additions et les décalages de bits). Par exemple sur des architectures modernes on peut travailler avec  , alors que les   sont stockés sur 32 bits.
  2. On peut appliquer un hachage vectoriel sur des blocs de données de l'entrée. Par exemple, on hache l'entrée par vecteurs de 16 mots et on applique le hachage de chaîne aux   hashs obtenus. Le hachage de chaîne, très lent, étant appliqué à un ensemble de données significativement plus petit le calcul devient presque aussi rapide que le hachage vectoriel.
  3. On peut choisir une puissance de deux comme diviseur ce qui permet de réduire l'opération modulo à un masquage de bits plus rapide que les divisions. La famille de fonctions NH utilise cette méthode.

Notes et références

modifier
(en) Cet article est partiellement ou en totalité issu de l’article de Wikipédia en anglais intitulé « Universal_hashing » (voir la liste des auteurs).
  1. a b c d et e (en) L. Carter et M. N. Wegman, « Universal Classes of Hash Functions », Journal of Computer and System Sciences, vol. 18, no 2,‎ , p. 143-154 (DOI 10.1016/0022-0000(79)90044-8).
  2. Voir par exemple (en) Peter Bro Miltersen, « Universal Hashing » [archive] [PDF], .
  3. (en) Rajeev Motwani et Prabhakar Raghavan, Randomized Algorithms, Cambridge University Press, (ISBN 0-521-47465-5), p. 221.
  4. (en) David Wagner (ed.), Advances in Cryptology - CRYPTO 2008 (lire en ligne), p. 145.
  5. (en) Jean-Philippe Aumasson, Willi Meier, Raphael Phan et Luca Henzen, The Hash Function BLAKE, (lire en ligne), p. 10.
  6. a et b (en) Ilya Baran, Erik D. Demaine et Mihai Pătraşcu, « Subquadratic Algorithms for 3SUM », Algorithmica, vol. 50, no 4,‎ , p. 584-596 (DOI 10.1007/s00453-007-9036-3, lire en ligne).
  7. (en) Martin Dietzfelbinger, Torben Hagerup, Jyrki Katajainen et Martti Penttonen, « A Reliable Randomized Algorithm for the Closest-Pair Problem », Journal of Algorithms, vol. 25, no 1,‎ , p. 19-51 (DOI 10.1006/jagm.1997.0873, lire en ligne [Postscript], consulté le ).
  8. (en) Mikkel Thorup (en), « Text-book algorithms at SODA ».
  9. (de) Philipp Woelfel, Über die Komplexität der Multiplikation in eingeschränkten Branchingprogrammodellen, Universität Dortmund, , PDF (lire en ligne).
  10. (en) Philipp Woelfel, « Efficient Strongly Universal and Optimally Universal Hashing », dans Mathematical Foundations of Computer Science 1999, coll. « LNCS » (no 1672), , PDF (DOI 10.1007/3-540-48340-3_24, lire en ligne), p. 262-272.
  11. a b c et d (en) Mikkel Thorup, « String hashing for linear probing », dans Proc. 20th ACM-SIAM Symposium on Discrete Algorithms (SODA), (DOI 10.1137/1.9781611973068.72, lire en ligne), p. 655-664, section 5.3.
  12. a et b (en) Martin Dietzfelbinger, Joseph Gil, Yossi Matias et Nicholas Pippenger, « Polynomial Hash Functions Are Reliable (Extended Abstract) », dans Proc. 19th International Colloquium on Automata, Languages and Programming (ICALP), , p. 235-246.
  13. (en) J. Black, S. Halevi, H. Krawczyk et T. Krovetz, « UMAC: Fast and Secure Message Authentication », dans Advances in Cryptology (CRYPTO '99), (lire en ligne), Equation 1.
  14. (en) Mihai Pătrașcu et Mikkel Thorup, « The power of simple tabulation hashing », dans Proceedings of the 43rd annual ACM Symposium on Theory of Computing (STOC '11), (DOI 10.1145/1993636.1993638, arXiv 1011.5200), p. 1-10.
  15. a et b (en) Owen Kaser et Daniel Lemire, « Strongly universal string hashing is fast », Computer Journal,‎ (DOI 10.1093/comjnl/bxt070, arXiv 1202.4961, lire en ligne).
  16. (en) « Hebrew University Course Slides ».
  17. (en) Peter Kankowsk, « Hash functions: An empirical comparison ».
  18. (en) Ozan Yigit, « String hash functions ».
  19. (en) B. Kernighan et D. Ritchie, The C Programming Language, , 2e éd., 118 p. (ISBN 0-13-110362-8), chap. 6.
  20. (en) « String (Java Platform SE 6) », sur docs.oracle.com.

Voir aussi

modifier

Articles connexes

modifier

Bibliographie

modifier

(en) Donald E. Knuth, The Art of Computer Programming, vol. 3 : Sorting and Searching, , 2e éd. [détail de l’édition]

Liens externes

modifier

(en) Open Data Structures - Section 5.1.1 - Multiplicative Hashing