2025-11-28 23:30:49 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"crypto"
|
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
|
"crypto/rsa"
|
|
|
|
|
|
"crypto/sha1"
|
|
|
|
|
|
"crypto/x509"
|
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
|
"encoding/pem"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strconv"
|
2026-07-09 17:18:26 +08:00
|
|
|
|
"sync"
|
2025-11-28 23:30:49 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-06-15 16:52:20 +08:00
|
|
|
|
"carrotskin/internal/model"
|
|
|
|
|
|
"carrotskin/internal/repository"
|
|
|
|
|
|
"carrotskin/pkg/redis"
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
"go.uber.org/zap"
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 常量定义
|
|
|
|
|
|
const (
|
2025-12-02 22:52:33 +08:00
|
|
|
|
KeySize = 4096
|
|
|
|
|
|
ExpirationDays = 90
|
|
|
|
|
|
RefreshDays = 60
|
|
|
|
|
|
PublicKeyRedisKey = "yggdrasil:public_key"
|
|
|
|
|
|
PrivateKeyRedisKey = "yggdrasil:private_key"
|
|
|
|
|
|
KeyExpirationRedisKey = "yggdrasil:key_expiration"
|
|
|
|
|
|
RedisTTL = 0 // 永不过期,由应用程序管理过期时间
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// SignatureService 签名服务(导出以便依赖注入)
|
|
|
|
|
|
type SignatureService struct {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
profileRepo repository.ProfileRepository
|
|
|
|
|
|
redis *redis.Client
|
2025-11-28 23:30:49 +08:00
|
|
|
|
logger *zap.Logger
|
2026-07-09 17:18:26 +08:00
|
|
|
|
|
|
|
|
|
|
// 缓存已解析的根密钥对,避免每次签名都重复 PEM 解析(PKCS1 解析开销较大)
|
|
|
|
|
|
rootKeyMu sync.RWMutex
|
|
|
|
|
|
rootPrivateKey *rsa.PrivateKey
|
|
|
|
|
|
rootPublicKeyPEM string
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// NewSignatureService 创建SignatureService实例
|
|
|
|
|
|
func NewSignatureService(
|
|
|
|
|
|
profileRepo repository.ProfileRepository,
|
|
|
|
|
|
redisClient *redis.Client,
|
|
|
|
|
|
logger *zap.Logger,
|
2025-12-03 15:27:12 +08:00
|
|
|
|
) *SignatureService {
|
|
|
|
|
|
return &SignatureService{
|
2025-12-02 22:52:33 +08:00
|
|
|
|
profileRepo: profileRepo,
|
|
|
|
|
|
redis: redisClient,
|
2025-11-28 23:30:49 +08:00
|
|
|
|
logger: logger,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// NewKeyPair 生成新的RSA密钥对
|
2026-06-15 16:40:36 +08:00
|
|
|
|
func (s *SignatureService) NewKeyPair(ctx context.Context) (*model.KeyPair, error) {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
return nil, fmt.Errorf("生成RSA密钥对失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// 获取公钥
|
|
|
|
|
|
publicKey := &privateKey.PublicKey
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// PEM编码私钥
|
|
|
|
|
|
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
|
|
|
|
|
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
|
|
|
|
Type: "RSA PRIVATE KEY",
|
|
|
|
|
|
Bytes: privateKeyBytes,
|
|
|
|
|
|
})
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// PEM编码公钥
|
|
|
|
|
|
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
return nil, fmt.Errorf("编码公钥失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
|
|
|
|
Type: "PUBLIC KEY",
|
|
|
|
|
|
Bytes: publicKeyBytes,
|
|
|
|
|
|
})
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// 计算时间
|
|
|
|
|
|
now := time.Now().UTC()
|
|
|
|
|
|
expiration := now.AddDate(0, 0, ExpirationDays)
|
|
|
|
|
|
refresh := now.AddDate(0, 0, RefreshDays)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// 获取Yggdrasil根密钥并签名公钥
|
2026-06-15 16:40:36 +08:00
|
|
|
|
yggPublicKey, yggPrivateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
return nil, fmt.Errorf("获取Yggdrasil根密钥失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// 构造签名消息(PEM 公钥 + 过期时间戳)
|
2025-12-02 22:52:33 +08:00
|
|
|
|
expiresAtMillis := expiration.UnixMilli()
|
2026-07-09 17:18:26 +08:00
|
|
|
|
message := make([]byte, 0, len(publicKeyPEM)+20)
|
|
|
|
|
|
message = append(message, publicKeyPEM...)
|
|
|
|
|
|
message = strconv.AppendInt(message, expiresAtMillis, 10)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// 使用SHA1withRSA签名
|
2026-07-09 17:18:26 +08:00
|
|
|
|
publicKeySignature, err := signWithRootKey(yggPrivateKey, message)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
return nil, fmt.Errorf("签名失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// V2签名:timestamp (8 bytes, big endian) + publicKey (DER)
|
2026-07-09 17:18:26 +08:00
|
|
|
|
messageV2 := make([]byte, 8+len(publicKeyBytes))
|
2025-12-02 22:52:33 +08:00
|
|
|
|
binary.BigEndian.PutUint64(messageV2[0:8], uint64(expiresAtMillis))
|
2026-07-09 17:18:26 +08:00
|
|
|
|
copy(messageV2[8:], publicKeyBytes)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
publicKeySignatureV2, err := signWithRootKey(yggPrivateKey, messageV2)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
return nil, fmt.Errorf("V2签名失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
return &model.KeyPair{
|
|
|
|
|
|
PrivateKey: string(privateKeyPEM),
|
|
|
|
|
|
PublicKey: string(publicKeyPEM),
|
2026-07-09 17:18:26 +08:00
|
|
|
|
PublicKeySignature: base64.StdEncoding.EncodeToString(publicKeySignature),
|
|
|
|
|
|
PublicKeySignatureV2: base64.StdEncoding.EncodeToString(publicKeySignatureV2),
|
2025-12-02 22:52:33 +08:00
|
|
|
|
YggdrasilPublicKey: yggPublicKey,
|
|
|
|
|
|
Expiration: expiration,
|
|
|
|
|
|
Refresh: refresh,
|
|
|
|
|
|
}, nil
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// signWithRootKey 使用根密钥对消息做 SHA1withRSA 签名
|
|
|
|
|
|
func signWithRootKey(privateKey *rsa.PrivateKey, message []byte) ([]byte, error) {
|
|
|
|
|
|
hashed := sha1.Sum(message)
|
|
|
|
|
|
return rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hashed[:])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// GetOrCreateYggdrasilKeyPair 获取或创建Yggdrasil根密钥对
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// 优先使用进程内缓存的已解析私钥;缓存未命中时从Redis加载并解析,再缓存。
|
2026-06-15 16:40:36 +08:00
|
|
|
|
func (s *SignatureService) GetOrCreateYggdrasilKeyPair(ctx context.Context) (string, *rsa.PrivateKey, error) {
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// 1. 读缓存(快路径)
|
|
|
|
|
|
s.rootKeyMu.RLock()
|
|
|
|
|
|
if s.rootPrivateKey != nil && s.isRootKeyValid() {
|
|
|
|
|
|
pub := s.rootPublicKeyPEM
|
|
|
|
|
|
priv := s.rootPrivateKey
|
|
|
|
|
|
s.rootKeyMu.RUnlock()
|
|
|
|
|
|
return pub, priv, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
s.rootKeyMu.RUnlock()
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 缓存未命中,加锁准备从 Redis 加载/生成
|
|
|
|
|
|
s.rootKeyMu.Lock()
|
|
|
|
|
|
defer s.rootKeyMu.Unlock()
|
|
|
|
|
|
|
|
|
|
|
|
// 双重检查:可能在等待锁期间已被其他 goroutine 填充
|
|
|
|
|
|
if s.rootPrivateKey != nil && s.isRootKeyValidLocked() {
|
|
|
|
|
|
return s.rootPublicKeyPEM, s.rootPrivateKey, nil
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// 尝试从Redis获取密钥(MGet 一次往返拿三个字段,减少网络往返)
|
|
|
|
|
|
pub, priv, err := s.loadRootKeyFromRedis(ctx)
|
|
|
|
|
|
if err == nil && priv != nil {
|
|
|
|
|
|
s.rootPrivateKey = priv
|
|
|
|
|
|
s.rootPublicKeyPEM = pub
|
|
|
|
|
|
s.logger.Info("从Redis加载Yggdrasil根密钥")
|
|
|
|
|
|
return pub, priv, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Redis 没有,生成新的根密钥对
|
2025-12-02 22:52:33 +08:00
|
|
|
|
s.logger.Info("生成新的Yggdrasil根密钥对")
|
|
|
|
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", nil, fmt.Errorf("生成RSA密钥失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// PEM编码私钥
|
2025-11-28 23:30:49 +08:00
|
|
|
|
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
|
2025-12-02 22:52:33 +08:00
|
|
|
|
privateKeyPEM := string(pem.EncodeToMemory(&pem.Block{
|
|
|
|
|
|
Type: "RSA PRIVATE KEY",
|
2025-11-28 23:30:49 +08:00
|
|
|
|
Bytes: privateKeyBytes,
|
2025-12-02 22:52:33 +08:00
|
|
|
|
}))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// PEM编码公钥
|
|
|
|
|
|
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", nil, fmt.Errorf("编码公钥失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2026-07-09 17:18:26 +08:00
|
|
|
|
publicKeyPEM := string(pem.EncodeToMemory(&pem.Block{
|
2025-12-02 22:52:33 +08:00
|
|
|
|
Type: "PUBLIC KEY",
|
2025-11-28 23:30:49 +08:00
|
|
|
|
Bytes: publicKeyBytes,
|
2025-12-02 22:52:33 +08:00
|
|
|
|
}))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// 计算过期时间(90天)
|
|
|
|
|
|
expiration := time.Now().AddDate(0, 0, ExpirationDays)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// 保存到Redis
|
|
|
|
|
|
if err := s.redis.Set(ctx, PublicKeyRedisKey, publicKeyPEM, RedisTTL); err != nil {
|
|
|
|
|
|
s.logger.Warn("保存公钥到Redis失败", zap.Error(err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := s.redis.Set(ctx, PrivateKeyRedisKey, privateKeyPEM, RedisTTL); err != nil {
|
|
|
|
|
|
s.logger.Warn("保存私钥到Redis失败", zap.Error(err))
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := s.redis.Set(ctx, KeyExpirationRedisKey, expiration.Format(time.RFC3339), RedisTTL); err != nil {
|
|
|
|
|
|
s.logger.Warn("保存密钥过期时间到Redis失败", zap.Error(err))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// 缓存已解析的私钥与公钥PEM
|
|
|
|
|
|
s.rootPrivateKey = privateKey
|
|
|
|
|
|
s.rootPublicKeyPEM = publicKeyPEM
|
2025-12-02 22:52:33 +08:00
|
|
|
|
return publicKeyPEM, privateKey, nil
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// loadRootKeyFromRedis 从Redis加载根密钥,使用 MGet 单次往返获取三字段
|
|
|
|
|
|
func (s *SignatureService) loadRootKeyFromRedis(ctx context.Context) (string, *rsa.PrivateKey, error) {
|
|
|
|
|
|
results, err := s.redis.MGet(ctx, PublicKeyRedisKey, PrivateKeyRedisKey, KeyExpirationRedisKey).Result()
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2026-07-09 17:18:26 +08:00
|
|
|
|
return "", nil, err
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2026-07-09 17:18:26 +08:00
|
|
|
|
if len(results) < 3 {
|
|
|
|
|
|
return "", nil, fmt.Errorf("MGet 返回字段数不足")
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
publicKeyPEM, _ := results[0].(string)
|
|
|
|
|
|
privateKeyPEM, _ := results[1].(string)
|
|
|
|
|
|
expStr, _ := results[2].(string)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
if publicKeyPEM == "" || privateKeyPEM == "" || expStr == "" {
|
|
|
|
|
|
return "", nil, fmt.Errorf("Redis中密钥字段不完整")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
expTime, err := time.Parse(time.RFC3339, expStr)
|
|
|
|
|
|
if err != nil || !time.Now().Before(expTime) {
|
|
|
|
|
|
return "", nil, fmt.Errorf("密钥已过期或过期时间解析失败")
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
block, _ := pem.Decode([]byte(privateKeyPEM))
|
|
|
|
|
|
if block == nil {
|
2026-07-09 17:18:26 +08:00
|
|
|
|
return "", nil, fmt.Errorf("解析PEM私钥失败")
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2025-12-02 22:52:33 +08:00
|
|
|
|
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2026-07-09 17:18:26 +08:00
|
|
|
|
return "", nil, fmt.Errorf("解析RSA私钥失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2026-07-09 17:18:26 +08:00
|
|
|
|
return publicKeyPEM, privateKey, nil
|
|
|
|
|
|
}
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// isRootKeyValid 在持有读锁时判断缓存密钥是否过期。
|
|
|
|
|
|
// 注意:缓存的私钥本身不带过期时间,过期信息存于 Redis(由 loadRootKeyFromRedis 校验)。
|
|
|
|
|
|
// 一旦加载成功即认为在进程生命周期内有效,由 Redis 过期触发重新生成。
|
|
|
|
|
|
func (s *SignatureService) isRootKeyValid() bool {
|
|
|
|
|
|
return s.isRootKeyValidLocked()
|
|
|
|
|
|
}
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
func (s *SignatureService) isRootKeyValidLocked() bool {
|
|
|
|
|
|
return s.rootPrivateKey != nil && s.rootPublicKeyPEM != ""
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// GetPublicKeyFromRedis 从Redis获取公钥
|
|
|
|
|
|
func (s *SignatureService) GetPublicKeyFromRedis(ctx context.Context) (string, error) {
|
|
|
|
|
|
publicKey, err := s.redis.Get(ctx, PublicKeyRedisKey)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("从Redis获取公钥失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if publicKey == "" {
|
|
|
|
|
|
// 如果Redis中没有,创建新的密钥对
|
|
|
|
|
|
publicKey, _, err = s.GetOrCreateYggdrasilKeyPair(ctx)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("创建新密钥对失败: %w", err)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-09 17:18:26 +08:00
|
|
|
|
return publicKey, nil
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
2026-02-23 13:26:53 +08:00
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
// SignStringWithSHA1withRSA 使用SHA1withRSA签名字符串
|
|
|
|
|
|
// 使用缓存的已解析根私钥;若缓存为空则触发加载/生成。
|
|
|
|
|
|
func (s *SignatureService) SignStringWithSHA1withRSA(ctx context.Context, data string) (string, error) {
|
|
|
|
|
|
_, privateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx)
|
2026-02-23 13:26:53 +08:00
|
|
|
|
if err != nil {
|
2026-07-09 17:18:26 +08:00
|
|
|
|
return "", fmt.Errorf("获取签名私钥失败: %w", err)
|
2026-02-23 13:26:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 17:18:26 +08:00
|
|
|
|
signature, err := signWithRootKey(privateKey, []byte(data))
|
2026-02-23 13:26:53 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", fmt.Errorf("签名失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return base64.StdEncoding.EncodeToString(signature), nil
|
|
|
|
|
|
}
|