Aller au contenu

Formats, bibliothèques et API : rendre la crypto utilisable

Formats, bibliothèques et API : rendre la crypto utilisable

Section intitulée « Formats, bibliothèques et API : rendre la crypto utilisable »

Les algorithmes modernes se cassent rarement. Ce qui se casse, c’est l’emballage : un format qui ne dit pas quel algorithme a servi, un analyseur qui accepte deux lectures du même message, une bibliothèque qui laisse au développeur le soin de choisir un nonce, une fonction de vérification dont personne ne lit le code de retour. Cette fiche traite de cette couche ingrate et décisive : formats de clés et de messages, sérialisation, choix des bibliothèques et chaîne d’approvisionnement logicielle. Elle part d’une règle simple, que l’histoire des vulnérabilités confirme avec une constance remarquable : l’algorithme est choisi par le code, jamais par le message.

→ Lire l’explication complète de la marmotte

Il faut d’abord distinguer format de clé et format de message. Les premiers transportent une clé ou un certificat ; les seconds transportent des données protégées.

Côté clés, DER (Distinguished Encoding Rules) est l’encodage binaire canonique des structures ASN.1 (Abstract Syntax Notation One) ; PEM (Privacy-Enhanced Mail) n’en est que l’habillage textuel, du DER en Base64 entre des lignes -----BEGIN ...-----, décrit par le RFC 7468. Les normes PKCS (Public-Key Cryptography Standards) donnent le contenu : PKCS#1 pour les clés RSA, PKCS#8 pour une clé privée quelconque, éventuellement chiffrée, PKCS#12 pour un paquet clé et certificats protégé par mot de passe. JWK (JSON Web Key, RFC 7517) représente une clé en JSON (JavaScript Object Notation).

Côté messages, JOSE (JSON Object Signing and Encryption) regroupe JWS (RFC 7515) pour la signature, JWE (RFC 7516) pour le chiffrement et JWA (RFC 7518) pour la liste des algorithmes. Le JWT (JSON Web Token, RFC 7519) n’est pas JOSE : c’est un format de revendications qui s’appuie sur JWS ou JWE. Les pages JWS et JWE les détaillent. COSE (CBOR Object Signing and Encryption, RFC 9052) transpose la même idée sur CBOR (Concise Binary Object Representation, RFC 8949), plus compact, apprécié dans l’embarqué et les clés d’accès FIDO2. Là encore, COSE est une construction de sécurité ; CBOR n’est qu’un encodage.

PASETO (Platform-Agnostic SEcurity TOkens) a été conçu en réaction aux excès de souplesse de JOSE : une version comme v4.public désigne un jeu d’algorithmes fixe (Ed25519 pour la signature), sans en-tête alg négociable. Pour les fichiers, age chiffre avec X25519 et ChaCha20-Poly1305 sans aucune option cryptographique, et minisign signe avec Ed25519. Leur force vient précisément de ce qu’ils interdisent.

Un message chiffré écrit aujourd’hui sera relu dans cinq ans, après au moins un changement d’algorithme ou de clé. Il doit donc porter tout ce qu’il faut pour être déchiffré sans deviner :

  • une version du format, qui fixe à elle seule l’algorithme et ses paramètres ;
  • un identifiant de clé, pour savoir laquelle utiliser après rotation ;
  • le nonce, unique par clé, et le sel si une clé est dérivée d’un mot de passe ;
  • le tag d’authentification ;
  • la définition des AAD (Additional Authenticated Data), données authentifiées mais non chiffrées.

L’en-tête lui-même doit être authentifié : s’il ne l’est pas, un attaquant modifie la version ou l’identifiant de clé et pousse le récepteur vers un chemin de code plus faible. La solution standard consiste à passer l’en-tête entier en AAD. Le fonctionnement de l’AEAD (Authenticated Encryption with Associated Data) est expliqué dans Chiffrer et authentifier en une seule construction, et la rotation des clés dans Exploiter les clés : rotation, KMS et secrets.

Le numéro de version ne doit pas servir à négocier. Le récepteur tient une liste des versions qu’il accepte ; une version inconnue ou retirée provoque un refus, pas une tentative.

Une signature porte sur des octets, pas sur des objets. Si l’émetteur et le récepteur ne produisent pas les mêmes octets à partir du même objet, ou n’interprètent pas les mêmes octets de la même façon, la signature ne protège plus rien.

JSON est le cas d’école. Le RFC 8259 recommande des noms de champs uniques sans l’imposer, et les analyseurs divergent face à un doublon : certains gardent la première valeur, d’autres la dernière. Le message {"montant": 10, "montant": 10000} peut ainsi être vérifié par un composant qui lit 10 et exécuté par un autre qui lit 10 000. L’ordre des champs, les espaces, l’échappement des caractères Unicode et la représentation des nombres varient eux aussi.

