Todas as pesquisas

Verificação de assinatura JCS: seis de sete bibliotecas da RFC 8785 concordam com a forma canônica, uma não

Sete bibliotecas JCS da RFC 8785 testadas: seis produzem JSON canônico idêntico, uma ordena chaves por code point, verificação de assinaturas no pior par falha em 10.2%.

Seis das sete bibliotecas de canonicalização JSON da RFC 8785 que testamos produzem uma saída idêntica em nível de bytes em todas as entradas válidas; a sétima não, e essa única divergência causa uma quebra na verificação de assinaturas. Executamos 432 valores JSON válidos em sete implementações independentes de JCS em cinco linguagens. Todas as sete coincidem com o exemplo prático do Apêndice B da RFC 8785, portanto, todas as sete são genuinamente JCS. Mas uma delas, a crate em Rust serde_jcs, ordena as chaves de objetos por code point Unicode em vez de code unit UTF-16, como exige a especificação. Em 4.7% das nossas entradas válidas, ela gera bytes diferentes das outras seis. Se ela estiver em um dos lados de uma assinatura e qualquer uma das outras bibliotecas estiver no outro, um objeto JSON assinado por uma pode falhar na verificação pela outra. Quando simulamos isso diretamente com HMAC, o pior par de bibliotecas falhou em verificar 10.2% das mensagens que deveria ter aceito.

A versão direta O JCS existe para que o assinador e o verificador reduzam um valor JSON exatamente aos mesmos bytes antes da assinatura. Na maior parte das vezes, funciona: seis bibliotecas em JavaScript, Python, Go e Java foram indistinguíveis em todo o nosso corpus. O risco não é que o JCS esteja quebrado, mas que sua assinatura é apenas tão canônica quanto a sua biblioteca menos compatível, e pelo menos uma biblioteca lançada não é compatível exatamente da forma que quebra assinaturas. Isso é relevante agora porque agent cards, credenciais verificáveis e payloads JWS estão sendo canonicalizados com JCS antes de serem assinados. Teste a biblioteca exata em ambas as extremidades do seu limite de confiança, não a especificação.

Por que um byte de divergência é uma falha de segurança

O JSON comum oferece liberdade demais. O mesmo objeto lógico pode ser escrito com suas chaves em qualquer ordem, com qualquer quantidade de espaços em branco e com números grafados de várias maneiras equivalentes. Essa flexibilidade é aceitável até você querer assinar o objeto, pois uma assinatura é calculada sobre bytes, não sobre o significado. Se o assinador e o verificador serializarem o objeto de formas diferentes, o verificador gera o hash de bytes distintos e a assinatura não é validada. Pior ainda, se um atacante conseguir encontrar duas sequências de bytes que um lado trata como iguais e o outro como diferentes, temos um ataque de canonicalização: uma mensagem manipulada que é verificada como uma coisa, enquanto significa outra. Um levantamento de 2026 cataloga falhas de canonicalização como uma classe recorrente de vulnerabilidade exatamente nesses sistemas.

RFC 8785, o JSON Canonicalization Scheme, é a solução. Ele elimina qualquer grau de liberdade: os nomes dos membros do objeto são ordenados, os números são serializados com o algoritmo Number.prototype.toString do ECMAScript, as strings recebem o escape mínimo permitido e espaços em branco insignificantes são descartados. Forneça o mesmo valor JSON a qualquer implementação compatível e você obterá os mesmos bytes, garantindo que ambas as extremidades de uma assinatura concordem sobre o que foi assinado. Esse esquema agora é estrutural. O protocolo Agent-to-Agent assina um agent card como um JWS sobre o conteúdo do card canonicalizado com JCS, e orienta os clientes a verificarem pelo menos uma assinatura antes de confiar no card. Credenciais verificáveis e fluxos JWS detached fazem o mesmo. Os bytes canônicos são o limite de confiança.

Portanto, a questão interessante não é se a especificação é sólida. É se as bibliotecas que as pessoas realmente implementam concordam entre si. Nós medimos isso.

O que testamos

