Fork me on GitHub

Pas de bébé et pas de géant

Le but de ce DM est d’implanter un algorithme pour la résolution du problème du logarithme discret, problème sous-jacent à la sécurité du protocole de Diffie-Hellman et de nombreux autres protocoles (El Gamal, DSA, etc.).

Le logarithme discret

Soit un nombre premier. Le problème du logarithme discret dans un corps fini est défini comme suit.

Entrées: Le nombre premier , un élément d’ordre maximal, un autre élément .

Sorties: L’unique exposant tel que .

Avant de commencer l’attaque

  1. Écrivez une fonction

    public static long inverse(long a, long p)
    

    qui calcule l’inversion en utilisant l’algorithme d’Euclide étendu (voir TD Torus Wars).

  2. Écrivez une fonction

    public static long modPow(long g, long x, long p)
    

    qui calcule en utilisant l’algorithme d’exponentiation binaire (voir TD Arithmétique modulaire). La fonction doit accepter des valeurs négatives pour g et pour x. Souvenez vous que .

  3. Testez vos fonctions sur des petites valeurs pour être sûrs qu’elle marchent bien. Enfin, vérifiez que

    modPow(-2, -12345, 1073741827) == 11418853
    

    Si vous n’obtenez pas ce résultat, l’un des problèmes possibles est dû à un dépassement de la précision des long : vous n’avez pas systématiquement réduit modulo p.

Attaque par recherche exhaustive

L’algorithme le plus simple que l’on puisse imaginer pour résoudre le problème du log discret consiste à essayer toutes les puissances jusqu’à tomber sur la bonne.

  1. Écrire un programme Java qui prend en entrée , et et qui donne en sortie le logarithme discret .

Remarque: Vous serez peut-être tentés d’utiliser la fonction modPow dans la boucle de ce programme pour calculer , , etc. Observez, cependant, que dans les deux boucles ci-dessous:

pour 0 <= x < p-1 :
    tmp = modPow(g, x, p)

et

tmp = 1
pour 0 <= x < p-1 :
    tmp = tmp * g % p

la variable tmp prend exactement les mêmes valeurs, mais la deuxième boucle est largement plus efficace car elle fait une seule multiplication à chaque itération.

  1. Testez votre programme sur des petites entrées dont vous connaissez le logarithme. Ensuite, allez résoudre les premiers challenges (jusqu’à ).

Suggestion: la résolution des challenges peut prendre du temps. Pour avoir une idée d’où votre algorithme en est, faites des affichages de contrôle réguliers dans vos boucles. Un bon compromis entre trop et pas assez est de faire un affichage toutes les itérations.

Précision arbitraire avec BigInteger

On a déjà fait la remarque : les données de type long sont limitées à 8 octets, c’est à dire 64 bits (représentation en complément à deux, qui utilise un bit pour le signe). Lorsque l’on multiplie entre eux deux long de taille supérieure à 32 bits, le résultat dépasse alors la taille maximale, et il est donc tronqué.

Pour pouvoir résoudre des logarithmes discrets plus grands, il va falloir utiliser une bibliothèque d’entiers multi-précision, garantissant un résultat toujours correct. Java implante cela dans l’objet java.math.BigInteger.

Les opérateurs usuels +, *, etc. ne sont pas utilisables sur les BigInteger : ils sont remplacés par des méthodes. Voici un bref comparatif des opérations arithmétiques sur les entiers (int ou long) et sur les BigInteger.


  entiers machine BigInteger
création int a = 10; BigInteger a = BigInteger.valueOf(10);
opérations c = a + b; c = a.add(b);
  c = a - b; c = a.subtract(b);
  c = a * b; c = a.multiply(b);
  c = a / b; c = a.divide(b);
  c = a % b; c = a.mod(b);¹
comparaisons a == b a.equals(b)
  a < b a.compareTo(b) == -1
  a > b a.compareTo(b) == 1
  b = a.modInverse(n);
  b = a.modPow(x, n);
convertir en int   int b = a.intValue();
convertir en long   long b = a.longValue();

¹a.mod(b) se comporte mieux que a % b, en ce que le résultat est toujours positif. Si vous voulez avoir exactement le même comportement que a % b, il faut utiliser a.remainder(b).


D’autres opérations encore sont définies sur les BigInteger, allez voir la documentation.

  1. Récrivez votre code en utilisant des BigInteger à la place des long. Vous pouvez vous épargner le travail de récrire inverse et modPow, car ils sont déjà implantés dans la bibliothèque Java.

