1111
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 47s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 47s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m3s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 51s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m6s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 51s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 47s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 47s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m3s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 58s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 51s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m6s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 45s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 51s
This commit is contained in:
77
pkg/plugin/sign/official.go
Normal file
77
pkg/plugin/sign/official.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// KeyEntry 密钥条目
|
||||
type KeyEntry struct {
|
||||
ID string // 密钥 ID
|
||||
PublicKey string // Base64 编码的公钥
|
||||
ValidFrom time.Time // 生效时间
|
||||
RevokedAt time.Time // 吊销时间(零值表示未吊销)
|
||||
}
|
||||
|
||||
// 官方公钥列表(支持密钥轮换)
|
||||
var officialKeys = []KeyEntry{
|
||||
{
|
||||
ID: "official-v1",
|
||||
PublicKey: "0A0xRthj0wgPg8X8GJZ6/EnNpAUw5v7O//XLty+P5Yw=",
|
||||
ValidFrom: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
// 添加新密钥时,在此处追加
|
||||
}
|
||||
|
||||
var (
|
||||
keyCache map[string]ed25519.PublicKey
|
||||
keyCacheOnce sync.Once
|
||||
)
|
||||
|
||||
// initKeyCache 初始化密钥缓存
|
||||
func initKeyCache() {
|
||||
keyCache = make(map[string]ed25519.PublicKey)
|
||||
for _, entry := range officialKeys {
|
||||
if pub, err := DecodePublicKey(entry.PublicKey); err == nil {
|
||||
keyCache[entry.ID] = pub
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetOfficialPublicKey 获取默认官方公钥(兼容旧接口)
|
||||
func GetOfficialPublicKey() (ed25519.PublicKey, error) {
|
||||
return GetPublicKeyByID("official-v1")
|
||||
}
|
||||
|
||||
// GetPublicKeyByID 根据 ID 获取公钥
|
||||
func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
|
||||
keyCacheOnce.Do(initKeyCache)
|
||||
|
||||
pub, ok := keyCache[keyID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown key ID: %s", keyID)
|
||||
}
|
||||
return pub, nil
|
||||
}
|
||||
|
||||
// IsKeyRevoked 检查密钥是否已吊销
|
||||
func IsKeyRevoked(keyID string) bool {
|
||||
for _, entry := range officialKeys {
|
||||
if entry.ID == keyID {
|
||||
return !entry.RevokedAt.IsZero()
|
||||
}
|
||||
}
|
||||
return true // 未知密钥视为已吊销
|
||||
}
|
||||
|
||||
// GetKeyEntry 获取密钥条目
|
||||
func GetKeyEntry(keyID string) *KeyEntry {
|
||||
for i := range officialKeys {
|
||||
if officialKeys[i].ID == keyID {
|
||||
return &officialKeys[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
107
pkg/plugin/sign/payload.go
Normal file
107
pkg/plugin/sign/payload.go
Normal file
@@ -0,0 +1,107 @@
|
||||
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"` // 源码 SHA256
|
||||
KeyID string `json:"key_id"` // 签名密钥 ID
|
||||
Timestamp int64 `json:"timestamp"` // 签名时间戳
|
||||
}
|
||||
|
||||
// SignedPlugin 已签名的插件
|
||||
type SignedPlugin struct {
|
||||
Payload PluginPayload `json:"payload"`
|
||||
Signature string `json:"signature"` // Base64 签名
|
||||
}
|
||||
|
||||
// NormalizeSource 规范化源码(统一换行符)
|
||||
func NormalizeSource(source string) string {
|
||||
// 统一换行符为 LF
|
||||
normalized := strings.ReplaceAll(source, "\r\n", "\n")
|
||||
normalized = strings.ReplaceAll(normalized, "\r", "\n")
|
||||
// 去除尾部空白
|
||||
normalized = strings.TrimRight(normalized, " \t\n")
|
||||
return normalized
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// VerifyPlugin 验证插件签名
|
||||
func VerifyPlugin(pub ed25519.PublicKey, signed *SignedPlugin, source string) error {
|
||||
// 验证源码哈希
|
||||
expectedHash := HashSource(source)
|
||||
if signed.Payload.SourceHash != expectedHash {
|
||||
return fmt.Errorf("source hash mismatch")
|
||||
}
|
||||
|
||||
// 序列化载荷
|
||||
data, err := json.Marshal(signed.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal payload: %w", err)
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
return VerifyBase64(pub, data, signed.Signature)
|
||||
}
|
||||
|
||||
// EncodeSignedPlugin 编码已签名插件为 JSON
|
||||
func EncodeSignedPlugin(sp *SignedPlugin) (string, error) {
|
||||
data, err := json.Marshal(sp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// DecodeSignedPlugin 从 JSON 解码已签名插件
|
||||
func DecodeSignedPlugin(data string) (*SignedPlugin, error) {
|
||||
var sp SignedPlugin
|
||||
if err := json.Unmarshal([]byte(data), &sp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sp, nil
|
||||
}
|
||||
58
pkg/plugin/sign/revocation.go
Normal file
58
pkg/plugin/sign/revocation.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// RevocationEntry 撤销条目
|
||||
type RevocationEntry struct {
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
Version string `json:"version,omitempty"` // 特定版本(空表示所有版本)
|
||||
Reason string `json:"reason"` // 撤销原因
|
||||
RevokedAt int64 `json:"revoked_at"` // 撤销时间戳
|
||||
}
|
||||
|
||||
// RevocationList 撤销列表
|
||||
type RevocationList struct {
|
||||
Version int `json:"version"` // 列表版本
|
||||
UpdatedAt int64 `json:"updated_at"` // 更新时间
|
||||
Entries []RevocationEntry `json:"entries"` // 撤销条目
|
||||
Signature string `json:"signature"` // 列表签名
|
||||
}
|
||||
|
||||
// 内置撤销列表(编译时确定)
|
||||
var builtinRevocations = []RevocationEntry{
|
||||
// 示例:{PluginName: "malicious-plugin", Reason: "security vulnerability"}
|
||||
}
|
||||
|
||||
var (
|
||||
revocationCache map[string][]RevocationEntry // pluginName -> entries
|
||||
revocationCacheOnce sync.Once
|
||||
)
|
||||
|
||||
// initRevocationCache 初始化撤销缓存
|
||||
func initRevocationCache() {
|
||||
revocationCache = make(map[string][]RevocationEntry)
|
||||
for _, entry := range builtinRevocations {
|
||||
revocationCache[entry.PluginName] = append(
|
||||
revocationCache[entry.PluginName], entry)
|
||||
}
|
||||
}
|
||||
|
||||
// IsPluginRevoked 检查插件是否被撤销
|
||||
func IsPluginRevoked(name, version string) (bool, string) {
|
||||
revocationCacheOnce.Do(initRevocationCache)
|
||||
|
||||
entries, ok := revocationCache[name]
|
||||
if !ok {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
// 空版本表示所有版本都被撤销
|
||||
if entry.Version == "" || entry.Version == version {
|
||||
return true, entry.Reason
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
92
pkg/plugin/sign/sign.go
Normal file
92
pkg/plugin/sign/sign.go
Normal file
@@ -0,0 +1,92 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// VerifyBase64 验证 Base64 编码的签名
|
||||
func VerifyBase64(publicKey ed25519.PublicKey, data []byte, sigB64 string) error {
|
||||
sig, err := base64.StdEncoding.DecodeString(sigB64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode signature: %w", err)
|
||||
}
|
||||
if !Verify(publicKey, data, sig) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodePublicKey 编码公钥为 Base64
|
||||
func EncodePublicKey(pub ed25519.PublicKey) string {
|
||||
return base64.StdEncoding.EncodeToString(pub)
|
||||
}
|
||||
|
||||
// DecodePublicKey 从 Base64 解码公钥
|
||||
func DecodePublicKey(s string) (ed25519.PublicKey, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) != ed25519.PublicKeySize {
|
||||
return nil, ErrInvalidPublicKey
|
||||
}
|
||||
return ed25519.PublicKey(data), nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
47
pkg/plugin/sign/version.go
Normal file
47
pkg/plugin/sign/version.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CompareVersions 比较两个版本号
|
||||
// 返回: -1 (v1 < v2), 0 (v1 == v2), 1 (v1 > v2)
|
||||
func CompareVersions(v1, v2 string) int {
|
||||
parts1 := parseVersion(v1)
|
||||
parts2 := parseVersion(v2)
|
||||
|
||||
maxLen := len(parts1)
|
||||
if len(parts2) > maxLen {
|
||||
maxLen = len(parts2)
|
||||
}
|
||||
|
||||
for i := 0; i < maxLen; i++ {
|
||||
var p1, p2 int
|
||||
if i < len(parts1) {
|
||||
p1 = parts1[i]
|
||||
}
|
||||
if i < len(parts2) {
|
||||
p2 = parts2[i]
|
||||
}
|
||||
|
||||
if p1 < p2 {
|
||||
return -1
|
||||
}
|
||||
if p1 > p2 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseVersion(v string) []int {
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
parts := strings.Split(v, ".")
|
||||
result := make([]int, len(parts))
|
||||
for i, p := range parts {
|
||||
n, _ := strconv.Atoi(p)
|
||||
result[i] = n
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user