Firmare un JWT con ML-DSA in Delphi

· Componenti
Firmare un JWT con ML-DSA in Delphi

Un JWT è una cosa piccola con una portata lunga. È la prova di identità tra i vostri servizi, e l'algoritmo che lo firma tende a essere la parte di un sistema più lenta da cambiare, perché ogni emittente e ogni verificatore devono muoversi insieme. Per questo vale la pena sapere che l'opzione esiste prima di averne bisogno.

sgcWebSockets 2026.10 aggiunge al client e al server JWT gli algoritmi JWT post-quantistici di RFC 9964: ML-DSA-44, ML-DSA-65 e ML-DSA-87. Si affiancano a HS, RS ed ES come altri tre valori della stessa proprietà, e tutto il resto del vostro token resta com'era.

Emettere un token

Scegliete l'algoritmo nell'header, fornite al componente una chiave privata e firmate:

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;

La chiave privata è un normale PEM PKCS#8. Vengono lette tutte e tre le forme definite da RFC 9881, quindi una chiave memorizzata come seed di 32 byte, come chiave espansa, o come entrambe, funziona in ogni caso.

Convalidarne uno

Il lato server ha la stessa forma, con la chiave pubblica e la famiglia attivate:

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);

Ogni famiglia viene attivata singolarmente, quindi una distribuzione che emette solo token ML-DSA può disattivare le altre, e un token che arriva con alg impostato su qualcos'altro viene rifiutato invece di essere controllato.

Il set di parametri deve corrispondere

C'è una regola in RFC 9964 che vale la pena dire ad alta voce, perché è l'errore che altrimenti passerebbe inosservato: la chiave deve appartenere al set di parametri indicato da alg. Una chiave che non corrisponde viene rifiutata. Firmare con quella sbagliata genera un'eccezione, e convalidare con quella sbagliata restituisce false invece di generare un'eccezione, così un attaccante non può usare questa differenza per scoprire nulla.

Chiavi come JSON Web Key

RFC 9964 registra un tipo di chiave per questi algoritmi, AKP, e la libreria lo legge e lo scrive, il che è quello che serve quando le chiavi vengono pubblicate su un endpoint JWKS invece di essere lasciate in un file:

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;

Il membro privato è il seed di 32 byte, che è quanto definito dall'RFC, e il lettore è rigoroso al riguardo: i membri devono essere base64url canonico della lunghezza esatta, alg è obbligatorio, e quando è presente una chiave privata deve corrispondere a quella pubblica nella stessa JWK. Una JWK che non rispetta anche solo uno di questi punti viene rifiutata invece di essere caricata a metà.

Interoperabilità, dimostrata

Un formato di firma è utile solo se qualcun altro produce gli stessi byte. Due verifiche vengono distribuite come test, non come semplici affermazioni:

Già che c'eravamo: un segreto HMAC binario

Una limitazione non correlata è scomparsa nello stesso intervento. Gli algoritmi HS ricevevano la chiave come testo, e una vera chiave HMAC è composta da byte casuali, la maggior parte dei quali non è affatto testo valido. Ora esiste una seconda proprietà che riceve la chiave come base64url, la stessa codifica usata dal membro k di una JWK di tipo oct:

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

Quando è impostata, ha la precedenza sul segreto testuale, sia sul client sia sul server.

Claim scritti da qualcun altro

Un'altra correzione utile da sapere se accettate token da altri emittenti. Una stringa JSON può essere scritta come UTF-8 grezzo oppure con ogni carattere non ASCII sottoposto a escape, e librerie diverse scelgono in modo diverso. Ora entrambe le forme si risolvono nello stesso testo, così un nome con un accento viene letto correttamente indipendentemente da quale emittente lo abbia prodotto, e un header alg scritto con escape viene riconosciuto come l'algoritmo che rappresenta.

Aggiornamento

I tre algoritmi vengono aggiunti in fondo all'elenco esistente, così un valore ordinale memorizzato conserva il suo significato e nulla di ciò che già fate cambia. Firmare con ML-DSA richiede il pacchetto sgcCrypto, e una build priva di esso rifiuta questi token con un messaggio chiaro invece di fallire in modo oscuro.

Continua a leggere

Guardalo

C'è un breve video su questo argomento sul canale eSeGeCe.

Domande, commenti o aiuto con la migrazione? Mettetevi in contatto — riceverete una risposta dalle persone che hanno scritto il codice.