Files
crypto/ecdsa/canonical_test.go

277 lines
7.4 KiB
Go
Raw Normal View History

2025-10-09 15:10:39 -04:00
package ecdsa
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"math/big"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCanonicalizeSignature(t *testing.T) {
curve := elliptic.P256()
N := curve.Params().N
halfN := new(big.Int).Div(N, big.NewInt(2))
// Test canonical signature (s <= N/2)
r := big.NewInt(12345)
s := new(big.Int).Sub(halfN, big.NewInt(1)) // s = N/2 - 1
rCanon, sCanon, err := CanonicalizeSignature(r, s, curve)
require.NoError(t, err)
assert.Equal(t, r, rCanon)
assert.Equal(t, s, sCanon)
// Test non-canonical signature (s > N/2)
sNonCanon := new(big.Int).Add(halfN, big.NewInt(1)) // s = N/2 + 1
rCanon, sCanon, err = CanonicalizeSignature(r, sNonCanon, curve)
require.NoError(t, err)
assert.Equal(t, r, rCanon)
// sCanon should be N - sNonCanon
expected := new(big.Int).Sub(N, sNonCanon)
assert.Equal(t, expected, sCanon)
}
func TestIsSignatureCanonical(t *testing.T) {
curve := elliptic.P256()
N := curve.Params().N
halfN := new(big.Int).Div(N, big.NewInt(2))
// Test canonical signature
r := big.NewInt(12345)
s := halfN // s = N/2 (boundary case, still canonical)
assert.True(t, IsSignatureCanonical(r, s, curve))
// Test non-canonical signature
sNonCanon := new(big.Int).Add(halfN, big.NewInt(1))
assert.False(t, IsSignatureCanonical(r, sNonCanon, curve))
// Test invalid r (r = 0)
assert.False(t, IsSignatureCanonical(big.NewInt(0), s, curve))
// Test invalid r (r >= N)
assert.False(t, IsSignatureCanonical(N, s, curve))
// Test invalid s (s = 0)
assert.False(t, IsSignatureCanonical(r, big.NewInt(0), curve))
// Test nil inputs
assert.False(t, IsSignatureCanonical(nil, s, curve))
assert.False(t, IsSignatureCanonical(r, nil, curve))
assert.False(t, IsSignatureCanonical(r, s, nil))
}
func TestValidateAndCanonicalizeSignature(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
message := []byte("test message")
hash := sha256.Sum256(message)
// Sign with standard ECDSA
r, s, err := ecdsa.Sign(rand.Reader, priv, hash[:])
require.NoError(t, err)
// Validate and canonicalize
rCanon, sCanon, err := ValidateAndCanonicalizeSignature(&priv.PublicKey, hash[:], r, s)
require.NoError(t, err)
// Verify canonical signature
valid := ecdsa.Verify(&priv.PublicKey, hash[:], rCanon, sCanon)
assert.True(t, valid)
// Ensure signature is canonical
assert.True(t, IsSignatureCanonical(rCanon, sCanon, priv.Curve))
// Test with invalid signature
wrongR := new(big.Int).Add(r, big.NewInt(1))
_, _, err = ValidateAndCanonicalizeSignature(&priv.PublicKey, hash[:], wrongR, s)
assert.Error(t, err)
}
func TestRejectNonCanonical(t *testing.T) {
curve := elliptic.P256()
N := curve.Params().N
halfN := new(big.Int).Div(N, big.NewInt(2))
r := big.NewInt(12345)
// Test canonical signature
sCanon := new(big.Int).Sub(halfN, big.NewInt(1))
err := RejectNonCanonical(r, sCanon, curve)
assert.NoError(t, err)
// Test non-canonical signature
sNonCanon := new(big.Int).Add(halfN, big.NewInt(1))
err = RejectNonCanonical(r, sNonCanon, curve)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not in canonical form")
// Test nil inputs
err = RejectNonCanonical(nil, sCanon, curve)
assert.Error(t, err)
err = RejectNonCanonical(r, nil, curve)
assert.Error(t, err)
err = RejectNonCanonical(r, sCanon, nil)
assert.Error(t, err)
}
func TestCompareSignatures(t *testing.T) {
curve := elliptic.P256()
N := curve.Params().N
halfN := new(big.Int).Div(N, big.NewInt(2))
r := big.NewInt(12345)
s1 := new(big.Int).Sub(halfN, big.NewInt(1))
// Same signature should be equal
equal, err := CompareSignatures(r, s1, r, s1, curve)
require.NoError(t, err)
assert.True(t, equal)
// Canonical and non-canonical versions of same signature should be equal
s1NonCanon := new(big.Int).Sub(N, s1)
equal, err = CompareSignatures(r, s1, r, s1NonCanon, curve)
require.NoError(t, err)
assert.True(t, equal)
// Different signatures should not be equal
s2 := new(big.Int).Sub(halfN, big.NewInt(10))
equal, err = CompareSignatures(r, s1, r, s2, curve)
require.NoError(t, err)
assert.False(t, equal)
}
func TestSignatureBytes(t *testing.T) {
curve := elliptic.P256()
N := curve.Params().N
halfN := new(big.Int).Div(N, big.NewInt(2))
r := big.NewInt(12345)
s := new(big.Int).Sub(halfN, big.NewInt(1))
// Convert to bytes
sigBytes, err := SignatureBytes(r, s, curve)
require.NoError(t, err)
assert.Len(t, sigBytes, 64) // 32 bytes for r, 32 bytes for s on P-256
// Test with non-canonical s - should be canonicalized
sNonCanon := new(big.Int).Sub(N, s)
sigBytesNonCanon, err := SignatureBytes(r, sNonCanon, curve)
require.NoError(t, err)
// Both should produce the same bytes (after canonicalization)
assert.Equal(t, sigBytes, sigBytesNonCanon)
}
func TestSignatureFromBytes(t *testing.T) {
curve := elliptic.P256()
// Create a signature
r := big.NewInt(12345)
s := big.NewInt(67890)
// Convert to bytes
sigBytes, err := SignatureBytes(r, s, curve)
require.NoError(t, err)
// Convert back from bytes
rRecovered, sRecovered, err := SignatureFromBytes(sigBytes, curve)
require.NoError(t, err)
// Should be canonical
assert.True(t, IsSignatureCanonical(rRecovered, sRecovered, curve))
// Values should match (after canonicalization)
rCanon, sCanon, err := CanonicalizeSignature(r, s, curve)
require.NoError(t, err)
assert.Equal(t, rCanon, rRecovered)
assert.Equal(t, sCanon, sRecovered)
// Test with invalid length
_, _, err = SignatureFromBytes([]byte("too short"), curve)
assert.Error(t, err)
}
func TestNormalizeSignature(t *testing.T) {
curve := elliptic.P256()
N := curve.Params().N
halfN := new(big.Int).Div(N, big.NewInt(2))
r := big.NewInt(12345)
s := new(big.Int).Add(halfN, big.NewInt(1)) // Non-canonical
// Normalize should canonicalize
rNorm, sNorm, err := NormalizeSignature(r, s, curve)
require.NoError(t, err)
assert.True(t, IsSignatureCanonical(rNorm, sNorm, curve))
assert.Equal(t, r, rNorm)
// sNorm should be N - s
expected := new(big.Int).Sub(N, s)
assert.Equal(t, expected, sNorm)
}
func TestCanonicalWithRealSignatures(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
message := []byte("test canonical signatures")
hash := sha256.Sum256(message)
// Generate multiple signatures and ensure all can be canonicalized
for i := 0; i < 10; i++ {
r, s, err := ecdsa.Sign(rand.Reader, priv, hash[:])
require.NoError(t, err)
// Canonicalize
rCanon, sCanon, err := CanonicalizeSignature(r, s, priv.Curve)
require.NoError(t, err)
// Should be canonical
assert.True(t, IsSignatureCanonical(rCanon, sCanon, priv.Curve))
// Should still verify
valid := ecdsa.Verify(&priv.PublicKey, hash[:], rCanon, sCanon)
assert.True(t, valid)
}
}
func BenchmarkCanonicalizeSignature(b *testing.B) {
curve := elliptic.P256()
N := curve.Params().N
halfN := new(big.Int).Div(N, big.NewInt(2))
r := big.NewInt(12345)
s := new(big.Int).Add(halfN, big.NewInt(1))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _, _ = CanonicalizeSignature(r, s, curve)
}
}
func BenchmarkIsSignatureCanonical(b *testing.B) {
curve := elliptic.P256()
N := curve.Params().N
halfN := new(big.Int).Div(N, big.NewInt(2))
r := big.NewInt(12345)
s := new(big.Int).Sub(halfN, big.NewInt(1))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = IsSignatureCanonical(r, s, curve)
}
}