Merge pull request #434 from rupinder-syngh/feat/publisher
Feat/publisher plugin
This commit is contained in:
16
pkg/plugin/definition/publisher.go
Normal file
16
pkg/plugin/definition/publisher.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package definition
|
||||
|
||||
import "context"
|
||||
|
||||
// Publisher defines the general publisher interface for messaging plugins.
|
||||
type Publisher interface {
|
||||
// Publish sends a message (as a byte slice) using the underlying messaging system.
|
||||
Publish(ctx context.Context, msg []byte) error
|
||||
|
||||
Close() error // Important for releasing resources.
|
||||
}
|
||||
|
||||
type PublisherProvider interface {
|
||||
// New initializes a new publisher instance with the given configuration.
|
||||
New(ctx context.Context, config map[string]string) (Publisher, error)
|
||||
}
|
||||
22
pkg/plugin/definition/signVerifier.go
Normal file
22
pkg/plugin/definition/signVerifier.go
Normal file
@@ -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)
|
||||
}
|
||||
24
pkg/plugin/definition/signer.go
Normal file
24
pkg/plugin/definition/signer.go
Normal file
@@ -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)
|
||||
}
|
||||
25
pkg/plugin/implementation/signVerifier/cmd/plugin.go
Normal file
25
pkg/plugin/implementation/signVerifier/cmd/plugin.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||
|
||||
verifier "github.com/beckn/beckn-onix/pkg/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) (definition.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{}
|
||||
89
pkg/plugin/implementation/signVerifier/cmd/plugin_test.go
Normal file
89
pkg/plugin/implementation/signVerifier/cmd/plugin_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
120
pkg/plugin/implementation/signVerifier/signVerifier.go
Normal file
120
pkg/plugin/implementation/signVerifier/signVerifier.go
Normal file
@@ -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
|
||||
}
|
||||
153
pkg/plugin/implementation/signVerifier/signVerifier_test.go
Normal file
153
pkg/plugin/implementation/signVerifier/signVerifier_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
24
pkg/plugin/implementation/signer/cmd/plugin.go
Normal file
24
pkg/plugin/implementation/signer/cmd/plugin.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||
"github.com/beckn/beckn-onix/pkg/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{}
|
||||
101
pkg/plugin/implementation/signer/cmd/plugin_test.go
Normal file
101
pkg/plugin/implementation/signer/cmd/plugin_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
77
pkg/plugin/implementation/signer/signer.go
Normal file
77
pkg/plugin/implementation/signer/signer.go
Normal file
@@ -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
|
||||
}
|
||||
104
pkg/plugin/implementation/signer/signer_test.go
Normal file
104
pkg/plugin/implementation/signer/signer_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
129
pkg/plugin/manager.go
Normal file
129
pkg/plugin/manager.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"plugin"
|
||||
"strings"
|
||||
|
||||
"github.com/beckn/beckn-onix/pkg/plugin/definition"
|
||||
)
|
||||
|
||||
// Config represents the plugin manager configuration.
|
||||
type Config struct {
|
||||
Root string `yaml:"root"`
|
||||
Signer PluginConfig `yaml:"signer"`
|
||||
Verifier PluginConfig `yaml:"verifier"`
|
||||
Publisher PluginConfig `yaml:"publisher"`
|
||||
}
|
||||
|
||||
// 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
|
||||
pb definition.PublisherProvider
|
||||
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 publisher plugin.
|
||||
pb, err := provider[definition.PublisherProvider](cfg.Root, cfg.Publisher.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load publisher 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, pb: pb, 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
|
||||
}
|
||||
|
||||
// Publisher retrieves the publisher plugin instance.
|
||||
func (m *Manager) Publisher(ctx context.Context) (definition.Publisher, error) {
|
||||
if m.pb == nil {
|
||||
return nil, fmt.Errorf("publisher plugin provider not loaded")
|
||||
}
|
||||
|
||||
publisher, err := m.pb.New(ctx, m.cfg.Publisher.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize publisher: %w", err)
|
||||
}
|
||||
return publisher, nil
|
||||
}
|
||||
Reference in New Issue
Block a user