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:
| Coluna | O que contem |
|---|---|
id | UUID |
subaccount_id | FK para a subconta do operador |
kid | ID da chave aparece no header JWT e na resposta JWKS |
algorithm | Sempre RS256 hoje |
public_key_pem | Chave publica SPKI codificada em PEM |
private_key_pem | Chave privada PKCS#8 codificada em PEM |
is_active | Exatamente uma linha por subconta tem is_active = true |
created_at | Hora de geracao |
rotated_at | Quando esta chave deixou de estar ativa (null se ainda ativa) |
expires_at | Quando 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():
- Verifica
mds_jwks_keyspor uma linha comis_active = true. - Se nenhuma existe, gera um par de chaves RS256 fresco via
crypto.generateKeyPairSync('rsa', { modulusLength: 2048 })do Node. - Converte o PEM para formato JWK (
n,e,kty,alg,kid,use). - Insere a linha com um
kidrecem gerado (formato:mds-<YYYY-MM>-<short_hash>). - 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:
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.
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.
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.
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.):
- Gere um novo par de chaves imediatamente (passos acima).
- Defina a linha vazada para
expires_at = now()-- ela cai do JWKS instantaneamente, entao nenhuma requisicao em voo pode verificar com ela. - Notifique quaisquer cidades que dependem de verificacao JWT (elas re-buscarao o JWKS silenciosamente).
- Audite
mds_policy_audite 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-JWTausente em uma resposta --mds_jwks_keysnao tem linha comis_active = true. Acesse/.well-known/jwks.jsondiretamente 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
kidda 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
- Configuracao do MDS Provider -- referencia completa de endpoints.
- Solucao de Problemas -- quando JWTs ou JWKS dao errado.