Libsodium pour les nuls, ou la cryptographie en PHP

La cryptographie, plus communément appelé crypto, est une des disciplines de la cryptologie s’attachant à protéger des messages (assurant confidentialité, authenticité et intégrité) en s’aidant souvent de secrets ou clés.

via wikipedia

Avez-vous déjà eu besoin :

  • de protéger l’accès à une ressource sans pour autant stocker d’information en base ?
  • de stocker des informations sensibles de manière sécurisée ?
  • de vérifier l’authenticité d’un message ?

Après la lecture de cet article, vous saurez comment résoudre ces problématiques de manière sécurisée.

Qu’est ce que libsodium ?

Libsodium est une librairie écrite en C pour faire de la cryptographie. Elle est moderne (v1 en 2014), portable, écrite par Frank Denis 🇫🇷 et surtout très simple d’utilisation.

L’extension libsodium est packagée par défaut depuis PHP 7.2. Pour les versions de PHP antérieures à 7.2, il faudra installer une extension packagée en PECL. À la place de la PECL, il est aussi possible d’utiliser le polyfill sodium_compat.

L’extension libsodium va exposer une série de fonctions dans PHP afin de faciliter nos usages cryptographiques. Vous pouvez vérifier que l’extension est bien installée grâce à php --re sodium qui va lister toutes les fonctions, classes, et constantes définies par l’extension.

Prérequi à la crypto : Générer des valeurs aléatoires

La génération de valeurs aléatoires est quelque chose de très compliqué en informatique. À tel point que certaines entreprises comme Cloudflare utilisent des LavaLamp pour en générer. La génération de valeurs est primordiale lorsque l’on fait de la crypto. Un générateur de nombres (pseudo) aléatoires est ce que l’on appelle un Cryptographically Secure Pseudo-Random Number Generator (CSPRNG).

libsodium expose plusieurs fonctions pour en générer. Cependant depuis PHP 7.0, il existe deux nouvelles fonctions random_bytes() et random_int() qui ont un niveau pseudo aléatoire acceptable. C’est pourquoi ces fonctions n’ont pas été portées lors de l’intégration de libsodium à PHP 7.2.

Des cas d’usages ?

À travers une série de use cases concrets, nous allons voir comment utiliser cette extension.

Protéger l’accès à une ressource

Prenons l’exemple d’un site web qui affiche la position géographique d’un arrêt de bus sur une carte. Le fournisseur des cartes (mapbox, google maps, …) nous propose une API, mais cette API est authentifiée et rate limitée (nous ne pouvons l’appeler que X fois par jour).

Nous avons besoin de protéger notre point d’entrée d’API qui retourne une carte en fonction de certains paramètres. Nous pourrions stocker en base de données toutes les possibilités, mais cela nécessitera du code, de la maintenance, et de la place en base de données.

Une alternative possible consiste à signer les requêtes :

  1. le serveur génère du HTML contenant une balise image :
<img src="/image-map?lat=xxx&long=yyy&signature=zzzz">
  1. le navigateur demande l’image au serveur ;
  2. le serveur va vérifier que la signature de xxx et yyy est bien zzzz ;
  3. le serveur va chercher l’image chez le fournisseur de cartes ;
  4. le serveur renvoie l’image ;

Il est maintenant impossible de demander une carte qui n’a pas été émise par le serveur. Autrement dit, personne ne peut utiliser vos credentials d’API pour générer ses propres cartes qu’il utilisera sur son site.

Il faut commencer par générer une clé qu’il faudra bien garder au chaud et ne jamais perdre :

$key = sodium_crypto_generichash_keygen();

Puis il faut générer une signature pour une carte :

// Construire un message que l'on va signer
$msg = "lat=XXXX;long=YYYY";

// Générer la signature
$signature = sodium_crypto_generichash($msg, $key);
$signatureAsTxt = bin2hex($signature);
dump(['signature' => $signature, 'signatureAsTxt' => $signatureAsTxt]);

Ce qui va nous afficher quelque chose comme :

array:2 [
  "signature" => b"\x19íOSÉý\x11YßÜ>B╩ûÐrõl\x02>^n b>ª©[ÖìÈ"
  "signatureAsTxt" => "19a14f5390ec1159e19a3e42ca96d172e46c023e5e6eb6623ea6b8b25b998dd4"
]

que nous pourrons envoyer au client :

