package service import ( "context" "crypto" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/base64" "encoding/binary" "encoding/pem" "fmt" "strconv" "strings" "sync" "testing" pkgRedis "carrotskin/pkg/redis" miniredis "github.com/alicebob/miniredis/v2" goRedis "github.com/redis/go-redis/v9" "go.uber.org/zap" ) // newTestSignatureService 构造一个基于 miniredis 的 SignatureService,用于测试。 // 返回服务实例与清理函数。 func newTestSignatureService(t *testing.T) (*SignatureService, *miniredis.Miniredis, func()) { t.Helper() mr, err := miniredis.Run() if err != nil { t.Fatalf("启动 miniredis 失败: %v", err) } rdb := goRedis.NewClient(&goRedis.Options{Addr: mr.Addr()}) client := &pkgRedis.Client{Client: rdb} svc := NewSignatureService(nil, client, zap.NewNop()) cleanup := func() { _ = rdb.Close() mr.Close() } return svc, mr, cleanup } // TestSignatureService_SignString_Deterministic 验证 SHA1withRSA 签名的确定性: // 相同 data + 相同根密钥应产生完全相同的签名(PKCS#1v1.5 非随机化)。 // 这也是本次重构(缓存已解析私钥)后输出与重构前一致的核心保证。 func TestSignatureService_SignString_Deterministic(t *testing.T) { svc, _, cleanup := newTestSignatureService(t) defer cleanup() ctx := context.Background() // 首次签名会生成并缓存根密钥 sig1, err := svc.SignStringWithSHA1withRSA(ctx, "test-data-123") if err != nil { t.Fatalf("首次签名失败: %v", err) } // 第二次签名应命中缓存的已解析私钥,输出必须与首次完全一致 sig2, err := svc.SignStringWithSHA1withRSA(ctx, "test-data-123") if err != nil { t.Fatalf("第二次签名失败: %v", err) } if sig1 != sig2 { t.Fatalf("签名不确定:sig1=%q sig2=%q", sig1, sig2) } // 不同数据应产生不同签名 sig3, err := svc.SignStringWithSHA1withRSA(ctx, "different-data") if err != nil { t.Fatalf("不同数据签名失败: %v", err) } if sig1 == sig3 { t.Fatal("不同数据的签名不应相同") } } // TestSignatureService_SignString_MatchesReference 验证重构后的签名与 // 重构前的参考实现(直接从 Redis 读取私钥 PEM 并解析、逐次签名)产生相同字节序列。 // 这是签名一致性的回归测试:只要 data 与根私钥相同,新旧实现输出一致。 func TestSignatureService_SignString_MatchesReference(t *testing.T) { svc, mr, cleanup := newTestSignatureService(t) defer cleanup() ctx := context.Background() // 触发根密钥生成并写入 miniredis if _, err := svc.SignStringWithSHA1withRSA(ctx, "consistency-check"); err != nil { t.Fatalf("触发根密钥生成失败: %v", err) } // 从 miniredis 读取根私钥 PEM,模拟重构前每次签名都重新解析的逻辑 privPEM, err := mr.Get(PrivateKeyRedisKey) if err != nil { t.Fatalf("读取根私钥失败: %v", err) } cases := []string{ "", "a", strings.Repeat("x", 1024), "eyJ0aW1lc3RhbXAiOjE3MDAwMDAwMDAwMDAsInByb2ZpbGVJZCI6ImFiYzEyIiwicHJvZmlsZU5hbWUiOiJUZXN0In0=", } for _, data := range cases { // 重构后实现 got, err := svc.SignStringWithSHA1withRSA(ctx, data) if err != nil { t.Fatalf("SignStringWithSHA1withRSA(%q) 失败: %v", data, err) } // 参考实现(重构前逻辑):pem.Decode + ParsePKCS1PrivateKey + SignPKCS1v15 block, _ := pem.Decode([]byte(privPEM)) if block == nil { t.Fatalf("参考实现解析 PEM 失败") } priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { t.Fatalf("参考实现解析 RSA 私钥失败: %v", err) } hashed := sha1.Sum([]byte(data)) sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA1, hashed[:]) if err != nil { t.Fatalf("参考实现签名失败: %v", err) } want := base64.StdEncoding.EncodeToString(sig) if got != want { t.Fatalf("签名与参考实现不一致 data=%q\n got=%q\nwant=%q", data, got, want) } } } // TestSignatureService_RootKeyCachedAcrossReloads 验证: // 同一进程内多次 GetOrCreateYggdrasilKeyPair 返回同一已解析私钥实例(缓存生效), // 且不重复访问 Redis。 func TestSignatureService_RootKeyCachedAcrossReloads(t *testing.T) { svc, mr, cleanup := newTestSignatureService(t) defer cleanup() ctx := context.Background() // 首次生成 pub1, priv1, err := svc.GetOrCreateYggdrasilKeyPair(ctx) if err != nil { t.Fatalf("首次 GetOrCreate 失败: %v", err) } // 第二次应命中缓存,返回同一私钥指针 pub2, priv2, err := svc.GetOrCreateYggdrasilKeyPair(ctx) if err != nil { t.Fatalf("第二次 GetOrCreate 失败: %v", err) } if priv1 != priv2 { t.Fatal("缓存未命中:返回了不同的私钥实例") } if pub1 != pub2 { t.Fatal("公钥 PEM 不一致") } // Redis 仅应被写入一次(首次生成);记录当前 key 数量,再次调用不应增加 keysBefore := len(mr.Keys()) _, _, _ = svc.GetOrCreateYggdrasilKeyPair(ctx) if got := len(mr.Keys()); got != keysBefore { t.Fatalf("缓存命中后仍写入 Redis:keys before=%d after=%d", keysBefore, got) } } // TestSignatureService_NewKeyPair_SignatureVerifies 验证 NewKeyPair 生成的 // publicKeySignature / publicKeySignatureV2 能用根公钥正确验签, // 且消息构造与重构前格式一致(PEM公钥+时间戳;DER公钥+时间戳)。 func TestSignatureService_NewKeyPair_SignatureVerifies(t *testing.T) { svc, mr, cleanup := newTestSignatureService(t) defer cleanup() ctx := context.Background() kp, err := svc.NewKeyPair(ctx) if err != nil { t.Fatalf("NewKeyPair 失败: %v", err) } // 取根公钥用于验签 rootPubPEM, err := mr.Get(PublicKeyRedisKey) if err != nil { t.Fatalf("读取根公钥失败: %v", err) } block, _ := pem.Decode([]byte(rootPubPEM)) if block == nil { t.Fatal("解析根公钥 PEM 失败") } rootPub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { t.Fatalf("解析根公钥失败: %v", err) } rsaRootPub, ok := rootPub.(*rsa.PublicKey) if !ok { t.Fatal("根公钥不是 RSA 公钥") } expiresAtMillis := kp.Expiration.UnixMilli() // V1 消息:重构前为 string(publicKeyPEM) + strconv.FormatInt(expiresAtMillis, 10) // 重构后用 append 拼接,字节序列应完全一致 pubPEM := []byte(kp.PublicKey) v1Message := make([]byte, 0, len(pubPEM)+20) v1Message = append(v1Message, pubPEM...) v1Message = strconv.AppendInt(v1Message, expiresAtMillis, 10) sig1, err := base64.StdEncoding.DecodeString(kp.PublicKeySignature) if err != nil { t.Fatalf("解码 publicKeySignature 失败: %v", err) } hashed1 := sha1.Sum(v1Message) if err := rsa.VerifyPKCS1v15(rsaRootPub, crypto.SHA1, hashed1[:], sig1); err != nil { t.Fatalf("V1 签名验签失败: %v", err) } // V2 消息:重构前为 timestamp(8 BE) + 玩家公钥 DER // 玩家公钥 DER 来自 kp.PublicKey 的 PEM 解码 playerBlock, _ := pem.Decode([]byte(kp.PublicKey)) if playerBlock == nil { t.Fatal("解析玩家公钥 PEM 失败") } playerDER := playerBlock.Bytes // PKIX 编码的 SubjectPublicKeyInfo DER v2Message := make([]byte, 8+len(playerDER)) binary.BigEndian.PutUint64(v2Message[0:8], uint64(expiresAtMillis)) copy(v2Message[8:], playerDER) sig2, err := base64.StdEncoding.DecodeString(kp.PublicKeySignatureV2) if err != nil { t.Fatalf("解码 publicKeySignatureV2 失败: %v", err) } hashed2 := sha1.Sum(v2Message) if err := rsa.VerifyPKCS1v15(rsaRootPub, crypto.SHA1, hashed2[:], sig2); err != nil { t.Fatalf("V2 签名验签失败: %v", err) } } // TestSignatureService_NewKeyPair_V1MessageBytes 验证 V1 消息字节构造与重构前完全一致。 // 重构前:[]byte(string(publicKeyPEM) + strconv.FormatInt(expiresAtMillis, 10)) // 重构后:append(publicKeyPEM...); strconv.AppendInt(msg, expiresAtMillis, 10) func TestSignatureService_NewKeyPair_V1MessageBytes(t *testing.T) { // 构造一个固定的 PEM 公钥与时间戳,对比两种构造方式的字节 pubPEM := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: []byte("dummy-der-bytes"), }) expiresAtMillis := int64(1700000000000) oldStyle := []byte(string(pubPEM) + strconv.FormatInt(expiresAtMillis, 10)) newStyle := make([]byte, 0, len(pubPEM)+20) newStyle = append(newStyle, pubPEM...) newStyle = strconv.AppendInt(newStyle, expiresAtMillis, 10) if string(oldStyle) != string(newStyle) { t.Fatalf("V1 消息字节不一致\n old=%q\n new=%q", oldStyle, newStyle) } } // TestSignatureService_GetOrCreateYggdrasilKeyPair_LoadsFromRedis 验证: // 当进程内缓存为空(新实例)但 Redis 中已有有效密钥时,能正确加载而非重新生成。 // 使用同一 miniredis 实例,避免新旧实例切换时数据丢失。 func TestSignatureService_GetOrCreateYggdrasilKeyPair_LoadsFromRedis(t *testing.T) { mr, err := miniredis.Run() if err != nil { t.Fatalf("启动 miniredis 失败: %v", err) } t.Cleanup(mr.Close) rdb := goRedis.NewClient(&goRedis.Options{Addr: mr.Addr()}) t.Cleanup(func() { _ = rdb.Close() }) client := &pkgRedis.Client{Client: rdb} ctx := context.Background() // 实例1 生成根密钥(写入 miniredis) svc1 := NewSignatureService(nil, client, zap.NewNop()) pub1, priv1, err := svc1.GetOrCreateYggdrasilKeyPair(ctx) if err != nil { t.Fatalf("实例1生成失败: %v", err) } // 新建实例2(缓存为空),但 Redis 中已有密钥 svc2 := NewSignatureService(nil, client, zap.NewNop()) pub2, priv2, err := svc2.GetOrCreateYggdrasilKeyPair(ctx) if err != nil { t.Fatalf("实例2加载失败: %v", err) } if pub1 != pub2 { t.Fatal("从 Redis 加载的公钥 PEM 不一致") } // 加载的私钥应与原始私钥在数学上等价(N 相同) if priv1.N.Cmp(priv2.N) != 0 { t.Fatal("从 Redis 加载的私钥与原始私钥不匹配") } } // TestSignatureService_SignConcurrent 验证签名缓存层在并发下的正确性(无数据竞争)。 func TestSignatureService_SignConcurrent(t *testing.T) { svc, _, cleanup := newTestSignatureService(t) defer cleanup() ctx := context.Background() // 先生成根密钥 seed, err := svc.SignStringWithSHA1withRSA(ctx, "seed") if err != nil { t.Fatalf("种子签名失败: %v", err) } const n = 32 var wg sync.WaitGroup errs := make(chan error, n) sigs := make(chan string, n) wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() sig, err := svc.SignStringWithSHA1withRSA(ctx, "seed") if err != nil { errs <- err return } sigs <- sig }() } wg.Wait() close(errs) close(sigs) for err := range errs { t.Fatalf("并发签名出错: %v", err) } for sig := range sigs { if sig != seed { t.Fatalf("并发签名结果与预期不一致:got=%q want=%q", sig, seed) } } } // TestSignatureService_KeyPairFieldsPopulated 验证 NewKeyPair 返回的 KeyPair 字段完整。 func TestSignatureService_KeyPairFieldsPopulated(t *testing.T) { svc, _, cleanup := newTestSignatureService(t) defer cleanup() ctx := context.Background() kp, err := svc.NewKeyPair(ctx) if err != nil { t.Fatalf("NewKeyPair 失败: %v", err) } checks := []struct { name string got string }{ {"PrivateKey", kp.PrivateKey}, {"PublicKey", kp.PublicKey}, {"PublicKeySignature", kp.PublicKeySignature}, {"PublicKeySignatureV2", kp.PublicKeySignatureV2}, {"YggdrasilPublicKey", kp.YggdrasilPublicKey}, } for _, c := range checks { if c.got == "" { t.Errorf("KeyPair.%s 为空", c.name) } } if kp.Expiration.IsZero() || kp.Refresh.IsZero() { t.Error("KeyPair.Expiration/Refresh 未设置") } if !kp.Refresh.Before(kp.Expiration) { t.Error("Refresh 应早于 Expiration") } } // 确保引用的包被使用(避免未使用导入在编辑后报错) var _ = fmt.Sprintf // BenchmarkSignStringWithSHA1withRSA 衡量缓存命中路径的签名性能(无 PEM 解析、无 Redis 往返)。 func BenchmarkSignStringWithSHA1withRSA(b *testing.B) { mr, err := miniredis.Run() if err != nil { b.Fatalf("启动 miniredis 失败: %v", err) } defer mr.Close() rdb := goRedis.NewClient(&goRedis.Options{Addr: mr.Addr()}) defer func() { _ = rdb.Close() }() client := &pkgRedis.Client{Client: rdb} ctx := context.Background() svc := NewSignatureService(nil, client, zap.NewNop()) // 预热:生成根密钥并填充缓存 if _, err := svc.SignStringWithSHA1withRSA(ctx, "warmup"); err != nil { b.Fatalf("预热失败: %v", err) } data := strings.Repeat("x", 256) b.ResetTimer() for i := 0; i < b.N; i++ { if _, err := svc.SignStringWithSHA1withRSA(ctx, data); err != nil { b.Fatalf("签名失败: %v", err) } } }