diff --git a/README.md b/README.md index e8422e9..67992fc 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,12 @@ exe 内的依赖来自三处,新增第三方包时对号入座: `--encrypt` / `--license` 默认**都关**(不加参数即内部调试产物:明文模块、不校验授权); 对外分发必须同时加这两个开关。 +`--license` 需要厂商授权公钥(未提供直接构建失败,避免打出名不副实的"授权版")。首次: +`uv run chongming-license keygen -o license_private.pem` 生成密钥对(一次性;私钥离线保管, +绝不进仓库/产物),把输出的公钥写入根目录 `license_public.key`,或用 +`CHONGMING_LICENSE_PUBKEY=` / `CHONGMING_LICENSE_PUBKEY_FILE=<文件路径>` 指定。 +构建会把公钥注入产物的 `[license] public_key` 并烘焙进代码(现场改配置/环境变量关不掉强制校验)。 + ### 进程插件打包与容器部署 ```bash diff --git a/config.toml b/config.toml index 315180f..f678d07 100644 --- a/config.toml +++ b/config.toml @@ -102,8 +102,10 @@ config_path = "./config/workers/config.toml" [pyinstaller_build_import] # 打包收集清单(PyInstaller --hidden-import / Nuitka --include-module)。 -# 网关本体依赖随 chongming-gateway 包发布,打包时能沿 `from chongming.app import app` -# 静态跟到;本清单保留两类: +# 网关本体(chongming-gateway 包)**未混淆时**能沿 `from chongming.app import app` +# 静态跟到;但 --encrypt 会先 PyArmor 混淆 main.py / server.py,此时静态分析跟不到, +# 网关入口包必须显式列出(见 base 前几项)。本清单保留三类: +# - 网关入口包(chongming / chongming.app 与骨架直接引用的子模块) # - 动态导入、静态分析看不到的(granian 内部子模块、amqtt 插件、 # sqlite_vfs.contrib、chongming_worker 子模块等) # - public 骨架自身(server / utils)与服务器可选实现(uvloop 等) @@ -122,6 +124,14 @@ base = [ "chongming_request", "chongming_auth", "server", + # 网关入口包:--encrypt 时 main.py / server.py 先被 PyArmor 混淆,里面的 + # `from chongming.app import app` 对 PyInstaller 静态分析不可见 —— 漏了会在 + # 打包期"成功"、产物启动即报 + # "无法导入网关(chongming.app): No module named 'chongming'" + "chongming", + "chongming.app", + "chongming.app.core.logger", + "chongming.app.core.parent_death", "module_bank", # 模块化打包工具 "sqlite_vfs", "sqlite_vfs.core", diff --git a/public/config.toml b/public/config.toml index 94abbd6..dc81841 100644 --- a/public/config.toml +++ b/public/config.toml @@ -28,6 +28,14 @@ launch_worker_count = "cache" algorithm = "HS256" secret_key = "your-secret-key-change-in-production" +[license] +# 机器码授权(厂商 Ed25519 签发):`uv run build --license` 在打包前把厂商公钥注入 +# 这里,并把"本产物是否为授权版"烘焙进产物代码(编译后部署侧改不了)。 +# public_key 留空 = 未接入授权:开发态不校验;未加 --license 的产物也不校验 +# (插件照常加载,不是砖头包)。可选键:path(授权文件,默认 ./license.lic)、 +# min_match(指纹容错阈值)、enabled(仅开发态生效;打包产物以构建期烘焙为准)。 +public_key = "" + [production] debug = false env = "production" diff --git a/test/test_build_flags.py b/test/test_build_flags.py index 834b240..8e08c08 100644 --- a/test/test_build_flags.py +++ b/test/test_build_flags.py @@ -20,6 +20,7 @@ import os import shutil import sys import tempfile +import tomllib from pathlib import Path SRC = Path(__file__).parent @@ -158,10 +159,59 @@ mod, err = import_from_bank(encrypted_bank, "demo", key=encrypted_bank.with_suff check("加密包用 .key 里的密钥可导入", mod is not None, str(err)) mod, err = import_from_bank(encrypted_bank, "demo", key=None) -check("加密包缺密钥无法导入(说明 .key 必须随包提供给加载端)", mod is None and isinstance(err, ImportError), str(err)) +check("加密包不主动取密钥无法导入(install_importer 不会自己读包内密钥)", + mod is None and isinstance(err, ImportError), str(err)) + +# 运行端取密钥的口径:public/utils/launch.py 的 init_module_bank 用 packer.get_key() +# 从**包内**取(它不读旁挂 .key 文件)。打包时没写进包内,加密产物启动就是 +# "模块 'plugins' 的字节码是加密的,但没有提供解密密钥" —— 插件全加载失败、 +# 契约校验不过,网关直接起不来(实测踩过) +embedded = b.PythonToSQLite(str(encrypted_bank)).get_key() +check("加密包把密钥写进包内(运行端 get_key() 取得到)", embedded == key, f"embedded={embedded!r}") + +mod, err = import_from_bank(encrypted_bank, "demo", key=embedded) +check("加密包按运行端口径(get_key() 取出的密钥)可导入", mod is not None, str(err)) # ========== 3. 授权构建:烘焙标记 / 公钥注入 ========== +# 3.0 先验真实模板/骨架的构建契约:下面的用例(含 make_project)都自造 config.toml, +# 所以"模板漏了 [license] 段 / 漏了网关收集项"这类漂移测不出来 —— 而两种漏法都不好在 +# 构建期发现(前者首报是"缺少厂商授权公钥",要补完公钥才暴露"未找到 [license] 段的 +# public_key";后者更隐蔽:打包"成功",产物启动才报 No module named 'chongming')。 +# 两处契约都在模板(生成新项目用)与根骨架(本仓打包用)的配置里,一起验。 +GATEWAY_ENTRY_MODULES = ("chongming", "chongming.app") # --encrypt 时静态分析看不到 +for label, proj_root in ( + ("模板", SRC.parent / "utils" / "template"), + ("根骨架", SRC.parent), +): + try: + injected = b._set_license_pubkey( + (proj_root / "public" / "config.toml").read_text(encoding="utf-8"), "TESTKEY" + ) + check( + f"{label} public/config.toml 可注入 [license] public_key", + 'public_key = "TESTKEY"' in injected, + ) + except Exception as exc: # noqa: BLE001 - 缺段/缺键都算契约破坏 + check(f"{label} public/config.toml 可注入 [license] public_key", False, str(exc)[:60]) + + # 收集清单要给 --encrypt 兜底:PyArmor 混淆 main.py/server.py 后,打包器跟不到 + # 里面的 `from chongming.app import app`,网关入口包只能靠这里显式声明 + with open(proj_root / "config.toml", "rb") as f: + pack_list = tomllib.load(f)["pyinstaller_build_import"] + listed = ( + set(pack_list.get("base", [])) + | set(pack_list.get("linux", [])) + | set(pack_list.get("windows", [])) + ) + missing = [m for m in GATEWAY_ENTRY_MODULES if m not in listed] + check( + f"{label} config.toml 收集清单含网关入口包", + not missing, + f"缺 {missing}" if missing else "", + ) + + def make_project(root: Path) -> Path: """造一个最小构建现场:public/{main.py,server.py,config.toml}""" (root / "public" / "utils").mkdir(parents=True)