<img src="/image-map?lat=XXXX&long=YYYY&signature=19a14f5390ec1159e19a3e42ca96d172e46c023e5e6eb6623ea6b8b25b998dd4">

Lorsque une requête HTTP arrive, nous allons refaire le même processus puis vérifier que les deux signatures sont identiques :

$lat = $_GET['lat'];
$long = $_GET['long'];
$signatureAsTxt2 = $_GET['signature'];
$msg2 = "lat=$lat;long=$long";
$signature2 = sodium_crypto_generichash($msg2, $key);

// Nous vérifions les deux signatures :

$ok = hash_equals($signature2, hex2bin($signatureAsTxt2));
if (!$ok) {
    throw new \Exception('Cette requête n\'est pas valide');
}

La génération du message ($msg) peut être une tâche fastidieuse s’il y a beaucoup de paramètres. Dans ce cas, il sera plus simple d’utiliser les fonctions sodium_crypto_generichash_*() :

$hashState = sodium_crypto_generichash_init();
sodium_crypto_generichash_update($hashState, 'XXXX');
sodium_crypto_generichash_update($hashState, 'YYYY');
$msg = sodium_crypto_generichash_final($hashState);
//...

Stocker des informations dans un fichier via un mot de passe

Prenons l’exemple d’un projet où certains développeurs peuvent se connecter via SSH aux serveurs de production. Vous ne voulez pas avoir un fichier avec tous les mots de passe sur votre réseau (ou chez Google Drive, Dropbox, Github, …) en clair, et vous avez raison. Cependant, stocker un fichier chiffré dans le dépôt Git que l’on ne peut « ouvrir » qu’avec un mot de passe vous convient ? Alors cette section est faite pour vous.

Il faut commencer par générer une clé qu’il faudra bien garder au chaud et ne jamais perdre :

$key = sodium_crypto_secretbox_keygen();

Ensuite il faut générer un nonce). Contrairement à la clé, le nonce n’a pas besoin d’être privé. Nous pourrons le sauvegarder à côté de la valeur chiffrée.

$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);

Et enfin, nous pouvons chiffrer le message :

$encrypted = sodium_crypto_secretbox('my super secret data', $nonce, $key);

Et pour déchiffrer le message chiffré :

$decrypted = sodium_crypto_secretbox_open($encrypted, $nonce, $key);

Authentifier un document

Pour ce cas d’usage, prenons l’exemple du serveur qui génère des factures au format PDF. Il est assez facile de modifier un PDF, et donc vous voulez garantir l’intégrité du document et de son contenu. Concrètement, vous ne voulez pas que quelqu’un de mal intentionné fabrique de fausses factures qui auraient été émises par votre site web. Vous voulez également que n’importe qui puisse valider que le document a bien été émis par votre site web. Par exemple, qu’une assurance puisse vérifier qu’un client a bel et bien acheté tel ou tel article.

Grâce à ce système, les assureurs n’auront pas besoin de communiquer avec vous pour authentifier la facture.

La première étape consiste en la génération d’une clé qu’il faudra conserver secrètement :

$keyPair = sodium_crypto_sign_keypair();

À partir de cette clé, nous allons extraire une partie privée qui va nous servir à signer des documents et une partie publique que nous pourrons diffuser à tout le monde :

$keyPairSecret = sodium_crypto_sign_secretkey($keyPair);
$keyPairPublic = sodium_crypto_sign_publickey($keyPair);

Grâce à cette clé, nous pouvons maintenant signer le document :

$message = 'Hello';

$signature = sodium_crypto_sign_detached($message, $keyPairSecret);

Maintenant, nous pouvons envoyer le document avec sa signature au client. Celui-ci et l’assureur pourront authentifier le document de la manière suivante :

$ok = sodium_crypto_sign_verify_detached($signature, $message, $keyPairPublic);
if (!$ok) {
    throw new \Exception('Le document a été modifié');
}

Envoyer un message super sécurisé à un utilisateur

Dans certains cas, nous voulons un niveau de sécurité maximal où même HTTPS ne suffit pas. HTTPS peut être compromis à différents niveaux : une faille dans SSL, une entreprise qui fait un Man In The Middle, etc… Pour palier ce problème, il est possible de générer un message que seule une personne pourra lire. Pour ce faire, nous allons utiliser un chiffrement asymétrique (comme dans le chapitre précédent) mais avec plus de clés.

