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:
5
.github/workflows/sign.yml
vendored
5
.github/workflows/sign.yml
vendored
@@ -20,10 +20,7 @@ jobs:
|
|||||||
go-version: '1.21'
|
go-version: '1.21'
|
||||||
|
|
||||||
- name: Build signtool
|
- name: Build signtool
|
||||||
run: |
|
run: go build -o signtool ./tools/signtool
|
||||||
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
|
|
||||||
|
|
||||||
- name: Sign plugins
|
- name: Sign plugins
|
||||||
env:
|
env:
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
# Signatures (auto-generated)
|
# Build artifacts
|
||||||
*.sig
|
signtool
|
||||||
|
signtool.exe
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -15,13 +15,19 @@ fi
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
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"
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
for manifest in plugins/*/manifest.json; do
|
for manifest in plugins/*/manifest.json; do
|
||||||
if [ ! -f "$manifest" ]; then
|
[ -f "$manifest" ] || continue
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
dir=$(dirname "$manifest")
|
dir=$(dirname "$manifest")
|
||||||
name=$(jq -r '.name' "$manifest")
|
name=$(jq -r '.name' "$manifest")
|
||||||
@@ -34,7 +40,7 @@ for manifest in plugins/*/manifest.json; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Signing: $name v$version"
|
echo "Signing: $name v$version"
|
||||||
signtool sign -key "$KEY_FILE" \
|
"$SIGNTOOL" sign -key "$KEY_FILE" \
|
||||||
-name "$name" \
|
-name "$name" \
|
||||||
-version "$version" \
|
-version "$version" \
|
||||||
"$plugin_file"
|
"$plugin_file"
|
||||||
|
|||||||
106
tools/signtool/main.go
Normal file
106
tools/signtool/main.go
Normal 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)
|
||||||
|
}
|
||||||
70
tools/signtool/sign/payload.go
Normal file
70
tools/signtool/sign/payload.go
Normal 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
|
||||||
|
}
|
||||||
68
tools/signtool/sign/sign.go
Normal file
68
tools/signtool/sign/sign.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user