feat: add ring signatures for anonymous group signing

This commit is contained in:
Zach Kelling
2025-12-20 14:02:56 -08:00
parent a3d4cd1b71
commit 37bd49c508
5 changed files with 1866 additions and 0 deletions
+352
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+295
View File
@@ -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)
}
})
}
+574
View File
@@ -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))
}