27 juin 2021 — Cryptographie, Python
La commande ci-dessous permet de générer un nombre premier sur 256 bits :
$ openssl prime -generate -bits 25697883500005213530030432141140344527501513253652173335444305361382001588159937
Cette fonctionnalité d’OpenSSL est utilisée notamment pour générer des clefs privées RSA
(openssl genrsa
, on pourra consulter cette entrée au sujet de RSA).
Nous allons présenter ici les quelques concepts mis en oeuvre dans l’algorithme de génération de nombres premiers utilisé par OpenSSL.
Les nombres premiers sont les entiers qui n’ont que deux diviseurs : et lui-même.
Dans un premier temps, nous nous intéressons au problème de décision suivant :
Étant donné un nombre entier positif , est-il premier ?
La fonction basique ci-dessous, en Python, permet de déterminer si un nombre donné est premier :
def is_prime(n):if n <= 1:return Falsefor i in range(2, n):if n % i == 0:return Falsereturn Trueprint([i for i in range(100) if is_prime(i)])
On affiche ainsi les nombres premiers inférieurs à 100 : 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97.
Cependant, cette fonction montre ses limites dès qu’on veut tester des nombres dont les facteurs non triviaux sont « grands » :
is_prime(4208312609**2) # Hangs forever
Le Petit théorème de Fermat, énoncé vers 1640, peut se formuler ainsi :
Si est un nombre premier et un entier quelconque, alors est un multiple de .
Exercice :
Ceci nous donne un critère de non-primalité d’un entier, que l’on peut illustrer par le programme suivant :
def fermat_test(n, max_witness=100):witnesses = range(2, min(n, max_witness + 1))for a in witnesses:if pow(a, n - 1, n) != 1:print(f"{n} is not a prime ({a} is a witness)")returnprint(f"{n} might be a prime")
Par exemple :
>>> fermat_test(4208312609) # 4208312609 is prime4208312609 might be a prime>>> fermat_test(4208312609**2)17709895015068386881 is not a prime (2 is a witness)>>> fermat_test(5472940991761) # 5472940991761 = 199 * 241 * 863 * 1322335472940991761 might be a prime
Le nombre (identifié dans cet article) est un nombre de Carmichael : ce sont les nombres qui ne sont pas premiers mais vérifient pour tout entier premier avec .
Ils sont relativement rares, mais suffisent à mettre en défaut l’algorithme ci-dessus, car il n’est pas facile de trouver des témoins.
Par exemple, et chacun de ces quatre facteurs est un témoin pour le test de Fermat (car si divise …).
L’identification de ces facteurs n’est cependant pas un problème simple, puisque cela suffirait à montrer la non-primalité de …
💡 Le test de Fermat est utilisé dans les tests unitaires de CPython,
comme test sommaire de la primalité du paramètre utilisé par la fonction int.__hash__
.
Si est un nombre premier, on peut écrire avec et impair.
D’après le petit théorème de Fermat, si , alors .
Le nombre est donc solution de l’équation , ou de façon équivalente .
On en déduit, car est premier, que .
Ainsi, par récurrence finie on montre que l’une des deux propositions suivantes est vraie :
Le test de Miller-Rabin consiste à chercher, pour un entier et un témoin donné, si cette propriété est mise en défaut :
COMPOSITE = 'COMPOSITE'NO_WITNESS = 'NO_WITNESS'def miller_rabin_test(n, witnesses):assert n >= 3# Find greatest r s.t. 2^r | n - 1r = 1while (n - 1) % 2**(r + 1) == 0:r += 1d = (n - 1) // 2**rfor a in witnesses:# Check first equalityx = pow(a, d, n)if x in (1, n - 1):continue# Check successive squaresfor i in range(1, r):x = pow(x, 2, n)if x == n - 1:breakif x == 1:return COMPOSITEelse:return COMPOSITEreturn NO_WITNESS
L’algorithme présenté ci-dessus est celui implémenté dans OpenSSL.
Il permet de discriminer rapidement les cas ci-dessous :
>>> miller_rabin_test(4208312609**2, [2])'COMPOSITE'>>> miller_rabin_test(5472940991761, [2])'COMPOSITE'
Cependant, il existe toujours pour certains témoins des « faux-positifs » (on parle de pseudo-premiers forts), comme ci-dessous :
>>> miller_rabin_test(47197, [3])'NO_WITNESS'
La fiabilité des tests de Fermat et de Miller-Rabin réside dans la capacité à trouver, pour un nombre composé, un témoin.
Dans le cas du test de Fermat, on a vu que pour certains nombres (par exemple ceux de Carmichael), il n’est pas garanti que l’on puisse trouver efficacement un témoin.
Dans le cas du test de Miller-Rabin, on a le résultat suivant pour un entier impair et non premier :
La proportion des entiers entre et qui sont témoins de Miller-Rabin pour est supérieure à .
On pourra consulter ce document par Keith Conrad pour une preuve du résultat.
Ceci nous donne un critère permettant de contrôler la probabilité de détection d’un nombre composite, lorsque les témoins sont choisis selon une loi uniforme.
Par exemple :
from random import randrangedef is_probable_prime(n, rounds=10):if n == 1:return Falseif n in (2, 3):return Truereturn miller_rabin_test(n,(randrange(2, n - 2) for _ in range(rounds))) != COMPOSITE
On vérifie ensuite :
>>> is_probable_prime(4208312609)True>>> is_probable_prime(4208312609**2)False
Dans le second cas, la probabilité que ne soit pas détecté comme composite est inférieure à .
Pour des nombres sur 2048 bits, OpenSSL teste jusqu’à 128 nombres aléatoire, ce qui permet d’obtenir une probabilité d’erreur inférieure à .
Dans un formalisme théorique, le problème de décision PRIMES consiste à trouver un algorithme qui, pour un nombre représenté par chiffres binaires, détermine s’il est premier.
L’algorithme déterministe (fonction is_prime
en début d’article), consistant à tester successivement tous les entiers entre et , effectue
un nombre d’opération en (où est une constante qui dépendra des détails d’implémentation de la division).
Comme , ce nombre d’opérations exprimé en fonction de est un .
Ainsi, le problème PRIMES est décidé en temps exponentiel par cet algorithme.
Si l’on autorise l’utilisation d’une source de nombres aléatoires, on parle d’algorithme randomisé.
Le test de Miller-Rabin présenté plus haut nous a permis de définir une fonction is_probable_prime
qui renvoie
en temps polynomial — je vous laisse le justifier — un résultat avec une probabilité d’erreur inférieure à
(pour une passe de Miller-Rabin).
Du point de vue théorique, ce résultat montre que PRIMES appartient à la classe de complexité BPP.
Des chercheurs de l’Indian Institute of Technology Kanpur ont montré en 2002, l’existence d’un algorithme déterministe qui décide de la primalité en temps polynomial en . Ce résultat théorique fort ne donne pas d’implémentation utilisable en pratique.
Nous avons évoqué le test de la primalité d’un entier donné. Comment passer de ce problème à celui de la génération de nombre premiers d’une taille fixe ?
Une première approche consiste à générer aléatoirement des nombres impairs sur bits — dans l’intervalle — jusqu’à en trouver un qui passe le test de primalité.
La question qui se pose est de savoir combien d’essais-erreurs sont nécessaires pour trouver un nombre premier avec cette procédure ?
Commençons par une approche empirique :
import statisticsdef gen_prime_trials(bits):i = 1while True:rnd = randrange(2**(bits - 1) + 1, 2**bits, 2)if is_probable_prime(rnd):return ii += 1print("b\tTrials\tTrials / b")for b in range(2, 64, 5):avg = statistics.mean(gen_prime_trials(b) for _ in range(10**4))print(f"{b}\t{float(avg):.3}\t{avg / b:.3}")
On constate que le nombre moyen d’essais nécessaires semble être (asymptotiquement) proportionnel au nombre de bits :
b Trials Trials / b2 1.0 0.57 2.46 0.35112 3.99 0.33317 5.76 0.33922 7.49 0.3427 9.16 0.33932 11.0 0.34437 12.5 0.33942 14.4 0.34247 16.2 0.34552 17.8 0.34257 20.0 0.35162 21.3 0.343
Le théorème des nombres premiers est un résultat fondamental qui peut se formuler de la façon suivante :
En désignant par le compte des nombres premiers , on a l’équivalence asymptotique : .
Dans l’intervalle , on aura donc « de l’ordre de » nombres premiers.
Pour un tirage aléatoire parmi des nombres impairs de cet intervalle, ceci nous donne une probabilité .
En considérant l’espérance de la loi géométrique associée, on en déduit le nombre moyen d’essais :
On explique ainsi le résultat empirique précédent.
💡 Une petite subtilité
Pour un tirage uniforme d’un nombre de taille , on a montré qu’on pouvait contrôler la probabilité que is_probable_prime(n)
donne un faux-positif.
La procédure gen_prime_trials(b)
effectue des appels successifs à la fonction is_probable_prime
, la probabilité d’obtenir un nombre composite avec cette procédure
est donc supérieure à .
L’article Average case error estimates for the strong probable prime test s’intéresse à l’estimation et au contrôle de cette probabilité.
La procédure décrite ci-dessus présente des limitations pratiques : pour générer un nombre premier sur bits (minimum recommandé aujourd’hui), il faut en moyenne essais.
Chacun de ces essais comporte en particulier les opérations suivantes, qui sont gourmandes en resources et en temps :
Afin de générer des nombres premiers plus rapidement, la plupart des implémentations utilisent des heuristiques qui diminuent le nombre d’appels à ces deux opérations.
OpenSSL génère par exemple un nombre aléatoire incrémenté successivement et testé par une méthode de crible (fonction probable_prime), la librarie crypto de Go intègre également une procédure similaire.
Voici une version simplifiée, en Python, de cette heuristique :
SMALL_PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67,71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149,151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227,229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307,311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389,397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467,479, 487, 491, 499]def has_small_factor(n):for p in SMALL_PRIMES:if p**2 > n:return Falseif n % p == 0:return Truereturn Falsedef gen_prime(bits):while True:rnd = randrange(2**(bits - 1) + 1, 2**bits, 2)delta = 0while rnd + delta < 2**bits:if has_small_factor(rnd + delta):delta += 2elif is_probable_prime(rnd + delta):return rnd + deltaelse:break
Ce programme, bien que simpliste et écrit en Python contrairement à OpenSSL (en C), permet déjà de générer en un temps raisonnable des nombres premiers sur 2048 bits :
>>> gen_prime(2048)20016542084102816971661133761528779520597748125394575671412761118759355941307177853087753246463390886375884900098872655420132639282144229658911423922253339799479017628529934164934573860924527945316043013801018208593884812493637099206436602646960717917991271819643392847147408920343373508517164752603508076643843155606155367699307063482046551001080336218108256721430098736604339134326415304585164153064354334304830212337639704234421718450852252715496644629052246845882396636840210395509260188719227105608356795207222875067678558742355313318698844909063214755154640315114450028082650226711129793844459698307019042102663
💡 Comme indiqué dans le Handbook of Applied Cryptography, un théorème de Mertens
permet de donner une approximation de la probabilité qu’un nombre composé soit éliminé par le crible has_small_factor
.
L’heuristique présentée précédemment introduit un biais potentiel :
le tirage aléatoire effectué par rnd = randrange(...)
est uniforme, mais la procédure
incrémentale consistant à chercher parmi les nombres de la forme rnd + delta
introduit un biais..
On peut facilement le constater en générant une série de petits nombres premiers :
>>> c = Counter(gen_prime(10) for _ in range(10**4))>>> c.most_common(3)[(907, 403), (541, 316), (967, 291)]>>> c.most_common()[-3:][(601, 36), (883, 35), (619, 34)
Ci-dessus, parmi les nombres premiers entre et , on remarque que des nombres comme , ou reviennent relativement peu souvent (on s’attendrait à ce qu’ils apparaissent environ fois en moyenne pour une loi uniforme).
Ces nombres sont des nombres premiers jumeaux (successeurs respectifs de , et ).
Au contraire, est généré plus souvent — le nombre premier qui le précède, , est relativement éloigné.
Ce biais, qui a été discuté sur la mailing list OpenSSL, contribue à faire baisser l’entropie effective du générateur de nombres premiers.
Cet article et cette entrée de blog suggèrent que les collisions observées en pratique sont principalement dues à des défauts d’implémentation de générateurs de nombres aléatoires.
Nous avons volontairement passé sous silence ce sujet crucial, il nécessiterait un article à part entière…
Le code source des fonctions présentées est disponible ici.
Maths et applications, avec les mains et avec du code 💻
Suivre sur Twitter