Cet article est la suite de mon article présentant les PRNG, les CSPRNG et l’entropie. Il a pour but de présenter l’implémentation que j’ai faite pour mon projet d’ADA d’un CSPRNG-like
Source d’inspiration
Pour la création de mon CSPRNG, je me suis beaucoup inspiré d’une part du générateur de pseudo-aléatoire de Linux, /dev/random, et d’autre part de The Linux PseudoRandom Number Generator Revisited. Ce dernier est un rapport publié en 2012 par 4 chercheurs sur le fonctionnement du CSPRNG de Linux, avec une analyse de sécurité. Je vous en conseille fortement sa lecture, tant il est passionant et relativement accessible.
Le générateur de pseudo-aléatoire de linux est situé dans le fichier drivers/char/random.c, et j’ai principalement utilisé cette interface pour m’y retrouver et naviguer dans le code du kernel facilement.
Le CSPRNG
Pour des raisons de simplicité, et en tenant compte de diverses contraintes auxquelles j’ai du faire face, j’ai bien évidemment adapté ce générateur pour le simplifier et le faire correspondre plus précisemment à nos besoins.
Attention ! La première règle lors de lecture de documentation sur la cryptographie est ne cherchez pas à réimplémenter les choses vous-mêmes, vous allez vous planter
. Je ne prétends pas pouvoir déroger à cette règle, et je peux même affirmer de manière quasi-certaine que le CSPRNG que je présente ici n’en est pas un, et possède des failles d’implémentation (j’en possède quelques unes en tête). Néanmoins, je considère ce travail comme étant intéressant, comme étant une bonne approche pour se familiariser avec des concepts de cryptographie, et je vous déconseille donc de l’utiliser pour des projets sérieux pour lesquels la sécurité est vraiment importante
La pool d’entropie
De la perception que je peux avoir du générateur de Linux, tout s’articule autours de la pool d’entropie.
En réalité, comme présenté dans The Linux PseudoRandom Number Generator Revisited, Linux possède 3 pool d’entropies, une “principale” et deux autres pour /dev/random et pour /dev/urandom. Je me suis contenté d’une seule pool, ce qui me semblait largement suffisant
Une pool d’entropie peut se représenter comme une sorte de gros tableau de taille fixe dans lequel on va mélanger de l’aléatoire. De la même manière qu’une piscine, la taille du tableau est fixe, mais la quantité d’entropie contenue dedans (l’eau de la pisicne pour poursuivre l’analogie) peut varier.
Cette piscine d’entropie, d’aléatoire donc, représente l’état interne de notre PRNG. Le contenu de cette piscine d’entropie ne doit pas être divulgué, pour éviter de perdre en sécurité.
Pour notre implémentation, je me suis orienté vers une pool consituée d’un tableau de 64 uint32_t
. La quantité maximale d’entropie est donc de 64 * 32 bits, ce qui semble suffisant.
La collecte d’entropie
Pour collecter de l’entropie, il faut essayer de trouver des sources de “pur” aléatoire qui seraient disponibles sur notre système. Notre projet étant prévu pour une carte embarquée, cette dernière dispose d’un accéléromètre 3 dimensions, qui semble être un bon candidat pour extraire des bits d’aléatoire pur, via les légers mouvements qu’il détecte de manière permanente, et le bruit naturel des capteurs.
Pour récupérer les informations du gyroscope, on se base sur les bibliothèques ADA fournies, donc l’une qui nous expose plusieurs interfaces Gyro
pour configurer l’accéléromètre et récupérer des valeurs :
Il suffit de collecter périodiquement l’état actuel de l’accéléromètre et calculer le delta entre 2 valeurs. Avec ce delta, on a réussi à récupérer une source d’aléatoire, et on a des valeurs prêtes à être exploitées, via notre fonction de mixage.
La board, en train de collecter de l’entropie dans sa pool grâce à l’accéléromètre. En la remuant, j’augmente le delta mesuré (première ligne), pour remplir plus vite l’entropie contenue dans la board (ligne 3)
La fonction de mixage
Pour pouvoir intégrer ces quelques bits d’aléatoire issu du capteur, il faut utiliser une fonction de mixage. Cette fonction s’occupe de mélanger la pool et d’y intégrer ces quelques bits, pour les répartir et éviter “d’écraser” l’aléatoire déjà présent dedans. Pour procéder ainsi, je me suis inspiré encore une fois de la fonction de mixage de linux pour écrire la mienne.
Cette fonction prend en argument un int
d’entropie à intégrer, et la struct
représentant ma pool (avec son state). On itère sur des groupes de 8 bits de l’int
(char
par char
), et chaque octet est d’abord rol32
selon un facteur variable, puis est intégré à la pool grâce à un Linear Feedback Shift Register.
Les paramètres du LFSR sont les mêmes que celui de linux, en tenant compte du fait que celui-ci est conçu pour une piscine de 128 uint32_t
(contre 64 pour la mienne). Ces paramètres ne sont donc pas optimaux pour ce cas concret, mais je m’en contenterai.
L’octet ainsi obtenu n’est pas directemnt intégré dans la pool, mais est d’abord légérement modifé. Les 3 bits de poids faible sont envoyé dans une twist table, permettant d’effectuer rapidement une multiplication sur GF(2^32) (lookup-table). Le Linux PseudoRandom Number Generator Revisited explique que cette twist table permet de respecter les recommendations des polynômes CRC-32-IEEE 802.3. Les 29 bits de poids forts sont eux shiftés de 3 vers la droite, et le tout est XORé.
Le code de la fonction est donc le suivant :
La fonction d’extraction
L’étape suivante pour avoir un CSPRNG qui fonctionne et qui est utilisable, est d’avoir un moyen de pouvoir extraire de l’entropie de notre pool, et de la renvoyer sous forme de bits pseudo-aléatoires.
De la même manière que pour Linux, ma fonction pour extraire de l’aléatoire de la pool procède en plusieurs étapes :
- Un hash de la pool entière est effectué avec chacha20
- Chacha20 n’étant pas une fonction de hashage, le résultat produit est plié sur un tableau de 4
uint32_t
(avec des super XOR) - Le hash est mis de coté, et est remixé dans la pool en utilisant la fonction d’insertion présenté ci-dessus
- Un extrait de la pool est de nouveau pris, à un index variable
- Cet extrait de 4
uint32_t
est XORé avec le hash de la pool, et est plié sur 2uint32_t
- Ce résultat final est renvoyé, un byte à la fois, à l’utilisateur
Le code est le suivant, en reprenant et modifiant l’extrait de code précédant :
Ce code est évidemment perfectible, mais c’est déjà un bon début pour extraire des valeurs aléatoires.
On notera l’utilisation d’une fonction maison, credit_entropy
, à la fin de la fonction. Cette fonction est utilisée pour mettre à jour le compteur d’entropie de la pool, présenté par la suite.
A propos de chacha20
Chacha20 est un algorithme de chiffrement par flot qui date de 2008. Cet algorithme possède plusieurs caractéristiques, notamment celle d’être assez rapide à implémenter et surtout d’être très rapide car assez simpliste. En revanche, malgré sa simplicité, l’algorithme est néanmoins sûr. Il existe plusieurs cryptanalises qui ont été effectué, et aucune n’a fait part de véritable problèmes avec Chacha20. Comme souvent dans ce genre de cas, cela signifie qu’il existe potentiellement des faiblesses dans l’algorithme qui ne sont pas connues, mais qu’il est bien plus probable que les faiblesses viennent d’erreurs d’implémentations, souvent par canaux auxilliaire comme des attaques temporelles.
Ici Chacha20 est utilisé comme fonction de hashage, ce qui est un usage un peu détourné, pour certaines raisons. La première est la simplicité d’implémentation de Chacha20 : seulement quelques dizaines de lignes. De plus, Chacha20 est une fonction de chiffrement par flot qui est très performante, et qui fonctionne donc très bien sur du matériel embarquée. Par ailleurs, les propriétés de cette fonction de chiffrement dans l’utilisation que j’en ai sont très similaires à une fonction de hashage :
- Pas de possibilité de revenir en arrière (car il faudrait la clé, et celle-ci est modifiée au fur et à mesure)
- Obtention d’un résultat assimilable à une empreinte
- Probabilité réduite de collisions, et bonne distribution des résultats parmi l’ensemble des probabilités
rngtest-ons chacha20
Même si effectivement certains raccourcis sont pris et certaines approximations sont faites (encore une fois, ce que je présente n’a pas pour but d’être parfaitement robuste face à n’importe quelle attaque (et ne l’est pas !), ne faites pas confiance à mon algorithme sorti à moitié de mon chapeau si vous êtes paranos (ou juste sain d’esprit)), on peut avoir une approximation de la véracité du dernier point présenté juste au dessus assez facilement :
rngtest
est un binaire permettant de tester la répartition aléatoire d’un bloc de données, en suivant les recommendations de la FIPS 140-2. En l’occurence, je teste un flux de données pseudo-aléatoire chiffré par flux avec chacha20 grâce à openssl (avec un super mot de passe). Les résultats de rngtest
sont assez bons (0.08% d’échec pour les blocs, ce qui est faible et proche du taux d’échec de /dev/urandom
lui même).
De plus, j’étais curieux de découvrir Chacha20 concrètement, et vu les contraintes qui me sont imposées pour ce projet, il est clairement plus marrant d’utiliser Chacha20 qu’un banal sha1. (bouuuh sha1, bouh !).
Comptons de l’entropie
Un lecteur aggueri aura remarqué la présence dans ma struct entropy_pool
d’une valeur entropy_count
. En effet, il est bien beau d’avoir une piscine remplie d’aléatoire, mais il est plus d’intéressant d’avoir une piscine remplie d’aléatoire disposant d’une sonde indiquant à la louche combien “d’aléatoire” j’ai dedans.
C’est donc le rôle de cet attribut, dont la valeur n’est censé varier qu’entre 0 et ma superbe macro MAX_ENTROPY
qui a pour valeur (POOL_SIZE << (5 + ENTROPY_SHIFT))
, genre à peu près le nombre de bits qu’il y a dans la pool, fois 8 (explications plus tard).
Ce compteur n’a pas pour but d’être modifé manuellement, mais plutôt via la fonction int credit_entropy(int nb_bits, struct entropy_pool *pool)
et sa petite soeur bien utile, int entropy_estimator(int x)
.
entropy_estimator
Dans le fonctionnement de mon PRNG, il n’est pas du ressort de la fonction d’ajout de créditer de l’entropie, mais de l’appelant de le faire.
Mon PRNG étant fortement inspiré de celui de Linux, l’ajout d’entropie nulle dans la pool n’est pas censé réduire l’entropie. Il est prouvé pour la fonction de mixage de linux que l’entropie de la piscine après ajout est au moins égale à l’entropie de la piscine avant ajout (avec des subtilités rigolotes, mais je vous invite une fois de plus à lire The Linux PseudoRandom Number Generator Revisited pour ça).
Ainsi, un utilisateur désirant ajouter de l’entropie dans la piscine peut le faire sans se soucier outre-mesure de la potentiel piètre qualité de son entropie, car le compteur ne sera pas nécéssairement incrémenté. Plus précisement, dans le pire des cas, l’entropie réelle de la piscine n’est pas augmentée (car l’entropie ajoutée était effectivement basse) et son compteur non plus. Dans le meilleur - mais pessimiste - des cas l’entropie réelle augmente, et le compteur reste tel quel.
L’estimation de la quantité d’entropie contenue dans un int dépend évidemment de la source, mais dans mon cas la source est unique (du moins pour le moment, j’espère recevoir du matériel … spécial, pour obtenir une autre source d’entropie, dans un futur proche), donc la fonction aussi. Cette dernière est également inspirée de l’estimation pessimiste de l’entropie ajoutée dans linux pour des sources variées, comme celle des variations des cycles d’horloges.
Voici le code de celle ci, où X est l’entropie désireuse d’être ajoutée :
credit_entropy
Une fois le nombre de bits d’entropie à ajouter comptabilisé, ce dernier est crédité via une fonction credit_entropy
. Cette dernière s’assure que le compteur ne dépasse pas les limites [0, MAX_ENTROPY]
, et calcule la quantité réelle d’entropie à ajouter. En effet, plus la piscine est remplie, plus “l’aléatoire se marche sur les pieds” pour parler par analogie. Il est plus difficile d’ajouter réellement de l’aléatoire quand la grosse majorité des bits sont déjà aléatoires. La fonction credit_entropy
tente donc, toujours en s’inspirant du travail fait pour le PRNG de linux, de rendre compte de cet effet.
Il est intéressant de noter qu’à cause de cet effet, l’entropie à rajouter est souvent légèrement inférieure au nombre entier juste au dessus. Par exemple, si la pool contient 1 bit d’entropie, le calcul du nombre réel de bits d’entropie à ajouter pour un appel à credit_entropy
avec 8 bits à crédite donnera une valeur proche de 7.98. Il serait donc dommage de perdre le crédit des 0.98 bits en arrondissant à 7, mais cela semble néanmoins être une mauvaise idée que de créditer plus que ce qui est réellement apporté.
Pour cette raison, le compteur d’entropie tiens également compte de 8ième de bits. Ainsi, la macro ENTROPY_SHIFT
permet le passage de nombre entiers de bits à des 8ièmes de bits. Ainsi, pour reprendre l’exemple d’au dessus, 7.875 bits seront réellement crédités.
Encore une fois, je ne suis (malheureusement) pas l’auteur de cette idée, et je ne fais que l’emprunter à Linux, toujours en adaptant pour mes besoins :
La fonction renvoie une enum, utilisée pour savoir côté ADA à quel point la piscine d’entropie est pleine. (EMPTY
et FULL
sont les extrêmes, et les 3 valeurs restantes se séparent chacune un tiers).
Résultats
Les résultats obtenus via ce pseudo-CSPRNG sont en réalité assez satisfaisant, et largement suffisant pour nos besoins de génération de nombre premiers. Le pourcentage de blocks non-retenus par rngtest
est de 0.35% en moyenne, là où /dev/urandom
se situe autours des 0.09% (facteur 4 donc). Il faut d’ailleurs retenir que ce test n’est qu’un indicateur, et n’est ni nécéssaire ni suffisant pour indiquer une quelconque faiblesse dans un CSPRNG, tant les vecteurs d’attaques sont nombreux.
En pratique, il m’a fallut plusieurs journées de travail pour débroussailler le terrain, comprendre l’implémentation de linux, les optimisations qui sont faites, réduire le scope de ce dont j’avais réellement besoin et l’implémenter. Je suis assez satisfait du résultat obtenu, car même si je sais que mon implémentation ne tiendrai pas vraiment la route face à un adversaire avec beaucoup (du moins je l’espère !) de moyens, l’implémentation tiens compte de contraintes de la carte, est capable de trouver sa source d’entropie et fonctionne au final très bien.
Le code au total fait quelques centaines de lignes - ce qui est finalement assez peu -, même s’il est perfectible. Je pense le mettre en ligne quelque part dans un futur proche, où les PR seront les bienvenues.
Leave a Comment
Your email address will not be published. Required fields are marked *