安全指南

哈希与校验和:验证文件,并选对算法

哈希函数保证什么、不保证什么,如何验证下载以及这种验证到底值多少,MD5 与 SHA-1 为什么被淘汰,以及密码存储为什么需要一个刻意放慢的算法。

哈希函数把任意输入变成定长摘要。这句简单的描述背后,是三件差别极大的工作——检测损坏、证明真实性、存储密码——而每件工作的正确答案都不一样。绝大多数麻烦,都来自把其中一件的答案套用到另一件上。

哈希既不是加密,也不是编码

编码按设计就是任何人都能逆转的。Base64、URL 百分号编码、十六进制,存在的目的是让字节通过只接受文本的通道;没有密钥,也没有秘密,把它称作「混淆」都算抬举。

加密是持有密钥才能逆转的。密文加上正确的密钥得到明文,这就是它存在的全部意义。如果你以后还要读到这份数据,你要的是加密,而安全性取决于密钥管理而非算法本身。

哈希则根本不可逆,因为它不是单射:输入长度任意而输出定长,于是有无穷多个输入映射到同一个摘要。没有密钥,也没有回头路。你换来的是一个紧凑的值,它会在输入发生任何变化时彻底改变——翻转一个比特,输出大约有一半的比特会变。让哈希有用的正是这条性质,而不是什么保密性。

同一个函数,输入相差一个字节
$ printf 'hello' | shasum -a 256
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

$ printf 'hello\n' | shasum -a 256
5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03

只多了一个换行,两个摘要就毫无共同之处。这也是「两个校验和
本该一致却对不上」最常见的单一原因:echo 会追加换行,
printf 不会。

验证下载,以及这次验证究竟证明了什么

操作本身很简单:用 shasum -a 256、sha256sum 或 Get-FileHash 算出收到文件的摘要,再和发布方公布的摘要逐字符比对——要比完整字符串,而不是首尾几个字符。接下来是通常被略过的那部分。和下载放在同一个网页上的校验和,只能证明你收到的字节就是那台服务器发出的字节。它能发现传输被截断、镜像损坏、磁盘不稳定;它发现不了服务器被入侵,因为替换文件的人也顺手替换了旁边的摘要。要对抗有主动攻击者的完整性,需要的是签名——对校验和文件的 GPG 签名、经过签名的发布产物、或包管理器对仓库密钥的校验——因为没有发布方的私钥,签名无法被重新生成。

实用规则是:用校验和抓意外,用签名抓攻击,并且对自己诚实——你刚才做的到底是哪一种。

校验一整个下载目录
$ cat SHA256SUMS
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824  greeting.txt

$ shasum -a 256 -c SHA256SUMS
greeting.txt: OK

# 以及多数教程跳过的那一步——确认 SHA256SUMS 本身
# 确实来自发布方:
$ gpg --verify SHA256SUMS.asc SHA256SUMS
gpg: Good signature from "Example Project Release Key"

# 没有第二步,你只是拿「提供下载的那台服务器」
# 去验证了「它提供的下载」。

选哪个算法,以及各自现在还能干什么

密码学哈希函数被要求具备三种抗性,并且会按顺序失守:抗原像、抗第二原像、抗碰撞。抗碰撞是最弱的,也是最先被攻破的,因为攻击者可以同时选择两个输入。

这个次序解释了下表里一个看似矛盾的地方:MD5 在碰撞上被彻底攻破,却至今没有实用的原像攻击。所以 MD5 在「检测没人攻击的文件是否意外损坏」这件事上仍然够用,而在任何由攻击者提供输入的场景下完全不适用——证书、签名、下载完整性校验、对不可信内容做去重,都属于后者。