Reunimos sete implementações independentes de JCS em cinco linguagens, escolhidas por serem as que um desenvolvedor provavelmente obteria em um gerenciador de pacotes:

Cada uma foi executada em seu próprio contêiner com imagem oficial, garantindo que o resultado seja reproduzível e que nenhuma biblioteca influencie outra. Criamos um corpus de 440 entradas manipuladas, sendo 432 delas JSON válido, classificadas por categoria: formatação de números perto dos limites do IEEE-754 e do ECMAScript, zero negativo, casos de ordenação de chaves com caracteres que se ordenam de forma diferente em UTF-16 e em code point, escape e normalização Unicode, chaves duplicadas, surrogates isolados e casos extremos estruturais. Enviamos os mesmos bytes para todas as bibliotecas e capturamos sua saída canônica, preservando os bytes com exatidão para que nenhuma divergência fosse um artefato do nosso próprio ferramental.

Dois oráculos analisaram as saídas. O primeiro é um teste diferencial direto: para cada entrada, as bibliotecas que tiveram sucesso produzem os mesmos bytes? O segundo é o próprio ataque: com uma chave HMAC-SHA256 fixa, uma biblioteca assinadora canonicaliza uma entrada e gera a tag, e então uma biblioteca verificadora canonicaliza a mesma entrada e valida a tag. Cada par ordenado de bibliotecas, cada entrada. Uma falha aí representa uma assinatura entre bibliotecas que deveria ser verificada, mas não é.

Antes de confiar em qualquer resultado, verificamos o dado de referência. Todas as sete implementações reproduzem a saída canônica do Apêndice B da RFC 8785 byte a byte, incluindo seus números deliberadamente complexos e sua string com escape. Todas são JCS de verdade. É isso que torna a divergência abaixo significativa: não se trata de uma biblioteca quebrada contra uma correta, mas de sete bibliotecas aparentemente corretas, uma das quais falha silenciosamente em um caso extremo.

O resultado: 4.7% das entradas válidas divergem, e sempre da mesma forma

Nas 426 entradas válidas e inequívocas que todas as bibliotecas aceitaram, seis das sete foram idênticas em nível de bytes em todas elas. Não apenas próximas: idênticas, em cada valor. A sétima, serde_jcs, divergiu em 20 dessas 426 entradas, o que representa 4.7%, e foi a única minoria em todas as vezes. Quando as bibliotecas JCS divergem, no nosso corpus, é uma biblioteca contra todas as outras.

Divergência entre bibliotecas em 426 entradas válidas e inequívocas, por categoria
CategoriaEntradasDivergentesCausa
Ordenação de chaves8014ordem de code point vs UTF-16
Formatação de números1946inteiros acima de 253
Zero negativo180todas concordam (-0 para 0)
Escape Unicode760todas concordam
Normalização Unicode270todas concordam (sem NFC/NFD silencioso)
Estrutural300todas concordam

Duas categorias concentram toda a divergência, e a primeira é a que realmente importa.

A divergência na ordenação de chaves: code point versus code unit UTF-16

A RFC 8785 é explícita ao definir que os nomes dos membros dos objetos são ordenados por sua codificação UTF-16, comparando code units. Esse detalhe existe porque o JCS herda a ordenação de strings do ECMAScript, e strings no ECMAScript são sequências de code units UTF-16. Para caracteres no Basic Multilingual Plane, a ordem de code unit e a ordem de code point são as mesmas, então quase ninguém percebe. Elas divergem apenas para caracteres do plano suplementar, aqueles acima de U+FFFF codificados como um par de surrogates. Um emoji como U+1F600 tem o code point 0x1F600, que é maior que um caractere do Basic Multilingual Plane como U+E000. Mas sua codificação UTF-16 começa com a code unit de high-surrogate 0xD83D, que é menor que 0xE000. Portanto, sob as duas regras, as duas chaves são ordenadas de maneiras opostas.

Quatorze das nossas entradas de ordenação de chaves atingiram exatamente essa linha de falha, e a serde_jcs ordena por code point, enquanto as outras seis ordenam por code unit UTF-16. Aqui está o menor caso, um objeto com duas chaves:

