Skip to content

fix(security): wheel 补齐完整性清单三件套;enforce 开着却没清单改为拒绝启动 - #117

Merged
ReSerendipity merged 2 commits into
mainfrom
fix/wheel-integrity-manifest-packaging
Sep 22, 2026
Merged

ReSerendipity merged 2 commits into
mainfrom
fix/wheel-integrity-manifest-packaging

Conversation

@ReSerendipity

Copy link
Copy Markdown
Owner

这条修什么

发完 v2.2.2 之后按"产物也要回读"的习惯解开 wheel 核对,发现 integrated_app/security/只有 .py
integrity_manifest.json、清单的 .sig.ed25519、验签公钥 manifest_signing_public_key.pem
三件都没打进包。而 config.yaml 默认就是 security.integrity_selfcheck.enforce: true
run_startup_selfcheck() 在"清单不存在"分支只 logger.info("[SELF-CHECK] 完整性清单不存在,跳过自检")
就返回 skipped=16 / manifest_signed=False,enforce 也不拦。

合起来的后果:pip install 那条部署路径上,P0 的核心模块完整性保护一条都没有执行,
而配置声称它在强制运行
。Docker 镜像与便携包不受影响(两者另外拷了源码树,所以容器启动探测
一直是绿的,把这个缺口遮住了)。

两处一起改(缺一半都不对)

  1. pyproject.toml[tool.setuptools.package-data] 逐条点名三件套;
  2. integrity_selfcheck.pyenforce 开着却没有清单 → RuntimeError 拒绝启动,报错给出三条出路
    (源码检出生成清单 / 装出来的包是 wheel 漏打 / 确要关保护就显式写 enforce=false)。
    只改 2 会把 pip 安装路径直接变成起不来;只改 1 则下次打包行为一变又静默回到"不校验"。
    非强制模式保持原语义(返回 skipped,不抛)。

原注释里"清单缺失仍跳过不阻断(避免误伤首次部署)"这个判断被推翻:enforce=true 是默认值,
所以"没有清单"从来不是首次部署的状态,而是分发产物坏了

验收:不靠"配置看起来对"

层次 做法 结果
产物前后对比 python -m build 打 wheel,对比包内条目 1062 → 1065,三件逐条 OK
装出来的环境 把 wheel 解到临时目录、sys.path 指过去真跑自检 有清单:enforce=Truetotal/passed/failed/signed = 16/16/0/True
同一环境的负向 把清单挪走 enforce=True 拒绝启动enforce=Falseskipped=16
CI 常驻核对 Build (sdist/wheel) 作业新增一步:解开 wheel 断言三件在场 本地按同一逻辑跑通(缺失即 exit 1)
单元守卫 tests/test_integrity_selfcheck_packaging.py 8 条 全绿

守卫里有两条反空验证:① 仓库自带三件套,所以 enforce=True 在源码检出下应当真跑完 16 个模块且
0 失败(否则"没清单就拒启动"可能只是测试自己造的假阳性);② CI 若不再核对 wheel,那条测试直接红 ——
因为只查 pyproject 声明不算数,setuptools 行为一变就会重新出现"配置对、产物里没有"。

签名清单

integrity_selfcheck.py 属 16 个被签模块 → 清单已重算 + Ed25519 重签:
sign_integrity_manifest.py --verify PASS、check_integrity_manifest_sync.py 16/16 一致。

发版影响

