🔮 ML-KEM-768 Kyber Key Encapsulation
📋 Quick Navigation
📖 Overview
ML-KEM-768 is NIST's standardized Module-Lattice-Based Key Encapsulation Mechanism, formerly known as CRYSTALS-Kyber. It provides post-quantum secure key exchange with excellent performance characteristics and is the primary NIST-approved algorithm for post-quantum key establishment, standardized in FIPS 203.
✨ Key Features
Quantum-Resistant
Based on the Module Learning With Errors (MLWE) problem
High Performance
Fast key generation, encapsulation, and decapsulation
Compact Design
Reasonable key and ciphertext sizes for practical use
NIST Standardized
Approved in NIST FIPS 203 standard
Proven Security
Extensive cryptanalysis and security evaluation
KEM Design
Key encapsulation mechanism for secure key exchange
🎯 Security Levels
🔒 Classical Security
- 256-bit equivalent: Comparable to AES-256 against classical attacks
- Lattice-based: Security relies on Module-LWE problem hardness
- Conservative estimates: Well-studied mathematical foundations
🔮 Post-Quantum Security
- NIST Level 3: 192-bit post-quantum security level
- Quantum resistance: Secure against Shor's and Grover's algorithms
- Future-proof: Designed for long-term security
🔧 Algorithm Parameters
📊 Technical Parameters
🛡️ Security Properties
Quantum Resistance
Secure against quantum computer attacks using Shor's algorithm
Lattice-Based
Security based on Module Learning With Errors (MLWE) problem
IND-CCA2 Security
Indistinguishable under adaptive chosen-ciphertext attacks
Forward Secrecy
Past communications remain secure if long-term keys are compromised
🧮 Mathematical Foundation
ML-KEM-768 is based on the Module Learning With Errors (MLWE) problem:
Given (A, b = As + e) where s, e are small, find s
Usage Examples
Basic Key Encapsulation
from metamui_crypto import MLKem768
# Key Generation
keypair = MLKem768.generate_keypair()
public_key = keypair.public_key
private_key = keypair.private_key
# Encapsulation (Sender)
ciphertext, shared_secret_sender = MLKem768.encapsulate(public_key)
# Decapsulation (Receiver)
shared_secret_receiver = MLKem768.decapsulate(ciphertext, private_key)
# Both parties now have the same shared secret
assert shared_secret_sender == shared_secret_receiver
Hybrid Key Exchange
from metamui_crypto import MLKem768, X25519
import hashlib
# Generate both classical and post-quantum keys
x25519_keypair = X25519.generate_keypair()
mlkem_keypair = MLKem768.generate_keypair()
# Sender side
# Classical ECDH
x25519_shared = X25519.compute_shared_secret(
x25519_keypair.private_key,
receiver_x25519_public
)
# Post-quantum KEM
mlkem_ciphertext, mlkem_shared = MLKem768.encapsulate(
receiver_mlkem_public
)
# Combine secrets
hybrid_secret = hashlib.sha256(
x25519_shared + mlkem_shared
).digest()
# Send: x25519_public, mlkem_ciphertext
Serialization
from metamui_crypto import MLKem768
# Generate keypair
keypair = MLKem768.generate_keypair()
# Serialize keys
public_key_bytes = keypair.public_key.to_bytes()
private_key_bytes = keypair.private_key.to_bytes()
# Store or transmit...
# Deserialize keys
public_key = MLKem768.PublicKey.from_bytes(public_key_bytes)
private_key = MLKem768.PrivateKey.from_bytes(private_key_bytes)
Error Handling
from metamui_crypto import MLKem768
from metamui_crypto.errors import CryptoError
try:
# Attempt decapsulation
shared_secret = MLKem768.decapsulate(ciphertext, private_key)
except CryptoError as e:
print(f"Decapsulation failed: {e}")
# Handle error appropriately
Implementation Details
Core Components
- Number Theoretic Transform (NTT)
- Accelerates polynomial multiplication
- Uses Cooley-Tukey butterfly operations
- Prime modulus: q = 3329
- CRITICAL: All polynomial operations must maintain NTT domain consistency
- NOTE: Encryption/decryption require proper NTT domain transformations
- Polynomial Arithmetic
- Ring: R_q = Z_q[X]/(X^256 + 1)
- Degree: n = 256
- Module dimension: k = 3 (for ML-KEM-768)
- Domain Operations: Polynomials must be in correct domain (NTT or normal) for each operation
- Compression
- Public keys and ciphertexts are compressed
- Reduces bandwidth requirements
- Maintains security properties
- Error Correction
- Uses error-correcting codes
- Ensures correct decapsulation
- Negligible failure probability
Security Features
- IND-CCA2 Security: Secure against chosen-ciphertext attacks
- Constant-Time Operations: Protected against timing attacks
- Side-Channel Resistance: Implementations avoid secret-dependent branches
- Secure Random Number Generation: Uses system CSPRNG
Performance Characteristics
Speed Benchmarks
| Operation | Time (μs) | Cycles |
|---|---|---|
| Key Generation | 500 | 1.2M |
| Encapsulation | 600 | 1.4M |
| Decapsulation | 700 | 1.7M |
Comparison with Classical Algorithms
| Algorithm | KeyGen | Encap/Sign | Decap/Verify | Total Size* |
|---|---|---|---|---|
| ML-KEM-768 | 500 μs | 600 μs | 700 μs | 3.6 KB |
| RSA-2048 | 100 ms | 2 ms | 0.1 ms | 0.5 KB |
| ECDH-P256 | 200 μs | 400 μs | 400 μs | 0.1 KB |
*Total size = public key + ciphertext/signature
Integration Guide
TLS Integration
# Hybrid TLS key exchange pseudocode
def hybrid_key_exchange():
# Generate ephemeral keys
ecdh_key = generate_ecdh_key()
mlkem_key = MLKem768.generate_keypair()
# Send public keys
send(ecdh_key.public, mlkem_key.public_key)
# Receive peer's public keys
peer_ecdh, peer_mlkem = receive()
# Compute shared secrets
ecdh_shared = ecdh_key.compute_shared(peer_ecdh)
mlkem_ct, mlkem_shared = MLKem768.encapsulate(peer_mlkem)
# Derive session key
session_key = kdf(ecdh_shared || mlkem_shared)
return session_key
Storage Considerations
# Efficient key storage
class MLKemKeyStore:
def store_keypair(self, keypair, identifier):
# Store public key (can be public)
with open(f"{identifier}.pub", "wb") as f:
f.write(keypair.public_key.to_bytes())
# Store private key (must be protected)
encrypted_private = encrypt_key(
keypair.private_key.to_bytes(),
master_key
)
with open(f"{identifier}.key", "wb") as f:
f.write(encrypted_private)
def load_keypair(self, identifier):
# Load public key
with open(f"{identifier}.pub", "rb") as f:
public_key = MLKem768.PublicKey.from_bytes(f.read())
# Load and decrypt private key
with open(f"{identifier}.key", "rb") as f:
encrypted = f.read()
private_bytes = decrypt_key(encrypted, master_key)
private_key = MLKem768.PrivateKey.from_bytes(private_bytes)
return MLKem768.Keypair(public_key, private_key)
Migration Strategy
From Classical to Hybrid
- Phase 1: Add ML-KEM alongside existing algorithms
# Existing code ecdh_secret = compute_ecdh_secret(...) # Add ML-KEM mlkem_secret = compute_mlkem_secret(...) combined = hash(ecdh_secret || mlkem_secret) - Phase 2: Monitor and optimize
- Track performance impact
- Optimize implementation
- Ensure compatibility
- Phase 3: Increase reliance on PQ
- Use ML-KEM as primary
- Keep classical as fallback
- Plan sunset timeline
Backward Compatibility
class HybridKEM:
def __init__(self, enable_pq=True):
self.enable_pq = enable_pq
def encapsulate(self, public_keys):
secrets = []
ciphertexts = {}
# Always use classical
ct_classical, secret_classical = classical_kem(
public_keys['classical']
)
secrets.append(secret_classical)
ciphertexts['classical'] = ct_classical
# Optionally use PQ
if self.enable_pq and 'mlkem' in public_keys:
ct_mlkem, secret_mlkem = MLKem768.encapsulate(
public_keys['mlkem']
)
secrets.append(secret_mlkem)
ciphertexts['mlkem'] = ct_mlkem
# Combine all secrets
return ciphertexts, hash(b''.join(secrets))
Common Pitfalls
1. NTT Domain Inconsistency
# Bad: Mixing NTT domains incorrectly
# poly_ntt = ntt(poly1)
# result = poly_add(poly_ntt, poly2) # WRONG! poly2 not in NTT domain
# Good: Ensure domain consistency
poly1_ntt = ntt(poly1)
poly2_ntt = ntt(poly2)
result_ntt = poly_add(poly1_ntt, poly2_ntt)
result = inv_ntt(result_ntt)
2. Reusing Randomness
# Bad: Reusing randomness
# random_bytes = os.urandom(32)
# ct1, ss1 = MLKem768.encapsulate(pk1, randomness=random_bytes)
# ct2, ss2 = MLKem768.encapsulate(pk2, randomness=random_bytes) # INSECURE!
# Good: Fresh randomness for each operation
ct1, ss1 = MLKem768.encapsulate(pk1) # Uses fresh randomness
ct2, ss2 = MLKem768.encapsulate(pk2) # Uses fresh randomness
2. Incorrect Serialization
# Bad: Using string representation
# public_key_str = str(public_key) # Loses information!
# Good: Using proper serialization
public_key_bytes = public_key.to_bytes()
public_key_restored = MLKem768.PublicKey.from_bytes(public_key_bytes)
3. Ignoring Decapsulation Failures
# Bad: Ignoring potential failures
# shared_secret = MLKem768.decapsulate(ciphertext, private_key)
# Good: Handle failures appropriately
try:
shared_secret = MLKem768.decapsulate(ciphertext, private_key)
except CryptoError:
# Log error, use alternative method, or abort
shared_secret = fallback_key_exchange()
Test Vectors
# NIST test vector example
test_vector = {
"seed": "7c9935a0b07694aa0c6d10e4db6b1add2fd81a25ccb148032dcd739936737f2d",
"public_key": "5a27b7f148b0b3f1d2d0e3f4...", # 1184 bytes
"private_key": "8f2a9b3c4d5e6f7081929394...", # 2400 bytes
"ciphertext": "a1b2c3d4e5f67890abcdef01...", # 1088 bytes
"shared_secret": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
# Verify implementation
keypair = MLKem768.keypair_from_seed(bytes.fromhex(test_vector["seed"]))
assert keypair.public_key.to_bytes().hex() == test_vector["public_key"]
Resources
- NIST FIPS 203 - ML-KEM Standard
- Original Kyber Paper - Algorithm details
- Security Analysis - Security properties
- Implementation Guide - Detailed implementation notes
- NIST PQC Project - Post-quantum standardization