advanced
JWKS
JWT
RS256

Gerenciamento de Chaves JWKS

Pares de chaves RS256 por subconta, rotacao de kid, janelas de carencia e onde chaves privadas sao armazenadas.

Equipe Levy FleetsMay 18, 20269 min read

Gerenciamento de Chaves JWKS

Toda resposta MDS 2.0 carrega um header MDS-JWT -- um JWT assinado em RS256 que prova que o sha256 do corpo da resposta veio de sua subconta. O lado publico desse par de chaves e publicado em .well-known/jwks.json. Este artigo cobre como essas chaves sao armazenadas, rotacionadas e recuperadas.

Armazenamento

Chaves vivem na tabela mds_jwks_keys, uma ou mais linhas por subconta:

ColunaO que contem
idUUID
subaccount_idFK para a subconta do operador
kidID da chave aparece no header JWT e na resposta JWKS
algorithmSempre RS256 hoje
public_key_pemChave publica SPKI codificada em PEM
private_key_pemChave privada PKCS#8 codificada em PEM
is_activeExatamente uma linha por subconta tem is_active = true
created_atHora de geracao
rotated_atQuando esta chave deixou de estar ativa (null se ainda ativa)
expires_atQuando a chave cai fora da janela de carencia JWKS

Armazenamento de chave privada

private_key_pem esta atualmente armazenado sem criptografia no Supabase. Mover para pgsodium / Supabase Vault esta no roadmap antes de irmos para um cliente enterprise pagante. Trate a linha como sensivel -- nao a inclua em exportacoes, backups nao criptografados ou compartilhe com cidades.

Lazy minting

Na primeira chamada a /api/mds/{subaccountId}/.well-known/jwks.json ou qualquer endpoint /provider/v2/*, getOrCreateActiveJwksKey():

  1. Verifica mds_jwks_keys por uma linha com is_active = true.
  2. Se nenhuma existe, gera um par de chaves RS256 fresco via crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }) do Node.
  3. Converte o PEM para formato JWK (n, e, kty, alg, kid, use).
  4. Insere a linha com um kid recem gerado (formato: mds-<YYYY-MM>-<short_hash>).
  5. Retorna a linha ativa ao chamador.

Isso significa que um operador pode ser totalmente onboardado com zero configuracao manual de chave -- o primeiro ping do validador da cidade dispara o mint.

Rotacao

Rotacao anual e a cadencia recomendada, correspondendo a orientacao do OMF.

Para rotacionar:

1

Gerar uma nova chave

Insira uma nova linha com is_active = true e um kid fresco. O par de chaves e gerado no servidor; voce nunca toca a chave privada.

2

Demover a chave anterior

Defina is_active = false e rotated_at = now() na linha ativa anterior. Defina expires_at = now() + 7 dias para dar tempo as cidades de atualizar seu cache JWKS.

3

Confirmar que o JWKS publica ambas

Acesse /.well-known/jwks.json e verifique que ambos kids aparecem no array keys. JWTs assinados antes da rotacao continuam a verificar; novas respostas usam a nova chave.

4

Apos a janela de carencia

Delete a linha demovida (ou marque expires_at = now()). Ela sai do JWKS automaticamente.

Forma da resposta JWKS

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "mds-2026-05-a7c2",
      "use": "sig",
      "alg": "RS256",
      "n": "0vx7agoebGcQ...",
      "e": "AQAB"
    },
    {
      "kty": "RSA",
      "kid": "mds-2025-05-9f1d",
      "use": "sig",
      "alg": "RS256",
      "n": "1vyx8wagoeb2X...",
      "e": "AQAB"
    }
  ]
}

Cidades escolhem a chave certa combinando o kid do header JWT contra os kids no JWKS. Publicamos chaves ativas + de janela de carencia; chaves rotacionadas e expiradas nunca sao publicadas.

Verificando um JWT localmente

O mds-provider-validator do OMF faz isso automaticamente, mas voce pode verificar a mao:

# Puxar uma resposta
curl -i -H "Authorization: Bearer <token>" \
  https://fleets.levyelectric.com/api/mds/<subaccountId>/provider/v2/vehicles/status > resp.txt

# Extrair o JWT
JWT=$(grep -i '^MDS-JWT:' resp.txt | awk '{print $2}' | tr -d '\r')

# Buscar o JWKS
curl https://fleets.levyelectric.com/api/mds/<subaccountId>/.well-known/jwks.json > jwks.json

# Verificar com mkjwk ou jose CLI
jose jws ver -i <(echo "$JWT") -k <(jq '.keys[0]' jwks.json)

Uma verificacao limpa retorna payload verified mais o payload do JWT (issuer, audience, sha256 do corpo, iat, exp).

Recuperacao se uma chave privada vazar

Se a chave privada e exposta (logada acidentalmente, compartilhada com uma cidade por engano etc.):

  1. Gere um novo par de chaves imediatamente (passos acima).
  2. Defina a linha vazada para expires_at = now() -- ela cai do JWKS instantaneamente, entao nenhuma requisicao em voo pode verificar com ela.
  3. Notifique quaisquer cidades que dependem de verificacao JWT (elas re-buscarao o JWKS silenciosamente).
  4. Audite mds_policy_audit e Sentry por quaisquer requisicoes anomalas na janela do vazamento.

Auth de token bearer nao e afetada por vazamentos JWKS -- o JWT e uma camada de integridade adicional, nao a credencial primaria. Cidades que apenas validam tokens bearer nao veem impacto.

Solucao de problemas

  • Header MDS-JWT ausente em uma resposta -- mds_jwks_keys nao tem linha com is_active = true. Acesse /.well-known/jwks.json diretamente para forcar um lazy mint.
  • JWKS retorna {"keys": []} -- provavelmente a mesma causa raiz. O lazy mint deve povoa-lo na proxima leitura.
  • Verificacao JWT falha com "kid mismatch" -- o cache JWKS da cidade esta velho. Eles devem re-buscar e tentar novamente. Se ainda falhar, confirme que o kid da linha ativa corresponde ao que o header de resposta carrega.
  • Verificacao JWT falha com "signature invalid" -- a chave publica em JWKS nao corresponde a chave privada que assinou o JWT. Provavelmente duas escritas correram em paralelo no lazy mint; desative a linha mais antiga para limpar.

Proximos passos