mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
161 lines
4.6 KiB
Go
161 lines
4.6 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package staking
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/luxfi/crypto/mldsa"
|
|
"github.com/luxfi/filesystem/perms"
|
|
)
|
|
|
|
var (
|
|
ErrMLDSAVerificationFailure = errors.New("staking: ML-DSA verification failure")
|
|
ErrInvalidPQKeySize = errors.New("staking: invalid post-quantum key size")
|
|
)
|
|
|
|
// PQKeyPair represents a post-quantum ML-DSA key pair for hybrid staking
|
|
type PQKeyPair struct {
|
|
PrivateKey *mldsa.PrivateKey
|
|
PublicKey *mldsa.PublicKey
|
|
Mode mldsa.Mode
|
|
}
|
|
|
|
// NewPQKeyPair generates a new ML-DSA-65 key pair (NIST Level 3, 192-bit security)
|
|
func NewPQKeyPair() (*PQKeyPair, error) {
|
|
priv, err := mldsa.GenerateKey(rand.Reader, mldsa.MLDSA65)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate ML-DSA key: %w", err)
|
|
}
|
|
return &PQKeyPair{
|
|
PrivateKey: priv,
|
|
PublicKey: priv.PublicKey,
|
|
Mode: mldsa.MLDSA65,
|
|
}, nil
|
|
}
|
|
|
|
// Sign creates an ML-DSA signature over the given message
|
|
func (kp *PQKeyPair) Sign(msg []byte) ([]byte, error) {
|
|
return kp.PrivateKey.Sign(rand.Reader, msg, nil)
|
|
}
|
|
|
|
// Verify checks an ML-DSA signature
|
|
func (kp *PQKeyPair) Verify(msg, sig []byte) bool {
|
|
return kp.PublicKey.VerifySignature(msg, sig)
|
|
}
|
|
|
|
// PublicKeyBytes returns the serialized public key
|
|
func (kp *PQKeyPair) PublicKeyBytes() []byte {
|
|
return kp.PublicKey.Bytes()
|
|
}
|
|
|
|
// PrivateKeyBytes returns the serialized private key
|
|
func (kp *PQKeyPair) PrivateKeyBytes() []byte {
|
|
return kp.PrivateKey.Bytes()
|
|
}
|
|
|
|
// InitNodePQKeyPair generates and stores an ML-DSA key pair for hybrid staking.
|
|
// The keys will be placed at [keyPath] and [pubKeyPath].
|
|
// If there is already a file at [keyPath], returns nil (no-op).
|
|
func InitNodePQKeyPair(keyPath, pubKeyPath string) error {
|
|
// If there is already a file at [keyPath], do nothing
|
|
if _, err := os.Stat(keyPath); !os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
|
|
kp, err := NewPQKeyPair()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure directories exist
|
|
if err := os.MkdirAll(filepath.Dir(keyPath), perms.ReadWriteExecute); err != nil {
|
|
return fmt.Errorf("couldn't create path for PQ key: %w", err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(pubKeyPath), perms.ReadWriteExecute); err != nil {
|
|
return fmt.Errorf("couldn't create path for PQ public key: %w", err)
|
|
}
|
|
|
|
// Write private key
|
|
var keyBuff bytes.Buffer
|
|
if err := pem.Encode(&keyBuff, &pem.Block{
|
|
Type: "ML-DSA-65 PRIVATE KEY",
|
|
Bytes: kp.PrivateKeyBytes(),
|
|
}); err != nil {
|
|
return fmt.Errorf("couldn't encode PQ private key: %w", err)
|
|
}
|
|
if err := os.WriteFile(keyPath, keyBuff.Bytes(), perms.ReadOnly); err != nil {
|
|
return fmt.Errorf("couldn't write PQ private key: %w", err)
|
|
}
|
|
|
|
// Write public key
|
|
var pubBuff bytes.Buffer
|
|
if err := pem.Encode(&pubBuff, &pem.Block{
|
|
Type: "ML-DSA-65 PUBLIC KEY",
|
|
Bytes: kp.PublicKeyBytes(),
|
|
}); err != nil {
|
|
return fmt.Errorf("couldn't encode PQ public key: %w", err)
|
|
}
|
|
if err := os.WriteFile(pubKeyPath, pubBuff.Bytes(), perms.ReadOnly); err != nil {
|
|
return fmt.Errorf("couldn't write PQ public key: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadPQKeyPair loads an ML-DSA key pair from PEM files
|
|
func LoadPQKeyPair(keyPath, pubKeyPath string) (*PQKeyPair, error) {
|
|
// Load private key
|
|
keyPEM, err := os.ReadFile(keyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't read PQ private key: %w", err)
|
|
}
|
|
keyBlock, _ := pem.Decode(keyPEM)
|
|
if keyBlock == nil || keyBlock.Type != "ML-DSA-65 PRIVATE KEY" {
|
|
return nil, errors.New("failed to decode PQ private key PEM")
|
|
}
|
|
priv, err := mldsa.PrivateKeyFromBytes(mldsa.MLDSA65, keyBlock.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't parse PQ private key: %w", err)
|
|
}
|
|
|
|
// Load public key
|
|
pubPEM, err := os.ReadFile(pubKeyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't read PQ public key: %w", err)
|
|
}
|
|
pubBlock, _ := pem.Decode(pubPEM)
|
|
if pubBlock == nil || pubBlock.Type != "ML-DSA-65 PUBLIC KEY" {
|
|
return nil, errors.New("failed to decode PQ public key PEM")
|
|
}
|
|
pub, err := mldsa.PublicKeyFromBytes(pubBlock.Bytes, mldsa.MLDSA65)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't parse PQ public key: %w", err)
|
|
}
|
|
|
|
return &PQKeyPair{
|
|
PrivateKey: priv,
|
|
PublicKey: pub,
|
|
Mode: mldsa.MLDSA65,
|
|
}, nil
|
|
}
|
|
|
|
// VerifyPQSignature verifies an ML-DSA signature given a public key
|
|
func VerifyPQSignature(pubKeyBytes, msg, sig []byte) error {
|
|
pub, err := mldsa.PublicKeyFromBytes(pubKeyBytes, mldsa.MLDSA65)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid PQ public key: %w", err)
|
|
}
|
|
if !pub.VerifySignature(msg, sig) {
|
|
return ErrMLDSAVerificationFailure
|
|
}
|
|
return nil
|
|
}
|