"""构建期授权守护测试:公钥注入 + 私钥泄漏守卫。 对齐 `test_build_plugin.py` 对"插件包不得包含 secret_key"的同类断言 —— **签发私钥绝不能进入部署产物**:它是授权的唯一信任根,一旦随包发布, 任何人都能签发授权,机器码绑定形同虚设。 运行方式:`python test/test_license_build.py`,或 `uv run pytest test/test_license_build.py`。 """ import os import sys import tempfile import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "utils" / "cli" / "src")) from chongming_cli.build import ( # noqa: E402 ALLOW_NO_KEY_ENV, LICENSE_PUBKEY_ENV, _resolve_license_public_key, _set_license_pubkey, _stage_license_guard, ) failures: list[str] = [] def check(name, cond, detail=""): status = "PASS" if cond else "FAIL" print(f"[{status}] {name}" + (f" — {detail}" if detail else "")) if not cond: failures.append(name) def test_pubkey_injection_is_section_scoped(): """公钥只写进 [license] 段,注释与其它段不受影响。""" src = (ROOT / "public" / "config.toml").read_text(encoding="utf-8") out = _set_license_pubkey(src, "AAAABBBB=") body = tomllib.loads(out) check("注入后 TOML 可解析", body["license"]["public_key"] == "AAAABBBB=", str(body["license"].get("public_key"))) # 只改了 [license] 段内那一行 lic_start = out.index("[license]") prod_start = out.index("[production]") check("公钥位于 [license] 段内", 'public_key = "AAAABBBB="' in out[lic_start:prod_start]) check("[production] 段未被误改", "public_key" not in out[prod_start:]) check("注释保留", "# 机器码授权" in out) check("其它段结构完整", "[production.module_system]" in out) check("[license] 段仍保留 path/min_match", body["license"]["path"] == "license.lic" and body["license"]["min_match"] == 3) diff = [i for i, (a, b) in enumerate(zip(src.splitlines(), out.splitlines())) if a != b] check("仅一行发生变化", len(diff) == 1, f"变化行号={diff}") def test_pubkey_resolution_order(): """公钥解析优先级:环境变量 > 文件环境变量 > 默认文件 license_public.key。 必须在**空的临时目录**里跑:默认回退路径 ``license_public.key`` 是相对 CWD 的, 而厂商在项目根放过这个文件是正常用法(也正是构建取公钥的正规途径), 测试不能依赖它"不存在"。 """ tmp = Path(tempfile.mkdtemp()) key_file = tmp / "pk.txt" key_file.write_text("FROMFILE", encoding="utf-8") old_cwd = os.getcwd() old_env = dict(os.environ) try: os.chdir(tmp) # 空目录:既无 license_public.key 也无其它干扰 for var in (LICENSE_PUBKEY_ENV, "CHONGMING_LICENSE_PUBKEY_FILE"): os.environ.pop(var, None) check("三者都无时返回空", _resolve_license_public_key() == "") # 默认文件回退(构建按此取公钥,之前漏测) (tmp / "license_public.key").write_text("FROMDEFAULT", encoding="utf-8") check("回退到默认文件 license_public.key", _resolve_license_public_key() == "FROMDEFAULT") os.environ["CHONGMING_LICENSE_PUBKEY_FILE"] = str(key_file) check("文件环境变量优先于默认文件", _resolve_license_public_key() == "FROMFILE") os.environ[LICENSE_PUBKEY_ENV] = "FROMENV" check("环境变量优先于文件", _resolve_license_public_key() == "FROMENV") finally: os.chdir(old_cwd) os.environ.clear() os.environ.update(old_env) def test_guard_detects_private_key(): """守护能检出私钥(按内容,与扩展名无关,含跨块边界)。""" tmp = Path(tempfile.mkdtemp()) build = tmp / "build" (build / "sub").mkdir(parents=True) old_cwd = os.getcwd() try: os.chdir(tmp) (build / "ok.txt").write_text("正常内容\n", encoding="utf-8") check("干净产物通过", _stage_license_guard() == "未发现私钥泄漏") # 证书类 .pem 是合法的构建内容,不应被误判 (build / "cert.pem").write_text( "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n", encoding="utf-8" ) check("合法证书 .pem 不误判", _stage_license_guard() == "未发现私钥泄漏") # 私钥改名藏进普通文件 -> 按内容仍能检出 leak = build / "sub" / "innocent.txt" leak.write_text("-----BEGIN PRIVATE KEY-----\nMIIEvQ\n", encoding="utf-8") try: _stage_license_guard() check("按内容检出改名的私钥", False, "未检出") except RuntimeError as exc: check("按内容检出改名的私钥", "innocent.txt" in str(exc)) leak.unlink() # 跨 1MB 块边界的私钥 (build / "big.bin").write_bytes( b"x" * (1 << 20) + b"-----BEGIN RSA PRIVATE KEY-----" + b"y" * (1 << 20) ) try: _stage_license_guard() check("检出跨块边界的私钥", False, "未检出") except RuntimeError as exc: check("检出跨块边界的私钥", "big.bin" in str(exc)) (build / "big.bin").unlink() # 文件名叫 license_private* 的一律拦下 (build / "license_private.pem").write_text("(truncated)", encoding="utf-8") try: _stage_license_guard() check("按文件名拦下授权私钥文件", False, "未检出") except RuntimeError as exc: check("按文件名拦下授权私钥文件", "license_private" in str(exc)) finally: os.chdir(old_cwd) def test_build_fails_without_pubkey(): """打包时缺少公钥 -> 构建失败(避免产出拒绝所有插件的砖头包)。""" import chongming_cli.build as build_mod tmp = Path(tempfile.mkdtemp()) old_cwd = os.getcwd() old_env = dict(os.environ) try: # 复刻 build/ 最小结构:_stage_prepare_build_dir 需要 public/ 与 config.toml (tmp / "public" / "utils").mkdir(parents=True) (tmp / "public" / "main.py").write_text("", encoding="utf-8") (tmp / "public" / "server.py").write_text("", encoding="utf-8") (tmp / "public" / "config.toml").write_text( (ROOT / "public" / "config.toml").read_text(encoding="utf-8"), encoding="utf-8" ) os.chdir(tmp) for var in (LICENSE_PUBKEY_ENV, "CHONGMING_LICENSE_PUBKEY_FILE", ALLOW_NO_KEY_ENV): os.environ.pop(var, None) try: build_mod._stage_prepare_build_dir({}, "pyinstaller") # type: ignore[arg-type] check("缺少公钥时构建失败", False, "未抛异常") except RuntimeError as exc: check("缺少公钥时构建失败", LICENSE_PUBKEY_ENV in str(exc)) # 显式放行时才允许无公钥构建 os.environ[ALLOW_NO_KEY_ENV] = "1" build_mod._stage_prepare_build_dir({}, "pyinstaller") # type: ignore[arg-type] built = tomllib.loads((tmp / "build" / "config.toml").read_text(encoding="utf-8")) check("显式放行时构建成功且公钥为空", built["license"]["public_key"] == "") # 提供公钥后应注入到产物配置 os.environ[LICENSE_PUBKEY_ENV] = "INJECTEDKEY=" build_mod._stage_prepare_build_dir({}, "pyinstaller") # type: ignore[arg-type] built = tomllib.loads((tmp / "build" / "config.toml").read_text(encoding="utf-8")) check("公钥被注入产物配置", built["license"]["public_key"] == "INJECTEDKEY=") check("产物配置仍含 min_match", built["license"]["min_match"] == 3) finally: os.chdir(old_cwd) os.environ.clear() os.environ.update(old_env) def test_mbank_signing_is_not_a_trust_root(): """.mbank 验签用随包发布的对称密钥 —— 不能作为授权信任根。 本测试固化这一认知:只要 sqlite_vfs 的 sign 仍是 HMAC,就说明它只能算防篡改, 授权必须依赖非对称签名(chongming_license 的 Ed25519)。 """ import inspect from sqlite_vfs import core as vfs_core src = inspect.getsource(vfs_core.SQLiteVFS.sign) check("确认 .mbank 签名为 HMAC(对称)", "hmac.new" in src, "若已改为非对称,可更新本测试与文档说明") check("确认验证端同样是对称 HMAC", "hmac.new" in inspect.getsource(vfs_core.SQLiteVFS.verify_signature)) def main(): tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] for fn in tests: print(f"\n--- {fn.__name__} ---") fn() print("\n" + "=" * 56) if failures: print(f"❌ {len(failures)} 项未通过: {failures}") return 1 print("✅ 全部通过") return 0 if __name__ == "__main__": sys.exit(main())