SHA-1 的黄昏:从 Git 到碰撞攻击
什么是 SHA-1?
SHA-1(Secure Hash Algorithm 1)是一个输出 160 位(20 字节)哈希值的加密哈希函数。由 NSA 设计,1995 年作为 NIST 联邦标准发布。它的工作很简单:输入任意数据,吐出一个 40 位十六进制字符串。仅此而已。
但它曾经的身份远不止”一个哈希函数”这么简单。在 2000 年代,SHA-1 是数字世界的默认选择——SSL 证书用它签名,Git 用它做内容寻址,无数软件下载站用 SHA-1 校验和来”证明”文件没有被篡改。
然后,2017 年 2 月 23 日,Google 和 CWI Amsterdam 联合发布了一份公告。标题很克制,但内容震动了整个安全社区:他们成功地制造了一次 SHA-1 碰撞——生成了两个内容完全不同的 PDF 文件,但它们的 SHA-1 哈希值一模一样。
我当时看到这条新闻的时候,第一反应是”迟早的事,但没想到这么快”。理论上的不安全变成实际攻击,SHA-1 的丧钟正式敲响。
SHA-1 的工作原理
SHA-1 和 SHA-256 属于同一家族结构——Merkle-Damgård 构造。核心流程分三步:填充、分块压缩、输出。
消息填充
首先,对原始消息补位。规则和 SHA-256 类似:先在消息末尾补一个 1 位,接着补足量的 0 位,最后用 64 位来表示原始消息的长度。填充的目标是把消息凑成 512 位的整数倍,这样后续处理才能一块一块进行。
讲个具体例子。假设消息是字符串 “abc”,三个字节共 24 位。补一个 1 位,再补 423 个 0 位(到 448 位),最后补上 64 位的长度值(24)。总计:消息自身 24 + 填充 1 + 423 个 0 + 64 位长度 = 512 位,正好一块。
80 轮压缩
每一块 512 位数据被分成 16 个 32 位的”字”,然后 SHA-1 用消息调度算法把这 16 个字扩展为 80 个字——每个新字是前面某些字的异或、然后再循环左移 1 位。
有了 80 个消息字,接下来就是 80 轮迭代。SHA-1 维护 5 个 32 位工作变量(初始值来自特定的十六进制常数),每轮用当前的一个消息字和当前状态做混合——非线性函数、循环移位、常数加法——80 轮后,输出 160 位的最终哈希。
SHA-1 的 80 轮 vs SHA-256 的 64 轮,乍看 SHA-1 更多回合、貌似更安全?但安全不是数轮数。SHA-1 的轮函数设计有几个结构上的弱点——尤其是消息扩展算法的线性度太高,导致不同消息字之间可以建立代数关系,这是后来碰撞攻击的突破口。
核心特性
| 特性 | 说明 |
|---|---|
| 输出长度 | 160 位(20 字节,40 个十六进制字符) |
| 内部结构 | Merkle-Damgård(512 位分组,5 个 32 位工作变量) |
| 轮数 | 80 轮 |
| 安全状态 | 已破解——实际碰撞攻击已实现(SHAttered, 2017) |
| 抗碰撞性 | 理论 2^80,实际攻击成本 < 2^63 |
| 标准化 | FIPS PUB 180-1(已被 180-4 取代) |
Merkle-Damgård 结构有一个共同的短板:长度扩展攻击。知道 SHA-1(M) 和 M 的长度,不用知道 M 的内容就能算出 SHA-1(M + padding + extra)。SHA-256 也有这个问题,但人家抗碰撞性至少还是完整的。SHA-1 是两头都没守住。
实际应用场景
1. Git 内容寻址(正在迁移中)
Git 应该是 SHA-1 最后一个还在大规模使用的堡垒了。Git 用它来标识每一个对象——commit、tree、blob——对象的哈希值就是它的名字。你敲 git log 看到的每一条 commit 记录前面那串 40 位十六进制数,就是一个 SHA-1 值。
Git 直到 2017 年还在用 SHA-1,我当时就在想:连 Google 都证伪了 SHA-1 的抗碰撞性,Git 社区怎么还没动静?其实不是没动静,是迁移太难了。Git 的整个数据模型都建立在”哈希唯一标识内容”这个假设上,改哈希算法相当于给 Git 换心脏。2017 年就开始讨论,2020 年才推出初步的 SHA-256 支持,过渡期可能还要持续十年以上。
这件事也让我反思了自己对”协议升级”的理解。以前总想”既然破了就换呗”,但真正碰到 Git 这种全球基础设施级别的系统,一个算法替换牵扯到的是每一个仓库、每一份历史记录的兼容问题,不是打一行命令就能解决的。
2. SSL/TLS 证书(已禁用)
2017 年 1 月起,Chrome 和 Firefox 开始标记 SHA-1 证书为不安全。之前在 HTTPS 里看到绿色锁图标的站,如果证书链里有 SHA-1 签名,浏览器直接报红。说实话,这个迁移是互联网基础设施史上为数不多干净利落的行动——大家都不等,说禁就禁。
3. 完整性校验(残留场景)
很多旧版的 ISO 镜像下载站还在提供 SHA-1 校验和。比如某些 Linux 发行版的早期版本、旧版 JDK 安装包,都只附带了 SHA-1 和 MD5 校验值。今天再只靠 SHA-1 做文件完整性校验已经不靠谱了——一个蓄意的攻击者完全可以用碰撞技术生成两个不同文件,校验值却一模一样。
4. 旧版证书和软件签名
如果你手上还有 2016 年之前的 APK 安装包,它的签名证书大概率是 SHA-1。Google Play 从 2018 年起强制新的应用上传必须用 SHA-256 签名,但旧应用仍然可以活在自己的 SHA-1 签名保护下。
把 SHA-1 想成一个退休的老兵——曾经忠诚可靠,现在年龄到了,该换岗了。
常见误区
误区一:SHA-1 碰撞就意味着它能被”解密”
碰撞不是解密。碰撞是说:能找到两个不同的输入,它们的 SHA-1 输出相同。这不等于能从哈希值反推原始输入。但碰撞确实破坏了对 SHA-1 唯一性的信任——如果两个 PDF 能有一样的 SHA-1,那 SHA-1 就不能再用来鉴定”这文件是不是原始那一个”了。
误区二:Git 用 SHA-1 不安全,所以要立刻迁移
Git 的威胁模型比较特殊。SHAttered 攻击要求攻击者能控制文件的前缀内容(那两个碰撞 PDF 前面都是一大堆精心构造的 JPEG 数据块)。在 Git 里,攻击者要制造一个有同样 SHA-1 的恶意 commit 替换干净的 commit,他需要能精确控制 commit 内容的每一个字节(包括树哈希、父 commit、提交者信息、时间戳),而这些字段里有很多是不可控的。换句话说,SHAttered 级别的碰撞在 Git 的实际攻击面上严重受限。这也是为什么 Git 社区的迁移节奏相对从容,不是因为拖延,是因为实际风险窗口评估后没那么急迫。
误区三:160 位和 256 位只差 96 位,安全差别不大
这个差距是量级上的,不是加法上的。160 位的抗碰撞安全上界是 2^80(生日攻击),SHA-1 实际已经降到 2^63 左右——超级计算机可以在合理时间内完成。而 256 位的安全上界是 2^128。2^80 和 2^128 之间差了几十个数量级——如果 2^80 是一桶水,2^128 是整个太平洋。别用直觉判断指数差距。
SHA-1 vs SHA-256 vs MD5
| SHA-1 | SHA-256 | MD5 | |
|---|---|---|---|
| 输出长度 | 160 位 | 256 位 | 128 位 |
| 内部轮数 | 80 轮 | 64 轮 | 64 轮 |
| 抗碰撞安全 | 已破(< 2^63) | 强(~2^128) | 已破(< 2^18) |
| 标准状态 | 退役 | 当前标准 | 退役 |
| 主要残留 | Git(迁移中) | 全网在用 | 非安全校验 |
| 标准化年份 | 1995 | 2001 | 1992 |
MD5 比 SHA-1 死得更早也更彻底。2004 年王小云团队宣布 MD5 碰撞攻击之后,全网只花了几年就基本抛弃了它。SHA-1 的淘汰过程反而更漫长——不是因为它更安全,是因为它在关键基础设施(尤其是 Git 和 CA 系统)里扎得太深。
这三代哈希算法的兴衰其实勾勒了密码学的一个铁律:一个哈希函数从”理论上存在弱点”到”实际可利用”的窗口,远比多数人想象的短。等你听到警钟的时候,早就该跑了。
常见问题
Q: SHAttered 攻击到底做了什么?
Google 和 CWI 团队利用 SHA-1 消息扩展的线性弱点,构造了两个精心设计的 PDF 文件。这两个文件打开后显示完全不同的内容,但它们的 SHA-1 哈希值完全相同。计算成本相当于 110 个 GPU 跑一整年(约 9×10^18 次 SHA-1 运算)。这是人类历史上第一次公开证明 SHA-1 碰撞已经从理论变成了工程现实。
Q: 我还能用 SHA-1 做密码存储吗?
绝对不能。SHA-1 本身不适合密码存储——它计算太快了,秒级上亿次的速率让 GPU 暴力破解极其高效。即使没有碰撞攻击,SHA-1 作为密码哈希也是不合格的。密码存储应该用 bcrypt、scrypt 或 Argon2,这些算法故意算得慢、吃内存,专门对抗暴力破解。
Q: 怎么检查我的系统还在不走运地依赖 SHA-1?
Linux 跑 openssl x509 -in cert.pem -text | grep "Signature Algorithm" 可以看到证书签名算法,如果出现 sha1WithRSAEncryption 就得安排了。Windows 的 certmgr.msc 也能查证书详情。Git 仓库的话,Git 官方已经提供了 sha256 仓库格式的实验性支持,有兴趣可以去翻 git help hash-function-transition。
Q: SHA-1 和 SHA-2 的关系是什么?名字相近,区别大吗?
名字像,实质完全两件事。SHA-1 和 SHA-2 虽然都用 Merkle-Damgård 结构,但轮函数、消息扩展、初始常量完全不同。SHA-1 说到底是 SHA-0 的修补版(1993 年 SHA-0 被发现弱点后紧急修改了一个循环位移量就变成了 SHA-1),而 SHA-2 是重新设计的。SHA-1 的命门——消息扩展线性攻击——在 SHA-2 上完全不存在。
Q: 为什么不每个人都直接换成 BLAKE3 或 SHA-3?
BLAKE3 和 SHA-3(Keccak)确实在设计和理论安全性上优于 SHA-2,尤其是海绵结构天然抗长度扩展攻击。但 SHA-256 在长达 20 年的实际部署中积累了海量的安全分析——有时候,一个被全世界盯着看了 20 年的算法比一个理论上更优雅但分析历史短的新算法更让人放心。密码学不是纯理论比赛,部署历史和实践检验占了很大权重。