diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..67f3590 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/beckn/beckn-onix + +go 1.23.0 + +toolchain go1.23.7 + +require golang.org/x/crypto v0.36.0 + +require ( + golang.org/x/sys v0.31.0 // indirect + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d05e730 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/shared/plugin/definition/signVerifier.go b/shared/plugin/definition/signVerifier.go new file mode 100644 index 0000000..fe36358 --- /dev/null +++ b/shared/plugin/definition/signVerifier.go @@ -0,0 +1,22 @@ +package definition + +import "context" + +// Verifier defines the method for verifying signatures. +type Verifier interface { + // Verify checks the validity of the signature for the given body. + Verify(ctx context.Context, body []byte, header []byte, publicKeyBase64 string) (bool, error) + Close() error // Close for releasing resources +} + +// VerifierProvider initializes a new Verifier instance with the given config. +type VerifierProvider interface { + // New creates a new Verifier instance based on the provided config. + New(ctx context.Context, config map[string]string) (Verifier, func() error, error) +} + +// PublicKeyManager is the interface for key management plugin. +type PublicKeyManager interface { + // PublicKey retrieves the public key for the given subscriberID and keyID. + PublicKey(ctx context.Context, subscriberID string, keyID string) (string, error) +} diff --git a/shared/plugin/definition/signer.go b/shared/plugin/definition/signer.go new file mode 100644 index 0000000..84db5f5 --- /dev/null +++ b/shared/plugin/definition/signer.go @@ -0,0 +1,24 @@ +package definition + +import "context" + +// Signer defines the method for signing. +type Signer interface { + // Sign generates a signature for the given body and privateKeyBase64. + // The signature is created with the given timestamps: createdAt (signature creation time) + // and expiresAt (signature expiration time). + Sign(ctx context.Context, body []byte, privateKeyBase64 string, createdAt, expiresAt int64) (string, error) + Close() error // Close for releasing resources +} + +// SignerProvider initializes a new signer instance with the given config. +type SignerProvider interface { + // New creates a new signer instance based on the provided config. + New(ctx context.Context, config map[string]string) (Signer, func() error, error) +} + +// PrivateKeyManager is the interface for key management plugin. +type PrivateKeyManager interface { + // PrivateKey retrieves the private key for the given subscriberID and keyID. + PrivateKey(ctx context.Context, subscriberID string, keyID string) (string, error) +} diff --git a/shared/plugin/implementation/signVerifier/cmd/plugin.go b/shared/plugin/implementation/signVerifier/cmd/plugin.go new file mode 100644 index 0000000..1e4fb06 --- /dev/null +++ b/shared/plugin/implementation/signVerifier/cmd/plugin.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "errors" + + "github.com/beckn/beckn-onix/shared/plugin/definition" + + plugin "github.com/beckn/beckn-onix/shared/plugin/definition" + verifier "github.com/beckn/beckn-onix/shared/plugin/implementation/signVerifier" +) + +// VerifierProvider provides instances of Verifier. +type VerifierProvider struct{} + +// New initializes a new Verifier instance. +func (vp VerifierProvider) New(ctx context.Context, config map[string]string) (plugin.Verifier, func() error, error) { + if ctx == nil { + return nil, nil, errors.New("context cannot be nil") + } + + return verifier.New(ctx, &verifier.Config{}) +} + +// Provider is the exported symbol that the plugin manager will look for. +var Provider definition.VerifierProvider = VerifierProvider{} diff --git a/shared/plugin/implementation/signVerifier/cmd/plugin_test.go b/shared/plugin/implementation/signVerifier/cmd/plugin_test.go new file mode 100644 index 0000000..85caee5 --- /dev/null +++ b/shared/plugin/implementation/signVerifier/cmd/plugin_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "testing" +) + +// TestVerifierProviderSuccess tests successful creation of a verifier. +func TestVerifierProviderSuccess(t *testing.T) { + provider := VerifierProvider{} + + tests := []struct { + name string + ctx context.Context + config map[string]string + }{ + { + name: "Successful creation", + ctx: context.Background(), + config: map[string]string{}, + }, + { + name: "Nil context", + ctx: context.TODO(), + config: map[string]string{}, + }, + { + name: "Empty config", + ctx: context.Background(), + config: map[string]string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + verifier, close, err := provider.New(tt.ctx, tt.config) + + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + if verifier == nil { + t.Fatal("Expected verifier instance to be non-nil") + } + if close != nil { + if err := close(); err != nil { + t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err) + } + } + }) + } +} + +// TestVerifierProviderFailure tests cases where verifier creation should fail. +func TestVerifierProviderFailure(t *testing.T) { + provider := VerifierProvider{} + + tests := []struct { + name string + ctx context.Context + config map[string]string + wantErr bool + }{ + { + name: "Nil context failure", + ctx: nil, + config: map[string]string{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + verifierInstance, close, err := provider.New(tt.ctx, tt.config) + + if (err != nil) != tt.wantErr { + t.Fatalf("Expected error: %v, but got: %v", tt.wantErr, err) + } + if verifierInstance != nil { + t.Fatal("Expected verifier instance to be nil") + } + if close != nil { + if err := close(); err != nil { + t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err) + } + } + + }) + } +} diff --git a/shared/plugin/implementation/signVerifier/signVerifier.go b/shared/plugin/implementation/signVerifier/signVerifier.go new file mode 100644 index 0000000..963d137 --- /dev/null +++ b/shared/plugin/implementation/signVerifier/signVerifier.go @@ -0,0 +1,120 @@ +package verifier + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "fmt" + "strconv" + "strings" + "time" + + "golang.org/x/crypto/blake2b" +) + +// Config struct for Verifier. +type Config struct { +} + +// Verifier implements the Verifier interface. +type Verifier struct { + config *Config +} + +// New creates a new Verifier instance. +func New(ctx context.Context, config *Config) (*Verifier, func() error, error) { + v := &Verifier{config: config} + + return v, v.Close, nil +} + +// Verify checks the signature for the given payload and public key. +func (v *Verifier) Verify(ctx context.Context, body []byte, header []byte, publicKeyBase64 string) (bool, error) { + createdTimestamp, expiredTimestamp, signature, err := parseAuthHeader(string(header)) + if err != nil { + // TODO: Return appropriate error code when Error Code Handling Module is ready + return false, fmt.Errorf("error parsing header: %w", err) + } + + signatureBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + // TODO: Return appropriate error code when Error Code Handling Module is ready + return false, fmt.Errorf("error decoding signature: %w", err) + } + + currentTime := time.Now().Unix() + if createdTimestamp > currentTime || currentTime > expiredTimestamp { + // TODO: Return appropriate error code when Error Code Handling Module is ready + return false, fmt.Errorf("signature is expired or not yet valid") + } + + createdTime := time.Unix(createdTimestamp, 0) + expiredTime := time.Unix(expiredTimestamp, 0) + + signingString := hash(body, createdTime.Unix(), expiredTime.Unix()) + + decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKeyBase64) + if err != nil { + // TODO: Return appropriate error code when Error Code Handling Module is ready + return false, fmt.Errorf("error decoding public key: %w", err) + } + + if !ed25519.Verify(ed25519.PublicKey(decodedPublicKey), []byte(signingString), signatureBytes) { + // TODO: Return appropriate error code when Error Code Handling Module is ready + return false, fmt.Errorf("signature verification failed") + } + + return true, nil +} + +// parseAuthHeader extracts signature values from the Authorization header. +func parseAuthHeader(header string) (int64, int64, string, error) { + header = strings.TrimPrefix(header, "Signature ") + + parts := strings.Split(header, ",") + signatureMap := make(map[string]string) + + for _, part := range parts { + keyValue := strings.SplitN(strings.TrimSpace(part), "=", 2) + if len(keyValue) == 2 { + key := strings.TrimSpace(keyValue[0]) + value := strings.Trim(keyValue[1], "\"") + signatureMap[key] = value + } + } + + createdTimestamp, err := strconv.ParseInt(signatureMap["created"], 10, 64) + if err != nil { + // TODO: Return appropriate error code when Error Code Handling Module is ready + return 0, 0, "", fmt.Errorf("invalid created timestamp: %w", err) + } + + expiredTimestamp, err := strconv.ParseInt(signatureMap["expires"], 10, 64) + if err != nil { + // TODO: Return appropriate error code when Error Code Handling Module is ready + return 0, 0, "", fmt.Errorf("invalid expires timestamp: %w", err) + } + + signature := signatureMap["signature"] + if signature == "" { + // TODO: Return appropriate error code when Error Code Handling Module is ready + return 0, 0, "", fmt.Errorf("signature missing in header") + } + + return createdTimestamp, expiredTimestamp, signature, nil +} + +// hash constructs a signing string for verification. +func hash(payload []byte, createdTimestamp, expiredTimestamp int64) string { + hasher, _ := blake2b.New512(nil) + hasher.Write(payload) + hashSum := hasher.Sum(nil) + digestB64 := base64.StdEncoding.EncodeToString(hashSum) + + return fmt.Sprintf("(created): %d\n(expires): %d\ndigest: BLAKE-512=%s", createdTimestamp, expiredTimestamp, digestB64) +} + +// Close releases resources (mock implementation returning nil). +func (v *Verifier) Close() error { + return nil +} diff --git a/shared/plugin/implementation/signVerifier/signVerifier_test.go b/shared/plugin/implementation/signVerifier/signVerifier_test.go new file mode 100644 index 0000000..36da03a --- /dev/null +++ b/shared/plugin/implementation/signVerifier/signVerifier_test.go @@ -0,0 +1,153 @@ +package verifier + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "strconv" + "testing" + "time" +) + +// generateTestKeyPair generates a new ED25519 key pair for testing. +func generateTestKeyPair() (string, string) { + publicKey, privateKey, _ := ed25519.GenerateKey(nil) + return base64.StdEncoding.EncodeToString(privateKey), base64.StdEncoding.EncodeToString(publicKey) +} + +// signTestData creates a valid signature for test cases. +func signTestData(privateKeyBase64 string, body []byte, createdAt, expiresAt int64) string { + privateKeyBytes, _ := base64.StdEncoding.DecodeString(privateKeyBase64) + privateKey := ed25519.PrivateKey(privateKeyBytes) + + signingString := hash(body, createdAt, expiresAt) + signature := ed25519.Sign(privateKey, []byte(signingString)) + + return base64.StdEncoding.EncodeToString(signature) +} + +// TestVerifySuccessCases tests all valid signature verification cases. +func TestVerifySuccess(t *testing.T) { + privateKeyBase64, publicKeyBase64 := generateTestKeyPair() + + tests := []struct { + name string + body []byte + createdAt int64 + expiresAt int64 + }{ + { + name: "Valid Signature", + body: []byte("Test Payload"), + createdAt: time.Now().Unix(), + expiresAt: time.Now().Unix() + 3600, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + signature := signTestData(privateKeyBase64, tt.body, tt.createdAt, tt.expiresAt) + header := `Signature created="` + strconv.FormatInt(tt.createdAt, 10) + + `", expires="` + strconv.FormatInt(tt.expiresAt, 10) + + `", signature="` + signature + `"` + + verifier, close, _ := New(context.Background(), &Config{}) + valid, err := verifier.Verify(context.Background(), tt.body, []byte(header), publicKeyBase64) + + if err != nil { + t.Fatalf("Expected no error, but got: %v", err) + } + if !valid { + t.Fatal("Expected signature verification to succeed") + } + if close != nil { + if err := close(); err != nil { + t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err) + } + } + }) + } +} + +// TestVerifyFailureCases tests all invalid signature verification cases. +func TestVerifyFailure(t *testing.T) { + privateKeyBase64, publicKeyBase64 := generateTestKeyPair() + _, wrongPublicKeyBase64 := generateTestKeyPair() + + tests := []struct { + name string + body []byte + header string + pubKey string + }{ + { + name: "Missing Authorization Header", + body: []byte("Test Payload"), + header: "", + pubKey: publicKeyBase64, + }, + { + name: "Malformed Header", + body: []byte("Test Payload"), + header: `InvalidSignature created="wrong"`, + pubKey: publicKeyBase64, + }, + { + name: "Invalid Base64 Signature", + body: []byte("Test Payload"), + header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) + + `", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) + + `", signature="!!INVALIDBASE64!!"`, + pubKey: publicKeyBase64, + }, + { + name: "Expired Signature", + body: []byte("Test Payload"), + header: `Signature created="` + strconv.FormatInt(time.Now().Unix()-7200, 10) + + `", expires="` + strconv.FormatInt(time.Now().Unix()-3600, 10) + + `", signature="` + signTestData(privateKeyBase64, []byte("Test Payload"), time.Now().Unix()-7200, time.Now().Unix()-3600) + `"`, + pubKey: publicKeyBase64, + }, + { + name: "Invalid Public Key", + body: []byte("Test Payload"), + header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) + + `", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) + + `", signature="` + signTestData(privateKeyBase64, []byte("Test Payload"), time.Now().Unix(), time.Now().Unix()+3600) + `"`, + pubKey: wrongPublicKeyBase64, + }, + { + name: "Invalid Expires Timestamp", + body: []byte("Test Payload"), + header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) + + `", expires="invalid_timestamp"`, + pubKey: publicKeyBase64, + }, + { + name: "Signature Missing in Headers", + body: []byte("Test Payload"), + header: `Signature created="` + strconv.FormatInt(time.Now().Unix(), 10) + + `", expires="` + strconv.FormatInt(time.Now().Unix()+3600, 10) + `"`, + pubKey: publicKeyBase64, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + verifier, close, _ := New(context.Background(), &Config{}) + valid, err := verifier.Verify(context.Background(), tt.body, []byte(tt.header), tt.pubKey) + + if err == nil { + t.Fatal("Expected an error but got none") + } + if valid { + t.Fatal("Expected verification to fail") + } + if close != nil { + if err := close(); err != nil { + t.Fatalf("Test %q failed: cleanup function returned an error: %v", tt.name, err) + } + } + }) + } +} diff --git a/shared/plugin/implementation/signer/cmd/plugin.go b/shared/plugin/implementation/signer/cmd/plugin.go new file mode 100644 index 0000000..854ecbe --- /dev/null +++ b/shared/plugin/implementation/signer/cmd/plugin.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + "errors" + + "github.com/beckn/beckn-onix/shared/plugin/definition" + "github.com/beckn/beckn-onix/shared/plugin/implementation/signer" +) + +// SignerProvider implements the definition.SignerProvider interface. +type SignerProvider struct{} + +// New creates a new Signer instance using the provided configuration. +func (p SignerProvider) New(ctx context.Context, config map[string]string) (definition.Signer, func() error, error) { + if ctx == nil { + return nil, nil, errors.New("context cannot be nil") + } + + return signer.New(ctx, &signer.Config{}) +} + +// Provider is the exported symbol that the plugin manager will look for. +var Provider definition.SignerProvider = SignerProvider{} diff --git a/shared/plugin/implementation/signer/cmd/plugin_test.go b/shared/plugin/implementation/signer/cmd/plugin_test.go new file mode 100644 index 0000000..e4730d5 --- /dev/null +++ b/shared/plugin/implementation/signer/cmd/plugin_test.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "testing" +) + +// TestSignerProviderSuccess verifies successful scenarios for SignerProvider. +func TestSignerProviderSuccess(t *testing.T) { + provider := SignerProvider{} + + successTests := []struct { + name string + ctx context.Context + config map[string]string + }{ + { + name: "Valid Config", + ctx: context.Background(), + config: map[string]string{}, + }, + { + name: "Unexpected Config Key", + ctx: context.Background(), + config: map[string]string{"unexpected_key": "some_value"}, + }, + { + name: "Empty Config", + ctx: context.Background(), + config: map[string]string{}, + }, + { + name: "Config with empty TTL", + ctx: context.Background(), + config: map[string]string{"ttl": ""}, + }, + { + name: "Config with negative TTL", + ctx: context.Background(), + config: map[string]string{"ttl": "-100"}, + }, + { + name: "Config with non-numeric TTL", + ctx: context.Background(), + config: map[string]string{"ttl": "not_a_number"}, + }, + } + + for _, tt := range successTests { + t.Run(tt.name, func(t *testing.T) { + signer, close, err := provider.New(tt.ctx, tt.config) + + if err != nil { + t.Fatalf("Test %q failed: expected no error, but got: %v", tt.name, err) + } + if signer == nil { + t.Fatalf("Test %q failed: signer instance should not be nil", tt.name) + } + if close != nil { + if err := close(); err != nil { + t.Fatalf("Cleanup function returned an error: %v", err) + } + } + }) + } +} + +// TestSignerProviderFailure verifies failure scenarios for SignerProvider. +func TestSignerProviderFailure(t *testing.T) { + provider := SignerProvider{} + + failureTests := []struct { + name string + ctx context.Context + config map[string]string + wantErr bool + }{ + { + name: "Nil Context", + ctx: nil, + config: map[string]string{}, + wantErr: true, + }, + } + + for _, tt := range failureTests { + t.Run(tt.name, func(t *testing.T) { + signerInstance, close, err := provider.New(tt.ctx, tt.config) + + if (err != nil) != tt.wantErr { + t.Fatalf("Test %q failed: expected error: %v, got: %v", tt.name, tt.wantErr, err) + } + if signerInstance != nil { + t.Fatalf("Test %q failed: expected signer instance to be nil", tt.name) + } + if close != nil { + t.Fatalf("Test %q failed: expected cleanup function to be nil", tt.name) + } + }) + } +} diff --git a/shared/plugin/implementation/signer/signer.go b/shared/plugin/implementation/signer/signer.go new file mode 100644 index 0000000..c1f2af9 --- /dev/null +++ b/shared/plugin/implementation/signer/signer.go @@ -0,0 +1,77 @@ +package signer + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "errors" + "fmt" + + "golang.org/x/crypto/blake2b" +) + +// Config holds the configuration for the signing process. +type Config struct { +} + +// Signer implements the Signer interface and handles the signing process. +type Signer struct { + config *Config +} + +// New creates a new Signer instance with the given configuration. +func New(ctx context.Context, config *Config) (*Signer, func() error, error) { + s := &Signer{config: config} + + return s, s.Close, nil +} + +// hash generates a signing string using BLAKE-512 hashing. +func hash(payload []byte, createdAt, expiresAt int64) (string, error) { + hasher, _ := blake2b.New512(nil) + + _, err := hasher.Write(payload) + if err != nil { + return "", fmt.Errorf("failed to hash payload: %w", err) + } + + hashSum := hasher.Sum(nil) + digestB64 := base64.StdEncoding.EncodeToString(hashSum) + + return fmt.Sprintf("(created): %d\n(expires): %d\ndigest: BLAKE-512=%s", createdAt, expiresAt, digestB64), nil +} + +// generateSignature signs the given signing string using the provided private key. +func generateSignature(signingString []byte, privateKeyBase64 string) ([]byte, error) { + privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64) + if err != nil { + return nil, fmt.Errorf("error decoding private key: %w", err) + } + + if len(privateKeyBytes) != ed25519.PrivateKeySize { + return nil, errors.New("invalid private key length") + } + + privateKey := ed25519.PrivateKey(privateKeyBytes) + return ed25519.Sign(privateKey, signingString), nil +} + +// Sign generates a digital signature for the provided payload. +func (s *Signer) Sign(ctx context.Context, body []byte, privateKeyBase64 string, createdAt, expiresAt int64) (string, error) { + signingString, err := hash(body, createdAt, expiresAt) + if err != nil { + return "", err + } + + signature, err := generateSignature([]byte(signingString), privateKeyBase64) + if err != nil { + return "", err + } + + return base64.StdEncoding.EncodeToString(signature), nil +} + +// Close releases resources (mock implementation returning nil). +func (s *Signer) Close() error { + return nil +} diff --git a/shared/plugin/implementation/signer/signer_test.go b/shared/plugin/implementation/signer/signer_test.go new file mode 100644 index 0000000..6a25da1 --- /dev/null +++ b/shared/plugin/implementation/signer/signer_test.go @@ -0,0 +1,104 @@ +package signer + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "strings" + "testing" + "time" +) + +// generateTestKeys generates a test private and public key pair in base64 encoding. +func generateTestKeys() (string, string) { + publicKey, privateKey, _ := ed25519.GenerateKey(nil) + return base64.StdEncoding.EncodeToString(privateKey), base64.StdEncoding.EncodeToString(publicKey) +} + +// TestSignSuccess tests the Sign method with valid inputs to ensure it produces a valid signature. +func TestSignSuccess(t *testing.T) { + privateKey, _ := generateTestKeys() + config := Config{} + signer, close, _ := New(context.Background(), &config) + + successTests := []struct { + name string + payload []byte + privateKey string + createdAt int64 + expiresAt int64 + }{ + { + name: "Valid Signing", + payload: []byte("test payload"), + privateKey: privateKey, + createdAt: time.Now().Unix(), + expiresAt: time.Now().Unix() + 3600, + }, + } + + for _, tt := range successTests { + t.Run(tt.name, func(t *testing.T) { + signature, err := signer.Sign(context.Background(), tt.payload, tt.privateKey, tt.createdAt, tt.expiresAt) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if len(signature) == 0 { + t.Errorf("expected a non-empty signature, but got empty") + } + if close != nil { + if err := close(); err != nil { + t.Fatalf("Cleanup function returned an error: %v", err) + } + } + }) + } +} + +// TestSignFailure tests the Sign method with invalid inputs to ensure proper error handling. +func TestSignFailure(t *testing.T) { + config := Config{} + signer, close, _ := New(context.Background(), &config) + + failureTests := []struct { + name string + payload []byte + privateKey string + createdAt int64 + expiresAt int64 + expectErrString string + }{ + { + name: "Invalid Private Key", + payload: []byte("test payload"), + privateKey: "invalid_key", + createdAt: time.Now().Unix(), + expiresAt: time.Now().Unix() + 3600, + expectErrString: "error decoding private key", + }, + { + name: "Short Private Key", + payload: []byte("test payload"), + privateKey: base64.StdEncoding.EncodeToString([]byte("short_key")), + createdAt: time.Now().Unix(), + expiresAt: time.Now().Unix() + 3600, + expectErrString: "invalid private key length", + }, + } + + for _, tt := range failureTests { + t.Run(tt.name, func(t *testing.T) { + _, err := signer.Sign(context.Background(), tt.payload, tt.privateKey, tt.createdAt, tt.expiresAt) + if err == nil { + t.Errorf("expected error but got none") + } else if !strings.Contains(err.Error(), tt.expectErrString) { + t.Errorf("expected error message to contain %q, got %v", tt.expectErrString, err) + } + if close != nil { + if err := close(); err != nil { + t.Fatalf("Cleanup function returned an error: %v", err) + } + } + }) + } +} diff --git a/shared/plugin/manager.go b/shared/plugin/manager.go new file mode 100644 index 0000000..e31fc98 --- /dev/null +++ b/shared/plugin/manager.go @@ -0,0 +1,108 @@ +package plugin + +import ( + "context" + "fmt" + "path/filepath" + "plugin" + "strings" + + "github.com/beckn/beckn-onix/shared/plugin/definition" +) + +// Config represents the plugin manager configuration. +type Config struct { + Root string `yaml:"root"` + Signer PluginConfig `yaml:"signer"` + Verifier PluginConfig `yaml:"verifier"` +} + +// PluginConfig represents configuration details for a plugin. +type PluginConfig struct { + ID string `yaml:"id"` + Config map[string]string `yaml:"config"` +} + +// Manager handles dynamic plugin loading and management. +type Manager struct { + sp definition.SignerProvider + vp definition.VerifierProvider + cfg *Config +} + +// NewManager initializes a new Manager with the given configuration file. +func NewManager(ctx context.Context, cfg *Config) (*Manager, error) { + if cfg == nil { + return nil, fmt.Errorf("configuration cannot be nil") + } + + // Load signer plugin + sp, err := provider[definition.SignerProvider](cfg.Root, cfg.Signer.ID) + if err != nil { + return nil, fmt.Errorf("failed to load signer plugin: %w", err) + } + + // Load verifier plugin + vp, err := provider[definition.VerifierProvider](cfg.Root, cfg.Verifier.ID) + if err != nil { + return nil, fmt.Errorf("failed to load Verifier plugin: %w", err) + } + + return &Manager{sp: sp, vp: vp, cfg: cfg}, nil +} + +// provider loads a plugin dynamically and retrieves its provider instance. +func provider[T any](root, id string) (T, error) { + var zero T + if len(strings.TrimSpace(id)) == 0 { + return zero, nil + } + + p, err := plugin.Open(pluginPath(root, id)) + if err != nil { + return zero, fmt.Errorf("failed to open plugin %s: %w", id, err) + } + + symbol, err := p.Lookup("Provider") + if err != nil { + return zero, fmt.Errorf("failed to find Provider symbol in plugin %s: %w", id, err) + } + + prov, ok := symbol.(*T) + if !ok { + return zero, fmt.Errorf("failed to cast Provider for %s", id) + } + + return *prov, nil +} + +// pluginPath constructs the path to the plugin shared object file. +func pluginPath(root, id string) string { + return filepath.Join(root, id+".so") +} + +// Signer retrieves the signing plugin instance. +func (m *Manager) Signer(ctx context.Context) (definition.Signer, func() error, error) { + if m.sp == nil { + return nil, nil, fmt.Errorf("signing plugin provider not loaded") + } + + signer, close, err := m.sp.New(ctx, m.cfg.Signer.Config) + if err != nil { + return nil, nil, fmt.Errorf("failed to initialize signer: %w", err) + } + return signer, close, nil +} + +// Verifier retrieves the verification plugin instance. +func (m *Manager) Verifier(ctx context.Context) (definition.Verifier, func() error, error) { + if m.vp == nil { + return nil, nil, fmt.Errorf("Verifier plugin provider not loaded") + } + + Verifier, close, err := m.vp.New(ctx, m.cfg.Verifier.Config) + if err != nil { + return nil, nil, fmt.Errorf("failed to initialize Verifier: %w", err) + } + return Verifier, close, nil +}