mirror of
https://github.com/sonr-io/crypto.git
synced 2026-01-11 20:08:57 +00:00
467 lines
13 KiB
Go
467 lines
13 KiB
Go
// Package crypto provides comprehensive security tests for cryptographic implementations
|
|
package crypto
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math/big"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sonr-io/crypto/argon2"
|
|
ecdsaPkg "github.com/sonr-io/crypto/ecdsa"
|
|
"github.com/sonr-io/crypto/password"
|
|
"github.com/sonr-io/crypto/wasm"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestTimingAttackResistance verifies constant-time operations
|
|
func TestTimingAttackResistance(t *testing.T) {
|
|
// Test Argon2 constant-time comparison
|
|
kdf := argon2.New(argon2.LightConfig())
|
|
password := []byte("correct-password")
|
|
hash, err := kdf.HashPassword(password)
|
|
require.NoError(t, err)
|
|
|
|
// Measure timing for correct vs incorrect passwords
|
|
correctTimes := make([]time.Duration, 100)
|
|
incorrectTimes := make([]time.Duration, 100)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
// Time correct password
|
|
start := time.Now()
|
|
_, _ = argon2.VerifyPassword(password, hash)
|
|
correctTimes[i] = time.Since(start)
|
|
|
|
// Time incorrect password
|
|
wrongPassword := []byte("wrong-password-x")
|
|
start = time.Now()
|
|
_, _ = argon2.VerifyPassword(wrongPassword, hash)
|
|
incorrectTimes[i] = time.Since(start)
|
|
}
|
|
|
|
// Calculate average times
|
|
var correctAvg, incorrectAvg time.Duration
|
|
for i := 0; i < 100; i++ {
|
|
correctAvg += correctTimes[i]
|
|
incorrectAvg += incorrectTimes[i]
|
|
}
|
|
correctAvg /= 100
|
|
incorrectAvg /= 100
|
|
|
|
// Times should be similar (within 20% variance)
|
|
diff := correctAvg - incorrectAvg
|
|
if diff < 0 {
|
|
diff = -diff
|
|
}
|
|
maxDiff := correctAvg / 5 // 20% threshold
|
|
|
|
assert.Less(t, diff, maxDiff, "timing difference suggests non-constant-time comparison")
|
|
}
|
|
|
|
// TestSignatureMalleabilityAttack tests protection against signature malleability
|
|
func TestSignatureMalleabilityAttack(t *testing.T) {
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
message := []byte("transaction data")
|
|
hash := sha256.Sum256(message)
|
|
|
|
// Create deterministic signature
|
|
r, s, err := ecdsaPkg.DeterministicSign(priv, hash[:])
|
|
require.NoError(t, err)
|
|
|
|
// Verify original signature
|
|
assert.True(t, ecdsa.Verify(&priv.PublicKey, hash[:], r, s))
|
|
|
|
// Create malleable signature (r, -s mod N)
|
|
N := priv.Curve.Params().N
|
|
sMalleable := new(big.Int).Sub(N, s)
|
|
|
|
// Standard ECDSA would accept this, but our canonical check should reject it
|
|
assert.False(t, ecdsaPkg.VerifyDeterministic(&priv.PublicKey, hash[:], r, sMalleable),
|
|
"malleable signature should be rejected")
|
|
|
|
// Canonicalize should fix it
|
|
rCanon, sCanon, err := ecdsaPkg.CanonicalizeSignature(r, sMalleable, priv.Curve)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, s, sCanon, "canonicalized signature should match original")
|
|
assert.True(t, ecdsa.Verify(&priv.PublicKey, hash[:], rCanon, sCanon))
|
|
}
|
|
|
|
// TestNonceReuseAttack verifies protection against nonce reuse
|
|
func TestNonceReuseAttack(t *testing.T) {
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
// Sign same message multiple times with deterministic ECDSA
|
|
message := []byte("sensitive data")
|
|
hash := sha256.Sum256(message)
|
|
|
|
signatures := make([]struct{ r, s *big.Int }, 10)
|
|
for i := 0; i < 10; i++ {
|
|
r, s, err := ecdsaPkg.DeterministicSign(priv, hash[:])
|
|
require.NoError(t, err)
|
|
signatures[i].r = r
|
|
signatures[i].s = s
|
|
}
|
|
|
|
// All signatures should be identical (deterministic)
|
|
for i := 1; i < 10; i++ {
|
|
assert.Equal(t, signatures[0].r, signatures[i].r,
|
|
"deterministic signatures should use same nonce")
|
|
assert.Equal(t, signatures[0].s, signatures[i].s,
|
|
"deterministic signatures should be identical")
|
|
}
|
|
|
|
// Different messages should use different nonces
|
|
message2 := []byte("different data")
|
|
hash2 := sha256.Sum256(message2)
|
|
r2, s2, err := ecdsaPkg.DeterministicSign(priv, hash2[:])
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, signatures[0].r, r2,
|
|
"different messages must use different nonces")
|
|
assert.NotEqual(t, signatures[0].s, s2,
|
|
"different messages must produce different signatures")
|
|
}
|
|
|
|
// TestPasswordDictionaryAttack tests resistance to dictionary attacks
|
|
func TestPasswordDictionaryAttack(t *testing.T) {
|
|
commonPasswords := []string{
|
|
"password", "123456", "password123", "admin", "letmein",
|
|
"welcome", "monkey", "dragon", "master", "qwerty",
|
|
}
|
|
|
|
validator := password.NewValidator(password.DefaultPasswordConfig())
|
|
|
|
// All common passwords should be rejected
|
|
for _, pwd := range commonPasswords {
|
|
err := validator.Validate([]byte(pwd))
|
|
assert.Error(t, err, "common password '%s' should be rejected", pwd)
|
|
}
|
|
|
|
// Test that Argon2 makes dictionary attacks expensive
|
|
kdf := argon2.New(argon2.DefaultConfig())
|
|
|
|
start := time.Now()
|
|
for _, pwd := range commonPasswords {
|
|
hash, err := kdf.HashPassword([]byte(pwd))
|
|
require.NoError(t, err)
|
|
|
|
// Try to crack with dictionary
|
|
for _, attempt := range commonPasswords {
|
|
_, _ = argon2.VerifyPassword([]byte(attempt), hash)
|
|
}
|
|
}
|
|
elapsed := time.Since(start)
|
|
|
|
// Should take significant time (> 1 second for 100 attempts)
|
|
assert.Greater(t, elapsed, 1*time.Second,
|
|
"Argon2 should make dictionary attacks expensive")
|
|
}
|
|
|
|
// TestWASMHashCollisionAttack tests resistance to hash collision attacks
|
|
func TestWASMHashCollisionAttack(t *testing.T) {
|
|
verifier := wasm.NewHashVerifier()
|
|
|
|
// Create two different modules
|
|
module1 := []byte("wasm module version 1.0")
|
|
module2 := []byte("wasm module version 2.0")
|
|
|
|
hash1 := verifier.ComputeHash(module1)
|
|
hash2 := verifier.ComputeHash(module2)
|
|
|
|
// Hashes must be different
|
|
assert.NotEqual(t, hash1, hash2,
|
|
"different modules must have different hashes")
|
|
|
|
// Test collision resistance with similar modules
|
|
similarModules := make([][]byte, 100)
|
|
hashes := make(map[string]bool)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
similarModules[i] = []byte(fmt.Sprintf("wasm module version 1.%d", i))
|
|
hash := verifier.ComputeHash(similarModules[i])
|
|
|
|
// Check for collisions
|
|
assert.False(t, hashes[hash],
|
|
"hash collision detected for module %d", i)
|
|
hashes[hash] = true
|
|
}
|
|
}
|
|
|
|
// TestRaceConditionSafety tests thread safety of cryptographic operations
|
|
func TestRaceConditionSafety(t *testing.T) {
|
|
// Test concurrent Argon2 operations
|
|
kdf := argon2.New(argon2.LightConfig())
|
|
password := []byte("test-password")
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, 100)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
hash, err := kdf.HashPassword(password)
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
|
|
valid, err := argon2.VerifyPassword(password, hash)
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
if !valid {
|
|
errors <- fmt.Errorf("password verification failed")
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
// Check for any errors
|
|
for err := range errors {
|
|
t.Errorf("concurrent operation failed: %v", err)
|
|
}
|
|
|
|
// Test concurrent ECDSA signing
|
|
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
message := []byte("concurrent test")
|
|
hash := sha256.Sum256(message)
|
|
|
|
signatures := make(chan struct{ r, s *big.Int }, 100)
|
|
var sigWg sync.WaitGroup
|
|
|
|
for i := 0; i < 100; i++ {
|
|
sigWg.Add(1)
|
|
go func() {
|
|
defer sigWg.Done()
|
|
|
|
r, s, err := ecdsaPkg.DeterministicSign(priv, hash[:])
|
|
if err == nil {
|
|
signatures <- struct{ r, s *big.Int }{r, s}
|
|
}
|
|
}()
|
|
}
|
|
|
|
sigWg.Wait()
|
|
close(signatures)
|
|
|
|
// All signatures should be identical (deterministic)
|
|
var firstSig struct{ r, s *big.Int }
|
|
count := 0
|
|
for sig := range signatures {
|
|
if count == 0 {
|
|
firstSig = sig
|
|
} else {
|
|
assert.Equal(t, firstSig.r, sig.r,
|
|
"concurrent signatures should be identical")
|
|
assert.Equal(t, firstSig.s, sig.s,
|
|
"concurrent signatures should be identical")
|
|
}
|
|
count++
|
|
}
|
|
assert.Equal(t, 100, count, "all concurrent operations should succeed")
|
|
}
|
|
|
|
// TestMemoryExhaustionAttack tests resistance to memory exhaustion
|
|
func TestMemoryExhaustionAttack(t *testing.T) {
|
|
// Test with high memory Argon2 config
|
|
config := &argon2.Config{
|
|
Time: 1,
|
|
Memory: 128 * 1024, // 128MB
|
|
Parallelism: 4,
|
|
SaltLength: 32,
|
|
KeyLength: 32,
|
|
}
|
|
|
|
// Validate config prevents excessive memory use
|
|
err := argon2.ValidateConfig(config)
|
|
assert.NoError(t, err, "reasonable memory config should be valid")
|
|
|
|
// Test with excessive memory request
|
|
excessiveConfig := &argon2.Config{
|
|
Time: 1,
|
|
Memory: 4 * 1024 * 1024, // 4GB - should be rejected
|
|
Parallelism: 4,
|
|
SaltLength: 32,
|
|
KeyLength: 32,
|
|
}
|
|
|
|
// This should be caught by reasonable implementations
|
|
kdf := argon2.New(excessiveConfig)
|
|
|
|
// Attempt derivation with memory limit
|
|
password := []byte("test")
|
|
salt := make([]byte, 32)
|
|
_, _ = rand.Read(salt)
|
|
|
|
// Monitor memory usage (simplified test)
|
|
start := time.Now()
|
|
_ = kdf.DeriveKey(password, salt)
|
|
elapsed := time.Since(start)
|
|
|
|
// Excessive memory should cause noticeable delay
|
|
assert.Less(t, elapsed, 10*time.Second,
|
|
"operation should complete in reasonable time")
|
|
}
|
|
|
|
// TestSaltReuseVulnerability tests that salts are unique
|
|
func TestSaltReuseVulnerability(t *testing.T) {
|
|
kdf := argon2.New(argon2.DefaultConfig())
|
|
|
|
salts := make(map[string]bool)
|
|
|
|
// Generate many salts
|
|
for i := 0; i < 1000; i++ {
|
|
salt, err := kdf.GenerateSalt()
|
|
require.NoError(t, err)
|
|
|
|
saltStr := string(salt)
|
|
assert.False(t, salts[saltStr],
|
|
"salt reuse detected at iteration %d", i)
|
|
salts[saltStr] = true
|
|
}
|
|
}
|
|
|
|
// TestWeakRandomnessDetection tests quality of random number generation
|
|
func TestWeakRandomnessDetection(t *testing.T) {
|
|
// Generate multiple ECDSA keys and check for patterns
|
|
keys := make([]*ecdsa.PrivateKey, 100)
|
|
|
|
for i := 0; i < 100; i++ {
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
keys[i] = key
|
|
}
|
|
|
|
// Check that all private keys are unique
|
|
seen := make(map[string]bool)
|
|
for i, key := range keys {
|
|
keyStr := key.D.String()
|
|
assert.False(t, seen[keyStr],
|
|
"duplicate private key detected at index %d", i)
|
|
seen[keyStr] = true
|
|
}
|
|
|
|
// Check distribution (simplified chi-square test)
|
|
// Count leading bits
|
|
zeros := 0
|
|
ones := 0
|
|
for _, key := range keys {
|
|
if key.D.Bit(0) == 0 {
|
|
zeros++
|
|
} else {
|
|
ones++
|
|
}
|
|
}
|
|
|
|
// Should be roughly 50/50 distribution
|
|
// For 100 samples, we expect ~50 each, but allow for statistical variance
|
|
// Using binomial distribution, 99% confidence interval is approximately ±3 standard deviations
|
|
// Standard deviation = sqrt(n*p*(1-p)) = sqrt(100*0.5*0.5) = 5
|
|
// So we allow difference up to 3*5 = 15, but we'll be more lenient with 25
|
|
diff := zeros - ones
|
|
if diff < 0 {
|
|
diff = -diff
|
|
}
|
|
assert.Less(t, diff, 25,
|
|
"random bit distribution appears biased: %d zeros, %d ones", zeros, ones)
|
|
}
|
|
|
|
// TestCryptographicAgility tests ability to switch algorithms
|
|
func TestCryptographicAgility(t *testing.T) {
|
|
// Test different Argon2 configurations
|
|
configs := []*argon2.Config{
|
|
argon2.LightConfig(),
|
|
argon2.DefaultConfig(),
|
|
argon2.HighSecurityConfig(),
|
|
}
|
|
|
|
password := []byte("test-password")
|
|
|
|
for i, config := range configs {
|
|
kdf := argon2.New(config)
|
|
|
|
hash, err := kdf.HashPassword(password)
|
|
require.NoError(t, err, "config %d should work", i)
|
|
|
|
// Verify with same config
|
|
valid, err := argon2.VerifyPassword(password, hash)
|
|
require.NoError(t, err)
|
|
assert.True(t, valid, "config %d verification should succeed", i)
|
|
}
|
|
|
|
// Test different elliptic curves
|
|
curves := []elliptic.Curve{
|
|
elliptic.P224(),
|
|
elliptic.P256(),
|
|
elliptic.P384(),
|
|
elliptic.P521(),
|
|
}
|
|
|
|
message := []byte("test message")
|
|
hashMsg := sha256.Sum256(message)
|
|
|
|
for _, curve := range curves {
|
|
priv, err := ecdsa.GenerateKey(curve, rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
r, s, err := ecdsaPkg.DeterministicSign(priv, hashMsg[:])
|
|
require.NoError(t, err)
|
|
|
|
valid := ecdsa.Verify(&priv.PublicKey, hashMsg[:], r, s)
|
|
assert.True(t, valid, "curve %s should work", curve.Params().Name)
|
|
}
|
|
}
|
|
|
|
// BenchmarkSecurityOperations benchmarks security-critical operations
|
|
func BenchmarkSecurityOperations(b *testing.B) {
|
|
b.Run("Argon2Default", func(b *testing.B) {
|
|
kdf := argon2.New(argon2.DefaultConfig())
|
|
password := []byte("benchmark-password")
|
|
salt, _ := kdf.GenerateSalt()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = kdf.DeriveKey(password, salt)
|
|
}
|
|
})
|
|
|
|
b.Run("ECDSADeterministic", func(b *testing.B) {
|
|
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
hash := sha256.Sum256([]byte("benchmark"))
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, _ = ecdsaPkg.DeterministicSign(priv, hash[:])
|
|
}
|
|
})
|
|
|
|
b.Run("WASMHashVerification", func(b *testing.B) {
|
|
verifier := wasm.NewHashVerifier()
|
|
module := make([]byte, 1024*1024) // 1MB module
|
|
rand.Read(module)
|
|
hash := verifier.ComputeHash(module)
|
|
verifier.AddTrustedHash("bench", hash)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = verifier.VerifyHash("bench", module)
|
|
}
|
|
})
|
|
}
|