fix: replace ML-KEM random stub with crypto/mlkem, complete precompile registration

The pure-Go ML-KEM fallback was filling keys/ciphertexts/secrets with
rand.Read, silently producing incorrect results without CGO+liboqs.
Replaced with Go 1.24+ crypto/mlkem (FIPS 203) for both 768 and 1024
parameter sets.

Completed ML-DSA (0x0110-0x0118), ML-KEM (0x0120-0x0128), and SLH-DSA
(0x0130-0x0138) precompile registration that was left as a comment.
This commit is contained in:
Hanzo AI
2025-12-27 16:28:18 -08:00
parent 6c6e8fc648
commit 50d06e10ae
7 changed files with 522 additions and 131 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ func NewHybridKEM() KEM {
// In production, this should handle errors properly
return &HybridKEMImpl{
x25519: &X25519Impl{},
mlkem: &MLKEM768Impl{k: 3},
mlkem: &MLKEM768Impl{},
}
}
+89 -129
View File
@@ -1,128 +1,94 @@
package kem
// NOTE: This file contains pure Go implementations that are used when CGO is not available.
// When CGO is enabled and liboqs is installed, the optimized versions in mlkem_cgo.go will be used instead.
// Pure Go implementation of ML-KEM using crypto/mlkem from the Go standard library (FIPS 203).
// When CGO is enabled and liboqs is installed, the optimized versions in mlkem_c.go are used instead.
import (
"crypto/rand"
"crypto/mlkem"
"crypto/subtle"
"errors"
)
// MLKEM768 implements ML-KEM-768 (Kyber768)
type MLKEM768Impl struct {
k int // k parameter (3 for ML-KEM-768)
}
// MLKEM768Impl implements ML-KEM-768 using Go's crypto/mlkem (FIPS 203)
type MLKEM768Impl struct{}
// MLKEM768PublicKey represents an ML-KEM-768 public key
// MLKEM768PublicKey represents an ML-KEM-768 encapsulation key
type MLKEM768PublicKey struct {
data []byte
}
// MLKEM768PrivateKey represents an ML-KEM-768 private key
// MLKEM768PrivateKey represents an ML-KEM-768 decapsulation key (seed form)
type MLKEM768PrivateKey struct {
data []byte
pk *MLKEM768PublicKey
}
// GenerateKeyPair generates a new ML-KEM-768 key pair.
// This is the pure-Go fallback; when CGO and liboqs are available,
// mlkem_c.go provides the optimized implementation.
// GenerateKeyPair generates a new ML-KEM-768 key pair using crypto/mlkem.
func (m *MLKEM768Impl) GenerateKeyPair() (PublicKey, PrivateKey, error) {
pk := &MLKEM768PublicKey{
data: make([]byte, mlkem768PublicKeySize),
}
sk := &MLKEM768PrivateKey{
data: make([]byte, mlkem768PrivateKeySize),
pk: pk,
}
// Generate random key material for the pure-Go fallback.
if _, err := rand.Read(pk.data); err != nil {
return nil, nil, err
}
if _, err := rand.Read(sk.data); err != nil {
dk, err := mlkem.GenerateKey768()
if err != nil {
return nil, nil, err
}
ekBytes := dk.EncapsulationKey().Bytes()
dkBytes := dk.Bytes()
pk := &MLKEM768PublicKey{data: ekBytes}
sk := &MLKEM768PrivateKey{data: dkBytes, pk: pk}
return pk, sk, nil
}
// Encapsulate generates a shared secret and ciphertext
// Encapsulate generates a shared secret and ciphertext from an encapsulation key.
func (m *MLKEM768Impl) Encapsulate(pk PublicKey) ([]byte, []byte, error) {
mlkemPK, ok := pk.(*MLKEM768PublicKey)
if !ok {
return nil, nil, errors.New("invalid public key type")
}
ciphertext := make([]byte, mlkem768CiphertextSize)
sharedSecret := make([]byte, mlkem768SharedSecretSize)
// Pure-Go fallback: randomized output. CGO+liboqs provides real KEM.
if _, err := rand.Read(ciphertext); err != nil {
return nil, nil, err
}
if _, err := rand.Read(sharedSecret); err != nil {
ek, err := mlkem.NewEncapsulationKey768(mlkemPK.data)
if err != nil {
return nil, nil, err
}
// In production, this would perform actual ML-KEM encapsulation
_ = mlkemPK.data
sharedSecret, ciphertext := ek.Encapsulate()
return ciphertext, sharedSecret, nil
}
// Decapsulate recovers the shared secret from ciphertext
// Decapsulate recovers the shared secret from ciphertext using the decapsulation key.
func (m *MLKEM768Impl) Decapsulate(sk PrivateKey, ciphertext []byte) ([]byte, error) {
mlkemSK, ok := sk.(*MLKEM768PrivateKey)
if !ok {
return nil, errors.New("invalid private key type")
}
if len(ciphertext) != mlkem768CiphertextSize {
if len(ciphertext) != mlkem.CiphertextSize768 {
return nil, errors.New("invalid ciphertext size")
}
sharedSecret := make([]byte, mlkem768SharedSecretSize)
// Pure-Go fallback: randomized output. CGO+liboqs provides real KEM.
if _, err := rand.Read(sharedSecret); err != nil {
dk, err := mlkem.NewDecapsulationKey768(mlkemSK.data)
if err != nil {
return nil, err
}
// In production, this would perform actual ML-KEM decapsulation
_ = mlkemSK.data
_ = ciphertext
return sharedSecret, nil
return dk.Decapsulate(ciphertext)
}
// PublicKeySize returns the size of public keys
func (m *MLKEM768Impl) PublicKeySize() int {
return mlkem768PublicKeySize
}
// PublicKeySize returns the size of ML-KEM-768 encapsulation keys.
func (m *MLKEM768Impl) PublicKeySize() int { return mlkem.EncapsulationKeySize768 }
// PrivateKeySize returns the size of private keys
func (m *MLKEM768Impl) PrivateKeySize() int {
return mlkem768PrivateKeySize
}
// PrivateKeySize returns the size of ML-KEM-768 decapsulation key seeds.
func (m *MLKEM768Impl) PrivateKeySize() int { return mlkem.SeedSize }
// CiphertextSize returns the size of ciphertexts
func (m *MLKEM768Impl) CiphertextSize() int {
return mlkem768CiphertextSize
}
// CiphertextSize returns the size of ML-KEM-768 ciphertexts.
func (m *MLKEM768Impl) CiphertextSize() int { return mlkem.CiphertextSize768 }
// SharedSecretSize returns the size of shared secrets
func (m *MLKEM768Impl) SharedSecretSize() int {
return mlkem768SharedSecretSize
}
// SharedSecretSize returns the size of ML-KEM-768 shared secrets.
func (m *MLKEM768Impl) SharedSecretSize() int { return mlkem.SharedKeySize }
// Bytes returns the raw bytes of the public key
func (pk *MLKEM768PublicKey) Bytes() []byte {
return pk.data
}
// Bytes returns the raw bytes of the public key.
func (pk *MLKEM768PublicKey) Bytes() []byte { return pk.data }
// Equal checks if two public keys are equal
// Equal checks if two public keys are equal in constant time.
func (pk *MLKEM768PublicKey) Equal(other PublicKey) bool {
otherPK, ok := other.(*MLKEM768PublicKey)
if !ok {
@@ -131,17 +97,13 @@ func (pk *MLKEM768PublicKey) Equal(other PublicKey) bool {
return subtle.ConstantTimeCompare(pk.data, otherPK.data) == 1
}
// Bytes returns the raw bytes of the private key
func (sk *MLKEM768PrivateKey) Bytes() []byte {
return sk.data
}
// Bytes returns the raw bytes of the private key seed.
func (sk *MLKEM768PrivateKey) Bytes() []byte { return sk.data }
// Public returns the public key corresponding to the private key
func (sk *MLKEM768PrivateKey) Public() PublicKey {
return sk.pk
}
// Public returns the public key corresponding to the private key.
func (sk *MLKEM768PrivateKey) Public() PublicKey { return sk.pk }
// Equal checks if two private keys are equal
// Equal checks if two private keys are equal in constant time.
func (sk *MLKEM768PrivateKey) Equal(other PrivateKey) bool {
otherSK, ok := other.(*MLKEM768PrivateKey)
if !ok {
@@ -150,84 +112,86 @@ func (sk *MLKEM768PrivateKey) Equal(other PrivateKey) bool {
return subtle.ConstantTimeCompare(sk.data, otherSK.data) == 1
}
// MLKEM1024 implementation (similar structure, different parameters)
type MLKEM1024Impl struct {
k int // k parameter (4 for ML-KEM-1024)
}
// MLKEM1024Impl implements ML-KEM-1024 using Go's crypto/mlkem (FIPS 203)
type MLKEM1024Impl struct{}
// MLKEM1024PublicKey represents an ML-KEM-1024 public key
// MLKEM1024PublicKey represents an ML-KEM-1024 encapsulation key
type MLKEM1024PublicKey struct {
data []byte
}
// MLKEM1024PrivateKey represents an ML-KEM-1024 private key
// MLKEM1024PrivateKey represents an ML-KEM-1024 decapsulation key (seed form)
type MLKEM1024PrivateKey struct {
data []byte
pk *MLKEM1024PublicKey
}
// GenerateKeyPair generates a new ML-KEM-1024 key pair
// GenerateKeyPair generates a new ML-KEM-1024 key pair using crypto/mlkem.
func (m *MLKEM1024Impl) GenerateKeyPair() (PublicKey, PrivateKey, error) {
// Similar to ML-KEM-768 but with different sizes
pk := &MLKEM1024PublicKey{
data: make([]byte, mlkem1024PublicKeySize),
}
sk := &MLKEM1024PrivateKey{
data: make([]byte, mlkem1024PrivateKeySize),
pk: pk,
}
if _, err := rand.Read(pk.data); err != nil {
return nil, nil, err
}
if _, err := rand.Read(sk.data); err != nil {
dk, err := mlkem.GenerateKey1024()
if err != nil {
return nil, nil, err
}
ekBytes := dk.EncapsulationKey().Bytes()
dkBytes := dk.Bytes()
pk := &MLKEM1024PublicKey{data: ekBytes}
sk := &MLKEM1024PrivateKey{data: dkBytes, pk: pk}
return pk, sk, nil
}
// Encapsulate for ML-KEM-1024
// Encapsulate generates a shared secret and ciphertext from an encapsulation key.
func (m *MLKEM1024Impl) Encapsulate(pk PublicKey) ([]byte, []byte, error) {
ciphertext := make([]byte, mlkem1024CiphertextSize)
sharedSecret := make([]byte, mlkem1024SharedSecretSize)
if _, err := rand.Read(ciphertext); err != nil {
return nil, nil, err
mlkemPK, ok := pk.(*MLKEM1024PublicKey)
if !ok {
return nil, nil, errors.New("invalid public key type")
}
if _, err := rand.Read(sharedSecret); err != nil {
ek, err := mlkem.NewEncapsulationKey1024(mlkemPK.data)
if err != nil {
return nil, nil, err
}
sharedSecret, ciphertext := ek.Encapsulate()
return ciphertext, sharedSecret, nil
}
// Decapsulate for ML-KEM-1024
// Decapsulate recovers the shared secret from ciphertext using the decapsulation key.
func (m *MLKEM1024Impl) Decapsulate(sk PrivateKey, ciphertext []byte) ([]byte, error) {
if len(ciphertext) != mlkem1024CiphertextSize {
mlkemSK, ok := sk.(*MLKEM1024PrivateKey)
if !ok {
return nil, errors.New("invalid private key type")
}
if len(ciphertext) != mlkem.CiphertextSize1024 {
return nil, errors.New("invalid ciphertext size")
}
sharedSecret := make([]byte, mlkem1024SharedSecretSize)
if _, err := rand.Read(sharedSecret); err != nil {
dk, err := mlkem.NewDecapsulationKey1024(mlkemSK.data)
if err != nil {
return nil, err
}
return sharedSecret, nil
return dk.Decapsulate(ciphertext)
}
// Size methods for ML-KEM-1024
func (m *MLKEM1024Impl) PublicKeySize() int { return mlkem1024PublicKeySize }
func (m *MLKEM1024Impl) PrivateKeySize() int { return mlkem1024PrivateKeySize }
func (m *MLKEM1024Impl) CiphertextSize() int { return mlkem1024CiphertextSize }
func (m *MLKEM1024Impl) SharedSecretSize() int { return mlkem1024SharedSecretSize }
// PublicKeySize returns the size of ML-KEM-1024 encapsulation keys.
func (m *MLKEM1024Impl) PublicKeySize() int { return mlkem.EncapsulationKeySize1024 }
// Bytes returns the raw bytes of the public key
func (pk *MLKEM1024PublicKey) Bytes() []byte {
return pk.data
}
// PrivateKeySize returns the size of ML-KEM-1024 decapsulation key seeds.
func (m *MLKEM1024Impl) PrivateKeySize() int { return mlkem.SeedSize }
// Equal checks if two public keys are equal
// CiphertextSize returns the size of ML-KEM-1024 ciphertexts.
func (m *MLKEM1024Impl) CiphertextSize() int { return mlkem.CiphertextSize1024 }
// SharedSecretSize returns the size of ML-KEM-1024 shared secrets.
func (m *MLKEM1024Impl) SharedSecretSize() int { return mlkem.SharedKeySize }
// Bytes returns the raw bytes of the public key.
func (pk *MLKEM1024PublicKey) Bytes() []byte { return pk.data }
// Equal checks if two public keys are equal in constant time.
func (pk *MLKEM1024PublicKey) Equal(other PublicKey) bool {
otherPK, ok := other.(*MLKEM1024PublicKey)
if !ok {
@@ -236,17 +200,13 @@ func (pk *MLKEM1024PublicKey) Equal(other PublicKey) bool {
return subtle.ConstantTimeCompare(pk.data, otherPK.data) == 1
}
// Bytes returns the raw bytes of the private key
func (sk *MLKEM1024PrivateKey) Bytes() []byte {
return sk.data
}
// Bytes returns the raw bytes of the private key seed.
func (sk *MLKEM1024PrivateKey) Bytes() []byte { return sk.data }
// Public returns the public key corresponding to the private key
func (sk *MLKEM1024PrivateKey) Public() PublicKey {
return sk.pk
}
// Public returns the public key corresponding to the private key.
func (sk *MLKEM1024PrivateKey) Public() PublicKey { return sk.pk }
// Equal checks if two private keys are equal
// Equal checks if two private keys are equal in constant time.
func (sk *MLKEM1024PrivateKey) Equal(other PrivateKey) bool {
otherSK, ok := other.(*MLKEM1024PrivateKey)
if !ok {
+3 -1
View File
@@ -16,7 +16,9 @@ func GetAllPostQuantumPrecompiles() map[Address]PrecompiledContract {
// Register all precompile types
RegisterSHAKE(registry)
RegisterLamport(registry)
// ML-DSA, ML-KEM, SLH-DSA would be registered here when moved to this package
RegisterMLDSA(registry)
RegisterMLKEM(registry)
RegisterSLHDSA(registry)
return registry.Contracts()
}
+148
View File
@@ -0,0 +1,148 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// ML-DSA (FIPS 204) precompiled contracts for EVM
// Provides on-chain post-quantum signature verification
package precompile
import (
"crypto/rand"
"errors"
"github.com/luxfi/crypto/mldsa"
)
// ML-DSA precompile addresses (0x0110-0x0119)
var (
MLDSA44VerifyAddress = HexToAddress("0x0000000000000000000000000000000000000110")
MLDSA65VerifyAddress = HexToAddress("0x0000000000000000000000000000000000000111")
MLDSA87VerifyAddress = HexToAddress("0x0000000000000000000000000000000000000112")
MLDSA44SignAddress = HexToAddress("0x0000000000000000000000000000000000000113")
MLDSA65SignAddress = HexToAddress("0x0000000000000000000000000000000000000114")
MLDSA87SignAddress = HexToAddress("0x0000000000000000000000000000000000000115")
MLDSA44KeyGenAddress = HexToAddress("0x0000000000000000000000000000000000000116")
MLDSA65KeyGenAddress = HexToAddress("0x0000000000000000000000000000000000000117")
MLDSA87KeyGenAddress = HexToAddress("0x0000000000000000000000000000000000000118")
)
// Gas costs for ML-DSA operations
const (
mldsa44VerifyGas = 120000
mldsa65VerifyGas = 180000
mldsa87VerifyGas = 250000
mldsa44SignGas = 200000
mldsa65SignGas = 300000
mldsa87SignGas = 400000
mldsaKeyGenGas = 500000
)
// mldsaVerify implements ML-DSA signature verification for a given mode.
type mldsaVerify struct {
mode mldsa.Mode
gas uint64
}
func (v *mldsaVerify) RequiredGas(input []byte) uint64 { return v.gas }
func (v *mldsaVerify) Run(input []byte) ([]byte, error) {
// Input: [public_key][signature][message]
pubKeySize := mldsa.GetPublicKeySize(v.mode)
sigSize := mldsa.GetSignatureSize(v.mode)
if len(input) < pubKeySize+sigSize+1 {
return nil, errors.New("input too short")
}
pubKeyBytes := input[:pubKeySize]
sigBytes := input[pubKeySize : pubKeySize+sigSize]
message := input[pubKeySize+sigSize:]
pubKey, err := mldsa.PublicKeyFromBytes(pubKeyBytes, v.mode)
if err != nil {
return nil, err
}
valid := pubKey.VerifySignature(message, sigBytes)
result := make([]byte, 32)
if valid {
result[31] = 0x01
}
return result, nil
}
// mldsaSign implements ML-DSA signing for a given mode.
type mldsaSign struct {
mode mldsa.Mode
gas uint64
}
func (s *mldsaSign) RequiredGas(input []byte) uint64 { return s.gas }
func (s *mldsaSign) Run(input []byte) ([]byte, error) {
// Input: [private_key][message]
privKeySize := 0
switch s.mode {
case mldsa.MLDSA44:
privKeySize = mldsa.MLDSA44PrivateKeySize
case mldsa.MLDSA65:
privKeySize = mldsa.MLDSA65PrivateKeySize
case mldsa.MLDSA87:
privKeySize = mldsa.MLDSA87PrivateKeySize
default:
return nil, errors.New("invalid ML-DSA mode")
}
if len(input) < privKeySize+1 {
return nil, errors.New("input too short")
}
privKeyBytes := input[:privKeySize]
message := input[privKeySize:]
privKey, err := mldsa.PrivateKeyFromBytes(s.mode, privKeyBytes)
if err != nil {
return nil, err
}
return privKey.Sign(rand.Reader, message, nil)
}
// mldsaKeyGen implements ML-DSA key pair generation for a given mode.
type mldsaKeyGen struct {
mode mldsa.Mode
}
func (k *mldsaKeyGen) RequiredGas(input []byte) uint64 { return mldsaKeyGenGas }
func (k *mldsaKeyGen) Run(input []byte) ([]byte, error) {
privKey, err := mldsa.GenerateKey(rand.Reader, k.mode)
if err != nil {
return nil, err
}
pubBytes := privKey.PublicKey.Bytes()
privBytes := privKey.Bytes()
// Output: [public_key][private_key]
result := make([]byte, len(pubBytes)+len(privBytes))
copy(result, pubBytes)
copy(result[len(pubBytes):], privBytes)
return result, nil
}
// RegisterMLDSA registers all ML-DSA precompiles.
func RegisterMLDSA(registry *Registry) {
registry.Register(MLDSA44VerifyAddress, &mldsaVerify{mode: mldsa.MLDSA44, gas: mldsa44VerifyGas})
registry.Register(MLDSA65VerifyAddress, &mldsaVerify{mode: mldsa.MLDSA65, gas: mldsa65VerifyGas})
registry.Register(MLDSA87VerifyAddress, &mldsaVerify{mode: mldsa.MLDSA87, gas: mldsa87VerifyGas})
registry.Register(MLDSA44SignAddress, &mldsaSign{mode: mldsa.MLDSA44, gas: mldsa44SignGas})
registry.Register(MLDSA65SignAddress, &mldsaSign{mode: mldsa.MLDSA65, gas: mldsa65SignGas})
registry.Register(MLDSA87SignAddress, &mldsaSign{mode: mldsa.MLDSA87, gas: mldsa87SignGas})
registry.Register(MLDSA44KeyGenAddress, &mldsaKeyGen{mode: mldsa.MLDSA44})
registry.Register(MLDSA65KeyGenAddress, &mldsaKeyGen{mode: mldsa.MLDSA65})
registry.Register(MLDSA87KeyGenAddress, &mldsaKeyGen{mode: mldsa.MLDSA87})
}
func init() {
RegisterMLDSA(PostQuantumRegistry)
}
+135
View File
@@ -0,0 +1,135 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// ML-KEM (FIPS 203) precompiled contracts for EVM
// Provides on-chain post-quantum key encapsulation
package precompile
import (
"errors"
"github.com/luxfi/crypto/mlkem"
)
// ML-KEM precompile addresses (0x0120-0x0129)
var (
MLKEM512EncapsulateAddress = HexToAddress("0x0000000000000000000000000000000000000120")
MLKEM768EncapsulateAddress = HexToAddress("0x0000000000000000000000000000000000000121")
MLKEM1024EncapsulateAddress = HexToAddress("0x0000000000000000000000000000000000000122")
MLKEM512DecapsulateAddress = HexToAddress("0x0000000000000000000000000000000000000123")
MLKEM768DecapsulateAddress = HexToAddress("0x0000000000000000000000000000000000000124")
MLKEM1024DecapsulateAddress = HexToAddress("0x0000000000000000000000000000000000000125")
MLKEM512KeyGenAddress = HexToAddress("0x0000000000000000000000000000000000000126")
MLKEM768KeyGenAddress = HexToAddress("0x0000000000000000000000000000000000000127")
MLKEM1024KeyGenAddress = HexToAddress("0x0000000000000000000000000000000000000128")
)
// Gas costs for ML-KEM operations
const (
mlkem512EncapsulateGas = 80000
mlkem768EncapsulateGas = 100000
mlkem1024EncapsulateGas = 130000
mlkem512DecapsulateGas = 80000
mlkem768DecapsulateGas = 100000
mlkem1024DecapsulateGas = 130000
mlkemKeyGenGas = 200000
)
// mlkemEncapsulate implements ML-KEM encapsulation for a given mode.
type mlkemEncapsulate struct {
mode mlkem.Mode
gas uint64
}
func (e *mlkemEncapsulate) RequiredGas(input []byte) uint64 { return e.gas }
func (e *mlkemEncapsulate) Run(input []byte) ([]byte, error) {
// Input: [encapsulation_key]
expectedSize := mlkem.GetPublicKeySize(e.mode)
if len(input) != expectedSize {
return nil, errors.New("invalid encapsulation key size")
}
pk, err := mlkem.PublicKeyFromBytes(input, e.mode)
if err != nil {
return nil, err
}
ciphertext, sharedKey, err := pk.Encapsulate()
if err != nil {
return nil, err
}
// Output: [shared_key (32)][ciphertext]
result := make([]byte, len(sharedKey)+len(ciphertext))
copy(result, sharedKey)
copy(result[len(sharedKey):], ciphertext)
return result, nil
}
// mlkemDecapsulate implements ML-KEM decapsulation for a given mode.
type mlkemDecapsulate struct {
mode mlkem.Mode
gas uint64
}
func (d *mlkemDecapsulate) RequiredGas(input []byte) uint64 { return d.gas }
func (d *mlkemDecapsulate) Run(input []byte) ([]byte, error) {
// Input: [private_key][ciphertext]
privKeySize := mlkem.GetPrivateKeySize(d.mode)
ctSize := mlkem.GetCiphertextSize(d.mode)
if len(input) != privKeySize+ctSize {
return nil, errors.New("invalid input size")
}
privKeyBytes := input[:privKeySize]
ciphertext := input[privKeySize:]
sk, err := mlkem.PrivateKeyFromBytes(privKeyBytes, d.mode)
if err != nil {
return nil, err
}
return sk.Decapsulate(ciphertext)
}
// mlkemKeyGen implements ML-KEM key pair generation for a given mode.
type mlkemKeyGen struct {
mode mlkem.Mode
}
func (k *mlkemKeyGen) RequiredGas(input []byte) uint64 { return mlkemKeyGenGas }
func (k *mlkemKeyGen) Run(input []byte) ([]byte, error) {
pk, sk, err := mlkem.GenerateKey(k.mode)
if err != nil {
return nil, err
}
pubBytes := pk.Bytes()
privBytes := sk.Bytes()
// Output: [public_key][private_key]
result := make([]byte, len(pubBytes)+len(privBytes))
copy(result, pubBytes)
copy(result[len(pubBytes):], privBytes)
return result, nil
}
// RegisterMLKEM registers all ML-KEM precompiles.
func RegisterMLKEM(registry *Registry) {
registry.Register(MLKEM512EncapsulateAddress, &mlkemEncapsulate{mode: mlkem.MLKEM512, gas: mlkem512EncapsulateGas})
registry.Register(MLKEM768EncapsulateAddress, &mlkemEncapsulate{mode: mlkem.MLKEM768, gas: mlkem768EncapsulateGas})
registry.Register(MLKEM1024EncapsulateAddress, &mlkemEncapsulate{mode: mlkem.MLKEM1024, gas: mlkem1024EncapsulateGas})
registry.Register(MLKEM512DecapsulateAddress, &mlkemDecapsulate{mode: mlkem.MLKEM512, gas: mlkem512DecapsulateGas})
registry.Register(MLKEM768DecapsulateAddress, &mlkemDecapsulate{mode: mlkem.MLKEM768, gas: mlkem768DecapsulateGas})
registry.Register(MLKEM1024DecapsulateAddress, &mlkemDecapsulate{mode: mlkem.MLKEM1024, gas: mlkem1024DecapsulateGas})
registry.Register(MLKEM512KeyGenAddress, &mlkemKeyGen{mode: mlkem.MLKEM512})
registry.Register(MLKEM768KeyGenAddress, &mlkemKeyGen{mode: mlkem.MLKEM768})
registry.Register(MLKEM1024KeyGenAddress, &mlkemKeyGen{mode: mlkem.MLKEM1024})
}
func init() {
RegisterMLKEM(PostQuantumRegistry)
}
+6
View File
@@ -647,6 +647,12 @@ func TestPrecompileRegistry(t *testing.T) {
t.Run("AllPrecompilesRegistered", func(t *testing.T) {
// Check that all expected addresses are registered
expectedAddresses := []string{
// ML-DSA (FIPS 204)
"0x0110", "0x0111", "0x0112", "0x0113", "0x0114", "0x0115", "0x0116", "0x0117", "0x0118",
// ML-KEM (FIPS 203)
"0x0120", "0x0121", "0x0122", "0x0123", "0x0124", "0x0125", "0x0126", "0x0127", "0x0128",
// SLH-DSA (FIPS 205)
"0x0130", "0x0131", "0x0132", "0x0133", "0x0134", "0x0135", "0x0136", "0x0137", "0x0138",
// SHAKE
"0x0140", "0x0141", "0x0142", "0x0143", "0x0144", "0x0145", "0x0146", "0x0147", "0x0148",
// Lamport
+140
View File
@@ -0,0 +1,140 @@
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// SLH-DSA (FIPS 205) precompiled contracts for EVM
// Provides on-chain post-quantum stateless hash-based signature verification
package precompile
import (
"crypto/rand"
"errors"
"github.com/luxfi/crypto/slhdsa"
)
// SLH-DSA precompile addresses (0x0130-0x0139)
var (
SLHDSA128sVerifyAddress = HexToAddress("0x0000000000000000000000000000000000000130")
SLHDSA128fVerifyAddress = HexToAddress("0x0000000000000000000000000000000000000131")
SLHDSA192sVerifyAddress = HexToAddress("0x0000000000000000000000000000000000000132")
SLHDSA192fVerifyAddress = HexToAddress("0x0000000000000000000000000000000000000133")
SLHDSA256sVerifyAddress = HexToAddress("0x0000000000000000000000000000000000000134")
SLHDSA256fVerifyAddress = HexToAddress("0x0000000000000000000000000000000000000135")
SLHDSA128sSignAddress = HexToAddress("0x0000000000000000000000000000000000000136")
SLHDSA192sSignAddress = HexToAddress("0x0000000000000000000000000000000000000137")
SLHDSA256sSignAddress = HexToAddress("0x0000000000000000000000000000000000000138")
)
// Gas costs for SLH-DSA operations
const (
slhdsa128sVerifyGas = 300000
slhdsa128fVerifyGas = 200000
slhdsa192sVerifyGas = 400000
slhdsa192fVerifyGas = 300000
slhdsa256sVerifyGas = 500000
slhdsa256fVerifyGas = 400000
slhdsaSignGas = 1000000
)
// slhdsaVerify implements SLH-DSA signature verification for a given mode.
type slhdsaVerify struct {
mode slhdsa.Mode
gas uint64
}
func (v *slhdsaVerify) RequiredGas(input []byte) uint64 { return v.gas }
func (v *slhdsaVerify) Run(input []byte) ([]byte, error) {
// Input: [public_key][signature][message]
pubKeySize := slhdsa.GetPublicKeySize(v.mode)
sigSize := slhdsa.GetSignatureSize(v.mode)
if len(input) < pubKeySize+sigSize+1 {
return nil, errors.New("input too short")
}
pubKeyBytes := input[:pubKeySize]
sigBytes := input[pubKeySize : pubKeySize+sigSize]
message := input[pubKeySize+sigSize:]
pubKey, err := slhdsa.PublicKeyFromBytes(pubKeyBytes, v.mode)
if err != nil {
return nil, err
}
valid := pubKey.VerifySignature(message, sigBytes)
result := make([]byte, 32)
if valid {
result[31] = 0x01
}
return result, nil
}
// slhdsaSign implements SLH-DSA signing for a given mode.
type slhdsaSign struct {
mode slhdsa.Mode
gas uint64
}
func (s *slhdsaSign) RequiredGas(input []byte) uint64 { return s.gas }
func (s *slhdsaSign) Run(input []byte) ([]byte, error) {
// Input: [private_key_bytes][message]
// SLH-DSA private keys are mode-dependent in size; we need the full
// serialized private key as produced by PrivateKey.Bytes().
// Since we don't have a PrivateKeyFromBytes that detects size, callers
// must provide the correctly-sized key for the mode.
pubKeySize := slhdsa.GetPublicKeySize(s.mode)
if pubKeySize == 0 {
return nil, errors.New("invalid SLH-DSA mode")
}
// SLH-DSA private key is 4*n bytes where n depends on security level:
// 128-bit: n=16 -> 64 bytes, 192-bit: n=24 -> 96 bytes, 256-bit: n=32 -> 128 bytes
var privKeySize int
switch s.mode {
case slhdsa.SHA2_128s, slhdsa.SHAKE_128s, slhdsa.SHA2_128f, slhdsa.SHAKE_128f:
privKeySize = 64
case slhdsa.SHA2_192s, slhdsa.SHAKE_192s, slhdsa.SHA2_192f, slhdsa.SHAKE_192f:
privKeySize = 96
case slhdsa.SHA2_256s, slhdsa.SHAKE_256s, slhdsa.SHA2_256f, slhdsa.SHAKE_256f:
privKeySize = 128
default:
return nil, errors.New("invalid SLH-DSA mode")
}
if len(input) < privKeySize+1 {
return nil, errors.New("input too short")
}
privKeyBytes := input[:privKeySize]
message := input[privKeySize:]
privKey, err := slhdsa.PrivateKeyFromBytes(s.mode, privKeyBytes)
if err != nil {
return nil, err
}
return privKey.Sign(rand.Reader, message, nil)
}
// RegisterSLHDSA registers all SLH-DSA precompiles.
func RegisterSLHDSA(registry *Registry) {
// Verification precompiles (small and fast variants)
registry.Register(SLHDSA128sVerifyAddress, &slhdsaVerify{mode: slhdsa.SHA2_128s, gas: slhdsa128sVerifyGas})
registry.Register(SLHDSA128fVerifyAddress, &slhdsaVerify{mode: slhdsa.SHA2_128f, gas: slhdsa128fVerifyGas})
registry.Register(SLHDSA192sVerifyAddress, &slhdsaVerify{mode: slhdsa.SHA2_192s, gas: slhdsa192sVerifyGas})
registry.Register(SLHDSA192fVerifyAddress, &slhdsaVerify{mode: slhdsa.SHA2_192f, gas: slhdsa192fVerifyGas})
registry.Register(SLHDSA256sVerifyAddress, &slhdsaVerify{mode: slhdsa.SHA2_256s, gas: slhdsa256sVerifyGas})
registry.Register(SLHDSA256fVerifyAddress, &slhdsaVerify{mode: slhdsa.SHA2_256f, gas: slhdsa256fVerifyGas})
// Signing precompiles (small signature variants only -- fast variants are for
// off-chain use where gas is irrelevant)
registry.Register(SLHDSA128sSignAddress, &slhdsaSign{mode: slhdsa.SHA2_128s, gas: slhdsaSignGas})
registry.Register(SLHDSA192sSignAddress, &slhdsaSign{mode: slhdsa.SHA2_192s, gas: slhdsaSignGas})
registry.Register(SLHDSA256sSignAddress, &slhdsaSign{mode: slhdsa.SHA2_256s, gas: slhdsaSignGas})
}
func init() {
RegisterSLHDSA(PostQuantumRegistry)
}