v2.2.2 的 wheel 带这个缺口(已在 Release 说明里公开写明)。合入后若要一个"装出来就真校验"的包,
需要再发一次补丁版;是否现在发 2.2.3 归所有者定(#115 那条 RP 自动开的 release PR 正挂着)。

Refs: #113(版本位闸)、#114(release-please 真修)、Release v2.2.2 说明里的"已知未覆盖"第 4 条

发完 v2.2.2 后按"产物也要回读"解开 wheel 核对,发现 integrated_app/security/ 里只有 .py:
integrity_manifest.json、清单的 .sig.ed25519、验签公钥 manifest_signing_public_key.pem
三件都没进包。而 config.yaml 默认就是 security.integrity_selfcheck.enforce: true,
`run_startup_selfcheck()` 在"清单不存在"分支只 logger.info("跳过自检") 就返回
skipped=16 / manifest_signed=False,enforce 也不拦 —— 于是**纯 pip 安装那条部署路径上
P0 核心模块完整性保护一条都没执行,而配置声称它在强制运行**。
Docker 与便携包不受影响(两者另外拷了源码树,容器启动探测因此一直绿,把这个缺口遮住了)。

两处一起改,缺一半都不对:
- pyproject 的 [tool.setuptools.package-data] 逐条点名三件套(只改下面那条会把 pip 路径变成起不来);
- integrity_selfcheck:enforce 开着却没清单 → RuntimeError 拒绝启动,报错给三条出路
  (源码检出生成清单 / 装出来的包是 wheel 漏打 / 确要关保护就显式写 enforce=false)。
  非强制模式保持原语义(返回 skipped、不抛)。
  原注释"清单缺失仍跳过不阻断(避免误伤首次部署)"的判断被推翻:enforce=true 是默认值,
  所以"没有清单"从来不是首次部署的状态,而是分发产物坏了。

验收不靠"配置看起来对"(setuptools 行为一变,声明对了产物也可能没有):
- 本机 python -m build 前后对比:包内条目 1062 → 1065,三件逐条 OK;
- 把 wheel 解到临时目录当安装环境真跑:有清单时 enforce=True 返回 16/16/0/signed=True;
  挪走清单则拒绝启动,enforce=False 仍 skipped=16;
- CI 的 Build (sdist/wheel) 作业加一步产物核对(缺失即 exit 1);
- tests/test_integrity_selfcheck_packaging.py 共 8 条,含两条反空验证:
  ①仓库自带三件套 → enforce=True 在源码检出下必须真跑完 16 个模块且 0 失败;
  ②CI 若不再核对 wheel 就红(防止只查声明的假安全感)。

清单:integrity_selfcheck.py 属 16 个被签模块,已重算 + Ed25519 重签
(--verify PASS、check_integrity_manifest_sync 16/16)。
本地门禁:全量 2112 passed / 111 skipped / 0 failed(首轮那 1 条是
test_circuit_breaker::test_half_open_failure_reopens 的计时 flake:reset_timeout=0.01 +
sleep(0.02) 靠挂钟,隔离重跑 3/3 通过、整轮复跑 0 failed),mypy 棘轮 103 不变,
verify_cloud_native 全绿,check_spec_refs new=0。

Signed-off-by: ReSerendipity <zengyangc@outlook.com>
写清三件事:缺口的真实影响面(只有纯 pip 安装路径中招,Docker/便携因另拷源码树而被遮住)、
两处必须一起改的原因(只改 enforce 会让 pip 路径起不来、只改 package-data 则下次打包行为一变又静默回到不校验)、
以及验收层次(1062→1065 的产物对比、把 wheel 解到临时目录真跑 enforce 的正负两向、CI 常驻核对)。

Signed-off-by: ReSerendipity <zengyangc@outlook.com>
@ReSerendipity

Copy link
Copy Markdown
Owner Author

Performance Benchmark 这条红与本 PR 无关:已量化证明

先给结论:那条门禁的判据本身在制造随机红。以下是从它自己上传的产物里取出来的数字。

我下载了三个 run 的 benchmark-results-* 产物(两份提交之间只差 docs/DOD.md 一个文件
代码完全相同):

benchmark 03:18 成功 run 的 mean 03:20 失败 run 的 mean 偏差 03:47 重跑 偏差
test_adaptive_cache_capacity_adaptation 0.000088 0.000072 −18.6% 0.000070 −20.7%
test_audio_merge_performance 0.058784 0.012027 −79.5% 0.047191 −19.7%
test_cache_hit_miss_latency 0.000590 0.000406 −31.2% 0.000419 −29.0%
test_cache_operations_performance 0.001525 0.001101 −27.8% 0.001130 −25.9%
test_text_splitting_performance 0.188862 0.156183 −17.3% 0.158885 −15.9%

门槛是 --benchmark-compare-fail=mean:20%同一份代码在 runner 之间的摆动就有 17%–80%
所以这条门禁会在与改动无关的 PR 上随机变红;本例里"回退"的方向甚至是变快(负值 = 耗时更短),
--benchmark-compare-fail=mean:20% 对两侧都算失败。

另外两点机制问题(不是本 PR 要修的):

  1. 基线是"缓存里上一份 JSON",而 RUNS=$(find ... | grep -v "ci-<本次 sha>" | wc -l)
    只排除了本次 sha —— 于是同一个 commit 的上一次 run 会被当成"历史基线"
    变成"自己和自己比"(本次失败 run 恢复出来的基线正是 2 分钟前同一个 PR 的成功 run)。
  2. 失败步骤 6 秒就结束(03:47:3903:47:45),细节全被 >> $GITHUB_STEP_SUMMARY 重定向走,
    日志里只剩 exit code 1 —— 红得没有可读证据,只能去下载 artifact 反推(我就是这么查的)。

本 PR 的实际验收在哪条上

真正要看的证据是 Build (sdist/wheel) 作业里我新加的那一步(已 pass):解开 wheel 断言
integrity_manifest.json / .sig.ed25519 / manifest_signing_public_key.pem 三件在场。
本机同一逻辑的实测:包内条目 1062 → 1065,三件逐条 OK;再把 wheel 解到临时目录当安装环境跑
enforce=True16/16/0/signed=True,把清单挪走 → 拒绝启动。

必需检查(Lint (ruff)Test (pytest) (3.12, ubuntu-latest)Typecheck (mypy ratchet)、DCO)
全绿,本地全量 2112 passed / 0 failed。按上面这些证据合并;benchmark 那条另开 issue 跟进。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant