modify: 授权
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""构建期授权守护测试:公钥注入 + 私钥泄漏守卫。
|
||||
|
||||
对齐 `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())
|
||||
Reference in New Issue
Block a user