Verificación de firmas JCS: seis de siete librerías RFC 8785 coinciden en la forma canónica, una no

Seis de las siete librerías de canonicalización JSON según RFC 8785 que evaluamos generan salidas idénticas byte a byte para cualquier entrada válida; la séptima no, y esa única discrepancia provoca una divergencia en la verificación de firmas. Evaluamos 432 valores JSON válidos en siete implementaciones independientes de JCS en cinco lenguajes. Las siete coinciden con el ejemplo resuelto en el Apéndice B del RFC 8785, por lo que las siete son formalmente JCS. Sin embargo, una de ellas, el crate de Rust serde_jcs, ordena las claves de los objetos por punto de código Unicode en lugar de hacerlo por unidad de código UTF-16, tal como exige la especificación. En el 4.7% de nuestras entradas válidas emite bytes distintos a las otras seis. Si se utiliza de un lado de la firma y cualquiera de las otras librerías se utiliza del otro, un objeto JSON firmado por una puede fallar al verificarse con la otra. Cuando simulamos esto de forma directa con HMAC, el peor emparejamiento de librerías no pudo verificar el 10.2% de los mensajes que debería haber aceptado.
Por qué un byte de discrepancia es un fallo de seguridad
El formato JSON estándar ofrece demasiada libertad. Un mismo objeto lógico puede escribirse con sus claves en cualquier orden, con cualquier cantidad de espacios en blanco y con números representados de múltiples formas equivalentes. Esa flexibilidad es aceptable hasta que se desea firmar el objeto, ya que una firma se calcula sobre bytes, no sobre significado. Si el firmante y el verificador serializan el objeto de manera distinta, el verificador calcula el hash de bytes diferentes y la firma no se valida. Peor aún, si un atacante encuentra dos cadenas de bytes que un extremo interpreta como iguales y el otro como diferentes, se produce un ataque de canonicalización: un mensaje manipulado que se verifica como una cosa pero significa otra. Un estudio de 2026 cataloga los fallos de canonicalización como una clase recurrente de vulnerabilidad exactamente en este tipo de sistemas.
RFC 8785, el JSON Canonicalization Scheme, es la solución. Fija cada grado de libertad: se ordenan los nombres de los miembros de los objetos, los números se serializan con el algoritmo de ECMAScript Number.prototype.toString, las cadenas reciben el escape mínimo legal y se eliminan los espacios en blanco no significativos. Al pasar el mismo valor JSON por cualquier implementación conforme se obtienen los mismos bytes, de modo que ambos extremos de la firma coinciden en lo que se firmó. El esquema ahora sostiene infraestructura crítica. El protocolo Agent-to-Agent firma una tarjeta de agente como un JWS sobre el contenido de la tarjeta canonicalizado con JCS, y aconseja a los clientes verificar al menos una firma antes de confiar en ella. Las credenciales verificables y los flujos JWS desacoplados hacen lo mismo. Los bytes canónicos representan el límite de confianza.
Por tanto, la pregunta relevante no es si la especificación es sólida. Es si las librerías que la gente despliega en producción coinciden entre sí. Lo hemos medido.
Qué evaluamos
Reunimos siete implementaciones independientes de JCS en cinco lenguajes, elegidas por ser las que un desarrollador probablemente descargaría de un registro de paquetes:
canonicalize4.0.0 para JavaScript (npm), el módulo ampliamente utilizado del linaje de erdtman.- La implementación de referencia
cyberphone/json-canonicalizationen Python, el código de referencia del propio autor. - El paquete de PyPI
jcs, versión 0.2.1, una implementación independiente en Python. gowebpki/jcsv1.0.1 para Go.- La implementación de referencia
cyberphone/json-canonicalizationen Go. serde_jcs0.1.0 para Rust.java-json-canonicalization1.1 para Java (el artefacto de Maven de erdtman).
Cada una se ejecutó en su propio contenedor con imagen oficial, para que el resultado fuera reproducible y ninguna librería pudiera interferir con otra. Creamos un corpus de 440 entradas diseñadas a medida, 432 de ellas JSON válidos, clasificadas por categoría: formato numérico cerca de los límites de IEEE-754 y ECMAScript, cero negativo, ordenación de claves con caracteres que difieren bajo UTF-16 respecto a punto de código, secuencias de escape y normalización Unicode, claves duplicadas, sustitutos aislados y casos límite estructurales. Enviamos exactamente los mismos bytes a cada librería y capturamos su salida canónica, conservando los bytes de forma idéntica para que ninguna divergencia fuera resultado de nuestras propias herramientas.
Ejecutamos dos oráculos sobre las salidas. El primero es un análisis diferencial directo: para cada entrada, ¿las librerías que respondieron correctamente producen los mismos bytes? El segundo es el ataque real: tomar una clave fija HMAC-SHA256, hacer que una librería firmante canonicalice una entrada y genere la etiqueta, y luego pedir a una librería verificadora que canonicalice la misma entrada y compruebe la etiqueta. Cada par ordenado de librerías, cada entrada. Un fallo en este punto representa una firma entre librerías que debería verificarse y no lo hace.
Antes de dar nada por sentado, verificamos la base de referencia. Las siete implementaciones reproducen byte a byte la salida canónica del Apéndice B del RFC 8785, incluidos sus números deliberadamente complejos y su cadena con caracteres de escape. Todas son JCS auténtico. Eso es lo que da relevancia a la discrepancia analizada a continuación: no se trata de una librería rota frente a una correcta, sino de siete librerías aparentemente correctas, donde una de ellas falla silenciosamente en un caso límite.
El resultado: el 4.7% de las entradas válidas diverge, y siempre del mismo modo
En las 426 entradas válidas y sin ambigüedades que aceptaron todas las librerías, seis de las siete generaron salidas idénticas byte a byte en su totalidad. No similares: idénticas, en cada uno de los valores. La séptima, serde_jcs, discrepó en 20 de esas 426 entradas, lo que representa un 4.7%, y fue la única discrepante en cada ocasión. En nuestro corpus, cuando las librerías JCS discrepan, se trata de una sola librería contra el resto.
| Categoría | Entradas | Divergentes | Causa |
|---|---|---|---|
| Ordenación de claves | 80 | 14 | orden por punto de código vs UTF-16 |
| Formato de números | 194 | 6 | enteros superiores a 253 |
| Cero negativo | 18 | 0 | todas coinciden (-0 a 0) |
| Escape de Unicode | 76 | 0 | todas coinciden |
| Normalización Unicode | 27 | 0 | todas coinciden (sin NFC/NFD silencioso) |
| Estructural | 30 | 0 | todas coinciden |
Dos categorías concentran la totalidad de la divergencia, y la primera es la más crítica.
La discrepancia en la ordenación de claves: punto de código frente a unidad de código UTF-16
El RFC 8785 especifica explícitamente que los nombres de los miembros de un objeto se ordenan según su codificación UTF-16, comparando unidades de código. Este detalle se debe a que JCS hereda el criterio de ordenación de cadenas de ECMAScript, y las cadenas en ECMAScript son secuencias de unidades de código UTF-16. En el caso de los caracteres del Plano Multilingüe Básico, el orden por unidades de código y por punto de código es idéntico, por lo que casi nadie lo percibe. Solo divergen en los caracteres de los planos suplementarios, aquellos por encima de U+FFFF que se codifican mediante un par subrogado. Un emoji como U+1F600 tiene el punto de código 0x1F600, que es mayor que un carácter del Plano Multilingüe Básico como U+E000. Sin embargo, su codificación UTF-16 comienza con la unidad de código subrogada alta 0xD83D, que es menor que 0xE000. Por lo tanto, bajo las dos reglas, ambas claves se ordenan de forma opuesta.
Catorce de nuestras entradas de ordenación de claves cayeron exactamente en esta línea de falla, y serde_jcs ordena por punto de código mientras que las otras seis lo hacen por unidad de código UTF-16. Este es el caso más simple, un objeto con dos claves:
input: {"😀": 1, "\uE000": 2}
six libs: {"😀":1,"\uE000":2} 😀 first (0xD83D < 0xE000)
serde_jcs: {"\uE000":2,"😀":1} 😀 last (0x1F600 > 0xE000)
El carácter de uso privado U+E000 se muestra aquí como una secuencia de escape para facilitar la lectura; JCS en realidad lo emite como un byte literal, y ambas librerías coinciden en ese aspecto. En lo que no coinciden es en el orden de los dos miembros, y con eso basta. Si se firma el objeto de la izquierda con HMAC o JWS y se entrega a un verificador que ejecute serde_jcs, este lo canonicaliza como el objeto de la derecha, calcula una etiqueta distinta y rechaza una firma que era perfectamente válida. Si se invierten los roles, ocurre exactamente lo mismo en sentido contrario. Ambas librerías mantienen un desacuerdo estable y silencioso sobre cuál es la forma canónica de este objeto.
Esto no constituye un hallazgo exótico sobre el estándar. Es la clásica trampa de conformidad de JCS, señalada en el propio RFC y en análisis previos sobre canonicalización. Lo que aporta nuestra medición es la tasa base: de siete librerías publicadas e instalables que superan el vector de prueba del propio RFC, una sigue implementando esto de forma incorrecta hoy en día. La conformidad con el ejemplo principal no garantiza conformidad en los casos límite.
La discrepancia numérica, y por qué la reportamos por separado
Las otras seis entradas divergentes corresponden a enteros superiores a 253, como 9007199254740993. JCS especifica que los números se serializan mediante el algoritmo de número a cadena de ECMAScript, el cual opera a través de un flotante de doble precisión IEEE-754. Por encima de 253 un double ya no puede representar todos los enteros, por lo que 9007199254740993 se redondea a 9007199254740992. Seis librerías hacen exactamente eso. serde_jcs en cambio conserva el entero de forma exacta. Destacamos esto con transparencia como un hallazgo menor frente a la discrepancia en la ordenación de claves: el RFC 8785 está definido únicamente para números dentro del rango de doble precisión IEEE-754, por lo que un entero superior a 253 constituye técnicamente una entrada fuera de especificación que un emisor jamás debería enviar. No obstante, sigue siendo JSON sintácticamente válido, continúa generando una divergencia a nivel de bytes entre librerías reales, y cualquier sistema que admita identificadores numéricos superiores a un entero seguro de JavaScript puede verse afectado. Lo mantenemos en el análisis diferencial pero fuera de las conclusiones principales.
Todo lo demás se mantuvo consistente. El cero negativo se canonicalizó a 0 en todos los casos. Ninguna librería aplicó normalización Unicode silenciosa, por lo que una cadena en NFC y la misma cadena en NFD permanecieron diferenciadas en las siete, lo cual es correcto: JCS no normaliza. El escape de cadenas fue uniforme. Las discrepancias son acotadas y puntuales, lo que las hace peligrosas en lugar de evidentes.
Transformando la discrepancia en un fallo de firma
Una divergencia en los bytes canónicos solo resulta relevante si rompe algo. Así que hicimos que rompiera algo. Utilizando una única clave HMAC-SHA256 fija, configuramos cada librería para actuar como firmante y como verificador sobre cada entrada válida en la que ambas produjeron bytes: 42 pares ordenados, 11,754 verificaciones en total. Una comprobación falla cuando los bytes canónicos del firmante y los del verificador difieren, ya que en ese caso las etiquetas resultantes no coinciden.
En el conjunto de todos los pares, falló el 1.87% de las verificaciones. Dicha cifra se encuentra diluida por las seis librerías que nunca discrepan entre sí, por lo que la forma rigurosa de interpretarla es por pares. Cada par que no incluye a serde_jcs verificó al 100%. Cada combinación que empareja a serde_jcs con una librería conforme falló en las entradas divergentes. El peor caso, la referencia en Go de cyberphone como firmante y serde_jcs como verificador, falló 15 de 147 comprobaciones: una tasa de fallo en verificación de firmas del 10.2% entre dos librerías que superan el vector de prueba del RFC. En un despliegue que combine lenguajes a través de un límite de confianza, escenario habitual en arquitecturas de agente a agente, este es el fallo que se observaría en producción.
El otro factor: algunas librerías rechazan lo que otras aceptan
Junto a las discrepancias a nivel de bytes, detectamos una amplia superficie de aceptación frente a rechazo: 288 entradas en las que al menos una librería generó una salida y otra devolvió un error. Esto no constituye un desacuerdo de forma canónica, pero representa un riesgo de interoperabilidad en sí mismo y divide a las librerías en distintos grupos.
- Escalares en la raíz. Los dos parsers del linaje de referencia,
cyberphoneen Go yerdtmanen Java, rechazan un simple0o"x"como entrada raíz; las demás lo canonicalizan. Si su protocolo llega a firmar un escalar JSON aislado, la mitad de estas librerías no lo procesará en absoluto. - Claves duplicadas. Ante
{"a":1,"a":2}, los tres parsers de Go y Java rechazan el objeto directamente, mientras que las implementaciones de JavaScript, Python y Rust conservan silenciosamente el último valor. El tratamiento de claves duplicadas no está lo suficientemente especificado, hasta el punto de que firmante y verificador pueden discrepar sobre si el mensaje es siquiera válido. - Sustitutos aislados. Un sustituto no emparejado en una cadena constituye JSON inválido, y aquí las librerías adoptan caminos dispares: una emite el carácter de reemplazo U+FFFD, otra transmite los bytes no UTF-8 sin procesar directamente, y el resto lo rechaza. Cualquiera de estos comportamientos en un extremo combinado con uno distinto en el otro representa una divergencia inminente.
Mantenemos esto al margen de la cifra principal porque se trata de discrepancias de aceptación frente a rechazo y no de byte contra byte, pero la lección es idéntica: la garantía reside en la implementación, no en las siglas del estándar.
El reflejo de nuestro resultado en CBOR
Llevamos a cabo el mismo experimento en otra codificación, en CBOR binario, y obtuvimos el resultado inverso. Allí, deterministic CBOR no resultó determinista entre librerías: el 26% de los valores produjo más de una codificación canónica, y el desacuerdo estuvo distribuido a lo largo de todo el espectro de librerías. JCS es la contraparte ordenada. Sus reglas son más estrictas y sus librerías coinciden en su mayoría, por lo que la divergencia se reduce a un caso atípico y a un puñado de entradas límite en lugar de abarcar una cuarta parte de todo el conjunto. Ese contraste es la conclusión práctica. La seguridad de un esquema de canonicalización no es una propiedad intrínseca del estándar por sí solo; es el producto del rigor del esquema y de la conformidad de las librerías concretas en su flujo de datos. JCS proporciona un esquema superior. No le exime de verificar sus librerías.
Evalúe su propia librería en diez segundos
No necesita nuestro entorno de pruebas para determinar de qué lado de esta discrepancia se encuentra. Canonicalice este objeto con la librería JCS que utilice en su entorno:
{ "😀": 1, "\uE000": 2 }
Si la salida coloca la clave del emoji en primer lugar, su librería ordena por unidad de código UTF-16 y coincide con el RFC y la mayoría. Si coloca la clave de uso privado en primer lugar, su librería ordena por punto de código y discrepará con cualquier homólogo conforme en cualquier objeto cuyas claves mezclen caracteres de los planos suplementarios con el Plano Multilingüe Básico. Ejecute esta misma comprobación en ambos extremos de cada límite de confianza que firme JSON, especialmente cuando los dos extremos utilicen lenguajes distintos y sobre todo si alguna de sus claves puede contener Unicode controlado por un atacante.
Cómo lo medimos, para que un perfil escéptico pueda confiar en los datos
El conjunto de datos es enteramente propio y sintético. No interactuamos con ningún servicio de terceros, no realizamos escaneos ni recopilamos datos reales; cada entrada es un valor JSON desarrollado específicamente para sondear un caso límite determinado. Las librerías son paquetes públicos de código abierto, evaluados en sus versiones publicadas y nombrados porque señalar el comportamiento de una implementación es el propósito fundamental de una prueba de conformidad, no una acusación contra nadie.
- Aislamiento. Cada implementación se ejecutó en su propio contenedor con imagen oficial, procesando el corpus idéntico y escribiendo su salida canónica conservando los bytes de forma exacta, de modo que cualquier divergencia corresponde a la librería y no a nuestra infraestructura.
- Base de referencia. Las siete reproducen byte a byte la salida canónica del Apéndice B del RFC 8785, lo que confirma que se trata de implementaciones JCS legítimas y no de serializadores aproximados.
- Dos oráculos independientes. Un análisis diferencial a nivel de bytes y un proceso real de firma y verificación HMAC a través de los 42 pares ordenados de librerías. Los fallos de firma son la materialización práctica del diferencial, no una afirmación aislada.
- Segmentación rigurosa. Separamos las divergencias a nivel de bytes en entradas válidas (la cifra principal) respecto al comportamiento de aceptación frente a rechazo y a las entradas técnicamente fuera de especificación, para que ninguna métrica asuma más peso del que le corresponde.
El entorno de pruebas, el generador del corpus y los scripts de ejecución por librería se conservan para asegurar la reproducibilidad del método; lo que reportamos aquí es el resultado consolidado.
Por qué esto importa, y sus límites reales
La canonicalización está volviendo a sostener silenciosamente componentes críticos. El impulso para dotar a los agentes autónomos de identidades verificables, mediante tarjetas de agente firmadas y credenciales verificables, delega la decisión de confianza en una cadena de procesamiento JCS y posterior firma, a menudo con el firmante y el verificador desarrollados en distintos lenguajes por equipos diferentes. Esa es exactamente la condición bajo la cual una brecha de conformidad en una sola librería se traduce en un fallo de verificación. La buena noticia de este censo es que JCS ofrece una alta interoperabilidad: al elegir dos librerías conformes, son indistinguibles. La mala noticia es que la conformidad no queda garantizada por la etiqueta del paquete, y el único punto donde una librería distribuida sigue fallando hoy, el orden de claves para caracteres en planos suplementarios, resulta alcanzable por un atacante y fácil de pasar por alto.
Un resultado sin sus limitaciones es simple marketing, por lo que exponemos aquí los límites.
- Siete librerías, no el ecosistema completo. Se trata de implementaciones comunes e instalables, pero existen más, y una implementación en C o C-sharp que no hayamos evaluado podría situarse a cualquier lado de la discrepancia. El método es extrapolable; los recuentos exactos corresponden específicamente a este conjunto y a estas versiones.
- Baja probabilidad en payloads habituales. La discrepancia en el orden de las claves solo se manifiesta cuando las claves de los objetos contienen caracteres de planos suplementarios, poco comunes en tarjetas de agente convencionales o credenciales con nombres de campo en ASCII. El riesgo es real pero condicional, y un atacante con control sobre una clave es quien fuerza dicha condición deliberadamente.
- Las versiones evolucionan. Las librerías se corrigen. Un crate que hoy ordena por punto de código puede ajustarse a la norma mañana, razón por la cual la recomendación permanente es evaluar la versión específica que despliegue en lugar de confiar o desconfiar de un nombre.
- La discrepancia numérica corresponde a entradas límite. Los enteros por encima de 253 caen fuera del rango para el que JCS está definido, por lo que tratamos esa divergencia como un hallazgo secundario de menor alcance y lo dejamos fuera de las conclusiones principales.
Preguntas frecuentes
¿Qué es el JSON Canonicalization Scheme (JCS)?
JCS, definido en el RFC 8785, es un conjunto de reglas que transforma un valor JSON en una cadena de bytes determinista única. Ordena los nombres de los miembros de los objetos, serializa los números mediante el algoritmo de número a cadena de ECMAScript, aplica el escape mínimo de cadenas y descarta los espacios en blanco no significativos. Dos partes que parten del mismo valor JSON generan exactamente los mismos bytes, lo que permite calcular hashes o firmar dichos bytes con coincidencia plena. JCS es lo que permite firmar JSON sin acordar previamente su representación exacta.
¿Por qué falla la verificación de firmas JCS entre distintos lenguajes?
Falla cuando la librería del firmante y la del verificador no coinciden en los bytes canónicos para un mismo valor. En nuestra evaluación, seis de siete implementaciones coincidieron byte a byte, pero un crate de Rust ordenó las claves de los objetos por punto de código Unicode en lugar de hacerlo por unidad de código UTF-16, como exige el RFC 8785. Ante un objeto cuyas claves contienen un emoji u otro carácter de un plano suplementario, ambas librerías emiten las claves en un orden distinto, por lo que la etiqueta sobre el objeto difiere y la verificación falla.
¿Las implementaciones de JCS generan la misma salida canónica?
En su gran mayoría. En 426 entradas válidas y sin ambigüedades procesadas por siete implementaciones en cinco lenguajes, seis resultaron idénticas byte a byte en cada entrada y las siete coincidieron con el Apéndice B del RFC 8785. Una divergió en el 4.7% de las entradas, y cada divergencia implicó una clave de objeto en plano suplementario o un entero superior a 2 elevado a la 53. JCS interoperará adecuadamente con datos comunes y solo se bifurca en casos límite específicos.
¿Cómo previene JCS la elusión de firmas?
Elimina la libertad que otorga el JSON convencional (el orden de claves, los espacios en blanco y las múltiples representaciones numéricas), de modo que un valor corresponde estrictamente a una secuencia de bytes y no puede reescribirse en un equivalente de apariencia distinta. Dicha garantía se mantiene únicamente si ambas partes utilizan una implementación conforme. Una implementación no conforme reintroduce la ambigüedad que JCS debía eliminar, motivo por el cual la conformidad de la implementación, y no el simple uso de JCS, es la propiedad determinante.
¿Cómo utiliza A2A el estándar JCS para firmar tarjetas de agente?
En el protocolo Agent-to-Agent, una tarjeta de agente es un documento JSON que describe la identidad y capacidades de un agente. Su firma es un JWS sobre el contenido de la tarjeta tras haber sido canonicalizado con JCS, y se recomienda a los clientes verificar al menos una firma antes de otorgar confianza a la tarjeta. Dado que los bytes firmados corresponden a la salida de JCS, un cliente cuya librería JCS canonicalice de manera distinta a la del firmante computará una entrada diferente para la verificación.
¿Qué es un ataque de confusión por canonicalización?
Explota una discrepancia entre el modo en que un firmante y un verificador reducen un mensaje a su forma canónica. Si ambos generan bytes distintos para el mismo valor lógico, un atacante puede elaborar una entrada que se verifique como una cosa mientras significa otra, o provocar el fallo de una firma legítima. La canonicalización constituye una clase recurrente de vulnerabilidad en sistemas que firman datos estructurados, dado que la seguridad depende de que cada parte calcule exactamente la misma forma canónica.
¿Es el RFC 8785 un estándar oficial de la IETF?
El RFC 8785 es un RFC de categoría Informativa publicado por la IETF en junio de 2020. Es una especificación formal y referenciable del JSON Canonicalization Scheme, citada en desarrollos posteriores sobre JSON firmado, credenciales verificables y seguridad de agente a agente. Su condición de Informativo implica que no es un Estándar de Internet obligatorio, pero constituye la definición de referencia a la que apuntan las implementaciones.
Lecturas relacionadas
- Deterministic CBOR no es determinista entre librerías. El mismo análisis diferencial aplicado a otra codificación, en CBOR binario, donde el 26% de los valores obtuvo más de una forma canónica.
- Seguridad en servidores MCP remotos: qué muestra un censo de 19,321 servidores. La superficie de herramientas de agentes que estas tarjetas firmadas buscan proteger, medida a escala global.
- Lo que demuestran los certificados. Análisis de garantías criptográficas directamente desde los bytes en lugar de confiar en la etiqueta, el mismo método aplicado a certificados.