Un cas concret d’utilisation est un client mail, où la sécurité doit être maximale.

Le principe est le suivant :

  1. Alice crée une paire de clés asymétriques (publique et privée) ;
  2. Bob crée une paire de clés asymétriques (publique et privée) ;
  3. Ils s’échangent leurs clés publiques ;
  4. Alice chiffre son message avec sa clé privée puis avec la clé publique de Bob ;
  5. Alice envoie alors le message chiffré à Bob ;
  6. Bob reçoit le message chiffré d’Alice ;
  7. Bob va alors déchiffrer le message avec sa clé privée, puis avec la clé publique d’Alice.

Grâce à ce double chiffrement :

  • Le message n’est déchiffrable que par Bob ;
  • Même Alice ne peut pas déchiffrer le message qu’elle a elle-même chiffré ;
  • Bob peut s’assurer que ce soit Alice qui a émis le message.

Pour réaliser ceci, il va falloir exécuter les étapes suivantes :

Sur l’ordinateur d’Alice (en général, un serveur) :

// Cette clé est à garder secrète
$aliceKey = sodium_crypto_box_keypair();
$aliceKeySecret = sodium_crypto_box_secretkey($aliceKey);
// Cette clé est diffusée à Bob
$aliceKeyPublic = sodium_crypto_box_publickey($aliceKey);

Sur l’ordinateur de Bob (en général, en Javascript dans le navigateur, ou sur un autre serveur dans le cas d’API) :

// Cette clé est à garder secrète
$bobKey = sodium_crypto_box_keypair();
$bobKeySecret = sodium_crypto_box_secretkey($bobKey);
// Cette clé est diffusée à Alice
$bobKeyPublic = sodium_crypto_box_publickey($bobKey);

Alice va chiffrer son message et l’envoyer à Bob :

$message = 'hello';

$nonce = random_bytes(SODIUM_CRYPTO_BOX_NONCEBYTES);

$encryptionKey = sodium_crypto_box_keypair_from_secretkey_and_publickey($aliceKeySecret, $bobKeyPublic);
$encrypted = sodium_crypto_box($message, $nonce, $encryptionKey);

Bob n’a plus qu’à déchiffrer le message d’Alice :

$decryptionKey = sodium_crypto_box_keypair_from_secretkey_and_publickey($bobKeySecret, $aliceKeyPublic);
$decrypted = sodium_crypto_box_open($encrypted, $nonce, $decryptionKey);

Utilitaires

libsodium expose aussi quelques utilitaires qui sont plus sécurisés que les fonctions natives de PHP. En effet, libsodium est moins sensible au Side Channel Attack :

  • sodium_bin2hex() / sodium_hex2bin() : Conversion binaire <-> hexa ;
  • sodium_bin2base64() / sodium_base642bin() : Conversion binaire <-> base64 ;
  • sodium_increment() : Incrémente un nombre (très grand);
  • sodium_compare() / sodium_memcmp() : Compare deux valeurs en temps constant. Cependant depuis PHP 5.6 hash_equals() est suffisant ;
  • sodium_pad() / sodium_unpad() : Permet de compléter une string par des 0 pour obtenir une taille fixe. Cela permet d’éviter à un attaquant de trouver la taille du message.
  • sodium_memzero() : Permet de supprimer la valeur d’une variable de la mémoire.

Cependant, ne vous inquiétez pas, les fonctions de base de PHP sont très bien. PHP, de part sa nature (langage pour faire du web), est peu sensible aux SCA.

Conclusion

L’extension libsodium est native à PHP depuis la 7.2. Elle est à connaître dès que l’on veut améliorer la sécurité de son application. Son API est simple et ne nécessite pas de grosses connaissances en cryptographie. Cependant, elle est très mal documentée sur php.net. Il ne faut pas hésiter à aller voir la documentation de la lib C sous-jacente qui est assez complète et pleine d’exemples.

Enfin, Paragonie, une entreprise spécialisée dans la sécurité informatique, a publié halite, une surcouche à libsodium. Son but est de simplifier l’utilisation de libsodium, de mettre en avant les bonnes pratiques, et d’ajouter encore une couche de sécurité.

Nos formations sur le sujet

  • Symfony avancée

    Décou­vrez les fonc­tion­na­li­tés et concepts avan­cés de Symfo­ny

blog comments powered by Disqus