Le TP est à rendre, par email, avant le dimanche 24 avril à 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 :
tar
(éventuellement au format tar.gz
ou tar.bz2
),
tp2-nom-prenom
contenant votre code (elle ne crée rien d'autre dans le répertoire courant),
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 :
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 :
README
au format texte (mais avec un minimum de mise en page),
.odt
),
Attention : les points suivants ne rapportent rien, mais ne pas les respecter pourra retrancher jusqu'à 10 points sur la note finale :
-Wall -Werror -Wextra -pedantic -std=c99
,
info614
, ou info-614
ou info 614
,
Makefile
tp2-gmp.h
et le fichier tp2-rsa.h
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 :
mpz_t
pour le type des entiers signés (mp
pour « multiprecision », z
pour le symbole mathématique des entiers Z, et _t
pour « type »),
mp_limb_t
pour le type des « digit » (normalement un entier machine 32 ou 64 bits).
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 :
mpq_t
pour le type des rationnels,
mpf_t
pour le type des flottants avec mantisse arbitraire et exposant borné.
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.
En regardant le fichier /usr/include/gmp.h
(ou /usr/include/gmp-ARCH.h
), cherchez les définitions des types
mp_limb_t
,
mpz_t
.
Qu'en pensez-vous ?
mpz_init(mpz_t grand_entier)
pour initialiser vos entiers multiprécision. Comme la re-allocation de mémoire est automatique, on n'a pas besoin de préciser la taille de l'entier...
mpz_clear (mpz_t grand_entier)
lorsque vous n'avez plus besoin d'utiliser des grands entiers.
"a := a*b"
, on peut faire mpz_mul(a, a, b)
sans avoir besoin de passer par une variable intermédiaire.
É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
.)
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.
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...)
Pour manipuler les fichiers, vous utiliserez de préférence les fonctions
FILE *fopen(const char *path, const char *mode);
int fclose(FILE *fp);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
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); }
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
void mpz_nextprime (mpz_t ROP, mpz_t OP)
,
int mpz_probab_prime_p (mpz_t N, int REPS)
,
void mpz_urandomb (mpz_t ROP, gmp_randstate_t STATE, unsigned long int N)
,
void mpz_urandomm (mpz_t ROP, gmp_randstate_t STATE, mpz_t N)
.
void mpz_gcd (mpz_t rop, mpz_t op1, mpz_t op2)
.
int mpz_invert (mpz_t rop, mpz_t op1, mpz_t op2)
.
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.
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
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.
É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.
É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.
(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) ;