10 janv. 2011

Une librairie de chiffrement/déchiffrement avec OpenSSL

De temps en temps, des idées de design appliqués à un problème particulier me traversent l'esprit. De temps en temps, je les implémente, histoire de voir ce que ça donne. De temps en temps, je suis content de ce que j'obtiens, parce le résultat me parait clair, simple à utiliser et suffisamment efficace pour pouvoir être utilisé dans le cadre d'un projet commercial. Le code que je vais vous présenter aujourd'hui atteint au moins la moitié de ce troisième état - comprendre : j'en suis assez content, mais il est perfectible. Du coup, je vous en présente ici les grandes lignes.

Aujourd'hui : une encapsulation des moteurs de chiffrement Blowfish et RSA de la librairie open source OpenSSL.

Le code lié à cet article a déjà été publié sur developez.com, donc si vous êtes un fidèle de ce forum, vous avez peut être déjà jeté un coup d'oeil dessus. Je vais continuer à maintenir les deux repository (ici et sur DVP), mais les annonces de nouvelles version se feront principalement sur DVP (pour des raisons évidentes : ce blog n'est pas un système de news). Vous pouvez aussi vous abonner à la toute nouvelle newsletter AL&D, et je promet de vous tenir au courant par ce biais des évolutions dans ce code.

Pourquoi encapsuler OpenSSL ?

Il suffit de regarder l'interface de cette librairie pour comprendre : elle est tout sauf lisible. L'autre point est que la librairie est notoirement mal documentée[1] - et de fait, il est difficile d'écrire un code l'utilisation correctement. En l'encapsulant, on réduit nettement le travail du programmeur qui souhaite l'utiliser. Bien sûr, on limite dans une certaine mesure cette même utilisation - mais en dehors des cas complexes, il est quand même relativement aisé de prévoir une interface permettant de faire tout ce qu'il est commun de faire avec cette librairie.

Les points chauds du code

Stratégies choisies à la compilation

Si vous êtes un lecteur assidu de ce blog, il n'y a aucune technique utilisée dans cette librairie qui soit hors de portée. L'idée maitresse est une exploitation judicieuse des stratégies choisies à la compilation - les policy d'Andrei Alexandrescu. Le principe de base est le suivant : pour définir complètement un système de chiffrement, j'ai besoin de connaitre deux choses.

  1. Le type d'algorithme (symétrique, comme Blowfish ou XOR, ou asymétrique comme RSA) ; ce choix détermine les fonctionnalités offertes (c'est à dire le "quoi").
  2. L'algorithme spécifique (Blowfish, XOR, RSA,...) ; ce choix détermine la façon dont les fonctionnalités sont rendues (le "comment").

J'ai choisi de proposer le "comment" sous la forme d'une stratégie choisie au moment de la compilation. Il y a une raison à cela : l'influence au runtime est uniquement liée à l'algorithme, et pas au choix de l'algorithme. En outre, cette technique me permet de spécifier une interface commune pour tous les algorithmes symétriques (et une autre pour tous les algorithmes asymétriques).

L'ensemble des décisions découlent de ces deux choix : le type de la clef, le fait de l'existence ou non d'une clef publique liée à la clef privée, etc. Le "quoi" permet de définir une interface, le "comment" permet de régler les détails d'implémentation. Du coup, cette approche me permet de définir deux classes principales :

  • security::symetric_cryptograph<Cipher,Rng> : un système de chiffrement symétrique basé sur l'algorithme précisé par le paramètre template Cipher (la stratégie)
  • security::asymetric_cryptograph<Cipher,Rng> : un système de chiffrement en clef publique / clef privée basé sur Cipher.

Les deux systèmes différents ont bien évidemment des besoins différents : Blowfish ne peut pas devenir un algorithme asymétrique par la simple volonté d'une mauvaise définition C++. Les types paramètre Cipher ont donc des interfaces différentes, selon les fonctionnalités qu'ils offrent.