input:      {"😀": 1, "\uE000": 2}

six libs:   {"😀":1,"\uE000":2}     😀 first  (0xD83D < 0xE000)
serde_jcs:  {"\uE000":2,"😀":1}     😀 last   (0x1F600 > 0xE000)

O caractere de uso privado U+E000 é exibido aqui com escape para facilitar a leitura; o JCS na verdade o emite como um byte literal, e ambas as bibliotecas concordam nessa parte. No que elas não concordam é na ordem dos dois membros, e isso é o bastante. Assine o objeto da esquerda com HMAC ou JWS, entregue-o a um verificador executando serde_jcs, e o verificador o canonicaliza como o objeto da direita, calcula uma tag diferente e rejeita uma assinatura perfeitamente válida. Inverta os papéis e o mesmo acontece no sentido oposto. As duas bibliotecas têm uma divergência estável e silenciosa sobre qual é a forma canônica desse objeto.

Esta não é uma descoberta exótica sobre o esquema. Trata-se da clássica armadilha de conformidade do JCS, apontada na própria RFC e em artigos anteriores sobre canonicalização. O que nossa medição acrescenta é a taxa base: de sete bibliotecas lançadas e instaláveis que passam no próprio vetor de teste da RFC, uma ainda erra nisso hoje. Conformidade com o exemplo principal não implica conformidade nos casos extremos.

A divergência numérica e por que a relatamos separadamente

As outras seis entradas divergentes são números inteiros maiores que 253, como 9007199254740993. O JCS especifica que os números são serializados por meio do algoritmo number-to-string do ECMAScript, que utiliza um double IEEE-754. Acima de 253, um double já não consegue representar todos os inteiros, então 9007199254740993 arredonda para 9007199254740992. Seis bibliotecas fazem exatamente isso. A serde_jcs prefere preservar o inteiro com exatidão. Destacamos com franqueza que esse é um achado mais fraco do que a divergência na ordenação de chaves: a RFC 8785 é definida apenas para números no intervalo de double do IEEE-754, de modo que um inteiro acima de 253 pode ser considerado uma entrada fora da especificação que um chamador nunca deveria passar. Mesmo assim, continua sendo um JSON sintaticamente válido, ainda produz uma divergência em nível de bytes entre bibliotecas reais, e um sistema que aceite identificadores numéricos maiores que um safe integer do JavaScript ainda pode cair nisso. Mantemos isso na análise diferencial e fora do título principal.

Todo o restante se manteve. O zero negativo foi canonicalizado para 0 em todos os casos. Nenhuma biblioteca aplicou normalização Unicode silenciosamente, portanto uma string em NFC e a mesma string em NFD permaneceram distintas em todas as sete, o que é o correto: o JCS não normaliza. O escape de strings foi uniforme. As divergências são pontuais e específicas, o que as torna perigosas em vez de óbvias.

Transformando a divergência em uma falha de assinatura

Uma divergência nos bytes canônicos só é interessante se quebrar algo. Então fizemos quebrar. Usando uma chave HMAC-SHA256 fixa, fizemos cada biblioteca atuar como assinadora e cada biblioteca atuar como verificadora em todas as entradas válidas nas quais ambas produziram bytes: 42 pares ordenados, 11,754 verificações no total. Uma verificação falha quando os bytes canônicos do assinador e os do verificador diferem, pois as tags resultantes serão diferentes.

Em todos os pares, 1.87% das verificações falharam. Esse número é diluído pelas seis bibliotecas que nunca discordam entre si, portanto a forma correta de interpretá-lo é por par. Todo par que não envolve a serde_jcs verificou com 100% de sucesso. Todo par que combina a serde_jcs com uma biblioteca compatível falhou nas entradas divergentes. A pior combinação, a referência em Go da cyberphone como assinadora e a serde_jcs como verificadora, falhou em 15 de 147 verificações, uma taxa de falha de verificação de assinatura de 10.2% entre duas bibliotecas que passam no vetor de teste da RFC. Em uma implantação que combina linguagens através de um limite de confiança, e sistemas agent-to-agent fazem exatamente isso, essa é a falha que você realmente veria.

