feat: 打包时烘焙授权决策(--license 变为可选,消除砖头包风险)
prepare 阶段把"是否授权版"写进 chongming/_build_flags.py(随产物编译、部署侧 不可改): - --license + 公钥:烘焙授权版 —— 强制校验,公钥随代码内置,现场即便把 config.toml 里的公钥换成自己的并自签 license 也过不了验签;同时保留原有 注入产物配置(仅作镜像/可见性) - 不加 --license:烘焙非授权版 —— 不校验授权、插件照常加载(与开发态一致), 即"打包不配 license"是合法选项,不再是拒绝所有插件的砖头包 - --license 缺公钥:默认仍让构建失败;ALLOW_NO_KEY=1 显式放行时降级为 非授权版(旧行为是产出砖头包) - 公钥在构建期校验格式,非法即终止构建;烘焙文件的常量名与网关读取端 chongming.app.core.licensing 对齐
This commit is contained in:
+74
-15
@@ -27,14 +27,16 @@
|
|||||||
做 PyArmor 混淆。**默认关闭**(产物为明文,默认只适合内部
|
做 PyArmor 混淆。**默认关闭**(产物为明文,默认只适合内部
|
||||||
调试;对外分发必须加这个参数)
|
调试;对外分发必须加这个参数)
|
||||||
--license 打开授权:注入厂商授权公钥(缺公钥则构建失败)、跑授权私钥
|
--license 打开授权:注入厂商授权公钥(缺公钥则构建失败)、跑授权私钥
|
||||||
泄漏守卫。**默认关闭**(不注入、不守卫、缺公钥也不报错,
|
泄漏守卫。**默认关闭**(license 是可选构建项:不加则产物为
|
||||||
产物无法校验任何授权;对外分发必须加这个参数)
|
"非授权版"——不校验授权、插件照常加载,与开发态一致;
|
||||||
|
加上则产物为"授权版"——强制校验,现场改配置/环境变量关不掉。
|
||||||
|
两者由构建期烘焙进产物,见 chongming/_build_flags.py)
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
uv run build --only mbank # 依赖未变时,仅重新打包 .mbank 模块文件
|
uv run build --only mbank # 依赖未变时,仅重新打包 .mbank 模块文件
|
||||||
uv run build nuitka --only mbank,prepare
|
uv run build nuitka --only mbank,prepare
|
||||||
uv run build --encrypt --license # 对外分发:加密模块 + 注入授权公钥
|
uv run build --encrypt --license # 对外分发:加密模块 + 授权版产物
|
||||||
uv run build # 内部调试:明文模块、不带授权
|
uv run build # 内部/无授权版:明文模块、不校验授权
|
||||||
|
|
||||||
依赖收集(--only pack 生效):
|
依赖收集(--only pack 生效):
|
||||||
打包收集清单 = 根 config.toml [pyinstaller_build_import](网关本体依赖)
|
打包收集清单 = 根 config.toml [pyinstaller_build_import](网关本体依赖)
|
||||||
@@ -99,7 +101,7 @@ class BuildOptions:
|
|||||||
"""构建开关(命令行可选参数,默认全部关闭 = 明文调试产物)"""
|
"""构建开关(命令行可选参数,默认全部关闭 = 明文调试产物)"""
|
||||||
|
|
||||||
encrypt: bool = False # --encrypt: 打开 mbank 加密与 PyArmor 混淆
|
encrypt: bool = False # --encrypt: 打开 mbank 加密与 PyArmor 混淆
|
||||||
license: bool = False # --license: 打开授权公钥注入与私钥泄漏守卫
|
license: bool = False # --license: 授权版产物(注入公钥 + 烘焙强制校验);默认非授权版
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -387,33 +389,46 @@ def _stage_prepare_build_dir(config: Config, mode: str, options: BuildOptions) -
|
|||||||
jwt_key = Encryption.generate_key()
|
jwt_key = Encryption.generate_key()
|
||||||
content = content.replace("your-secret-key-change-in-production", jwt_key)
|
content = content.replace("your-secret-key-change-in-production", jwt_key)
|
||||||
|
|
||||||
# 注入厂商授权公钥(机器码授权)—— 默认不做,加 --license 才注入
|
# 注入厂商授权公钥(机器码授权)—— 默认不做,加 --license 才注入;
|
||||||
|
# 两条路径都会把"本产物是否为授权版"烘焙进 chongming/_build_flags.py
|
||||||
|
# (编译进产物、部署侧不可改)—— 未加 --license 的产物是不校验授权、
|
||||||
|
# 插件照常加载的正常版本,而不是"拒绝所有插件"的砖头包。
|
||||||
if options.license:
|
if options.license:
|
||||||
pubkey = _resolve_license_public_key()
|
pubkey = _resolve_license_public_key()
|
||||||
if pubkey:
|
if pubkey:
|
||||||
|
from chongming_license import public_key_fingerprint
|
||||||
|
|
||||||
content = _set_license_pubkey(content, pubkey)
|
content = _set_license_pubkey(content, pubkey)
|
||||||
|
_write_build_flags(enforce=True, public_key=pubkey)
|
||||||
|
print(
|
||||||
|
f" ✓ 已注入厂商授权公钥(指纹 {public_key_fingerprint(pubkey)}):"
|
||||||
|
"产物为授权版,强制校验授权(现场改配置/环境变量关不掉)"
|
||||||
|
)
|
||||||
elif os.environ.get(ALLOW_NO_KEY_ENV) == "1":
|
elif os.environ.get(ALLOW_NO_KEY_ENV) == "1":
|
||||||
# 公钥缺失时打包产物会拒绝**全部**插件(没人能证明被授权)= 砖头设备,
|
# 无公钥 = 谁都证明不了被授权。此时**不再**烘焙"授权版"(那会拒绝
|
||||||
# 故默认让构建失败;仅内部演示允许显式放行。
|
# 全部插件 = 砖头设备),而是烘焙非授权版:插件照常加载、不校验授权。
|
||||||
|
_write_build_flags(enforce=False)
|
||||||
print(
|
print(
|
||||||
f" ⚠️ 未提供授权公钥且设置了 {ALLOW_NO_KEY_ENV}=1:"
|
f" ⚠️ 未提供授权公钥且设置了 {ALLOW_NO_KEY_ENV}=1:"
|
||||||
"本产物无法校验任何授权(所有插件都不会加载),仅限内部演示"
|
"本产物为非授权版(不校验授权,插件照常加载),仅限内部演示"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"缺少厂商授权公钥:打包产物会拒绝加载所有插件(设备变砖)。\n"
|
"缺少厂商授权公钥:无法产出可校验授权的产物。\n"
|
||||||
f" 请用环境变量 {LICENSE_PUBKEY_ENV}=<base64 公钥> 或 "
|
f" 请用环境变量 {LICENSE_PUBKEY_ENV}=<base64 公钥> 或 "
|
||||||
f"{LICENSE_PUBKEY_FILE_ENV}=<文件路径> 提供,\n"
|
f"{LICENSE_PUBKEY_FILE_ENV}=<文件路径> 提供,\n"
|
||||||
f" 或把公钥写入 {DEFAULT_LICENSE_PUBKEY_FILE}"
|
f" 或把公钥写入 {DEFAULT_LICENSE_PUBKEY_FILE}"
|
||||||
"(由 `uv run chongming-license keygen` 输出)。\n"
|
"(由 `uv run chongming-license keygen` 输出)。\n"
|
||||||
f" 仅内部演示无授权构建可去掉 --license,"
|
" 不需要授权校验的内部/演示构建请**去掉 --license**"
|
||||||
f"或设 {ALLOW_NO_KEY_ENV}=1 强制放行。"
|
f"(产物不校验、插件照常加载),或设 {ALLOW_NO_KEY_ENV}=1 强制放行。"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 默认不注入:产物不带厂商授权信任根(模板里已写着的公钥也不动)
|
# 非授权版:明确烘焙"不校验授权"(产物内编译,部署侧改不了)——
|
||||||
|
# 打包不含 license 因此始终是合法选项,产物与开发态一致、插件照常加载。
|
||||||
|
_write_build_flags(enforce=False)
|
||||||
print(
|
print(
|
||||||
" ⚠️ 未注入厂商授权公钥(默认):本产物无法校验任何授权,"
|
" ⚠️ 未注入厂商授权公钥(默认):本产物为非授权版(不校验授权,"
|
||||||
"对外分发请加 --license"
|
"插件照常加载),对外分发请加 --license"
|
||||||
)
|
)
|
||||||
cfg_path.write_text(content, encoding="utf-8")
|
cfg_path.write_text(content, encoding="utf-8")
|
||||||
|
|
||||||
@@ -457,6 +472,50 @@ def _resolve_license_public_key() -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _write_build_flags(enforce: bool, public_key: str = "") -> Path:
|
||||||
|
"""把"本产物是否为授权版"烘焙成 ``chongming/_build_flags.py``(随代码编译)。
|
||||||
|
|
||||||
|
格式契约与网关读取端 ``chongming.app.core.licensing`` 对齐(常量名一致):
|
||||||
|
该模块在打包 venv 的网关包内生成,编译进产物后**部署侧改不了** —— 所以
|
||||||
|
"打包不配 license" 可以是合法选项(烘焙 False = 不校验、插件照常加载),
|
||||||
|
而授权版产物的强制校验也不会被现场改 config.toml / 环境变量关掉。
|
||||||
|
|
||||||
|
授权版同时把公钥烘焙进代码:现场即便把 config.toml 里的公钥换成自己的
|
||||||
|
并自签一份 license,也过不了内置公钥的验签(配置公钥只在未烘焙时兜底)。
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
from chongming_license import load_public_key, public_key_fingerprint
|
||||||
|
|
||||||
|
fingerprint = ""
|
||||||
|
if public_key:
|
||||||
|
# 失败即构建期配置错误:趁早报错,避免打出一个"谁都验不过"的授权版产物
|
||||||
|
load_public_key(public_key)
|
||||||
|
fingerprint = public_key_fingerprint(public_key)
|
||||||
|
|
||||||
|
spec = importlib.util.find_spec("chongming")
|
||||||
|
if spec is None or not spec.submodule_search_locations:
|
||||||
|
raise RuntimeError(
|
||||||
|
"未找到 chongming 网关包,无法写入构建期授权标记(_build_flags.py)。\n"
|
||||||
|
" 请确认构建 venv 已安装 chongming-gateway(uv sync)"
|
||||||
|
)
|
||||||
|
target = Path(next(iter(spec.submodule_search_locations))) / "_build_flags.py"
|
||||||
|
target.write_text(
|
||||||
|
"\n".join([
|
||||||
|
'"""构建期烘焙的授权决策 —— 由打包脚本生成,请勿手改。',
|
||||||
|
"",
|
||||||
|
"编译进产物后部署侧不可修改:部署目录的 config.toml / 环境变量都影响不了",
|
||||||
|
"这份决策。读取端:chongming.app.core.licensing。",
|
||||||
|
'"""',
|
||||||
|
f"LICENSE_ENFORCE = {enforce!r}",
|
||||||
|
f'LICENSE_PUBLIC_KEY = "{public_key}"',
|
||||||
|
f'PUBLIC_KEY_FINGERPRINT = "{fingerprint}"',
|
||||||
|
]) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
def _set_license_pubkey(text: str, pubkey: str) -> str:
|
def _set_license_pubkey(text: str, pubkey: str) -> str:
|
||||||
"""把公钥写进 [license] 段的 public_key,保留其余注释与排版。
|
"""把公钥写进 [license] 段的 public_key,保留其余注释与排版。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user