Deux stratégies existent. La première est la canonicalisation : définir une forme unique et signer cette forme. Le RFC 8785 (JSON Canonicalization Scheme) le fait pour JSON, et CBOR définit un encodage déterministe. La canonicalisation est fragile : il suffit d’une divergence d’implémentation pour retomber dans l’ambiguïté, comme l’ont montré pendant des années les attaques par enveloppement de signatures XML. La seconde, plus robuste, consiste à signer les octets bruts et à les transmettre tels quels : JWS signe la chaîne Base64url de l’en-tête et de la charge utile, pas un objet reconstruit.

Dans tous les cas, il faut savoir quels sont les champs signés. Une signature qui couvre le corps mais pas l’en-tête, ou le contenu mais pas le destinataire, permet de rejouer un message valide dans un autre contexte. Et le parser strict est la dernière ligne de défense : il refuse les doublons, les champs inconnus dans les zones signées, les encodages non canoniques et les données en trop après la fin de la structure.

Le meilleur indicateur de sûreté d’une bibliothèque est la quantité de décisions qu’elle laisse à son utilisateur. Une API haut niveau expose des opérations, « chiffrer ce message pour cette clé », avec des paramètres automatiques : nonce aléatoire généré en interne, algorithme fixé, tag vérifié avant toute restitution du clair.

BibliothèqueLangageNiveauRemarque
libsodiumC, liaisons nombreusesHautcrypto_secretbox, crypto_box, choix d’algorithmes figés
TinkJava, Go, C++, PythonHautJeux de clés avec rotation intégrée, primitives par usage
OpenSSLCBas et moyenUniversel, API EVP vaste, nombreuses façons de mal faire
BoringSSLCBasFork de Google, sans garantie de stabilité d’API pour les tiers
ringRustMoyenAPI restreinte, dérivée de code BoringSSL
RustCryptoRustBasEnsemble de crates en Rust pur, à assembler soi-même

Avec libsodium, chiffrer un message tient en un appel dont aucun paramètre n’est piégeux :

unsigned char nonce[crypto_secretbox_NONCEBYTES];
randombytes_buf(nonce, sizeof nonce);
crypto_secretbox_easy(chiffre, message, longueur, nonce, cle);
if (crypto_secretbox_open_easy(clair, chiffre, longueur_chiffre, nonce, cle) != 0) {
/* message falsifié : le tampon clair est inutilisable, on s'arrête */
}

Au-delà de l’API, le choix tient à la maintenance (fréquence des correctifs, réactivité aux vulnérabilités), à l’audit publié, à l’interopérabilité avec les autres composants et au langage, la sûreté mémoire de Rust évitant toute une classe de défauts. La gestion d’erreur est un sujet à part entière : dans l’API historique d’OpenSSL, certaines fonctions de vérification renvoient 1 pour succès, 0 pour échec et −1 pour erreur, si bien qu’un test if (resultat) accepte une erreur comme un succès. OpenSSL lui-même a corrigé ce défaut dans ses propres appels en 2009 (CVE-2008-5077). Les test vectors (vecteurs de test) officiels et ceux du projet Wycheproof permettent de vérifier qu’une intégration produit les bonnes valeurs et rejette les entrées malformées.

La cryptographie protège aussi le logiciel lui-même. La signature de release lie un artefact publié à une clé du projet ; elle n’a de valeur que si l’installation la vérifie et si la clé publique a été obtenue par un autre canal que l’artefact. Une attestation va plus loin : elle décrit comment l’artefact a été produit (dépôt source, commit, système de construction). Le cadre SLSA (Supply-chain Levels for Software Artifacts) et le format in-toto structurent ces attestations de provenance ; Sigstore propose des signatures liées à une identité de compte plutôt qu’à une clé longue durée, enregistrées dans un journal de transparence.

La reproductibilité permet à un tiers de reconstruire l’artefact à partir des sources et d’obtenir les mêmes octets : la signature prouve alors davantage que la seule volonté du signataire. Le sujet est traité dans Pourquoi les builds reproductibles échouent souvent. Le SBOM (Software Bill of Materials), aux formats SPDX ou CycloneDX, liste les composants embarqués : voir SBOM pour inventorier les composants réellement exécutés. La rotation de clés de signature doit être prévue : publier la nouvelle clé signée par l’ancienne, annoncer une date de fin. Enfin, la vérification CI (Continuous Integration) bloque toute dépendance ou image dont la signature ou l’attestation ne correspond pas à la politique.

La signature de code n’est pas la signature de commit : la première atteste un artefact distribué, la seconde l’auteur d’une modification dans un dépôt. Un commit signé peut produire un binaire compromis par une chaîne de construction piégée. Les deux sont présentés dans Signer commits et artefacts contre quelles attaques.

Format de message AEAD versionné. Un format minimal, entièrement authentifié :

