26 novembre 2020 — Cryptographie, Go, Diffie-Hellman
Le protocole TLS (anciennement SSL) permet de sécuriser les communications sur Internet — authentification, contrôle d’intégrité et confidentialité des échanges.
Ce protocole définit notamment un handshake au cours duquel client et serveur s’accordent sur le choix de la suite cryptographique à utiliser pour sécuriser la communication ultérieure et échangent des clefs cryptographiques.
La page SSL Labs permet de scanner un serveur et de déterminer les versions de TLS et des suites cryptographiques (cipher suites) supportées.
Voici un extrait du résultat pour lipsum.dev :
On constate en particulier que le format des suites cryptographiques a changé avec TLS 1.3.
Dans la RFC 8446 qui définit ce protocole il est indiqué :
Static RSA and Diffie-Hellman cipher suites have been removed; all public-key based key exchange mechanisms now provide forward secrecy.
Nous proposons ici de présenter quelques notions mathématiques qui permettent la mise en oeuvre de cette confidentialité persistante (forward secrecy).
En particulier, TLS 1.3 repose sur des variantes d’un mécanisme d’échange de clefs imaginé en 1976 et utilisant des concepts mathématiques issus de la théorie des groupes.
Une des caractéristiques des mathématiques est l’abstraction.
Cette abstraction consiste parfois à observer différents ensembles, les opérations associées et en déduire des structures communes.
L’une des structures algébriques les plus classiques est celle de groupe, qui intervient dans de nombreuses branches des mathématiques mais aussi en physique théorique, cristallographie, etc.
Un groupe est un ensemble muni d’une opération (notée ici ) qui permet d’effectuer des calculs sur cet ensemble en associant à deux éléments un troisième élément de .
Pour que cette opération définisse une structure de groupe, on exige :
L’exponentiation en base consiste à appliquer l’opération de façon itérée à un élément d’un groupe .
On définit ainsi puis par récurrence pour .
Pour calculer , on peut naïvement effectuer des multiplications successives pour calculer la suite .
Si l’on procède ainsi, le nombre de multiplications à effectuer est .
Il existe une approche beaucoup plus efficace qui consiste à multiplier les résultats intermédiaires par eux-mêmes.
Par exemple, pour calculer , on peut remarquer que (notation binaire) puis :
Ce calcul fait intervenir multiplications au lieu des de la méthode naïve. Pour un exposant , ce nombre de multiplications est de l’ordre de .
Notons que cet algorithme d’exponentiation rapide ne fait aucune hypothèse sur le groupe dont il est question ni sur la loi de composition .
💻 En voici une implémentation (en Go), dans le cas classique de la multiplication des entiers :
// Computes a^n in O(log n) stepsfunc Exponentiate(a int, n int) int {res := 1for n > 0 {if n%2 == 1 {res *= an = (n - 1) / 2} else {n /= 2}a *= a}return res}
Pour un groupe et un élément de ce groupe, le problème du logarithme en base consiste à résoudre l’équation d’inconnue .
Suivant les cas, il existera zéro, une ou plusieurs solutions à cette équation — parfois une infinité.
Un groupe cyclique est un cas particulier de groupe qui a les propriétés additionnelles suivantes :
On a alors et nécessairement (je vous laisse le prouver) .
Le logarithme (discret) en base sur un groupe cyclique à éléments et généré par peut être défini comme une fonction qui vérifie pour tout .
Cette relation ne donne a priori ni formule explicite de calcul ni algorithme autre qu’une méthode par force brute consistant à tester successivement avec .
On effectue alors en moyenne de l’ordre de opérations d’exponentation.
Il est cependant possible de le calculer de façon plus efficace :
Ce paragraphe visant à présenter cet algorithme est facultatif, vous pourrez librement y revenir ensuite.
Cet algorithme proposé en 1971 permet d’obtenir en un nombre d’opérations de l’ordre de .
Notons un groupe cyclique à éléments et l’arrondi supérieur de (noté ).
Définissons alors :
L’application définie par est surjective.
Je vous laisse vous en convaincre en exercice, en considérant pour quelconque la division euclidienne de par .
équivaut à résoudre soit
On est donc amené à chercher une collision entre les séquences et , qui contiennent chacun éléments.
On sait résoudre ce problème en opérations (par exemple avec une table de hachage).
Les indices obtenus vérifient alors ce qui résoud le problème du logarithme discret.
En 1976 est publié un article intitulé New Directions in Cryptography.
Cet article définit l’un des premiers mécanismes cryptographiques d’échange de clef, qui porte le nom d’échange de clés Diffie-Hellman.
Ce mécanisme permet à deux personnes, que nous appellerons Amandine et Bastien, d’établir une clef secrète S que eux seuls connaissent en échangeant sur un canal non sécurisé.
Nous verrons plus loin comment ce mécanisme est utilisé en pratique dans le protocole TLS.
À l’issue de cet échange, Amandine et Bastien ont tous deux connaissances du secret partagé .
Les clefs et circulant sur des canaux non sécurisés, elles peuvent être utilisées par une tierce personne pour tenter d’obtenir le secret .
Si cette personne est capable de calculer l’un des logarithmes discrets ou , elle peut alors obtenir l’un des exposants secrets ou et recalculer .
La résolution du logarithme discret est donc un vecteur d’attaque potentiel contre le mécanisme de Diffie-Hellman.
Bien que cela ne soit pas prouvé dans le cas général, on considère d’ailleurs que Diffie-Hellman est aussi sûr que le problème du logarithme discret.
La difficulté de calcul du logarithme sera donc déterminante dans le choix du groupe cyclique utilisé en pratique.
⚠️ Les implémentations naïves présentées dans cet article ne doivent évidemment pas être utilisées en production, leur but est purement didactique.
Nous proposons ci-dessous une implémentation avec différents groupes cycliques.
Cette implémentation est écrite en Go, dont nous utiliserons le module crypto, mais il n’est pas nécessaire d’être familier avec ce langage pour lire la suite.
CyclicGroupL’interface ci-dessous définit ce qu’on attendra d’une implémentation d’un groupe cyclique.
On y retrouve les notions suivantes :
Identity, Compose, Inverse qui correspondent aux axiomes d’un groupe (élément neutre, loi de composition interne et inversion)Order, Generator qui correspondent au cas d’un groupe cycliquetype CyclicGroup interface {Identity() interface{}Compose(a, b interface{}) interface{}Inverse(a interface{}) interface{}Exponentiate(a interface{}, exp *big.Int) interface{}Generator() interface{}Order() *big.IntString() string}
💡 On a utilisé des empty interface (qui peuvent contenir n’importe quelle valeur) pour typer les éléments du groupe, dont nous ne savons rien a priori. Dans d’autres langages, on pourrait utiliser des types génériques (inexistant en Go).
Rappelons quelques définitions vues dans un article précédent.
On note l’ensemble des nombres entiers modulo ( désigne un entier positif quelconque).
Compter dans revient à compter avec les nombres en considérant tous les calculs comme effectués modulo .
Par exemple, modulo , on a et la table de multiplication de est la suivante :
| x | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 2 | 0 | 2 | 4 | 6 | 1 | 3 | 5 |
| 3 | 0 | 3 | 6 | 2 | 5 | 1 | 4 |
| 4 | 0 | 4 | 1 | 5 | 2 | 6 | 3 |
| 5 | 0 | 5 | 3 | 1 | 6 | 4 | 2 |
| 6 | 0 | 6 | 5 | 4 | 3 | 2 | 1 |
On sait que lorsque est un nombre premier les éléments de différents de sont tous inversibles pour la multiplication : est un corps que l’on notera aussi .
On remarque également sur ce particulier que permet de générer les éléments non nuls de :
, , , , , .
De fait, on montre qu’il existe toujours au moins un nombre dans qui permet de générer tous les éléments non nuls par exponentiation, comme c’est le cas de dans .
Un tel nombre est appelé une racine primitive modulo .
Dès lors, l’ensemble des éléments non nuls de muni de la multiplication est un groupe cyclique, d’ordre .
MultGroupVoici ci-dessous une implémentation très simple des groupes multiplicatifs modulo .
Notons que l’on utilise le type big.Int pour manipuler des entiers de taille arbitraire.
type MultGroup struct {P *big.IntGen *big.Int}func (group *MultGroup) Order() *big.Int {// P - 1return new(big.Int).Add(group.P, big.NewInt(-1))}func (group *MultGroup) Identity() interface{} {return big.NewInt(1)}func (group *MultGroup) Compose(a, b interface{}) interface{} {// a * b % Preturn new(big.Int).Mod(new(big.Int).Mul(a.(*big.Int), b.(*big.Int)),group.P,)}func (group *MultGroup) Inverse(a interface{}) interface{} {return new(big.Int).ModInverse(a.(*big.Int), group.P)}func (group *MultGroup) Exponentiate(a interface{}, exp *big.Int) interface{} {return new(big.Int).Exp(a.(*big.Int), exp, group.P)}func (group *MultGroup) Generator() interface{} {return group.Gen}
💡 On utilise l’exponentiation modulaire des entiers implémentée nativement en Go, plus efficace que l’exponentiation rapide générique présentée précédemment du fait d’optimisations spécifiques au calcul modulaire.
Je vous propose une implémentation naïve (cliquez sur le bouton Run ▶️ pour l’exécuter) de Diffie-Hellman en Go.
Le premier résultat utilise le groupe multiplicatif du corps :
---Diffie-Hellman: F_1291799 multiplicative groupGroup order 1291798Generator 17---Amandine generated secret key:1266948Amandine transmitted (public) key:91778Bastien generated secret key:81716Bastien transmitted (public) key:1089774Amandine computed shared secret:489232Bastien computed shared secret:489232
Évidemment, ce groupe d’ordre n’est pas à utiliser en pratique.
Je laisse d’ailleurs aux lecteurs et lectrices le soin d’implémenter l’algorithme baby-step giant-step décrit plus haut pour casser le secret ci-dessus…
Notons que les groupes multiplicatifs associés aux corps finis sont le choix d’origine pour l’échange de Diffie-Hellman et qu’ils sont encore utilisés aujourd’hui, avec des groupes dont le paramètre est sur 2048 bits ou plus.
L’équation d’inconnues dans le corps (avec ) définit une courbe elliptique sur .
Le sujet est bien trop vaste pour une vraie présentation dans le cadre de cet article, remarquons simplement ceci :
💡 Parmi les courbes qui ont ces propriétés, les courbes elliptiques ont de plus un genre égal à . Le genre désigne ici une notion de géométrie algébrique qui coïncide dans le cas des courbes complexes avec une notion topologique assez visuelle : les courbes elliptiques sur ont la même topologie qu’un donut.
À toute courbe elliptique peut être associée une loi de groupe.
Cette loi est commutative et associe à deux points et de la courbe un troisième point noté .
L’idée générale est qu’en considérant deux points situés sur cette courbe, on peut en produire un troisième par un calcul algébrique.
Sans rentrer dans les développements, deux points et d’une courbe plane définissent une droite — l’unique droite qui passe par et (si , on considère la droite tangente à la courbe en ce point).
Cette droite admet une équation de la forme où .
La recherche de tous les points d’intersections de cette droite avec une courbe elliptique amène à résoudre une équation du troisième degré en .
Par exemple pour la courbe définie plus haut :
Sous certaines hypothèses supplémentaires (notamment pour prendre en compte le cas ci-dessus…), on en déduit un mécanisme permettant d’associer aux points et un troisième point .
On montre alors que l’opération ainsi introduite définit une loi de groupe commutatif.
Notons que malgré le vocabulaire de la géométrie plane utilisé ici (droite, tangente), cette construction est purement algébrique et possible sur n’importe quel corps de base — en particulier les corps finis.
Le package elliptic de Go introduit une interface Curve (reproduite ci-dessous)
décrivant une courbe elliptique ainsi que différentes implémentations.
type Curve interface {// Params returns the parameters for the curve.Params() *CurveParams// IsOnCurve reports whether the given (x,y) lies on the curve.IsOnCurve(x, y *big.Int) bool// Add returns the sum of (x1,y1) and (x2,y2)Add(x1, y1, x2, y2 *big.Int) (x, y *big.Int)// Double returns 2*(x,y)Double(x1, y1 *big.Int) (x, y *big.Int)// ScalarMult returns k*(Bx,By) where k is a number in big-endian form.ScalarMult(x1, y1 *big.Int, k []byte) (x, y *big.Int)// ScalarBaseMult returns k*G, where G is the base point of the group// and k is an integer in big-endian form.ScalarBaseMult(k []byte) (x, y *big.Int)}
Ce package implémente notamment les courbes P-256 et P-384 recommandées par le NIST et définies sur des corps avec premier (sur et bits respectivement).
Ces courbes sont très utilisées en pratique, bien qu’elles soient l’objet de soupçons de backdoor par la NSA…
Ci-dessous, voici ce que donne notre implémentation de Diffie-Hellman avec la courbe P-256 (exécuter en ligne) :
---Diffie Hellman: Elliptic Curve P-256Field F_115792089210356248762697446949407573530086143415290314195533631308867097853951Group order 115792089210356248762697446949407573529996955224135760342422259061068512044369---Amandine generated secret key:7342462952232507609821760396776010434242597936110644963427068432176148435477Amandine transmitted (public) key:4298064935869640021519201203011003733572235803818690789383769808073692516886Bastien generated secret key:110170395102730437573897103177357816686836430099641375606421793187578417842509Bastien transmitted (public) key:6169708491757107974945465047131128230446890469460906853603235544851377683418Amandine computed shared secret:85470343014045070211971750616744066840724631640773885304746048731416023347568Bastien computed shared secret:85470343014045070211971750616744066840724631640773885304746048731416023347568
💡 On remarque que l’ordre du groupe généré est relativement proche de celui du corps fini utilisé :
sur leurs chiffres, les premiers coincident.
C’est une conséquence d’un théorème connu…
Avant TLS 1.3, il était possible d’utiliser le même couple de clefs cryptographiques pour l’authentification et l’échange des clefs de session.
Ce mécanisme est qualifié de statique (par exemple static RSA, lorsque l’algorithme sous-jacent est RSA).
Avec TLS 1.3, des clefs asymétriques à usage unique (clefs éphémères) doivent être utilisées pour chaque session.
Ceci permet de garantir une confidentialité persistante : si une clef privée est compromise dans le futur, elle ne pourra pas être utilisée pour déchiffrer a posteriori des communications chiffrées qui auraient été conservées.
Le mécanisme de Diffie-Hellman permet de satisfaire cette exigence, en partageant une clef secrète — la clé « pre-master » — basée sur des clefs secrètes (client) et (serveur) éphémères.
Les lecteurs et lectrices souhaitant en savoir plus sur le fonctionnement de TLS pourront lire cette présentation par Cloudflare.
Le livre An Introduction to Mathematical Cryptography, utilisé comme source d’inspiration pour cet article, est une référence pour approfondir les notions théoriques.