O outro eixo: algumas bibliotecas rejeitam o que outras aceitam

Além das divergências em nível de bytes, encontramos uma grande superfície de aceitação versus rejeição: 288 entradas em que pelo menos uma biblioteca retornou saída e outra retornou erro. Isso não é uma discordância de forma canônica, mas é um risco de interoperabilidade por si só, dividindo as bibliotecas em campos opostos.

Mantemos isso fora do número de destaque porque se trata de aceitação versus rejeição, não de byte contra byte, mas a lição é a mesma: a garantia reside na implementação, não na sigla de três letras.

O reflexo inverso do nosso resultado com CBOR

Executamos o mesmo experimento em outra codificação, no CBOR binário, e obtivemos o cenário oposto. Nele, o CBOR determinístico não foi determinístico entre bibliotecas: 26% dos valores resultaram em mais de uma codificação canônica, e a divergência se espalhou por todo o conjunto de bibliotecas. O JCS é o irmão organizado. Suas regras são mais estritas e suas bibliotecas concordam na maioria dos casos, portanto a divergência se reduz a um ponto fora da curva e a um punhado de entradas extremas, em vez de um quarto de tudo. Esse contraste é a lição prática. A segurança de um esquema de canonicalização não é uma propriedade apenas do esquema; é o produto do rigor do esquema e da conformidade das bibliotecas específicas no seu caminho. O JCS oferece um esquema melhor. Ele não isenta você de verificar suas bibliotecas.

Teste sua própria biblioteca em dez segundos

Você não precisa do nosso harness para descobrir de que lado dessa divisão você está. Canonicalize este objeto com a biblioteca JCS que você utiliza:

{ "😀": 1, "\uE000": 2 }

Se a saída colocar a chave com emoji primeiro, sua biblioteca ordena por code unit UTF-16 e está em conformidade com a RFC e com a maioria. Se ela colocar a chave de uso privado primeiro, sua biblioteca ordena por code point e divergirá de um par compatível em qualquer objeto cujas chaves misturem caracteres do plano suplementar e do Basic Multilingual Plane. Execute essa mesma verificação em ambas as extremidades de cada limite de confiança que assina JSON, especialmente onde as duas extremidades usam linguagens diferentes e sobretudo se qualquer uma das suas chaves puder conter Unicode influenciado por invasores.

Como medimos isso, para que os céticos possam confiar nos números

O conjunto de dados é inteiramente nosso e sintético. Não acessamos nenhum serviço de terceiros, não realizamos varreduras nem coletamos dados do mundo real; cada entrada é um valor JSON que escrevemos para testar um caso extremo específico. As bibliotecas são pacotes públicos de código aberto, testados em suas versões publicadas e identificados nominalmente porque apontar o comportamento de uma implementação de especificação é todo o propósito de um teste de conformidade, e não uma acusação contra ninguém.

O harness, o gerador de corpus e os executores de cada biblioteca foram mantidos para que o método seja reproduzível; relatamos aqui apenas o agregado.

Por que isso importa e os limites realistas

A canonicalização está silenciosamente se tornando estrutural de novo. O esforço para fornecer identidades verificáveis a agentes autônomos, por meio de agent cards assinados e credenciais verificáveis, passa a decisão de confiança por um pipeline de JCS seguido de assinatura, frequentemente com o assinador e o verificador escritos em linguagens diferentes por equipes distintas. Essa é exatamente a condição na qual uma falha de conformidade em uma única biblioteca se transforma em uma quebra de verificação. A boa notícia desse levantamento é que o JCS tem boa interoperabilidade: escolha duas bibliotecas em conformidade e elas serão indistinguíveis. A má notícia é que a conformidade não é garantida pelo rótulo, e o único ponto onde uma biblioteca lançada ainda erra, a ordenação de chaves para caracteres do plano suplementar, é acessível a invasores e fácil de passar despercebido.

