From dd52c483518e81aaa646dd07c4ce757d97c3897a Mon Sep 17 00:00:00 2001 From: Flik Date: Mon, 29 Dec 2025 19:05:56 +0800 Subject: [PATCH] feat: add signtool to plugin repository - Migrate signing tool from GoTunnel main project - Self-contained, no external dependencies - Updated CI workflow to build locally --- .github/workflows/sign.yml | 5 +- .gitignore | 5 +- go.mod | 3 + scripts/sign-all.sh | 14 +++-- tools/signtool/main.go | 106 +++++++++++++++++++++++++++++++++ tools/signtool/sign/payload.go | 70 ++++++++++++++++++++++ tools/signtool/sign/sign.go | 68 +++++++++++++++++++++ 7 files changed, 261 insertions(+), 10 deletions(-) create mode 100644 go.mod create mode 100644 tools/signtool/main.go create mode 100644 tools/signtool/sign/payload.go create mode 100644 tools/signtool/sign/sign.go diff --git a/.github/workflows/sign.yml b/.github/workflows/sign.yml index ec0c9bb..471dd22 100644 --- a/.github/workflows/sign.yml +++ b/.github/workflows/sign.yml @@ -20,10 +20,7 @@ jobs: go-version: '1.21' - name: Build signtool - run: | - git clone --depth 1 https://github.com/your-org/gotunnel.git /tmp/gotunnel - cd /tmp/gotunnel - go build -o /usr/local/bin/signtool ./cmd/signtool + run: go build -o signtool ./tools/signtool - name: Sign plugins env: diff --git a/.gitignore b/.gitignore index 0c9e274..6ed60fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -# Signatures (auto-generated) -*.sig +# Build artifacts +signtool +signtool.exe # OS .DS_Store diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..26d4b69 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/gotunnel/plugins + +go 1.21 diff --git a/scripts/sign-all.sh b/scripts/sign-all.sh index 73254f4..378c59a 100755 --- a/scripts/sign-all.sh +++ b/scripts/sign-all.sh @@ -15,13 +15,19 @@ fi SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(dirname "$SCRIPT_DIR")" +SIGNTOOL="$REPO_ROOT/signtool" + +# 如果 signtool 不存在,尝试构建 +if [ ! -f "$SIGNTOOL" ]; then + echo "Building signtool..." + cd "$REPO_ROOT" + go build -o signtool ./tools/signtool +fi cd "$REPO_ROOT" for manifest in plugins/*/manifest.json; do - if [ ! -f "$manifest" ]; then - continue - fi + [ -f "$manifest" ] || continue dir=$(dirname "$manifest") name=$(jq -r '.name' "$manifest") @@ -34,7 +40,7 @@ for manifest in plugins/*/manifest.json; do fi echo "Signing: $name v$version" - signtool sign -key "$KEY_FILE" \ + "$SIGNTOOL" sign -key "$KEY_FILE" \ -name "$name" \ -version "$version" \ "$plugin_file" diff --git a/tools/signtool/main.go b/tools/signtool/main.go new file mode 100644 index 0000000..0bc6f6a --- /dev/null +++ b/tools/signtool/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/gotunnel/plugins/tools/signtool/sign" +) + +func main() { + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + switch os.Args[1] { + case "keygen": + cmdKeygen() + case "sign": + cmdSign(os.Args[2:]) + default: + printUsage() + os.Exit(1) + } +} + +func printUsage() { + fmt.Println("GoTunnel Plugin Sign Tool") + fmt.Println() + fmt.Println("Usage:") + fmt.Println(" signtool keygen") + fmt.Println(" signtool sign -key KEY -name NAME -version VER FILE") +} + +func cmdKeygen() { + kp, err := sign.GenerateKeyPair() + if err != nil { + fmt.Fprintf(os.Stderr, "生成密钥失败: %v\n", err) + os.Exit(1) + } + + fmt.Println("=== 密钥对已生成 ===") + fmt.Println() + fmt.Println("私钥 (请妥善保管):") + fmt.Println(sign.EncodePrivateKey(kp.PrivateKey)) + fmt.Println() + fmt.Println("公钥 (内置到客户端):") + fmt.Println(sign.EncodePublicKey(kp.PublicKey)) +} + +func cmdSign(args []string) { + fs := flag.NewFlagSet("sign", flag.ExitOnError) + keyFile := fs.String("key", "", "私钥文件路径") + name := fs.String("name", "", "插件名称") + version := fs.String("version", "", "插件版本") + keyID := fs.String("keyid", "official-v1", "密钥 ID") + fs.Parse(args) + + if *keyFile == "" || *name == "" || *version == "" || fs.NArg() < 1 { + fmt.Println("用法: signtool sign -key KEY -name NAME -version VER FILE") + os.Exit(1) + } + + // 读取私钥 + keyData, err := os.ReadFile(*keyFile) + if err != nil { + fmt.Fprintf(os.Stderr, "读取私钥失败: %v\n", err) + os.Exit(1) + } + + privKey, err := sign.DecodePrivateKey(strings.TrimSpace(string(keyData))) + if err != nil { + fmt.Fprintf(os.Stderr, "解析私钥失败: %v\n", err) + os.Exit(1) + } + + // 读取插件文件 + pluginFile := fs.Arg(0) + source, err := os.ReadFile(pluginFile) + if err != nil { + fmt.Fprintf(os.Stderr, "读取文件失败: %v\n", err) + os.Exit(1) + } + + // 创建载荷并签名 + payload := sign.CreatePayload(*name, *version, string(source), *keyID) + signed, err := sign.SignPlugin(privKey, payload) + if err != nil { + fmt.Fprintf(os.Stderr, "签名失败: %v\n", err) + os.Exit(1) + } + + // 写入签名文件 + sigData, _ := sign.EncodeSignedPlugin(signed) + sigFile := pluginFile + ".sig" + if err := os.WriteFile(sigFile, []byte(sigData), 0644); err != nil { + fmt.Fprintf(os.Stderr, "写入签名失败: %v\n", err) + os.Exit(1) + } + + fmt.Printf("签名成功: %s\n", sigFile) + fmt.Printf(" 插件: %s v%s\n", *name, *version) + fmt.Printf(" 哈希: %s\n", payload.SourceHash) +} diff --git a/tools/signtool/sign/payload.go b/tools/signtool/sign/payload.go new file mode 100644 index 0000000..0cb1a71 --- /dev/null +++ b/tools/signtool/sign/payload.go @@ -0,0 +1,70 @@ +package sign + +import ( + "crypto/ed25519" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" +) + +// PluginPayload 插件签名载荷 +type PluginPayload struct { + Name string `json:"name"` + Version string `json:"version"` + SourceHash string `json:"source_hash"` + KeyID string `json:"key_id"` + Timestamp int64 `json:"timestamp"` +} + +// SignedPlugin 已签名的插件 +type SignedPlugin struct { + Payload PluginPayload `json:"payload"` + Signature string `json:"signature"` +} + +// NormalizeSource 规范化源码 +func NormalizeSource(source string) string { + normalized := strings.ReplaceAll(source, "\r\n", "\n") + normalized = strings.ReplaceAll(normalized, "\r", "\n") + return strings.TrimRight(normalized, " \t\n") +} + +// HashSource 计算源码哈希 +func HashSource(source string) string { + normalized := NormalizeSource(source) + hash := sha256.Sum256([]byte(normalized)) + return hex.EncodeToString(hash[:]) +} + +// CreatePayload 创建签名载荷 +func CreatePayload(name, version, source, keyID string) *PluginPayload { + return &PluginPayload{ + Name: name, + Version: version, + SourceHash: HashSource(source), + KeyID: keyID, + Timestamp: time.Now().Unix(), + } +} + +// SignPlugin 签名插件 +func SignPlugin(priv ed25519.PrivateKey, payload *PluginPayload) (*SignedPlugin, error) { + data, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("marshal payload: %w", err) + } + sig := SignBase64(priv, data) + return &SignedPlugin{Payload: *payload, Signature: sig}, nil +} + +// EncodeSignedPlugin 编码为 JSON +func EncodeSignedPlugin(sp *SignedPlugin) (string, error) { + data, err := json.Marshal(sp) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/tools/signtool/sign/sign.go b/tools/signtool/sign/sign.go new file mode 100644 index 0000000..49a1815 --- /dev/null +++ b/tools/signtool/sign/sign.go @@ -0,0 +1,68 @@ +package sign + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" +) + +var ( + ErrInvalidSignature = errors.New("invalid signature") + ErrInvalidPublicKey = errors.New("invalid public key") + ErrInvalidPrivateKey = errors.New("invalid private key") +) + +// KeyPair Ed25519 密钥对 +type KeyPair struct { + PublicKey ed25519.PublicKey + PrivateKey ed25519.PrivateKey +} + +// GenerateKeyPair 生成新的密钥对 +func GenerateKeyPair() (*KeyPair, error) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("generate key: %w", err) + } + return &KeyPair{PublicKey: pub, PrivateKey: priv}, nil +} + +// Sign 使用私钥签名数据 +func Sign(privateKey ed25519.PrivateKey, data []byte) []byte { + return ed25519.Sign(privateKey, data) +} + +// Verify 使用公钥验证签名 +func Verify(publicKey ed25519.PublicKey, data, signature []byte) bool { + return ed25519.Verify(publicKey, data, signature) +} + +// SignBase64 签名并返回 Base64 编码 +func SignBase64(privateKey ed25519.PrivateKey, data []byte) string { + sig := Sign(privateKey, data) + return base64.StdEncoding.EncodeToString(sig) +} + +// EncodePublicKey 编码公钥为 Base64 +func EncodePublicKey(pub ed25519.PublicKey) string { + return base64.StdEncoding.EncodeToString(pub) +} + +// EncodePrivateKey 编码私钥为 Base64 +func EncodePrivateKey(priv ed25519.PrivateKey) string { + return base64.StdEncoding.EncodeToString(priv) +} + +// DecodePrivateKey 从 Base64 解码私钥 +func DecodePrivateKey(s string) (ed25519.PrivateKey, error) { + data, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, err + } + if len(data) != ed25519.PrivateKeySize { + return nil, ErrInvalidPrivateKey + } + return ed25519.PrivateKey(data), nil +}