Bitcoin Core 提交签名验证:verify-commits 工具的原理、配置与安全使用指南
【免费下载链接】bitcoinBitcoin Core integration/staging tree项目地址: https://gitcode.com/GitHub_Trending/bi/bitcoin
Bitcoin Core 的contrib/verify-commits/目录提供了一套用于验证 Git 提交是否由可信维护者 PGP 签名的工具链,其核心是 Python 脚本 verify-commits.py。本文基于 contrib/verify-commits/README.md 的完整说明,结合仓库中的实际脚本、数据文件与 CI 集成代码,讲解如何安全地验证origin/master的整条签名链、各配置文件(信任根、受信密钥、异常豁免列表)的格式与作用,以及密钥过期/吊销场景下的处理机制。读完本文,你将能够独立运行该工具、理解它检查的三类问题(签名有效性、Tree-SHA512 一致性、合并整洁性),并知道在 CI 中如何低成本的做签名健全性检查。
为什么要验证提交的 PGP 签名
从 Git 历史看,维护者通过签名合并提交(merge commit)来表明"我确认这段内容进入了主线"。但签名本身只有配合一份"哪些密钥是可信的"清单才有意义,而且还需要回答两个问题:
- 信任从哪开始:仓库历史很长,不可能要求所有历史提交都有签名,因此需要一个"信任根"(root of trust),只验证该提交之后的历史;
- 历史文件内容如何防伪:仅仅验证"提交对象被签名"还不够,攻击者理论上可以构造内容不同的提交。Bitcoin Core 的做法是让合并提交的提交信息里携带一行
Tree-SHA512: <hash>,对提交对应的完整文件树做 SHA-512 摘要承诺,验证工具会重新计算并与提交信息比对。
verify-commits.py 就是围绕这两点实现的:它沿第一父提交(first parent)链从目标提交一路回溯到信任根,逐提交检查签名、树摘要和合并整洁性。
安全使用 verify-commits.py:不能"先 checkout 再验证"
这是 README 中最强调的一点:你不能用一个不可信的脚本来验证它自己。如果你先git checkout,再运行刚 checkout 下来的verify-commits.py,那么你所运行的脚本本身可能已经被投毒(backdoor),验证结果毫无意义。
正确顺序是:先在你本地保留一份可信版本的 verify-commits,用它去验证远端分支,确认整条历史都被可信密钥签名之后,再进行 checkout:
git fetch origin && \ ./contrib/verify-commits/verify-commits.py origin/master && \ git checkout origin/master三步分别对应:拉取远端引用(不切换工作区)→ 用当前可信的脚本验证origin/master→ 验证通过后再检出。README 同时坦诚说明:这一流程的 UI/UX 目前还不够好,欢迎通过 pull request 改进,降低出错概率。
需要注意的一个前提:除非指定--clean-merge 0,脚本会尝试验证每个合并提交都能"干净地重新合并",这要求git 版本不低于 2.38.0(git merge-tree --write-tree是 2.38 引入的行为)。
数据配置文件:信任根、受信密钥与豁免列表
脚本启动时从自身所在目录读取一批数据文件(见 verify-commits.py 第 100-108 行),这是整个机制的"配置面"。
trusted-git-root:PGP 信任根
文件 trusted-git-root 应包含单个 Git 提交哈希,它是"第一个未签名的提交",即信任链的起点。当前仓库中该文件内容为:
88a7294356e75bbaa136c9427c64e239f7c6fd40验证循环的终止条件就是走到这个提交(verify-commits.py 第 126-133 行):如果当前提交等于信任根,输出 "There is a valid path from ... to ... where all commits are signed!" 并成功退出;如果当前提交可证明早于信任根(predates判断),也视为合法停止点。注意predates()会拒绝与信任根历史分叉的提交——即既不是其祖先、也不是其后代的提交会直接报错退出,防止验证一条"看起来对"但实际分叉的历史。
trusted-sha512-root-commit:Tree-SHA512 信任根
文件 trusted-sha512-root-commit 包含"第一个没有 SHA512 根承诺的提交"哈希(当前为309bf16257b2395ce502017be627186b749ee749)。回溯到该提交时,Tree-SHA512 检查关闭("All Tree-SHA512s matched up to ..."),因为该提交之前的历史没有树摘要承诺可验。
trusted-keys:受信签名者指纹清单
文件 trusted-keys 是换行分隔的PGP 主密钥指纹(primary key,不是子密钥)列表。当前仓库包含 5 个受信指纹。脚本解析git verify-commit --raw输出中的[GNUPG:] VALIDSIG记录,取其中密钥指纹与该清单比对(verify-commits.py 第 151-158 行);而[GNUPG:] REVKEYSIG(已吊销)和[GNUPG:] EXPKEYSIG(已过期)记录默认会导致验证失败,除非该提交出现在allow-revsig-commits中。
三个豁免列表文件
- allow-revsig-commits:换行分隔的提交哈希清单,用于放行那些被"已过期/已吊销密钥"签名的历史提交(下一节详述)。当前仓库中该文件为空。
- allow-unclean-merge-commits:放行无法干净重放的合并提交(例如合并后有人为修改解决冲突的场景)。当前为空。
- allow-incorrect-sha512-commits:放行 Tree-SHA512 不匹配的提交。当前为空。
README 只列出了前两个文件;从 verify-commits.py 的实际读取逻辑可以看到完整的五个数据文件。
验证循环:脚本到底检查了什么
verify-commits.py的主循环从目标提交(默认HEAD,可通过位置参数指定,如origin/master)开始,每轮取该提交的第一父提交继续回溯,直到到达信任根。对每个提交依次执行以下检查:
- 签名检查:调用
git -c gpg.program=<目录>/gpg.sh verify-commit --raw <commit>,通过本目录自带的 gpg.sh 包装 GPG 调用(详见下文"禁用 SHA1")。要求出现VALIDSIG且指纹在trusted-keys中。 - Tree-SHA512 检查(可关闭,见 CLI 参数):tree_sha512sum() 用
git ls-tree -r递归列出整棵树的全部 blob,再以git cat-file --batch批量流式读取所有文件内容,对每个文件先算 SHA-512,再按"摘要 文件名\n"的形式折叠进总摘要(README 注明该算法取自 bitcoin-maintainer-tools 的 github-merge.py)。然后把Tree-SHA512: <hash>与提交信息逐行比对,不匹配即报错。 - 拒绝章鱼合并(octopus merge):任何超过 2 个父提交的合并直接失败。
- 干净合并检查:对双父提交,用
git merge-tree --write-tree <parent1> <parent2>重放合并,比对重建出的树是否与合并提交实际树一致;不一致则打印 diff 并失败。该检查受--clean-merge天数限制控制,且旧版本 git 报 128 退出码时提示需要 git 2.38+。
gpg.sh:为什么额外禁用 SHA1 签名
脚本没有直接调用系统 gpg,而是通过-c gpg.program=.../gpg.sh注入 gpg.sh。其核心逻辑:
- 始终使用
--trust-model always(信任判定交给脚本自己,用指纹清单完成); - 当环境变量
BITCOIN_VERIFY_COMMITS_ALLOW_SHA1 != 1(默认路径)时,追加--weak-digest sha1,拒绝使用 SHA1 的签名(包括自签名); - 对 gpg 1.4.1/2.0.x 等过旧版本,直接报错要求升级到 2.1.10 以上。
脚本注释解释了动机:签名对象是提交对象,而提交内容部分由不受信任的输入(pull request)决定;理论上攻击者可以构造一个自己预先做出 SHA1 碰撞的提交对象,因此禁用 SHA1 是一种"双保险"措施。另外,回溯到信任根(没有 SHA1 承诺要求的旧历史)之前,脚本会把BITCOIN_VERIFY_COMMITS_ALLOW_SHA1设为 1 放宽此限制(见no_sha1变量,verify-commits.py 第 116、146 行)。
命令行参数
verify-commits.py的完整用法为verify-commits.py [options] [commit id](argparse 定义):
| 参数 | 默认值 | 作用 |
|---|---|---|
[commit id] | HEAD | 验证回溯的终点提交,如origin/master。提交引用不能包含空格 |
--clean-merge <NUMBER> | inf(无限,即全部检查) | 只对最近 N 天内的提交检查"干净合并"。传0则完全跳过该检查,此时不再强依赖 git 2.38+ |
--disable-tree-check | 未指定(默认开启) | 关闭 Tree-SHA512 检查(注意它是action='store_false',默认状态即为检查开启) |
导入受信 PGP 密钥
验证前提是本机 GPG 密钥环里能拿到受信签名者的公钥。README 给出的命令是:
gpg --keyserver hkps://keys.openpgp.org --recv-keys $(<contrib/verify-commits/trusted-keys)它把 trusted-keys 中的指纹列表展开为--recv-keys的参数逐个从密钥服务器导入。CI 环境的等价写法见下文。
在 CI 中做签名健全性检查
Bitcoin Core 的 CI 并不在每次构建时做全量历史验证,而是用了一个低成本策略,实现在 ci/lint/06_script.sh 第 27-38 行(由LINT_CI_SANITY_CHECK_COMMIT_SIG=1触发):
# 仅对最后 10 个提交做健全性检查,及时发现缺签名、缺密钥或密钥过期 git log HEAD~10 -1 --format='%H' > ./contrib/verify-commits/trusted-sha512-root-commit git log HEAD~10 -1 --format='%H' > ./contrib/verify-commits/trusted-git-root mapfile -t KEYS < contrib/verify-commits/trusted-keys git config user.email "ci@ci.ci" git config user.name "ci" gpg --keyserver hkps://keys.openpgp.org --recv-keys "${KEYS[@]}" && \ ./contrib/verify-commits/verify-commits.py思路是:把两个信任根临时改写为HEAD~10,于是验证范围收缩为"最近 10 个提交",再导入受信密钥并运行脚本。注释说明:master 分支每次 push 通常只有一个新合并提交,发布分支也只有少量提交,所以抽查最近 10 个提交就足以低成本地暴露"缺签名、缺密钥、密钥过期"三类问题。GPG 依赖的安装则见 ci/lint/01_install.sh。
密钥过期或吊销怎么办
这是 README 专门用一节讨论的运维问题:当某个曾为旧提交签名过的密钥(或子密钥)过期/被吊销后,verify-commits会对所有用该密钥签过的历史提交开始报错。此时不需要把信任根trusted-git-root向后推进(那样会放弃对更早历史的验证),正确做法是:
- 找出所有被该密钥签名、且因过期/吊销而校验失败的提交哈希;
- 将它们逐行加入 allow-revsig-commits。
这样历史提交的签名仍被验证(确认REVKEYSIG/EXPKEYSIG对应的确实是当初那个密钥签的),但新提交无法再用已过期/吊销的密钥通过验证——因为allow_revsig = current_commit in revsig_allowed只对该清单中的提交生效(verify-commits.py 第 147、156-157 行)。
README 还给出一种构建清单的土办法:编辑verify-commits.py,让每个提交分别用BITCOIN_VERIFY_COMMITS_ALLOW_REVSIG置 1 和置 0 各测一遍,把只有在置 1 时才通过的提交打印出来。需要说明的是,当前仓库版本的脚本中该环境变量已不在源码中出现(改为读取allow-revsig-commits数据文件),README 描述的是一种针对旧版脚本的手工调试手段,属于 README 自述的"work in progress"的一部分。
已知局限与注意事项
- README 自身声明:这是一个"未完成的工作"(incomplete work in progress)。README 提到的
pre-push-hook.sh(维护者推送前自查签名钩子)在当前仓库的contrib/verify-commits/目录中并不存在,实际目录内文件为README.md、verify-commits.py、gpg.sh、trusted-git-root、trusted-sha512-root-commit、trusted-keys及三个allow-*豁免清单。 - 版本前提:干净合并检查需要 git ≥ 2.38.0;gpg 建议 ≥ 2.1.10(旧版本会被 gpg.sh 明确拒绝)。
- 调试日志:设置环境变量
CI=true可开启 debug 级日志,脚本在处理长历史时用它输出进度防止 CI 超时(verify-commits.py 第 89-91、124 行)。 - 信任模型:验证结果的可信度上限是你手中那份
verify-commits.py与数据文件的可信度,因此"先用可信版本验证、再 checkout"的顺序是整个安全模型的基石,不可颠倒。
小结
contrib/verify-commits/展示了 Bitcoin Core 如何把"签名信任链"工程化:用trusted-git-root划定验证起点,用trusted-sha512-root-commit与提交信息中的Tree-SHA512行绑定文件树内容,用trusted-keys收敛有效签名者,用三个allow-*清单优雅地处理过期密钥、非干净合并与摘要异常等历史包袱;再配合 CI 中"只抽查最近 10 个提交"的低成本健全性检查,使签名验证既能全量兜底、又能日常化运行。理解这套机制后,你可以把同样的"信任根 + 受信密钥 + 内容承诺"思路迁移到其他需要防历史篡改的 Git 项目中。
【免费下载链接】bitcoinBitcoin Core integration/staging tree项目地址: https://gitcode.com/GitHub_Trending/bi/bitcoin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考