mirror of
https://github.com/luxfi/crypto.git
synced 2026-10-30 16:59:13 +00:00
LatticeSignature.Verify recomputed a tag and compared it to the one supplied with the signature. Every input to that hash is either public or attacker- chosen: the message and ring are public, and keyImage and every s[i] arrive in the signature itself. latticeCommitment is SHA-512(s ‖ pk ‖ keyImage ‖ i) and nothing more. No secret key, and no value derived from one, was ever tested. So a forgery costs nothing: pick 32 random bytes for keyImage and 64 per ring member, run the same hash this function runs, and send the result. Verify returns true for any ring, for any message, without knowing any member's key. Signing derived s[signerIndex] from the secret, but verification could not tell that apart from the random s[j] beside it. The file's own header already said this scheme is not post-quantum and is for development only. That understates it — it is not a signature scheme at all, and it was reachable as ring.LatticeLSAG. Verification now refuses. Restoring it means a real LSAG construction where each s[i] satisfies a per-position relation binding the signer's secret to the challenge, so that a reproducible tag is a consequence of signing rather than the definition of validity. The tests now pin the refusal: LSAG still verifies, LatticeLSAG must not, and serialization round-trips while verification still refuses — the encoding was never the problem. Co-authored-by: Hanzo Dev <[email protected]>
301 lines
9.2 KiB
Go
301 lines
9.2 KiB
Go
// 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.False(t, valid,
|
|
"LatticeLSAG must not verify while its construction is universally forgeable")
|
|
|
|
// 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.False(t, parsedSig.Verify(message, ringPubKeys),
|
|
"LatticeLSAG must not verify after a round-trip either: the encoding is fine, the construction is not")
|
|
|
|
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)
|
|
|
|
// LSAG verifies with its own ring. LatticeLSAG does not verify at all —
|
|
// its tag is recomputable from attacker-supplied inputs alone, so its
|
|
// verifier fails closed rather than accept a forgery.
|
|
require.True(t, lsagSig.Verify(message, lsagRing))
|
|
require.False(t, latticeSig.Verify(message, latticeRing),
|
|
"LatticeLSAG must not verify while its construction is universally forgeable")
|
|
|
|
// 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)
|
|
}
|
|
})
|
|
}
|