feat: add signtool to plugin repository

- Migrate signing tool from GoTunnel main project
- Self-contained, no external dependencies
- Updated CI workflow to build locally
This commit is contained in:
Flik
2025-12-29 19:05:56 +08:00
parent 6b38f133f4
commit dd52c48351
7 changed files with 261 additions and 10 deletions

View File

@@ -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:

5
.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Signatures (auto-generated)
*.sig
# Build artifacts
signtool
signtool.exe
# OS
.DS_Store

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/gotunnel/plugins
go 1.21

View File

@@ -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"

106
tools/signtool/main.go Normal file
View File

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

View File

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

View File

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