mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 02:11:40 +00:00
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user