GoSuda

Esperienza nell'uso di MLDSA e MLKEM nel linguaggio Go

By snowmerak views ...

Panoramica

Contesto

Da parecchio tempo il rapido calcolo dei computer quantistici è considerato una minaccia per i sistemi crittografici esistenti. Algoritmi come RSA ed ECC, infatti, rischiano di essere decifrati grazie a questa capacità di calcolo. Tuttavia, a partire da alcuni anni fa, il concetto di computer quantistico ha iniziato a delinearsi concretamente, e sono quindi iniziate la ricerca e lo sviluppo di alternative, mentre il NIST ha portato avanti la standardizzazione della PQC (Post-Quantum Cryptography, crittografia post-quantistica).

MLDSA e MLKEM

Alla fine, nell'agosto 2024, il NIST ha adottato come standard MLKEM e MLDSA, basati su CRYSTALS-Kyber e CRYSTALS-Dilithium. Entrambi gli algoritmi operano sulla base del problema MLWE (Module Learning with Errors). Questa tipologia è da noi definita crittografia basata su reticoli.

La crittografia basata su reticoli, come suggerisce il nome, è un sistema crittografico fondato sulla difficoltà dei problemi matematici definiti su reticoli. Non possiedo una conoscenza matematica approfondita in materia, ma riassumendo in una riga, si tratta di il problema di risolvere un sistema di equazioni lineari con rumore su un reticolo modulare. Non è facile farsi un'idea precisa di quanto sia difficile, ma si dice che tali problemi siano talmente ardui da non poter essere risolti nemmeno dai computer quantistici.

MLDSA

Vediamo innanzitutto MLDSA.

Struttura

MLDSA, come si evince dal nome, è un algoritmo di firma asimmetrica e si articola nelle seguenti due fasi:

  1. Generazione della firma: si genera una firma per il messaggio utilizzando la chiave privata
  2. Verifica della firma: si verifica la validità della firma generata utilizzando la chiave pubblica

Inoltre, MLDSA presenta le seguenti tre caratteristiche:

  1. Strong existential unforgeability: non è possibile generare un'altra firma valida a partire da una firma e dalla chiave pubblica.
  2. Chosen message attack: non è possibile generare una nuova firma valida, utilizzando la chiave pubblica, a partire dalla firma di un messaggio qualsiasi.
  3. Side-channel attack: durante la firma vengono sempre utilizzati nuovi valori casuali e valori pseudocasuali derivati dal messaggio, il che ne aumenta il livello di sicurezza.
  4. Domain separation: si utilizzano seed diversi per parametri diversi, prevenendo così problemi di sicurezza dovuti alla ripetizione.

Codice

Ora mostrerò un semplice esempio di codice in linguaggio Go. In questo esempio è stato utilizzato il mldsa di cloudflare/circl.

 1package main
 2
 3import (
 4	"crypto"
 5	"crypto/rand"
 6	"encoding/base64"
 7	"fmt"
 8
 9	"github.com/cloudflare/circl/sign/mldsa/mldsa44"
10)
11
12func main() {
13    // Genera la chiave con la specifica mldsa44.
14	pub, priv, err := mldsa44.GenerateKey(rand.Reader)
15	if err != nil {
16		panic(err)
17	}
18
19	message := []byte("Hello, World!")
20
21    // Genera la firma.
22    // Un punto da tenere presente è che, nella versione attuale al 22 dicembre 2024, si verifica un errore se il valore non è crypto.Hash(0).
23	signature, err := priv.Sign(rand.Reader, message, crypto.Hash(0))
24	if err != nil {
25		panic(err)
26	}
27
28	encodedSignature := base64.URLEncoding.EncodeToString(signature)
29	fmt.Println(len(encodedSignature), encodedSignature)
30
31    // Esegue la verifica richiamando lo scheme della chiave pubblica.
32	ok := pub.Scheme().Verify(pub, message, signature, nil)
33	fmt.Println(ok)
34}
13228 oaSaOA-...
2true

Il valore della firma è stato omesso perché troppo lungo. Per visualizzarlo integralmente, eseguite il codice nel playground.

Anche se codificato in base64, che una firma occupi 3228 byte è certamente un po' gravoso. Pensando che in futuro potremmo dover scambiare firme di questa dimensione per proteggerci dai computer quantistici, la cosa mi preoccupa un po'...

MLKEM

Struttura

