Selaa lähdekoodia

feat: add tests for third-party signature verification

kurokobo 5 kuukautta sitten
vanhempi
commit
01747753f7

+ 227 - 0
cmd/commandline/signature/signature_test.go

@@ -0,0 +1,227 @@
+package signature
+
+import (
+	_ "embed"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/langgenius/dify-plugin-daemon/internal/utils/encryption"
+)
+
+//go:embed testdata/dummy_plugin.difypkg
+var dummyPlugin []byte
+
+func TestGenerateKeyPair(t *testing.T) {
+	// create a temporary directory for testing
+	tempDir := t.TempDir()
+
+	// create a key pair
+	keyPairName := filepath.Join(tempDir, "test_key_pair")
+	GenerateKeyPair(keyPairName)
+	privateKeyPath := keyPairName + ".private.pem"
+	publicKeyPath := keyPairName + ".public.pem"
+
+	// check if the key files are created
+	if _, err := os.Stat(privateKeyPath); os.IsNotExist(err) {
+		t.Errorf("Private key file was not created: %s", privateKeyPath)
+	}
+	if _, err := os.Stat(publicKeyPath); os.IsNotExist(err) {
+		t.Errorf("Public key file was not created: %s", publicKeyPath)
+	}
+
+	// check if the key files can be loaded
+	privateKeyBytes, err := os.ReadFile(privateKeyPath)
+	if err != nil {
+		t.Fatalf("Failed to read private key file: %v", err)
+	}
+	publicKeyBytes, err := os.ReadFile(publicKeyPath)
+	if err != nil {
+		t.Fatalf("Failed to read public key file: %v", err)
+	}
+
+	// check if the keys can be loaded
+	_, err = encryption.LoadPrivateKey(privateKeyBytes)
+	if err != nil {
+		t.Errorf("Failed to load private key: %v", err)
+	}
+	_, err = encryption.LoadPublicKey(publicKeyBytes)
+	if err != nil {
+		t.Errorf("Failed to load public key: %v", err)
+	}
+}
+
+func TestSignAndVerify(t *testing.T) {
+	// create a temporary directory for testing
+	tempDir := t.TempDir()
+
+	// extract the minimal plugin content from the embedded data to a file
+	dummyPluginPath := filepath.Join(tempDir, "dummy_plugin.difypkg")
+	if err := os.WriteFile(dummyPluginPath, dummyPlugin, 0644); err != nil {
+		t.Fatalf("Failed to create dummy plugin file: %v", err)
+	}
+
+	// create two key pairs
+	keyPair1Name := filepath.Join(tempDir, "test_key_pair_1")
+	keyPair2Name := filepath.Join(tempDir, "test_key_pair_2")
+	GenerateKeyPair(keyPair1Name)
+	GenerateKeyPair(keyPair2Name)
+	privateKey1Path := keyPair1Name + ".private.pem"
+	publicKey1Path := keyPair1Name + ".public.pem"
+	privateKey2Path := keyPair2Name + ".private.pem"
+	publicKey2Path := keyPair2Name + ".public.pem"
+
+	// test case definition for table-driven tests
+	type testCase struct {
+		name          string
+		signKeyPath   string
+		verifyKeyPath string
+		expectSuccess bool
+	}
+
+	// test cases
+	tests := []testCase{
+		{
+			name:          "sign with keypair1, verify with keypair1",
+			signKeyPath:   privateKey1Path,
+			verifyKeyPath: publicKey1Path,
+			expectSuccess: true,
+		},
+		{
+			name:          "sign with keypair1, verify with keypair2",
+			signKeyPath:   privateKey1Path,
+			verifyKeyPath: publicKey2Path,
+			expectSuccess: false,
+		},
+		{
+			name:          "sign with keypair2, verify with keypair2",
+			signKeyPath:   privateKey2Path,
+			verifyKeyPath: publicKey2Path,
+			expectSuccess: true,
+		},
+		{
+			name:          "sign with keypair2, verify with keypair1",
+			signKeyPath:   privateKey2Path,
+			verifyKeyPath: publicKey1Path,
+			expectSuccess: false,
+		},
+		{
+			name:          "sign with keypair1, verify without key",
+			signKeyPath:   privateKey1Path,
+			verifyKeyPath: "",
+			expectSuccess: false,
+		},
+		{
+			name:          "sign with keypair2, verify without key",
+			signKeyPath:   privateKey2Path,
+			verifyKeyPath: "",
+			expectSuccess: false,
+		},
+	}
+
+	// execute each test case
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// create a temporary file for each test case
+			testPluginPath := filepath.Join(tempDir, "test_plugin_"+tt.name+".difypkg")
+			if err := os.WriteFile(testPluginPath, dummyPlugin, 0644); err != nil {
+				t.Fatalf("Failed to create test plugin file: %v", err)
+			}
+
+			// sign the plugin
+			Sign(testPluginPath, tt.signKeyPath)
+
+			// get the path of the signed plugin
+			dir := filepath.Dir(testPluginPath)
+			base := filepath.Base(testPluginPath)
+			ext := filepath.Ext(base)
+			name := base[:len(base)-len(ext)]
+			dummyPluginPath := filepath.Join(dir, name+".signed"+ext)
+
+			// check if the signed plugin file was created
+			if _, err := os.Stat(dummyPluginPath); os.IsNotExist(err) {
+				t.Fatalf("Signed plugin file was not created: %s", dummyPluginPath)
+			}
+
+			// verify the signed plugin and check the result
+			err := Verify(dummyPluginPath, tt.verifyKeyPath)
+			if tt.expectSuccess && err != nil {
+				t.Errorf("Expected verification to succeed, but got error: %v", err)
+			} else if !tt.expectSuccess && err == nil {
+				t.Errorf("Expected verification to fail, but it succeeded")
+			}
+		})
+	}
+}
+
+// TestVerifyUnsigned tests verification of an unsigned difypkg file
+func TestVerifyUnsigned(t *testing.T) {
+	// create a temporary directory for testing
+	tempDir := t.TempDir()
+
+	// extract the minimal plugin content from the embedded data to a file
+	dummyPluginPath := filepath.Join(tempDir, "dummy_plugin.difypkg")
+	if err := os.WriteFile(dummyPluginPath, dummyPlugin, 0644); err != nil {
+		t.Fatalf("Failed to create dummy plugin file: %v", err)
+	}
+
+	// create a key pair
+	keyPairName := filepath.Join(tempDir, "test_key_pair")
+	GenerateKeyPair(keyPairName)
+	publicKeyPath := keyPairName + ".public.pem"
+
+	// Try to verify the unsigned plugin file
+	err := Verify(dummyPluginPath, publicKeyPath)
+	if err == nil {
+		t.Errorf("Expected verification of unsigned file to fail, but it succeeded")
+	}
+}
+
+// TestVerifyTampered tests verification of a tampered signed difypkg file
+func TestVerifyTampered(t *testing.T) {
+	// create a temporary directory for testing
+	tempDir := t.TempDir()
+
+	// extract the minimal plugin content from the embedded data to a file
+	dummyPluginPath := filepath.Join(tempDir, "dummy_plugin.difypkg")
+	if err := os.WriteFile(dummyPluginPath, dummyPlugin, 0644); err != nil {
+		t.Fatalf("Failed to create dummy plugin file: %v", err)
+	}
+
+	// create a key pair
+	keyPairName := filepath.Join(tempDir, "test_key_pair")
+	GenerateKeyPair(keyPairName)
+	privateKeyPath := keyPairName + ".private.pem"
+	publicKeyPath := keyPairName + ".public.pem"
+
+	// Sign the plugin
+	Sign(dummyPluginPath, privateKeyPath)
+
+	// Get the path of the signed plugin
+	dir := filepath.Dir(dummyPluginPath)
+	base := filepath.Base(dummyPluginPath)
+	ext := filepath.Ext(base)
+	name := base[:len(base)-len(ext)]
+	signedDummyPluginPath := filepath.Join(dir, name+".signed"+ext)
+
+	// Read the signed plugin
+	signedPluginData, err := os.ReadFile(signedDummyPluginPath)
+	if err != nil {
+		t.Fatalf("Failed to read signed plugin file: %v", err)
+	}
+
+	// tamper the signed plugin data
+	signedPluginData[len(signedPluginData)-10] = 0
+
+	// write the tampered data back to the file
+	tamperedPluginPath := filepath.Join(tempDir, "tampered_plugin.signed.difypkg")
+	if err := os.WriteFile(tamperedPluginPath, signedPluginData, 0644); err != nil {
+		t.Fatalf("Failed to write tampered plugin file: %v", err)
+	}
+
+	// try to verify the tampered plugin file
+	err = Verify(tamperedPluginPath, publicKeyPath)
+	if err == nil {
+		t.Errorf("Expected verification of tampered file to fail, but it succeeded")
+	}
+}

