Hacker News 中文摘要

RSS订阅

Git 3.0 将默认 SHA-256 恐酿大错 -- Git 3.0's upcoming SHA-256 default will be a costly mistake

作者搁置此事数年,因更智者专注此道而不愿指手画脚。他认为 Git(Git) 3.0 发布将令众人耗费大量时间焦虑却收效甚微,且鲜有人知即将发生。他邀读者拿爆米花,讲述即将到来的 Git 3.0 一项破坏性变更将成代价高昂的全球性灾难,却几乎无实用价值。他简述 Git 是内容寻址数据库,计算内容哈希作为键值库键,相同内容全球得相同哈希。

正文

从哪里说起呢?这事我已经憋了几年,主要是因为更聪明的人一直在关注它,而我不太喜欢当后座驾驶员。不过我觉得 Git 3.0 发布即将让所有人付出大量时间和焦虑,却几乎没什么好处,而几乎没人知道要发生什么。

所以,抓把爆米花,听我讲一个 Git 3.0 即将到来的破坏性变更如何变成一场巨大的、代价高昂的全球事故,而它几乎没有实际价值。

Git 是一个内容寻址数据库。这意味着你想在其中存储和传输数据时,Git 会计算内容的哈希,把它用作键值数据库的键(值就是内容本身)。同样的内容在全球总是得到同样的哈希。

Git 使用 SHA-1 哈希作为键值对象数据库中的键

这很好,因为同样的文件内容永远不会被存两次。还有个很酷的性质:commit 会编码前一个 commit 的哈希,这种完整性会传播——不改变之后所有东西的哈希,你就无法改变任何东西的哈希。这给了它「密码学完整性」:对最新 commit 做哈希,本质上也就对之前可能数百万个文件内容、树和 commit 做了哈希。

在 Git 里,这个哈希函数一直是 SHA-1。Linus 在 2005 年 Git 诞生时选的,20 年来一直运转良好——相对快,而且两个不同文件意外哈希到同一个值在实际意义上不可能。据我所知,在所有仓库里做过的每一个文件、树和 commit 中,这从未发生过——数以十亿计。

数学上,对 SHA-1 的 160 位输出,生日界意味着你需要在一个项目里约 1.4 septillion 个随机文件(1.4 万亿亿个,即 1,400,000,000,000,000 亿个)才可能发生意外文件哈希碰撞。

但有个问题:数学上 SHA-1 现在被认为是半「破」的,因为已发表过碰撞攻击(2017 年的 SHAttered,2020 年的 SHA-1 is a Shambles)——实际利用不太可行,但理论上可能。

所以作为回应,经过非常聪明的人大量工作后,即将到来的 Git 3.0 计划把默认哈希算法从半破的 SHA-1 换成更强的 SHA-256。

但先停一下。「破」是什么意思?

这很重要,因为它可能不是普通人以为的「破」。从密码学哈希的角度,破本质上意味着找到碰撞并非不可能。换句话说,只要钱和 GPU 足够多,对某些内容形状,你可以故意造出两个不同的东西哈希到完全相同的值。

一把破了的 SHA 能干什么?在清晨……

这意味着虽然两个不同内容的正常文件意外碰撞仍然几乎不可能,但技术上「制造」两个哈希相同的不同文件并非不可能。所以理论上存在攻击向量:有人可以把一个文件的内容换成恶意版本,Git 分辨不出来,因为哈希在数学上完全相同。

这些论文表明 SHA-1 有一种理论上可被现代 GPU 农场利用的性质,如今花几万美元就能制造有目的的碰撞,而 SHA-256 没有这个性质。

听起来很吓人吧?其实不。

花一分钟退一步谈谈这些碰撞。考虑对哈希函数的攻击时,主要有两个问题(假设我做极其愚蠢的简化):一是「碰撞攻击」,二是「第二原像攻击」。

碰撞攻击是:你明知要当攻击者,但先假装好人直到获得信任。你故意生成两个哈希相同的文件——一个良性,一个恶意。你给人良性那个,直到被信任,然后换成恶意那个,因为哈希匹配,Git 分辨不出来。你甚至可以让签了名的 tag 或 commit 看起来像是签了恶意文件。

