Cette année, j’ai eu l’occasion de faire un peu de cryptologie.
Alors moi, dans l’idée, calculer n^(pq) dans un groupe cyclique
d’ordre d, ou décaler-xorer les bits 17 fois pour calculer un hash
cryptographique, ça ne me tente pas du tout.
J’ai cependant découvert qu’il y a d’autres aspects de la crypto qui ne concernent pas l’analyse technique des algorithmes, mais plutôt une analyse « sémantique » de leur utilisation. Par exemple, on a beau avoir un super algorithme de chiffrement, si on envoie la clé en clair avec le message chiffré, ce n’est pas sécurisé. Il y a des façons moins évidentes de se tromper et des gens qui cherchent comment éviter ces pièges.
Dans ce billet, je présenterai juste un exemple assez simple. Ne vous attendez pas à des merveilles tout de suite, mais elles viendront peut-être plus tard.
Étude formelle des protocoles cryptographiques
On fait une différence entre l’étude « formelle » et l’étude « calculatoire » des systèmes utilisant la cryptographie. Dans le modèle formel, on considère les algorithmes cryptographiques comme des boîtes noires, qui marchent toujours et sont incassables, et on étudie l’utilisation qui est faite de ces boîtes noires : sous des conditions données, est-ce que telle utilisation est sûre ?
Dans le modèle calculatoire, on met les mains dans le cambouis en regardant dans les boîtes, dans le but d’avoir des estimations quantitatives de la difficulté calculatoire pour casser une utilisation aux paramètres de sécurité (taille des clés, etc.) donnés. Par exemple, si vous utilisez tel algorithme de telle façon et espérez garantir telle propriété de sécurité, il faudra au moins 280 calculs à un attaquant pour la casser.
Je me limiterai au modèle formel, en utilisant certaines des boîtes noires et notations suivantes :
- chiffrement symétrique
On peut encoder ou décoder un message
Mavec la cléK. Sans la clé, il n’y aucun moyen de décoder le message. Je notesenc(M,K)le message chiffré.- chiffrement asymétrique
Un utilisateur
Upossède une clé publiquepk(U)et une clé secrètesk(U). Il diffuse la clé publique, et garde la clé secrète… secrète. On peut chiffrer un message avec une clé publiquepk(X), et alors pour le déchiffrer il faut avoirsk(X). Cela permet de faire des échanges sans partager un secret commun, comme dans le cas symétrique. Je noteaenc(M,pk(X))le message chiffré.- signature
Toujours dans un cadre asymétrique,
Upeut « signer » un messageMdans le but de garantir qu’il vient bien de lui. Il utilise pour cela sa clé secrète, et obtient une signature qui peut être vérifiée par tout possesseur de la clé publiquepk(U). On notesign(M, sk(X))la signature, et il y a une opérationcheck(M, S, pk(X))qui renvoie vrai si et seulement siSest bien la signature du messageMavec la clé privée correspondant àpk(X). Bien sûr, on ne peut pas récupérersk(X)à partir design(M, sk(X))…
Ça c’est ce que peut faire tout le monde, en particulier les « gentils », pour utiliser la crypto en espérant arriver à leurs fins. Mais que peuvent faire les méchants ? On fait en général des hypothèses paranoïaques : on considère que tout message envoyé sur le réseau peut être lu et même stoppé (avant qu’il arrive à son destinataire) par un méchant attaquant.
Un cas d’école : le protocole d’authentification de Needham-Schroeder
Le cas d’école d’étude des protocoles cryptographiques est le protocole d’authentification de Needham-Schroeder. C’est un protocole destiné à « authentifier » des agents qui communiquent entre eux, c’est-à-dire à garantir qu’ils sont bien en train de parler à la personne à laquelle ils pensent parler.
L’idée générale est d’envoyer un « défi » (challenge) à la personne avec laquelle on parle : un nombre aléatoire qu’on vient de générer, donc introuvable (on appelle un tel nombre un nonce, affreux anglicisme proche du mot « hapax »), chiffré avec la clé de la personne avec laquelle on pense parler. Si elle arrive à déchiffrer ce nonce et à nous le renvoyer, elle est authentifiée.
Pour toute paire d’agents A et B, le protocole procède comme
suit :
A → B : aenc((N_A, A), pk(B))
B → A : aenc((N_A, N_B), pk(A))
A → B : aenc(N_B, pk(B))
A génère un nonce N_A et envoie la paire (N_A, A) (ici A
représente une donnée qui l’identifie : son nom, son adresse mail…)
à B. Il génère à son tour un nonce N_B et renvoie les deux
à A, qui répond en renvoyant N_B. Si tout se passe bien, les deux
ont bien répondu à leurs défis respectifs et se sont mutuellement
authentifiés.
Il est crucial que les défis soient générés aléatoirement, à chaque
session d’authentification. Sinon un attaquant pourrait écouter les
messages et les « rejouer » ensuite dans le futur, en se faisant
passer pour l’un des participants. Par exemple si N_B était une
constante choisie une fois pour toute par B, un attaquant C ayant
observé cet échange pourrait entamer la session en prétendant être
A, et à la troisième étape renvoyer le message aenc(N_B, pk(B)),
faisant ainsi croire à B qu’il est à nouveau en train de parler avec
A.
La notation que j’ai utilisée pour le protocole est relativement informelle. On peut la formaliser un peu mieux, ou encore passer à des descriptions plus fines, qui décrivent par exemple précisément le moment de génération d’un nonce. Les cryptographes utilisent pour cela des variantes du π-calcul, un calcul de processus développé au départ comme base théorique de la programmation distribuée.
Il y a aussi de l’implicite au niveau du lien entre une identité A, et
ses clés pk(A) et sk(A). En particulier, sk n’est pas une simple
fonction, car on veut que seul A puisse calculer sk(A). La façon
dont on associe pk(A) à A peut aussi être modélisée. Ici j’ai
considéré que les participants « connaissent » les clés publiques,
mais on peut aussi supposer un (ou plusieurs) serveurs de
certifications, supposés de confiance, et qui indiquent la clé
publique associée à une identité. Cela complique un peu le protocole,
de façon inessentielle ici.
Enfin, il convient de faire une dernière remarque sur ce protocole :
après l’authentification, N_A et N_B sont des secrets partagés
entre A et B. Il a donc été suggéré d’utiliser ces secrets
partagés pour une communication entre A et B, par exemple comme
clé d’un chiffrage symétrique. En plus d’authentifier A et B, on
souhaiterait donc garantir le secret de ces nonces.
… pas si sûr qu’il n’y paraît
Le protocole que j’ai décrit plus haut est en fait non sûr : il ne
garantit pas l’authenticité des participants. Imaginons un ensemble
d’agents qui s’authentifient mutuellement en utilisant ce
protocole. Certains, comme A et B, sont honnêtes, mais d’autres,
comme I, sont mauvais et cherchent à subvertir le protocole à leurs
fins propres.
Si A entame une communication avec I en essayant de
s’authentifier, I peut en profiter pour se faire passer pour A
auprès de B. Je noterai Î pour l’agent I se faisant passer pour
A ; c’est toujours I, mais l’intention est différente :
A → I : aenc((N_A, A), pk(I))
Î → B : aenc((N_A, A), pk(B))
B → Î : aenc((N_A, N_B), pk(A))
I → A : aenc((N_A, N_B), pk(A))
A → I : aenc(N_B, pk(I))
Î → B : aenc(N_B, pk(B))
L’idée de I est proche des man in the middle
attacks : transférer les paquets sans trop y toucher,
dans le but que les personnes aux deux bouts ne se rendent pas compte
du problème et fassent tout le travail pour lui. Il envoie la demande
de connexion de A à B, et attend la réponse de B. Il ne peut
pas la lire puisqu’elle est chiffrée avec la clé de A (B pense
parler à A). Mais il peut se servir de A pour la déchiffrer à sa
place ! Il envoie ainsi la réponse de B, qui contient le défi N_B,
à A, comme si c’était son propre défi. A le déchiffre et renvoie
N_B à I, qui peut alors répondre triomphalement à B.
L’authentification est brisée, puisque B pense avoir conclu une
session avec A alors que ce dernier n’a jamais parlé à B. De plus,
si les nonce étaient ensuite utilisés comme clé de chiffrement de
messages post-authentification, cela pourrait avoir des conséquences
graves. Par exemple si B est une banque et A un de ses clients :
Î → B : senc("I'm A, please transfer all my money to I", (N_A, N_B))
Quand on parle de « cas d’école », on veut dire qu’il a été très étudié, pas que c’est un exemple fait sur mesure pour les étudiants. Le protocole de Needham-Schroeder a été publié en 1978, et la faille n’a été découverte par Gavin Lowe qu’en 1995. Inutile de dire que les personnes ayant utilisé le protocole entre temps ont dû être très, très stressées…
Gavin Lowe, simultanément à son attaque, a proposé de corriger le protocole de la façon suivante :
A → B : aenc((N_A, A), pk(B))
B → A : aenc((N_A, B, N_B), pk(A))
A → B : aenc(N_B, pk(B))
Dans la deuxième étape, B envoie son identité en plus du
défi. Ainsi, A peut bien vérifier qu’il provient bien de la personne
auprès de laquelle elle essaie de s’authentifier. Dans le cas de
l’attaque, où A pense parler à I qui s’en sert pour le mettre en
communication avec B, la réponse de I à A, issue de B,
deviendrait alors :
I → A : aenc((N_A, B, N_B), pk(A))
et A, voyant que le message ne provient pas de I, interromprait
immédiatement la tentative d’authentification.
Gavin Lowe conclut ainsi son article (PS, 5 pages) :
We conjecture that the revised protocol is safe against all attacks — at least, those attacks not dependent upon properties of the encryption method used. Proving this formally is the topic of current research.
C’était current research en 1995, maintenant on sait faire… tant qu’on sait ce qu’on doit prouver. Ce sera peut-être le sujet d’un prochain billet.
# Cygal
03.04.11, 01:46.