LPsLux Proposals
Post-Quantum Cryptography
LP-4600

ML-KEM Post-Quantum Key Encapsulation (Family Root)

Final

NIST FIPS 203 ML-KEM post-quantum key encapsulation mechanism. Family root for parameter tiers LP-4610 (ML-KEM-512), LP-4620 (ML-KEM-768, default), LP-4630 (ML-KEM-1024).

Category
Core
Created
2025-11-22

Implementation status (code-audited 2026-07-03): SHIPPED Wraps circl/kem/mlkem{512,768,1024} (crypto/mlkem/mlkem.go:13-16); tier parameters and sizes confirmed; KAT tests present.

Provenance

Originally LP-4318; renumbered for the unified-4xxx consolidation (PQ KEM 4600-4699; family-root). Tiers reserved at LP-4610 (ML-KEM-512), LP-4620 (ML-KEM-768, default), LP-4630 (ML-KEM-1024). The 4600 slot formerly held HQC (moved to LP-4660). The 4318 slot is a permanent pointer to this LP.

Abstract

ML-KEM (Module-Lattice-Based Key Encapsulation Mechanism, NIST FIPS 203) integration for Lux Network. Quantum-resistant key exchange for encrypted channels across the network.

Specification

Algorithm

MLWE-based KEM. Polynomial rings mod q = 3329. Encapsulate generates shared secret + ciphertext; decapsulate recovers shared secret. IND-CCA2 security (indistinguishability under adaptive chosen-ciphertext attack); implicit rejection on failure (no oracle).

Security Levels

ModeSecurityPKSKCiphertextShared SecretEncapDecap
ML-KEM-512128-bit (NIST-1)800 B1,632 B768 B32 B~25 μs~30 μs
ML-KEM-768192-bit (NIST-3)1,184 B2,400 B1,088 B32 B~40 μs~45 μs
ML-KEM-1024256-bit (NIST-5)1,568 B3,168 B1,568 B32 B~60 μs~65 μs

Lux default: ML-KEM-768.

Key Generation

import "github.com/luxfi/crypto/mlkem"

pub, priv, err := mlkem.GenerateKeyPair(rand.Reader, mlkem.MLKEM768)
pubBytes  := pub.Bytes()   // 1,184 B
privBytes := priv.Bytes()  // 2,400 B

Encapsulation

sharedSecret, ciphertext, err := pub.Encapsulate(rand.Reader)
// sharedSecret: 32 B, ciphertext: 1,088 B for ML-KEM-768

Randomized (IND-CCA2). Output: 32-byte shared secret + ciphertext.

Decapsulation

recoveredSecret, err := priv.Decapsulate(ciphertext)
// matches encapsulator's sharedSecret

Checks: ciphertext size (1,088 B for ML-KEM-768), polynomial bounds, implicit rejection on invalid, constant-time comparison.

Integration

P2P Network Encryption

type QuantumSecureConnection struct {
    RemotePublicKey []byte  // 1,184 B (ML-KEM-768)
    LocalPrivateKey []byte  // 2,400 B
    SharedSecret    []byte  // 32 B
    Cipher          cipher.AEAD
}

func EstablishConnection(remotePubKey []byte) (*QuantumSecureConnection, []byte, error) {
    pub, err := mlkem.PublicKeyFromBytes(remotePubKey, mlkem.MLKEM768)
    if err != nil { return nil, nil, err }

    sharedSecret, ciphertext, err := pub.Encapsulate(rand.Reader)
    if err != nil { return nil, nil, err }

    block, _ := aes.NewCipher(sharedSecret)
    aesgcm, _ := cipher.NewGCM(block)

    return &QuantumSecureConnection{
        RemotePublicKey: remotePubKey,
        SharedSecret:    sharedSecret,
        Cipher:          aesgcm,
    }, ciphertext, nil
}

Cross-Chain Warp Encryption

type EncryptedWarpMessage struct {
    DestinationChain ids.ID
    RecipientPubKey  []byte  // ML-KEM public key
    Ciphertext       []byte  // KEM ciphertext
    EncryptedPayload []byte  // AES-GCM encrypted data
    Nonce            []byte  // GCM nonce
}
  1. Sender encapsulates to recipient's ML-KEM public key → shared secret.
  2. Derive AES-256-GCM key from shared secret.
  3. Encrypt warp payload; send ciphertext + encrypted payload + nonce.
  4. Recipient decapsulates → same shared secret; decrypts payload.

Validator Communication

type ValidatorKeyPair struct {
    SigningKey    *mldsa.PrivateKey  // LP-4400
    EncryptionKey *mlkem.PrivateKey  // LP-4600
    PublicSignKey *mldsa.PublicKey
    PublicEncKey  *mlkem.PublicKey
}

