소스 검색

feat: signer

Yeuoly 1 년 전
부모
커밋
a90db19af0

+ 34 - 0
cmd/license/generate/main.go

@@ -0,0 +1,34 @@
+package main
+
+import (
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+)
+
+func main() {
+	key_pair, err := rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		panic(err)
+	}
+
+	// encrypt as pem
+	new_private_key := x509.MarshalPKCS1PrivateKey(key_pair)
+	new_public_key := x509.MarshalPKCS1PublicKey(&key_pair.PublicKey)
+	if err != nil {
+		panic(err)
+	}
+
+	private_key_pem := pem.EncodeToMemory(&pem.Block{
+		Type:  "RSA PRIVATE KEY",
+		Bytes: new_private_key,
+	})
+	public_key_pem := pem.EncodeToMemory(&pem.Block{
+		Type:  "RSA PUBLIC KEY",
+		Bytes: new_public_key,
+	})
+
+	println(string(private_key_pem))
+	println(string(public_key_pem))
+}

+ 47 - 0
cmd/license/sign/main.go

@@ -0,0 +1,47 @@
+package main
+
+import (
+	"flag"
+	"os"
+
+	"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_packager/signer"
+	"github.com/langgenius/dify-plugin-daemon/internal/utils/log"
+)
+
+func main() {
+	var (
+		in_path  string
+		out_path string
+		help     bool
+	)
+
+	flag.StringVar(&in_path, "in", "", "input plugin file path")
+	flag.StringVar(&out_path, "out", "", "output plugin file path")
+	flag.BoolVar(&help, "help", false, "show help")
+	flag.Parse()
+
+	if help || in_path == "" || out_path == "" {
+		flag.PrintDefaults()
+		os.Exit(0)
+	}
+
+	// read plugin
+	plugin, err := os.ReadFile(in_path)
+	if err != nil {
+		log.Panic("failed to read plugin file %v", err)
+	}
+
+	// sign plugin
+	plugin_file, err := signer.SignPlugin(plugin)
+	if err != nil {
+		log.Panic("failed to sign plugin %v", err)
+	}
+
+	// write signature
+	err = os.WriteFile(out_path, plugin_file, 0644)
+	if err != nil {
+		log.Panic("failed to write plugin file %v", err)
+	}
+
+	log.Info("plugin signed successfully, output path: %s", out_path)
+}

cmd/packer/main.go → cmd/packager/main.go


+ 1 - 0
go.mod

