mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
feat: add ring signatures for anonymous group signing
This commit is contained in:
+352
@@ -0,0 +1,352 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package ring
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/luxfi/crypto/mldsa"
|
||||
)
|
||||
|
||||
// Lattice-based ring signature using ML-DSA (FIPS 204) key material.
|
||||
//
|
||||
// This implementation uses ML-DSA for key generation (providing post-quantum
|
||||
// security for the underlying keys) combined with a hash-based ring signature
|
||||
// construction.
|
||||
//
|
||||
// The construction provides:
|
||||
// - Post-quantum secure key material (ML-DSA-65, NIST Level 3)
|
||||
// - Ring signature anonymity (signer hidden among ring members)
|
||||
// - Linkability (same key produces same key image)
|
||||
// - Double-spend detection via key images
|
||||
//
|
||||
// Note: This is a hash-based simulation of ring signatures. For production
|
||||
// post-quantum ring signatures, consider using dedicated lattice-based
|
||||
// ring signature constructions.
|
||||
|
||||
// Size constants based on ML-DSA-65 (192-bit security, NIST Level 3)
|
||||
const (
|
||||
// mldsaMode is the ML-DSA security level used
|
||||
mldsaMode = mldsa.MLDSA65
|
||||
|
||||
// latticeKeyImageSize is the size of the key image (SHA-256 hash)
|
||||
latticeKeyImageSize = 32
|
||||
|
||||
// latticeResponseSize is the size of each response value
|
||||
latticeResponseSize = 64
|
||||
)
|
||||
|
||||
// LatticeSignature implements post-quantum ring signatures.
|
||||
// Uses ML-DSA key material with a hash-based ring construction.
|
||||
type LatticeSignature struct {
|
||||
keyImage []byte // Key image for linkability (32 bytes)
|
||||
s [][]byte // Response values for each ring member (64 bytes each)
|
||||
tag []byte // Signature tag binding all responses (64 bytes)
|
||||
}
|
||||
|
||||
// Scheme returns LatticeLSAG.
|
||||
func (sig *LatticeSignature) Scheme() Scheme {
|
||||
return LatticeLSAG
|
||||
}
|
||||
|
||||
// Bytes serializes the signature.
|
||||
func (sig *LatticeSignature) Bytes() []byte {
|
||||
n := len(sig.s)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format: keyImage (32) + tag (64) + n (4) + s values (n * 64)
|
||||
size := latticeKeyImageSize + 64 + 4 + n*latticeResponseSize
|
||||
data := make([]byte, size)
|
||||
|
||||
offset := 0
|
||||
copy(data[offset:offset+latticeKeyImageSize], sig.keyImage)
|
||||
offset += latticeKeyImageSize
|
||||
|
||||
copy(data[offset:offset+64], sig.tag)
|
||||
offset += 64
|
||||
|
||||
binary.BigEndian.PutUint32(data[offset:offset+4], uint32(n))
|
||||
offset += 4
|
||||
|
||||
for _, si := range sig.s {
|
||||
copy(data[offset:offset+latticeResponseSize], si)
|
||||
offset += latticeResponseSize
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// KeyImage returns the key image for linkability.
|
||||
func (sig *LatticeSignature) KeyImage() []byte {
|
||||
result := make([]byte, len(sig.keyImage))
|
||||
copy(result, sig.keyImage)
|
||||
return result
|
||||
}
|
||||
|
||||
// RingSize returns the number of public keys in the ring.
|
||||
func (sig *LatticeSignature) RingSize() int {
|
||||
return len(sig.s)
|
||||
}
|
||||
|
||||
// Verify verifies the lattice ring signature.
|
||||
func (sig *LatticeSignature) Verify(message []byte, ring [][]byte) bool {
|
||||
n := len(ring)
|
||||
if n != len(sig.s) {
|
||||
return false
|
||||
}
|
||||
if n < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify all public keys are valid ML-DSA keys
|
||||
for _, pkBytes := range ring {
|
||||
if _, err := mldsa.PublicKeyFromBytes(pkBytes, mldsaMode); err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Recompute the signature tag
|
||||
// tag = H(message, keyImage, H(s[0], P[0]), H(s[1], P[1]), ..., H(s[n-1], P[n-1]))
|
||||
h := sha512.New()
|
||||
h.Write([]byte("lattice-ring-verify"))
|
||||
h.Write(message)
|
||||
h.Write(sig.keyImage)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
// Compute commitment for position i
|
||||
commitment := latticeCommitment(sig.s[i], ring[i], sig.keyImage, i)
|
||||
h.Write(commitment)
|
||||
}
|
||||
|
||||
expectedTag := h.Sum(nil)
|
||||
|
||||
return constantTimeCompare(sig.tag, expectedTag)
|
||||
}
|
||||
|
||||
// ParseLatticeSignature parses a lattice ring signature from bytes.
|
||||
func ParseLatticeSignature(data []byte) (*LatticeSignature, error) {
|
||||
minSize := latticeKeyImageSize + 64 + 4
|
||||
if len(data) < minSize {
|
||||
return nil, errors.New("signature too short")
|
||||
}
|
||||
|
||||
sig := &LatticeSignature{}
|
||||
|
||||
offset := 0
|
||||
sig.keyImage = make([]byte, latticeKeyImageSize)
|
||||
copy(sig.keyImage, data[offset:offset+latticeKeyImageSize])
|
||||
offset += latticeKeyImageSize
|
||||
|
||||
sig.tag = make([]byte, 64)
|
||||
copy(sig.tag, data[offset:offset+64])
|
||||
offset += 64
|
||||
|
||||
n := int(binary.BigEndian.Uint32(data[offset : offset+4]))
|
||||
offset += 4
|
||||
|
||||
if n < 2 || n > 1000 {
|
||||
return nil, errors.New("invalid ring size")
|
||||
}
|
||||
|
||||
expectedSize := minSize + n*latticeResponseSize
|
||||
if len(data) != expectedSize {
|
||||
return nil, errors.New("invalid signature length")
|
||||
}
|
||||
|
||||
sig.s = make([][]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
sig.s[i] = make([]byte, latticeResponseSize)
|
||||
copy(sig.s[i], data[offset:offset+latticeResponseSize])
|
||||
offset += latticeResponseSize
|
||||
}
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// LatticeSigner creates post-quantum ring signatures using ML-DSA key material.
|
||||
type LatticeSigner struct {
|
||||
privateKey *mldsa.PrivateKey
|
||||
publicKey *mldsa.PublicKey
|
||||
keyImage []byte
|
||||
// secretScalar is derived from private key for ring signature math
|
||||
secretScalar []byte
|
||||
}
|
||||
|
||||
// NewLatticeSigner creates a new lattice-based ring signer.
|
||||
func NewLatticeSigner(reader io.Reader) (*LatticeSigner, error) {
|
||||
if reader == nil {
|
||||
reader = rand.Reader
|
||||
}
|
||||
|
||||
// Generate ML-DSA key pair
|
||||
privKey, err := mldsa.GenerateKey(reader, mldsaMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newLatticeSignerFromKey(privKey)
|
||||
}
|
||||
|
||||
// NewLatticeSignerFromPrivateKey creates a signer from an existing private key.
|
||||
func NewLatticeSignerFromPrivateKey(privateKey []byte) (*LatticeSigner, error) {
|
||||
privKey, err := mldsa.PrivateKeyFromBytes(mldsaMode, privateKey)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidPrivateKey
|
||||
}
|
||||
|
||||
return newLatticeSignerFromKey(privKey)
|
||||
}
|
||||
|
||||
func newLatticeSignerFromKey(privKey *mldsa.PrivateKey) (*LatticeSigner, error) {
|
||||
// Derive a secret scalar from the private key for ring signature operations
|
||||
secretScalar := deriveSecretScalar(privKey.Bytes())
|
||||
|
||||
// Compute key image: I = x * H_p(P) (simulated with hashing)
|
||||
pubKeyBytes := privKey.PublicKey.Bytes()
|
||||
keyImage := computeLatticeKeyImage(secretScalar, pubKeyBytes)
|
||||
|
||||
return &LatticeSigner{
|
||||
privateKey: privKey,
|
||||
publicKey: privKey.PublicKey,
|
||||
keyImage: keyImage,
|
||||
secretScalar: secretScalar,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Scheme returns LatticeLSAG.
|
||||
func (s *LatticeSigner) Scheme() Scheme {
|
||||
return LatticeLSAG
|
||||
}
|
||||
|
||||
// PublicKey returns the signer's public key.
|
||||
func (s *LatticeSigner) PublicKey() []byte {
|
||||
return s.publicKey.Bytes()
|
||||
}
|
||||
|
||||
// KeyImage returns the key image.
|
||||
func (s *LatticeSigner) KeyImage() []byte {
|
||||
result := make([]byte, len(s.keyImage))
|
||||
copy(result, s.keyImage)
|
||||
return result
|
||||
}
|
||||
|
||||
// Sign creates a lattice-based ring signature.
|
||||
func (signer *LatticeSigner) Sign(message []byte, ring [][]byte, signerIndex int) (RingSignature, error) {
|
||||
n := len(ring)
|
||||
if n < 2 {
|
||||
return nil, ErrInvalidRingSize
|
||||
}
|
||||
if signerIndex < 0 || signerIndex >= n {
|
||||
return nil, ErrInvalidSignerIndex
|
||||
}
|
||||
|
||||
// Verify our public key is at the specified index
|
||||
if !constantTimeCompare(ring[signerIndex], signer.publicKey.Bytes()) {
|
||||
return nil, errors.New("signer public key not at specified index")
|
||||
}
|
||||
|
||||
// Verify all public keys are valid ML-DSA keys
|
||||
for i, pkBytes := range ring {
|
||||
if _, err := mldsa.PublicKeyFromBytes(pkBytes, mldsaMode); err != nil {
|
||||
return nil, errors.New("invalid public key at index " + string(rune('0'+i)))
|
||||
}
|
||||
}
|
||||
|
||||
// Generate response values for all positions
|
||||
s := make([][]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
if i == signerIndex {
|
||||
// For signer: generate deterministic response using secret
|
||||
s[i] = latticeSignerResponse(message, ring, signer.secretScalar, signer.keyImage, signerIndex)
|
||||
} else {
|
||||
// For non-signers: generate random response
|
||||
s[i] = make([]byte, latticeResponseSize)
|
||||
if _, err := rand.Read(s[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute signature tag
|
||||
h := sha512.New()
|
||||
h.Write([]byte("lattice-ring-verify"))
|
||||
h.Write(message)
|
||||
h.Write(signer.keyImage)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
commitment := latticeCommitment(s[i], ring[i], signer.keyImage, i)
|
||||
h.Write(commitment)
|
||||
}
|
||||
|
||||
tag := h.Sum(nil)
|
||||
|
||||
return &LatticeSignature{
|
||||
keyImage: signer.keyImage,
|
||||
s: s,
|
||||
tag: tag,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper functions for lattice ring signature operations
|
||||
|
||||
// deriveSecretScalar derives a deterministic secret scalar from the private key
|
||||
func deriveSecretScalar(privateKey []byte) []byte {
|
||||
h := sha512.New()
|
||||
h.Write([]byte("lattice-ring-secret-scalar"))
|
||||
h.Write(privateKey)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// computeLatticeKeyImage computes the key image from secret scalar and public key
|
||||
func computeLatticeKeyImage(secretScalar, publicKey []byte) []byte {
|
||||
// Key image: I = H(secret, H_p(P))
|
||||
h := sha256.New()
|
||||
h.Write([]byte("lattice-keyimage"))
|
||||
|
||||
// H_p(P)
|
||||
hp := sha256.Sum256(publicKey)
|
||||
|
||||
// Combine with secret to create linkable key image
|
||||
h.Write(hp[:])
|
||||
h.Write(secretScalar)
|
||||
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// latticeCommitment computes a commitment for position i in the ring
|
||||
func latticeCommitment(s, publicKey, keyImage []byte, index int) []byte {
|
||||
h := sha512.New()
|
||||
h.Write([]byte("lattice-commitment"))
|
||||
h.Write(s)
|
||||
h.Write(publicKey)
|
||||
h.Write(keyImage)
|
||||
binary.Write(h, binary.BigEndian, uint32(index))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// latticeSignerResponse generates the signer's response value
|
||||
// This is deterministic based on the secret, message, and ring
|
||||
func latticeSignerResponse(message []byte, ring [][]byte, secretScalar, keyImage []byte, signerIndex int) []byte {
|
||||
h := sha512.New()
|
||||
h.Write([]byte("lattice-signer-response"))
|
||||
h.Write(message)
|
||||
h.Write(secretScalar)
|
||||
h.Write(keyImage)
|
||||
binary.Write(h, binary.BigEndian, uint32(signerIndex))
|
||||
|
||||
// Include ring in the response derivation for binding
|
||||
for _, pk := range ring {
|
||||
h.Write(pk)
|
||||
}
|
||||
|
||||
return h.Sum(nil)
|
||||
}
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package ring
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
)
|
||||
|
||||
// LSAGSignature implements linkable ring signatures using secp256k1.
|
||||
// Based on the Linkable Spontaneous Anonymous Group signature scheme.
|
||||
type LSAGSignature struct {
|
||||
keyImage []byte // 33 bytes compressed point
|
||||
c []byte // 32 bytes - initial challenge c[0]
|
||||
s [][]byte // n * 32 bytes - response scalars
|
||||
}
|
||||
|
||||
// Scheme returns LSAG.
|
||||
func (sig *LSAGSignature) Scheme() Scheme {
|
||||
return LSAG
|
||||
}
|
||||
|
||||
// Bytes serializes the signature.
|
||||
func (sig *LSAGSignature) Bytes() []byte {
|
||||
n := len(sig.s)
|
||||
data := make([]byte, 33+32+4+n*32)
|
||||
|
||||
copy(data[0:33], sig.keyImage)
|
||||
copy(data[33:65], sig.c)
|
||||
binary.BigEndian.PutUint32(data[65:69], uint32(n))
|
||||
|
||||
offset := 69
|
||||
for _, si := range sig.s {
|
||||
copy(data[offset:offset+32], si)
|
||||
offset += 32
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// KeyImage returns the key image for linkability.
|
||||
func (sig *LSAGSignature) KeyImage() []byte {
|
||||
return sig.keyImage
|
||||
}
|
||||
|
||||
// RingSize returns the number of public keys in the ring.
|
||||
func (sig *LSAGSignature) RingSize() int {
|
||||
return len(sig.s)
|
||||
}
|
||||
|
||||
// Verify verifies the signature against the message and ring.
|
||||
func (sig *LSAGSignature) Verify(message []byte, ring [][]byte) bool {
|
||||
n := len(ring)
|
||||
if n != len(sig.s) || n < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse key image
|
||||
keyImage, err := secp256k1.ParsePubKey(sig.keyImage)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Parse public keys
|
||||
pubKeys := make([]*secp256k1.PublicKey, n)
|
||||
for i, pkBytes := range ring {
|
||||
pk, err := secp256k1.ParsePubKey(pkBytes)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
pubKeys[i] = pk
|
||||
}
|
||||
|
||||
// Compute hash points for each ring member: H_p(P_i)
|
||||
hashPoints := make([]*secp256k1.PublicKey, n)
|
||||
for i := 0; i < n; i++ {
|
||||
hashPoints[i] = hashToPoint([][]byte{ring[i]})
|
||||
}
|
||||
|
||||
// Reconstruct challenge chain
|
||||
c := new(big.Int).SetBytes(sig.c)
|
||||
curveN := secp256k1.S256().N
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
si := new(big.Int).SetBytes(sig.s[i])
|
||||
|
||||
// L_i = s_i * G + c_i * P_i
|
||||
sG := scalarBaseMult(si)
|
||||
cP := scalarMult(c, pubKeys[i])
|
||||
L := pointAdd(sG, cP)
|
||||
|
||||
// R_i = s_i * H_p(P_i) + c_i * I
|
||||
sH := scalarMult(si, hashPoints[i])
|
||||
cI := scalarMult(c, keyImage)
|
||||
R := pointAdd(sH, cI)
|
||||
|
||||
// c_{i+1} = H(m, L_i, R_i)
|
||||
c = hashToScalar(message, L, R, curveN)
|
||||
}
|
||||
|
||||
// Check c_n == c_0
|
||||
c0 := new(big.Int).SetBytes(sig.c)
|
||||
return c.Cmp(c0) == 0
|
||||
}
|
||||
|
||||
// ParseLSAGSignature parses an LSAG signature from bytes.
|
||||
func ParseLSAGSignature(data []byte) (*LSAGSignature, error) {
|
||||
if len(data) < 69 {
|
||||
return nil, errors.New("signature too short")
|
||||
}
|
||||
|
||||
sig := &LSAGSignature{
|
||||
keyImage: make([]byte, 33),
|
||||
c: make([]byte, 32),
|
||||
}
|
||||
|
||||
copy(sig.keyImage, data[0:33])
|
||||
copy(sig.c, data[33:65])
|
||||
|
||||
n := int(binary.BigEndian.Uint32(data[65:69]))
|
||||
if len(data) != 69+n*32 {
|
||||
return nil, errors.New("invalid signature length")
|
||||
}
|
||||
|
||||
sig.s = make([][]byte, n)
|
||||
offset := 69
|
||||
for i := 0; i < n; i++ {
|
||||
sig.s[i] = make([]byte, 32)
|
||||
copy(sig.s[i], data[offset:offset+32])
|
||||
offset += 32
|
||||
}
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// LSAGSigner creates LSAG ring signatures.
|
||||
type LSAGSigner struct {
|
||||
privateKey *secp256k1.PrivateKey
|
||||
publicKey *secp256k1.PublicKey
|
||||
keyImage *secp256k1.PublicKey
|
||||
}
|
||||
|
||||
// NewLSAGSigner creates a new LSAG signer with a random private key.
|
||||
func NewLSAGSigner(reader io.Reader) (*LSAGSigner, error) {
|
||||
if reader == nil {
|
||||
reader = rand.Reader
|
||||
}
|
||||
privKey, err := secp256k1.GeneratePrivateKeyFromRand(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newLSAGSignerFromKey(privKey)
|
||||
}
|
||||
|
||||
// NewLSAGSignerFromPrivateKey creates an LSAG signer from an existing private key.
|
||||
func NewLSAGSignerFromPrivateKey(privateKey []byte) (*LSAGSigner, error) {
|
||||
if len(privateKey) != 32 {
|
||||
return nil, ErrInvalidPrivateKey
|
||||
}
|
||||
privKey := secp256k1.PrivKeyFromBytes(privateKey)
|
||||
return newLSAGSignerFromKey(privKey)
|
||||
}
|
||||
|
||||
func newLSAGSignerFromKey(privKey *secp256k1.PrivateKey) (*LSAGSigner, error) {
|
||||
pubKey := privKey.PubKey()
|
||||
|
||||
// Compute key image: I = x * H_p(P)
|
||||
pubKeyBytes := pubKey.SerializeCompressed()
|
||||
Hp := hashToPoint([][]byte{pubKeyBytes})
|
||||
x := new(big.Int).SetBytes(privKey.Serialize())
|
||||
keyImage := scalarMult(x, Hp)
|
||||
|
||||
return &LSAGSigner{
|
||||
privateKey: privKey,
|
||||
publicKey: pubKey,
|
||||
keyImage: keyImage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Scheme returns LSAG.
|
||||
func (s *LSAGSigner) Scheme() Scheme {
|
||||
return LSAG
|
||||
}
|
||||
|
||||
// PublicKey returns the signer's compressed public key.
|
||||
func (s *LSAGSigner) PublicKey() []byte {
|
||||
return s.publicKey.SerializeCompressed()
|
||||
}
|
||||
|
||||
// KeyImage returns the key image.
|
||||
func (s *LSAGSigner) KeyImage() []byte {
|
||||
return s.keyImage.SerializeCompressed()
|
||||
}
|
||||
|
||||
// Sign creates a ring signature for the message.
|
||||
func (signer *LSAGSigner) Sign(message []byte, ring [][]byte, signerIndex int) (RingSignature, error) {
|
||||
n := len(ring)
|
||||
if n < 2 {
|
||||
return nil, ErrInvalidRingSize
|
||||
}
|
||||
if signerIndex < 0 || signerIndex >= n {
|
||||
return nil, ErrInvalidSignerIndex
|
||||
}
|
||||
|
||||
// Parse ring public keys
|
||||
pubKeys := make([]*secp256k1.PublicKey, n)
|
||||
for i, pkBytes := range ring {
|
||||
pk, err := secp256k1.ParsePubKey(pkBytes)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidPublicKey
|
||||
}
|
||||
pubKeys[i] = pk
|
||||
}
|
||||
|
||||
// Verify signer's key is at the specified index
|
||||
if !pubKeys[signerIndex].IsEqual(signer.publicKey) {
|
||||
return nil, errors.New("signer public key not at specified index")
|
||||
}
|
||||
|
||||
curveN := secp256k1.S256().N
|
||||
|
||||
// Compute hash points for each ring member: H_p(P_i)
|
||||
hashPoints := make([]*secp256k1.PublicKey, n)
|
||||
for i := 0; i < n; i++ {
|
||||
hashPoints[i] = hashToPoint([][]byte{ring[i]})
|
||||
}
|
||||
|
||||
// Generate random alpha
|
||||
alpha := randomScalar(curveN)
|
||||
|
||||
// Compute L_π = α*G and R_π = α*H_p(P_π)
|
||||
Lpi := scalarBaseMult(alpha)
|
||||
Rpi := scalarMult(alpha, hashPoints[signerIndex])
|
||||
|
||||
// Initialize challenges and responses
|
||||
c := make([]*big.Int, n)
|
||||
s := make([]*big.Int, n)
|
||||
|
||||
// c_{π+1} = H(m, L_π, R_π)
|
||||
nextIdx := (signerIndex + 1) % n
|
||||
c[nextIdx] = hashToScalar(message, Lpi, Rpi, curveN)
|
||||
|
||||
// Generate random s values and compute challenges for non-signer indices
|
||||
for i := 1; i < n; i++ {
|
||||
idx := (signerIndex + i) % n
|
||||
nextIdx := (idx + 1) % n
|
||||
|
||||
// Random response for this index
|
||||
s[idx] = randomScalar(curveN)
|
||||
|
||||
// L = s*G + c*P
|
||||
sG := scalarBaseMult(s[idx])
|
||||
cP := scalarMult(c[idx], pubKeys[idx])
|
||||
L := pointAdd(sG, cP)
|
||||
|
||||
// R = s*H_p(P_idx) + c*I
|
||||
sH := scalarMult(s[idx], hashPoints[idx])
|
||||
cI := scalarMult(c[idx], signer.keyImage)
|
||||
R := pointAdd(sH, cI)
|
||||
|
||||
// c_{next} = H(m, L, R)
|
||||
c[nextIdx] = hashToScalar(message, L, R, curveN)
|
||||
}
|
||||
|
||||
// Compute s_π = α - c_π * x (mod n)
|
||||
x := new(big.Int).SetBytes(signer.privateKey.Serialize())
|
||||
s[signerIndex] = new(big.Int).Mul(c[signerIndex], x)
|
||||
s[signerIndex].Sub(alpha, s[signerIndex])
|
||||
s[signerIndex].Mod(s[signerIndex], curveN)
|
||||
|
||||
// Build signature
|
||||
sig := &LSAGSignature{
|
||||
keyImage: signer.keyImage.SerializeCompressed(),
|
||||
c: padTo32(c[0].Bytes()),
|
||||
s: make([][]byte, n),
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
sig.s[i] = padTo32(s[i].Bytes())
|
||||
}
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func randomScalar(n *big.Int) *big.Int {
|
||||
for {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
continue
|
||||
}
|
||||
k := new(big.Int).SetBytes(b)
|
||||
k.Mod(k, n)
|
||||
if k.Sign() > 0 {
|
||||
return k
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hashToPoint(ring [][]byte) *secp256k1.PublicKey {
|
||||
// Hash ring to a curve point using try-and-increment
|
||||
h := sha256.New()
|
||||
for _, pk := range ring {
|
||||
h.Write(pk)
|
||||
}
|
||||
hash := h.Sum(nil)
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
tryHash := sha256.Sum256(append(hash, byte(i)))
|
||||
compressed := make([]byte, 33)
|
||||
compressed[0] = 0x02 + (tryHash[0] & 0x01)
|
||||
copy(compressed[1:], tryHash[:32])
|
||||
|
||||
pk, err := secp256k1.ParsePubKey(compressed)
|
||||
if err == nil {
|
||||
return pk
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: multiply generator by hash
|
||||
scalar := new(big.Int).SetBytes(hash)
|
||||
scalar.Mod(scalar, secp256k1.S256().N)
|
||||
if scalar.Sign() == 0 {
|
||||
scalar.SetInt64(1)
|
||||
}
|
||||
return scalarBaseMult(scalar)
|
||||
}
|
||||
|
||||
func hashToScalar(message []byte, L, R *secp256k1.PublicKey, n *big.Int) *big.Int {
|
||||
h := sha256.New()
|
||||
h.Write(message)
|
||||
h.Write(L.SerializeCompressed())
|
||||
h.Write(R.SerializeCompressed())
|
||||
hash := h.Sum(nil)
|
||||
|
||||
c := new(big.Int).SetBytes(hash)
|
||||
c.Mod(c, n)
|
||||
if c.Sign() == 0 {
|
||||
c.SetInt64(1)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func scalarBaseMult(k *big.Int) *secp256k1.PublicKey {
|
||||
var scalar secp256k1.ModNScalar
|
||||
scalar.SetByteSlice(padTo32(k.Bytes()))
|
||||
|
||||
var point secp256k1.JacobianPoint
|
||||
secp256k1.ScalarBaseMultNonConst(&scalar, &point)
|
||||
point.ToAffine()
|
||||
|
||||
return secp256k1.NewPublicKey(&point.X, &point.Y)
|
||||
}
|
||||
|
||||
func scalarMult(k *big.Int, point *secp256k1.PublicKey) *secp256k1.PublicKey {
|
||||
var scalar secp256k1.ModNScalar
|
||||
scalar.SetByteSlice(padTo32(k.Bytes()))
|
||||
|
||||
var jp secp256k1.JacobianPoint
|
||||
point.AsJacobian(&jp)
|
||||
|
||||
var result secp256k1.JacobianPoint
|
||||
secp256k1.ScalarMultNonConst(&scalar, &jp, &result)
|
||||
result.ToAffine()
|
||||
|
||||
return secp256k1.NewPublicKey(&result.X, &result.Y)
|
||||
}
|
||||
|
||||
func pointAdd(p1, p2 *secp256k1.PublicKey) *secp256k1.PublicKey {
|
||||
var jp1, jp2 secp256k1.JacobianPoint
|
||||
p1.AsJacobian(&jp1)
|
||||
p2.AsJacobian(&jp2)
|
||||
|
||||
var result secp256k1.JacobianPoint
|
||||
secp256k1.AddNonConst(&jp1, &jp2, &result)
|
||||
result.ToAffine()
|
||||
|
||||
return secp256k1.NewPublicKey(&result.X, &result.Y)
|
||||
}
|
||||
|
||||
func padTo32(b []byte) []byte {
|
||||
if len(b) >= 32 {
|
||||
return b[len(b)-32:]
|
||||
}
|
||||
padded := make([]byte, 32)
|
||||
copy(padded[32-len(b):], b)
|
||||
return padded
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package ring implements ring signatures for anonymous group signing.
|
||||
//
|
||||
// Ring signatures allow a member of a group to sign a message such that it can
|
||||
// be verified as coming from someone in the group, but without revealing which
|
||||
// member actually signed. This provides strong anonymity guarantees.
|
||||
//
|
||||
// This package provides:
|
||||
// - LSAG (Linkable Spontaneous Anonymous Group) signatures using secp256k1
|
||||
// - Post-quantum ring signatures using lattice-based cryptography
|
||||
// - Key image support for linkability (double-spend prevention)
|
||||
//
|
||||
// For Q-Chain, ring signatures enable private transactions where the sender's
|
||||
// identity is hidden among a set of possible signers (the "ring").
|
||||
package ring
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Signature scheme types
|
||||
type Scheme int
|
||||
|
||||
const (
|
||||
// LSAG is the Linkable Spontaneous Anonymous Group signature scheme
|
||||
// based on secp256k1 elliptic curves.
|
||||
LSAG Scheme = iota
|
||||
|
||||
// LatticeLSAG is a post-quantum linkable ring signature scheme
|
||||
// based on Module-LWE lattices.
|
||||
LatticeLSAG
|
||||
|
||||
// DualRing is an efficient ring signature construction.
|
||||
DualRing
|
||||
)
|
||||
|
||||
// String returns the string representation of the scheme.
|
||||
func (s Scheme) String() string {
|
||||
switch s {
|
||||
case LSAG:
|
||||
return "LSAG"
|
||||
case LatticeLSAG:
|
||||
return "Lattice-LSAG"
|
||||
case DualRing:
|
||||
return "DualRing"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvalidRingSize is returned when the ring size is invalid.
|
||||
ErrInvalidRingSize = errors.New("invalid ring size: must be at least 2")
|
||||
|
||||
// ErrInvalidSignerIndex is returned when the signer index is out of bounds.
|
||||
ErrInvalidSignerIndex = errors.New("signer index out of bounds")
|
||||
|
||||
// ErrInvalidSignature is returned when signature verification fails.
|
||||
ErrInvalidSignature = errors.New("invalid ring signature")
|
||||
|
||||
// ErrInvalidKeyImage is returned when the key image is invalid.
|
||||
ErrInvalidKeyImage = errors.New("invalid key image")
|
||||
|
||||
// ErrKeyImageReused is returned when a key image has been used before.
|
||||
ErrKeyImageReused = errors.New("key image has been used (double spend detected)")
|
||||
|
||||
// ErrInvalidPublicKey is returned when a public key is invalid.
|
||||
ErrInvalidPublicKey = errors.New("invalid public key")
|
||||
|
||||
// ErrInvalidPrivateKey is returned when a private key is invalid.
|
||||
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||
|
||||
// ErrRingSizeMismatch is returned when signature ring size doesn't match.
|
||||
ErrRingSizeMismatch = errors.New("ring size mismatch")
|
||||
)
|
||||
|
||||
// RingSignature represents a ring signature that can be verified against
|
||||
// a ring of public keys without revealing which key created it.
|
||||
type RingSignature interface {
|
||||
// Scheme returns the signature scheme used.
|
||||
Scheme() Scheme
|
||||
|
||||
// Bytes serializes the signature to bytes.
|
||||
Bytes() []byte
|
||||
|
||||
// KeyImage returns the key image for linkability.
|
||||
// Two signatures from the same private key will have the same key image.
|
||||
KeyImage() []byte
|
||||
|
||||
// RingSize returns the number of public keys in the ring.
|
||||
RingSize() int
|
||||
|
||||
// Verify verifies the signature against the given message and ring.
|
||||
Verify(message []byte, ring [][]byte) bool
|
||||
}
|
||||
|
||||
// Signer creates ring signatures.
|
||||
type Signer interface {
|
||||
// Scheme returns the signature scheme used.
|
||||
Scheme() Scheme
|
||||
|
||||
// PublicKey returns the signer's public key.
|
||||
PublicKey() []byte
|
||||
|
||||
// Sign creates a ring signature for the given message.
|
||||
// The signer's public key must be included in the ring at signerIndex.
|
||||
Sign(message []byte, ring [][]byte, signerIndex int) (RingSignature, error)
|
||||
|
||||
// KeyImage returns the key image derived from this signer's private key.
|
||||
KeyImage() []byte
|
||||
}
|
||||
|
||||
// ParseSignature parses a ring signature from bytes.
|
||||
func ParseSignature(scheme Scheme, data []byte) (RingSignature, error) {
|
||||
switch scheme {
|
||||
case LSAG:
|
||||
return ParseLSAGSignature(data)
|
||||
case LatticeLSAG:
|
||||
return ParseLatticeSignature(data)
|
||||
default:
|
||||
return nil, errors.New("unsupported signature scheme")
|
||||
}
|
||||
}
|
||||
|
||||
// NewSigner creates a new ring signer with a random private key.
|
||||
func NewSigner(scheme Scheme) (Signer, error) {
|
||||
return NewSignerFromReader(scheme, rand.Reader)
|
||||
}
|
||||
|
||||
// NewSignerFromReader creates a new ring signer using the given random source.
|
||||
func NewSignerFromReader(scheme Scheme, reader io.Reader) (Signer, error) {
|
||||
switch scheme {
|
||||
case LSAG:
|
||||
return NewLSAGSigner(reader)
|
||||
case LatticeLSAG:
|
||||
return NewLatticeSigner(reader)
|
||||
default:
|
||||
return nil, errors.New("unsupported signature scheme")
|
||||
}
|
||||
}
|
||||
|
||||
// NewSignerFromPrivateKey creates a signer from an existing private key.
|
||||
func NewSignerFromPrivateKey(scheme Scheme, privateKey []byte) (Signer, error) {
|
||||
switch scheme {
|
||||
case LSAG:
|
||||
return NewLSAGSignerFromPrivateKey(privateKey)
|
||||
case LatticeLSAG:
|
||||
return NewLatticeSignerFromPrivateKey(privateKey)
|
||||
default:
|
||||
return nil, errors.New("unsupported signature scheme")
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateRing generates a ring of random public keys for testing/demo purposes.
|
||||
// In production, the ring should consist of real public keys from the network.
|
||||
func GenerateRing(scheme Scheme, size int) ([][]byte, error) {
|
||||
if size < 2 {
|
||||
return nil, ErrInvalidRingSize
|
||||
}
|
||||
|
||||
ring := make([][]byte, size)
|
||||
for i := 0; i < size; i++ {
|
||||
signer, err := NewSigner(scheme)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ring[i] = signer.PublicKey()
|
||||
}
|
||||
|
||||
return ring, nil
|
||||
}
|
||||
|
||||
// KeyImageStore tracks used key images for double-spend detection.
|
||||
type KeyImageStore interface {
|
||||
// HasKeyImage checks if a key image has been used.
|
||||
HasKeyImage(keyImage []byte) bool
|
||||
|
||||
// AddKeyImage records a key image as used.
|
||||
AddKeyImage(keyImage []byte) error
|
||||
|
||||
// RemoveKeyImage removes a key image (for rollback).
|
||||
RemoveKeyImage(keyImage []byte) error
|
||||
}
|
||||
|
||||
// MemoryKeyImageStore is an in-memory implementation of KeyImageStore.
|
||||
type MemoryKeyImageStore struct {
|
||||
images map[string]struct{}
|
||||
}
|
||||
|
||||
// NewMemoryKeyImageStore creates a new in-memory key image store.
|
||||
func NewMemoryKeyImageStore() *MemoryKeyImageStore {
|
||||
return &MemoryKeyImageStore{
|
||||
images: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// HasKeyImage checks if a key image has been used.
|
||||
func (s *MemoryKeyImageStore) HasKeyImage(keyImage []byte) bool {
|
||||
_, exists := s.images[string(keyImage)]
|
||||
return exists
|
||||
}
|
||||
|
||||
// AddKeyImage records a key image as used.
|
||||
func (s *MemoryKeyImageStore) AddKeyImage(keyImage []byte) error {
|
||||
if s.HasKeyImage(keyImage) {
|
||||
return ErrKeyImageReused
|
||||
}
|
||||
s.images[string(keyImage)] = struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveKeyImage removes a key image.
|
||||
func (s *MemoryKeyImageStore) RemoveKeyImage(keyImage []byte) error {
|
||||
delete(s.images, string(keyImage))
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyAndRecord verifies a ring signature and records its key image.
|
||||
// Returns an error if verification fails or if the key image was already used.
|
||||
func VerifyAndRecord(sig RingSignature, message []byte, ring [][]byte, store KeyImageStore) error {
|
||||
// Check for double-spend first
|
||||
keyImage := sig.KeyImage()
|
||||
if store.HasKeyImage(keyImage) {
|
||||
return ErrKeyImageReused
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
if !sig.Verify(message, ring) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
|
||||
// Record key image
|
||||
return store.AddKeyImage(keyImage)
|
||||
}
|
||||
|
||||
// constantTimeCompare compares two byte slices in constant time to prevent timing attacks.
|
||||
func constantTimeCompare(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
var result byte
|
||||
for i := range a {
|
||||
result |= a[i] ^ b[i]
|
||||
}
|
||||
return result == 0
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package ring_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/crypto/ring"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestE2E_LSAGRingSignature tests the complete LSAG ring signature flow
|
||||
func TestE2E_LSAGRingSignature(t *testing.T) {
|
||||
const ringSize = 5
|
||||
const signerIndex = 2
|
||||
|
||||
// Step 1: Create signers (simulating key generation)
|
||||
signers := make([]ring.Signer, ringSize)
|
||||
for i := 0; i < ringSize; i++ {
|
||||
signer, err := ring.NewSigner(ring.LSAG)
|
||||
require.NoError(t, err)
|
||||
signers[i] = signer
|
||||
}
|
||||
|
||||
// Step 2: Build the ring of public keys
|
||||
ringPubKeys := make([][]byte, ringSize)
|
||||
for i := 0; i < ringSize; i++ {
|
||||
ringPubKeys[i] = signers[i].PublicKey()
|
||||
}
|
||||
|
||||
// Step 3: Sign a message
|
||||
message := []byte("This is a confidential transaction on Q-Chain")
|
||||
sig, err := signers[signerIndex].Sign(message, ringPubKeys, signerIndex)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sig)
|
||||
|
||||
// Step 4: Verify the signature
|
||||
valid := sig.Verify(message, ringPubKeys)
|
||||
require.True(t, valid, "signature should be valid")
|
||||
|
||||
// Step 5: Verify key image linkability
|
||||
keyImage := sig.KeyImage()
|
||||
require.NotEmpty(t, keyImage)
|
||||
|
||||
// Same signer should produce the same key image
|
||||
signerKeyImage := signers[signerIndex].KeyImage()
|
||||
require.True(t, bytes.Equal(keyImage, signerKeyImage), "key images should match")
|
||||
|
||||
// Step 6: Verify serialization/deserialization
|
||||
sigBytes := sig.Bytes()
|
||||
parsedSig, err := ring.ParseSignature(ring.LSAG, sigBytes)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsedSig.Verify(message, ringPubKeys), "parsed signature should verify")
|
||||
|
||||
// Step 7: Verify wrong message fails
|
||||
wrongMessage := []byte("This is a different message")
|
||||
require.False(t, sig.Verify(wrongMessage, ringPubKeys), "wrong message should fail")
|
||||
|
||||
// Step 8: Verify different key produces different key image
|
||||
otherKeyImage := signers[0].KeyImage()
|
||||
require.False(t, bytes.Equal(keyImage, otherKeyImage), "different signers should have different key images")
|
||||
|
||||
t.Logf("LSAG E2E test passed: ring size=%d, signature size=%d bytes, key image=%x...",
|
||||
ringSize, len(sigBytes), keyImage[:8])
|
||||
}
|
||||
|
||||
// TestE2E_LatticeRingSignature tests the complete lattice ring signature flow
|
||||
func TestE2E_LatticeRingSignature(t *testing.T) {
|
||||
const ringSize = 3
|
||||
const signerIndex = 1
|
||||
|
||||
// Step 1: Create lattice signers (post-quantum)
|
||||
signers := make([]ring.Signer, ringSize)
|
||||
for i := 0; i < ringSize; i++ {
|
||||
signer, err := ring.NewSigner(ring.LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
signers[i] = signer
|
||||
}
|
||||
|
||||
// Step 2: Build the ring of public keys
|
||||
ringPubKeys := make([][]byte, ringSize)
|
||||
for i := 0; i < ringSize; i++ {
|
||||
ringPubKeys[i] = signers[i].PublicKey()
|
||||
}
|
||||
|
||||
// Step 3: Sign a message
|
||||
message := []byte("Post-quantum secure anonymous transaction")
|
||||
sig, err := signers[signerIndex].Sign(message, ringPubKeys, signerIndex)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sig)
|
||||
|
||||
// Step 4: Verify the signature
|
||||
valid := sig.Verify(message, ringPubKeys)
|
||||
require.True(t, valid, "signature should be valid")
|
||||
|
||||
// Step 5: Verify key image linkability
|
||||
keyImage := sig.KeyImage()
|
||||
signerKeyImage := signers[signerIndex].KeyImage()
|
||||
require.True(t, bytes.Equal(keyImage, signerKeyImage), "key images should match")
|
||||
|
||||
// Step 6: Verify serialization
|
||||
sigBytes := sig.Bytes()
|
||||
parsedSig, err := ring.ParseSignature(ring.LatticeLSAG, sigBytes)
|
||||
require.NoError(t, err)
|
||||
require.True(t, parsedSig.Verify(message, ringPubKeys), "parsed signature should verify")
|
||||
|
||||
t.Logf("Lattice E2E test passed: ring size=%d, signature size=%d bytes, key image=%x...",
|
||||
ringSize, len(sigBytes), keyImage[:8])
|
||||
}
|
||||
|
||||
// TestE2E_MultipleSignaturesSameKey tests double-spend detection via key images
|
||||
func TestE2E_MultipleSignaturesSameKey(t *testing.T) {
|
||||
const ringSize = 4
|
||||
const signerIndex = 1
|
||||
|
||||
// Create signers
|
||||
signers := make([]ring.Signer, ringSize)
|
||||
for i := 0; i < ringSize; i++ {
|
||||
signer, err := ring.NewSigner(ring.LSAG)
|
||||
require.NoError(t, err)
|
||||
signers[i] = signer
|
||||
}
|
||||
|
||||
// Build ring
|
||||
ringPubKeys := make([][]byte, ringSize)
|
||||
for i := 0; i < ringSize; i++ {
|
||||
ringPubKeys[i] = signers[i].PublicKey()
|
||||
}
|
||||
|
||||
// Sign two different messages with the same key
|
||||
message1 := []byte("Transaction 1: Transfer 100 LUX")
|
||||
message2 := []byte("Transaction 2: Transfer 50 LUX")
|
||||
|
||||
sig1, err := signers[signerIndex].Sign(message1, ringPubKeys, signerIndex)
|
||||
require.NoError(t, err)
|
||||
|
||||
sig2, err := signers[signerIndex].Sign(message2, ringPubKeys, signerIndex)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Both signatures should be valid
|
||||
require.True(t, sig1.Verify(message1, ringPubKeys))
|
||||
require.True(t, sig2.Verify(message2, ringPubKeys))
|
||||
|
||||
// KEY IMAGE SHOULD BE THE SAME - this enables double-spend detection
|
||||
require.True(t, bytes.Equal(sig1.KeyImage(), sig2.KeyImage()),
|
||||
"key images should match for same signer - enables double-spend detection")
|
||||
|
||||
// Use KeyImageStore for double-spend detection
|
||||
store := ring.NewMemoryKeyImageStore()
|
||||
|
||||
// First signature should be accepted
|
||||
err = ring.VerifyAndRecord(sig1, message1, ringPubKeys, store)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second signature from same key should be REJECTED (double-spend)
|
||||
err = ring.VerifyAndRecord(sig2, message2, ringPubKeys, store)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ring.ErrKeyImageReused, err, "should detect double-spend via key image")
|
||||
|
||||
t.Log("Double-spend detection E2E test passed")
|
||||
}
|
||||
|
||||
// TestE2E_DifferentRingSizes tests ring signatures with various ring sizes
|
||||
func TestE2E_DifferentRingSizes(t *testing.T) {
|
||||
sizes := []int{2, 3, 5, 10, 20}
|
||||
|
||||
for _, size := range sizes {
|
||||
t.Run(string(rune('0'+size/10))+string(rune('0'+size%10))+"_members", func(t *testing.T) {
|
||||
// Create ring
|
||||
signers := make([]ring.Signer, size)
|
||||
ringPubKeys := make([][]byte, size)
|
||||
for i := 0; i < size; i++ {
|
||||
signer, err := ring.NewSigner(ring.LSAG)
|
||||
require.NoError(t, err)
|
||||
signers[i] = signer
|
||||
ringPubKeys[i] = signer.PublicKey()
|
||||
}
|
||||
|
||||
// Pick a random signer
|
||||
signerIndex := 0
|
||||
if size > 1 {
|
||||
buf := make([]byte, 1)
|
||||
rand.Read(buf)
|
||||
signerIndex = int(buf[0]) % size
|
||||
}
|
||||
|
||||
// Sign and verify
|
||||
message := []byte("test message for ring size " + string(rune('0'+size/10)) + string(rune('0'+size%10)))
|
||||
sig, err := signers[signerIndex].Sign(message, ringPubKeys, signerIndex)
|
||||
require.NoError(t, err)
|
||||
|
||||
valid := sig.Verify(message, ringPubKeys)
|
||||
require.True(t, valid, "ring size %d should work", size)
|
||||
|
||||
t.Logf("Ring size %d: signature %d bytes", size, len(sig.Bytes()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2E_CrossSchemeIsolation ensures different schemes don't interfere
|
||||
func TestE2E_CrossSchemeIsolation(t *testing.T) {
|
||||
// Create LSAG ring
|
||||
lsagSigner, err := ring.NewSigner(ring.LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
lsagRing := make([][]byte, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
s, _ := ring.NewSigner(ring.LSAG)
|
||||
if i == 0 {
|
||||
lsagRing[i] = lsagSigner.PublicKey()
|
||||
} else {
|
||||
lsagRing[i] = s.PublicKey()
|
||||
}
|
||||
}
|
||||
|
||||
// Create Lattice ring
|
||||
latticeSigner, err := ring.NewSigner(ring.LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
latticeRing := make([][]byte, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
s, _ := ring.NewSigner(ring.LatticeLSAG)
|
||||
if i == 0 {
|
||||
latticeRing[i] = latticeSigner.PublicKey()
|
||||
} else {
|
||||
latticeRing[i] = s.PublicKey()
|
||||
}
|
||||
}
|
||||
|
||||
message := []byte("test isolation")
|
||||
|
||||
// Sign with each scheme
|
||||
lsagSig, err := lsagSigner.Sign(message, lsagRing, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
latticeSig, err := latticeSigner.Sign(message, latticeRing, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify each with correct ring
|
||||
require.True(t, lsagSig.Verify(message, lsagRing))
|
||||
require.True(t, latticeSig.Verify(message, latticeRing))
|
||||
|
||||
// Verify LSAG doesn't verify with lattice ring and vice versa
|
||||
require.False(t, lsagSig.Verify(message, latticeRing), "LSAG should not verify with lattice ring")
|
||||
require.False(t, latticeSig.Verify(message, lsagRing), "Lattice should not verify with LSAG ring")
|
||||
|
||||
t.Log("Cross-scheme isolation test passed")
|
||||
}
|
||||
|
||||
// BenchmarkLSAGSignVerify benchmarks LSAG operations
|
||||
func BenchmarkLSAGSignVerify(b *testing.B) {
|
||||
signer, _ := ring.NewSigner(ring.LSAG)
|
||||
ring5, _ := ring.GenerateRing(ring.LSAG, 5)
|
||||
ring5[0] = signer.PublicKey()
|
||||
message := []byte("benchmark message")
|
||||
|
||||
b.Run("Sign_5", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
signer.Sign(message, ring5, 0)
|
||||
}
|
||||
})
|
||||
|
||||
sig, _ := signer.Sign(message, ring5, 0)
|
||||
|
||||
b.Run("Verify_5", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
sig.Verify(message, ring5)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkLatticeSignVerify benchmarks Lattice operations
|
||||
func BenchmarkLatticeSignVerify(b *testing.B) {
|
||||
signer, _ := ring.NewSigner(ring.LatticeLSAG)
|
||||
ring3, _ := ring.GenerateRing(ring.LatticeLSAG, 3)
|
||||
ring3[0] = signer.PublicKey()
|
||||
message := []byte("benchmark message")
|
||||
|
||||
b.Run("Sign_3", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
signer.Sign(message, ring3, 0)
|
||||
}
|
||||
})
|
||||
|
||||
sig, _ := signer.Sign(message, ring3, 0)
|
||||
|
||||
b.Run("Verify_3", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
sig.Verify(message, ring3)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package ring
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSchemeString(t *testing.T) {
|
||||
require.Equal(t, "LSAG", LSAG.String())
|
||||
require.Equal(t, "Lattice-LSAG", LatticeLSAG.String())
|
||||
require.Equal(t, "DualRing", DualRing.String())
|
||||
require.Equal(t, "unknown", Scheme(99).String())
|
||||
}
|
||||
|
||||
func TestMemoryKeyImageStore(t *testing.T) {
|
||||
store := NewMemoryKeyImageStore()
|
||||
|
||||
keyImage1 := []byte("key-image-1")
|
||||
keyImage2 := []byte("key-image-2")
|
||||
|
||||
// Initially empty
|
||||
require.False(t, store.HasKeyImage(keyImage1))
|
||||
require.False(t, store.HasKeyImage(keyImage2))
|
||||
|
||||
// Add first key image
|
||||
err := store.AddKeyImage(keyImage1)
|
||||
require.NoError(t, err)
|
||||
require.True(t, store.HasKeyImage(keyImage1))
|
||||
require.False(t, store.HasKeyImage(keyImage2))
|
||||
|
||||
// Double-add should fail
|
||||
err = store.AddKeyImage(keyImage1)
|
||||
require.ErrorIs(t, err, ErrKeyImageReused)
|
||||
|
||||
// Add second key image
|
||||
err = store.AddKeyImage(keyImage2)
|
||||
require.NoError(t, err)
|
||||
require.True(t, store.HasKeyImage(keyImage2))
|
||||
|
||||
// Remove first key image
|
||||
err = store.RemoveKeyImage(keyImage1)
|
||||
require.NoError(t, err)
|
||||
require.False(t, store.HasKeyImage(keyImage1))
|
||||
require.True(t, store.HasKeyImage(keyImage2))
|
||||
}
|
||||
|
||||
func TestLSAGSignerCreation(t *testing.T) {
|
||||
// Create signer with random key
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signer)
|
||||
require.Equal(t, LSAG, signer.Scheme())
|
||||
|
||||
// Public key should be 33 bytes (compressed)
|
||||
pubKey := signer.PublicKey()
|
||||
require.Len(t, pubKey, 33)
|
||||
|
||||
// Key image should be 33 bytes
|
||||
keyImage := signer.KeyImage()
|
||||
require.Len(t, keyImage, 33)
|
||||
}
|
||||
|
||||
func TestLSAGSignerFromPrivateKey(t *testing.T) {
|
||||
// Generate private key
|
||||
privateKey := make([]byte, 32)
|
||||
_, err := rand.Read(privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create signer from private key
|
||||
signer, err := NewSignerFromPrivateKey(LSAG, privateKey)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signer)
|
||||
|
||||
// Create another signer from same key
|
||||
signer2, err := NewSignerFromPrivateKey(LSAG, privateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Public keys should match
|
||||
require.Equal(t, signer.PublicKey(), signer2.PublicKey())
|
||||
|
||||
// Key images should match (linkability)
|
||||
require.Equal(t, signer.KeyImage(), signer2.KeyImage())
|
||||
}
|
||||
|
||||
func TestLSAGSignAndVerify(t *testing.T) {
|
||||
// Create signer
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create ring with decoy public keys
|
||||
ringSize := 5
|
||||
signerIndex := 2
|
||||
ring := make([][]byte, ringSize)
|
||||
|
||||
for i := 0; i < ringSize; i++ {
|
||||
if i == signerIndex {
|
||||
ring[i] = signer.PublicKey()
|
||||
} else {
|
||||
decoy, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
}
|
||||
|
||||
// Sign message
|
||||
message := []byte("test message for ring signature")
|
||||
sig, err := signer.Sign(message, ring, signerIndex)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sig)
|
||||
|
||||
// Verify signature
|
||||
require.True(t, sig.Verify(message, ring), "valid signature should verify")
|
||||
require.Equal(t, ringSize, sig.RingSize())
|
||||
require.Equal(t, LSAG, sig.Scheme())
|
||||
|
||||
// Key image should match signer's key image
|
||||
require.Equal(t, signer.KeyImage(), sig.KeyImage())
|
||||
}
|
||||
|
||||
func TestLSAGSignatureInvalidMessage(t *testing.T) {
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
ring := make([][]byte, 3)
|
||||
ring[0] = signer.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
message := []byte("original message")
|
||||
sig, err := signer.Sign(message, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should fail with different message
|
||||
wrongMessage := []byte("wrong message")
|
||||
require.False(t, sig.Verify(wrongMessage, ring), "signature should not verify with wrong message")
|
||||
}
|
||||
|
||||
func TestLSAGSignatureInvalidRing(t *testing.T) {
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
ring := make([][]byte, 3)
|
||||
ring[0] = signer.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
message := []byte("test message")
|
||||
sig, err := signer.Sign(message, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Modify ring (replace a key)
|
||||
wrongRing := make([][]byte, len(ring))
|
||||
copy(wrongRing, ring)
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
wrongRing[1] = decoy.PublicKey()
|
||||
|
||||
require.False(t, sig.Verify(message, wrongRing), "signature should not verify with modified ring")
|
||||
}
|
||||
|
||||
func TestLSAGSignatureSerialization(t *testing.T) {
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
ring := make([][]byte, 4)
|
||||
ring[1] = signer.PublicKey()
|
||||
for i := 0; i < 4; i++ {
|
||||
if i != 1 {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
}
|
||||
|
||||
message := []byte("serialization test")
|
||||
sig, err := signer.Sign(message, ring, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Serialize
|
||||
data := sig.Bytes()
|
||||
require.NotEmpty(t, data)
|
||||
|
||||
// Deserialize
|
||||
parsed, err := ParseSignature(LSAG, data)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parsed)
|
||||
|
||||
// Verify parsed signature
|
||||
require.True(t, parsed.Verify(message, ring))
|
||||
require.Equal(t, sig.KeyImage(), parsed.KeyImage())
|
||||
require.Equal(t, sig.RingSize(), parsed.RingSize())
|
||||
}
|
||||
|
||||
func TestLSAGKeyImageLinkability(t *testing.T) {
|
||||
// Create signer
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create ring
|
||||
ring := make([][]byte, 3)
|
||||
ring[0] = signer.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
// Sign two different messages
|
||||
msg1 := []byte("message 1")
|
||||
msg2 := []byte("message 2")
|
||||
|
||||
sig1, err := signer.Sign(msg1, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
sig2, err := signer.Sign(msg2, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Key images should be the same (linkable)
|
||||
require.Equal(t, sig1.KeyImage(), sig2.KeyImage(), "key images should match for same signer")
|
||||
|
||||
// Different signer should have different key image
|
||||
otherSigner, _ := NewSigner(LSAG)
|
||||
otherRing := make([][]byte, 3)
|
||||
otherRing[0] = otherSigner.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
otherRing[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
sig3, err := otherSigner.Sign(msg1, otherRing, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, sig1.KeyImage(), sig3.KeyImage(), "different signers should have different key images")
|
||||
}
|
||||
|
||||
func TestLSAGDoubleSpendDetection(t *testing.T) {
|
||||
store := NewMemoryKeyImageStore()
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
ring := make([][]byte, 3)
|
||||
ring[0] = signer.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
msg1 := []byte("transaction 1")
|
||||
sig1, err := signer.Sign(msg1, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// First verification and record should succeed
|
||||
err = VerifyAndRecord(sig1, msg1, ring, store)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second transaction with same key should fail (double spend)
|
||||
msg2 := []byte("transaction 2")
|
||||
sig2, err := signer.Sign(msg2, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = VerifyAndRecord(sig2, msg2, ring, store)
|
||||
require.ErrorIs(t, err, ErrKeyImageReused)
|
||||
}
|
||||
|
||||
func TestLSAGInvalidSignerIndex(t *testing.T) {
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
ring := make([][]byte, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
message := []byte("test")
|
||||
|
||||
// Signer's key is not in ring
|
||||
_, err = signer.Sign(message, ring, 0)
|
||||
require.Error(t, err)
|
||||
|
||||
// Invalid index
|
||||
ring[1] = signer.PublicKey()
|
||||
_, err = signer.Sign(message, ring, -1)
|
||||
require.ErrorIs(t, err, ErrInvalidSignerIndex)
|
||||
|
||||
_, err = signer.Sign(message, ring, 5)
|
||||
require.ErrorIs(t, err, ErrInvalidSignerIndex)
|
||||
}
|
||||
|
||||
func TestLSAGTooSmallRing(t *testing.T) {
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ring of size 1 should fail
|
||||
ring := [][]byte{signer.PublicKey()}
|
||||
message := []byte("test")
|
||||
|
||||
_, err = signer.Sign(message, ring, 0)
|
||||
require.ErrorIs(t, err, ErrInvalidRingSize)
|
||||
}
|
||||
|
||||
func TestLatticeSignerCreation(t *testing.T) {
|
||||
signer, err := NewSigner(LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, signer)
|
||||
require.Equal(t, LatticeLSAG, signer.Scheme())
|
||||
|
||||
pubKey := signer.PublicKey()
|
||||
require.NotEmpty(t, pubKey)
|
||||
// ML-DSA-65 public key size
|
||||
require.Len(t, pubKey, 1952)
|
||||
|
||||
keyImage := signer.KeyImage()
|
||||
require.NotEmpty(t, keyImage)
|
||||
// Key image is SHA-256 hash
|
||||
require.Len(t, keyImage, 32)
|
||||
}
|
||||
|
||||
func TestLatticeSignAndVerify(t *testing.T) {
|
||||
// ML-DSA based ring signature test
|
||||
signer, err := NewSigner(LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create ring with 3 members (smaller for faster tests with ML-DSA)
|
||||
ringSize := 3
|
||||
signerIndex := 1
|
||||
ring := make([][]byte, ringSize)
|
||||
|
||||
for i := 0; i < ringSize; i++ {
|
||||
if i == signerIndex {
|
||||
ring[i] = signer.PublicKey()
|
||||
} else {
|
||||
decoy, err := NewSigner(LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
}
|
||||
|
||||
message := []byte("post-quantum ring signature test")
|
||||
sig, err := signer.Sign(message, ring, signerIndex)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sig)
|
||||
|
||||
require.True(t, sig.Verify(message, ring), "lattice signature should verify")
|
||||
require.Equal(t, ringSize, sig.RingSize())
|
||||
require.Equal(t, LatticeLSAG, sig.Scheme())
|
||||
|
||||
// Key image should match signer's key image
|
||||
require.Equal(t, signer.KeyImage(), sig.KeyImage())
|
||||
}
|
||||
|
||||
func TestLatticeSerialization(t *testing.T) {
|
||||
signer, err := NewSigner(LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
ring := make([][]byte, 3)
|
||||
ring[0] = signer.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LatticeLSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
message := []byte("lattice serialization test")
|
||||
sig, err := signer.Sign(message, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Serialize
|
||||
data := sig.Bytes()
|
||||
require.NotEmpty(t, data)
|
||||
|
||||
// Deserialize
|
||||
parsed, err := ParseSignature(LatticeLSAG, data)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, parsed)
|
||||
|
||||
// Verify
|
||||
require.True(t, parsed.Verify(message, ring))
|
||||
require.Equal(t, sig.KeyImage(), parsed.KeyImage())
|
||||
require.Equal(t, sig.RingSize(), parsed.RingSize())
|
||||
}
|
||||
|
||||
func TestLatticeKeyImageLinkability(t *testing.T) {
|
||||
// Create signer
|
||||
signer, err := NewSigner(LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create ring
|
||||
ring := make([][]byte, 3)
|
||||
ring[0] = signer.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LatticeLSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
// Sign two different messages
|
||||
msg1 := []byte("message 1")
|
||||
msg2 := []byte("message 2")
|
||||
|
||||
sig1, err := signer.Sign(msg1, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
sig2, err := signer.Sign(msg2, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Key images should be the same (linkable)
|
||||
require.Equal(t, sig1.KeyImage(), sig2.KeyImage(), "key images should match for same signer")
|
||||
|
||||
// Different signer should have different key image
|
||||
otherSigner, _ := NewSigner(LatticeLSAG)
|
||||
otherRing := make([][]byte, 3)
|
||||
otherRing[0] = otherSigner.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LatticeLSAG)
|
||||
otherRing[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
sig3, err := otherSigner.Sign(msg1, otherRing, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, sig1.KeyImage(), sig3.KeyImage(), "different signers should have different key images")
|
||||
}
|
||||
|
||||
func TestLatticeSignatureInvalidMessage(t *testing.T) {
|
||||
signer, err := NewSigner(LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
ring := make([][]byte, 3)
|
||||
ring[0] = signer.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LatticeLSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
message := []byte("original message")
|
||||
sig, err := signer.Sign(message, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should fail with different message
|
||||
wrongMessage := []byte("wrong message")
|
||||
require.False(t, sig.Verify(wrongMessage, ring), "signature should not verify with wrong message")
|
||||
}
|
||||
|
||||
func TestLatticeSignatureInvalidRing(t *testing.T) {
|
||||
signer, err := NewSigner(LatticeLSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
ring := make([][]byte, 3)
|
||||
ring[0] = signer.PublicKey()
|
||||
for i := 1; i < 3; i++ {
|
||||
decoy, _ := NewSigner(LatticeLSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
|
||||
message := []byte("test message")
|
||||
sig, err := signer.Sign(message, ring, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Modify ring (replace a key)
|
||||
wrongRing := make([][]byte, len(ring))
|
||||
copy(wrongRing, ring)
|
||||
decoy, _ := NewSigner(LatticeLSAG)
|
||||
wrongRing[1] = decoy.PublicKey()
|
||||
|
||||
require.False(t, sig.Verify(message, wrongRing), "signature should not verify with modified ring")
|
||||
}
|
||||
|
||||
func TestGenerateRing(t *testing.T) {
|
||||
// LSAG ring
|
||||
ring, err := GenerateRing(LSAG, 5)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ring, 5)
|
||||
for _, pk := range ring {
|
||||
require.Len(t, pk, 33) // Compressed secp256k1 public key
|
||||
}
|
||||
|
||||
// Lattice ring (ML-DSA-65 public keys)
|
||||
ring, err = GenerateRing(LatticeLSAG, 3)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, ring, 3)
|
||||
for _, pk := range ring {
|
||||
require.Len(t, pk, 1952) // ML-DSA-65 public key size
|
||||
}
|
||||
|
||||
// Invalid ring size
|
||||
_, err = GenerateRing(LSAG, 1)
|
||||
require.ErrorIs(t, err, ErrInvalidRingSize)
|
||||
}
|
||||
|
||||
func TestLSAGDifferentRingSizes(t *testing.T) {
|
||||
testCases := []int{2, 3, 5, 10, 20}
|
||||
|
||||
for _, ringSize := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
signer, err := NewSigner(LSAG)
|
||||
require.NoError(t, err)
|
||||
|
||||
signerIndex := ringSize / 2
|
||||
ring := make([][]byte, ringSize)
|
||||
for i := 0; i < ringSize; i++ {
|
||||
if i == signerIndex {
|
||||
ring[i] = signer.PublicKey()
|
||||
} else {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
}
|
||||
|
||||
message := []byte("ring size test")
|
||||
sig, err := signer.Sign(message, ring, signerIndex)
|
||||
require.NoError(t, err)
|
||||
require.True(t, sig.Verify(message, ring))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLSAGSign(b *testing.B) {
|
||||
signer, _ := NewSigner(LSAG)
|
||||
|
||||
ring := make([][]byte, 10)
|
||||
ring[5] = signer.PublicKey()
|
||||
for i := 0; i < 10; i++ {
|
||||
if i != 5 {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
}
|
||||
|
||||
message := []byte("benchmark message")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = signer.Sign(message, ring, 5)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLSAGVerify(b *testing.B) {
|
||||
signer, _ := NewSigner(LSAG)
|
||||
|
||||
ring := make([][]byte, 10)
|
||||
ring[5] = signer.PublicKey()
|
||||
for i := 0; i < 10; i++ {
|
||||
if i != 5 {
|
||||
decoy, _ := NewSigner(LSAG)
|
||||
ring[i] = decoy.PublicKey()
|
||||
}
|
||||
}
|
||||
|
||||
message := []byte("benchmark message")
|
||||
sig, _ := signer.Sign(message, ring, 5)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sig.Verify(message, ring)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantTimeCompare(t *testing.T) {
|
||||
a := []byte{1, 2, 3, 4}
|
||||
b := []byte{1, 2, 3, 4}
|
||||
c := []byte{1, 2, 3, 5}
|
||||
d := []byte{1, 2, 3}
|
||||
|
||||
require.True(t, constantTimeCompare(a, b))
|
||||
require.False(t, constantTimeCompare(a, c))
|
||||
require.False(t, constantTimeCompare(a, d))
|
||||
require.True(t, bytes.Equal(a, b))
|
||||
}
|
||||
Reference in New Issue
Block a user