BIN
cmd/commandline/signature/testdata/dummy_plugin.difypkg


+ 348 - 56
pkg/plugin_packager/packager_test.go

@@ -1,15 +1,19 @@
 package plugin_packager
 
 import (
-	_ "embed"
+	"crypto/rsa"
+	"embed"
 	"fmt"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 
+	"github.com/langgenius/dify-plugin-daemon/internal/utils/encryption"
 	"github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder"
 	"github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/packager"
 	"github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/signer"
+	"github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/signer/withkey"
 )
 
 //go:embed testdata/manifest.yaml
@@ -27,64 +31,106 @@ var ignored []byte
 //go:embed testdata/_assets/test.svg
 var test_svg []byte
 
-func TestPackagerAndVerifier(t *testing.T) {
+//go:embed testdata/keys
+var keys embed.FS
+
+// createMinimalPlugin creates a minimal test plugin and returns the zip file
+func createMinimalPlugin(t *testing.T) []byte {
 	// create a temp directory
-	os.RemoveAll("temp")
-	if err := os.Mkdir("temp", 0755); err != nil {
-		t.Errorf("failed to create temp directory: %s", err.Error())
-		return
+	tempDir := t.TempDir()
+
+	// create basic files
+	if err := os.WriteFile(filepath.Join(tempDir, "manifest.yaml"), manifest, 0644); err != nil {
+		t.Errorf("failed to write manifest: %s", err.Error())
+		return nil
 	}
-	defer func() {
-		os.RemoveAll("temp")
-		os.Remove("temp")
-	}()
+
+	if err := os.WriteFile(filepath.Join(tempDir, "neko.yaml"), neko, 0644); err != nil {
+		t.Errorf("failed to write neko: %s", err.Error())
+		return nil
+	}
+
+	// create _assets directory and files
+	if err := os.MkdirAll(filepath.Join(tempDir, "_assets"), 0755); err != nil {
+		t.Errorf("failed to create _assets directory: %s", err.Error())
+		return nil
+	}
+
+	if err := os.WriteFile(filepath.Join(tempDir, "_assets/test.svg"), test_svg, 0644); err != nil {
+		t.Errorf("failed to write test.svg: %s", err.Error())
+		return nil
+	}
+
+	// create decoder
+	originDecoder, err := decoder.NewFSPluginDecoder(tempDir)
+	if err != nil {
+		t.Errorf("failed to create decoder: %s", err.Error())
+		return nil
+	}
+
+	// create packager
+	packager := packager.NewPackager(originDecoder)
+
+	// pack
+	zip, err := packager.Pack(52428800)
+	if err != nil {
+		t.Errorf("failed to pack: %s", err.Error())
+		return nil
+	}
+
+	return zip
+}
+
+func TestPackagerAndVerifier(t *testing.T) {
+	// create a temp directory
+	tempDir := t.TempDir()
 
 	// create manifest
-	if err := os.WriteFile("temp/manifest.yaml", manifest, 0644); err != nil {
+	if err := os.WriteFile(filepath.Join(tempDir, "manifest.yaml"), manifest, 0644); err != nil {
 		t.Errorf("failed to write manifest: %s", err.Error())
 		return
 	}
 
-	if err := os.WriteFile("temp/neko.yaml", neko, 0644); err != nil {
+	if err := os.WriteFile(filepath.Join(tempDir, "neko.yaml"), neko, 0644); err != nil {
 		t.Errorf("failed to write neko: %s", err.Error())
 		return
 	}
 
 	// create .difyignore
-	if err := os.WriteFile("temp/.difyignore", dify_ignore, 0644); err != nil {
+	if err := os.WriteFile(filepath.Join(tempDir, ".difyignore"), dify_ignore, 0644); err != nil {
 		t.Errorf("failed to write .difyignore: %s", err.Error())
 		return
 	}
 
 	// create ignored
-	if err := os.WriteFile("temp/ignored", ignored, 0644); err != nil {
+	if err := os.WriteFile(filepath.Join(tempDir, "ignored"), ignored, 0644); err != nil {
 		t.Errorf("failed to write ignored: %s", err.Error())
 		return
 	}
 
 	// create ignored_paths
-	if err := os.MkdirAll("temp/ignored_paths", 0755); err != nil {
+	if err := os.MkdirAll(filepath.Join(tempDir, "ignored_paths"), 0755); err != nil {
 		t.Errorf("failed to create ignored_paths directory: %s", err.Error())
 		return
 	}
 
 	// create ignored_paths/ignored
-	if err := os.WriteFile("temp/ignored_paths/ignored", ignored, 0644); err != nil {
+	if err := os.WriteFile(filepath.Join(tempDir, "ignored_paths/ignored"), ignored, 0644); err != nil {
 		t.Errorf("failed to write ignored_paths/ignored: %s", err.Error())
 		return
 	}
 
-	if err := os.MkdirAll("temp/_assets", 0755); err != nil {
+	if err := os.MkdirAll(filepath.Join(tempDir, "_assets"), 0755); err != nil {
 		t.Errorf("failed to create _assets directory: %s", err.Error())
 		return
 	}
 
-	if err := os.WriteFile("temp/_assets/test.svg", test_svg, 0644); err != nil {
+	if err := os.WriteFile(filepath.Join(tempDir, "_assets/test.svg"), test_svg, 0644); err != nil {
 		t.Errorf("failed to write test.svg: %s", err.Error())
 		return
 	}
 
-	originDecoder, err := decoder.NewFSPluginDecoder("temp")
+	originDecoder, err := decoder.NewFSPluginDecoder(tempDir)
 	if err != nil {
 		t.Errorf("failed to create decoder: %s", err.Error())
 		return
@@ -160,76 +206,322 @@ func TestPackagerAndVerifier(t *testing.T) {
 }
 
 func TestWrongSign(t *testing.T) {
-	// create a temp directory
-	if err := os.Mkdir("temp", 0755); err != nil {
-		t.Errorf("failed to create temp directory: %s", err.Error())
+	// create a minimal test plugin
+	zip := createMinimalPlugin(t)
+	if zip == nil {
 		return
 	}
-	defer func() {
-		os.RemoveAll("temp")
-		os.Remove("temp")
-	}()
 
-	// create manifest
-	if err := os.WriteFile("temp/manifest.yaml", manifest, 0644); err != nil {
-		t.Errorf("failed to write manifest: %s", err.Error())
+	// sign
+	signed, err := signer.SignPlugin(zip)
+	if err != nil {
+		t.Errorf("failed to sign: %s", err.Error())
 		return
 	}
 
-	if err := os.WriteFile("temp/neko.yaml", neko, 0644); err != nil {
-		t.Errorf("failed to write neko: %s", err.Error())
+	// modify the signed file, signature is at the end of the file
+	signed[len(signed)-1] = 0
+	signed[len(signed)-2] = 0
+
+	// create a new decoder
+	signedDecoder, err := decoder.NewZipPluginDecoder(signed)
+	if err != nil {
+		t.Errorf("failed to create zip decoder: %s", err.Error())
 		return
 	}
 
-	// create _assets directory
-	if err := os.MkdirAll("temp/_assets", 0755); err != nil {
-		t.Errorf("failed to create _assets directory: %s", err.Error())
+	// verify (expected to fail)
+	err = decoder.VerifyPlugin(signedDecoder)
+	if err == nil {
+		t.Errorf("should fail to verify")
 		return
 	}
+}
 
-	// create _assets/test.svg
-	if err := os.WriteFile("temp/_assets/test.svg", test_svg, 0644); err != nil {
-		t.Errorf("failed to write test.svg: %s", err.Error())
-		return
+// loadPublicKeyFile loads a key file from the embed.FS and returns the public key
+func loadPublicKeyFile(t *testing.T, keyFile string) *rsa.PublicKey {
+	keyBytes, err := keys.ReadFile(filepath.Join("testdata/keys", keyFile))
+	if err != nil {
+		t.Fatalf("failed to read key file: %s", err.Error())
+	}
+	key, err := encryption.LoadPublicKey(keyBytes)
+	if err != nil {
+		t.Fatalf("failed to load public key: %s", err.Error())
 	}
+	return key
+}
 
-	originDecoder, err := decoder.NewFSPluginDecoder("temp")
+// loadPrivateKeyFile loads a key file from the embed.FS and returns the private key
+func loadPrivateKeyFile(t *testing.T, keyFile string) *rsa.PrivateKey {
+	keyBytes, err := keys.ReadFile(filepath.Join("testdata/keys", keyFile))
 	if err != nil {
-		t.Errorf("failed to create decoder: %s", err.Error())
+		t.Fatalf("failed to read key file: %s", err.Error())
+	}
+	key, err := encryption.LoadPrivateKey(keyBytes)
+	if err != nil {
+		t.Fatalf("failed to load private key: %s", err.Error())
+	}
+	return key
+}
+
+// extractPublicKey extracts the key file from the embed.FS and returns the file path
+func extractKeyFile(t *testing.T, keyFile string, tmpDir string) string {
+	keyBytes, err := keys.ReadFile(filepath.Join("testdata/keys", keyFile))
+	if err != nil {
+		t.Fatalf("failed to read key file: %s", err.Error())
+	}
+	keyPath := filepath.Join(tmpDir, keyFile)
+	if err := os.WriteFile(keyPath, keyBytes, 0644); err != nil {
+		t.Fatalf("failed to write key file: %s", err.Error())
+	}
+	return keyPath
+}
+
+func TestSignPluginWithPrivateKey(t *testing.T) {
+	// load public keys from embed.FS
+	publicKey1 := loadPublicKeyFile(t, "test_key_pair_1.public.pem")
+	publicKey2 := loadPublicKeyFile(t, "test_key_pair_2.public.pem")
+
+	// load private keys from embed.FS
+	privateKey1 := loadPrivateKeyFile(t, "test_key_pair_1.private.pem")
+	privateKey2 := loadPrivateKeyFile(t, "test_key_pair_2.private.pem")
+
+	// create a minimal test plugin
+	zip := createMinimalPlugin(t)
+	if zip == nil {
 		return
 	}
 
-	packager := packager.NewPackager(originDecoder)
+	// sign with private key 1 and create decoder
+	signed1, err := withkey.SignPluginWithPrivateKey(zip, privateKey1)
+	if err != nil {
+		t.Errorf("failed to sign with private key 1: %s", err.Error())
+		return
+	}
+	signedDecoder1, err := decoder.NewZipPluginDecoder(signed1)
+	if err != nil {
+		t.Errorf("failed to create zip decoder: %s", err.Error())
+		return
+	}
 
-	// pack
-	zip, err := packager.Pack(52428800)
+	// sign with private key 2 and create decoder
+	signed2, err := withkey.SignPluginWithPrivateKey(zip, privateKey2)
 	if err != nil {
-		t.Errorf("failed to pack: %s", err.Error())
+		t.Errorf("failed to sign with private key 2: %s", err.Error())
+		return
+	}
+	signedDecoder2, err := decoder.NewZipPluginDecoder(signed2)
+	if err != nil {
+		t.Errorf("failed to create zip decoder: %s", err.Error())
 		return
 	}
 
-	// sign
-	signed, err := signer.SignPlugin(zip)
+	// tamper the signed1 file and create decoder
+	modifiedSigned1 := make([]byte, len(signed1))
+	copy(modifiedSigned1, signed1)
+	modifiedSigned1[len(modifiedSigned1)-10] = 0
+	modifiedDecoder1, err := decoder.NewZipPluginDecoder(modifiedSigned1)
 	if err != nil {
-		t.Errorf("failed to sign: %s", err.Error())
+		t.Errorf("failed to create zip decoder: %s", err.Error())
 		return
 	}
 
-	// modify the signed file, signature is at the end of the file
-	signed[len(signed)-1] = 0
-	signed[len(signed)-2] = 0
+	// define test cases
+	tests := []struct {
+		name          string
+		signedDecoder decoder.PluginDecoder
+		publicKeys    []*rsa.PublicKey
+		expectSuccess bool
+	}{
+		{
+			name:          "verify plugin signed with private key 1 using embedded public key (should fail)",
+			signedDecoder: signedDecoder1,
+			publicKeys:    nil, // use embedded public key
+			expectSuccess: false,
+		},
+		{
+			name:          "verify plugin signed with private key 1 using public key 1 (should succeed)",
+			signedDecoder: signedDecoder1,
+			publicKeys:    []*rsa.PublicKey{publicKey1},
+			expectSuccess: true,
+		},
+		{
+			name:          "verify plugin signed with private key 1 using public key 2 (should fail)",
+			signedDecoder: signedDecoder1,
+			publicKeys:    []*rsa.PublicKey{publicKey2},
+			expectSuccess: false,
+		},
+		{
+			name:          "verify plugin signed with private key 2 using public key 1 (should fail)",
+			signedDecoder: signedDecoder2,
+			publicKeys:    []*rsa.PublicKey{publicKey1},
+			expectSuccess: false,
+		},
+		{
+			name:          "verify plugin signed with private key 2 using public key 2 (should succeed)",
+			signedDecoder: signedDecoder2,
+			publicKeys:    []*rsa.PublicKey{publicKey2},
+			expectSuccess: true,
+		},
+		{
+			name:          "verify modified plugin signed with private key 1 using public key 1 (should fail)",
+			signedDecoder: modifiedDecoder1,
+			publicKeys:    []*rsa.PublicKey{publicKey1},
+			expectSuccess: false,
+		},
+		{
+			name:          "verify modified plugin signed with private key 1 using public key 2 (should fail)",
+			signedDecoder: modifiedDecoder1,
+			publicKeys:    []*rsa.PublicKey{publicKey2},
+			expectSuccess: false,
+		},
+	}
 
-	// create a new decoder
-	signedDecoder, err := decoder.NewZipPluginDecoder(signed)
+	// run test cases
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var err error
+			if tt.publicKeys == nil {
+				err = decoder.VerifyPlugin(tt.signedDecoder)
+			} else {
+				err = decoder.VerifyPluginWithPublicKeys(tt.signedDecoder, tt.publicKeys)
+			}
+
+			if tt.expectSuccess && err != nil {
+				t.Errorf("expected success but got error: %s", err.Error())
+			}
+			if !tt.expectSuccess && err == nil {
+				t.Errorf("expected failure but got success")
+			}
+		})
+	}
+}
+
+func TestVerifyPluginWithThirdPartyKeys(t *testing.T) {
+	// create a temporary directory for the public key files (needed for storing the paths in environment variable)
+	tempDir := t.TempDir()
+
+	// extract public keys to files from embed.FS (needed for storing the paths in environment variable)
+	publicKey1Path := extractKeyFile(t, "test_key_pair_1.public.pem", tempDir)
+	publicKey2Path := extractKeyFile(t, "test_key_pair_2.public.pem", tempDir)
+
+	// load private keys from embed.FS
+	privateKey1 := loadPrivateKeyFile(t, "test_key_pair_1.private.pem")
+	privateKey2 := loadPrivateKeyFile(t, "test_key_pair_2.private.pem")
+
+	// create a minimal test plugin
+	zip := createMinimalPlugin(t)
+	if zip == nil {
+		return
+	}
+
+	// sign with private key 1 and create decoder
+	signed1, err := withkey.SignPluginWithPrivateKey(zip, privateKey1)
+	if err != nil {
+		t.Errorf("failed to sign with private key 1: %s", err.Error())
+		return
+	}
+	signedDecoder1, err := decoder.NewZipPluginDecoder(signed1)
 	if err != nil {
 		t.Errorf("failed to create zip decoder: %s", err.Error())
 		return
 	}
 
-	// verify
-	err = decoder.VerifyPlugin(signedDecoder)
-	if err == nil {
-		t.Errorf("should fail to verify")
+	// sign with private key 2 and create decoder
+	signed2, err := withkey.SignPluginWithPrivateKey(zip, privateKey2)
+	if err != nil {
+		t.Errorf("failed to sign with private key 2: %s", err.Error())
+		return
+	}
+	signedDecoder2, err := decoder.NewZipPluginDecoder(signed2)
+	if err != nil {
+		t.Errorf("failed to create zip decoder: %s", err.Error())
+		return
+	}
+
+	// tamper the signed1 file and create decoder
+	modifiedSigned1 := make([]byte, len(signed1))
+	copy(modifiedSigned1, signed1)
+	modifiedSigned1[len(modifiedSigned1)-10] = 0
+	modifiedDecoder1, err := decoder.NewZipPluginDecoder(modifiedSigned1)
+	if err != nil {
+		t.Errorf("failed to create zip decoder: %s", err.Error())
 		return
 	}
+
+	// define test cases
+	tests := []struct {
+		name          string
+		keyPaths      string
+		signedDecoder decoder.PluginDecoder
+		expectSuccess bool
+	}{
+		{
+			name:          "third-party verification with public key 1 (should succeed)",
+			keyPaths:      publicKey1Path,
+			signedDecoder: signedDecoder1,
+			expectSuccess: true,
+		},
+		{
+			name:          "third-party verification with public key 2 (should fail)",
+			keyPaths:      publicKey2Path,
+			signedDecoder: signedDecoder1,
+			expectSuccess: false,
+		},
+		{
+			name:          "third-party verification with both keys (should succeed)",
+			keyPaths:      fmt.Sprintf("%s,%s", publicKey1Path, publicKey2Path),
+			signedDecoder: signedDecoder1,
+			expectSuccess: true,
+		},
+		{
+			name:          "third-party verification with empty key path (should fail)",
+			keyPaths:      "",
+			signedDecoder: signedDecoder1,
+			expectSuccess: false,
+		},
+		{
+			name:          "third-party verification with non-existent key path (should fail)",
+			keyPaths:      "/non/existent/path.pem",
+			signedDecoder: signedDecoder1,
+			expectSuccess: false,
+		},
+		{
+			name:          "third-party verification with multiple keys including non-existent path (should fail)",
+			keyPaths:      fmt.Sprintf("%s,%s,/non/existent/path.pem", publicKey1Path, publicKey2Path),
+			signedDecoder: signedDecoder1,
+			expectSuccess: false,
+		},
+		{
+			name:          "third-party verification with multiple keys including extra spaces (should succeed)",
+			keyPaths:      fmt.Sprintf(" %s , %s ", publicKey1Path, publicKey2Path),
+			signedDecoder: signedDecoder1,
+			expectSuccess: true,
+		},
+		{
+			name:          "third-party verification with both keys, for file signed with key 2 (should succeed)",
+			keyPaths:      fmt.Sprintf("%s,%s", publicKey1Path, publicKey2Path),
+			signedDecoder: signedDecoder2,
+			expectSuccess: true,
+		},
+		{
+			name:          "third-party verification with both keys, for modified file (should fail)",
+			keyPaths:      fmt.Sprintf("%s,%s", publicKey1Path, publicKey2Path),
+			signedDecoder: modifiedDecoder1,
+			expectSuccess: false,
+		},
+	}
+
+	// run test cases
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := decoder.VerifyPluginWithPublicKeyPaths(tt.signedDecoder, strings.Split(tt.keyPaths, ","))
+			if tt.expectSuccess && err != nil {
+				t.Errorf("expected success but got error: %s", err.Error())
+			}
+			if !tt.expectSuccess && err == nil {
+				t.Errorf("expected failure but got success")
+			}
+		})
+	}
 }

+ 51 - 0
pkg/plugin_packager/testdata/keys/test_key_pair_1.private.pem

@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----

+ 13 - 0
pkg/plugin_packager/testdata/keys/test_key_pair_1.public.pem

@@ -0,0 +1,13 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIICCgKCAgEAygLk/TIn0Wz4RMK16NW8rK/1UWGuYvcilJ+9rKBKX4Wr+0Nm6T2m
+EiGw9S2UUm4HeUS+F42pQTLV282Sz0lLn/mbWCwppXCC2Olfux4oT/w6YKBCc0Lb
+XForVPVmNFLPjyZwTxk1N/kAqJY+9Ip3/Nrpfej/C7l5+CH9wIkYDBKzYvThZ6Ud
++tcHEozob7b7Hazu5qwmMg1A/+nRUbPnaQFh0PrBhbuBH0CEZ6xkpNOoPmhlmmGZ
+NZzSw8RTXznUpar24USLcKJ/nvq8vxoeVmWG9uf68/DgyTyfXp/xrUPiEH5LkUAN
+HhC22t4JxpmzAjf/LAYYE0k0zYaOsE853PHdcpNtUe3NNfE8ld2vufupmFY2CCtI
+dBmYB/q41tRKk9wQGCwb4dMApbGXFHJzpq5UlYl8pYclOSX+u/9odcbCwW5gvdzv
+2QekB194hrefriMsP2h840ZlvP2DRfVfa2aDYJHVK1sJSAgcDbERFs/xk1qiTXyK
+Nepulfeh/f3EnvkpjX+h25ffQ0nyV35FIXL8Ce3in1f1uBS9O6W1j67Hy9kr0Fr+
+f2mL/ZKdKBS2x0uAdr0FlH6fcyFmOKhy4yTeuWrESxbUSPopQfLh79dj9fG97oQ5
+SRYtdi2y+DeZId8/3X2PhIZiqsfL6+HwrguLh35xMeWRoSdWqaQU/1kCAwEAAQ==
+-----END RSA PUBLIC KEY-----

+ 51 - 0
pkg/plugin_packager/testdata/keys/test_key_pair_2.private.pem

@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----

+ 13 - 0
pkg/plugin_packager/testdata/keys/test_key_pair_2.public.pem

@@ -0,0 +1,13 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIICCgKCAgEAuMwtWE1RqrQH1dHUq8W9eG7k0JvkHYyBKLC7p8ykptxEFJR1RUCK
+EGEOuw2cVXqAyWekIqTONodH5N+1RzZmnprQ+u34HTL6bhfIBnpBE4ng7zEfFun3
+zRcRweHUj2RfFhgzNRk4CZnPPn5gJj7K12hlazY0XHFmM6jIr5GZJ/P8wwfGJxX7
+mxB5XShVmI4/5E2iFEGlL24/VqC5IA+zGLpeV7Uxr5sq9qwe0SIbbntjWp+ZmUGs
+ZBZ0CJjqGLPkFXTFklNW9T3defLmZoGRZujZxkDIP7jiLoL8rqfwCFVnNflmONHM
+Z3Kj9d5Jth85IO/FYQyNnoiqlLLRXAd/qJ81jWmjiPbBZJvdni2vr4k11FJpV+87
+h0znWufa33P0B9BOJcMA1VEI9RJ8Mf6li3Col9WVf5Th98g3IM8mUAeMutl0tc2G
+66PbH/B5SRINQ+95K1PE49jlgdqqMak+ScImANRyXNQYi3IAPjyjE8l8OvzFRzOk
+esPTEXWeXx8ffMHr6Qnq4djiuLQHHoCgcxOaK17NOmX4nf1eUixbQ1KBf5B2OUiV
+deH91G3MQCDBAcHiThnWywXymbXnvDhFqqy1EeqGBNMA3uIV+YYMgdTh23ZZbHCx
+T73YS8siJACjANVQqBj0qygaRQ+46RpKTjhatGHiKSpXHpXVL8w/mZMCAwEAAQ==
+-----END RSA PUBLIC KEY-----