第二原像攻击是:你看到一个想替换的文件,然后造出第二个做恶意事情、又恰好匹配那个哈希的文件,让人在不知情下拉下来。重要的是,这并不要求原文件作者就是攻击者。

碰撞攻击与第二原像攻击

在开始吐槽前,我最想强调的是:虽然第二原像攻击看起来更令人担忧,但几乎所有广泛使用的哈希函数都不受它影响。Git 就算用 MD5(被认为极其破的哈希函数)也实际上免疫第二原像攻击。MD5 才算「完全破」,SHA-1 不是,它强得多。

「实际上免疫」是什么意思?如果地球上约 30 亿块 GPU 全都神奇地换成 RTX 5090,全都 100% 时间只做 MD5,暴力破解某个特定原像的期望时间仍然约 160 亿年(中位数约 110 亿年),大致是宇宙年龄。

所以任何现实中有意义的攻击向量都依赖碰撞攻击,也就是说引入原文件的人同时也预计算了恶意版本,打算在被接受后注入。

不过,假设我们极度、不现实地保守,甚至假设原像是容易的。假设你找到一种方法,能在一小时内造出匹配笔记本上任何已知文件哈希的第二原像,现在你能轻松替换任何文件。恭喜,你能拿下地球上任何代码库!

但有个小问题:你怎么让不认识你、也从没拉过之前版本内容的人拉取那个文件?你怎么让他们以对你有用的方式运行它?之后所有争论和问题都取决于这些答案,但答案通常不在讨论里。

即使假设 SHA-1 轻易可破,即使假设第二原像可以制造(甚至便宜),也不代表可用的攻击向量真的容易利用。原因是:哈希并不是 SCM 世界里信任的真正机制。它确实提供了密码学完整性,但信任的根本基础不是它。

Linus 在 Git 诞生时就争论过这点:

我真的认为人们不应该把 sha1 当作「安全」。真正的安全在于分发。

信任基于「你从哪里拉取」,一直如此。

考虑一下把不受信内容弄进代码库的真实攻击是什么样。这是最坏情况:某个源代码文件(哈希攻击的情况下更可能是二进制文件)被恶意插入而你不知情。这种事其实经常发生。

不是因为人们花几十万美元租 GPU 生成随机熵,好让某个二进制文件恰好匹配 SHA-1 校验和。而是因为有人社工攻击,拿到某个被数百万项目使用的 npm 包的写权限。这样就不是一个难检测的二进制文件,而是依赖里的每个文件被每个把它放进 package.json 的项目盲目拉入,之前没有任何校验和匹配。

这比暴力破解哈希碰撞并操纵不受信拉取简单、便宜、成功率高大约十亿倍。如果我想把不受信代码弄进 Android,贿赂或说服一个流行下游项目的维护者、接管它、往已经受信任的源 URL 注入难检测的代码,比设计一个容易检测的哈希碰撞、放到没人会拉取的 URL 简单得多。

你觉得没有疲惫的开源维护者会为了 4 万美元一次性付款交出高使用率、又没人爱的项目的维护权吗?这样一来不用租 GPU,一天搞定,你能随意替换任何文件。

换句话说,当存在无偿开源维护者和低信任包管理 forge 的时候,哈希碰撞攻击可能是把不受信代码弄上系统的最蠢方式。

回到 Linus 的论点:我从 github.com/rust-lang/rust 拉代码,不是因为我信任签了最新 commit SHA 的 GPG 签名、认为随便什么源都行。我信任 GitHub 的认证足够严密,不太可能有人恶意推送。这也是我不从 randomhash.onion/hAAAxx0r/rust 拉的原因。

我根本不在乎哈希算法是什么,也不在乎在大二进制文件上生成碰撞少了多少亿年计算——Rust 显然根本不会把那种文件提交到仓库。这纯粹是荒谬的前提。

