Post-Quantum Cryptography

🔮 ML-KEM-768 Kyber Key Encapsulation

Security Level 192-bit (NIST Level 3)
Performance ⭐⭐⭐⭐⭐ Excellent
Quantum Resistant ✅ Yes
Standardization NIST FIPS 203
Public Key Size 1,184 bytes
Ciphertext Size 1,088 bytes

📖 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 Level
NIST Level 3
192-bit post-quantum security
Public Key Size
1,184 bytes
Encapsulation key size
Private Key Size
2,400 bytes
Decapsulation key size
Ciphertext Size
1,088 bytes
Encapsulated key size
Shared Secret
32 bytes
Generated secret length
Failure Probability
2^-164
Decapsulation failure rate

🛡️ 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
A Random matrix over polynomial ring
s Secret vector with small coefficients
e Error vector with small coefficients

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

  1. 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
  2. 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
  3. Compression
    • Public keys and ciphertexts are compressed
    • Reduces bandwidth requirements
    • Maintains security properties
  4. 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

  1. 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)
    
  2. Phase 2: Monitor and optimize
    • Track performance impact
    • Optimize implementation
    • Ensure compatibility
  3. 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