mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
The module already called itself github.com/luxfi/node while containing no node in Go: cmd/luxd was twenty-one lines that printed the size of a few types, and the Go column of the matrix measured a VM plugin. A repo that fetches the thing it claims to be has not replaced it, so the Go sources move in — 1515 files, the daemon and everything under it — and the build reaches outside this module for nothing. Rust and C++ sit beside it unchanged and are still measured against it by the differential. That is the point of one repo: the reference implementation and the two ports are the same checkout, so parity is a thing you can run rather than a thing you assert. Verified: builds, and the closure carries no lux-private — checked at build time rather than believed. The daemon starts twelve chains on a local network (P X C Q Z A B G K M F, plus D through its plugin) and reports healthy.
835 lines
27 KiB
Go
835 lines
27 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package warp
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/cloudflare/circl/kem/mlkem/mlkem768"
|
|
"github.com/luxfi/crypto/bls"
|
|
"github.com/luxfi/crypto/threshold"
|
|
"github.com/luxfi/math/set"
|
|
_ "github.com/luxfi/threshold/scheme/bls" // Register BLS threshold scheme
|
|
)
|
|
|
|
var (
|
|
_ Signature = (*BitSetSignature)(nil)
|
|
_ Signature = (*CoronaSignature)(nil)
|
|
_ Signature = (*HybridBLSCoronaSignature)(nil) // Deprecated: use CoronaSignature
|
|
|
|
ErrInvalidBitSet = errors.New("bitset is invalid")
|
|
ErrInsufficientWeight = errors.New("signature weight is insufficient")
|
|
ErrInvalidSignature = errors.New("signature is invalid")
|
|
ErrParseSignature = errors.New("failed to parse signature")
|
|
ErrInvalidCoronaSignature = errors.New("corona signature is invalid")
|
|
ErrMissingCoronaPublicKey = errors.New("missing corona public key for validator")
|
|
ErrCoronaPublicKeyMismatch = errors.New("corona public key does not match the validator's registered key")
|
|
ErrCoronaGroupKeyMismatch = errors.New("corona public keys are not a single threshold group key")
|
|
ErrDecryptionFailed = errors.New("ML-KEM decryption failed")
|
|
ErrInvalidCiphertext = errors.New("invalid ciphertext")
|
|
)
|
|
|
|
type Signature interface {
|
|
fmt.Stringer
|
|
|
|
// NumSigners is the number of [bls.PublicKeys] that participated in the
|
|
// [Signature]. This is exposed because users of these signatures typically
|
|
// impose a verification fee that is a function of the number of
|
|
// signers.
|
|
NumSigners() (int, error)
|
|
|
|
// Verify that this signature was signed by at least [quorumNum]/[quorumDen]
|
|
// of the validators of [msg.SourceChainID] at [pChainHeight].
|
|
//
|
|
// Invariant: [msg] is correctly initialized.
|
|
Verify(
|
|
msg *UnsignedMessage,
|
|
networkID uint32,
|
|
validators CanonicalValidatorSet,
|
|
quorumNum uint64,
|
|
quorumDen uint64,
|
|
) error
|
|
}
|
|
|
|
type BitSetSignature struct {
|
|
// Signers is a big-endian byte slice encoding which validators signed this
|
|
// message.
|
|
Signers []byte `serialize:"true"`
|
|
Signature [bls.SignatureLen]byte `serialize:"true"`
|
|
}
|
|
|
|
func (s *BitSetSignature) NumSigners() (int, error) {
|
|
// Parse signer bit vector
|
|
//
|
|
// We assert that the length of [signerIndices.Bytes()] is equal
|
|
// to [len(s.Signers)] to ensure that [s.Signers] does not have
|
|
// any unnecessary zero-padding to represent the [set.Bits].
|
|
signerIndices := set.BitsFromBytes(s.Signers)
|
|
if len(signerIndices.Bytes()) != len(s.Signers) {
|
|
return 0, ErrInvalidBitSet
|
|
}
|
|
return signerIndices.Len(), nil
|
|
}
|
|
|
|
func (s *BitSetSignature) Verify(
|
|
msg *UnsignedMessage,
|
|
networkID uint32,
|
|
validators CanonicalValidatorSet,
|
|
quorumNum uint64,
|
|
quorumDen uint64,
|
|
) error {
|
|
if msg.NetworkID != networkID {
|
|
return ErrWrongNetworkID
|
|
}
|
|
|
|
// Parse signer bit vector
|
|
//
|
|
// We assert that the length of [signerIndices.Bytes()] is equal
|
|
// to [len(s.Signers)] to ensure that [s.Signers] does not have
|
|
// any unnecessary zero-padding to represent the [set.Bits].
|
|
signerIndices := set.BitsFromBytes(s.Signers)
|
|
if len(signerIndices.Bytes()) != len(s.Signers) {
|
|
return ErrInvalidBitSet
|
|
}
|
|
|
|
// Get the validators that (allegedly) signed the message.
|
|
signers, err := FilterValidators(signerIndices, validators.Validators)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Because [signers] is a subset of [validators.Validators], this can never error.
|
|
sigWeight, _ := SumWeight(signers)
|
|
|
|
// Make sure the signature's weight is sufficient.
|
|
err = VerifyWeight(
|
|
sigWeight,
|
|
validators.TotalWeight,
|
|
quorumNum,
|
|
quorumDen,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse the aggregate signature
|
|
aggSig, err := bls.SignatureFromBytes(s.Signature[:])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrParseSignature, err)
|
|
}
|
|
|
|
// Create the aggregate public key
|
|
aggPubKey, err := AggregatePublicKeys(signers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify the signature
|
|
unsignedBytes := msg.Bytes()
|
|
if !bls.Verify(aggPubKey, aggSig, unsignedBytes) {
|
|
return ErrInvalidSignature
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *BitSetSignature) String() string {
|
|
return fmt.Sprintf("BitSetSignature(Signers = %x, Signature = %x)", s.Signers, s.Signature)
|
|
}
|
|
|
|
// VerifyWeight returns [nil] if [sigWeight] is at least [quorumNum]/[quorumDen]
|
|
// of [totalWeight].
|
|
// If [sigWeight >= totalWeight * quorumNum / quorumDen] then return [nil]
|
|
func VerifyWeight(
|
|
sigWeight uint64,
|
|
totalWeight uint64,
|
|
quorumNum uint64,
|
|
quorumDen uint64,
|
|
) error {
|
|
// Verifies that quorumNum * totalWeight <= quorumDen * sigWeight
|
|
scaledTotalWeight := new(big.Int).SetUint64(totalWeight)
|
|
scaledTotalWeight.Mul(scaledTotalWeight, new(big.Int).SetUint64(quorumNum))
|
|
scaledSigWeight := new(big.Int).SetUint64(sigWeight)
|
|
scaledSigWeight.Mul(scaledSigWeight, new(big.Int).SetUint64(quorumDen))
|
|
if scaledTotalWeight.Cmp(scaledSigWeight) == 1 {
|
|
return fmt.Errorf(
|
|
"%w: %d*%d > %d*%d",
|
|
ErrInsufficientWeight,
|
|
quorumNum,
|
|
totalWeight,
|
|
quorumDen,
|
|
sigWeight,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Warp 1.5: Corona Signature (Post-Quantum Safe, replaces BLS)
|
|
// =============================================================================
|
|
|
|
// Corona is a lattice-based threshold signature scheme from LWE
|
|
// Paper: https://eprint.iacr.org/2024/1113
|
|
// Implementation: github.com/luxfi/corona
|
|
//
|
|
// Key properties:
|
|
// - Post-quantum secure (based on LWE hardness)
|
|
// - Native threshold support (t-of-n signing in 2 rounds)
|
|
// - Ring-LWE with NTT-friendly prime Q = 0x1000000004A01 (48-bit)
|
|
// - Parameters: M=8, N=7, Dbar=48, Kappa=23
|
|
|
|
// Corona signature constants (from github.com/luxfi/corona/sign/config.go)
|
|
const (
|
|
// CoronaQ is the NTT-friendly prime modulus
|
|
CoronaQ = 0x1000000004A01 // 48-bit prime
|
|
|
|
// CoronaM is the matrix dimension M
|
|
CoronaM = 8
|
|
|
|
// CoronaN is the matrix dimension N
|
|
CoronaN = 7
|
|
|
|
// CoronaKappa is the hash output bound
|
|
CoronaKappa = 23
|
|
|
|
// CoronaDbar is the signature dimension
|
|
CoronaDbar = 48
|
|
|
|
// CoronaKeySize is the symmetric key size in bytes (256 bits)
|
|
CoronaKeySize = 32
|
|
)
|
|
|
|
// ML-KEM security levels per FIPS 203
|
|
const (
|
|
// MLKEM768CiphertextLen is ML-KEM-768 ciphertext length (NIST Level 3) - DEFAULT
|
|
MLKEM768CiphertextLen = 1088
|
|
|
|
// MLKEM768PublicKeyLen is ML-KEM-768 public key length
|
|
MLKEM768PublicKeyLen = 1184
|
|
|
|
// MLKEM768SharedSecretLen is the shared secret length
|
|
MLKEM768SharedSecretLen = 32
|
|
|
|
// MLKEM1024CiphertextLen is ML-KEM-1024 ciphertext length (NIST Level 5)
|
|
MLKEM1024CiphertextLen = 1568
|
|
|
|
// AESGCMNonceLen is the nonce length for AES-256-GCM
|
|
AESGCMNonceLen = 12
|
|
|
|
// AESGCMTagLen is the authentication tag length
|
|
AESGCMTagLen = 16
|
|
)
|
|
|
|
// CoronaSignature is the Warp 1.5 quantum-safe signature type.
|
|
// This is the recommended signature type for all new Warp messages.
|
|
// It uses Corona (LWE-based) threshold signatures for post-quantum security.
|
|
//
|
|
// Corona properties:
|
|
// - Native threshold support (no need for separate TSS layer)
|
|
// - Two-round signing protocol
|
|
// - Post-quantum secure (based on LWE hardness)
|
|
// - Paper: https://eprint.iacr.org/2024/1113
|
|
//
|
|
// Replaces: BitSetSignature (BLS), HybridBLSCoronaSignature
|
|
// Security: Post-quantum secure (LWE-based)
|
|
// Size: Variable based on threshold parameters
|
|
type CoronaSignature struct {
|
|
// Signers is a big-endian byte slice encoding which validators signed
|
|
Signers []byte `serialize:"true"`
|
|
|
|
// Signature is the Corona threshold signature
|
|
// Contains: c (challenge polynomial), z (response vector), Delta (hint vector)
|
|
// Size depends on threshold parameters (M, N, Dbar, Kappa)
|
|
Signature []byte `serialize:"true"`
|
|
}
|
|
|
|
// NumSigners returns the number of validators that participated in signing
|
|
func (s *CoronaSignature) NumSigners() (int, error) {
|
|
signerIndices := set.BitsFromBytes(s.Signers)
|
|
if len(signerIndices.Bytes()) != len(s.Signers) {
|
|
return 0, ErrInvalidBitSet
|
|
}
|
|
return signerIndices.Len(), nil
|
|
}
|
|
|
|
// Verify validates the Corona (ML-DSA) threshold signature
|
|
func (s *CoronaSignature) Verify(
|
|
msg *UnsignedMessage,
|
|
networkID uint32,
|
|
validators CanonicalValidatorSet,
|
|
quorumNum uint64,
|
|
quorumDen uint64,
|
|
) error {
|
|
if msg.NetworkID != networkID {
|
|
return ErrWrongNetworkID
|
|
}
|
|
|
|
// Parse signer bit vector
|
|
signerIndices := set.BitsFromBytes(s.Signers)
|
|
if len(signerIndices.Bytes()) != len(s.Signers) {
|
|
return ErrInvalidBitSet
|
|
}
|
|
|
|
// Get the validators that (allegedly) signed the message
|
|
signers, err := FilterValidators(signerIndices, validators.Validators)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify signer weight meets quorum
|
|
sigWeight, _ := SumWeight(signers)
|
|
if err := VerifyWeight(sigWeight, validators.TotalWeight, quorumNum, quorumDen); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Collect Corona public keys from signers
|
|
rtPubKeys := make([][]byte, 0, len(signers))
|
|
for _, signer := range signers {
|
|
if len(signer.CoronaPubKey) == 0 {
|
|
return fmt.Errorf("%w: validator missing Corona key", ErrMissingCoronaPublicKey)
|
|
}
|
|
rtPubKeys = append(rtPubKeys, signer.CoronaPubKey)
|
|
}
|
|
|
|
// Aggregate the Corona public keys for threshold verification
|
|
aggregatedPK, err := AggregateCoronaPublicKeys(rtPubKeys)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to aggregate Corona public keys: %w", err)
|
|
}
|
|
|
|
// Verify the Corona (LWE-based) signature
|
|
if !VerifyCoronaSignature(aggregatedPK, msg.Bytes(), s.Signature) {
|
|
return ErrInvalidCoronaSignature
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CoronaSignature) String() string {
|
|
return fmt.Sprintf("CoronaSignature(Signers = %x, Sig = %x...)",
|
|
s.Signers, s.Signature[:min(32, len(s.Signature))])
|
|
}
|
|
|
|
// =============================================================================
|
|
// Warp 1.5: Encrypted Payload (ML-KEM + AES-256-GCM)
|
|
// =============================================================================
|
|
|
|
// EncryptedWarpPayload provides quantum-safe encryption for confidential
|
|
// cross-chain messages using ML-KEM (FIPS 203) key encapsulation.
|
|
//
|
|
// Use cases:
|
|
// - Private bridge transfers (hidden amounts/recipients)
|
|
// - Sealed-bid cross-chain auctions
|
|
// - Confidential governance votes
|
|
// - MEV protection (encrypt intent until committed)
|
|
//
|
|
// Encryption: ML-KEM-768 (NIST Level 3) + AES-256-GCM
|
|
// Security: 192-bit post-quantum for key exchange, 256-bit symmetric
|
|
type EncryptedWarpPayload struct {
|
|
// EncapsulatedKey is the ML-KEM ciphertext containing the encapsulated shared secret
|
|
// Size: 1088 bytes for ML-KEM-768
|
|
EncapsulatedKey []byte `serialize:"true"`
|
|
|
|
// Nonce is the AES-GCM nonce (12 bytes)
|
|
Nonce []byte `serialize:"true"`
|
|
|
|
// Ciphertext is the AES-256-GCM encrypted payload
|
|
// Includes 16-byte authentication tag at the end
|
|
Ciphertext []byte `serialize:"true"`
|
|
|
|
// RecipientKeyID identifies which ML-KEM public key was used
|
|
// This allows recipients to know which private key to use for decryption
|
|
RecipientKeyID []byte `serialize:"true"`
|
|
}
|
|
|
|
// Encrypt creates an encrypted Warp payload using ML-KEM + AES-256-GCM
|
|
//
|
|
// Parameters:
|
|
// - plaintext: The message to encrypt
|
|
// - recipientPubKey: The recipient's ML-KEM-768 public key
|
|
// - recipientKeyID: Identifier for the recipient's key (e.g., hash of pubkey)
|
|
//
|
|
// Returns:
|
|
// - EncryptedWarpPayload containing the encrypted message
|
|
// - error if encryption fails
|
|
func EncryptWarpPayload(plaintext []byte, recipientPubKey []byte, recipientKeyID []byte) (*EncryptedWarpPayload, error) {
|
|
// Validate recipient public key length
|
|
if len(recipientPubKey) != MLKEM768PublicKeyLen {
|
|
return nil, fmt.Errorf("invalid ML-KEM public key length: got %d, expected %d",
|
|
len(recipientPubKey), MLKEM768PublicKeyLen)
|
|
}
|
|
|
|
// ML-KEM Encapsulation: generate shared secret and ciphertext
|
|
sharedSecret, encapsulatedKey, err := mlkemEncapsulate(recipientPubKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ML-KEM encapsulation failed: %w", err)
|
|
}
|
|
|
|
// Generate random nonce for AES-GCM
|
|
nonce, err := generateSecureRandom(AESGCMNonceLen)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate nonce: %w", err)
|
|
}
|
|
|
|
// Encrypt plaintext with AES-256-GCM using the shared secret as the key
|
|
ciphertext, err := aesGCMEncrypt(sharedSecret, nonce, plaintext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("AES-GCM encryption failed: %w", err)
|
|
}
|
|
|
|
return &EncryptedWarpPayload{
|
|
EncapsulatedKey: encapsulatedKey,
|
|
Nonce: nonce,
|
|
Ciphertext: ciphertext,
|
|
RecipientKeyID: recipientKeyID,
|
|
}, nil
|
|
}
|
|
|
|
// Decrypt decrypts an encrypted Warp payload using the recipient's ML-KEM private key
|
|
//
|
|
// Parameters:
|
|
// - recipientPrivKey: The recipient's ML-KEM-768 private key
|
|
//
|
|
// Returns:
|
|
// - The decrypted plaintext
|
|
// - error if decryption fails (wrong key, tampered ciphertext, etc.)
|
|
func (e *EncryptedWarpPayload) Decrypt(recipientPrivKey []byte) ([]byte, error) {
|
|
// Validate ciphertext structure
|
|
if len(e.EncapsulatedKey) != MLKEM768CiphertextLen {
|
|
return nil, fmt.Errorf("%w: invalid encapsulated key length", ErrInvalidCiphertext)
|
|
}
|
|
if len(e.Nonce) != AESGCMNonceLen {
|
|
return nil, fmt.Errorf("%w: invalid nonce length", ErrInvalidCiphertext)
|
|
}
|
|
if len(e.Ciphertext) < AESGCMTagLen {
|
|
return nil, fmt.Errorf("%w: ciphertext too short", ErrInvalidCiphertext)
|
|
}
|
|
|
|
// ML-KEM Decapsulation: recover shared secret from encapsulated key
|
|
sharedSecret, err := mlkemDecapsulate(recipientPrivKey, e.EncapsulatedKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: ML-KEM decapsulation failed: %v", ErrDecryptionFailed, err)
|
|
}
|
|
|
|
// Decrypt ciphertext with AES-256-GCM
|
|
plaintext, err := aesGCMDecrypt(sharedSecret, e.Nonce, e.Ciphertext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: AES-GCM decryption failed: %v", ErrDecryptionFailed, err)
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// Size returns the total size of the encrypted payload in bytes
|
|
func (e *EncryptedWarpPayload) Size() int {
|
|
return len(e.EncapsulatedKey) + len(e.Nonce) + len(e.Ciphertext) + len(e.RecipientKeyID)
|
|
}
|
|
|
|
func (e *EncryptedWarpPayload) String() string {
|
|
return fmt.Sprintf("EncryptedWarpPayload(KeyID = %x, Size = %d bytes)",
|
|
e.RecipientKeyID[:min(8, len(e.RecipientKeyID))], e.Size())
|
|
}
|
|
|
|
// =============================================================================
|
|
// ML-KEM Cryptographic Primitives (FIPS 203)
|
|
// =============================================================================
|
|
|
|
// mlkemEncapsulate performs ML-KEM-768 encapsulation
|
|
// Returns: (sharedSecret, ciphertext, error)
|
|
func mlkemEncapsulate(publicKey []byte) ([]byte, []byte, error) {
|
|
if len(publicKey) != MLKEM768PublicKeyLen {
|
|
return nil, nil, fmt.Errorf("invalid public key length: got %d, expected %d",
|
|
len(publicKey), MLKEM768PublicKeyLen)
|
|
}
|
|
|
|
// Get the ML-KEM-768 scheme
|
|
scheme := mlkem768.Scheme()
|
|
|
|
// Unmarshal the public key
|
|
pk, err := scheme.UnmarshalBinaryPublicKey(publicKey)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to unmarshal public key: %w", err)
|
|
}
|
|
|
|
// Encapsulate to generate shared secret and ciphertext
|
|
ciphertext, sharedSecret, err := scheme.Encapsulate(pk)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("ML-KEM encapsulation failed: %w", err)
|
|
}
|
|
|
|
return sharedSecret, ciphertext, nil
|
|
}
|
|
|
|
// mlkemDecapsulate performs ML-KEM-768 decapsulation
|
|
// Returns: (sharedSecret, error)
|
|
func mlkemDecapsulate(privateKey []byte, ciphertext []byte) ([]byte, error) {
|
|
if len(ciphertext) != MLKEM768CiphertextLen {
|
|
return nil, fmt.Errorf("invalid ciphertext length: got %d, expected %d",
|
|
len(ciphertext), MLKEM768CiphertextLen)
|
|
}
|
|
|
|
// Get the ML-KEM-768 scheme
|
|
scheme := mlkem768.Scheme()
|
|
|
|
// Unmarshal the private key
|
|
sk, err := scheme.UnmarshalBinaryPrivateKey(privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal private key: %w", err)
|
|
}
|
|
|
|
// Decapsulate to recover the shared secret
|
|
sharedSecret, err := scheme.Decapsulate(sk, ciphertext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ML-KEM decapsulation failed: %w", err)
|
|
}
|
|
|
|
return sharedSecret, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// AES-256-GCM Cryptographic Primitives
|
|
// =============================================================================
|
|
|
|
// aesGCMEncrypt encrypts plaintext using AES-256-GCM
|
|
func aesGCMEncrypt(key []byte, nonce []byte, plaintext []byte) ([]byte, error) {
|
|
if len(key) != 32 {
|
|
return nil, errors.New("AES-256 requires 32-byte key")
|
|
}
|
|
if len(nonce) != AESGCMNonceLen {
|
|
return nil, errors.New("AES-GCM requires 12-byte nonce")
|
|
}
|
|
|
|
// Create AES cipher
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
|
}
|
|
|
|
// Create GCM mode
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
// Encrypt and authenticate
|
|
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
|
return ciphertext, nil
|
|
}
|
|
|
|
// aesGCMDecrypt decrypts ciphertext using AES-256-GCM
|
|
func aesGCMDecrypt(key []byte, nonce []byte, ciphertext []byte) ([]byte, error) {
|
|
if len(key) != 32 {
|
|
return nil, errors.New("AES-256 requires 32-byte key")
|
|
}
|
|
if len(nonce) != AESGCMNonceLen {
|
|
return nil, errors.New("AES-GCM requires 12-byte nonce")
|
|
}
|
|
if len(ciphertext) < AESGCMTagLen {
|
|
return nil, errors.New("ciphertext too short")
|
|
}
|
|
|
|
// Create AES cipher
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
|
}
|
|
|
|
// Create GCM mode
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
// Decrypt and verify authentication
|
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("authentication failed: %w", err)
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// generateSecureRandom generates cryptographically secure random bytes
|
|
func generateSecureRandom(length int) ([]byte, error) {
|
|
b := make([]byte, length)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate random bytes: %w", err)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// =============================================================================
|
|
// Hybrid BLS+Corona Signature (DEPRECATED - use CoronaSignature)
|
|
// =============================================================================
|
|
|
|
// CoronaSignatureLen is a placeholder for legacy compatibility.
|
|
// Actual Corona signature size depends on threshold parameters.
|
|
// Corona uses LWE-based signatures, not ML-DSA.
|
|
// See: https://eprint.iacr.org/2024/1113
|
|
//
|
|
// Deprecated: Use CoronaSignature type which handles variable-length signatures.
|
|
const CoronaSignatureLen = 3309
|
|
|
|
// HybridBLSCoronaSignature implements a quantum-safe hybrid signature combining:
|
|
// - BLS aggregate signatures (classical security, compact)
|
|
// - Corona lattice signatures (post-quantum security, larger)
|
|
//
|
|
// Both signatures MUST verify for the message to be considered valid.
|
|
// This provides security against both classical and quantum attackers.
|
|
//
|
|
// Migration path:
|
|
// 1. Pre-quantum: BLS-only (BitSetSignature)
|
|
// 2. Transition: HybridBLSCoronaSignature (both required)
|
|
// 3. Post-quantum: Corona-only (future)
|
|
//
|
|
// Wire format: this type is the 4th type registered in the warp codec
|
|
// (codec.go), which assigns it codec typeID 0x03. The typeID, field
|
|
// order, and field encodings are part of the on-chain wire format and
|
|
// MUST NOT change. The Go-side type name is metadata only and was renamed
|
|
// from HybridBLSRTSignature in the threshold sweep; wire bytes are
|
|
// byte-equal pre/post rename.
|
|
type HybridBLSCoronaSignature struct {
|
|
// Signers is a big-endian byte slice encoding which validators signed
|
|
Signers []byte `serialize:"true"`
|
|
|
|
// BLSSignature is the aggregated BLS signature (96 bytes)
|
|
BLSSignature [bls.SignatureLen]byte `serialize:"true"`
|
|
|
|
// CoronaSignature is the aggregated Corona lattice signature
|
|
// Uses threshold signing to produce a single combined signature
|
|
CoronaSignature []byte `serialize:"true"`
|
|
|
|
// CoronaPublicKeys contains the Corona public keys for each signer
|
|
// in the same order as indicated by the Signers bitset
|
|
// This is needed because validators may have different Corona keys than BLS keys
|
|
CoronaPublicKeys [][]byte `serialize:"true"`
|
|
}
|
|
|
|
// NumSigners returns the number of validators that participated in signing
|
|
func (s *HybridBLSCoronaSignature) NumSigners() (int, error) {
|
|
signerIndices := set.BitsFromBytes(s.Signers)
|
|
if len(signerIndices.Bytes()) != len(s.Signers) {
|
|
return 0, ErrInvalidBitSet
|
|
}
|
|
return signerIndices.Len(), nil
|
|
}
|
|
|
|
// Verify validates both BLS and Corona signatures
|
|
// Both MUST be valid for the hybrid signature to be accepted
|
|
func (s *HybridBLSCoronaSignature) Verify(
|
|
msg *UnsignedMessage,
|
|
networkID uint32,
|
|
validators CanonicalValidatorSet,
|
|
quorumNum uint64,
|
|
quorumDen uint64,
|
|
) error {
|
|
if msg.NetworkID != networkID {
|
|
return ErrWrongNetworkID
|
|
}
|
|
|
|
// Parse signer bit vector
|
|
signerIndices := set.BitsFromBytes(s.Signers)
|
|
if len(signerIndices.Bytes()) != len(s.Signers) {
|
|
return ErrInvalidBitSet
|
|
}
|
|
|
|
// Get the validators that (allegedly) signed the message
|
|
signers, err := FilterValidators(signerIndices, validators.Validators)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify signer weight meets quorum
|
|
sigWeight, _ := SumWeight(signers)
|
|
if err := VerifyWeight(sigWeight, validators.TotalWeight, quorumNum, quorumDen); err != nil {
|
|
return err
|
|
}
|
|
|
|
// === BLS Signature Verification ===
|
|
if err := s.verifyBLS(msg, signers); err != nil {
|
|
return fmt.Errorf("BLS verification failed: %w", err)
|
|
}
|
|
|
|
// === Corona Signature Verification ===
|
|
if err := s.verifyCorona(msg, signers); err != nil {
|
|
return fmt.Errorf("Corona verification failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// verifyBLS verifies the BLS aggregate signature
|
|
func (s *HybridBLSCoronaSignature) verifyBLS(msg *UnsignedMessage, signers []*Validator) error {
|
|
// Parse the aggregate BLS signature
|
|
aggSig, err := bls.SignatureFromBytes(s.BLSSignature[:])
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %w", ErrParseSignature, err)
|
|
}
|
|
|
|
// Create the aggregate public key
|
|
aggPubKey, err := AggregatePublicKeys(signers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify the BLS signature
|
|
unsignedBytes := msg.Bytes()
|
|
if !bls.Verify(aggPubKey, aggSig, unsignedBytes) {
|
|
return ErrInvalidSignature
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// verifyCorona verifies the Corona lattice-based signature
|
|
func (s *HybridBLSCoronaSignature) verifyCorona(msg *UnsignedMessage, signers []*Validator) error {
|
|
// Validate we have Corona public keys for all signers
|
|
if len(s.CoronaPublicKeys) != len(signers) {
|
|
return fmt.Errorf("%w: got %d keys, expected %d",
|
|
ErrMissingCoronaPublicKey, len(s.CoronaPublicKeys), len(signers))
|
|
}
|
|
|
|
// Validate Corona signature is present
|
|
if len(s.CoronaSignature) == 0 {
|
|
return ErrInvalidCoronaSignature
|
|
}
|
|
|
|
// Verification keys come from the validator records, and the keys the
|
|
// message carries must equal them. A key read out of the signature being
|
|
// checked would let the signature's author pick the key it is checked
|
|
// against, which leaves the post-quantum leg authenticating nothing.
|
|
registered := make([][]byte, len(signers))
|
|
for i, signer := range signers {
|
|
if len(signer.CoronaPubKey) == 0 {
|
|
return fmt.Errorf("%w: signer %d", ErrMissingCoronaPublicKey, i)
|
|
}
|
|
if !bytes.Equal(s.CoronaPublicKeys[i], signer.CoronaPubKey) {
|
|
return fmt.Errorf("%w: signer %d", ErrCoronaPublicKeyMismatch, i)
|
|
}
|
|
registered[i] = signer.CoronaPubKey
|
|
}
|
|
|
|
// Aggregate the Corona public keys
|
|
aggregatedCoronaPK, err := AggregateCoronaPublicKeys(registered)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to aggregate Corona public keys: %w", err)
|
|
}
|
|
|
|
// Verify the Corona signature
|
|
unsignedBytes := msg.Bytes()
|
|
if !VerifyCoronaSignature(aggregatedCoronaPK, unsignedBytes, s.CoronaSignature) {
|
|
return ErrInvalidCoronaSignature
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *HybridBLSCoronaSignature) String() string {
|
|
return fmt.Sprintf("HybridBLSCoronaSignature(Signers = %x, BLS = %x, Corona = %x)",
|
|
s.Signers, s.BLSSignature, s.CoronaSignature[:min(32, len(s.CoronaSignature))])
|
|
}
|
|
|
|
// =============================================================================
|
|
// Corona Signature Functions
|
|
// =============================================================================
|
|
|
|
// AggregateCoronaPublicKeys aggregates multiple Corona public keys
|
|
// into a single combined public key for threshold verification.
|
|
// This uses the threshold package's SchemeCorona.
|
|
func AggregateCoronaPublicKeys(publicKeys [][]byte) ([]byte, error) {
|
|
if len(publicKeys) == 0 {
|
|
return nil, errors.New("no public keys to aggregate")
|
|
}
|
|
|
|
// Every share of a Corona threshold key verifies against the one group
|
|
// public key, and the aggregate returned below is publicKeys[0]. Keys that
|
|
// differ therefore have no aggregate: returning the first would verify one
|
|
// signer's key while the caller's quorum tally counted all of them.
|
|
for i, pk := range publicKeys[1:] {
|
|
if !bytes.Equal(pk, publicKeys[0]) {
|
|
return nil, fmt.Errorf("%w: key %d differs from key 0", ErrCoronaGroupKeyMismatch, i+1)
|
|
}
|
|
}
|
|
|
|
// Get the Corona threshold scheme
|
|
if !threshold.HasScheme(threshold.SchemeCorona) {
|
|
return nil, errors.New("Corona threshold scheme is not registered")
|
|
}
|
|
|
|
scheme, err := threshold.GetScheme(threshold.SchemeCorona)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get Corona scheme: %w", err)
|
|
}
|
|
|
|
// Parse all public keys using the threshold scheme
|
|
parsedKeys := make([]threshold.PublicKey, len(publicKeys))
|
|
for i, pk := range publicKeys {
|
|
parsed, err := scheme.ParsePublicKey(pk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse public key %d: %w", i, err)
|
|
}
|
|
parsedKeys[i] = parsed
|
|
}
|
|
|
|
// For threshold signatures, the "aggregated" public key is typically
|
|
// just the group public key that all shares were generated from.
|
|
// In Corona threshold protocol, all signers share the same group key,
|
|
// so we can use any of the parsed keys directly.
|
|
// The actual threshold verification is done by the scheme's verifier.
|
|
//
|
|
// Return the first public key as the aggregated key since they should
|
|
// all represent the same group public key in a proper threshold setup.
|
|
return parsedKeys[0].Bytes(), nil
|
|
}
|
|
|
|
// VerifyCoronaSignature verifies a Corona lattice-based signature.
|
|
// This uses the threshold package's SchemeCorona verifier.
|
|
func VerifyCoronaSignature(publicKey []byte, message []byte, signature []byte) bool {
|
|
// Basic sanity checks
|
|
if len(publicKey) < 32 || len(signature) < 64 || len(message) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Get the Corona threshold scheme
|
|
if !threshold.HasScheme(threshold.SchemeCorona) {
|
|
// Corona scheme must be registered for production use
|
|
// This should be done via: import _ "github.com/luxfi/crypto/threshold/corona"
|
|
return false
|
|
}
|
|
|
|
scheme, err := threshold.GetScheme(threshold.SchemeCorona)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Parse the public key
|
|
pk, err := scheme.ParsePublicKey(publicKey)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Create a verifier for the public key
|
|
verifier, err := scheme.NewVerifier(pk)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Verify using the threshold verifier
|
|
// This performs full lattice-based signature verification
|
|
return verifier.VerifyBytes(message, signature)
|
|
}
|
|
|
|
// min returns the smaller of two integers
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|