Post-Quantum Cryptography
NIST-standardized quantum-resistant algorithms - ML-DSA, ML-KEM, and SLH-DSA
Post-Quantum Cryptography
Lux implements NIST-standardized post-quantum cryptographic algorithms to ensure long-term security against quantum computing threats. This includes ML-DSA (Module Lattice Digital Signature Algorithm), ML-KEM (Module Lattice Key Encapsulation Mechanism), and SLH-DSA (Stateless Hash-Based Digital Signature Algorithm).
Overview
Post-quantum cryptography protects against attacks from both classical and quantum computers:
- Quantum Resistance: Secure against Shor's algorithm and Grover's algorithm
- NIST Standardized: Implements FIPS 204 (ML-DSA), FIPS 203 (ML-KEM), and FIPS 205 (SLH-DSA)
- Multiple Security Levels: Support for NIST security levels 2, 3, and 5
- Hybrid Deployment: Can be used alongside classical algorithms during transition
ML-DSA (Module Lattice Digital Signature Algorithm)
ML-DSA, formerly known as CRYSTALS-Dilithium, provides quantum-resistant digital signatures based on lattice problems.
Security Parameters
| Parameter Set | Security Level | Public Key | Private Key | Signature | Description |
|---|---|---|---|---|---|
| ML-DSA-44 | NIST Level 2 | 1,312 bytes | 2,528 bytes | 2,420 bytes | 128-bit classical security |
| ML-DSA-65 | NIST Level 3 | 1,952 bytes | 4,000 bytes | 3,293 bytes | 192-bit classical security |
| ML-DSA-87 | NIST Level 5 | 2,592 bytes | 4,864 bytes | 4,595 bytes | 256-bit classical security |
Implementation
Key Generation
Generate ML-DSA key pairs:
package main
import (
"crypto/rand"
"fmt"
"log"
"github.com/luxfi/crypto/mldsa"
)
func generateMLDSAKeys() {
// Generate ML-DSA-65 key pair (recommended for most applications)
publicKey, privateKey, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
if err != nil {
log.Fatal("Key generation failed:", err)
}
// Serialize keys for storage
pubBytes, _ := publicKey.MarshalBinary()
privBytes, _ := privateKey.MarshalBinary()
fmt.Printf("ML-DSA-65 Public Key Size: %d bytes\n", len(pubBytes))
fmt.Printf("ML-DSA-65 Private Key Size: %d bytes\n", len(privBytes))
// Generate different security levels
modes := []mldsa.Mode{mldsa.MLDSA44, mldsa.MLDSA65, mldsa.MLDSA87}
for _, mode := range modes {
pk, sk, _ := mldsa.GenerateKey(rand.Reader, mode)
fmt.Printf("Mode %v: PK=%d bytes, SK=%d bytes\n",
mode, pk.Size(), sk.Size())
}
}Signing and Verification
Sign messages with ML-DSA:
func signWithMLDSA(privateKey *mldsa.PrivateKey, message []byte) ([]byte, error) {
// Sign the message
signature, err := privateKey.Sign(rand.Reader, message, nil)
if err != nil {
return nil, fmt.Errorf("signing failed: %w", err)
}
fmt.Printf("Signature size: %d bytes\n", len(signature))
return signature, nil
}
func verifyMLDSA(publicKey *mldsa.PublicKey, message, signature []byte) bool {
// Verify the signature
err := publicKey.Verify(message, signature)
return err == nil
}
// Example usage
func exampleMLDSA() {
message := []byte("Quantum-resistant signature test")
// Generate keys
publicKey, privateKey, _ := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
// Sign
signature, err := signWithMLDSA(privateKey, message)
if err != nil {
log.Fatal(err)
}
// Verify
valid := verifyMLDSA(publicKey, message, signature)
fmt.Printf("Signature valid: %v\n", valid)
}Performance Characteristics
| Operation | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
|---|---|---|---|
| Key Generation | 46 μs | 72 μs | 119 μs |
| Sign | 234 μs | 417 μs | 525 μs |
| Verify | 71 μs | 108 μs | 159 μs |
ML-KEM (Module Lattice Key Encapsulation Mechanism)
ML-KEM, formerly known as CRYSTALS-Kyber, provides quantum-resistant key exchange using lattice-based cryptography.
Security Parameters
| Parameter Set | Security Level | Public Key | Private Key | Ciphertext | Shared Secret |
|---|---|---|---|---|---|
| ML-KEM-512 | NIST Level 1 | 800 bytes | 1,632 bytes | 768 bytes | 32 bytes |
| ML-KEM-768 | NIST Level 3 | 1,184 bytes | 2,400 bytes | 1,088 bytes | 32 bytes |
| ML-KEM-1024 | NIST Level 5 | 1,568 bytes | 3,168 bytes | 1,568 bytes | 32 bytes |
Implementation
Key Exchange Protocol
Implement quantum-safe key exchange:
package main
import (
"crypto/rand"
"fmt"
"github.com/luxfi/crypto/mlkem"
)
func quantumSafeKeyExchange() {
// Alice generates key pair
alicePublic, alicePrivate, err := mlkem.GenerateKey(rand.Reader, mlkem.MLKEM768)
if err != nil {
panic(err)
}
// Bob encapsulates shared secret using Alice's public key
ciphertext, bobSharedSecret, err := mlkem.Encapsulate(alicePublic)
if err != nil {
panic(err)
}
// Alice decapsulates to get the same shared secret
aliceSharedSecret, err := mlkem.Decapsulate(alicePrivate, ciphertext)
if err != nil {
panic(err)
}
// Verify both parties have the same secret
if bytes.Equal(aliceSharedSecret, bobSharedSecret) {
fmt.Println("Key exchange successful!")
fmt.Printf("Shared secret: %x\n", aliceSharedSecret)
}
}Hybrid Key Exchange
Combine ML-KEM with classical ECDH for transitional security:
type HybridKeyExchange struct {
mlkemPublic *mlkem.PublicKey
mlkemPrivate *mlkem.PrivateKey
ecdhPublic *ecdh.PublicKey
ecdhPrivate *ecdh.PrivateKey
}
func (hke *HybridKeyExchange) GenerateKeys() error {
// Generate ML-KEM keys
mlkemPub, mlkemPriv, err := mlkem.GenerateKey(rand.Reader, mlkem.MLKEM768)
if err != nil {
return err
}
hke.mlkemPublic = mlkemPub
hke.mlkemPrivate = mlkemPriv
// Generate ECDH keys (X25519)
ecdhPriv, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return err
}
hke.ecdhPrivate = ecdhPriv
hke.ecdhPublic = ecdhPriv.PublicKey()
return nil
}
func (hke *HybridKeyExchange) Encapsulate(peerMLKEM *mlkem.PublicKey, peerECDH *ecdh.PublicKey) ([]byte, []byte, error) {
// ML-KEM encapsulation
mlkemCiphertext, mlkemSecret, err := mlkem.Encapsulate(peerMLKEM)
if err != nil {
return nil, nil, err
}
// ECDH shared secret
ecdhSecret, err := hke.ecdhPrivate.ECDH(peerECDH)
if err != nil {
return nil, nil, err
}
// Combine secrets using KDF
combinedSecret := kdf.DeriveKey(
append(mlkemSecret, ecdhSecret...),
[]byte("hybrid-key-exchange"),
32,
)
// Return ciphertext and combined secret
hybridCiphertext := append(mlkemCiphertext, hke.ecdhPublic.Bytes()...)
return hybridCiphertext, combinedSecret, nil
}Performance Characteristics
| Operation | ML-KEM-512 | ML-KEM-768 | ML-KEM-1024 |
|---|---|---|---|
| Key Generation | 28 μs | 44 μs | 65 μs |
| Encapsulate | 35 μs | 53 μs | 78 μs |
| Decapsulate | 39 μs | 59 μs | 86 μs |
SLH-DSA (Stateless Hash-Based Digital Signature Algorithm)
SLH-DSA, also known as SPHINCS+, provides hash-based signatures that don't require state management.
Security Parameters
| Parameter Set | Security Level | Public Key | Private Key | Signature | Hash Function |
|---|---|---|---|---|---|
| SLH-DSA-SHA2-128f | NIST Level 1 | 32 bytes | 64 bytes | 17,088 bytes | SHA-256 |
| SLH-DSA-SHA2-192f | NIST Level 3 | 48 bytes | 96 bytes | 35,664 bytes | SHA-256 |
| SLH-DSA-SHA2-256f | NIST Level 5 | 64 bytes | 128 bytes | 49,856 bytes | SHA-256 |
| SLH-DSA-SHAKE-128f | NIST Level 1 | 32 bytes | 64 bytes | 17,088 bytes | SHAKE256 |
Implementation
Hash-Based Signatures
Implement stateless hash-based signatures:
package main
import (
"crypto/rand"
"fmt"
"github.com/luxfi/crypto/slhdsa"
)
func hashBasedSignatures() {
// Generate SLH-DSA key pair
publicKey, privateKey, err := slhdsa.GenerateKey(rand.Reader, slhdsa.SLHDSA_SHA2_128f)
if err != nil {
panic(err)
}
message := []byte("Hash-based signature test")
// Sign message
signature, err := privateKey.Sign(rand.Reader, message, nil)
if err != nil {
panic(err)
}
fmt.Printf("SLH-DSA signature size: %d bytes\n", len(signature))
// Verify signature
err = publicKey.Verify(message, signature)
if err == nil {
fmt.Println("Signature verified successfully")
}
}
// Demonstrate small key, large signature trade-off
func compareSignatureSizes() {
modes := []struct {
name string
mode slhdsa.Mode
}{
{"SLH-DSA-128f", slhdsa.SLHDSA_SHA2_128f},
{"SLH-DSA-192f", slhdsa.SLHDSA_SHA2_192f},
{"SLH-DSA-256f", slhdsa.SLHDSA_SHA2_256f},
}
for _, m := range modes {
pk, sk, _ := slhdsa.GenerateKey(rand.Reader, m.mode)
sig, _ := sk.Sign(rand.Reader, []byte("test"), nil)
pkBytes, _ := pk.MarshalBinary()
skBytes, _ := sk.MarshalBinary()
fmt.Printf("%s: PK=%d, SK=%d, Sig=%d bytes\n",
m.name, len(pkBytes), len(skBytes), len(sig))
}
}Performance Characteristics
| Operation | SLH-DSA-128f | SLH-DSA-192f | SLH-DSA-256f |
|---|---|---|---|
| Key Generation | 3.2 ms | 5.7 ms | 8.1 ms |
| Sign | 85 ms | 152 ms | 274 ms |
| Verify | 3.8 ms | 7.2 ms | 13.5 ms |
Hybrid Deployment Strategies
Dual Signature Approach
Use both classical and post-quantum signatures:
type HybridSignature struct {
Classical []byte // ECDSA or BLS signature
PostQuantum []byte // ML-DSA signature
}
type HybridSigner struct {
classicalKey crypto.Signer // ECDSA or BLS
quantumKey *mldsa.PrivateKey // ML-DSA
}
func (hs *HybridSigner) Sign(message []byte) (*HybridSignature, error) {
// Sign with classical algorithm
classicalSig, err := hs.classicalKey.Sign(rand.Reader, message, crypto.SHA256)
if err != nil {
return nil, fmt.Errorf("classical signing failed: %w", err)
}
// Sign with post-quantum algorithm
quantumSig, err := hs.quantumKey.Sign(rand.Reader, message, nil)
if err != nil {
return nil, fmt.Errorf("quantum signing failed: %w", err)
}
return &HybridSignature{
Classical: classicalSig,
PostQuantum: quantumSig,
}, nil
}
func (hs *HybridSignature) Verify(message []byte, classicalPub, quantumPub interface{}) bool {
// Both signatures must be valid
classicalValid := verifyClassical(classicalPub, message, hs.Classical)
quantumValid := verifyQuantum(quantumPub, message, hs.PostQuantum)
return classicalValid && quantumValid
}Migration Timeline
Recommended transition strategy for post-quantum deployment:
type CryptoEra int
const (
Classical CryptoEra = iota // Current: ECDSA/BLS only
Hybrid // Transitional: Both algorithms
PostQuantum // Future: PQ algorithms only
)
type CryptoConfig struct {
Era CryptoEra
RequireClassical bool
RequireQuantum bool
AcceptLegacy bool
}
func GetCryptoConfig(timestamp time.Time) CryptoConfig {
quantumTransition := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
quantumOnly := time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
switch {
case timestamp.Before(quantumTransition):
return CryptoConfig{
Era: Classical,
RequireClassical: true,
RequireQuantum: false,
AcceptLegacy: true,
}
case timestamp.Before(quantumOnly):
return CryptoConfig{
Era: Hybrid,
RequireClassical: true,
RequireQuantum: true,
AcceptLegacy: true,
}
default:
return CryptoConfig{
Era: PostQuantum,
RequireClassical: false,
RequireQuantum: true,
AcceptLegacy: false,
}
}
}Security Analysis
Quantum Resistance Levels
| Algorithm | Classical Security | Quantum Security | Based On |
|---|---|---|---|
| ML-DSA | 128-256 bits | 64-128 bits | Module-LWE |
| ML-KEM | 128-256 bits | 64-128 bits | Module-LWE |
| SLH-DSA | 128-256 bits | 64-128 bits | Hash functions |
| ECDSA | 128-256 bits | Broken | Discrete log |
| RSA | 112-128 bits | Broken | Factorization |
Attack Resistance
- Shor's Algorithm: All three PQ algorithms resist quantum period finding
- Grover's Algorithm: Security parameters chosen to maintain adequate security margin
- Side Channels: Implementations include countermeasures against timing attacks
- Fault Attacks: Error correction and verification in lattice-based schemes
Performance Optimization
Batch Operations
Process multiple signatures efficiently:
func batchVerifyMLDSA(publicKeys []*mldsa.PublicKey, messages [][]byte, signatures [][]byte) bool {
if len(publicKeys) != len(messages) || len(messages) != len(signatures) {
return false
}
// Parallel verification using goroutines
results := make(chan bool, len(publicKeys))
for i := range publicKeys {
go func(idx int) {
err := publicKeys[idx].Verify(messages[idx], signatures[idx])
results <- (err == nil)
}(i)
}
// Collect results
for i := 0; i < len(publicKeys); i++ {
if !<-results {
return false
}
}
return true
}Caching Strategies
type PQSignatureCache struct {
cache *lru.Cache
mu sync.RWMutex
}
func (c *PQSignatureCache) VerifyWithCache(pubKey *mldsa.PublicKey, message, signature []byte) bool {
// Generate cache key
key := sha256.Sum256(append(append(pubKey.Bytes(), message...), signature...))
// Check cache
c.mu.RLock()
if result, ok := c.cache.Get(key); ok {
c.mu.RUnlock()
return result.(bool)
}
c.mu.RUnlock()
// Verify and cache result
valid := (pubKey.Verify(message, signature) == nil)
c.mu.Lock()
c.cache.Add(key, valid)
c.mu.Unlock()
return valid
}Testing
Comprehensive Test Suite
func TestPostQuantumInterop(t *testing.T) {
// Test vector from NIST
testVectors := []struct {
algorithm string
pubKey string
message string
signature string
valid bool
}{
// Add NIST test vectors here
}
for _, tv := range testVectors {
t.Run(tv.algorithm, func(t *testing.T) {
// Decode test vector
pubKey, _ := hex.DecodeString(tv.pubKey)
message, _ := hex.DecodeString(tv.message)
signature, _ := hex.DecodeString(tv.signature)
// Verify signature
var valid bool
switch tv.algorithm {
case "ML-DSA-65":
pk, _ := mldsa.ParsePublicKey(pubKey)
valid = (pk.Verify(message, signature) == nil)
case "ML-KEM-768":
// KEM doesn't have signatures, skip
t.Skip("KEM is for key exchange, not signatures")
case "SLH-DSA-128f":
pk, _ := slhdsa.ParsePublicKey(pubKey)
valid = (pk.Verify(message, signature) == nil)
}
require.Equal(t, tv.valid, valid)
})
}
}Best Practices
-
Algorithm Selection
- Use ML-DSA-65 for general signatures (balanced size/speed)
- Use ML-KEM-768 for key exchange (NIST Level 3)
- Use SLH-DSA only when small keys are critical
-
Key Management
- Store post-quantum keys separately from classical keys
- Implement key rotation before quantum computers become viable
- Use HSMs that support post-quantum algorithms
-
Performance Considerations
- Cache signature verifications
- Use parallel processing for batch operations
- Consider signature size impact on network bandwidth
-
Migration Planning
- Start with hybrid mode (classical + PQ)
- Monitor quantum computing advances
- Plan for algorithm agility