哈希(哈希值、摘要)是一个"单向的指纹机":内容变一个字,指纹就变一半;但从指纹反推不出内容——它不负责保密,只负责让"被改过"这件事藏不住。
5-4 里 CA 对证书签了名——那个"签名"里面签的其实不是证书全文,而是它的哈希。这一节就是来解释这件事的:为什么签一个哈希,就等于签了整份内容。
哈希也是本章的一条暗线:5-6 的数字签名靠它,5-13 的代码签名靠它,1-9 里讲的登录口令存储也靠它——只是最后那一处必须换一种专门为"慢"而设计的哈希。
一、哈希是什么:三个必须同时成立的性质
哈希函数(hash function)把任意长度的输入,映射成固定长度的输出(叫哈希值、摘要、指纹)。它要做到三件事:
- 定长:输入 1 个字节和 1 个 GB,输出的长度一样(SHA-256 永远 256 位)。
- 单向:从输出反推不出输入——只能靠猜,猜完再算一遍比对。
- 抗碰撞:极难找到两个不同的输入、却有相同的输出。注意是"极难"而不是"不可能"——MD5 和 SHA-1 就是在这条上倒下的。
这三条里,第 3 条是最容易出事、也最容易被忽略的一条:如果你能找到两份内容哈希相同,你就可以把其中一份替换成另一份,而所有基于哈希的校验都会说"没问题"。
二、动手 Demo:定长、雪崩效应、和速度的双刃剑
import hashlib, time
# ① 定长:同一段文本,三种算法各自的输出
for algo in ("md5", "sha1", "sha256"):
print(algo, len(hashlib.new(algo, b"hello").hexdigest()) * 4, "位")
# ② 雪崩效应:改一个字符,输出有多少位翻转
a = hashlib.sha256(b"hello").hexdigest()
b = hashlib.sha256(b"hellp").hexdigest()
# 逐位异或统计不同位数
# ③ 速度:为什么密码不能直接用 SHA-256
t0 = time.perf_counter()
for i in range(200000):
hashlib.sha256(b"password" + str(i).encode()).digest()
rate = 200000 / (time.perf_counter() - t0)
print("每秒约 %.0f 万个" % (rate / 1e4))
实跑输出:
一、同一个输入,三种算法各算一遍(都是十六进制)
MD5 128 位 5d41402abc4b2a76b9719d911017c592
SHA1 160 位 aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
SHA256 256 位 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
→ 输出长度固定,与输入多长完全无关:1 个字节和 1 GB 都是这么长。
二、改一个字符,输出面目全非(雪崩效应)
SHA-256(hello) = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
SHA-256(hellp) = fdd7585e08c4e2afd71dcabdb4636c89d557a3f42db9e2040c8bbd1708aa4ce7
两个结果(共 256 位)里有 131 位不一样
→ 接近一半的位都翻转了。这就是【摘要】的用处:
内容变一个字,摘要就变一半,任何人都看得出被改过。
三、几种算法的现状(2026)
算法 长度 2026 状况 说明
MD5 128 位 已被攻破 2004 年碰撞;不要用于任何安全用途
SHA-1 160 位 已弃用 2017 年 SHAttered 实际碰撞
SHA-256 256 位 当前安全 通用默认;CPU 有硬件加速指令
SHA-3 可变 当前安全 结构完全不同,当备份用
Argon2id 可变 密码专用 故意慢、吃内存,密码存储标准答案
四、为什么【绝对不能】直接用 SHA-256 存密码(本机实测速度)
单线程 Python 逐个试密码(含字符串拼接开销):每秒约 49 万个
换成一块显卡(GPU):每秒约【几十亿】次。
→ 8 位大小写字母加数字的密码,理论空间约 2.2e14 种,
按每秒 10 亿次算,平均约 1.3 天就能试完。
真正该用的做法是【故意慢】:
同一台机器上,PBKDF2 加盐迭代 10 万次,算一次要 120.3 毫秒
→ 单次从微秒级抬到毫秒级,GPU 的批量优势被削掉几千倍。
五、盐:让同一个密码在两个用户那里哈希不同
密码都是 123456,盐不同:
salt_A 2437ae551d97cd6c84dde464
salt_B 97bf8851da2af14bf0d28df2
→ 结果完全不同。这样【预先算好的彩虹表】直接失效,
而且两个用同一个密码的用户,库被拖了也看不出来谁和谁一样。
读这个输出:
- 第二段那 131 位是本节最直观的证据:
hello和hellp只差一个字母,而 256 位里翻了 131 位——大约一半。这叫雪崩效应:输入改一点,输出翻天覆地。 它的实用价值在于"不用比对全文":你只要比对 32 字节的摘要,就等于比对了整份文件。 - 第四段是本节最实用的一段,而且结论反直觉:哈希快,对于"校验文件"是优点,对于"存密码"是灾难。本机单线程一秒能试 49 万个,换成 GPU 是每秒几十亿——8 位大小写字母加数字的密码平均 1.3 天就试完。 所以密码必须用"故意慢"的算法(bcrypt/scrypt/Argon2id),把单次成本从微秒抬到毫秒。
- 第五段解释了"盐"的作用:同一个密码
123456,加不同的盐之后哈希完全不同。这挡住的是"预先算好的彩虹表"(攻击者不能拿一张现成的表批量对照),也顺带让"两个用户密码相同"这件事在数据库里看不出来。
三、哈希用在哪:一张分工表
| 用途 | 用哪种 | 为什么 |
|---|---|---|
| 文件完整性校验 | SHA-256 | 要快,而且只需要"改没改"这一个答案 |
| 数字签名、证书 | SHA-256 | 签名只签 32 字节摘要,成本与内容长度无关 |
| 密码存储 | Argon2id(其次 bcrypt/scrypt) | 必须故意慢,还要加盐 |
| 消息认证(带密钥的哈希) | HMAC-SHA256 | 同时验证"没改过"和"是知道密钥的人发的" |
| 不要再用 | MD5、SHA-1 | 碰撞已经被实际做出来 |
这张表里最值得记的是最后一行:"算法被攻破"这件事是有具体形态的——不是"能反推出原文",而是"能造出两份内容、哈希相同"。一旦能造碰撞,完整性校验就彻底失效了,因为攻击者可以把恶意版本替换成合法版本,而摘要一模一样。
四、⚠️ 2026 现状
- 【SHA-256 是当前默认答案,而且经得起时间】。 它从 2001 年发布至今二十多年,最好的攻击也只影响到"轮数被削减的玩具版本",完整的 64 轮没有任何实际碰撞。SHA-3(2015 年标准化) 用的是完全不同的海绵结构,它的定位不是"替换 SHA-2",而是"万一 SHA-2 哪天出事时的备份"。普通业务用 SHA-256 就够;只有在法规明确要求算法多样性时才需要同时上 SHA-3。
- 【MD5 与 SHA-1 已经是"明确不能用",而不是"尽量别用"】。 MD5 在 2004 年就被做出实际碰撞;SHA-1 在 2017 年被 Google 的 SHAttered 项目做出实际碰撞,2020 年的后续研究把"选前缀碰撞"的成本压到约 4.5 万美元(租用 GPU),并且在真实场景里伪造了 PGP 签名。今天所有主流浏览器都会拒绝 SHA-1 签名的证书。
- 【密码存储上有一条几乎所有开发者都踩过的坑:不要用 SHA-256 直接存】。 原因就是 Demo 第四段那个速度:快哈希对攻击者是帮忙。正确做法是"加盐 + 专门为慢而设计的算法",Argon2id 是 2026 年的首选(2015 年密码哈希竞赛胜出,RFC 9106),bcrypt 和 scrypt 仍可用。判断标准很简单:这个算法的设计目标里有没有"故意慢、吃内存"这一条。
- 【一个正在变重要、但常被忽略的地方:算法要"敏捷"】。 MD5 和 SHA-1 当年也都是"当时的安全选择"——它们倒下的过程提醒我们:任何哈希算法都有寿命。所以工程上要留一条"换算法的路":签名和摘要要能被批量重算、证书和代码签名要能重新生成(这就是 5-13 里"时间戳"和"重新签名"存在的原因)。
30 秒实验
今天做两个动作,各 15 秒:
- 打开终端算一个哈希(Windows 下
certutil -hashfile 某个文件 SHA256,Linux/macOS 下shasum -a 256 某个文件);然后随便改动那个文件里的一个字符,再算一次。两次结果会长得完全无关——这就是雪崩效应在你机器上的实拍。 - 想一个你正在用的网站,问自己一句:如果它把密码用 SHA-256 存起来(不加盐、不迭代),库被拖走之后会发生什么?——答案:攻击者拿着字典批量算,一秒钟几十亿次,绝大多数常见密码几分钟内就还原了。这个念头值得留在脑子里——它是"为什么密码长度和复杂度有意义"的最直接解释。
这一节要带走的一句是:哈希不保密,它只做一件事——把"改过"这件事变成藏不住的。所以它既能当签名和证书的地基,也会在最不该省的地方(密码)因为"太快"而变成漏洞。
常见误区
-
错误:哈希和加密是一回事,都是把内容变成看不懂的样子。——纠正:两者在一件关键的事上正好相反——能不能还原。加密是可逆的(有密钥就能解回来),哈希是不可逆的(谁也算不回去)。由此带来一个实用判断:如果你看到某个系统说"我们能把你忘记的密码发回给你",那它一定没有把密码做哈希存储——这是严重的存储缺陷。更普遍的说法是:需要还原的内容用加密,只需要比对的内容用哈希。抓包看到一个 32 字节的十六进制串,你可以判断它多半是摘要,不是密文。
-
错误:既然有哈希,那文件下载完对一下哈希就知道安全了。——纠正:哈希只证明"文件与发布者给出的摘要一致",前提是"你拿到的摘要本身没被换掉"。如果下载页和文件都被同一个攻击者替换了,摘要也会一起被换——你算出来的结果"对得上",但对的是一份被篡改的内容。这就是为什么更可靠的校验方式是"用签名"(5-13):签名把内容和"发布者的身份"绑在一起,而单纯一个哈希串不绑任何身份。想清楚这一点,就理解了"哈希"和"数字签名"的分工。
-
错误:密码加上 SHA-256 加盐就够了。——纠正:"加盐"解决的是"彩虹表"和"两个用户同密码"的问题,但没有解决"快"的问题。攻击者拿到一个加盐的 SHA-256 哈希,仍然可以用 GPU 每秒试几十亿次——盐只能防止"一次破解一批",防不住"针对单个密码的暴力枚举"。真正需要的是"故意的慢":PBKDF2/bcrypt/scrypt/Argon2id 通过"迭代很多轮"或"吃大量内存"把单次成本抬到毫秒级——本机实测一次迭代 10 万次的 PBKDF2 就要 120 毫秒,正好比快哈希慢了几万倍。 这才是"存密码"和"存文件摘要"必须用不同算法的原因。
-
错误:SHA-3 更新,所以应该优先用它。——纠正:SHA-3 的定位是"备份",不是"升级版"。它和 SHA-2 是两条平行的技术路线,标准化的目的正是"万一 SHA-2 的构造被攻破,还有一条独立的路可用"——而不是"因为它更新所以更好"。在绝大多数场景下,SHA-256 仍然是默认选择(更快、硬件加速支持更广、生态更成熟)。只有在"法规要求算法多样性"或"需要双算法冗余"时,才需要同时用 SHA-3。
自测三问(点开对答案)
1. "抗碰撞"和"单向"是两个不同的性质,它们各自挡的是什么? —— **单向(抗原像)挡的是"从哈希反推原文"**:**攻击者不能从摘要算出内容,只能猜一个、算一遍、比对**——**所以哈希不会泄露原文,即使用来存密码,泄露的也只是"这份密码的指纹"。** **抗碰撞挡的是"找到两份内容哈希相同"**:**攻击者不能把恶意版本伪装成合法版本**——**这是保护"完整性校验"和"签名"的关键。** **两者的实际重要性不同**:**单向不够强可能被字典攻击绕过(这也是密码必须用慢哈希的原因);抗碰撞一旦倒下,整个完整性体系就崩了**——**MD5 和 SHA-1 的退场,全都是因为抗碰撞被做出来了。** 2. 为什么"文件完整性校验"用 SHA-256,而"密码存储"必须换算法? —— **因为这两种场景对"速度"的要求正好相反**。**校验文件时,快是优点**(大文件也要秒级出结果);**存密码时,快是致命缺点**——**攻击者拿到的是一份哈希,他唯一能做的就是"猜一个密码、算一遍、比对",所以"算一遍有多快"直接决定了他每秒能试多少个**。**本机单线程 SHA-256 每秒约 49 万个,GPU 每秒几十亿个**,**8 位密码平均 1.3 天就试完**;**而 Argon2id 这类算法通过"故意慢 + 吃内存"把单次成本抬到毫秒级,还能让 GPU 的并行优势失效**。**一句话:同一个数学工具,用在不同场景,"快"这个属性的意义完全变了。** 3. 为什么说"只要基于 MD5 的签名,就等于没有签名"? —— **因为签名的安全性建立在哈希的抗碰撞上**。**签名的流程是"对内容算哈希,然后签这个哈希"**(5-6)——**签名保护的是"这个哈希值",而"这个哈希值代表哪份内容"是由哈希的抗碰撞性保证的。** **如果 MD5 能被做出碰撞,攻击者就能造出两份哈希相同的文件:一份是你签过的合法文件,另一份是夹带私货的恶意文件。** **你签的是合法那份,但攻击者拿去用的是恶意那份——验证照样通过。** **这就是"危殆化"的真实形态:算法倒下,它上面盖的所有东西一起失效**(这也是 5-10 的核心主题)。- ☐ 下一节(5-6 在签名中运用公开密钥加密技术)
相关笔记
- 公开密钥加密的核心技术 —— 上一篇:CA 签名签的就是哈希
- 在签名中运用公开密钥加密技术 —— 下一篇:把哈希和公钥加密拼起来
- 针对密码的攻击 —— 口令存储为什么必须用慢哈希
- 为软件添加数字签名 —— 同一个哈希,用在发布流程里
- 🛡️ 网络与信息安全 · 索引 —— 全课路线图