Assinar um JWT com ML-DSA em Delphi

· Componentes
Assinar um JWT com ML-DSA em Delphi

Um JWT é algo pequeno com um alcance enorme. É a prova de identidade entre seus serviços, e o algoritmo que o assina costuma ser a parte de um sistema mais lenta de mudar, porque cada emissor e cada verificador precisam mudar ao mesmo tempo. Por isso vale a pena saber que a opção existe antes de você precisar dela.

O sgcWebSockets 2026.10 adiciona ao cliente e ao servidor JWT os algoritmos JWT pós-quânticos da RFC 9964: ML-DSA-44, ML-DSA-65 e ML-DSA-87. Eles ficam ao lado de HS, RS e ES como mais três valores da mesma propriedade, e tudo o mais no seu token permanece como estava.

Emitindo um token

Escolha o algoritmo no cabeçalho, forneça ao componente uma chave privada e assine:

oJWT := TsgcHTTP_JWT_Client.Create(nil);
oJWT.JWTOptions.Header.alg := jwtMLDSA65;
oJWT.JWTOptions.Algorithms.MLDSA.PrivateKey.Text := vPrivatePEM;
oJWT.JWTOptions.Payload.iss := 'my-service';
oJWT.JWTOptions.Payload.sub := 'user-1';

vToken := oJWT.Sign;

A chave privada é um PEM PKCS#8 comum. As três formas definidas pela RFC 9881 são lidas, de modo que uma chave armazenada como a semente de 32 bytes, como a chave expandida, ou como ambas, funciona em qualquer um dos casos.

Validando um token

O lado do servidor tem o mesmo formato, com a chave pública e a família ativadas:

oServer := TsgcHTTP_JWT_Server.Create(nil);
oServer.JWTOptions.Algorithms.MLDSA.Enabled := True;
oServer.JWTOptions.Algorithms.MLDSA.PublicKey.Text := vPublicPEM;

if oServer.Validate(vToken, vHeader, vPayload, vError) then
  ShowMessage(vPayload);

Cada família é habilitada de forma independente, de modo que uma implantação que só emite tokens ML-DSA pode desativar as outras, e um token que chega com alg definido para outra coisa é recusado em vez de verificado.

O conjunto de parâmetros precisa corresponder

Há uma regra na RFC 9964 que vale a pena declarar explicitamente, porque é o erro que passaria despercebido: a chave precisa pertencer ao conjunto de parâmetros que o alg indica. Uma chave que não corresponde é recusada. Assinar com a errada gera uma exceção, e validar com a errada retorna falso em vez de gerar exceção, de modo que um atacante não pode usar essa diferença para aprender nada.

Chaves como JSON Web Keys

A RFC 9964 registra um tipo de chave para esses algoritmos, AKP, e a biblioteca lê e grava esse tipo, o que é exatamente o que você quer quando as chaves são publicadas em um endpoint JWKS em vez de guardadas em um arquivo:

uses
  sgcHTTP_JWT_MLDSA;

var
  vJWK, vPublicPEM, vPrivatePEM: string;
begin
  // {"kty":"AKP","alg":"ML-DSA-65","pub":"...","priv":"..."}
  vJWK := sgcMLDSA_ExportPrivateJWK(mldsa65, oSeed);

  // and back again, as the PEM the components read
  sgcMLDSA_ImportJWKAsPEM(vJWK, vPublicPEM, vPrivatePEM);
end;

O membro privado é a semente de 32 bytes, que é o que a RFC define, e o leitor é rigoroso quanto a isso: os membros devem ser base64url canônico com exatamente o comprimento correto, alg é obrigatório, e quando uma chave privada está presente ela precisa concordar com a chave pública no mesmo JWK. Um JWK que falha em qualquer um desses pontos é rejeitado em vez de carregado pela metade.

Interoperabilidade demonstrada

Um formato de assinatura só é útil se outra pessoa produzir os mesmos bytes. Duas verificações são entregues como testes, e não apenas como afirmações:

Já que estávamos ali: um segredo HMAC binário

Uma limitação sem relação com o assunto desapareceu na mesma passagem. Os algoritmos HS recebiam a chave como texto, e uma chave HMAC de verdade é composta de bytes aleatórios, a maioria dos quais nem sequer é texto válido. Agora existe uma segunda propriedade que recebe a chave como base64url, a mesma codificação usada pelo membro k de um JWK do tipo oct:

oJWT.JWTOptions.Algorithms.HS.SecretBase64URL := 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ...';

Quando definida, ela tem precedência sobre o segredo em texto, tanto no cliente quanto no servidor.

Claims escritos por outra pessoa

Mais uma correção que vale a pena conhecer se você aceita tokens de outros emissores. Uma string JSON pode ser escrita como UTF-8 puro ou com cada caractere não ASCII escapado, e bibliotecas diferentes escolhem de formas diferentes. As duas formas agora resultam no mesmo texto, de modo que um nome com acento é lido corretamente qualquer que seja o emissor que o produziu, e um cabeçalho alg escrito com escapes é reconhecido como o algoritmo que ele representa.

Atualizando

Os três algoritmos são adições ao final da lista existente, de modo que um ordinal já armazenado mantém seu significado e nada do que você já faz muda. Assinar com ML-DSA exige o pacote sgcCrypto, e uma build sem ele recusa esses tokens com uma mensagem clara em vez de falhar de forma obscura.

Leia também

Assista

Há um vídeo curto sobre isso no canal eSeGeCe.

Perguntas, feedback ou ajuda com a migração? Entre em contato — você receberá uma resposta das pessoas que escreveram o código.