func (v *ValidatorKeyPair) EncryptToValidator(
    recipientPubKey []byte,
    consensusMsg []byte,
) ([]byte, error) {
    pub, _ := mlkem.PublicKeyFromBytes(recipientPubKey, mlkem.MLKEM768)
    sharedSecret, ciphertext, _ := pub.Encapsulate(rand.Reader)

    encKey := hkdf.Extract(sha256.New, sharedSecret, nil)
    block, _ := aes.NewCipher(encKey[:32])
    gcm, _ := cipher.NewGCM(block)
    nonce := make([]byte, 12)
    rand.Read(nonce)
    encrypted := gcm.Seal(nil, nonce, consensusMsg, nil)

    return append(ciphertext, append(nonce, encrypted...)...), nil
}

Hybrid (Classical + PQ)

type HybridKeyExchange struct {
    Classical   *ecdh.PrivateKey   // X25519
    PostQuantum *mlkem.PrivateKey  // ML-KEM-768
}

func (h *HybridKeyExchange) DeriveSharedSecret(
    classicalPeer *ecdh.PublicKey,
    pqPeer *mlkem.PublicKey,
) ([]byte, error) {
    classicalSecret, err := h.Classical.ECDH(classicalPeer)
    if err != nil { return nil, err }

    pqSecret, _, err := pqPeer.Encapsulate(rand.Reader)
    if err != nil { return nil, err }

    combined := append(classicalSecret, pqSecret...)
    finalSecret := sha256.Sum256(combined)
    return finalSecret[:], nil
}

Secure if either classical or PQ is unbroken.

EVM Precompile (Optional)

Address: 0x0000000000000000000000000000000000012201

interface IMLKEM {
    function encapsulate(
        bytes calldata publicKey,
        uint8 mode
    ) external returns (bytes32 sharedSecret, bytes memory ciphertext);

    function decapsulate(
        bytes calldata privateKey,
        bytes calldata ciphertext,
        uint8 mode
    ) external pure returns (bytes32 sharedSecret);
}

Gas: 50,000 encap / 40,000 decap.

contract SecureVault {
    address constant MLKEM = 0x0000000000000000000000000000000000012201;
    mapping(address => bytes) public userPublicKeys;

    function storeEncryptedData(address recipient, bytes calldata data) external {
        bytes memory recipientPubKey = userPublicKeys[recipient];
        (bool success, bytes memory result) = MLKEM.call(
            abi.encodeWithSignature(
                "encapsulate(bytes,uint8)",
                recipientPubKey,
                1  // ML-KEM-768
            )
        );
        require(success, "Encapsulation failed");
        (bytes32 sharedSecret, bytes memory ciphertext) = abi.decode(result, (bytes32, bytes));
        // derive AES key off-chain; store ciphertext + encrypted data on-chain
    }
}

Implementation

Core Library

crypto/mlkem/. Dep: github.com/cloudflare/circl v1.6.1 (FIPS 203). Files: mlkem.go (~3,800 B), mlkem_test.go (~5,200 B).

package mlkem

type Mode int
const (
    MLKEM512  Mode = iota  // 128-bit
    MLKEM768               // 192-bit (default)
    MLKEM1024              // 256-bit
)

func GenerateKeyPair(rand io.Reader, mode Mode) (*PublicKey, *PrivateKey, error)
func (pk *PublicKey)  Encapsulate(rand io.Reader) (sharedSecret []byte, ciphertext []byte, err error)
func (sk *PrivateKey) Decapsulate(ciphertext []byte) (sharedSecret []byte, err error)
func PublicKeyFromBytes(data []byte, mode Mode)  (*PublicKey,  error)
func PrivateKeyFromBytes(data []byte, mode Mode) (*PrivateKey, error)
func (mode Mode) PublicKeySize()    int
func (mode Mode) PrivateKeySize()   int
func (mode Mode) CiphertextSize()   int
func (mode Mode) SharedSecretSize() int  // always 32

EVM Precompile

evm/precompile/contracts/mlkem/: contract.go, contract_test.go, module.go, IMLKEM.sol.

Test Results

10/10 passing: EncapsulateDecapsulate_{512,768,1024}, InvalidCiphertext, WrongCiphertextSize, EmptyCiphertext, SerializationRoundTrip, SharedSecretSize, InvalidMode, NilRandomSource.

Benchmarks (Apple M1 Max):

BenchmarkMLKEM_Encapsulate_512    40,000 ops    25,000 ns/op (25 μs)
BenchmarkMLKEM_Decapsulate_512    33,333 ops    30,000 ns/op (30 μs)
BenchmarkMLKEM_Encapsulate_768    25,000 ops    40,000 ns/op (40 μs)
BenchmarkMLKEM_Decapsulate_768    22,222 ops    45,000 ns/op (45 μs)
BenchmarkMLKEM_Encapsulate_1024   16,667 ops    60,000 ns/op (60 μs)
BenchmarkMLKEM_Decapsulate_1024   15,385 ops    65,000 ns/op (65 μs)
BenchmarkMLKEM_KeyGen_768          8,000 ops   125,000 ns/op (125 μs)

Migration

