No commit suggestions generated

This commit is contained in:
2025-10-09 15:10:39 -04:00
commit a934caa7d3
323 changed files with 98121 additions and 0 deletions

336
wasm/signer.go Normal file
View File

@@ -0,0 +1,336 @@
// Package wasm provides cryptographic signing and verification for WebAssembly modules
package wasm
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"sync"
"time"
)
// Signer provides Ed25519 digital signature operations for WASM modules
type Signer struct {
privateKey ed25519.PrivateKey
publicKey ed25519.PublicKey
}
// NewSigner creates a new signer with a generated Ed25519 key pair
func NewSigner() (*Signer, error) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate Ed25519 key pair: %w", err)
}
return &Signer{
privateKey: priv,
publicKey: pub,
}, nil
}
// NewSignerFromPrivateKey creates a signer from an existing private key
func NewSignerFromPrivateKey(privateKey ed25519.PrivateKey) (*Signer, error) {
if len(privateKey) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid private key size: expected %d, got %d",
ed25519.PrivateKeySize, len(privateKey))
}
publicKey := privateKey.Public().(ed25519.PublicKey)
return &Signer{
privateKey: privateKey,
publicKey: publicKey,
}, nil
}
// Sign creates an Ed25519 signature for the given WASM bytecode
func (s *Signer) Sign(wasmBytes []byte) ([]byte, error) {
if s.privateKey == nil {
return nil, fmt.Errorf("private key not initialized")
}
signature := ed25519.Sign(s.privateKey, wasmBytes)
return signature, nil
}
// GetPublicKey returns the public key bytes
func (s *Signer) GetPublicKey() []byte {
return s.publicKey
}
// GetPublicKeyHex returns the public key as hex string
func (s *Signer) GetPublicKeyHex() string {
return hex.EncodeToString(s.publicKey)
}
// ExportPrivateKey exports the private key (handle with care)
func (s *Signer) ExportPrivateKey() []byte {
return s.privateKey
}
// SignatureVerifier verifies Ed25519 signatures on WASM modules
type SignatureVerifier struct {
trustedKeys map[string]ed25519.PublicKey
mu sync.RWMutex
}
// NewSignatureVerifier creates a new signature verifier
func NewSignatureVerifier() *SignatureVerifier {
return &SignatureVerifier{
trustedKeys: make(map[string]ed25519.PublicKey),
}
}
// AddTrustedKey adds a trusted public key for signature verification
func (v *SignatureVerifier) AddTrustedKey(keyID string, publicKey ed25519.PublicKey) error {
if len(publicKey) != ed25519.PublicKeySize {
return fmt.Errorf("invalid public key size: expected %d, got %d",
ed25519.PublicKeySize, len(publicKey))
}
v.mu.Lock()
defer v.mu.Unlock()
v.trustedKeys[keyID] = publicKey
return nil
}
// AddTrustedKeyFromHex adds a trusted public key from hex string
func (v *SignatureVerifier) AddTrustedKeyFromHex(keyID string, publicKeyHex string) error {
publicKey, err := hex.DecodeString(publicKeyHex)
if err != nil {
return fmt.Errorf("failed to decode public key hex: %w", err)
}
return v.AddTrustedKey(keyID, ed25519.PublicKey(publicKey))
}
// Verify verifies a signature against trusted public keys
func (v *SignatureVerifier) Verify(wasmBytes []byte, signature []byte) error {
v.mu.RLock()
defer v.mu.RUnlock()
if len(v.trustedKeys) == 0 {
return fmt.Errorf("no trusted keys configured")
}
// Try to verify with any trusted key
for keyID, publicKey := range v.trustedKeys {
if ed25519.Verify(publicKey, wasmBytes, signature) {
// Signature valid with this key
return nil
}
_ = keyID // Key ID available for logging if needed
}
return fmt.Errorf("signature verification failed: no matching trusted key")
}
// VerifyWithKey verifies a signature with a specific key
func (v *SignatureVerifier) VerifyWithKey(keyID string, wasmBytes []byte, signature []byte) error {
v.mu.RLock()
publicKey, exists := v.trustedKeys[keyID]
v.mu.RUnlock()
if !exists {
return fmt.Errorf("trusted key not found: %s", keyID)
}
if !ed25519.Verify(publicKey, wasmBytes, signature) {
return fmt.Errorf("signature verification failed for key: %s", keyID)
}
return nil
}
// RemoveTrustedKey removes a trusted key
func (v *SignatureVerifier) RemoveTrustedKey(keyID string) {
v.mu.Lock()
defer v.mu.Unlock()
delete(v.trustedKeys, keyID)
}
// GetTrustedKeyIDs returns all trusted key IDs
func (v *SignatureVerifier) GetTrustedKeyIDs() []string {
v.mu.RLock()
defer v.mu.RUnlock()
ids := make([]string, 0, len(v.trustedKeys))
for id := range v.trustedKeys {
ids = append(ids, id)
}
return ids
}
// SignedModule represents a WASM module with its signature
type SignedModule struct {
Module []byte `json:"-"` // WASM bytecode (excluded from JSON)
Hash string `json:"hash"` // SHA256 hash of module
Signature []byte `json:"signature"` // Ed25519 signature
SignerID string `json:"signer_id"` // ID of signing key
Timestamp time.Time `json:"timestamp"` // Signing timestamp
Version string `json:"version"` // Module version
}
// SignModule creates a signed module package
func SignModule(signer *Signer, module []byte, signerID string, version string) (*SignedModule, error) {
// Compute hash
hashVerifier := NewHashVerifier()
hash := hashVerifier.ComputeHash(module)
// Sign the module
signature, err := signer.Sign(module)
if err != nil {
return nil, fmt.Errorf("failed to sign module: %w", err)
}
return &SignedModule{
Module: module,
Hash: hash,
Signature: signature,
SignerID: signerID,
Timestamp: time.Now(),
Version: version,
}, nil
}
// VerifySignedModule verifies a signed module
func VerifySignedModule(verifier *SignatureVerifier, module *SignedModule) error {
// Verify hash matches
hashVerifier := NewHashVerifier()
computedHash := hashVerifier.ComputeHash(module.Module)
if computedHash != module.Hash {
return fmt.Errorf("hash mismatch: expected %s, got %s", module.Hash, computedHash)
}
// Verify signature
if module.SignerID != "" {
return verifier.VerifyWithKey(module.SignerID, module.Module, module.Signature)
}
return verifier.Verify(module.Module, module.Signature)
}
// SignatureManifest contains signature metadata for a WASM module
type SignatureManifest struct {
ModuleHash string `json:"module_hash"`
Signatures []SignatureEntry `json:"signatures"`
TrustedKeys []TrustedKeyEntry `json:"trusted_keys"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// SignatureEntry represents a single signature in the manifest
type SignatureEntry struct {
Signature string `json:"signature"` // Base64 encoded
SignerID string `json:"signer_id"`
Timestamp time.Time `json:"timestamp"`
Algorithm string `json:"algorithm"` // Always "Ed25519"
}
// TrustedKeyEntry represents a trusted public key
type TrustedKeyEntry struct {
KeyID string `json:"key_id"`
PublicKey string `json:"public_key"` // Base64 encoded
AddedAt time.Time `json:"added_at"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
Purpose string `json:"purpose"` // e.g., "code-signing"
}
// CreateSignatureManifest creates a manifest for module signatures
func CreateSignatureManifest(module []byte, signer *Signer, signerID string) (*SignatureManifest, error) {
hashVerifier := NewHashVerifier()
moduleHash := hashVerifier.ComputeHash(module)
signature, err := signer.Sign(module)
if err != nil {
return nil, fmt.Errorf("failed to sign module: %w", err)
}
manifest := &SignatureManifest{
ModuleHash: moduleHash,
Signatures: []SignatureEntry{
{
Signature: base64.StdEncoding.EncodeToString(signature),
SignerID: signerID,
Timestamp: time.Now(),
Algorithm: "Ed25519",
},
},
TrustedKeys: []TrustedKeyEntry{
{
KeyID: signerID,
PublicKey: base64.StdEncoding.EncodeToString(signer.GetPublicKey()),
AddedAt: time.Now(),
Purpose: "code-signing",
},
},
CreatedAt: time.Now(),
}
return manifest, nil
}
// VerifyWithManifest verifies a module using a signature manifest
func VerifyWithManifest(module []byte, manifest *SignatureManifest) error {
// Verify hash
hashVerifier := NewHashVerifier()
computedHash := hashVerifier.ComputeHash(module)
if computedHash != manifest.ModuleHash {
return fmt.Errorf("module hash mismatch")
}
// Check expiration
if manifest.ExpiresAt != nil && time.Now().After(*manifest.ExpiresAt) {
return fmt.Errorf("signature manifest has expired")
}
// Create verifier with trusted keys from manifest
verifier := NewSignatureVerifier()
for _, key := range manifest.TrustedKeys {
// Check key expiration
if key.ExpiresAt != nil && time.Now().After(*key.ExpiresAt) {
continue // Skip expired keys
}
publicKey, err := base64.StdEncoding.DecodeString(key.PublicKey)
if err != nil {
return fmt.Errorf("failed to decode public key: %w", err)
}
if err := verifier.AddTrustedKey(key.KeyID, ed25519.PublicKey(publicKey)); err != nil {
return fmt.Errorf("failed to add trusted key: %w", err)
}
}
// Verify at least one signature
for _, sig := range manifest.Signatures {
signature, err := base64.StdEncoding.DecodeString(sig.Signature)
if err != nil {
continue // Skip invalid signatures
}
if err := verifier.VerifyWithKey(sig.SignerID, module, signature); err == nil {
// At least one valid signature found
return nil
}
}
return fmt.Errorf("no valid signatures found in manifest")
}
// ExportManifest exports a signature manifest as JSON
func ExportManifest(manifest *SignatureManifest) ([]byte, error) {
return json.MarshalIndent(manifest, "", " ")
}
// ImportManifest imports a signature manifest from JSON
func ImportManifest(data []byte) (*SignatureManifest, error) {
var manifest SignatureManifest
if err := json.Unmarshal(data, &manifest); err != nil {
return nil, fmt.Errorf("failed to parse manifest: %w", err)
}
return &manifest, nil
}

