Files
2026-07-10 10:24:29 +08:00

183 lines
7.0 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using Xunit;
using XWorld.PublishTool;
using XWorld.Server.Host;
namespace XWorld.Server.Host.Tests
{
/// <summary>
/// 设计§7:服务端加载时签名验证测试(TDD RED→GREEN)。
/// 验证 GameModuleLoader 在配置公钥时执行签名+md5 校验,未配置时跳过(向后兼容)。
/// </summary>
public class GameModuleSignatureTests : IDisposable
{
private readonly string _tempRoot;
private readonly string _gameDir; // {tempRoot}/rps/1/
public GameModuleSignatureTests()
{
_tempRoot = Path.Combine(Path.GetTempPath(), "sig_test_" + Guid.NewGuid().ToString("N"));
_gameDir = Path.Combine(_tempRoot, "rps", "1");
Directory.CreateDirectory(_gameDir);
// 从已构建的 TestGames/rps/1 复制 3 个文件
string src = Path.Combine(TestGames.Root, "rps", "1");
foreach (var name in new[] { "RPS.Server.dll", "RPS.Core.dll", "game.json" })
File.Copy(Path.Combine(src, name), Path.Combine(_gameDir, name), overwrite: true);
}
public void Dispose()
{
if (Directory.Exists(_tempRoot))
{
// Windows 下 ALC 加载的 DLL 可能还被锁住;先做 GC 等待释放,再删除
for (int i = 0; i < 5; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
try { Directory.Delete(_tempRoot, true); }
catch (UnauthorizedAccessException) { /* Windows 文件锁:测试结束后 GC 会最终释放;不影响测试结果 */ }
}
}
// 生成签名 fixture 并写入 files.txt + files.txt.sig
private static (string pubPem, string privPem) GenerateKeyPair()
{
using var rsa = RSA.Create(2048);
return (rsa.ExportRSAPublicKeyPem(), rsa.ExportRSAPrivateKeyPem());
}
private void WriteSignature(string privPem)
{
var fm = new FilesManifest();
foreach (var name in new[] { "RPS.Server.dll", "RPS.Core.dll", "game.json" })
{
string p = Path.Combine(_gameDir, name);
byte[] b = File.ReadAllBytes(p);
fm.Add(name, Md5Util.OfBytes(b), b.Length);
}
File.WriteAllText(Path.Combine(_gameDir, "files.txt"), fm.Render());
File.WriteAllBytes(Path.Combine(_gameDir, "files.txt.sig"),
ManifestSigner.Sign(fm.Digest(), privPem));
}
[Fact]
public void Load_WithValidSignature_Succeeds()
{
var (pubPem, privPem) = GenerateKeyPair();
WriteSignature(privPem);
var module = new GameModuleLoader(pubPem).Load(_tempRoot, "rps", 1);
Assert.NotNull(module);
Assert.Equal("rps", module.GameId);
Assert.Equal(1, module.Version);
var room = module.CreateRoom();
Assert.NotNull(room);
// 卸载 ALC,释放 Windows 文件锁,以便 Dispose() 能清理临时目录
room = null;
module.Unload();
GC.Collect();
GC.WaitForPendingFinalizers();
}
[Fact]
public void Load_TamperedDll_Rejected()
{
var (pubPem, privPem) = GenerateKeyPair();
WriteSignature(privPem);
// 篡改 RPS.Core.dll 的一个字节,md5 随之改变
string coreDll = Path.Combine(_gameDir, "RPS.Core.dll");
byte[] bytes = File.ReadAllBytes(coreDll);
bytes[0] ^= 0xFF; // 翻转首字节
File.WriteAllBytes(coreDll, bytes);
var ex = Assert.ThrowsAny<Exception>(() =>
new GameModuleLoader(pubPem).Load(_tempRoot, "rps", 1));
Assert.Contains("md5", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Load_WrongKey_Rejected()
{
var (_, privPem) = GenerateKeyPair();
WriteSignature(privPem);
// 用另一组密钥的公钥验证 → 签名不符
var (wrongPubPem, _) = GenerateKeyPair();
var ex = Assert.ThrowsAny<Exception>(() =>
new GameModuleLoader(wrongPubPem).Load(_tempRoot, "rps", 1));
Assert.Contains("签名", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Load_MissingSignature_WhenKeyConfigured_Rejected()
{
var (pubPem, _) = GenerateKeyPair();
// 故意不写 files.txt.sig(签名文件缺失)
var ex = Assert.ThrowsAny<Exception>(() =>
new GameModuleLoader(pubPem).Load(_tempRoot, "rps", 1));
Assert.Contains("签名", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Load_NoKeyConfigured_SkipsVerification()
{
// 不配置公钥,使用无签名的原始 TestGames/rps/1 目录
// 向后兼容:无公钥时跳过验证,正常加载
var module = new GameModuleLoader().Load(TestGames.Root, "rps", 1);
Assert.NotNull(module);
Assert.Equal("rps", module.GameId);
var room = module.CreateRoom();
Assert.NotNull(room);
}
[Fact]
public void Load_TamperedGameJson_Rejected()
{
// 构建签名 fixture(与其他测试相同方式)
var (pubPem, privPem) = GenerateKeyPair();
WriteSignature(privPem);
// 篡改 game.json:追加一个空格,改变其 md5,使清单 md5 校验失败
File.AppendAllText(Path.Combine(_gameDir, "game.json"), " ");
Assert.ThrowsAny<Exception>(() =>
new GameModuleLoader(pubPem).Load(_tempRoot, "rps", 1));
}
[Fact]
public void Load_TamperedManifestWithoutResign_Rejected()
{
// 构建签名 fixture
var (pubPem, privPem) = GenerateKeyPair();
WriteSignature(privPem);
// 攻击者篡改 RPS.Core.dll 一字节,并重算其 md5 写回 files.txt
// 但没有私钥无法重签 files.txt.sig → 验签时发现 Digest 与 sig 不符 → 拒绝
string coreDll = Path.Combine(_gameDir, "RPS.Core.dll");
byte[] bytes = File.ReadAllBytes(coreDll);
bytes[0] ^= 0xFF;
File.WriteAllBytes(coreDll, bytes);
// 重算被篡改 DLL 的 md5,重写 files.txtfiles.txt.sig 保持不变)
string newMd5 = Md5Util.OfFile(coreDll);
var fm = FilesManifest.Parse(File.ReadAllText(Path.Combine(_gameDir, "files.txt")));
fm.Entries["RPS.Core.dll"].Md5 = newMd5;
File.WriteAllText(Path.Combine(_gameDir, "files.txt"), fm.Render());
Assert.ThrowsAny<Exception>(() =>
new GameModuleLoader(pubPem).Load(_tempRoot, "rps", 1));
}
}
}