Go 项目,包含: - 服务端 updater:两阶段协议,ECDSA 签名验证,AES-GCM 加密 - 发送端 dcu-send:Gitea Action CLI - internal/auth:加解密/签名/会话管理 - internal/docker:Docker CLI 容器查找/拉取/重建 - action/:Gitea Action 定义 - deploy/Dockerfile:多阶段构建 - .gitea/workflows/build.yaml:CI/CD
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
// Package auth 提供加解密、签名验签和会话密钥管理。
|
||||
// 发送端 (dcu-send) 和接收端 (updater) 共用此包。
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// Encrypt 用 AES-256-GCM 加密明文,返回 nonce+ciphertext。
|
||||
// key 必须是 32 字节。
|
||||
func Encrypt(plaintext []byte, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aes new cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aes gcm: %w", err)
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, fmt.Errorf("nonce: %w", err)
|
||||
}
|
||||
|
||||
// GCM 附加认证数据 (AAD) 传空
|
||||
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// Decrypt 用 AES-256-GCM 解密,输入为 nonce+ciphertext。
|
||||
func Decrypt(data []byte, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aes new cipher: %w", err)
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aes gcm: %w", err)
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("aes decrypt: %w", err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// DeriveKey 用 HKDF-SHA256 从 shared_secret 和 context 派生出 AES-256 密钥。
|
||||
// salt: 每个请求的 nonce(16 字节)
|
||||
// info: 协议标识,如 "dcu-updater/v1"
|
||||
func DeriveKey(sharedSecret []byte, salt []byte, info string) []byte {
|
||||
hkdf := hkdf.New(sha256.New, sharedSecret, salt, []byte(info))
|
||||
key := make([]byte, 32) // AES-256
|
||||
if _, err := io.ReadFull(hkdf, key); err != nil {
|
||||
// HKDF 使用 SHA-256,从不会返回错误
|
||||
panic(fmt.Sprintf("hkdf read: %v", err))
|
||||
}
|
||||
return key
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ParseECDSAPrivateKey 解析 PEM 编码的 ECDSA 私钥。
|
||||
func ParseECDSAPrivateKey(pemData []byte) (*ecdsa.PrivateKey, error) {
|
||||
block, _ := pem.Decode(pemData)
|
||||
if block == nil || block.Type != "EC PRIVATE KEY" {
|
||||
return nil, fmt.Errorf("invalid EC private key PEM")
|
||||
}
|
||||
key, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse EC private key: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// ParseECDSAPublicKey 解析 PEM 编码的 ECDSA 公钥。
|
||||
func ParseECDSAPublicKey(pemData []byte) (*ecdsa.PublicKey, error) {
|
||||
block, _ := pem.Decode(pemData)
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
return nil, fmt.Errorf("invalid public key PEM")
|
||||
}
|
||||
key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse public key: %w", err)
|
||||
}
|
||||
|
||||
pubKey, ok := key.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("key is not ECDSA")
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// sessionEntry 存储一次 Phase 1 协商的会话密钥。
|
||||
type sessionEntry struct {
|
||||
key []byte // AES-256 会话密钥
|
||||
expiry time.Time
|
||||
}
|
||||
|
||||
// SessionManager 管理内存中的会话密钥(Nonce → AES Key)。
|
||||
// 每个 Phase 1 请求生成一个 session,Phase 2 消费后删除。
|
||||
type SessionManager struct {
|
||||
mu sync.Mutex
|
||||
store map[string]*sessionEntry
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewSessionManager 创建会话密钥管理器。
|
||||
// ttl: 密钥过期时间(如 30 秒)
|
||||
func NewSessionManager(ttl time.Duration) *SessionManager {
|
||||
sm := &SessionManager{
|
||||
store: make(map[string]*sessionEntry),
|
||||
ttl: ttl,
|
||||
}
|
||||
// 后台协程定期清理过期密钥
|
||||
go sm.cleanupLoop()
|
||||
return sm
|
||||
}
|
||||
|
||||
// GenerateKey 生成 AES-256 会话密钥,用指定的 keyID 存储(通常用 nonce)。
|
||||
func (sm *SessionManager) GenerateKey(keyID string) ([]byte, error) {
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
return nil, fmt.Errorf("generate session key: %w", err)
|
||||
}
|
||||
|
||||
sm.mu.Lock()
|
||||
sm.store[keyID] = &sessionEntry{
|
||||
key: key,
|
||||
expiry: time.Now().Add(sm.ttl),
|
||||
}
|
||||
sm.mu.Unlock()
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// GetKey 获取并删除会话密钥(一次性使用)。
|
||||
// 返回 nil 表示 keyID 不存在或已过期。
|
||||
func (sm *SessionManager) GetKey(keyID string) []byte {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
entry, ok := sm.store[keyID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
delete(sm.store, keyID)
|
||||
|
||||
if time.Now().After(entry.expiry) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return entry.key
|
||||
}
|
||||
|
||||
// cleanupLoop 定期清理过期密钥。
|
||||
func (sm *SessionManager) cleanupLoop() {
|
||||
ticker := time.NewTicker(sm.ttl)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
sm.mu.Lock()
|
||||
now := time.Now()
|
||||
for id, entry := range sm.store {
|
||||
if now.After(entry.expiry) {
|
||||
delete(sm.store, id)
|
||||
}
|
||||
}
|
||||
sm.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// NonceCache 用于防重放攻击的 Nonce 缓存。
|
||||
type NonceCache struct {
|
||||
mu sync.Mutex
|
||||
store map[string]time.Time
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewNonceCache 创建 Nonce 缓存。
|
||||
// ttl: Nonce 有效时间(如 60 秒)
|
||||
func NewNonceCache(ttl time.Duration) *NonceCache {
|
||||
nc := &NonceCache{
|
||||
store: make(map[string]time.Time),
|
||||
ttl: ttl,
|
||||
}
|
||||
go nc.cleanupLoop()
|
||||
return nc
|
||||
}
|
||||
|
||||
// Check 检查并记录 Nonce。返回 true 表示 Nonce 有效(未使用过)。
|
||||
// 返回 false 表示 Nonce 已存在(重放攻击)。
|
||||
func (nc *NonceCache) Check(nonce string) bool {
|
||||
nc.mu.Lock()
|
||||
defer nc.mu.Unlock()
|
||||
|
||||
if _, exists := nc.store[nonce]; exists {
|
||||
return false
|
||||
}
|
||||
|
||||
nc.store[nonce] = time.Now().Add(nc.ttl)
|
||||
return true
|
||||
}
|
||||
|
||||
func (nc *NonceCache) cleanupLoop() {
|
||||
ticker := time.NewTicker(nc.ttl)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
nc.mu.Lock()
|
||||
now := time.Now()
|
||||
for n, expiry := range nc.store {
|
||||
if now.After(expiry) {
|
||||
delete(nc.store, n)
|
||||
}
|
||||
}
|
||||
nc.mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Sign ECDSA P-256 签名,返回 DER 编码的 Base64 签名。
|
||||
func Sign(key *ecdsa.PrivateKey, data []byte) (string, error) {
|
||||
hash := sha256.Sum256(data)
|
||||
sig, err := ecdsa.SignASN1(rand.Reader, key, hash[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ecdsa sign: %w", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(sig), nil
|
||||
}
|
||||
|
||||
// Verify 验证 ECDSA P-256 签名。
|
||||
// sigBase64 是 DER 编码的 Base64 签名。
|
||||
func Verify(key *ecdsa.PublicKey, data []byte, sigBase64 string) bool {
|
||||
sig, err := base64.StdEncoding.DecodeString(sigBase64)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
hash := sha256.Sum256(data)
|
||||
return ecdsa.VerifyASN1(key, hash[:], sig)
|
||||
}
|
||||
Reference in New Issue
Block a user