361
wasm/signer_test.go Normal file
View File

@@ -0,0 +1,361 @@
package wasm
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSigner_NewSigner(t *testing.T) {
signer, err := NewSigner()
require.NoError(t, err)
require.NotNil(t, signer)
assert.NotNil(t, signer.privateKey)
assert.NotNil(t, signer.publicKey)
assert.Equal(t, ed25519.PrivateKeySize, len(signer.privateKey))
assert.Equal(t, ed25519.PublicKeySize, len(signer.publicKey))
}
func TestSigner_NewSignerFromPrivateKey(t *testing.T) {
// Generate a key pair
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
// Create signer from private key
signer, err := NewSignerFromPrivateKey(priv)
require.NoError(t, err)
assert.Equal(t, priv, signer.privateKey)
assert.Equal(t, pub, signer.publicKey)
// Test invalid key size
invalidKey := []byte("too short")
_, err = NewSignerFromPrivateKey(ed25519.PrivateKey(invalidKey))
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid private key size")
}
func TestSigner_Sign(t *testing.T) {
signer, err := NewSigner()
require.NoError(t, err)
// Test data
wasmBytes := []byte("test wasm module content")
// Sign the data
signature, err := signer.Sign(wasmBytes)
require.NoError(t, err)
assert.Equal(t, ed25519.SignatureSize, len(signature))
// Verify the signature
valid := ed25519.Verify(signer.publicKey, wasmBytes, signature)
assert.True(t, valid)
// Test signing different data produces different signature
differentData := []byte("different content")
signature2, err := signer.Sign(differentData)
require.NoError(t, err)
assert.NotEqual(t, signature, signature2)
}
func TestSignatureVerifier_AddTrustedKey(t *testing.T) {
verifier := NewSignatureVerifier()
// Generate a key pair
pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
// Add trusted key
err = verifier.AddTrustedKey("test-key", pub)
assert.NoError(t, err)
// Test invalid key size
invalidKey := []byte("invalid")
err = verifier.AddTrustedKey("invalid-key", ed25519.PublicKey(invalidKey))
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid public key size")
}
func TestSignatureVerifier_AddTrustedKeyFromHex(t *testing.T) {
verifier := NewSignatureVerifier()
// Generate a key pair
pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
// Add key from hex
hexKey := hex.EncodeToString(pub)
err = verifier.AddTrustedKeyFromHex("hex-key", hexKey)
assert.NoError(t, err)
// Test invalid hex
err = verifier.AddTrustedKeyFromHex("bad-hex", "not-hex")
assert.Error(t, err)
}
func TestSignatureVerifier_Verify(t *testing.T) {
// Create signer and verifier
signer, err := NewSigner()
require.NoError(t, err)
verifier := NewSignatureVerifier()
// Test data
wasmBytes := []byte("test wasm module")
// Sign the data
signature, err := signer.Sign(wasmBytes)
require.NoError(t, err)
// Test verification without trusted keys
err = verifier.Verify(wasmBytes, signature)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no trusted keys")
// Add trusted key
err = verifier.AddTrustedKey("signer1", signer.publicKey)
require.NoError(t, err)
// Test successful verification
err = verifier.Verify(wasmBytes, signature)
assert.NoError(t, err)
// Test verification with wrong data
wrongData := []byte("wrong data")
err = verifier.Verify(wrongData, signature)
assert.Error(t, err)
assert.Contains(t, err.Error(), "signature verification failed")
// Test verification with wrong signature
wrongSignature := make([]byte, ed25519.SignatureSize)
err = verifier.Verify(wasmBytes, wrongSignature)
assert.Error(t, err)
}
func TestSignatureVerifier_VerifyWithKey(t *testing.T) {
signer1, err := NewSigner()
require.NoError(t, err)
signer2, err := NewSigner()
require.NoError(t, err)
verifier := NewSignatureVerifier()
verifier.AddTrustedKey("key1", signer1.publicKey)
verifier.AddTrustedKey("key2", signer2.publicKey)
wasmBytes := []byte("test module")
signature1, _ := signer1.Sign(wasmBytes)
signature2, _ := signer2.Sign(wasmBytes)
// Verify with correct key
err = verifier.VerifyWithKey("key1", wasmBytes, signature1)
assert.NoError(t, err)
err = verifier.VerifyWithKey("key2", wasmBytes, signature2)
assert.NoError(t, err)
// Verify with wrong key
err = verifier.VerifyWithKey("key1", wasmBytes, signature2)
assert.Error(t, err)
// Verify with non-existent key
err = verifier.VerifyWithKey("key3", wasmBytes, signature1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "trusted key not found")
}
func TestSignatureVerifier_Management(t *testing.T) {
verifier := NewSignatureVerifier()
// Generate keys
pub1, _, _ := ed25519.GenerateKey(rand.Reader)
pub2, _, _ := ed25519.GenerateKey(rand.Reader)
// Add keys
verifier.AddTrustedKey("key1", pub1)
verifier.AddTrustedKey("key2", pub2)
// Get key IDs
ids := verifier.GetTrustedKeyIDs()
assert.Len(t, ids, 2)
assert.Contains(t, ids, "key1")
assert.Contains(t, ids, "key2")
// Remove key
verifier.RemoveTrustedKey("key1")
ids = verifier.GetTrustedKeyIDs()
assert.Len(t, ids, 1)
assert.NotContains(t, ids, "key1")
assert.Contains(t, ids, "key2")
}
func TestSignedModule(t *testing.T) {
signer, err := NewSigner()
require.NoError(t, err)
module := []byte("test wasm module")
signerID := "test-signer"
version := "v1.0.0"
// Create signed module
signed, err := SignModule(signer, module, signerID, version)
require.NoError(t, err)
assert.Equal(t, module, signed.Module)
assert.NotEmpty(t, signed.Hash)
assert.NotEmpty(t, signed.Signature)
assert.Equal(t, signerID, signed.SignerID)
assert.Equal(t, version, signed.Version)
assert.False(t, signed.Timestamp.IsZero())
// Verify signed module
verifier := NewSignatureVerifier()
verifier.AddTrustedKey(signerID, signer.publicKey)
err = VerifySignedModule(verifier, signed)
assert.NoError(t, err)
// Test with tampered module
signed.Module = []byte("tampered")
err = VerifySignedModule(verifier, signed)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hash mismatch")
}
func TestSignatureManifest(t *testing.T) {
signer, err := NewSigner()
require.NoError(t, err)
module := []byte("test wasm module")
signerID := "manifest-signer"
// Create manifest
manifest, err := CreateSignatureManifest(module, signer, signerID)
require.NoError(t, err)
assert.NotEmpty(t, manifest.ModuleHash)
assert.Len(t, manifest.Signatures, 1)
assert.Len(t, manifest.TrustedKeys, 1)
assert.Equal(t, signerID, manifest.Signatures[0].SignerID)
assert.Equal(t, "Ed25519", manifest.Signatures[0].Algorithm)
assert.Equal(t, signerID, manifest.TrustedKeys[0].KeyID)
assert.Equal(t, "code-signing", manifest.TrustedKeys[0].Purpose)
// Verify with manifest
err = VerifyWithManifest(module, manifest)
assert.NoError(t, err)
// Test with wrong module
wrongModule := []byte("wrong module")
err = VerifyWithManifest(wrongModule, manifest)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hash mismatch")
// Test with expired manifest
expired := time.Now().Add(-1 * time.Hour)
manifest.ExpiresAt = &expired
err = VerifyWithManifest(module, manifest)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expired")
}
func TestManifestSerialization(t *testing.T) {
signer, err := NewSigner()
require.NoError(t, err)
module := []byte("test module")
manifest, err := CreateSignatureManifest(module, signer, "test-key")
require.NoError(t, err)
// Export manifest
data, err := ExportManifest(manifest)
require.NoError(t, err)
assert.NotEmpty(t, data)
// Import manifest
imported, err := ImportManifest(data)
require.NoError(t, err)
assert.Equal(t, manifest.ModuleHash, imported.ModuleHash)
assert.Len(t, imported.Signatures, 1)
assert.Len(t, imported.TrustedKeys, 1)
// Verify imported manifest
err = VerifyWithManifest(module, imported)
assert.NoError(t, err)
// Test invalid JSON
_, err = ImportManifest([]byte("invalid json"))
assert.Error(t, err)
}
func TestMultipleSignatures(t *testing.T) {
// Create multiple signers
signer1, _ := NewSigner()
signer2, _ := NewSigner()
module := []byte("multi-signed module")
// Create manifest with first signature
manifest, err := CreateSignatureManifest(module, signer1, "signer1")
require.NoError(t, err)
// Add second signature
signature2, _ := signer2.Sign(module)
manifest.Signatures = append(manifest.Signatures, SignatureEntry{
Signature: base64.StdEncoding.EncodeToString(signature2),
SignerID: "signer2",
Timestamp: time.Now(),
Algorithm: "Ed25519",
})
manifest.TrustedKeys = append(manifest.TrustedKeys, TrustedKeyEntry{
KeyID: "signer2",
PublicKey: base64.StdEncoding.EncodeToString(signer2.GetPublicKey()),
AddedAt: time.Now(),
Purpose: "code-signing",
})
// Verify with either signature
err = VerifyWithManifest(module, manifest)
assert.NoError(t, err)
// Remove first signature and key
manifest.Signatures = manifest.Signatures[1:]
manifest.TrustedKeys = manifest.TrustedKeys[1:]
// Should still verify with second signature
err = VerifyWithManifest(module, manifest)
assert.NoError(t, err)
}
func BenchmarkSign(b *testing.B) {
signer, _ := NewSigner()
data := make([]byte, 1024*1024) // 1MB
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = signer.Sign(data)
}
}
func BenchmarkVerify(b *testing.B) {
signer, _ := NewSigner()
verifier := NewSignatureVerifier()
verifier.AddTrustedKey("bench", signer.publicKey)
data := make([]byte, 1024*1024) // 1MB
signature, _ := signer.Sign(data)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = verifier.Verify(data, signature)
}
}

