Todas las investigaciones

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

Siete librerías RFC 8785 JCS evaluadas: seis generan JSON canónico idéntico, una ordena claves por punto de código, el peor emparejamiento falla el 10.2% de las firmas.

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.

La versión directa JCS existe para que el firmante y el verificador reduzcan un valor JSON exactamente a los mismos bytes antes de firmar. En su mayor parte funciona: seis librerías en JavaScript, Python, Go y Java fueron indistinguibles en todo nuestro corpus. El riesgo no es que JCS esté roto, sino que su firma es tan canónica como su librería menos conforme a la norma, y al menos una librería publicada incumple la especificación precisamente en el aspecto que rompe las firmas. Esto es crítico ahora porque las tarjetas de agente, las credenciales verificables y los payloads JWS se están canonicalizando con JCS antes de firmarse. Evalúe la librería exacta en ambos extremos de su límite de confianza, no la especificación.

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:

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.

Divergencia entre librerías en 426 entradas válidas y sin ambigüedad, por categoría
CategoríaEntradasDivergentesCausa
Ordenación de claves8014orden por punto de código vs UTF-16
Formato de números1946enteros superiores a 253
Cero negativo180todas coinciden (-0 a 0)
Escape de Unicode760todas coinciden
Normalización Unicode270todas coinciden (sin NFC/NFD silencioso)
Estructural300todas 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.

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.

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.

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