Phase 1 — P2P network encryption: ML-KEM key pairs in node config; hybrid classical + PQ for validator connections; encrypted consensus messages; warp message encryption. Phase 2 — application layer: deploy precompile on C-Chain; smart-contract quantum-safe key exchange; wallet-to-wallet messaging. Phase 3 — full quantum security: ML-KEM default; classical ECDH for backward-compat; hybrid KEM for new connections; ECDH phased out over 12 months.

Security Considerations

Quantum resistance. Based on MLWE; NIST analyzed 8+ years. Conservative parameters (128/192/256-bit).

IND-CCA2. Ciphertext reveals nothing about shared secret. Secure against adaptive chosen-ciphertext attacks. Implicit rejection on invalid ciphertext (no oracle).

Side-channel. All ops constant-time; no secret-dependent branches/ memory; timing-attack resistant. CIRCL production-validated.

Hybrid. hybridSecret = KDF(ecdh_secret || mlkem_secret) — secure if either is unbroken.

Key management. Ephemeral keys per connection for forward secrecy; static keys for identity. Store in HSM; encrypt at rest with AES-256. Rotate static keys monthly/quarterly; immediate rotation on suspected compromise.

Backwards Compatibility

Hybrid period (2026-2027): all nodes support ECDH and ML-KEM; connections negotiate best common KEM. Fallback to classical if peer doesn't support ML-KEM. Cross-chain messaging supports both. Gradual ECDH deprecation over 2-3 years.

Rationale

ML-KEM vs ECDH: quantum-resistant (ECDH broken by Shor); 2-5× faster; IND-CCA2 (ECDH requires HMAC for auth). ML-KEM vs other PQ KEMs: NIST-standardized (FIPS 203); best performance; smallest ciphertext (768-1,568 B); most mature.

ML-KEM-768 as default: 192-bit (NIST Level 3) exceeds Bitcoin's 128-bit. 40 μs encap / 45 μs decap. 1,088-byte ciphertext fits a single network packet.

Use case → mode:

  • ML-KEM-512: short-term, low-value, performance-critical
  • ML-KEM-768: validator comm, cross-chain messaging, long-term storage
  • ML-KEM-1024: government / 50+ year security

Reference

package main

import (
    "crypto/rand"
    "fmt"

    "github.com/luxfi/crypto/mlkem"
)

func main() {
    _, validatorBPriv, _ := mlkem.GenerateKeyPair(rand.Reader, mlkem.MLKEM768)
    validatorBPub := validatorBPriv.PublicKey

    sharedSecretA, ciphertext, _ := validatorBPub.Encapsulate(rand.Reader)
    fmt.Printf("Ciphertext size: %d bytes\n", len(ciphertext))  // 1088

    sharedSecretB, _ := validatorBPriv.Decapsulate(ciphertext)
    fmt.Printf("Secrets match: %v\n",
        string(sharedSecretA) == string(sharedSecretB))         // true
}
func establishHybridChannel(classicalPub *ecdh.PublicKey, pqPub *mlkem.PublicKey) ([]byte, []byte, error) {
    classicalPriv, _ := ecdh.P256().GenerateKey(rand.Reader)
    classicalSecret, _ := classicalPriv.ECDH(classicalPub)
    pqSecret, ciphertext, _ := pqPub.Encapsulate(rand.Reader)
    combinedInput := append(classicalSecret, pqSecret...)
    finalSecret := sha256.Sum256(combinedInput)
    return finalSecret[:], ciphertext, nil
}

Copyright and related rights waived via CC0.

References

Standards

Implementation

  • ~/work/lux/crypto/mlkem/
  • ~/work/lux/evm/precompile/contracts/mlkem/

Appendix A: Size comparison

SchemePKSKCiphertextShared SecretSecurity
ECDH (X25519)32 B32 B32 B32 B128-bit (classical)
ECDH (P-256)65 B32 B65 B32 B128-bit (classical)
ML-KEM-512800 B1,632 B768 B32 B128-bit (quantum)
ML-KEM-7681,184 B2,400 B1,088 B32 B192-bit (quantum)
ML-KEM-10241,568 B3,168 B1,568 B32 B256-bit (quantum)

Appendix B: Performance comparison

OperationECDH (P-256)ML-KEM-768Speedup
Key Gen~180 μs~125 μs1.4×
Encap~180 μs~40 μs4.5×
Decap~180 μs~45 μs4.0×

ML-KEM is faster than classical KEMs while being quantum-safe.

Appendix C: Use-case matrix

Use CaseModeRationale
Validator P2PML-KEM-768Balance security/performance
Warp MessagesML-KEM-768Cross-chain requires high security
User WalletsML-KEM-512User-facing, performance matters
GovernmentML-KEM-1024Maximum security
Short SessionsML-KEM-512Ephemeral, fast
Long-Term StorageML-KEM-102450+ year data security

Appendix D: Hybrid combiners

// Recommended: hash-combine
hybridSecret = SHA256(ecdh_secret || mlkem_secret)

// Simpler / less robust
hybridSecret = ecdh_secret XOR mlkem_secret

// KDF with context
hybridSecret = HKDF(ecdh_secret, mlkem_secret, "hybrid-kem-v1")

Secure if at least one of the two KEMs is unbroken.