From e7abcbdbb7c0eb0dd8f37f0cfc642c02c2970799 Mon Sep 17 00:00:00 2001 From: Mayuresh Nirhali Date: Sat, 13 Sep 2025 08:11:32 +0530 Subject: [PATCH 1/2] Initial commit for Issue503 --- install/build-plugins.sh | 1 + .../implementation/simplekeymanager/README.md | 162 +++++++ .../simplekeymanager/cmd/plugin.go | 45 ++ .../simplekeymanager/cmd/plugin_test.go | 212 +++++++++ .../simplekeymanager/simplekeymanager.go | 377 +++++++++++++++ .../simplekeymanager/simplekeymanager_test.go | 432 ++++++++++++++++++ pkg/plugin/manager.go | 22 + 7 files changed, 1251 insertions(+) create mode 100644 pkg/plugin/implementation/simplekeymanager/README.md create mode 100644 pkg/plugin/implementation/simplekeymanager/cmd/plugin.go create mode 100644 pkg/plugin/implementation/simplekeymanager/cmd/plugin_test.go create mode 100644 pkg/plugin/implementation/simplekeymanager/simplekeymanager.go create mode 100644 pkg/plugin/implementation/simplekeymanager/simplekeymanager_test.go diff --git a/install/build-plugins.sh b/install/build-plugins.sh index ada7ad2..f998e0e 100755 --- a/install/build-plugins.sh +++ b/install/build-plugins.sh @@ -11,6 +11,7 @@ plugins=( "decrypter" "encrypter" "keymanager" + "simplekeymanager" "publisher" "reqpreprocessor" "router" diff --git a/pkg/plugin/implementation/simplekeymanager/README.md b/pkg/plugin/implementation/simplekeymanager/README.md new file mode 100644 index 0000000..8f534ab --- /dev/null +++ b/pkg/plugin/implementation/simplekeymanager/README.md @@ -0,0 +1,162 @@ +# SimpleKeyManager Plugin + +A simple keymanager plugin for beckn-onix that reads Ed25519 and X25519 keys from configuration instead of using external secret management systems like HashiCorp Vault. + +## Overview + +This plugin provides a lightweight alternative to the vault keymanager by reading cryptographic keys directly from configuration. It's designed for development environments and simpler deployments that don't require the complexity of external secret management. + +## Features + +- **Ed25519 + X25519 Key Support**: Supports Ed25519 signing keys and X25519 encryption keys +- **Configuration-Based**: Reads keys from YAML configuration instead of environment variables +- **Multiple Formats**: Supports both PEM and Base64 encoded keys +- **Auto-detection**: Automatically detects key format (PEM vs Base64) +- **Zero Dependencies**: No external services required (unlike vault keymanager) +- **Memory Storage**: Stores keysets in memory for fast access + +## Configuration + +### Basic Configuration + +In your beckn-onix configuration file: + +```yaml +plugins: + keymanager: + id: simplekeymanager + config: + networkParticipant: bap-network + keyId: bap-network-key + signingPrivateKey: uc5WYG/eke0PVGyQ9JNVLpwQL0K9JIZfHfqUHdLBTaY= + signingPublicKey: kUSiFNAD3+6oE7KffKucxZ74e6g4i9VM6ypImg4rVCM= + encrPrivateKey: uc5WYG/eke0PVGyQ9JNVLpwQL0K9JIZfHfqUHdLBTaY= + encrPublicKey: kUSiFNAD3+6oE7KffKucxZ74e6g4i9VM6ypImg4rVCM= +``` + +### Configuration Options + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `networkParticipant` | string | Yes | Identifier for the keyset, represents subscriberId or networkParticipant name | +| `keyId` | string | Yes | Unique Key id for the keyset | +| `signingPrivateKey` | string | Yes* | Ed25519 private key for signing (Base64 or PEM) | +| `signingPublicKey` | string | Yes* | Ed25519 public key for signing (Base64 or PEM) | +| `encrPrivateKey` | string | Yes* | X25519 private key for encryption (Base64 or PEM) | +| `encrPublicKey` | string | Yes* | X25519 public key for encryption (Base64 or PEM) | + +*Required if any key is provided. If keys are configured, all four keys must be provided. + +### PEM Format Example + +```yaml +plugins: + keymanager: + id: simplekeymanager + config: + networkParticipant: bap-network + keyId: bap-network-key + signingPrivateKey: | + MC4CAQAwBQYDK0NiAAEguc5WYG/eke0PVGyQ9JNVLpwQL0K9JIZfHfqUHdLBTaY= + signingPublicKey: | + MCowBQYDK0NiAAEhkUSiFNAD3+6oE7KffKucxZ74e6g4i9VM6ypImg4rVCM= + encrPrivateKey: | + MC4CAQAwBQYDK0NiAAEguc5WYG/eke0PVGyQ9JNVLpwQL0K9JIZfHfqUHdLBTaY= + encrPublicKey: | + MCowBQYDK0NiAAEhkUSiFNAD3+6oE7KffKucxZ74e6g4i9VM6ypImg4rVCM= +``` + +## Key Generation + +### Ed25519 Signing Keys + +```bash +# Generate Ed25519 signing key pair +openssl genpkey -algorithm Ed25519 -out signing_private.pem +openssl pkey -in signing_private.pem -pubout -out signing_public.pem + +# Convert to base64 (single line) +signing_private_b64=$(openssl pkey -in signing_private.pem -outform DER | base64 -w 0) +signing_public_b64=$(openssl pkey -in signing_public.pem -pubin -outform DER | base64 -w 0) +``` + +### X25519 Encryption Keys + +```bash +# Generate X25519 encryption key pair +openssl genpkey -algorithm X25519 -out encr_private.pem +openssl pkey -in encr_private.pem -pubout -out encr_public.pem + +# Convert to base64 (single line) +encr_private_b64=$(openssl pkey -in encr_private.pem -outform DER | base64 -w 0) +encr_public_b64=$(openssl pkey -in encr_public.pem -pubin -outform DER | base64 -w 0) +``` + +## Usage + +The plugin implements the same `KeyManager` interface as the vault keymanager: + +- `GenerateKeyset() (*model.Keyset, error)` - Generate new key pair +- `InsertKeyset(ctx, keyID, keyset) error` - Store keyset in memory +- `Keyset(ctx, keyID) (*model.Keyset, error)` - Retrieve keyset from memory +- `DeleteKeyset(ctx, keyID) error` - Delete keyset from memory +- `LookupNPKeys(ctx, subscriberID, uniqueKeyID) (string, string, error)` - Lookup public keys from registry + +### Example Usage in Code + +```go +// The keyset from config is automatically loaded with the configured keyId +keyset, err := keyManager.Keyset(ctx, "bap-network") +if err != nil { + log.Fatal(err) +} + +// Generate new keys programmatically +newKeyset, err := keyManager.GenerateKeyset() +if err != nil { + log.Fatal(err) +} + +// Store the new keyset +err = keyManager.InsertKeyset(ctx, "new-key-id", newKeyset) +if err != nil { + log.Fatal(err) +} +``` + +## Comparison with Vault KeyManager + +| Feature | SimpleKeyManager | Vault KeyManager | +|---------|------------------|------------------| +| **Setup Complexity** | Very Low (config only) | High (requires Vault) | +| **Configuration** | YAML configuration | Vault connection + secrets | +| **Dependencies** | None | HashiCorp Vault | +| **Security** | Basic (config-based) | Advanced (centralized secrets) | +| **Key Rotation** | Manual config update | Automated options | +| **Audit Logging** | Application logs only | Full audit trails | +| **Multi-tenancy** | Limited (memory-based) | Full support | +| **Best for** | Development/Testing/Simple deployments | Production/Enterprise | + +## Testing + +Run tests with: +```bash +cd pkg/plugin/implementation/simplekeymanager +go test -v ./... +``` + +## Installation + +1. The plugin is automatically built with beckn-onix +2. Configure the plugin in your beckn-onix configuration file. Change in configuration requires restart of service. +3. The plugin will be loaded automatically when beckn-onix starts + +## Security Considerations + +- Configuration files contain sensitive key material +- Use proper file permissions for config files +- Implement regular key rotation + +## License + +This plugin follows the same license as the main beckn-onix project. diff --git a/pkg/plugin/implementation/simplekeymanager/cmd/plugin.go b/pkg/plugin/implementation/simplekeymanager/cmd/plugin.go new file mode 100644 index 0000000..aec9882 --- /dev/null +++ b/pkg/plugin/implementation/simplekeymanager/cmd/plugin.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + + "github.com/beckn/beckn-onix/pkg/log" + "github.com/beckn/beckn-onix/pkg/plugin/definition" + "github.com/beckn/beckn-onix/pkg/plugin/implementation/simplekeymanager" +) + +// simpleKeyManagerProvider implements the plugin provider for the SimpleKeyManager plugin. +type simpleKeyManagerProvider struct{} + +// newSimpleKeyManagerFunc is a function type that creates a new SimpleKeyManager instance. +var newSimpleKeyManagerFunc = simplekeymanager.New + +// New creates and initializes a new SimpleKeyManager instance using the provided cache, registry lookup, and configuration. +func (k *simpleKeyManagerProvider) New(ctx context.Context, cache definition.Cache, registry definition.RegistryLookup, cfg map[string]string) (definition.KeyManager, func() error, error) { + config := &simplekeymanager.Config{ + NetworkParticipant: cfg["networkParticipant"], + KeyID: cfg["keyId"], + SigningPrivateKey: cfg["signingPrivateKey"], + SigningPublicKey: cfg["signingPublicKey"], + EncrPrivateKey: cfg["encrPrivateKey"], + EncrPublicKey: cfg["encrPublicKey"], + } + log.Debugf(ctx, "SimpleKeyManager config mapped: np=%s, keyId=%s, has_signing_private=%v, has_signing_public=%v, has_encr_private=%v, has_encr_public=%v", + config.NetworkParticipant, + config.KeyID, + config.SigningPrivateKey != "", + config.SigningPublicKey != "", + config.EncrPrivateKey != "", + config.EncrPublicKey != "") + + km, cleanup, err := newSimpleKeyManagerFunc(ctx, cache, registry, config) + if err != nil { + log.Error(ctx, err, "Failed to initialize SimpleKeyManager") + return nil, nil, err + } + log.Debugf(ctx, "SimpleKeyManager instance created successfully") + return km, cleanup, nil +} + +// Provider is the exported instance of simpleKeyManagerProvider used for plugin registration. +var Provider = simpleKeyManagerProvider{} diff --git a/pkg/plugin/implementation/simplekeymanager/cmd/plugin_test.go b/pkg/plugin/implementation/simplekeymanager/cmd/plugin_test.go new file mode 100644 index 0000000..3229c3c --- /dev/null +++ b/pkg/plugin/implementation/simplekeymanager/cmd/plugin_test.go @@ -0,0 +1,212 @@ +package main + +import ( + "context" + "testing" + "time" + + "github.com/beckn/beckn-onix/pkg/model" + "github.com/beckn/beckn-onix/pkg/plugin/implementation/simplekeymanager" +) + +// Mock implementations for testing +type mockCache struct{} + +func (m *mockCache) Get(ctx context.Context, key string) (string, error) { + return "", nil +} + +func (m *mockCache) Set(ctx context.Context, key string, value string, ttl time.Duration) error { + return nil +} + +func (m *mockCache) Clear(ctx context.Context) error { + return nil +} + +func (m *mockCache) Delete(ctx context.Context, key string) error { + return nil +} + +type mockRegistry struct{} + +func (m *mockRegistry) Lookup(ctx context.Context, sub *model.Subscription) ([]model.Subscription, error) { + return nil, nil +} + +func TestSimpleKeyManagerProvider_New(t *testing.T) { + provider := &simpleKeyManagerProvider{} + ctx := context.Background() + cache := &mockCache{} + registry := &mockRegistry{} + + tests := []struct { + name string + config map[string]string + wantErr bool + }{ + { + name: "empty config", + config: map[string]string{}, + wantErr: false, + }, + { + name: "valid config with keys", + config: map[string]string{ + "networkParticipant": "bap-one", + "keyId": "test-key", + "signingPrivateKey": "dGVzdC1zaWduaW5nLXByaXZhdGU=", + "signingPublicKey": "dGVzdC1zaWduaW5nLXB1YmxpYw==", + "encrPrivateKey": "dGVzdC1lbmNyLXByaXZhdGU=", + "encrPublicKey": "dGVzdC1lbmNyLXB1YmxpYw==", + }, + wantErr: false, + }, + { + name: "invalid config - partial keys", + config: map[string]string{ + "keyId": "test-key", + "signingPrivateKey": "dGVzdC1zaWduaW5nLXByaXZhdGU=", + // Missing other required keys + }, + wantErr: true, + }, + { + name: "config with only keyId", + config: map[string]string{ + "keyId": "test-key", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + km, cleanup, err := provider.New(ctx, cache, registry, tt.config) + + if (err != nil) != tt.wantErr { + t.Errorf("simpleKeyManagerProvider.New() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if km == nil { + t.Error("simpleKeyManagerProvider.New() returned nil keymanager") + } + if cleanup == nil { + t.Error("simpleKeyManagerProvider.New() returned nil cleanup function") + } + if cleanup != nil { + // Test that cleanup doesn't panic + err := cleanup() + if err != nil { + t.Errorf("cleanup() error = %v", err) + } + } + } else { + if km != nil { + t.Error("simpleKeyManagerProvider.New() should return nil keymanager on error") + } + if cleanup != nil { + t.Error("simpleKeyManagerProvider.New() should return nil cleanup on error") + } + } + }) + } +} + +func TestSimpleKeyManagerProvider_NewWithNilDependencies(t *testing.T) { + provider := &simpleKeyManagerProvider{} + ctx := context.Background() + config := map[string]string{} + + // Test with nil cache + _, _, err := provider.New(ctx, nil, &mockRegistry{}, config) + if err == nil { + t.Error("simpleKeyManagerProvider.New() should fail with nil cache") + } + + // Test with nil registry + _, _, err = provider.New(ctx, &mockCache{}, nil, config) + if err == nil { + t.Error("simpleKeyManagerProvider.New() should fail with nil registry") + } + + // Test with both nil + _, _, err = provider.New(ctx, nil, nil, config) + if err == nil { + t.Error("simpleKeyManagerProvider.New() should fail with nil dependencies") + } +} + +func TestConfigMapping(t *testing.T) { + provider := &simpleKeyManagerProvider{} + ctx := context.Background() + cache := &mockCache{} + registry := &mockRegistry{} + + // Test config mapping + configMap := map[string]string{ + "networkParticipant": "mapped-np", + "keyId": "mapped-key-id", + "signingPrivateKey": "mapped-signing-private", + "signingPublicKey": "mapped-signing-public", + "encrPrivateKey": "mapped-encr-private", + "encrPublicKey": "mapped-encr-public", + } + + // We can't directly test the config mapping without exposing internals, + // but we can test that the mapping doesn't cause errors + _, cleanup, err := provider.New(ctx, cache, registry, configMap) + if err != nil { + t.Errorf("Config mapping failed: %v", err) + return + } + + if cleanup != nil { + cleanup() + } +} + +// Test that the provider implements the correct interface +// This is a compile-time check to ensure interface compliance +func TestProviderInterface(t *testing.T) { + provider := &simpleKeyManagerProvider{} + ctx := context.Background() + + // This should compile if the interface is implemented correctly + _, _, err := provider.New(ctx, &mockCache{}, &mockRegistry{}, map[string]string{}) + + // We expect an error here because of missing dependencies, but the call should compile + if err == nil { + // This might succeed with mocks, which is fine + t.Log("Provider.New() succeeded with mock dependencies") + } else { + t.Logf("Provider.New() failed as expected: %v", err) + } +} + +func TestNewSimpleKeyManagerFunc(t *testing.T) { + // Test that the function variable is set + if newSimpleKeyManagerFunc == nil { + t.Error("newSimpleKeyManagerFunc is nil") + } + + // Test that it points to the correct function + ctx := context.Background() + cache := &mockCache{} + registry := &mockRegistry{} + cfg := &simplekeymanager.Config{} + + // This should call the actual New function + _, cleanup, err := newSimpleKeyManagerFunc(ctx, cache, registry, cfg) + + if err != nil { + t.Logf("newSimpleKeyManagerFunc failed as expected with mocks: %v", err) + } else { + t.Log("newSimpleKeyManagerFunc succeeded with mock dependencies") + if cleanup != nil { + cleanup() + } + } +} diff --git a/pkg/plugin/implementation/simplekeymanager/simplekeymanager.go b/pkg/plugin/implementation/simplekeymanager/simplekeymanager.go new file mode 100644 index 0000000..51a2595 --- /dev/null +++ b/pkg/plugin/implementation/simplekeymanager/simplekeymanager.go @@ -0,0 +1,377 @@ +package simplekeymanager + +import ( + "context" + "crypto/ecdh" + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "strings" + + "github.com/beckn/beckn-onix/pkg/log" + "github.com/beckn/beckn-onix/pkg/model" + "github.com/beckn/beckn-onix/pkg/plugin/definition" + "github.com/google/uuid" +) + +// Config holds configuration parameters for SimpleKeyManager. +type Config struct { + NetworkParticipant string `yaml:"networkParticipant" json:"networkParticipant"` + KeyID string `yaml:"keyId" json:"keyId"` + SigningPrivateKey string `yaml:"signingPrivateKey" json:"signingPrivateKey"` + SigningPublicKey string `yaml:"signingPublicKey" json:"signingPublicKey"` + EncrPrivateKey string `yaml:"encrPrivateKey" json:"encrPrivateKey"` + EncrPublicKey string `yaml:"encrPublicKey" json:"encrPublicKey"` +} + +// SimpleKeyMgr provides methods for managing cryptographic keys using configuration. +type SimpleKeyMgr struct { + Registry definition.RegistryLookup + Cache definition.Cache + keysets map[string]*model.Keyset // In-memory storage for keysets +} + +var ( + // ErrEmptyKeyID indicates that the provided key ID is empty. + ErrEmptyKeyID = errors.New("invalid request: keyID cannot be empty") + + // ErrNilKeySet indicates that the provided keyset is nil. + ErrNilKeySet = errors.New("keyset cannot be nil") + + // ErrEmptySubscriberID indicates that the provided subscriber ID is empty. + ErrEmptySubscriberID = errors.New("invalid request: subscriberID cannot be empty") + + // ErrEmptyUniqueKeyID indicates that the provided unique key ID is empty. + ErrEmptyUniqueKeyID = errors.New("invalid request: uniqueKeyID cannot be empty") + + // ErrSubscriberNotFound indicates that no subscriber was found with the provided credentials. + ErrSubscriberNotFound = errors.New("no subscriber found with given credentials") + + // ErrNilCache indicates that the cache implementation is nil. + ErrNilCache = errors.New("cache implementation cannot be nil") + + // ErrNilRegistryLookup indicates that the registry lookup implementation is nil. + ErrNilRegistryLookup = errors.New("registry lookup implementation cannot be nil") + + // ErrKeysetNotFound indicates that the requested keyset was not found. + ErrKeysetNotFound = errors.New("keyset not found") + + // ErrInvalidConfig indicates that the configuration is invalid. + ErrInvalidConfig = errors.New("invalid configuration") +) + +// ValidateCfg validates the SimpleKeyManager configuration. +func ValidateCfg(cfg *Config) error { + if cfg == nil { + return fmt.Errorf("%w: config cannot be nil", ErrInvalidConfig) + } + + // But if keys are provided, all must be provided + hasKeys := cfg.SigningPrivateKey != "" || cfg.SigningPublicKey != "" || + cfg.EncrPrivateKey != "" || cfg.EncrPublicKey != "" || cfg.NetworkParticipant != "" || + cfg.KeyID != "" + + if hasKeys { + if cfg.SigningPrivateKey == "" { + return fmt.Errorf("%w: signingPrivateKey is required when keys are configured", ErrInvalidConfig) + } + if cfg.SigningPublicKey == "" { + return fmt.Errorf("%w: signingPublicKey is required when keys are configured", ErrInvalidConfig) + } + if cfg.EncrPrivateKey == "" { + return fmt.Errorf("%w: encrPrivateKey is required when keys are configured", ErrInvalidConfig) + } + if cfg.EncrPublicKey == "" { + return fmt.Errorf("%w: encrPublicKey is required when keys are configured", ErrInvalidConfig) + } + if cfg.NetworkParticipant == "" { + return fmt.Errorf("%w: networkParticipant is required when keys are configured", ErrInvalidConfig) + } + if cfg.KeyID == "" { + return fmt.Errorf("%w: keyId is required when keys are configured", ErrInvalidConfig) + } + } + + return nil +} + +var ( + ed25519KeyGenFunc = ed25519.GenerateKey + x25519KeyGenFunc = ecdh.X25519().GenerateKey + uuidGenFunc = uuid.NewRandom +) + +// New creates a new SimpleKeyMgr instance with the provided configuration, cache, and registry lookup. +func New(ctx context.Context, cache definition.Cache, registryLookup definition.RegistryLookup, cfg *Config) (*SimpleKeyMgr, func() error, error) { + log.Info(ctx, "Initializing SimpleKeyManager plugin") + + // Validate configuration. + if err := ValidateCfg(cfg); err != nil { + log.Error(ctx, err, "Invalid configuration for SimpleKeyManager") + return nil, nil, err + } + + // Check if cache implementation is provided. + if cache == nil { + log.Error(ctx, ErrNilCache, "Cache is nil in SimpleKeyManager initialization") + return nil, nil, ErrNilCache + } + + // Check if registry lookup implementation is provided. + if registryLookup == nil { + log.Error(ctx, ErrNilRegistryLookup, "RegistryLookup is nil in SimpleKeyManager initialization") + return nil, nil, ErrNilRegistryLookup + } + + log.Info(ctx, "Creating SimpleKeyManager instance") + + // Create SimpleKeyManager instance. + skm := &SimpleKeyMgr{ + Registry: registryLookup, + Cache: cache, + keysets: make(map[string]*model.Keyset), + } + + // Try to load keys from configuration if they exist + if err := skm.loadKeysFromConfig(ctx, cfg); err != nil { + log.Error(ctx, err, "Failed to load keys from configuration") + return nil, nil, err + } + + // Cleanup function to release SimpleKeyManager resources. + cleanup := func() error { + log.Info(ctx, "Cleaning up SimpleKeyManager resources") + skm.Cache = nil + skm.Registry = nil + skm.keysets = nil + return nil + } + + log.Info(ctx, "SimpleKeyManager plugin initialized successfully") + return skm, cleanup, nil +} + +// GenerateKeyset generates a new signing (Ed25519) and encryption (X25519) key pair. +func (skm *SimpleKeyMgr) GenerateKeyset() (*model.Keyset, error) { + signingPublic, signingPrivate, err := ed25519KeyGenFunc(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate signing key pair: %w", err) + } + + encrPrivateKey, err := x25519KeyGenFunc(rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate encryption key pair: %w", err) + } + encrPublicKey := encrPrivateKey.PublicKey().Bytes() + uuid, err := uuidGenFunc() + if err != nil { + return nil, fmt.Errorf("failed to generate unique key id uuid: %w", err) + } + return &model.Keyset{ + UniqueKeyID: uuid.String(), + SigningPrivate: encodeBase64(signingPrivate.Seed()), + SigningPublic: encodeBase64(signingPublic), + EncrPrivate: encodeBase64(encrPrivateKey.Bytes()), + EncrPublic: encodeBase64(encrPublicKey), + }, nil +} + +// InsertKeyset stores the given keyset in memory under the specified key ID. +func (skm *SimpleKeyMgr) InsertKeyset(ctx context.Context, keyID string, keys *model.Keyset) error { + if keyID == "" { + return ErrEmptyKeyID + } + if keys == nil { + return ErrNilKeySet + } + + log.Debugf(ctx, "Storing keyset for keyID: %s", keyID) + skm.keysets[keyID] = keys + log.Debugf(ctx, "Successfully stored keyset for keyID: %s", keyID) + return nil +} + +// DeleteKeyset deletes the keyset for the given key ID from memory. +func (skm *SimpleKeyMgr) DeleteKeyset(ctx context.Context, keyID string) error { + if keyID == "" { + return ErrEmptyKeyID + } + + log.Debugf(ctx, "Deleting keyset for keyID: %s", keyID) + if _, exists := skm.keysets[keyID]; !exists { + log.Warnf(ctx, "Keyset not found for keyID: %s", keyID) + return ErrKeysetNotFound + } + + delete(skm.keysets, keyID) + log.Debugf(ctx, "Successfully deleted keyset for keyID: %s", keyID) + return nil +} + +// Keyset retrieves the keyset for the given key ID from memory. +func (skm *SimpleKeyMgr) Keyset(ctx context.Context, keyID string) (*model.Keyset, error) { + if keyID == "" { + return nil, ErrEmptyKeyID + } + + log.Debugf(ctx, "Retrieving keyset for keyID: %s", keyID) + keyset, exists := skm.keysets[keyID] + if !exists { + log.Warnf(ctx, "Keyset not found for keyID: %s", keyID) + return nil, ErrKeysetNotFound + } + + // Return a copy to prevent external modifications + copyKeyset := &model.Keyset{ + SubscriberID: keyset.SubscriberID, + UniqueKeyID: keyset.UniqueKeyID, + SigningPrivate: keyset.SigningPrivate, + SigningPublic: keyset.SigningPublic, + EncrPrivate: keyset.EncrPrivate, + EncrPublic: keyset.EncrPublic, + } + + log.Debugf(ctx, "Successfully retrieved keyset for keyID: %s", keyID) + return copyKeyset, nil +} + +// LookupNPKeys retrieves the signing and encryption public keys for the given subscriber ID and unique key ID. +func (skm *SimpleKeyMgr) LookupNPKeys(ctx context.Context, subscriberID, uniqueKeyID string) (string, string, error) { + if err := validateParams(subscriberID, uniqueKeyID); err != nil { + return "", "", err + } + + cacheKey := fmt.Sprintf("%s_%s", subscriberID, uniqueKeyID) + cachedData, err := skm.Cache.Get(ctx, cacheKey) + if err == nil { + var keys model.Keyset + if err := json.Unmarshal([]byte(cachedData), &keys); err == nil { + log.Debugf(ctx, "Found cached keys for subscriber: %s, uniqueKeyID: %s", subscriberID, uniqueKeyID) + return keys.SigningPublic, keys.EncrPublic, nil + } + } + + log.Debugf(ctx, "Cache miss, looking up registry for subscriber: %s, uniqueKeyID: %s", subscriberID, uniqueKeyID) + subscribers, err := skm.Registry.Lookup(ctx, &model.Subscription{ + Subscriber: model.Subscriber{ + SubscriberID: subscriberID, + }, + KeyID: uniqueKeyID, + }) + if err != nil { + return "", "", fmt.Errorf("failed to lookup registry: %w", err) + } + if len(subscribers) == 0 { + return "", "", ErrSubscriberNotFound + } + + log.Debugf(ctx, "Successfully looked up keys for subscriber: %s, uniqueKeyID: %s", subscriberID, uniqueKeyID) + return subscribers[0].SigningPublicKey, subscribers[0].EncrPublicKey, nil +} + +// loadKeysFromConfig loads keys from configuration if they exist +func (skm *SimpleKeyMgr) loadKeysFromConfig(ctx context.Context, cfg *Config) error { + // Check if all keys are provided in configuration + if cfg.SigningPrivateKey != "" && cfg.SigningPublicKey != "" && + cfg.EncrPrivateKey != "" && cfg.EncrPublicKey != "" { + + log.Info(ctx, "Loading keys from configuration") + + signingPrivate, err := skm.parseKey(cfg.SigningPrivateKey) + if err != nil { + return fmt.Errorf("failed to parse signingPrivateKey: %w", err) + } + + signingPublic, err := skm.parseKey(cfg.SigningPublicKey) + if err != nil { + return fmt.Errorf("failed to parse signingPublicKey: %w", err) + } + + encrPrivate, err := skm.parseKey(cfg.EncrPrivateKey) + if err != nil { + return fmt.Errorf("failed to parse encrPrivateKey: %w", err) + } + + encrPublic, err := skm.parseKey(cfg.EncrPublicKey) + if err != nil { + return fmt.Errorf("failed to parse encrPublicKey: %w", err) + } + + // Determine keyID - use configured keyId or default to "default" + networkParticipant := cfg.NetworkParticipant + keyId := cfg.KeyID + + // Create keyset from configuration + keyset := &model.Keyset{ + SubscriberID: networkParticipant, + UniqueKeyID: keyId, + SigningPrivate: encodeBase64(signingPrivate), + SigningPublic: encodeBase64(signingPublic), + EncrPrivate: encodeBase64(encrPrivate), + EncrPublic: encodeBase64(encrPublic), + } + + // Store the keyset using the keyID + skm.keysets[networkParticipant] = keyset + log.Infof(ctx, "Successfully loaded keyset from configuration with keyID: %s", keyId) + } else { + log.Debug(ctx, "No keys found in configuration, keyset storage will be empty initially") + } + + return nil +} + +// parseKey auto-detects and parses key data (PEM or base64) +func (skm *SimpleKeyMgr) parseKey(keyData string) ([]byte, error) { + keyData = strings.TrimSpace(keyData) + if keyData == "" { + return nil, fmt.Errorf("key data is empty") + } + + // Auto-detect format: if starts with "-----BEGIN", it's PEM; otherwise base64 + if strings.HasPrefix(keyData, "-----BEGIN") { + return skm.parsePEMKey(keyData) + } else { + return skm.parseBase64Key(keyData) + } +} + +// parsePEMKey parses PEM encoded key +func (skm *SimpleKeyMgr) parsePEMKey(keyData string) ([]byte, error) { + block, _ := pem.Decode([]byte(keyData)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM key") + } + + return block.Bytes, nil +} + +// parseBase64Key parses base64 encoded key +func (skm *SimpleKeyMgr) parseBase64Key(keyData string) ([]byte, error) { + decoded, err := base64.StdEncoding.DecodeString(keyData) + if err != nil { + return nil, fmt.Errorf("failed to decode base64 key: %w", err) + } + + return decoded, nil +} + +// validateParams checks that subscriberID and uniqueKeyID are not empty. +func validateParams(subscriberID, uniqueKeyID string) error { + if subscriberID == "" { + return ErrEmptySubscriberID + } + if uniqueKeyID == "" { + return ErrEmptyUniqueKeyID + } + return nil +} + +// encodeBase64 returns the base64-encoded string of the given data. +func encodeBase64(data []byte) string { + return base64.StdEncoding.EncodeToString(data) +} diff --git a/pkg/plugin/implementation/simplekeymanager/simplekeymanager_test.go b/pkg/plugin/implementation/simplekeymanager/simplekeymanager_test.go new file mode 100644 index 0000000..44eb3f2 --- /dev/null +++ b/pkg/plugin/implementation/simplekeymanager/simplekeymanager_test.go @@ -0,0 +1,432 @@ +package simplekeymanager + +import ( + "context" + "encoding/base64" + "testing" + "time" + + "github.com/beckn/beckn-onix/pkg/model" +) + +// Mock implementations for testing +type mockCache struct{} + +func (m *mockCache) Get(ctx context.Context, key string) (string, error) { + return "", nil +} + +func (m *mockCache) Set(ctx context.Context, key string, value string, ttl time.Duration) error { + return nil +} + +func (m *mockCache) Clear(ctx context.Context) error { + return nil +} + +func (m *mockCache) Delete(ctx context.Context, key string) error { + return nil +} + +type mockRegistry struct{} + +func (m *mockRegistry) Lookup(ctx context.Context, sub *model.Subscription) ([]model.Subscription, error) { + return []model.Subscription{ + { + Subscriber: model.Subscriber{ + SubscriberID: sub.SubscriberID, + }, + KeyID: sub.KeyID, + SigningPublicKey: "test-signing-public-key", + EncrPublicKey: "test-encr-public-key", + }, + }, nil +} + +func TestValidateCfg(t *testing.T) { + tests := []struct { + name string + cfg *Config + wantErr bool + }{ + { + name: "nil config", + cfg: nil, + wantErr: true, + }, + { + name: "empty config", + cfg: &Config{}, + wantErr: false, + }, + { + name: "valid config with all keys", + cfg: &Config{ + NetworkParticipant: "test-np", + KeyID: "test-key", + SigningPrivateKey: "dGVzdC1zaWduaW5nLXByaXZhdGU=", + SigningPublicKey: "dGVzdC1zaWduaW5nLXB1YmxpYw==", + EncrPrivateKey: "dGVzdC1lbmNyLXByaXZhdGU=", + EncrPublicKey: "dGVzdC1lbmNyLXB1YmxpYw==", + }, + wantErr: false, + }, + { + name: "partial keys - should fail", + cfg: &Config{ + KeyID: "test-key", + SigningPrivateKey: "dGVzdC1zaWduaW5nLXByaXZhdGU=", + // Missing other keys + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateCfg(tt.cfg) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateCfg() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestNew(t *testing.T) { + ctx := context.Background() + cache := &mockCache{} + registry := &mockRegistry{} + + tests := []struct { + name string + cfg *Config + wantErr bool + }{ + { + name: "valid empty config", + cfg: &Config{}, + wantErr: false, + }, + { + name: "nil config", + cfg: nil, + wantErr: true, + }, + { + name: "valid config with keys", + cfg: &Config{ + NetworkParticipant: "test-np", + KeyID: "test-key", + SigningPrivateKey: "dGVzdC1zaWduaW5nLXByaXZhdGU=", + SigningPublicKey: "dGVzdC1zaWduaW5nLXB1YmxpYw==", + EncrPrivateKey: "dGVzdC1lbmNyLXByaXZhdGU=", + EncrPublicKey: "dGVzdC1lbmNyLXB1YmxpYw==", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + skm, cleanup, err := New(ctx, cache, registry, tt.cfg) + + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if skm == nil { + t.Error("New() returned nil SimpleKeyMgr") + } + if cleanup == nil { + t.Error("New() returned nil cleanup function") + } + if cleanup != nil { + cleanup() // Test cleanup doesn't panic + } + } + }) + } +} + +func TestNewWithNilDependencies(t *testing.T) { + ctx := context.Background() + cfg := &Config{} + + // Test nil cache + _, _, err := New(ctx, nil, &mockRegistry{}, cfg) + if err == nil { + t.Error("New() should fail with nil cache") + } + + // Test nil registry + _, _, err = New(ctx, &mockCache{}, nil, cfg) + if err == nil { + t.Error("New() should fail with nil registry") + } +} + +func TestGenerateKeyset(t *testing.T) { + skm := &SimpleKeyMgr{} + + keyset, err := skm.GenerateKeyset() + if err != nil { + t.Errorf("GenerateKeyset() error = %v", err) + return + } + + if keyset == nil { + t.Error("GenerateKeyset() returned nil keyset") + return + } + + // Check that all fields are populated + if keyset.SubscriberID == "" { + t.Error("GenerateKeyset() SubscriberID is empty") + } + if keyset.UniqueKeyID == "" { + t.Error("GenerateKeyset() UniqueKeyID is empty") + } + if keyset.SigningPrivate == "" { + t.Error("GenerateKeyset() SigningPrivate is empty") + } + if keyset.SigningPublic == "" { + t.Error("GenerateKeyset() SigningPublic is empty") + } + if keyset.EncrPrivate == "" { + t.Error("GenerateKeyset() EncrPrivate is empty") + } + if keyset.EncrPublic == "" { + t.Error("GenerateKeyset() EncrPublic is empty") + } +} + +func TestInsertKeyset(t *testing.T) { + skm := &SimpleKeyMgr{ + keysets: make(map[string]*model.Keyset), + } + ctx := context.Background() + + keyset := &model.Keyset{ + SubscriberID: "test-np", + UniqueKeyID: "test-uuid", + SigningPrivate: "test-signing-private", + SigningPublic: "test-signing-public", + EncrPrivate: "test-encr-private", + EncrPublic: "test-encr-public", + } + + // Test successful insertion + err := skm.InsertKeyset(ctx, "test-key", keyset) + if err != nil { + t.Errorf("InsertKeyset() error = %v", err) + } + + // Verify insertion + stored, exists := skm.keysets["test-key"] + if !exists { + t.Error("InsertKeyset() did not store keyset") + } + if stored != keyset { + t.Error("InsertKeyset() stored different keyset") + } + + // Test error cases + err = skm.InsertKeyset(ctx, "", keyset) + if err == nil { + t.Error("InsertKeyset() should fail with empty keyID") + } + + err = skm.InsertKeyset(ctx, "test-key2", nil) + if err == nil { + t.Error("InsertKeyset() should fail with nil keyset") + } +} + +func TestKeyset(t *testing.T) { + originalKeyset := &model.Keyset{ + SubscriberID: "test-np", + UniqueKeyID: "test-uuid", + SigningPrivate: "test-signing-private", + SigningPublic: "test-signing-public", + EncrPrivate: "test-encr-private", + EncrPublic: "test-encr-public", + } + + skm := &SimpleKeyMgr{ + keysets: map[string]*model.Keyset{ + "test-key": originalKeyset, + }, + } + ctx := context.Background() + + // Test successful retrieval + keyset, err := skm.Keyset(ctx, "test-key") + if err != nil { + t.Errorf("Keyset() error = %v", err) + return + } + + if keyset == nil { + t.Error("Keyset() returned nil") + return + } + + // Verify it's a copy, not the same instance + if keyset == originalKeyset { + t.Error("Keyset() should return a copy, not original") + } + + // Verify content is the same + if keyset.UniqueKeyID != originalKeyset.UniqueKeyID { + t.Error("Keyset() copy has different UniqueKeyID") + } + + // Test error cases + _, err = skm.Keyset(ctx, "") + if err == nil { + t.Error("Keyset() should fail with empty keyID") + } + + _, err = skm.Keyset(ctx, "non-existent") + if err == nil { + t.Error("Keyset() should fail with non-existent keyID") + } +} + +func TestDeleteKeyset(t *testing.T) { + originalKeyset := &model.Keyset{ + UniqueKeyID: "test-uuid", + } + + skm := &SimpleKeyMgr{ + keysets: map[string]*model.Keyset{ + "test-key": originalKeyset, + }, + } + ctx := context.Background() + + // Test successful deletion + err := skm.DeleteKeyset(ctx, "test-key") + if err != nil { + t.Errorf("DeleteKeyset() error = %v", err) + } + + // Verify deletion + _, exists := skm.keysets["test-key"] + if exists { + t.Error("DeleteKeyset() did not delete keyset") + } + + // Test error cases + err = skm.DeleteKeyset(ctx, "") + if err == nil { + t.Error("DeleteKeyset() should fail with empty keyID") + } + + err = skm.DeleteKeyset(ctx, "non-existent") + if err == nil { + t.Error("DeleteKeyset() should fail with non-existent keyID") + } +} + +func TestLookupNPKeys(t *testing.T) { + skm := &SimpleKeyMgr{ + Cache: &mockCache{}, + Registry: &mockRegistry{}, + } + ctx := context.Background() + + // Test successful lookup + signingKey, encrKey, err := skm.LookupNPKeys(ctx, "test-subscriber", "test-key") + if err != nil { + t.Errorf("LookupNPKeys() error = %v", err) + return + } + + if signingKey == "" { + t.Error("LookupNPKeys() returned empty signing key") + } + if encrKey == "" { + t.Error("LookupNPKeys() returned empty encryption key") + } + + // Test error cases + _, _, err = skm.LookupNPKeys(ctx, "", "test-key") + if err == nil { + t.Error("LookupNPKeys() should fail with empty subscriberID") + } + + _, _, err = skm.LookupNPKeys(ctx, "test-subscriber", "") + if err == nil { + t.Error("LookupNPKeys() should fail with empty uniqueKeyID") + } +} + +func TestParseKey(t *testing.T) { + skm := &SimpleKeyMgr{} + + // Test base64 key + testData := "hello world" + base64Data := base64.StdEncoding.EncodeToString([]byte(testData)) + + result, err := skm.parseKey(base64Data) + if err != nil { + t.Errorf("parseKey() error = %v", err) + return + } + + if string(result) != testData { + t.Errorf("parseKey() = %s, want %s", string(result), testData) + } + + // Test error cases + _, err = skm.parseKey("") + if err == nil { + t.Error("parseKey() should fail with empty input") + } + + _, err = skm.parseKey("invalid-base64!") + if err == nil { + t.Error("parseKey() should fail with invalid base64") + } +} + +func TestLoadKeysFromConfig(t *testing.T) { + skm := &SimpleKeyMgr{ + keysets: make(map[string]*model.Keyset), + } + ctx := context.Background() + + // Test with valid config + cfg := &Config{ + NetworkParticipant: "test-np", + KeyID: "test-key", + SigningPrivateKey: base64.StdEncoding.EncodeToString([]byte("signing-private")), + SigningPublicKey: base64.StdEncoding.EncodeToString([]byte("signing-public")), + EncrPrivateKey: base64.StdEncoding.EncodeToString([]byte("encr-private")), + EncrPublicKey: base64.StdEncoding.EncodeToString([]byte("encr-public")), + } + + err := skm.loadKeysFromConfig(ctx, cfg) + if err != nil { + t.Errorf("loadKeysFromConfig() error = %v", err) + return + } + + // Verify keyset was loaded + _, exists := skm.keysets["test-np"] + if !exists { + t.Error("loadKeysFromConfig() did not load keyset") + } + + // Test with empty config (should not error) + skm2 := &SimpleKeyMgr{ + keysets: make(map[string]*model.Keyset), + } + err = skm2.loadKeysFromConfig(ctx, &Config{}) + if err != nil { + t.Errorf("loadKeysFromConfig() with empty config error = %v", err) + } +} diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 1fa6655..c44135b 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -339,6 +339,28 @@ func (m *Manager) KeyManager(ctx context.Context, cache definition.Cache, rClien return km, nil } +// KeyManager returns a KeyManager instance based on the provided configuration. +// It reuses the loaded provider. +func (m *Manager) SimpleKeyManager(ctx context.Context, cache definition.Cache, rClient definition.RegistryLookup, cfg *Config) (definition.KeyManager, error) { + + kmp, err := provider[definition.KeyManagerProvider](m.plugins, cfg.ID) + if err != nil { + return nil, fmt.Errorf("failed to load provider for %s: %w", cfg.ID, err) + } + km, closer, err := kmp.New(ctx, cache, rClient, cfg.Config) + if err != nil { + return nil, err + } + if closer != nil { + m.closers = append(m.closers, func() { + if err := closer(); err != nil { + panic(err) + } + }) + } + return km, nil +} + // Validator implements handler.PluginManager. func (m *Manager) Validator(ctx context.Context, cfg *Config) (definition.SchemaValidator, error) { panic("unimplemented") From d92c03c85dc806139b734e0d71a89aa9c4525bb6 Mon Sep 17 00:00:00 2001 From: Mayuresh A Nirhali Date: Sat, 13 Sep 2025 08:43:27 +0530 Subject: [PATCH 2/2] Update README.md removed ref to pem keys. --- .../implementation/simplekeymanager/README.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pkg/plugin/implementation/simplekeymanager/README.md b/pkg/plugin/implementation/simplekeymanager/README.md index 8f534ab..86adfb9 100644 --- a/pkg/plugin/implementation/simplekeymanager/README.md +++ b/pkg/plugin/implementation/simplekeymanager/README.md @@ -47,25 +47,6 @@ plugins: *Required if any key is provided. If keys are configured, all four keys must be provided. -### PEM Format Example - -```yaml -plugins: - keymanager: - id: simplekeymanager - config: - networkParticipant: bap-network - keyId: bap-network-key - signingPrivateKey: | - MC4CAQAwBQYDK0NiAAEguc5WYG/eke0PVGyQ9JNVLpwQL0K9JIZfHfqUHdLBTaY= - signingPublicKey: | - MCowBQYDK0NiAAEhkUSiFNAD3+6oE7KffKucxZ74e6g4i9VM6ypImg4rVCM= - encrPrivateKey: | - MC4CAQAwBQYDK0NiAAEguc5WYG/eke0PVGyQ9JNVLpwQL0K9JIZfHfqUHdLBTaY= - encrPublicKey: | - MCowBQYDK0NiAAEhkUSiFNAD3+6oE7KffKucxZ74e6g4i9VM6ypImg4rVCM= -``` - ## Key Generation ### Ed25519 Signing Keys