Consignes

Le TP est à rendre, par email, avant le dimanche 11 décembre à 23h59...

Pour ce TP comme pour les suivants, la pièce la plus importante sera une archive tp2-nom-prenom.tar contenant votre programme. J'insiste sur les points suivants :

  1. l'archive est au format tar (éventuellement au format tar.gz ou tar.bz2),
  2. l'archive contient un répertoire tp2-nom-prenom contenant votre code (elle ne crée rien d'autre dans le répertoire courant),
  3. l'archive ne contient pas de fichier binaire ou exécutable (seulement des fichiers sources),
  4. l'archive contient un fichier Makefile pour compiler votre programme, avec au minimum les cibles all et clean.

Le non-respect des ces consignes risque de me mettre de mauvaise humeur... (Remarque : un fichier Makefile basique est disponible ici. Il suppose l'existence de deux fichiers tp2-nom-prenom-gmp.c et ``tp2-nom-prenom-rsa.c.)

Attention, le seul fait que votre programme fonctionne ne suffit pas pour avoir une bonne note. Les points suivants seront pris en compte :

  1. l'architecture de votre programme (code découpé en fonctions etc.),
  2. la lisibilité de votre programme (choix pertinent pour les noms de variables etc.),
  3. la présence de commentaires aux endroits appropriés,
  4. la présence de documentation pour vos fonctions.

Certaines questions appellent à une réponse que vous pouvez mettre en commentaire dans votre fichier principal. Si vous le souhaitez vraiment, vous pouvez aussi m'envoyer un rapport de TP. Le format de ce rapport sera au choix, mais par ordre de préférence :

Attention : les points suivants ne rapportent rien, mais ne pas les respecter pourra retrancher jusqu'à 10 points sur la note finale :

Liens utiles


1. La bibliothèque GMP et les grands entiers

1.1. Les types de données

La bibliothèque GMP est une bibliothèque pour manipuler des nombres (entiers, rationnels ou flottants) avec une précision arbitraire. Les initiales signifient « GNU Multi Precision ». Pour compiler du code utilisant cette bibliothèque avec gcc, il faut ajouter le drapeau de compilation -lgmp. (Il faut bien entendu que la bibliothèque soit installée sur votre machine. Pour Debian ou Ubuntu, il s'agit du paquet libgmp3-dev.)

Nous allons essentiellement nous intéresser aux nombres entiers. Pour GMP, un entier est simplement un tableau de « chiffres ». Les types suivants sont définis par GMP :

En fait, le type mp_limb_t est plus général que juste des « digit », mais cette intuition est suffisante pour les entiers.

Les autres types importants sont :

1.2. Les opérations

Je vous renvoie à la documentation, et plus particulièrement à la partie sur les entiers et celle sur les fonctions de formatage.

J'attire votre attention sur le fait que toutes les opérations sur les grands entiers sont en passage par adresse. Ceci vient du fait que les grands entiers sont stockés dans des tableaux, et que l'on veut pouvoir contrôler l'utilisation de la mémoire.

1.3. Structure d'un grand entier

En regardant le fichier /usr/include/gmp.h (ou /usr/include/gmp-ARCH.h), cherchez les définitions des types

Qu'en pensez-vous ?

Écrivez une petite fonction qui affiche le contenu d'un grand entier, case par case. Affichez également les autres informations contenues dans l'entier :

void affiche_grand_entier(mpz_t n) ;

Pensez à tester votre fonction sur des entiers petits, grands, positifs, négatifs, etc. Expliquez ce que vous observez...

Pour faciliter l'affichage des cases, vous pouvez utiliser la fonction gmp_printf qui permet d'afficher des entiers, des digits et autres, tout en suivant les conventions de la fonction printf habituelle. (Voir ici pour les nouveautés par rapport à printf.)

1.4. Arithmétique

Programmez votre propre version de la fonction mpz_powm qui permet de calculer x^n (mod m) de deux manières différentes :

void puissance_naive (mpz_t resultat, mpz_t x, mpz_t n, mpz_t m) ;
void puissance_chinoise (mpz_t resultat, mpz_t x, mpz_t n, mpz_t m) ;

Vous n'avez bien entendu pas le droit d'utiliser la fonction mpz_powm, mais vous pouvez utiliser les fonctions mpz_add, mpz_mul ou mpz_cmp.

Comparez l'efficacité de vos deux fonctions, et comparez la seconde avec la fonction de GMP. Expliquez votre démarche et donnez vos résultats.

Bonus

Reprogrammez votre propre version de la fonction qui calcule le pgcd de deux nombres ainsi que les nombres de Bezout associés :

void pgcd_bezout (mpz_t g, mpz_t x, mpz_t y, const mpz_t a, const mpz_t b) ;

N'oubliez pas de tester votre fonction.

Pour commencer, vous pouvez faire une fonction

void pgcd (mpz_t g, const mpz_t a, const mpz_t b) ;

qui calcule uniquement le pgcd, sans calculer les nombres de Bezout.

2. RSA

2.1. Rappel sur RSA

Le principe de RSA est basé sur le théorème d'Euler : a^φ(n) = 1 (mod n) si a est premier avec n. (Dans notre cas, n=pq et φ(n) = (p-1)(q-1)). RSA fonctionne de la manière suivante :

La clé publique est e (et base) et la clé privée est d (et base). Les nombres p et q ne servent plus, sauf si l'ont souhaite optimiser les calculs. (Et il faut les garder secrets...)

2.2. Rappels : entrées / sorties en C

Pour manipuler les fichiers, vous utiliserez de préférence les fonctions

et similaires.

Voici également deux fonctions pour écrire et lire le contenu d'une clé en ASCII dans un fichier :

/*
 * lecture d'une clé (base et clé) dans un fichier.
 * Le format du fichier est simplement :
 *   - première ligne : base de la clé en base 62
 *   - seconde ligne : clé en base 62
 */
void lire_cle(char *fichier, mpz_t base, mpz_t cle_e) {
  FILE *cle ;

  if (NULL == (cle = fopen(fichier, "r"))) {
      fprintf(stderr, "Erreur lors de l'ouverture du fichier \"%s\".\nAbandon.\n", fichier);
      exit(-1);
  }

  char str[MAX_SIZE] ;
  fgets(str, MAX_SIZE, cle);
  mpz_set_str(base, str, 62);
  fgets(str, MAX_SIZE, cle);
  mpz_set_str(cle_e, str, 62);
  fclose(cle);
}

/*
 * écriture d'une clé (privée ou publique) dans un fichier
 * Le format du fichier est simplement :
 *   - première ligne : base de la clé en base 62
 *   - seconde ligne : clé (d ou e) en base 62
 *
 * Attention, le fichier est écrasé s'il existait.
 */
void ecrire_cle(char *fichier, mpz_t base, mpz_t cle_e) {
  FILE *cle ;

  if (NULL == (cle = fopen(fichier, "w+"))) {
      fprintf(stderr, "Erreur lors de l'ouverture du fichier \"%s\".\nAbandon.\n", fichier);
      exit(-1);
  }

  fputs(mpz_get_str(NULL, 62, base), cle);
  fputs("\n",cle);
  fputs(mpz_get_str(NULL, 62, cle_e), cle);
  fputs("\n",cle);
  fclose(cle);
}

3. Implémentation

3.1. Génération de la paire clé privée / clé publique

Pour générer une paire clé privée / clé publique, il faut choisir deux nombres premiers p et q de tailles similaires. En partant d'un nombre e premier avec (p-1)(q-1), calculez l'inverse de e modulo (p-1)(q-1).

Pour générer ces clés, il faut donc chercher des nombres premiers assez grand (plusieurs centaines de chiffres). Vous pouvez pour ceci utiliser les fonctions

Pour générer le nombre e, vous pouvez simplement tirer un nombre au hasard et vérifier s'il est premier avec (p-1)(q-1). S'il ne l'est pas, recommencez. (L'espérance du nombre de tirage est assez faible et cette méthode fonctionne donc bien...) Vous pouvez utiliser votre fonction pgcd ou bezout, ou les fonctions de GMP.

Pour initialiser le générateur aléatoire, vous pouvez utilisez :

  /* initialisation de la graine pour la génération des nombres aléatoires */
  gmp_randstate_t state;
  gmp_randinit_default (state);
  gmp_randseed_ui (state, (unsigned) time(NULL));

Écrivez une fonction

/*
 * génère une paire de clés privée / publique.
 *   - "taille_base" est la taille, en bits, de la base (second argument)
 *   - base est la base (c'est le produit des nombres p et q générés
 *     temporairement dans la fonction)
 *   - cle_d et cle_e sont la clé privée et publique
 */
void genere_cles(unsigned taille_base, mpz_t base, mpz_t cle_d, mpz_t cle_e) ;

pour générer une paire clé publique / clé privée sur taille_base bits.

3.2. Codage à partir de la clé publique

Vous devrez découper le flot entrant en morceaux de taille appropriée et traiter chaque morceau séparément... Écrivez une fonction

/*
 * écrit "nb_octets" octets du nombre "bloc" dans le fichier. Si l'entier est trop petit,
 * complète par des 0 devant pour ecrire exactement "nb_octets" octets.
 * La valeur de retour est le nombre total d'octets écrits.
 */
int mpz_fwrite(mpz_t bloc, int nb_octets, FILE *fichier) ;

pour pouvoir écrire un bloc (vu comme grand-entier) dans un fichier. Reportez-vous au fichier tp2-rsa.h pour plus de détails.

Vous devrez pour ceci utiliser la structure interne des grands entiers.

La fonction mpz_fread qui permet de lire un certain nombre d'octets dans un fichier pour donner un grand entier se trouve ici :

/*
 * lit au plus "nb_octets" octets dans un fichier et les met dans un grand entier.
 * La valeur de retour est le nombre d'octets lus.
 */
int mpz_fread(mpz_t bloc, int nb_octets, FILE *fichier) {
  int i;
  unsigned c;
  int nb_limb = 1 + ((nb_octets-1) / sizeof(mp_limb_t));
  if (bloc->_mp_alloc < nb_limb) {
    mpz_realloc(bloc,nb_limb);
  }
  for(i=0; i<bloc->_mp_alloc ; i++) bloc->_mp_d[i] = 0;

  c = fread(bloc->_mp_d, 1, nb_octets, fichier);
  bloc->_mp_size = 1 + ((c-1) / sizeof(mp_limb_t));

  return(c);
}

Écrivez une fonction

int coder_RSA(mpz_t base, mpz_t cle_e, FILE *fichier_clair, FILE *fichier_crypte);

qui permet de coder le contenu du fichier fichier_clair et de le mettre dans le fichier fichier_crypte en utilisant le clé publique base et cle_e.

Après le dernier bloc lu, vous mettrez quelques octets (4, càd la taille d'un entier) supplémentaires pour connaître la taille du dernier bloc lu. (Le dernier bloc ne tombe probablement pas juste...)

Réfléchissez bien à la taille des blocs que vous utiliserez pour découper votre fichier.

3.3. Décodage à partir de la clé privée

Écrivez une fonction

int decoder_RSA(mpz_t base, mpz_t cle_d, FILE *fichier_crypte, FILE *fichier_clair);

qui permet de décoder le contenu du fichier fichier_crypte et de le mettre sur le fichier fichier_clair.

Attention à bien gérer correctement le dernier bloc.

réfléchissez bien à la taille des blocs que vous utiliserez pour découper votre fichier.

3.4. Petite interface

Écrivez une petite interface dans votre fonction main pour faciliter l'utilisation de votre programme. Vous devez pouvoir :

Voici par exemple ce que fait ma commande :

Utilisation : ./pierre-hyvernat-rsa [options]
  -h :         affiche ce message d'aide
  -g :         génère une paire de clés publique / privée
  -X :         encrype
  -D :         decrypte
  -N n :       précise la taille de la clé (utile avec -g) ; par défaut : 512
  -c fichier : précise le fichier contenant la clé publique à utiliser
               obligatoire avec -X et -g
  -C fichier : précise le fichier contenant la clé privée à utiliser
               obligatoire avec -D et -g
  -I fichier : précise le fichier à traiter ; par défaut : STDIN
  -O fichier : précise le fichier de sortie ; par défaut : STDOUT

Vous devrez pour ceci utilisez les argument argv et argc passés à la fonction main pour savoir ce qu'il faut faire. L'utilisation de la fonction getopt déclarée dans unistd.h pourra vous être utile, mais ce n'est pas obligatoire.

3.5. Test

3.6. Finalisation

(Bonus)

Pour rendre la crytpanalyse difficile, il faut chercher un clé qui vérifie certaine propriétés. En utilisant ce paragraphe de wikipedia, programmez une seconde version de la fonction

void genere_cles(unsigned taille_base, mpz_t base, mpz_t cle_d, mpz_t cle_e) ;

(Bonus)

Pour le moment, notre version de RSA permet encore d'intervertir des blocs. Corrigez ceci...

Allez également lire ceci et cela. Implantez un schéma de remplissage pour corriger ça...