news 2026/9/7 23:52:55

Bitcoin Core 提交签名验证:verify-commits 工具的原理、配置与安全使用指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bitcoin Core 提交签名验证:verify-commits 工具的原理、配置与安全使用指南

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.0git 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)开始,每轮取该提交的第一父提交继续回溯,直到到达信任根。对每个提交依次执行以下检查:

  1. 签名检查:调用git -c gpg.program=<目录>/gpg.sh verify-commit --raw <commit>,通过本目录自带的 gpg.sh 包装 GPG 调用(详见下文"禁用 SHA1")。要求出现VALIDSIG且指纹在trusted-keys中。
  2. 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>与提交信息逐行比对,不匹配即报错。
  3. 拒绝章鱼合并(octopus merge):任何超过 2 个父提交的合并直接失败。
  4. 干净合并检查:对双父提交,用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向后推进(那样会放弃对更早历史的验证),正确做法是:

  1. 找出所有被该密钥签名、且因过期/吊销而校验失败的提交哈希;
  2. 将它们逐行加入 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.mdverify-commits.pygpg.shtrusted-git-roottrusted-sha512-root-committrusted-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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 23:52:04

Android高校教室预约管理平台全流程开发:从需求设计到答辩准备

最近几年我带过的计算机类毕设项目里&#xff0c;Android方向的选题来来回回就那几类&#xff1a;电商、新闻、点餐、备忘录……真正有区分度、能拿得出手去答辩的&#xff0c;反而不多。高校教室预约管理平台算是一个经典且经久不衰的方向——业务场景真实、用户角色清晰、功能…

作者头像 李华
网站建设 2026/9/7 23:48:40

MyBatis-Plus枚举映射:告别魔法数字,实现类型安全的状态管理

说真的&#xff0c;每次看到项目代码里出现 status 1 、 if (status 2) 这种“魔法数字”&#xff0c;我心里都发毛。订单状态1是什么&#xff1f;支付类型2又是什么&#xff1f;新手接手代码必须对着数据库注释猜&#xff0c;猜错就是线上事故。我在维护一个老订单系统时…

作者头像 李华
网站建设 2026/9/7 23:46:59

JSP+Servlet+MySQL汉服电商网站设计与实现:从数据库到部署全攻略

作为一名老Java Web开发者&#xff0c;看到“jsp福建汉服天下电子商务网站设计与实现”这个标题&#xff0c;第一反应确实是有点怀念。这几乎是每个计算机专业学生都绕不开的课设标配——用JSPServletMySQL撑起一个完整的电商项目。但说真的&#xff0c;把它做好、做稳、做得出…

作者头像 李华
网站建设 2026/9/7 23:45:12

显式 GC 的使用:留与去,如何选择?

目录 一、什么是显式 GC? (一) 垃圾回收的基本原理 (二)显式 GC 方法和行为 1. System.gc() 方法 2. 显式 GC 的行为 (三)显式 GC 的使用场景与风险 1. JVM 如何处理显式 GC 2. 显式 GC 的风险 二、显式 GC 对性能的影响 (一) 全 GC 与 STW 1. Full GC 是如…

作者头像 李华