GoSuda

Experiencia de uso de MLDSA y MLKEM en el lenguaje Go

By snowmerak views ...

Descripción general

Antecedentes

Desde hace bastante tiempo, la rápida capacidad de cómputo de las computadoras cuánticas se ha reconocido como una amenaza para los sistemas criptográficos existentes. Esto se debe a que RSA y ECC, por ejemplo, podrían ser descifrados mediante esta capacidad computacional. Sin embargo, desde hace algunos años, a medida que el concepto de computadora cuántica comenzó a hacerse tangible, se iniciaron la investigación y el desarrollo de alternativas, y el NIST ha llevado a cabo la estandarización de la PQC (criptografía resistente a la computación cuántica).

MLDSA y MLKEM

Finalmente, en agosto de 2024, el NIST adoptó como estándar MLKEM y MLDSA, basados en CRYSTALS-Kyber y CRYSTALS-Dilithium. Ambos algoritmos funcionan sobre la base del problema MLWE (Module Learning with Errors). A este tipo de formulación lo denominamos criptografía basada en retículos.

La criptografía basada en retículos es, como su nombre indica, un sistema criptográfico basado en la dificultad de problemas matemáticos sobre retículos. Yo tampoco poseo un conocimiento matemático profundo al respecto, pero resumido en una frase se dice que es el problema de resolver ecuaciones lineales con ruido en un retículo modular. No logro hacerme una idea de cuán difícil es, pero se afirma que este tipo de problemas es tan complejo que ni siquiera las computadoras cuánticas pueden resolverlo.

MLDSA

Veamos primero MLDSA.

Composición

MLDSA es, como indica su nombre, un algoritmo de firma asimétrica que consta de los siguientes dos pasos:

  1. Generación de la firma: se genera una firma para el mensaje utilizando la clave privada
  2. Verificación de la firma: se comprueba la validez de la firma generada utilizando la clave pública

Además, MLDSA presenta las siguientes tres características:

  1. Strong existential unforgeability: con una firma y una clave pública no es posible generar otra firma válida distinta.
  2. Chosen message attack: con la clave pública tampoco es posible generar una nueva firma válida a partir de la firma de cualquier mensaje.
  3. Side-channel attack: durante la firma se utilizan continuamente nuevos valores aleatorios y valores pseudoaleatorios derivados del mensaje, lo que proporciona un alto nivel de seguridad.
  4. Domain separation: se utilizan semillas distintas para parámetros distintos, lo que previene problemas de seguridad por repetición.

Código

A continuación, les mostraré un ejemplo sencillo en el lenguaje Go. En este ejemplo se utilizó mldsa de 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 las claves con la especificación 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 a tener en cuenta es que, según la versión vigente al 22 de diciembre de 2024, si crypto.Hash(0) no se utiliza se produce un error.
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    // Verifica la firma invocando el scheme de la clave pública.
32	ok := pub.Scheme().Verify(pub, message, signature, nil)
33	fmt.Println(ok)
34}
13228 oaSaOA-...
2true

Omití el valor de la firma por ser demasiado largo. Si desea ver el texto completo, ejecútelo en el playground.

Aunque está codificado en base64, es comprensible que resulte algo pesado que sean 3228 bytes. Pensar que, en breve, quizás tengamos que intercambiar firmas de este tamaño para protegernos de las computadoras cuánticas me resulta un poco preocupante...

MLKEM

Composición

MLKEM es un mecanismo de encapsulación de claves (Key Encapsulation Mechanism). KEM es un algoritmo que, mediante un método de criptografía de clave pública, permite generar una clave compartida entre dos partes. El mecanismo de intercambio de claves de MLKEM sigue el siguiente proceso:

  1. Encapsulación de la clave: el remitente utiliza la clave pública del destinatario para generar un mensaje cifrado (texto cifrado) y una clave compartida (shared key). Este mensaje cifrado se entrega inicialmente al destinatario para su uso.
  2. Desencapsulación de la clave: el destinatario utiliza su clave privada para extraer la clave compartida del mensaje cifrado.

MLKEM tiene un total de tres parámetros: MLKEM-512, MLKEM-768 y MLKEM-1024. Cuanto menor sea el valor, más pequeñas serán las claves y los textos cifrados; cuanto mayor sea el valor, más largas serán las claves y los textos cifrados, y mayor será el nivel de seguridad.

Código

MLKEM se incorporará en Go 1.24, por lo que, en el momento actual, se utilizó go 1.24rc1, que está disponible.

 1package main
 2
 3import (
 4	"crypto/mlkem"
 5	"encoding/base64"
 6	"fmt"
 7)
 8
 9func main() {
10    // Genera el PrivateKey del receptor.
11	receiverKey, err := mlkem.GenerateKey1024()
12	if err != nil {
13		panic(err)
14	}
15
16    // En MLKEM se utiliza el término EncapsulationKey en lugar de PublicKey.
17	receiverPubKey := receiverKey.EncapsulationKey()
18
19    // Para mostrar de forma sencilla que es posible extraer y volver a utilizar la EncapsulationKey mediante Bytes() y NewEncapsulationKeyX, se duplicó el objeto.
20    // Por supuesto, en la práctica este proceso equivale a que el remitente convierta en objeto la EncapsulationKey pública del receptor, que estaba publicada en forma de texto.
21	clonedReceiverPubKey, err := mlkem.NewEncapsulationKey1024(receiverPubKey.Bytes())
22	if err != nil {
23		panic(err)
24	}
25
26    // Con Encapsulate, el remitente genera el texto cifrado y la clave compartida.
27	cipherText, SenderSharedKey := clonedReceiverPubKey.Encapsulate()
28
29    // Se duplicó a propósito para mostrar cómo guardar y recuperar la clave privada del receptor.
30	clonedReceiverKey, err := mlkem.NewDecapsulationKey1024(receiverKey.Bytes())
31	if err != nil {
32		panic(err)
33	}
34
35    // El receptor utiliza su clave privada para aplicar Decapsulate al texto cifrado y generar otra clave compartida.
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=

Como resultado, se puede comprobar que se genera una clave compartida del mismo tamaño.

Este código también puede consultarse en el playground.

Conclusión

Las especificaciones de cada algoritmo, su nivel de seguridad y los tamaños de las claves privadas, las claves públicas, las firmas o los textos cifrados pueden resumirse como sigue. Cada uno exhibe tamaños considerables, en consonancia con su denominación PQC.

AlgoritmoNivel de seguridad NISTTamaño de clave privadaTamaño de clave públicaTamaño de firma/texto cifrado
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

Gracias a estos algoritmos, esperamos poder utilizar una internet suficientemente segura incluso en computadoras cuánticas; no obstante, parece inevitable que, debido al aumento relativo del tamaño de las claves y de las firmas/textos cifrados, se requieran más operaciones.

Aun así, dado que Go implementa cada algoritmo de forma eficaz, esperamos que se utilice activamente en los lugares adecuados para proteger su seguridad.