Experiência de uso de MLDSA e MLKEM na linguagem Go

Visão geral
Contexto
Há bastante tempo, a rápida capacidade de cálculo dos computadores quânticos é reconhecida como uma ameaça aos sistemas criptográficos existentes. Isso ocorre porque algoritmos como RSA e ECC podem ser quebrados por meio dessa capacidade computacional. No entanto, a partir de alguns anos atrás, com a conceituação dos computadores quânticos tornando-se mais concreta, passou-se a pesquisar e desenvolver alternativas a eles, e o NIST vem conduzindo a padronização da PQC (criptografia pós-quântica).
MLDSA e MLKEM
Finalmente, em agosto de 2024, o NIST adotou como padrão o MLKEM e o MLDSA, baseados em CRYSTALS-Kyber e CRYSTALS-Dilithium. Esses dois algoritmos operam com base no problema denominado MLWE (Module Learning with Errors). Essa forma é chamada por nós de criptografia baseada em reticulados.
A criptografia baseada em reticulados é, como o nome sugere, um sistema criptográfico fundamentado na dificuldade de problemas matemáticos sobre reticulados. Embora eu não possua um conhecimento matemático aprofundado sobre o tema, resumidamente, ela é descrita como o problema de resolver equações lineares com ruído em um reticulado modular. Não tenho uma noção clara de quão difícil ele é, mas dizem que esses problemas são tão árduos que nem mesmo computadores quânticos conseguem resolvê-los.
MLDSA
Então, vamos primeiro conhecer o MLDSA.
Estrutura
Como o nome indica, o MLDSA é um algoritmo de assinatura assimétrica e segue, no total, as seguintes 2 etapas:
- Geração de assinatura: gera-se uma assinatura para a mensagem utilizando a chave privada
- Verificação de assinatura: confirma-se a validade da assinatura gerada utilizando a chave pública
Além disso, o MLDSA possui as seguintes 4 propriedades:
- strong existential unforgeability: não é possível gerar outra assinatura válida a partir de uma assinatura e de uma chave pública.
- chosen message attack: mesmo com a assinatura de qualquer mensagem, não é possível gerar uma nova assinatura válida utilizando a chave pública.
- side-channel attack: durante a assinatura, utilizam-se continuamente novos valores aleatórios e valores pseudoaleatórios derivados da mensagem, o que aumenta o nível de segurança.
- domain separation: utilizam-se seeds diferentes para parâmetros diferentes, prevenindo problemas de segurança decorrentes de repetição.
Código
Agora, vou apresentar um exemplo simples em Go. Neste exemplo, utilizou-se o 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 // Gera uma chave com a especificação 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 // Gera a assinatura.
22 // Um ponto a observar é que, na versão atual de 22 de dezembro de 2024, ocorre erro se crypto.Hash(0) não for utilizado.
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 // Realiza a verificação chamando o scheme da chave pública.
32 ok := pub.Scheme().Verify(pub, message, signature, nil)
33 fmt.Println(ok)
34}
13228 oaSaOA-...
2true
Omitimos o valor da assinatura por ser muito longo. Caso deseje ver o conteúdo completo, execute no playground.
Embora tenha sido codificado em base64, é natural que 3228 bytes representem um peso considerável. Ao pensar que, em breve, poderemos precisar trocar assinaturas desse tamanho para nos protegermos de computadores quânticos, isso realmente parece um pouco pesado...
MLKEM
Estrutura
O MLKEM é um mecanismo de encapsulamento de chaves (Key Encapsulation Mechanism). O KEM é um algoritmo que permite gerar uma chave compartilhada entre duas partes por meio de criptografia de chave pública. O mecanismo de troca de chaves do MLKEM segue o seguinte processo:
- Encapsulamento de chave: o remetente utiliza a chave pública do destinatário para gerar a mensagem cifrada (cipher text) e a chave compartilhada (shared key). Essa mensagem cifrada é inicialmente transmitida ao destinatário para que ele a utilize.
- Desencapsulamento de chave: o destinatário utiliza sua chave privada para extrair a chave compartilhada da mensagem cifrada.
O MLKEM possui 3 parâmetros: MLKEM-512, MLKEM-768 e MLKEM-1024. Quanto menor o parâmetro, menores são as chaves e os textos cifrados gerados; quanto maior, mais longas são as chaves e os textos cifrados, e maior é o nível de segurança.
Código
Como o MLKEM será adicionado ao Go 1.24, utilizou-se o go 1.24rc1, que está disponível no momento atual.
1package main
2
3import (
4 "crypto/mlkem"
5 "encoding/base64"
6 "fmt"
7)
8
9func main() {
10 // Gera a PrivateKey do receptor.
11 receiverKey, err := mlkem.GenerateKey1024()
12 if err != nil {
13 panic(err)
14 }
15
16 // No MLKEM, utiliza-se o termo EncapsulationKey em vez de PublicKey.
17 receiverPubKey := receiverKey.EncapsulationKey()
18
19 // Para demonstrar de forma simples que a chave pode ser extraída com Bytes() da EncapsulationKey e restaurada com NewEncapsulationKey1024, ela foi duplicada.
20 // Na prática, este processo corresponde ao remetente criando um objeto a partir da chave EncapsulationKey do destinatário, que foi publicada em formato de texto.
21 clonedReceiverPubKey, err := mlkem.NewEncapsulationKey1024(receiverPubKey.Bytes())
22 if err != nil {
23 panic(err)
24 }
25
26 // Com Encapsulate, o remetente gera o texto cifrado e a chave compartilhada.
27 cipherText, SenderSharedKey := clonedReceiverPubKey.Encapsulate()
28
29 // A chave será duplicada intencionalmente para demonstrar o armazenamento e a recuperação da chave privada do receptor.
30 clonedReceiverKey, err := mlkem.NewDecapsulationKey1024(receiverKey.Bytes())
31 if err != nil {
32 panic(err)
33 }
34
35 // O receptor utiliza sua chave privada para realizar o Decapsulate do texto cifrado e gerar outra chave compartilhada.
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, pode-se confirmar que é gerada uma chave compartilhada do mesmo tamanho!
Este código também pode ser conferido no playground.
Conclusão
As especificações de cada algoritmo, seus níveis de segurança e os tamanhos das chaves privadas, chaves públicas, assinaturas ou textos cifrados podem ser resumidos na tabela a seguir. Todos apresentam tamanhos consideráveis, sem demérito ao nome PQC.
| Algoritmo | Nível de segurança NIST | Tamanho da chave privada | Tamanho da chave pública | Tamanho da assinatura/texto cifrado |
|---|---|---|---|---|
| ML-DSA-44 | 2 | 2,560 | 1,312 | 2,420 |
| ML-DSA-65 | 3 | 4,032 | 1,952 | 3,309 |
| ML-DSA-87 | 5 | 4,896 | 2,592 | 4,627 |
| ML-KEM-512 | 1 | 1,632 | 800 | 768 |
| ML-KEM-768 | 3 | 2,400 | 1,184 | 1,088 |
| ML-KEM-1024 | 5 | 3,168 | 1,568 | 1,568 |
Esperamos que, com esses algoritmos, seja possível usar a internet com segurança suficiente mesmo em computadores quânticos; entretanto, é inevitável que haja mais operações devido ao aumento relativo das chaves e dos tamanhos de assinaturas/textos cifrados.
Ainda assim, como a linguagem Go implementa cada algoritmo de forma eficaz, esperamos que ela seja amplamente utilizada para proteger a segurança de vocês nos locais apropriados!