TP et projets#
Cryptographie
TP Chiffrer, casser, signer#
de César à RSA, en Python
Durée : 3 h (deux séances) · Sur machine, par deux
Fichiers à télécharger
Consignes
-
Fichier à télécharger (lien ci-dessus) :
tp_crypto_depart.py. Il contient un texte françaisTEXTE, un tableauFREQ_FRdes fréquences des lettres en français, la fonctionnormaliser(majuscules, sans accents), des squelettes de fonctions et des tests. Le lancer affiche l’état des tests. -
Les messages sont en majuscules sans accents ; seules les lettres
AàZsont chiffrées, les autres caractères (espaces, ponctuation) sont recopiés tels quels. -
Niveau : ★ échauffement, ★★ classique (niveau bac), ★★★ défi ; les coups de pouce et les corrigés se déplient sous chaque exercice. Le symbole ▶ signale ce qui est à programmer et tester.
-
Usage de l’IA, indiqué sur chaque exercice : aucune ; en appui (déboguer, reformuler, vérifier ; la réponse finale est écrite et comprise par vous) ; l’exercice s’appuie sur une réponse d’IA. Voir la charte d’usage de l’IA.
But du TP
Programmer les chiffrements du cours et les attaques qui les cassent : César par force brute puis par analyse de fréquences, Vigenère colonne par colonne, le masque jetable réutilisé ; puis construire un RSA complet — génération de clés avec l’algorithme d’Euclide étendu, chiffrement d’un message, signature — et mesurer pourquoi sa sécurité tient : factoriser \(n\) devient vite hors de portée. Produit final : une petite bibliothèque de chiffrement et d’attaques, testée, et un tableau de mesures.
Partie A — César : chiffrer, puis casser sans la clé#
★ Exercice 1 — César, version complète #
-
▶ Compléter
decaler(lettre, k): une majuscule de rang \(x\) devient la lettre de rang(x + k) % 26; tout autre caractère est renvoyé inchangé. Puis complétercesar(texte, k). Exemple :cesar("BONJOUR !", 3)vaut"ERQMRXU !". -
▶ Chiffrer
clair = normaliser(TEXTE)avec la clé \(11\), afficher le début du chiffré, puis le déchiffrer.
Corrigé
-
César et Vigenère conservent les fréquences des lettres (globalement, ou colonne par colonne) ; le masque réutilisé laisse fuir \(m_1 \oplus m_2\). Un bon chiffrement ne doit rien laisser paraître de la structure du message.
-
RSA : choisir \(p\), \(q\) premiers (
premier_aleatoire) ; \(n = pq\), \(\varphi = (p-1)(q-1)\) ; \(e\) premier avec \(\varphi\) (pgcd) ; \(d\) inverse de \(e\) modulo \(\varphi\) (euclide_etendu,inverse_modulaire). -
La sécurité repose sur la difficulté de factoriser \(n\) : chaque paire de chiffres en plus multiplie le temps par \(10\) (partie E).
-
Chiffrer : clé publique du destinataire (confidentialité). Signer : clé privée de l’expéditeur (authenticité, intégrité).
def decaler(lettre, k):
if "A" <= lettre <= "Z":
return chr((ord(lettre) - 65 + k) % 26 + 65)
return lettre
def cesar(texte, k):
resultat = ""
for c in texte:
resultat = resultat + decaler(c, k)
return resultat
clair = normaliser(TEXTE)
chiffre = cesar(clair, 11)
print(chiffre[:50]) # OPAFTD W'LYETBFTEP, WPD SZXXPD NSPCNSPYE L NLNSPC
print(cesar(chiffre, -11) == clair) # True
★ Exercice 2 — La force brute #
On a intercepté : KXGWXS-OHNL WXOTGM EX FNLXX HVXTGHZKTIABJNX T FBWB.
-
▶ Afficher les \(26\) déchiffrements possibles (un par ligne, précédé de la clé). Lequel est le bon ? Quelle est la clé ?
-
Un programme doit faire ce tri tout seul. Proposer une idée pour reconnaître automatiquement « du français ».
Corrigé
1. for k in range(26): print(k, cesar(message, -k)). Seule la clé 19 donne du français : RENDEZ-VOUS DEVANT LE MUSEE OCEANOGRAPHIQUE A MIDI.
2. Compter les mots d’un dictionnaire présents dans chaque candidat, ou comparer les fréquences de ses lettres à celles du français (c’est l’idée de l’exercice suivant).
★★ Exercice 3 — L’analyse de fréquences, automatisée #
-
▶ Compléter
frequences(texte)(dictionnaire lettre \(\to\) nombre d’apparitions, pour les lettresAàZseulement) etplus_frequente(texte). Quelles sont les trois lettres les plus fréquentes declair? -
▶ Compléter
casser_cesar(chiffre), qui suppose que la lettre la plus fréquente du chiffré est unEet en déduit la clé. Vérifier qu’elle retrouve la clé \(11\) sur le long texte. -
▶ Essayer
casser_cesarsurcesar("ATTAQUE A L AUBE", 7). Quelle clé trouve-t-on ? Pourquoi l’hypothèse « la plus fréquente est unE» échoue-t-elle ici ? -
▶ Méthode plus robuste : compléter
score(texte), la somme des valeursFREQ_FR[c]pour toutes les lettrescdu texte (un texte français contient beaucoup de lettres fréquentes, donc a un score élevé), puiscasser_cesar_score(chiffre)qui renvoie la clé \(k\) pour laquellecesar(chiffre, -k)a le meilleur score. Tester sur le message court, sur le message intercepté, et sur le long texte.Coup de pouce
Même structure qu’une recherche de maximum : une boucle
for k in range(26), deux variablesmeilleure_cleetmeilleur_score.
Corrigé
def frequences(texte):
compte = {}
for c in texte:
if "A" <= c <= "Z":
if c in compte:
compte[c] += 1
else:
compte[c] = 1 # premiere apparition
return compte
def plus_frequente(texte):
compte = frequences(texte)
meilleure = None
for lettre in compte:
if meilleure is None or compte[lettre] > compte[meilleure]:
meilleure = lettre
return meilleure
def casser_cesar(chiffre):
return (ord(plus_frequente(chiffre)) - ord("E")) % 26
def score(texte):
s = 0
for c in texte:
if "A" <= c <= "Z":
s += FREQ_FR[c]
return s
def casser_cesar_score(chiffre):
meilleure_cle = 0
meilleur_score = -1
for k in range(26):
sc = score(cesar(chiffre, -k))
if sc > meilleur_score:
meilleur_score = sc
meilleure_cle = k
return meilleure_cle
1. Dans clair : E (\(297\) fois), S (\(150\)), A (\(108\)).
2. casser_cesar(cesar(clair, 11)) renvoie bien 11 (sur le message intercepté : 19).
3. Le chiffré est HAAHXBL H S HBIL ; sa lettre la plus fréquente est H (le A du clair) : la fonction renvoie \(3\) au lieu de \(7\). Sur un message de \(13\) lettres, les fréquences n’ont rien de statistique : ici, aucun E n’est la lettre la plus fréquente.
4. casser_cesar_score trouve \(7\) sur le message court, \(19\) sur le message intercepté et \(11\) sur le long texte : elle utilise toutes les lettres et pas seulement la plus fréquente.
Partie B — Vigenère, et comment Babbage l’a cassé#
★ Exercice 4 — Chiffrer avec une clé-mot #
▶ Compléter vigenere(texte, cle, sens=1) : la \(i\)e lettre du texte est décalée selon la lettre cle[i % len(cle)] (A \(\to 0\), B \(\to 1\)…) ; avec sens = -1, on décale en arrière (déchiffrement). Les caractères qui ne sont pas des lettres sont recopiés et ne font pas avancer la clé. Vérifier l’exemple du cours : vigenere("NSI ROCKS", "CLE") vaut "PDM TZGMD".
Coup de pouce
Garder un compteur i des lettres déjà chiffrées, distinct de la position dans le texte ; réutiliser decaler avec le décalage sens * k.
Corrigé
def vigenere(texte, cle, sens=1):
resultat = ""
i = 0 # nombre de lettres deja chiffrees
for c in texte:
if "A" <= c <= "Z":
k = ord(cle[i % len(cle)]) - 65
resultat = resultat + decaler(c, sens * k)
i += 1
else:
resultat = resultat + c
return resultat
vigenere("NSI ROCKS", "CLE") vaut "PDM TZGMD", et vigenere("PDM TZGMD", "CLE", -1) redonne "NSI ROCKS".
★★ Exercice 5 — Casser Vigenère quand on connaît la longueur de la clé #
On a chiffré clair avec une clé de \(6\) lettres : chiffre = vigenere(clair, "MONACO"). Faisons comme si l’on ignorait la clé.
-
Les lettres de rangs \(0, 6, 12, \dots\) du texte sont toutes décalées avec la même lettre de la clé. Par quel chiffrement, déjà cassé, sont-elles donc chiffrées ?
-
▶ Compléter
colonnes(chiffre, L), qui renvoie la liste des \(L\) chaînes formées des lettres de rangs \(0, L, 2L\dots\), puis \(1, L+1, \dots\), etc. (les caractères qui ne sont pas des lettres sont ignorés). Exemple :colonnes("AB CDE", 2)vaut["ACE", "BD"].Coup de pouce
Partir d’une liste de
Lchaînes vides. Parcourir le chiffré en comptant les lettres avec un compteuri(comme dansvigenere) : la lettre de rangis’ajoute à la chaîne d’indicei % L. -
▶ En déduire
casser_vigenere(chiffre, L), qui casse chaque colonne aveccasser_cesar_scoreet renvoie la clé. Retrouve-t-onMONACO? Déchiffrer.
Corrigé
1. Elles sont toutes décalées du même nombre de rangs : c’est un chiffre de César, que l’on sait casser.
2. et 3.
def colonnes(chiffre, L):
cols = ["" for j in range(L)] # L chaines vides
i = 0 # rang de la lettre dans le chiffre
for c in chiffre:
if "A" <= c <= "Z":
cols[i % L] = cols[i % L] + c
i += 1
return cols
def casser_vigenere(chiffre, L):
cle = ""
for col in colonnes(chiffre, L):
cle = cle + chr(casser_cesar_score(col) + 65)
return cle
casser_vigenere(vigenere(clair, "MONACO"), 6) renvoie "MONACO", et le déchiffrement redonne le texte clair. Chaque colonne compte environ \(250\) lettres : assez pour que les fréquences parlent.
★★★ Exercice 6 — Défi : trouver la longueur de la clé #
au-delà du programme L’indice de coïncidence d’un texte est la probabilité que deux lettres tirées au hasard soient identiques : si \(n_A, n_B, \dots\) sont les nombres d’apparitions des lettres et \(N\) le nombre total de lettres, $\(IC = \frac{n_A(n_A-1) + n_B(n_B-1) + \dots + n_Z(n_Z-1)}{N(N-1)}.\)$
-
▶ Écrire
indice_coincidence(texte). Calculer l’indice declair, puis d’un texte de \(2\,000\) lettres tirées au hasard. Qu’est-ce qui les distingue ? -
▶ Pour \(L\) de \(1\) à \(8\), calculer la moyenne des indices des colonnes de
chiffre. Pour quelles valeurs de \(L\) retrouve-t-on l’indice d’un texte français ? Pourquoi ? -
▶ Écrire
longueur_cle(chiffre), qui renvoie la plus petite longueur \(L\) dont la moyenne dépasse \(0{,}065\), puis casser sans aucune indicationvigenere(clair, "SECRETS").
Corrigé
def indice_coincidence(texte):
compte = frequences(texte)
n = 0
s = 0
for lettre in compte:
x = compte[lettre]
n += x
s += x * (x - 1)
return s / (n * (n - 1))
def longueur_cle(chiffre, Lmax=10, seuil=0.065):
for L in range(1, Lmax + 1):
total = 0
for col in colonnes(chiffre, L):
total += indice_coincidence(col)
if total / L > seuil:
return L
return None
1. \(IC(\texttt{clair}) \approx 0{,}083\) ; pour \(2\,000\) lettres au hasard, \(IC \approx 0{,}038\) à \(0{,}039\) selon le tirage (proche de \(1/26\)). Le français répète beaucoup certaines lettres (E, S, A…), le hasard non.
2. Moyennes pour la clé MONACO, \(L = 1\) à \(8\) : \(0{,}052\) ; \(0{,}062\) ; \(0{,}063\) ; \(0{,}062\) ; \(0{,}051\) ; 0,083 ; \(0{,}052\) ; \(0{,}063\). Seul \(L = 6\) (et ses multiples) redonne l’indice du français : chaque colonne est alors un simple César, qui conserve l’indice ; pour un autre \(L\), une colonne mélange plusieurs décalages et l’indice baisse.
3. Pour vigenere(clair, "SECRETS"), longueur_cle renvoie \(7\) et casser_vigenere(…, 7) renvoie "SECRETS" : Vigenère cassé sans aucune indication.
Partie C — Le masque jetable, et l’erreur fatale#
En Python, le type bytes représente une suite d’octets : b"RDV" est la suite 82, 68, 86, et bytes([3, 3]) la suite 3, 3. On accède à l’octet i par m[i], qui est un entier.
★★ Exercice 7 — Un masque, deux messages #
-
▶ Compléter
xor_octets(a, b), qui renvoie la suite desa[i] ^ b[i](construire la liste de ces entiers, puis la convertir avecbytes(liste)). -
▶ Créer un masque aléatoire de la longueur du message (\(22\) octets) :
import secretspuismasque = secrets.token_bytes(22). Chiffrerm1 = b"RDV A 18H SOUS LE PONT", afficher le chiffré (c1.hex()l’affiche en hexadécimal) et le déchiffrer. Pourquoi utiliser le modulesecretsplutôt querandompour fabriquer une clé ? -
▶ L’expéditeur, paresseux, réutilise le même masque pour
m2 = b"LE CODE EST 4071 MERCI". Ève interceptec1etc2. Calculerxor_octets(c1, c2)et le comparer àxor_octets(m1, m2). Justifier l’égalité avec la propriété \((m \oplus k) \oplus k = m\). -
▶ Ève devine le premier message (un rendez-vous, elle a vu la scène). Montrer qu’elle obtient alors le second sans connaître le masque. Conclure sur la règle « jetable ».
Corrigé
import secrets
def xor_octets(a, b):
resultat = [a[i] ^ b[i] for i in range(len(a))]
return bytes(resultat)
m1 = b"RDV A 18H SOUS LE PONT"
m2 = b"LE CODE EST 4071 MERCI"
masque = secrets.token_bytes(22)
c1 = xor_octets(m1, masque)
c2 = xor_octets(m2, masque) # ERREUR : masque reutilise
print(xor_octets(c1, masque)) # b'RDV A 18H SOUS LE PONT'
print(xor_octets(c1, c2) == xor_octets(m1, m2)) # True
print(xor_octets(xor_octets(c1, c2), m1)) # b'LE CODE EST 4071 MERCI'
2. random produit une suite prévisible (elle est entièrement déterminée par sa graine) : elle convient aux simulations, pas aux secrets. secrets utilise l’aléa du système d’exploitation, imprévisible.
3. \(c_1 \oplus c_2 = (m_1 \oplus k) \oplus (m_2 \oplus k) = m_1 \oplus m_2 \oplus (k \oplus k) = m_1 \oplus m_2\) : le masque disparaît.
4. \((c_1 \oplus c_2) \oplus m_1 = m_2\) : connaissant (ou devinant) un message, Ève lit l’autre sans le masque. Le masque jetable n’est inviolable que s’il ne sert qu’une seule fois.
Partie D — Un RSA complet#
★ Exercice 8 — Six jeux de clés #
Voici six jeux de clés RSA. Pour chacun : \(n = pq\), \(\varphi = (p-1)(q-1)\), \(d\) est l’inverse de \(e\) modulo \(\varphi\), et \(C = M^{e} \bmod n\).
| \(p\) | \(q\) | \(n=pq\) | \(\varphi\) | \(e\) | \(d\) | \(M\) | \(C=M^{e}\bmod n\) |
|---|---|---|---|---|---|---|---|
| \(3\) | \(11\) | \(33\) | \(20\) | \(3\) | \(7\) | \(4\) | \(31\) |
| \(5\) | \(11\) | \(3\) | \(7\) | ||||
| \(5\) | \(7\) | \(35\) | \(24\) | \(5\) | \(5\) | \(10\) | \(5\) |
| \(7\) | \(11\) | \(77\) | \(60\) | \(7\) | \(43\) | \(2\) | \(51\) |
| \(11\) | \(13\) | \(7\) | \(9\) | ||||
| \(13\) | \(17\) | \(221\) | \(192\) | \(5\) | \(77\) | \(8\) | \(60\) |
-
À la main, sur le cahier, compléter la deuxième ligne (\(p=5\), \(q=11\)) : trouver \(d\) en testant \(d = 1, 2, 3\dots\), puis calculer \(C\). Vérifier que \(C^{d} \bmod n\) redonne \(M\) (calcul fait par Python :
pow(C, d, n)). -
▶ Écrire une boucle qui, pour chaque ligne, calcule \(n\), \(\varphi\), \(d\) (avec
pow(e, -1, phi)) et \(C\), et vérifie par unassertque le déchiffrement redonne \(M\). En déduire la cinquième ligne. -
Dans la troisième ligne, \(d = e\) : clé publique et clé privée ont le même exposant. Pourquoi ce jeu de clés serait-il catastrophique en vrai ?
Corrigé
1. \(n = 55\), \(\varphi = 4 \times 10 = 40\) ; \(3d \equiv 1\ [40]\) : \(3 \times 27 = 81 = 2 \times 40 + 1\), donc \(d = 27\). \(C = 7^{3} \bmod 55 = 343 \bmod 55 = 13\), et pow(13, 27, 55) vaut \(7\).
2.
jeux = [(3, 11, 3, 4), (5, 11, 3, 7), (5, 7, 5, 10),
(7, 11, 7, 2), (11, 13, 7, 9), (13, 17, 5, 8)]
for p, q, e, M in jeux:
n = p * q
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
C = pow(M, e, n)
assert pow(C, d, n) == M
print(p, q, n, phi, e, d, M, C)
Cinquième ligne : \(n = 143\), \(\varphi = 120\), \(d = 103\), \(C = 48\). Les six vérifications passent.
3. Si \(d = e\), la clé privée est connue de tous (elle est égale à la clé publique) : n’importe qui déchiffre. Cela arrive pour de tout petits \(\varphi\) ; il faut le refuser lors de la génération.
★★ Exercice 9 — Calculer \(d\) soi-même : l’algorithme d’Euclide étendu #
au-delà du programme Tester \(d = 1, 2, 3\dots\) est impossible quand \(\varphi\) a des centaines de chiffres. On utilise l’algorithme d’Euclide étendu, qui calcule \(g = \mathrm{pgcd}(a, b)\) et deux entiers \(u\), \(v\) tels que \(a u + b v = g\). Il est récursif :
-
si \(b = 0\) : renvoyer \((a, 1, 0)\) (car \(a \times 1 + 0 \times 0 = a\)) ;
-
sinon : calculer \((g, u', v')\) pour le couple \((b,\ a \,\%\, b)\), puis renvoyer \((g,\ v',\ u' - (a\,//\,b) \times v')\).
-
▶ Compléter
pgcd(a, b)(version itérative : tant que \(b \neq 0\), remplacer \((a, b)\) par \((b, a \,\%\, b)\)) eteuclide_etendu(a, b). Vérifier :euclide_etendu(240, 46)vaut(2, -9, 47), et \(240 \times (-9) + 46 \times 47 = 2\). -
Si \(\mathrm{pgcd}(e, \varphi) = 1\), on obtient \(e u + \varphi v = 1\). Expliquer pourquoi \(u \,\%\, \varphi\) est alors l’inverse de \(e\) modulo \(\varphi\).
-
▶ En déduire
inverse_modulaire(e, phi)(qui renvoieNonesi \(\mathrm{pgcd}(e, \varphi) \neq 1\)), et la comparer àpow(e, -1, phi)sur les six jeux de clés.
Corrigé
def pgcd(a, b):
while b != 0:
a, b = b, a % b
return a
def euclide_etendu(a, b):
if b == 0: # condition d'arret
return (a, 1, 0)
g, u, v = euclide_etendu(b, a % b)
return (g, v, u - (a // b) * v)
def inverse_modulaire(e, phi):
g, u, v = euclide_etendu(e, phi)
if g != 1:
return None
return u % phi
1. euclide_etendu(240, 46) vaut (2, -9, 47) et \(240 \times (-9) + 46 \times 47 = -2160 + 2162 = 2\).
2. \(e u = 1 - \varphi v\) : le reste de la division de \(e u\) par \(\varphi\) vaut \(1\), c’est-à-dire \(e u \equiv 1\ [\varphi]\). Comme \(u\) peut être négatif (ici euclide_etendu(3, 40) vaut (1, -13, 1)), on prend u % phi, compris entre \(0\) et \(\varphi - 1\) : -13 % 40 vaut \(27\), et l’on retrouve bien \(d = 27\) pour \(p = 5\), \(q = 11\).
3. inverse_modulaire et pow(e, -1, phi) coïncident sur les six jeux ; inverse_modulaire(4, 20) renvoie None (\(\mathrm{pgcd} = 4\)).
★★ Exercice 10 — Fabriquer ses propres clés #
-
▶ Compléter
est_premier(n)(on teste les diviseurs \(d\) tant que \(d \times d \leqslant n\)). Écrirepremier_aleatoire(mini, maxi), qui tire des entiers au hasard entreminietmaxijusqu’à en trouver un premier. -
▶ Écrire
generer_cles(): tirer deux premiers distincts \(p\), \(q\) entre \(1\,000\) et \(9\,999\), prendre \(e = 65\,537\) (l’exposant utilisé en pratique), recommencer si \(\mathrm{pgcd}(e, \varphi) \neq 1\), et renvoyer((e, n), (d, n)). Afficher vos clés. -
Pourquoi faut-il que \(p \neq q\) ? Pourquoi
pgcd(e, phi)doit-il valoir \(1\) ?
Corrigé
def est_premier(n):
if n < 2:
return False
d = 2
while d * d <= n:
if n % d == 0:
return False
d += 1
return True
def premier_aleatoire(mini, maxi):
p = random.randint(mini, maxi)
while not est_premier(p):
p = random.randint(mini, maxi)
return p
def generer_cles(mini=1000, maxi=9999, e=65537):
p = premier_aleatoire(mini, maxi)
q = premier_aleatoire(mini, maxi)
while p == q or pgcd(e, (p - 1) * (q - 1)) != 1:
p = premier_aleatoire(mini, maxi) # on recommence
q = premier_aleatoire(mini, maxi)
n = p * q
phi = (p - 1) * (q - 1)
return (e, n), (inverse_modulaire(e, phi), n)
2. Avec random.seed(2026) : clé publique (65537, 32431187), clé privée (18049793, 32431187) (\(n = 4931 \times 6577\)).
3. Si \(p = q\), \(n = p^2\) et l’on retrouve \(p\) par une simple racine carrée ; de plus, la formule \(\varphi = (p-1)(q-1)\) n’est plus la bonne et le déchiffrement échoue. Si \(\mathrm{pgcd}(e, \varphi) \neq 1\), \(e\) n’a pas d’inverse modulo \(\varphi\) : pas de clé privée.
★★ Exercice 11 — Chiffrer un vrai message #
-
▶ Écrire
chiffrer_message(message, cle_publique), qui renvoie la liste despow(ord(c), e, n)pour chaque caractèrec, etdechiffrer_message(nombres, cle_privee), qui reconstruit la chaîne avecchr. Chiffrer puis déchiffrer"Rendez-vous à 18 h". Pourquoi faut-il \(n >\)ord(c)pour tout caractère ? -
Observer la liste chiffrée : que remarque-t-on pour les lettres répétées ? Quelle attaque de la partie A redevient possible ? (En pratique, RSA ajoute un « remplissage » aléatoire au message, et ne sert qu’à chiffrer une clé de session AES, comme dans HTTPS.)
Corrigé
def chiffrer_message(message, cle_publique):
e, n = cle_publique
return [pow(ord(c), e, n) for c in message]
def dechiffrer_message(nombres, cle_privee):
d, n = cle_privee
message = ""
for x in nombres:
message = message + chr(pow(x, d, n))
return message
1. Le déchiffrement redonne "Rendez-vous à 18 h". RSA travaille modulo \(n\) : un nombre \(M \geqslant n\) serait ramené à son reste et ne pourrait pas être retrouvé.
2. Début de la liste : [28766602, 17717906, 4785882, 22928393, 17717906, …] : les deux e de « Rendez » donnent le même nombre \(17\,717\,906\). Caractère par caractère, RSA se comporte comme un chiffrement par substitution : l’analyse de fréquences de la partie A s’applique. D’où le remplissage aléatoire en pratique.
★★ Exercice 12 — Signer #
Pour signer, on chiffre avec la clé privée ; tout le monde vérifie avec la clé publique. On ne signe pas le message lui-même mais son empreinte (fonction fournie empreinte(message, n), un nombre entre \(0\) et \(n-1\) calculé par la fonction de hachage SHA-256 : changer une seule lettre du message change complètement l’empreinte).
-
▶ Écrire
signer(message, cle_privee), qui renvoie \(h^{d} \bmod n\) où \(h\) est l’empreinte, etverifier(message, signature, cle_publique), qui renvoieTruesi \(s^{e} \bmod n\) est égal à l’empreinte du message. -
▶ Signer
"Je dois 10 euros a Bob", vérifier la signature, puis vérifier la même signature avec le message"Je dois 1000 euros a Bob". Qu’est-ce qu’une signature garantit ?
Corrigé
def signer(message, cle_privee):
d, n = cle_privee
return pow(empreinte(message, n), d, n)
def verifier(message, signature, cle_publique):
e, n = cle_publique
return pow(signature, e, n) == empreinte(message, n)
Avec les clés de l’exercice 10 : la signature vaut \(24\,670\,169\) ; verifier renvoie True pour le message signé et False pour « 1000 euros ». La signature garantit l’authenticité (seul le détenteur de la clé privée a pu la produire) et l’intégrité (le message n’a pas été modifié) ; elle ne le rend pas secret.
Partie E — Factoriser \(n\) : l’expérience qui rassure#
★★ Exercice 13 — Quand la factorisation devient lente #
-
▶ Compléter
factoriser(n), qui cherche le plus petit diviseur \(d \geqslant 2\) de \(n\) (tant que \(d \times d \leqslant n\)) et renvoie(d, n // d), ouNonesi \(n\) est premier. Vérifier :factoriser(3233)vaut(53, 61). -
▶ Chacun des nombres suivants est le produit de deux nombres premiers. Mesurer le temps de
factoriser(avectime.perf_counter()), puis recopier et compléter le tableau :
| \(n\) | chiffres de \(n\) | facteurs trouvés | durée (s) |
|---|---|---|---|
1373249 |
\(7\) | ||
133763569 |
\(9\) | ||
13334100011 |
\(11\) | ||
1333361000071 |
\(13\) | ||
133333863333859 |
\(15\) |
-
Par combien la durée est-elle multipliée quand \(n\) gagne deux chiffres ? Expliquer ce facteur à partir du nombre de tours de boucle.
-
Une clé RSA réelle de \(2\,048\) bits a un \(n\) de \(617\) chiffres. En prolongeant vos mesures, estimer la durée de
factorisersur un tel nombre, et la comparer à l’âge de l’Univers (environ \(4 \times 10^{17}\) secondes). (Il existe des algorithmes de factorisation bien meilleurs, mais aucun ne casse une telle clé.) -
▶ Attaque : échanger sa clé publique avec un autre binôme, puis factoriser son \(n\), recalculer \(\varphi\) et \(d\), et déchiffrer un message qu’il vous a envoyé. Combien de temps a-t-il fallu ? Qu’en conclure sur des nombres premiers à \(4\) chiffres ?
Corrigé
def factoriser(n):
d = 2
while d * d <= n:
if n % d == 0:
return d, n // d
d += 1
return None
for n in (1373249, 133763569, 13334100011, 1333361000071, 133333863333859):
debut = time.perf_counter()
f = factoriser(n)
print(n, len(str(n)), f, time.perf_counter() - debut)
| \(n\) | chiffres | facteurs | durée mesurée |
|---|---|---|---|
1373249 |
\(7\) | \(1\,009 \times 1\,361\) | \(0{,}0001\) s |
133763569 |
\(9\) | \(10\,007 \times 13\,367\) | \(0{,}001\) s |
13334100011 |
\(11\) | \(100\,003 \times 133\,337\) | \(0{,}013\) s |
1333361000071 |
\(13\) | \(1\,000\,003 \times 1\,333\,357\) | \(0{,}13\) s |
133333863333859 |
\(15\) | \(10\,000\,019 \times 13\,333\,361\) | \(1{,}26\) s |
3. La durée est multipliée par environ 10 à chaque fois que \(n\) gagne deux chiffres. La boucle tourne jusqu’au plus petit facteur \(p \approx \sqrt{n}\) ; quand \(n\) est multiplié par \(100\), \(\sqrt{n}\) (donc le nombre de tours) est multiplié par \(10\). Le coût est exponentiel en le nombre de chiffres de \(n\).
4. De \(15\) à \(617\) chiffres, on ajoute \(602\) chiffres, soit \(301\) fois un facteur \(10\) : environ \(1{,}26 \times 10^{301}\) secondes, soit plus de \(10^{283}\) fois l’âge de l’Univers.
5. Avec des premiers à \(4\) chiffres, \(n\) en a \(7\) ou \(8\) : factoriser(32431187) renvoie (4931, 6577) instantanément, puis \(\varphi = 4930 \times 6576\) et d = pow(65537, -1, phi) : la clé privée est retrouvée et le message déchiffré en moins d’une seconde. De tels nombres ne protègent rien : la sécurité exige des premiers d’environ \(300\) chiffres chacun (un \(n\) de \(2\,048\) bits a \(617\) chiffres, comme dans la question 4).
Bilan du TP#
À rédiger (une quinzaine de lignes)
-
Pour César, Vigenère et le masque jetable réutilisé : quelle faiblesse a-t-on exploitée ? Qu’est-ce qu’un bon chiffrement ne doit pas conserver du message ?
-
Résumer en quatre étapes la fabrication d’une paire de clés RSA, en citant les fonctions écrites.
-
Sur quoi repose la sécurité de RSA ? Appuyer la réponse sur les mesures de la partie E.
-
Chiffrer et signer : quelle clé utilise-t-on dans chaque cas, et qu’est-ce que chacun garantit ?