feat(plugin): 实现插件安全验证和审计日志功能
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Failing after 19s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been skipped

- 添加插件签名验证机制,支持远程证书吊销列表
- 增加插件安装时的安全检查和签名验证
- 实现插件版本存储的HMAC完整性校验
- 添加插件审计日志记录插件安装和验证事件
- 增加JS插件沙箱安全限制配置
- 添加插件商店API的签名URL字段支持
- 实现安全配置的自动刷新机制
This commit is contained in:
Flik
2025-12-30 22:06:33 +08:00
parent 4d2a2a7117
commit 42e11e0aca
13 changed files with 686 additions and 31 deletions

154
pkg/plugin/audit/audit.go Normal file
View File

@@ -0,0 +1,154 @@
package audit
import (
"encoding/json"
"log"
"os"
"path/filepath"
"sync"
"time"
)
// EventType 审计事件类型
type EventType string
const (
EventPluginInstall EventType = "plugin_install"
EventPluginUninstall EventType = "plugin_uninstall"
EventPluginStart EventType = "plugin_start"
EventPluginStop EventType = "plugin_stop"
EventPluginVerify EventType = "plugin_verify"
EventPluginReject EventType = "plugin_reject"
EventConfigChange EventType = "config_change"
)
// Event 审计事件
type Event struct {
Timestamp time.Time `json:"timestamp"`
Type EventType `json:"type"`
PluginName string `json:"plugin_name,omitempty"`
Version string `json:"version,omitempty"`
ClientID string `json:"client_id,omitempty"`
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Details map[string]string `json:"details,omitempty"`
}
// Logger 审计日志记录器
type Logger struct {
path string
file *os.File
mu sync.Mutex
enabled bool
}
var (
defaultLogger *Logger
loggerOnce sync.Once
)
// NewLogger 创建审计日志记录器
func NewLogger(dataDir string) (*Logger, error) {
path := filepath.Join(dataDir, "audit.log")
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, err
}
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return nil, err
}
return &Logger{path: path, file: file, enabled: true}, nil
}
// InitDefault 初始化默认日志记录器
func InitDefault(dataDir string) error {
var err error
loggerOnce.Do(func() {
defaultLogger, err = NewLogger(dataDir)
})
return err
}
// Log 记录审计事件
func (l *Logger) Log(event Event) {
if l == nil || !l.enabled {
return
}
event.Timestamp = time.Now()
l.mu.Lock()
defer l.mu.Unlock()
data, err := json.Marshal(event)
if err != nil {
log.Printf("[Audit] Marshal error: %v", err)
return
}
if _, err := l.file.Write(append(data, '\n')); err != nil {
log.Printf("[Audit] Write error: %v", err)
}
}
// Close 关闭日志文件
func (l *Logger) Close() error {
if l == nil || l.file == nil {
return nil
}
return l.file.Close()
}
// LogEvent 使用默认记录器记录事件
func LogEvent(event Event) {
if defaultLogger != nil {
defaultLogger.Log(event)
}
}
// LogPluginInstall 记录插件安装事件
func LogPluginInstall(pluginName, version, clientID string, success bool, msg string) {
LogEvent(Event{
Type: EventPluginInstall,
PluginName: pluginName,
Version: version,
ClientID: clientID,
Success: success,
Message: msg,
})
}
// LogPluginVerify 记录插件验证事件
func LogPluginVerify(pluginName, version string, success bool, msg string) {
LogEvent(Event{
Type: EventPluginVerify,
PluginName: pluginName,
Version: version,
Success: success,
Message: msg,
})
}
// LogPluginReject 记录插件拒绝事件
func LogPluginReject(pluginName, version, reason string) {
LogEvent(Event{
Type: EventPluginReject,
PluginName: pluginName,
Version: version,
Success: false,
Message: reason,
})
}
// LogWithDetails 记录带详情的事件
func LogWithDetails(eventType EventType, pluginName string, success bool, msg string, details map[string]string) {
LogEvent(Event{
Type: eventType,
PluginName: pluginName,
Success: success,
Message: msg,
Details: details,
})
}

View File

