🛡️ 第五章 什么是密码、数字签名、数字证书
📄 一句话概括

哈希(哈希值、摘要)是一个"单向的指纹机":内容变一个字,指纹就变一半;但从指纹反推不出内容——它不负责保密,只负责让"被改过"这件事藏不住。

📝 本节和前面几节的关系

5-4 里 CA 对证书签了名——那个"签名"里面签的其实不是证书全文,而是它的哈希。这一节就是来解释这件事的:为什么签一个哈希,就等于签了整份内容。

哈希也是本章的一条暗线:5-6 的数字签名靠它,5-13 的代码签名靠它,1-9 里讲的登录口令存储也靠它——只是最后那一处必须换一种专门为"慢"而设计的哈希。

一、哈希是什么:三个必须同时成立的性质

哈希函数(hash function)把任意长度的输入,映射成固定长度的输出(叫哈希值、摘要、指纹)。它要做到三件事:

  1. 定长:输入 1 个字节和 1 个 GB,输出的长度一样(SHA-256 永远 256 位)。
  2. 单向:从输出反推不出输入——只能靠猜,猜完再算一遍比对。
  3. 抗碰撞:极难找到两个不同的输入、却有相同的输出。注意是"极难"而不是"不可能"——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
  → 结果完全不同。这样【预先算好的彩虹表】直接失效,
     而且两个用同一个密码的用户,库被拖了也看不出来谁和谁一样。

读这个输出:

三、哈希用在哪:一张分工表

用途 用哪种 为什么
文件完整性校验 SHA-256 要快,而且只需要"改没改"这一个答案
数字签名、证书 SHA-256 签名只签 32 字节摘要,成本与内容长度无关
密码存储 Argon2id(其次 bcrypt/scrypt) 必须故意慢,还要加盐
消息认证(带密钥的哈希) HMAC-SHA256 同时验证"没改过"和"是知道密钥的人发的"
不要再用 MD5、SHA-1 碰撞已经被实际做出来

这张表里最值得记的是最后一行:"算法被攻破"这件事是有具体形态的——不是"能反推出原文",而是"能造出两份内容、哈希相同"。一旦能造碰撞,完整性校验就彻底失效了,因为攻击者可以把恶意版本替换成合法版本,而摘要一模一样。

四、⚠️ 2026 现状

30 秒实验

今天做两个动作,各 15 秒:

  1. 打开终端算一个哈希(Windows 下 certutil -hashfile 某个文件 SHA256,Linux/macOS 下 shasum -a 256 某个文件);然后随便改动那个文件里的一个字符,再算一次。两次结果会长得完全无关——这就是雪崩效应在你机器上的实拍。
  2. 想一个你正在用的网站,问自己一句:如果它把密码用 SHA-256 存起来(不加盐、不迭代),库被拖走之后会发生什么?——答案:攻击者拿着字典批量算,一秒钟几十亿次,绝大多数常见密码几分钟内就还原了。这个念头值得留在脑子里——它是"为什么密码长度和复杂度有意义"的最直接解释。

这一节要带走的一句是:哈希不保密,它只做一件事——把"改过"这件事变成藏不住的。所以它既能当签名和证书的地基,也会在最不该省的地方(密码)因为"太快"而变成漏洞。

常见误区

  1. 错误:哈希和加密是一回事,都是把内容变成看不懂的样子。——纠正:两者在一件关键的事上正好相反——能不能还原。加密是可逆的(有密钥就能解回来),哈希是不可逆的(谁也算不回去)。由此带来一个实用判断:如果你看到某个系统说"我们能把你忘记的密码发回给你",那它一定没有把密码做哈希存储——这是严重的存储缺陷。更普遍的说法是:需要还原的内容用加密,只需要比对的内容用哈希。抓包看到一个 32 字节的十六进制串,你可以判断它多半是摘要,不是密文。

  2. 错误:既然有哈希,那文件下载完对一下哈希就知道安全了。——纠正:哈希只证明"文件与发布者给出的摘要一致",前提是"你拿到的摘要本身没被换掉"。如果下载页和文件都被同一个攻击者替换了,摘要也会一起被换——你算出来的结果"对得上",但对的是一份被篡改的内容。这就是为什么更可靠的校验方式是"用签名"(5-13):签名把内容和"发布者的身份"绑在一起,而单纯一个哈希串不绑任何身份。想清楚这一点,就理解了"哈希"和"数字签名"的分工。

  3. 错误:密码加上 SHA-256 加盐就够了。——纠正:"加盐"解决的是"彩虹表"和"两个用户同密码"的问题,但没有解决"快"的问题。攻击者拿到一个加盐的 SHA-256 哈希,仍然可以用 GPU 每秒试几十亿次——盐只能防止"一次破解一批",防不住"针对单个密码的暴力枚举"。真正需要的是"故意的慢":PBKDF2/bcrypt/scrypt/Argon2id 通过"迭代很多轮"或"吃大量内存"把单次成本抬到毫秒级——本机实测一次迭代 10 万次的 PBKDF2 就要 120 毫秒,正好比快哈希慢了几万倍。 这才是"存密码"和"存文件摘要"必须用不同算法的原因。

  4. 错误: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 的核心主题)。

相关笔记