Um resultado sem limites claros é apenas marketing, portanto aqui estão as ressalvas.

Perguntas frequentes

O que é o JSON Canonicalization Scheme (JCS)?

O JCS, definido na RFC 8785, é um conjunto de regras que transforma um valor JSON em uma sequência de bytes determinística. Ele ordena os nomes dos membros dos objetos, serializa números com o algoritmo number-to-string do ECMAScript, aplica escape mínimo de strings e descarta espaços em branco insignificantes. Duas partes que partem do mesmo valor JSON produzem os mesmos bytes, permitindo gerar hashes ou assinar esses bytes com correspondência exata. O JCS é o que permite assinar JSON sem a necessidade de combinar previamente sua grafia exata.

Por que a verificação de assinaturas JCS falha entre linguagens diferentes?

Ela falha quando a biblioteca do assinador e a do verificador discordam sobre os bytes canônicos para o mesmo valor. Em nosso teste, seis de sete implementações concordaram byte a byte, mas uma crate em Rust ordenou as chaves de objetos por code point Unicode em vez de code unit UTF-16, como exige a RFC 8785. Para um objeto cujas chaves incluem um emoji ou outro caractere do plano suplementar, as duas bibliotecas emitem as chaves em ordens diferentes, fazendo com que a tag sobre o objeto seja diferente e a verificação falhe.

As implementações de JCS produzem a mesma saída canônica?

Na maior parte das vezes. Em 426 entradas válidas e inequívocas fornecidas a sete implementações em cinco linguagens, seis foram idênticas byte a byte em todas as entradas e todas as sete coincidiram com o Apêndice B da RFC 8785. Uma divergiu em 4.7% das entradas, e cada divergência envolveu uma chave de objeto no plano suplementar ou um número inteiro superior a 2 elevado à 53ª potência. O JCS apresenta boa interoperabilidade em dados comuns e diverge apenas em casos extremos específicos.

Como o JCS impede um bypass de assinatura?

Ele remove a liberdade que o JSON comum oferece: ordem de chaves, espaços em branco e as várias formas de representar um número, de modo que um valor seja mapeado para uma única sequência de bytes e não possa ser recodificado em um equivalente visualmente diferente. Essa garantia só se sustenta se ambos os lados usarem uma implementação em conformidade. Uma implementação não compatível reintroduz a ambiguidade que o JCS deveria eliminar, e é por isso que a conformidade da implementação, e não o simples uso do JCS, é a propriedade essencial.

Como o A2A usa o JCS para assinar agent cards?

No protocolo Agent-to-Agent, um agent card é um documento JSON que descreve a identidade e as capacidades de um agente. Sua assinatura é um JWS sobre o conteúdo do card após esse conteúdo ser canonicalizado com JCS, e os clientes são orientados a verificar pelo menos uma assinatura antes de confiar em um card. Como os bytes assinados são a saída do JCS, um cliente cuja biblioteca JCS canonicalize de forma diferente da do assinador calculará uma entrada distinta para a verificação.

O que é um ataque de confusão de canonicalização?

Ele explora uma divergência entre a forma como o assinador e o verificador reduzem uma mensagem à forma canônica. Se os dois produzirem bytes diferentes para o mesmo valor lógico, um invasor pode criar uma entrada que é verificada como uma coisa enquanto significa outra, ou fazer com que uma assinatura legítima falhe. A canonicalização é uma classe recorrente de vulnerabilidades em sistemas que assinam dados estruturados, pois a segurança depende de todas as partes calcularem a mesma forma canônica.

A RFC 8785 é um padrão oficial da IETF?

A RFC 8785 é uma RFC informativa da IETF publicada em junho de 2020. É uma especificação estável e de referência do JSON Canonicalization Scheme, citada por trabalhos posteriores sobre JSON assinado, credenciais verificáveis e segurança agent-to-agent. O status informativo significa que ela não é um padrão obrigatório da Internet (Internet Standard), mas é a definição apontada pelas implementações.

Leituras relacionadas