Bien sûr, il y a deux moyens d'implémenter une policy : soit l'objet est instancié par le client, et la policy est une classe tout à fait normale ; soit la policy n'est jamais instanciée, et la classe se comporte en fait comme un nom d'espace particulier - toutes les méthodes sont en fait des fonctions statiques à la classe ; les constructeurs et les destructeurs sont cachés pour évité qu'on instancie la classe. C'est cette solution que j'ai choisi pour les deux classes sus-citées, dans le but express de les rendre copiable et constructible par copie (puisque ces classes ont aussi un constructeur par défaut, elles peuvent être stockées dans un conteneur de la librairie standard). Un Cipher particulier n'est pas censé être dépendant d'un état intrinsèque, donc il est inutile de transformer ce qui est essentiellement un service en un objet.

Une seconde policy mineure doit être spécifier : un générateur de nombre aléatoire (qu'on souhaite compatiible avec les exigences d'un système de chiffrement ; en anglais RNG, pour Random Number Generator). Contrairement à l'algorithme de chiffrement, ce RNG est instancié - car contrairement à un algorithme de chiffrement, il est souvent dépendant d'un contexte. Quoi qu'il en soit, ce contexte est souvent copiable (deux cas peuvent survenir : deux RNG avec le même contexte génèrent les même valeurs - le RNG est un PRNG, ou pseudo-random number generator - soit les nombres générés sont vraiment aléatoire, et le contexte ne sert qu'à contrôler l'entropie ; dans ce cas, je n'ai trouvé aucune étude cryptographique qui précise que c'est une mauvaise chose de le copier ; mais je peux les avoir simplement manqué)

Les classes de clef cryptographiques correspondant à ces deux types de systèmes (security::symetric_key<> et security::asymetric_key<>) sont paramétrées par la même classe Cipher. Des typedefs pour les services les plus fréquemment utilisés sont prévus afin de faciliter l'écriture du code[2].. Au final, on se retrouve avec les classes suivantes :

  • security::blowfish_cryptograph (et security::blowfish_key), un système de chiffrement symétrique fonctionnant sur des clefs de 256 bits.
  • security::blowfish_cryptograph (et security::blowfish_key), un second système de chiffrement symétrique - qui n'est PAS résistant aux attaques (évitez de l'utiliser à la place d'un système de cryptographie forte).
  • security::rsa_cryptograph (et security::rsa_key), un système de chiffrement asymétrique basée sur une paire clef public / clef privée.

SHA-256

Voici une des rares implémentations de l'algorithme SHA-256 qui associe ces deux qualités : elle est écrite dans un C++ moderne ; et elle fonctionne sur les systèmes en little endian. L'algorithme est accessible via la fonction security::sha256() qui dispose de plusieurs surcharges. Une optimisation intéressante utilise les iterator tags pour implémenter deux versions différentes d'un algorithme particulier en fonction du type d'itérateurs passé en paramètre.

  • si on fournit à la fonction des itérateurs à accès aléatoire (std::random_access_iterator), on évite une recopie dans un buffer. Les données entre deux itérateurs de ce type sont contiguës en mémoire.
  • dans tous les autres cas, on ne sait pas quelle structure est utilisée en mémoire ; on ne peut donc pas y accéder de manière séquentielle à partir du premier, et on doit donc reconstruire un buffer avant de continuer le traitement.

