* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+190
View File
@@ -0,0 +1,190 @@
package plugin
import (
"context"
"fmt"
"log/slog"
"github.com/asynkron/protoactor-go/actor"
)
// Actor represents the UCAN actor that handles MPC-based cryptographic operations.
// It maintains a WebAssembly plugin for secure UCAN token operations and
// uses behavioral state management to handle different actor lifecycle phases.
type Actor struct {
ctx context.Context // Context for managing plugin lifecycle
behavior actor.Behavior // Behavioral state manager for handling different phases
enclave Plugin // MPC enclave plugin instance
config *EnclaveConfig // Configuration for enclave initialization (optional)
}
// NewActor creates a new UCAN actor instance.
// The actor starts in an uninitialized state and transitions to initialized
// once the MPC enclave plugin is successfully loaded.
func NewActor() actor.Actor {
return &Actor{
ctx: context.Background(),
behavior: actor.NewBehavior(),
}
}
// Props returns the actor properties configuration for creating new UCAN actors.
// It uses NewActor as the producer function to create UCAN actor instances.
func Props() *actor.Props {
return actor.PropsFromProducer(NewActor)
}
// PropsWithConfig returns actor properties configured with specific enclave data.
// This allows creating actors with pre-configured enclave data for testing or specific use cases.
func PropsWithConfig(config *EnclaveConfig) *actor.Props {
return actor.PropsFromProducer(func() actor.Actor {
return &Actor{
ctx: context.Background(),
behavior: actor.NewBehavior(),
config: config, // Store config for initialization
}
})
}
// Receive is the main message handler for the UCAN actor.
// It handles actor lifecycle messages and delegates other messages to the current behavior.
// The actor uses behavioral patterns to handle different states (uninitialized vs initialized).
func (a *Actor) Receive(c actor.Context) {
switch c.Message().(type) {
case *actor.Started:
a.handleStarted(c)
case *actor.Stopping:
a.handleStopping(c)
default:
a.behavior.Receive(c)
}
}
// Initialized handles UCAN operation messages once the actor is fully initialized.
// This method is set as the behavior after the MPC enclave plugin is successfully loaded.
// It processes NewOriginToken, NewAttenuatedToken, SignData, VerifyData, and GetIssuerDID requests.
func (a *Actor) Initialized(c actor.Context) {
switch msg := c.Message().(type) {
case *NewOriginTokenRequest:
a.handleNewOriginToken(c, msg)
case *NewAttenuatedTokenRequest:
a.handleNewAttenuatedToken(c, msg)
case *SignDataRequest:
a.handleSignData(c, msg)
case *VerifyDataRequest:
a.handleVerifyData(c, msg)
case *GetIssuerDIDResponse: // Used as request for DID retrieval
a.handleGetIssuerDID(c)
}
}
// handleStarted initializes the MPC enclave plugin when the actor starts.
// It loads the WebAssembly plugin and transitions the actor to the initialized state.
// If plugin loading fails, the actor remains in an uninitialized state.
func (a *Actor) handleStarted(c actor.Context) {
a.ctx = context.Background()
// Use provided config if available, otherwise use default
var config *EnclaveConfig
if a.config != nil {
config = a.config
} else {
config = DefaultEnclaveConfig()
}
// For testing, we need to provide mock enclave data
// In production, this would be provided by the caller
if config.EnclaveData == nil {
c.Logger().Warn("No enclave data provided, actor will not initialize",
slog.String("config", fmt.Sprintf("%+v", config)),
)
return
}
c.Logger().Info("Attempting to load MPC enclave plugin",
slog.String("config", fmt.Sprintf("%+v", config)),
)
e, err := LoadPluginWithManager(a.ctx, config)
if err != nil {
c.Logger().Error("Failed to create MPC enclave host",
slog.String("error", err.Error()),
slog.String("config", fmt.Sprintf("%+v", config)),
)
return
}
a.enclave = e
c.Logger().Info("MPC enclave actor started successfully",
slog.String("config", fmt.Sprintf("%+v", config)),
)
a.behavior.Become(a.Initialized)
}
// handleStopping performs cleanup when the actor is stopping.
// It releases the MPC enclave plugin resources and cleans up the context.
func (a *Actor) handleStopping(c actor.Context) {
a.ctx.Done()
a.enclave = nil
c.Logger().Info("MPC enclave plugin done")
}
// handleNewOriginToken processes UCAN origin token creation requests by delegating to the MPC enclave plugin.
// It validates the request and responds with the generated UCAN token or an error.
func (a *Actor) handleNewOriginToken(context actor.Context, msg *NewOriginTokenRequest) {
resp, err := a.enclave.NewOriginToken(msg)
if err != nil {
context.Logger().Error("failed to create origin token", slog.String("error", err.Error()))
context.Respond(err)
return
}
context.Respond(resp)
}
// handleNewAttenuatedToken processes UCAN attenuated token creation requests by delegating to the MPC enclave plugin.
// It creates a delegated token with reduced permissions and responds with the token or an error.
func (a *Actor) handleNewAttenuatedToken(context actor.Context, msg *NewAttenuatedTokenRequest) {
resp, err := a.enclave.NewAttenuatedToken(msg)
if err != nil {
context.Logger().
Error("failed to create attenuated token", slog.String("error", err.Error()))
context.Respond(err)
return
}
context.Respond(resp)
}
// handleSignData processes data signing requests by delegating to the MPC enclave plugin.
// It creates a cryptographic signature using MPC and responds with the signature or an error.
func (a *Actor) handleSignData(context actor.Context, msg *SignDataRequest) {
resp, err := a.enclave.SignData(msg)
if err != nil {
context.Logger().Error("failed to sign data", slog.String("error", err.Error()))
context.Respond(err)
return
}
context.Respond(resp)
}
// handleVerifyData processes signature verification requests by delegating to the MPC enclave plugin.
// It validates the signature against the data and responds with the verification result or an error.
func (a *Actor) handleVerifyData(context actor.Context, msg *VerifyDataRequest) {
resp, err := a.enclave.VerifyData(msg)
if err != nil {
context.Logger().Error("failed to verify data", slog.String("error", err.Error()))
context.Respond(err)
return
}
context.Respond(resp)
}
// handleGetIssuerDID processes DID retrieval requests by delegating to the MPC enclave plugin.
// It retrieves the issuer DID, address, and chain code from the enclave.
func (a *Actor) handleGetIssuerDID(context actor.Context) {
resp, err := a.enclave.GetIssuerDID()
if err != nil {
context.Logger().Error("failed to get issuer DID", slog.String("error", err.Error()))
context.Respond(err)
return
}
context.Respond(resp)
}
+295
View File
@@ -0,0 +1,295 @@
package plugin
import (
"encoding/json"
"fmt"
"time"
extism "github.com/extism/go-sdk"
"github.com/sonr-io/sonr/crypto/mpc"
)
// EnclaveConfig represents the MPC enclave configuration for the Motor plugin.
// This configuration is passed to the plugin via PDK environment variables.
type EnclaveConfig struct {
// ChainID specifies the blockchain network identifier (e.g., "sonr-testnet-1")
ChainID string `json:"chain_id" yaml:"chain_id"`
// EnclaveData contains the MPC enclave data with private key material
EnclaveData *mpc.EnclaveData `json:"enclave_data" yaml:"enclave_data"`
// VaultConfig provides additional vault configuration parameters
VaultConfig VaultConfig `json:"vault_config" yaml:"vault_config"`
// Security settings for plugin operations
Security SecurityConfig `json:"security" yaml:"security"`
// Timeout configurations for various operations
Timeouts TimeoutConfig `json:"timeouts" yaml:"timeouts"`
}
// VaultConfig specifies vault-specific configuration parameters.
type VaultConfig struct {
// IPFSEndpoint specifies the IPFS endpoint for vault operations
IPFSEndpoint string `json:"ipfs_endpoint" yaml:"ipfs_endpoint"`
// MaxVaultSize limits the maximum size of vault data in bytes
MaxVaultSize int64 `json:"max_vault_size" yaml:"max_vault_size"`
// EnableCompression enables compression for vault data
EnableCompression bool `json:"enable_compression" yaml:"enable_compression"`
// BackupEnabled enables automatic backup of vault data
BackupEnabled bool `json:"backup_enabled" yaml:"backup_enabled"`
// Custom metadata for vault operations
Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"`
}
// SecurityConfig defines security parameters for plugin operations.
type SecurityConfig struct {
// RequiredAttestations specifies required security attestations
RequiredAttestations []string `json:"required_attestations" yaml:"required_attestations"`
// MaxTokenLifetime limits the maximum lifetime of generated tokens
MaxTokenLifetime time.Duration `json:"max_token_lifetime" yaml:"max_token_lifetime"`
// RequireAudience enforces audience validation for all tokens
RequireAudience bool `json:"require_audience" yaml:"require_audience"`
// AllowedOrigins specifies allowed origins for token delegation
AllowedOrigins []string `json:"allowed_origins" yaml:"allowed_origins"`
}
// TimeoutConfig specifies timeout values for various plugin operations.
type TimeoutConfig struct {
// TokenCreation timeout for UCAN token creation operations
TokenCreation time.Duration `json:"token_creation" yaml:"token_creation"`
// Signature timeout for cryptographic signing operations
Signature time.Duration `json:"signature" yaml:"signature"`
// Verification timeout for signature verification operations
Verification time.Duration `json:"verification" yaml:"verification"`
// PluginInit timeout for plugin initialization
PluginInit time.Duration `json:"plugin_init" yaml:"plugin_init"`
}
// DefaultEnclaveConfig returns a default enclave configuration with sensible defaults.
func DefaultEnclaveConfig() *EnclaveConfig {
return &EnclaveConfig{
ChainID: "sonr-testnet-1",
VaultConfig: VaultConfig{
IPFSEndpoint: "127.0.0.1:5001",
MaxVaultSize: 10 * 1024 * 1024, // 10MB
EnableCompression: true,
BackupEnabled: false,
Metadata: make(map[string]string),
},
Security: SecurityConfig{
RequiredAttestations: []string{},
MaxTokenLifetime: 24 * time.Hour,
RequireAudience: true,
AllowedOrigins: []string{"*"},
},
Timeouts: TimeoutConfig{
TokenCreation: 30 * time.Second,
Signature: 10 * time.Second,
Verification: 5 * time.Second,
PluginInit: 15 * time.Second,
},
}
}
// Validate checks that the enclave configuration is valid and complete.
func (c *EnclaveConfig) Validate() error {
if c.ChainID == "" {
return fmt.Errorf("chain_id is required")
}
if c.EnclaveData == nil {
return fmt.Errorf("enclave_data is required")
}
if !c.EnclaveData.IsValid() {
return fmt.Errorf("enclave_data is invalid")
}
// Validate vault configuration
if err := c.VaultConfig.Validate(); err != nil {
return fmt.Errorf("vault_config validation failed: %w", err)
}
// Validate security configuration
if err := c.Security.Validate(); err != nil {
return fmt.Errorf("security configuration validation failed: %w", err)
}
return nil
}
// ToManifestConfig converts the enclave configuration to Extism manifest config.
// This is used to pass configuration to the WASM plugin via environment variables.
func (c *EnclaveConfig) ToManifestConfig() (map[string]string, error) {
config := make(map[string]string)
// Add chain ID
config["chain_id"] = c.ChainID
// Serialize and add enclave data
if c.EnclaveData != nil {
enclaveBytes, err := json.Marshal(c.EnclaveData)
if err != nil {
return nil, fmt.Errorf("failed to marshal enclave data: %w", err)
}
config["enclave"] = string(enclaveBytes)
}
// Serialize and add vault configuration
vaultBytes, err := json.Marshal(c.VaultConfig)
if err != nil {
return nil, fmt.Errorf("failed to marshal vault config: %w", err)
}
config["vault_config"] = string(vaultBytes)
// Serialize and add security configuration
securityBytes, err := json.Marshal(c.Security)
if err != nil {
return nil, fmt.Errorf("failed to marshal security config: %w", err)
}
config["security_config"] = string(securityBytes)
// Serialize and add timeout configuration
timeoutBytes, err := json.Marshal(c.Timeouts)
if err != nil {
return nil, fmt.Errorf("failed to marshal timeout config: %w", err)
}
config["timeout_config"] = string(timeoutBytes)
return config, nil
}
// Validate checks that the vault configuration is valid.
func (v *VaultConfig) Validate() error {
if v.MaxVaultSize <= 0 {
return fmt.Errorf("max_vault_size must be positive")
}
if v.MaxVaultSize > 100*1024*1024 { // 100MB limit
return fmt.Errorf("max_vault_size exceeds maximum allowed (100MB)")
}
return nil
}
// Validate checks that the security configuration is valid.
func (s *SecurityConfig) Validate() error {
if s.MaxTokenLifetime <= 0 {
return fmt.Errorf("max_token_lifetime must be positive")
}
if s.MaxTokenLifetime > 30*24*time.Hour { // 30 days limit
return fmt.Errorf("max_token_lifetime exceeds maximum allowed (30 days)")
}
return nil
}
// LoaderConfig represents configuration for the plugin loader itself.
type LoaderConfig struct {
// EnableWASI enables WebAssembly System Interface for the plugin
EnableWASI bool
// MemoryLimit sets the maximum memory limit for the plugin in bytes
MemoryLimit uint32
// AllowHttpRequests enables HTTP requests from the plugin
AllowHttpRequests bool
// LogLevel sets the logging level for plugin operations
LogLevel string
// MaxConcurrentPlugins limits the number of concurrent plugin instances
MaxConcurrentPlugins int
}
// DefaultLoaderConfig returns a default loader configuration.
func DefaultLoaderConfig() *LoaderConfig {
return &LoaderConfig{
EnableWASI: true,
MemoryLimit: 64 * 1024 * 1024, // 64MB
AllowHttpRequests: false,
LogLevel: "info",
MaxConcurrentPlugins: 10,
}
}
// ToPluginConfig converts the loader configuration to Extism plugin config.
func (l *LoaderConfig) ToPluginConfig() extism.PluginConfig {
return extism.PluginConfig{
EnableWasi: l.EnableWASI,
}
}
// PluginState represents the runtime state of a plugin instance.
type PluginState struct {
// ID is the unique identifier for this plugin instance
ID string
// Config is the configuration used to create this plugin
Config *EnclaveConfig
// Plugin is the underlying Extism plugin instance
Plugin *extism.Plugin
// CreatedAt is the timestamp when the plugin was created
CreatedAt time.Time
// LastUsed is the timestamp of the last plugin operation
LastUsed time.Time
// IsHealthy indicates whether the plugin is in a healthy state
IsHealthy bool
// ErrorCount tracks the number of errors encountered
ErrorCount int
// MaxErrors is the maximum number of errors before marking unhealthy
MaxErrors int
}
// UpdateHealth updates the plugin health status based on operation result.
func (s *PluginState) UpdateHealth(err error) {
s.LastUsed = time.Now()
if err != nil {
s.ErrorCount++
if s.ErrorCount >= s.MaxErrors {
s.IsHealthy = false
}
} else {
// Reset error count on successful operation
s.ErrorCount = 0
s.IsHealthy = true
}
}
// IsExpired checks if the plugin instance should be considered expired.
func (s *PluginState) IsExpired(maxIdleTime time.Duration) bool {
return time.Since(s.LastUsed) > maxIdleTime
}
// NewPluginState creates a new plugin state with default values.
func NewPluginState(id string, config *EnclaveConfig, plugin *extism.Plugin) *PluginState {
return &PluginState{
ID: id,
Config: config,
Plugin: plugin,
CreatedAt: time.Now(),
LastUsed: time.Now(),
IsHealthy: true,
ErrorCount: 0,
MaxErrors: 5, // Allow up to 5 errors before marking as unhealthy
}
}
+295
View File
@@ -0,0 +1,295 @@
package plugin
import (
"fmt"
"testing"
"time"
"github.com/sonr-io/sonr/crypto/mpc"
)
// createTestEnclaveData creates mock enclave data for testing
func createTestEnclaveData() *mpc.EnclaveData {
// Generate a real enclave for testing to ensure IsValid() returns true
enclave, err := mpc.NewEnclave()
if err != nil {
// Fallback to mock data if real enclave generation fails
// This provides compatibility for environments without proper MPC support
testPubBytes := make([]byte, 65)
for i := range testPubBytes {
testPubBytes[i] = byte(i % 256)
}
return &mpc.EnclaveData{
PubHex: "03a1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12",
PubBytes: testPubBytes,
ValShare: nil,
UserShare: nil,
Nonce: make([]byte, 32),
Curve: mpc.K256Name,
}
}
return enclave.GetData()
}
func TestEnclaveConfigValidation(t *testing.T) {
t.Run("valid config", func(t *testing.T) {
config := DefaultEnclaveConfig()
config.EnclaveData = createTestEnclaveData()
if err := config.Validate(); err != nil {
t.Errorf("Valid config failed validation: %v", err)
}
})
t.Run("missing chain_id", func(t *testing.T) {
config := DefaultEnclaveConfig()
config.ChainID = ""
config.EnclaveData = createTestEnclaveData()
if err := config.Validate(); err == nil {
t.Error("Expected validation error for missing chain_id")
}
})
t.Run("missing enclave_data", func(t *testing.T) {
config := DefaultEnclaveConfig()
config.EnclaveData = nil
if err := config.Validate(); err == nil {
t.Error("Expected validation error for missing enclave_data")
}
})
t.Run("invalid vault config", func(t *testing.T) {
config := DefaultEnclaveConfig()
config.EnclaveData = createTestEnclaveData()
config.VaultConfig.MaxVaultSize = -1 // Invalid size
if err := config.Validate(); err == nil {
t.Error("Expected validation error for invalid vault config")
}
})
}
func TestVaultConfigValidation(t *testing.T) {
t.Run("valid vault config", func(t *testing.T) {
config := DefaultEnclaveConfig().VaultConfig
if err := config.Validate(); err != nil {
t.Errorf("Valid vault config failed validation: %v", err)
}
})
t.Run("negative max vault size", func(t *testing.T) {
config := VaultConfig{
MaxVaultSize: -1,
}
if err := config.Validate(); err == nil {
t.Error("Expected validation error for negative max vault size")
}
})
t.Run("excessive max vault size", func(t *testing.T) {
config := VaultConfig{
MaxVaultSize: 200 * 1024 * 1024, // 200MB (exceeds 100MB limit)
}
if err := config.Validate(); err == nil {
t.Error("Expected validation error for excessive max vault size")
}
})
}
func TestSecurityConfigValidation(t *testing.T) {
t.Run("valid security config", func(t *testing.T) {
config := DefaultEnclaveConfig().Security
if err := config.Validate(); err != nil {
t.Errorf("Valid security config failed validation: %v", err)
}
})
t.Run("negative token lifetime", func(t *testing.T) {
config := SecurityConfig{
MaxTokenLifetime: -time.Hour,
}
if err := config.Validate(); err == nil {
t.Error("Expected validation error for negative token lifetime")
}
})
t.Run("excessive token lifetime", func(t *testing.T) {
config := SecurityConfig{
MaxTokenLifetime: 40 * 24 * time.Hour, // 40 days (exceeds 30 day limit)
}
if err := config.Validate(); err == nil {
t.Error("Expected validation error for excessive token lifetime")
}
})
}
func TestToManifestConfig(t *testing.T) {
config := DefaultEnclaveConfig()
config.EnclaveData = createTestEnclaveData()
manifestConfig, err := config.ToManifestConfig()
if err != nil {
t.Fatalf("ToManifestConfig failed: %v", err)
}
// Check required keys are present
requiredKeys := []string{
"chain_id",
"enclave",
"vault_config",
"security_config",
"timeout_config",
}
for _, key := range requiredKeys {
if _, exists := manifestConfig[key]; !exists {
t.Errorf("Missing required key in manifest config: %s", key)
}
}
// Check chain_id value
if manifestConfig["chain_id"] != config.ChainID {
t.Errorf(
"Chain ID mismatch: expected %s, got %s",
config.ChainID,
manifestConfig["chain_id"],
)
}
}
func TestDefaultConfigurations(t *testing.T) {
t.Run("default enclave config", func(t *testing.T) {
config := DefaultEnclaveConfig()
if config.ChainID == "" {
t.Error("Default enclave config should have non-empty chain_id")
}
if config.VaultConfig.MaxVaultSize <= 0 {
t.Error("Default vault config should have positive max_vault_size")
}
if config.Security.MaxTokenLifetime <= 0 {
t.Error("Default security config should have positive max_token_lifetime")
}
})
t.Run("default loader config", func(t *testing.T) {
config := DefaultLoaderConfig()
if !config.EnableWASI {
t.Error("Default loader config should enable WASI")
}
if config.MemoryLimit <= 0 {
t.Error("Default loader config should have positive memory limit")
}
if config.MaxConcurrentPlugins <= 0 {
t.Error("Default loader config should have positive max concurrent plugins")
}
})
}
func TestPluginStateManagement(t *testing.T) {
config := DefaultEnclaveConfig()
config.EnclaveData = createTestEnclaveData()
state := NewPluginState("test-plugin", config, nil)
t.Run("initial state", func(t *testing.T) {
if state.ID != "test-plugin" {
t.Errorf("Expected plugin ID 'test-plugin', got %s", state.ID)
}
if !state.IsHealthy {
t.Error("New plugin state should be healthy")
}
if state.ErrorCount != 0 {
t.Errorf("New plugin state should have zero error count, got %d", state.ErrorCount)
}
})
t.Run("health updates", func(t *testing.T) {
// Test successful operation
state.UpdateHealth(nil)
if !state.IsHealthy {
t.Error("Plugin should remain healthy after successful operation")
}
if state.ErrorCount != 0 {
t.Error("Error count should reset after successful operation")
}
// Test error handling
testError := fmt.Errorf("test error")
for i := 0; i < state.MaxErrors; i++ {
state.UpdateHealth(testError)
}
if state.IsHealthy {
t.Error("Plugin should be unhealthy after max errors")
}
if state.ErrorCount != state.MaxErrors {
t.Errorf("Expected error count %d, got %d", state.MaxErrors, state.ErrorCount)
}
})
t.Run("expiration check", func(t *testing.T) {
maxIdleTime := 1 * time.Second
// Plugin should not be expired immediately
if state.IsExpired(maxIdleTime) {
t.Error("Plugin should not be expired immediately")
}
// Simulate old last used time
state.LastUsed = time.Now().Add(-2 * time.Second)
if !state.IsExpired(maxIdleTime) {
t.Error("Plugin should be expired after max idle time")
}
})
}
func TestManagerConfiguration(t *testing.T) {
loaderConfig := DefaultLoaderConfig()
manager := NewManager(loaderConfig)
defer manager.Close()
if manager.loaderConfig != loaderConfig {
t.Error("Manager should use provided loader config")
}
if len(manager.plugins) != 0 {
t.Error("New manager should have no plugins initially")
}
}
func TestCreateEnclaveConfig(t *testing.T) {
chainID := "test-chain"
enclaveData := createTestEnclaveData()
config := CreateEnclaveConfig(chainID, enclaveData)
if config.ChainID != chainID {
t.Errorf("Expected chain ID %s, got %s", chainID, config.ChainID)
}
if config.EnclaveData != enclaveData {
t.Error("Enclave data should match provided data")
}
// Should have default values for other fields
if config.VaultConfig.MaxVaultSize <= 0 {
t.Error("Should have default vault config values")
}
}
+219
View File
@@ -0,0 +1,219 @@
package plugin
import (
"bytes"
"compress/zlib"
"crypto/ed25519"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"time"
extism "github.com/extism/go-sdk"
"github.com/sonr-io/sonr/crypto/wasm"
)
// motrPluginBytes contains the embedded WebAssembly bytecode for the cryptographic enclave.
// This is embedded at compile time and loaded into the WASM runtime for secure operations.
//
//go:embed vault.wasm
var motrPluginBytes []byte
// motrPluginHash is the SHA256 hash of the embedded WASM module
// This will be computed at runtime for verification
var motrPluginHash string
// hashVerifier is the global hash verifier for WASM modules
var hashVerifier = wasm.NewHashVerifier()
// signatureVerifier is the global signature verifier for WASM modules
var signatureVerifier = wasm.NewSignatureVerifier()
// pluginSignatureManifest stores the signature manifest for the plugin
var pluginSignatureManifest *wasm.SignatureManifest
// init initializes the hash and signature verifiers
func init() {
// Compute hash of embedded WASM module
motrPluginHash = hashVerifier.ComputeHash(motrPluginBytes)
// Add as trusted hash
hashVerifier.AddTrustedHash("motr", motrPluginHash)
// Initialize signature verification (signatures will be added via configuration)
// In production, trusted keys would be loaded from secure configuration
initializeTrustedSigningKeys()
}
// initializeTrustedSigningKeys loads trusted signing keys for verification
func initializeTrustedSigningKeys() {
// These would typically come from secure configuration
// For now, we'll prepare the infrastructure for key management
// Production keys would be loaded from config files or environment
}
// MotrPluginRaw contains the raw WebAssembly bytecode for the cryptographic enclave.
func MotrPluginRaw() ([]byte, error) {
var b bytes.Buffer
w := zlib.NewWriter(&b)
defer w.Close()
_, err := w.Write(motrPluginBytes)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
// GetManifest returns the WebAssembly manifest configuration for the MPC-based UCAN enclave plugin.
// This manifest specifies the WASM bytecode and configuration required to run
// the MPC-based UCAN token operations.
func GetManifest() extism.Manifest {
return extism.Manifest{
Wasm: []extism.Wasm{
extism.WasmData{
Data: motrPluginBytes,
},
},
Config: map[string]string{},
}
}
// GetManifestWithEnclave returns a WebAssembly manifest with MPC enclave configuration.
// This allows passing enclave data and vault configuration to the Motor plugin via PDK environment.
// DEPRECATED: Use GetManifestFromConfig for enhanced configuration support.
func GetManifestWithEnclave(
chainID string,
enclaveData []byte,
vaultConfig map[string]any,
) extism.Manifest {
// Prepare configuration for PDK environment variables
config := map[string]string{
"chain_id": chainID,
}
// Add enclave data as JSON-encoded environment variable
if len(enclaveData) > 0 {
config["enclave"] = string(enclaveData) // Motor plugin expects JSON-encoded enclave data
}
// Add vault configuration if provided
if len(vaultConfig) > 0 {
if configBytes, err := json.Marshal(vaultConfig); err == nil {
config["vault_config"] = string(configBytes)
}
}
return extism.Manifest{
Wasm: []extism.Wasm{
extism.WasmData{
Data: motrPluginBytes,
},
},
Config: config,
}
}
// GetManifestFromConfig creates a WebAssembly manifest from an EnclaveConfig.
// This is the preferred method for creating manifests with comprehensive configuration.
func GetManifestFromConfig(config *EnclaveConfig) (extism.Manifest, error) {
manifestConfig, err := config.ToManifestConfig()
if err != nil {
return extism.Manifest{}, fmt.Errorf(
"failed to convert enclave config to manifest config: %w",
err,
)
}
return extism.Manifest{
Wasm: []extism.Wasm{
extism.WasmData{
Data: motrPluginBytes,
},
},
Config: manifestConfig,
}, nil
}
// ValidateManifest validates that a manifest contains required configuration.
func ValidateManifest(manifest extism.Manifest) error {
if len(manifest.Wasm) == 0 {
return fmt.Errorf("manifest must contain WASM data")
}
// Check for required configuration keys
requiredKeys := []string{"chain_id"}
for _, key := range requiredKeys {
if _, exists := manifest.Config[key]; !exists {
return fmt.Errorf("manifest missing required config key: %s", key)
}
}
// Validate enclave data if present
if enclaveStr, exists := manifest.Config["enclave"]; exists && enclaveStr != "" {
var enclaveData map[string]any
if err := json.Unmarshal([]byte(enclaveStr), &enclaveData); err != nil {
return fmt.Errorf("invalid enclave data in manifest: %w", err)
}
}
return nil
}
// GetPluginConfig returns the configuration for the WebAssembly plugin runtime.
// It enables WASI (WebAssembly System Interface) for file system and system call access.
func GetPluginConfig() extism.PluginConfig {
return extism.PluginConfig{
EnableWasi: true,
}
}
// VerifyPluginIntegrity verifies the integrity of the WASM plugin
func VerifyPluginIntegrity(wasmBytes []byte) error {
return hashVerifier.VerifyHash("motr", wasmBytes)
}
// GetPluginHash returns the SHA256 hash of the embedded WASM module
func GetPluginHash() string {
return motrPluginHash
}
// VerifyPluginSignature verifies the signature of the WASM plugin
func VerifyPluginSignature(wasmBytes []byte, signature []byte) error {
return signatureVerifier.Verify(wasmBytes, signature)
}
// SetPluginSignatureManifest sets the signature manifest for the plugin
func SetPluginSignatureManifest(manifest *wasm.SignatureManifest) error {
if manifest == nil {
return fmt.Errorf("manifest cannot be nil")
}
pluginSignatureManifest = manifest
// Load trusted keys from manifest
for _, key := range manifest.TrustedKeys {
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 := signatureVerifier.AddTrustedKey(key.KeyID, ed25519.PublicKey(publicKey)); err != nil {
return fmt.Errorf("failed to add trusted key: %w", err)
}
}
return nil
}
// GetPluginSignatureManifest returns the current signature manifest
func GetPluginSignatureManifest() *wasm.SignatureManifest {
return pluginSignatureManifest
}
// AddTrustedSigningKey adds a trusted public key for signature verification
func AddTrustedSigningKey(keyID string, publicKeyHex string) error {
return signatureVerifier.AddTrustedKeyFromHex(keyID, publicKeyHex)
}
+260
View File
@@ -0,0 +1,260 @@
package plugin_test
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/sonr-io/sonr/crypto/mpc"
"github.com/sonr-io/sonr/x/dwn/client/plugin"
)
// ExampleLoadPluginWithEnclave demonstrates how to load and use the refactored
// Motor plugin as an MPC-based UCAN KeyshareSource.
func ExampleLoadPluginWithEnclave() {
ctx := context.Background()
// Example MPC enclave data (in practice, this would be real enclave data)
enclaveData := &mpc.EnclaveData{
// This would contain actual MPC enclave configuration
// For example purposes, we'll assume this is properly initialized
}
// Serialize enclave data for the plugin
enclaveJSON, err := json.Marshal(enclaveData)
if err != nil {
fmt.Printf("Failed to marshal enclave data: %v\n", err)
return
}
// Optional vault configuration
vaultConfig := map[string]any{
"auto_lock_timeout": 300, // 5 minutes
"key_rotation_interval": 86400, // 24 hours
"supported_chains": []string{"sonr", "cosmos", "ethereum"},
}
// Load the plugin with enclave configuration
p, err := plugin.LoadPluginWithEnclave(ctx, "sonr-testnet-1", enclaveJSON, vaultConfig)
if err != nil {
fmt.Printf("Failed to load plugin: %v\n", err)
return
}
// Get issuer DID and address
issuerResp, err := p.GetIssuerDID()
if err != nil {
fmt.Printf("Failed to get issuer DID: %v\n", err)
return
}
fmt.Printf("Issuer DID: %s\n", issuerResp.IssuerDID)
fmt.Printf("Address: %s\n", issuerResp.Address)
fmt.Printf("Chain Code: %s\n", issuerResp.ChainCode)
// Create a UCAN origin token
originReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:example-audience",
Attenuations: []map[string]any{
{
"can": []string{"sign", "verify"},
"with": "vault://example-vault",
},
},
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
}
originResp, err := p.NewOriginToken(originReq)
if err != nil {
fmt.Printf("Failed to create origin token: %v\n", err)
return
}
fmt.Printf("Origin Token: %s\n", originResp.Token)
// Create an attenuated token from the origin token
attenuatedReq := &plugin.NewAttenuatedTokenRequest{
ParentToken: originResp.Token,
AudienceDID: "did:sonr:delegated-user",
Attenuations: []map[string]any{
{
"can": []string{"sign"}, // More restrictive than parent
"with": "vault://example-vault",
},
},
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(), // Shorter than parent
}
attenuatedResp, err := p.NewAttenuatedToken(attenuatedReq)
if err != nil {
fmt.Printf("Failed to create attenuated token: %v\n", err)
return
}
fmt.Printf("Attenuated Token: %s\n", attenuatedResp.Token)
// Sign some data
signReq := &plugin.SignDataRequest{
Data: []byte("Hello, UCAN world!"),
}
signResp, err := p.SignData(signReq)
if err != nil {
fmt.Printf("Failed to sign data: %v\n", err)
return
}
fmt.Printf("Signature: %x\n", signResp.Signature)
// Verify the signature
verifyReq := &plugin.VerifyDataRequest{
Data: []byte("Hello, UCAN world!"),
Signature: signResp.Signature,
}
verifyResp, err := p.VerifyData(verifyReq)
if err != nil {
fmt.Printf("Failed to verify signature: %v\n", err)
return
}
fmt.Printf("Signature valid: %t\n", verifyResp.Valid)
fmt.Println("Example completed successfully!")
}
// TestAdvancedOperationsExample demonstrates advanced UCAN operations
// including comprehensive signing and verification workflows.
func TestAdvancedOperationsExample(t *testing.T) {
t.Skip("Example test - skipped during normal test runs")
ctx := context.Background()
// Example MPC enclave data
enclaveData, _ := json.Marshal(&mpc.EnclaveData{})
// Load plugin with enhanced configuration
p, err := plugin.LoadPluginWithEnclave(ctx, "sonr-testnet-1", enclaveData, nil)
if err != nil {
fmt.Printf("Failed to load plugin: %v\n", err)
return
}
// UCAN-based signing approach
signReq := &plugin.SignDataRequest{
Data: []byte("UCAN secure message"),
}
signResp, err := p.SignData(signReq)
if err != nil {
fmt.Printf("Signing failed: %v\n", err)
return
}
fmt.Printf("Signature: %x\n", signResp.Signature)
// Verify the signature
verifyReq := &plugin.VerifyDataRequest{
Data: []byte("UCAN secure message"),
Signature: signResp.Signature,
}
verifyResp, err := p.VerifyData(verifyReq)
if err != nil {
fmt.Printf("Verification failed: %v\n", err)
return
}
fmt.Printf("Signature valid: %t\n", verifyResp.Valid)
fmt.Println("Enhanced UCAN operations completed!")
}
// TestTokenWorkflowExample demonstrates a complete UCAN token workflow
// including token creation, delegation, and validation.
func TestTokenWorkflowExample(t *testing.T) {
t.Skip("Example test - skipped during normal test runs")
ctx := context.Background()
// Load plugin with enclave configuration
enclaveData, _ := json.Marshal(&mpc.EnclaveData{})
p, err := plugin.LoadPluginWithEnclave(ctx, "sonr-testnet-1", enclaveData, nil)
if err != nil {
fmt.Printf("Failed to load plugin: %v\n", err)
return
}
// Step 1: Get issuer information
issuer, err := p.GetIssuerDID()
if err != nil {
fmt.Printf("Failed to get issuer: %v\n", err)
return
}
fmt.Printf("Vault Issuer: %s\n", issuer.IssuerDID)
// Step 2: Create admin token with broad permissions
adminReq := &plugin.NewOriginTokenRequest{
AudienceDID: "did:sonr:admin",
Attenuations: []map[string]any{
{
"can": []string{"admin", "sign", "verify", "delegate"},
"with": fmt.Sprintf("vault://%s", issuer.Address),
},
},
ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Unix(), // 7 days
}
adminToken, err := p.NewOriginToken(adminReq)
if err != nil {
fmt.Printf("Failed to create admin token: %v\n", err)
return
}
fmt.Printf("Admin Token: %s...\n", adminToken.Token[:50])
// Step 3: Delegate signing permission to a user
userReq := &plugin.NewAttenuatedTokenRequest{
ParentToken: adminToken.Token,
AudienceDID: "did:sonr:user123",
Attenuations: []map[string]any{
{
"can": []string{"sign"}, // Only signing permission
"with": fmt.Sprintf("vault://%s", issuer.Address),
},
},
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), // 24 hours
}
userToken, err := p.NewAttenuatedToken(userReq)
if err != nil {
fmt.Printf("Failed to create user token: %v\n", err)
return
}
fmt.Printf("User Token: %s...\n", userToken.Token[:50])
// Step 4: Further delegate read-only permission
readOnlyReq := &plugin.NewAttenuatedTokenRequest{
ParentToken: userToken.Token,
AudienceDID: "did:sonr:readonly",
Attenuations: []map[string]any{
{
"can": []string{"verify"}, // Only verification permission
"with": fmt.Sprintf("vault://%s", issuer.Address),
},
},
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(), // 1 hour
}
readOnlyToken, err := p.NewAttenuatedToken(readOnlyReq)
if err != nil {
fmt.Printf("Failed to create read-only token: %v\n", err)
return
}
fmt.Printf("Read-only Token: %s...\n", readOnlyToken.Token[:50])
fmt.Println("UCAN token delegation workflow completed successfully!")
}
+499
View File
@@ -0,0 +1,499 @@
package plugin
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
extism "github.com/extism/go-sdk"
"github.com/sonr-io/sonr/crypto/mpc"
"github.com/sonr-io/sonr/crypto/wasm"
)
// Manager handles the lifecycle of Motor plugin instances with health monitoring,
// automatic recovery, and efficient resource management.
type Manager struct {
mu sync.RWMutex
plugins map[string]*PluginState
loaderConfig *LoaderConfig
// Cleanup configuration
cleanupInterval time.Duration
maxIdleTime time.Duration
// Background cleanup goroutine
stopCleanup chan struct{}
cleanupWG sync.WaitGroup
}
// NewManager creates a new plugin manager with the specified configuration.
func NewManager(loaderConfig *LoaderConfig) *Manager {
if loaderConfig == nil {
loaderConfig = DefaultLoaderConfig()
}
m := &Manager{
plugins: make(map[string]*PluginState),
loaderConfig: loaderConfig,
cleanupInterval: 5 * time.Minute,
maxIdleTime: 30 * time.Minute,
stopCleanup: make(chan struct{}),
}
// Start background cleanup goroutine
m.cleanupWG.Add(1)
go m.cleanupLoop()
return m
}
// LoadPlugin loads a Motor plugin with the specified enclave configuration.
// Returns a cached instance if available, or creates a new one.
func (m *Manager) LoadPlugin(ctx context.Context, config *EnclaveConfig) (Plugin, error) {
// Validate configuration
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid enclave configuration: %w", err)
}
// Generate plugin ID based on configuration
pluginID := m.generatePluginID(config)
m.mu.RLock()
state, exists := m.plugins[pluginID]
m.mu.RUnlock()
// Check if we have a healthy cached instance
if exists && state.IsHealthy && !state.IsExpired(m.maxIdleTime) {
state.UpdateHealth(nil) // Update last used timestamp
return &managedPluginImpl{
state: state,
manager: m,
}, nil
}
// Create new plugin instance
return m.createPlugin(ctx, pluginID, config)
}
// LoadPluginWithID loads a plugin with a specific ID for testing or debugging.
func (m *Manager) LoadPluginWithID(
ctx context.Context,
id string,
config *EnclaveConfig,
) (Plugin, error) {
if err := config.Validate(); err != nil {
return nil, fmt.Errorf("invalid enclave configuration: %w", err)
}
return m.createPlugin(ctx, id, config)
}
// createPlugin creates a new plugin instance with the given configuration.
func (m *Manager) createPlugin(
ctx context.Context,
id string,
config *EnclaveConfig,
) (Plugin, error) {
m.mu.Lock()
defer m.mu.Unlock()
// Check concurrent plugin limit
if len(m.plugins) >= m.loaderConfig.MaxConcurrentPlugins {
return nil, fmt.Errorf(
"maximum concurrent plugins limit reached (%d)",
m.loaderConfig.MaxConcurrentPlugins,
)
}
// Verify WASM integrity before loading
if err := VerifyPluginIntegrity(motrPluginBytes); err != nil {
return nil, fmt.Errorf("WASM integrity verification failed: %w", err)
}
// Verify signature if manifest is available (optional for now)
if manifest := GetPluginSignatureManifest(); manifest != nil {
if err := wasm.VerifyWithManifest(motrPluginBytes, manifest); err != nil {
// Log warning but don't fail for backward compatibility
// In production, this should return an error
// Using fmt.Printf as we don't have access to pdk here
fmt.Printf("WARNING: WASM signature verification failed: %v\n", err)
}
}
// Convert configuration to manifest format
manifestConfig, err := config.ToManifestConfig()
if err != nil {
return nil, fmt.Errorf("failed to convert config to manifest: %w", err)
}
// Create Extism manifest
manifest := extism.Manifest{
Wasm: []extism.Wasm{
extism.WasmData{
Data: motrPluginBytes,
},
},
Config: manifestConfig,
}
// Create plugin with timeout
pluginConfig := m.loaderConfig.ToPluginConfig()
// Create context with timeout for plugin initialization
initCtx, cancel := context.WithTimeout(ctx, config.Timeouts.PluginInit)
defer cancel()
plugin, err := extism.NewPlugin(initCtx, manifest, pluginConfig, []extism.HostFunction{})
if err != nil {
return nil, fmt.Errorf("failed to create plugin: %w", err)
}
// Create plugin state
state := NewPluginState(id, config, plugin)
m.plugins[id] = state
return &managedPluginImpl{
state: state,
manager: m,
}, nil
}
// RecoverPlugin attempts to recover a failed plugin instance.
func (m *Manager) RecoverPlugin(ctx context.Context, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
state, exists := m.plugins[id]
if !exists {
return fmt.Errorf("plugin %s not found", id)
}
// Close existing plugin
if state.Plugin != nil {
state.Plugin.Close(ctx)
}
// Recreate plugin with same configuration
manifestConfig, err := state.Config.ToManifestConfig()
if err != nil {
return fmt.Errorf("failed to convert config to manifest: %w", err)
}
manifest := extism.Manifest{
Wasm: []extism.Wasm{
extism.WasmData{
Data: motrPluginBytes,
},
},
Config: manifestConfig,
}
pluginConfig := m.loaderConfig.ToPluginConfig()
initCtx, cancel := context.WithTimeout(ctx, state.Config.Timeouts.PluginInit)
defer cancel()
plugin, err := extism.NewPlugin(initCtx, manifest, pluginConfig, []extism.HostFunction{})
if err != nil {
return fmt.Errorf("failed to recover plugin: %w", err)
}
// Update state
state.Plugin = plugin
state.IsHealthy = true
state.ErrorCount = 0
state.LastUsed = time.Now()
return nil
}
// GetPluginStats returns statistics for a specific plugin.
func (m *Manager) GetPluginStats(id string) (*PluginStats, error) {
m.mu.RLock()
defer m.mu.RUnlock()
state, exists := m.plugins[id]
if !exists {
return nil, fmt.Errorf("plugin %s not found", id)
}
return &PluginStats{
ID: state.ID,
CreatedAt: state.CreatedAt,
LastUsed: state.LastUsed,
IsHealthy: state.IsHealthy,
ErrorCount: state.ErrorCount,
ChainID: state.Config.ChainID,
UptimeDuration: time.Since(state.CreatedAt),
IdleDuration: time.Since(state.LastUsed),
}, nil
}
// ListPlugins returns a list of all currently managed plugins.
func (m *Manager) ListPlugins() []string {
m.mu.RLock()
defer m.mu.RUnlock()
ids := make([]string, 0, len(m.plugins))
for id := range m.plugins {
ids = append(ids, id)
}
return ids
}
// ClosePlugin closes and removes a specific plugin instance.
func (m *Manager) ClosePlugin(id string) error {
m.mu.Lock()
defer m.mu.Unlock()
state, exists := m.plugins[id]
if !exists {
return fmt.Errorf("plugin %s not found", id)
}
if state.Plugin != nil {
state.Plugin.Close(context.Background())
}
delete(m.plugins, id)
return nil
}
// Close shuts down the manager and all managed plugins.
func (m *Manager) Close() error {
// Stop cleanup goroutine
close(m.stopCleanup)
m.cleanupWG.Wait()
m.mu.Lock()
defer m.mu.Unlock()
// Close all plugins
for id, state := range m.plugins {
if state.Plugin != nil {
state.Plugin.Close(context.Background())
}
delete(m.plugins, id)
}
return nil
}
// generatePluginID generates a unique plugin ID based on configuration.
func (m *Manager) generatePluginID(config *EnclaveConfig) string {
// Use chain ID and enclave data hash for unique identification
if config.EnclaveData != nil && len(config.EnclaveData.PubBytes) > 8 {
pubKeyHash := fmt.Sprintf("%x", config.EnclaveData.PubBytes[:8])
return fmt.Sprintf("%s_%s", config.ChainID, pubKeyHash)
}
return fmt.Sprintf("%s_%d", config.ChainID, time.Now().UnixNano())
}
// cleanupLoop runs periodic cleanup of expired and unhealthy plugins.
func (m *Manager) cleanupLoop() {
defer m.cleanupWG.Done()
ticker := time.NewTicker(m.cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.cleanupExpiredPlugins()
case <-m.stopCleanup:
return
}
}
}
// cleanupExpiredPlugins removes expired and unhealthy plugin instances.
func (m *Manager) cleanupExpiredPlugins() {
m.mu.Lock()
defer m.mu.Unlock()
var toRemove []string
for id, state := range m.plugins {
if !state.IsHealthy || state.IsExpired(m.maxIdleTime) {
if state.Plugin != nil {
state.Plugin.Close(context.Background())
}
toRemove = append(toRemove, id)
}
}
for _, id := range toRemove {
delete(m.plugins, id)
}
}
// PluginStats contains statistics and status information for a plugin instance.
type PluginStats struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
LastUsed time.Time `json:"last_used"`
IsHealthy bool `json:"is_healthy"`
ErrorCount int `json:"error_count"`
ChainID string `json:"chain_id"`
UptimeDuration time.Duration `json:"uptime_duration"`
IdleDuration time.Duration `json:"idle_duration"`
}
// managedPluginImpl implements the Plugin interface with health monitoring.
type managedPluginImpl struct {
state *PluginState
manager *Manager
}
// UCAN Token Operations with health monitoring
// callTokenMethod is a helper method to reduce duplication between token creation methods
func (p *managedPluginImpl) callTokenMethod(
methodName string,
request any,
) (*UCANTokenResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), p.state.Config.Timeouts.TokenCreation)
defer cancel()
reqBytes, err := json.Marshal(request)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
_, r, err := p.state.Plugin.CallWithContext(ctx, methodName, reqBytes)
if err != nil {
p.state.UpdateHealth(err)
// Attempt recovery on failure
if !p.state.IsHealthy {
if recoverErr := p.manager.RecoverPlugin(ctx, p.state.ID); recoverErr == nil {
// Retry after recovery
_, r, err = p.state.Plugin.CallWithContext(ctx, methodName, reqBytes)
}
}
if err != nil {
return nil, err
}
}
var resp UCANTokenResponse
if err := json.Unmarshal(r, &resp); err != nil {
p.state.UpdateHealth(err)
return nil, err
}
p.state.UpdateHealth(nil)
return &resp, nil
}
// NewOriginToken creates a new origin UCAN token with health monitoring and recovery.
func (p *managedPluginImpl) NewOriginToken(req *NewOriginTokenRequest) (*UCANTokenResponse, error) {
return p.callTokenMethod("new_origin_token", req)
}
func (p *managedPluginImpl) NewAttenuatedToken(
req *NewAttenuatedTokenRequest,
) (*UCANTokenResponse, error) {
return p.callTokenMethod("new_attenuated_token", req)
}
// Cryptographic Operations with health monitoring
func (p *managedPluginImpl) SignData(req *SignDataRequest) (*SignDataResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), p.state.Config.Timeouts.Signature)
defer cancel()
reqBytes, err := json.Marshal(req)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
_, r, err := p.state.Plugin.CallWithContext(ctx, "sign_data", reqBytes)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
var resp SignDataResponse
if err := json.Unmarshal(r, &resp); err != nil {
p.state.UpdateHealth(err)
return nil, err
}
p.state.UpdateHealth(nil)
return &resp, nil
}
func (p *managedPluginImpl) VerifyData(req *VerifyDataRequest) (*VerifyDataResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), p.state.Config.Timeouts.Verification)
defer cancel()
reqBytes, err := json.Marshal(req)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
_, r, err := p.state.Plugin.CallWithContext(ctx, "verify_data", reqBytes)
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
var resp VerifyDataResponse
if err := json.Unmarshal(r, &resp); err != nil {
p.state.UpdateHealth(err)
return nil, err
}
p.state.UpdateHealth(nil)
return &resp, nil
}
// Identity Operations with health monitoring
func (p *managedPluginImpl) GetIssuerDID() (*GetIssuerDIDResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, r, err := p.state.Plugin.CallWithContext(ctx, "get_issuer_did", []byte{})
if err != nil {
p.state.UpdateHealth(err)
return nil, err
}
var resp GetIssuerDIDResponse
if err := json.Unmarshal(r, &resp); err != nil {
p.state.UpdateHealth(err)
return nil, err
}
p.state.UpdateHealth(nil)
return &resp, nil
}
// DefaultManager is a package-level default manager instance.
var DefaultManager *Manager
// init initializes the default manager.
func init() {
DefaultManager = NewManager(DefaultLoaderConfig())
}
// LoadPluginWithDefaultManager is a convenience function that uses the default manager.
func LoadPluginWithDefaultManager(ctx context.Context, config *EnclaveConfig) (Plugin, error) {
return DefaultManager.LoadPlugin(ctx, config)
}
// CreateEnclaveConfig is a helper function to create enclave configuration from MPC data.
func CreateEnclaveConfig(chainID string, enclaveData *mpc.EnclaveData) *EnclaveConfig {
config := DefaultEnclaveConfig()
config.ChainID = chainID
config.EnclaveData = enclaveData
return config
}
+203
View File
@@ -0,0 +1,203 @@
// Package plugin provides a high-level interface for interacting with the Motor WebAssembly enclave plugin.
//
// The Motor plugin operates as an MPC-based UCAN (User-Controlled Authorization Networks)
// KeyshareSource, providing sophisticated decentralized authorization capabilities. This package abstracts the
// underlying WASM implementation and provides type-safe method calls for:
//
// - UCAN token creation and delegation
// - MPC-based cryptographic signing and verification
// - DID generation and identity management
// - Secure enclave configuration and management
//
// # Usage Example
//
// Basic usage with enclave configuration:
//
// ctx := context.Background()
// enclaveData, _ := json.Marshal(&mpc.EnclaveData{...})
// plugin, err := LoadPluginWithEnclave(ctx, "sonr-testnet-1", enclaveData, nil)
// if err != nil {
// log.Fatal(err)
// }
//
// // Create UCAN token
// req := &NewOriginTokenRequest{
// AudienceDID: "did:sonr:audience",
// Attenuations: []map[string]any{
// {"can": []string{"sign"}, "with": "vault://example"},
// },
// }
// resp, err := plugin.NewOriginToken(req)
package plugin
import (
"context"
"encoding/json"
"fmt"
extism "github.com/extism/go-sdk"
)
// Plugin defines the interface for cryptographic operations provided by the WebAssembly enclave.
// It abstracts the underlying WASM implementation and provides type-safe method calls.
// Updated to match the refactored MPC-based UCAN KeyshareSource Motor plugin.
type Plugin interface {
// UCAN Token Operations
// NewOriginToken creates a new UCAN origin token using MPC signing.
NewOriginToken(req *NewOriginTokenRequest) (*UCANTokenResponse, error)
// NewAttenuatedToken creates a delegated UCAN token with attenuated permissions.
NewAttenuatedToken(req *NewAttenuatedTokenRequest) (*UCANTokenResponse, error)
// Cryptographic Operations
// SignData signs arbitrary data using the MPC enclave.
SignData(req *SignDataRequest) (*SignDataResponse, error)
// VerifyData verifies a signature against data using the MPC enclave.
VerifyData(req *VerifyDataRequest) (*VerifyDataResponse, error)
// Identity Operations
// GetIssuerDID retrieves the issuer DID, address, and chain code from the enclave.
GetIssuerDID() (*GetIssuerDIDResponse, error)
}
// LoadPluginWithEnclave initializes and loads the WebAssembly MPC-based UCAN enclave plugin
// with the specified enclave configuration. This method provides basic enclave configuration
// but does not include advanced features like health monitoring and automatic recovery.
//
// For production use, consider LoadPluginWithManager which provides enhanced lifecycle management.
//
// Parameters:
// - ctx: Context for plugin initialization
// - chainID: Chain ID for the enclave (e.g., "sonr-testnet-1")
// - enclaveData: JSON-encoded MPC enclave data
// - vaultConfig: Optional vault configuration parameters
//
// Returns a Plugin interface for UCAN token operations and MPC cryptographic functions.
func LoadPluginWithEnclave(
ctx context.Context,
chainID string,
enclaveData []byte,
vaultConfig map[string]any,
) (Plugin, error) {
// Verify WASM integrity before loading
if err := VerifyPluginIntegrity(motrPluginBytes); err != nil {
return nil, fmt.Errorf("WASM integrity verification failed: %w", err)
}
manifest := GetManifestWithEnclave(chainID, enclaveData, vaultConfig)
cfg := GetPluginConfig()
plugin, err := extism.NewPlugin(ctx, manifest, cfg, []extism.HostFunction{})
if err != nil {
return nil, err
}
return &pluginImpl{plugin: plugin}, nil
}
// LoadPluginWithManager loads a Motor plugin using the enhanced plugin manager.
// This is the recommended method for production use as it provides:
// - Health monitoring and automatic recovery
// - Plugin instance caching and reuse
// - Comprehensive configuration validation
// - Background cleanup of expired instances
//
// Parameters:
// - ctx: Context for plugin initialization
// - config: Complete enclave configuration including timeouts, security settings, etc.
//
// Returns a managed Plugin interface with enhanced error handling and recovery.
func LoadPluginWithManager(ctx context.Context, config *EnclaveConfig) (Plugin, error) {
return DefaultManager.LoadPlugin(ctx, config)
}
type pluginImpl struct {
plugin *extism.Plugin
}
// UCAN Token Operations - Primary interface for the refactored Motor plugin
func (p *pluginImpl) NewOriginToken(req *NewOriginTokenRequest) (*UCANTokenResponse, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
_, r, err := p.plugin.Call("new_origin_token", reqBytes)
if err != nil {
return nil, err
}
var resp UCANTokenResponse
if err := json.Unmarshal(r, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// NewAttenuatedToken creates an attenuated UCAN token by delegating from a parent token.
func (p *pluginImpl) NewAttenuatedToken(
req *NewAttenuatedTokenRequest,
) (*UCANTokenResponse, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
_, r, err := p.plugin.Call("new_attenuated_token", reqBytes)
if err != nil {
return nil, err
}
var resp UCANTokenResponse
if err := json.Unmarshal(r, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// Cryptographic Operations
func (p *pluginImpl) SignData(req *SignDataRequest) (*SignDataResponse, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
_, r, err := p.plugin.Call("sign_data", reqBytes)
if err != nil {
return nil, err
}
var resp SignDataResponse
if err := json.Unmarshal(r, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (p *pluginImpl) VerifyData(req *VerifyDataRequest) (*VerifyDataResponse, error) {
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
_, r, err := p.plugin.Call("verify_data", reqBytes)
if err != nil {
return nil, err
}
var resp VerifyDataResponse
if err := json.Unmarshal(r, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// Identity Operations
func (p *pluginImpl) GetIssuerDID() (*GetIssuerDIDResponse, error) {
_, r, err := p.plugin.Call("get_issuer_did", []byte{})
if err != nil {
return nil, err
}
var resp GetIssuerDIDResponse
if err := json.Unmarshal(r, &resp); err != nil {
return nil, err
}
return &resp, nil
}
@@ -0,0 +1,293 @@
// Package plugin provides security integration tests for WASM plugin system
package plugin
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"testing"
"time"
"github.com/sonr-io/sonr/crypto/wasm"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestPluginIntegrityVerification tests end-to-end plugin verification
func TestPluginIntegrityVerification(t *testing.T) {
// Get plugin bytes from embedded data
pluginBytes := motrPluginBytes
if len(pluginBytes) == 0 {
t.Skip("Plugin binary not available")
}
verifier := wasm.NewHashVerifier()
// Compute hash of actual plugin
actualHash := verifier.ComputeHash(pluginBytes)
t.Logf("Plugin hash: %s", actualHash)
// Add as trusted
verifier.AddTrustedHash("motr.wasm", actualHash)
// Verify succeeds with correct binary
err := verifier.VerifyHash("motr.wasm", pluginBytes)
assert.NoError(t, err, "valid plugin should verify")
// Tamper with plugin
tamperedPlugin := make([]byte, len(pluginBytes))
copy(tamperedPlugin, pluginBytes)
if len(tamperedPlugin) > 100 {
tamperedPlugin[100] ^= 0xFF // Flip bits
}
// Verification should fail
err = verifier.VerifyHash("motr.wasm", tamperedPlugin)
assert.Error(t, err, "tampered plugin should not verify")
assert.Contains(t, err.Error(), "hash verification failed")
}
// TestPluginSignatureVerification tests Ed25519 signature verification
func TestPluginSignatureVerification(t *testing.T) {
// Get plugin bytes from embedded data
pluginBytes := motrPluginBytes
if len(pluginBytes) == 0 {
t.Skip("Plugin binary not available")
}
// Generate signing keypair
_, privKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
// Create signer from private key
signer, err := wasm.NewSignerFromPrivateKey(privKey)
require.NoError(t, err)
// Create manifest using helper function
manifest, err := wasm.CreateSignatureManifest(pluginBytes, signer, "test-key")
require.NoError(t, err)
// Verify signature using the standalone function
err = wasm.VerifyWithManifest(pluginBytes, manifest)
assert.NoError(t, err, "valid signature should verify")
// Tamper with plugin
tamperedPlugin := make([]byte, len(pluginBytes))
copy(tamperedPlugin, pluginBytes)
if len(tamperedPlugin) > 0 {
tamperedPlugin[0] ^= 0xFF
}
// Verification should fail
err = wasm.VerifyWithManifest(tamperedPlugin, manifest)
assert.Error(t, err, "tampered plugin should not verify")
}
// TestPluginHashChainUpdate tests secure plugin updates
func TestPluginHashChainUpdate(t *testing.T) {
chain := wasm.NewHashChain()
// Simulate plugin update sequence
versions := []struct {
version string
hash string
}{
{"v1.0.0", "hash-v1.0.0"},
{"v1.0.1", "hash-v1.0.1"},
{"v1.1.0", "hash-v1.1.0"},
{"v2.0.0", "hash-v2.0.0"},
}
// Add versions to chain
for i, v := range versions {
timestamp := time.Now().Add(time.Duration(i) * time.Hour).Unix()
err := chain.AddEntry(v.version, v.hash, timestamp)
require.NoError(t, err)
}
// Verify chain integrity
err := chain.VerifyChain()
assert.NoError(t, err, "hash chain should be valid")
// Get latest version
latest, err := chain.GetLatestEntry()
require.NoError(t, err)
assert.Equal(t, "v2.0.0", latest.Version)
assert.Equal(t, "hash-v2.0.0", latest.Hash)
// Verify the chain has proper linkage
// The fourth entry (v2.0.0) should have the third entry's hash (v1.1.0) as its previous hash
assert.Equal(t, "hash-v1.1.0", latest.PreviousHash)
}
// TestPluginSizeRestrictions tests plugin size validation
func TestPluginSizeRestrictions(t *testing.T) {
policy := wasm.DefaultSecurityPolicy()
// Test various sizes
testCases := []struct {
name string
size int
allowed bool
}{
{"tiny", 1024, true},
{"small", 100 * 1024, true},
{"medium", 1024 * 1024, true},
{"large", 5 * 1024 * 1024, true},
{"max", 10 * 1024 * 1024, true},
{"oversized", 11 * 1024 * 1024, false},
{"huge", 100 * 1024 * 1024, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
module := make([]byte, tc.size)
err := policy.Validate(module)
if tc.allowed {
assert.NoError(t, err, "size %d should be allowed", tc.size)
} else {
assert.Error(t, err, "size %d should be rejected", tc.size)
if err != nil {
assert.Contains(t, err.Error(), "exceeds maximum allowed size")
}
}
})
}
}
// TestPluginUpdateRollback tests safe rollback mechanism
func TestPluginUpdateRollback(t *testing.T) {
verifier := wasm.NewHashVerifier()
chain := wasm.NewHashChain()
// Current version
currentPlugin := []byte("plugin v1.0.0 content")
currentHash := verifier.ComputeHash(currentPlugin)
verifier.AddTrustedHash("motr.wasm", currentHash)
err := chain.AddEntry("v1.0.0", currentHash, time.Now().Unix())
require.NoError(t, err)
// Update to new version
newPlugin := []byte("plugin v1.1.0 content with bugs")
newHash := verifier.ComputeHash(newPlugin)
// Simulate update
verifier.AddTrustedHash("motr.wasm", newHash)
err = chain.AddEntry("v1.1.0", newHash, time.Now().Add(1*time.Hour).Unix())
require.NoError(t, err)
// Verify new version works
err = verifier.VerifyHash("motr.wasm", newPlugin)
assert.NoError(t, err)
// Simulate rollback needed (new version has issues)
// Since we added v1.0.0 first, we know its hash
// Rollback to previous version using the known hash
verifier.AddTrustedHash("motr.wasm", currentHash)
// Add rollback entry to chain
err = chain.AddEntry("v1.1.1-rollback", currentHash, time.Now().Add(2*time.Hour).Unix())
require.NoError(t, err)
// Verify rollback works
err = verifier.VerifyHash("motr.wasm", currentPlugin)
assert.NoError(t, err, "rollback to previous version should work")
// New plugin should still fail if not updated
err = verifier.VerifyHash("motr.wasm", newPlugin)
assert.Error(t, err, "rolled back version should not verify new plugin")
}
// TestConcurrentPluginVerification tests thread safety
func TestConcurrentPluginVerification(t *testing.T) {
// Create test plugin
plugin := make([]byte, 1024*1024) // 1MB
_, err := rand.Read(plugin)
require.NoError(t, err)
verifier := wasm.NewHashVerifier()
hash := verifier.ComputeHash(plugin)
verifier.AddTrustedHash("concurrent-test", hash)
// Run concurrent verifications
done := make(chan bool, 100)
errors := make(chan error, 100)
for i := 0; i < 100; i++ {
go func() {
err := verifier.VerifyHash("concurrent-test", plugin)
if err != nil {
errors <- err
}
done <- true
}()
}
// Wait for all goroutines
for i := 0; i < 100; i++ {
<-done
}
close(errors)
// Check for errors
for err := range errors {
t.Errorf("concurrent verification failed: %v", err)
}
}
// TestPluginMemoryProtection tests memory safety
func TestPluginMemoryProtection(t *testing.T) {
// Test that sensitive data is cleared
// Create sensitive data
sensitiveData := []byte("sensitive-key-material")
// Simulate key operations
dataCopy := make([]byte, len(sensitiveData))
copy(dataCopy, sensitiveData)
// Clear original
for i := range sensitiveData {
sensitiveData[i] = 0
}
// Verify cleared
assert.True(t, bytes.Equal(sensitiveData, make([]byte, len(sensitiveData))),
"sensitive data should be cleared")
// Copy should still exist (for this test)
assert.NotEqual(t, dataCopy, sensitiveData,
"copy should be different from cleared data")
}
// BenchmarkPluginVerification benchmarks plugin verification
func BenchmarkPluginVerification(b *testing.B) {
plugin := make([]byte, 1024*1024) // 1MB plugin
rand.Read(plugin)
verifier := wasm.NewHashVerifier()
hash := verifier.ComputeHash(plugin)
verifier.AddTrustedHash("bench", hash)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = verifier.VerifyHash("bench", plugin)
}
}
// BenchmarkSignatureVerification benchmarks signature verification
func BenchmarkSignatureVerification(b *testing.B) {
plugin := make([]byte, 1024*1024) // 1MB
rand.Read(plugin)
_, privKey, _ := ed25519.GenerateKey(rand.Reader)
signer, _ := wasm.NewSignerFromPrivateKey(privKey)
manifest, _ := wasm.CreateSignatureManifest(plugin, signer, "bench-key")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = wasm.VerifyWithManifest(plugin, manifest)
}
}
+61
View File
@@ -0,0 +1,61 @@
package plugin
// UCAN Token Request/Response types matching Motor plugin
// NewOriginTokenRequest represents a request to create a new UCAN origin token.
type NewOriginTokenRequest struct {
AudienceDID string `json:"audience_did"` // Target audience DID for the token
Attenuations []map[string]any `json:"attenuations,omitempty"` // Capability attenuations
Facts []string `json:"facts,omitempty"` // Additional facts to include
NotBefore int64 `json:"not_before,omitempty"` // Token validity start time
ExpiresAt int64 `json:"expires_at,omitempty"` // Token expiration time
}
// NewAttenuatedTokenRequest represents a request to create a delegated UCAN token.
type NewAttenuatedTokenRequest struct {
ParentToken string `json:"parent_token"` // Parent token to derive from
AudienceDID string `json:"audience_did"` // Target audience DID for the token
Attenuations []map[string]any `json:"attenuations,omitempty"` // Capability attenuations
Facts []string `json:"facts,omitempty"` // Additional facts to include
NotBefore int64 `json:"not_before,omitempty"` // Token validity start time
ExpiresAt int64 `json:"expires_at,omitempty"` // Token expiration time
}
// UCANTokenResponse contains the result of UCAN token creation.
type UCANTokenResponse struct {
Token string `json:"token"` // Generated UCAN token
Issuer string `json:"issuer"` // Issuer DID of the token
Address string `json:"address"` // Address derived from issuer
Error string `json:"error,omitempty"` // Error message if creation failed
}
// SignDataRequest represents a request to sign arbitrary data.
type SignDataRequest struct {
Data []byte `json:"data"` // Data bytes to sign
}
// SignDataResponse contains the result of data signing.
type SignDataResponse struct {
Signature []byte `json:"signature"` // Generated signature bytes
Error string `json:"error,omitempty"` // Error message if signing failed
}
// VerifyDataRequest represents a request to verify a signature.
type VerifyDataRequest struct {
Data []byte `json:"data"` // Original data that was signed
Signature []byte `json:"signature"` // Signature bytes to verify
}
// VerifyDataResponse contains the result of signature verification.
type VerifyDataResponse struct {
Valid bool `json:"valid"` // Whether the signature is valid
Error string `json:"error,omitempty"` // Error message if verification failed
}
// GetIssuerDIDResponse contains issuer DID and address information.
type GetIssuerDIDResponse struct {
IssuerDID string `json:"issuer_did"` // Issuer DID derived from enclave
Address string `json:"address"` // Address derived from enclave
ChainCode string `json:"chain_code"` // Deterministic chain code
Error string `json:"error,omitempty"` // Error message if retrieval failed
}