算法摘要长度2026 年的状态适合用来做什么
MD5128 位 / 32 个十六进制字符2004 年起碰撞已可轻易构造,选择前缀碰撞已实用化仅限非对抗性的损坏检测和历史系统互通。
SHA-1160 位 / 40 个十六进制字符2017 年公开碰撞,2020 年选择前缀碰撞;NIST 计划 2030 年前完全淘汰不要用于任何新用途,既有用途请迁移。
SHA-256256 位 / 64 个十六进制字符安全;默认选择文件完整性、签名、HMAC、证书指纹。
SHA-512512 位 / 128 个十六进制字符安全;在 64 位硬件上往往比 SHA-256 更快与 SHA-256 相同的场景,但希望留更大余量时。
SHA-3(SHA3-256)256 位 / 64 个十六进制字符安全;内部构造与 SHA-2 不同算法多样性,以及需要天然免疫长度扩展的场景。
BLAKE2 / BLAKE3长度可配,常用 256 位安全;软件实现比 SHA-2 快得多高吞吐完整性校验、内容寻址。
bcrypt / scrypt / Argon2含盐与参数的编码字符串安全,且刻意很慢只用于密码,绝不用于文件完整性。

MD5 和 SHA-1 为什么被淘汰

暴力找碰撞的理论下界是生日界:n 位摘要约需 2^(n/2) 的工作量,MD5 是 2^64,SHA-1 是 2^80。两者最终远远低于各自的下界,原因是结构性弱点,而不是硬件追了上来。

SHA-1 的第一个公开的相同前缀碰撞由阿姆斯特丹 CWI 与 Google 的研究者在 2017 年演示,他们造出了两份 SHA-1 摘要相同、内容不同的 PDF。2020 年又出现了选择前缀碰撞,当时估算的云计算成本在数万美元量级——也就是说,只要有动机就够得着。NIST 已为 SHA-1 在所有联邦应用中的使用设定了 2030 年的终止期限。

HMAC:涉及共享密钥时该用的东西

纯哈希能证明消息没有损坏,却证明不了是谁发的,因为任何人都能算哈希。当两个参与方持有共享密钥时——webhook 签名、API 请求签名——你需要的是消息认证码。

最直觉的构造,也就是把密钥和消息拼起来做哈希,对 SHA-2 家族以及 MD5、SHA-1 都是不安全的。这些函数采用 Merkle–Damgård 构造,摘要就是处理完输入之后的完整内部状态。攻击者只要知道 H(secret || message) 和密钥长度,就能从那个状态继续推进,为一条更长的消息算出合法摘要,而全程不需要知道密钥。这就是长度扩展性质,它在自制签名方案中造成过真实漏洞。

RFC 2104 定义的 HMAC 通过用两个派生密钥做两次哈希来解决这个问题,攻击者无法再从中间状态续接。它到处都有实现,代价大约是两次哈希调用,并且不需要你发挥任何创造力。SHA-3、BLAKE2 和 BLAKE3 天然不受长度扩展影响,也各自提供带密钥模式,但 HMAC-SHA-256 仍是安全的默认选择,因为它的实现无处不在。

还有一个与构造同等重要的实现细节:比对 MAC 要用常数时间。普通字符串比较一旦遇到不同字节就立即返回,这个时间差在足够多的请求下是可测量的,足以让攻击者逐字节还原出一个合法签名。每个平台都提供了正确的函数——Node 的 crypto.timingSafeEqual、Python 的 hmac.compare_digest、Go 的 subtle.ConstantTimeCompare。

对同一个输入,加上密钥做 HMAC
消息:hello
密钥:shared-demo-key

HMAC-SHA-256 =
88edf304eb62855ca689db051a37e434b6b3b56773d944aaf068e9fdd636a880

对比同一条消息的无密钥摘要:
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

第二个任何人都算得出来,第一个只有持有密钥的人算得出来——
这恰恰就是「这个文件是完整的」和「这条消息是你发的」
之间的区别。

密码存储要的是慢哈希,不是快哈希

SHA-256 是按「快」设计的,硬件也确实配合:一块商用 GPU 每秒能做 10^10 量级的 SHA-256 运算。面对一张泄露出来的 SHA-256 密码哈希表,攻击者并不去逆转函数——他们猜。先跑泄露语料库,再跑带变形规则的字典,最后做暴力扫描,一边算哈希一边比对。大多数真实用户密码在最初几分钟内就会落网。加盐是必要的但不充分:它让预计算彩虹表失效、迫使攻击者逐账号开工,却丝毫没有放慢单次猜测。

