mirror of
https://github.com/sonr-io/crypto.git
synced 2026-01-12 04:09:13 +00:00
No commit suggestions generated
This commit is contained in:
336
wasm/signer.go
Normal file
336
wasm/signer.go
Normal 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
361
wasm/signer_test.go
Normal 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
224
wasm/verifier.go
Normal 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
226
wasm/verifier_test.go
Normal 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[:])
|
||||
}
|
||||
Reference in New Issue
Block a user