test: 构建开关测试适配授权烘焙(--license 可选、缺公钥不再出砖头包)

prepare 现在把"是否为授权版"烘焙进 chongming/_build_flags.py:
- 默认(不加 --license):断言烘焙 enforce=False,产物不校验、插件照常加载
- --license 缺公钥:仍断言构建失败;ALLOW_NO_KEY=1 放行时断言降级为非授权版
- --license + 公钥:改用合法的 base64 Ed25519 公钥(构建期会校验格式),
  断言注入 config.toml 之外还烘焙了 enforce=True + 公钥 + 指纹,且生成物是
  合法 Python;新增非法公钥应终止构建的用例
- 测试结束恢复构建 venv 内的烘焙标记(不留副作用)
This commit is contained in:
2026-10-14 10:57:14 +08:00
parent 4cdb9c7049
commit 774f44f830
+72 -4
View File
@@ -8,7 +8,10 @@ build 命令开关(--encrypt / --license)冒烟测试
2. 默认(不加密):.mbank 存明文字节码(encrypted=false)、不写 .key(并清掉旧 2. 默认(不加密):.mbank 存明文字节码(encrypted=false)、不写 .key(并清掉旧
的)、无 packer_key 记录,且**不需要密钥即可导入**;加 --encrypt 那条路仍然 的)、无 packer_key 记录,且**不需要密钥即可导入**;加 --encrypt 那条路仍然
加密、仍然写 .key,且用 .key 里的密钥能正常导入 加密、仍然写 .key,且用 .key 里的密钥能正常导入
3. 默认(不授权):prepare 不要求/不注入授权公钥;加 --license 时缺公钥构建失败 3. 授权构建:prepare 把"是否为授权版"烘焙进 chongming/_build_flags.py(编译进
产物、部署侧不可改)—— 不加 --license = 非授权版(不校验、插件照常加载),
加 --license = 授权版(注入并内置公钥、强制校验);--license 缺公钥默认构建
失败,显式放行时降级为非授权版(而不是拒绝所有插件的砖头包);非法公钥报错
运行: uv run python test/test_build_flags.py 运行: uv run python test/test_build_flags.py
""" """
@@ -158,7 +161,7 @@ mod, err = import_from_bank(encrypted_bank, "demo", key=None)
check("加密包缺密钥无法导入(说明 .key 必须随包提供给加载端)", mod is None and isinstance(err, ImportError), str(err)) check("加密包缺密钥无法导入(说明 .key 必须随包提供给加载端)", mod is None and isinstance(err, ImportError), str(err))
# ========== 3. 授权公钥注入 / 默认不注入 ========== # ========== 3. 授权构建:烘焙标记 / 公钥注入 ==========
def make_project(root: Path) -> Path: def make_project(root: Path) -> Path:
"""造一个最小构建现场:public/{main.py,server.py,config.toml}""" """造一个最小构建现场:public/{main.py,server.py,config.toml}"""
(root / "public" / "utils").mkdir(parents=True) (root / "public" / "utils").mkdir(parents=True)
@@ -173,30 +176,90 @@ def make_project(root: Path) -> Path:
return root return root
def valid_pubkey() -> str:
"""一把合法的 base64 裸 32 字节 Ed25519 公钥(构建期会校验格式)。"""
import base64
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
priv = Ed25519PrivateKey.generate()
return base64.b64encode(
priv.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
).decode("ascii")
def flags_path() -> Path:
"""烘焙标记的落盘位置(网关包内;CLI 用同一方式定位)。"""
spec = importlib.util.find_spec("chongming")
assert spec and spec.submodule_search_locations, "构建 venv 未安装 chongming-gateway"
return Path(next(iter(spec.submodule_search_locations))) / "_build_flags.py"
def read_flags() -> str:
return flags_path().read_text(encoding="utf-8")
env_keys = (b.LICENSE_PUBKEY_ENV, b.LICENSE_PUBKEY_FILE_ENV, b.ALLOW_NO_KEY_ENV) env_keys = (b.LICENSE_PUBKEY_ENV, b.LICENSE_PUBKEY_FILE_ENV, b.ALLOW_NO_KEY_ENV)
saved_env = {k: os.environ.pop(k, None) for k in env_keys} # 环境变量不该影响测试结论 saved_env = {k: os.environ.pop(k, None) for k in env_keys} # 环境变量不该影响测试结论
cwd = Path.cwd() cwd = Path.cwd()
proj = make_project(tmp / "proj_default") proj = make_project(tmp / "proj_default")
flags_file = flags_path()
flags_backup = flags_file.read_text(encoding="utf-8") if flags_file.exists() else None
os.chdir(proj) os.chdir(proj)
try: try:
# 默认(不授权):产物烘焙"非授权版"——不校验授权、插件照常加载(不是砖头包)
msg = b._stage_prepare_build_dir(None, "pyinstaller", b.BuildOptions()) msg = b._stage_prepare_build_dir(None, "pyinstaller", b.BuildOptions())
conf = (proj / "build" / "config.toml").read_text(encoding="utf-8") conf = (proj / "build" / "config.toml").read_text(encoding="utf-8")
check("默认(不授权)的 prepare 不报错", msg == "资源准备完成", msg) check("默认(不授权)的 prepare 不报错", msg == "资源准备完成", msg)
check("默认不注入公钥(模板 public_key 保持空)", 'public_key = ""' in conf) check("默认不注入公钥(模板 public_key 保持空)", 'public_key = ""' in conf)
check("JWT 密钥仍会注入", "your-secret-key-change-in-production" not in conf) check("JWT 密钥仍会注入", "your-secret-key-change-in-production" not in conf)
check("默认烘焙非授权版标记(enforce=False)", flags_file.exists() and "LICENSE_ENFORCE = False" in read_flags())
# --license 缺公钥:默认让构建失败(避免打出名不副实的"授权版")
try: try:
b._stage_prepare_build_dir(None, "pyinstaller", b.BuildOptions(license=True)) b._stage_prepare_build_dir(None, "pyinstaller", b.BuildOptions(license=True))
check("--license 缺授权公钥应失败", False) check("--license 缺授权公钥应失败", False)
except RuntimeError as exc: except RuntimeError as exc:
check("--license 缺授权公钥应失败", "缺少厂商授权公钥" in str(exc)) check("--license 缺授权公钥应失败", "缺少厂商授权公钥" in str(exc))
os.environ[b.LICENSE_PUBKEY_ENV] = "test-pubkey-b64" # --license 缺公钥 + 显式放行:烘焙"非授权版"(旧行为是产出拒绝所有插件的砖头包)
os.environ[b.ALLOW_NO_KEY_ENV] = "1"
try:
b._stage_prepare_build_dir(None, "pyinstaller", b.BuildOptions(license=True))
check("显式放行时不报错", True)
check("显式放行时烘焙非授权版标记(enforce=False)",
"LICENSE_ENFORCE = False" in read_flags())
finally:
os.environ.pop(b.ALLOW_NO_KEY_ENV, None)
# --license + 公钥:注入产物配置 + 烘焙授权版标记(公钥随代码内置)
pubkey = valid_pubkey()
os.environ[b.LICENSE_PUBKEY_ENV] = pubkey
try: try:
b._stage_prepare_build_dir(None, "pyinstaller", b.BuildOptions(license=True)) b._stage_prepare_build_dir(None, "pyinstaller", b.BuildOptions(license=True))
conf = (proj / "build" / "config.toml").read_text(encoding="utf-8") conf = (proj / "build" / "config.toml").read_text(encoding="utf-8")
check("--license + 提供公钥后注入 [license] public_key", 'public_key = "test-pubkey-b64"' in conf) flags_src = read_flags()
ns: dict = {}
exec(compile(flags_src, str(flags_file), "exec"), ns) # 生成物必须是合法 Python
check("--license + 公钥:注入 [license] public_key", f'public_key = "{pubkey}"' in conf)
check("--license + 公钥:烘焙授权版标记", ns.get("LICENSE_ENFORCE") is True)
check("--license + 公钥:公钥随代码内置", ns.get("LICENSE_PUBLIC_KEY") == pubkey)
check("--license + 公钥:记录公钥指纹", bool(ns.get("PUBLIC_KEY_FINGERPRINT")))
finally:
os.environ.pop(b.LICENSE_PUBKEY_ENV, None)
# 非法公钥:构建期即报错(否则会打出"谁都验不过"的授权版产物)
os.environ[b.LICENSE_PUBKEY_ENV] = "not-a-real-key"
try:
b._stage_prepare_build_dir(None, "pyinstaller", b.BuildOptions(license=True))
check("非法公钥应终止构建", False)
except Exception as exc: # noqa: BLE001 - 具体异常类型由公钥库决定
check("非法公钥应终止构建", "公钥" in str(exc) or "key" in str(exc).lower(), str(exc)[:60])
finally: finally:
os.environ.pop(b.LICENSE_PUBKEY_ENV, None) os.environ.pop(b.LICENSE_PUBKEY_ENV, None)
finally: finally:
@@ -204,6 +267,11 @@ finally:
for k, v in saved_env.items(): for k, v in saved_env.items():
if v is not None: if v is not None:
os.environ[k] = v os.environ[k] = v
# 清理写进构建 venv 的烘焙标记(测试不应留下副作用)
if flags_backup is None:
flags_file.unlink(missing_ok=True)
else:
flags_file.write_text(flags_backup, encoding="utf-8")
shutil.rmtree(tmp, ignore_errors=True) shutil.rmtree(tmp, ignore_errors=True)