密码哈希函数被设计成慢的,现代的几种还是「内存困难」的,目的就是让攻击者的 GPU 优势崩塌。bcrypt 有一个成本因子,每加一档工作量翻倍。scrypt 和 Argon2 还额外要求每次计算占用可配置数量的内存,这在 GPU 或 ASIC 上并行化的代价很高。它们的输出是一串编码字符串,里面带着算法、参数和盐,因此日后可以提高参数而不作废已有的哈希。

新系统请用 Argon2id。OWASP 当前的基线是 19 MiB 内存、2 次迭代、并行度 1;在登录端点的延迟预算开始抱怨之前,尽量往上调。bcrypt 取成本因子 10 及以上仍然是完全体面的选择,而且可用性更广——唯一要注意的是它会静默截断 72 字节以外的输入,所以如果你接受长口令短语,请先做一次预哈希。PBKDF2-HMAC-SHA-256 在合规要求强制时可以接受,迭代次数取 60 万以上,但它不是内存困难的,因此面对专用硬件是三者中最弱的。

做对了的密码哈希长什么样
bcrypt,成本因子 10(一条真实的 htpasswd -B 输出):
  $2y$10$BGmm0BZWcCbLgebJ4eSabOpuY0/mHQl/iSB6wNIqSFwTnVn9J1cDW
   |   |  |                      |
   |   |  +-- 22 字符盐值          +-- 31 字符摘要
   |   +-- 成本:2^10 轮密钥编排
   +-- 算法标识

Argon2id,OWASP 基线参数(这里展示的是版式,盐与摘要
用占位符表示,而不是编造出来的值):
  $argon2id$v=19$m=19456,t=2,p=1$<base64 盐值>$<base64 摘要>

两者都把参数编码在内,验证方直接从存储值里读取参数。
为新密码提高成本,不会破坏已经存下来的那些。

浏览器哈希工具适合做什么

本站的哈希工具在页面内用浏览器的 Web Crypto 实现计算 MD5、SHA-1、SHA-256 和 SHA-512,不上传任何内容。这让它很适合那些你一周要做几十次的检查:给一小段字符串算哈希、确认两份配置数据是否逐字节相同、为缓存键生成指纹、或者把别人发来的摘要和你在别处算出来的对一遍。

它不适合用来校验大文件下载,我们宁愿先说清楚,也不想让你在一个四 GB 镜像走到 60% 时才发现。在浏览器标签页里做哈希意味着把文件读进页面内存,而这块内存是有上限的,还要和这个标签页正在做的其他事情共享。你的操作系统本来就有为此打造的命令,它以流式方式处理文件,更快也更可靠。请用 shasum -a 256、sha256sum 或 Get-FileHash,把浏览器留给短输入。

还有两条边界值得直说。第一,哈希工具无法验证签名,而如上文所述,签名才是真正抵抗攻击者的那道检查;那需要 GPG 或你的包管理器。第二,对一个秘密做哈希并不会让它变得可以随便经手——API 密钥的摘要不是密钥,但密钥本身在进来的路上待在一个文本框里,随之而来的剪贴板与扩展暴露一样不少。如果那个值是活体凭证,请在你自己的机器上算。

要点回顾

  • 编码人人可逆,加密持钥可逆,哈希根本不可逆——不要把第三种当成藏数据的手段。
  • 下载页旁边的校验和只能抓损坏,只有签名能抓被入侵的服务器,务必分清自己刚才验的是哪一种。
  • 凡是攻击者能影响的输入一律用 SHA-256 及以上;MD5 和 SHA-1 只剩下「检测意外损坏」这一种用法。
  • 涉及共享密钥时用 HMAC,而不是把密钥和消息拼起来做哈希,并且用常数时间函数比对结果。
  • 密码请用 Argon2id 或成本 10 以上的 bcrypt 存储——加了盐的 SHA-256 依然是快哈希,扛不住 GPU 猜测。

继续了解相关检查与工具