Utilisez votre code pour résoudre les challenges suivants. Attention : cela peut prendre une dizaine de minutes de résoudre un logarithme de taille (32 bits) avec l’algorithme naïf… difficile d’aller au dela des 35 bits !

Pas de Bébé, pas de géant

Il est facile de se rendre compte que la recherche exhaustive fait en moyenne multiplications d’entiers modulo . Dès que dépasse les 30 bits, ce coût commence à devenir prohibitif.

L’algorithme pas de bébé - pas de géant fait partie de la famille des algorithmes meet in the middle. Il permet de réduire le temps de calcul de la recherche exhaustive en échange d’une occupation de mémoire accrue. Avec une occupation de mémoire de , il permet de trouver un logarithme discret en seulement multiplications. L’optimum est atteint lorsque (en supposant qu’on ait assez de mémoire à disposition), alors le temps de calcul devient de . Avec assez de mémoire vive à disposition, il devrait alors être possible de calculer des logarithmes discrets pour jusqu’à 60 bits sur des machines ordinaires.

Description de l’algorithme

Entrées: Le nombre premier , un élément d’ordre maximal, un autre élément .

Paramètres: Un entier (en général une puissance de 2).

Sortie: L’unique exposant tel que .

L’algorithme se développe en deux phases:

Lorsqu’on a une collision, on a trouvé une égalité du type

par des opérations arithmétiques simples, il est alors immédiat que

le logarithme discret recherché est alors .

Exemple

Démontrons l’utilisation de l’algorithme sur des petits entiers. Nous allons choisir , . Fixons comme paramètre, ce qui est très proche de (on aurait pu prendre , mais nous sommes paresseux).

Les pas de bébé donnent le tableau suivant, qu’on va stocker en mémoire

On calcule . On fait maintenant les pas de géant (pour cette phase, pas besoin de stocker les résultats dans une table):

On déduit que

d’où

où l’avant-dernière égalité découle du petit thèorème de Fermat (qui affirme que pour tout et tout premier). Le logarithme discret cherché est donc 73 (ou -27, ce qui est tout à fait équivalent).

À vos javac

  1. Écrivez un programme qui implante l’algorithme pas de bébé - pas de géant. Il doit prendre en entrée les nombres , et , ainsi que le paramètre déterminant son utilisation de la mémoire.

Vérifiez votre programme sur les challenges que vous avez déjà résolu.

Tables de hachage et optimisation de la mémoire

L’efficacité de l’algorithme pas de bébé - pas de géant dépend crucialement de la rapidité avec laquelle on peut effectuer les recherches dans la table des pas de bébé. Si l’on utilise une bête liste chaînée, par exemple, chacun des pas de géant engendrera une recherche parmi éléments, ce qui demande opérations, pour un coût total moyen de . Aucun gain asymptotique par rapport à l’algorithme naïf, donc.

Les tables de hachage sont une structure de données qui permet de faire des recherches dans une table en temps O(1) en moyenne. Java en offre deux implantation à peu près équivalentes : java.util.HashMap et java.util.Hashtable

Vous pouvez déclarer une table de hachage contenant des BigInteger comme suit:

Hashtable<BigInteger,BigInteger> table = new Hashtable<BigInteger,BigInteger>(size, 1);

size est le nombre maximal d’éléments que la table va contenir ( dans notre cas). Remarquez que size doit être de type int, sinon le compilateur vous donnera une erreur.

Vous pouvez ensuite insérer des éléments avec la méthode .put et les chercher avec la méthode .get:

table.put(BigInteger.valueOf(10), BigInteger.valueOf(1));
System.out.println(table.get(BigInteger.valueOf(10));     // Affiche 1
System.out.println(table.get(BigInteger.valueOf(1)));     // Affiche null
  1. Modifiez votre code pour qu’il utilise une table de Hachage pour stocker les pas de bébé. Résolvez les challenges suivants (les 40 bits devraient être atteignables).

Remarque: Vous aurez peut-être besoin d’augmenter la mémoire à disposition de la JVM afin de résoudre certains challenges. Pour cela, vous pouvez lancer la JVM avec le paramètre -Xmx, comme ceci (initialisation de la mémoire à 3 giga-octets) :

java -Xmx3g PasDeBebe

Optimisation de la mémoire

Lorsque frôle les 20 bits, les tables de hachage de Java commencent à devenir insuffisants. Il y a deux raisons pour cela:

En ce qui concerne le premier point, on peut réduire considérablement l’occupation mémoire en stockant des long dans la table. Ceci ne fait de sens que si , alors vous pourrez récupérer la valeur de vos BigInteger au format long en appelant la méthode .longValue(). Remarquez, cependant que les calculs (en particulier les multiplications) doivent être faits avec BigInteger, car le résultat du produit de deux entiers de 63 bits (avant réduction modulo ) ne tient pas dans un long.

En ce qui concerne le deuxième point il faudrait pouvoir se passer des listes chaînées. Rappelons le principe d’une table de Hachage. Une table de Hachage est un grand tableau, indexé de 0 jusqu’à . Lorsqu’on insère un élément dans la table, on calcule son hash , c’est à dire une valeur, dépendante uniquement de , comprise entre et , et l’on insère dans la case du tableau.

Lorsqu’on cherche une valeur dans la table, on calcule son hash , et on regarde s’il est dans la case du tableau.

Il arrive fréquemment que deux éléments et aient le même hash . Dans ce cas on a une collision, et il faut trouver un moyen de stocker les deux valeurs dans la même case du tableau. Deux solutions sont communément employées:

Remarquez que pour implanter un adressage ouvert, le tableau doit être légèrement plus grand que la quantité de données à stocker (en général on alloue entre 125% et 140% du nombre maximal d’entrées).

La page Wikipedia décrit assez clairement les différentes méthodes de Hachage.

À vos javac

La technique de l’adressage ouvert est parfaitement adaptée pour l’algorithme pas de bébé - pas de géant. En effet nous savons d’avance que nous allons stocker couples d’entiers (des long), nous pouvons donc allouer au début de l’algorithme deux tableaux de long de taille légèrement plus grande que . Aucune allocation dynamique de mémoire n’aura lieu lorsque l’on insérera des valeurs dans ces tableaux (contrairement au cas des BigInteger).

Il nous faut choisir une fonction de hachage pour calculer pour chaque entier . Si le tableau utilisé pour la table est de dimension , un choix simple et efficace consiste à prendre les bits de poids faible de comme valeur du hash . Plus généralement, si la dimension du tableau est un entier quelconque , la fonction est un choix raisonnable.

  1. Créez votre propre table de hachage à adressage ouvert, et utilisez-la dans l’algorithme précédent.

Essayez de résoudre les challenges restants (50 bits devrait être faisable, 60 bits va être dur…). À moins que vous n’ayez un super-calculateur à la maison, doté de centaines de GO de RAM, vous serez sans doutes amenés à utiliser un paramètre bien inférieur à .

Le mot de la fin

Déposez tous vos fichiers .java dans la boîte de dépôt sur e-campus2, ou envoyez-les par mail à votre enseignant. Incluez tout le code, même celui de l’algorithme naïf avec les long. Pas la peine d’envoyer les logarithmes discrets que vous aurez calculés : nous avons des traces de vos exploits ! La date limite pour envoyer vos fichiers est le vendredi 10 mai à 4h du matin. Un point de pénalité pour chaque heure de retard: le 10 mai à 23h59 c’est votre dernière chance !

Parmi les challenges proposés, il y en a qui ne sont pas raisonnablement attaquables par les pas de bébé - pas de géant. Ce n’est pas la fin de l’histoire, bien évidemment !

L’algorithme rho de Pollard (aussi appelé méthode des kangourous) est un autre algorithme qui tourne en temps , mais avec une bien moindre complexité en mémoire (il utilise, en effet un nombre constant de registres). Cet algorithme est aussi basé sur une recherche de collisions entre éléments de , et vous devriez, si vous le souhaitez, réussir sans difficulté quelques challenges en plus avec son aide.

Le record actuel de calcul de log discret dans est détenu par Antoine Joux et Reynald Lercier (2005) sur un nombre , à l’aide de l’algorithme Number Field Sieve.

D’autres records plus impressionnants ont été obtenus pour d’autres structures que . Par exemple, le record de log discret dans les corps finis binaires a été obtenu le 11 avril 2013 par Robert Granger, Faruk Gologlu, Gary McGuire, and Jens Zumbragel sur un corps fini de éléments, grâce à une variante du Function Field Sieve due à Antoine Joux et eux mêmes.

Un autre record remarquable a été annoncé le 6 avril 2013 par Razvan Barbulescu, Cyril Bouvier, Jérémie Detrey, Pierrick Gaudry, Hamza Jeljeli, Emmanuel Thomé, Marion Videau et Paul Zimmermann, qui ont calculé un log discret dans un corps fini à éléments en utilisant le Function Field Sieve. Ce record est remarquable car 809 est un nombre premier (contrairement à 6120).

De quoi faire pâlir nos maigres exploits avec Java !