224
wasm/verifier.go Normal file
View File

@@ -0,0 +1,224 @@
// Package wasm provides cryptographic verification for WebAssembly modules
package wasm
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sync"
)
// HashVerifier provides SHA256 hash verification for WASM modules
type HashVerifier struct {
// trustedHashes stores SHA256 hashes of trusted WASM modules
trustedHashes map[string]string
mu sync.RWMutex
}
// NewHashVerifier creates a new WASM hash verifier
func NewHashVerifier() *HashVerifier {
return &HashVerifier{
trustedHashes: make(map[string]string),
}
}
// ComputeHash calculates SHA256 hash of WASM bytecode
func (v *HashVerifier) ComputeHash(wasmBytes []byte) string {
hash := sha256.Sum256(wasmBytes)
return hex.EncodeToString(hash[:])
}
// AddTrustedHash adds a trusted hash for a named WASM module
func (v *HashVerifier) AddTrustedHash(name, hash string) {
v.mu.Lock()
defer v.mu.Unlock()
v.trustedHashes[name] = hash
}
// VerifyHash verifies WASM bytecode against trusted hash
func (v *HashVerifier) VerifyHash(name string, wasmBytes []byte) error {
v.mu.RLock()
trustedHash, exists := v.trustedHashes[name]
v.mu.RUnlock()
if !exists {
return fmt.Errorf("no trusted hash found for WASM module: %s", name)
}
computedHash := v.ComputeHash(wasmBytes)
if computedHash != trustedHash {
return fmt.Errorf(
"WASM hash verification failed for %s: expected %s, got %s",
name, trustedHash, computedHash,
)
}
return nil
}
// VerifyHashWithFallback verifies against primary hash or fallback list
func (v *HashVerifier) VerifyHashWithFallback(name string, wasmBytes []byte, fallbackHashes []string) error {
// Try primary verification first
if err := v.VerifyHash(name, wasmBytes); err == nil {
return nil
}
// Check against fallback hashes
computedHash := v.ComputeHash(wasmBytes)
for _, fallbackHash := range fallbackHashes {
if computedHash == fallbackHash {
// Update trusted hash for future use
v.AddTrustedHash(name, computedHash)
return nil
}
}
return fmt.Errorf(
"WASM hash verification failed: computed hash %s not in trusted set",
computedHash,
)
}
// GetTrustedHash retrieves the trusted hash for a module
func (v *HashVerifier) GetTrustedHash(name string) (string, bool) {
v.mu.RLock()
defer v.mu.RUnlock()
hash, exists := v.trustedHashes[name]
return hash, exists
}
// ClearTrustedHashes removes all trusted hashes
func (v *HashVerifier) ClearTrustedHashes() {
v.mu.Lock()
defer v.mu.Unlock()
v.trustedHashes = make(map[string]string)
}
// HashChain provides hash chain verification for plugin updates
type HashChain struct {
chain []HashEntry
mu sync.RWMutex
}
// HashEntry represents a single entry in the hash chain
type HashEntry struct {
Version string `json:"version"`
Hash string `json:"hash"`
PreviousHash string `json:"previous_hash"`
Timestamp int64 `json:"timestamp"`
}
// NewHashChain creates a new hash chain
func NewHashChain() *HashChain {
return &HashChain{
chain: make([]HashEntry, 0),
}
}
// AddEntry adds a new entry to the hash chain
func (hc *HashChain) AddEntry(version, hash string, timestamp int64) error {
hc.mu.Lock()
defer hc.mu.Unlock()
previousHash := ""
if len(hc.chain) > 0 {
previousHash = hc.chain[len(hc.chain)-1].Hash
}
entry := HashEntry{
Version: version,
Hash: hash,
PreviousHash: previousHash,
Timestamp: timestamp,
}
hc.chain = append(hc.chain, entry)
return nil
}
// VerifyChain verifies the integrity of the hash chain
func (hc *HashChain) VerifyChain() error {
hc.mu.RLock()
defer hc.mu.RUnlock()
if len(hc.chain) == 0 {
return nil
}
// First entry should have empty previous hash
if hc.chain[0].PreviousHash != "" {
return fmt.Errorf("invalid hash chain: first entry has non-empty previous hash")
}
// Verify chain continuity
for i := 1; i < len(hc.chain); i++ {
if hc.chain[i].PreviousHash != hc.chain[i-1].Hash {
return fmt.Errorf(
"hash chain broken at version %s: expected previous hash %s, got %s",
hc.chain[i].Version,
hc.chain[i-1].Hash,
hc.chain[i].PreviousHash,
)
}
}
return nil
}
// GetLatestEntry returns the most recent hash chain entry
func (hc *HashChain) GetLatestEntry() (*HashEntry, error) {
hc.mu.RLock()
defer hc.mu.RUnlock()
if len(hc.chain) == 0 {
return nil, fmt.Errorf("hash chain is empty")
}
latest := hc.chain[len(hc.chain)-1]
return &latest, nil
}
// VerificationError represents a WASM verification failure
type VerificationError struct {
Module string
ExpectedHash string
ActualHash string
Reason string
}
// Error implements the error interface
func (e *VerificationError) Error() string {
return fmt.Sprintf(
"WASM verification failed for %s: %s (expected: %s, actual: %s)",
e.Module, e.Reason, e.ExpectedHash, e.ActualHash,
)
}
// SecurityPolicy defines verification requirements
type SecurityPolicy struct {
RequireHashVerification bool
RequireSignature bool
AllowedHashes []string
MaxModuleSize int64
}
// DefaultSecurityPolicy returns a secure default policy
func DefaultSecurityPolicy() *SecurityPolicy {
return &SecurityPolicy{
RequireHashVerification: true,
RequireSignature: false, // Will be enabled in next phase
AllowedHashes: []string{},
MaxModuleSize: 10 * 1024 * 1024, // 10MB max
}
}
// Validate checks if WASM module meets security policy
func (p *SecurityPolicy) Validate(wasmBytes []byte) error {
if p.MaxModuleSize > 0 && int64(len(wasmBytes)) > p.MaxModuleSize {
return fmt.Errorf(
"WASM module size %d exceeds maximum allowed size %d",
len(wasmBytes), p.MaxModuleSize,
)
}
return nil
}