@@ -34,6 +34,7 @@ require (
 	github.com/panjf2000/ants v1.3.0 // indirect
 	github.com/panjf2000/gnet/v2 v2.5.5 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+	github.com/shopspring/decimal v1.4.0
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.12 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect

+ 2 - 0
go.sum

@@ -59,6 +59,8 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
 github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
+github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

+ 1 - 0
internal/core/license/private_key/.gitignore

@@ -0,0 +1 @@
+PRIVATE_KEY.pem

+ 6 - 0
internal/core/license/private_key/key.go

@@ -0,0 +1,6 @@
+package private_key
+
+import _ "embed"
+
+//go:embed PRIVATE_KEY.pem
+var PRIVATE_KEY []byte

+ 13 - 0
internal/core/license/public_key/PUBLIC_KEY.pem

@@ -0,0 +1,13 @@
+-----BEGIN RSA PUBLIC KEY-----
+MIICCgKCAgEAwUY1cndCmqclHHlCn/PVB011n0m+vmXT2M/QWbViM/2oqO2S5lXl
+VYwZjbN+r3xjUxnzCK6YC7fCI0CXJ69RPTpfRc9meV0TPb50g01kJI6HWL+SziZs
+i+Mmul5ji7W4ebpOUNUPuBgFFfzrOv4BSO6tWVfpiqFTdfF3o2XT3FUja2rMCSQO
+dsGvuhX5/YYAnOH4suxGo+nWda/QC+ohM4z8yLuM6fo7QPmkjTXN+jr7+RZ6JA7g
+6XT7TdMK8oJcqMm15E0pRmgfNNLvqSeVcVM5OgmteTpSn7ekSdCMOpai3Cnc4zOa
+PWSViiHTDu9AIoDbSBRQbvOaQ339eXOlHnomvg6WK6hw3hYoE+ASkv5qK9BwuWqn
+xdUQ99qJ04eNTGjqCo65P9DKzpDb5BT5cxg40Utue0MbuN6pLPwhqkvnVmExuykw
+aLQmTwnLODOD4gc5UU1dUeSUOxQeSoL8zSqL809ZV89B31jYOSFqcKEbyhoKaen3
+dQSUFxEIca7Juj4E4tC+V1RsU2GSaVRCKEtJ4RBnDkQvB4aRBL+zpc26aIBKb3G3
+0a9BWRvXRy+gr/qx8fDJgOuF23j1WXEDVsNtK6vpHGQkDUmsBsE2U+Sag9l2Vnq1
+hrqLbjz2PPOHsBkg09IlM6SNr2NaEWvVJnC/szW8EQ51QBz8XJOnEmUCAwEAAQ==
+-----END RSA PUBLIC KEY-----

+ 6 - 0
internal/core/license/public_key/key.go

@@ -0,0 +1,6 @@
+package public_key
+
+import _ "embed"
+
+//go:embed PUBLIC_KEY.pem
+var PUBLIC_KEY []byte

+ 117 - 0
internal/core/plugin_packager/signer/sign.go

@@ -0,0 +1,117 @@
+package signer
+
+import (
+	"archive/zip"
+	"bytes"
+	"crypto/sha256"
+	"encoding/base64"
+	"io"
+	"strconv"
+	"time"
+
+	"github.com/langgenius/dify-plugin-daemon/internal/core/license/private_key"
+	"github.com/langgenius/dify-plugin-daemon/internal/utils/encryption"
+	"github.com/langgenius/dify-plugin-daemon/internal/utils/parser"
+)
+
+/*
+	DifyPlugin is a file type that represents a plugin, it's designed to based on zip file format.
+	When signing a plugin, we use RSA-4096 to create a signature for the plugin and write the signature
+	into comment field of the zip file.
+*/
+
+// SignPlugin is a function that signs a plugin
+// It takes a plugin as a stream of bytes and signs it with RSA-4096
+func SignPlugin(plugin []byte) ([]byte, error) {
+	// load private key
+	private_key, err := encryption.LoadPrivateKey(private_key.PRIVATE_KEY)
+	if err != nil {
+		return nil, err
+	}
+
+	// construct zip
+	zip_reader, err := zip.NewReader(bytes.NewReader(plugin), int64(len(plugin)))
+	if err != nil {
+		return nil, err
+	}
+
+	data := new(bytes.Buffer)
+	// read one by one
+	for _, file := range zip_reader.File {
+		// read file bytes
+		file_reader, err := file.Open()
+		if err != nil {
+			return nil, err
+		}
+		defer file_reader.Close()
+
+		temp_data := new(bytes.Buffer)
+		_, err = temp_data.ReadFrom(file_reader)
+		if err != nil {
+			return nil, err
+		}
+
+		// calculate sha256 hash of the file
+		hash := sha256.New()
+		hash.Write(temp_data.Bytes())
+		hashed := hash.Sum(nil)
+
+		// write the hash into data
+		data.Write(hashed)
+	}
+
+	// get current time
+	ct := time.Now().Unix()
+
+	// convert time to bytes
+	time_string := strconv.FormatInt(ct, 10)
+
+	// write the time into data
+	data.Write([]byte(time_string))
+
+	// sign the data
+	signature, err := encryption.RSASign(private_key, data.Bytes())
+	if err != nil {
+		return nil, err
+	}
+
+	// write the signature into the comment field of the zip file
+	zip_buffer := new(bytes.Buffer)
+	zip_writer := zip.NewWriter(zip_buffer)
+	for _, file := range zip_reader.File {
+		file_writer, err := zip_writer.Create(file.Name)
+		if err != nil {
+			return nil, err
+		}
+
+		file_reader, err := file.Open()
+		if err != nil {
+			return nil, err
+		}
+		defer file_reader.Close()
+
+		_, err = io.Copy(file_writer, file_reader)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	comments := parser.MarshalJson(map[string]string{
+		"signature": base64.StdEncoding.EncodeToString(signature),
+		"time":      time_string,
+	})
+
+	// write signature
+	err = zip_writer.SetComment(comments)
+	if err != nil {
+		return nil, err
+	}
+
+	// close the zip writer
+	err = zip_writer.Close()
+	if err != nil {
+		return nil, err
+	}
+
+	return zip_buffer.Bytes(), nil
+}

+ 105 - 0
internal/utils/encryption/rsa.go

@@ -0,0 +1,105 @@
+package encryption
+
+import (
+	"crypto"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/pem"
+	"errors"
+	"io"
+)
+
+func RSASign(rsaPrivateKey *rsa.PrivateKey, data []byte) ([]byte, error) {
+	hashed := sha256.Sum256(data)
+	return rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashed[:])
+}
+
+func VerifySign(rsaPublicKey *rsa.PublicKey, data []byte, sign []byte) error {
+	hashed := sha256.Sum256(data)
+	return rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed[:], sign)
+}
+
+func AESEncrypt(aesKey []byte, data []byte) ([]byte, error) {
+	block, err := aes.NewCipher(aesKey)
+	if err != nil {
+		return nil, err
+	}
+
+	aesGCM, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	nonce := make([]byte, aesGCM.NonceSize())
+	if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
+		return nil, err
+	}
+
+	cipherText := aesGCM.Seal(nonce, nonce, data, nil)
+	return cipherText, nil
+}
+
+func AESDecrypt(aesKey []byte, data []byte) ([]byte, error) {
+	block, err := aes.NewCipher(aesKey)
+	if err != nil {
+		return nil, err
+	}
+
+	aesGCM, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	nonceSize := aesGCM.NonceSize()
+	if len(data) < nonceSize {
+		return nil, errors.New("ciphertext too short")
+	}
+
+	nonce, cipherText := data[:nonceSize], data[nonceSize:]
+	plainText, err := aesGCM.Open(nil, nonce, cipherText, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return plainText, nil
+}
+
+func LoadPrivateKey(data []byte) (*rsa.PrivateKey, error) {
+	private_key_block, rest := pem.Decode(data)
+	if private_key_block == nil || private_key_block.Type != "RSA PRIVATE KEY" {
+		return nil, errors.New("failed to decode PEM block containing private key")
+	}
+
+	if len(rest) != 0 {
+		return nil, errors.New("extra data included in the PEM block")
+	}
+
+	private_key, err := x509.ParsePKCS1PrivateKey(private_key_block.Bytes)
+	if err != nil {
+		return nil, err
+	}
+
+	return private_key, nil
+}
+
+func LoadPublicKey(data []byte) (*rsa.PublicKey, error) {
+	public_key_block, rest := pem.Decode(data)
+	if public_key_block == nil || public_key_block.Type != "RSA PUBLIC KEY" {
+		return nil, errors.New("failed to decode PEM block containing public key")
+	}
+
+	if len(rest) != 0 {
+		return nil, errors.New("extra data included in the PEM block")
+	}
+
+	public_key, err := x509.ParsePKCS1PublicKey(public_key_block.Bytes)
+	if err != nil {
+		return nil, err
+	}
+
+	return public_key, nil
+}