Vérification de signature JCS : six bibliothèques RFC 8785 sur sept s'accordent sur la forme canonique, une diverge

Six des sept bibliothèques de canonicalisation JSON RFC 8785 que nous avons testées produisent une sortie identique à l'octet près pour chaque entrée valide ; la septième fait exception, et ce simple désaccord provoque une divergence de vérification de signature. Nous avons soumis 432 valeurs JSON valides à sept implémentations JCS indépendantes dans cinq langages. Les sept correspondent à l'exemple détaillé de l'annexe B de la RFC 8785, elles respectent donc toutes formellement JCS. Mais l'une d'elles, le crate Rust serde_jcs, trie les clés d'objet par point de code Unicode plutôt que par unité de code UTF-16 comme l'exige la spécification. Sur 4.7% de nos entrées valides, elle émet des octets différents des six autres. Si elle se trouve d'un côté d'une signature et que l'une des autres bibliothèques se trouve de l'autre, un objet JSON signé par l'une peut échouer à être vérifié par l'autre. Lorsque nous avons simulé cela directement avec HMAC, la pire paire de bibliothèques a échoué à vérifier 10.2% des messages qu'elle aurait dû accepter.
Pourquoi un octet de désaccord constitue un bug de sécurité
Le format JSON brut offre trop de libertés. Le même objet logique peut être écrit avec ses clés dans n'importe quel ordre, avec une quantité arbitraire d'espaces blancs, et avec des nombres représentés de plusieurs manières équivalentes. Cette flexibilité ne pose pas de problème jusqu'à ce que vous souhaitiez signer l'objet, car une signature est calculée sur des octets, pas sur du sens. Si le signataire et le vérificateur sérialisent l'objet différemment, le vérificateur hache des octets différents et la signature échoue. Pire encore, si un attaquant parvient à trouver deux chaînes d'octets qu'un côté considère comme égales et que l'autre considère comme différentes, vous obtenez une attaque par canonicalisation : un message forgé qui est validé d'une certaine façon tout en signifiant autre chose. Une étude de 2026 recense les défauts de canonicalisation comme une classe récurrente de vulnérabilités dans précisément ce type de systèmes.
RFC 8785, le JSON Canonicalization Scheme, apporte la solution. Il verrouille chaque degré de liberté : les noms de membres d'objets sont triés, les nombres sont sérialisés avec l'algorithme Number.prototype.toString d'ECMAScript, les chaînes reçoivent l'échappement minimal autorisé, et les espaces superflus sont supprimés. Fournissez la même valeur JSON à n'importe quelle implémentation conforme et vous obtenez les mêmes octets, de sorte que les deux extrémités d'une signature s'accordent sur ce qui a été signé. Ce schéma est désormais critique. Le protocole Agent-to-Agent signe une carte d'agent sous forme de JWS sur le contenu de la carte canonicalisé avec JCS, et conseille aux clients de vérifier au moins une signature avant de faire confiance à la carte. Les verifiable credentials et les flux JWS détachés font de même. Les octets canoniques constituent la frontière de confiance.
La vraie question n'est donc pas de savoir si la spécification est solide. Elle est de savoir si les bibliothèques réellement déployées sont en accord entre elles. Nous l'avons mesuré.
Ce que nous avons testé
Nous avons rassemblé sept implémentations JCS indépendantes réparties sur cinq langages, sélectionnées parce qu'elles correspondent à ce qu'un développeur est susceptible d'installer depuis un gestionnaire de paquets :
canonicalize4.0.0 pour JavaScript (npm), le module largement utilisé issu de la lignée d'erdtman.- L'implémentation de référence
cyberphone/json-canonicalizationen Python, le propre code de référence de l'auteur. - Le paquet PyPI
jcs, version 0.2.1, une implémentation Python distincte. gowebpki/jcsv1.0.1 pour Go.- L'implémentation de référence
cyberphone/json-canonicalizationen Go. serde_jcs0.1.0 pour Rust.java-json-canonicalization1.1 pour Java (l'artefact Maven d'erdtman).
Chacune tournait dans son propre conteneur issu d'une image officielle, garantissant des résultats reproductibles et évitant toute interférence entre bibliothèques. Nous avons construit un corpus de 440 entrées forgées, dont 432 JSON valides, étiquetées par catégorie : formatage des nombres aux limites de l'IEEE-754 et d'ECMAScript, zéro négatif, cas de tri des clés avec des caractères dont l'ordre diffère en UTF-16 par rapport aux points de code, échappement et normalisation Unicode, clés en double, surrogates isolés et cas limites structurels. Nous avons soumis les mêmes octets à chaque bibliothèque et capturé sa sortie canonique, en conservant exactement les octets pour qu'aucune divergence ne résulte de notre propre outillage.
Deux oracles ont analysé les sorties. Le premier est un différentiel pur : pour chaque entrée, les bibliothèques qui ont réussi produisent-elles les mêmes octets ? Le second est l'attaque elle-même : prendre une clé HMAC-SHA256 fixe, faire canonicaliser et signer une entrée par une bibliothèque signataire, puis faire canonicaliser la même entrée et vérifier le tag par une bibliothèque vérificatrice. Chaque paire ordonnée de bibliothèques, chaque entrée. Un échec correspond ici à une signature inter-bibliothèques qui devrait être valide et ne l'est pas.
Avant d'accorder la moindre confiance à ces résultats, nous avons vérifié la référence de base. Les sept implémentations reproduisent la sortie canonique de l'annexe B de la RFC 8785 à l'octet près, y compris ses nombres délibérément atypiques et sa chaîne échappée. Elles implémentent toutes réellement JCS. C'est ce qui donne tout son sens au désaccord ci-dessous : il ne s'agit pas d'une bibliothèque cassée opposée à une bibliothèque correcte, mais de sept bibliothèques en apparence correctes, dont l'une commet discrètement une erreur sur un cas limite.
Le résultat : 4.7% des entrées valides divergent, et toujours de la même manière
Sur les 426 entrées valides et non ambiguës acceptées par l'ensemble des bibliothèques, six sur sept étaient identiques à l'octet près sur chacune d'elles. Pas proches : identiques, sur chaque valeur. La septième, serde_jcs, divergeait sur 20 de ces 426 entrées, soit 4.7%, et se retrouvait systématiquement seule en minorité. Lorsque les bibliothèques JCS divergent, dans notre corpus, c'est une bibliothèque isolée contre toutes les autres.
| Catégorie | Entrées | Divergentes | Cause |
|---|---|---|---|
| Tri des clés | 80 | 14 | ordre des points de code vs UTF-16 |
| Formatage des nombres | 194 | 6 | entiers supérieurs à 253 |
| Zéro négatif | 18 | 0 | toutes d'accord (-0 vers 0) |
| Échappement Unicode | 76 | 0 | toutes d'accord |
| Normalisation Unicode | 27 | 0 | toutes d'accord (pas de NFC/NFD silencieux) |
| Structurel | 30 | 0 | toutes d'accord |
Deux catégories concentrent l'intégralité des divergences, et la première est celle qui compte vraiment.
La divergence de tri des clés : point de code contre unité de code UTF-16
La RFC 8785 stipule explicitement que les noms de membres d'objets sont triés selon leur encodage UTF-16, en comparant les unités de code. Ce détail s'explique par le fait que JCS hérite de l'ordre des chaînes d'ECMAScript, et que les chaînes ECMAScript sont des séquences d'unités de code UTF-16. Pour les caractères du plan multilingue de base (BMP), l'ordre des unités de code et celui des points de code sont identiques, si bien que presque personne ne s'en aperçoit. Ils ne divergent que pour les caractères des plans supplémentaires, situés au-delà de U+FFFF, qui sont encodés sous forme de paire de surrogates. Un emoji tel que U+1F600 a pour point de code 0x1F600, ce qui est supérieur à un caractère du plan multilingue de base comme U+E000. Cependant, son encodage UTF-16 commence par l'unité de code de surrogate supérieur 0xD83D, qui est inférieur à 0xE000. Ainsi, selon les deux règles, les deux clés sont triées dans des ordres opposés.
Quatorze de nos entrées de tri de clés touchent exactement cette ligne de faille, et serde_jcs trie par point de code là où les six autres trient par unité de code UTF-16. Voici le cas le plus réduit, un objet avec deux clés :
input: {"😀": 1, "\uE000": 2}
six libs: {"😀":1,"\uE000":2} 😀 first (0xD83D < 0xE000)
serde_jcs: {"\uE000":2,"😀":1} 😀 last (0x1F600 > 0xE000)
Le caractère à usage privé U+E000 est affiché ici sous forme échappée pour la lisibilité ; JCS l'émet en réalité sous forme d'octet littéral, et les deux bibliothèques s'accordent sur ce point. Ce sur quoi elles divergent, c'est l'ordre des deux membres, et cela suffit. Signez l'objet de gauche avec HMAC ou JWS, transmettez-le à un vérificateur utilisant serde_jcs, et le vérificateur le canonicalise sous la forme de l'objet de droite, calcule un tag différent et rejette une signature qui était parfaitement valide. Inversez les rôles et la même chose se produit dans l'autre sens. Les deux bibliothèques ont un désaccord stable et silencieux sur ce qu'est la forme canonique de cet objet.
Il ne s'agit pas d'une découverte inédite ou exotique sur le schéma. C'est le piège de conformité classique de JCS, mentionné dans la RFC elle-même et dans des analyses antérieures sur la canonicalisation. Ce que notre mesure apporte, c'est le taux de base : sur sept bibliothèques distribuées et installables qui passent toutes le propre vecteur de test de la RFC, l'une commet encore cette erreur aujourd'hui. La conformité à l'exemple principal n'implique pas la conformité sur les cas limites.
La divergence sur les nombres, et pourquoi nous la traitons séparément
Les six autres entrées divergentes sont des entiers supérieurs à 253, tels que 9007199254740993. JCS spécifie que les nombres sont sérialisés via l'algorithme ECMAScript de conversion nombre-vers-chaîne, qui repose sur un double IEEE-754. Au-delà de 253, un double ne peut plus représenter chaque entier, donc 9007199254740993 s'arrondit en 9007199254740992. Six bibliothèques font exactement cela. serde_jcs préserve au contraire l'entier exact. Nous soulignons honnêtement qu'il s'agit d'un constat moins critique que la divergence de tri des clés : la RFC 8785 n'est définie que pour les nombres compris dans la plage des doubles IEEE-754, si bien qu'un entier supérieur à 253 constitue sans doute une entrée hors spécification qu'un appelant ne devrait jamais fournir. Cependant, cela reste du JSON syntaxiquement valide, produit bien une divergence au niveau de l'octet entre de vraies bibliothèques, et un système acceptant des identifiants numériques supérieurs à un entier sûr JavaScript peut parfaitement s'y heurter. Nous le conservons dans le différentiel tout en l'écartant des conclusions principales.
Tout le reste a tenu. Le zéro négatif a été canonicalisé en 0 partout. Aucune bibliothèque n'a appliqué silencieusement de normalisation Unicode, si bien qu'une chaîne en NFC et la même chaîne en NFD sont restées distinctes sur l'ensemble des sept, ce qui est conforme : JCS ne normalise pas. L'échappement des chaînes s'est révélé uniforme. Les désaccords sont ciblés et spécifiques, ce qui les rend dangereux plutôt qu'évidents.
Transformer la divergence en échec de signature
Une divergence dans les octets canoniques n'a d'intérêt que si elle casse quelque chose. Nous l'avons donc mise en situation de rupture. En utilisant une clé HMAC-SHA256 fixe, nous avons fait agir chaque bibliothèque en tant que signataire et chaque bibliothèque en tant que vérificatrice, sur chaque entrée valide où les deux produisaient des octets : 42 paires ordonnées, 11,754 vérifications au total. Une vérification échoue lorsque les octets canoniques du signataire et ceux du vérificateur diffèrent, car les tags diffèrent alors.
Sur l'ensemble des paires, 1.87% des vérifications ont échoué. Ce chiffre est dilué par les six bibliothèques qui ne divergent jamais entre elles, la façon rigoureuse de l'analyser est donc par paire. Toute paire qui ne fait pas intervenir serde_jcs a vérifié à 100%. Toute paire associant serde_jcs avec une bibliothèque conforme a échoué sur les entrées divergentes. La pire association, celle de la référence Go cyberphone comme signataire et serde_jcs comme vérificateur, a échoué sur 15 des 147 contrôles, soit un taux d'échec de vérification de signature de 10.2% entre deux bibliothèques qui valident pourtant toutes deux le vecteur de test de la RFC. Dans un déploiement mêlant plusieurs langages à travers une frontière de confiance, ce que font précisément les systèmes agent-to-agent, c'est l'échec auquel vous seriez concrètement confronté.
L'autre axe : certaines bibliothèques rejettent ce que d'autres acceptent
Parallèlement aux divergences au niveau des octets, nous avons constaté une vaste surface d'acceptation ou de rejet : 288 entrées pour lesquelles au moins une bibliothèque renvoyait un résultat et une autre renvoyait une erreur. Il ne s'agit pas d'un désaccord sur la forme canonique, mais d'un risque d'interopérabilité à part entière, qui divise les bibliothèques en plusieurs camps.
- Scalaires de premier niveau. Les deux parseurs issus de la lignée de référence,
cyberphoneen Go eterdtmanen Java, rejettent un simple0ou"x"en entrée de premier niveau ; les autres le canonicalisent. Si votre protocole en vient à signer un scalaire JSON isolé, la moitié de ces bibliothèques ne le traitera tout simplement pas. - Clés en double. Face à
{"a":1,"a":2}, les trois parseurs Go et Java rejettent catégoriquement l'objet, tandis que les implémentations JavaScript, Python et Rust conservent silencieusement la dernière valeur. La gestion des clés en double est suffisamment peu spécifiée pour qu'un signataire et un vérificateur soient en désaccord sur la validité même du message. - Surrogates isolés. Un surrogate non apparié dans une chaîne constitue du JSON invalide, et les comportements divergent : l'une émet le caractère de remplacement U+FFFD, une autre transmet directement les octets non-UTF-8 bruts, et le reste rejette. N'importe lequel de ces comportements d'un côté combiné à un comportement différent de l'autre est une divergence garantie.
Nous laissons cela hors du chiffre principal car il s'agit d'un problème d'acceptation contre rejet, et non d'octet contre octet, mais la leçon reste la même : la garantie réside dans l'implémentation, pas dans l'acronyme de trois lettres.
Le reflet inverse de notre résultat sur CBOR
Nous avons mené la même expérience sur un autre encodage, le format binaire CBOR, et obtenu le profil inverse. Là-bas, le CBOR déterministe n'était pas déterministe d'une bibliothèque à l'autre : 26% des valeurs aboutissaient à plus d'un encodage canonique, et le désaccord touchait l'ensemble des bibliothèques. JCS est le pendant rigoureux. Ses règles sont plus strictes et ses bibliothèques s'accordent pour la plupart, de sorte que la divergence se résume à une anomalie isolée et à une poignée d'entrées limites plutôt qu'à un quart de l'ensemble. Ce contraste constitue le véritable enseignement pratique. La sécurité d'un schéma de canonicalisation n'est pas une propriété intrinsèque du seul schéma ; elle résulte de la rigueur du schéma combinée à la conformité des bibliothèques spécifiques utilisées sur votre chemin d'exécution. JCS vous offre un meilleur schéma. Il ne vous dispense pas pour autant de vérifier vos bibliothèques.
Testez votre propre bibliothèque en dix secondes
Vous n'avez pas besoin de notre banc de test pour savoir de quel côté de cette divergence vous vous situez. Canonicalisez cet objet avec la bibliothèque JCS que vous déployez :
{ "😀": 1, "\uE000": 2 }
Si la sortie place la clé emoji en premier, votre bibliothèque trie par unité de code UTF-16 et respecte la RFC ainsi que la majorité. Si elle place la clé à usage privé en premier, votre bibliothèque trie par point de code et divergera avec un pair conforme sur tout objet dont les clés mélangent des caractères des plans supplémentaires et du plan multilingue de base. Exécutez ce même test aux deux extrémités de chaque frontière de confiance qui signe du JSON, en particulier lorsque les deux côtés utilisent des langages différents, et surtout si l'une de vos clés peut contenir de l'Unicode contrôlé par un attaquant.
Notre méthode de mesure, pour que les plus sceptiques puissent se fier aux chiffres
Le jeu de données est entièrement conçu par nos soins et synthétique. Nous n'avons sollicité aucun service tiers, rien scanné ni collecté aucune donnée réelle ; chaque entrée est une valeur JSON que nous avons rédigée pour sonder un cas limite précis. Les bibliothèques sont des paquets open source publics, testés dans leurs versions publiées, et nommés parce que caractériser le comportement d'une implémentation de spécification est la raison d'être d'un test de conformité, et non une mise en accusation.
- Isolation. Chaque implémentation s'exécutait dans son propre conteneur issu d'une image officielle, lisant un corpus identique et écrivant sa sortie canonique avec une conservation exacte des octets, de sorte qu'une divergence incombe à la bibliothèque et non à notre pipeline.
- Vérité terrain. Les sept reproduisent la sortie canonique de l'annexe B de la RFC 8785 à l'octet près, ce qui confirme qu'il s'agit d'authentiques implémentations JCS et non de sérialiseurs approximatifs.
- Deux oracles indépendants. Un différentiel au niveau de l'octet et un cycle signature-vérification HMAC en conditions réelles sur l'ensemble des 42 paires ordonnées de bibliothèques. Les échecs de signature concrétisent simplement ce différentiel, ils ne constituent pas une affirmation distincte.
- Catégorisation rigoureuse. Nous séparons les divergences au niveau de l'octet sur entrées valides (le résultat principal) des comportements d'acceptation contre rejet et des entrées potentiellement hors spécification, afin qu'aucun chiffre ne soit artificiellement gonflé.
Le banc de test, le générateur de corpus et les exécuteurs par bibliothèque sont conservés pour que la méthode soit reproductible ; seul l'agrégat est présenté ici.
Pourquoi cela compte, et les limites objectives
La canonicalisation redevient discrètement un pilier d'architecture. La volonté d'attribuer aux agents autonomes des identités vérifiables, via des cartes d'agent signées et des verifiable credentials, fait reposer la décision de confiance sur un pipeline de canonicalisation JCS puis signature, souvent avec un signataire et un vérificateur écrits dans des langages différents par des équipes distinctes. C'est précisément la condition sous laquelle un défaut de conformité d'une seule bibliothèque se transforme en échec de vérification. La bonne nouvelle de cet état des lieux est que JCS interopère bien : choisissez deux bibliothèques conformes et elles sont indiscernables. La mauvaise nouvelle est que la conformité n'est pas garantie par l'étiquette, et le seul point sur lequel une bibliothèque distribuée fait encore défaut, l'ordre des clés pour les caractères des plans supplémentaires, est à la fois accessible à un attaquant et facile à négliger.
Un résultat sans ses limites relève du marketing, voici donc le périmètre.
- Sept bibliothèques, pas tout l'écosystème. Ce sont des implémentations courantes et installables, mais il en existe d'autres, et une implémentation C ou C-sharp que nous n'avons pas testée pourrait se trouver d'un côté ou de l'autre de la divergence. La méthode se généralise ; les décomptes exacts sont spécifiques à cet ensemble dans ces versions précises.
- Faible probabilité dans les charges utiles types. La divergence de tri des clés ne frappe que lorsque les clés d'objet contiennent des caractères de plans supplémentaires, ce qui est rare dans les cartes d'agent classiques ou les credentials utilisant des noms de champs ASCII. Le risque est réel mais conditionnel, et un attaquant ayant le contrôle d'une clé est précisément celui qui remplira intentionnellement cette condition.
- Les versions évoluent. Les bibliothèques sont corrigées. Un crate qui trie par point de code aujourd'hui pourra être conforme demain, c'est précisément pourquoi la recommandation pérenne est de tester la version spécifique que vous déployez plutôt que d'accorder une confiance aveugle ou un rejet de principe à un nom.
- La divergence sur les nombres relève d'entrées limites. Les entiers supérieurs à 253 se situent hors du domaine de définition de JCS, nous traitons donc cette divergence comme un constat secondaire, de moindre portée, et la laissons hors des conclusions principales.
Foire aux questions
Qu'est-ce que le JSON Canonicalization Scheme (JCS) ?
JCS, défini dans la RFC 8785, est un ensemble de règles qui transforme une valeur JSON en une chaîne d'octets déterministe unique. Il trie les noms des membres d'objets, sérialise les nombres avec l'algorithme ECMAScript de conversion nombre-vers-chaîne, applique un échappement minimal des chaînes et supprime les espaces superflus. Deux parties partant de la même valeur JSON produisent les mêmes octets, ce qui leur permet de hacher ou de signer ces octets et d'obtenir un résultat identique. JCS est ce qui permet de signer du JSON sans devoir convenir à l'avance de sa syntaxe exacte.
Pourquoi la vérification de signature JCS échoue-t-elle entre différents langages ?
Elle échoue lorsque la bibliothèque du signataire et celle du vérificateur sont en désaccord sur les octets canoniques pour une même valeur. Lors de notre test, six implémentations sur sept concordaient à l'octet près, mais un crate Rust triait les clés d'objets par point de code Unicode au lieu de l'unité de code UTF-16 comme l'exige la RFC 8785. Pour un objet dont les clés comportent un emoji ou un autre caractère de plan supplémentaire, les deux bibliothèques émettent les clés dans un ordre différent, le tag calculé sur l'objet diffère donc et la vérification échoue.
Les implémentations JCS produisent-elles la même sortie canonique ?
La plupart du temps. Sur 426 entrées valides et non ambiguës soumises à sept implémentations dans cinq langages, six étaient identiques à l'octet près sur chaque entrée et les sept concordaient avec l'annexe B de la RFC 8785. Une seule a divergé sur 4.7% des entrées, et chaque divergence impliquait soit une clé d'objet du plan supplémentaire, soit un entier supérieur à 2 puissance 53. JCS interopère bien sur les données ordinaires et ne diverge que sur des cas limites précis.
Comment JCS empêche-t-il le contournement de signature ?
Il élimine les libertés offertes par le JSON standard, l'ordre des clés, les espaces et les multiples écritures d'un nombre, de sorte qu'une valeur corresponde à une seule chaîne d'octets et ne puisse pas être ré-encodée sous une forme équivalente d'apparence différente. Cette garantie ne tient que si les deux parties utilisent une implémentation conforme. Une implémentation non conforme réintroduit l'ambiguïté même que JCS était censé éliminer, raison pour laquelle la conformité de l'implémentation, et pas seulement l'usage de JCS, est la propriété déterminante.
Comment A2A utilise-t-il JCS pour signer les cartes d'agent ?
Dans le protocole Agent-to-Agent, une carte d'agent est un document JSON décrivant l'identité et les capacités d'un agent. Sa signature est un JWS appliqué au contenu de la carte après canonicalisation avec JCS, et il est conseillé aux clients de vérifier au moins une signature avant d'accorder leur confiance à la carte. Comme les octets signés correspondent à la sortie JCS, un client dont la bibliothèque JCS canonicalise différemment de celle du signataire calculera une entrée différente pour la vérification.
Qu'est-ce qu'une attaque par confusion de canonicalisation ?
Elle exploite un désaccord sur la manière dont un signataire et un vérificateur réduisent un message à sa forme canonique. Si les deux produisent des octets différents pour la même valeur logique, un attaquant peut forger une entrée validée d'une certaine façon tout en signifiant autre chose, ou faire échouer une signature légitime. La canonicalisation constitue une classe récurrente de vulnérabilités dans les systèmes qui signent des données structurées, car la sécurité repose sur le fait que chaque partie calcule une forme canonique rigoureusement identique.
La RFC 8785 est-elle un standard officiel de l'IETF ?
La RFC 8785 est une RFC informative de l'IETF publiée en juin 2020. Il s'agit d'une spécification stable et citable du JSON Canonicalization Scheme, reprise par des travaux ultérieurs sur le JSON signé, les verifiable credentials et la sécurité agent-to-agent. Le statut informatif signifie qu'il ne s'agit pas d'un standard Internet obligatoire, mais c'est la définition vers laquelle pointent les implémentations.
Lectures associées
- Le CBOR déterministe n'est pas déterministe d'une bibliothèque à l'autre. Le même différentiel appliqué à un autre encodage, le format binaire CBOR, où 26% des valeurs obtenaient plus d'une forme canonique.
- Sécurité des serveurs MCP distants : ce que révèle un recensement de 19,321 serveurs. La surface d'outillage des agents que ces cartes signées sont censées protéger, mesurée à l'échelle globale.
- Ce que prouvent les certificats. Extraire les garanties cryptographiques directement depuis les octets plutôt que de se fier à l'étiquette, la même méthode appliquée aux certificats.