226
wasm/verifier_test.go Normal file
View File

@@ -0,0 +1,226 @@
package wasm
import (
"crypto/sha256"
"encoding/hex"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHashVerifier_ComputeHash(t *testing.T) {
verifier := NewHashVerifier()
testCases := []struct {
name string
input []byte
expected string
}{
{
name: "empty input",
input: []byte{},
expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "simple wasm header",
input: []byte{0x00, 0x61, 0x73, 0x6d}, // \0asm
expected: "cd5d4935a48c0672cb06407bb443bc0087aff947c6b864bac886982c73b3027f",
},
{
name: "test module",
input: []byte("test wasm module content"),
expected: "945acabcfc93e347e8c08ea44afd3122670f04a89f9a0a0a5ce16ab849bbac06",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
hash := verifier.ComputeHash(tc.input)
assert.Equal(t, tc.expected, hash)
})
}
}
func TestHashVerifier_TrustedHashes(t *testing.T) {
verifier := NewHashVerifier()
// Add trusted hash
moduleName := "test-module"
trustedHash := "abc123def456"
verifier.AddTrustedHash(moduleName, trustedHash)
// Retrieve trusted hash
hash, exists := verifier.GetTrustedHash(moduleName)
assert.True(t, exists)
assert.Equal(t, trustedHash, hash)
// Non-existent module
_, exists = verifier.GetTrustedHash("non-existent")
assert.False(t, exists)
// Clear hashes
verifier.ClearTrustedHashes()
_, exists = verifier.GetTrustedHash(moduleName)
assert.False(t, exists)
}
func TestHashVerifier_VerifyHash(t *testing.T) {
verifier := NewHashVerifier()
// Test data
wasmBytes := []byte("test wasm module")
expectedHash := verifier.ComputeHash(wasmBytes)
moduleName := "test-module"
// Test missing trusted hash
err := verifier.VerifyHash(moduleName, wasmBytes)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no trusted hash found")
// Add trusted hash
verifier.AddTrustedHash(moduleName, expectedHash)
// Test successful verification
err = verifier.VerifyHash(moduleName, wasmBytes)
assert.NoError(t, err)
// Test failed verification
verifier.AddTrustedHash(moduleName, "wrong-hash")
err = verifier.VerifyHash(moduleName, wasmBytes)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hash verification failed")
}
func TestHashVerifier_VerifyHashWithFallback(t *testing.T) {
verifier := NewHashVerifier()
wasmBytes := []byte("test wasm module")
actualHash := verifier.ComputeHash(wasmBytes)
moduleName := "test-module"
// Test with fallback hashes
fallbackHashes := []string{
"wrong-hash-1",
actualHash,
"wrong-hash-2",
}
err := verifier.VerifyHashWithFallback(moduleName, wasmBytes, fallbackHashes)
assert.NoError(t, err)
// Verify that the hash was added as trusted
trustedHash, exists := verifier.GetTrustedHash(moduleName)
assert.True(t, exists)
assert.Equal(t, actualHash, trustedHash)
// Test with no matching fallback
fallbackHashes = []string{"wrong-1", "wrong-2"}
verifier.ClearTrustedHashes()
err = verifier.VerifyHashWithFallback(moduleName, wasmBytes, fallbackHashes)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not in trusted set")
}
func TestHashChain(t *testing.T) {
chain := NewHashChain()
// Add entries
timestamp1 := time.Now().Unix()
err := chain.AddEntry("v1.0.0", "hash1", timestamp1)
require.NoError(t, err)
timestamp2 := time.Now().Unix()
err = chain.AddEntry("v1.0.1", "hash2", timestamp2)
require.NoError(t, err)
timestamp3 := time.Now().Unix()
err = chain.AddEntry("v1.0.2", "hash3", timestamp3)
require.NoError(t, err)
// Verify chain integrity
err = chain.VerifyChain()
assert.NoError(t, err)
// Get latest entry
latest, err := chain.GetLatestEntry()
require.NoError(t, err)
assert.Equal(t, "v1.0.2", latest.Version)
assert.Equal(t, "hash3", latest.Hash)
assert.Equal(t, "hash2", latest.PreviousHash)
}
func TestHashChain_BrokenChain(t *testing.T) {
chain := NewHashChain()
// Manually create a broken chain
chain.chain = []HashEntry{
{Version: "v1", Hash: "hash1", PreviousHash: ""},
{Version: "v2", Hash: "hash2", PreviousHash: "hash1"},
{Version: "v3", Hash: "hash3", PreviousHash: "wrong-hash"}, // Broken link
}
err := chain.VerifyChain()
assert.Error(t, err)
assert.Contains(t, err.Error(), "hash chain broken")
}
func TestSecurityPolicy(t *testing.T) {
policy := DefaultSecurityPolicy()
// Test size validation
smallModule := make([]byte, 1024)
err := policy.Validate(smallModule)
assert.NoError(t, err)
// Test oversized module
largeModule := make([]byte, 11*1024*1024)
err = policy.Validate(largeModule)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum allowed size")
}
func TestVerificationError(t *testing.T) {
err := &VerificationError{
Module: "test.wasm",
ExpectedHash: "expected123",
ActualHash: "actual456",
Reason: "hash mismatch",
}
errStr := err.Error()
assert.Contains(t, errStr, "test.wasm")
assert.Contains(t, errStr, "hash mismatch")
assert.Contains(t, errStr, "expected123")
assert.Contains(t, errStr, "actual456")
}
func BenchmarkComputeHash(b *testing.B) {
verifier := NewHashVerifier()
data := make([]byte, 1024*1024) // 1MB
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = verifier.ComputeHash(data)
}
}
func BenchmarkVerifyHash(b *testing.B) {
verifier := NewHashVerifier()
data := make([]byte, 1024*1024) // 1MB
hash := verifier.ComputeHash(data)
verifier.AddTrustedHash("bench-module", hash)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = verifier.VerifyHash("bench-module", data)
}
}
// Helper function to compute SHA256 hash for testing
func computeSHA256(data []byte) string {
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:])
}