Initial commit: docker-compose-updater
Build and Push / build (push) Failing after 13m20s

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:
ilovintit
2026-06-08 15:16:46 +08:00
commit cea9b941cf
21 changed files with 1874 additions and 0 deletions
+75
View File
@@ -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: 每个请求的 nonce16 字节)
// 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
}
+39
View File
@@ -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
}
+136
View File
@@ -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 请求生成一个 sessionPhase 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()
}
}
+30
View File
@@ -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)
}