← All posts

As três vidas da minha chave de assinatura JWT

Authagonal·July 17, 2026
authjwtkey-managementkmsvaultecdsaoidc

Uma única chave privada assina todos os tokens que o nosso servidor de autenticação emite. Perca-a e um atacante cunha um token válido para qualquer usuário, sem precisar de senha, e os seus logs mostram um login impecável. É o segredo mais valioso de todo o sistema e, ao longo da vida, ela se mudou duas vezes: nasceu dentro do processo da aplicação, depois foi exilada para um cofre do qual já não consegue sair e, por fim, reencarnou como um tipo de chave completamente diferente. Cada mudança aconteceu num emissor em produção, com tokens já em trânsito e validadores fazendo cache de um estado que não controlamos. Eis o que essas mudanças de fato exigem — e a que mais nos ensinou foi justamente a que não anunciamos.

Vida um: a chave que o servidor guardava no bolso

No início, a chave morava onde era usada. O servidor gerava uma chave RSA-2048, mantinha-a em memória e assinava seus tokens com RS256. Esse é o padrão em quase todo lugar, e funciona muito bem — até o momento em que você pronuncia em voz alta a palavra custódia. A chave privada estava ao alcance de qualquer coisa capaz de ler a memória do processo, sua configuração ou um backup de um deles. Queríamos poder dizer aos clientes que um banco de dados vazado não expõe nada de útil, mas uma chave de assinatura a apenas um processo de distância desse banco transformava essa afirmação numa mentira. A chave precisava deixar o prédio.

Vida dois: a chave que se mudou para um cofre do qual não consegue sair

Introduzimos uma costura — um ISignatureProvider que o servidor chama sempre que precisa de uma assinatura — e colocamos um KMS por trás dela. Agora a chave é gerada dentro do transit engine do Vault e nunca é exportada. A aplicação não a detém; ela envia ao cofre os bytes a serem assinados e recebe de volta uma assinatura. Ela consegue usar a chave, mas não consegue exfiltrá-la.

Essa distinção é o ponto central. Um dump de memória, um arquivo de configuração derramado, um backup vazado — nenhum deles contém mais a chave, porque a chave nunca esteve em nenhum desses lugares. Comprometer a aplicação agora dá ao atacante a capacidade de pedir ao cofre que assine, algo que podemos limitar por taxa, auditar e revogar. Já não lhe dá a própria chave, contra o que nada poderíamos fazer uma vez que ela tivesse escapado.

Aqui está a parte que não colocamos nas notas de versão. O mesmo commit que moveu a chave para o cofre também reduziu silenciosamente pela metade a nossa contagem de iterações do PBKDF2, de 100.000 para 50.000. Um único diff, duas mudanças de segurança apontando em direções opostas: a manchete era “assinar no KMS” e, pegando carona por baixo, ia um fator de trabalho de hashing de senha cortado pela metade, numa linha que ninguém observava porque a história era sobre outra coisa. Ambos são código de segurança, então os dois foram revisados como uma única mudança “mais segura” e aprovados sem maior escrutínio, apoiados na força da metade boa.

A correção teve uma linha. A lição, não. Um diff rotulado como mais seguro continua sendo um diff, e as constantes sensíveis à segurança dentro dele — contagens de iterações, tamanhos de chave, timeouts, nomes de algoritmos — não herdam nenhuma auréola da mensagem de commit. Hoje tratamos um número de fator de trabalho da mesma forma que tratamos a escolha de um algoritmo: algo que se afirma e se verifica, não algo em que você confia porque a mudança ao redor era uma boa ideia.

Vida três: a chave que encolheu para ficar mais rápida

Com a assinatura por trás de um KMS, cada assinatura é uma ida e volta pela rede, e a assinatura RSA é do tipo caro. Então a chave mudou de espécie: RS256 sobre RSA-2048 virou ES256 sobre a curva P-256. A assinatura com curva elíptica é cerca de uma ordem de magnitude mais barata que a RSA, uma assinatura P-256 tem 64 bytes onde a da RSA-2048 tem 256, e o conjunto de chaves públicas que os validadores baixam encolhe na mesma medida. Chave menor, assinatura menor, JWKS menor, tokens mais rápidos — tudo isso por escolher uma curva melhor.

O problema é que não dá para trocar o algoritmo de assinatura de uma vez num emissor em produção. Todo token já emitido foi assinado com RS256 e precisa continuar validando até expirar. Todo validador tem a sua chave pública antiga em cache. Troque o algoritmo num único movimento e você invalida, no mesmo instante, todos os tokens em trânsito e todos os conjuntos de chaves em cache — uma queda autoinfligida disfarçada de melhoria.

O que funcionou foi parar de fingir que sempre houve apenas uma chave. O repositório de chaves passou a ser agnóstico quanto ao algoritmo: ele guarda a chave RSA e a chave EC ao mesmo tempo e publica ambas no JWKS, cada uma sob seu próprio key id e algoritmo. Um seletor de chave ativa, separado, decide qual chave assina os novos tokens, e ele se afasta sozinho da chave RSA legada no instante em que uma chave EC passa a existir — sem flag de configuração, sem deploy cronometrado para coincidir com uma rotação. Durante a sobreposição, o discovery anuncia ambas as chaves públicas, de modo que os tokens assinados com qualquer uma delas continuam validando. Assim que o último token RS256 expira, o discovery passa a anunciar apenas ES256 e a validação fixa ValidAlgorithms = ES256, o que também bate a porta na cara dos ataques de confusão de algoritmo que tentam convencer um validador a aceitar o esquema errado. A migração é um crossfade que o emissor executa em si mesmo, não um interruptor que alguém aciona.

A lição, destilada

Uma chave de assinatura parece uma constante: um segredo, definido uma vez, referenciado para sempre. A nossa não era. Ao longo da vida ela mudou de custódia, do processo para um KMS, e mudou de espécie, de RSA para curva elíptica, e ambas as mudanças tiveram de ser feitas com tokens em trânsito e validadores fazendo cache de coisas que não nos pertencem. A propriedade que tornou cada mudança sobrevivível foi a mesma nas duas vezes: o sistema nunca presumiu que havia exatamente uma chave, um algoritmo ou um único lar. Construa o repositório de chaves para guardar várias chaves e para anunciar com honestidade quais ele guarda, e a rotação — de onde a chave mora ou de que tipo de chave ela é — deixa de ser uma queda que você agenda e passa a ser uma propriedade que o próprio emissor gerencia.

Se você opera autenticação, aquilo que assina seus tokens deveria ser o objeto mais chato, mais inspecionável e menos esperto que você possui. O nosso levou três vidas para chegar lá. Valeu cada uma delas.