Au niveau du code, ça se passe comme ça :

 namespace details {
template <class InIt, class IteratorTag> struct sha256_helper { static void execute(InIt first, InIt last, std::vector<unsigned char>& output, const IteratorTag&) { ''// algorithme général, avec copie de buffer } }
template <class InIt> struct sha256_helper<InIt, std::random_access_iterator_tag> { static void execute(InIt first, InIt last, std::vector<unsigned char>& output, const std::random_access_iterator_tag&) { // version spécialisée pour les itérateurs à accès aléaloires } };
}
template <class InIt> void sha256(InIt first, InIt last, std::vector<unsigned char>& output) { typedef typename std::iterator_traits<InIt>::iterator_category iterator_tag;
details::sha256_helper<InIt,iterator_tag>::execute(first, last, output, iterator_tag()); }

Ce code se situe dans srs/security/security_sha_digest.h.

Je n'ai pas effectué de tests de rapidité particuliers, mais l'implémentation semble suffisamment efficace pour être utilisé en production.

Les axes d'amélioration

Dans le code actuel lié à ce billet, la taille des clefs de chiffrement n'est pas paramétrable ; pour changer ce problème, il faudrait modifier les classes de storage qui sont définies ; en procédant ainsi, on devrait pouvoir rajouter aisément un troisième paramètre template aux classes symetric_cryptograph et asymetric_cryptograph qui permettrait de fixer la taille des clefs. L'idéal est de pouvoir faire ça sans faire dépendre le programme utilisateur des headers de OpenSSL - là, ça se complique.

Autre axe d'amélioration possible : l'indépendance vis-a-vis du type std::string. Il est tout à fait légitime de vouloir dépendre de std::basic_string<>, mais std::string est trop réducteur (on a vu dans mon article précédent à quel point l'internationalisation était un sujet sérieux : une libraire C++ ne devrait pas dépendre du type de caractère utilisé pour encoder les chaines de caractère). Ca fait pas mal de changements, car il faut prévoir de supporter les trois paramètres template de basic_string<>. Factoriser ça dans le code tout en le gardant relativement simple à écrire et à lire est une gageure.

Troisième axe d'amélioration : ajouter des fonctionnalités haut niveau. Parmi celles auxquelles je peut penser, il y a des fonctions de signature et de vérification de signature. Celles ci s'appuient nécessairement sur un asymetric_cryptograph<C,RNG> et sur les fonctions de hachage (sha256). On peut rajouter aussi une notion de pipeline qui permettrait de chainer les opération de chiffrement, d'encodage, etc. Dans l'esprit, un pipeline<f,g,h> appliquerait la fonction y = f(g(h(x))) dans un sens, et la fonction inverse x = h_1(g_1(f_1(x))) pour retrouver l'entrée - si une telle opération existe ; ce n'est pas le cas si l'une des opérations est une fonction de hachage. En parlant de celles-ci, d'autres fonctions de la famille SHA-2 peuvent aussi être implémentée (SHA-224, SHA-384 et SHA-512[3]). L'algorithme MD5 et les familles d'algorithmes SHA-0 et SHA-1 ont subi des attaques qui les rendent plus sensibles - voire dangereux dans le cas de MD5 et SHA-0 - à utiliser dans le domaine de la cryptographie, donc je ne vais pas les implémenter.

Enfin, et de manière évidente, il pourrait être intéressant de transformer le code pour utiliser le maximum de fonctionnalités offertes par la dernière mouture du standard C++ sur les compilateurs qui les supporte, dès lors que ces fonctionnalités ont du sens.

Ces améliorations vont être ajoutées petit à petit (pour certaines ; toutes ne présentent pas le même intérêt) dans le code, histoire d'obtenir une librairie de chiffrement en C++ qui ait un sens (au niveau des fonctionnalités offertes) tout en restant aisément utilisable. Je ne prétends pas que le code soit au niveau de celui offert dans la librairie standard au dans Boost, mais il n'en reste pas moins un des seul code s'attachant à proposer une interface C++ moderne pour effectuer des opérations de chiffrement[4].

Information importante

Le code fait partie d'une librairie plus large. Je l'ai limité au strict minimum, et il m'a fallut reprendre une bonne partie du système de Makefile. Le code compile très aisément sous Linux dès lors que la package openssl-dev est installé : il suffit de décompresser l'archive et de lancer la commande make && make install pour construire deux librairies et copier l'intégralité des fichiers headers dans un répertoire ./install. Je n'ai pas prévu encore le système de compilation sous Windows, mais ça ne saurait tarder. A noter que certains options peuvent être changées en modifiant Makefile.opt à la racine, et que ce système de build est susceptible de changements important pour les futures versions.

Notes

[1] pour information, j'ai mis trois jours pour trouver les informations nécessaires à une utilisation correcte de RSA_public_encrypt() ; et encore, j'ai du en deviner certaines par moi même en piochant dans le code source de la librairie. Visiblement, pour certains, open source signifie aussi le code est la documentation

[2] comme std::string est un typedef sur std::basic_string<char> pour des questions de simplicité

[3] leur implémentation étant quasiment identique, on devrait pouvoir factoriser les différences dans des policy

[4] non, crypto n'offre pas une interface C moderne et facilement utlisable. Depuis quand sha256 est devenu un ensemble de fonctionnalités nécessitant la création d'une classe ?

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet