Uso de MLDSA e MLKEM na Linguagem Go
Visão Geral
Contexto
Há bastante tempo, a rápida computação dos computadores quânticos tem sido percebida como uma ameaça aos sistemas criptográficos existentes. Isso se deve ao fato de que sistemas como RSA e ECC podem ser decifrados por essa capacidade computacional dos computadores quânticos. No entanto, há alguns anos, o conceito de computador quântico começou a se concretizar, e alternativas para ele começaram a ser pesquisadas e desenvolvidas, com o NIST padronizando o PQC (Post-Quantum Cryptography).
MLDSA e MLKEM
Eventualmente, o NIST adotou o MLKEM e o MLDSA, baseados em CRYSTALS-Kyber e CRYSTALS-Dilithium, respectivamente, como padrões em agosto de 2024. Ambos os algoritmos operam com base no problema do MLWE (Module Learning with Errors). Nós nos referimos a esse formato como criptografia baseada em rede.
A criptografia baseada em rede, como o nome sugere, é um sistema criptográfico baseado na dificuldade de problemas matemáticos em uma rede. Embora eu não possua conhecimento matemático aprofundado sobre o assunto, em uma frase, pode ser resumido como "o problema de resolver equações lineares com ruído em uma rede de módulos". Não se tem uma noção clara de quão difícil é, mas diz-se que esses problemas são tão difíceis que não podem ser resolvidos nem mesmo por computadores quânticos.
MLDSA
Agora, vamos examinar o MLDSA.
Composição
O MLDSA, como o nome indica, é um algoritmo de assinatura assimétrica que passa pelas duas etapas seguintes:
- Geração de assinatura: Cria uma assinatura para uma mensagem usando uma chave privada.
- Verificação de assinatura: Verifica a validade da assinatura gerada usando uma chave pública.
E o MLDSA possui as três características a seguir:
- strong existential unforgeability: Não é possível gerar outra assinatura válida a partir de uma assinatura e uma chave pública.
- chosen message attack: Não é possível gerar uma nova assinatura válida com uma chave pública a partir de uma assinatura para qualquer mensagem.
- side-channel attack: A segurança é alta, pois novos valores aleatórios e valores pseudoaleatórios derivados da mensagem são continuamente usados durante a assinatura.
- domain separation: Previne problemas de segurança repetitivos, garantindo que diferentes seeds sejam usadas para diferentes parâmetros.
Código
Agora, demonstrarei um exemplo simples de código em Go. Neste exemplo, foi utilizado 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 uma assinatura.
22 // Uma coisa a observar é que, com base na versão atual de 22 de dezembro de 2024, ocorrerá um erro se não for 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 // Chama o esquema da chave pública para realizar a verificação.
32 ok := pub.Scheme().Verify(pub, message, signature, nil)
33 fmt.Println(ok)
34}
13228 oaSaOA-...
2true
O valor da assinatura é muito longo e foi omitido. Se desejar ver o texto completo, execute-o no playground.
Embora codificado em base64, um tamanho de 3228 bytes pode ser um pouco oneroso. É um pouco preocupante pensar que em breve poderemos ter que lidar com esse tamanho de assinatura para combater computadores quânticos.
MLKEM
Composição
MLKEM é um Mecanismo de Encapsulamento de Chave (Key Encapsulation Mechanism). KEM é um algoritmo que permite a duas partes gerar uma chave compartilhada usando criptografia de chave pública. O mecanismo de troca de chaves do MLKEM segue o seguinte processo:
- Encapsulamento de chave: O remetente usa a chave pública do destinatário para gerar uma mensagem criptografada (cipher text) e uma chave compartilhada (shared key). Esta mensagem criptografada é inicialmente entregue ao destinatário para uso.
- Desencapsulamento de chave: O destinatário usa sua chave privada para extrair a chave compartilhada da mensagem criptografada.
Existem três parâmetros no MLKEM. Existem MLKEM-512, MLKEM-768 e MLKEM-1024. Quanto menor o número, menores as chaves e o texto criptografado, e quanto maior o número, maiores as chaves e o texto criptografado, e maior o nível de segurança.
Código
O MLKEM será adicionado ao Go 1.24, portanto, usei o Go 1.24rc1, que está disponível no momento.
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, o termo EncapsulationKey é usado em vez de PublicKey.
17 receiverPubKey := receiverKey.EncapsulationKey()
18
19 // Para demonstrar que a chave pode ser extraída e reutilizada com Bytes() e NewEncapsulationKeyX da EncapsulationKey, ela foi clonada.
20 // Na realidade, este processo seria como o remetente criando um objeto a partir da chave EncapsulationKey do destinatário que estava publicamente disponível como texto.
21 clonedReceiverPubKey, err := mlkem.NewEncapsulationKey1024(receiverPubKey.Bytes())
22 if err != nil {
23 panic(err)
24 }
25
26 // O remetente gera o texto cifrado e a chave compartilhada com Encapsulate.
27 cipherText, SenderSharedKey := clonedReceiverPubKey.Encapsulate()
28
29 // Eu clonei intencionalmente a chave privada do receptor para mostrar como ela é armazenada e recuperada.
30 clonedReceiverKey, err := mlkem.NewDecapsulationKey1024(receiverKey.Bytes())
31 if err != nil {
32 panic(err)
33 }
34
35 // O receptor usa a chave privada para Decapsulate o 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 verificar que chaves compartilhadas do mesmo tamanho são geradas!
Este código também pode ser encontrado no playground.
Conclusão
As especificações de cada algoritmo, os níveis de segurança e os tamanhos da chave privada, chave pública, assinatura ou texto cifrado podem ser resumidos da seguinte forma. Cada um deles ostenta um tamanho considerável, digno do nome PQC.
| Algoritmo | Nível de Segurança NIST | Tamanho da Chave Privada | Tamanho da Chave Pública | Tamanho da Assinatura/Cifração |
|---|---|---|---|---|
| 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 |
Com esses algoritmos, esperamos poder usar uma internet suficientemente segura mesmo com computadores quânticos. No entanto, parece inevitável que haverá mais computação devido aos tamanhos relativamente maiores das chaves e das assinaturas/textos cifrados.
Ainda assim, como cada algoritmo é implementado de forma eficaz na linguagem Go, esperamos que eles sejam ativamente utilizados para proteger sua segurança nos locais apropriados!