@@ -48,6 +48,11 @@ func (p *JSPlugin) SetSandbox(sandbox *Sandbox) {
// init 初始化 JS 运行时
func (p *JSPlugin) init() error {
// 设置栈深度限制(防止递归攻击)
if p.sandbox.MaxStackDepth > 0 {
p.vm.SetMaxCallStackSize(p.sandbox.MaxStackDepth)
}
// 注入基础 API
p.vm.Set("log", p.jsLog)
p.vm.Set("config", p.jsGetConfig)

View File

@@ -21,6 +21,10 @@ type Sandbox struct {
MaxReadSize int64
// 最大文件写入大小 (bytes)
MaxWriteSize int64
// 最大内存使用量 (bytes)0 表示不限制
MaxMemory int64
// 最大调用栈深度
MaxStackDepth int
}
// DefaultSandbox 返回默认沙箱配置(最小权限)
@@ -30,8 +34,10 @@ func DefaultSandbox() *Sandbox {
WritablePaths: []string{},
DeniedPaths: defaultDeniedPaths(),
AllowNetwork: false,
MaxReadSize: 10 * 1024 * 1024, // 10MB
MaxWriteSize: 1 * 1024 * 1024, // 1MB
MaxReadSize: 10 * 1024 * 1024, // 10MB
MaxWriteSize: 1 * 1024 * 1024, // 1MB
MaxMemory: 64 * 1024 * 1024, // 64MB
MaxStackDepth: 1000, // 最大调用栈深度
}
}

View File

@@ -2,17 +2,21 @@ package sign
import (
"crypto/ed25519"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sync"
"time"
)
// KeyEntry 密钥条目
type KeyEntry struct {
ID string // 密钥 ID
PublicKey string // Base64 编码的公钥
ValidFrom time.Time // 生效时间
RevokedAt time.Time // 吊销时间(零值表示未吊销)
ID string `json:"id"`
PublicKey string `json:"public_key"`
ValidFrom time.Time `json:"valid_from"`
RevokedAt time.Time `json:"revoked_at,omitempty"`
}
// 官方公钥列表(支持密钥轮换)
@@ -28,8 +32,30 @@ var officialKeys = []KeyEntry{
var (
keyCache map[string]ed25519.PublicKey
keyCacheOnce sync.Once
keyMu sync.RWMutex
remoteKeys []KeyEntry
)
// KeyListConfig 远程公钥列表配置
type KeyListConfig struct {
RemoteURL string
FetchInterval time.Duration
RequestTimeout time.Duration
}
var keyListConfig = KeyListConfig{
RemoteURL: "",
FetchInterval: 24 * time.Hour,
RequestTimeout: 10 * time.Second,
}
// SetKeyListConfig 设置远程公钥列表配置
func SetKeyListConfig(cfg KeyListConfig) {
keyMu.Lock()
defer keyMu.Unlock()
keyListConfig = cfg
}
// initKeyCache 初始化密钥缓存
func initKeyCache() {
keyCache = make(map[string]ed25519.PublicKey)
@@ -49,7 +75,10 @@ func GetOfficialPublicKey() (ed25519.PublicKey, error) {
func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
keyCacheOnce.Do(initKeyCache)
keyMu.RLock()
pub, ok := keyCache[keyID]
keyMu.RUnlock()
if !ok {
return nil, fmt.Errorf("unknown key ID: %s", keyID)
}
@@ -58,11 +87,20 @@ func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
// IsKeyRevoked 检查密钥是否已吊销
func IsKeyRevoked(keyID string) bool {
// 先检查内置密钥
for _, entry := range officialKeys {
if entry.ID == keyID {
return !entry.RevokedAt.IsZero()
}
}
// 再检查远程密钥
keyMu.RLock()
defer keyMu.RUnlock()
for _, entry := range remoteKeys {
if entry.ID == keyID {
return !entry.RevokedAt.IsZero()
}
}
return true // 未知密钥视为已吊销
}
@@ -73,5 +111,108 @@ func GetKeyEntry(keyID string) *KeyEntry {
return &officialKeys[i]
}
}
keyMu.RLock()
defer keyMu.RUnlock()
for i := range remoteKeys {
if remoteKeys[i].ID == keyID {
return &remoteKeys[i]
}
}
return nil
}
// KeyList 远程公钥列表结构
type KeyList struct {
Version int `json:"version"`
UpdatedAt int64 `json:"updated_at"`
Keys []KeyEntry `json:"keys"`
Signature string `json:"signature"`
}
// FetchRemoteKeyList 从远程拉取公钥列表
func FetchRemoteKeyList() error {
keyMu.RLock()
cfg := keyListConfig
keyMu.RUnlock()
if cfg.RemoteURL == "" {
return nil
}
client := &http.Client{Timeout: cfg.RequestTimeout}
resp, err := client.Get(cfg.RemoteURL)
if err != nil {
return fmt.Errorf("fetch key list: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("fetch key list: status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read key list: %w", err)
}
var list KeyList
if err := json.Unmarshal(body, &list); err != nil {
return fmt.Errorf("parse key list: %w", err)
}
// 验证签名(使用内置公钥验证)
if err := verifyKeyListSignature(&list); err != nil {
return fmt.Errorf("verify key list: %w", err)
}
return updateKeyCache(&list)
}
// verifyKeyListSignature 验证公钥列表签名
func verifyKeyListSignature(list *KeyList) error {
if list.Signature == "" {
return fmt.Errorf("missing signature")
}
// 使用内置公钥验证(必须用内置密钥签名)
pubKey, err := GetOfficialPublicKey()
if err != nil {
return err
}
signData := struct {
Version int `json:"version"`
UpdatedAt int64 `json:"updated_at"`
Keys []KeyEntry `json:"keys"`
}{
Version: list.Version,
UpdatedAt: list.UpdatedAt,
Keys: list.Keys,
}
data, err := json.Marshal(signData)
if err != nil {
return err
}
return VerifyBase64(pubKey, data, list.Signature)
}
// updateKeyCache 更新公钥缓存
func updateKeyCache(list *KeyList) error {
keyMu.Lock()
defer keyMu.Unlock()
// 保存远程密钥列表
remoteKeys = list.Keys
// 更新缓存
for _, entry := range list.Keys {
if pub, err := DecodePublicKey(entry.PublicKey); err == nil {
keyCache[entry.ID] = pub
}
}
log.Printf("[KeyList] Updated with %d keys", len(list.Keys))
return nil
}

View File

@@ -1,7 +1,13 @@
package sign
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sync"
"time"
)
// RevocationEntry 撤销条目
@@ -20,7 +26,7 @@ type RevocationList struct {
Signature string `json:"signature"` // 列表签名
}
// 内置撤销列表(编译时确定)
// 内置撤销列表(编译时确定,作为 fallback
var builtinRevocations = []RevocationEntry{
// 示例:{PluginName: "malicious-plugin", Reason: "security vulnerability"}
}
@@ -28,8 +34,42 @@ var builtinRevocations = []RevocationEntry{
var (
revocationCache map[string][]RevocationEntry // pluginName -> entries
revocationCacheOnce sync.Once
revocationMu sync.RWMutex
currentListVersion int
lastFetchTime time.Time
)
// RevocationConfig 远程撤销列表配置
type RevocationConfig struct {
RemoteURL string // 远程撤销列表 URL
FetchInterval time.Duration // 拉取间隔
RequestTimeout time.Duration // 请求超时
VerifySignature bool // 是否验证签名
}
var defaultRevocationConfig = RevocationConfig{
RemoteURL: "", // 默认为空,不启用远程拉取
FetchInterval: 1 * time.Hour,
RequestTimeout: 10 * time.Second,
VerifySignature: true,
}
var revocationConfig = defaultRevocationConfig
// SetRevocationConfig 设置远程撤销列表配置
func SetRevocationConfig(cfg RevocationConfig) {
revocationMu.Lock()
defer revocationMu.Unlock()
revocationConfig = cfg
}
// GetRevocationConfig 获取当前配置
func GetRevocationConfig() RevocationConfig {
revocationMu.RLock()
defer revocationMu.RUnlock()
return revocationConfig
}
// initRevocationCache 初始化撤销缓存
func initRevocationCache() {
revocationCache = make(map[string][]RevocationEntry)
@@ -43,6 +83,9 @@ func initRevocationCache() {
func IsPluginRevoked(name, version string) (bool, string) {
revocationCacheOnce.Do(initRevocationCache)
revocationMu.RLock()
defer revocationMu.RUnlock()
entries, ok := revocationCache[name]
if !ok {
return false, ""
@@ -56,3 +99,144 @@ func IsPluginRevoked(name, version string) (bool, string) {
}
return false, ""
}
// FetchRemoteRevocationList 从远程拉取撤销列表
func FetchRemoteRevocationList() error {
cfg := GetRevocationConfig()
if cfg.RemoteURL == "" {
return nil // 未配置远程 URL跳过
}
// 检查是否需要刷新
revocationMu.RLock()
if time.Since(lastFetchTime) < cfg.FetchInterval {
revocationMu.RUnlock()
return nil
}
revocationMu.RUnlock()
// 发起 HTTP 请求
client := &http.Client{Timeout: cfg.RequestTimeout}
resp, err := client.Get(cfg.RemoteURL)
if err != nil {
return fmt.Errorf("fetch revocation list: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("fetch revocation list: status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read revocation list: %w", err)
}
var list RevocationList
if err := json.Unmarshal(body, &list); err != nil {
return fmt.Errorf("parse revocation list: %w", err)
}
// 验证签名
if cfg.VerifySignature {
if err := verifyRevocationListSignature(&list); err != nil {
return fmt.Errorf("verify revocation list: %w", err)
}
}
// 更新缓存
return updateRevocationCache(&list)
}
// verifyRevocationListSignature 验证撤销列表签名
func verifyRevocationListSignature(list *RevocationList) error {
if list.Signature == "" {
return fmt.Errorf("missing signature")
}
// 获取官方公钥
pubKey, err := GetOfficialPublicKey()
if err != nil {
return fmt.Errorf("get public key: %w", err)
}
// 构造待签名数据(不含签名字段)
signData := struct {
Version int `json:"version"`
UpdatedAt int64 `json:"updated_at"`
Entries []RevocationEntry `json:"entries"`
}{
Version: list.Version,
UpdatedAt: list.UpdatedAt,
Entries: list.Entries,
}
data, err := json.Marshal(signData)
if err != nil {
return fmt.Errorf("marshal sign data: %w", err)
}
return VerifyBase64(pubKey, data, list.Signature)
}
// updateRevocationCache 更新撤销缓存
func updateRevocationCache(list *RevocationList) error {
revocationMu.Lock()
defer revocationMu.Unlock()
// 检查版本号,防止回滚攻击
if list.Version < currentListVersion {
return fmt.Errorf("revocation list version rollback: %d < %d", list.Version, currentListVersion)
}
// 重建缓存:先加载内置列表,再合并远程列表
newCache := make(map[string][]RevocationEntry)
for _, entry := range builtinRevocations {
newCache[entry.PluginName] = append(newCache[entry.PluginName], entry)
}
for _, entry := range list.Entries {
newCache[entry.PluginName] = append(newCache[entry.PluginName], entry)
}
revocationCache = newCache
currentListVersion = list.Version
lastFetchTime = time.Now()
log.Printf("[Revocation] Updated to version %d with %d entries", list.Version, len(list.Entries))
return nil
}
// StartRevocationRefresher 启动后台刷新协程
func StartRevocationRefresher(stopCh <-chan struct{}) {
cfg := GetRevocationConfig()
if cfg.RemoteURL == "" {
return
}
// 立即执行一次
if err := FetchRemoteRevocationList(); err != nil {
log.Printf("[Revocation] Initial fetch failed: %v", err)
}
ticker := time.NewTicker(cfg.FetchInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := FetchRemoteRevocationList(); err != nil {
log.Printf("[Revocation] Refresh failed: %v", err)
}
case <-stopCh:
log.Printf("[Revocation] Refresher stopped")
return
}
}
}
// GetRevocationListVersion 获取当前撤销列表版本
func GetRevocationListVersion() int {
revocationMu.RLock()
defer revocationMu.RUnlock()
return currentListVersion
}