modify: 授权

This commit is contained in:
2026-09-21 14:03:47 +08:00
parent 66e82c02e7
commit 8a3dc8036c
19 changed files with 2826 additions and 30 deletions
+224
View File
@@ -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())