MLKEM è un meccanismo di incapsulamento di chiavi (Key Encapsulation Mechanism). Il KEM è un algoritmo che, mediante la crittografia a chiave pubblica, consente di generare una chiave condivisa tra due parti. Lo scambio di chiavi del MLKEM segue il seguente procedimento:

  1. Incapsulamento della chiave: il mittente utilizza la chiave pubblica del destinatario per generare un messaggio cifrato (cipher text) e una chiave condivisa (shared key). Tale messaggio cifrato viene inizialmente trasmesso al destinatario affinché lo utilizzi.
  2. Decapsulamento della chiave: il destinatario utilizza la propria chiave privata per estrarre la chiave condivisa dal messaggio cifrato.

MLKEM prevede tre parametri: MLKEM-512, MLKEM-768 e MLKEM-1024. Al diminuire del valore, si ottengono chiavi e testi cifrati più piccoli; all'aumentare del valore, si ottengono chiavi e testi cifrati più lunghi e un livello di sicurezza più elevato.

Codice

Poiché MLKEM sarà aggiunto in Go 1.24, è stata utilizzata la versione go 1.24rc1, attualmente disponibile.

 1package main
 2
 3import (
 4	"crypto/mlkem"
 5	"encoding/base64"
 6	"fmt"
 7)
 8
 9func main() {
10    // Genera la PrivateKey del destinatario.
11	receiverKey, err := mlkem.GenerateKey1024()
12	if err != nil {
13		panic(err)
14	}
15
16    // In MLKEM si utilizza il termine EncapsulationKey invece di PublicKey.
17	receiverPubKey := receiverKey.EncapsulationKey()
18
19    // Per mostrare in modo semplice che è possibile estrarre e riutilizzare la EncapsulationKey tramite Bytes() e NewEncapsulationKeyX, la si è duplicata.
20    // Naturalmente, nella pratica, questo processo corrisponde a quando il mittente crea un oggetto a partire dalla EncapsulationKey del destinatario, pubblicata in forma testuale.
21	clonedReceiverPubKey, err := mlkem.NewEncapsulationKey1024(receiverPubKey.Bytes())
22	if err != nil {
23		panic(err)
24	}
25
26    // Con Encapsulate, il mittente genera il testo cifrato e la chiave condivisa.
27	cipherText, SenderSharedKey := clonedReceiverPubKey.Encapsulate()
28
29    // Si è deliberatamente duplicata la chiave per mostrare come salvare e recuperare la chiave privata del destinatario.
30	clonedReceiverKey, err := mlkem.NewDecapsulationKey1024(receiverKey.Bytes())
31	if err != nil {
32		panic(err)
33	}
34
35    // Il destinatario, usando la propria chiave privata, esegue Decapsulate sul testo cifrato e genera un'altra chiave condivisa.
36	sharedKeyReceiver, err := clonedReceiverKey.Decapsulate(cipherText)
37	if err != nil {
38		panic(err)
39	}
40
41	fmt.Println(base64.StdEncoding.EncodeToString(SenderSharedKey))
42	fmt.Println(base64.StdEncoding.EncodeToString(sharedKeyReceiver))
43}
1Q1ciS818WFHTK7D4MTvsQvciMTGF+dSGqMllOxW80ew=
2Q1ciS818WFHTK7D4MTvsQvciMTGF+dSGqMllOxW80ew=

Si può quindi constatare che viene generata una chiave condivisa della stessa dimensione!

Questo codice è disponibile anche nel playground.

Conclusione

Le specifiche di ciascun algoritmo, il livello di sicurezza e le dimensioni della chiave privata, della chiave pubblica, della firma o del testo cifrato possono essere riassunte come segue. Ciascuno di essi, come ci si aspetta da algoritmi che si fregiano del nome PQC, presenta dimensioni considerevoli.

AlgoritmoLivello di sicurezza NISTDimensione chiave privataDimensione chiave pubblicaDimensione firma/testo cifrato
ML-DSA-4422.5601.3122.420
ML-DSA-6534.0321.9523.309
ML-DSA-8754.8962.5924.627
ML-KEM-51211.632800768
ML-KEM-76832.4001.1841.088
ML-KEM-102453.1681.5681.568

Grazie a questi algoritmi, speriamo di poter utilizzare un internet sufficientemente sicuro anche nell'era dei computer quantistici, ma l'aumento delle dimensioni delle chiavi e delle firme/testi cifrati comporterà inevitabilmente un maggior carico computazionale.

Ciò nonostante, Go implementa efficacemente ciascun algoritmo, e auspico che venga utilizzato attivamente, nei punti opportuni, per proteggere la sicurezza di tutti voi!