mirror of
https://github.com/luxfi/lamport.git
synced 2026-10-30 16:21:45 +00:00
302 lines
8.9 KiB
Go
302 lines
8.9 KiB
Go
// Package primitives provides core Lamport one-time signature types and operations.
|
|
//
|
|
// Lamport signatures are hash-based and provide quantum resistance using only
|
|
// Keccak-256 - no special cryptographic assumptions beyond hash function security.
|
|
//
|
|
// SECURITY: Each Lamport key pair MUST only be used to sign ONE message.
|
|
// Signing two messages with the same key reveals half of the private key,
|
|
// potentially allowing forgery.
|
|
//
|
|
// See: LP-4105 (Lamport OTS for Lux Safe)
|
|
package primitives
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
|
|
"golang.org/x/crypto/sha3"
|
|
)
|
|
|
|
const (
|
|
// KeyBits is the number of bits in the message (256 for keccak256 output)
|
|
KeyBits = 256
|
|
|
|
// PreimageSize is the size of each private key preimage (32 bytes)
|
|
PreimageSize = 32
|
|
|
|
// HashSize is the size of keccak256 output (32 bytes)
|
|
HashSize = 32
|
|
|
|
// PrivateKeySize is the total size of a Lamport private key
|
|
// 256 bits * 2 sides * 32 bytes = 16,384 bytes
|
|
PrivateKeySize = KeyBits * 2 * PreimageSize
|
|
|
|
// PublicKeySize is the total size of a Lamport public key
|
|
// 256 bits * 2 sides * 32 bytes = 16,384 bytes
|
|
PublicKeySize = KeyBits * 2 * HashSize
|
|
|
|
// SignatureSize is the size of a Lamport signature
|
|
// 256 revealed preimages * 32 bytes = 8,192 bytes
|
|
SignatureSize = KeyBits * PreimageSize
|
|
|
|
// PublicKeyHashSize is 32 bytes (keccak256 of public key)
|
|
PublicKeyHashSize = 32
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidPublicKey indicates the public key format is invalid
|
|
ErrInvalidPublicKey = errors.New("lamport: invalid public key")
|
|
|
|
// ErrInvalidSignature indicates the signature format is invalid
|
|
ErrInvalidSignature = errors.New("lamport: invalid signature")
|
|
|
|
// ErrKeyAlreadyUsed indicates an attempt to reuse a one-time key
|
|
ErrKeyAlreadyUsed = errors.New("lamport: key already used (one-time property violated)")
|
|
|
|
// ErrVerificationFailed indicates signature verification failed
|
|
ErrVerificationFailed = errors.New("lamport: signature verification failed")
|
|
|
|
// ErrInvalidMessage indicates the message format is invalid
|
|
ErrInvalidMessage = errors.New("lamport: invalid message (must be 32 bytes)")
|
|
|
|
// ErrKeyChainExhausted indicates no more keys available in chain
|
|
ErrKeyChainExhausted = errors.New("lamport: key chain exhausted")
|
|
)
|
|
|
|
// PrivateKey represents a Lamport private key.
|
|
// It consists of 256 pairs of 32-byte preimages.
|
|
// SECURITY: This key MUST only be used to sign ONE message.
|
|
type PrivateKey struct {
|
|
// Preimages is [256][2][32]byte - preimage[i][bit] for each bit position
|
|
Preimages [KeyBits][2][PreimageSize]byte
|
|
|
|
// Used tracks whether this key has been used (one-time property)
|
|
Used bool
|
|
}
|
|
|
|
// PublicKey represents a Lamport public key.
|
|
// It consists of 256 pairs of 32-byte hashes (keccak256 of preimages).
|
|
type PublicKey struct {
|
|
// Hashes is [256][2][32]byte - hash[i][bit] for each bit position
|
|
Hashes [KeyBits][2][HashSize]byte
|
|
}
|
|
|
|
// Signature represents a Lamport signature.
|
|
// It consists of 256 revealed preimages (one for each bit of the message).
|
|
type Signature struct {
|
|
// Preimages is [256][32]byte - the revealed preimage for each bit
|
|
Preimages [KeyBits][PreimageSize]byte
|
|
}
|
|
|
|
// KeyPair holds a Lamport key pair for convenience.
|
|
type KeyPair struct {
|
|
Private *PrivateKey
|
|
Public *PublicKey
|
|
}
|
|
|
|
// KeyChain manages a chain of one-time Lamport keys for continuous operation.
|
|
// As each key is used, the next key in the chain becomes active.
|
|
type KeyChain struct {
|
|
// Keys is the list of available key pairs
|
|
Keys []*KeyPair
|
|
|
|
// CurrentIndex is the index of the current (unused) key
|
|
CurrentIndex int
|
|
|
|
// UsedCount tracks how many keys have been used
|
|
UsedCount int
|
|
}
|
|
|
|
// Keccak256 computes the Keccak-256 hash of data.
|
|
func Keccak256(data []byte) [HashSize]byte {
|
|
h := sha3.NewLegacyKeccak256()
|
|
h.Write(data)
|
|
var result [HashSize]byte
|
|
h.Sum(result[:0])
|
|
return result
|
|
}
|
|
|
|
// Keccak256Multi computes keccak256 of multiple byte slices.
|
|
func Keccak256Multi(parts ...[]byte) [HashSize]byte {
|
|
h := sha3.NewLegacyKeccak256()
|
|
for _, p := range parts {
|
|
h.Write(p)
|
|
}
|
|
var result [HashSize]byte
|
|
h.Sum(result[:0])
|
|
return result
|
|
}
|
|
|
|
// Bytes serializes the public key to bytes.
|
|
func (pk *PublicKey) Bytes() []byte {
|
|
out := make([]byte, PublicKeySize)
|
|
for i := 0; i < KeyBits; i++ {
|
|
copy(out[i*64:i*64+32], pk.Hashes[i][0][:])
|
|
copy(out[i*64+32:i*64+64], pk.Hashes[i][1][:])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Hash returns the keccak256 hash of the public key (PKH).
|
|
// This is used on-chain to store a compact representation.
|
|
func (pk *PublicKey) Hash() [PublicKeyHashSize]byte {
|
|
return Keccak256(pk.Bytes())
|
|
}
|
|
|
|
// FromBytes deserializes a public key from bytes.
|
|
func (pk *PublicKey) FromBytes(data []byte) error {
|
|
if len(data) != PublicKeySize {
|
|
return ErrInvalidPublicKey
|
|
}
|
|
for i := 0; i < KeyBits; i++ {
|
|
copy(pk.Hashes[i][0][:], data[i*64:i*64+32])
|
|
copy(pk.Hashes[i][1][:], data[i*64+32:i*64+64])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Bytes serializes the signature to bytes.
|
|
func (sig *Signature) Bytes() []byte {
|
|
out := make([]byte, SignatureSize)
|
|
for i := 0; i < KeyBits; i++ {
|
|
copy(out[i*32:(i+1)*32], sig.Preimages[i][:])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// FromBytes deserializes a signature from bytes.
|
|
func (sig *Signature) FromBytes(data []byte) error {
|
|
if len(data) != SignatureSize {
|
|
return ErrInvalidSignature
|
|
}
|
|
for i := 0; i < KeyBits; i++ {
|
|
copy(sig.Preimages[i][:], data[i*32:(i+1)*32])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ToCalldata converts the signature to Solidity-compatible calldata format.
|
|
// Returns bytes[256] for use with verify_u256.
|
|
func (sig *Signature) ToCalldata() [][]byte {
|
|
result := make([][]byte, KeyBits)
|
|
for i := 0; i < KeyBits; i++ {
|
|
preimage := make([]byte, PreimageSize)
|
|
copy(preimage, sig.Preimages[i][:])
|
|
result[i] = preimage
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ToCalldata converts public key to bytes32[2][256] for Solidity.
|
|
func (pk *PublicKey) ToCalldata() [KeyBits][2][HashSize]byte {
|
|
return pk.Hashes
|
|
}
|
|
|
|
// ComputeDomainSeparator computes the domain separator for threshold signing.
|
|
func ComputeDomainSeparator(moduleAddress [20]byte, chainID uint64) [32]byte {
|
|
var buf [52]byte // 20 + 32 for chainID as uint256
|
|
copy(buf[:20], moduleAddress[:])
|
|
binary.BigEndian.PutUint64(buf[44:52], chainID) // Right-aligned like Solidity uint256
|
|
return Keccak256(buf[:])
|
|
}
|
|
|
|
// ComputeThresholdMessage computes the final message for threshold signing.
|
|
// This matches the Solidity: keccak256(abi.encodePacked(safeTxHash, nextPKH, address(this), block.chainid))
|
|
func ComputeThresholdMessage(safeTxHash, nextPKH [32]byte, moduleAddress [20]byte, chainID uint64) [32]byte {
|
|
var buf [116]byte // 32 + 32 + 20 + 32 (chainid as uint256)
|
|
copy(buf[0:32], safeTxHash[:])
|
|
copy(buf[32:64], nextPKH[:])
|
|
copy(buf[64:84], moduleAddress[:])
|
|
binary.BigEndian.PutUint64(buf[108:116], chainID)
|
|
return Keccak256(buf[:])
|
|
}
|
|
|
|
// GenerateKeyPair generates a new Lamport key pair using crypto/rand.
|
|
func GenerateKeyPair() (*KeyPair, error) {
|
|
return GenerateKeyPairFromReader(rand.Reader)
|
|
}
|
|
|
|
// GenerateKeyPairFromReader generates a new Lamport key pair from the given random source.
|
|
func GenerateKeyPairFromReader(random io.Reader) (*KeyPair, error) {
|
|
priv := &PrivateKey{}
|
|
pub := &PublicKey{}
|
|
|
|
// Generate random preimages and compute public key hashes
|
|
for i := 0; i < KeyBits; i++ {
|
|
for bit := 0; bit < 2; bit++ {
|
|
if _, err := io.ReadFull(random, priv.Preimages[i][bit][:]); err != nil {
|
|
return nil, err
|
|
}
|
|
pub.Hashes[i][bit] = Keccak256(priv.Preimages[i][bit][:])
|
|
}
|
|
}
|
|
|
|
return &KeyPair{Private: priv, Public: pub}, nil
|
|
}
|
|
|
|
// NewKeyChain creates a new key chain with the specified number of keys.
|
|
func NewKeyChain(numKeys int) (*KeyChain, error) {
|
|
if numKeys <= 0 {
|
|
return nil, errors.New("lamport: numKeys must be positive")
|
|
}
|
|
|
|
chain := &KeyChain{
|
|
Keys: make([]*KeyPair, numKeys),
|
|
CurrentIndex: 0,
|
|
UsedCount: 0,
|
|
}
|
|
|
|
for i := 0; i < numKeys; i++ {
|
|
kp, err := GenerateKeyPair()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chain.Keys[i] = kp
|
|
}
|
|
|
|
return chain, nil
|
|
}
|
|
|
|
// Current returns the current (unused) key pair.
|
|
func (kc *KeyChain) Current() (*KeyPair, error) {
|
|
if kc.CurrentIndex >= len(kc.Keys) {
|
|
return nil, ErrKeyChainExhausted
|
|
}
|
|
return kc.Keys[kc.CurrentIndex], nil
|
|
}
|
|
|
|
// NextPKH returns the hash of the next public key (for key rotation).
|
|
func (kc *KeyChain) NextPKH() ([32]byte, error) {
|
|
nextIdx := kc.CurrentIndex + 1
|
|
if nextIdx >= len(kc.Keys) {
|
|
return [32]byte{}, errors.New("lamport: no next key available")
|
|
}
|
|
return kc.Keys[nextIdx].Public.Hash(), nil
|
|
}
|
|
|
|
// Advance marks the current key as used and advances to the next.
|
|
func (kc *KeyChain) Advance() error {
|
|
if kc.CurrentIndex >= len(kc.Keys) {
|
|
return ErrKeyChainExhausted
|
|
}
|
|
kc.Keys[kc.CurrentIndex].Private.Used = true
|
|
kc.CurrentIndex++
|
|
kc.UsedCount++
|
|
return nil
|
|
}
|
|
|
|
// Remaining returns the number of unused keys remaining.
|
|
func (kc *KeyChain) Remaining() int {
|
|
return len(kc.Keys) - kc.CurrentIndex
|
|
}
|
|
|
|
// GetBit returns the bit at position i (0-255) of a 32-byte message.
|
|
// Bit 0 is the most significant bit of the first byte.
|
|
func GetBit(message [32]byte, i int) int {
|
|
byteIdx := i / 8
|
|
bitIdx := 7 - (i % 8)
|
|
return int((message[byteIdx] >> bitIdx) & 1)
|
|
}
|