Format de message AEAD versionné, octet par octet. Octet 0 : 0x54, octet magique du format. Octet 1 : 0x02, version 2, soit XChaCha20-Poly1305 avec une clé de 256 bits. Octets 2 à 5 : identifiant de clé sur 32 bits. Octets 6 à 29 : nonce aléatoire de 24 octets. À partir de l'octet 30 : le chiffré, suivi du tag Poly1305 de 16 octets. Les données associées couvrent les octets 0 à 29 inclus.

Le nonce de 24 octets autorise un tirage aléatoire sans crainte raisonnable de collision. Une version 3 pourra changer d’algorithme ; le lecteur refusera toute version absente de sa liste.

JOSE, COSE ou PASETO ? Pour des jetons d’API web en interopérabilité avec un fournisseur d’identité, JOSE s’impose de fait, à condition de fixer côté serveur l’algorithme attendu. Pour des objets contraints en taille, COSE. Pour des jetons internes sans contrainte d’interopérabilité, PASETO, qui supprime la négociation.

Vérifier une signature de release.

Fenêtre de terminal
# Signature minisign : la clé publique vient du site du projet, pas de l'archive
minisign -Vm outil-1.4.2.tar.gz -p outil.pub
# Signature OpenPGP détachée
gpg --verify outil-1.4.2.tar.gz.asc outil-1.4.2.tar.gz
# Contrôle d'empreinte à partir d'un fichier lui-même signé
sha256sum -c SHA256SUMS

Vérifier une empreinte téléchargée au même endroit que l’archive ne prouve que l’absence de corruption accidentelle.

Tests négatifs de parsing. Une suite de tests utile soumet au moins : un champ en double, un alg à none, un alg symétrique avec une clé publique comme secret, un tag tronqué, une version inconnue, des octets en trop après la structure, un nonce de mauvaise longueur et un message vide. Chacun doit être rejeté, avec une erreur explicite, et sans que le clair partiel soit restitué.

Le parser permissif, qui « corrige » une entrée au lieu de la rejeter. L’algorithme accepté depuis le message : l’en-tête JWT "alg": "none", ou la confusion entre RS256 et HS256 où la clé publique RSA devient un secret HMAC (Hash-based Message Authentication Code) connu de tous. Le format sans version, qui condamne à deviner. L’erreur ignorée, qui transforme un échec de vérification en succès. La donnée signée partiellement, qui laisse un champ critique hors de la signature.

Un format de clé (PEM, JWK) n’est pas un format de message (JWE, COSE). JOSE est une famille de constructions ; JWT est un format de jetons bâti dessus. COSE apporte la sécurité ; CBOR apporte l’encodage. La signature de code atteste un artefact ; la signature de commit atteste une modification de source.

Un format robuste dit tout ce qu’il faut pour être relu, authentifie son propre en-tête et refuse ce qu’il ne connaît pas. Une bonne bibliothèque prend les décisions délicates à la place du développeur et signale bruyamment ses échecs. Une chaîne de distribution saine signe, atteste et vérifie. Rien de cela n’est spectaculaire, ce qui explique sans doute pourquoi les attaquants y trouvent si souvent leur bonheur.

Sans signature de publication vérifiée à l’installation, une version modifiée déposée sur un miroir de téléchargement ou dans un dépôt de paquets s’installe exactement comme l’authentique, et elle hérite de toute la confiance accordée au projet. Sans lecture stricte des formats, deux composants qui interprètent le même message de deux façons ne vérifient pas la même chose : l’un contrôle la signature d’une version du contenu, l’autre exécute une autre version, et chacun est persuadé d’avoir fait son travail. La ruche inspecte alors soigneusement l’étiquette du pot pendant qu’on lui livre autre chose dedans.

Protéger des données demande plus qu’un bon algorithme : il faut les ranger dans un format précis et les manipuler avec un outil fiable. Un format de message protégé doit contenir un numéro de version, le nom de l’algorithme, les valeurs aléatoires utilisées (le nonce et le sel), l’étiquette de contrôle qui détecte les modifications, et les données associées, protégées sans être chiffrées. Sans ces informations, on ne peut plus changer d’algorithme plus tard sans casser les anciens messages. Le choix de l’algorithme doit être fixé par le programme, jamais lu dans le message, sinon l’attaquant peut imposer le plus faible. Ensuite, la mise en forme des données, appelée sérialisation, doit être sans ambiguïté : si deux programmes lisent le même texte de deux façons, une signature valable pour l’un peut couvrir autre chose pour l’autre. On impose donc une forme unique, un ordre des champs, et un lecteur strict qui refuse tout ce qui sort de la règle. Pour le code, on préfère des bibliothèques maintenues et auditées, avec des interfaces de haut niveau qui choisissent seules les bons paramètres et signalent clairement les erreurs. Enfin, les logiciels eux-mêmes se protègent : chaque version publiée est signée, accompagnée de la liste de ses composants et d’une attestation d’origine, et l’installation vérifie cette signature avant de faire confiance au paquet.

À lire aussi

Pages voisines