Feature/signing plugin (#402)

* test commit

* delete test commit

* signing plugin - changes for review

* Initial commit : beckn Signing Plugin Module

* Added verification plugin

post review discussion with leads below changes are made
1. Commented out the signature expiration validation code for both the signing and verification plugins. will update it based on the confirmation.

* Create signing_plugin_test.go

Added Test Cases for Signing Plugin

* Signing and Verification Plugin

Added signing plugin and verification plugin with the unit test cases, achieving the following code coverage
Signing Plugin : 91.9%
Verification Plugin : 92.3%

* Added plugin.go to handle the dynamic loading and usage of the plugin implementation

* Update the code to meet the linting standards

* Added Test Cases for plugin.go

1.Added test cases for plugin.go for both signer and verifier.
2.Added new Function Close to release the resources (mock implementation)
3.Fixed camelCase Issue.

* Updated the code coverage for signing plugin

Raised code coverage from 85 to 92 for signing plugin

* Changes for review Comments

1. updated directory names from Signer to signer
2. Updated Verifier plugin to take header value
3. Updated the config to use a pointer in the signing plugin

* Updated directory name for signer and verifier

* Removed the Duplicate directories Signer and Verifier

* Updated the code to pass the timestamp as a parameter for the signing plugin

* Updates on the review comments

* Update on the Review Comments

* Test commit for code coverage

* Update on the review Comments

1. Renaming of NewSigner to New
2. Removed of .so files.
3. Removed external libraries.

* Test commit for code coverage

* udpate as per the golint standards

* update on the code review comments

1. Rename of Validator to Verifier
2. Removed as a pointer for plugins
3. comment updated for Signer

* Test Commit for the code coverage

* test commit for code coverage

* test commit for code coverage

* test commit for code coverage

* updated code on review comments

* update on review comments

* update on review comments

---------

Co-authored-by: mohit3367 <mohitkatare4@gmail.com>
This commit is contained in:
MohitKatare-protean
2025-03-13 17:12:49 +05:30
committed by GitHub
parent b0c827fbd4
commit 9722c3bf68
13 changed files with 868 additions and 0 deletions

View File

@@ -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{}

View 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)
}
})
}
}

View 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
}

View 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)
}
}
})
}
}