我见过的所有支持这场昂贵 SHA-256 迁移的攻击场景都不现实。它们都能被许多更强、更简单的源和内容保护措施便宜地缓解,而不是靠稍强的哈希算法。

如果我们把 SHA-1 看作在可信仓库里生成内容唯一键的足够好、足够快的方法,接受哈希本身不用于信任,那就没有理由替换它。用 MD5 大概也没问题。意外内容碰撞在任何合理代码库中数学上几乎不可能,其余可以用签名内容验证、外部认证和社会信任机制处理。

不接受这点,就会陷入循环:每次有人发现理论碰撞漏洞,就要迁移整个 Git 生态。经历多年 SHA-1 到 SHA-256 迁移后,量子计算机破了 256,我们又回到同样的蠢船。就因为我们把密码学完整性和信任混为一谈。

现在,代价呢?不管你怎算,都不会便宜或容易。Emily Shaffer 刚做了演讲谈 Google 如何准备应对,画面不好看。

分解一下 Git 3.0 带这个新默认值发布时会发生什么。

首先,所有用 git init 创建的新仓库都会用 SHA-256 内容哈希算法。你可以自己试:git init --object-format=sha256,然后提交文件,git log 会显示一个 64 位的 commit 哈希(比如 11043f6a3be7d21e999dc84550886306bee65f4faf4fc9226979108fd1a0b1af)。

你首先注意到的(除了长得多的哈希值)是,你不能把这个代码推到 GitHub——虽然到 Git 3.0 发布时几乎肯定会修复。事实上,这大概正是目前延迟 3.0 的主要原因。当你确实想推到 GitHub(或任何 host),你需要在服务器创建仓库时告诉他们这是 sha256 项目。每个项目现在要么在这个桶要么在那个桶,不能混。

这立刻会让人沮丧,因为他们需要知道自己用哪个版本的 git 跑的 git init,并确保去 GitHub 创建服务器仓库时选对。

forge 上新建仓库会需要类似这样的选项,但你怎么知道该选哪个?又有哪个普通人会知道这意味什么、或你选哪个极其重要?

我们将会看到很多这种情况:git push origin main 报错「fatal: the receiving end does not support this repository's hash algorithm」。原因有好几个——取决于 host 怎么标记,你很难知道需要把上游仓库初始化成什么格式。可能上游是 256,而你本地用 3.0 前的二进制初始化,或者反过来。

库、链接和工具,天哪!

对库来说这真的很麻烦,因为 submodule 只能同类型项目使用,所以它们需要两个版本,才能被现有项目和新项目都用。

forge 也许能通过保留另一格式的镜像来处理两种格式,但这会增加 GitHub 等站点几乎每次操作的负载,还会进一步混淆信任问题。

对于决定从 SHA-1 转 SHA-256 的现有项目,需要把项目里每个对象转成新格式,这会破坏所有现有签名。所有参与或使用该项目的人都需要同时切换,避免 split head 问题。或者用镜像设置,把写权限从一个切到另一个。这仍然无法解决其他没有 256 版本镜像的对象替换问题。

如果转换了,Slack、邮件里所有含 SHA-1 哈希的 URL 或链接都不再有效,需要重定向(假设可能——你没换 host 或换到没有映射的地方)。全球所有期望 40 字符哈希的内部或其他工具都会坏掉,需要更新来猜测或检测哈希格式。

而且,虽然核心 Git 有处理两种格式仓库的功能,不是所有库都有,几乎没哪个有完整支持。因为 Git 作为项目开发时用了非重入、GPL 许可、不可链接的库,更广生态里大多数项目用从零重实现。这些库都零支持或部分支持,所有不 fork-exec 调 Git 二进制的脚本和工具都会在这些新仓库上出某种程度的故障。

如果你的工具依赖这些库中的任何一个,某些操作可能就没戏了

我还能继续说,但这是个巨大、广泛、仍大体未解决的问题,没人有共识或好方案。Emily 甚至指出 Google 可能直接设置内部系统级覆盖,确保 Google 所有新项目仍是 SHA-1,尽可能让一切回退 3.0 新默认。

