mirror of
https://github.com/btouchard/ackify-ce.git
synced 2026-02-28 10:48:47 -06:00
e95185f9c7
Major refactoring to modernize the application architecture: Backend changes: - Restructure API with v1 versioning and modular handlers - Add comprehensive OpenAPI specification - Implement RESTful endpoints for documents, signatures, admin - Add checksum verification system for document integrity - Add server-side runtime injection of ACKIFY_BASE_URL and meta tags - Generate dynamic Open Graph/Twitter Card meta tags for unfurling - Remove legacy HTML template handlers - Isolate backend source on dedicated folder - Improve tests suite Frontend changes: - Migrate from Go templates to Vue.js 3 SPA with TypeScript - Add Tailwind CSS with shadcn/vue components - Implement i18n support (fr, en, es, de, it) - Add admin dashboard for document and signer management - Add signature tracking with file checksum verification - Add embed page with sign button linking to main app - Implement dark mode and accessibility features - Auto load file to compute checksum Infrastructure: - Update Dockerfile for SPA build process - Simplify deployment with embedded frontend assets - Add migration for checksum_verifications table This enables better UX, proper link previews on social platforms, and provides a foundation for future enhancements.
392 lines
13 KiB
Go
392 lines
13 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
package crypto
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/btouchard/ackify-ce/backend/internal/domain/models"
|
|
)
|
|
|
|
// TestCryptoIntegration tests the integrations between signature generation and nonce generation
|
|
func TestCryptoIntegration(t *testing.T) {
|
|
t.Run("signature with generated nonce", func(t *testing.T) {
|
|
signer, err := NewEd25519Signer()
|
|
require.NoError(t, err)
|
|
|
|
user := testUserAlice
|
|
docID := "integrations-test-doc"
|
|
timestamp := time.Now().UTC()
|
|
|
|
// Generate a nonce
|
|
nonce, err := GenerateNonce()
|
|
require.NoError(t, err)
|
|
|
|
// Create signature with generated nonce
|
|
hash, sig, err := signer.CreateSignature(docID, user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEmpty(t, hash)
|
|
assert.NotEmpty(t, sig)
|
|
|
|
// Verify hash is SHA-256
|
|
hashBytes, err := base64.StdEncoding.DecodeString(hash)
|
|
require.NoError(t, err)
|
|
assert.Len(t, hashBytes, 32, "Hash should be SHA-256 (32 bytes)")
|
|
|
|
// Verify signature is Ed25519
|
|
sigBytes, err := base64.StdEncoding.DecodeString(sig)
|
|
require.NoError(t, err)
|
|
assert.Len(t, sigBytes, 64, "Signature should be Ed25519 (64 bytes)")
|
|
})
|
|
|
|
t.Run("different nonces produce different signatures", func(t *testing.T) {
|
|
signer, err := NewEd25519Signer()
|
|
require.NoError(t, err)
|
|
|
|
user := testUserBob
|
|
docID := "nonce-diff-test"
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
// Generate two different nonces
|
|
nonce1, err := GenerateNonce()
|
|
require.NoError(t, err)
|
|
|
|
nonce2, err := GenerateNonce()
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEqual(t, nonce1, nonce2, "Nonces should be different")
|
|
|
|
// Create signatures with different nonces
|
|
hash1, sig1, err := signer.CreateSignature(docID, user, timestamp, nonce1, "")
|
|
require.NoError(t, err)
|
|
|
|
hash2, sig2, err := signer.CreateSignature(docID, user, timestamp, nonce2, "")
|
|
require.NoError(t, err)
|
|
|
|
// Different nonces should produce different signatures
|
|
assert.NotEqual(t, hash1, hash2, "Different nonces should produce different hashes")
|
|
assert.NotEqual(t, sig1, sig2, "Different nonces should produce different signatures")
|
|
})
|
|
|
|
t.Run("replay attack prevention", func(t *testing.T) {
|
|
signer, err := NewEd25519Signer()
|
|
require.NoError(t, err)
|
|
|
|
user := testUserCharlie
|
|
docID := "replay-test-doc"
|
|
timestamp := time.Now().UTC()
|
|
|
|
// Simulate multiple signature attempts for same document
|
|
signatures := make(map[string]bool)
|
|
nonces := make(map[string]bool)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
// Generate unique nonce for each attempt
|
|
nonce, err := GenerateNonce()
|
|
require.NoError(t, err)
|
|
|
|
// Verify nonce is unique
|
|
assert.False(t, nonces[nonce], "Nonce should be unique for replay protection")
|
|
nonces[nonce] = true
|
|
|
|
// Create signature
|
|
hash, sig, err := signer.CreateSignature(docID, user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Verify signature is unique
|
|
assert.False(t, signatures[sig], "Signature should be unique due to nonce")
|
|
signatures[sig] = true
|
|
|
|
// All should have different hashes due to nonce
|
|
assert.NotEmpty(t, hash)
|
|
assert.NotEmpty(t, sig)
|
|
}
|
|
|
|
assert.Len(t, signatures, 10, "All signatures should be unique")
|
|
assert.Len(t, nonces, 10, "All nonces should be unique")
|
|
})
|
|
}
|
|
|
|
// TestSHA256Hashing tests SHA-256 hashing functionality indirectly through signature creation
|
|
func TestSHA256Hashing(t *testing.T) {
|
|
signer, err := NewEd25519Signer()
|
|
require.NoError(t, err)
|
|
|
|
t.Run("consistent hashing", func(t *testing.T) {
|
|
user := testUserAlice
|
|
docID := "hash-test-doc"
|
|
timestamp := time.Date(2024, 3, 15, 10, 30, 0, 0, time.UTC)
|
|
nonce := "consistent-nonce"
|
|
|
|
// Create signature multiple times
|
|
hash1, _, err := signer.CreateSignature(docID, user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
hash2, _, err := signer.CreateSignature(docID, user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, hash1, hash2, "Same input should produce same hash")
|
|
})
|
|
|
|
t.Run("hash changes with input changes", func(t *testing.T) {
|
|
user := testUserBob
|
|
baseTimestamp := time.Date(2024, 4, 1, 14, 0, 0, 0, time.UTC)
|
|
baseNonce := "base-nonce"
|
|
|
|
// Base signature
|
|
baseHash, _, err := signer.CreateSignature("base-doc", user, baseTimestamp, baseNonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Test different document ID
|
|
hash1, _, err := signer.CreateSignature("different-doc", user, baseTimestamp, baseNonce, "")
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, baseHash, hash1, "Different docID should produce different hash")
|
|
|
|
// Test different user
|
|
differentUser := testUserCharlie
|
|
hash2, _, err := signer.CreateSignature("base-doc", differentUser, baseTimestamp, baseNonce, "")
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, baseHash, hash2, "Different user should produce different hash")
|
|
|
|
// Test different timestamp
|
|
differentTime := baseTimestamp.Add(time.Hour)
|
|
hash3, _, err := signer.CreateSignature("base-doc", user, differentTime, baseNonce, "")
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, baseHash, hash3, "Different timestamp should produce different hash")
|
|
|
|
// Test different nonce
|
|
hash4, _, err := signer.CreateSignature("base-doc", user, baseTimestamp, "different-nonce", "")
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, baseHash, hash4, "Different nonce should produce different hash")
|
|
})
|
|
|
|
t.Run("hash properties", func(t *testing.T) {
|
|
user := testUserAlice
|
|
docID := "props-test"
|
|
timestamp := time.Now().UTC()
|
|
nonce := "props-nonce"
|
|
|
|
hashB64, _, err := signer.CreateSignature(docID, user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Decode hash
|
|
hashBytes, err := base64.StdEncoding.DecodeString(hashB64)
|
|
require.NoError(t, err)
|
|
|
|
// SHA-256 properties
|
|
assert.Len(t, hashBytes, 32, "SHA-256 hash should be 32 bytes")
|
|
assert.NotEqual(t, make([]byte, 32), hashBytes, "Hash should not be all zeros")
|
|
|
|
// Verify it's actually SHA-256 by recreating manually
|
|
expectedPayload := "doc_id=" + docID + "\n" +
|
|
"user_sub=" + user.Sub + "\n" +
|
|
"user_email=" + user.NormalizedEmail() + "\n" +
|
|
"signed_at=" + timestamp.UTC().Format(time.RFC3339Nano) + "\n" +
|
|
"nonce=" + nonce + "\n"
|
|
|
|
expectedHash := sha256.Sum256([]byte(expectedPayload))
|
|
expectedHashB64 := base64.StdEncoding.EncodeToString(expectedHash[:])
|
|
|
|
assert.Equal(t, expectedHashB64, hashB64, "Hash should match manual SHA-256 calculation")
|
|
})
|
|
|
|
t.Run("avalanche effect", func(t *testing.T) {
|
|
// Test that small changes in input produce large changes in hash (avalanche effect)
|
|
user := testUserAlice
|
|
timestamp := time.Now().UTC()
|
|
nonce := "avalanche-test"
|
|
|
|
// Base hash
|
|
baseHash, _, err := signer.CreateSignature("testdoc", user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Change one character in docID
|
|
modHash, _, err := signer.CreateSignature("testdoC", user, timestamp, nonce, "") // Changed 'c' to 'C'
|
|
require.NoError(t, err)
|
|
|
|
// Decode both hashes
|
|
baseBytes, err := base64.StdEncoding.DecodeString(baseHash)
|
|
require.NoError(t, err)
|
|
|
|
modBytes, err := base64.StdEncoding.DecodeString(modHash)
|
|
require.NoError(t, err)
|
|
|
|
// Count different bits (should be approximately 50% for good hash function)
|
|
differentBits := 0
|
|
for i := range baseBytes {
|
|
xor := baseBytes[i] ^ modBytes[i]
|
|
for xor != 0 {
|
|
differentBits++
|
|
xor &= xor - 1 // Clear lowest set bit
|
|
}
|
|
}
|
|
|
|
// SHA-256 should have good avalanche effect
|
|
totalBits := len(baseBytes) * 8
|
|
percentage := float64(differentBits) / float64(totalBits)
|
|
|
|
// Should be roughly 50% different bits (allow 30-70% range for single test)
|
|
assert.Greater(t, percentage, 0.3, "Avalanche effect should change at least 30%% of bits")
|
|
assert.Less(t, percentage, 0.7, "Avalanche effect should not change more than 70%% of bits")
|
|
})
|
|
}
|
|
|
|
// TestCorruptionDetection tests that signature corruption is detectable
|
|
func TestCorruptionDetection(t *testing.T) {
|
|
signer, err := NewEd25519Signer()
|
|
require.NoError(t, err)
|
|
|
|
t.Run("hash corruption detection", func(t *testing.T) {
|
|
user := testUserAlice
|
|
docID := "corruption-test"
|
|
timestamp := time.Now().UTC()
|
|
nonce := "corruption-nonce"
|
|
|
|
originalHash, originalSig, err := signer.CreateSignature(docID, user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Corrupt the hash
|
|
hashBytes, err := base64.StdEncoding.DecodeString(originalHash)
|
|
require.NoError(t, err)
|
|
|
|
hashBytes[0] ^= 0x01 // Flip one bit
|
|
corruptedHash := base64.StdEncoding.EncodeToString(hashBytes)
|
|
|
|
assert.NotEqual(t, originalHash, corruptedHash, "Corrupted hash should be different")
|
|
|
|
// Original signature won't match corrupted hash when verified
|
|
// (This would be caught during verification process)
|
|
assert.NotEmpty(t, originalSig)
|
|
})
|
|
|
|
t.Run("signature corruption detection", func(t *testing.T) {
|
|
user := testUserBob
|
|
docID := "sig-corruption-test"
|
|
timestamp := time.Now().UTC()
|
|
nonce := "sig-corruption-nonce"
|
|
|
|
originalHash, originalSig, err := signer.CreateSignature(docID, user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Corrupt the signature
|
|
sigBytes, err := base64.StdEncoding.DecodeString(originalSig)
|
|
require.NoError(t, err)
|
|
|
|
sigBytes[63] ^= 0xFF // Flip bits in last byte
|
|
corruptedSig := base64.StdEncoding.EncodeToString(sigBytes)
|
|
|
|
assert.NotEqual(t, originalSig, corruptedSig, "Corrupted signature should be different")
|
|
assert.NotEmpty(t, originalHash) // Hash should remain valid
|
|
})
|
|
|
|
t.Run("payload tampering detection", func(t *testing.T) {
|
|
user := testUserCharlie
|
|
docID := "tamper-test"
|
|
timestamp := time.Date(2024, 5, 1, 16, 45, 0, 0, time.UTC)
|
|
nonce := "tamper-nonce"
|
|
|
|
// Original signature
|
|
originalHash, originalSig, err := signer.CreateSignature(docID, user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Create signature for tampered data (different docID)
|
|
tamperedHash, tamperedSig, err := signer.CreateSignature("tampered-doc", user, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Tampered data produces different hash and signature
|
|
assert.NotEqual(t, originalHash, tamperedHash, "Tampered payload should produce different hash")
|
|
assert.NotEqual(t, originalSig, tamperedSig, "Tampered payload should produce different signature")
|
|
})
|
|
}
|
|
|
|
// TestBusinessRuleEnforcement tests that cryptographic functions support business rules
|
|
func TestBusinessRuleEnforcement(t *testing.T) {
|
|
t.Run("unique signatures per document-user pair", func(t *testing.T) {
|
|
signer, err := NewEd25519Signer()
|
|
require.NoError(t, err)
|
|
|
|
user := testUserAlice
|
|
docID := "business-rule-test"
|
|
timestamp := time.Now().UTC()
|
|
|
|
// Create signatures with different nonces (simulating different attempts)
|
|
nonce1, err := GenerateNonce()
|
|
require.NoError(t, err)
|
|
|
|
nonce2, err := GenerateNonce()
|
|
require.NoError(t, err)
|
|
|
|
hash1, sig1, err := signer.CreateSignature(docID, user, timestamp, nonce1, "")
|
|
require.NoError(t, err)
|
|
|
|
hash2, sig2, err := signer.CreateSignature(docID, user, timestamp, nonce2, "")
|
|
require.NoError(t, err)
|
|
|
|
// Different nonces create different signatures
|
|
// This supports business rule that each signing attempt must be unique
|
|
assert.NotEqual(t, hash1, hash2, "Different nonces should create different hashes")
|
|
assert.NotEqual(t, sig1, sig2, "Different nonces should create different signatures")
|
|
})
|
|
|
|
t.Run("email normalization consistency", func(t *testing.T) {
|
|
signer, err := NewEd25519Signer()
|
|
require.NoError(t, err)
|
|
|
|
// Create users with same email in different cases
|
|
user1 := &models.User{
|
|
Sub: "user-case-test",
|
|
Email: "Test.User@EXAMPLE.COM",
|
|
Name: "Test User",
|
|
}
|
|
|
|
user2 := &models.User{
|
|
Sub: "user-case-test",
|
|
Email: "test.user@example.com",
|
|
Name: "Test User",
|
|
}
|
|
|
|
docID := "email-case-test"
|
|
timestamp := time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC)
|
|
nonce := "case-nonce"
|
|
|
|
hash1, sig1, err := signer.CreateSignature(docID, user1, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
hash2, sig2, err := signer.CreateSignature(docID, user2, timestamp, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Should produce same signature due to email normalization
|
|
assert.Equal(t, hash1, hash2, "Email case should not affect signature due to normalization")
|
|
assert.Equal(t, sig1, sig2, "Email case should not affect signature due to normalization")
|
|
})
|
|
|
|
t.Run("timestamp precision handling", func(t *testing.T) {
|
|
signer, err := NewEd25519Signer()
|
|
require.NoError(t, err)
|
|
|
|
user := testUserAlice
|
|
docID := "timestamp-precision-test"
|
|
nonce := "precision-nonce"
|
|
|
|
// Test that nanosecond precision is maintained in signatures
|
|
timestamp1 := time.Date(2024, 7, 1, 10, 30, 15, 123456789, time.UTC)
|
|
timestamp2 := time.Date(2024, 7, 1, 10, 30, 15, 123456790, time.UTC) // 1 nanosecond different
|
|
|
|
hash1, sig1, err := signer.CreateSignature(docID, user, timestamp1, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
hash2, sig2, err := signer.CreateSignature(docID, user, timestamp2, nonce, "")
|
|
require.NoError(t, err)
|
|
|
|
// Even 1 nanosecond difference should produce different signatures
|
|
assert.NotEqual(t, hash1, hash2, "Nanosecond precision should be maintained")
|
|
assert.NotEqual(t, sig1, sig2, "Nanosecond precision should be maintained")
|
|
})
|
|
}
|