个人看法:这是为一个理论问题寻找不必要的方案,即使那个理论上的、不实际的问题,也能用更简单直接的方式解决,只针对可能真正在意的少数项目。

不要用哈希来信任内容。 就像 Linus 20 年前说的。

如果你假设仓库源可信,这些都不重要。99% 的人只用可信仓库源。对几乎所有 Git 用户,花哨的后门哈希碰撞攻击无关紧要——如果只在一个仓库工作,写权限被攻陷,任何人都能在 main 分支放任何东西,可能没人会注意。不需要花哨对象替换技巧来骗人。

如果你不完全信任源(另外 1%),那也许有别的更简单更好的方法,在全球化哈希末日之前该试试。

换个角度怎么样?比如用不同算法独立重新哈希树内容,然后把那个头注入我们签名的对象,这样两种哈希都签了。一个(SHA-1)用于内容检索,另一个(比如 SHA-256)用于独立内容验证。

我想深入一点,因为我觉得这类方法能解决几乎所有甚至理论上的问题,而不用分裂整个 Git 生态、让所有人沮丧。

假设你想依赖外部库或供应商,想缓解可能的对象碰撞攻击。你不能信任 commit 或 tag 的 SSH/GPG 签名,因为它签的是我们用来存内容的 SHA-1 哈希,而那个已被证明可以不被检测地替换,而不改变你签的东西。

所以把它们分开。签 commit 或 tag 时,独立计算树里所有内容的 SHA-256(或 BLAKE3 等)哈希,作为新头注入该对象,然后被签名。现在签名既哈希了基于 SHA-1 的内容和历史,又额外哈希了签名时独立计算的树内容哈希。

在 Git 对象的签名字段中独立计算树内容哈希

当把它拉到另一个项目时,你可以选择通过公钥验证签名,确保检出的内容匹配两个签名。

这不是新想法。Colin Walters 的 git-evtag 从 2015 年就几乎正是这么做的:它是 git tag -s 的替代品,在签名前给 tag 加一个 Git-EVTag-v0-SHA512 校验和,覆盖 commit、tree 和每个 blob(递归进 submodule),可以与同一棵树的 SHA-1 独立验证。

如果 SHA-256 某天被攻破,我们可以加新哈希函数支持,在意的项目可以开始要求它(比如 tree-blake3 头)。甚至可以用多个哈希,验证无、任一或全部用于内容信任。每次算法信任被攻破,我们只需加新哈希和验证方法,不用迁移存在的每个项目。

缺点可能是我们不能信任每个 commit,只能信任有头且被签名的对象,但对几乎所有在意的项目可能没问题。这种情况下签名的信任不再传播整个历史,但这真是多大问题?这类项目几乎肯定都钉在 tagged release 上。

创建这个独立内容签名会稍贵,但只在你签东西时。我做了个概念验证,跑在能想象的最坏情况——Chromium,递归所有 submodule 校验和。那是 35GB 工作树、210 万文件。我的工具生成校验和(所有主树文件、所有 submodule 文件)用了 5 秒(M5 Mac 多线程)。这基本是绝对最坏情况。Linux 树 1.5GB 用了 257ms,Git 项目 17ms。对大多数项目甚至可以放进每个 commit。也可以回填——回去给过去任何项目的 commit 加签名校验和的 tag,很容易。

独立验证树内容并不贵

每个有人关心的树都能独立校验和签名,核心哈希模型不变。技术上甚至比单用 SHA-256 更安全,因为现在需要内容在两种哈希算法下都碰撞才有问题。

我们可以停止用 SHA-1 信任内容,而不用扔掉 SHA-1、破坏整个生态。可以加另一个内容信任向量,而不用给少数有信任问题的项目造成巨大混乱。

这个双签名方法还可以说解决了 NIST 合规问题。NIST 2030 的 SHA-1 截止日期是关于用 SHA-1「施加密码学保护」,不是关于 SHA-1 存在于栈里。如果每个签名也覆盖 SHA-256 内容哈希,SHA-1 就不再保护任何东西,只是内容寻址键。FIPS 模式系统已经处理这个:OpenSSL 3 让应用请求非 FIPS 实现来哈希它不用于安全的东西(Python 的 usedforsecurity=False 就是这个),而 Git 对象 ID 甚至不走 OpenSSL,用自己内置的 SHA-1 代码,完全在任何 FIPS 加密模块之外。

我还可以更进一步,完全去掉 sha1dc(「碰撞检测」)的计算税,我们都付这个来避免这种特定类型的碰撞。如果我们假设这不是该做这种检查的地方,很多情况下 clone 和 push 都能提速。

评论

社区对 Git 计划默认切换 SHA-256 分歧明显,多数人担忧子模块与工具链断裂将引发实际灾难,少数人认为过渡简单且拖延会增加未来安全风险。

认为子模块与工具兼容性是真正痛点

  • 不少维护者指出 SHA-1 子模块无法在 SHA-256 仓库中使用,导致依赖大量子模块的项目必须并行维护分裂分支,且周边工具广度使迁移远非易事(较多人) > 若上游切到 SHA-256 而子模块不支持,将不得不维护转换后的分支并用不同哈希,简直一团糟(@pavon)

认为提交历史中的哈希引用会断裂且变更徒劳

  • 现有提交信息里大量“参见某 SHA-1 提交”的文案将失效,类比 master 改 main 只带来折腾无实质收益,除非保留兼容模式(较多人) > 每个包含旧哈希引用的提交信息都会坏掉,除非保留兼容模式,否则是列车事故(@purpleidea)

认为迁移风险被夸大,实际可平滑回退或配置

  • 反对灾难论,称如同 Y2K 式恐慌;可通过 git 3.0 默认开 SHA-256 或显式设回 SHA-1 解决,并质疑反对者未参与长期讨论(少数人) > 不切换到 SHA-256 才会在十年后 SHA-1 破时全员同一天被迫切换,那才是大问题(@ltbarcly3)

认为哈希碰撞在实践中有真实攻击面,原文论点错误

  • 有人逐条反驳原文“碰撞无关”说,指出 SHAttered 已是实际概念验证,且通过拉取请求合并恶意碰撞对象可在审查时骗过开发者,触发非预期行为(较多人) > 在 GitHub 环境里陌生人提交 PR 被 fetch 审查,若碰撞能植入不同代码,“你拉取时就完了”不能作为安全边界(@Strilanc)

认为 Git 哈希本就不是安全机制,安全在别处

  • 引用 Linus 原意:SHA-1 仅为内容一致性校验而非安全证书;若依赖任意 Git 哈希做认证本身就不合理,不必为此折腾工具链(少数人) > 若你决定用任何形式的 Git 哈希作为代码真实性证书,那是椅子与键盘间的问题,强哈希救不了(@kazinator)

认为应设计更优兼容方案而非硬切换

  • 技术派提议 SHA-256 对象可引用预登记的 SHA-1 对象白名单,或提交同时携带双哈希,甚至用别名区分初始化以避免意外不兼容(较多人) > 应当允许 SHA-256 提交引用截止日前的 SHA-1 对象并冻结名单,从而彻底堵住新碰撞进入(@amluto)

有价值的补充

  • Fossil SCM 在 SHAttered 攻击公开六天后便支持 SHA3-256 且完全向后兼容,旧提交哈希不变,反衬 Git 决策迟缓(@gandreani 冷知识)
  • SHA-256 被 NIST 认定为量子安全,未列入后量子迁移指导,原文担忧量子破解需再迁移属误解(@r3trohack3r 技术修正)
  • xz 后门是维护者信任被攻破而非哈希碰撞,印证供应链威胁主要在信任层(@JaumeGar 类比)
  • WebKit 曾因合并碰撞演示 PDF 搞崩 SVN 仓库,证明碰撞可触发版本工具非预期损坏(@Strilanc 案例)
  • 文章提议以签名建立信任并便于未来换算法,比单纯换哈希更优雅,获部分读者认同(@6thbit 精巧提案)